Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts
5281 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 { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js';
7
import { timeout } from '../../../../../base/common/async.js';
8
import { CancellationToken } from '../../../../../base/common/cancellation.js';
9
import { Codicon } from '../../../../../base/common/codicons.js';
10
import { toErrorMessage } from '../../../../../base/common/errorMessage.js';
11
import { Emitter, Event } from '../../../../../base/common/event.js';
12
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
13
import { Lazy } from '../../../../../base/common/lazy.js';
14
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
15
import { URI } from '../../../../../base/common/uri.js';
16
import { localize, localize2 } from '../../../../../nls.js';
17
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
18
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
19
import { ILogService } from '../../../../../platform/log/common/log.js';
20
import product from '../../../../../platform/product/common/product.js';
21
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
22
import { IWorkspaceTrustManagementService } from '../../../../../platform/workspace/common/workspaceTrust.js';
23
import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js';
24
import { nullExtensionDescription } from '../../../../services/extensions/common/extensions.js';
25
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../../common/tools/languageModelToolsService.js';
26
import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../../common/participants/chatAgents.js';
27
import { ChatEntitlement, ChatEntitlementContext, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
28
import { ChatModel, ChatRequestModel, IChatRequestModel, IChatRequestVariableData } from '../../common/model/chatModel.js';
29
import { ChatMode } from '../../common/chatModes.js';
30
import { ChatRequestAgentPart, ChatRequestToolPart } from '../../common/requestParser/chatParserTypes.js';
31
import { IChatProgress, IChatService } from '../../common/chatService/chatService.js';
32
import { IChatRequestToolEntry } from '../../common/attachments/chatVariableEntries.js';
33
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
34
import { ILanguageModelsService } from '../../common/languageModels.js';
35
import { CHAT_OPEN_ACTION_ID, CHAT_SETUP_ACTION_ID } from '../actions/chatActions.js';
36
import { ChatViewId, IChatWidgetService } from '../chat.js';
37
import { IViewsService } from '../../../../services/views/common/viewsService.js';
38
import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js';
39
import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';
40
import { CodeAction, CodeActionList, Command, NewSymbolName, NewSymbolNameTriggerKind } from '../../../../../editor/common/languages.js';
41
import { ITextModel } from '../../../../../editor/common/model.js';
42
import { IRange, Range } from '../../../../../editor/common/core/range.js';
43
import { ISelection, Selection } from '../../../../../editor/common/core/selection.js';
44
import { ResourceMap } from '../../../../../base/common/map.js';
45
import { CodeActionKind } from '../../../../../editor/contrib/codeAction/common/types.js';
46
import { ACTION_START as INLINE_CHAT_START } from '../../../inlineChat/common/inlineChat.js';
47
import { IPosition } from '../../../../../editor/common/core/position.js';
48
import { IMarker, IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js';
49
import { ChatSetupController } from './chatSetupController.js';
50
import { ChatSetupAnonymous, ChatSetupStep, IChatSetupResult } from './chatSetup.js';
51
import { ChatSetup } from './chatSetupRunner.js';
52
import { chatViewsWelcomeRegistry } from '../viewsWelcome/chatViewsWelcome.js';
53
import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';
54
import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js';
55
import { IHostService } from '../../../../services/host/browser/host.js';
56
57
const defaultChat = {
58
extensionId: product.defaultChatAgent?.extensionId ?? '',
59
chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '',
60
provider: product.defaultChatAgent?.provider ?? { default: { id: '', name: '' }, enterprise: { id: '', name: '' }, apple: { id: '', name: '' }, google: { id: '', name: '' } },
61
outputChannelId: product.defaultChatAgent?.chatExtensionOutputId ?? '',
62
};
63
64
const ToolsAgentContextKey = ContextKeyExpr.and(
65
ContextKeyExpr.equals(`config.${ChatConfiguration.AgentEnabled}`, true),
66
ContextKeyExpr.not(`previewFeaturesDisabled`) // Set by extension
67
);
68
69
export class SetupAgent extends Disposable implements IChatAgentImplementation {
70
71
static registerDefaultAgents(instantiationService: IInstantiationService, location: ChatAgentLocation, mode: ChatModeKind, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): { agent: SetupAgent; disposable: IDisposable } {
72
return instantiationService.invokeFunction(accessor => {
73
const chatAgentService = accessor.get(IChatAgentService);
74
75
let description;
76
if (mode === ChatModeKind.Ask) {
77
description = ChatMode.Ask.description.get();
78
} else if (mode === ChatModeKind.Edit) {
79
description = ChatMode.Edit.description.get();
80
} else {
81
description = ChatMode.Agent.description.get();
82
}
83
84
let id: string;
85
switch (location) {
86
case ChatAgentLocation.Chat:
87
if (mode === ChatModeKind.Ask) {
88
id = 'setup.chat';
89
} else if (mode === ChatModeKind.Edit) {
90
id = 'setup.edits';
91
} else {
92
id = 'setup.agent';
93
}
94
break;
95
case ChatAgentLocation.Terminal:
96
id = 'setup.terminal';
97
break;
98
case ChatAgentLocation.EditorInline:
99
id = 'setup.editor';
100
break;
101
case ChatAgentLocation.Notebook:
102
id = 'setup.notebook';
103
break;
104
}
105
106
return SetupAgent.doRegisterAgent(instantiationService, chatAgentService, id, `${defaultChat.provider.default.name} Copilot` /* Do NOT change, this hides the username altogether in Chat */, true, description, location, mode, context, controller);
107
});
108
}
109
110
static registerBuiltInAgents(instantiationService: IInstantiationService, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): IDisposable {
111
return instantiationService.invokeFunction(accessor => {
112
const chatAgentService = accessor.get(IChatAgentService);
113
114
const disposables = new DisposableStore();
115
116
// Register VSCode agent
117
const { disposable: vscodeDisposable } = SetupAgent.doRegisterAgent(instantiationService, chatAgentService, 'setup.vscode', 'vscode', false, localize2('vscodeAgentDescription', "Ask questions about VS Code").value, ChatAgentLocation.Chat, ChatModeKind.Agent, context, controller);
118
disposables.add(vscodeDisposable);
119
120
// Register workspace agent
121
const { disposable: workspaceDisposable } = SetupAgent.doRegisterAgent(instantiationService, chatAgentService, 'setup.workspace', 'workspace', false, localize2('workspaceAgentDescription', "Ask about your workspace").value, ChatAgentLocation.Chat, ChatModeKind.Agent, context, controller);
122
disposables.add(workspaceDisposable);
123
124
// Register terminal agent
125
const { disposable: terminalDisposable } = SetupAgent.doRegisterAgent(instantiationService, chatAgentService, 'setup.terminal.agent', 'terminal', false, localize2('terminalAgentDescription', "Ask how to do something in the terminal").value, ChatAgentLocation.Chat, ChatModeKind.Agent, context, controller);
126
disposables.add(terminalDisposable);
127
128
// Register tools
129
disposables.add(SetupTool.registerTool(instantiationService, {
130
id: 'setup_tools_createNewWorkspace',
131
source: ToolDataSource.Internal,
132
icon: Codicon.newFolder,
133
displayName: localize('setupToolDisplayName', "New Workspace"),
134
modelDescription: 'Scaffold a new workspace in VS Code',
135
userDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"),
136
canBeReferencedInPrompt: true,
137
toolReferenceName: 'new',
138
when: ContextKeyExpr.true(),
139
}));
140
141
return disposables;
142
});
143
}
144
145
private static doRegisterAgent(instantiationService: IInstantiationService, chatAgentService: IChatAgentService, id: string, name: string, isDefault: boolean, description: string, location: ChatAgentLocation, mode: ChatModeKind, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): { agent: SetupAgent; disposable: IDisposable } {
146
const disposables = new DisposableStore();
147
disposables.add(chatAgentService.registerAgent(id, {
148
id,
149
name,
150
isDefault,
151
isCore: true,
152
modes: [mode],
153
when: mode === ChatModeKind.Agent ? ToolsAgentContextKey?.serialize() : undefined,
154
slashCommands: [],
155
disambiguation: [],
156
locations: [location],
157
metadata: { helpTextPrefix: SetupAgent.SETUP_NEEDED_MESSAGE },
158
description,
159
extensionId: nullExtensionDescription.identifier,
160
extensionVersion: undefined,
161
extensionDisplayName: nullExtensionDescription.name,
162
extensionPublisherId: nullExtensionDescription.publisher
163
}));
164
165
const agent = disposables.add(instantiationService.createInstance(SetupAgent, context, controller, location));
166
disposables.add(chatAgentService.registerAgentImplementation(id, agent));
167
if (mode === ChatModeKind.Agent) {
168
chatAgentService.updateAgent(id, { themeIcon: Codicon.tools });
169
}
170
171
return { agent, disposable: disposables };
172
}
173
174
private static readonly SETUP_NEEDED_MESSAGE = new MarkdownString(localize('settingUpCopilotNeeded', "You need to set up GitHub Copilot and be signed in to use Chat."));
175
private static readonly TRUST_NEEDED_MESSAGE = new MarkdownString(localize('trustNeeded', "You need to trust this workspace to use Chat."));
176
177
private static readonly CHAT_RETRY_COMMAND_ID = 'workbench.action.chat.retrySetup';
178
179
private readonly _onUnresolvableError = this._register(new Emitter<void>());
180
readonly onUnresolvableError = this._onUnresolvableError.event;
181
182
private readonly pendingForwardedRequests = new ResourceMap<Promise<void>>();
183
184
constructor(
185
private readonly context: ChatEntitlementContext,
186
private readonly controller: Lazy<ChatSetupController>,
187
private readonly location: ChatAgentLocation,
188
@IInstantiationService private readonly instantiationService: IInstantiationService,
189
@ILogService private readonly logService: ILogService,
190
@ITelemetryService private readonly telemetryService: ITelemetryService,
191
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
192
@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,
193
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,
194
@IViewsService private readonly viewsService: IViewsService,
195
@IContextKeyService private readonly contextKeyService: IContextKeyService,
196
) {
197
super();
198
199
this.registerCommands();
200
}
201
202
private registerCommands(): void {
203
204
// Retry chat command
205
this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_RETRY_COMMAND_ID, async (accessor, sessionResource: URI) => {
206
const hostService = accessor.get(IHostService);
207
const chatWidgetService = accessor.get(IChatWidgetService);
208
209
const widget = chatWidgetService.getWidgetBySessionResource(sessionResource);
210
await widget?.clear();
211
212
hostService.reload();
213
}));
214
}
215
216
async invoke(request: IChatAgentRequest, progress: (parts: IChatProgress[]) => void): Promise<IChatAgentResult> {
217
return this.instantiationService.invokeFunction(async accessor /* using accessor for lazy loading */ => {
218
const chatService = accessor.get(IChatService);
219
const languageModelsService = accessor.get(ILanguageModelsService);
220
const chatWidgetService = accessor.get(IChatWidgetService);
221
const chatAgentService = accessor.get(IChatAgentService);
222
const languageModelToolsService = accessor.get(ILanguageModelToolsService);
223
const defaultAccountService = accessor.get(IDefaultAccountService);
224
225
return this.doInvoke(request, part => progress([part]), chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService, defaultAccountService);
226
});
227
}
228
229
private async doInvoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService, defaultAccountService: IDefaultAccountService): Promise<IChatAgentResult> {
230
if (
231
!this.context.state.installed || // Extension not installed: run setup to install
232
this.context.state.disabled || // Extension disabled: run setup to enable
233
this.context.state.untrusted || // Workspace untrusted: run setup to ask for trust
234
this.context.state.entitlement === ChatEntitlement.Available || // Entitlement available: run setup to sign up
235
(
236
this.context.state.entitlement === ChatEntitlement.Unknown && // Entitlement unknown: run setup to sign in / sign up
237
!this.chatEntitlementService.anonymous // unless anonymous access is enabled
238
)
239
) {
240
return this.doInvokeWithSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService, defaultAccountService);
241
}
242
243
return this.doInvokeWithoutSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService);
244
}
245
246
private async doInvokeWithoutSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise<IChatAgentResult> {
247
const requestModel = chatWidgetService.getWidgetBySessionResource(request.sessionResource)?.viewModel?.model.getRequests().at(-1);
248
if (!requestModel) {
249
this.logService.error('[chat setup] Request model not found, cannot redispatch request.');
250
return {}; // this should not happen
251
}
252
253
progress({
254
kind: 'progressMessage',
255
content: new MarkdownString(localize('waitingChat', "Getting chat ready...")),
256
});
257
258
await this.forwardRequestToChat(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
259
260
return {};
261
}
262
263
private async forwardRequestToChat(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {
264
try {
265
await this.doForwardRequestToChat(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
266
} catch (error) {
267
this.logService.error('[chat setup] Failed to forward request to chat', error);
268
269
progress({
270
kind: 'warning',
271
content: new MarkdownString(localize('copilotUnavailableWarning', "Failed to get a response. Please try again."))
272
});
273
}
274
}
275
276
private async doForwardRequestToChat(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {
277
if (this.pendingForwardedRequests.has(requestModel.session.sessionResource)) {
278
throw new Error('Request already in progress');
279
}
280
281
const forwardRequest = this.doForwardRequestToChatWhenReady(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
282
this.pendingForwardedRequests.set(requestModel.session.sessionResource, forwardRequest);
283
284
try {
285
await forwardRequest;
286
} finally {
287
this.pendingForwardedRequests.delete(requestModel.session.sessionResource);
288
}
289
}
290
291
private async doForwardRequestToChatWhenReady(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {
292
const widget = chatWidgetService.getWidgetBySessionResource(requestModel.session.sessionResource);
293
const modeInfo = widget?.input.currentModeInfo;
294
295
// We need a signal to know when we can resend the request to
296
// Chat. Waiting for the registration of the agent is not
297
// enough, we also need a language/tools model to be available.
298
299
let agentActivated = false;
300
let agentReady = false;
301
let languageModelReady = false;
302
let toolsModelReady = false;
303
304
const whenAgentActivated = this.whenAgentActivated(chatService).then(() => agentActivated = true);
305
const whenAgentReady = this.whenAgentReady(chatAgentService, modeInfo?.kind)?.then(() => agentReady = true);
306
if (!whenAgentReady) {
307
agentReady = true;
308
}
309
const whenLanguageModelReady = this.whenLanguageModelReady(languageModelsService, requestModel.modelId)?.then(() => languageModelReady = true);
310
if (!whenLanguageModelReady) {
311
languageModelReady = true;
312
}
313
const whenToolsModelReady = this.whenToolsModelReady(languageModelToolsService, requestModel)?.then(() => toolsModelReady = true);
314
if (!whenToolsModelReady) {
315
toolsModelReady = true;
316
}
317
318
if (whenLanguageModelReady instanceof Promise || whenAgentReady instanceof Promise || whenToolsModelReady instanceof Promise) {
319
const timeoutHandle = setTimeout(() => {
320
progress({
321
kind: 'progressMessage',
322
content: new MarkdownString(localize('waitingChat2', "Chat is almost ready...")),
323
});
324
}, 10000);
325
326
const disposables = new DisposableStore();
327
disposables.add(toDisposable(() => clearTimeout(timeoutHandle)));
328
try {
329
const ready = await Promise.race([
330
timeout(this.environmentService.remoteAuthority ? 60000 /* increase for remote scenarios */ : 20000).then(() => 'timedout'),
331
this.whenPanelAgentHasGuidance(disposables).then(() => 'panelGuidance'),
332
Promise.allSettled([
333
whenAgentActivated,
334
whenAgentReady,
335
whenLanguageModelReady,
336
whenToolsModelReady
337
])
338
]);
339
340
if (ready === 'panelGuidance') {
341
const warningMessage = localize('chatTookLongWarningExtension', "Please try again.");
342
343
progress({
344
kind: 'markdownContent',
345
content: new MarkdownString(warningMessage)
346
});
347
348
// This means Chat is unhealthy and we cannot retry the
349
// request. Signal this to the outside via an event.
350
this._onUnresolvableError.fire();
351
return;
352
}
353
354
if (ready === 'timedout') {
355
let warningMessage: string;
356
if (this.chatEntitlementService.anonymous) {
357
warningMessage = localize('chatTookLongWarningAnonymous', "Chat took too long to get ready. Please ensure that the extension `{0}` is installed and enabled. Click restart to try again if this issue persists.", defaultChat.chatExtensionId);
358
} else {
359
warningMessage = localize('chatTookLongWarning', "Chat took too long to get ready. Please ensure you are signed in to {0} and that the extension `{1}` is installed and enabled. Click restart to try again if this issue persists.", defaultChat.provider.default.name, defaultChat.chatExtensionId);
360
}
361
362
// Compute language model diagnostic info
363
const languageModelIds = languageModelsService.getLanguageModelIds();
364
let languageModelDefaultCount = 0;
365
for (const id of languageModelIds) {
366
const model = languageModelsService.lookupLanguageModel(id);
367
if (model?.isDefaultForLocation[ChatAgentLocation.Chat]) {
368
languageModelDefaultCount++;
369
}
370
}
371
372
// Compute agent diagnostic info
373
const defaultAgent = chatAgentService.getDefaultAgent(this.location, modeInfo?.kind);
374
const agentHasDefault = !!defaultAgent;
375
const agentDefaultIsCore = defaultAgent?.isCore ?? false;
376
const contributedDefaultAgent = chatAgentService.getContributedDefaultAgent(this.location);
377
const agentHasContributedDefault = !!contributedDefaultAgent;
378
const agentContributedDefaultIsCore = contributedDefaultAgent?.isCore ?? false;
379
const agentActivatedCount = chatAgentService.getActivatedAgents().length;
380
381
this.logService.warn(warningMessage, {
382
agentActivated,
383
agentReady,
384
agentHasDefault,
385
agentDefaultIsCore,
386
agentHasContributedDefault,
387
agentContributedDefaultIsCore,
388
agentActivatedCount,
389
agentLocation: this.location,
390
agentModeKind: modeInfo?.kind,
391
languageModelReady,
392
languageModelCount: languageModelIds.length,
393
languageModelDefaultCount,
394
languageModelHasRequestedModel: !!requestModel.modelId,
395
toolsModelReady
396
});
397
398
type ChatSetupTimeoutClassification = {
399
owner: 'chrmarti';
400
comment: 'Provides insight into chat setup timeouts.';
401
agentActivated: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the agent was activated.' };
402
agentReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the agent was ready.' };
403
agentHasDefault: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a default agent exists for the location and mode.' };
404
agentDefaultIsCore: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the default agent is a core agent.' };
405
agentHasContributedDefault: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a contributed default agent exists for the location.' };
406
agentContributedDefaultIsCore: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the contributed default agent is a core agent.' };
407
agentActivatedCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of activated agents at timeout.' };
408
agentLocation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat agent location.' };
409
agentModeKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat mode kind.' };
410
languageModelReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the language model was ready.' };
411
languageModelCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of registered language models at timeout.' };
412
languageModelDefaultCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of language models with isDefaultForLocation[Chat] set.' };
413
languageModelHasRequestedModel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a specific model ID was requested.' };
414
toolsModelReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the tools model was ready.' };
415
isRemote: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether this is a remote scenario.' };
416
isAnonymous: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether anonymous access is enabled.' };
417
matchingWelcomeViewWhen: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The when clause of the matching extension welcome view, if any.' };
418
};
419
type ChatSetupTimeoutEvent = {
420
agentActivated: boolean;
421
agentReady: boolean;
422
agentHasDefault: boolean;
423
agentDefaultIsCore: boolean;
424
agentHasContributedDefault: boolean;
425
agentContributedDefaultIsCore: boolean;
426
agentActivatedCount: number;
427
agentLocation: string;
428
agentModeKind: string;
429
languageModelReady: boolean;
430
languageModelCount: number;
431
languageModelDefaultCount: number;
432
languageModelHasRequestedModel: boolean;
433
toolsModelReady: boolean;
434
isRemote: boolean;
435
isAnonymous: boolean;
436
matchingWelcomeViewWhen: string;
437
};
438
const chatViewPane = this.viewsService.getActiveViewWithId(ChatViewId) as ChatViewPane | undefined;
439
const matchingWelcomeView = chatViewPane?.getMatchingWelcomeView();
440
441
this.telemetryService.publicLog2<ChatSetupTimeoutEvent, ChatSetupTimeoutClassification>('chatSetup.timeout', {
442
agentActivated,
443
agentReady,
444
agentHasDefault,
445
agentDefaultIsCore,
446
agentHasContributedDefault,
447
agentContributedDefaultIsCore,
448
agentActivatedCount,
449
agentLocation: this.location,
450
agentModeKind: modeInfo?.kind ?? '',
451
languageModelReady,
452
languageModelCount: languageModelIds.length,
453
languageModelDefaultCount,
454
languageModelHasRequestedModel: !!requestModel.modelId,
455
toolsModelReady,
456
isRemote: !!this.environmentService.remoteAuthority,
457
isAnonymous: this.chatEntitlementService.anonymous,
458
matchingWelcomeViewWhen: matchingWelcomeView?.when.serialize() ?? (chatViewPane ? 'noWelcomeView' : 'noChatViewPane'),
459
});
460
461
progress({
462
kind: 'warning',
463
content: new MarkdownString(warningMessage)
464
});
465
466
progress({
467
kind: 'command',
468
command: {
469
id: SetupAgent.CHAT_RETRY_COMMAND_ID,
470
title: localize('retryChat', "Restart"),
471
arguments: [requestModel.session.sessionResource]
472
}
473
});
474
475
// This means Chat is unhealthy and we cannot retry the
476
// request. Signal this to the outside via an event.
477
this._onUnresolvableError.fire();
478
return;
479
}
480
} finally {
481
disposables.dispose();
482
}
483
}
484
485
await chatService.resendRequest(requestModel, {
486
...widget?.getModeRequestOptions(),
487
modeInfo,
488
userSelectedModelId: widget?.input.currentLanguageModel
489
});
490
}
491
492
private async whenPanelAgentHasGuidance(disposables: DisposableStore): Promise<void> {
493
const panelAgentHasGuidance = () => chatViewsWelcomeRegistry.get().some(descriptor => this.contextKeyService.contextMatchesRules(descriptor.when));
494
495
if (panelAgentHasGuidance()) {
496
return;
497
}
498
499
return new Promise<void>(resolve => {
500
let descriptorKeys: Set<string> = new Set();
501
const updateDescriptorKeys = () => {
502
const descriptors = chatViewsWelcomeRegistry.get();
503
descriptorKeys = new Set(descriptors.flatMap(d => d.when.keys()));
504
};
505
updateDescriptorKeys();
506
507
const onDidChangeRegistry = Event.map(chatViewsWelcomeRegistry.onDidChange, () => 'registry' as const);
508
const onDidChangeRelevantContext = Event.map(
509
Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(descriptorKeys)),
510
() => 'context' as const
511
);
512
513
disposables.add(Event.any(
514
onDidChangeRegistry,
515
onDidChangeRelevantContext
516
)(source => {
517
if (source === 'registry') {
518
updateDescriptorKeys();
519
}
520
if (panelAgentHasGuidance()) {
521
resolve();
522
}
523
}));
524
});
525
}
526
527
private whenLanguageModelReady(languageModelsService: ILanguageModelsService, modelId: string | undefined): Promise<unknown> | void {
528
const hasModelForRequest = () => {
529
if (modelId) {
530
return !!languageModelsService.lookupLanguageModel(modelId);
531
}
532
533
for (const id of languageModelsService.getLanguageModelIds()) {
534
const model = languageModelsService.lookupLanguageModel(id);
535
if (model?.isDefaultForLocation[ChatAgentLocation.Chat]) {
536
return true;
537
}
538
}
539
540
return false;
541
};
542
543
if (hasModelForRequest()) {
544
return;
545
}
546
547
return Event.toPromise(Event.filter(languageModelsService.onDidChangeLanguageModels, () => hasModelForRequest()));
548
}
549
550
private whenToolsModelReady(languageModelToolsService: ILanguageModelToolsService, requestModel: IChatRequestModel): Promise<unknown> | void {
551
const needsToolsModel = requestModel.message.parts.some(part => part instanceof ChatRequestToolPart);
552
if (!needsToolsModel) {
553
return; // No tools in this request, no need to check
554
}
555
556
// check that tools other than setup. and internal tools are registered.
557
for (const tool of languageModelToolsService.getAllToolsIncludingDisabled()) {
558
if (tool.id.startsWith('copilot_')) {
559
return; // we have tools!
560
}
561
}
562
563
return Event.toPromise(Event.filter(languageModelToolsService.onDidChangeTools, () => {
564
for (const tool of languageModelToolsService.getAllToolsIncludingDisabled()) {
565
if (tool.id.startsWith('copilot_')) {
566
return true; // we have tools!
567
}
568
}
569
570
return false; // no external tools found
571
}));
572
}
573
574
private whenAgentReady(chatAgentService: IChatAgentService, mode: ChatModeKind | undefined): Promise<unknown> | void {
575
const defaultAgent = chatAgentService.getDefaultAgent(this.location, mode);
576
if (defaultAgent && !defaultAgent.isCore) {
577
return; // we have a default agent from an extension!
578
}
579
580
return Event.toPromise(Event.filter(chatAgentService.onDidChangeAgents, () => {
581
const defaultAgent = chatAgentService.getDefaultAgent(this.location, mode);
582
return Boolean(defaultAgent && !defaultAgent.isCore);
583
}));
584
}
585
586
private async whenAgentActivated(chatService: IChatService): Promise<void> {
587
try {
588
await chatService.activateDefaultAgent(this.location);
589
} catch (error) {
590
this.logService.error(error);
591
}
592
}
593
594
private async doInvokeWithSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService, defaultAccountService: IDefaultAccountService): Promise<IChatAgentResult> {
595
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'chat' });
596
597
const widget = chatWidgetService.getWidgetBySessionResource(request.sessionResource);
598
const requestModel = widget?.viewModel?.model.getRequests().at(-1);
599
600
const setupListener = Event.runAndSubscribe(this.controller.value.onDidChange, (() => {
601
switch (this.controller.value.step) {
602
case ChatSetupStep.SigningIn:
603
progress({
604
kind: 'progressMessage',
605
content: new MarkdownString(localize('setupChatSignIn2', "Signing in to {0}...", defaultAccountService.getDefaultAccountAuthenticationProvider().name)),
606
});
607
break;
608
case ChatSetupStep.Installing:
609
progress({
610
kind: 'progressMessage',
611
content: new MarkdownString(localize('installingChat', "Getting chat ready...")),
612
});
613
break;
614
}
615
}));
616
617
let result: IChatSetupResult | undefined = undefined;
618
try {
619
result = await ChatSetup.getInstance(this.instantiationService, this.context, this.controller).run({
620
disableChatViewReveal: true, // we are already in a chat context
621
forceAnonymous: this.chatEntitlementService.anonymous ? ChatSetupAnonymous.EnabledWithoutDialog : undefined // only enable anonymous selectively
622
});
623
} catch (error) {
624
this.logService.error(`[chat setup] Error during setup: ${toErrorMessage(error)}`);
625
} finally {
626
setupListener.dispose();
627
}
628
629
// User has agreed to run the setup
630
if (typeof result?.success === 'boolean') {
631
if (result.success) {
632
if (result.dialogSkipped) {
633
await widget?.clear(); // make room for the Chat welcome experience
634
} else if (requestModel) {
635
let newRequest = this.replaceAgentInRequestModel(requestModel, chatAgentService); // Replace agent part with the actual Chat agent...
636
newRequest = this.replaceToolInRequestModel(newRequest); // ...then replace any tool parts with the actual Chat tools
637
638
await this.forwardRequestToChat(newRequest, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
639
}
640
} else {
641
progress({
642
kind: 'warning',
643
content: new MarkdownString(localize('chatSetupError', "Chat setup failed."))
644
});
645
}
646
}
647
648
// User has cancelled the setup
649
else {
650
progress({
651
kind: 'markdownContent',
652
content: this.workspaceTrustManagementService.isWorkspaceTrusted() ? SetupAgent.SETUP_NEEDED_MESSAGE : SetupAgent.TRUST_NEEDED_MESSAGE
653
});
654
}
655
656
return {};
657
}
658
659
private replaceAgentInRequestModel(requestModel: IChatRequestModel, chatAgentService: IChatAgentService): IChatRequestModel {
660
const agentPart = requestModel.message.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);
661
if (!agentPart) {
662
return requestModel;
663
}
664
665
const agentId = agentPart.agent.id.replace(/setup\./, `${defaultChat.extensionId}.`.toLowerCase());
666
const githubAgent = chatAgentService.getAgent(agentId);
667
if (!githubAgent) {
668
return requestModel;
669
}
670
671
const newAgentPart = new ChatRequestAgentPart(agentPart.range, agentPart.editorRange, githubAgent);
672
673
return new ChatRequestModel({
674
session: requestModel.session as ChatModel,
675
message: {
676
parts: requestModel.message.parts.map(part => {
677
if (part instanceof ChatRequestAgentPart) {
678
return newAgentPart;
679
}
680
return part;
681
}),
682
text: requestModel.message.text
683
},
684
variableData: requestModel.variableData,
685
timestamp: Date.now(),
686
attempt: requestModel.attempt,
687
modeInfo: requestModel.modeInfo,
688
confirmation: requestModel.confirmation,
689
locationData: requestModel.locationData,
690
attachedContext: requestModel.attachedContext,
691
isCompleteAddedRequest: requestModel.isCompleteAddedRequest,
692
});
693
}
694
695
private replaceToolInRequestModel(requestModel: IChatRequestModel): IChatRequestModel {
696
const toolPart = requestModel.message.parts.find((r): r is ChatRequestToolPart => r instanceof ChatRequestToolPart);
697
if (!toolPart) {
698
return requestModel;
699
}
700
701
const toolId = toolPart.toolId.replace(/setup.tools\./, `copilot_`.toLowerCase());
702
const newToolPart = new ChatRequestToolPart(
703
toolPart.range,
704
toolPart.editorRange,
705
toolPart.toolName,
706
toolId,
707
toolPart.displayName,
708
toolPart.icon
709
);
710
711
const chatRequestToolEntry: IChatRequestToolEntry = {
712
id: toolId,
713
name: 'new',
714
range: toolPart.range,
715
kind: 'tool',
716
value: undefined
717
};
718
719
const variableData: IChatRequestVariableData = {
720
variables: [chatRequestToolEntry]
721
};
722
723
return new ChatRequestModel({
724
session: requestModel.session as ChatModel,
725
message: {
726
parts: requestModel.message.parts.map(part => {
727
if (part instanceof ChatRequestToolPart) {
728
return newToolPart;
729
}
730
return part;
731
}),
732
text: requestModel.message.text
733
},
734
variableData: variableData,
735
timestamp: Date.now(),
736
attempt: requestModel.attempt,
737
modeInfo: requestModel.modeInfo,
738
confirmation: requestModel.confirmation,
739
locationData: requestModel.locationData,
740
attachedContext: [chatRequestToolEntry],
741
isCompleteAddedRequest: requestModel.isCompleteAddedRequest,
742
});
743
}
744
}
745
746
export class SetupTool implements IToolImpl {
747
748
static registerTool(instantiationService: IInstantiationService, toolData: IToolData): IDisposable {
749
return instantiationService.invokeFunction(accessor => {
750
const toolService = accessor.get(ILanguageModelToolsService);
751
752
const tool = instantiationService.createInstance(SetupTool);
753
return toolService.registerTool(toolData, tool);
754
});
755
}
756
757
async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise<IToolResult> {
758
const result: IToolResult = {
759
content: [
760
{
761
kind: 'text',
762
value: ''
763
}
764
]
765
};
766
767
return result;
768
}
769
770
async prepareToolInvocation?(parameters: unknown, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
771
return undefined;
772
}
773
}
774
775
export class AINewSymbolNamesProvider {
776
777
static registerProvider(instantiationService: IInstantiationService, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): IDisposable {
778
return instantiationService.invokeFunction(accessor => {
779
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
780
781
const provider = instantiationService.createInstance(AINewSymbolNamesProvider, context, controller);
782
return languageFeaturesService.newSymbolNamesProvider.register('*', provider);
783
});
784
}
785
786
constructor(
787
private readonly context: ChatEntitlementContext,
788
private readonly controller: Lazy<ChatSetupController>,
789
@IInstantiationService private readonly instantiationService: IInstantiationService,
790
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,
791
) {
792
}
793
794
async provideNewSymbolNames(model: ITextModel, range: IRange, triggerKind: NewSymbolNameTriggerKind, token: CancellationToken): Promise<NewSymbolName[] | undefined> {
795
await this.instantiationService.invokeFunction(accessor => {
796
return ChatSetup.getInstance(this.instantiationService, this.context, this.controller).run({
797
forceAnonymous: this.chatEntitlementService.anonymous ? ChatSetupAnonymous.EnabledWithDialog : undefined
798
});
799
});
800
801
return [];
802
}
803
}
804
805
export class ChatCodeActionsProvider {
806
807
static registerProvider(instantiationService: IInstantiationService): IDisposable {
808
return instantiationService.invokeFunction(accessor => {
809
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
810
811
const provider = instantiationService.createInstance(ChatCodeActionsProvider);
812
return languageFeaturesService.codeActionProvider.register('*', provider);
813
});
814
}
815
816
constructor(
817
@IMarkerService private readonly markerService: IMarkerService,
818
) {
819
}
820
821
async provideCodeActions(model: ITextModel, range: Range | Selection): Promise<CodeActionList | undefined> {
822
const actions: CodeAction[] = [];
823
824
// "Generate" if the line is whitespace only
825
// "Modify" if there is a selection
826
let generateOrModifyTitle: string | undefined;
827
let generateOrModifyCommand: Command | undefined;
828
if (range.isEmpty()) {
829
const textAtLine = model.getLineContent(range.startLineNumber);
830
if (/^\s*$/.test(textAtLine)) {
831
generateOrModifyTitle = localize('generate', "Generate");
832
generateOrModifyCommand = AICodeActionsHelper.generate(range);
833
}
834
} else {
835
const textInSelection = model.getValueInRange(range);
836
if (!/^\s*$/.test(textInSelection)) {
837
generateOrModifyTitle = localize('modify', "Modify");
838
generateOrModifyCommand = AICodeActionsHelper.modify(range);
839
}
840
}
841
842
if (generateOrModifyTitle && generateOrModifyCommand) {
843
actions.push({
844
kind: CodeActionKind.RefactorRewrite.append('copilot').value,
845
isAI: true,
846
title: generateOrModifyTitle,
847
command: generateOrModifyCommand,
848
});
849
}
850
851
const markers = AICodeActionsHelper.warningOrErrorMarkersAtRange(this.markerService, model.uri, range);
852
if (markers.length > 0) {
853
854
// "Fix" if there are diagnostics in the range
855
actions.push({
856
kind: CodeActionKind.QuickFix.append('copilot').value,
857
isAI: true,
858
diagnostics: markers,
859
title: localize('fix', "Fix"),
860
command: AICodeActionsHelper.fixMarkers(markers, range)
861
});
862
863
// "Explain" if there are diagnostics in the range
864
actions.push({
865
kind: CodeActionKind.QuickFix.append('explain').append('copilot').value,
866
isAI: true,
867
diagnostics: markers,
868
title: localize('explain', "Explain"),
869
command: AICodeActionsHelper.explainMarkers(markers)
870
});
871
}
872
873
return {
874
actions,
875
dispose() { }
876
};
877
}
878
}
879
880
export class AICodeActionsHelper {
881
882
static warningOrErrorMarkersAtRange(markerService: IMarkerService, resource: URI, range: Range | Selection): IMarker[] {
883
return markerService
884
.read({ resource, severities: MarkerSeverity.Error | MarkerSeverity.Warning })
885
.filter(marker => range.startLineNumber <= marker.endLineNumber && range.endLineNumber >= marker.startLineNumber);
886
}
887
888
static modify(range: Range): Command {
889
return {
890
id: INLINE_CHAT_START,
891
title: localize('modify', "Modify"),
892
arguments: [
893
{
894
initialSelection: this.rangeToSelection(range),
895
initialRange: range,
896
position: range.getStartPosition()
897
} satisfies { initialSelection: ISelection; initialRange: IRange; position: IPosition }
898
]
899
};
900
}
901
902
static generate(range: Range): Command {
903
return {
904
id: INLINE_CHAT_START,
905
title: localize('generate', "Generate"),
906
arguments: [
907
{
908
initialSelection: this.rangeToSelection(range),
909
initialRange: range,
910
position: range.getStartPosition()
911
} satisfies { initialSelection: ISelection; initialRange: IRange; position: IPosition }
912
]
913
};
914
}
915
916
private static rangeToSelection(range: Range): ISelection {
917
return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
918
}
919
920
static explainMarkers(markers: IMarker[]): Command {
921
return {
922
id: CHAT_OPEN_ACTION_ID,
923
title: localize('explain', "Explain"),
924
arguments: [
925
{
926
query: `@workspace /explain ${markers.map(marker => marker.message).join(', ')}`,
927
isPartialQuery: true
928
} satisfies { query: string; isPartialQuery: boolean }
929
]
930
};
931
}
932
933
static fixMarkers(markers: IMarker[], range: Range): Command {
934
return {
935
id: INLINE_CHAT_START,
936
title: localize('fix', "Fix"),
937
arguments: [
938
{
939
message: `/fix ${markers.map(marker => marker.message).join(', ')}`,
940
initialSelection: this.rangeToSelection(range),
941
initialRange: range,
942
position: range.getStartPosition()
943
} satisfies { message: string; initialSelection: ISelection; initialRange: IRange; position: IPosition }
944
]
945
};
946
}
947
}
948
949