feat: instrument accepting and rejecting chat edits (#230748)

* feat: instrument accepting and rejecting chat edits

* eslint

* Fix compile
This commit is contained in:
Joyce Er
2024-10-08 00:29:59 -07:00
committed by GitHub
parent 4ba135f790
commit 3709bf2387
7 changed files with 60 additions and 16 deletions

View File

@@ -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,

View File

@@ -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 };
}

View File

@@ -4342,6 +4342,11 @@ export class ChatCompletionItem implements vscode.ChatCompletionItem {
}
}
export enum ChatEditingSessionActionOutcome {
Accepted = 1,
Rejected = 2
}
//#endregion
//#region Interactive Editor

View File

@@ -170,7 +170,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
public triggerEditComputation(responseModel: IChatResponseModel): Promise<void> {
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<void> {
const entry = await this._getOrCreateModifiedFileEntry(resource);
private async _acceptTextEdits(resource: URI, textEdits: TextEdit[], responseModel: IChatResponseModel): Promise<void> {
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<ModifiedFileEntry> {
private async _getOrCreateModifiedFileEntry(resource: URI, responseModel: IChatResponseModel): Promise<ModifiedFileEntry> {
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<ModifiedFileEntry> {
private async _createModifiedFileEntry(resource: URI, responseModel: IChatResponseModel, mustExist = false): Promise<ModifiedFileEntry> {
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<IResolvedTextEditorModel>,
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<void> {
@@ -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<void> {
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
});
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 {