Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts
5303 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/actions/chatContextKeys.js';22import { applyingChatEditsFailedContextKey, isChatEditingActionContext } from '../../common/editing/chatEditingService.js';23import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatService } from '../../common/chatService/chatService.js';24import { isResponseVM } from '../../common/model/chatViewModel.js';25import { ChatModeKind } from '../../common/constants.js';26import { IChatAccessibilityService, 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: unknown[]) {57const item = args[0];58if (!isResponseVM(item)) {59return;60}6162const chatService = accessor.get(IChatService);63chatService.notifyUserAction({64agentId: item.agent?.id,65command: item.slashCommand?.name,66sessionResource: item.session.sessionResource,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: unknown[]) {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,121sessionResource: item.session.sessionResource,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: unknown[]) {156const item = args[0];157if (!isResponseVM(item)) {158return;159}160161const chatService = accessor.get(IChatService);162chatService.notifyUserAction({163agentId: item.agent?.id,164command: item.slashCommand?.name,165sessionResource: item.session.sessionResource,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: unknown[]) {202const chatWidgetService = accessor.get(IChatWidgetService);203const chatAccessibilityService = accessor.get(IChatAccessibilityService);204const chatService = accessor.get(IChatService);205const configurationService = accessor.get(IConfigurationService);206const dialogService = accessor.get(IDialogService);207208let item = args[0];209if (isChatEditingActionContext(item)) {210// Resolve chat editing action context to the last response VM211item = chatWidgetService.getWidgetBySessionResource(item.sessionResource)?.viewModel?.getItems().at(-1);212}213if (!isResponseVM(item)) {214return;215}216217const chatModel = chatService.getSession(item.sessionResource);218const chatRequests = chatModel?.getRequests();219if (!chatRequests) {220return;221}222const itemIndex = chatRequests?.findIndex(request => request.id === item.requestId);223const widget = chatWidgetService.getWidgetBySessionResource(item.sessionResource);224const mode = widget?.input.currentModeKind;225if (chatModel && (mode === ChatModeKind.Edit || mode === ChatModeKind.Agent)) {226const currentEditingSession = widget?.viewModel?.model.editingSession;227if (!currentEditingSession) {228return;229}230231// Prompt if the last request modified the working set and the user hasn't already disabled the dialog232const entriesModifiedInLastRequest = currentEditingSession.entries.get().filter((entry) => entry.lastModifyingRequestId === item.requestId);233const shouldPrompt = entriesModifiedInLastRequest.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRetry') === true;234const confirmation = shouldPrompt235? await dialogService.confirm({236title: localize('chat.retryLast.confirmation.title2', "Do you want to retry your last request?"),237message: entriesModifiedInLastRequest.length === 1238? localize('chat.retry.confirmation.message2', "This will undo edits made to {0} since this request.", basename(entriesModifiedInLastRequest[0].modifiedURI))239: 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),240primaryButton: localize('chat.retry.confirmation.primaryButton', "Yes"),241checkbox: { label: localize('chat.retry.confirmation.checkbox', "Don't ask again"), checked: false },242type: 'info'243})244: { confirmed: true };245246if (!confirmation.confirmed) {247return;248}249250if (confirmation.checkboxChecked) {251await configurationService.updateValue('chat.editing.confirmEditRequestRetry', false);252}253254// Reset the snapshot to the first stop (undefined undo index)255const snapshotRequest = chatRequests[itemIndex];256if (snapshotRequest) {257await currentEditingSession.restoreSnapshot(snapshotRequest.id, undefined);258}259}260const request = chatModel?.getRequests().find(candidate => candidate.id === item.requestId);261const languageModelId = widget?.input.currentLanguageModel;262263chatAccessibilityService.acceptRequest(item.sessionResource);264chatService.resendRequest(request!, {265userSelectedModelId: languageModelId,266attempt: (request?.attempt ?? -1) + 1,267...widget?.getModeRequestOptions(),268});269}270});271272registerAction2(class InsertToNotebookAction extends Action2 {273constructor() {274super({275id: 'workbench.action.chat.insertIntoNotebook',276title: localize2('interactive.insertIntoNotebook.label', "Insert into Notebook"),277f1: false,278category: CHAT_CATEGORY,279icon: Codicon.insert,280menu: {281id: MenuId.ChatMessageFooter,282group: 'navigation',283isHiddenByDefault: true,284when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, ChatContextKeys.isResponse, ChatContextKeys.responseIsFiltered.negate())285}286});287}288289async run(accessor: ServicesAccessor, ...args: unknown[]) {290const item = args[0];291if (!isResponseVM(item)) {292return;293}294295const editorService = accessor.get(IEditorService);296297if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) {298const notebookEditor = editorService.activeEditorPane.getControl() as INotebookEditor;299300if (!notebookEditor.hasModel()) {301return;302}303304if (notebookEditor.isReadOnly) {305return;306}307308const value = item.response.toString();309const splitContents = splitMarkdownAndCodeBlocks(value);310311const focusRange = notebookEditor.getFocus();312const index = Math.max(focusRange.end, 0);313const bulkEditService = accessor.get(IBulkEditService);314315await bulkEditService.apply(316[317new ResourceNotebookCellEdit(notebookEditor.textModel.uri,318{319editType: CellEditType.Replace,320index: index,321count: 0,322cells: splitContents.map(content => {323const kind = content.type === 'markdown' ? CellKind.Markup : CellKind.Code;324const language = content.type === 'markdown' ? 'markdown' : content.language;325const mime = content.type === 'markdown' ? 'text/markdown' : `text/x-${content.language}`;326return {327cellKind: kind,328language,329mime,330source: content.content,331outputs: [],332metadata: {}333};334})335}336)337],338{ quotableLabel: 'Insert into Notebook' }339);340}341}342});343}344345interface MarkdownContent {346type: 'markdown';347content: string;348}349350interface CodeContent {351type: 'code';352language: string;353content: string;354}355356type Content = MarkdownContent | CodeContent;357358function splitMarkdownAndCodeBlocks(markdown: string): Content[] {359const lexer = new marked.Lexer();360const tokens = lexer.lex(markdown);361362const splitContent: Content[] = [];363364let markdownPart = '';365tokens.forEach((token) => {366if (token.type === 'code') {367if (markdownPart.trim()) {368splitContent.push({ type: 'markdown', content: markdownPart });369markdownPart = '';370}371splitContent.push({372type: 'code',373language: token.lang || '',374content: token.text,375});376} else {377markdownPart += token.raw;378}379});380381if (markdownPart.trim()) {382splitContent.push({ type: 'markdown', content: markdownPart });383}384385return splitContent;386}387388389