Hook up feedback/user-action events for inline chat participants (#210040)

- add a special user-action
- inline chat controller doesn't call handleFeedback directly anymore
- ReplyResonse knows its chat model response
This commit is contained in:
Johannes Rieken
2024-04-10 13:15:40 +02:00
committed by GitHub
parent 13a88a8da4
commit 6a06de0033
8 changed files with 86 additions and 43 deletions

View File

@@ -2697,6 +2697,8 @@ export namespace ChatAgentUserActionEvent {
} else if (event.action.kind === 'followUp') {
const followupAction: vscode.ChatFollowupAction = { kind: 'followUp', followup: ChatFollowup.to(event.action.followup) };
return { action: followupAction, result: ehResult };
} else if (event.action.kind === 'inlineChat') {
return { action: { kind: 'editor', accepted: event.action.action === 'accepted' }, result: ehResult };
} else {
return { action: event.action, result: ehResult };
}

View File

@@ -215,7 +215,12 @@ export interface IChatBugReportAction {
kind: 'bug';
}
export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction;
export interface IChatInlineChatCodeAction {
kind: 'inlineChat';
action: 'accepted' | 'discarded';
}
export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction;
export interface IChatUserActionEvent {
action: ChatUserAction;

View File

@@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/em
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { localize, localize2 } from 'vs/nls';
import { Action2, IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
@@ -388,7 +388,7 @@ export class ReportIssueForBugCommand extends AbstractInlineChatAction {
}
override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController): void {
ctrl.feedbackLast(InlineChatResponseFeedbackKind.Bug);
ctrl.reportBug();
}
}

View File

@@ -38,7 +38,7 @@ import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionPrompt } f
import { IInlineChatSessionService } from './inlineChatSessionService';
import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies';
import { InlineChatZoneWidget } from './inlineChatZoneWidget';
import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { StashedSession } from './inlineChatSession';
import { IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model';
@@ -1108,19 +1108,16 @@ export class InlineChatController implements IEditorContribution {
this._strategy?.toggleDiff?.();
}
feedbackLast(kind: InlineChatResponseFeedbackKind) {
if (this._session?.lastExchange?.response instanceof ReplyResponse) {
this._session.provider.handleInlineChatResponseFeedback?.(this._session.session, this._session.lastExchange.response.raw, kind);
switch (kind) {
case InlineChatResponseFeedbackKind.Helpful:
this._ctxLastFeedbackKind.set('helpful');
break;
case InlineChatResponseFeedbackKind.Unhelpful:
this._ctxLastFeedbackKind.set('unhelpful');
break;
default:
break;
}
reportBug() {
if (this._session?.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) {
const response = this._session.lastExchange.response.chatResponse;
this._chatService.notifyUserAction({
sessionId: response.session.sessionId,
requestId: response.requestId,
agentId: response.agent?.id,
result: response.result,
action: { kind: 'bug' }
});
this._zone.value.widget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 });
}
}
@@ -1132,8 +1129,18 @@ export class InlineChatController implements IEditorContribution {
}
acceptSession(): void {
if (this._session?.lastExchange?.response instanceof ReplyResponse) {
this._session.provider.handleInlineChatResponseFeedback?.(this._session.session, this._session.lastExchange.response.raw, InlineChatResponseFeedbackKind.Accepted);
if (this._session?.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) {
const response = this._session?.lastExchange?.response.chatResponse;
this._chatService.notifyUserAction({
sessionId: response.session.sessionId,
requestId: response.requestId,
agentId: response.agent?.id,
result: response.result,
action: {
kind: 'inlineChat',
action: 'accepted'
}
});
}
this._messages.fire(Message.ACCEPT_SESSION);
}
@@ -1154,8 +1161,18 @@ export class InlineChatController implements IEditorContribution {
const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000, computeMoves: false }, 'advanced');
result = this._session.asChangedText(diff?.changes ?? []);
if (this._session.lastExchange?.response instanceof ReplyResponse) {
this._session.provider.handleInlineChatResponseFeedback?.(this._session.session, this._session.lastExchange.response.raw, InlineChatResponseFeedbackKind.Undone);
if (this._session.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) {
const response = this._session?.lastExchange?.response.chatResponse;
this._chatService.notifyUserAction({
sessionId: response.session.sessionId,
requestId: response.requestId,
agentId: response.agent?.id,
result: response.result,
action: {
kind: 'inlineChat',
action: 'discarded'
}
});
}
}

View File

@@ -32,7 +32,7 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ILogService } from 'vs/platform/log/common/log';
import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
@@ -323,6 +323,7 @@ export class ReplyResponse {
readonly modelAltVersionId: number,
progressEdits: TextEdit[][],
readonly requestId: string,
readonly chatResponse: IChatResponseModel | undefined,
@ITextFileService private readonly _textFileService: ITextFileService,
@ILanguageService private readonly _languageService: ILanguageService,
) {

View File

@@ -59,7 +59,7 @@ class BridgeAgent implements IChatAgentImplementation {
return data;
}
async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> {
async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, _history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> {
if (token.isCancellationRequested) {
return {};
@@ -129,7 +129,9 @@ class BridgeAgent implements IChatAgentImplementation {
const markdownContents = result.message ?? new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false });
response = this._instaService.createInstance(ReplyResponse, result, markdownContents, session.textModelN.uri, modelAltVersionIdNow, progressEdits, request.requestId);
const chatModelRequest = session.chatModel.getRequests().find(candidate => candidate.id === request.requestId);
response = this._instaService.createInstance(ReplyResponse, result, markdownContents, session.textModelN.uri, modelAltVersionIdNow, progressEdits, request.requestId, chatModelRequest?.response);
} else {
response = new EmptyResponse();
@@ -141,9 +143,6 @@ class BridgeAgent implements IChatAgentImplementation {
this._postLastResponse({ id: request.requestId, response });
// TODO@jrieken
// result?.placeholder
// result?.wholeRange
return {
metadata: {
@@ -315,18 +314,19 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
}
if (otherEditorAgent) {
brigdeAgent.clear();
_logService.debug(`REMOVED bridge agent "${agentData.id}", found "${otherEditorAgent.id}"`);
bridgeStore.clear();
_logService.info(`REMOVED bridge agent "${agentData.id}", found "${otherEditorAgent.id}"`);
} else if (!myEditorAgent) {
brigdeAgent.value = this._chatAgentService.registerDynamicAgent(agentData, this._instaService.createInstance(BridgeAgent, agentData, this._sessions, data => {
bridgeStore.value = this._chatAgentService.registerDynamicAgent(agentData, this._instaService.createInstance(BridgeAgent, agentData, this._sessions, data => {
this._lastResponsesFromBridgeAgent.set(data.id, data.response);
}));
_logService.debug(`ADDED bridge agent "${agentData.id}"`);
_logService.info(`ADDED bridge agent "${agentData.id}"`);
}
};
this._store.add(this._chatAgentService.onDidChangeAgents(() => addOrRemoveBridgeAgent()));
const brigdeAgent = this._store.add(new MutableDisposable());
const bridgeStore = this._store.add(new MutableDisposable());
addOrRemoveBridgeAgent();
@@ -465,7 +465,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
session.textModelN.uri,
modelAltVersionIdNow,
[],
e.request.id
e.request.id,
e.request.response
);
}
}
@@ -475,20 +476,32 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
}));
store.add(this._chatService.onDidPerformUserAction(e => {
if (e.sessionId !== chatModel.sessionId || e.action.kind !== 'vote') {
if (e.sessionId !== chatModel.sessionId) {
return;
}
// TODO@jrieken VALIDATE candidate is proper, e.g check with `session.exchanges`
const request = chatModel.getRequests().find(request => request.id === e.requestId);
const candidate = request?.response?.result?.metadata?.inlineChatResponse;
if (candidate) {
provider.handleInlineChatResponseFeedback?.(
rawSession,
candidate,
e.action.direction === InteractiveSessionVoteDirection.Down ? InlineChatResponseFeedbackKind.Unhelpful : InlineChatResponseFeedbackKind.Helpful
);
if (!candidate) {
return;
}
let kind: InlineChatResponseFeedbackKind | undefined;
if (e.action.kind === 'vote') {
kind = e.action.direction === InteractiveSessionVoteDirection.Down ? InlineChatResponseFeedbackKind.Unhelpful : InlineChatResponseFeedbackKind.Helpful;
} else if (e.action.kind === 'bug') {
kind = InlineChatResponseFeedbackKind.Bug;
} else if (e.action.kind === 'inlineChat') {
kind = e.action.action === 'accepted' ? InlineChatResponseFeedbackKind.Accepted : InlineChatResponseFeedbackKind.Undone;
}
if (!kind) {
return;
}
provider.handleInlineChatResponseFeedback?.(rawSession, candidate, kind);
}));
store.add(this._inlineChatService.onDidChangeProviders(e => {

View File

@@ -644,7 +644,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito
response = new EmptyResponse();
} else {
const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false });
const replyResponse = response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits, request.requestId);
const replyResponse = response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits, request.requestId, undefined);
for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) {
await this._makeChanges(replyResponse.allLocalEdits[i], undefined);
}
@@ -747,7 +747,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito
}
const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false });
const response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), [], request.requestId);
const response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), [], request.requestId, undefined);
const followups = await this._activeSession.provider.provideFollowups(this._activeSession.session, response.raw, token);
if (followups && this._widget) {
const widget = this._widget;

View File

@@ -281,9 +281,14 @@ declare module 'vscode' {
kind: 'bug';
}
export interface ChatEditorAction {
kind: 'editor';
accepted: boolean;
}
export interface ChatUserActionEvent {
readonly result: ChatResult;
readonly action: ChatCopyAction | ChatInsertAction | ChatTerminalAction | ChatCommandAction | ChatFollowupAction | ChatBugReportAction;
readonly action: ChatCopyAction | ChatInsertAction | ChatTerminalAction | ChatCommandAction | ChatFollowupAction | ChatBugReportAction | ChatEditorAction;
}
export interface ChatVariableValue {