Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: disallow sending another question while auto-applying chat edits #230557

Merged
merged 4 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { localize2 } from '../../../../../nls.js';
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
import { IChatAgentService } from '../../common/chatAgents.js';
import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_LANGUAGE_MODELS_ARE_USER_SELECTABLE, CONTEXT_IN_CHAT_INPUT, CONTEXT_PARTICIPANT_SUPPORTS_MODEL_PICKER } from '../../common/chatContextKeys.js';
import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js';
import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_LANGUAGE_MODELS_ARE_USER_SELECTABLE, CONTEXT_PARTICIPANT_SUPPORTS_MODEL_PICKER } from '../../common/chatContextKeys.js';
import { applyingChatEditsContextKey } from '../../common/chatEditingService.js';
import { chatAgentLeader, extractAgentAndCommand } from '../../common/chatParserTypes.js';
import { IChatService } from '../../common/chatService.js';
import { IChatWidget, IChatWidgetService } from '../chat.js';
Expand All @@ -37,7 +38,7 @@ export class SubmitAction extends Action2 {
f1: false,
category: CHAT_CATEGORY,
icon: Codicon.send,
precondition: ContextKeyExpr.and(CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()),
precondition: ContextKeyExpr.and(CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), ContextKeyExpr.or(CONTEXT_CHAT_LOCATION.notEqualsTo(ChatAgentLocation.EditingSession), ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()))),
keybinding: {
when: CONTEXT_IN_CHAT_INPUT,
primary: KeyCode.Enter,
Expand All @@ -51,7 +52,7 @@ export class SubmitAction extends Action2 {
{
id: MenuId.ChatExecute,
order: 4,
when: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(),
when: ContextKeyExpr.and(CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), ContextKeyExpr.or(CONTEXT_CHAT_LOCATION.notEqualsTo(ChatAgentLocation.EditingSession), ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()))),
group: 'navigation',
},
]
Expand Down Expand Up @@ -166,7 +167,7 @@ export class CancelAction extends Action2 {
icon: Codicon.stopCircle,
menu: {
id: MenuId.ChatExecute,
when: CONTEXT_CHAT_REQUEST_IN_PROGRESS,
when: ContextKeyExpr.and(CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_CHAT_LOCATION.notEqualsTo(ChatAgentLocation.EditingSession)),
order: 4,
group: 'navigation',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP
currentWidth: number,
private readonly codeBlockModelCollection: CodeBlockModelCollection,
private readonly rendererOptions: IChatListItemRendererOptions,
isResponseComplete: boolean,
@IContextKeyService contextKeyService: IContextKeyService,
@ITextModelService private readonly textModelService: ITextModelService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
Expand All @@ -74,7 +75,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP
const result = this._register(renderer.render(markdown, {
fillInIncompleteTokens,
codeBlockRendererSync: (languageId, text, raw) => {
const isCodeBlockComplete = !raw || raw?.endsWith('```');
const isCodeBlockComplete = isResponseComplete || !raw || raw?.endsWith('```');
joyceerhl marked this conversation as resolved.
Show resolved Hide resolved
const index = codeBlockIndex++;
let textModel: Promise<IResolvedTextEditorModel>;
let range: Range | undefined;
Expand Down
50 changes: 49 additions & 1 deletion src/vs/workbench/contrib/chat/browser/chatEditingActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@
*--------------------------------------------------------------------------------------------*/

import { Codicon } from '../../../../base/common/codicons.js';
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { ResourceSet } from '../../../../base/common/map.js';
import { URI } from '../../../../base/common/uri.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { localize, localize2 } from '../../../../nls.js';
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
import { EditorActivation } from '../../../../platform/editor/common/editor.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { IListService } from '../../../../platform/list/browser/listService.js';
import { GroupsOrder, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, WorkingSetEntryState } from '../common/chatEditingService.js';
import { ChatAgentLocation } from '../common/chatAgents.js';
import { CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS } from '../common/chatContextKeys.js';
import { applyingChatEditsContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, WorkingSetEntryState } from '../common/chatEditingService.js';
import { IChatService } from '../common/chatService.js';
import { CHAT_CATEGORY } from './actions/chatActions.js';
import { IChatExecuteActionContext } from './actions/chatExecuteActions.js';
import { IChatWidget, IChatWidgetService } from './chat.js';

abstract class WorkingSetAction extends Action2 {
Expand Down Expand Up @@ -273,3 +279,45 @@ registerAction2(class AddFilesToWorkingSetAction extends Action2 {
}
}
});

registerAction2(class CancelAction extends Action2 {
static readonly ID = 'workbench.action.chat.editing.cancel';
constructor() {
super({
id: CancelAction.ID,
title: localize2('workbench.action.chat.editing.cancel.label', "Cancel"),
f1: false,
category: CHAT_CATEGORY,
icon: Codicon.stopCircle,
menu: {
id: MenuId.ChatExecute,
when: ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.EditingSession), ContextKeyExpr.or(CONTEXT_CHAT_REQUEST_IN_PROGRESS, applyingChatEditsContextKey)),
joyceerhl marked this conversation as resolved.
Show resolved Hide resolved
order: 4,
group: 'navigation',
},
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.CtrlCmd | KeyCode.Escape,
win: { primary: KeyMod.Alt | KeyCode.Backspace },
}
});
}

run(accessor: ServicesAccessor, ...args: any[]) {
const context: IChatExecuteActionContext | undefined = args[0];

const widgetService = accessor.get(IChatWidgetService);
const widget = context?.widget ?? widgetService.lastFocusedWidget;
if (!widget) {
return;
}

const chatService = accessor.get(IChatService);
if (widget.viewModel) {
chatService.cancelCurrentRequestForSession(widget.viewModel.sessionId);
}

const chatEditingService = accessor.get(IChatEditingService);
chatEditingService.currentAutoApplyOperation?.cancel();
}
});
12 changes: 11 additions & 1 deletion src/vs/workbench/contrib/chat/browser/chatEditingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { MultiDiffEditor } from '../../multiDiffEditor/browser/multiDiffEditor.j
import { MultiDiffEditorInput } from '../../multiDiffEditor/browser/multiDiffEditorInput.js';
import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from '../../multiDiffEditor/browser/multiDiffSourceResolverService.js';
import { ICodeMapperResponse, ICodeMapperService } from '../common/chatCodeMapperService.js';
import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, ChatEditingSessionState, decidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../common/chatEditingService.js';
import { applyingChatEditsContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, ChatEditingSessionState, decidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../common/chatEditingService.js';
import { IChatResponseModel } from '../common/chatModel.js';
import { IChatService } from '../common/chatService.js';

Expand All @@ -44,6 +44,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
private readonly _currentSessionObs = observableValue<ChatEditingSession | null>(this, null);
private readonly _currentSessionDisposeListener = this._register(new MutableDisposable());

private readonly _currentAutoApplyOperationObs = observableValue<CancellationTokenSource | null>(this, null);
get currentAutoApplyOperation(): CancellationTokenSource | null {
return this._currentAutoApplyOperationObs.get();
}

get currentEditingSession(): IChatEditingSession | null {
return this._currentSessionObs.get();
}
Expand Down Expand Up @@ -78,6 +83,9 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
this._register(bindContextKey(inChatEditingSessionContextKey, contextKeyService, (reader) => {
return this._currentSessionObs.read(reader) !== null;
}));
this._register(bindContextKey(applyingChatEditsContextKey, contextKeyService, (reader) => {
return this._currentAutoApplyOperationObs.read(reader) !== null;
}));
this._register(this._chatService.onDidDisposeSession((e) => {
if (e.reason === 'cleared' && this._currentSessionObs.get()?.chatSessionId === e.sessionId) {
void this._currentSessionObs.get()?.stop();
Expand Down Expand Up @@ -208,6 +216,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
};
session.acceptStreamingEditsStart();
const cancellationTokenSource = new CancellationTokenSource();
this._currentAutoApplyOperationObs.set(cancellationTokenSource, undefined);
try {
if (editorPane) {
await editorPane?.showWhile(builder(stream, cancellationTokenSource.token));
Expand All @@ -223,6 +232,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
}
} finally {
cancellationTokenSource.dispose();
this._currentAutoApplyOperationObs.set(null, undefined);
session.resolve();
}
}
Expand Down
13 changes: 7 additions & 6 deletions src/vs/workbench/contrib/chat/browser/chatInputPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer;
this.initAttachedContext(this.attachedContextContainer);
this._register(this._attachmentModel.onDidChangeContext(() => this.initAttachedContext(this.attachedContextContainer)));
this.renderChatEditingSessionState(null, undefined, widget);
this.renderChatEditingSessionState(null, widget);

const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(inputContainer));
CONTEXT_IN_CHAT_INPUT.bindTo(inputScopedContextKeyService).set(true);
Expand Down Expand Up @@ -799,10 +799,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
hoverElement.appendChild(img);
}

async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null, initialState?: boolean, chatWidget?: IChatWidget) {
dom.setVisibility(Boolean(chatEditingSession) || Boolean(initialState), this.chatEditingSessionWidgetContainer);
async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null, chatWidget?: IChatWidget) {
dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer);

if (!chatEditingSession && !initialState) {
if (!chatEditingSession) {
dom.clearNode(this.chatEditingSessionWidgetContainer);
this._chatEditsDisposables.clear();
this._chatEditList = undefined;
Expand All @@ -811,7 +811,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
return;
}

if (this._chatEditList && chatEditingSession?.state.get() === ChatEditingSessionState.Idle) {
const currentChatEditingState = chatEditingSession.state.get();
if (this._chatEditList && (currentChatEditingState === ChatEditingSessionState.Idle || currentChatEditingState === ChatEditingSessionState.Initial && !chatWidget?.viewModel?.requestInProgress)) {
this._chatEditsProgress?.stop();
this._chatEditsProgress?.dispose();
this._chatEditsProgress = undefined;
Expand Down Expand Up @@ -894,7 +895,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
return;
}

if (!this._chatEditsProgress && (chatEditingSession.state.get() === ChatEditingSessionState.StreamingEdits || chatWidget?.viewModel?.requestInProgress)) {
if (!this._chatEditsProgress && (currentChatEditingState === ChatEditingSessionState.StreamingEdits || chatWidget?.viewModel?.requestInProgress)) {
this._chatEditsProgress = new ProgressBar(innerContainer);
this._chatEditsProgress.infinite().show(500);
}
Expand Down
12 changes: 6 additions & 6 deletions src/vs/workbench/contrib/chat/browser/chatListRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
content: value,
preceedingContentParts: parts,
};
const newPart = this.renderChatContentPart(data, templateData, context);
const newPart = this.renderChatContentPart(data, templateData, context, false);
if (newPart) {
templateData.value.appendChild(newPart.domNode);
parts.push(newPart);
Expand Down Expand Up @@ -650,7 +650,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
preceedingContentParts,
contentIndex: index,
};
const newPart = this.renderChatContentPart(partToRender, templateData, context);
const newPart = this.renderChatContentPart(partToRender, templateData, context, true);
if (newPart) {
// Maybe the part can't be rendered in this context, but this shouldn't really happen
if (alreadyRenderedPart) {
Expand Down Expand Up @@ -766,7 +766,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
return diff;
}

private renderChatContentPart(content: IChatRendererContent, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined {
private renderChatContentPart(content: IChatRendererContent, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext, isProgressiveRender: boolean): IChatContentPart | undefined {
if (content.kind === 'treeData') {
return this.renderTreeData(content, templateData, context);
} else if (content.kind === 'progressMessage') {
Expand All @@ -782,7 +782,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
} else if (content.kind === 'warning') {
return this.instantiationService.createInstance(ChatWarningContentPart, 'warning', content.content, this.renderer);
} else if (content.kind === 'markdownContent') {
return this.renderMarkdown(content.content, templateData, context);
return this.renderMarkdown(content.content, templateData, context, isProgressiveRender);
} else if (content.kind === 'references') {
return this.renderContentReferencesListData(content, undefined, context, templateData);
} else if (content.kind === 'codeCitations') {
Expand Down Expand Up @@ -880,11 +880,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
return textEditPart;
}

private renderMarkdown(markdown: IMarkdownString, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart {
private renderMarkdown(markdown: IMarkdownString, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext, isProgressiveRender: boolean): IChatContentPart {
const element = context.element;
const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete || !!element.renderData);
const codeBlockStartIndex = context.preceedingContentParts.reduce((acc, part) => acc + (part instanceof ChatMarkdownContentPart ? part.codeblocks.length : 0), 0);
const markdownPart = this.instantiationService.createInstance(ChatMarkdownContentPart, markdown, context, this._editorPool, fillInIncompleteTokens, codeBlockStartIndex, this.renderer, this._currentLayoutWidth, this.codeBlockModelCollection, this.rendererOptions);
const markdownPart = this.instantiationService.createInstance(ChatMarkdownContentPart, markdown, context, this._editorPool, fillInIncompleteTokens, codeBlockStartIndex, this.renderer, this._currentLayoutWidth, this.codeBlockModelCollection, this.rendererOptions, !isProgressiveRender);
const markdownPartId = markdownPart.id;
markdownPart.addDisposable(markdownPart.onDidChangeHeight(() => {
markdownPart.layout(this._currentLayoutWidth);
Expand Down
6 changes: 5 additions & 1 deletion src/vs/workbench/contrib/chat/browser/chatWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
}

private async renderChatEditingSessionState(session: IChatEditingSession | null) {
this.inputPart.renderChatEditingSessionState(session, undefined, this);
this.inputPart.renderChatEditingSessionState(session, this);

if (this.bodyDimension) {
this.layout(this.bodyDimension.height, this.bodyDimension.width);
Expand Down Expand Up @@ -816,6 +816,10 @@ export class ChatWidget extends Disposable implements IChatWidget {
revealLastElement(this.tree);
this.focusInput();
}

if (this.chatEditingService.currentEditingSession && this.chatEditingService.currentEditingSession?.chatSessionId === this.viewModel?.sessionId) {
this.renderChatEditingSessionState(this.chatEditingService.currentEditingSession);
}
}));
this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => {
// Ensure that view state is saved here, because we will load it again when a new model is assigned
Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/contrib/chat/common/chatEditingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
import { Event } from '../../../../base/common/event.js';
import { IObservable, ITransaction } from '../../../../base/common/observable.js';
import { URI } from '../../../../base/common/uri.js';
Expand All @@ -20,6 +21,7 @@ export interface IChatEditingService {
readonly onDidCreateEditingSession: Event<IChatEditingSession>;

readonly currentEditingSession: IChatEditingSession | null;
readonly currentAutoApplyOperation: CancellationTokenSource | null;

startOrContinueEditingSession(chatSessionId: string, options?: { silent: boolean }): Promise<IChatEditingSession>;
addFileToWorkingSet(resource: URI): Promise<void>;
Expand Down Expand Up @@ -76,3 +78,4 @@ export const chatEditingWidgetFileStateContextKey = new RawContextKey<WorkingSet
export const decidedChatEditingResourceContextKey = new RawContextKey<string[]>('decidedChatEditingResource', []);
export const chatEditingResourceContextKey = new RawContextKey<string | undefined>('chatEditingResource', undefined);
export const inChatEditingSessionContextKey = new RawContextKey<boolean | undefined>('inChatEditingSession', undefined);
export const applyingChatEditsContextKey = new RawContextKey<boolean | undefined>('isApplyingChatEdits', undefined);
Loading