Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts
5257 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 { CancellationToken } from '../../../../../base/common/cancellation.js';
7
import { Codicon } from '../../../../../base/common/codicons.js';
8
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
9
import { alert } from '../../../../../base/browser/ui/aria/aria.js';
10
import { basename } from '../../../../../base/common/resources.js';
11
import { URI, UriComponents } from '../../../../../base/common/uri.js';
12
import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
13
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
14
import { Position } from '../../../../../editor/common/core/position.js';
15
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
16
import { isLocation, Location } from '../../../../../editor/common/languages.js';
17
import { ITextModel } from '../../../../../editor/common/model.js';
18
import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';
19
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
20
import { localize, localize2 } from '../../../../../nls.js';
21
import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
22
import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js';
23
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
24
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
25
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
26
import { EditorActivation } from '../../../../../platform/editor/common/editor.js';
27
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
28
import { IEditorPane } from '../../../../common/editor.js';
29
import { IEditorService } from '../../../../services/editor/common/editorService.js';
30
import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js';
31
import { isChatViewTitleActionContext } from '../../common/actions/chatActions.js';
32
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
33
import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';
34
import { IChatService } from '../../common/chatService/chatService.js';
35
import { isChatTreeItem, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';
36
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
37
import { CHAT_CATEGORY } from '../actions/chatActions.js';
38
import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js';
39
import { IAgentSession, isAgentSession } from '../agentSessions/agentSessionsModel.js';
40
import { AgentSessionProviders } from '../agentSessions/agentSessions.js';
41
42
export abstract class EditingSessionAction extends Action2 {
43
44
constructor(opts: Readonly<IAction2Options>) {
45
super({
46
category: CHAT_CATEGORY,
47
...opts
48
});
49
}
50
51
run(accessor: ServicesAccessor, ...args: unknown[]) {
52
const context = getEditingSessionContext(accessor, args);
53
if (!context || !context.editingSession) {
54
return;
55
}
56
57
return this.runEditingSessionAction(accessor, context.editingSession, context.chatWidget, ...args);
58
}
59
60
// eslint-disable-next-line @typescript-eslint/no-explicit-any
61
abstract runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): any;
62
}
63
64
export type EditingSessionActionContext = { editingSession?: IChatEditingSession; chatWidget: IChatWidget };
65
66
/**
67
* Resolve view title toolbar context. If none, return context from the lastFocusedWidget.
68
*/
69
// eslint-disable-next-line @typescript-eslint/no-explicit-any
70
export function getEditingSessionContext(accessor: ServicesAccessor, args: any[]): EditingSessionActionContext | undefined {
71
const arg0 = args.at(0);
72
const context = isChatViewTitleActionContext(arg0) ? arg0 : undefined;
73
74
const chatWidgetService = accessor.get(IChatWidgetService);
75
const chatEditingService = accessor.get(IChatEditingService);
76
let chatWidget = context ? chatWidgetService.getWidgetBySessionResource(context.sessionResource) : undefined;
77
if (!chatWidget) {
78
chatWidget = chatWidgetService.lastFocusedWidget ?? chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat).find(w => w.supportsChangingModes);
79
}
80
81
if (!chatWidget?.viewModel) {
82
return;
83
}
84
85
const editingSession = chatEditingService.getEditingSession(chatWidget.viewModel.model.sessionResource);
86
return { editingSession, chatWidget };
87
}
88
89
90
abstract class WorkingSetAction extends EditingSessionAction {
91
92
runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {
93
94
const uris: URI[] = [];
95
if (URI.isUri(args[0])) {
96
uris.push(args[0]);
97
} else if (chatWidget) {
98
uris.push(...chatWidget.input.selectedElements);
99
}
100
if (!uris.length) {
101
return;
102
}
103
104
return this.runWorkingSetAction(accessor, editingSession, chatWidget, ...uris);
105
}
106
107
// eslint-disable-next-line @typescript-eslint/no-explicit-any
108
abstract runWorkingSetAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget | undefined, ...uris: URI[]): any;
109
}
110
111
registerAction2(class OpenFileInDiffAction extends WorkingSetAction {
112
constructor() {
113
super({
114
id: 'chatEditing.openFileInDiff',
115
title: localize2('open.fileInDiff', 'Open Changes in Diff Editor'),
116
icon: Codicon.diffSingle,
117
menu: [{
118
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
119
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),
120
order: 2,
121
group: 'navigation'
122
}],
123
});
124
}
125
126
async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, _chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {
127
const editorService = accessor.get(IEditorService);
128
129
130
for (const uri of uris) {
131
132
let pane: IEditorPane | undefined = editorService.activeEditorPane;
133
if (!pane) {
134
pane = await editorService.openEditor({ resource: uri });
135
}
136
137
if (!pane) {
138
return;
139
}
140
141
const editedFile = currentEditingSession.getEntry(uri);
142
editedFile?.getEditorIntegration(pane).toggleDiff(undefined, true);
143
}
144
}
145
});
146
147
registerAction2(class AcceptAction extends WorkingSetAction {
148
constructor() {
149
super({
150
id: 'chatEditing.acceptFile',
151
title: localize2('accept.file', 'Keep'),
152
icon: Codicon.check,
153
menu: [{
154
when: ContextKeyExpr.and(ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), ContextKeyExpr.notIn(chatEditingResourceContextKey.key, decidedChatEditingResourceContextKey.key)),
155
id: MenuId.MultiDiffEditorFileToolbar,
156
order: 0,
157
group: 'navigation',
158
}, {
159
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
160
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),
161
order: 0,
162
group: 'navigation'
163
}],
164
});
165
}
166
167
async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {
168
await currentEditingSession.accept(...uris);
169
}
170
});
171
172
registerAction2(class DiscardAction extends WorkingSetAction {
173
constructor() {
174
super({
175
id: 'chatEditing.discardFile',
176
title: localize2('discard.file', 'Undo'),
177
icon: Codicon.discard,
178
menu: [{
179
when: ContextKeyExpr.and(ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), ContextKeyExpr.notIn(chatEditingResourceContextKey.key, decidedChatEditingResourceContextKey.key)),
180
id: MenuId.MultiDiffEditorFileToolbar,
181
order: 2,
182
group: 'navigation',
183
}, {
184
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
185
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),
186
order: 1,
187
group: 'navigation'
188
}],
189
});
190
}
191
192
async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {
193
await currentEditingSession.reject(...uris);
194
}
195
});
196
197
export class ChatEditingAcceptAllAction extends EditingSessionAction {
198
199
constructor() {
200
super({
201
id: 'chatEditing.acceptAllFiles',
202
title: localize('accept', 'Keep'),
203
icon: Codicon.check,
204
tooltip: localize('acceptAllEdits', 'Keep All Edits'),
205
precondition: hasUndecidedChatEditingResourceContextKey,
206
keybinding: {
207
primary: KeyMod.CtrlCmd | KeyCode.Enter,
208
when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput),
209
weight: KeybindingWeight.WorkbenchContrib,
210
},
211
menu: [
212
213
{
214
id: MenuId.ChatEditingWidgetToolbar,
215
group: 'navigation',
216
order: 0,
217
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey))
218
}
219
]
220
});
221
}
222
223
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {
224
await editingSession.accept();
225
}
226
}
227
registerAction2(ChatEditingAcceptAllAction);
228
229
export class ChatEditingDiscardAllAction extends EditingSessionAction {
230
231
constructor() {
232
super({
233
id: 'chatEditing.discardAllFiles',
234
title: localize('discard', 'Undo'),
235
icon: Codicon.discard,
236
tooltip: localize('discardAllEdits', 'Undo All Edits'),
237
precondition: hasUndecidedChatEditingResourceContextKey,
238
menu: [
239
{
240
id: MenuId.ChatEditingWidgetToolbar,
241
group: 'navigation',
242
order: 1,
243
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), hasUndecidedChatEditingResourceContextKey)
244
}
245
],
246
keybinding: {
247
when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput, ChatContextKeys.inputHasText.negate()),
248
weight: KeybindingWeight.WorkbenchContrib,
249
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
250
},
251
});
252
}
253
254
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {
255
await discardAllEditsWithConfirmation(accessor, editingSession);
256
}
257
}
258
registerAction2(ChatEditingDiscardAllAction);
259
260
export class ToggleExplanationWidgetAction extends EditingSessionAction {
261
262
static readonly ID = 'chatEditing.toggleExplanationWidget';
263
264
constructor() {
265
super({
266
id: ToggleExplanationWidgetAction.ID,
267
title: localize('explainButton', 'Explain'),
268
tooltip: localize('toggleExplanationTooltip', 'Toggle Change Explanations'),
269
precondition: hasUndecidedChatEditingResourceContextKey,
270
menu: [
271
{
272
id: MenuId.ChatEditingWidgetToolbar,
273
group: 'navigation',
274
order: 2,
275
when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ContextKeyExpr.has(`config.${ChatConfiguration.ExplainChangesEnabled}`))
276
}
277
],
278
});
279
}
280
281
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {
282
if (editingSession.hasExplanations()) {
283
editingSession.clearExplanations();
284
} else {
285
await editingSession.triggerExplanationGeneration();
286
}
287
}
288
}
289
registerAction2(ToggleExplanationWidgetAction);
290
291
export async function discardAllEditsWithConfirmation(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession): Promise<boolean> {
292
293
const dialogService = accessor.get(IDialogService);
294
295
// Ask for confirmation if there are any edits
296
const entries = currentEditingSession.entries.get().filter(e => e.state.get() === ModifiedFileEntryState.Modified);
297
if (entries.length > 0) {
298
const confirmation = await dialogService.confirm({
299
title: localize('chat.editing.discardAll.confirmation.title', "Undo all edits?"),
300
message: entries.length === 1
301
? localize('chat.editing.discardAll.confirmation.oneFile', "This will undo changes made in {0}. Do you want to proceed?", basename(entries[0].modifiedURI))
302
: localize('chat.editing.discardAll.confirmation.manyFiles', "This will undo changes made in {0} files. Do you want to proceed?", entries.length),
303
primaryButton: localize('chat.editing.discardAll.confirmation.primaryButton', "Yes"),
304
type: 'info'
305
});
306
if (!confirmation.confirmed) {
307
return false;
308
}
309
}
310
311
await currentEditingSession.reject();
312
return true;
313
}
314
315
export class ChatEditingShowChangesAction extends EditingSessionAction {
316
static readonly ID = 'chatEditing.viewChanges';
317
static readonly LABEL = localize('chatEditing.viewChanges', 'View All Edits');
318
319
constructor() {
320
super({
321
id: ChatEditingShowChangesAction.ID,
322
title: { value: ChatEditingShowChangesAction.LABEL, original: ChatEditingShowChangesAction.LABEL },
323
tooltip: ChatEditingShowChangesAction.LABEL,
324
f1: true,
325
icon: Codicon.diffMultiple,
326
precondition: hasUndecidedChatEditingResourceContextKey,
327
menu: [
328
{
329
id: MenuId.ChatEditingWidgetToolbar,
330
group: 'navigation',
331
order: 4,
332
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey))
333
}
334
],
335
});
336
}
337
338
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): Promise<void> {
339
await editingSession.show();
340
}
341
}
342
registerAction2(ChatEditingShowChangesAction);
343
344
export class ViewAllSessionChangesAction extends Action2 {
345
static readonly ID = 'chatEditing.viewAllSessionChanges';
346
347
constructor() {
348
super({
349
id: ViewAllSessionChangesAction.ID,
350
title: localize2('chatEditing.viewAllSessionChanges', 'View All Changes'),
351
icon: Codicon.diffMultiple,
352
category: CHAT_CATEGORY,
353
precondition: ChatContextKeys.hasAgentSessionChanges,
354
menu: [
355
{
356
id: MenuId.ChatEditingSessionChangesToolbar,
357
group: 'navigation',
358
order: 10,
359
when: ChatContextKeys.hasAgentSessionChanges
360
},
361
{
362
id: MenuId.AgentSessionItemToolbar,
363
group: 'navigation',
364
order: 0,
365
when: ChatContextKeys.hasAgentSessionChanges
366
}
367
],
368
});
369
}
370
371
override async run(accessor: ServicesAccessor, sessionOrSessionResource?: URI | IAgentSession): Promise<void> {
372
const agentSessionsService = accessor.get(IAgentSessionsService);
373
const commandService = accessor.get(ICommandService);
374
const chatEditingService = accessor.get(IChatEditingService);
375
376
if (!URI.isUri(sessionOrSessionResource) && !isAgentSession(sessionOrSessionResource)) {
377
return;
378
}
379
380
const sessionResource = URI.isUri(sessionOrSessionResource)
381
? sessionOrSessionResource
382
: sessionOrSessionResource.resource;
383
384
const session = agentSessionsService.getSession(sessionResource);
385
const changes = session?.changes;
386
387
if (!session || !changes) {
388
return;
389
}
390
391
if (
392
session.providerType === AgentSessionProviders.Background ||
393
session.providerType === AgentSessionProviders.Cloud
394
) {
395
if (!Array.isArray(changes) || changes.length === 0) {
396
return;
397
}
398
399
// Use agent session changes
400
const resources = changes.map(d => ({
401
originalUri: d.originalUri,
402
modifiedUri: d.modifiedUri
403
}));
404
405
await commandService.executeCommand('_workbench.openMultiDiffEditor', {
406
multiDiffSourceUri: sessionResource.with({ scheme: sessionResource.scheme + '-worktree-changes' }),
407
title: localize('chatEditing.allChanges.title', 'All Session Changes'),
408
resources,
409
});
410
411
session?.setRead(true);
412
return;
413
}
414
415
// Use edit session changes
416
const editingSession = chatEditingService.getEditingSession(sessionResource);
417
await editingSession?.show();
418
session?.setRead(true);
419
}
420
}
421
registerAction2(ViewAllSessionChangesAction);
422
423
async function restoreSnapshotWithConfirmationByRequestId(accessor: ServicesAccessor, sessionResource: URI, requestId: string): Promise<void> {
424
const configurationService = accessor.get(IConfigurationService);
425
const dialogService = accessor.get(IDialogService);
426
const chatWidgetService = accessor.get(IChatWidgetService);
427
const widget = chatWidgetService.getWidgetBySessionResource(sessionResource);
428
const chatService = accessor.get(IChatService);
429
const chatModel = chatService.getSession(sessionResource);
430
if (!chatModel) {
431
return;
432
}
433
434
const session = chatModel.editingSession;
435
if (!session) {
436
return;
437
}
438
439
const chatRequests = chatModel.getRequests();
440
const itemIndex = chatRequests.findIndex(request => request.id === requestId);
441
if (itemIndex === -1) {
442
return;
443
}
444
445
const editsToUndo = chatRequests.length - itemIndex;
446
447
const requestsToRemove = chatRequests.slice(itemIndex);
448
const requestIdsToRemove = new Set(requestsToRemove.map(request => request.id));
449
const entriesModifiedInRequestsToRemove = session.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? [];
450
const shouldPrompt = entriesModifiedInRequestsToRemove.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRemoval') === true;
451
452
let message: string;
453
if (editsToUndo === 1) {
454
if (entriesModifiedInRequestsToRemove.length === 1) {
455
message = localize('chat.removeLast.confirmation.message2', "This will remove your last request and undo the edits made to {0}. Do you want to proceed?", basename(entriesModifiedInRequestsToRemove[0].modifiedURI));
456
} else {
457
message = localize('chat.removeLast.confirmation.multipleEdits.message', "This will remove your last request and undo edits made to {0} files in your working set. Do you want to proceed?", entriesModifiedInRequestsToRemove.length);
458
}
459
} else {
460
if (entriesModifiedInRequestsToRemove.length === 1) {
461
message = localize('chat.remove.confirmation.message2', "This will remove all subsequent requests and undo edits made to {0}. Do you want to proceed?", basename(entriesModifiedInRequestsToRemove[0].modifiedURI));
462
} else {
463
message = localize('chat.remove.confirmation.multipleEdits.message', "This will remove all subsequent requests and undo edits made to {0} files in your working set. Do you want to proceed?", entriesModifiedInRequestsToRemove.length);
464
}
465
}
466
467
const confirmation = shouldPrompt
468
? await dialogService.confirm({
469
title: editsToUndo === 1
470
? localize('chat.removeLast.confirmation.title', "Do you want to undo your last edit?")
471
: localize('chat.remove.confirmation.title', "Do you want to undo {0} edits?", editsToUndo),
472
message: message,
473
primaryButton: localize('chat.remove.confirmation.primaryButton', "Yes"),
474
checkbox: { label: localize('chat.remove.confirmation.checkbox', "Don't ask again"), checked: false },
475
type: 'info'
476
})
477
: { confirmed: true };
478
479
if (!confirmation.confirmed) {
480
widget?.viewModel?.model.setCheckpoint(undefined);
481
return;
482
}
483
484
if (confirmation.checkboxChecked) {
485
await configurationService.updateValue('chat.editing.confirmEditRequestRemoval', false);
486
}
487
488
// Restore the snapshot to what it was before the request(s) that we deleted
489
const snapshotRequestId = chatRequests[itemIndex].id;
490
await session.restoreSnapshot(snapshotRequestId, undefined);
491
}
492
493
async function restoreSnapshotWithConfirmation(accessor: ServicesAccessor, item: ChatTreeItem): Promise<void> {
494
const requestId = isRequestVM(item) ? item.id :
495
isResponseVM(item) ? item.requestId : undefined;
496
497
if (!requestId) {
498
return;
499
}
500
501
await restoreSnapshotWithConfirmationByRequestId(accessor, item.sessionResource, requestId);
502
}
503
504
registerAction2(class RemoveAction extends Action2 {
505
constructor() {
506
super({
507
id: 'workbench.action.chat.undoEdits',
508
title: localize2('chat.undoEdits.label', "Undo Requests"),
509
f1: false,
510
category: CHAT_CATEGORY,
511
icon: Codicon.discard,
512
keybinding: {
513
primary: KeyCode.Delete,
514
mac: {
515
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
516
},
517
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
518
weight: KeybindingWeight.WorkbenchContrib,
519
},
520
menu: [
521
{
522
id: MenuId.ChatMessageTitle,
523
group: 'navigation',
524
order: 2,
525
when: ContextKeyExpr.and(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input').negate(), ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, false), ChatContextKeys.lockedToCodingAgent.negate()),
526
}
527
]
528
});
529
}
530
531
async run(accessor: ServicesAccessor, ...args: unknown[]) {
532
let item = args[0] as ChatTreeItem | undefined;
533
const chatWidgetService = accessor.get(IChatWidgetService);
534
const configurationService = accessor.get(IConfigurationService);
535
const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;
536
if (!isResponseVM(item) && !isRequestVM(item)) {
537
item = widget?.getFocus();
538
}
539
540
if (!item) {
541
return;
542
}
543
544
await restoreSnapshotWithConfirmation(accessor, item);
545
546
if (isRequestVM(item) && configurationService.getValue('chat.undoRequests.restoreInput')) {
547
widget?.focusInput();
548
widget?.input.setValue(item.messageText, false);
549
}
550
}
551
});
552
553
registerAction2(class RestoreCheckpointAction extends Action2 {
554
constructor() {
555
super({
556
id: 'workbench.action.chat.restoreCheckpoint',
557
title: localize2('chat.restoreCheckpoint.label', "Restore Checkpoint"),
558
tooltip: localize2('chat.restoreCheckpoint.tooltip', "Restores workspace and chat to this point"),
559
f1: false,
560
category: CHAT_CATEGORY,
561
keybinding: {
562
primary: KeyCode.Delete,
563
mac: {
564
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
565
},
566
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
567
weight: KeybindingWeight.WorkbenchContrib,
568
},
569
menu: [
570
{
571
id: MenuId.ChatMessageCheckpoint,
572
group: 'navigation',
573
order: 2,
574
when: ContextKeyExpr.and(ChatContextKeys.isRequest, ChatContextKeys.lockedToCodingAgent.negate())
575
}
576
]
577
});
578
}
579
580
async run(accessor: ServicesAccessor, ...args: unknown[]) {
581
let item = args[0] as ChatTreeItem | undefined;
582
const chatWidgetService = accessor.get(IChatWidgetService);
583
const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;
584
if (!isResponseVM(item) && !isRequestVM(item)) {
585
item = widget?.getFocus();
586
}
587
588
if (!item) {
589
return;
590
}
591
592
if (isRequestVM(item)) {
593
widget?.focusInput();
594
widget?.input.setValue(item.messageText, false);
595
}
596
597
widget?.viewModel?.model.setCheckpoint(item.id);
598
await restoreSnapshotWithConfirmation(accessor, item);
599
}
600
});
601
602
registerAction2(class RestoreLastCheckpoint extends Action2 {
603
constructor() {
604
super({
605
id: 'workbench.action.chat.restoreLastCheckpoint',
606
title: localize2('chat.restoreLastCheckpoint.label', "Restore to Last Checkpoint"),
607
f1: true,
608
category: CHAT_CATEGORY,
609
icon: Codicon.discard,
610
precondition: ContextKeyExpr.and(
611
ChatContextKeys.inChatSession,
612
ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, true),
613
ChatContextKeys.lockedToCodingAgent.negate()
614
),
615
menu: [
616
{
617
id: MenuId.ChatMessageFooter,
618
group: 'navigation',
619
order: 1,
620
when: ContextKeyExpr.and(ContextKeyExpr.in(ChatContextKeys.itemId.key, ChatContextKeys.lastItemId.key), ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, true), ChatContextKeys.lockedToCodingAgent.negate()),
621
}
622
]
623
});
624
}
625
626
async run(accessor: ServicesAccessor, ...args: unknown[]) {
627
let item = args[0] as ChatTreeItem | undefined;
628
const chatWidgetService = accessor.get(IChatWidgetService);
629
const chatService = accessor.get(IChatService);
630
const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;
631
if (!isResponseVM(item) && !isRequestVM(item)) {
632
item = widget?.getFocus();
633
}
634
635
const sessionResource = widget?.viewModel?.sessionResource ?? (isChatTreeItem(item) ? item.sessionResource : undefined);
636
if (!sessionResource) {
637
return;
638
}
639
640
const chatModel = chatService.getSession(sessionResource);
641
if (!chatModel?.editingSession) {
642
return;
643
}
644
645
const checkpointRequest = chatModel.checkpoint;
646
if (!checkpointRequest) {
647
alert(localize('chat.restoreCheckpoint.none', 'There is no checkpoint to restore.'));
648
return;
649
}
650
651
widget?.viewModel?.model.setCheckpoint(checkpointRequest.id);
652
widget?.focusInput();
653
widget?.input.setValue(checkpointRequest.message.text, false);
654
655
await restoreSnapshotWithConfirmationByRequestId(accessor, sessionResource, checkpointRequest.id);
656
}
657
});
658
659
registerAction2(class EditAction extends Action2 {
660
constructor() {
661
super({
662
id: 'workbench.action.chat.editRequests',
663
title: localize2('chat.editRequests.label', "Edit Request"),
664
f1: false,
665
category: CHAT_CATEGORY,
666
icon: Codicon.edit,
667
keybinding: {
668
primary: KeyCode.Enter,
669
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
670
weight: KeybindingWeight.WorkbenchContrib,
671
},
672
menu: [
673
{
674
id: MenuId.ChatMessageTitle,
675
group: 'navigation',
676
order: 2,
677
when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'hover'), ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input')))
678
}
679
]
680
});
681
}
682
683
async run(accessor: ServicesAccessor, ...args: unknown[]) {
684
let item = args[0] as ChatTreeItem | undefined;
685
const chatWidgetService = accessor.get(IChatWidgetService);
686
const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;
687
if (!isResponseVM(item) && !isRequestVM(item)) {
688
item = widget?.getFocus();
689
}
690
691
if (!item) {
692
return;
693
}
694
695
if (isRequestVM(item)) {
696
widget?.startEditing(item.id);
697
}
698
}
699
});
700
701
export interface ChatEditingActionContext {
702
readonly sessionResource: URI;
703
readonly requestId: string;
704
readonly uri: URI;
705
readonly stopId: string | undefined;
706
}
707
708
registerAction2(class OpenWorkingSetHistoryAction extends Action2 {
709
710
static readonly id = 'chat.openFileUpdatedBySnapshot';
711
constructor() {
712
super({
713
id: OpenWorkingSetHistoryAction.id,
714
title: localize('chat.openFileUpdatedBySnapshot.label', "Open File"),
715
menu: [{
716
id: MenuId.ChatEditingCodeBlockContext,
717
group: 'navigation',
718
order: 0,
719
},]
720
});
721
}
722
723
override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
724
const context = args[0] as ChatEditingActionContext | undefined;
725
if (!context?.sessionResource) {
726
return;
727
}
728
729
const editorService = accessor.get(IEditorService);
730
await editorService.openEditor({ resource: context.uri });
731
}
732
});
733
734
registerAction2(class OpenWorkingSetHistoryAction extends Action2 {
735
736
static readonly id = 'chat.openFileSnapshot';
737
constructor() {
738
super({
739
id: OpenWorkingSetHistoryAction.id,
740
title: localize('chat.openSnapshot.label', "Open File Snapshot"),
741
menu: [{
742
id: MenuId.ChatEditingCodeBlockContext,
743
group: 'navigation',
744
order: 1,
745
},]
746
});
747
}
748
749
override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
750
const context = args[0] as ChatEditingActionContext | undefined;
751
if (!context?.sessionResource) {
752
return;
753
}
754
755
const chatService = accessor.get(IChatService);
756
const chatEditingService = accessor.get(IChatEditingService);
757
const editorService = accessor.get(IEditorService);
758
759
const chatModel = chatService.getSession(context.sessionResource);
760
if (!chatModel) {
761
return;
762
}
763
764
const snapshot = chatEditingService.getEditingSession(chatModel.sessionResource)?.getSnapshotUri(context.requestId, context.uri, context.stopId);
765
if (snapshot) {
766
const editor = await editorService.openEditor({ resource: snapshot, label: localize('chatEditing.snapshot', '{0} (Snapshot)', basename(context.uri)), options: { activation: EditorActivation.ACTIVATE } });
767
if (isCodeEditor(editor)) {
768
editor.updateOptions({ readOnly: true });
769
}
770
}
771
}
772
});
773
774
registerAction2(class ResolveSymbolsContextAction extends EditingSessionAction {
775
constructor() {
776
super({
777
id: 'workbench.action.edits.addFilesFromReferences',
778
title: localize2('addFilesFromReferences', "Add Files From References"),
779
f1: false,
780
category: CHAT_CATEGORY,
781
menu: {
782
id: MenuId.ChatInputSymbolAttachmentContext,
783
group: 'navigation',
784
order: 1,
785
when: ContextKeyExpr.and(ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask), EditorContextKeys.hasReferenceProvider)
786
}
787
});
788
}
789
790
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): Promise<void> {
791
if (args.length === 0 || !isLocation(args[0])) {
792
return;
793
}
794
795
const textModelService = accessor.get(ITextModelService);
796
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
797
const symbol = args[0] as Location;
798
799
const modelReference = await textModelService.createModelReference(symbol.uri);
800
const textModel = modelReference.object.textEditorModel;
801
if (!textModel) {
802
return;
803
}
804
805
const position = new Position(symbol.range.startLineNumber, symbol.range.startColumn);
806
807
const [references, definitions, implementations] = await Promise.all([
808
this.getReferences(position, textModel, languageFeaturesService),
809
this.getDefinitions(position, textModel, languageFeaturesService),
810
this.getImplementations(position, textModel, languageFeaturesService)
811
]);
812
813
// Sort the references, definitions and implementations by
814
// how important it is that they make it into the working set as it has limited size
815
const attachments = [];
816
for (const reference of [...definitions, ...implementations, ...references]) {
817
attachments.push(chatWidget.attachmentModel.asFileVariableEntry(reference.uri));
818
}
819
820
chatWidget.attachmentModel.addContext(...attachments);
821
}
822
823
private async getReferences(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
824
const referenceProviders = languageFeaturesService.referenceProvider.all(textModel);
825
826
const references = await Promise.all(referenceProviders.map(async (referenceProvider) => {
827
return await referenceProvider.provideReferences(textModel, position, { includeDeclaration: true }, CancellationToken.None) ?? [];
828
}));
829
830
return references.flat();
831
}
832
833
private async getDefinitions(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
834
const definitionProviders = languageFeaturesService.definitionProvider.all(textModel);
835
836
const definitions = await Promise.all(definitionProviders.map(async (definitionProvider) => {
837
return await definitionProvider.provideDefinition(textModel, position, CancellationToken.None) ?? [];
838
}));
839
840
return definitions.flat();
841
}
842
843
private async getImplementations(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
844
const implementationProviders = languageFeaturesService.implementationProvider.all(textModel);
845
846
const implementations = await Promise.all(implementationProviders.map(async (implementationProvider) => {
847
return await implementationProvider.provideImplementation(textModel, position, CancellationToken.None) ?? [];
848
}));
849
850
return implementations.flat();
851
}
852
});
853
854
export class ViewPreviousEditsAction extends EditingSessionAction {
855
static readonly Id = 'chatEditing.viewPreviousEdits';
856
static readonly Label = localize('chatEditing.viewPreviousEdits', 'View Previous Edits');
857
858
constructor() {
859
super({
860
id: ViewPreviousEditsAction.Id,
861
title: { value: ViewPreviousEditsAction.Label, original: ViewPreviousEditsAction.Label },
862
tooltip: ViewPreviousEditsAction.Label,
863
f1: true,
864
icon: Codicon.diffMultiple,
865
precondition: ContextKeyExpr.and(ChatContextKeys.enabled, hasUndecidedChatEditingResourceContextKey.negate()),
866
menu: [
867
{
868
id: MenuId.ChatEditingWidgetToolbar,
869
group: 'navigation',
870
order: 4,
871
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey.negate()))
872
}
873
],
874
});
875
}
876
877
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): Promise<void> {
878
await editingSession.show(true);
879
}
880
}
881
registerAction2(ViewPreviousEditsAction);
882
883
/**
884
* Workbench command to explore accepting working set changes from an extension. Executing
885
* the command will accept the changes for the provided resources across all edit sessions.
886
*/
887
CommandsRegistry.registerCommand('_chat.editSessions.accept', async (accessor: ServicesAccessor, resources: UriComponents[]) => {
888
if (resources.length === 0) {
889
return;
890
}
891
892
const uris = resources.map(resource => URI.revive(resource));
893
const chatEditingService = accessor.get(IChatEditingService);
894
for (const editingSession of chatEditingService.editingSessionsObs.get()) {
895
await editingSession.accept(...uris);
896
}
897
});
898
899