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