Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupController.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { toErrorMessage } from '../../../../../base/common/errorMessage.js';
7
import { isCancellationError } from '../../../../../base/common/errors.js';
8
import { Emitter } from '../../../../../base/common/event.js';
9
import { Disposable } from '../../../../../base/common/lifecycle.js';
10
import Severity from '../../../../../base/common/severity.js';
11
import { StopWatch } from '../../../../../base/common/stopwatch.js';
12
import { isObject } from '../../../../../base/common/types.js';
13
import { localize } from '../../../../../nls.js';
14
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
15
import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
16
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../../platform/configuration/common/configurationRegistry.js';
17
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
18
import { ILogService } from '../../../../../platform/log/common/log.js';
19
import product from '../../../../../platform/product/common/product.js';
20
import { IProductService } from '../../../../../platform/product/common/productService.js';
21
import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js';
22
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
23
import { Registry } from '../../../../../platform/registry/common/platform.js';
24
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
25
import { IActivityService, ProgressBadge } from '../../../../services/activity/common/activity.js';
26
import { AuthenticationSession, IAuthenticationService } from '../../../../services/authentication/common/authentication.js';
27
import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
28
import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
29
import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, isProUser } from '../../../../services/chat/common/chatEntitlementService.js';
30
import { CHAT_OPEN_ACTION_ID } from '../actions/chatActions.js';
31
import { ChatViewId, ChatViewContainerId } from '../chat.js';
32
import { ChatSetupAnonymous, ChatSetupStep, ChatSetupResultValue, InstallChatEvent, InstallChatClassification, refreshTokens } from './chatSetup.js';
33
34
const defaultChat = {
35
chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '',
36
provider: product.defaultChatAgent?.provider ?? { default: { id: '', name: '' }, enterprise: { id: '', name: '' }, apple: { id: '', name: '' }, google: { id: '', name: '' } },
37
providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '',
38
completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '',
39
};
40
41
export interface IChatSetupControllerOptions {
42
readonly forceSignIn?: boolean;
43
readonly useSocialProvider?: string;
44
readonly useEnterpriseProvider?: boolean;
45
readonly additionalScopes?: readonly string[];
46
readonly forceAnonymous?: ChatSetupAnonymous;
47
}
48
49
export class ChatSetupController extends Disposable {
50
51
private readonly _onDidChange = this._register(new Emitter<void>());
52
readonly onDidChange = this._onDidChange.event;
53
54
private _step = ChatSetupStep.Initial;
55
get step(): ChatSetupStep { return this._step; }
56
57
constructor(
58
private readonly context: ChatEntitlementContext,
59
private readonly requests: ChatEntitlementRequests,
60
@ITelemetryService private readonly telemetryService: ITelemetryService,
61
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
62
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
63
@IProductService private readonly productService: IProductService,
64
@ILogService private readonly logService: ILogService,
65
@IProgressService private readonly progressService: IProgressService,
66
@IActivityService private readonly activityService: IActivityService,
67
@ICommandService private readonly commandService: ICommandService,
68
@IDialogService private readonly dialogService: IDialogService,
69
@IConfigurationService private readonly configurationService: IConfigurationService,
70
@ILifecycleService private readonly lifecycleService: ILifecycleService,
71
@IQuickInputService private readonly quickInputService: IQuickInputService,
72
) {
73
super();
74
75
this.registerListeners();
76
}
77
78
private registerListeners(): void {
79
this._register(this.context.onDidChange(() => this._onDidChange.fire()));
80
}
81
82
private setStep(step: ChatSetupStep): void {
83
if (this._step === step) {
84
return;
85
}
86
87
this._step = step;
88
this._onDidChange.fire();
89
}
90
91
async setup(options: IChatSetupControllerOptions = {}): Promise<ChatSetupResultValue> {
92
const watch = new StopWatch(false);
93
const title = localize('setupChatProgress', "Getting chat ready...");
94
const badge = this.activityService.showViewContainerActivity(ChatViewContainerId, {
95
badge: new ProgressBadge(() => title),
96
});
97
98
try {
99
return await this.progressService.withProgress({
100
location: ProgressLocation.Window,
101
command: CHAT_OPEN_ACTION_ID,
102
title,
103
}, () => this.doSetup(options, watch));
104
} finally {
105
badge.dispose();
106
}
107
}
108
109
private async doSetup(options: IChatSetupControllerOptions, watch: StopWatch): Promise<ChatSetupResultValue> {
110
this.context.suspend(); // reduces flicker
111
112
let success: ChatSetupResultValue = false;
113
try {
114
const providerId = ChatEntitlementRequests.providerId(this.configurationService);
115
let session: AuthenticationSession | undefined;
116
let entitlement: ChatEntitlement | undefined;
117
118
let signIn: boolean;
119
if (options.forceSignIn) {
120
signIn = true; // forced to sign in
121
} else if (this.context.state.entitlement === ChatEntitlement.Unknown) {
122
if (options.forceAnonymous) {
123
signIn = false; // forced to anonymous without sign in
124
} else {
125
signIn = true; // sign in since we are signed out
126
}
127
} else {
128
signIn = false; // already signed in
129
}
130
131
if (signIn) {
132
this.setStep(ChatSetupStep.SigningIn);
133
const result = await this.signIn(options);
134
if (!result.session) {
135
this.doInstall(); // still install the extension in the background to remind the user to sign-in eventually
136
137
const provider = options.useSocialProvider ?? (options.useEnterpriseProvider ? defaultChat.provider.enterprise.id : defaultChat.provider.default.id);
138
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider });
139
return undefined; // treat as cancelled because signing in already triggers an error dialog
140
}
141
142
session = result.session;
143
entitlement = result.entitlement;
144
}
145
146
// Await Install
147
this.setStep(ChatSetupStep.Installing);
148
success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch, options);
149
} finally {
150
this.setStep(ChatSetupStep.Initial);
151
this.context.resume();
152
}
153
154
return success;
155
}
156
157
private async signIn(options: IChatSetupControllerOptions): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> {
158
let session: AuthenticationSession | undefined;
159
let entitlements;
160
try {
161
({ session, entitlements } = await this.requests.signIn(options));
162
} catch (e) {
163
this.logService.error(`[chat setup] signIn: error ${e}`);
164
}
165
166
if (!session && !this.lifecycleService.willShutdown) {
167
const { confirmed } = await this.dialogService.confirm({
168
type: Severity.Error,
169
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),
170
detail: localize('unknownSignInErrorDetail', "You must be signed in to use AI features."),
171
primaryButton: localize('retry', "Retry")
172
});
173
174
if (confirmed) {
175
return this.signIn(options);
176
}
177
}
178
179
return { session, entitlement: entitlements?.entitlement };
180
}
181
182
private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch, options: IChatSetupControllerOptions): Promise<ChatSetupResultValue> {
183
const wasRunning = this.context.state.installed && !this.context.state.disabled;
184
let signUpResult: boolean | { errorCode: number } | undefined = undefined;
185
186
let provider: string;
187
if (options.forceAnonymous && entitlement === ChatEntitlement.Unknown) {
188
provider = 'anonymous';
189
} else {
190
provider = options.useSocialProvider ?? (options.useEnterpriseProvider ? defaultChat.provider.enterprise.id : defaultChat.provider.default.id);
191
}
192
193
let sessions = session ? [session] : undefined;
194
try {
195
if (
196
!options.forceAnonymous && // User is not asking for anonymous access
197
entitlement !== ChatEntitlement.Free && // User is not signed up to Copilot Free
198
!isProUser(entitlement) && // User is not signed up for a Copilot subscription
199
entitlement !== ChatEntitlement.Unavailable // User is eligible for Copilot Free
200
) {
201
if (!sessions) {
202
try {
203
// Consider all sessions for the provider to be suitable for signing up
204
const existingSessions = await this.authenticationService.getSessions(providerId);
205
sessions = existingSessions.length > 0 ? [...existingSessions] : undefined;
206
} catch (error) {
207
// ignore - errors can throw if a provider is not registered
208
}
209
210
if (!sessions || sessions.length === 0) {
211
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider });
212
return false; // unexpected
213
}
214
}
215
216
signUpResult = await this.requests.signUpFree(sessions);
217
218
if (typeof signUpResult !== 'boolean' /* error */) {
219
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode, provider });
220
}
221
}
222
223
await this.doInstallWithRetry();
224
} catch (error) {
225
this.logService.error(`[chat setup] install: error ${error}`);
226
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider });
227
return false;
228
}
229
230
if (typeof signUpResult === 'boolean' /* not an error case */ || typeof signUpResult === 'undefined' /* already signed up */) {
231
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: wasRunning && !signUpResult ? 'alreadyInstalled' : 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider });
232
}
233
234
if (wasRunning) {
235
// We always trigger refresh of tokens to help the user
236
// get out of authentication issues that can happen when
237
// for example the sign-up ran after the extension tried
238
// to use the authentication information to mint a token
239
refreshTokens(this.commandService);
240
}
241
242
return true;
243
}
244
245
private async doInstallWithRetry(): Promise<void> {
246
let error: Error | undefined;
247
try {
248
await this.doInstall();
249
} catch (e) {
250
this.logService.error(`[chat setup] install: error ${error}`);
251
error = e;
252
}
253
254
if (error) {
255
if (!this.lifecycleService.willShutdown) {
256
const { confirmed } = await this.dialogService.confirm({
257
type: Severity.Error,
258
message: localize('unknownSetupError', "An error occurred while setting up chat. Would you like to try again?"),
259
detail: error && !isCancellationError(error) ? toErrorMessage(error) : undefined,
260
primaryButton: localize('retry', "Retry")
261
});
262
263
if (confirmed) {
264
return this.doInstallWithRetry();
265
}
266
}
267
268
throw error;
269
}
270
}
271
272
private async doInstall(): Promise<void> {
273
await this.extensionsWorkbenchService.install(defaultChat.chatExtensionId, {
274
enable: true,
275
isApplicationScoped: true, // install into all profiles
276
isMachineScoped: false, // do not ask to sync
277
installEverywhere: true, // install in local and remote
278
installPreReleaseVersion: this.productService.quality !== 'stable'
279
}, ChatViewId);
280
}
281
282
async setupWithProvider(options: IChatSetupControllerOptions): Promise<ChatSetupResultValue> {
283
const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
284
registry.registerConfiguration({
285
'id': 'copilot.setup',
286
'type': 'object',
287
'properties': {
288
[defaultChat.completionsAdvancedSetting]: {
289
'type': 'object',
290
'properties': {
291
'authProvider': {
292
'type': 'string'
293
}
294
}
295
},
296
[defaultChat.providerUriSetting]: {
297
'type': 'string'
298
}
299
}
300
});
301
302
if (options.useEnterpriseProvider) {
303
const success = await this.handleEnterpriseInstance();
304
if (!success) {
305
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedEnterpriseSetup', installDuration: 0, signUpErrorCode: undefined, provider: undefined });
306
return success; // not properly configured, abort
307
}
308
}
309
310
let existingAdvancedSetting = this.configurationService.inspect(defaultChat.completionsAdvancedSetting).user?.value;
311
if (!isObject(existingAdvancedSetting)) {
312
existingAdvancedSetting = {};
313
}
314
315
if (options.useEnterpriseProvider) {
316
await this.configurationService.updateValue(`${defaultChat.completionsAdvancedSetting}`, {
317
...existingAdvancedSetting,
318
'authProvider': defaultChat.provider.enterprise.id
319
}, ConfigurationTarget.USER);
320
} else {
321
await this.configurationService.updateValue(`${defaultChat.completionsAdvancedSetting}`, Object.keys(existingAdvancedSetting).length > 0 ? {
322
...existingAdvancedSetting,
323
'authProvider': undefined
324
} : undefined, ConfigurationTarget.USER);
325
}
326
327
return this.setup({ ...options, forceSignIn: true });
328
}
329
330
private async handleEnterpriseInstance(): Promise<ChatSetupResultValue> {
331
const domainRegEx = /^[a-zA-Z\-_]+$/;
332
const fullUriRegEx = /^(https:\/\/)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.ghe\.com\/?$/;
333
334
const uri = this.configurationService.getValue<string>(defaultChat.providerUriSetting);
335
if (typeof uri === 'string' && fullUriRegEx.test(uri)) {
336
return true; // already setup with a valid URI
337
}
338
339
let isSingleWord = false;
340
const result = await this.quickInputService.input({
341
prompt: localize('enterpriseInstance', "What is your {0} instance?", defaultChat.provider.enterprise.name),
342
placeHolder: localize('enterpriseInstancePlaceholder', 'i.e. "octocat" or "https://octocat.ghe.com"...'),
343
ignoreFocusLost: true,
344
value: uri,
345
validateInput: async value => {
346
isSingleWord = false;
347
if (!value) {
348
return undefined;
349
}
350
351
if (domainRegEx.test(value)) {
352
isSingleWord = true;
353
return {
354
content: localize('willResolveTo', "Will resolve to {0}", `https://${value}.ghe.com`),
355
severity: Severity.Info
356
};
357
} if (!fullUriRegEx.test(value)) {
358
return {
359
content: localize('invalidEnterpriseInstance', 'You must enter a valid {0} instance (i.e. "octocat" or "https://octocat.ghe.com")', defaultChat.provider.enterprise.name),
360
severity: Severity.Error
361
};
362
}
363
364
return undefined;
365
}
366
});
367
368
if (!result) {
369
return undefined; // canceled
370
}
371
372
let resolvedUri = result;
373
if (isSingleWord) {
374
resolvedUri = `https://${resolvedUri}.ghe.com`;
375
} else {
376
const normalizedUri = result.toLowerCase();
377
const hasHttps = normalizedUri.startsWith('https://');
378
if (!hasHttps) {
379
resolvedUri = `https://${result}`;
380
}
381
}
382
383
await this.configurationService.updateValue(defaultChat.providerUriSetting, resolvedUri, ConfigurationTarget.USER);
384
385
return true;
386
}
387
}
388
389