Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/chatSetup.css';6import { $ } from '../../../../../base/browser/dom.js';7import { IButton } from '../../../../../base/browser/ui/button/button.js';8import { Dialog, DialogContentsAlignment } from '../../../../../base/browser/ui/dialog/dialog.js';9import { coalesce } from '../../../../../base/common/arrays.js';10import { Codicon } from '../../../../../base/common/codicons.js';11import { toErrorMessage } from '../../../../../base/common/errorMessage.js';12import { MarkdownString } from '../../../../../base/common/htmlContent.js';13import { Lazy } from '../../../../../base/common/lazy.js';14import { DisposableStore } from '../../../../../base/common/lifecycle.js';15import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';16import { localize } from '../../../../../nls.js';17import { ICommandService } from '../../../../../platform/commands/common/commands.js';18import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';19import { createWorkbenchDialogOptions } from '../../../../../platform/dialogs/browser/dialog.js';20import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';21import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';22import { ILayoutService } from '../../../../../platform/layout/browser/layoutService.js';23import { ILogService } from '../../../../../platform/log/common/log.js';24import product from '../../../../../platform/product/common/product.js';25import { ITelemetryService, TelemetryLevel } from '../../../../../platform/telemetry/common/telemetry.js';26import { IWorkspaceTrustRequestService } from '../../../../../platform/workspace/common/workspaceTrust.js';27import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js';28import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService, isProUser } from '../../../../services/chat/common/chatEntitlementService.js';29import { IChatWidgetService } from '../chat.js';30import { ChatSetupController } from './chatSetupController.js';31import { IChatSetupResult, ChatSetupAnonymous, InstallChatEvent, InstallChatClassification, ChatSetupStrategy, ChatSetupResultValue } from './chatSetup.js';3233const defaultChat = {34publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '',35provider: product.defaultChatAgent?.provider ?? { default: { id: '', name: '' }, enterprise: { id: '', name: '' }, apple: { id: '', name: '' }, google: { id: '', name: '' } },36manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '',37completionsRefreshTokenCommand: product.defaultChatAgent?.completionsRefreshTokenCommand ?? '',38chatRefreshTokenCommand: product.defaultChatAgent?.chatRefreshTokenCommand ?? '',39termsStatementUrl: product.defaultChatAgent?.termsStatementUrl ?? '',40privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? ''41};4243export class ChatSetup {4445private static instance: ChatSetup | undefined = undefined;46static getInstance(instantiationService: IInstantiationService, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): ChatSetup {47let instance = ChatSetup.instance;48if (!instance) {49instance = ChatSetup.instance = instantiationService.invokeFunction(accessor => {50return 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));51});52}5354return instance;55}5657private pendingRun: Promise<IChatSetupResult> | undefined = undefined;5859private skipDialogOnce = false;6061private constructor(62private readonly context: ChatEntitlementContext,63private readonly controller: Lazy<ChatSetupController>,64@ITelemetryService private readonly telemetryService: ITelemetryService,65@ILayoutService private readonly layoutService: IWorkbenchLayoutService,66@IKeybindingService private readonly keybindingService: IKeybindingService,67@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,68@ILogService private readonly logService: ILogService,69@IConfigurationService private readonly configurationService: IConfigurationService,70@IChatWidgetService private readonly widgetService: IChatWidgetService,71@IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService,72@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,73) { }7475skipDialog(): void {76this.skipDialogOnce = true;77}7879async run(options?: { disableChatViewReveal?: boolean; forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous }): Promise<IChatSetupResult> {80if (this.pendingRun) {81return this.pendingRun;82}8384this.pendingRun = this.doRun(options);8586try {87return await this.pendingRun;88} finally {89this.pendingRun = undefined;90}91}9293private async doRun(options?: { disableChatViewReveal?: boolean; forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous }): Promise<IChatSetupResult> {94this.context.update({ later: false });9596const dialogSkipped = this.skipDialogOnce;97this.skipDialogOnce = false;9899const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({100message: localize('chatWorkspaceTrust', "AI features are currently only supported in trusted workspaces.")101});102if (!trusted) {103this.context.update({ later: true });104this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: 0, signUpErrorCode: undefined, provider: undefined });105106return { dialogSkipped, success: undefined /* canceled */ };107}108109let setupStrategy: ChatSetupStrategy;110if (!options?.forceSignInDialog && (dialogSkipped || isProUser(this.chatEntitlementService.entitlement) || this.chatEntitlementService.entitlement === ChatEntitlement.Free)) {111setupStrategy = ChatSetupStrategy.DefaultSetup; // existing pro/free users setup without a dialog112} else if (options?.forceAnonymous === ChatSetupAnonymous.EnabledWithoutDialog) {113setupStrategy = ChatSetupStrategy.DefaultSetup; // anonymous setup without a dialog114} else {115setupStrategy = await this.showDialog(options);116}117118if (setupStrategy === ChatSetupStrategy.DefaultSetup && ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider.enterprise.id) {119setupStrategy = ChatSetupStrategy.SetupWithEnterpriseProvider; // users with a configured provider go through provider setup120}121122if (setupStrategy !== ChatSetupStrategy.Canceled && !options?.disableChatViewReveal) {123// Show the chat view now to better indicate progress124// while installing the extension or returning from sign in125this.widgetService.revealWidget();126}127128let success: ChatSetupResultValue = undefined;129try {130switch (setupStrategy) {131case ChatSetupStrategy.SetupWithEnterpriseProvider:132success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: true, useSocialProvider: undefined, additionalScopes: options?.additionalScopes, forceAnonymous: options?.forceAnonymous });133break;134case ChatSetupStrategy.SetupWithoutEnterpriseProvider:135success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: undefined, additionalScopes: options?.additionalScopes, forceAnonymous: options?.forceAnonymous });136break;137case ChatSetupStrategy.SetupWithAppleProvider:138success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: 'apple', additionalScopes: options?.additionalScopes, forceAnonymous: options?.forceAnonymous });139break;140case ChatSetupStrategy.SetupWithGoogleProvider:141success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: 'google', additionalScopes: options?.additionalScopes, forceAnonymous: options?.forceAnonymous });142break;143case ChatSetupStrategy.DefaultSetup:144success = await this.controller.value.setup({ ...options, forceAnonymous: options?.forceAnonymous });145break;146case ChatSetupStrategy.Canceled:147this.context.update({ later: true });148this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedMaybeLater', installDuration: 0, signUpErrorCode: undefined, provider: undefined });149break;150}151} catch (error) {152this.logService.error(`[chat setup] Error during setup: ${toErrorMessage(error)}`);153success = false;154}155156return { success, dialogSkipped };157}158159private async showDialog(options?: { forceSignInDialog?: boolean; forceAnonymous?: ChatSetupAnonymous }): Promise<ChatSetupStrategy> {160const disposables = new DisposableStore();161162const buttons = this.getButtons(options);163164const dialog = disposables.add(new Dialog(165this.layoutService.activeContainer,166this.getDialogTitle(options),167buttons.map(button => button[0]),168createWorkbenchDialogOptions({169type: 'none',170extraClasses: ['chat-setup-dialog'],171detail: ' ', // workaround allowing us to render the message in large172icon: Codicon.copilotLarge,173alignment: DialogContentsAlignment.Vertical,174cancelId: buttons.length - 1,175disableCloseButton: true,176renderFooter: footer => footer.appendChild(this.createDialogFooter(disposables, options)),177buttonOptions: buttons.map(button => button[2])178}, this.keybindingService, this.layoutService)179));180181const { button } = await dialog.show();182disposables.dispose();183184return buttons[button]?.[1] ?? ChatSetupStrategy.Canceled;185}186187private getButtons(options?: { forceSignInDialog?: boolean; forceAnonymous?: ChatSetupAnonymous }): Array<[string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined]> {188type ContinueWithButton = [string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined];189const styleButton = (...classes: string[]) => ({ styleButton: (button: IButton) => button.element.classList.add(...classes) });190191let buttons: Array<ContinueWithButton>;192if (!options?.forceAnonymous && (this.context.state.entitlement === ChatEntitlement.Unknown || options?.forceSignInDialog)) {193const defaultProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.default.name), ChatSetupStrategy.SetupWithoutEnterpriseProvider, styleButton('continue-button', 'default')];194const defaultProviderLink: ContinueWithButton = [defaultProviderButton[0], defaultProviderButton[1], styleButton('link-button')];195196const enterpriseProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.enterprise.name), ChatSetupStrategy.SetupWithEnterpriseProvider, styleButton('continue-button', 'default')];197const enterpriseProviderLink: ContinueWithButton = [enterpriseProviderButton[0], enterpriseProviderButton[1], styleButton('link-button')];198199const googleProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.google.name), ChatSetupStrategy.SetupWithGoogleProvider, styleButton('continue-button', 'google')];200const appleProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.apple.name), ChatSetupStrategy.SetupWithAppleProvider, styleButton('continue-button', 'apple')];201202if (ChatEntitlementRequests.providerId(this.configurationService) !== defaultChat.provider.enterprise.id) {203buttons = coalesce([204defaultProviderButton,205googleProviderButton,206appleProviderButton,207enterpriseProviderLink208]);209} else {210buttons = coalesce([211enterpriseProviderButton,212googleProviderButton,213appleProviderButton,214defaultProviderLink215]);216}217} else {218buttons = [[localize('setupAIButton', "Use AI Features"), ChatSetupStrategy.DefaultSetup, undefined]];219}220221buttons.push([localize('skipForNow', "Skip for now"), ChatSetupStrategy.Canceled, styleButton('link-button', 'skip-button')]);222223return buttons;224}225226private getDialogTitle(options?: { forceSignInDialog?: boolean; forceAnonymous?: ChatSetupAnonymous }): string {227if (this.chatEntitlementService.anonymous) {228if (options?.forceAnonymous) {229return localize('startUsing', "Start using AI Features");230} else {231return localize('enableMore', "Enable more AI features");232}233}234235if (this.context.state.entitlement === ChatEntitlement.Unknown || options?.forceSignInDialog) {236return localize('signIn', "Sign in to use AI Features");237}238239return localize('startUsing', "Start using AI Features");240}241242private createDialogFooter(disposables: DisposableStore, options?: { forceAnonymous?: ChatSetupAnonymous }): HTMLElement {243const element = $('.chat-setup-dialog-footer');244245246let footer: string;247if (options?.forceAnonymous || this.telemetryService.telemetryLevel === TelemetryLevel.NONE) {248footer = 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);249} else {250footer = 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);251}252element.appendChild($('p', undefined, disposables.add(this.markdownRendererService.render(new MarkdownString(footer, { isTrusted: true }))).element));253254return element;255}256}257258//#endregion259260export function refreshTokens(commandService: ICommandService): void {261// ugly, but we need to signal to the extension that entitlements changed262commandService.executeCommand(defaultChat.completionsRefreshTokenCommand);263commandService.executeCommand(defaultChat.chatRefreshTokenCommand);264}265266267