diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 7afaa349233..83e055e8934 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1742,6 +1742,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I EditSessionIdentityMatch: EditSessionIdentityMatch, InteractiveSessionVoteDirection: extHostTypes.InteractiveSessionVoteDirection, ChatCopyKind: extHostTypes.ChatCopyKind, + ChatEditingSessionActionOutcome: extHostTypes.ChatEditingSessionActionOutcome, InteractiveEditorResponseFeedbackKind: extHostTypes.InteractiveEditorResponseFeedbackKind, DebugStackFrame: extHostTypes.DebugStackFrame, DebugThread: extHostTypes.DebugThread, diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 12d7dc77d44..426f4128330 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2868,6 +2868,8 @@ export namespace ChatAgentUserActionEvent { return { action: followupAction, result: ehResult }; } else if (event.action.kind === 'inlineChat') { return { action: { kind: 'editor', accepted: event.action.action === 'accepted' }, result: ehResult }; + } else if (event.action.kind === 'chatEditingSessionAction') { + return { action: { kind: 'chatEditingSessionAction', outcome: event.action.outcome === 'accepted' ? types.ChatEditingSessionActionOutcome.Accepted : types.ChatEditingSessionActionOutcome.Rejected, uri: URI.revive(event.action.uri), hasRemainingEdits: event.action.hasRemainingEdits }, result: ehResult }; } else { return { action: event.action, result: ehResult }; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 9f1ac00eab0..956c9897bcb 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4342,6 +4342,11 @@ export class ChatCompletionItem implements vscode.ChatCompletionItem { } } +export enum ChatEditingSessionActionOutcome { + Accepted = 1, + Rejected = 2 +} + //#endregion //#region Interactive Editor diff --git a/src/vs/workbench/contrib/chat/browser/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditingService.ts index 3c28b83913b..eee5300f872 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditingService.ts @@ -170,7 +170,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic public triggerEditComputation(responseModel: IChatResponseModel): Promise { return this._continueEditingSession(async (builder, token) => { const codeMapperResponse: ICodeMapperResponse = { - textEdit: (resource, edits) => builder.textEdits(resource, edits), + textEdit: (resource, edits) => builder.textEdits(resource, edits, responseModel), }; await this._codeMapperService.mapCodeFromResponse(responseModel, codeMapperResponse, token); }, { silent: true }); @@ -246,8 +246,8 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } const stream: IChatEditingSessionStream = { - textEdits: (resource: URI, textEdits: TextEdit[]) => { - session.acceptTextEdits(resource, textEdits); + textEdits: (resource: URI, textEdits: TextEdit[], responseModel: IChatResponseModel) => { + session.acceptTextEdits(resource, textEdits, responseModel); } }; session.acceptStreamingEditsStart(); @@ -573,14 +573,14 @@ class ChatEditingSession extends Disposable implements IChatEditingSession { this._sequencer.queue(() => this._acceptStreamingEditsStart()); } - acceptTextEdits(resource: URI, textEdits: TextEdit[]): void { + acceptTextEdits(resource: URI, textEdits: TextEdit[], responseModel: IChatResponseModel): void { if (this._state.get() === ChatEditingSessionState.Disposed) { // we don't throw in this case because there could be a builder still connected to a disposed session return; } // ensure that the edits are processed sequentially - this._sequencer.queue(() => this._acceptTextEdits(resource, textEdits)); + this._sequencer.queue(() => this._acceptTextEdits(resource, textEdits, responseModel)); } resolve(): void { @@ -606,8 +606,8 @@ class ChatEditingSession extends Disposable implements IChatEditingSession { this._onDidChange.fire(); } - private async _acceptTextEdits(resource: URI, textEdits: TextEdit[]): Promise { - const entry = await this._getOrCreateModifiedFileEntry(resource); + private async _acceptTextEdits(resource: URI, textEdits: TextEdit[], responseModel: IChatResponseModel): Promise { + const entry = await this._getOrCreateModifiedFileEntry(resource, responseModel); entry.applyEdits(textEdits); await this._editorService.openEditor({ resource: entry.modifiedURI, options: { inactive: true } }); } @@ -617,13 +617,13 @@ class ChatEditingSession extends Disposable implements IChatEditingSession { this._onDidChange.fire(); } - private async _getOrCreateModifiedFileEntry(resource: URI): Promise { + private async _getOrCreateModifiedFileEntry(resource: URI, responseModel: IChatResponseModel): Promise { const existingEntry = this._entries.find(e => e.resource.toString() === resource.toString()); if (existingEntry) { return existingEntry; } - const entry = await this._createModifiedFileEntry(resource); + const entry = await this._createModifiedFileEntry(resource, responseModel); this._register(entry); this._entries = [...this._entries, entry]; this._entriesObs.set(this._entries, undefined); @@ -632,17 +632,17 @@ class ChatEditingSession extends Disposable implements IChatEditingSession { return entry; } - private async _createModifiedFileEntry(resource: URI, mustExist = false): Promise { + private async _createModifiedFileEntry(resource: URI, responseModel: IChatResponseModel, mustExist = false): Promise { try { const ref = await this._textModelService.createModelReference(resource); - return this._instantiationService.createInstance(ModifiedFileEntry, resource, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }); + return this._instantiationService.createInstance(ModifiedFileEntry, resource, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }, responseModel); } catch (err) { if (mustExist) { throw err; } // this file does not exist yet, create it and try again await this._bulkEditService.apply({ edits: [{ newResource: resource }] }); - return this._createModifiedFileEntry(resource, true); + return this._createModifiedFileEntry(resource, responseModel, true); } } @@ -684,10 +684,12 @@ class ModifiedFileEntry extends Disposable implements IModifiedFileEntry { public readonly resource: URI, resourceRef: IReference, private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, + private readonly _responseModel: IChatResponseModel, @IModelService modelService: IModelService, @ITextModelService textModelService: ITextModelService, @ILanguageService languageService: ILanguageService, - @IBulkEditService public readonly _bulkEditService: IBulkEditService, + @IBulkEditService public readonly bulkEditService: IBulkEditService, + @IChatService private readonly _chatService: IChatService, ) { super(); this.doc = resourceRef.object.textEditorModel; @@ -722,6 +724,7 @@ class ModifiedFileEntry extends Disposable implements IModifiedFileEntry { this.docSnapshot.setValue(this.doc.createSnapshot()); this._stateObs.set(WorkingSetEntryState.Accepted, transaction); await this.collapse(transaction); + this._notifyAction('accepted'); } async reject(transaction: ITransaction | undefined): Promise { @@ -732,9 +735,21 @@ class ModifiedFileEntry extends Disposable implements IModifiedFileEntry { this.doc.setValue(this.docSnapshot.createSnapshot()); this._stateObs.set(WorkingSetEntryState.Rejected, transaction); await this.collapse(transaction); + this._notifyAction('rejected'); } async collapse(transaction: ITransaction | undefined): Promise { this._multiDiffEntryDelegate.collapse(transaction); } + + private _notifyAction(outcome: 'accepted' | 'rejected') { + this._chatService.notifyUserAction({ + action: { kind: 'chatEditingSessionAction', uri: this.resource, hasRemainingEdits: false, outcome }, + agentId: this._responseModel.agent?.id, + command: this._responseModel.slashCommand?.name, + sessionId: this._responseModel.session.sessionId, + requestId: this._responseModel.requestId, + result: this._responseModel.result + }); + } } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 8852838d5a2..b5bdc4c743e 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -69,7 +69,7 @@ export interface IModifiedFileEntry { } export interface IChatEditingSessionStream { - textEdits(resource: URI, textEdits: TextEdit[]): void; + textEdits(resource: URI, textEdits: TextEdit[], responseModel: IChatResponseModel): void; } export const enum ChatEditingSessionState { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 8737fc42f28..a0e45216316 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -326,7 +326,15 @@ export interface IChatInlineChatCodeAction { action: 'accepted' | 'discarded'; } -export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatApplyAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction; + +export interface IChatEditingSessionAction { + kind: 'chatEditingSessionAction'; + uri: URI; + hasRemainingEdits: boolean; + outcome: 'accepted' | 'rejected'; +} + +export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatApplyAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction | IChatEditingSessionAction; export interface IChatUserActionEvent { action: ChatUserAction; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 070114cbefb..de74f6e015d 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -358,9 +358,22 @@ declare module 'vscode' { accepted: boolean; } + export interface ChatEditingSessionAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'chatEditingSessionAction'; + uri: Uri; + hasRemainingEdits: boolean; + outcome: ChatEditingSessionActionOutcome; + } + + export enum ChatEditingSessionActionOutcome { + Accepted = 1, + Rejected = 2 + } + export interface ChatUserActionEvent { readonly result: ChatResult; - readonly action: ChatCopyAction | ChatInsertAction | ChatApplyAction | ChatTerminalAction | ChatCommandAction | ChatFollowupAction | ChatBugReportAction | ChatEditorAction; + readonly action: ChatCopyAction | ChatInsertAction | ChatApplyAction | ChatTerminalAction | ChatCommandAction | ChatFollowupAction | ChatBugReportAction | ChatEditorAction | ChatEditingSessionAction; } export interface ChatPromptReference {