Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSetup.ts
3296 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 { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js';10import { coalesce } from '../../../../base/common/arrays.js';11import { timeout } from '../../../../base/common/async.js';12import { CancellationToken } from '../../../../base/common/cancellation.js';13import { Codicon } from '../../../../base/common/codicons.js';14import { toErrorMessage } from '../../../../base/common/errorMessage.js';15import { isCancellationError } from '../../../../base/common/errors.js';16import { Emitter, Event } from '../../../../base/common/event.js';17import { MarkdownString } from '../../../../base/common/htmlContent.js';18import { Lazy } from '../../../../base/common/lazy.js';19import { Disposable, DisposableStore, IDisposable, markAsSingleton, MutableDisposable } from '../../../../base/common/lifecycle.js';20import Severity from '../../../../base/common/severity.js';21import { StopWatch } from '../../../../base/common/stopwatch.js';22import { equalsIgnoreCase } from '../../../../base/common/strings.js';23import { isObject } from '../../../../base/common/types.js';24import { URI } from '../../../../base/common/uri.js';25import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';26import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';27import { localize, localize2 } from '../../../../nls.js';28import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';29import { ICommandService } from '../../../../platform/commands/common/commands.js';30import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';31import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';32import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';33import { createWorkbenchDialogOptions } from '../../../../platform/dialogs/browser/dialog.js';34import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';35import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';36import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';37import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';38import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';39import { ILogService } from '../../../../platform/log/common/log.js';40import { IOpenerService } from '../../../../platform/opener/common/opener.js';41import product from '../../../../platform/product/common/product.js';42import { IProductService } from '../../../../platform/product/common/productService.js';43import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';44import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';45import { Registry } from '../../../../platform/registry/common/platform.js';46import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js';47import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js';48import { IWorkbenchContribution } from '../../../common/contributions.js';49import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js';50import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js';51import { AuthenticationSession, IAuthenticationService } from '../../../services/authentication/common/authentication.js';52import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';53import { EnablementState, IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js';54import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extensions/browser/extensionUrlHandler.js';55import { nullExtensionDescription } from '../../../services/extensions/common/extensions.js';56import { IHostService } from '../../../services/host/browser/host.js';57import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js';58import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';59import { IViewsService } from '../../../services/views/common/viewsService.js';60import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../../chat/common/languageModelToolsService.js';61import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';62import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../common/chatAgents.js';63import { ChatContextKeys } from '../common/chatContextKeys.js';64import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService, isProUser } from '../common/chatEntitlementService.js';65import { ChatModel, ChatRequestModel, IChatRequestModel, IChatRequestVariableData } from '../common/chatModel.js';66import { ChatMode } from '../common/chatModes.js';67import { ChatRequestAgentPart, ChatRequestToolPart } from '../common/chatParserTypes.js';68import { IChatProgress, IChatService } from '../common/chatService.js';69import { IChatRequestToolEntry } from '../common/chatVariableEntries.js';70import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../common/constants.js';71import { ILanguageModelsService } from '../common/languageModels.js';72import { CHAT_CATEGORY, CHAT_OPEN_ACTION_ID, CHAT_SETUP_ACTION_ID } from './actions/chatActions.js';73import { ChatViewId, IChatWidgetService, showCopilotView } from './chat.js';74import { CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js';75import { chatViewsWelcomeRegistry } from './viewsWelcome/chatViewsWelcome.js';76import { IPreferencesService } from '../../../services/preferences/common/preferences.js';7778const defaultChat = {79extensionId: product.defaultChatAgent?.extensionId ?? '',80chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '',81documentationUrl: product.defaultChatAgent?.documentationUrl ?? '',82skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '',83publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '',84manageOveragesUrl: product.defaultChatAgent?.manageOverageUrl ?? '',85upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '',86provider: product.defaultChatAgent?.provider ?? { default: { id: '', name: '' }, enterprise: { id: '', name: '' }, apple: { id: '', name: '' }, google: { id: '', name: '' } },87providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '',88providerScopes: product.defaultChatAgent?.providerScopes ?? [[]],89manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '',90completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '',91walkthroughCommand: product.defaultChatAgent?.walkthroughCommand ?? '',92completionsRefreshTokenCommand: product.defaultChatAgent?.completionsRefreshTokenCommand ?? '',93chatRefreshTokenCommand: product.defaultChatAgent?.chatRefreshTokenCommand ?? '',94termsStatementUrl: product.defaultChatAgent?.termsStatementUrl ?? '',95privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? ''96};9798//#region Contribution99100const ToolsAgentContextKey = ContextKeyExpr.and(101ContextKeyExpr.equals(`config.${ChatConfiguration.AgentEnabled}`, true),102ContextKeyExpr.not(`previewFeaturesDisabled`) // Set by extension103);104105class SetupAgent extends Disposable implements IChatAgentImplementation {106107static registerDefaultAgents(instantiationService: IInstantiationService, location: ChatAgentLocation, mode: ChatModeKind | undefined, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): { agent: SetupAgent; disposable: IDisposable } {108return instantiationService.invokeFunction(accessor => {109const chatAgentService = accessor.get(IChatAgentService);110111let id: string;112let description = ChatMode.Ask.description.get();113switch (location) {114case ChatAgentLocation.Panel:115if (mode === ChatModeKind.Ask) {116id = 'setup.chat';117} else if (mode === ChatModeKind.Edit) {118id = 'setup.edits';119description = ChatMode.Edit.description.get();120} else {121id = 'setup.agent';122description = ChatMode.Agent.description.get();123}124break;125case ChatAgentLocation.Terminal:126id = 'setup.terminal';127break;128case ChatAgentLocation.Editor:129id = 'setup.editor';130break;131case ChatAgentLocation.Notebook:132id = 'setup.notebook';133break;134}135136return SetupAgent.doRegisterAgent(instantiationService, chatAgentService, id, `${defaultChat.provider.default.name} Copilot`, true, description, location, mode, context, controller);137});138}139140static registerBuiltInAgents(instantiationService: IInstantiationService, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): { disposable: IDisposable } {141return instantiationService.invokeFunction(accessor => {142const chatAgentService = accessor.get(IChatAgentService);143144const disposables = new DisposableStore();145146// Register VSCode agent147const { disposable: vscodeDisposable } = SetupAgent.doRegisterAgent(instantiationService, chatAgentService, 'setup.vscode', 'vscode', false, localize2('vscodeAgentDescription', "Ask questions about VS Code").value, ChatAgentLocation.Panel, undefined, context, controller);148disposables.add(vscodeDisposable);149150// Register workspace agent151const { disposable: workspaceDisposable } = SetupAgent.doRegisterAgent(instantiationService, chatAgentService, 'setup.workspace', 'workspace', false, localize2('workspaceAgentDescription', "Ask about your workspace").value, ChatAgentLocation.Panel, undefined, context, controller);152disposables.add(workspaceDisposable);153154// Register terminal agent155const { 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);156disposables.add(terminalDisposable);157158// Register tools159disposables.add(SetupTool.registerTool(instantiationService, {160id: 'setup_tools_createNewWorkspace',161source: ToolDataSource.Internal,162icon: Codicon.newFolder,163displayName: localize('setupToolDisplayName', "New Workspace"),164modelDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"),165userDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"),166canBeReferencedInPrompt: true,167toolReferenceName: 'new',168when: ContextKeyExpr.true(),169}).disposable);170171return { disposable: disposables };172});173}174175private 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 } {176const disposables = new DisposableStore();177disposables.add(chatAgentService.registerAgent(id, {178id,179name,180isDefault,181isCore: true,182modes: mode ? [mode] : [ChatModeKind.Ask],183when: mode === ChatModeKind.Agent ? ToolsAgentContextKey?.serialize() : undefined,184slashCommands: [],185disambiguation: [],186locations: [location],187metadata: { helpTextPrefix: SetupAgent.SETUP_NEEDED_MESSAGE },188description,189extensionId: nullExtensionDescription.identifier,190extensionVersion: undefined,191extensionDisplayName: nullExtensionDescription.name,192extensionPublisherId: nullExtensionDescription.publisher193}));194195const agent = disposables.add(instantiationService.createInstance(SetupAgent, context, controller, location));196disposables.add(chatAgentService.registerAgentImplementation(id, agent));197if (mode === ChatModeKind.Agent) {198chatAgentService.updateAgent(id, { themeIcon: Codicon.tools });199}200201return { agent, disposable: disposables };202}203204private static readonly SETUP_NEEDED_MESSAGE = new MarkdownString(localize('settingUpCopilotNeeded', "You need to set up GitHub Copilot and be signed in to use Chat."));205private static readonly TRUST_NEEDED_MESSAGE = new MarkdownString(localize('trustNeeded', "You need to trust this workspace to use Chat."));206207private readonly _onUnresolvableError = this._register(new Emitter<void>());208readonly onUnresolvableError = this._onUnresolvableError.event;209210private readonly pendingForwardedRequests = new Map<string, Promise<void>>();211212constructor(213private readonly context: ChatEntitlementContext,214private readonly controller: Lazy<ChatSetupController>,215private readonly location: ChatAgentLocation,216@IInstantiationService private readonly instantiationService: IInstantiationService,217@ILogService private readonly logService: ILogService,218@IConfigurationService private readonly configurationService: IConfigurationService,219@ITelemetryService private readonly telemetryService: ITelemetryService,220@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,221@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService222) {223super();224}225226async invoke(request: IChatAgentRequest, progress: (parts: IChatProgress[]) => void): Promise<IChatAgentResult> {227return this.instantiationService.invokeFunction(async accessor /* using accessor for lazy loading */ => {228const chatService = accessor.get(IChatService);229const languageModelsService = accessor.get(ILanguageModelsService);230const chatWidgetService = accessor.get(IChatWidgetService);231const chatAgentService = accessor.get(IChatAgentService);232const languageModelToolsService = accessor.get(ILanguageModelToolsService);233234return this.doInvoke(request, part => progress([part]), chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService);235});236}237238private async doInvoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise<IChatAgentResult> {239if (!this.context.state.installed || this.context.state.disabled || this.context.state.untrusted || this.context.state.entitlement === ChatEntitlement.Available || this.context.state.entitlement === ChatEntitlement.Unknown) {240return this.doInvokeWithSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService);241}242243return this.doInvokeWithoutSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService);244}245246private async doInvokeWithoutSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise<IChatAgentResult> {247const requestModel = chatWidgetService.getWidgetBySessionId(request.sessionId)?.viewModel?.model.getRequests().at(-1);248if (!requestModel) {249this.logService.error('[chat setup] Request model not found, cannot redispatch request.');250return {}; // this should not happen251}252253progress({254kind: 'progressMessage',255content: new MarkdownString(localize('waitingChat', "Getting chat ready...")),256});257258await this.forwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);259260return {};261}262263private async forwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {264try {265await this.doForwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);266} catch (error) {267progress({268kind: 'warning',269content: new MarkdownString(localize('copilotUnavailableWarning', "Failed to get a response. Please try again."))270});271}272}273274private async doForwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {275if (this.pendingForwardedRequests.has(requestModel.session.sessionId)) {276throw new Error('Request already in progress');277}278279const forwardRequest = this.doForwardRequestToCopilotWhenReady(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);280this.pendingForwardedRequests.set(requestModel.session.sessionId, forwardRequest);281282try {283await forwardRequest;284} finally {285this.pendingForwardedRequests.delete(requestModel.session.sessionId);286}287}288289private async doForwardRequestToCopilotWhenReady(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {290const widget = chatWidgetService.getWidgetBySessionId(requestModel.session.sessionId);291const modeInfo = widget?.input.currentModeInfo;292const languageModel = widget?.input.currentLanguageModel;293294// We need a signal to know when we can resend the request to295// Copilot. Waiting for the registration of the agent is not296// enough, we also need a language/tools model to be available.297298let agentReady = false;299let languageModelReady = false;300let toolsModelReady = false;301302const whenAgentReady = this.whenAgentReady(chatAgentService, modeInfo?.kind)?.then(() => agentReady = true);303const whenLanguageModelReady = this.whenLanguageModelReady(languageModelsService)?.then(() => languageModelReady = true);304const whenToolsModelReady = this.whenToolsModelReady(languageModelToolsService, requestModel)?.then(() => toolsModelReady = true);305306if (whenLanguageModelReady instanceof Promise || whenAgentReady instanceof Promise || whenToolsModelReady instanceof Promise) {307const timeoutHandle = setTimeout(() => {308progress({309kind: 'progressMessage',310content: new MarkdownString(localize('waitingChat2', "Chat is almost ready...")),311});312}, 10000);313314try {315const ready = await Promise.race([316timeout(this.environmentService.remoteAuthority ? 60000 /* increase for remote scenarios */ : 20000).then(() => 'timedout'),317this.whenDefaultAgentFailed(chatService).then(() => 'error'),318Promise.allSettled([whenLanguageModelReady, whenAgentReady, whenToolsModelReady])319]);320321if (ready === 'error' || ready === 'timedout') {322let warningMessage: string;323if (ready === 'timedout') {324warningMessage = 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);325} else {326warningMessage = 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);327}328329this.logService.warn(warningMessage, {330agentReady: whenAgentReady ? agentReady : undefined,331languageModelReady: whenLanguageModelReady ? languageModelReady : undefined,332toolsModelReady: whenToolsModelReady ? toolsModelReady : undefined333});334335progress({336kind: 'warning',337content: new MarkdownString(warningMessage)338});339340// This means Copilot is unhealthy and we cannot retry the341// request. Signal this to the outside via an event.342this._onUnresolvableError.fire();343return;344}345} finally {346clearTimeout(timeoutHandle);347}348}349350await chatService.resendRequest(requestModel, {351...widget?.getModeRequestOptions(),352modeInfo,353userSelectedModelId: languageModel,354});355}356357private whenLanguageModelReady(languageModelsService: ILanguageModelsService): Promise<unknown> | void {358const hasDefaultModel = () => {359for (const id of languageModelsService.getLanguageModelIds()) {360const model = languageModelsService.lookupLanguageModel(id);361if (model && model.isDefault) {362return true; // we have language models!363}364}365return false;366};367if (hasDefaultModel()) {368return; // we have language models!369}370371return Event.toPromise(Event.filter(languageModelsService.onDidChangeLanguageModels, () => hasDefaultModel() ?? false));372}373374private whenToolsModelReady(languageModelToolsService: ILanguageModelToolsService, requestModel: IChatRequestModel): Promise<unknown> | void {375const needsToolsModel = requestModel.message.parts.some(part => part instanceof ChatRequestToolPart);376if (!needsToolsModel) {377return; // No tools in this request, no need to check378}379380// check that tools other than setup. and internal tools are registered.381for (const tool of languageModelToolsService.getTools()) {382if (tool.id.startsWith('copilot_')) {383return; // we have tools!384}385}386387return Event.toPromise(Event.filter(languageModelToolsService.onDidChangeTools, () => {388for (const tool of languageModelToolsService.getTools()) {389if (tool.id.startsWith('copilot_')) {390return true; // we have tools!391}392}393394return false; // no external tools found395}));396}397398private whenAgentReady(chatAgentService: IChatAgentService, mode: ChatModeKind | undefined): Promise<unknown> | void {399const defaultAgent = chatAgentService.getDefaultAgent(this.location, mode);400if (defaultAgent && !defaultAgent.isCore) {401return; // we have a default agent from an extension!402}403404return Event.toPromise(Event.filter(chatAgentService.onDidChangeAgents, () => {405const defaultAgent = chatAgentService.getDefaultAgent(this.location, mode);406return Boolean(defaultAgent && !defaultAgent.isCore);407}));408}409410private async whenDefaultAgentFailed(chatService: IChatService): Promise<void> {411return new Promise<void>(resolve => {412chatService.activateDefaultAgent(this.location).catch(() => resolve());413});414}415416private async doInvokeWithSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise<IChatAgentResult> {417this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'chat' });418419const widget = chatWidgetService.getWidgetBySessionId(request.sessionId);420const requestModel = widget?.viewModel?.model.getRequests().at(-1);421422const setupListener = Event.runAndSubscribe(this.controller.value.onDidChange, (() => {423switch (this.controller.value.step) {424case ChatSetupStep.SigningIn:425progress({426kind: 'progressMessage',427content: new MarkdownString(localize('setupChatSignIn2', "Signing in to {0}...", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider.enterprise.id ? defaultChat.provider.enterprise.name : defaultChat.provider.default.name)),428});429break;430case ChatSetupStep.Installing:431progress({432kind: 'progressMessage',433content: new MarkdownString(localize('installingChat', "Getting chat ready...")),434});435break;436}437}));438439let result: IChatSetupResult | undefined = undefined;440try {441result = await ChatSetup.getInstance(this.instantiationService, this.context, this.controller).run({ disableChatViewReveal: true /* we are already in a chat context */ });442} catch (error) {443this.logService.error(`[chat setup] Error during setup: ${toErrorMessage(error)}`);444} finally {445setupListener.dispose();446}447448// User has agreed to run the setup449if (typeof result?.success === 'boolean') {450if (result.success) {451if (result.dialogSkipped) {452widget?.clear(); // make room for the Chat welcome experience453} else if (requestModel) {454let newRequest = this.replaceAgentInRequestModel(requestModel, chatAgentService); // Replace agent part with the actual Copilot agent...455newRequest = this.replaceToolInRequestModel(newRequest); // ...then replace any tool parts with the actual Copilot tools456457await this.forwardRequestToCopilot(newRequest, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);458}459} else {460progress({461kind: 'warning',462content: new MarkdownString(localize('chatSetupError', "Chat setup failed."))463});464}465}466467// User has cancelled the setup468else {469progress({470kind: 'markdownContent',471content: this.workspaceTrustManagementService.isWorkspaceTrusted() ? SetupAgent.SETUP_NEEDED_MESSAGE : SetupAgent.TRUST_NEEDED_MESSAGE472});473}474475return {};476}477478private replaceAgentInRequestModel(requestModel: IChatRequestModel, chatAgentService: IChatAgentService): IChatRequestModel {479const agentPart = requestModel.message.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);480if (!agentPart) {481return requestModel;482}483484const agentId = agentPart.agent.id.replace(/setup\./, `${defaultChat.extensionId}.`.toLowerCase());485const githubAgent = chatAgentService.getAgent(agentId);486if (!githubAgent) {487return requestModel;488}489490const newAgentPart = new ChatRequestAgentPart(agentPart.range, agentPart.editorRange, githubAgent);491492return new ChatRequestModel({493session: requestModel.session as ChatModel,494message: {495parts: requestModel.message.parts.map(part => {496if (part instanceof ChatRequestAgentPart) {497return newAgentPart;498}499return part;500}),501text: requestModel.message.text502},503variableData: requestModel.variableData,504timestamp: Date.now(),505attempt: requestModel.attempt,506modeInfo: requestModel.modeInfo,507confirmation: requestModel.confirmation,508locationData: requestModel.locationData,509attachedContext: requestModel.attachedContext,510isCompleteAddedRequest: requestModel.isCompleteAddedRequest,511});512}513514private replaceToolInRequestModel(requestModel: IChatRequestModel): IChatRequestModel {515const toolPart = requestModel.message.parts.find((r): r is ChatRequestToolPart => r instanceof ChatRequestToolPart);516if (!toolPart) {517return requestModel;518}519520const toolId = toolPart.toolId.replace(/setup.tools\./, `copilot_`.toLowerCase());521const newToolPart = new ChatRequestToolPart(522toolPart.range,523toolPart.editorRange,524toolPart.toolName,525toolId,526toolPart.displayName,527toolPart.icon528);529530const chatRequestToolEntry: IChatRequestToolEntry = {531id: toolId,532name: 'new',533range: toolPart.range,534kind: 'tool',535value: undefined536};537538const variableData: IChatRequestVariableData = {539variables: [chatRequestToolEntry]540};541542return new ChatRequestModel({543session: requestModel.session as ChatModel,544message: {545parts: requestModel.message.parts.map(part => {546if (part instanceof ChatRequestToolPart) {547return newToolPart;548}549return part;550}),551text: requestModel.message.text552},553variableData: variableData,554timestamp: Date.now(),555attempt: requestModel.attempt,556modeInfo: requestModel.modeInfo,557confirmation: requestModel.confirmation,558locationData: requestModel.locationData,559attachedContext: [chatRequestToolEntry],560isCompleteAddedRequest: requestModel.isCompleteAddedRequest,561});562}563}564565566class SetupTool extends Disposable implements IToolImpl {567568static registerTool(instantiationService: IInstantiationService, toolData: IToolData): { tool: SetupTool; disposable: IDisposable } {569return instantiationService.invokeFunction(accessor => {570const toolService = accessor.get(ILanguageModelToolsService);571572const disposables = new DisposableStore();573574const tool = instantiationService.createInstance(SetupTool);575disposables.add(toolService.registerTool(toolData, tool));576577return { tool, disposable: disposables };578});579}580581async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise<IToolResult> {582const result: IToolResult = {583content: [584{585kind: 'text',586value: ''587}588]589};590591return result;592}593594async prepareToolInvocation?(parameters: any, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {595return undefined;596}597}598599enum ChatSetupStrategy {600Canceled = 0,601DefaultSetup = 1,602SetupWithoutEnterpriseProvider = 2,603SetupWithEnterpriseProvider = 3,604SetupWithGoogleProvider = 4,605SetupWithAppleProvider = 5606}607608type ChatSetupResultValue = boolean /* success */ | undefined /* canceled */;609610interface IChatSetupResult {611readonly success: ChatSetupResultValue;612readonly dialogSkipped: boolean;613}614615class ChatSetup {616617private static instance: ChatSetup | undefined = undefined;618static getInstance(instantiationService: IInstantiationService, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): ChatSetup {619let instance = ChatSetup.instance;620if (!instance) {621instance = ChatSetup.instance = instantiationService.invokeFunction(accessor => {622return 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));623});624}625626return instance;627}628629private pendingRun: Promise<IChatSetupResult> | undefined = undefined;630631private skipDialogOnce = false;632633private constructor(634private readonly context: ChatEntitlementContext,635private readonly controller: Lazy<ChatSetupController>,636@IInstantiationService private readonly instantiationService: IInstantiationService,637@ITelemetryService private readonly telemetryService: ITelemetryService,638@ILayoutService private readonly layoutService: IWorkbenchLayoutService,639@IKeybindingService private readonly keybindingService: IKeybindingService,640@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,641@ILogService private readonly logService: ILogService,642@IConfigurationService private readonly configurationService: IConfigurationService,643@IViewsService private readonly viewsService: IViewsService,644@IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService645) { }646647skipDialog(): void {648this.skipDialogOnce = true;649}650651async run(options?: { disableChatViewReveal?: boolean; forceSignInDialog?: boolean; additionalScopes?: readonly string[] }): Promise<IChatSetupResult> {652if (this.pendingRun) {653return this.pendingRun;654}655656this.pendingRun = this.doRun(options);657658try {659return await this.pendingRun;660} finally {661this.pendingRun = undefined;662}663}664665private async doRun(options?: { disableChatViewReveal?: boolean; forceSignInDialog?: boolean; additionalScopes?: readonly string[] }): Promise<IChatSetupResult> {666this.context.update({ later: false });667668const dialogSkipped = this.skipDialogOnce;669this.skipDialogOnce = false;670671const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({672message: localize('chatWorkspaceTrust', "AI features are currently only supported in trusted workspaces.")673});674if (!trusted) {675this.context.update({ later: true });676this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: 0, signUpErrorCode: undefined, provider: undefined });677678return { dialogSkipped, success: undefined /* canceled */ };679}680681let setupStrategy: ChatSetupStrategy;682if (!options?.forceSignInDialog && (dialogSkipped || isProUser(this.chatEntitlementService.entitlement) || this.chatEntitlementService.entitlement === ChatEntitlement.Free)) {683setupStrategy = ChatSetupStrategy.DefaultSetup; // existing pro/free users setup without a dialog684} else {685setupStrategy = await this.showDialog(options);686}687688if (setupStrategy === ChatSetupStrategy.DefaultSetup && ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider.enterprise.id) {689setupStrategy = ChatSetupStrategy.SetupWithEnterpriseProvider; // users with a configured provider go through provider setup690}691692if (setupStrategy !== ChatSetupStrategy.Canceled && !options?.disableChatViewReveal) {693// Show the chat view now to better indicate progress694// while installing the extension or returning from sign in695showCopilotView(this.viewsService, this.layoutService);696}697698let success: ChatSetupResultValue = undefined;699try {700switch (setupStrategy) {701case ChatSetupStrategy.SetupWithEnterpriseProvider:702success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: true, useSocialProvider: undefined, additionalScopes: options?.additionalScopes });703break;704case ChatSetupStrategy.SetupWithoutEnterpriseProvider:705success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: undefined, additionalScopes: options?.additionalScopes });706break;707case ChatSetupStrategy.SetupWithAppleProvider:708success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: 'apple', additionalScopes: options?.additionalScopes });709break;710case ChatSetupStrategy.SetupWithGoogleProvider:711success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: 'google', additionalScopes: options?.additionalScopes });712break;713case ChatSetupStrategy.DefaultSetup:714success = await this.controller.value.setup(options);715break;716case ChatSetupStrategy.Canceled:717this.context.update({ later: true });718this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedMaybeLater', installDuration: 0, signUpErrorCode: undefined, provider: undefined });719break;720}721} catch (error) {722this.logService.error(`[chat setup] Error during setup: ${toErrorMessage(error)}`);723success = false;724}725726return { success, dialogSkipped };727}728729private async showDialog(options?: { forceSignInDialog?: boolean }): Promise<ChatSetupStrategy> {730const disposables = new DisposableStore();731732const dialogVariant = this.configurationService.getValue<'default' | 'apple' | unknown>('chat.setup.signInDialogVariant');733const buttons = this.getButtons(dialogVariant, options);734735const dialog = disposables.add(new Dialog(736this.layoutService.activeContainer,737this.getDialogTitle(options),738buttons.map(button => button[0]),739createWorkbenchDialogOptions({740type: 'none',741extraClasses: ['chat-setup-dialog'],742detail: ' ', // workaround allowing us to render the message in large743icon: Codicon.copilotLarge,744alignment: DialogContentsAlignment.Vertical,745cancelId: buttons.length - 1,746disableCloseButton: true,747renderFooter: this.telemetryService.telemetryLevel !== TelemetryLevel.NONE ? footer => footer.appendChild(this.createDialogFooter(disposables)) : undefined,748buttonOptions: buttons.map(button => button[2])749}, this.keybindingService, this.layoutService)750));751752const { button } = await dialog.show();753disposables.dispose();754755return buttons[button]?.[1] ?? ChatSetupStrategy.Canceled;756}757758private getButtons(variant: 'default' | 'apple' | unknown, options?: { forceSignInDialog?: boolean }): Array<[string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined]> {759type ContinueWithButton = [string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined];760const styleButton = (...classes: string[]) => ({ styleButton: (button: IButton) => button.element.classList.add(...classes) });761762let buttons: Array<ContinueWithButton>;763if (this.context.state.entitlement === ChatEntitlement.Unknown || options?.forceSignInDialog) {764const defaultProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.default.name), ChatSetupStrategy.SetupWithoutEnterpriseProvider, styleButton('continue-button', 'default')];765const defaultProviderLink: ContinueWithButton = [defaultProviderButton[0], defaultProviderButton[1], styleButton('link-button')];766767const enterpriseProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.enterprise.name), ChatSetupStrategy.SetupWithEnterpriseProvider, styleButton('continue-button', 'default')];768const enterpriseProviderLink: ContinueWithButton = [enterpriseProviderButton[0], enterpriseProviderButton[1], styleButton('link-button')];769770const googleProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.google.name), ChatSetupStrategy.SetupWithGoogleProvider, styleButton('continue-button', 'google')];771const appleProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.apple.name), ChatSetupStrategy.SetupWithAppleProvider, styleButton('continue-button', 'apple')];772773if (ChatEntitlementRequests.providerId(this.configurationService) !== defaultChat.provider.enterprise.id) {774buttons = coalesce([775defaultProviderButton,776googleProviderButton,777variant === 'apple' ? appleProviderButton : undefined,778enterpriseProviderLink779]);780} else {781buttons = coalesce([782enterpriseProviderButton,783googleProviderButton,784variant === 'apple' ? appleProviderButton : undefined,785defaultProviderLink786]);787}788} else {789buttons = [[localize('setupCopilotButton', "Set up Copilot"), ChatSetupStrategy.DefaultSetup, undefined]];790}791792buttons.push([localize('skipForNow', "Skip for now"), ChatSetupStrategy.Canceled, styleButton('link-button', 'skip-button')]);793794return buttons;795}796797private getDialogTitle(options?: { forceSignInDialog?: boolean }): string {798if (this.context.state.entitlement === ChatEntitlement.Unknown || options?.forceSignInDialog) {799return localize('signIn', "Sign in to use GitHub Copilot");800}801802return localize('startUsing', "Start using GitHub Copilot");803}804805private createDialogFooter(disposables: DisposableStore): HTMLElement {806const element = $('.chat-setup-dialog-footer');807808const markdown = this.instantiationService.createInstance(MarkdownRenderer, {});809810const 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);811element.appendChild($('p', undefined, disposables.add(markdown.render(new MarkdownString(footer, { isTrusted: true }))).element));812813return element;814}815}816817export class ChatSetupContribution extends Disposable implements IWorkbenchContribution {818819static readonly ID = 'workbench.contrib.chatSetup';820821constructor(822@IProductService private readonly productService: IProductService,823@IInstantiationService private readonly instantiationService: IInstantiationService,824@ICommandService private readonly commandService: ICommandService,825@ITelemetryService private readonly telemetryService: ITelemetryService,826@IChatEntitlementService chatEntitlementService: ChatEntitlementService,827@ILogService private readonly logService: ILogService,828@IContextKeyService private readonly contextKeyService: IContextKeyService829) {830super();831832const context = chatEntitlementService.context?.value;833const requests = chatEntitlementService.requests?.value;834if (!context || !requests) {835return; // disabled836}837838const controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, context, requests)));839840this.registerSetupAgents(context, controller);841this.registerActions(context, requests, controller);842this.registerUrlLinkHandler();843}844845private registerSetupAgents(context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): void {846const defaultAgentDisposables = markAsSingleton(new MutableDisposable()); // prevents flicker on window reload847const vscodeAgentDisposables = markAsSingleton(new MutableDisposable());848849const updateRegistration = () => {850if (!context.state.hidden && !context.state.disabled) {851852// Default Agents (always, even if installed to allow for speedy requests right on startup)853if (!defaultAgentDisposables.value) {854const disposables = defaultAgentDisposables.value = new DisposableStore();855856// Panel Agents857const panelAgentDisposables = disposables.add(new DisposableStore());858for (const mode of [ChatModeKind.Ask, ChatModeKind.Edit, ChatModeKind.Agent]) {859const { agent, disposable } = SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Panel, mode, context, controller);860panelAgentDisposables.add(disposable);861panelAgentDisposables.add(agent.onUnresolvableError(() => {862const panelAgentHasGuidance = chatViewsWelcomeRegistry.get().some(descriptor => this.contextKeyService.contextMatchesRules(descriptor.when));863if (panelAgentHasGuidance) {864// An unresolvable error from our agent registrations means that865// Copilot is unhealthy for some reason. We clear our panel866// registration to give Copilot a chance to show a custom message867// to the user from the views and stop pretending as if there was868// a functional agent.869this.logService.error('[chat setup] Unresolvable error from Copilot agent registration, clearing registration.');870panelAgentDisposables.dispose();871}872}));873}874875// Inline Agents876disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Terminal, undefined, context, controller).disposable);877disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Notebook, undefined, context, controller).disposable);878disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Editor, undefined, context, controller).disposable);879}880881// Built-In Agent + Tool (unless installed, signed-in and enabled)882if ((!context.state.installed || context.state.entitlement === ChatEntitlement.Unknown || context.state.entitlement === ChatEntitlement.Unresolved) && !vscodeAgentDisposables.value) {883const disposables = vscodeAgentDisposables.value = new DisposableStore();884disposables.add(SetupAgent.registerBuiltInAgents(this.instantiationService, context, controller).disposable);885}886} else {887defaultAgentDisposables.clear();888vscodeAgentDisposables.clear();889}890891if ((context.state.installed && context.state.entitlement !== ChatEntitlement.Unknown && context.state.entitlement !== ChatEntitlement.Unresolved) && !context.state.disabled) {892vscodeAgentDisposables.clear(); // we need to do this to prevent showing duplicate agent/tool entries in the list893}894};895896this._register(Event.runAndSubscribe(context.onDidChange, () => updateRegistration()));897}898899private registerActions(context: ChatEntitlementContext, requests: ChatEntitlementRequests, controller: Lazy<ChatSetupController>): void {900901class ChatSetupTriggerAction extends Action2 {902903static CHAT_SETUP_ACTION_LABEL = localize2('triggerChatSetup', "Use AI Features with Copilot for free...");904905constructor() {906super({907id: CHAT_SETUP_ACTION_ID,908title: ChatSetupTriggerAction.CHAT_SETUP_ACTION_LABEL,909category: CHAT_CATEGORY,910f1: true,911precondition: ContextKeyExpr.or(912ChatContextKeys.Setup.hidden,913ChatContextKeys.Setup.disabled,914ChatContextKeys.Setup.untrusted,915ChatContextKeys.Setup.installed.negate(),916ChatContextKeys.Entitlement.canSignUp917)918});919}920921override async run(accessor: ServicesAccessor, mode?: ChatModeKind, options?: { forceSignInDialog?: boolean; forceNoDialog?: boolean; additionalScopes?: readonly string[] }): Promise<boolean> {922const viewsService = accessor.get(IViewsService);923const layoutService = accessor.get(IWorkbenchLayoutService);924const instantiationService = accessor.get(IInstantiationService);925const dialogService = accessor.get(IDialogService);926const commandService = accessor.get(ICommandService);927const lifecycleService = accessor.get(ILifecycleService);928const configurationService = accessor.get(IConfigurationService);929930await context.update({ hidden: false });931configurationService.updateValue(ChatTeardownContribution.CHAT_DISABLED_CONFIGURATION_KEY, false);932933if (mode) {934const chatWidget = await showCopilotView(viewsService, layoutService);935chatWidget?.input.setChatMode(mode);936}937938if (options?.forceNoDialog) {939const chatWidget = await showCopilotView(viewsService, layoutService);940ChatSetup.getInstance(instantiationService, context, controller).skipDialog();941chatWidget?.acceptInput(localize('setupChat', "Set up chat."));942943return true;944}945946const setup = ChatSetup.getInstance(instantiationService, context, controller);947const { success } = await setup.run(options);948if (success === false && !lifecycleService.willShutdown) {949const { confirmed } = await dialogService.confirm({950type: Severity.Error,951message: localize('setupErrorDialog', "Chat setup failed. Would you like to try again?"),952primaryButton: localize('retry', "Retry"),953});954955if (confirmed) {956return Boolean(await commandService.executeCommand(CHAT_SETUP_ACTION_ID, mode, options));957}958}959960return Boolean(success);961}962}963964class ChatSetupTriggerForceSignInDialogAction extends Action2 {965966constructor() {967super({968id: 'workbench.action.chat.triggerSetupForceSignIn',969title: localize2('forceSignIn', "Sign in to use AI features")970});971}972973override async run(accessor: ServicesAccessor): Promise<unknown> {974const commandService = accessor.get(ICommandService);975const telemetryService = accessor.get(ITelemetryService);976977telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'api' });978979return commandService.executeCommand(CHAT_SETUP_ACTION_ID, undefined, { forceSignInDialog: true });980}981}982983class ChatSetupTriggerWithoutDialogAction extends Action2 {984985constructor() {986super({987id: 'workbench.action.chat.triggerSetupWithoutDialog',988title: ChatSetupTriggerAction.CHAT_SETUP_ACTION_LABEL989});990}991992override async run(accessor: ServicesAccessor): Promise<unknown> {993const commandService = accessor.get(ICommandService);994const telemetryService = accessor.get(ITelemetryService);995996telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'api' });997998return commandService.executeCommand(CHAT_SETUP_ACTION_ID, undefined, { forceNoDialog: true });999}1000}10011002class ChatSetupFromAccountsAction extends Action2 {10031004constructor() {1005super({1006id: 'workbench.action.chat.triggerSetupFromAccounts',1007title: localize2('triggerChatSetupFromAccounts', "Sign in to use AI features..."),1008menu: {1009id: MenuId.AccountsContext,1010group: '2_copilot',1011when: ContextKeyExpr.and(1012ChatContextKeys.Setup.hidden.negate(),1013ChatContextKeys.Setup.installed.negate(),1014ChatContextKeys.Entitlement.signedOut1015)1016}1017});1018}10191020override async run(accessor: ServicesAccessor): Promise<void> {1021const commandService = accessor.get(ICommandService);1022const telemetryService = accessor.get(ITelemetryService);10231024telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'accounts' });10251026return commandService.executeCommand(CHAT_SETUP_ACTION_ID);1027}1028}10291030const windowFocusListener = this._register(new MutableDisposable());1031class UpgradePlanAction extends Action2 {1032constructor() {1033super({1034id: 'workbench.action.chat.upgradePlan',1035title: localize2('managePlan', "Upgrade to GitHub Copilot Pro"),1036category: localize2('chat.category', 'Chat'),1037f1: true,1038precondition: ContextKeyExpr.and(1039ChatContextKeys.Setup.hidden.negate(),1040ContextKeyExpr.or(1041ChatContextKeys.Entitlement.canSignUp,1042ChatContextKeys.Entitlement.planFree1043)1044),1045menu: {1046id: MenuId.ChatTitleBarMenu,1047group: 'a_first',1048order: 1,1049when: ContextKeyExpr.and(1050ChatContextKeys.Entitlement.planFree,1051ContextKeyExpr.or(1052ChatContextKeys.chatQuotaExceeded,1053ChatContextKeys.completionsQuotaExceeded1054)1055)1056}1057});1058}10591060override async run(accessor: ServicesAccessor): Promise<void> {1061const openerService = accessor.get(IOpenerService);1062const hostService = accessor.get(IHostService);1063const commandService = accessor.get(ICommandService);10641065openerService.open(URI.parse(defaultChat.upgradePlanUrl));10661067const entitlement = context.state.entitlement;1068if (!isProUser(entitlement)) {1069// If the user is not yet Pro, we listen to window focus to refresh the token1070// when the user has come back to the window assuming the user signed up.1071windowFocusListener.value = hostService.onDidChangeFocus(focus => this.onWindowFocus(focus, commandService));1072}1073}10741075private async onWindowFocus(focus: boolean, commandService: ICommandService): Promise<void> {1076if (focus) {1077windowFocusListener.clear();10781079const entitlements = await requests.forceResolveEntitlement(undefined);1080if (entitlements?.entitlement && isProUser(entitlements?.entitlement)) {1081refreshTokens(commandService);1082}1083}1084}1085}10861087class EnableOveragesAction extends Action2 {1088constructor() {1089super({1090id: 'workbench.action.chat.manageOverages',1091title: localize2('manageOverages', "Manage GitHub Copilot Overages"),1092category: localize2('chat.category', 'Chat'),1093f1: true,1094precondition: ContextKeyExpr.and(1095ChatContextKeys.Setup.hidden.negate(),1096ContextKeyExpr.or(1097ChatContextKeys.Entitlement.planPro,1098ChatContextKeys.Entitlement.planProPlus,1099)1100),1101menu: {1102id: MenuId.ChatTitleBarMenu,1103group: 'a_first',1104order: 1,1105when: ContextKeyExpr.and(1106ContextKeyExpr.or(1107ChatContextKeys.Entitlement.planPro,1108ChatContextKeys.Entitlement.planProPlus,1109),1110ContextKeyExpr.or(1111ChatContextKeys.chatQuotaExceeded,1112ChatContextKeys.completionsQuotaExceeded1113)1114)1115}1116});1117}11181119override async run(accessor: ServicesAccessor): Promise<void> {1120const openerService = accessor.get(IOpenerService);1121openerService.open(URI.parse(defaultChat.manageOveragesUrl));1122}1123}11241125registerAction2(ChatSetupTriggerAction);1126registerAction2(ChatSetupTriggerForceSignInDialogAction);1127registerAction2(ChatSetupFromAccountsAction);1128registerAction2(ChatSetupTriggerWithoutDialogAction);1129registerAction2(UpgradePlanAction);1130registerAction2(EnableOveragesAction);1131}11321133private registerUrlLinkHandler(): void {1134this._register(ExtensionUrlHandlerOverrideRegistry.registerHandler({1135canHandleURL: url => {1136return url.scheme === this.productService.urlProtocol && equalsIgnoreCase(url.authority, defaultChat.chatExtensionId);1137},1138handleURL: async url => {1139const params = new URLSearchParams(url.query);1140this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'url', detail: params.get('referrer') ?? undefined });11411142await this.commandService.executeCommand(CHAT_SETUP_ACTION_ID, validateChatMode(params.get('mode')));11431144return true;1145}1146}));1147}1148}11491150export class ChatTeardownContribution extends Disposable implements IWorkbenchContribution {11511152static readonly ID = 'workbench.contrib.chatTeardown';11531154static readonly CHAT_DISABLED_CONFIGURATION_KEY = 'chat.disableAIFeatures';11551156constructor(1157@IChatEntitlementService chatEntitlementService: ChatEntitlementService,1158@IConfigurationService private readonly configurationService: IConfigurationService,1159@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,1160@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,1161@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,1162@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService1163) {1164super();11651166const context = chatEntitlementService.context?.value;1167if (!context) {1168return; // disabled1169}11701171this.registerListeners();1172this.registerActions();11731174this.handleChatDisabled(false);1175}11761177private handleChatDisabled(fromEvent: boolean): void {1178const chatDisabled = this.configurationService.inspect(ChatTeardownContribution.CHAT_DISABLED_CONFIGURATION_KEY);1179if (chatDisabled.value === true) {1180this.maybeEnableOrDisableExtension(typeof chatDisabled.workspaceValue === 'boolean' ? EnablementState.DisabledWorkspace : EnablementState.DisabledGlobally);1181if (fromEvent) {1182this.maybeHideAuxiliaryBar();1183}1184} else if (chatDisabled.value === false && fromEvent /* do not enable extensions unless its an explicit settings change */) {1185this.maybeEnableOrDisableExtension(typeof chatDisabled.workspaceValue === 'boolean' ? EnablementState.EnabledWorkspace : EnablementState.EnabledGlobally);1186}1187}11881189private async registerListeners(): Promise<void> {11901191// Configuration changes1192this._register(this.configurationService.onDidChangeConfiguration(e => {1193if (!e.affectsConfiguration(ChatTeardownContribution.CHAT_DISABLED_CONFIGURATION_KEY)) {1194return;1195}11961197this.handleChatDisabled(true);1198}));11991200// Extension installation1201await this.extensionsWorkbenchService.queryLocal();1202this._register(this.extensionsWorkbenchService.onChange(e => {1203if (e && !ExtensionIdentifier.equals(e.identifier.id, defaultChat.chatExtensionId)) {1204return; // unrelated event1205}12061207const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.chatExtensionId));1208if (defaultChatExtension?.local && this.extensionEnablementService.isEnabled(defaultChatExtension.local)) {1209this.configurationService.updateValue(ChatTeardownContribution.CHAT_DISABLED_CONFIGURATION_KEY, false);1210}1211}));1212}12131214private async maybeEnableOrDisableExtension(state: EnablementState.EnabledGlobally | EnablementState.EnabledWorkspace | EnablementState.DisabledGlobally | EnablementState.DisabledWorkspace): Promise<void> {1215const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.chatExtensionId));1216if (!defaultChatExtension) {1217return;1218}12191220await this.extensionsWorkbenchService.setEnablement([defaultChatExtension], state);1221await this.extensionsWorkbenchService.updateRunningExtensions(state === EnablementState.EnabledGlobally || state === EnablementState.EnabledWorkspace ? localize('restartExtensionHost.reason.enable', "Enabling AI features") : localize('restartExtensionHost.reason.disable', "Disabling AI features"));1222}12231224private maybeHideAuxiliaryBar(): void {1225const activeContainers = this.viewDescriptorService.getViewContainersByLocation(ViewContainerLocation.AuxiliaryBar).filter(1226container => this.viewDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 01227);1228if (1229(activeContainers.length === 0) || // chat view is already gone but we know it was there before1230(activeContainers.length === 1 && activeContainers.at(0)?.id === CHAT_SIDEBAR_PANEL_ID) // chat view is the only view which is going to go away1231) {1232this.layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); // hide if there are no views in the secondary sidebar1233}1234}12351236private registerActions(): void {12371238class ChatSetupHideAction extends Action2 {12391240static readonly ID = 'workbench.action.chat.hideSetup';1241static readonly TITLE = localize2('hideChatSetup', "Learn How to Hide AI Features");12421243constructor() {1244super({1245id: ChatSetupHideAction.ID,1246title: ChatSetupHideAction.TITLE,1247f1: true,1248category: CHAT_CATEGORY,1249precondition: ContextKeyExpr.and(1250ChatContextKeys.Setup.hidden.negate(),1251ChatContextKeys.Setup.installed.negate()1252),1253menu: {1254id: MenuId.ChatTitleBarMenu,1255group: 'z_hide',1256order: 1,1257when: ChatContextKeys.Setup.installed.negate()1258}1259});1260}12611262override async run(accessor: ServicesAccessor): Promise<void> {1263const preferencesService = accessor.get(IPreferencesService);12641265preferencesService.openSettings({ jsonEditor: false, query: `@id:${ChatTeardownContribution.CHAT_DISABLED_CONFIGURATION_KEY}` });1266}1267}12681269registerAction2(ChatSetupHideAction);1270}1271}12721273//#endregion12741275//#region Setup Controller12761277type InstallChatClassification = {1278owner: 'bpasero';1279comment: 'Provides insight into chat installation.';1280installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' };1281installDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration it took to install the extension.' };1282signUpErrorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The error code in case of an error signing up.' };1283provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider used for the chat installation.' };1284};1285type InstallChatEvent = {1286installResult: 'installed' | 'alreadyInstalled' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted' | 'failedNoSession' | 'failedMaybeLater' | 'failedEnterpriseSetup';1287installDuration: number;1288signUpErrorCode: number | undefined;1289provider: string | undefined;1290};12911292enum ChatSetupStep {1293Initial = 1,1294SigningIn,1295Installing1296}12971298class ChatSetupController extends Disposable {12991300private readonly _onDidChange = this._register(new Emitter<void>());1301readonly onDidChange = this._onDidChange.event;13021303private _step = ChatSetupStep.Initial;1304get step(): ChatSetupStep { return this._step; }13051306constructor(1307private readonly context: ChatEntitlementContext,1308private readonly requests: ChatEntitlementRequests,1309@ITelemetryService private readonly telemetryService: ITelemetryService,1310@IAuthenticationService private readonly authenticationService: IAuthenticationService,1311@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,1312@IProductService private readonly productService: IProductService,1313@ILogService private readonly logService: ILogService,1314@IProgressService private readonly progressService: IProgressService,1315@IActivityService private readonly activityService: IActivityService,1316@ICommandService private readonly commandService: ICommandService,1317@IDialogService private readonly dialogService: IDialogService,1318@IConfigurationService private readonly configurationService: IConfigurationService,1319@ILifecycleService private readonly lifecycleService: ILifecycleService,1320@IQuickInputService private readonly quickInputService: IQuickInputService,1321) {1322super();13231324this.registerListeners();1325}13261327private registerListeners(): void {1328this._register(this.context.onDidChange(() => this._onDidChange.fire()));1329}13301331private setStep(step: ChatSetupStep): void {1332if (this._step === step) {1333return;1334}13351336this._step = step;1337this._onDidChange.fire();1338}13391340async setup(options?: { forceSignIn?: boolean; useSocialProvider?: string; useEnterpriseProvider?: boolean; additionalScopes?: readonly string[] }): Promise<ChatSetupResultValue> {1341const watch = new StopWatch(false);1342const title = localize('setupChatProgress', "Getting chat ready...");1343const badge = this.activityService.showViewContainerActivity(CHAT_SIDEBAR_PANEL_ID, {1344badge: new ProgressBadge(() => title),1345});13461347try {1348return await this.progressService.withProgress({1349location: ProgressLocation.Window,1350command: CHAT_OPEN_ACTION_ID,1351title,1352}, () => this.doSetup(options ?? {}, watch));1353} finally {1354badge.dispose();1355}1356}13571358private async doSetup(options: { forceSignIn?: boolean; useSocialProvider?: string; useEnterpriseProvider?: boolean; additionalScopes?: readonly string[] }, watch: StopWatch): Promise<ChatSetupResultValue> {1359this.context.suspend(); // reduces flicker13601361let success: ChatSetupResultValue = false;1362try {1363const providerId = ChatEntitlementRequests.providerId(this.configurationService);1364let session: AuthenticationSession | undefined;1365let entitlement: ChatEntitlement | undefined;13661367// Entitlement Unknown or `forceSignIn`: we need to sign-in user1368if (this.context.state.entitlement === ChatEntitlement.Unknown || options.forceSignIn) {1369this.setStep(ChatSetupStep.SigningIn);1370const result = await this.signIn(options);1371if (!result.session) {1372this.doInstall(); // still install the extension in the background to remind the user to sign-in eventually13731374const provider = options.useSocialProvider ?? options.useEnterpriseProvider ? defaultChat.provider.enterprise.id : defaultChat.provider.default.id;1375this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider });1376return undefined; // treat as cancelled because signing in already triggers an error dialog1377}13781379session = result.session;1380entitlement = result.entitlement;1381}13821383// Await Install1384this.setStep(ChatSetupStep.Installing);1385success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch, options);1386} finally {1387this.setStep(ChatSetupStep.Initial);1388this.context.resume();1389}13901391return success;1392}13931394private async signIn(options: { useSocialProvider?: string; additionalScopes?: readonly string[] }): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> {1395let session: AuthenticationSession | undefined;1396let entitlements;1397try {1398({ session, entitlements } = await this.requests.signIn(options));1399} catch (e) {1400this.logService.error(`[chat setup] signIn: error ${e}`);1401}14021403if (!session && !this.lifecycleService.willShutdown) {1404const { confirmed } = await this.dialogService.confirm({1405type: Severity.Error,1406message: 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),1407detail: localize('unknownSignInErrorDetail', "You must be signed in to use AI features."),1408primaryButton: localize('retry', "Retry")1409});14101411if (confirmed) {1412return this.signIn(options);1413}1414}14151416return { session, entitlement: entitlements?.entitlement };1417}14181419private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch, options: { useSocialProvider?: string; useEnterpriseProvider?: boolean; additionalScopes?: readonly string[] }): Promise<ChatSetupResultValue> {1420const wasRunning = this.context.state.installed && !this.context.state.disabled;1421let signUpResult: boolean | { errorCode: number } | undefined = undefined;14221423const provider = options.useSocialProvider ?? options.useEnterpriseProvider ? defaultChat.provider.enterprise.id : defaultChat.provider.default.id;1424let sessions = session ? [session] : undefined;1425try {1426if (1427entitlement !== ChatEntitlement.Free && // User is not signed up to Copilot Free1428!isProUser(entitlement) && // User is not signed up for a Copilot subscription1429entitlement !== ChatEntitlement.Unavailable // User is eligible for Copilot Free1430) {1431if (!sessions) {1432try {1433// Consider all sessions for the provider to be suitable for signing up1434const existingSessions = await this.authenticationService.getSessions(providerId);1435sessions = existingSessions.length > 0 ? [...existingSessions] : undefined;1436} catch (error) {1437// ignore - errors can throw if a provider is not registered1438}14391440if (!sessions || sessions.length === 0) {1441this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider });1442return false; // unexpected1443}1444}14451446signUpResult = await this.requests.signUpFree(sessions);14471448if (typeof signUpResult !== 'boolean' /* error */) {1449this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode, provider });1450}1451}14521453await this.doInstallWithRetry();1454} catch (error) {1455this.logService.error(`[chat setup] install: error ${error}`);1456this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider });1457return false;1458}14591460if (typeof signUpResult === 'boolean') {1461this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: wasRunning && !signUpResult ? 'alreadyInstalled' : 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider });1462}14631464if (wasRunning) {1465// We always trigger refresh of tokens to help the user1466// get out of authentication issues that can happen when1467// for example the sign-up ran after the extension tried1468// to use the authentication information to mint a token1469refreshTokens(this.commandService);1470}14711472return true;1473}14741475private async doInstallWithRetry(): Promise<void> {1476let error: Error | undefined;1477try {1478await this.doInstall();1479} catch (e) {1480this.logService.error(`[chat setup] install: error ${error}`);1481error = e;1482}14831484if (error) {1485if (!this.lifecycleService.willShutdown) {1486const { confirmed } = await this.dialogService.confirm({1487type: Severity.Error,1488message: localize('unknownSetupError', "An error occurred while setting up chat. Would you like to try again?"),1489detail: error && !isCancellationError(error) ? toErrorMessage(error) : undefined,1490primaryButton: localize('retry', "Retry")1491});14921493if (confirmed) {1494return this.doInstallWithRetry();1495}1496}14971498throw error;1499}1500}15011502private async doInstall(): Promise<void> {1503await this.extensionsWorkbenchService.install(defaultChat.chatExtensionId, {1504enable: true,1505isApplicationScoped: true, // install into all profiles1506isMachineScoped: false, // do not ask to sync1507installEverywhere: true, // install in local and remote1508installPreReleaseVersion: this.productService.quality !== 'stable'1509}, ChatViewId);1510}15111512async setupWithProvider(options: { useEnterpriseProvider: boolean; useSocialProvider: string | undefined; additionalScopes?: readonly string[] }): Promise<ChatSetupResultValue> {1513const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);1514registry.registerConfiguration({1515'id': 'copilot.setup',1516'type': 'object',1517'properties': {1518[defaultChat.completionsAdvancedSetting]: {1519'type': 'object',1520'properties': {1521'authProvider': {1522'type': 'string'1523}1524}1525},1526[defaultChat.providerUriSetting]: {1527'type': 'string'1528}1529}1530});15311532if (options.useEnterpriseProvider) {1533const success = await this.handleEnterpriseInstance();1534if (!success) {1535this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedEnterpriseSetup', installDuration: 0, signUpErrorCode: undefined, provider: undefined });1536return success; // not properly configured, abort1537}1538}15391540let existingAdvancedSetting = this.configurationService.inspect(defaultChat.completionsAdvancedSetting).user?.value;1541if (!isObject(existingAdvancedSetting)) {1542existingAdvancedSetting = {};1543}15441545if (options.useEnterpriseProvider) {1546await this.configurationService.updateValue(`${defaultChat.completionsAdvancedSetting}`, {1547...existingAdvancedSetting,1548'authProvider': defaultChat.provider.enterprise.id1549}, ConfigurationTarget.USER);1550} else {1551await this.configurationService.updateValue(`${defaultChat.completionsAdvancedSetting}`, Object.keys(existingAdvancedSetting).length > 0 ? {1552...existingAdvancedSetting,1553'authProvider': undefined1554} : undefined, ConfigurationTarget.USER);1555}15561557return this.setup({ ...options, forceSignIn: true });1558}15591560private async handleEnterpriseInstance(): Promise<ChatSetupResultValue> {1561const domainRegEx = /^[a-zA-Z\-_]+$/;1562const fullUriRegEx = /^(https:\/\/)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.ghe\.com\/?$/;15631564const uri = this.configurationService.getValue<string>(defaultChat.providerUriSetting);1565if (typeof uri === 'string' && fullUriRegEx.test(uri)) {1566return true; // already setup with a valid URI1567}15681569let isSingleWord = false;1570const result = await this.quickInputService.input({1571prompt: localize('enterpriseInstance', "What is your {0} instance?", defaultChat.provider.enterprise.name),1572placeHolder: localize('enterpriseInstancePlaceholder', 'i.e. "octocat" or "https://octocat.ghe.com"...'),1573ignoreFocusLost: true,1574value: uri,1575validateInput: async value => {1576isSingleWord = false;1577if (!value) {1578return undefined;1579}15801581if (domainRegEx.test(value)) {1582isSingleWord = true;1583return {1584content: localize('willResolveTo', "Will resolve to {0}", `https://${value}.ghe.com`),1585severity: Severity.Info1586};1587} if (!fullUriRegEx.test(value)) {1588return {1589content: localize('invalidEnterpriseInstance', 'You must enter a valid {0} instance (i.e. "octocat" or "https://octocat.ghe.com")', defaultChat.provider.enterprise.name),1590severity: Severity.Error1591};1592}15931594return undefined;1595}1596});15971598if (!result) {1599return undefined; // canceled1600}16011602let resolvedUri = result;1603if (isSingleWord) {1604resolvedUri = `https://${resolvedUri}.ghe.com`;1605} else {1606const normalizedUri = result.toLowerCase();1607const hasHttps = normalizedUri.startsWith('https://');1608if (!hasHttps) {1609resolvedUri = `https://${result}`;1610}1611}16121613await this.configurationService.updateValue(defaultChat.providerUriSetting, resolvedUri, ConfigurationTarget.USER);16141615return true;1616}1617}16181619//#endregion16201621function refreshTokens(commandService: ICommandService): void {1622// ugly, but we need to signal to the extension that entitlements changed1623commandService.executeCommand(defaultChat.completionsRefreshTokenCommand);1624commandService.executeCommand(defaultChat.chatRefreshTokenCommand);1625}162616271628