From 2920cccf4562929f648189da191379e6915716fb Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 11:57:39 +0100 Subject: [PATCH 01/21] chat - rewrite welcome --- src/vs/base/browser/ui/toggle/toggle.ts | 4 + src/vs/base/common/product.ts | 3 +- .../browser/markdownRenderer.ts | 2 +- .../browser/chatParticipantContributions.ts | 2 - .../chat/browser/chatSetup.contribution.ts | 436 +++++++++--------- .../chat/browser/media/chatViewSetup.css | 25 + .../chat/browser/media/chatViewWelcome.css | 6 - .../viewsWelcome/chatViewWelcomeController.ts | 65 ++- .../browser/viewsWelcome/chatViewsWelcome.ts | 4 +- .../contrib/chat/common/chatContextKeys.ts | 2 - 10 files changed, 302 insertions(+), 247 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index a232e531e0258..a6c913662f93a 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -266,6 +266,10 @@ export class Checkbox extends Widget { return this.checkbox.checked; } + get enabled(): boolean { + return this.checkbox.enabled; + } + set checked(newIsChecked: boolean) { this.checkbox.checked = newIsChecked; diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index fcb54c94eb396..5dbddfb8578f2 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -311,6 +311,7 @@ export interface IDefaultChatAgent { readonly documentationUrl: string; readonly privacyStatementUrl: string; readonly collectionDocumentationUrl: string; + readonly skusDocumentationUrl: string; readonly providerId: string; readonly providerName: string; readonly providerScopes: string[]; @@ -318,5 +319,5 @@ export interface IDefaultChatAgent { readonly entitlementChatEnabled: string; readonly entitlementSkuKey: string; readonly entitlementSku30DTrialValue: string; - readonly entitlementSkuAlternateUrl: string; + readonly entitlementSkuLimitedUrl: string; } diff --git a/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts b/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts index 33d1adc99c80f..61ece2ec8e1e1 100644 --- a/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts +++ b/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts @@ -32,7 +32,7 @@ export interface IMarkdownRendererOptions { * Markdown renderer that can render codeblocks with the editor mechanics. This * renderer should always be preferred. */ -export class MarkdownRenderer { +export class MarkdownRenderer implements IDisposable { private static _ttpTokenizer = createTrustedTypesPolicy('tokenizeToString', { createHTML(html: string) { diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 9eefdcd1709bb..2c67aa0f28c42 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -321,8 +321,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), when: ContextKeyExpr.or( ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.signingIn, - ChatContextKeys.Setup.installing, ChatContextKeys.Setup.installed, ChatContextKeys.panelParticipantRegistered, ChatContextKeys.extensionInvalid diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 26f4abedbfa52..b47c4b1c4d362 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import './media/chatViewSetup.css'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -26,7 +27,6 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; -import { CHAT_CATEGORY } from './actions/chatActions.js'; import { showChatView, ChatViewId } from './chat.js'; import { IChatAgentService } from '../common/chatAgents.js'; import { Event } from '../../../../base/common/event.js'; @@ -34,13 +34,17 @@ import product from '../../../../platform/product/common/product.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IChatViewsWelcomeContributionRegistry, ChatViewsWelcomeExtensions } from './viewsWelcome/chatViewsWelcome.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { getActiveElement } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, EventType, getActiveElement } from '../../../../base/browser/dom.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; +import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; +import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -50,6 +54,7 @@ const defaultChat = { documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '', collectionDocumentationUrl: product.defaultChatAgent?.collectionDocumentationUrl ?? '', + skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '', providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [], @@ -57,7 +62,7 @@ const defaultChat = { entitlementSkuKey: product.defaultChatAgent?.entitlementSkuKey ?? '', entitlementSku30DTrialValue: product.defaultChatAgent?.entitlementSku30DTrialValue ?? '', entitlementChatEnabled: product.defaultChatAgent?.entitlementChatEnabled ?? '', - entitlementSkuAlternateUrl: product.defaultChatAgent?.entitlementSkuAlternateUrl ?? '' + entitlementSkuLimitedUrl: product.defaultChatAgent?.entitlementSkuLimitedUrl ?? '' }; type ChatSetupEntitlementEnablementClassification = { @@ -88,24 +93,21 @@ interface IChatEntitlement { readonly chatSku30DTrial?: boolean; } +enum ChatEntitlement { + Unknown = 1, + Applicable, + Limited, + Payed, + Blocked +} + const UNKNOWN_CHAT_ENTITLEMENT: IChatEntitlement = {}; class ChatSetupContribution extends Disposable implements IWorkbenchContribution { - private readonly chatSetupSignedInContextKey = ChatContextKeys.Setup.signedIn.bindTo(this.contextKeyService); - private readonly chatSetupEntitledContextKey = ChatContextKeys.Setup.entitled.bindTo(this.contextKeyService); - - private readonly chatSetupState = this.instantiationService.createInstance(ChatSetupState); - - private resolvedEntitlement: IChatEntitlement | undefined = undefined; - constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IProductService private readonly productService: IProductService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionService private readonly extensionService: IExtensionService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -116,15 +118,11 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution this.registerChatWelcome(); - this.registerEntitlementListeners(); - this.registerAuthListeners(); - + this.checkEntitlements(); this.checkExtensionInstallation(); } private registerChatWelcome(): void { - const header = localize('setupHeader', "{0} is your AI pair programmer.", defaultChat.name); - const footer = localize({ key: 'setupFooter', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}).", defaultChat.privacyStatementUrl); // Setup: Triggered (signed-out) Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ @@ -132,17 +130,12 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution when: ContextKeyExpr.and( ChatContextKeys.Setup.triggered, ChatContextKeys.Setup.signedIn.negate(), - ChatContextKeys.Setup.signingIn.negate(), - ChatContextKeys.Setup.installing.negate(), ChatContextKeys.Setup.installed.negate() )!, icon: defaultChat.icon, - content: new MarkdownString([ - header, - `[${localize('signInAndSetup', "Sign in to use {0}", defaultChat.name)}](command:${ChatSetupSignInAndInstallChatAction.ID})`, - footer, - `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, - ].join('\n\n'), { isTrusted: true }), + content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, { + entitlement: ChatEntitlement.Unknown + })).element, }); // Setup: Triggered (signed-in) @@ -151,49 +144,52 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution when: ContextKeyExpr.and( ChatContextKeys.Setup.triggered, ChatContextKeys.Setup.signedIn, - ChatContextKeys.Setup.signingIn.negate(), - ChatContextKeys.Setup.installing.negate(), ChatContextKeys.Setup.installed.negate() )!, icon: defaultChat.icon, - content: new MarkdownString([ - header, - `[${localize('setup', "Install {0}", defaultChat.name)}](command:${ChatSetupInstallAction.ID})`, - footer, - `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, - ].join('\n\n'), { isTrusted: true }) + content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, { + entitlement: ChatEntitlement.Applicable + })).element, }); + } - // Setup: Signing-in - Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: defaultChat.chatWelcomeTitle, - when: ContextKeyExpr.and( - ChatContextKeys.Setup.signingIn, - ChatContextKeys.Setup.installed.negate() - )!, - icon: defaultChat.icon, - disableFirstLinkToButton: true, - content: new MarkdownString([ - header, - localize('setupChatSigningIn', "$(loading~spin) Signing in to {0}...", defaultChat.providerName), - footer, - `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, - ].join('\n\n'), { isTrusted: true, supportThemeIcons: true }), - }); + private checkEntitlements(): void { + const entitlementsResolver = this._register(this.instantiationService.createInstance(ChatSetupEntitlementResolver)); + entitlementsResolver.resolve(); + } - // Setup: Installing - Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: defaultChat.chatWelcomeTitle, - when: ChatContextKeys.Setup.installing, - icon: defaultChat.icon, - disableFirstLinkToButton: true, - content: new MarkdownString([ - header, - localize('setupChatInstalling', "$(loading~spin) Setting up Chat for you..."), - footer, - `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, - ].join('\n\n'), { isTrusted: true, supportThemeIcons: true }), - }); + private async checkExtensionInstallation(): Promise { + const extensions = await this.extensionManagementService.getInstalled(); + + const chatInstalled = !!extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); + this.instantiationService.createInstance(ChatSetupState).update({ chatInstalled }); + } +} + +class ChatSetupEntitlementResolver extends Disposable { + + private readonly chatSetupSignedInContextKey = ChatContextKeys.Setup.signedIn.bindTo(this.contextKeyService); + private readonly chatSetupEntitledContextKey = ChatContextKeys.Setup.entitled.bindTo(this.contextKeyService); + + private readonly chatSetupState = this.instantiationService.createInstance(ChatSetupState); + + private resolvedEntitlement: IChatEntitlement | undefined = undefined; + + constructor( + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IExtensionService private readonly extensionService: IExtensionService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + } + + resolve(): void { + this.registerEntitlementListeners(); + this.registerAuthListeners(); + + this.handleDeclaredAuthProviders(); } private registerEntitlementListeners(): void { @@ -231,28 +227,27 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution } private registerAuthListeners(): void { - const hasProviderSessions = async () => { - const sessions = await this.authenticationService.getSessions(defaultChat.providerId); - return sessions.length > 0; - }; - - const handleDeclaredAuthProviders = async () => { - if (this.authenticationService.declaredProviders.find(p => p.id === defaultChat.providerId)) { - this.chatSetupSignedInContextKey.set(await hasProviderSessions()); - } - }; - this._register(this.authenticationService.onDidChangeDeclaredProviders(handleDeclaredAuthProviders)); - this._register(this.authenticationService.onDidRegisterAuthenticationProvider(handleDeclaredAuthProviders)); - - handleDeclaredAuthProviders(); + this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.handleDeclaredAuthProviders())); + this._register(this.authenticationService.onDidRegisterAuthenticationProvider(() => this.handleDeclaredAuthProviders())); this._register(this.authenticationService.onDidChangeSessions(async ({ providerId }) => { if (providerId === defaultChat.providerId) { - this.chatSetupSignedInContextKey.set(await hasProviderSessions()); + this.chatSetupSignedInContextKey.set(await this.hasProviderSessions()); } })); } + private async hasProviderSessions(): Promise { + const sessions = await this.authenticationService.getSessions(defaultChat.providerId); + return sessions.length > 0; + } + + private async handleDeclaredAuthProviders(): Promise { + if (this.authenticationService.declaredProviders.find(p => p.id === defaultChat.providerId)) { + this.chatSetupSignedInContextKey.set(await this.hasProviderSessions()); + } + } + private async resolveEntitlement(session: AuthenticationSession | undefined): Promise { if (!session) { return; @@ -270,7 +265,7 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution const cts = new CancellationTokenSource(); this._register(toDisposable(() => cts.dispose(true))); - const context = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementUrl, 'GET', session, cts.token)); + const context = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementUrl, 'GET', undefined, session, cts.token)); if (!context) { return UNKNOWN_CHAT_ENTITLEMENT; } @@ -303,18 +298,164 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution return this.resolvedEntitlement; } +} - private async checkExtensionInstallation(): Promise { - const extensions = await this.extensionManagementService.getInstalled(); +interface IChatSetupWelcomeContentOptions { + readonly entitlement: ChatEntitlement | undefined; +} - const chatInstalled = !!extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); - this.chatSetupState.update({ chatInstalled }); +class ChatSetupWelcomeContent extends Disposable { + + readonly element = $('.chat-setup-view'); + + constructor( + private readonly options: IChatSetupWelcomeContentOptions, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IViewsService private readonly viewsService: IViewsService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IProductService private readonly productService: IProductService, + @IChatAgentService private readonly chatAgentService: IChatAgentService + ) { + super(); + + this.create(); + } + + private create(): void { + const markdown = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); + + // Header + this.element.appendChild($('p')).textContent = localize('setupHeader', "{0} is your AI pair programmer.", defaultChat.name); + + // SKU Limited Sign-up + let telemetryCheckbox: Checkbox | undefined; + if (this.options.entitlement === ChatEntitlement.Unknown || this.options.entitlement === ChatEntitlement.Applicable) { + const skuHeader = localize({ key: 'skuHeader', comment: ['{Locked="]({0})"}'] }, "You are entitled to sign-up for {0} [limited access]({1}).", defaultChat.name, defaultChat.skusDocumentationUrl); + this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(skuHeader, { isTrusted: true }))).element); + + const telemetryLabel = localize('setupLimitedCheckbox', "Allow {0} to collect usage data", defaultChat.name); + + const telemetryCheckboxContainer = this.element.appendChild($('p')); + telemetryCheckbox = this._register(new Checkbox(telemetryLabel, false, defaultCheckboxStyles)); + telemetryCheckboxContainer.appendChild(telemetryCheckbox.domNode); + + const telemetryCheckboxLabelContainer = telemetryCheckboxContainer.appendChild($('div')); + telemetryCheckboxLabelContainer.textContent = telemetryLabel; + this._register(addDisposableListener(telemetryCheckboxLabelContainer, EventType.CLICK, () => { + if (telemetryCheckbox?.enabled) { + telemetryCheckbox.checked = !telemetryCheckbox.checked; + } + })); + } + + // Setup Button + if (this.options.entitlement !== ChatEntitlement.Blocked) { + const buttonRow = this.element.appendChild($('p')); + + const button = this._register(new Button(buttonRow, { ...defaultButtonStyles, supportIcons: true })); + this.updateControls(button, telemetryCheckbox, false); + + this._register(button.onDidClick(async () => { + this.updateControls(button, telemetryCheckbox, true); + + await this.setup(telemetryCheckbox?.checked); + + this.updateControls(button, telemetryCheckbox, false); + })); + } + + // Footer + const footer = localize({ key: 'privacyFooter', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}). You can [learn more]({1}) about {2}.", defaultChat.privacyStatementUrl, defaultChat.documentationUrl, defaultChat.name); + this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(footer, { isTrusted: true }))).element); + } + + private updateControls(button: Button, telemetryCheckbox: Checkbox | undefined, setupRunning: boolean): void { + if (setupRunning) { + button.enabled = false; + button.label = localize('setupChatInstalling', "$(loading~spin) Setting up {0}...", defaultChat.name); + telemetryCheckbox?.disable(); + } else { + button.enabled = true; + button.label = this.options.entitlement === ChatEntitlement.Unknown ? + localize('signInAndSetup', "Sign in and Setup {0}", defaultChat.name) : + localize('setup', "Setup {0}", defaultChat.name); + telemetryCheckbox?.enable(); + } + } + + private async setup(enableTelemetry: boolean | undefined): Promise { + let session: AuthenticationSession | undefined; + if (this.options.entitlement === ChatEntitlement.Unknown) { + session = await this.signIn(); + if (!session) { + return false; // user cancelled + } + } + + return this.install(session, enableTelemetry); + } + + private async signIn(): Promise { + let session: AuthenticationSession | undefined; + try { + showChatView(this.viewsService); + session = await this.authenticationService.createSession(defaultChat.providerId, defaultChat.providerScopes); + } catch (error) { + // noop + } + + if (!session) { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false }); + } + + return session; + } + + private async install(session: AuthenticationSession | undefined, enableTelemetry: boolean | undefined): Promise { + const signedIn = !!session; + const activeElement = getActiveElement(); + + let installResult: 'installed' | 'cancelled' | 'failedInstall'; + try { + showChatView(this.viewsService); + + if (this.options.entitlement === ChatEntitlement.Unknown || this.options.entitlement === ChatEntitlement.Applicable) { + await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuLimitedUrl, 'POST', { + public_code_suggestions: 'enabled', + restricted_telemetry: enableTelemetry ? 'enabled' : 'disabled' + }, session, CancellationToken.None)); + } + + await this.extensionsWorkbenchService.install(defaultChat.extensionId, { + enable: true, + isMachineScoped: false, + installPreReleaseVersion: this.productService.quality !== 'stable' + }, ChatViewId); + + installResult = 'installed'; + } catch (error) { + installResult = isCancellationError(error) ? 'cancelled' : 'failedInstall'; + } + + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn }); + + await Promise.race([timeout(5000), Event.toPromise(this.chatAgentService.onDidChangeAgents)]); // reduce flicker (https://github.com/microsoft/vscode-copilot/issues/9274) + + if (activeElement === getActiveElement()) { + (await showChatView(this.viewsService))?.focusInput(); + } + + return installResult === 'installed'; } } class ChatSetupRequestHelper { - static async request(accessor: ServicesAccessor, url: string, type: 'GET' | 'POST', session: AuthenticationSession | undefined, token: CancellationToken): Promise { + static async request(accessor: ServicesAccessor, url: string, type: 'GET', body: undefined, session: AuthenticationSession | undefined, token: CancellationToken): Promise; + static async request(accessor: ServicesAccessor, url: string, type: 'POST', body: object, session: AuthenticationSession | undefined, token: CancellationToken): Promise; + static async request(accessor: ServicesAccessor, url: string, type: 'GET' | 'POST', body: object | undefined, session: AuthenticationSession | undefined, token: CancellationToken): Promise { const requestService = accessor.get(IRequestService); const logService = accessor.get(ILogService); const authenticationService = accessor.get(IAuthenticationService); @@ -331,7 +472,7 @@ class ChatSetupRequestHelper { return await requestService.request({ type, url, - data: type === 'POST' ? JSON.stringify({}) : undefined, + data: type === 'POST' ? JSON.stringify(body) : undefined, headers: { 'Authorization': `Bearer ${session.accessToken}` } @@ -472,128 +613,7 @@ class ChatSetupHideAction extends Action2 { } } -class ChatSetupInstallAction extends Action2 { - - static readonly ID = 'workbench.action.chat.install'; - static readonly TITLE = localize2('installChat', "Install {0}", defaultChat.name); - - constructor() { - super({ - id: ChatSetupInstallAction.ID, - title: ChatSetupInstallAction.TITLE, - category: CHAT_CATEGORY, - menu: { - id: MenuId.ChatCommandCenter, - group: 'a_first', - order: 0, - when: ContextKeyExpr.and( - ChatContextKeys.Setup.signedIn, - ChatContextKeys.Setup.installed.negate() - ) - } - }); - } - - override run(accessor: ServicesAccessor): Promise { - return ChatSetupInstallAction.install(accessor, undefined); - } - - static async install(accessor: ServicesAccessor, session: AuthenticationSession | undefined) { - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - const productService = accessor.get(IProductService); - const telemetryService = accessor.get(ITelemetryService); - const contextKeyService = accessor.get(IContextKeyService); - const viewsService = accessor.get(IViewsService); - const chatAgentService = accessor.get(IChatAgentService); - const instantiationService = accessor.get(IInstantiationService); - - const signedIn = !!session; - const setupInstallingContextKey = ChatContextKeys.Setup.installing.bindTo(contextKeyService); - const activeElement = getActiveElement(); - - let installResult: 'installed' | 'cancelled' | 'failedInstall'; - try { - setupInstallingContextKey.set(true); - showChatView(viewsService); - - await instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuAlternateUrl, 'POST', session, CancellationToken.None)); - - await extensionsWorkbenchService.install(defaultChat.extensionId, { - enable: true, - isMachineScoped: false, - installPreReleaseVersion: productService.quality !== 'stable' - }, ChatViewId); - - installResult = 'installed'; - } catch (error) { - installResult = isCancellationError(error) ? 'cancelled' : 'failedInstall'; - } - - telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn }); - - await Promise.race([timeout(5000), Event.toPromise(chatAgentService.onDidChangeAgents)]); // reduce flicker (https://github.com/microsoft/vscode-copilot/issues/9274) - - setupInstallingContextKey.reset(); - - if (activeElement === getActiveElement()) { - (await showChatView(viewsService))?.focusInput(); - } - } -} - -class ChatSetupSignInAndInstallChatAction extends Action2 { - - static readonly ID = 'workbench.action.chat.signInAndInstall'; - static readonly TITLE = localize2('signInAndInstallChat', "Sign in to use {0}", defaultChat.name); - - constructor() { - super({ - id: ChatSetupSignInAndInstallChatAction.ID, - title: ChatSetupSignInAndInstallChatAction.TITLE, - category: CHAT_CATEGORY, - menu: { - id: MenuId.ChatCommandCenter, - group: 'a_first', - order: 0, - when: ContextKeyExpr.and( - ChatContextKeys.Setup.signedIn.negate(), - ChatContextKeys.Setup.installed.negate() - ) - } - }); - } - - override async run(accessor: ServicesAccessor): Promise { - const authenticationService = accessor.get(IAuthenticationService); - const instantiationService = accessor.get(IInstantiationService); - const telemetryService = accessor.get(ITelemetryService); - const contextKeyService = accessor.get(IContextKeyService); - const viewsService = accessor.get(IViewsService); - - const setupSigningInContextKey = ChatContextKeys.Setup.signingIn.bindTo(contextKeyService); - - let session: AuthenticationSession | undefined; - try { - setupSigningInContextKey.set(true); - showChatView(viewsService); - session = await authenticationService.createSession(defaultChat.providerId, defaultChat.providerScopes); - } catch (error) { - // noop - } finally { - setupSigningInContextKey.reset(); - } - - if (session) { - instantiationService.invokeFunction(accessor => ChatSetupInstallAction.install(accessor, session)); - } else { - telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false }); - } - } -} - registerAction2(ChatSetupTriggerAction); registerAction2(ChatSetupHideAction); -registerAction2(ChatSetupInstallAction); -registerAction2(ChatSetupSignInAndInstallChatAction); registerWorkbenchContribution2('workbench.chat.setup', ChatSetupContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css new file mode 100644 index 0000000000000..988c082bdc611 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-welcome-view .chat-setup-view { + text-align: initial; + + p { + width: 100%; + } + + .codicon[class*='codicon-'] { + font-size: 13px; + line-height: 1.4em; + vertical-align: bottom; + } + + .monaco-button { + text-align: center; + display: inline-block; + width: 100%; + padding: 4px 7px; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index dffc746a0072c..657d1f5869a78 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -68,12 +68,6 @@ div.chat-welcome-view { a { color: var(--vscode-textLink-foreground); } - - .codicon[class*='codicon-'] { - font-size: 13px; - line-height: 1.4em; - vertical-align: bottom; - } } .monaco-button { diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index c68a5442d8cca..b9f7624d6cf00 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -82,13 +82,20 @@ export class ChatViewWelcomeController extends Disposable { this.renderDisposables.clear(); dom.clearNode(this.element!); - const enabledDescriptor = descriptors.find(d => this.contextKeyService.contextMatchesRules(d.when)); + const matchingDescriptors = descriptors.filter(descriptor => this.contextKeyService.contextMatchesRules(descriptor.when)); + let enabledDescriptor: IChatViewsWelcomeDescriptor | undefined; + for (const descriptor of matchingDescriptors) { + if (typeof descriptor.content === 'function') { + enabledDescriptor = descriptor; // when multiple descriptors match, prefer a "core" one over a "descriptive" one + break; + } + } + enabledDescriptor = enabledDescriptor ?? matchingDescriptors.at(0); if (enabledDescriptor) { const content: IChatViewWelcomeContent = { icon: enabledDescriptor.icon, title: enabledDescriptor.title, - message: enabledDescriptor.content, - disableFirstLinkToButton: enabledDescriptor.disableFirstLinkToButton, + message: enabledDescriptor.content }; const welcomeView = this.renderDisposables.add(this.instantiationService.createInstance(ChatViewWelcomePart, content, { firstLinkToButton: true, location: this.location })); this.element!.appendChild(welcomeView.element); @@ -102,8 +109,7 @@ export class ChatViewWelcomeController extends Disposable { export interface IChatViewWelcomeContent { icon?: ThemeIcon; title: string; - message: IMarkdownString; - disableFirstLinkToButton?: boolean; + message: IMarkdownString | ((disposables: DisposableStore) => HTMLElement); tips?: IMarkdownString; } @@ -126,38 +132,47 @@ export class ChatViewWelcomePart extends Disposable { this.element = dom.$('.chat-welcome-view'); try { + const renderer = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); + + // Icon const icon = dom.append(this.element, $('.chat-welcome-view-icon')); + if (content.icon) { + icon.appendChild(renderIcon(content.icon)); + } + + // Title const title = dom.append(this.element, $('.chat-welcome-view-title')); + title.textContent = content.title; + // Preview indicator if (options?.location === ChatAgentLocation.EditingSession) { const featureIndicator = dom.append(this.element, $('.chat-welcome-view-indicator')); featureIndicator.textContent = localize('preview', 'PREVIEW'); } + // Message const message = dom.append(this.element, $('.chat-welcome-view-message')); - - if (content.icon) { - icon.appendChild(renderIcon(content.icon)); - } - - title.textContent = content.title; - const renderer = this.instantiationService.createInstance(MarkdownRenderer, {}); - const messageResult = this._register(renderer.render(content.message)); - const firstLink = options?.firstLinkToButton && !content.disableFirstLinkToButton ? messageResult.element.querySelector('a') : undefined; - if (firstLink) { - const target = firstLink.getAttribute('data-href'); - const button = this._register(new Button(firstLink.parentElement!, defaultButtonStyles)); - button.label = firstLink.textContent ?? ''; - if (target) { - this._register(button.onDidClick(() => { - this.openerService.open(target, { allowCommands: true }); - })); + if (typeof content.message === 'function') { + dom.append(message, content.message(this._register(new DisposableStore()))); + } else { + const messageResult = this._register(renderer.render(content.message)); + const firstLink = options?.firstLinkToButton ? messageResult.element.querySelector('a') : undefined; + if (firstLink) { + const target = firstLink.getAttribute('data-href'); + const button = this._register(new Button(firstLink.parentElement!, defaultButtonStyles)); + button.label = firstLink.textContent ?? ''; + if (target) { + this._register(button.onDidClick(() => { + this.openerService.open(target, { allowCommands: true }); + })); + } + firstLink.replaceWith(button.element); } - firstLink.replaceWith(button.element); - } - dom.append(message, messageResult.element); + dom.append(message, messageResult.element); + } + // Tips if (content.tips) { const tips = dom.append(this.element, $('.chat-welcome-view-tips')); const tipsResult = this._register(renderer.render(content.tips)); diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts index b08baae1460bc..e0e7383bf403e 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts @@ -5,6 +5,7 @@ import { Emitter, Event } from '../../../../../base/common/event.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { ContextKeyExpression } from '../../../../../platform/contextkey/common/contextkey.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; @@ -16,8 +17,7 @@ export const enum ChatViewsWelcomeExtensions { export interface IChatViewsWelcomeDescriptor { icon?: ThemeIcon; title: string; - content: IMarkdownString; - disableFirstLinkToButton?: boolean; + content: IMarkdownString | ((disposables: DisposableStore) => HTMLElement); when: ContextKeyExpression; } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index d5a31e868e169..48940b4604227 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -46,8 +46,6 @@ export namespace ChatContextKeys { entitled: new RawContextKey('chatSetupEntitled', false, { type: 'boolean', description: localize('chatSetupEntitled', "True when chat setup is offered for a signed-in, entitled user.") }), triggered: new RawContextKey('chatSetupTriggered', false, { type: 'boolean', description: localize('chatSetupTriggered', "True when chat setup is triggered.") }), - installing: new RawContextKey('chatSetupInstalling', false, { type: 'boolean', description: localize('chatSetupInstalling', "True when chat setup is installing chat.") }), - signingIn: new RawContextKey('chatSetupSigningIn', false, { type: 'boolean', description: localize('chatSetupSigningIn', "True when chat setup is waiting for signing in.") }), installed: new RawContextKey('chatSetupInstalled', false, { type: 'boolean', description: localize('chatSetupInstalled', "True when the chat extension is installed.") }), }; From 16c86e2b4180e2e72b1c6b518987aa0d1496c4e4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 12:05:47 +0100 Subject: [PATCH 02/21] make actions f1 able --- .../chat/browser/chatSetup.contribution.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index b47c4b1c4d362..6b499008e8550 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -539,12 +539,20 @@ class ChatSetupState { class ChatSetupTriggerAction extends Action2 { static readonly ID = 'workbench.action.chat.triggerSetup'; - static readonly TITLE = localize2('triggerChatSetup', "Trigger Chat Setup"); + static readonly TITLE = localize2('triggerChatSetup', "Setup {0}...", defaultChat.name); constructor() { super({ id: ChatSetupTriggerAction.ID, - title: ChatSetupTriggerAction.TITLE + title: ChatSetupTriggerAction.TITLE, + f1: true, + precondition: ChatContextKeys.Setup.installed.negate(), + menu: { + id: MenuId.ChatCommandCenter, + group: 'a_first', + order: 1, + when: ChatContextKeys.Setup.installed.negate() + } }); } @@ -568,14 +576,11 @@ class ChatSetupHideAction extends Action2 { id: ChatSetupHideAction.ID, title: ChatSetupHideAction.TITLE, f1: true, - precondition: ContextKeyExpr.and( - ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.installed.negate() - ), + precondition: ChatContextKeys.Setup.installed.negate(), menu: { id: MenuId.ChatCommandCenter, group: 'a_first', - order: 1, + order: 2, when: ChatContextKeys.Setup.installed.negate() } }); From 4c512e2ab854966c20d2ec442c28793d72278057 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 12:10:22 +0100 Subject: [PATCH 03/21] feat: enhance chat setup logic to include configuration check --- .../contrib/chat/browser/chatSetup.contribution.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 6b499008e8550..f345b5b7fca91 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -487,6 +487,7 @@ class ChatSetupRequestHelper { class ChatSetupState { + private static readonly CHAT_SETUP_ENABLED = 'chat.experimental.offerSetup'; private static readonly CHAT_SETUP_TRIGGERD = 'chat.setupTriggered'; private static readonly CHAT_EXTENSION_INSTALLED = 'chat.extensionInstalled'; @@ -496,7 +497,8 @@ class ChatSetupState { constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { this.updateContext(); } @@ -521,10 +523,11 @@ class ChatSetupState { } private updateContext(): void { + const chatSetupEnabled = this.configurationService.getValue(ChatSetupState.CHAT_SETUP_ENABLED); const chatSetupTriggered = this.storageService.getBoolean(ChatSetupState.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE, false); const chatInstalled = this.storageService.getBoolean(ChatSetupState.CHAT_EXTENSION_INSTALLED, StorageScope.PROFILE, false); - const showChatSetup = chatSetupTriggered && !chatInstalled; + const showChatSetup = chatSetupEnabled && chatSetupTriggered && !chatInstalled; if (showChatSetup) { // this is ugly but fixes flicker from a previous chat install this.storageService.remove('chat.welcomeMessageContent.panel', StorageScope.APPLICATION); From 8e9afa29218513f21a7e4aef3fd416c7d5a0302d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 12:15:30 +0100 Subject: [PATCH 04/21] revert --- .../workbench/contrib/chat/browser/chatSetup.contribution.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index f345b5b7fca91..8891f614fdefd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -487,7 +487,6 @@ class ChatSetupRequestHelper { class ChatSetupState { - private static readonly CHAT_SETUP_ENABLED = 'chat.experimental.offerSetup'; private static readonly CHAT_SETUP_TRIGGERD = 'chat.setupTriggered'; private static readonly CHAT_EXTENSION_INSTALLED = 'chat.extensionInstalled'; @@ -498,7 +497,6 @@ class ChatSetupState { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IConfigurationService private readonly configurationService: IConfigurationService ) { this.updateContext(); } @@ -523,11 +521,10 @@ class ChatSetupState { } private updateContext(): void { - const chatSetupEnabled = this.configurationService.getValue(ChatSetupState.CHAT_SETUP_ENABLED); const chatSetupTriggered = this.storageService.getBoolean(ChatSetupState.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE, false); const chatInstalled = this.storageService.getBoolean(ChatSetupState.CHAT_EXTENSION_INSTALLED, StorageScope.PROFILE, false); - const showChatSetup = chatSetupEnabled && chatSetupTriggered && !chatInstalled; + const showChatSetup = chatSetupTriggered && !chatInstalled; if (showChatSetup) { // this is ugly but fixes flicker from a previous chat install this.storageService.remove('chat.welcomeMessageContent.panel', StorageScope.APPLICATION); From 7f7bfd1b0de957ee87e9b6cb4f956500648c965d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 12:52:26 +0100 Subject: [PATCH 05/21] feat: enhance chat setup and entitlement handling with improved state management --- .../chat/browser/chatSetup.contribution.ts | 126 ++++++++++-------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 8891f614fdefd..738450f35af15 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -29,7 +29,7 @@ import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { showChatView, ChatViewId } from './chat.js'; import { IChatAgentService } from '../common/chatAgents.js'; -import { Event } from '../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; import product from '../../../../platform/product/common/product.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; @@ -97,7 +97,8 @@ enum ChatEntitlement { Unknown = 1, Applicable, Limited, - Payed, + Trialing, + Paying, Blocked } @@ -105,10 +106,14 @@ const UNKNOWN_CHAT_ENTITLEMENT: IChatEntitlement = {}; class ChatSetupContribution extends Disposable implements IWorkbenchContribution { + private readonly chatSetupState = this.instantiationService.createInstance(ChatSetupState); + private readonly entitlementsResolver = this._register(this.instantiationService.createInstance(ChatSetupEntitlementResolver)); + constructor( @IProductService private readonly productService: IProductService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IExtensionService private readonly extensionService: IExtensionService, ) { super(); @@ -118,74 +123,66 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution this.registerChatWelcome(); - this.checkEntitlements(); this.checkExtensionInstallation(); } private registerChatWelcome(): void { - - // Setup: Triggered (signed-out) - Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: defaultChat.chatWelcomeTitle, - when: ContextKeyExpr.and( - ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.signedIn.negate(), - ChatContextKeys.Setup.installed.negate() - )!, - icon: defaultChat.icon, - content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, { - entitlement: ChatEntitlement.Unknown - })).element, - }); - - // Setup: Triggered (signed-in) Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ title: defaultChat.chatWelcomeTitle, when: ContextKeyExpr.and( ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.signedIn, ChatContextKeys.Setup.installed.negate() )!, icon: defaultChat.icon, - content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, { - entitlement: ChatEntitlement.Applicable - })).element, + content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, this.entitlementsResolver)).element, }); } - private checkEntitlements(): void { - const entitlementsResolver = this._register(this.instantiationService.createInstance(ChatSetupEntitlementResolver)); - entitlementsResolver.resolve(); - } - private async checkExtensionInstallation(): Promise { + this._register(this.extensionService.onDidChangeExtensions(result => { + for (const extension of result.removed) { + if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { + this.chatSetupState.update({ chatInstalled: false }); + break; + } + } + + for (const extension of result.added) { + if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { + this.chatSetupState.update({ chatInstalled: true }); + break; + } + } + })); + const extensions = await this.extensionManagementService.getInstalled(); const chatInstalled = !!extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); - this.instantiationService.createInstance(ChatSetupState).update({ chatInstalled }); + this.chatSetupState.update({ chatInstalled }); } } class ChatSetupEntitlementResolver extends Disposable { + private _entitlement = ChatEntitlement.Unknown; + get entitlement() { return this._entitlement; } + + private readonly _onDidChangeEntitlement = this._register(new Emitter()); + readonly onDidChangeEntitlement = this._onDidChangeEntitlement.event; + private readonly chatSetupSignedInContextKey = ChatContextKeys.Setup.signedIn.bindTo(this.contextKeyService); private readonly chatSetupEntitledContextKey = ChatContextKeys.Setup.entitled.bindTo(this.contextKeyService); - private readonly chatSetupState = this.instantiationService.createInstance(ChatSetupState); - private resolvedEntitlement: IChatEntitlement | undefined = undefined; constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, - @IExtensionService private readonly extensionService: IExtensionService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); - } - resolve(): void { this.registerEntitlementListeners(); this.registerAuthListeners(); @@ -193,28 +190,13 @@ class ChatSetupEntitlementResolver extends Disposable { } private registerEntitlementListeners(): void { - this._register(this.extensionService.onDidChangeExtensions(result => { - for (const extension of result.removed) { - if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { - this.chatSetupState.update({ chatInstalled: false }); - break; - } - } - - for (const extension of result.added) { - if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { - this.chatSetupState.update({ chatInstalled: true }); - break; - } - } - })); - this._register(this.authenticationService.onDidChangeSessions(e => { if (e.providerId === defaultChat.providerId) { if (e.event.added?.length) { this.resolveEntitlement(e.event.added[0]); } else if (e.event.removed?.length) { - this.chatSetupEntitledContextKey.set(false); + this.resolvedEntitlement = undefined; + this.update({ entitled: false }); } } })); @@ -232,7 +214,7 @@ class ChatSetupEntitlementResolver extends Disposable { this._register(this.authenticationService.onDidChangeSessions(async ({ providerId }) => { if (providerId === defaultChat.providerId) { - this.chatSetupSignedInContextKey.set(await this.hasProviderSessions()); + this.update({ signedIn: await this.hasProviderSessions() }); } })); } @@ -244,7 +226,7 @@ class ChatSetupEntitlementResolver extends Disposable { private async handleDeclaredAuthProviders(): Promise { if (this.authenticationService.declaredProviders.find(p => p.id === defaultChat.providerId)) { - this.chatSetupSignedInContextKey.set(await this.hasProviderSessions()); + this.update({ signedIn: await this.hasProviderSessions() }); } } @@ -254,7 +236,7 @@ class ChatSetupEntitlementResolver extends Disposable { } const entitlement = await this.doResolveEntitlement(session); - this.chatSetupEntitledContextKey.set(!!entitlement.chatEnabled); + this.update({ entitled: !!entitlement.chatEnabled }); } private async doResolveEntitlement(session: AuthenticationSession): Promise { @@ -298,10 +280,29 @@ class ChatSetupEntitlementResolver extends Disposable { return this.resolvedEntitlement; } + + private update(context: { entitled: boolean }): void; + private update(context: { signedIn: boolean }): void; + private update(context: { entitled?: boolean; signedIn?: boolean }): void { + if (typeof context.entitled === 'boolean') { + this.chatSetupEntitledContextKey.set(context.entitled); + } + + if (typeof context.signedIn === 'boolean') { + this.chatSetupSignedInContextKey.set(context.signedIn); + + const entitlement = this._entitlement; + this._entitlement = context.signedIn ? ChatEntitlement.Applicable : ChatEntitlement.Unknown; + if (entitlement !== this._entitlement) { + this._onDidChangeEntitlement.fire(this._entitlement); + } + } + } } interface IChatSetupWelcomeContentOptions { - readonly entitlement: ChatEntitlement | undefined; + readonly entitlement: ChatEntitlement; + readonly onDidChangeEntitlement: Event; } class ChatSetupWelcomeContent extends Disposable { @@ -352,18 +353,27 @@ class ChatSetupWelcomeContent extends Disposable { // Setup Button if (this.options.entitlement !== ChatEntitlement.Blocked) { + let setupRunning = false; + const buttonRow = this.element.appendChild($('p')); const button = this._register(new Button(buttonRow, { ...defaultButtonStyles, supportIcons: true })); this.updateControls(button, telemetryCheckbox, false); this._register(button.onDidClick(async () => { - this.updateControls(button, telemetryCheckbox, true); + setupRunning = true; + this.updateControls(button, telemetryCheckbox, setupRunning); - await this.setup(telemetryCheckbox?.checked); + try { + await this.setup(telemetryCheckbox?.checked); + } finally { + setupRunning = false; + } - this.updateControls(button, telemetryCheckbox, false); + this.updateControls(button, telemetryCheckbox, setupRunning); })); + + this._register(this.options.onDidChangeEntitlement(() => this.updateControls(button, telemetryCheckbox, setupRunning))); } // Footer From 29c96392d6d0f3766dea3d53f7e6bf38ef582040 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 12:58:03 +0100 Subject: [PATCH 06/21] less timeout --- .../workbench/contrib/chat/browser/chatSetup.contribution.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 738450f35af15..5948286f60110 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -18,7 +18,6 @@ import { CancellationToken, CancellationTokenSource } from '../../../../base/com import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IRequestContext } from '../../../../base/parts/request/common/request.js'; -import { timeout } from '../../../../base/common/async.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../nls.js'; @@ -28,7 +27,6 @@ import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { showChatView, ChatViewId } from './chat.js'; -import { IChatAgentService } from '../common/chatAgents.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import product from '../../../../platform/product/common/product.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -317,7 +315,6 @@ class ChatSetupWelcomeContent extends Disposable { @IViewsService private readonly viewsService: IViewsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IProductService private readonly productService: IProductService, - @IChatAgentService private readonly chatAgentService: IChatAgentService ) { super(); @@ -451,8 +448,6 @@ class ChatSetupWelcomeContent extends Disposable { this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn }); - await Promise.race([timeout(5000), Event.toPromise(this.chatAgentService.onDidChangeAgents)]); // reduce flicker (https://github.com/microsoft/vscode-copilot/issues/9274) - if (activeElement === getActiveElement()) { (await showChatView(this.viewsService))?.focusInput(); } From d7222e3495bbe50ebde3cc8fb356e2c7805cbd61 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 13:37:58 +0100 Subject: [PATCH 07/21] feat: update chat watermark entries to check for chat setup installation --- src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts | 4 ++-- src/vs/workbench/contrib/chat/browser/actions/chatActions.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 3c96d9f9ffc09..0c7e37ccc0972 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -38,8 +38,8 @@ const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "F const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: { web: ContextKeyExpr.equals('terminalProcessSupported', true) } }; const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: { web: ContextKeyExpr.equals('terminalProcessSupported', true) } }; const openSettings: WatermarkEntry = { text: localize('watermark.openSettings', "Open Settings"), id: 'workbench.action.openSettings' }; -const openChat: WatermarkEntry = { text: localize('watermark.openChat', "Open Chat"), id: 'workbench.action.chat.open', when: { native: ContextKeyExpr.equals('chatPanelParticipantRegistered', true), web: ContextKeyExpr.equals('chatPanelParticipantRegistered', true) } }; -const openCopilotEdits: WatermarkEntry = { text: localize('watermark.openCopilotEdits', "Open Copilot Edits"), id: 'workbench.action.chat.openEditSession', when: { native: ContextKeyExpr.equals('chatEditingParticipantRegistered', true), web: ContextKeyExpr.equals('chatEditingParticipantRegistered', true) } }; +const openChat: WatermarkEntry = { text: localize('watermark.openChat', "Open Chat"), id: 'workbench.action.chat.open', when: { native: ContextKeyExpr.equals('chatSetupInstalled', true), web: ContextKeyExpr.equals('chatSetupInstalled', true) } }; +const openCopilotEdits: WatermarkEntry = { text: localize('watermark.openCopilotEdits', "Open Copilot Edits"), id: 'workbench.action.chat.openEditSession', when: { native: ContextKeyExpr.equals('chatSetupInstalled', true), web: ContextKeyExpr.equals('chatSetupInstalled', true) } }; const emptyWindowEntries: WatermarkEntry[] = coalesce([ showCommands, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 59c8caf43aea9..9ee591e5fc2d0 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -562,8 +562,8 @@ export class ChatCommandCenterRendering implements IWorkbenchContribution { const chatExtensionInstalled = agentService.getAgents().some(agent => agent.isDefault); const primaryAction = instantiationService.createInstance(MenuItemAction, { - id: chatExtensionInstalled ? CHAT_OPEN_ACTION_ID : 'workbench.action.chat.triggerSetup', // TODO@bpasero revisit layering of this action - title: OpenChatGlobalAction.TITLE, + id: chatExtensionInstalled ? CHAT_OPEN_ACTION_ID : 'workbench.action.chat.triggerSetup', + title: chatExtensionInstalled ? OpenChatGlobalAction.TITLE : localize2('triggerChatSetup', "Setup {0}...", defaultChat.name), icon: defaultChat.icon, }, undefined, undefined, undefined, undefined); From 298f7e35111b375c174ded445471bafe52cc63f9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 14:23:04 +0100 Subject: [PATCH 08/21] feat: update chat setup labels for clarity and adjust telemetry settings --- .../contrib/chat/browser/chatSetup.contribution.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 5948286f60110..bf98b59eadfea 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -381,13 +381,13 @@ class ChatSetupWelcomeContent extends Disposable { private updateControls(button: Button, telemetryCheckbox: Checkbox | undefined, setupRunning: boolean): void { if (setupRunning) { button.enabled = false; - button.label = localize('setupChatInstalling', "$(loading~spin) Setting up {0}...", defaultChat.name); + button.label = localize('setupChatInstalling', "$(loading~spin) Completing Setup..."); telemetryCheckbox?.disable(); } else { button.enabled = true; button.label = this.options.entitlement === ChatEntitlement.Unknown ? - localize('signInAndSetup', "Sign in and Setup {0}", defaultChat.name) : - localize('setup', "Setup {0}", defaultChat.name); + localize('signInAndSetup', "Sign in and Complete Setup") : + localize('setup', "Complete Setup"); telemetryCheckbox?.enable(); } } @@ -431,7 +431,7 @@ class ChatSetupWelcomeContent extends Disposable { if (this.options.entitlement === ChatEntitlement.Unknown || this.options.entitlement === ChatEntitlement.Applicable) { await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuLimitedUrl, 'POST', { public_code_suggestions: 'enabled', - restricted_telemetry: enableTelemetry ? 'enabled' : 'disabled' + restricted_telemetry: enableTelemetry ? 'disabled' : 'enabled' }, session, CancellationToken.None)); } From cfd6caed5857bfd3867ee6e1578a532fd1e0743a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 14:31:05 +0100 Subject: [PATCH 09/21] add all opts --- .../chat/browser/chatSetup.contribution.ts | 62 ++++++++++++------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index bf98b59eadfea..98463e2760738 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -43,6 +43,7 @@ import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform import { Button } from '../../../../base/browser/ui/button/button.js'; import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { coalesce } from '../../../../base/common/arrays.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -329,23 +330,16 @@ class ChatSetupWelcomeContent extends Disposable { // SKU Limited Sign-up let telemetryCheckbox: Checkbox | undefined; + let detectionCheckbox: Checkbox | undefined; if (this.options.entitlement === ChatEntitlement.Unknown || this.options.entitlement === ChatEntitlement.Applicable) { const skuHeader = localize({ key: 'skuHeader', comment: ['{Locked="]({0})"}'] }, "You are entitled to sign-up for {0} [limited access]({1}).", defaultChat.name, defaultChat.skusDocumentationUrl); this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(skuHeader, { isTrusted: true }))).element); - const telemetryLabel = localize('setupLimitedCheckbox', "Allow {0} to collect usage data", defaultChat.name); + const telemetryLabel = localize('telemetryLabel', "Allow to collect usage data"); + telemetryCheckbox = this.createCheckBox(telemetryLabel, false); - const telemetryCheckboxContainer = this.element.appendChild($('p')); - telemetryCheckbox = this._register(new Checkbox(telemetryLabel, false, defaultCheckboxStyles)); - telemetryCheckboxContainer.appendChild(telemetryCheckbox.domNode); - - const telemetryCheckboxLabelContainer = telemetryCheckboxContainer.appendChild($('div')); - telemetryCheckboxLabelContainer.textContent = telemetryLabel; - this._register(addDisposableListener(telemetryCheckboxLabelContainer, EventType.CLICK, () => { - if (telemetryCheckbox?.enabled) { - telemetryCheckbox.checked = !telemetryCheckbox.checked; - } - })); + const detectionLabel = localize('detectionLabel', "Allow suggestions matching public code"); + detectionCheckbox = this.createCheckBox(detectionLabel, true); } // Setup Button @@ -355,22 +349,22 @@ class ChatSetupWelcomeContent extends Disposable { const buttonRow = this.element.appendChild($('p')); const button = this._register(new Button(buttonRow, { ...defaultButtonStyles, supportIcons: true })); - this.updateControls(button, telemetryCheckbox, false); + this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), false); this._register(button.onDidClick(async () => { setupRunning = true; - this.updateControls(button, telemetryCheckbox, setupRunning); + this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning); try { - await this.setup(telemetryCheckbox?.checked); + await this.setup(telemetryCheckbox?.checked, detectionCheckbox?.checked); } finally { setupRunning = false; } - this.updateControls(button, telemetryCheckbox, setupRunning); + this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning); })); - this._register(this.options.onDidChangeEntitlement(() => this.updateControls(button, telemetryCheckbox, setupRunning))); + this._register(this.options.onDidChangeEntitlement(() => this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning))); } // Footer @@ -378,21 +372,41 @@ class ChatSetupWelcomeContent extends Disposable { this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(footer, { isTrusted: true }))).element); } - private updateControls(button: Button, telemetryCheckbox: Checkbox | undefined, setupRunning: boolean): void { + private createCheckBox(label: string, checked: boolean) { + const checkboxContainer = this.element.appendChild($('p')); + const checkbox = this._register(new Checkbox(label, checked, defaultCheckboxStyles)); + checkboxContainer.appendChild(checkbox.domNode); + + const telemetryCheckboxLabelContainer = checkboxContainer.appendChild($('div')); + telemetryCheckboxLabelContainer.textContent = label; + this._register(addDisposableListener(telemetryCheckboxLabelContainer, EventType.CLICK, () => { + if (checkbox?.enabled) { + checkbox.checked = !checkbox.checked; + } + })); + + return checkbox; + } + + private updateControls(button: Button, checkboxes: Checkbox[], setupRunning: boolean): void { if (setupRunning) { button.enabled = false; button.label = localize('setupChatInstalling', "$(loading~spin) Completing Setup..."); - telemetryCheckbox?.disable(); + for (const checkbox of checkboxes) { + checkbox.disable(); + } } else { button.enabled = true; button.label = this.options.entitlement === ChatEntitlement.Unknown ? localize('signInAndSetup', "Sign in and Complete Setup") : localize('setup', "Complete Setup"); - telemetryCheckbox?.enable(); + for (const checkbox of checkboxes) { + checkbox.enable(); + } } } - private async setup(enableTelemetry: boolean | undefined): Promise { + private async setup(enableTelemetry: boolean | undefined, enableDetection: boolean | undefined): Promise { let session: AuthenticationSession | undefined; if (this.options.entitlement === ChatEntitlement.Unknown) { session = await this.signIn(); @@ -401,7 +415,7 @@ class ChatSetupWelcomeContent extends Disposable { } } - return this.install(session, enableTelemetry); + return this.install(session, enableTelemetry, enableDetection); } private async signIn(): Promise { @@ -420,7 +434,7 @@ class ChatSetupWelcomeContent extends Disposable { return session; } - private async install(session: AuthenticationSession | undefined, enableTelemetry: boolean | undefined): Promise { + private async install(session: AuthenticationSession | undefined, enableTelemetry: boolean | undefined, enableDetection: boolean | undefined): Promise { const signedIn = !!session; const activeElement = getActiveElement(); @@ -430,7 +444,7 @@ class ChatSetupWelcomeContent extends Disposable { if (this.options.entitlement === ChatEntitlement.Unknown || this.options.entitlement === ChatEntitlement.Applicable) { await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuLimitedUrl, 'POST', { - public_code_suggestions: 'enabled', + public_code_suggestions: enableDetection ? 'enabled' : 'disabled', restricted_telemetry: enableTelemetry ? 'disabled' : 'enabled' }, session, CancellationToken.None)); } From 2ee8a218917302ea7501aa08e81b7ddd7e66e335 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 14:35:51 +0100 Subject: [PATCH 10/21] wording --- src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 98463e2760738..14b11b33e8282 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -332,7 +332,7 @@ class ChatSetupWelcomeContent extends Disposable { let telemetryCheckbox: Checkbox | undefined; let detectionCheckbox: Checkbox | undefined; if (this.options.entitlement === ChatEntitlement.Unknown || this.options.entitlement === ChatEntitlement.Applicable) { - const skuHeader = localize({ key: 'skuHeader', comment: ['{Locked="]({0})"}'] }, "You are entitled to sign-up for {0} [limited access]({1}).", defaultChat.name, defaultChat.skusDocumentationUrl); + const skuHeader = localize({ key: 'skuHeader', comment: ['{Locked="]({0})"}'] }, "Setup will sign you up to {0} [limited access]({1}).", defaultChat.name, defaultChat.skusDocumentationUrl); this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(skuHeader, { isTrusted: true }))).element); const telemetryLabel = localize('telemetryLabel', "Allow to collect usage data"); From aee6abbf6832529d9f384df926de2c1847ddb57c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 15:40:54 +0100 Subject: [PATCH 11/21] shared --- .../chat/browser/chatSetup.contribution.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 14b11b33e8282..96ae58d0279de 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -133,7 +133,7 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution ChatContextKeys.Setup.installed.negate() )!, icon: defaultChat.icon, - content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, this.entitlementsResolver)).element, + content: () => ChatSetupWelcomeContent.getInstance(this.instantiationService, this.entitlementsResolver).element, }); } @@ -306,6 +306,15 @@ interface IChatSetupWelcomeContentOptions { class ChatSetupWelcomeContent extends Disposable { + private static INSTANCE: ChatSetupWelcomeContent | undefined; + static getInstance(instantiationService: IInstantiationService, options: IChatSetupWelcomeContentOptions): ChatSetupWelcomeContent { + if (!ChatSetupWelcomeContent.INSTANCE) { + ChatSetupWelcomeContent.INSTANCE = instantiationService.createInstance(ChatSetupWelcomeContent, options); + } + + return ChatSetupWelcomeContent.INSTANCE; + } + readonly element = $('.chat-setup-view'); constructor( @@ -372,7 +381,7 @@ class ChatSetupWelcomeContent extends Disposable { this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(footer, { isTrusted: true }))).element); } - private createCheckBox(label: string, checked: boolean) { + private createCheckBox(label: string, checked: boolean): Checkbox { const checkboxContainer = this.element.appendChild($('p')); const checkbox = this._register(new Checkbox(label, checked, defaultCheckboxStyles)); checkboxContainer.appendChild(checkbox.domNode); @@ -392,6 +401,7 @@ class ChatSetupWelcomeContent extends Disposable { if (setupRunning) { button.enabled = false; button.label = localize('setupChatInstalling', "$(loading~spin) Completing Setup..."); + for (const checkbox of checkboxes) { checkbox.disable(); } @@ -400,6 +410,7 @@ class ChatSetupWelcomeContent extends Disposable { button.label = this.options.entitlement === ChatEntitlement.Unknown ? localize('signInAndSetup', "Sign in and Complete Setup") : localize('setup', "Complete Setup"); + for (const checkbox of checkboxes) { checkbox.enable(); } From fa248b6c1cc1c2d9ec7a3dd7b6966151e1f99d54 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 16:17:29 +0100 Subject: [PATCH 12/21] feat: remove unused signedIn context key from chat setup --- .../workbench/contrib/chat/browser/chatSetup.contribution.ts | 3 --- src/vs/workbench/contrib/chat/common/chatContextKeys.ts | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 96ae58d0279de..3090f8f0f440d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -169,7 +169,6 @@ class ChatSetupEntitlementResolver extends Disposable { private readonly _onDidChangeEntitlement = this._register(new Emitter()); readonly onDidChangeEntitlement = this._onDidChangeEntitlement.event; - private readonly chatSetupSignedInContextKey = ChatContextKeys.Setup.signedIn.bindTo(this.contextKeyService); private readonly chatSetupEntitledContextKey = ChatContextKeys.Setup.entitled.bindTo(this.contextKeyService); private resolvedEntitlement: IChatEntitlement | undefined = undefined; @@ -288,8 +287,6 @@ class ChatSetupEntitlementResolver extends Disposable { } if (typeof context.signedIn === 'boolean') { - this.chatSetupSignedInContextKey.set(context.signedIn); - const entitlement = this._entitlement; this._entitlement = context.signedIn ? ChatEntitlement.Applicable : ChatEntitlement.Unknown; if (entitlement !== this._entitlement) { diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 48940b4604227..9e12fc95c2202 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -42,11 +42,8 @@ export namespace ChatContextKeys { export const languageModelsAreUserSelectable = new RawContextKey('chatModelsAreUserSelectable', false, { type: 'boolean', description: localize('chatModelsAreUserSelectable', "True when the chat model can be selected manually by the user.") }); export const Setup = { - signedIn: new RawContextKey('chatSetupSignedIn', false, { type: 'boolean', description: localize('chatSetupSignedIn', "True when chat setup is offered for a signed-in user.") }), entitled: new RawContextKey('chatSetupEntitled', false, { type: 'boolean', description: localize('chatSetupEntitled', "True when chat setup is offered for a signed-in, entitled user.") }), - triggered: new RawContextKey('chatSetupTriggered', false, { type: 'boolean', description: localize('chatSetupTriggered', "True when chat setup is triggered.") }), - installed: new RawContextKey('chatSetupInstalled', false, { type: 'boolean', description: localize('chatSetupInstalled', "True when the chat extension is installed.") }), }; } From c18f27c1693121024e2590f0305cdf18147b78a1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 16:24:33 +0100 Subject: [PATCH 13/21] regions --- .../chat/browser/chatSetup.contribution.ts | 78 ++++++++++++------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 3090f8f0f440d..f8c302a3f9779 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -64,34 +64,6 @@ const defaultChat = { entitlementSkuLimitedUrl: product.defaultChatAgent?.entitlementSkuLimitedUrl ?? '' }; -type ChatSetupEntitlementEnablementClassification = { - entitled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat setup entitled' }; - trial: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is subscribed to chat trial' }; - owner: 'bpasero'; - comment: 'Reporting if the user is chat setup entitled'; -}; - -type ChatSetupEntitlementEnablementEvent = { - entitled: boolean; - trial: boolean; -}; - -type InstallChatClassification = { - owner: 'bpasero'; - comment: 'Provides insight into chat installation.'; - installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' }; - signedIn: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user did sign in prior to installing the extension.' }; -}; -type InstallChatEvent = { - installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn'; - signedIn: boolean; -}; - -interface IChatEntitlement { - readonly chatEnabled?: boolean; - readonly chatSku30DTrial?: boolean; -} - enum ChatEntitlement { Unknown = 1, Applicable, @@ -101,7 +73,7 @@ enum ChatEntitlement { Blocked } -const UNKNOWN_CHAT_ENTITLEMENT: IChatEntitlement = {}; +//#region Contribution class ChatSetupContribution extends Disposable implements IWorkbenchContribution { @@ -161,6 +133,29 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution } } +//#endregion + +//#region Entitlements Resolver + +type ChatSetupEntitlementEnablementClassification = { + entitled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat setup entitled' }; + trial: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is subscribed to chat trial' }; + owner: 'bpasero'; + comment: 'Reporting if the user is chat setup entitled'; +}; + +type ChatSetupEntitlementEnablementEvent = { + entitled: boolean; + trial: boolean; +}; + +interface IChatEntitlement { + readonly chatEnabled?: boolean; + readonly chatSku30DTrial?: boolean; +} + +const UNKNOWN_CHAT_ENTITLEMENT: IChatEntitlement = {}; + class ChatSetupEntitlementResolver extends Disposable { private _entitlement = ChatEntitlement.Unknown; @@ -296,6 +291,21 @@ class ChatSetupEntitlementResolver extends Disposable { } } +//#endregion + +//#region Setup Rendering + +type InstallChatClassification = { + owner: 'bpasero'; + comment: 'Provides insight into chat installation.'; + installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' }; + signedIn: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user did sign in prior to installing the extension.' }; +}; +type InstallChatEvent = { + installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn'; + signedIn: boolean; +}; + interface IChatSetupWelcomeContentOptions { readonly entitlement: ChatEntitlement; readonly onDidChangeEntitlement: Event; @@ -478,6 +488,10 @@ class ChatSetupWelcomeContent extends Disposable { } } +//#endregion + +//#region Helpers + class ChatSetupRequestHelper { static async request(accessor: ServicesAccessor, url: string, type: 'GET', body: undefined, session: AuthenticationSession | undefined, token: CancellationToken): Promise; @@ -563,6 +577,10 @@ class ChatSetupState { } } +//#endregion + +//#region Actions + class ChatSetupTriggerAction extends Action2 { static readonly ID = 'workbench.action.chat.triggerSetup'; @@ -645,6 +663,8 @@ class ChatSetupHideAction extends Action2 { } } +//#endregion + registerAction2(ChatSetupTriggerAction); registerAction2(ChatSetupHideAction); From 2546c412c8270a9a9151d443af188f6adaf41aaf Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 22 Nov 2024 19:56:12 +0100 Subject: [PATCH 14/21] fix bad use of value --- src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index f8c302a3f9779..808cd50ed9269 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -463,7 +463,7 @@ class ChatSetupWelcomeContent extends Disposable { if (this.options.entitlement === ChatEntitlement.Unknown || this.options.entitlement === ChatEntitlement.Applicable) { await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuLimitedUrl, 'POST', { public_code_suggestions: enableDetection ? 'enabled' : 'disabled', - restricted_telemetry: enableTelemetry ? 'disabled' : 'enabled' + restricted_telemetry: enableTelemetry ? 'enabled' : 'disabled' }, session, CancellationToken.None)); } From 3353f2e9dc6bc1e4fbc1489e05db8a8ef0f775da Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 23 Nov 2024 08:21:51 +0100 Subject: [PATCH 15/21] sku checks --- src/vs/base/common/product.ts | 3 +- .../chat/browser/chatSetup.contribution.ts | 159 +++++++++--------- 2 files changed, 82 insertions(+), 80 deletions(-) diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 5dbddfb8578f2..6bdb61149c59b 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -317,7 +317,6 @@ export interface IDefaultChatAgent { readonly providerScopes: string[]; readonly entitlementUrl: string; readonly entitlementChatEnabled: string; - readonly entitlementSkuKey: string; - readonly entitlementSku30DTrialValue: string; readonly entitlementSkuLimitedUrl: string; + readonly entitlementSkuLimitedEnabled: string; } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 808cd50ed9269..1f6dca1bc88d8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -58,19 +58,20 @@ const defaultChat = { providerName: product.defaultChatAgent?.providerName ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [], entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', - entitlementSkuKey: product.defaultChatAgent?.entitlementSkuKey ?? '', - entitlementSku30DTrialValue: product.defaultChatAgent?.entitlementSku30DTrialValue ?? '', entitlementChatEnabled: product.defaultChatAgent?.entitlementChatEnabled ?? '', - entitlementSkuLimitedUrl: product.defaultChatAgent?.entitlementSkuLimitedUrl ?? '' + entitlementSkuLimitedUrl: product.defaultChatAgent?.entitlementSkuLimitedUrl ?? '', + entitlementSkuLimitedEnabled: product.defaultChatAgent?.entitlementSkuLimitedEnabled ?? '' }; enum ChatEntitlement { + /** Signed out */ Unknown = 1, - Applicable, - Limited, - Trialing, - Paying, - Blocked + /** Not yet resolved */ + Unresolved, + /** Signed in and entitled to Sign-up */ + Available, + /** Signed in but not entitled to Sign-up */ + Unavailable } //#region Contribution @@ -137,25 +138,18 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution //#region Entitlements Resolver -type ChatSetupEntitlementEnablementClassification = { +type ChatSetupEntitlementClassification = { entitled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat setup entitled' }; - trial: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is subscribed to chat trial' }; + entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; owner: 'bpasero'; - comment: 'Reporting if the user is chat setup entitled'; + comment: 'Reporting chat setup entitlements'; }; -type ChatSetupEntitlementEnablementEvent = { +type ChatSetupEntitlementEvent = { entitled: boolean; - trial: boolean; + entitlement: ChatEntitlement; }; -interface IChatEntitlement { - readonly chatEnabled?: boolean; - readonly chatSku30DTrial?: boolean; -} - -const UNKNOWN_CHAT_ENTITLEMENT: IChatEntitlement = {}; - class ChatSetupEntitlementResolver extends Disposable { private _entitlement = ChatEntitlement.Unknown; @@ -166,7 +160,7 @@ class ChatSetupEntitlementResolver extends Disposable { private readonly chatSetupEntitledContextKey = ChatContextKeys.Setup.entitled.bindTo(this.contextKeyService); - private resolvedEntitlement: IChatEntitlement | undefined = undefined; + private resolvedEntitlement: ChatEntitlement | undefined = undefined; constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -186,17 +180,17 @@ class ChatSetupEntitlementResolver extends Disposable { this._register(this.authenticationService.onDidChangeSessions(e => { if (e.providerId === defaultChat.providerId) { if (e.event.added?.length) { - this.resolveEntitlement(e.event.added[0]); + this.resolveEntitlement(e.event.added.at(0)); } else if (e.event.removed?.length) { this.resolvedEntitlement = undefined; - this.update({ entitled: false }); + this.update(this.toEntitlement(false)); } } })); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(async e => { if (e.id === defaultChat.providerId) { - this.resolveEntitlement((await this.authenticationService.getSessions(e.id))[0]); + this.resolveEntitlement((await this.authenticationService.getSessions(e.id)).at(0)); } })); } @@ -207,19 +201,36 @@ class ChatSetupEntitlementResolver extends Disposable { this._register(this.authenticationService.onDidChangeSessions(async ({ providerId }) => { if (providerId === defaultChat.providerId) { - this.update({ signedIn: await this.hasProviderSessions() }); + this.update(this.toEntitlement(await this.hasProviderSessions())); } })); } + private toEntitlement(hasSession: boolean, skuLimitedAvailable?: boolean): ChatEntitlement { + if (!hasSession) { + return ChatEntitlement.Unknown; + } + + if (typeof this.resolvedEntitlement !== 'undefined') { + return this.resolvedEntitlement; + } + + if (typeof skuLimitedAvailable === 'boolean') { + return skuLimitedAvailable ? ChatEntitlement.Available : ChatEntitlement.Unavailable; + } + + return ChatEntitlement.Unresolved; + } + private async hasProviderSessions(): Promise { const sessions = await this.authenticationService.getSessions(defaultChat.providerId); + return sessions.length > 0; } private async handleDeclaredAuthProviders(): Promise { - if (this.authenticationService.declaredProviders.find(p => p.id === defaultChat.providerId)) { - this.update({ signedIn: await this.hasProviderSessions() }); + if (this.authenticationService.declaredProviders.find(provider => provider.id === defaultChat.providerId)) { + this.update(this.toEntitlement(await this.hasProviderSessions())); } } @@ -228,11 +239,10 @@ class ChatSetupEntitlementResolver extends Disposable { return; } - const entitlement = await this.doResolveEntitlement(session); - this.update({ entitled: !!entitlement.chatEnabled }); + this.update(await this.doResolveEntitlement(session)); } - private async doResolveEntitlement(session: AuthenticationSession): Promise { + private async doResolveEntitlement(session: AuthenticationSession): Promise { if (this.resolvedEntitlement) { return this.resolvedEntitlement; } @@ -240,53 +250,48 @@ class ChatSetupEntitlementResolver extends Disposable { const cts = new CancellationTokenSource(); this._register(toDisposable(() => cts.dispose(true))); - const context = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementUrl, 'GET', undefined, session, cts.token)); - if (!context) { - return UNKNOWN_CHAT_ENTITLEMENT; + const response = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementUrl, 'GET', undefined, session, cts.token)); + if (!response) { + return ChatEntitlement.Unresolved; } - if (context.res.statusCode && context.res.statusCode !== 200) { - return UNKNOWN_CHAT_ENTITLEMENT; + if (response.res.statusCode && response.res.statusCode !== 200) { + return ChatEntitlement.Unresolved; } - const result = await asText(context); + const result = await asText(response); if (!result) { - return UNKNOWN_CHAT_ENTITLEMENT; + return ChatEntitlement.Unresolved; } let parsedResult: any; try { parsedResult = JSON.parse(result); } catch (err) { - return UNKNOWN_CHAT_ENTITLEMENT; + return ChatEntitlement.Unresolved; } - this.resolvedEntitlement = { - chatEnabled: Boolean(parsedResult[defaultChat.entitlementChatEnabled]), - chatSku30DTrial: parsedResult[defaultChat.entitlementSkuKey] === defaultChat.entitlementSku30DTrialValue - }; + const entitled = Boolean(parsedResult[defaultChat.entitlementChatEnabled]); + this.chatSetupEntitledContextKey.set(entitled); + + const skuLimitedAvailable = Boolean(parsedResult[defaultChat.entitlementSkuLimitedEnabled]); + this.resolvedEntitlement = this.toEntitlement(entitled, skuLimitedAvailable); - this.telemetryService.publicLog2('chatInstallEntitlement', { - entitled: !!this.resolvedEntitlement.chatEnabled, - trial: !!this.resolvedEntitlement.chatSku30DTrial + console.log(skuLimitedAvailable, this.resolvedEntitlement); + + this.telemetryService.publicLog2('chatInstallEntitlement', { + entitled, + entitlement: this.resolvedEntitlement }); return this.resolvedEntitlement; } - private update(context: { entitled: boolean }): void; - private update(context: { signedIn: boolean }): void; - private update(context: { entitled?: boolean; signedIn?: boolean }): void { - if (typeof context.entitled === 'boolean') { - this.chatSetupEntitledContextKey.set(context.entitled); - } - - if (typeof context.signedIn === 'boolean') { - const entitlement = this._entitlement; - this._entitlement = context.signedIn ? ChatEntitlement.Applicable : ChatEntitlement.Unknown; - if (entitlement !== this._entitlement) { - this._onDidChangeEntitlement.fire(this._entitlement); - } + private update(newEntitlement: ChatEntitlement): void { + const entitlement = this._entitlement; + this._entitlement = newEntitlement; + if (entitlement !== this._entitlement) { + this._onDidChangeEntitlement.fire(this._entitlement); } } } @@ -347,7 +352,7 @@ class ChatSetupWelcomeContent extends Disposable { // SKU Limited Sign-up let telemetryCheckbox: Checkbox | undefined; let detectionCheckbox: Checkbox | undefined; - if (this.options.entitlement === ChatEntitlement.Unknown || this.options.entitlement === ChatEntitlement.Applicable) { + if (this.options.entitlement !== ChatEntitlement.Unavailable) { const skuHeader = localize({ key: 'skuHeader', comment: ['{Locked="]({0})"}'] }, "Setup will sign you up to {0} [limited access]({1}).", defaultChat.name, defaultChat.skusDocumentationUrl); this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(skuHeader, { isTrusted: true }))).element); @@ -359,29 +364,27 @@ class ChatSetupWelcomeContent extends Disposable { } // Setup Button - if (this.options.entitlement !== ChatEntitlement.Blocked) { - let setupRunning = false; + let setupRunning = false; - const buttonRow = this.element.appendChild($('p')); + const buttonRow = this.element.appendChild($('p')); - const button = this._register(new Button(buttonRow, { ...defaultButtonStyles, supportIcons: true })); - this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), false); + const button = this._register(new Button(buttonRow, { ...defaultButtonStyles, supportIcons: true })); + this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), false); - this._register(button.onDidClick(async () => { - setupRunning = true; - this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning); + this._register(button.onDidClick(async () => { + setupRunning = true; + this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning); - try { - await this.setup(telemetryCheckbox?.checked, detectionCheckbox?.checked); - } finally { - setupRunning = false; - } + try { + await this.setup(telemetryCheckbox?.checked, detectionCheckbox?.checked); + } finally { + setupRunning = false; + } - this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning); - })); + this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning); + })); - this._register(this.options.onDidChangeEntitlement(() => this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning))); - } + this._register(this.options.onDidChangeEntitlement(() => this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning))); // Footer const footer = localize({ key: 'privacyFooter', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}). You can [learn more]({1}) about {2}.", defaultChat.privacyStatementUrl, defaultChat.documentationUrl, defaultChat.name); @@ -460,7 +463,7 @@ class ChatSetupWelcomeContent extends Disposable { try { showChatView(this.viewsService); - if (this.options.entitlement === ChatEntitlement.Unknown || this.options.entitlement === ChatEntitlement.Applicable) { + if (this.options.entitlement !== ChatEntitlement.Unavailable) { await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuLimitedUrl, 'POST', { public_code_suggestions: enableDetection ? 'enabled' : 'disabled', restricted_telemetry: enableTelemetry ? 'enabled' : 'disabled' @@ -503,7 +506,7 @@ class ChatSetupRequestHelper { try { if (!session) { - session = (await authenticationService.getSessions(defaultChat.providerId))[0]; + session = (await authenticationService.getSessions(defaultChat.providerId)).at(0); } if (!session) { From 327692ee955c604a9b04640f12e6da7c78b93774 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 23 Nov 2024 08:25:43 +0100 Subject: [PATCH 16/21] wording --- .../contrib/chat/browser/chatSetup.contribution.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 1f6dca1bc88d8..b494b76b18281 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -7,7 +7,7 @@ import './media/chatViewSetup.css'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; import { AuthenticationSession, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; @@ -356,8 +356,8 @@ class ChatSetupWelcomeContent extends Disposable { const skuHeader = localize({ key: 'skuHeader', comment: ['{Locked="]({0})"}'] }, "Setup will sign you up to {0} [limited access]({1}).", defaultChat.name, defaultChat.skusDocumentationUrl); this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(skuHeader, { isTrusted: true }))).element); - const telemetryLabel = localize('telemetryLabel', "Allow to collect usage data"); - telemetryCheckbox = this.createCheckBox(telemetryLabel, false); + const telemetryLabel = localize('telemetryLabel', "Allow {0} to use my data, including Prompts, Suggestions, and Code Snippets, for product improvements", defaultChat.providerName); + telemetryCheckbox = this.createCheckBox(telemetryLabel, this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? false : false); const detectionLabel = localize('detectionLabel', "Allow suggestions matching public code"); detectionCheckbox = this.createCheckBox(detectionLabel, true); From 98a8d4bb88c78964b6eda85b83fbbb35df3fa924 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 23 Nov 2024 08:38:54 +0100 Subject: [PATCH 17/21] logs --- .../contrib/chat/browser/chatSetup.contribution.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index b494b76b18281..fb541172f73d4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -167,6 +167,7 @@ class ChatSetupEntitlementResolver extends Disposable { @ITelemetryService private readonly telemetryService: ITelemetryService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILogService private readonly logService: ILogService, ) { super(); @@ -252,22 +253,27 @@ class ChatSetupEntitlementResolver extends Disposable { const response = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementUrl, 'GET', undefined, session, cts.token)); if (!response) { + this.logService.trace('[chat setup] entitlement: no response'); return ChatEntitlement.Unresolved; } if (response.res.statusCode && response.res.statusCode !== 200) { + this.logService.trace(`[chat setup] entitlement: unexpected status code ${response.res.statusCode}`); return ChatEntitlement.Unresolved; } const result = await asText(response); if (!result) { + this.logService.trace('[chat setup] entitlement: response has no content'); return ChatEntitlement.Unresolved; } let parsedResult: any; try { parsedResult = JSON.parse(result); + this.logService.trace(`[chat setup] entitlement: parsed result is ${JSON.stringify(parsedResult)}`); } catch (err) { + this.logService.trace(`[chat setup] entitlement: error parsing response (${err})`); return ChatEntitlement.Unresolved; } @@ -277,7 +283,7 @@ class ChatSetupEntitlementResolver extends Disposable { const skuLimitedAvailable = Boolean(parsedResult[defaultChat.entitlementSkuLimitedEnabled]); this.resolvedEntitlement = this.toEntitlement(entitled, skuLimitedAvailable); - console.log(skuLimitedAvailable, this.resolvedEntitlement); + this.logService.trace(`[chat setup] entitlement: resolved to ${this.resolvedEntitlement}`); this.telemetryService.publicLog2('chatInstallEntitlement', { entitled, From e6efcf8af240a2fa0aae206ee11d650d4041cf01 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 23 Nov 2024 08:52:58 +0100 Subject: [PATCH 18/21] fix scopes --- src/vs/base/common/product.ts | 2 +- .../chat/browser/chatSetup.contribution.ts | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 6bdb61149c59b..2c8fb00b77789 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -314,7 +314,7 @@ export interface IDefaultChatAgent { readonly skusDocumentationUrl: string; readonly providerId: string; readonly providerName: string; - readonly providerScopes: string[]; + readonly providerScopes: string[][]; readonly entitlementUrl: string; readonly entitlementChatEnabled: string; readonly entitlementSkuLimitedUrl: string; diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index fb541172f73d4..67b4ef8bfdba7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -56,7 +56,7 @@ const defaultChat = { skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '', providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', - providerScopes: product.defaultChatAgent?.providerScopes ?? [], + providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', entitlementChatEnabled: product.defaultChatAgent?.entitlementChatEnabled ?? '', entitlementSkuLimitedUrl: product.defaultChatAgent?.entitlementSkuLimitedUrl ?? '', @@ -223,16 +223,27 @@ class ChatSetupEntitlementResolver extends Disposable { return ChatEntitlement.Unresolved; } + private async handleDeclaredAuthProviders(): Promise { + if (this.authenticationService.declaredProviders.find(provider => provider.id === defaultChat.providerId)) { + this.update(this.toEntitlement(await this.hasProviderSessions())); + } + } + private async hasProviderSessions(): Promise { const sessions = await this.authenticationService.getSessions(defaultChat.providerId); + for (const session of sessions) { + for (const scopes of defaultChat.providerScopes) { + if (this.scopesMatch(session.scopes, scopes)) { + return true; + } + } + } - return sessions.length > 0; + return false; } - private async handleDeclaredAuthProviders(): Promise { - if (this.authenticationService.declaredProviders.find(provider => provider.id === defaultChat.providerId)) { - this.update(this.toEntitlement(await this.hasProviderSessions())); - } + private scopesMatch(scopes: ReadonlyArray, expectedScopes: string[]): boolean { + return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope)); } private async resolveEntitlement(session: AuthenticationSession | undefined): Promise { @@ -449,7 +460,7 @@ class ChatSetupWelcomeContent extends Disposable { let session: AuthenticationSession | undefined; try { showChatView(this.viewsService); - session = await this.authenticationService.createSession(defaultChat.providerId, defaultChat.providerScopes); + session = await this.authenticationService.createSession(defaultChat.providerId, defaultChat.providerScopes[0]); } catch (error) { // noop } From 47b6711503e74def4de8d9ee9a33454914a2c1b7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 23 Nov 2024 09:15:26 +0100 Subject: [PATCH 19/21] log --- .../chat/browser/chatSetup.contribution.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 67b4ef8bfdba7..58cc5cd1a1fa0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -35,7 +35,7 @@ import { IChatViewsWelcomeContributionRegistry, ChatViewsWelcomeExtensions } fro import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { $, addDisposableListener, EventType, getActiveElement } from '../../../../base/browser/dom.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; +import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; @@ -354,6 +354,7 @@ class ChatSetupWelcomeContent extends Disposable { @IViewsService private readonly viewsService: IViewsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IProductService private readonly productService: IProductService, + @ILogService private readonly logService: ILogService, ) { super(); @@ -481,10 +482,18 @@ class ChatSetupWelcomeContent extends Disposable { showChatView(this.viewsService); if (this.options.entitlement !== ChatEntitlement.Unavailable) { - await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuLimitedUrl, 'POST', { + const body = { public_code_suggestions: enableDetection ? 'enabled' : 'disabled', restricted_telemetry: enableTelemetry ? 'enabled' : 'disabled' - }, session, CancellationToken.None)); + }; + this.logService.trace(`[chat setup] install: signing up to limited SKU with ${JSON.stringify(body)}`); + + const response = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuLimitedUrl, 'POST', body, session, CancellationToken.None)); + if (response && this.logService.getLevel() === LogLevel.Trace) { + this.logService.trace(`[chat setup] install: response from signing up to limited SKU ${JSON.stringify(await asText(response))}`); + } + } else { + this.logService.trace('[chat setup] install: not signing up to limited SKU'); } await this.extensionsWorkbenchService.install(defaultChat.extensionId, { @@ -495,6 +504,8 @@ class ChatSetupWelcomeContent extends Disposable { installResult = 'installed'; } catch (error) { + this.logService.trace(`[chat setup] install: error ${error}`); + installResult = isCancellationError(error) ? 'cancelled' : 'failedInstall'; } @@ -539,7 +550,7 @@ class ChatSetupRequestHelper { } }, token); } catch (error) { - logService.error(error); + logService.error(`[chat setup] request: error ${error}`); return undefined; } From dd92f2d4ad0c78b85897d01f2d5d59627ed8c724 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 23 Nov 2024 09:15:55 +0100 Subject: [PATCH 20/21] . --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c08b11f0c0f0..be07c69264541 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.96.0", - "distro": "7d14bf7e9a283e1c9ca8b18ccb0c13274a3757cf", + "distro": "0f524a5cfa305bf4a8cc06ca7fd2d4363ec8c7c1", "author": { "name": "Microsoft Corporation" }, From c80f25e20aff11d8e442399bc3e61c952e14fc1e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 23 Nov 2024 09:28:25 +0100 Subject: [PATCH 21/21] . --- .../chat/browser/chatSetup.contribution.ts | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 58cc5cd1a1fa0..038ec3f393077 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -34,7 +34,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IChatViewsWelcomeContributionRegistry, ChatViewsWelcomeExtensions } from './viewsWelcome/chatViewsWelcome.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { $, addDisposableListener, EventType, getActiveElement } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, EventType, getActiveElement, setVisibility } from '../../../../base/browser/dom.js'; import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; @@ -43,7 +43,6 @@ import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform import { Button } from '../../../../base/browser/ui/button/button.js'; import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { coalesce } from '../../../../base/common/arrays.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -368,18 +367,14 @@ class ChatSetupWelcomeContent extends Disposable { this.element.appendChild($('p')).textContent = localize('setupHeader', "{0} is your AI pair programmer.", defaultChat.name); // SKU Limited Sign-up - let telemetryCheckbox: Checkbox | undefined; - let detectionCheckbox: Checkbox | undefined; - if (this.options.entitlement !== ChatEntitlement.Unavailable) { - const skuHeader = localize({ key: 'skuHeader', comment: ['{Locked="]({0})"}'] }, "Setup will sign you up to {0} [limited access]({1}).", defaultChat.name, defaultChat.skusDocumentationUrl); - this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(skuHeader, { isTrusted: true }))).element); + const skuHeader = localize({ key: 'skuHeader', comment: ['{Locked="]({0})"}'] }, "Setup will sign you up to {0} [limited access]({1}).", defaultChat.name, defaultChat.skusDocumentationUrl); + const skuHeaderElement = this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(skuHeader, { isTrusted: true }))).element); - const telemetryLabel = localize('telemetryLabel', "Allow {0} to use my data, including Prompts, Suggestions, and Code Snippets, for product improvements", defaultChat.providerName); - telemetryCheckbox = this.createCheckBox(telemetryLabel, this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? false : false); + const telemetryLabel = localize('telemetryLabel', "Allow {0} to use my data, including Prompts, Suggestions, and Code Snippets, for product improvements", defaultChat.providerName); + const { container: telemetryContainer, checkbox: telemetryCheckbox } = this.createCheckBox(telemetryLabel, this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? false : false); - const detectionLabel = localize('detectionLabel', "Allow suggestions matching public code"); - detectionCheckbox = this.createCheckBox(detectionLabel, true); - } + const detectionLabel = localize('detectionLabel', "Allow suggestions matching public code"); + const { container: detectionContainer, checkbox: detectionCheckbox } = this.createCheckBox(detectionLabel, true); // Setup Button let setupRunning = false; @@ -387,11 +382,11 @@ class ChatSetupWelcomeContent extends Disposable { const buttonRow = this.element.appendChild($('p')); const button = this._register(new Button(buttonRow, { ...defaultButtonStyles, supportIcons: true })); - this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), false); + this.updateControls(button, [telemetryCheckbox, detectionCheckbox], false); this._register(button.onDidClick(async () => { setupRunning = true; - this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning); + this.updateControls(button, [telemetryCheckbox, detectionCheckbox], setupRunning); try { await this.setup(telemetryCheckbox?.checked, detectionCheckbox?.checked); @@ -399,22 +394,29 @@ class ChatSetupWelcomeContent extends Disposable { setupRunning = false; } - this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning); + this.updateControls(button, [telemetryCheckbox, detectionCheckbox], setupRunning); })); - this._register(this.options.onDidChangeEntitlement(() => this.updateControls(button, coalesce([telemetryCheckbox, detectionCheckbox]), setupRunning))); - // Footer const footer = localize({ key: 'privacyFooter', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}). You can [learn more]({1}) about {2}.", defaultChat.privacyStatementUrl, defaultChat.documentationUrl, defaultChat.name); this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(footer, { isTrusted: true }))).element); + + // Update based on entilement changes + this._register(this.options.onDidChangeEntitlement(() => { + if (setupRunning) { + return; // do not change when setup running + } + setVisibility(this.options.entitlement !== ChatEntitlement.Unavailable, skuHeaderElement, telemetryContainer, detectionContainer); + this.updateControls(button, [telemetryCheckbox, detectionCheckbox], setupRunning); + })); } - private createCheckBox(label: string, checked: boolean): Checkbox { - const checkboxContainer = this.element.appendChild($('p')); + private createCheckBox(label: string, checked: boolean): { container: HTMLElement; checkbox: Checkbox } { + const container = this.element.appendChild($('p')); const checkbox = this._register(new Checkbox(label, checked, defaultCheckboxStyles)); - checkboxContainer.appendChild(checkbox.domNode); + container.appendChild(checkbox.domNode); - const telemetryCheckboxLabelContainer = checkboxContainer.appendChild($('div')); + const telemetryCheckboxLabelContainer = container.appendChild($('div')); telemetryCheckboxLabelContainer.textContent = label; this._register(addDisposableListener(telemetryCheckboxLabelContainer, EventType.CLICK, () => { if (checkbox?.enabled) { @@ -422,7 +424,7 @@ class ChatSetupWelcomeContent extends Disposable { } })); - return checkbox; + return { container, checkbox }; } private updateControls(button: Button, checkboxes: Checkbox[], setupRunning: boolean): void {