Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Codicon } from '../../../../../base/common/codicons.js';6import { marked } from '../../../../../base/common/marked/marked.js';7import { basename } from '../../../../../base/common/resources.js';8import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';9import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js';10import { localize, localize2 } from '../../../../../nls.js';11import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';12import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';13import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';14import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';15import { IEditorService } from '../../../../services/editor/common/editorService.js';16import { ResourceNotebookCellEdit } from '../../../bulkEdit/browser/bulkCellEdits.js';17import { MENU_INLINE_CHAT_WIDGET_SECONDARY } from '../../../inlineChat/common/inlineChat.js';18import { INotebookEditor } from '../../../notebook/browser/notebookBrowser.js';19import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js';20import { NOTEBOOK_IS_ACTIVE_EDITOR } from '../../../notebook/common/notebookContextKeys.js';21import { ChatContextKeys } from '../../common/chatContextKeys.js';22import { applyingChatEditsFailedContextKey, isChatEditingActionContext } from '../../common/chatEditingService.js';23import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatService } from '../../common/chatService.js';24import { isResponseVM } from '../../common/chatViewModel.js';25import { ChatModeKind } from '../../common/constants.js';26import { IChatWidgetService } from '../chat.js';27import { CHAT_CATEGORY } from './chatActions.js';2829export const MarkUnhelpfulActionId = 'workbench.action.chat.markUnhelpful';30const enableFeedbackConfig = 'config.telemetry.feedback.enabled';3132export function registerChatTitleActions() {33registerAction2(class MarkHelpfulAction extends Action2 {34constructor() {35super({36id: 'workbench.action.chat.markHelpful',37title: localize2('interactive.helpful.label', "Helpful"),38f1: false,39category: CHAT_CATEGORY,40icon: Codicon.thumbsup,41toggled: ChatContextKeys.responseVote.isEqualTo('up'),42menu: [{43id: MenuId.ChatMessageFooter,44group: 'navigation',45order: 2,46when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig))47}, {48id: MENU_INLINE_CHAT_WIDGET_SECONDARY,49group: 'navigation',50order: 1,51when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig))52}]53});54}5556run(accessor: ServicesAccessor, ...args: any[]) {57const item = args[0];58if (!isResponseVM(item)) {59return;60}6162const chatService = accessor.get(IChatService);63chatService.notifyUserAction({64agentId: item.agent?.id,65command: item.slashCommand?.name,66sessionId: item.sessionId,67requestId: item.requestId,68result: item.result,69action: {70kind: 'vote',71direction: ChatAgentVoteDirection.Up,72reason: undefined73}74});75item.setVote(ChatAgentVoteDirection.Up);76item.setVoteDownReason(undefined);77}78});7980registerAction2(class MarkUnhelpfulAction extends Action2 {81constructor() {82super({83id: MarkUnhelpfulActionId,84title: localize2('interactive.unhelpful.label', "Unhelpful"),85f1: false,86category: CHAT_CATEGORY,87icon: Codicon.thumbsdown,88toggled: ChatContextKeys.responseVote.isEqualTo('down'),89menu: [{90id: MenuId.ChatMessageFooter,91group: 'navigation',92order: 3,93when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ContextKeyExpr.has(enableFeedbackConfig))94}, {95id: MENU_INLINE_CHAT_WIDGET_SECONDARY,96group: 'navigation',97order: 2,98when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig))99}]100});101}102103run(accessor: ServicesAccessor, ...args: any[]) {104const item = args[0];105if (!isResponseVM(item)) {106return;107}108109const reason = args[1];110if (typeof reason !== 'string') {111return;112}113114item.setVote(ChatAgentVoteDirection.Down);115item.setVoteDownReason(reason as ChatAgentVoteDownReason);116117const chatService = accessor.get(IChatService);118chatService.notifyUserAction({119agentId: item.agent?.id,120command: item.slashCommand?.name,121sessionId: item.sessionId,122requestId: item.requestId,123result: item.result,124action: {125kind: 'vote',126direction: ChatAgentVoteDirection.Down,127reason: item.voteDownReason128}129});130}131});132133registerAction2(class ReportIssueForBugAction extends Action2 {134constructor() {135super({136id: 'workbench.action.chat.reportIssueForBug',137title: localize2('interactive.reportIssueForBug.label', "Report Issue"),138f1: false,139category: CHAT_CATEGORY,140icon: Codicon.report,141menu: [{142id: MenuId.ChatMessageFooter,143group: 'navigation',144order: 4,145when: ContextKeyExpr.and(ChatContextKeys.responseSupportsIssueReporting, ChatContextKeys.isResponse, ContextKeyExpr.has(enableFeedbackConfig))146}, {147id: MENU_INLINE_CHAT_WIDGET_SECONDARY,148group: 'navigation',149order: 3,150when: ContextKeyExpr.and(ChatContextKeys.responseSupportsIssueReporting, ChatContextKeys.isResponse, ContextKeyExpr.has(enableFeedbackConfig))151}]152});153}154155run(accessor: ServicesAccessor, ...args: any[]) {156const item = args[0];157if (!isResponseVM(item)) {158return;159}160161const chatService = accessor.get(IChatService);162chatService.notifyUserAction({163agentId: item.agent?.id,164command: item.slashCommand?.name,165sessionId: item.sessionId,166requestId: item.requestId,167result: item.result,168action: {169kind: 'bug'170}171});172}173});174175registerAction2(class RetryChatAction extends Action2 {176constructor() {177super({178id: 'workbench.action.chat.retry',179title: localize2('chat.retry.label', "Retry"),180f1: false,181category: CHAT_CATEGORY,182icon: Codicon.refresh,183menu: [184{185id: MenuId.ChatMessageFooter,186group: 'navigation',187when: ContextKeyExpr.and(188ChatContextKeys.isResponse,189ContextKeyExpr.in(ChatContextKeys.itemId.key, ChatContextKeys.lastItemId.key))190},191{192id: MenuId.ChatEditingWidgetToolbar,193group: 'navigation',194when: applyingChatEditsFailedContextKey,195order: 0196}197]198});199}200201async run(accessor: ServicesAccessor, ...args: any[]) {202const chatWidgetService = accessor.get(IChatWidgetService);203204let item = args[0];205if (isChatEditingActionContext(item)) {206// Resolve chat editing action context to the last response VM207item = chatWidgetService.getWidgetBySessionId(item.sessionId)?.viewModel?.getItems().at(-1);208}209if (!isResponseVM(item)) {210return;211}212213const chatService = accessor.get(IChatService);214const chatModel = chatService.getSession(item.sessionId);215const chatRequests = chatModel?.getRequests();216if (!chatRequests) {217return;218}219const itemIndex = chatRequests?.findIndex(request => request.id === item.requestId);220const widget = chatWidgetService.getWidgetBySessionId(item.sessionId);221const mode = widget?.input.currentModeKind;222if (chatModel && (mode === ChatModeKind.Edit || mode === ChatModeKind.Agent)) {223const configurationService = accessor.get(IConfigurationService);224const dialogService = accessor.get(IDialogService);225const currentEditingSession = widget?.viewModel?.model.editingSession;226if (!currentEditingSession) {227return;228}229230// Prompt if the last request modified the working set and the user hasn't already disabled the dialog231const entriesModifiedInLastRequest = currentEditingSession.entries.get().filter((entry) => entry.lastModifyingRequestId === item.requestId);232const shouldPrompt = entriesModifiedInLastRequest.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRetry') === true;233const confirmation = shouldPrompt234? await dialogService.confirm({235title: localize('chat.retryLast.confirmation.title2', "Do you want to retry your last request?"),236message: entriesModifiedInLastRequest.length === 1237? localize('chat.retry.confirmation.message2', "This will undo edits made to {0} since this request.", basename(entriesModifiedInLastRequest[0].modifiedURI))238: localize('chat.retryLast.confirmation.message2', "This will undo edits made to {0} files in your working set since this request. Do you want to proceed?", entriesModifiedInLastRequest.length),239primaryButton: localize('chat.retry.confirmation.primaryButton', "Yes"),240checkbox: { label: localize('chat.retry.confirmation.checkbox', "Don't ask again"), checked: false },241type: 'info'242})243: { confirmed: true };244245if (!confirmation.confirmed) {246return;247}248249if (confirmation.checkboxChecked) {250await configurationService.updateValue('chat.editing.confirmEditRequestRetry', false);251}252253// Reset the snapshot to the first stop (undefined undo index)254const snapshotRequest = chatRequests[itemIndex];255if (snapshotRequest) {256await currentEditingSession.restoreSnapshot(snapshotRequest.id, undefined);257}258}259const request = chatModel?.getRequests().find(candidate => candidate.id === item.requestId);260const languageModelId = widget?.input.currentLanguageModel;261262chatService.resendRequest(request!, {263userSelectedModelId: languageModelId,264attempt: (request?.attempt ?? -1) + 1,265...widget?.getModeRequestOptions(),266});267}268});269270registerAction2(class InsertToNotebookAction extends Action2 {271constructor() {272super({273id: 'workbench.action.chat.insertIntoNotebook',274title: localize2('interactive.insertIntoNotebook.label', "Insert into Notebook"),275f1: false,276category: CHAT_CATEGORY,277icon: Codicon.insert,278menu: {279id: MenuId.ChatMessageFooter,280group: 'navigation',281isHiddenByDefault: true,282when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, ChatContextKeys.isResponse, ChatContextKeys.responseIsFiltered.negate())283}284});285}286287async run(accessor: ServicesAccessor, ...args: any[]) {288const item = args[0];289if (!isResponseVM(item)) {290return;291}292293const editorService = accessor.get(IEditorService);294295if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) {296const notebookEditor = editorService.activeEditorPane.getControl() as INotebookEditor;297298if (!notebookEditor.hasModel()) {299return;300}301302if (notebookEditor.isReadOnly) {303return;304}305306const value = item.response.toString();307const splitContents = splitMarkdownAndCodeBlocks(value);308309const focusRange = notebookEditor.getFocus();310const index = Math.max(focusRange.end, 0);311const bulkEditService = accessor.get(IBulkEditService);312313await bulkEditService.apply(314[315new ResourceNotebookCellEdit(notebookEditor.textModel.uri,316{317editType: CellEditType.Replace,318index: index,319count: 0,320cells: splitContents.map(content => {321const kind = content.type === 'markdown' ? CellKind.Markup : CellKind.Code;322const language = content.type === 'markdown' ? 'markdown' : content.language;323const mime = content.type === 'markdown' ? 'text/markdown' : `text/x-${content.language}`;324return {325cellKind: kind,326language,327mime,328source: content.content,329outputs: [],330metadata: {}331};332})333}334)335],336{ quotableLabel: 'Insert into Notebook' }337);338}339}340});341}342343interface MarkdownContent {344type: 'markdown';345content: string;346}347348interface CodeContent {349type: 'code';350language: string;351content: string;352}353354type Content = MarkdownContent | CodeContent;355356function splitMarkdownAndCodeBlocks(markdown: string): Content[] {357const lexer = new marked.Lexer();358const tokens = lexer.lex(markdown);359360const splitContent: Content[] = [];361362let markdownPart = '';363tokens.forEach((token) => {364if (token.type === 'code') {365if (markdownPart.trim()) {366splitContent.push({ type: 'markdown', content: markdownPart });367markdownPart = '';368}369splitContent.push({370type: 'code',371language: token.lang || '',372content: token.text,373});374} else {375markdownPart += token.raw;376}377});378379if (markdownPart.trim()) {380splitContent.push({ type: 'markdown', content: markdownPart });381}382383return splitContent;384}385386387