Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts
5303 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Codicon } from '../../../../../base/common/codicons.js';
7
import { marked } from '../../../../../base/common/marked/marked.js';
8
import { basename } from '../../../../../base/common/resources.js';
9
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
10
import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js';
11
import { localize, localize2 } from '../../../../../nls.js';
12
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
13
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
14
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
15
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
16
import { IEditorService } from '../../../../services/editor/common/editorService.js';
17
import { ResourceNotebookCellEdit } from '../../../bulkEdit/browser/bulkCellEdits.js';
18
import { MENU_INLINE_CHAT_WIDGET_SECONDARY } from '../../../inlineChat/common/inlineChat.js';
19
import { INotebookEditor } from '../../../notebook/browser/notebookBrowser.js';
20
import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js';
21
import { NOTEBOOK_IS_ACTIVE_EDITOR } from '../../../notebook/common/notebookContextKeys.js';
22
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
23
import { applyingChatEditsFailedContextKey, isChatEditingActionContext } from '../../common/editing/chatEditingService.js';
24
import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatService } from '../../common/chatService/chatService.js';
25
import { isResponseVM } from '../../common/model/chatViewModel.js';
26
import { ChatModeKind } from '../../common/constants.js';
27
import { IChatAccessibilityService, IChatWidgetService } from '../chat.js';
28
import { CHAT_CATEGORY } from './chatActions.js';
29
30
export const MarkUnhelpfulActionId = 'workbench.action.chat.markUnhelpful';
31
const enableFeedbackConfig = 'config.telemetry.feedback.enabled';
32
33
export function registerChatTitleActions() {
34
registerAction2(class MarkHelpfulAction extends Action2 {
35
constructor() {
36
super({
37
id: 'workbench.action.chat.markHelpful',
38
title: localize2('interactive.helpful.label', "Helpful"),
39
f1: false,
40
category: CHAT_CATEGORY,
41
icon: Codicon.thumbsup,
42
toggled: ChatContextKeys.responseVote.isEqualTo('up'),
43
menu: [{
44
id: MenuId.ChatMessageFooter,
45
group: 'navigation',
46
order: 2,
47
when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig))
48
}, {
49
id: MENU_INLINE_CHAT_WIDGET_SECONDARY,
50
group: 'navigation',
51
order: 1,
52
when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig))
53
}]
54
});
55
}
56
57
run(accessor: ServicesAccessor, ...args: unknown[]) {
58
const item = args[0];
59
if (!isResponseVM(item)) {
60
return;
61
}
62
63
const chatService = accessor.get(IChatService);
64
chatService.notifyUserAction({
65
agentId: item.agent?.id,
66
command: item.slashCommand?.name,
67
sessionResource: item.session.sessionResource,
68
requestId: item.requestId,
69
result: item.result,
70
action: {
71
kind: 'vote',
72
direction: ChatAgentVoteDirection.Up,
73
reason: undefined
74
}
75
});
76
item.setVote(ChatAgentVoteDirection.Up);
77
item.setVoteDownReason(undefined);
78
}
79
});
80
81
registerAction2(class MarkUnhelpfulAction extends Action2 {
82
constructor() {
83
super({
84
id: MarkUnhelpfulActionId,
85
title: localize2('interactive.unhelpful.label', "Unhelpful"),
86
f1: false,
87
category: CHAT_CATEGORY,
88
icon: Codicon.thumbsdown,
89
toggled: ChatContextKeys.responseVote.isEqualTo('down'),
90
menu: [{
91
id: MenuId.ChatMessageFooter,
92
group: 'navigation',
93
order: 3,
94
when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ContextKeyExpr.has(enableFeedbackConfig))
95
}, {
96
id: MENU_INLINE_CHAT_WIDGET_SECONDARY,
97
group: 'navigation',
98
order: 2,
99
when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig))
100
}]
101
});
102
}
103
104
run(accessor: ServicesAccessor, ...args: unknown[]) {
105
const item = args[0];
106
if (!isResponseVM(item)) {
107
return;
108
}
109
110
const reason = args[1];
111
if (typeof reason !== 'string') {
112
return;
113
}
114
115
item.setVote(ChatAgentVoteDirection.Down);
116
item.setVoteDownReason(reason as ChatAgentVoteDownReason);
117
118
const chatService = accessor.get(IChatService);
119
chatService.notifyUserAction({
120
agentId: item.agent?.id,
121
command: item.slashCommand?.name,
122
sessionResource: item.session.sessionResource,
123
requestId: item.requestId,
124
result: item.result,
125
action: {
126
kind: 'vote',
127
direction: ChatAgentVoteDirection.Down,
128
reason: item.voteDownReason
129
}
130
});
131
}
132
});
133
134
registerAction2(class ReportIssueForBugAction extends Action2 {
135
constructor() {
136
super({
137
id: 'workbench.action.chat.reportIssueForBug',
138
title: localize2('interactive.reportIssueForBug.label', "Report Issue"),
139
f1: false,
140
category: CHAT_CATEGORY,
141
icon: Codicon.report,
142
menu: [{
143
id: MenuId.ChatMessageFooter,
144
group: 'navigation',
145
order: 4,
146
when: ContextKeyExpr.and(ChatContextKeys.responseSupportsIssueReporting, ChatContextKeys.isResponse, ContextKeyExpr.has(enableFeedbackConfig))
147
}, {
148
id: MENU_INLINE_CHAT_WIDGET_SECONDARY,
149
group: 'navigation',
150
order: 3,
151
when: ContextKeyExpr.and(ChatContextKeys.responseSupportsIssueReporting, ChatContextKeys.isResponse, ContextKeyExpr.has(enableFeedbackConfig))
152
}]
153
});
154
}
155
156
run(accessor: ServicesAccessor, ...args: unknown[]) {
157
const item = args[0];
158
if (!isResponseVM(item)) {
159
return;
160
}
161
162
const chatService = accessor.get(IChatService);
163
chatService.notifyUserAction({
164
agentId: item.agent?.id,
165
command: item.slashCommand?.name,
166
sessionResource: item.session.sessionResource,
167
requestId: item.requestId,
168
result: item.result,
169
action: {
170
kind: 'bug'
171
}
172
});
173
}
174
});
175
176
registerAction2(class RetryChatAction extends Action2 {
177
constructor() {
178
super({
179
id: 'workbench.action.chat.retry',
180
title: localize2('chat.retry.label', "Retry"),
181
f1: false,
182
category: CHAT_CATEGORY,
183
icon: Codicon.refresh,
184
menu: [
185
{
186
id: MenuId.ChatMessageFooter,
187
group: 'navigation',
188
when: ContextKeyExpr.and(
189
ChatContextKeys.isResponse,
190
ContextKeyExpr.in(ChatContextKeys.itemId.key, ChatContextKeys.lastItemId.key))
191
},
192
{
193
id: MenuId.ChatEditingWidgetToolbar,
194
group: 'navigation',
195
when: applyingChatEditsFailedContextKey,
196
order: 0
197
}
198
]
199
});
200
}
201
202
async run(accessor: ServicesAccessor, ...args: unknown[]) {
203
const chatWidgetService = accessor.get(IChatWidgetService);
204
const chatAccessibilityService = accessor.get(IChatAccessibilityService);
205
const chatService = accessor.get(IChatService);
206
const configurationService = accessor.get(IConfigurationService);
207
const dialogService = accessor.get(IDialogService);
208
209
let item = args[0];
210
if (isChatEditingActionContext(item)) {
211
// Resolve chat editing action context to the last response VM
212
item = chatWidgetService.getWidgetBySessionResource(item.sessionResource)?.viewModel?.getItems().at(-1);
213
}
214
if (!isResponseVM(item)) {
215
return;
216
}
217
218
const chatModel = chatService.getSession(item.sessionResource);
219
const chatRequests = chatModel?.getRequests();
220
if (!chatRequests) {
221
return;
222
}
223
const itemIndex = chatRequests?.findIndex(request => request.id === item.requestId);
224
const widget = chatWidgetService.getWidgetBySessionResource(item.sessionResource);
225
const mode = widget?.input.currentModeKind;
226
if (chatModel && (mode === ChatModeKind.Edit || mode === ChatModeKind.Agent)) {
227
const currentEditingSession = widget?.viewModel?.model.editingSession;
228
if (!currentEditingSession) {
229
return;
230
}
231
232
// Prompt if the last request modified the working set and the user hasn't already disabled the dialog
233
const entriesModifiedInLastRequest = currentEditingSession.entries.get().filter((entry) => entry.lastModifyingRequestId === item.requestId);
234
const shouldPrompt = entriesModifiedInLastRequest.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRetry') === true;
235
const confirmation = shouldPrompt
236
? await dialogService.confirm({
237
title: localize('chat.retryLast.confirmation.title2', "Do you want to retry your last request?"),
238
message: entriesModifiedInLastRequest.length === 1
239
? localize('chat.retry.confirmation.message2', "This will undo edits made to {0} since this request.", basename(entriesModifiedInLastRequest[0].modifiedURI))
240
: 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),
241
primaryButton: localize('chat.retry.confirmation.primaryButton', "Yes"),
242
checkbox: { label: localize('chat.retry.confirmation.checkbox', "Don't ask again"), checked: false },
243
type: 'info'
244
})
245
: { confirmed: true };
246
247
if (!confirmation.confirmed) {
248
return;
249
}
250
251
if (confirmation.checkboxChecked) {
252
await configurationService.updateValue('chat.editing.confirmEditRequestRetry', false);
253
}
254
255
// Reset the snapshot to the first stop (undefined undo index)
256
const snapshotRequest = chatRequests[itemIndex];
257
if (snapshotRequest) {
258
await currentEditingSession.restoreSnapshot(snapshotRequest.id, undefined);
259
}
260
}
261
const request = chatModel?.getRequests().find(candidate => candidate.id === item.requestId);
262
const languageModelId = widget?.input.currentLanguageModel;
263
264
chatAccessibilityService.acceptRequest(item.sessionResource);
265
chatService.resendRequest(request!, {
266
userSelectedModelId: languageModelId,
267
attempt: (request?.attempt ?? -1) + 1,
268
...widget?.getModeRequestOptions(),
269
});
270
}
271
});
272
273
registerAction2(class InsertToNotebookAction extends Action2 {
274
constructor() {
275
super({
276
id: 'workbench.action.chat.insertIntoNotebook',
277
title: localize2('interactive.insertIntoNotebook.label', "Insert into Notebook"),
278
f1: false,
279
category: CHAT_CATEGORY,
280
icon: Codicon.insert,
281
menu: {
282
id: MenuId.ChatMessageFooter,
283
group: 'navigation',
284
isHiddenByDefault: true,
285
when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, ChatContextKeys.isResponse, ChatContextKeys.responseIsFiltered.negate())
286
}
287
});
288
}
289
290
async run(accessor: ServicesAccessor, ...args: unknown[]) {
291
const item = args[0];
292
if (!isResponseVM(item)) {
293
return;
294
}
295
296
const editorService = accessor.get(IEditorService);
297
298
if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) {
299
const notebookEditor = editorService.activeEditorPane.getControl() as INotebookEditor;
300
301
if (!notebookEditor.hasModel()) {
302
return;
303
}
304
305
if (notebookEditor.isReadOnly) {
306
return;
307
}
308
309
const value = item.response.toString();
310
const splitContents = splitMarkdownAndCodeBlocks(value);
311
312
const focusRange = notebookEditor.getFocus();
313
const index = Math.max(focusRange.end, 0);
314
const bulkEditService = accessor.get(IBulkEditService);
315
316
await bulkEditService.apply(
317
[
318
new ResourceNotebookCellEdit(notebookEditor.textModel.uri,
319
{
320
editType: CellEditType.Replace,
321
index: index,
322
count: 0,
323
cells: splitContents.map(content => {
324
const kind = content.type === 'markdown' ? CellKind.Markup : CellKind.Code;
325
const language = content.type === 'markdown' ? 'markdown' : content.language;
326
const mime = content.type === 'markdown' ? 'text/markdown' : `text/x-${content.language}`;
327
return {
328
cellKind: kind,
329
language,
330
mime,
331
source: content.content,
332
outputs: [],
333
metadata: {}
334
};
335
})
336
}
337
)
338
],
339
{ quotableLabel: 'Insert into Notebook' }
340
);
341
}
342
}
343
});
344
}
345
346
interface MarkdownContent {
347
type: 'markdown';
348
content: string;
349
}
350
351
interface CodeContent {
352
type: 'code';
353
language: string;
354
content: string;
355
}
356
357
type Content = MarkdownContent | CodeContent;
358
359
function splitMarkdownAndCodeBlocks(markdown: string): Content[] {
360
const lexer = new marked.Lexer();
361
const tokens = lexer.lex(markdown);
362
363
const splitContent: Content[] = [];
364
365
let markdownPart = '';
366
tokens.forEach((token) => {
367
if (token.type === 'code') {
368
if (markdownPart.trim()) {
369
splitContent.push({ type: 'markdown', content: markdownPart });
370
markdownPart = '';
371
}
372
splitContent.push({
373
type: 'code',
374
language: token.lang || '',
375
content: token.text,
376
});
377
} else {
378
markdownPart += token.raw;
379
}
380
});
381
382
if (markdownPart.trim()) {
383
splitContent.push({ type: 'markdown', content: markdownPart });
384
}
385
386
return splitContent;
387
}
388
389