Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/scenarios/test-tools/workspace/chatSetup.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation and GitHub. All rights reserved.
3
*--------------------------------------------------------------------------------------------*/
4
5
import './media/chatSetup.css';
6
import { $ } from '../../../../base/browser/dom.js';
7
import { Dialog, DialogContentsAlignment } from '../../../../base/browser/ui/dialog/dialog.js';
8
import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js';
9
import { timeout } from '../../../../base/common/async.js';
10
import { CancellationToken } from '../../../../base/common/cancellation.js';
11
import { Codicon } from '../../../../base/common/codicons.js';
12
import { toErrorMessage } from '../../../../base/common/errorMessage.js';
13
import { isCancellationError } from '../../../../base/common/errors.js';
14
import { Emitter, Event } from '../../../../base/common/event.js';
15
import { MarkdownString } from '../../../../base/common/htmlContent.js';
16
import { Lazy } from '../../../../base/common/lazy.js';
17
import { Disposable, DisposableStore, IDisposable, markAsSingleton, MutableDisposable } from '../../../../base/common/lifecycle.js';
18
import Severity from '../../../../base/common/severity.js';
19
import { StopWatch } from '../../../../base/common/stopwatch.js';
20
import { equalsIgnoreCase } from '../../../../base/common/strings.js';
21
import { isObject } from '../../../../base/common/types.js';
22
import { URI } from '../../../../base/common/uri.js';
23
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
24
import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
25
import { localize, localize2 } from '../../../../nls.js';
26
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
27
import { ICommandService } from '../../../../platform/commands/common/commands.js';
28
import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
29
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
30
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
31
import { createWorkbenchDialogOptions } from '../../../../platform/dialogs/browser/dialog.js';
32
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
33
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
34
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
35
import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';
36
import { ILogService } from '../../../../platform/log/common/log.js';
37
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
38
import product from '../../../../platform/product/common/product.js';
39
import { IProductService } from '../../../../platform/product/common/productService.js';
40
import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
41
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
42
import { Registry } from '../../../../platform/registry/common/platform.js';
43
import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js';
44
import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js';
45
import { IWorkbenchContribution } from '../../../common/contributions.js';
46
import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js';
47
import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js';
48
import { AuthenticationSession, IAuthenticationService } from '../../../services/authentication/common/authentication.js';
49
import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extensions/browser/extensionUrlHandler.js';
50
import { nullExtensionDescription } from '../../../services/extensions/common/extensions.js';
51
import { IHostService } from '../../../services/host/browser/host.js';
52
import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js';
53
import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
54
import { IViewsService } from '../../../services/views/common/viewsService.js';
55
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js';
56
import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
57
import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../common/chatAgents.js';
58
import { ChatContextKeys } from '../common/chatContextKeys.js';
59
import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService, isProUser } from '../common/chatEntitlementService.js';
60
import { ChatModel, ChatRequestModel, IChatRequestModel, IChatRequestToolEntry, IChatRequestVariableData } from '../common/chatModel.js';
61
import { ChatRequestAgentPart, ChatRequestToolPart } from '../common/chatParserTypes.js';
62
import { IChatProgress, IChatService } from '../common/chatService.js';
63
import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../common/constants.js';
64
import { ILanguageModelsService } from '../common/languageModels.js';
65
import { CHAT_CATEGORY, CHAT_OPEN_ACTION_ID, CHAT_SETUP_ACTION_ID } from './actions/chatActions.js';
66
import { ChatViewId, IChatWidgetService, showCopilotView } from './chat.js';
67
import { CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js';
68
import { coalesce } from '../../../../base/common/arrays.js';
69
70
const defaultChat = {
71
extensionId: product.defaultChatAgent?.extensionId ?? '',
72
chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '',
73
documentationUrl: product.defaultChatAgent?.documentationUrl ?? '',
74
skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '',
75
publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '',
76
manageOverageUrl: product.defaultChatAgent?.manageOverageUrl ?? '',
77
upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '',
78
providerName: product.defaultChatAgent?.providerName ?? '',
79
enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '',
80
enterpriseProviderName: product.defaultChatAgent?.enterpriseProviderName ?? '',
81
alternativeProviderId: product.defaultChatAgent?.alternativeProviderId ?? '',
82
alternativeProviderName: product.defaultChatAgent?.alternativeProviderName ?? '',
83
providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '',
84
providerScopes: product.defaultChatAgent?.providerScopes ?? [[]],
85
manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '',
86
completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '',
87
walkthroughCommand: product.defaultChatAgent?.walkthroughCommand ?? '',
88
completionsRefreshTokenCommand: product.defaultChatAgent?.completionsRefreshTokenCommand ?? '',
89
chatRefreshTokenCommand: product.defaultChatAgent?.chatRefreshTokenCommand ?? '',
90
};
91
92
//#region Contribution
93
94
const ToolsAgentContextKey = ContextKeyExpr.and(
95
ContextKeyExpr.equals(`config.${ChatConfiguration.AgentEnabled}`, true),
96
ChatContextKeys.Editing.agentModeDisallowed.negate(),
97
ContextKeyExpr.not(`previewFeaturesDisabled`) // Set by extension
98
);
99
100
class SetupAgent extends Disposable implements IChatAgentImplementation {
101
102
static registerDefaultAgents(instantiationService: IInstantiationService, location: ChatAgentLocation, mode: ChatMode | undefined, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): { agent: SetupAgent; disposable: IDisposable } {
103
return instantiationService.invokeFunction(accessor => {
104
const chatAgentService = accessor.get(IChatAgentService);
105
106
let id: string;
107
let description = localize('chatDescription', "Ask Copilot");
108
switch (location) {
109
case ChatAgentLocation.Panel:
110
if (mode === ChatMode.Ask) {
111
id = 'setup.chat';
112
} else if (mode === ChatMode.Edit) {
113
id = 'setup.edits';
114
description = localize('editsDescription', "Edit files in your workspace");
115
} else {
116
id = 'setup.agent';
117
description = localize('agentDescription', "Edit files in your workspace in agent mode");
118
}
119
break;
120
case ChatAgentLocation.Terminal:
121
id = 'setup.terminal';
122
break;
123
case ChatAgentLocation.Editor:
124
id = 'setup.editor';
125
break;
126
case ChatAgentLocation.Notebook:
127
id = 'setup.notebook';
128
break;
129
}
130
131
return SetupAgent.doRegisterAgent(instantiationService, chatAgentService, id, `${defaultChat.providerName} Copilot`, true, description, location, mode, context, controller);
132
});
133
}
134
135
static registerVSCodeAgent(instantiationService: IInstantiationService, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): { agent: SetupAgent; disposable: IDisposable } {
136
return instantiationService.invokeFunction(accessor => {
137
const chatAgentService = accessor.get(IChatAgentService);
138
139
const disposables = new DisposableStore();
140
141
const { agent, disposable } = SetupAgent.doRegisterAgent(instantiationService, chatAgentService, 'setup.vscode', 'vscode', false, localize2('vscodeAgentDescription', "Ask questions about VS Code").value, ChatAgentLocation.Panel, undefined, context, controller);
142
disposables.add(disposable);
143
144
disposables.add(SetupTool.registerTool(instantiationService, {
145
id: 'setup.tools.createNewWorkspace',
146
source: {
147
type: 'internal',
148
},
149
icon: Codicon.newFolder,
150
displayName: localize('setupToolDisplayName', "New Workspace"),
151
modelDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"),
152
userDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"),
153
canBeReferencedInPrompt: true,
154
toolReferenceName: 'new',
155
when: ContextKeyExpr.true(),
156
}).disposable);
157
158
return { agent, disposable: disposables };
159
});
160
}
161
162
private static doRegisterAgent(instantiationService: IInstantiationService, chatAgentService: IChatAgentService, id: string, name: string, isDefault: boolean, description: string, location: ChatAgentLocation, mode: ChatMode | undefined, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): { agent: SetupAgent; disposable: IDisposable } {
163
const disposables = new DisposableStore();
164
disposables.add(chatAgentService.registerAgent(id, {
165
id,
166
name,
167
isDefault,
168
isCore: true,
169
modes: mode ? [mode] : [ChatMode.Ask],
170
when: mode === ChatMode.Agent ? ToolsAgentContextKey?.serialize() : undefined,
171
slashCommands: [],
172
disambiguation: [],
173
locations: [location],
174
metadata: { helpTextPrefix: SetupAgent.SETUP_NEEDED_MESSAGE },
175
description,
176
extensionId: nullExtensionDescription.identifier,
177
extensionDisplayName: nullExtensionDescription.name,
178
extensionPublisherId: nullExtensionDescription.publisher
179
}));
180
181
const agent = disposables.add(instantiationService.createInstance(SetupAgent, context, controller, location));
182
disposables.add(chatAgentService.registerAgentImplementation(id, agent));
183
if (mode === ChatMode.Agent) {
184
chatAgentService.updateAgent(id, { themeIcon: Codicon.tools });
185
}
186
187
return { agent, disposable: disposables };
188
}
189
190
private static readonly SETUP_NEEDED_MESSAGE = new MarkdownString(localize('settingUpCopilotNeeded', "You need to set up Copilot to use Chat."));
191
192
private readonly _onUnresolvableError = this._register(new Emitter<void>());
193
readonly onUnresolvableError = this._onUnresolvableError.event;
194
195
private readonly pendingForwardedRequests = new Map<string, Promise<void>>();
196
197
constructor(
198
private readonly context: ChatEntitlementContext,
199
private readonly controller: Lazy<ChatSetupController>,
200
private readonly location: ChatAgentLocation,
201
@IInstantiationService private readonly instantiationService: IInstantiationService,
202
@ILogService private readonly logService: ILogService,
203
@IConfigurationService private readonly configurationService: IConfigurationService,
204
@ITelemetryService private readonly telemetryService: ITelemetryService,
205
) {
206
super();
207
}
208
209
async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void): Promise<IChatAgentResult> {
210
return this.instantiationService.invokeFunction(async accessor /* using accessor for lazy loading */ => {
211
const chatService = accessor.get(IChatService);
212
const languageModelsService = accessor.get(ILanguageModelsService);
213
const chatWidgetService = accessor.get(IChatWidgetService);
214
const chatAgentService = accessor.get(IChatAgentService);
215
const languageModelToolsService = accessor.get(ILanguageModelToolsService);
216
217
return this.doInvoke(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService);
218
});
219
}
220
221
private async doInvoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise<IChatAgentResult> {
222
if (!this.context.state.installed || this.context.state.disabled || this.context.state.entitlement === ChatEntitlement.Available || this.context.state.entitlement === ChatEntitlement.Unknown) {
223
return this.doInvokeWithSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService);
224
}
225
226
return this.doInvokeWithoutSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService);
227
}
228
229
private async doInvokeWithoutSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise<IChatAgentResult> {
230
const requestModel = chatWidgetService.getWidgetBySessionId(request.sessionId)?.viewModel?.model.getRequests().at(-1);
231
if (!requestModel) {
232
this.logService.error('[chat setup] Request model not found, cannot redispatch request.');
233
return {}; // this should not happen
234
}
235
236
progress({
237
kind: 'progressMessage',
238
content: new MarkdownString(localize('waitingCopilot', "Getting Copilot ready.")),
239
});
240
241
await this.forwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
242
243
return {};
244
}
245
246
private async forwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {
247
try {
248
await this.doForwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
249
} catch (error) {
250
progress({
251
kind: 'warning',
252
content: new MarkdownString(localize('copilotUnavailableWarning', "Copilot failed to get a response. Please try again."))
253
});
254
}
255
}
256
257
private async doForwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {
258
if (this.pendingForwardedRequests.has(requestModel.session.sessionId)) {
259
throw new Error('Request already in progress');
260
}
261
262
const forwardRequest = this.doForwardRequestToCopilotWhenReady(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
263
this.pendingForwardedRequests.set(requestModel.session.sessionId, forwardRequest);
264
265
try {
266
await forwardRequest;
267
} finally {
268
this.pendingForwardedRequests.delete(requestModel.session.sessionId);
269
}
270
}
271
272
private async doForwardRequestToCopilotWhenReady(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {
273
const widget = chatWidgetService.getWidgetBySessionId(requestModel.session.sessionId);
274
const mode = widget?.input.currentMode;
275
const languageModel = widget?.input.currentLanguageModel;
276
277
// We need a signal to know when we can resend the request to
278
// Copilot. Waiting for the registration of the agent is not
279
// enough, we also need a language/tools model to be available.
280
281
const whenAgentReady = this.whenAgentReady(chatAgentService, mode);
282
const whenLanguageModelReady = this.whenLanguageModelReady(languageModelsService);
283
const whenToolsModelReady = this.whenToolsModelReady(languageModelToolsService, requestModel);
284
285
if (whenLanguageModelReady instanceof Promise || whenAgentReady instanceof Promise || whenToolsModelReady instanceof Promise) {
286
const timeoutHandle = setTimeout(() => {
287
progress({
288
kind: 'progressMessage',
289
content: new MarkdownString(localize('waitingCopilot2', "Copilot is almost ready.")),
290
});
291
}, 10000);
292
293
try {
294
const ready = await Promise.race([
295
timeout(20000).then(() => 'timedout'),
296
this.whenDefaultAgentFailed(chatService).then(() => 'error'),
297
Promise.allSettled([whenLanguageModelReady, whenAgentReady, whenToolsModelReady])
298
]);
299
300
if (ready === 'error' || ready === 'timedout') {
301
progress({
302
kind: 'warning',
303
content: new MarkdownString(ready === 'timedout' ?
304
localize('copilotTookLongWarning', "Copilot took too long to get ready. Please review the guidance in the Chat view.") :
305
localize('copilotFailedWarning', "Copilot failed to get ready. Please review the guidance in the Chat view.")
306
)
307
});
308
309
// This means Copilot is unhealthy and we cannot retry the
310
// request. Signal this to the outside via an event.
311
this._onUnresolvableError.fire();
312
return;
313
}
314
} finally {
315
clearTimeout(timeoutHandle);
316
}
317
}
318
319
await chatService.resendRequest(requestModel, { mode, userSelectedModelId: languageModel });
320
}
321
322
private whenLanguageModelReady(languageModelsService: ILanguageModelsService): Promise<unknown> | void {
323
for (const id of languageModelsService.getLanguageModelIds()) {
324
const model = languageModelsService.lookupLanguageModel(id);
325
if (model && model.isDefault) {
326
return; // we have language models!
327
}
328
}
329
330
return Event.toPromise(Event.filter(languageModelsService.onDidChangeLanguageModels, e => e.added?.some(added => added.metadata.isDefault) ?? false));
331
}
332
333
private whenToolsModelReady(languageModelToolsService: ILanguageModelToolsService, requestModel: IChatRequestModel): Promise<unknown> | void {
334
const needsToolsModel = requestModel.message.parts.some(part => part instanceof ChatRequestToolPart);
335
if (!needsToolsModel) {
336
return; // No tools in this request, no need to check
337
}
338
339
// check that tools other than setup. and internal tools are registered.
340
for (const tool of languageModelToolsService.getTools()) {
341
if (tool.source.type !== 'internal') {
342
return; // we have tools!
343
}
344
}
345
346
return Event.toPromise(Event.filter(languageModelToolsService.onDidChangeTools, () => {
347
for (const tool of languageModelToolsService.getTools()) {
348
if (tool.source.type !== 'internal') {
349
return true; // we have tools!
350
}
351
}
352
353
return false; // no external tools found
354
}));
355
}
356
357
private whenAgentReady(chatAgentService: IChatAgentService, mode: ChatMode | undefined): Promise<unknown> | void {
358
const defaultAgent = chatAgentService.getDefaultAgent(this.location, mode);
359
if (defaultAgent && !defaultAgent.isCore) {
360
return; // we have a default agent from an extension!
361
}
362
363
return Event.toPromise(Event.filter(chatAgentService.onDidChangeAgents, () => {
364
const defaultAgent = chatAgentService.getDefaultAgent(this.location, mode);
365
return Boolean(defaultAgent && !defaultAgent.isCore);
366
}));
367
}
368
369
private async whenDefaultAgentFailed(chatService: IChatService): Promise<void> {
370
return new Promise<void>(resolve => {
371
chatService.activateDefaultAgent(this.location).catch(() => resolve());
372
});
373
}
374
375
private async doInvokeWithSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise<IChatAgentResult> {
376
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'chat' });
377
378
const requestModel = chatWidgetService.getWidgetBySessionId(request.sessionId)?.viewModel?.model.getRequests().at(-1);
379
380
const setupListener = Event.runAndSubscribe(this.controller.value.onDidChange, (() => {
381
switch (this.controller.value.step) {
382
case ChatSetupStep.SigningIn:
383
progress({
384
kind: 'progressMessage',
385
content: new MarkdownString(localize('setupChatSignIn2', "Signing in to {0}.", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName)),
386
});
387
break;
388
case ChatSetupStep.Installing:
389
progress({
390
kind: 'progressMessage',
391
content: new MarkdownString(localize('installingCopilot', "Getting Copilot ready.")),
392
});
393
break;
394
}
395
}));
396
397
let result: IChatSetupResult | undefined = undefined;
398
try {
399
result = await ChatSetup.getInstance(this.instantiationService, this.context, this.controller).run();
400
} catch (error) {
401
this.logService.error(`[chat setup] Error during setup: ${toErrorMessage(error)}`);
402
} finally {
403
setupListener.dispose();
404
}
405
406
// User has agreed to run the setup
407
if (typeof result?.success === 'boolean') {
408
if (result.success) {
409
if (result.dialogSkipped) {
410
progress({
411
kind: 'markdownContent',
412
content: new MarkdownString(localize('copilotSetupSuccess', "Copilot setup finished successfully."))
413
});
414
} else if (requestModel) {
415
let newRequest = this.replaceAgentInRequestModel(requestModel, chatAgentService); // Replace agent part with the actual Copilot agent...
416
newRequest = this.replaceToolInRequestModel(newRequest); // ...then replace any tool parts with the actual Copilot tools
417
418
await this.forwardRequestToCopilot(newRequest, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
419
}
420
} else {
421
progress({
422
kind: 'warning',
423
content: new MarkdownString(localize('copilotSetupError', "Copilot setup failed."))
424
});
425
}
426
}
427
428
// User has cancelled the setup
429
else {
430
progress({
431
kind: 'markdownContent',
432
content: SetupAgent.SETUP_NEEDED_MESSAGE,
433
});
434
}
435
436
return {};
437
}
438
439
private replaceAgentInRequestModel(requestModel: IChatRequestModel, chatAgentService: IChatAgentService): IChatRequestModel {
440
const agentPart = requestModel.message.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);
441
if (!agentPart) {
442
return requestModel;
443
}
444
445
const agentId = agentPart.agent.id.replace(/setup\./, `${defaultChat.extensionId}.`.toLowerCase());
446
const githubAgent = chatAgentService.getAgent(agentId);
447
if (!githubAgent) {
448
return requestModel;
449
}
450
451
const newAgentPart = new ChatRequestAgentPart(agentPart.range, agentPart.editorRange, githubAgent);
452
453
return new ChatRequestModel({
454
session: requestModel.session as ChatModel,
455
message: {
456
parts: requestModel.message.parts.map(part => {
457
if (part instanceof ChatRequestAgentPart) {
458
return newAgentPart;
459
}
460
return part;
461
}),
462
text: requestModel.message.text
463
},
464
variableData: requestModel.variableData,
465
timestamp: Date.now(),
466
attempt: requestModel.attempt,
467
confirmation: requestModel.confirmation,
468
locationData: requestModel.locationData,
469
attachedContext: requestModel.attachedContext,
470
isCompleteAddedRequest: requestModel.isCompleteAddedRequest,
471
});
472
}
473
474
private replaceToolInRequestModel(requestModel: IChatRequestModel): IChatRequestModel {
475
const toolPart = requestModel.message.parts.find((r): r is ChatRequestToolPart => r instanceof ChatRequestToolPart);
476
if (!toolPart) {
477
return requestModel;
478
}
479
480
const toolId = toolPart.toolId.replace(/setup.tools\./, `copilot_`.toLowerCase());
481
const newToolPart = new ChatRequestToolPart(
482
toolPart.range,
483
toolPart.editorRange,
484
toolPart.toolName,
485
toolId,
486
toolPart.displayName,
487
toolPart.icon
488
);
489
490
const chatRequestToolEntry: IChatRequestToolEntry = {
491
id: toolId,
492
name: 'new',
493
range: toolPart.range,
494
kind: 'tool',
495
value: undefined
496
};
497
498
const variableData: IChatRequestVariableData = {
499
variables: [chatRequestToolEntry]
500
};
501
502
return new ChatRequestModel({
503
session: requestModel.session as ChatModel,
504
message: {
505
parts: requestModel.message.parts.map(part => {
506
if (part instanceof ChatRequestToolPart) {
507
return newToolPart;
508
}
509
return part;
510
}),
511
text: requestModel.message.text
512
},
513
variableData: variableData,
514
timestamp: Date.now(),
515
attempt: requestModel.attempt,
516
confirmation: requestModel.confirmation,
517
locationData: requestModel.locationData,
518
attachedContext: [chatRequestToolEntry],
519
isCompleteAddedRequest: requestModel.isCompleteAddedRequest,
520
});
521
}
522
}
523
524
525
class SetupTool extends Disposable implements IToolImpl {
526
527
static registerTool(instantiationService: IInstantiationService, toolData: IToolData): { tool: SetupTool; disposable: IDisposable } {
528
return instantiationService.invokeFunction(accessor => {
529
const toolService = accessor.get(ILanguageModelToolsService);
530
531
const disposables = new DisposableStore();
532
533
disposables.add(toolService.registerToolData(toolData));
534
535
const tool = instantiationService.createInstance(SetupTool);
536
disposables.add(toolService.registerToolImplementation(toolData.id, tool));
537
538
return { tool, disposable: disposables };
539
});
540
}
541
542
async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise<IToolResult> {
543
const result: IToolResult = {
544
content: [
545
{
546
kind: 'text',
547
value: ''
548
}
549
]
550
};
551
552
return result;
553
}
554
555
async prepareToolInvocation?(parameters: any, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
556
return undefined;
557
}
558
}
559
560
enum ChatSetupStrategy {
561
Canceled = 0,
562
DefaultSetup = 1,
563
SetupWithoutEnterpriseProvider = 2,
564
SetupWithEnterpriseProvider = 3
565
}
566
567
interface IChatSetupResult {
568
readonly success: boolean | undefined;
569
readonly dialogSkipped: boolean;
570
}
571
572
class ChatSetup {
573
574
private static instance: ChatSetup | undefined = undefined;
575
static getInstance(instantiationService: IInstantiationService, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): ChatSetup {
576
let instance = ChatSetup.instance;
577
if (!instance) {
578
instance = ChatSetup.instance = instantiationService.invokeFunction(accessor => {
579
return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService), accessor.get(ILogService), accessor.get(IConfigurationService));
580
});
581
}
582
583
return instance;
584
}
585
586
private pendingRun: Promise<IChatSetupResult> | undefined = undefined;
587
588
private skipDialogOnce = false;
589
590
private constructor(
591
private readonly context: ChatEntitlementContext,
592
private readonly controller: Lazy<ChatSetupController>,
593
@IInstantiationService private readonly instantiationService: IInstantiationService,
594
@ITelemetryService private readonly telemetryService: ITelemetryService,
595
@ILayoutService private readonly layoutService: IWorkbenchLayoutService,
596
@IKeybindingService private readonly keybindingService: IKeybindingService,
597
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,
598
@ILogService private readonly logService: ILogService,
599
@IConfigurationService private readonly configurationService: IConfigurationService
600
) { }
601
602
skipDialog(): void {
603
this.skipDialogOnce = true;
604
}
605
606
async run(): Promise<IChatSetupResult> {
607
if (this.pendingRun) {
608
return this.pendingRun;
609
}
610
611
this.pendingRun = this.doRun();
612
613
try {
614
return await this.pendingRun;
615
} finally {
616
this.pendingRun = undefined;
617
}
618
}
619
620
private async doRun(): Promise<IChatSetupResult> {
621
const dialogSkipped = this.skipDialogOnce;
622
this.skipDialogOnce = false;
623
624
let setupStrategy: ChatSetupStrategy;
625
if (dialogSkipped || isProUser(this.chatEntitlementService.entitlement) || this.chatEntitlementService.entitlement === ChatEntitlement.Limited) {
626
setupStrategy = ChatSetupStrategy.DefaultSetup; // existing pro/free users setup without a dialog
627
} else {
628
setupStrategy = await this.showDialog();
629
}
630
631
if (setupStrategy === ChatSetupStrategy.DefaultSetup && ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) {
632
setupStrategy = ChatSetupStrategy.SetupWithEnterpriseProvider; // users with a configured provider go through provider setup
633
}
634
635
let success = undefined;
636
try {
637
switch (setupStrategy) {
638
case ChatSetupStrategy.SetupWithEnterpriseProvider:
639
success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: true });
640
break;
641
case ChatSetupStrategy.SetupWithoutEnterpriseProvider:
642
success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false });
643
break;
644
case ChatSetupStrategy.DefaultSetup:
645
success = await this.controller.value.setup();
646
break;
647
case ChatSetupStrategy.Canceled:
648
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.ChatInstall', { installResult: 'failedMaybeLater', installDuration: 0, signUpErrorCode: undefined });
649
break;
650
}
651
} catch (error) {
652
this.logService.error(`[chat setup] Error during setup: ${toErrorMessage(error)}`);
653
success = false;
654
}
655
656
return { success, dialogSkipped };
657
}
658
659
private async showDialog(): Promise<ChatSetupStrategy> {
660
const disposables = new DisposableStore();
661
662
const buttons = this.getButtons();
663
664
const dialog = disposables.add(new Dialog(
665
this.layoutService.activeContainer,
666
this.getDialogTitle(),
667
buttons.map(button => button[0]),
668
createWorkbenchDialogOptions({
669
type: 'none',
670
icon: Codicon.copilotLarge,
671
alignment: DialogContentsAlignment.Vertical,
672
cancelId: -1, // not offered as button, but X can cancel
673
renderFooter: this.telemetryService.telemetryLevel !== TelemetryLevel.NONE ? footer => footer.appendChild(this.createDialogFooter(disposables)) : undefined,
674
buttonOptions: buttons.map(button => button[2])
675
}, this.keybindingService, this.layoutService)
676
));
677
678
const { button } = await dialog.show();
679
disposables.dispose();
680
681
return buttons[button]?.[1] ?? ChatSetupStrategy.Canceled;
682
}
683
684
private getButtons(): Array<[string, ChatSetupStrategy, { renderAsLink: boolean } | undefined]> {
685
if (this.context.state.entitlement === ChatEntitlement.Unknown) {
686
const supportAlternateProvider = this.configurationService.getValue('chat.setup.signInWithAlternateProvider') === true && defaultChat.alternativeProviderId;
687
688
if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) {
689
return coalesce([
690
[localize('continueWithProvider', "Continue with {0}", defaultChat.enterpriseProviderName), ChatSetupStrategy.SetupWithEnterpriseProvider, undefined],
691
supportAlternateProvider ? [localize('continueWithProvider', "Continue with {0}", defaultChat.alternativeProviderName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined] : undefined,
692
[localize('signInWithProvider', "Sign in with a {0} account", defaultChat.providerName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, { renderAsLink: true }]
693
]);
694
}
695
696
return coalesce([
697
[localize('continueWithProvider', "Continue with {0}", defaultChat.providerName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined],
698
supportAlternateProvider ? [localize('continueWithProvider', "Continue with {0}", defaultChat.alternativeProviderName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined] : undefined,
699
[localize('signInWithProvider', "Sign in with a {0} account", defaultChat.enterpriseProviderName), ChatSetupStrategy.SetupWithEnterpriseProvider, { renderAsLink: true }]
700
]);
701
}
702
703
return [[localize('setupCopilotButton', "Set up Copilot"), ChatSetupStrategy.DefaultSetup, undefined]];
704
}
705
706
private getDialogTitle(): string {
707
if (this.context.state.entitlement === ChatEntitlement.Unknown) {
708
return this.context.state.registered ? localize('signUp', "Sign in to use Copilot") : localize('signUpFree', "Sign in to use Copilot for free");
709
}
710
711
if (isProUser(this.context.state.entitlement)) {
712
return localize('copilotProTitle', "Start using Copilot Pro");
713
}
714
715
return this.context.state.registered ? localize('copilotTitle', "Start using Copilot") : localize('copilotFreeTitle', "Start using Copilot for free");
716
}
717
718
private createDialogFooter(disposables: DisposableStore): HTMLElement {
719
const element = $('.chat-setup-dialog-footer');
720
721
const markdown = this.instantiationService.createInstance(MarkdownRenderer, {});
722
723
// SKU Settings
724
const settings = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "GitHub Copilot Free, Pro and Pro+ may show [public code]({0}) suggestions and we may use your data for product improvement. You can change these [settings]({1}) at any time.", defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl);
725
element.appendChild($('p.setup-settings', undefined, disposables.add(markdown.render(new MarkdownString(settings, { isTrusted: true }))).element));
726
727
return element;
728
}
729
}
730
731
export class ChatSetupContribution extends Disposable implements IWorkbenchContribution {
732
733
static readonly ID = 'workbench.contrib.chatSetup';
734
735
constructor(
736
@IProductService private readonly productService: IProductService,
737
@IInstantiationService private readonly instantiationService: IInstantiationService,
738
@ICommandService private readonly commandService: ICommandService,
739
@ITelemetryService private readonly telemetryService: ITelemetryService,
740
@IChatEntitlementService chatEntitlementService: ChatEntitlementService,
741
@ILogService private readonly logService: ILogService,
742
) {
743
super();
744
745
const context = chatEntitlementService.context?.value;
746
const requests = chatEntitlementService.requests?.value;
747
if (!context || !requests) {
748
return; // disabled
749
}
750
751
const controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, context, requests)));
752
753
this.registerSetupAgents(context, controller);
754
this.registerActions(context, requests, controller);
755
this.registerUrlLinkHandler();
756
}
757
758
private registerSetupAgents(context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): void {
759
const defaultAgentDisposables = markAsSingleton(new MutableDisposable()); // prevents flicker on window reload
760
const vscodeAgentDisposables = markAsSingleton(new MutableDisposable());
761
762
const updateRegistration = () => {
763
if (!context.state.hidden && !context.state.disabled) {
764
765
// Default Agents (always, even if installed to allow for speedy requests right on startup)
766
if (!defaultAgentDisposables.value) {
767
const disposables = defaultAgentDisposables.value = new DisposableStore();
768
769
// Panel Agents
770
const panelAgentDisposables = disposables.add(new DisposableStore());
771
for (const mode of [ChatMode.Ask, ChatMode.Edit, ChatMode.Agent]) {
772
const { agent, disposable } = SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Panel, mode, context, controller);
773
panelAgentDisposables.add(disposable);
774
panelAgentDisposables.add(agent.onUnresolvableError(() => {
775
// An unresolvable error from our agent registrations means that
776
// Copilot is unhealthy for some reason. We clear our panel
777
// registration to give Copilot a chance to show a custom message
778
// to the user from the views and stop pretending as if there was
779
// a functional agent.
780
this.logService.error('[chat setup] Unresolvable error from Copilot agent registration, clearing registration.');
781
panelAgentDisposables.dispose();
782
}));
783
}
784
785
// Inline Agents
786
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Terminal, undefined, context, controller).disposable);
787
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Notebook, undefined, context, controller).disposable);
788
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Editor, undefined, context, controller).disposable);
789
}
790
791
// VSCode Agent + Tool (unless installed and enabled)
792
if (!(context.state.installed && !context.state.disabled) && !vscodeAgentDisposables.value) {
793
const disposables = vscodeAgentDisposables.value = new DisposableStore();
794
795
disposables.add(SetupAgent.registerVSCodeAgent(this.instantiationService, context, controller).disposable);
796
}
797
} else {
798
defaultAgentDisposables.clear();
799
vscodeAgentDisposables.clear();
800
}
801
802
if (context.state.installed && !context.state.disabled) {
803
vscodeAgentDisposables.clear(); // we need to do this to prevent showing duplicate agent/tool entries in the list
804
}
805
};
806
807
this._register(Event.runAndSubscribe(context.onDidChange, () => updateRegistration()));
808
}
809
810
private registerActions(context: ChatEntitlementContext, requests: ChatEntitlementRequests, controller: Lazy<ChatSetupController>): void {
811
const chatSetupTriggerContext = ContextKeyExpr.or(
812
ChatContextKeys.Setup.installed.negate(),
813
ChatContextKeys.Entitlement.canSignUp
814
);
815
816
const CHAT_SETUP_ACTION_LABEL = localize2('triggerChatSetup', "Use AI Features with Copilot for free...");
817
818
class ChatSetupTriggerAction extends Action2 {
819
820
constructor() {
821
super({
822
id: CHAT_SETUP_ACTION_ID,
823
title: CHAT_SETUP_ACTION_LABEL,
824
category: CHAT_CATEGORY,
825
f1: true,
826
precondition: chatSetupTriggerContext
827
});
828
}
829
830
override async run(accessor: ServicesAccessor, mode: ChatMode): Promise<boolean> {
831
const viewsService = accessor.get(IViewsService);
832
const layoutService = accessor.get(IWorkbenchLayoutService);
833
const instantiationService = accessor.get(IInstantiationService);
834
const dialogService = accessor.get(IDialogService);
835
const commandService = accessor.get(ICommandService);
836
const lifecycleService = accessor.get(ILifecycleService);
837
838
await context.update({ hidden: false });
839
840
const chatWidgetPromise = showCopilotView(viewsService, layoutService);
841
if (mode) {
842
const chatWidget = await chatWidgetPromise;
843
chatWidget?.input.setChatMode(mode);
844
}
845
846
const setup = ChatSetup.getInstance(instantiationService, context, controller);
847
const { success } = await setup.run();
848
if (success === false && !lifecycleService.willShutdown) {
849
const { confirmed } = await dialogService.confirm({
850
type: Severity.Error,
851
message: localize('setupErrorDialog', "Copilot setup failed. Would you like to try again?"),
852
primaryButton: localize('retry', "Retry"),
853
});
854
855
if (confirmed) {
856
return Boolean(await commandService.executeCommand(CHAT_SETUP_ACTION_ID));
857
}
858
}
859
860
return Boolean(success);
861
}
862
}
863
864
class ChatSetupTriggerWithoutDialogAction extends Action2 {
865
866
constructor() {
867
super({
868
id: 'workbench.action.chat.triggerSetupWithoutDialog',
869
title: CHAT_SETUP_ACTION_LABEL,
870
precondition: chatSetupTriggerContext
871
});
872
}
873
874
override async run(accessor: ServicesAccessor): Promise<void> {
875
const viewsService = accessor.get(IViewsService);
876
const layoutService = accessor.get(IWorkbenchLayoutService);
877
const instantiationService = accessor.get(IInstantiationService);
878
879
await context.update({ hidden: false });
880
881
const chatWidget = await showCopilotView(viewsService, layoutService);
882
ChatSetup.getInstance(instantiationService, context, controller).skipDialog();
883
chatWidget?.acceptInput(localize('setupCopilot', "Set up Copilot."));
884
}
885
}
886
887
class ChatSetupFromAccountsAction extends Action2 {
888
889
constructor() {
890
super({
891
id: 'workbench.action.chat.triggerSetupFromAccounts',
892
title: localize2('triggerChatSetupFromAccounts', "Sign in to use Copilot..."),
893
menu: {
894
id: MenuId.AccountsContext,
895
group: '2_copilot',
896
when: ContextKeyExpr.and(
897
ChatContextKeys.Setup.hidden.negate(),
898
ChatContextKeys.Setup.installed.negate(),
899
ChatContextKeys.Entitlement.signedOut
900
)
901
}
902
});
903
}
904
905
override async run(accessor: ServicesAccessor): Promise<void> {
906
const commandService = accessor.get(ICommandService);
907
const telemetryService = accessor.get(ITelemetryService);
908
909
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'accounts' });
910
911
return commandService.executeCommand(CHAT_SETUP_ACTION_ID);
912
}
913
}
914
915
class ChatSetupHideAction extends Action2 {
916
917
static readonly ID = 'workbench.action.chat.hideSetup';
918
static readonly TITLE = localize2('hideChatSetup', "Hide Copilot");
919
920
constructor() {
921
super({
922
id: ChatSetupHideAction.ID,
923
title: ChatSetupHideAction.TITLE,
924
f1: true,
925
category: CHAT_CATEGORY,
926
precondition: ContextKeyExpr.and(ChatContextKeys.Setup.installed.negate(), ChatContextKeys.Setup.hidden.negate()),
927
menu: {
928
id: MenuId.ChatTitleBarMenu,
929
group: 'z_hide',
930
order: 1,
931
when: ChatContextKeys.Setup.installed.negate()
932
}
933
});
934
}
935
936
override async run(accessor: ServicesAccessor): Promise<void> {
937
const viewsDescriptorService = accessor.get(IViewDescriptorService);
938
const layoutService = accessor.get(IWorkbenchLayoutService);
939
const dialogService = accessor.get(IDialogService);
940
941
const { confirmed } = await dialogService.confirm({
942
message: localize('hideChatSetupConfirm', "Are you sure you want to hide Copilot?"),
943
detail: localize('hideChatSetupDetail', "You can restore Copilot by running the '{0}' command.", CHAT_SETUP_ACTION_LABEL.value),
944
primaryButton: localize('hideChatSetupButton', "Hide Copilot")
945
});
946
947
if (!confirmed) {
948
return;
949
}
950
951
const location = viewsDescriptorService.getViewLocationById(ChatViewId);
952
953
await context.update({ hidden: true });
954
955
if (location === ViewContainerLocation.AuxiliaryBar) {
956
const activeContainers = viewsDescriptorService.getViewContainersByLocation(location).filter(container => viewsDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0);
957
if (activeContainers.length === 0) {
958
layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); // hide if there are no views in the secondary sidebar
959
}
960
}
961
}
962
}
963
964
const windowFocusListener = this._register(new MutableDisposable());
965
class UpgradePlanAction extends Action2 {
966
constructor() {
967
super({
968
id: 'workbench.action.chat.upgradePlan',
969
title: localize2('managePlan', "Upgrade to Copilot Pro"),
970
category: localize2('chat.category', 'Chat'),
971
f1: true,
972
precondition: ContextKeyExpr.or(
973
ChatContextKeys.Entitlement.canSignUp,
974
ChatContextKeys.Entitlement.limited,
975
),
976
menu: {
977
id: MenuId.ChatTitleBarMenu,
978
group: 'a_first',
979
order: 1,
980
when: ContextKeyExpr.and(
981
ChatContextKeys.Entitlement.limited,
982
ContextKeyExpr.or(
983
ChatContextKeys.chatQuotaExceeded,
984
ChatContextKeys.completionsQuotaExceeded
985
)
986
)
987
}
988
});
989
}
990
991
override async run(accessor: ServicesAccessor, from?: string): Promise<void> {
992
const openerService = accessor.get(IOpenerService);
993
const hostService = accessor.get(IHostService);
994
const commandService = accessor.get(ICommandService);
995
996
openerService.open(URI.parse(defaultChat.upgradePlanUrl));
997
998
const entitlement = context.state.entitlement;
999
if (!isProUser(entitlement)) {
1000
// If the user is not yet Pro, we listen to window focus to refresh the token
1001
// when the user has come back to the window assuming the user signed up.
1002
windowFocusListener.value = hostService.onDidChangeFocus(focus => this.onWindowFocus(focus, commandService));
1003
}
1004
}
1005
1006
private async onWindowFocus(focus: boolean, commandService: ICommandService): Promise<void> {
1007
if (focus) {
1008
windowFocusListener.clear();
1009
1010
const entitlements = await requests.forceResolveEntitlement(undefined);
1011
if (entitlements?.entitlement && isProUser(entitlements?.entitlement)) {
1012
refreshTokens(commandService);
1013
}
1014
}
1015
}
1016
}
1017
1018
class ManageAdditionalSpendAction extends Action2 {
1019
constructor() {
1020
super({
1021
id: 'workbench.action.chat.manageAdditionalSpend',
1022
title: localize2('manageAdditionalSpend', "Manage Copilot Additional Spend"),
1023
category: localize2('chat.category', 'Chat'),
1024
f1: true,
1025
precondition: ContextKeyExpr.or(
1026
ChatContextKeys.Entitlement.pro,
1027
ChatContextKeys.Entitlement.proPlus,
1028
),
1029
menu: {
1030
id: MenuId.ChatTitleBarMenu,
1031
group: 'a_first',
1032
order: 1,
1033
when: ContextKeyExpr.and(
1034
ContextKeyExpr.or(
1035
ChatContextKeys.Entitlement.pro,
1036
ChatContextKeys.Entitlement.proPlus,
1037
),
1038
ContextKeyExpr.or(
1039
ChatContextKeys.chatQuotaExceeded,
1040
ChatContextKeys.completionsQuotaExceeded
1041
)
1042
)
1043
}
1044
});
1045
}
1046
1047
override async run(accessor: ServicesAccessor, from?: string): Promise<void> {
1048
const openerService = accessor.get(IOpenerService);
1049
openerService.open(URI.parse(defaultChat.manageOverageUrl));
1050
}
1051
}
1052
1053
registerAction2(ChatSetupTriggerAction);
1054
registerAction2(ChatSetupFromAccountsAction);
1055
registerAction2(ChatSetupTriggerWithoutDialogAction);
1056
registerAction2(ChatSetupHideAction);
1057
registerAction2(UpgradePlanAction);
1058
registerAction2(ManageAdditionalSpendAction);
1059
}
1060
1061
private registerUrlLinkHandler(): void {
1062
this._register(ExtensionUrlHandlerOverrideRegistry.registerHandler({
1063
canHandleURL: url => {
1064
return url.scheme === this.productService.urlProtocol && equalsIgnoreCase(url.authority, defaultChat.chatExtensionId);
1065
},
1066
handleURL: async url => {
1067
const params = new URLSearchParams(url.query);
1068
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'url', detail: params.get('referrer') ?? undefined });
1069
1070
await this.commandService.executeCommand(CHAT_SETUP_ACTION_ID, validateChatMode(params.get('mode')));
1071
1072
return true;
1073
}
1074
}));
1075
}
1076
}
1077
1078
//#endregion
1079
1080
//#region Setup Controller
1081
1082
type InstallChatClassification = {
1083
owner: 'bpasero';
1084
comment: 'Provides insight into chat installation.';
1085
installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' };
1086
installDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration it took to install the extension.' };
1087
signUpErrorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The error code in case of an error signing up.' };
1088
};
1089
type InstallChatEvent = {
1090
installResult: 'installed' | 'alreadyInstalled' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted' | 'failedNoSession' | 'failedMaybeLater';
1091
installDuration: number;
1092
signUpErrorCode: number | undefined;
1093
};
1094
1095
enum ChatSetupStep {
1096
Initial = 1,
1097
SigningIn,
1098
Installing
1099
}
1100
1101
class ChatSetupController extends Disposable {
1102
1103
private readonly _onDidChange = this._register(new Emitter<void>());
1104
readonly onDidChange = this._onDidChange.event;
1105
1106
private _step = ChatSetupStep.Initial;
1107
get step(): ChatSetupStep { return this._step; }
1108
1109
constructor(
1110
private readonly context: ChatEntitlementContext,
1111
private readonly requests: ChatEntitlementRequests,
1112
@ITelemetryService private readonly telemetryService: ITelemetryService,
1113
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
1114
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
1115
@IProductService private readonly productService: IProductService,
1116
@ILogService private readonly logService: ILogService,
1117
@IProgressService private readonly progressService: IProgressService,
1118
@IActivityService private readonly activityService: IActivityService,
1119
@ICommandService private readonly commandService: ICommandService,
1120
@IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService,
1121
@IDialogService private readonly dialogService: IDialogService,
1122
@IConfigurationService private readonly configurationService: IConfigurationService,
1123
@ILifecycleService private readonly lifecycleService: ILifecycleService,
1124
@IQuickInputService private readonly quickInputService: IQuickInputService,
1125
) {
1126
super();
1127
1128
this.registerListeners();
1129
}
1130
1131
private registerListeners(): void {
1132
this._register(this.context.onDidChange(() => this._onDidChange.fire()));
1133
}
1134
1135
private setStep(step: ChatSetupStep): void {
1136
if (this._step === step) {
1137
return;
1138
}
1139
1140
this._step = step;
1141
this._onDidChange.fire();
1142
}
1143
1144
async setup(options?: { forceSignIn?: boolean }): Promise<boolean> {
1145
const watch = new StopWatch(false);
1146
const title = localize('setupChatProgress', "Getting Copilot ready...");
1147
const badge = this.activityService.showViewContainerActivity(CHAT_SIDEBAR_PANEL_ID, {
1148
badge: new ProgressBadge(() => title),
1149
});
1150
1151
try {
1152
return await this.progressService.withProgress({
1153
location: ProgressLocation.Window,
1154
command: CHAT_OPEN_ACTION_ID,
1155
title,
1156
}, () => this.doSetup(options ?? {}, watch));
1157
} finally {
1158
badge.dispose();
1159
}
1160
}
1161
1162
private async doSetup(options: { forceSignIn?: boolean }, watch: StopWatch): Promise<boolean> {
1163
this.context.suspend(); // reduces flicker
1164
1165
let success = false;
1166
try {
1167
const providerId = ChatEntitlementRequests.providerId(this.configurationService);
1168
let session: AuthenticationSession | undefined;
1169
let entitlement: ChatEntitlement | undefined;
1170
1171
// Entitlement Unknown or `forceSignIn`: we need to sign-in user
1172
if (this.context.state.entitlement === ChatEntitlement.Unknown || options.forceSignIn) {
1173
this.setStep(ChatSetupStep.SigningIn);
1174
const result = await this.signIn(providerId);
1175
if (!result.session) {
1176
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.ChatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined });
1177
return false;
1178
}
1179
1180
session = result.session;
1181
entitlement = result.entitlement;
1182
}
1183
1184
const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({
1185
message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.")
1186
});
1187
if (!trusted) {
1188
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.ChatInstall', { installResult: 'failedNotTrusted', installDuration: watch.elapsed(), signUpErrorCode: undefined });
1189
return false;
1190
}
1191
1192
// Install
1193
this.setStep(ChatSetupStep.Installing);
1194
success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch);
1195
} finally {
1196
this.setStep(ChatSetupStep.Initial);
1197
this.context.resume();
1198
}
1199
1200
return success;
1201
}
1202
1203
private async signIn(providerId: string): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> {
1204
let session: AuthenticationSession | undefined;
1205
let entitlements;
1206
try {
1207
({ session, entitlements } = await this.requests.signIn());
1208
} catch (e) {
1209
this.logService.error(`[chat setup] signIn: error ${e}`);
1210
}
1211
1212
if (!session && !this.lifecycleService.willShutdown) {
1213
const { confirmed } = await this.dialogService.confirm({
1214
type: Severity.Error,
1215
message: localize('unknownSignInError', "Failed to sign in to {0}. Would you like to try again?", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName),
1216
detail: localize('unknownSignInErrorDetail', "You must be signed in to use Copilot."),
1217
primaryButton: localize('retry', "Retry")
1218
});
1219
1220
if (confirmed) {
1221
return this.signIn(providerId);
1222
}
1223
}
1224
1225
return { session, entitlement: entitlements?.entitlement };
1226
}
1227
1228
private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch): Promise<boolean> {
1229
const wasRunning = this.context.state.installed && !this.context.state.disabled;
1230
let signUpResult: boolean | { errorCode: number } | undefined = undefined;
1231
1232
try {
1233
1234
if (
1235
entitlement !== ChatEntitlement.Limited && // User is not signed up to Copilot Free
1236
!isProUser(entitlement) && // User is not signed up for a Copilot subscription
1237
entitlement !== ChatEntitlement.Unavailable // User is eligible for Copilot Free
1238
) {
1239
if (!session) {
1240
try {
1241
session = (await this.authenticationService.getSessions(providerId)).at(0);
1242
} catch (error) {
1243
// ignore - errors can throw if a provider is not registered
1244
}
1245
1246
if (!session) {
1247
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.ChatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined });
1248
return false; // unexpected
1249
}
1250
}
1251
1252
signUpResult = await this.requests.signUpLimited(session);
1253
1254
if (typeof signUpResult !== 'boolean' /* error */) {
1255
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.ChatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode });
1256
}
1257
}
1258
1259
await this.doInstall();
1260
} catch (error) {
1261
this.logService.error(`[chat setup] install: error ${error}`);
1262
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.ChatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined });
1263
return false;
1264
}
1265
1266
if (typeof signUpResult === 'boolean') {
1267
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.ChatInstall', { installResult: wasRunning && !signUpResult ? 'alreadyInstalled' : 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined });
1268
}
1269
1270
if (wasRunning && signUpResult === true) {
1271
refreshTokens(this.commandService);
1272
}
1273
1274
return true;
1275
}
1276
1277
private async doInstall(): Promise<void> {
1278
let error: Error | undefined;
1279
try {
1280
await this.extensionsWorkbenchService.install(defaultChat.extensionId, {
1281
enable: true,
1282
isApplicationScoped: true, // install into all profiles
1283
isMachineScoped: false, // do not ask to sync
1284
installEverywhere: true, // install in local and remote
1285
installPreReleaseVersion: this.productService.quality !== 'stable'
1286
}, ChatViewId);
1287
} catch (e) {
1288
this.logService.error(`[chat setup] install: error ${error}`);
1289
error = e;
1290
}
1291
1292
if (error) {
1293
if (!this.lifecycleService.willShutdown) {
1294
const { confirmed } = await this.dialogService.confirm({
1295
type: Severity.Error,
1296
message: localize('unknownSetupError', "An error occurred while setting up Copilot. Would you like to try again?"),
1297
detail: error && !isCancellationError(error) ? toErrorMessage(error) : undefined,
1298
primaryButton: localize('retry', "Retry")
1299
});
1300
1301
if (confirmed) {
1302
return this.doInstall();
1303
}
1304
}
1305
1306
throw error;
1307
}
1308
}
1309
1310
async setupWithProvider(options: { useEnterpriseProvider: boolean }): Promise<boolean> {
1311
const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
1312
registry.registerConfiguration({
1313
'id': 'copilot.setup',
1314
'type': 'object',
1315
'properties': {
1316
[defaultChat.completionsAdvancedSetting]: {
1317
'type': 'object',
1318
'properties': {
1319
'authProvider': {
1320
'type': 'string'
1321
}
1322
}
1323
},
1324
[defaultChat.providerUriSetting]: {
1325
'type': 'string'
1326
}
1327
}
1328
});
1329
1330
if (options.useEnterpriseProvider) {
1331
const success = await this.handleEnterpriseInstance();
1332
if (!success) {
1333
return false; // not properly configured, abort
1334
}
1335
}
1336
1337
let existingAdvancedSetting = this.configurationService.inspect(defaultChat.completionsAdvancedSetting).user?.value;
1338
if (!isObject(existingAdvancedSetting)) {
1339
existingAdvancedSetting = {};
1340
}
1341
1342
if (options.useEnterpriseProvider) {
1343
await this.configurationService.updateValue(`${defaultChat.completionsAdvancedSetting}`, {
1344
...existingAdvancedSetting,
1345
'authProvider': defaultChat.enterpriseProviderId
1346
}, ConfigurationTarget.USER);
1347
} else {
1348
await this.configurationService.updateValue(`${defaultChat.completionsAdvancedSetting}`, Object.keys(existingAdvancedSetting).length > 0 ? {
1349
...existingAdvancedSetting,
1350
'authProvider': undefined
1351
} : undefined, ConfigurationTarget.USER);
1352
}
1353
1354
return this.setup({ ...options, forceSignIn: true });
1355
}
1356
1357
private async handleEnterpriseInstance(): Promise<boolean /* success */> {
1358
const domainRegEx = /^[a-zA-Z\-_]+$/;
1359
const fullUriRegEx = /^(https:\/\/)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.ghe\.com\/?$/;
1360
1361
const uri = this.configurationService.getValue<string>(defaultChat.providerUriSetting);
1362
if (typeof uri === 'string' && fullUriRegEx.test(uri)) {
1363
return true; // already setup with a valid URI
1364
}
1365
1366
let isSingleWord = false;
1367
const result = await this.quickInputService.input({
1368
prompt: localize('enterpriseInstance', "What is your {0} instance?", defaultChat.enterpriseProviderName),
1369
placeHolder: localize('enterpriseInstancePlaceholder', 'i.e. "octocat" or "https://octocat.ghe.com"...'),
1370
ignoreFocusLost: true,
1371
value: uri,
1372
validateInput: async value => {
1373
isSingleWord = false;
1374
if (!value) {
1375
return undefined;
1376
}
1377
1378
if (domainRegEx.test(value)) {
1379
isSingleWord = true;
1380
return {
1381
content: localize('willResolveTo', "Will resolve to {0}", `https://${value}.ghe.com`),
1382
severity: Severity.Info
1383
};
1384
} if (!fullUriRegEx.test(value)) {
1385
return {
1386
content: localize('invalidEnterpriseInstance', 'You must enter a valid {0} instance (i.e. "octocat" or "https://octocat.ghe.com")', defaultChat.enterpriseProviderName),
1387
severity: Severity.Error
1388
};
1389
}
1390
1391
return undefined;
1392
}
1393
});
1394
1395
if (!result) {
1396
const { confirmed } = await this.dialogService.confirm({
1397
type: Severity.Error,
1398
message: localize('enterpriseSetupError', "The provided {0} instance is invalid. Would you like to enter it again?", defaultChat.enterpriseProviderName),
1399
primaryButton: localize('retry', "Retry")
1400
});
1401
1402
if (confirmed) {
1403
return this.handleEnterpriseInstance();
1404
}
1405
1406
return false;
1407
}
1408
1409
let resolvedUri = result;
1410
if (isSingleWord) {
1411
resolvedUri = `https://${resolvedUri}.ghe.com`;
1412
} else {
1413
const normalizedUri = result.toLowerCase();
1414
const hasHttps = normalizedUri.startsWith('https://');
1415
if (!hasHttps) {
1416
resolvedUri = `https://${result}`;
1417
}
1418
}
1419
1420
await this.configurationService.updateValue(defaultChat.providerUriSetting, resolvedUri, ConfigurationTarget.USER);
1421
1422
return true;
1423
}
1424
}
1425
1426
//#endregion
1427
1428
function refreshTokens(commandService: ICommandService): void {
1429
// ugly, but we need to signal to the extension that entitlements changed
1430
commandService.executeCommand(defaultChat.completionsRefreshTokenCommand);
1431
commandService.executeCommand(defaultChat.chatRefreshTokenCommand);
1432
}
1433
1434