Skip to content

Commit 13d64e9

Browse files
authored
Add extra detail choice under chat response thumbs down (microsoft#226678)
* Add extra detail choice under chat response thumbs down * Put the experiment back * Fix thumbs down for new inline chat * Update test snapshots
1 parent 4887c67 commit 13d64e9

14 files changed

+216
-56
lines changed

src/vs/workbench/api/browser/mainThreadChatAgents2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
105105
for (const [handle, agent] of this._agents) {
106106
if (agent.id === e.agentId) {
107107
if (e.action.kind === 'vote') {
108-
this._proxy.$acceptFeedback(handle, e.result ?? {}, e.action.direction);
108+
this._proxy.$acceptFeedback(handle, e.result ?? {}, e.action);
109109
} else {
110110
this._proxy.$acceptAction(handle, e.result || {}, e);
111111
}

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views
5353
import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
5454
import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents';
5555
import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel';
56-
import { ChatAgentVoteDirection, IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService';
56+
import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from 'vs/workbench/contrib/chat/common/chatService';
5757
import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables';
5858
import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from 'vs/workbench/contrib/chat/common/languageModels';
5959
import { IToolData, IToolInvocation, IToolResult } from 'vs/workbench/contrib/chat/common/languageModelToolsService';
@@ -1282,7 +1282,7 @@ export type IChatAgentHistoryEntryDto = {
12821282
export interface ExtHostChatAgentsShape2 {
12831283
$invokeAgent(handle: number, request: Dto<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise<IChatAgentResult | undefined>;
12841284
$provideFollowups(request: Dto<IChatAgentRequest>, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise<IChatFollowup[]>;
1285-
$acceptFeedback(handle: number, result: IChatAgentResult, vote: ChatAgentVoteDirection, reportIssue?: boolean): void;
1285+
$acceptFeedback(handle: number, result: IChatAgentResult, voteAction: IChatVoteAction): void;
12861286
$acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void;
12871287
$invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise<IChatAgentCompletionItem[]>;
12881288
$provideWelcomeMessage(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined>;

src/vs/workbench/api/common/extHostChatAgents2.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
2424
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
2525
import * as extHostTypes from 'vs/workbench/api/common/extHostTypes';
2626
import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult, IChatAgentResultTimings } from 'vs/workbench/contrib/chat/common/chatAgents';
27-
import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatResponseErrorDetails, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService';
27+
import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatResponseErrorDetails, IChatUserActionEvent, IChatVoteAction } from 'vs/workbench/contrib/chat/common/chatService';
2828
import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
2929
import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier';
3030
import type * as vscode from 'vscode';
@@ -478,25 +478,29 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
478478
.map(f => typeConvert.ChatFollowup.from(f, request));
479479
}
480480

481-
$acceptFeedback(handle: number, result: IChatAgentResult, vote: ChatAgentVoteDirection, reportIssue?: boolean): void {
481+
$acceptFeedback(handle: number, result: IChatAgentResult, voteAction: IChatVoteAction): void {
482482
const agent = this._agents.get(handle);
483483
if (!agent) {
484484
return;
485485
}
486486

487487
const ehResult = typeConvert.ChatAgentResult.to(result);
488488
let kind: extHostTypes.ChatResultFeedbackKind;
489-
switch (vote) {
489+
switch (voteAction.direction) {
490490
case ChatAgentVoteDirection.Down:
491491
kind = extHostTypes.ChatResultFeedbackKind.Unhelpful;
492492
break;
493493
case ChatAgentVoteDirection.Up:
494494
kind = extHostTypes.ChatResultFeedbackKind.Helpful;
495495
break;
496496
}
497-
agent.acceptFeedback(reportIssue ?
498-
Object.freeze({ result: ehResult, kind, reportIssue }) :
499-
Object.freeze({ result: ehResult, kind }));
497+
498+
const feedback: vscode.ChatResultFeedback = {
499+
result: ehResult,
500+
kind,
501+
unhelpfulReason: isProposedApiEnabled(agent.extension, 'chatParticipantAdditions') ? voteAction.reason : undefined,
502+
};
503+
agent.acceptFeedback(Object.freeze(feedback));
500504
}
501505

502506
$acceptAction(handle: number, result: IChatAgentResult, event: IChatUserActionEvent): void {

src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/
1616
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
1717
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
1818
import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_ERROR, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE, CONTEXT_VOTE_UP_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys';
19-
import { IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
19+
import { IChatService, ChatAgentVoteDirection, ChatAgentVoteDownReason } from 'vs/workbench/contrib/chat/common/chatService';
2020
import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
2121
import { MENU_INLINE_CHAT_WIDGET_SECONDARY } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
2222
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
2323
import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon';
2424
import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
2525
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
2626

27+
export const MarkUnhelpfulActionId = 'workbench.action.chat.markUnhelpful';
28+
2729
export function registerChatTitleActions() {
2830
registerAction2(class MarkHelpfulAction extends Action2 {
2931
constructor() {
@@ -64,16 +66,18 @@ export function registerChatTitleActions() {
6466
action: {
6567
kind: 'vote',
6668
direction: ChatAgentVoteDirection.Up,
69+
reason: undefined
6770
}
6871
});
6972
item.setVote(ChatAgentVoteDirection.Up);
73+
item.setVoteDownReason(undefined);
7074
}
7175
});
7276

7377
registerAction2(class MarkUnhelpfulAction extends Action2 {
7478
constructor() {
7579
super({
76-
id: 'workbench.action.chat.markUnhelpful',
80+
id: MarkUnhelpfulActionId,
7781
title: localize2('interactive.unhelpful.label', "Unhelpful"),
7882
f1: false,
7983
category: CHAT_CATEGORY,
@@ -99,6 +103,14 @@ export function registerChatTitleActions() {
99103
return;
100104
}
101105

106+
const reason = args[1];
107+
if (typeof reason !== 'string') {
108+
return;
109+
}
110+
111+
item.setVote(ChatAgentVoteDirection.Down);
112+
item.setVoteDownReason(reason as ChatAgentVoteDownReason);
113+
102114
const chatService = accessor.get(IChatService);
103115
chatService.notifyUserAction({
104116
agentId: item.agent?.id,
@@ -109,9 +121,9 @@ export function registerChatTitleActions() {
109121
action: {
110122
kind: 'vote',
111123
direction: ChatAgentVoteDirection.Down,
124+
reason: item.voteDownReason
112125
}
113126
});
114-
item.setVote(ChatAgentVoteDirection.Down);
115127
}
116128
});
117129

src/vs/workbench/contrib/chat/browser/chatListRenderer.ts

Lines changed: 110 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom';
77
import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer';
88
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
99
import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
10+
import { DropdownMenuActionViewItem, IDropdownMenuActionViewItemOptions } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
1011
import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory';
1112
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
1213
import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
@@ -26,18 +27,20 @@ import { ThemeIcon } from 'vs/base/common/themables';
2627
import { URI } from 'vs/base/common/uri';
2728
import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer';
2829
import { localize } from 'vs/nls';
29-
import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
30+
import { IMenuEntryActionViewItemOptions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
3031
import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
3132
import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
3233
import { ICommandService } from 'vs/platform/commands/common/commands';
3334
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
3435
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
36+
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
3537
import { IHoverService } from 'vs/platform/hover/browser/hover';
3638
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
3739
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
3840
import { ILogService } from 'vs/platform/log/common/log';
3941
import { ColorScheme } from 'vs/platform/theme/common/theme';
4042
import { IThemeService } from 'vs/platform/theme/common/themeService';
43+
import { MarkUnhelpfulActionId } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions';
4144
import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat';
4245
import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover';
4346
import { ChatAttachmentsContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart';
@@ -61,9 +64,10 @@ import { ChatAgentLocation, IChatAgentMetadata } from 'vs/workbench/contrib/chat
6164
import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_ERROR, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
6265
import { IChatRequestVariableEntry, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel';
6366
import { chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
64-
import { ChatAgentVoteDirection, IChatConfirmation, IChatContentReference, IChatFollowup, IChatTask, IChatTreeData } from 'vs/workbench/contrib/chat/common/chatService';
67+
import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatConfirmation, IChatContentReference, IChatFollowup, IChatTask, IChatTreeData } from 'vs/workbench/contrib/chat/common/chatService';
6568
import { IChatCodeCitations, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
6669
import { getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
70+
import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue';
6771
import { annotateSpecialMarkdownContent } from '../common/annotations';
6872
import { CodeBlockModelCollection } from '../common/codeBlockModelCollection';
6973

@@ -276,26 +280,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
276280

277281
const contextKeyService = templateDisposables.add(this.contextKeyService.createScoped(rowContainer));
278282
const scopedInstantiationService = templateDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])));
279-
let titleToolbar: MenuWorkbenchToolBar | undefined;
280-
if (this.rendererOptions.noHeader) {
281-
header.classList.add('hidden');
282-
} else {
283-
titleToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarParent ?? header, MenuId.ChatMessageTitle, {
284-
menuOptions: {
285-
shouldForwardArgs: true
286-
},
287-
toolbarOptions: {
288-
shouldInlineSubmenu: submenu => submenu.actions.length <= 1
289-
},
290-
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
291-
if (action instanceof MenuItemAction && (action.item.id === 'workbench.action.chat.voteDown' || action.item.id === 'workbench.action.chat.voteUp')) {
292-
return scopedInstantiationService.createInstance(ChatVoteButton, action, options as IMenuEntryActionViewItemOptions);
293-
}
294-
return createActionViewItem(scopedInstantiationService, action, options);
295-
}
296-
}));
297-
}
298-
299283
const agentHover = templateDisposables.add(this.instantiationService.createInstance(ChatAgentHover));
300284
const hoverContent = () => {
301285
if (isResponseVM(template.currentElement) && template.currentElement.agent && !template.currentElement.agent.isDefault) {
@@ -318,7 +302,29 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
318302
this.hoverService.hideHover();
319303
}
320304
}));
321-
const template: IChatListItemTemplate = { avatarContainer, username, detail, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService, instantiationService: scopedInstantiationService, agentHover };
305+
const template: IChatListItemTemplate = { avatarContainer, username, detail, value, rowContainer, elementDisposables, templateDisposables, contextKeyService, instantiationService: scopedInstantiationService, agentHover };
306+
307+
if (this.rendererOptions.noHeader) {
308+
header.classList.add('hidden');
309+
} else {
310+
// Have to create the template first because actionViewItemProvider depends on the template and runs immediately
311+
(template as any).titleToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarParent ?? header, MenuId.ChatMessageTitle, {
312+
menuOptions: {
313+
shouldForwardArgs: true
314+
},
315+
toolbarOptions: {
316+
shouldInlineSubmenu: submenu => submenu.actions.length <= 1
317+
},
318+
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
319+
const currentElement = template.currentElement;
320+
if (action instanceof MenuItemAction && action.item.id === MarkUnhelpfulActionId && isResponseVM(currentElement)) {
321+
return scopedInstantiationService.createInstance(ChatVoteDownButton, action, options as IMenuEntryActionViewItemOptions);
322+
}
323+
return createActionViewItem(scopedInstantiationService, action, options);
324+
}
325+
}));
326+
}
327+
322328
return template;
323329
}
324330

@@ -949,9 +955,88 @@ export class ChatListDelegate implements IListVirtualDelegate<ChatTreeItem> {
949955
}
950956
}
951957

952-
class ChatVoteButton extends MenuEntryActionViewItem {
958+
const voteDownDetailLabels: Record<ChatAgentVoteDownReason, string> = {
959+
[ChatAgentVoteDownReason.IncorrectCode]: localize('incorrectCode', "Suggested incorrect code"),
960+
[ChatAgentVoteDownReason.DidNotFollowInstructions]: localize('didNotFollowInstructions', "Didn't follow instructions"),
961+
[ChatAgentVoteDownReason.MissingContext]: localize('missingContext', "Missing context"),
962+
[ChatAgentVoteDownReason.OffensiveOrUnsafe]: localize('offensiveOrUnsafe', "Offensive or unsafe"),
963+
[ChatAgentVoteDownReason.PoorlyWrittenOrFormatted]: localize('poorlyWrittenOrFormatted', "Poorly written or formatted"),
964+
[ChatAgentVoteDownReason.RefusedAValidRequest]: localize('refusedAValidRequest', "Refused a valid request"),
965+
[ChatAgentVoteDownReason.IncompleteCode]: localize('incompleteCode', "Incomplete code"),
966+
[ChatAgentVoteDownReason.WillReportIssue]: localize('reportIssue', "Report an issue"),
967+
[ChatAgentVoteDownReason.Other]: localize('other', "Other"),
968+
};
969+
970+
export class ChatVoteDownButton extends DropdownMenuActionViewItem {
971+
constructor(
972+
action: IAction,
973+
options: IDropdownMenuActionViewItemOptions | undefined,
974+
@ICommandService private readonly commandService: ICommandService,
975+
@IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService,
976+
@ILogService private readonly logService: ILogService,
977+
@IContextMenuService contextMenuService: IContextMenuService,
978+
) {
979+
super(action,
980+
{ getActions: () => this.getActions(), },
981+
contextMenuService,
982+
{
983+
...options,
984+
classNames: ThemeIcon.asClassNameArray(Codicon.thumbsdown),
985+
});
986+
}
987+
988+
getActions(): readonly IAction[] {
989+
return [
990+
this.getVoteDownDetailAction(ChatAgentVoteDownReason.IncorrectCode),
991+
this.getVoteDownDetailAction(ChatAgentVoteDownReason.DidNotFollowInstructions),
992+
this.getVoteDownDetailAction(ChatAgentVoteDownReason.IncompleteCode),
993+
this.getVoteDownDetailAction(ChatAgentVoteDownReason.MissingContext),
994+
this.getVoteDownDetailAction(ChatAgentVoteDownReason.PoorlyWrittenOrFormatted),
995+
this.getVoteDownDetailAction(ChatAgentVoteDownReason.RefusedAValidRequest),
996+
this.getVoteDownDetailAction(ChatAgentVoteDownReason.OffensiveOrUnsafe),
997+
this.getVoteDownDetailAction(ChatAgentVoteDownReason.Other),
998+
{
999+
id: 'reportIssue',
1000+
label: voteDownDetailLabels[ChatAgentVoteDownReason.WillReportIssue],
1001+
tooltip: '',
1002+
enabled: true,
1003+
class: undefined,
1004+
run: async (context: IChatResponseViewModel) => {
1005+
if (!isResponseVM(context)) {
1006+
this.logService.error('ChatVoteDownButton#run: invalid context');
1007+
return;
1008+
}
1009+
1010+
await this.commandService.executeCommand(MarkUnhelpfulActionId, context, ChatAgentVoteDownReason.WillReportIssue);
1011+
await this.issueService.openReporter({ extensionId: context.agent?.extensionId.value });
1012+
}
1013+
}
1014+
];
1015+
}
1016+
9531017
override render(container: HTMLElement): void {
9541018
super.render(container);
955-
container.classList.toggle('checked', this.action.checked);
1019+
1020+
this.element?.classList.toggle('checked', this.action.checked);
1021+
}
1022+
1023+
private getVoteDownDetailAction(reason: ChatAgentVoteDownReason): IAction {
1024+
const label = voteDownDetailLabels[reason];
1025+
return {
1026+
id: MarkUnhelpfulActionId,
1027+
label,
1028+
tooltip: '',
1029+
enabled: true,
1030+
checked: (this._context as IChatResponseViewModel).voteDownReason === reason,
1031+
class: undefined,
1032+
run: async (context: IChatResponseViewModel) => {
1033+
if (!isResponseVM(context)) {
1034+
this.logService.error('ChatVoteDownButton#getVoteDownDetailAction: invalid context');
1035+
return;
1036+
}
1037+
1038+
await this.commandService.executeCommand(MarkUnhelpfulActionId, context, reason);
1039+
}
1040+
};
9561041
}
9571042
}

0 commit comments

Comments
 (0)