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