Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.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 { CancellationToken } from '../../../../../base/common/cancellation.js';
7
import { Codicon } from '../../../../../base/common/codicons.js';
8
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
9
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
10
import { basename, relativePath } from '../../../../../base/common/resources.js';
11
import { ThemeIcon } from '../../../../../base/common/themables.js';
12
import { assertType } from '../../../../../base/common/types.js';
13
import { URI } from '../../../../../base/common/uri.js';
14
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
15
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
16
import { localize, localize2 } from '../../../../../nls.js';
17
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
18
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
19
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
20
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
21
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
22
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
23
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
24
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
25
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
26
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
27
import { IEditorService } from '../../../../services/editor/common/editorService.js';
28
import { IRemoteCodingAgent, IRemoteCodingAgentsService } from '../../../remoteCodingAgents/common/remoteCodingAgentsService.js';
29
import { IChatAgentHistoryEntry, IChatAgentService } from '../../common/chatAgents.js';
30
import { ChatContextKeys } from '../../common/chatContextKeys.js';
31
import { IChatModel, IChatRequestModel, toChatHistoryContent } from '../../common/chatModel.js';
32
import { IChatMode, IChatModeService } from '../../common/chatModes.js';
33
import { chatVariableLeader } from '../../common/chatParserTypes.js';
34
import { ChatRequestParser } from '../../common/chatRequestParser.js';
35
import { IChatPullRequestContent, IChatService } from '../../common/chatService.js';
36
import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';
37
import { ChatSessionUri } from '../../common/chatUri.js';
38
import { ChatRequestVariableSet, isChatRequestFileEntry } from '../../common/chatVariableEntries.js';
39
import { ChatAgentLocation, ChatConfiguration, ChatModeKind, } from '../../common/constants.js';
40
import { ILanguageModelChatMetadata } from '../../common/languageModels.js';
41
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
42
import { IChatWidget, IChatWidgetService } from '../chat.js';
43
import { getEditingSessionContext } from '../chatEditing/chatEditingActions.js';
44
import { IChatEditorOptions } from '../chatEditor.js';
45
import { ACTION_ID_NEW_CHAT, CHAT_CATEGORY, handleCurrentEditingSession, handleModeSwitch } from './chatActions.js';
46
47
export interface IVoiceChatExecuteActionContext {
48
readonly disableTimeout?: boolean;
49
}
50
51
export interface IChatExecuteActionContext {
52
widget?: IChatWidget;
53
inputValue?: string;
54
voice?: IVoiceChatExecuteActionContext;
55
}
56
57
abstract class SubmitAction extends Action2 {
58
async run(accessor: ServicesAccessor, ...args: any[]) {
59
const context: IChatExecuteActionContext | undefined = args[0];
60
const telemetryService = accessor.get(ITelemetryService);
61
const widgetService = accessor.get(IChatWidgetService);
62
const widget = context?.widget ?? widgetService.lastFocusedWidget;
63
if (widget?.viewModel?.editing) {
64
const configurationService = accessor.get(IConfigurationService);
65
const dialogService = accessor.get(IDialogService);
66
const chatService = accessor.get(IChatService);
67
const chatModel = chatService.getSession(widget.viewModel.sessionId);
68
if (!chatModel) {
69
return;
70
}
71
72
const session = chatModel.editingSession;
73
if (!session) {
74
return;
75
}
76
77
const requestId = widget.viewModel?.editing.id;
78
79
if (requestId) {
80
const chatRequests = chatModel.getRequests();
81
const itemIndex = chatRequests.findIndex(request => request.id === requestId);
82
const editsToUndo = chatRequests.length - itemIndex;
83
84
const requestsToRemove = chatRequests.slice(itemIndex);
85
const requestIdsToRemove = new Set(requestsToRemove.map(request => request.id));
86
const entriesModifiedInRequestsToRemove = session.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? [];
87
const shouldPrompt = entriesModifiedInRequestsToRemove.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRemoval') === true;
88
89
let message: string;
90
if (editsToUndo === 1) {
91
if (entriesModifiedInRequestsToRemove.length === 1) {
92
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));
93
} else {
94
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);
95
}
96
} else {
97
if (entriesModifiedInRequestsToRemove.length === 1) {
98
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));
99
} else {
100
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);
101
}
102
}
103
104
const confirmation = shouldPrompt
105
? await dialogService.confirm({
106
title: editsToUndo === 1
107
? localize('chat.removeLast.confirmation.title', "Do you want to undo your last edit?")
108
: localize('chat.remove.confirmation.title', "Do you want to undo {0} edits?", editsToUndo),
109
message: message,
110
primaryButton: localize('chat.remove.confirmation.primaryButton', "Yes"),
111
checkbox: { label: localize('chat.remove.confirmation.checkbox', "Don't ask again"), checked: false },
112
type: 'info'
113
})
114
: { confirmed: true };
115
116
type EditUndoEvent = {
117
editRequestType: string;
118
outcome: 'cancelled' | 'applied';
119
editsUndoCount: number;
120
};
121
122
type EditUndoEventClassification = {
123
owner: 'justschen';
124
comment: 'Event used to gain insights into when there are pending changes to undo, and whether edited requests are applied or cancelled.';
125
editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' };
126
outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the edit was cancelled or applied.' };
127
editsUndoCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of edits that would be undone.'; 'isMeasurement': true };
128
};
129
130
if (!confirmation.confirmed) {
131
telemetryService.publicLog2<EditUndoEvent, EditUndoEventClassification>('chat.undoEditsConfirmation', {
132
editRequestType: configurationService.getValue<string>('chat.editRequests'),
133
outcome: 'cancelled',
134
editsUndoCount: editsToUndo
135
});
136
return;
137
} else if (editsToUndo > 0) {
138
telemetryService.publicLog2<EditUndoEvent, EditUndoEventClassification>('chat.undoEditsConfirmation', {
139
editRequestType: configurationService.getValue<string>('chat.editRequests'),
140
outcome: 'applied',
141
editsUndoCount: editsToUndo
142
});
143
}
144
145
if (confirmation.checkboxChecked) {
146
await configurationService.updateValue('chat.editing.confirmEditRequestRemoval', false);
147
}
148
149
// Restore the snapshot to what it was before the request(s) that we deleted
150
const snapshotRequestId = chatRequests[itemIndex].id;
151
await session.restoreSnapshot(snapshotRequestId, undefined);
152
}
153
} else if (widget?.viewModel?.model.checkpoint) {
154
widget.viewModel.model.setCheckpoint(undefined);
155
}
156
widget?.acceptInput(context?.inputValue);
157
}
158
}
159
160
const whenNotInProgress = ChatContextKeys.requestInProgress.negate();
161
162
export class ChatSubmitAction extends SubmitAction {
163
static readonly ID = 'workbench.action.chat.submit';
164
165
constructor() {
166
const menuCondition = ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask);
167
168
super({
169
id: ChatSubmitAction.ID,
170
title: localize2('interactive.submit.label', "Send and Dispatch"),
171
f1: false,
172
category: CHAT_CATEGORY,
173
icon: Codicon.send,
174
toggled: {
175
condition: ChatContextKeys.lockedToCodingAgent,
176
icon: Codicon.sendToRemoteAgent,
177
tooltip: localize('sendToRemoteAgent', "Send to coding agent"),
178
},
179
keybinding: {
180
when: ChatContextKeys.inChatInput,
181
primary: KeyCode.Enter,
182
weight: KeybindingWeight.EditorContrib
183
},
184
menu: [
185
{
186
id: MenuId.ChatExecuteSecondary,
187
group: 'group_1',
188
order: 1,
189
when: ContextKeyExpr.and(menuCondition, ChatContextKeys.lockedToCodingAgent.negate()),
190
},
191
{
192
id: MenuId.ChatExecute,
193
order: 4,
194
when: ContextKeyExpr.and(
195
whenNotInProgress,
196
menuCondition,
197
),
198
group: 'navigation',
199
}]
200
});
201
}
202
}
203
204
export const ToggleAgentModeActionId = 'workbench.action.chat.toggleAgentMode';
205
206
export interface IToggleChatModeArgs {
207
modeId: ChatModeKind | string;
208
}
209
210
type ChatModeChangeClassification = {
211
owner: 'digitarald';
212
comment: 'Reporting when Chat mode is switched between different modes';
213
fromMode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The previous chat mode' };
214
toMode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The new chat mode' };
215
requestCount?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of requests in the current chat session'; 'isMeasurement': true };
216
};
217
218
type ChatModeChangeEvent = {
219
fromMode: string;
220
toMode: string;
221
requestCount: number;
222
};
223
224
class ToggleChatModeAction extends Action2 {
225
226
static readonly ID = ToggleAgentModeActionId;
227
228
constructor() {
229
super({
230
id: ToggleChatModeAction.ID,
231
title: localize2('interactive.toggleAgent.label', "Switch to Next Chat Mode"),
232
f1: true,
233
category: CHAT_CATEGORY,
234
precondition: ContextKeyExpr.and(
235
ChatContextKeys.enabled,
236
ChatContextKeys.requestInProgress.negate())
237
});
238
}
239
240
async run(accessor: ServicesAccessor, ...args: any[]) {
241
const commandService = accessor.get(ICommandService);
242
const configurationService = accessor.get(IConfigurationService);
243
const instaService = accessor.get(IInstantiationService);
244
const modeService = accessor.get(IChatModeService);
245
const telemetryService = accessor.get(ITelemetryService);
246
247
const context = getEditingSessionContext(accessor, args);
248
if (!context?.chatWidget) {
249
return;
250
}
251
252
const arg = args.at(0) as IToggleChatModeArgs | undefined;
253
const chatSession = context.chatWidget.viewModel?.model;
254
const requestCount = chatSession?.getRequests().length ?? 0;
255
const switchToMode = (arg && modeService.findModeById(arg.modeId)) ?? this.getNextMode(context.chatWidget, requestCount, configurationService, modeService);
256
257
const currentMode = context.chatWidget.input.currentModeObs.get();
258
if (switchToMode.id === currentMode.id) {
259
return;
260
}
261
262
const chatModeCheck = await instaService.invokeFunction(handleModeSwitch, context.chatWidget.input.currentModeKind, switchToMode.kind, requestCount, context.editingSession);
263
if (!chatModeCheck) {
264
return;
265
}
266
267
// Send telemetry for mode change
268
telemetryService.publicLog2<ChatModeChangeEvent, ChatModeChangeClassification>('chat.modeChange', {
269
fromMode: currentMode.id,
270
toMode: switchToMode.id,
271
requestCount: requestCount
272
});
273
274
context.chatWidget.input.setChatMode(switchToMode.id);
275
276
if (chatModeCheck.needToClearSession) {
277
await commandService.executeCommand(ACTION_ID_NEW_CHAT);
278
}
279
}
280
281
private getNextMode(chatWidget: IChatWidget, requestCount: number, configurationService: IConfigurationService, modeService: IChatModeService): IChatMode {
282
const modes = modeService.getModes();
283
const flat = [
284
...modes.builtin.filter(mode => {
285
return mode.kind !== ChatModeKind.Edit || configurationService.getValue(ChatConfiguration.Edits2Enabled) || requestCount === 0;
286
}),
287
...(modes.custom ?? []),
288
];
289
290
const curModeIndex = flat.findIndex(mode => mode.id === chatWidget.input.currentModeObs.get().id);
291
const newMode = flat[(curModeIndex + 1) % flat.length];
292
return newMode;
293
}
294
}
295
296
class SwitchToNextModelAction extends Action2 {
297
static readonly ID = 'workbench.action.chat.switchToNextModel';
298
299
constructor() {
300
super({
301
id: SwitchToNextModelAction.ID,
302
title: localize2('interactive.switchToNextModel.label', "Switch to Next Model"),
303
category: CHAT_CATEGORY,
304
f1: true,
305
precondition: ChatContextKeys.enabled,
306
});
307
}
308
309
override run(accessor: ServicesAccessor, ...args: any[]): void {
310
const widgetService = accessor.get(IChatWidgetService);
311
const widget = widgetService.lastFocusedWidget;
312
widget?.input.switchToNextModel();
313
}
314
}
315
316
export const ChatOpenModelPickerActionId = 'workbench.action.chat.openModelPicker';
317
class OpenModelPickerAction extends Action2 {
318
static readonly ID = ChatOpenModelPickerActionId;
319
320
constructor() {
321
super({
322
id: OpenModelPickerAction.ID,
323
title: localize2('interactive.openModelPicker.label', "Open Model Picker"),
324
category: CHAT_CATEGORY,
325
f1: false,
326
keybinding: {
327
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Period,
328
weight: KeybindingWeight.WorkbenchContrib,
329
when: ChatContextKeys.inChatInput
330
},
331
precondition: ChatContextKeys.enabled,
332
menu: {
333
id: MenuId.ChatInput,
334
order: 3,
335
group: 'navigation',
336
when:
337
ContextKeyExpr.and(
338
ChatContextKeys.lockedToCodingAgent.negate(),
339
ChatContextKeys.languageModelsAreUserSelectable,
340
ContextKeyExpr.or(
341
ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Panel),
342
ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Editor),
343
ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Notebook),
344
ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Terminal))
345
)
346
}
347
});
348
}
349
350
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
351
const widgetService = accessor.get(IChatWidgetService);
352
const widget = widgetService.lastFocusedWidget;
353
if (widget) {
354
widget.input.openModelPicker();
355
}
356
}
357
}
358
359
export class OpenModePickerAction extends Action2 {
360
static readonly ID = 'workbench.action.chat.openModePicker';
361
362
constructor() {
363
super({
364
id: OpenModePickerAction.ID,
365
title: localize2('interactive.openModePicker.label', "Open Mode Picker"),
366
tooltip: localize('setChatMode', "Set Mode"),
367
category: CHAT_CATEGORY,
368
f1: false,
369
precondition: ChatContextKeys.enabled,
370
keybinding: {
371
when: ContextKeyExpr.and(
372
ChatContextKeys.inChatInput,
373
ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel)),
374
primary: KeyMod.CtrlCmd | KeyCode.Period,
375
weight: KeybindingWeight.EditorContrib
376
},
377
menu: [
378
{
379
id: MenuId.ChatInput,
380
order: 1,
381
when: ContextKeyExpr.and(
382
ChatContextKeys.enabled,
383
ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel),
384
ChatContextKeys.inQuickChat.negate(),
385
ChatContextKeys.lockedToCodingAgent.negate()),
386
group: 'navigation',
387
},
388
]
389
});
390
}
391
392
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
393
const widgetService = accessor.get(IChatWidgetService);
394
const widget = widgetService.lastFocusedWidget;
395
if (widget) {
396
widget.input.openModePicker();
397
}
398
}
399
}
400
401
export const ChangeChatModelActionId = 'workbench.action.chat.changeModel';
402
class ChangeChatModelAction extends Action2 {
403
static readonly ID = ChangeChatModelActionId;
404
405
constructor() {
406
super({
407
id: ChangeChatModelAction.ID,
408
title: localize2('interactive.changeModel.label', "Change Model"),
409
category: CHAT_CATEGORY,
410
f1: false,
411
precondition: ChatContextKeys.enabled,
412
});
413
}
414
415
override run(accessor: ServicesAccessor, ...args: any[]): void {
416
const modelInfo: Pick<ILanguageModelChatMetadata, 'vendor' | 'id' | 'family'> = args[0];
417
// Type check the arg
418
assertType(typeof modelInfo.vendor === 'string' && typeof modelInfo.id === 'string' && typeof modelInfo.family === 'string');
419
const widgetService = accessor.get(IChatWidgetService);
420
const widgets = widgetService.getAllWidgets();
421
for (const widget of widgets) {
422
widget.input.switchModel(modelInfo);
423
}
424
}
425
}
426
427
export class ChatEditingSessionSubmitAction extends SubmitAction {
428
static readonly ID = 'workbench.action.edits.submit';
429
430
constructor() {
431
const menuCondition = ChatContextKeys.chatModeKind.notEqualsTo(ChatModeKind.Ask);
432
433
super({
434
id: ChatEditingSessionSubmitAction.ID,
435
title: localize2('edits.submit.label', "Send"),
436
f1: false,
437
category: CHAT_CATEGORY,
438
icon: Codicon.send,
439
menu: [
440
{
441
id: MenuId.ChatExecuteSecondary,
442
group: 'group_1',
443
when: ContextKeyExpr.and(whenNotInProgress, menuCondition),
444
order: 1
445
},
446
{
447
id: MenuId.ChatExecute,
448
order: 4,
449
when: ContextKeyExpr.and(
450
ChatContextKeys.requestInProgress.negate(),
451
menuCondition),
452
group: 'navigation',
453
}]
454
});
455
}
456
}
457
458
class SubmitWithoutDispatchingAction extends Action2 {
459
static readonly ID = 'workbench.action.chat.submitWithoutDispatching';
460
461
constructor() {
462
const precondition = ContextKeyExpr.and(
463
// if the input has prompt instructions attached, allow submitting requests even
464
// without text present - having instructions is enough context for a request
465
ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile),
466
whenNotInProgress,
467
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask),
468
);
469
470
super({
471
id: SubmitWithoutDispatchingAction.ID,
472
title: localize2('interactive.submitWithoutDispatch.label', "Send"),
473
f1: false,
474
category: CHAT_CATEGORY,
475
precondition,
476
keybinding: {
477
when: ChatContextKeys.inChatInput,
478
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Enter,
479
weight: KeybindingWeight.EditorContrib
480
},
481
menu: [
482
{
483
id: MenuId.ChatExecuteSecondary,
484
group: 'group_1',
485
order: 2,
486
when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask),
487
}
488
]
489
});
490
}
491
492
run(accessor: ServicesAccessor, ...args: any[]) {
493
const context: IChatExecuteActionContext | undefined = args[0];
494
495
const widgetService = accessor.get(IChatWidgetService);
496
const widget = context?.widget ?? widgetService.lastFocusedWidget;
497
widget?.acceptInput(context?.inputValue, { noCommandDetection: true });
498
}
499
}
500
export class CreateRemoteAgentJobAction extends Action2 {
501
static readonly ID = 'workbench.action.chat.createRemoteAgentJob';
502
503
static readonly markdownStringTrustedOptions = {
504
isTrusted: {
505
enabledCommands: [] as string[],
506
},
507
};
508
509
constructor() {
510
const precondition = ContextKeyExpr.and(
511
whenNotInProgress,
512
ChatContextKeys.remoteJobCreating.negate(),
513
);
514
515
super({
516
id: CreateRemoteAgentJobAction.ID,
517
// TODO(joshspicer): Generalize title, pull from contribution
518
title: localize2('actions.chat.createRemoteJob', "Delegate to Coding Agent"),
519
icon: Codicon.sendToRemoteAgent,
520
precondition,
521
toggled: {
522
condition: ChatContextKeys.remoteJobCreating,
523
icon: Codicon.sync,
524
tooltip: localize('remoteJobCreating', "Delegating to Coding Agent"),
525
},
526
menu: [
527
{
528
id: MenuId.ChatExecute,
529
group: 'navigation',
530
order: 3.4,
531
when: ContextKeyExpr.and(ChatContextKeys.hasRemoteCodingAgent, ChatContextKeys.lockedToCodingAgent.negate()),
532
},
533
{
534
id: MenuId.ChatExecuteSecondary,
535
group: 'group_3',
536
order: 1,
537
when: ContextKeyExpr.and(ChatContextKeys.hasRemoteCodingAgent, ChatContextKeys.lockedToCodingAgent.negate()),
538
}
539
]
540
});
541
}
542
543
private async pickCodingAgent<T extends IChatSessionsExtensionPoint | IRemoteCodingAgent>(
544
quickPickService: IQuickInputService,
545
options: T[]
546
): Promise<T | undefined> {
547
if (options.length === 0) {
548
return undefined;
549
}
550
if (options.length === 1) {
551
return options[0];
552
}
553
const pick = await quickPickService.pick(
554
options.map(a => ({
555
label: a.displayName,
556
description: a.description,
557
agent: a,
558
})),
559
{
560
title: localize('selectCodingAgent', "Select Coding Agent"),
561
}
562
);
563
if (!pick) {
564
return undefined;
565
}
566
return pick.agent;
567
}
568
569
private async createWithChatSessions(
570
chatSessionsService: IChatSessionsService,
571
quickPickService: IQuickInputService,
572
editorService: IEditorService,
573
chatModel: IChatModel,
574
addedRequest: IChatRequestModel,
575
userPrompt: string,
576
summary?: string
577
) {
578
const contributions = chatSessionsService.getAllChatSessionContributions();
579
const agent = await this.pickCodingAgent(quickPickService, contributions);
580
if (!agent) {
581
chatModel.completeResponse(addedRequest);
582
return;
583
}
584
const { type } = agent;
585
const newChatSession = await chatSessionsService.provideNewChatSessionItem(
586
type,
587
{
588
prompt: userPrompt,
589
request: {
590
agentId: '',
591
location: ChatAgentLocation.Panel,
592
message: userPrompt,
593
requestId: '',
594
sessionId: '',
595
variables: { variables: [] },
596
},
597
metadata: {
598
summary,
599
source: 'chatExecuteActions',
600
}
601
},
602
CancellationToken.None,
603
);
604
const options: IChatEditorOptions = {
605
pinned: true,
606
preferredTitle: newChatSession.label,
607
};
608
await editorService.openEditor({
609
resource: ChatSessionUri.forSession(type, newChatSession.id),
610
options,
611
});
612
613
}
614
615
private async createWithLegacy(
616
remoteCodingAgentService: IRemoteCodingAgentsService,
617
commandService: ICommandService,
618
quickPickService: IQuickInputService,
619
chatModel: IChatModel,
620
addedRequest: IChatRequestModel,
621
widget: IChatWidget,
622
userPrompt: string,
623
summary?: string,
624
) {
625
const agents = remoteCodingAgentService.getAvailableAgents();
626
const agent = await this.pickCodingAgent(quickPickService, agents);
627
if (!agent) {
628
chatModel.completeResponse(addedRequest);
629
return;
630
}
631
632
// Execute the remote command
633
const result: Omit<IChatPullRequestContent, 'kind'> | string | undefined = await commandService.executeCommand(agent.command, {
634
userPrompt,
635
summary: summary || userPrompt,
636
_version: 2, // Signal that we support the new response format
637
});
638
639
if (result && typeof result === 'object') { /* _version === 2 */
640
chatModel.acceptResponseProgress(addedRequest, { kind: 'pullRequest', ...result });
641
chatModel.acceptResponseProgress(addedRequest, {
642
kind: 'markdownContent', content: new MarkdownString(
643
localize('remoteAgentResponse2', "Your work will be continued in this pull request."),
644
CreateRemoteAgentJobAction.markdownStringTrustedOptions
645
)
646
});
647
} else if (typeof result === 'string') {
648
chatModel.acceptResponseProgress(addedRequest, {
649
kind: 'markdownContent',
650
content: new MarkdownString(
651
localize('remoteAgentResponse', "Coding agent response: {0}", result),
652
CreateRemoteAgentJobAction.markdownStringTrustedOptions
653
)
654
});
655
// Extension will open up the pull request in another view
656
widget.clear();
657
} else {
658
chatModel.acceptResponseProgress(addedRequest, {
659
kind: 'markdownContent',
660
content: new MarkdownString(
661
localize('remoteAgentError', "Coding agent session cancelled."),
662
CreateRemoteAgentJobAction.markdownStringTrustedOptions
663
)
664
});
665
}
666
}
667
668
/**
669
* Converts full URIs from the user's systems into workspace-relative paths for coding agent.
670
*/
671
private extractRelativeFromAttachedContext(attachedContext: ChatRequestVariableSet, workspaceContextService: IWorkspaceContextService): string[] {
672
const workspaceFolder = workspaceContextService.getWorkspace().folders[0];
673
if (!workspaceFolder) {
674
return [];
675
}
676
const relativePaths: string[] = [];
677
for (const contextEntry of attachedContext.asArray()) {
678
if (isChatRequestFileEntry(contextEntry)) { // TODO: Extend for more variable types as needed
679
if (!(contextEntry.value instanceof URI)) {
680
continue;
681
}
682
const fileUri = contextEntry.value;
683
const relativePathResult = relativePath(workspaceFolder.uri, fileUri);
684
if (relativePathResult) {
685
relativePaths.push(relativePathResult);
686
}
687
}
688
}
689
return relativePaths;
690
}
691
692
private extractChatTurns(historyEntries: IChatAgentHistoryEntry[]): string {
693
let result = '\n';
694
for (const entry of historyEntries) {
695
if (entry.request.message) {
696
result += `User: ${entry.request.message}\n`;
697
}
698
if (entry.response) {
699
for (const content of entry.response) {
700
if (content.kind === 'markdownContent') {
701
result += `AI: ${content.content.value}\n`;
702
}
703
}
704
}
705
}
706
return `${result}\n`;
707
}
708
709
async run(accessor: ServicesAccessor, ...args: any[]) {
710
const contextKeyService = accessor.get(IContextKeyService);
711
const remoteJobCreatingKey = ChatContextKeys.remoteJobCreating.bindTo(contextKeyService);
712
713
try {
714
remoteJobCreatingKey.set(true);
715
716
const configurationService = accessor.get(IConfigurationService);
717
const widgetService = accessor.get(IChatWidgetService);
718
const chatAgentService = accessor.get(IChatAgentService);
719
const commandService = accessor.get(ICommandService);
720
const quickPickService = accessor.get(IQuickInputService);
721
const remoteCodingAgentService = accessor.get(IRemoteCodingAgentsService);
722
const chatSessionsService = accessor.get(IChatSessionsService);
723
const editorService = accessor.get(IEditorService);
724
725
726
const widget = widgetService.lastFocusedWidget;
727
if (!widget) {
728
return;
729
}
730
const session = widget.viewModel?.sessionId;
731
if (!session) {
732
return;
733
}
734
const chatModel = widget.viewModel?.model;
735
if (!chatModel) {
736
return;
737
}
738
739
const chatRequests = chatModel.getRequests();
740
let userPrompt = widget.getInput();
741
if (!userPrompt) {
742
if (!chatRequests.length) {
743
// Nothing to do
744
return;
745
}
746
userPrompt = 'implement this.';
747
}
748
749
const attachedContext = widget.input.getAttachedAndImplicitContext(session);
750
widget.input.acceptInput(true);
751
752
const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel);
753
const instantiationService = accessor.get(IInstantiationService);
754
const requestParser = instantiationService.createInstance(ChatRequestParser);
755
const parsedRequest = requestParser.parseChatRequest(session, userPrompt, ChatAgentLocation.Panel);
756
757
758
// Add the request to the model first
759
const addedRequest = chatModel.addRequest(
760
parsedRequest,
761
{ variables: attachedContext.asArray() },
762
0,
763
undefined,
764
defaultAgent,
765
);
766
767
let summary: string = '';
768
const relativeAttachedContext = this.extractRelativeFromAttachedContext(attachedContext, accessor.get(IWorkspaceContextService));
769
if (relativeAttachedContext.length) {
770
summary += `\n\n${localize('attachedFiles', "The user has attached the following files from their workspace:")}\n${relativeAttachedContext.map(file => `- ${file}`).join('\n')}\n\n`;
771
}
772
773
if (defaultAgent && chatRequests.length > 1) {
774
chatModel.acceptResponseProgress(addedRequest, {
775
kind: 'progressMessage',
776
content: new MarkdownString(
777
localize('analyzingChatHistory', "Analyzing chat history"),
778
CreateRemoteAgentJobAction.markdownStringTrustedOptions
779
)
780
});
781
const historyEntries: IChatAgentHistoryEntry[] = chatRequests
782
.map(req => ({
783
request: {
784
sessionId: session,
785
requestId: req.id,
786
agentId: req.response?.agent?.id ?? '',
787
message: req.message.text,
788
command: req.response?.slashCommand?.name,
789
variables: req.variableData,
790
location: ChatAgentLocation.Panel,
791
editedFileEvents: req.editedFileEvents,
792
},
793
response: toChatHistoryContent(req.response!.response.value),
794
result: req.response?.result ?? {}
795
}));
796
797
// TODO: Determine a cutoff point where we stop including earlier history
798
// For example, if the user has already delegated to a coding agent once,
799
// prefer the conversation afterwards.
800
801
summary += 'The following is a snapshot of a chat conversation between a user and an AI coding assistant. Prioritize later messages in the conversation.';
802
summary += this.extractChatTurns(historyEntries);
803
summary += await chatAgentService.getChatSummary(defaultAgent.id, historyEntries, CancellationToken.None);
804
}
805
806
chatModel.acceptResponseProgress(addedRequest, {
807
kind: 'progressMessage',
808
content: new MarkdownString(
809
localize('creatingRemoteJob', "Delegating to coding agent"),
810
CreateRemoteAgentJobAction.markdownStringTrustedOptions
811
)
812
});
813
814
const isChatSessionsEnabled = configurationService.getValue<boolean>(ChatConfiguration.UseChatSessionsForCloudButton);
815
if (isChatSessionsEnabled) {
816
await this.createWithChatSessions(chatSessionsService, quickPickService, editorService, chatModel, addedRequest, userPrompt, summary);
817
} else {
818
await this.createWithLegacy(remoteCodingAgentService, commandService, quickPickService, chatModel, addedRequest, widget, userPrompt, summary);
819
}
820
821
chatModel.setResponse(addedRequest, {});
822
chatModel.completeResponse(addedRequest);
823
} finally {
824
remoteJobCreatingKey.set(false);
825
}
826
}
827
}
828
829
export class ChatSubmitWithCodebaseAction extends Action2 {
830
static readonly ID = 'workbench.action.chat.submitWithCodebase';
831
832
constructor() {
833
const precondition = ContextKeyExpr.and(
834
// if the input has prompt instructions attached, allow submitting requests even
835
// without text present - having instructions is enough context for a request
836
ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile),
837
whenNotInProgress,
838
);
839
840
super({
841
id: ChatSubmitWithCodebaseAction.ID,
842
title: localize2('actions.chat.submitWithCodebase', "Send with {0}", `${chatVariableLeader}codebase`),
843
precondition,
844
menu: {
845
id: MenuId.ChatExecuteSecondary,
846
group: 'group_1',
847
order: 3,
848
when: ContextKeyExpr.and(
849
ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Panel),
850
ChatContextKeys.lockedToCodingAgent.negate()
851
),
852
},
853
keybinding: {
854
when: ChatContextKeys.inChatInput,
855
primary: KeyMod.CtrlCmd | KeyCode.Enter,
856
weight: KeybindingWeight.EditorContrib
857
},
858
});
859
}
860
861
run(accessor: ServicesAccessor, ...args: any[]) {
862
const context: IChatExecuteActionContext | undefined = args[0];
863
864
const widgetService = accessor.get(IChatWidgetService);
865
const widget = context?.widget ?? widgetService.lastFocusedWidget;
866
if (!widget) {
867
return;
868
}
869
870
const languageModelToolsService = accessor.get(ILanguageModelToolsService);
871
const codebaseTool = languageModelToolsService.getToolByName('codebase');
872
if (!codebaseTool) {
873
return;
874
}
875
876
widget.input.attachmentModel.addContext({
877
id: codebaseTool.id,
878
name: codebaseTool.displayName ?? '',
879
fullName: codebaseTool.displayName ?? '',
880
value: undefined,
881
icon: ThemeIcon.isThemeIcon(codebaseTool.icon) ? codebaseTool.icon : undefined,
882
kind: 'tool'
883
});
884
widget.acceptInput();
885
}
886
}
887
888
class SendToNewChatAction extends Action2 {
889
constructor() {
890
const precondition = ContextKeyExpr.and(
891
// if the input has prompt instructions attached, allow submitting requests even
892
// without text present - having instructions is enough context for a request
893
ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile),
894
whenNotInProgress,
895
);
896
897
super({
898
id: 'workbench.action.chat.sendToNewChat',
899
title: localize2('chat.newChat.label', "Send to New Chat"),
900
precondition,
901
category: CHAT_CATEGORY,
902
f1: false,
903
menu: {
904
id: MenuId.ChatExecuteSecondary,
905
group: 'group_2',
906
when: ContextKeyExpr.and(
907
ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Panel),
908
ChatContextKeys.lockedToCodingAgent.negate()
909
)
910
},
911
keybinding: {
912
weight: KeybindingWeight.WorkbenchContrib,
913
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter,
914
when: ChatContextKeys.inChatInput,
915
}
916
});
917
}
918
919
async run(accessor: ServicesAccessor, ...args: any[]) {
920
const context: IChatExecuteActionContext | undefined = args[0];
921
922
const widgetService = accessor.get(IChatWidgetService);
923
const dialogService = accessor.get(IDialogService);
924
const widget = context?.widget ?? widgetService.lastFocusedWidget;
925
if (!widget) {
926
return;
927
}
928
929
const editingSession = widget.viewModel?.model.editingSession;
930
if (editingSession) {
931
if (!(await handleCurrentEditingSession(editingSession, undefined, dialogService))) {
932
return;
933
}
934
}
935
936
widget.clear();
937
await widget.waitForReady();
938
widget.acceptInput(context?.inputValue);
939
}
940
}
941
942
export const CancelChatActionId = 'workbench.action.chat.cancel';
943
export class CancelAction extends Action2 {
944
static readonly ID = CancelChatActionId;
945
constructor() {
946
super({
947
id: CancelAction.ID,
948
title: localize2('interactive.cancel.label', "Cancel"),
949
f1: false,
950
category: CHAT_CATEGORY,
951
icon: Codicon.stopCircle,
952
menu: [{
953
id: MenuId.ChatExecute,
954
when: ContextKeyExpr.and(
955
ChatContextKeys.requestInProgress,
956
ChatContextKeys.remoteJobCreating.negate()
957
),
958
order: 4,
959
group: 'navigation',
960
},
961
],
962
keybinding: {
963
weight: KeybindingWeight.WorkbenchContrib,
964
primary: KeyMod.CtrlCmd | KeyCode.Escape,
965
win: { primary: KeyMod.Alt | KeyCode.Backspace },
966
}
967
});
968
}
969
970
run(accessor: ServicesAccessor, ...args: any[]) {
971
const context: IChatExecuteActionContext | undefined = args[0];
972
const widgetService = accessor.get(IChatWidgetService);
973
const widget = context?.widget ?? widgetService.lastFocusedWidget;
974
if (!widget) {
975
return;
976
}
977
978
const chatService = accessor.get(IChatService);
979
if (widget.viewModel) {
980
chatService.cancelCurrentRequestForSession(widget.viewModel.sessionId);
981
}
982
}
983
}
984
985
export const CancelChatEditId = 'workbench.edit.chat.cancel';
986
export class CancelEdit extends Action2 {
987
static readonly ID = CancelChatEditId;
988
constructor() {
989
super({
990
id: CancelEdit.ID,
991
title: localize2('interactive.cancelEdit.label', "Cancel Edit"),
992
f1: false,
993
category: CHAT_CATEGORY,
994
icon: Codicon.x,
995
menu: [
996
{
997
id: MenuId.ChatMessageTitle,
998
group: 'navigation',
999
order: 1,
1000
when: ContextKeyExpr.and(ChatContextKeys.isRequest, ChatContextKeys.currentlyEditing, ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input'))
1001
}
1002
],
1003
keybinding: {
1004
primary: KeyCode.Escape,
1005
when: ContextKeyExpr.and(ChatContextKeys.inChatInput,
1006
EditorContextKeys.hoverVisible.toNegated(),
1007
EditorContextKeys.hasNonEmptySelection.toNegated(),
1008
EditorContextKeys.hasMultipleSelections.toNegated(),
1009
ContextKeyExpr.or(ChatContextKeys.currentlyEditing, ChatContextKeys.currentlyEditingInput)),
1010
weight: KeybindingWeight.EditorContrib - 5
1011
}
1012
});
1013
}
1014
1015
run(accessor: ServicesAccessor, ...args: any[]) {
1016
const context: IChatExecuteActionContext | undefined = args[0];
1017
1018
const widgetService = accessor.get(IChatWidgetService);
1019
const widget = context?.widget ?? widgetService.lastFocusedWidget;
1020
if (!widget) {
1021
return;
1022
}
1023
widget.finishedEditing();
1024
}
1025
}
1026
1027
1028
export function registerChatExecuteActions() {
1029
registerAction2(ChatSubmitAction);
1030
registerAction2(ChatEditingSessionSubmitAction);
1031
registerAction2(SubmitWithoutDispatchingAction);
1032
registerAction2(CancelAction);
1033
registerAction2(SendToNewChatAction);
1034
registerAction2(ChatSubmitWithCodebaseAction);
1035
registerAction2(CreateRemoteAgentJobAction);
1036
registerAction2(ToggleChatModeAction);
1037
registerAction2(SwitchToNextModelAction);
1038
registerAction2(OpenModelPickerAction);
1039
registerAction2(OpenModePickerAction);
1040
registerAction2(ChangeChatModelAction);
1041
registerAction2(CancelEdit);
1042
}
1043
1044