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