Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts
4780 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 { h } from '../../../../../base/browser/dom.js';
8
import { Disposable, IDisposable, markAsSingleton } from '../../../../../base/common/lifecycle.js';
9
import { Schemas } from '../../../../../base/common/network.js';
10
import { basename } from '../../../../../base/common/resources.js';
11
import { ThemeIcon } from '../../../../../base/common/themables.js';
12
import { URI } from '../../../../../base/common/uri.js';
13
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
14
import { isITextModel } from '../../../../../editor/common/model.js';
15
import { localize, localize2 } from '../../../../../nls.js';
16
import { ActionWidgetDropdownActionViewItem } from '../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js';
17
import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';
18
import { Action2, MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js';
19
import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';
20
import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js';
21
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
22
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
23
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
24
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
25
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
26
import { IWorkbenchContribution } from '../../../../common/contributions.js';
27
import { ResourceContextKey } from '../../../../common/contextkeys.js';
28
import { IEditorService } from '../../../../services/editor/common/editorService.js';
29
import { IChatAgentService } from '../../common/participants/chatAgents.js';
30
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
31
import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';
32
import { ChatModel } from '../../common/model/chatModel.js';
33
import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js';
34
import { IChatService } from '../../common/chatService/chatService.js';
35
import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';
36
import { ChatAgentLocation } from '../../common/constants.js';
37
import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js';
38
import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../agentSessions/agentSessions.js';
39
import { IChatWidgetService } from '../chat.js';
40
import { ctxHasEditorModification } from '../chatEditing/chatEditingEditorContextKeys.js';
41
import { CHAT_SETUP_ACTION_ID } from './chatActions.js';
42
import { PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js';
43
44
export const enum ActionLocation {
45
ChatWidget = 'chatWidget',
46
Editor = 'editor'
47
}
48
49
export class ContinueChatInSessionAction extends Action2 {
50
51
static readonly ID = 'workbench.action.chat.continueChatInSession';
52
53
constructor() {
54
super({
55
id: ContinueChatInSessionAction.ID,
56
title: localize2('continueChatInSession', "Continue Chat in..."),
57
tooltip: localize('continueChatInSession', "Continue Chat in..."),
58
precondition: ContextKeyExpr.and(
59
ChatContextKeys.enabled,
60
ChatContextKeys.requestInProgress.negate(),
61
ChatContextKeys.remoteJobCreating.negate(),
62
ChatContextKeys.hasCanDelegateProviders,
63
),
64
menu: [{
65
id: MenuId.ChatExecute,
66
group: 'navigation',
67
order: 3.4,
68
when: ContextKeyExpr.and(
69
ChatContextKeys.lockedToCodingAgent.negate(),
70
ChatContextKeys.hasCanDelegateProviders,
71
),
72
},
73
{
74
id: MenuId.EditorContent,
75
group: 'continueIn',
76
when: ContextKeyExpr.and(
77
ContextKeyExpr.equals(ResourceContextKey.Scheme.key, Schemas.untitled),
78
ContextKeyExpr.equals(ResourceContextKey.LangId.key, PROMPT_LANGUAGE_ID),
79
ContextKeyExpr.notEquals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),
80
ctxHasEditorModification.negate(),
81
ChatContextKeys.hasCanDelegateProviders,
82
),
83
}
84
]
85
});
86
}
87
88
override async run(): Promise<void> {
89
// Handled by a custom action item
90
}
91
}
92
export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionViewItem {
93
constructor(
94
action: MenuItemAction,
95
private readonly location: ActionLocation,
96
@IActionWidgetService actionWidgetService: IActionWidgetService,
97
@IContextKeyService private readonly contextKeyService: IContextKeyService,
98
@IKeybindingService keybindingService: IKeybindingService,
99
@IChatSessionsService chatSessionsService: IChatSessionsService,
100
@IInstantiationService instantiationService: IInstantiationService,
101
@IOpenerService openerService: IOpenerService
102
) {
103
super(action, {
104
actionProvider: ChatContinueInSessionActionItem.actionProvider(chatSessionsService, instantiationService, location),
105
actionBarActions: ChatContinueInSessionActionItem.getActionBarActions(openerService)
106
}, actionWidgetService, keybindingService, contextKeyService);
107
}
108
109
protected static getActionBarActions(openerService: IOpenerService) {
110
const learnMoreUrl = 'https://aka.ms/vscode-continue-chat-in';
111
return [{
112
id: 'workbench.action.chat.continueChatInSession.learnMore',
113
label: localize('chat.learnMore', "Learn More"),
114
tooltip: localize('chat.learnMore', "Learn More"),
115
class: undefined,
116
enabled: true,
117
run: async () => {
118
await openerService.open(URI.parse(learnMoreUrl));
119
}
120
}];
121
}
122
123
private static actionProvider(chatSessionsService: IChatSessionsService, instantiationService: IInstantiationService, location: ActionLocation): IActionWidgetDropdownActionProvider {
124
return {
125
getActions: () => {
126
const actions: IActionWidgetDropdownAction[] = [];
127
const contributions = chatSessionsService.getAllChatSessionContributions();
128
129
// Continue in Background
130
const backgroundContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Background);
131
if (backgroundContrib && backgroundContrib.canDelegate !== false) {
132
actions.push(this.toAction(AgentSessionProviders.Background, backgroundContrib, instantiationService, location));
133
}
134
135
// Continue in Cloud
136
const cloudContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Cloud);
137
if (cloudContrib && cloudContrib.canDelegate !== false) {
138
actions.push(this.toAction(AgentSessionProviders.Cloud, cloudContrib, instantiationService, location));
139
}
140
141
// Offer actions to enter setup if we have no contributions
142
if (actions.length === 0) {
143
actions.push(this.toSetupAction(AgentSessionProviders.Background, instantiationService));
144
actions.push(this.toSetupAction(AgentSessionProviders.Cloud, instantiationService));
145
}
146
147
return actions;
148
}
149
};
150
}
151
152
private static toAction(provider: AgentSessionProviders, contrib: IChatSessionsExtensionPoint, instantiationService: IInstantiationService, location: ActionLocation): IActionWidgetDropdownAction {
153
return {
154
id: contrib.type,
155
enabled: true,
156
icon: getAgentSessionProviderIcon(provider),
157
class: undefined,
158
description: `@${contrib.name}`,
159
label: getAgentSessionProviderName(provider),
160
tooltip: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)),
161
category: { label: localize('continueIn', "Continue In"), order: 0, showHeader: true },
162
run: () => instantiationService.invokeFunction(accessor => {
163
if (location === ActionLocation.Editor) {
164
return new CreateRemoteAgentJobFromEditorAction().run(accessor, contrib);
165
}
166
return new CreateRemoteAgentJobAction().run(accessor, contrib);
167
})
168
};
169
}
170
171
private static toSetupAction(provider: AgentSessionProviders, instantiationService: IInstantiationService): IActionWidgetDropdownAction {
172
return {
173
id: provider,
174
enabled: true,
175
icon: getAgentSessionProviderIcon(provider),
176
class: undefined,
177
label: getAgentSessionProviderName(provider),
178
tooltip: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)),
179
category: { label: localize('continueIn', "Continue In"), order: 0, showHeader: true },
180
run: () => instantiationService.invokeFunction(accessor => {
181
const commandService = accessor.get(ICommandService);
182
return commandService.executeCommand(CHAT_SETUP_ACTION_ID);
183
})
184
};
185
}
186
187
protected override renderLabel(element: HTMLElement): IDisposable | null {
188
if (this.location === ActionLocation.Editor) {
189
const view = h('span.action-widget-delegate-label', [
190
h('span', { className: ThemeIcon.asClassName(Codicon.forward) }),
191
h('span', [localize('continueInEllipsis', "Continue in...")])
192
]);
193
element.appendChild(view.root);
194
return null;
195
} else {
196
const icon = this.contextKeyService.contextMatchesRules(ChatContextKeys.remoteJobCreating) ? Codicon.sync : Codicon.forward;
197
element.classList.add(...ThemeIcon.asClassNameArray(icon));
198
return super.renderLabel(element);
199
}
200
}
201
}
202
203
const NEW_CHAT_SESSION_ACTION_ID = 'workbench.action.chat.openNewSessionEditor';
204
205
class CreateRemoteAgentJobAction {
206
constructor() { }
207
208
private openUntitledEditor(commandService: ICommandService, continuationTarget: IChatSessionsExtensionPoint) {
209
commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${continuationTarget.type}`);
210
}
211
212
async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint) {
213
const contextKeyService = accessor.get(IContextKeyService);
214
const commandService = accessor.get(ICommandService);
215
const widgetService = accessor.get(IChatWidgetService);
216
const chatAgentService = accessor.get(IChatAgentService);
217
const chatService = accessor.get(IChatService);
218
const editorService = accessor.get(IEditorService);
219
220
const remoteJobCreatingKey = ChatContextKeys.remoteJobCreating.bindTo(contextKeyService);
221
222
try {
223
remoteJobCreatingKey.set(true);
224
225
const widget = widgetService.lastFocusedWidget;
226
if (!widget || !widget.viewModel) {
227
return this.openUntitledEditor(commandService, continuationTarget);
228
}
229
230
// todo@connor4312: remove 'as' cast
231
const chatModel = widget.viewModel.model as ChatModel;
232
if (!chatModel) {
233
return;
234
}
235
236
const sessionResource = widget.viewModel.sessionResource;
237
const chatRequests = chatModel.getRequests();
238
let userPrompt = widget.getInput();
239
if (!userPrompt) {
240
if (!chatRequests.length) {
241
return this.openUntitledEditor(commandService, continuationTarget);
242
}
243
userPrompt = 'implement this.';
244
}
245
246
const attachedContext = widget.input.getAttachedAndImplicitContext(sessionResource);
247
widget.input.acceptInput(true);
248
249
// For inline editor mode, add selection or cursor information
250
if (widget.location === ChatAgentLocation.EditorInline) {
251
const activeEditor = editorService.activeTextEditorControl;
252
if (activeEditor) {
253
const model = activeEditor.getModel();
254
let activeEditorUri: URI | undefined = undefined;
255
if (model && isITextModel(model)) {
256
activeEditorUri = model.uri as URI;
257
}
258
const selection = activeEditor.getSelection();
259
if (activeEditorUri && selection) {
260
attachedContext.add({
261
kind: 'file',
262
id: 'vscode.implicit.selection',
263
name: basename(activeEditorUri),
264
value: {
265
uri: activeEditorUri,
266
range: selection
267
},
268
});
269
}
270
}
271
}
272
273
const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat);
274
const instantiationService = accessor.get(IInstantiationService);
275
const requestParser = instantiationService.createInstance(ChatRequestParser);
276
const continuationTargetType = continuationTarget.type;
277
278
// Add the request to the model first
279
const parsedRequest = requestParser.parseChatRequest(sessionResource, userPrompt, ChatAgentLocation.Chat);
280
const addedRequest = chatModel.addRequest(
281
parsedRequest,
282
{ variables: attachedContext.asArray() },
283
0,
284
undefined,
285
defaultAgent
286
);
287
288
await chatService.removeRequest(sessionResource, addedRequest.id);
289
const requestData = await chatService.sendRequest(sessionResource, userPrompt, {
290
agentIdSilent: continuationTargetType,
291
attachedContext: attachedContext.asArray(),
292
userSelectedModelId: widget.input.currentLanguageModel,
293
...widget.getModeRequestOptions()
294
});
295
296
if (requestData) {
297
await widget.handleDelegationExitIfNeeded(defaultAgent, requestData.agent);
298
}
299
} catch (e) {
300
console.error('Error creating remote coding agent job', e);
301
throw e;
302
} finally {
303
remoteJobCreatingKey.set(false);
304
}
305
}
306
}
307
308
class CreateRemoteAgentJobFromEditorAction {
309
constructor() { }
310
311
async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint) {
312
313
try {
314
const editorService = accessor.get(IEditorService);
315
const activeEditor = editorService.activeTextEditorControl;
316
const commandService = accessor.get(ICommandService);
317
318
if (!activeEditor) {
319
return;
320
}
321
const model = activeEditor.getModel();
322
if (!model || !isITextModel(model)) {
323
return;
324
}
325
const uri = model.uri;
326
const attachedContext = [toPromptFileVariableEntry(uri, PromptFileVariableKind.PromptFile, undefined, false, [])];
327
const prompt = `Follow instructions in [${basename(uri)}](${uri.toString()}).`;
328
await commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${continuationTarget.type}`, { prompt, attachedContext });
329
} catch (e) {
330
console.error('Error creating remote agent job from editor', e);
331
throw e;
332
}
333
}
334
}
335
336
export class ContinueChatInSessionActionRendering extends Disposable implements IWorkbenchContribution {
337
338
static readonly ID = 'chat.continueChatInSessionActionRendering';
339
340
constructor(
341
@IActionViewItemService actionViewItemService: IActionViewItemService,
342
@IInstantiationService instantiationService: IInstantiationService,
343
) {
344
super();
345
const disposable = actionViewItemService.register(MenuId.EditorContent, ContinueChatInSessionAction.ID, (action, options, instantiationService2) => {
346
if (!(action instanceof MenuItemAction)) {
347
return undefined;
348
}
349
return instantiationService.createInstance(ChatContinueInSessionActionItem, action, ActionLocation.Editor);
350
});
351
markAsSingleton(disposable);
352
}
353
}
354
355