Skip to content

Commit 07f2e22

Browse files
roblourensCopilot
andauthored
Work around for missing stop button (#307938)
* Work around for missing stop button For #283328 Co-authored-by: Copilot <copilot@github.com> * Simplify to chatSessionHasActiveRequest --------- Co-authored-by: Copilot <copilot@github.com>
1 parent 4e5ed5d commit 07f2e22

6 files changed

Lines changed: 107 additions & 27 deletions

File tree

src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -181,23 +181,7 @@ abstract class SubmitAction extends Action2 {
181181
}
182182
}
183183

184-
const requestInProgressOrPendingToolCall = ContextKeyExpr.or(
185-
ChatContextKeys.requestInProgress,
186-
ChatContextKeys.Editing.hasToolConfirmation,
187-
ChatContextKeys.Editing.hasQuestionCarousel,
188-
);
189-
const requestInProgressWithoutInput = ContextKeyExpr.and(
190-
ChatContextKeys.requestInProgress,
191-
ChatContextKeys.inputHasText.negate(),
192-
);
193-
const pendingToolCall = ContextKeyExpr.or(
194-
ChatContextKeys.Editing.hasToolConfirmation,
195-
ContextKeyExpr.and(ChatContextKeys.Editing.hasQuestionCarousel, ChatContextKeys.inputHasText.negate()),
196-
);
197-
const noQuestionCarouselOrHasInput = ContextKeyExpr.or(
198-
ChatContextKeys.Editing.hasQuestionCarousel.negate(),
199-
ChatContextKeys.inputHasText,
200-
);
184+
const whenNoActiveRequest = ChatContextKeys.hasActiveRequest.negate();
201185
const whenNotInProgress = ChatContextKeys.requestInProgress.negate();
202186

203187
export class ChatSubmitAction extends SubmitAction {
@@ -236,10 +220,9 @@ export class ChatSubmitAction extends SubmitAction {
236220
id: MenuId.ChatExecute,
237221
order: 4,
238222
when: ContextKeyExpr.and(
239-
whenNotInProgress,
223+
whenNoActiveRequest,
240224
menuCondition,
241225
ChatContextKeys.withinEditSessionDiff.negate(),
242-
noQuestionCarouselOrHasInput,
243226
),
244227
group: 'navigation',
245228
alt: {
@@ -253,8 +236,7 @@ export class ChatSubmitAction extends SubmitAction {
253236
order: 4,
254237
when: ContextKeyExpr.and(
255238
ContextKeyExpr.or(ctxHasEditorModification.negate(), ChatContextKeys.inputHasText),
256-
whenNotInProgress,
257-
ChatContextKeys.requestInProgress.negate(),
239+
whenNoActiveRequest,
258240
menuCondition
259241
),
260242
}]
@@ -735,7 +717,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction {
735717

736718
constructor() {
737719
const notInProgressOrEditing = ContextKeyExpr.and(
738-
ContextKeyExpr.or(whenNotInProgress, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.Sent)),
720+
ContextKeyExpr.or(whenNoActiveRequest, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.Sent)),
739721
ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.Queue),
740722
ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.Steer)
741723
);
@@ -760,8 +742,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction {
760742
order: 4,
761743
when: ContextKeyExpr.and(
762744
notInProgressOrEditing,
763-
menuCondition,
764-
noQuestionCarouselOrHasInput),
745+
menuCondition),
765746
group: 'navigation',
766747
alt: {
767748
id: 'workbench.action.chat.sendToNewChat',
@@ -919,7 +900,7 @@ export class CancelAction extends Action2 {
919900
menu: [{
920901
id: MenuId.ChatExecute,
921902
when: ContextKeyExpr.and(
922-
ContextKeyExpr.or(requestInProgressWithoutInput, pendingToolCall),
903+
ChatContextKeys.hasActiveRequest,
923904
ChatContextKeys.remoteJobCreating.negate(),
924905
ChatContextKeys.currentlyEditing.negate(),
925906
),
@@ -940,7 +921,7 @@ export class CancelAction extends Action2 {
940921
weight: KeybindingWeight.WorkbenchContrib,
941922
primary: KeyMod.CtrlCmd | KeyCode.Escape,
942923
when: ContextKeyExpr.and(
943-
requestInProgressOrPendingToolCall,
924+
ChatContextKeys.hasActiveRequest,
944925
ChatContextKeys.remoteJobCreating.negate()
945926
),
946927
win: { primary: KeyMod.Alt | KeyCode.Backspace },

src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
283283
private bodyDimension: dom.Dimension | undefined;
284284
private visibleChangeCount = 0;
285285
private requestInProgress: IContextKey<boolean>;
286+
private hasActiveRequest: IContextKey<boolean>;
286287
private agentInInput: IContextKey<boolean>;
287288

288289
private _visible = false;
@@ -446,6 +447,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
446447
ChatContextKeys.inQuickChat.bindTo(contextKeyService).set(isQuickChat(this));
447448
this.agentInInput = ChatContextKeys.inputHasAgent.bindTo(contextKeyService);
448449
this.requestInProgress = ChatContextKeys.requestInProgress.bindTo(contextKeyService);
450+
this.hasActiveRequest = ChatContextKeys.hasActiveRequest.bindTo(contextKeyService);
449451

450452
this._register(this.chatEntitlementService.onDidChangeAnonymous(() => this.renderWelcomeViewContentIfNeeded()));
451453

@@ -2023,6 +2025,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
20232025
}
20242026

20252027
this.requestInProgress.set(this.viewModel.model.requestInProgress.get());
2028+
this.hasActiveRequest.set(this.viewModel.model.hasActiveRequest.get());
20262029

20272030
// Update the editor's placeholder text when it changes in the view model
20282031
if (events?.some(e => e?.kind === 'changePlaceholder')) {

src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export namespace ChatContextKeys {
1818
export const responseIsFiltered = new RawContextKey<boolean>('chatSessionResponseFiltered', false, { type: 'boolean', description: localize('chatResponseFiltered', "True when the chat response was filtered out by the server.") });
1919
export const responseHasError = new RawContextKey<boolean>('chatSessionResponseError', false, { type: 'boolean', description: localize('chatResponseErrored', "True when the chat response resulted in an error.") });
2020
export const requestInProgress = new RawContextKey<boolean>('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") });
21+
export const hasActiveRequest = new RawContextKey<boolean>('chatSessionHasActiveRequest', false, { type: 'boolean', description: localize('chatSessionHasActiveRequest', "True when the current chat response has not completed, regardless of intermediate states like tool calls or elicitations.") });
2122
export const currentlyEditing = new RawContextKey<boolean>('chatSessionCurrentlyEditing', false, { type: 'boolean', description: localize('interactiveSessionCurrentlyEditing', "True when the current request is being edited.") });
2223
export const currentlyEditingInput = new RawContextKey<boolean>('chatSessionCurrentlyEditingInput', false, { type: 'boolean', description: localize('interactiveSessionCurrentlyEditingInput', "True when the current request input at the bottom is being edited.") });
2324

src/vs/workbench/contrib/chat/common/model/chatModel.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,12 @@ export interface IChatResponseModel {
277277
readonly isCanceled: boolean;
278278
readonly isPendingConfirmation: IObservable<{ startedWaitingAt: number; detail?: string } | undefined>;
279279
readonly isInProgress: IObservable<boolean>;
280+
/**
281+
* True whenever this response has not reached a terminal state yet.
282+
* Unlike {@link isInProgress}, this remains true during tool confirmations,
283+
* elicitations, and any other intermediate state.
284+
*/
285+
readonly isIncomplete: IObservable<boolean>;
280286
readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
281287
readonly shouldBeBlocked: IObservable<boolean>;
282288
readonly isCompleteAddedRequest: boolean;
@@ -1177,6 +1183,14 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
11771183

11781184
readonly isInProgress: IObservable<boolean>;
11791185

1186+
/**
1187+
* True whenever this response has not reached a terminal state yet.
1188+
* Unlike {@link isInProgress}, this remains true during tool confirmations,
1189+
* elicitations, and any other intermediate state. It only becomes false when
1190+
* the response completes, is cancelled, or fails.
1191+
*/
1192+
readonly isIncomplete: IObservable<boolean>;
1193+
11801194
private _responseView?: ResponseView;
11811195
public get response(): IResponse {
11821196
const undoStop = this._shouldBeRemovedOnSend?.afterUndoStop;
@@ -1264,6 +1278,10 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
12641278
&& (this._modelState.read(r).value === ResponseModelState.Pending || this._modelState.read(r).value === ResponseModelState.NeedsInput);
12651279
});
12661280

1281+
this.isIncomplete = this._modelState.map(state => {
1282+
return state.value === ResponseModelState.Pending || state.value === ResponseModelState.NeedsInput;
1283+
});
1284+
12671285
this._register(this._response.onDidChangeValue(() => this._onDidChange.fire(defaultChatResponseModelChangeReason)));
12681286
this.id = params.restoredId ?? 'response_' + generateUuid();
12691287

@@ -1458,6 +1476,8 @@ export interface IChatModel extends IDisposable {
14581476
readonly responderUsername: string;
14591477
/** True whenever a request is currently running */
14601478
readonly requestInProgress: IObservable<boolean>;
1479+
/** True whenever the last request has not reached a terminal state, regardless of intermediate states like tool calls or elicitations */
1480+
readonly hasActiveRequest: IObservable<boolean>;
14611481
/** Provides session information when a request needs user interaction to continue */
14621482
readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;
14631483
readonly inputPlaceholder?: string;
@@ -2115,6 +2135,7 @@ export class ChatModel extends Disposable implements IChatModel {
21152135
}
21162136

21172137
readonly requestInProgress: IObservable<boolean>;
2138+
readonly hasActiveRequest: IObservable<boolean>;
21182139
readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;
21192140

21202141
/** Input model for managing input state */
@@ -2291,6 +2312,10 @@ export class ChatModel extends Disposable implements IChatModel {
22912312
return request?.response?.isInProgress.read(r) ?? false;
22922313
});
22932314

2315+
this.hasActiveRequest = this.lastRequestObs.map((request, r) => {
2316+
return request?.response?.isIncomplete.read(r) ?? false;
2317+
});
2318+
22942319
this.requestNeedsInput = this.lastRequestObs.map((request, r) => {
22952320
const pendingInfo = request?.response?.isPendingConfirmation.read(r);
22962321
if (!pendingInfo) {

src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { IChatRequestImplicitVariableEntry, IChatRequestStringVariableEntry, ICh
2727
import { ChatAgentService, IChatAgentService } from '../../../common/participants/chatAgents.js';
2828
import { ChatModel, ChatRequestModel, ChatResponseResource, IChatRequestModeInfo, IExportableChatData, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, isExportableSessionData, isSerializableSessionData, normalizeSerializableChatData, Response } from '../../../common/model/chatModel.js';
2929
import { ChatRequestTextPart } from '../../../common/requestParser/chatParserTypes.js';
30-
import { ChatRequestQueueKind, IChatService, IChatTerminalToolInvocationData, IChatToolInvocation } from '../../../common/chatService/chatService.js';
30+
import { ChatRequestQueueKind, IChatService, IChatTerminalToolInvocationData, IChatToolInvocation, ResponseModelState } from '../../../common/chatService/chatService.js';
3131
import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js';
3232
import { MockChatService } from '../chatService/mockChatService.js';
3333

@@ -1067,6 +1067,75 @@ suite('ChatResponseModel', () => {
10671067
clock.restore();
10681068
}
10691069
});
1070+
1071+
test('isIncomplete stays true during tool confirmations', async () => {
1072+
const clock = sinon.useFakeTimers();
1073+
try {
1074+
const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true }));
1075+
1076+
const text = 'hello';
1077+
const request = model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0);
1078+
const response = request.response!;
1079+
1080+
// Initially incomplete and in progress
1081+
assert.strictEqual(response.isIncomplete.get(), true);
1082+
assert.strictEqual(response.isInProgress.get(), true);
1083+
1084+
// Add a pending tool confirmation
1085+
const toolState = observableValue<any>('state', { type: 1 /* IChatToolInvocation.StateKind.WaitingForConfirmation */, confirmationMessages: { title: 'Please confirm' } });
1086+
const toolInvocation = {
1087+
kind: 'toolInvocation',
1088+
invocationMessage: 'calling tool',
1089+
state: toolState
1090+
} as Partial<IChatToolInvocation> as IChatToolInvocation;
1091+
model.acceptResponseProgress(request, toolInvocation);
1092+
1093+
// isInProgress should be false (it factors out pending confirmations), but isIncomplete should remain true
1094+
assert.strictEqual(response.isInProgress.get(), false);
1095+
assert.strictEqual(response.isIncomplete.get(), true);
1096+
1097+
// Resolve tool confirmation
1098+
toolState.set({ type: 4 /* IChatToolInvocation.StateKind.Completed */ }, undefined);
1099+
assert.strictEqual(response.isInProgress.get(), true);
1100+
assert.strictEqual(response.isIncomplete.get(), true);
1101+
1102+
// Complete the response
1103+
response.complete();
1104+
assert.strictEqual(response.isInProgress.get(), false);
1105+
assert.strictEqual(response.isIncomplete.get(), false);
1106+
assert.strictEqual(response.state, ResponseModelState.Complete);
1107+
} finally {
1108+
clock.restore();
1109+
}
1110+
});
1111+
1112+
test('isIncomplete becomes false on cancellation', async () => {
1113+
const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true }));
1114+
1115+
const text = 'hello';
1116+
const request = model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0);
1117+
const response = request.response!;
1118+
1119+
assert.strictEqual(response.isIncomplete.get(), true);
1120+
1121+
model.cancelRequest(request);
1122+
assert.strictEqual(response.isIncomplete.get(), false);
1123+
assert.strictEqual(response.state, ResponseModelState.Cancelled);
1124+
});
1125+
1126+
test('hasActiveRequest reflects last request isIncomplete', async () => {
1127+
const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true }));
1128+
1129+
assert.strictEqual(model.hasActiveRequest.get(), false);
1130+
1131+
const text = 'hello';
1132+
const request = model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0);
1133+
1134+
assert.strictEqual(model.hasActiveRequest.get(), true);
1135+
1136+
request.response!.complete();
1137+
assert.strictEqual(model.hasActiveRequest.get(), false);
1138+
});
10701139
});
10711140

10721141
suite('ChatModel - Pending Requests', () => {

src/vs/workbench/contrib/chat/test/common/model/mockChatModel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class MockChatModel extends Disposable implements IChatModel {
2626
creationDate = Date.now();
2727
requests: IChatRequestModel[] = [];
2828
readonly requestInProgress = observableValue('requestInProgress', false);
29+
readonly hasActiveRequest = observableValue('hasActiveRequest', false);
2930
readonly requestNeedsInput = observableValue<IChatRequestNeedsInputInfo | undefined>('requestNeedsInput', undefined);
3031
readonly inputPlaceholder = undefined;
3132
readonly editingSession = undefined;

0 commit comments

Comments
 (0)