Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.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 './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 { coalesce } from '../../../../../base/common/arrays.js';
11
import { Codicon } from '../../../../../base/common/codicons.js';
12
import { toErrorMessage } from '../../../../../base/common/errorMessage.js';
13
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
14
import { Lazy } from '../../../../../base/common/lazy.js';
15
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
16
import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';
17
import { localize } from '../../../../../nls.js';
18
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
19
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
20
import { createWorkbenchDialogOptions } from '../../../../../platform/dialogs/browser/dialog.js';
21
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
22
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
23
import { ILayoutService } from '../../../../../platform/layout/browser/layoutService.js';
24
import { ILogService } from '../../../../../platform/log/common/log.js';
25
import product from '../../../../../platform/product/common/product.js';
26
import { ITelemetryService, TelemetryLevel } from '../../../../../platform/telemetry/common/telemetry.js';
27
import { IWorkspaceTrustRequestService } from '../../../../../platform/workspace/common/workspaceTrust.js';
28
import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js';
29
import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService, isProUser } from '../../../../services/chat/common/chatEntitlementService.js';
30
import { IChatWidgetService } from '../chat.js';
31
import { ChatSetupController } from './chatSetupController.js';
32
import { IChatSetupResult, ChatSetupAnonymous, InstallChatEvent, InstallChatClassification, ChatSetupStrategy, ChatSetupResultValue } from './chatSetup.js';
33
34
const defaultChat = {
35
publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '',
36
provider: product.defaultChatAgent?.provider ?? { default: { id: '', name: '' }, enterprise: { id: '', name: '' }, apple: { id: '', name: '' }, google: { id: '', name: '' } },
37
manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '',
38
completionsRefreshTokenCommand: product.defaultChatAgent?.completionsRefreshTokenCommand ?? '',
39
chatRefreshTokenCommand: product.defaultChatAgent?.chatRefreshTokenCommand ?? '',
40
termsStatementUrl: product.defaultChatAgent?.termsStatementUrl ?? '',
41
privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? ''
42
};
43
44
export class ChatSetup {
45
46
private static instance: ChatSetup | undefined = undefined;
47
static getInstance(instantiationService: IInstantiationService, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): ChatSetup {
48
let instance = ChatSetup.instance;
49
if (!instance) {
50
instance = ChatSetup.instance = instantiationService.invokeFunction(accessor => {
51
return new ChatSetup(context, controller, accessor.get(ITelemetryService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService) as ChatEntitlementService, accessor.get(ILogService), accessor.get(IConfigurationService), accessor.get(IChatWidgetService), accessor.get(IWorkspaceTrustRequestService), accessor.get(IMarkdownRendererService));
52
});
53
}
54
55
return instance;
56
}
57
58
private pendingRun: Promise<IChatSetupResult> | undefined = undefined;
59
60
private skipDialogOnce = false;
61
62
private constructor(
63
private readonly context: ChatEntitlementContext,
64
private readonly controller: Lazy<ChatSetupController>,
65
@ITelemetryService private readonly telemetryService: ITelemetryService,
66
@ILayoutService private readonly layoutService: IWorkbenchLayoutService,
67
@IKeybindingService private readonly keybindingService: IKeybindingService,
68
@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,
69
@ILogService private readonly logService: ILogService,
70
@IConfigurationService private readonly configurationService: IConfigurationService,
71
@IChatWidgetService private readonly widgetService: IChatWidgetService,
72
@IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService,
73
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
74
) { }
75
76
skipDialog(): void {
77
this.skipDialogOnce = true;
78
}
79
80
async run(options?: { disableChatViewReveal?: boolean; forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous }): Promise<IChatSetupResult> {
81
if (this.pendingRun) {
82
return this.pendingRun;
83
}
84
85
this.pendingRun = this.doRun(options);
86
87
try {
88
return await this.pendingRun;
89
} finally {
90
this.pendingRun = undefined;
91
}
92
}
93
94
private async doRun(options?: { disableChatViewReveal?: boolean; forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous }): Promise<IChatSetupResult> {
95
this.context.update({ later: false });
96
97
const dialogSkipped = this.skipDialogOnce;
98
this.skipDialogOnce = false;
99
100
const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({
101
message: localize('chatWorkspaceTrust', "AI features are currently only supported in trusted workspaces.")
102
});
103
if (!trusted) {
104
this.context.update({ later: true });
105
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: 0, signUpErrorCode: undefined, provider: undefined });
106
107
return { dialogSkipped, success: undefined /* canceled */ };
108
}
109
110
let setupStrategy: ChatSetupStrategy;
111
if (!options?.forceSignInDialog && (dialogSkipped || isProUser(this.chatEntitlementService.entitlement) || this.chatEntitlementService.entitlement === ChatEntitlement.Free)) {
112
setupStrategy = ChatSetupStrategy.DefaultSetup; // existing pro/free users setup without a dialog
113
} else if (options?.forceAnonymous === ChatSetupAnonymous.EnabledWithoutDialog) {
114
setupStrategy = ChatSetupStrategy.DefaultSetup; // anonymous setup without a dialog
115
} else {
116
setupStrategy = await this.showDialog(options);
117
}
118
119
if (setupStrategy === ChatSetupStrategy.DefaultSetup && ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider.enterprise.id) {
120
setupStrategy = ChatSetupStrategy.SetupWithEnterpriseProvider; // users with a configured provider go through provider setup
121
}
122
123
if (setupStrategy !== ChatSetupStrategy.Canceled && !options?.disableChatViewReveal) {
124
// Show the chat view now to better indicate progress
125
// while installing the extension or returning from sign in
126
this.widgetService.revealWidget();
127
}
128
129
let success: ChatSetupResultValue = undefined;
130
try {
131
switch (setupStrategy) {
132
case ChatSetupStrategy.SetupWithEnterpriseProvider:
133
success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: true, useSocialProvider: undefined, additionalScopes: options?.additionalScopes, forceAnonymous: options?.forceAnonymous });
134
break;
135
case ChatSetupStrategy.SetupWithoutEnterpriseProvider:
136
success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: undefined, additionalScopes: options?.additionalScopes, forceAnonymous: options?.forceAnonymous });
137
break;
138
case ChatSetupStrategy.SetupWithAppleProvider:
139
success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: 'apple', additionalScopes: options?.additionalScopes, forceAnonymous: options?.forceAnonymous });
140
break;
141
case ChatSetupStrategy.SetupWithGoogleProvider:
142
success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: 'google', additionalScopes: options?.additionalScopes, forceAnonymous: options?.forceAnonymous });
143
break;
144
case ChatSetupStrategy.DefaultSetup:
145
success = await this.controller.value.setup({ ...options, forceAnonymous: options?.forceAnonymous });
146
break;
147
case ChatSetupStrategy.Canceled:
148
this.context.update({ later: true });
149
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedMaybeLater', installDuration: 0, signUpErrorCode: undefined, provider: undefined });
150
break;
151
}
152
} catch (error) {
153
this.logService.error(`[chat setup] Error during setup: ${toErrorMessage(error)}`);
154
success = false;
155
}
156
157
return { success, dialogSkipped };
158
}
159
160
private async showDialog(options?: { forceSignInDialog?: boolean; forceAnonymous?: ChatSetupAnonymous }): Promise<ChatSetupStrategy> {
161
const disposables = new DisposableStore();
162
163
const buttons = this.getButtons(options);
164
165
const dialog = disposables.add(new Dialog(
166
this.layoutService.activeContainer,
167
this.getDialogTitle(options),
168
buttons.map(button => button[0]),
169
createWorkbenchDialogOptions({
170
type: 'none',
171
extraClasses: ['chat-setup-dialog'],
172
detail: ' ', // workaround allowing us to render the message in large
173
icon: Codicon.copilotLarge,
174
alignment: DialogContentsAlignment.Vertical,
175
cancelId: buttons.length - 1,
176
disableCloseButton: true,
177
renderFooter: footer => footer.appendChild(this.createDialogFooter(disposables, options)),
178
buttonOptions: buttons.map(button => button[2])
179
}, this.keybindingService, this.layoutService)
180
));
181
182
const { button } = await dialog.show();
183
disposables.dispose();
184
185
return buttons[button]?.[1] ?? ChatSetupStrategy.Canceled;
186
}
187
188
private getButtons(options?: { forceSignInDialog?: boolean; forceAnonymous?: ChatSetupAnonymous }): Array<[string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined]> {
189
type ContinueWithButton = [string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined];
190
const styleButton = (...classes: string[]) => ({ styleButton: (button: IButton) => button.element.classList.add(...classes) });
191
192
let buttons: Array<ContinueWithButton>;
193
if (!options?.forceAnonymous && (this.context.state.entitlement === ChatEntitlement.Unknown || options?.forceSignInDialog)) {
194
const defaultProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.default.name), ChatSetupStrategy.SetupWithoutEnterpriseProvider, styleButton('continue-button', 'default')];
195
const defaultProviderLink: ContinueWithButton = [defaultProviderButton[0], defaultProviderButton[1], styleButton('link-button')];
196
197
const enterpriseProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.enterprise.name), ChatSetupStrategy.SetupWithEnterpriseProvider, styleButton('continue-button', 'default')];
198
const enterpriseProviderLink: ContinueWithButton = [enterpriseProviderButton[0], enterpriseProviderButton[1], styleButton('link-button')];
199
200
const googleProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.google.name), ChatSetupStrategy.SetupWithGoogleProvider, styleButton('continue-button', 'google')];
201
const appleProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.apple.name), ChatSetupStrategy.SetupWithAppleProvider, styleButton('continue-button', 'apple')];
202
203
if (ChatEntitlementRequests.providerId(this.configurationService) !== defaultChat.provider.enterprise.id) {
204
buttons = coalesce([
205
defaultProviderButton,
206
googleProviderButton,
207
appleProviderButton,
208
enterpriseProviderLink
209
]);
210
} else {
211
buttons = coalesce([
212
enterpriseProviderButton,
213
googleProviderButton,
214
appleProviderButton,
215
defaultProviderLink
216
]);
217
}
218
} else {
219
buttons = [[localize('setupAIButton', "Use AI Features"), ChatSetupStrategy.DefaultSetup, undefined]];
220
}
221
222
buttons.push([localize('skipForNow', "Skip for now"), ChatSetupStrategy.Canceled, styleButton('link-button', 'skip-button')]);
223
224
return buttons;
225
}
226
227
private getDialogTitle(options?: { forceSignInDialog?: boolean; forceAnonymous?: ChatSetupAnonymous }): string {
228
if (this.chatEntitlementService.anonymous) {
229
if (options?.forceAnonymous) {
230
return localize('startUsing', "Start using AI Features");
231
} else {
232
return localize('enableMore', "Enable more AI features");
233
}
234
}
235
236
if (this.context.state.entitlement === ChatEntitlement.Unknown || options?.forceSignInDialog) {
237
return localize('signIn', "Sign in to use AI Features");
238
}
239
240
return localize('startUsing', "Start using AI Features");
241
}
242
243
private createDialogFooter(disposables: DisposableStore, options?: { forceAnonymous?: ChatSetupAnonymous }): HTMLElement {
244
const element = $('.chat-setup-dialog-footer');
245
246
247
let footer: string;
248
if (options?.forceAnonymous || this.telemetryService.telemetryLevel === TelemetryLevel.NONE) {
249
footer = localize({ key: 'settingsAnonymous', comment: ['{Locked="["}', '{Locked="]({1})"}', '{Locked="]({2})"}'] }, "By continuing, you agree to {0}'s [Terms]({1}) and [Privacy Statement]({2}).", defaultChat.provider.default.name, defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl);
250
} else {
251
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);
252
}
253
element.appendChild($('p', undefined, disposables.add(this.markdownRendererService.render(new MarkdownString(footer, { isTrusted: true }))).element));
254
255
return element;
256
}
257
}
258
259
//#endregion
260
261
export function refreshTokens(commandService: ICommandService): void {
262
// ugly, but we need to signal to the extension that entitlements changed
263
commandService.executeCommand(defaultChat.completionsRefreshTokenCommand);
264
commandService.executeCommand(defaultChat.chatRefreshTokenCommand);
265
}
266
267