From 7ec9c8eb079b4e7ea506568264ab3a6f0711ed85 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 26 Nov 2025 05:01:26 +0000 Subject: [PATCH] Support restoring edits for background sessions (#279270) * Support restoring edits for background sessions * revert change --- .../api/browser/mainThreadChatAgents2.ts | 2 +- .../api/browser/mainThreadChatSessions.ts | 3 ++- src/vs/workbench/api/common/extHost.protocol.ts | 1 + .../workbench/api/common/extHostChatAgents2.ts | 13 +++++++------ .../workbench/api/common/extHostChatSessions.ts | 1 + .../api/common/extHostTypeConverters.ts | 3 ++- src/vs/workbench/api/common/extHostTypes.ts | 13 ++++++++----- .../browser/chatEditing/chatEditingSession.ts | 16 ++++++---------- .../contrib/chat/common/chatEditingService.ts | 2 +- .../workbench/contrib/chat/common/chatModel.ts | 6 +++--- .../workbench/contrib/chat/common/chatService.ts | 2 ++ .../contrib/chat/common/chatServiceImpl.ts | 5 ++++- .../contrib/chat/common/chatSessionsService.ts | 1 + ...vscode.proposed.chatParticipantAdditions.d.ts | 7 ++++--- .../vscode.proposed.chatParticipantPrivate.d.ts | 5 +++++ 15 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 577b855d11c..1e229af643a 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -264,7 +264,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA const response = chatSession?.getRequests().at(-1)?.response; if (chatSession?.editingSession && responsePartHandle !== undefined && response) { const parts = progress.start - ? await chatSession.editingSession.startExternalEdits(response, responsePartHandle, revive(progress.resources)) + ? await chatSession.editingSession.startExternalEdits(response, responsePartHandle, revive(progress.resources), progress.undoStopId) : await chatSession.editingSession.stopExternalEdits(response, responsePartHandle); chatProgressParts.push(...parts); } diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index c2b171bcdc4..f45ec7372f9 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -120,7 +120,8 @@ export class ObservableChatSession extends Disposable implements IChatSession { prompt: turn.prompt, participant: turn.participant, command: turn.command, - variableData: variables ? { variables } : undefined + variableData: variables ? { variables } : undefined, + id: turn.id }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e0307629ded..7741ad87429 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3252,6 +3252,7 @@ export interface MainThreadChatStatusShape { } export type IChatSessionHistoryItemDto = { + id?: string; type: 'request'; prompt: string; participant: string; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 3c0b9afcd65..39a2ffea309 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -279,12 +279,13 @@ export class ChatAgentResponseStream { throwIfDone(this.externalEdit); const resources = Array.isArray(target) ? target : [target]; const operationId = taskHandlePool++; - - await send({ kind: 'externalEdits', start: true, resources }, operationId); + const undoStopId = generateUuid(); + await send({ kind: 'externalEdits', start: true, resources, undoStopId }, operationId); try { - return await callback(); + await callback(); + return undoStopId; } finally { - await send({ kind: 'externalEdits', start: false, resources }, operationId); + await send({ kind: 'externalEdits', start: false, resources, undoStopId }, operationId); } }, confirmation(title, message, data, buttons) { @@ -359,7 +360,7 @@ export class ChatAgentResponseStream { return this; } else if (part instanceof extHostTypes.ChatResponseExternalEditPart) { const p = this.externalEdit(part.uris, part.callback); - p.then(() => part.didGetApplied()); + p.then((value) => part.didGetApplied(value)); return this; } else { const dto = typeConvert.ChatResponsePart.from(part, that._commandsConverter, that._sessionDisposables); @@ -702,7 +703,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } const editedFileEvents = isProposedApiEnabled(extension, 'chatParticipantPrivate') ? h.request.editedFileEvents : undefined; - const turn = new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId, toolReferences, editedFileEvents); + const turn = new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId, toolReferences, editedFileEvents, h.request.requestId); res.push(turn); // RESPONSE turn diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index f38b5d8d32c..5cd735474c9 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -398,6 +398,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio const variables = turn.references.map(ref => this.convertReferenceToVariable(ref)); return { type: 'request' as const, + id: turn.id, prompt: turn.prompt, participant: turn.participant, command: turn.command, diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index fb7bad8be58..5a576226384 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2608,10 +2608,11 @@ export namespace ChatResponseCodeblockUriPart { kind: 'codeblockUri', uri: part.value, isEdit: part.isEdit, + undoStopId: part.undoStopId }; } export function to(part: Dto): vscode.ChatResponseCodeblockUriPart { - return new types.ChatResponseCodeblockUriPart(URI.revive(part.uri), part.isEdit); + return new types.ChatResponseCodeblockUriPart(URI.revive(part.uri), part.isEdit, part.undoStopId); } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 1e2266d3592..3f65deae449 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3166,14 +3166,14 @@ export class ChatResponseMultiDiffPart { } export class ChatResponseExternalEditPart { - applied: Thenable; - didGetApplied!: () => void; + applied: Thenable; + didGetApplied!: (value: string) => void; constructor( public uris: vscode.Uri[], public callback: () => Thenable, ) { - this.applied = new Promise((resolve) => { + this.applied = new Promise((resolve) => { this.didGetApplied = resolve; }); } @@ -3252,10 +3252,12 @@ export class ChatResponseReferencePart { export class ChatResponseCodeblockUriPart { isEdit?: boolean; + undoStopId?: string; value: vscode.Uri; - constructor(value: vscode.Uri, isEdit?: boolean) { + constructor(value: vscode.Uri, isEdit?: boolean, undoStopId?: string) { this.value = value; this.isEdit = isEdit; + this.undoStopId = undoStopId; } } @@ -3385,7 +3387,8 @@ export class ChatRequestTurn implements vscode.ChatRequestTurn2 { readonly references: vscode.ChatPromptReference[], readonly participant: string, readonly toolReferences: vscode.ChatLanguageModelToolReference[], - readonly editedFileEvents?: vscode.ChatRequestEditedFileEvent[] + readonly editedFileEvents?: vscode.ChatRequestEditedFileEvent[], + readonly id?: string ) { } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 37175c2420c..8279ff2010a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -16,7 +16,6 @@ import { derived, IObservable, IReader, ITransaction, observableValue, transacti import { isEqual } from '../../../../../base/common/resources.js'; import { hasKey, Mutable } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; -import { generateUuid } from '../../../../../base/common/uuid.js'; import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; @@ -89,7 +88,7 @@ class ThrottledSequencer extends Sequencer { } } -function createOpeningEditCodeBlock(uri: URI, isNotebook: boolean): IChatProgress[] { +function createOpeningEditCodeBlock(uri: URI, isNotebook: boolean, undoStopId: string): IChatProgress[] { return [ { kind: 'markdownContent', @@ -98,7 +97,8 @@ function createOpeningEditCodeBlock(uri: URI, isNotebook: boolean): IChatProgres { kind: 'codeblockUri', uri, - isEdit: true + isEdit: true, + undoStopId }, { kind: 'markdownContent', @@ -532,15 +532,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }; } - async startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[]): Promise { + async startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[], undoStopId: string): Promise { const snapshots = new ResourceMap(); const acquiredLockPromises: DeferredPromise[] = []; const releaseLockPromises: DeferredPromise[] = []; - const undoStopId = generateUuid(); - const progress: IChatProgress[] = [{ - kind: 'undoStop', - id: undoStopId, - }]; + const progress: IChatProgress[] = []; const telemetryInfo = this._getTelemetryInfoForModel(responseModel); await chatEditingSessionIsReady(this); @@ -566,7 +562,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const notebookUri = CellUri.parse(resource)?.notebook || resource; - progress.push(...createOpeningEditCodeBlock(resource, this._notebookService.hasSupportedNotebooks(notebookUri))); + progress.push(...createOpeningEditCodeBlock(resource, this._notebookService.hasSupportedNotebooks(notebookUri), undoStopId)); // Save to disk to ensure disk state is current before external edits await entry?.save(); diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index dfdbdd60c9d..335718a25de 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -134,7 +134,7 @@ export interface IChatEditingSession extends IDisposable { * agents that make changes on-disk rather than streaming edits through the * chat session. */ - startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[]): Promise; + startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[], undoStopId: string): Promise; stopExternalEdits(responseModel: IChatResponseModel, operationId: number): Promise; /** diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 7caa49e8ace..96796f900b9 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -1932,10 +1932,11 @@ export class ChatModel extends Disposable implements IChatModel { this._onDidChange.fire({ kind: 'setHidden' }); } - addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, modeInfo?: IChatRequestModeInfo, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string, userSelectedTools?: UserSelectedTools): ChatRequestModel { + addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, modeInfo?: IChatRequestModeInfo, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string, userSelectedTools?: UserSelectedTools, id?: string): ChatRequestModel { const editedFileEvents = [...this.currentEditedFileEvents.values()]; this.currentEditedFileEvents.clear(); const request = new ChatRequestModel({ + restoredId: id, session: this, message, variableData, @@ -2016,7 +2017,7 @@ export class ChatModel extends Disposable implements IChatModel { } else if (progress.kind === 'move') { this._onDidChange.fire({ kind: 'move', target: progress.uri, range: progress.range }); } else if (progress.kind === 'codeblockUri' && progress.isEdit) { - request.response.addUndoStop({ id: generateUuid(), kind: 'undoStop' }); + request.response.addUndoStop({ id: progress.undoStopId ?? generateUuid(), kind: 'undoStop' }); request.response.updateContent(progress, quiet); } else if (progress.kind === 'progressTaskResult') { // Should have been handled upstream, not sent to model @@ -2061,7 +2062,6 @@ export class ChatModel extends Disposable implements IChatModel { // Maybe something went wrong? return; } - request.response.setFollowups(followups); } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 4b401290b4d..12446114322 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -194,6 +194,7 @@ export interface IChatUndoStop { export interface IChatExternalEditsDto { kind: 'externalEdits'; + undoStopId: string; start: boolean; /** true=start, false=stop */ resources: UriComponents[]; } @@ -228,6 +229,7 @@ export interface IChatResponseCodeblockUriPart { kind: 'codeblockUri'; uri: URI; isEdit?: boolean; + undoStopId?: string; } export interface IChatAgentMarkdownContentWithVulnerability { diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index fb27fbf5522..22c570451bb 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -653,7 +653,10 @@ export class ChatService extends Disposable implements IChatService { undefined, // confirmation undefined, // locationData undefined, // attachments - true // isCompleteAddedRequest - this indicates it's a complete request, not user input + false, // Do not treat as requests completed, else edit pills won't show. + undefined, + undefined, + message.id ); } else { // response diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index c46bbb3dfaa..3fe57376b97 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -81,6 +81,7 @@ export interface IChatSessionItem { } export type IChatSessionHistoryItem = { + id?: string; type: 'request'; prompt: string; participant: string; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index f4ce44cd9e4..71520fa1ec2 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -32,7 +32,8 @@ declare module 'vscode' { export class ChatResponseCodeblockUriPart { isEdit?: boolean; value: Uri; - constructor(value: Uri, isEdit?: boolean); + undoStopId?: string; + constructor(value: Uri, isEdit?: boolean, undoStopId?: string); } /** @@ -170,7 +171,7 @@ declare module 'vscode' { export class ChatResponseExternalEditPart { uris: Uri[]; callback: () => Thenable; - applied: Thenable; + applied: Thenable; constructor(uris: Uri[], callback: () => Thenable); } @@ -314,7 +315,7 @@ declare module 'vscode' { * tracked as agent edits. This can be used to track edits made from * external tools that don't generate simple {@link textEdit textEdits}. */ - externalEdit(target: Uri | Uri[], callback: () => Thenable): Thenable; + externalEdit(target: Uri | Uri[], callback: () => Thenable): Thenable; markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): void; codeblockUri(uri: Uri, isEdit?: boolean): void; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 9f9d0a4b6a9..d477cb91b4a 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -148,6 +148,11 @@ declare module 'vscode' { } export class ChatResponseTurn2 { + /** + * The id of the chat response. Used to identity an interaction with any of the chat surfaces. + */ + readonly id?: string; + /** * The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented. */