Skip to content

Commit ef1e9ba

Browse files
authored
Fix coding agent cancellation via stop button. (microsoft#258406)
1 parent a5f6ece commit ef1e9ba

File tree

5 files changed

+57
-9
lines changed

5 files changed

+57
-9
lines changed

src/vs/workbench/api/browser/mainThreadChatSessions.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { Emitter, Event } from '../../../base/common/event.js';
99
import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
1010
import { revive } from '../../../base/common/marshalling.js';
1111
import { URI, UriComponents } from '../../../base/common/uri.js';
12+
import { localize } from '../../../nls.js';
13+
import { IDialogService } from '../../../platform/dialogs/common/dialogs.js';
1214
import { ILogService } from '../../../platform/log/common/log.js';
1315
import { ChatViewId } from '../../contrib/chat/browser/chat.js';
1416
import { ChatViewPane } from '../../contrib/chat/browser/chatViewPane.js';
@@ -42,6 +44,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
4244
constructor(
4345
private readonly _extHostContext: IExtHostContext,
4446
@IChatSessionsService private readonly _chatSessionsService: IChatSessionsService,
47+
@IDialogService private readonly _dialogService: IDialogService,
4548
@IEditorService private readonly _editorService: IEditorService,
4649
@ILogService private readonly _logService: ILogService,
4750
@IViewsService private readonly _viewsService: IViewsService,
@@ -88,12 +91,29 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
8891
const progressEmitter = new Emitter<IChatProgress[]>;
8992
const completionEmitter = new Emitter<void>();
9093
let progressEvent: Event<IChatProgress[]> | undefined = undefined;
94+
let interruptActiveResponseCallback: (() => Promise<boolean>) | undefined = undefined;
9195
if (sessionContent.hasActiveResponseCallback) {
9296
const requestId = 'ongoing';
9397
// set progress
9498
progressEvent = progressEmitter.event;
9599
// store the event emitter using a key that combines handle and session id
96100
const progressKey = `${providerHandle}_${id}_${requestId}`;
101+
interruptActiveResponseCallback = async () => {
102+
return this._dialogService.confirm({
103+
message: localize('interruptActiveResponse', 'Are you sure you want to interrupt the active session?')
104+
}).then(confirmed => {
105+
if (confirmed.confirmed) {
106+
this._proxy.$interruptChatSessionActiveResponse(providerHandle, id, requestId);
107+
return true;
108+
} else {
109+
progressEmitter.fire([{
110+
kind: 'progressMessage',
111+
content: { value: '' }
112+
}]);
113+
return false;
114+
}
115+
});
116+
};
97117
this._activeProgressEmitters.set(progressKey, progressEmitter);
98118
this._completionEmitters.set(progressKey, completionEmitter);
99119
}
@@ -127,6 +147,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
127147
}),
128148
progressEvent: progressEvent,
129149
requestHandler: requestHandler,
150+
interruptActiveResponseCallback: interruptActiveResponseCallback,
130151
dispose: () => {
131152
progressEmitter.dispose();
132153
completionEmitter.dispose();

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3147,6 +3147,7 @@ export interface ExtHostChatSessionsShape {
31473147
$provideChatSessionItems(providerHandle: number, token: CancellationToken): Promise<Dto<IChatSessionItem>[]>;
31483148

31493149
$provideChatSessionContent(providerHandle: number, sessionId: string, token: CancellationToken): Promise<ChatSessionDto>;
3150+
$interruptChatSessionActiveResponse(providerHandle: number, sessionId: string, requestId: string): Promise<void>;
31503151
$disposeChatSessionContent(providerHandle: number, sessionId: string): Promise<void>;
31513152
$invokeChatSessionRequestHandler(providerHandle: number, id: string, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise<IChatAgentResult>;
31523153
}

src/vs/workbench/api/common/extHostChatSessions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,12 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
208208
};
209209
}
210210

211+
async $interruptChatSessionActiveResponse(providerHandle: number, sessionId: string, requestId: string): Promise<void> {
212+
const key = `${providerHandle}_${sessionId}`;
213+
const entry = this._extHostChatSessions.get(key);
214+
entry?.disposeCts.cancel();
215+
}
216+
211217
async $disposeChatSessionContent(providerHandle: number, sessionId: string): Promise<void> {
212218
const key = `${providerHandle}_${sessionId}`;
213219
const entry = this._extHostChatSessions.get(key);

src/vs/workbench/contrib/chat/common/chatServiceImpl.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ErrorNoTelemetry } from '../../../../base/common/errors.js';
1111
import { Emitter, Event } from '../../../../base/common/event.js';
1212
import { MarkdownString } from '../../../../base/common/htmlContent.js';
1313
import { Iterable } from '../../../../base/common/iterator.js';
14-
import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
14+
import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
1515
import { revive } from '../../../../base/common/marshalling.js';
1616
import { autorun, derived, IObservable, ObservableMap } from '../../../../base/common/observable.js';
1717
import { StopWatch } from '../../../../base/common/stopwatch.js';
@@ -636,15 +636,34 @@ export class ChatService extends Disposable implements IChatService {
636636
}
637637
}
638638

639-
if (content.progressEvent) {
640-
disposables.add(content.progressEvent(e => {
641-
if (lastRequest) {
642-
for (const progress of e) {
643-
if (progress.kind === 'progressMessage' && progress.content.value === 'Session completed') {
644-
model?.completeResponse(lastRequest);
645-
} else {
646-
model?.acceptResponseProgress(lastRequest, progress);
639+
if (content.progressEvent && lastRequest) {
640+
const initialCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined);
641+
this._pendingRequests.set(model.sessionId, initialCancellationRequest);
642+
const cancellationListener = new MutableDisposable();
643+
644+
const createCancellationListener = (token: CancellationToken) => {
645+
return token.onCancellationRequested(() => {
646+
content.interruptActiveResponseCallback?.().then(userConfirmedInterruption => {
647+
if (!userConfirmedInterruption) {
648+
// User cancelled the interruption
649+
const newCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined);
650+
this._pendingRequests.set(model.sessionId, newCancellationRequest);
651+
cancellationListener.value = createCancellationListener(newCancellationRequest.cancellationTokenSource.token);
647652
}
653+
});
654+
});
655+
};
656+
657+
cancellationListener.value = createCancellationListener(initialCancellationRequest.cancellationTokenSource.token);
658+
disposables.add(cancellationListener);
659+
660+
disposables.add(content.progressEvent(e => {
661+
for (const progress of e) {
662+
if (progress.kind === 'progressMessage' && progress.content.value === 'Session completed') {
663+
model?.completeResponse(lastRequest);
664+
cancellationListener.clear();
665+
} else {
666+
model?.acceptResponseProgress(lastRequest, progress);
648667
}
649668
}
650669
}));

src/vs/workbench/contrib/chat/common/chatSessionsService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface ChatSession extends IDisposable {
4040
| { type: 'response'; parts: IChatProgress[] }>;
4141

4242
readonly progressEvent?: Event<IChatProgress[]>;
43+
readonly interruptActiveResponseCallback?: () => Promise<boolean>;
4344

4445
requestHandler?: (
4546
request: IChatAgentRequest,

0 commit comments

Comments
 (0)