Support restoring edits for background sessions (#279270)

* Support restoring edits for background sessions

* revert change
This commit is contained in:
Don Jayamanne
2025-11-26 05:01:26 +00:00
committed by GitHub
parent 2d69e225bf
commit 7ec9c8eb07
15 changed files with 48 additions and 32 deletions
@@ -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);
}
@@ -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
};
}
@@ -3252,6 +3252,7 @@ export interface MainThreadChatStatusShape {
}
export type IChatSessionHistoryItemDto = {
id?: string;
type: 'request';
prompt: string;
participant: string;
@@ -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
@@ -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,
@@ -2608,10 +2608,11 @@ export namespace ChatResponseCodeblockUriPart {
kind: 'codeblockUri',
uri: part.value,
isEdit: part.isEdit,
undoStopId: part.undoStopId
};
}
export function to(part: Dto<IChatResponseCodeblockUriPart>): vscode.ChatResponseCodeblockUriPart {
return new types.ChatResponseCodeblockUriPart(URI.revive(part.uri), part.isEdit);
return new types.ChatResponseCodeblockUriPart(URI.revive(part.uri), part.isEdit, part.undoStopId);
}
}
+8 -5
View File
@@ -3166,14 +3166,14 @@ export class ChatResponseMultiDiffPart {
}
export class ChatResponseExternalEditPart {
applied: Thenable<void>;
didGetApplied!: () => void;
applied: Thenable<string>;
didGetApplied!: (value: string) => void;
constructor(
public uris: vscode.Uri[],
public callback: () => Thenable<unknown>,
) {
this.applied = new Promise<void>((resolve) => {
this.applied = new Promise<string>((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
) { }
}
@@ -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<IChatProgress[]> {
async startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[], undoStopId: string): Promise<IChatProgress[]> {
const snapshots = new ResourceMap<string | undefined>();
const acquiredLockPromises: DeferredPromise<void>[] = [];
const releaseLockPromises: DeferredPromise<void>[] = [];
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();
@@ -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<IChatProgress[]>;
startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[], undoStopId: string): Promise<IChatProgress[]>;
stopExternalEdits(responseModel: IChatResponseModel, operationId: number): Promise<IChatProgress[]>;
/**
@@ -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);
}
@@ -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 {
@@ -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
@@ -81,6 +81,7 @@ export interface IChatSessionItem {
}
export type IChatSessionHistoryItem = {
id?: string;
type: 'request';
prompt: string;
participant: string;
@@ -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<unknown>;
applied: Thenable<void>;
applied: Thenable<string>;
constructor(uris: Uri[], callback: () => Thenable<unknown>);
}
@@ -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<T>(target: Uri | Uri[], callback: () => Thenable<T>): Thenable<T>;
externalEdit(target: Uri | Uri[], callback: () => Thenable<unknown>): Thenable<string>;
markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): void;
codeblockUri(uri: Uri, isEdit?: boolean): void;
@@ -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.
*/