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
3296 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/chatContextKeys.js';
23
import { applyingChatEditsFailedContextKey, isChatEditingActionContext } from '../../common/chatEditingService.js';
24
import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatService } from '../../common/chatService.js';
25
import { isResponseVM } from '../../common/chatViewModel.js';
26
import { ChatModeKind } from '../../common/constants.js';
27
import { 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: any[]) {
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
sessionId: item.sessionId,
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: any[]) {
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
sessionId: item.sessionId,
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: any[]) {
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
sessionId: item.sessionId,
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: any[]) {
203
const chatWidgetService = accessor.get(IChatWidgetService);
204
205
let item = args[0];
206
if (isChatEditingActionContext(item)) {
207
// Resolve chat editing action context to the last response VM
208
item = chatWidgetService.getWidgetBySessionId(item.sessionId)?.viewModel?.getItems().at(-1);
209
}
210
if (!isResponseVM(item)) {
211
return;
212
}
213
214
const chatService = accessor.get(IChatService);
215
const chatModel = chatService.getSession(item.sessionId);
216
const chatRequests = chatModel?.getRequests();
217
if (!chatRequests) {
218
return;
219
}
220
const itemIndex = chatRequests?.findIndex(request => request.id === item.requestId);
221
const widget = chatWidgetService.getWidgetBySessionId(item.sessionId);
222
const mode = widget?.input.currentModeKind;
223
if (chatModel && (mode === ChatModeKind.Edit || mode === ChatModeKind.Agent)) {
224
const configurationService = accessor.get(IConfigurationService);
225
const dialogService = accessor.get(IDialogService);
226
const currentEditingSession = widget?.viewModel?.model.editingSession;
227
if (!currentEditingSession) {
228
return;
229
}
230
231
// Prompt if the last request modified the working set and the user hasn't already disabled the dialog
232
const entriesModifiedInLastRequest = currentEditingSession.entries.get().filter((entry) => entry.lastModifyingRequestId === item.requestId);
233
const shouldPrompt = entriesModifiedInLastRequest.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRetry') === true;
234
const confirmation = shouldPrompt
235
? await dialogService.confirm({
236
title: localize('chat.retryLast.confirmation.title2', "Do you want to retry your last request?"),
237
message: entriesModifiedInLastRequest.length === 1
238
? 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),
240
primaryButton: localize('chat.retry.confirmation.primaryButton', "Yes"),
241
checkbox: { label: localize('chat.retry.confirmation.checkbox', "Don't ask again"), checked: false },
242
type: 'info'
243
})
244
: { confirmed: true };
245
246
if (!confirmation.confirmed) {
247
return;
248
}
249
250
if (confirmation.checkboxChecked) {
251
await configurationService.updateValue('chat.editing.confirmEditRequestRetry', false);
252
}
253
254
// Reset the snapshot to the first stop (undefined undo index)
255
const snapshotRequest = chatRequests[itemIndex];
256
if (snapshotRequest) {
257
await currentEditingSession.restoreSnapshot(snapshotRequest.id, undefined);
258
}
259
}
260
const request = chatModel?.getRequests().find(candidate => candidate.id === item.requestId);
261
const languageModelId = widget?.input.currentLanguageModel;
262
263
chatService.resendRequest(request!, {
264
userSelectedModelId: languageModelId,
265
attempt: (request?.attempt ?? -1) + 1,
266
...widget?.getModeRequestOptions(),
267
});
268
}
269
});
270
271
registerAction2(class InsertToNotebookAction extends Action2 {
272
constructor() {
273
super({
274
id: 'workbench.action.chat.insertIntoNotebook',
275
title: localize2('interactive.insertIntoNotebook.label', "Insert into Notebook"),
276
f1: false,
277
category: CHAT_CATEGORY,
278
icon: Codicon.insert,
279
menu: {
280
id: MenuId.ChatMessageFooter,
281
group: 'navigation',
282
isHiddenByDefault: true,
283
when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, ChatContextKeys.isResponse, ChatContextKeys.responseIsFiltered.negate())
284
}
285
});
286
}
287
288
async run(accessor: ServicesAccessor, ...args: any[]) {
289
const item = args[0];
290
if (!isResponseVM(item)) {
291
return;
292
}
293
294
const editorService = accessor.get(IEditorService);
295
296
if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) {
297
const notebookEditor = editorService.activeEditorPane.getControl() as INotebookEditor;
298
299
if (!notebookEditor.hasModel()) {
300
return;
301
}
302
303
if (notebookEditor.isReadOnly) {
304
return;
305
}
306
307
const value = item.response.toString();
308
const splitContents = splitMarkdownAndCodeBlocks(value);
309
310
const focusRange = notebookEditor.getFocus();
311
const index = Math.max(focusRange.end, 0);
312
const bulkEditService = accessor.get(IBulkEditService);
313
314
await bulkEditService.apply(
315
[
316
new ResourceNotebookCellEdit(notebookEditor.textModel.uri,
317
{
318
editType: CellEditType.Replace,
319
index: index,
320
count: 0,
321
cells: splitContents.map(content => {
322
const kind = content.type === 'markdown' ? CellKind.Markup : CellKind.Code;
323
const language = content.type === 'markdown' ? 'markdown' : content.language;
324
const mime = content.type === 'markdown' ? 'text/markdown' : `text/x-${content.language}`;
325
return {
326
cellKind: kind,
327
language,
328
mime,
329
source: content.content,
330
outputs: [],
331
metadata: {}
332
};
333
})
334
}
335
)
336
],
337
{ quotableLabel: 'Insert into Notebook' }
338
);
339
}
340
}
341
});
342
}
343
344
interface MarkdownContent {
345
type: 'markdown';
346
content: string;
347
}
348
349
interface CodeContent {
350
type: 'code';
351
language: string;
352
content: string;
353
}
354
355
type Content = MarkdownContent | CodeContent;
356
357
function splitMarkdownAndCodeBlocks(markdown: string): Content[] {
358
const lexer = new marked.Lexer();
359
const tokens = lexer.lex(markdown);
360
361
const splitContent: Content[] = [];
362
363
let markdownPart = '';
364
tokens.forEach((token) => {
365
if (token.type === 'code') {
366
if (markdownPart.trim()) {
367
splitContent.push({ type: 'markdown', content: markdownPart });
368
markdownPart = '';
369
}
370
splitContent.push({
371
type: 'code',
372
language: token.lang || '',
373
content: token.text,
374
});
375
} else {
376
markdownPart += token.raw;
377
}
378
});
379
380
if (markdownPart.trim()) {
381
splitContent.push({ type: 'markdown', content: markdownPart });
382
}
383
384
return splitContent;
385
}
386
387