diff --git a/package.json b/package.json index 3853e9610aa..cb3cbdf4d18 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.96.0", - "distro": "957bf1ee92a5478360b9eca4d38bbc1e3fc9d654", + "distro": "84739a47ae268764f8ef721a6ed19a591f9c6788", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index f4cfba7a1aa..62fc9653a5a 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -197,10 +197,14 @@ export interface IProductConfiguration { readonly defaultChatAgent?: { readonly extensionId: string; + readonly providerId: string; + readonly providerName: string; + readonly providerScopes: string[]; readonly name: string; readonly icon: string; readonly documentationUrl: string; readonly gettingStartedCommand: string; + readonly welcomeTitle: string; }; } diff --git a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts index 3a971d78c4b..f9dec8ce9b5 100644 --- a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts +++ b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts @@ -154,6 +154,10 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { } } + showDropdown(): void { + this._dropdown.show(); + } + override dispose() { this._primaryAction.dispose(); this._dropdown.dispose(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 303f34cbc26..3f457bedf05 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -11,20 +11,19 @@ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { EditorAction2, ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { EditorAction2 } from '../../../../../editor/browser/editorExtensions.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { SuggestController } from '../../../../../editor/contrib/suggest/browser/suggestController.js'; import { localize, localize2 } from '../../../../../nls.js'; import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; import { DropdownWithPrimaryActionViewItem } from '../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; import { Action2, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; -import { ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; import { ToggleTitleBarConfigAction } from '../../../../browser/parts/titlebar/titlebarActions.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; @@ -50,6 +49,10 @@ import { IHostService } from '../../../../services/host/browser/host.js'; import { isCancellationError } from '../../../../../base/common/errors.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; +import { IAuthenticationService } from '../../../../services/authentication/common/authentication.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; +import { IChatViewsWelcomeContributionRegistry, IChatViewsWelcomeDescriptor, ChatViewsWelcomeExtensions } from '../viewsWelcome/chatViewsWelcome.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; export const CHAT_CATEGORY = localize2('chat.category', 'Chat'); export const CHAT_OPEN_ACTION_ID = 'workbench.action.chat.open'; @@ -86,9 +89,13 @@ export interface IChatViewOpenRequestEntry { const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', name: product.defaultChatAgent?.name ?? '', + providerId: product.defaultChatAgent?.providerId ?? '', + providerName: product.defaultChatAgent?.providerName ?? '', + providerScopes: product.defaultChatAgent?.providerScopes ?? [], icon: Codicon[product.defaultChatAgent?.icon as keyof typeof Codicon ?? 'commentDiscussion'], documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', gettingStartedCommand: product.defaultChatAgent?.gettingStartedCommand ?? '', + welcomeTitle: product.defaultChatAgent?.welcomeTitle ?? '', }; class OpenChatGlobalAction extends Action2 { @@ -462,8 +469,8 @@ export function registerChatActions() { } }); - registerAction2(InstallChatWithPromptAction); - registerAction2(InstallChatWithoutPromptAction); + registerAction2(InstallChatAction); + registerAction2(SignInAndInstallChatAction); registerAction2(LearnMoreChatAction); } @@ -484,7 +491,11 @@ MenuRegistry.appendMenuItem(MenuId.CommandCenter, { icon: defaultChat.icon, when: ContextKeyExpr.and( ContextKeyExpr.has('config.chat.commandCenter.enabled'), - ContextKeyExpr.or(ChatContextKeys.panelParticipantRegistered, ChatContextKeys.installEntitled) + ContextKeyExpr.or( + ChatContextKeys.panelParticipantRegistered, + ChatContextKeys.ChatSetup.entitled, + ContextKeyExpr.has('config.chat.experimental.offerSetup') + ) ), order: 10001, }); @@ -497,7 +508,11 @@ registerAction2(class ToggleChatControl extends ToggleTitleBarConfigAction { localize('toggle.chatControlsDescription', "Toggle visibility of the Chat Controls in title bar"), 3, false, ContextKeyExpr.and( ContextKeyExpr.has('config.window.commandCenter'), - ContextKeyExpr.or(ChatContextKeys.panelParticipantRegistered, ChatContextKeys.installEntitled) + ContextKeyExpr.or( + ChatContextKeys.panelParticipantRegistered, + ChatContextKeys.ChatSetup.entitled, + ContextKeyExpr.has('config.chat.experimental.offerSetup') + ) ) ); } @@ -508,15 +523,41 @@ export class ChatCommandCenterRendering implements IWorkbenchContribution { static readonly ID = 'chat.commandCenterRendering'; private readonly _store = new DisposableStore(); + private _dropdown: DropdownWithPrimaryActionViewItem | undefined; constructor( @IActionViewItemService actionViewItemService: IActionViewItemService, @IChatAgentService agentService: IChatAgentService, @IInstantiationService instantiationService: IInstantiationService, ) { + // --- action to show dropdown + const that = this; + const showDropdownActionId = 'chatMenu.showDropdown'; + registerAction2(class OpenChatMenuDropdown extends Action2 { + constructor() { + super({ + id: showDropdownActionId, + title: defaultChat.name, + f1: false + }); + } + run() { + that._dropdown?.showDropdown(); + } + }); + // --- chat setup welcome + const descriptor: IChatViewsWelcomeDescriptor = { + title: defaultChat.welcomeTitle, + when: ChatContextKeys.ChatSetup.running, + icon: defaultChat.icon, + progress: localize('setupChatRunning', "Getting Chat ready for you..."), + content: new MarkdownString(`\n\n[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, { isTrusted: true }), + }; + Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register(descriptor); + + // --- dropdown menu this._store.add(actionViewItemService.register(MenuId.CommandCenter, MenuId.ChatCommandCenter, (action, options) => { - if (!(action instanceof SubmenuItemAction)) { return undefined; } @@ -530,21 +571,14 @@ export class ChatCommandCenterRendering implements IWorkbenchContribution { const chatExtensionInstalled = agentService.getAgents().some(agent => agent.isDefault); const primaryAction = instantiationService.createInstance(MenuItemAction, { - id: chatExtensionInstalled ? CHAT_OPEN_ACTION_ID : InstallChatWithPromptAction.ID, - title: chatExtensionInstalled ? OpenChatGlobalAction.TITLE : InstallChatWithPromptAction.TITLE, + id: chatExtensionInstalled ? CHAT_OPEN_ACTION_ID : showDropdownActionId, + title: chatExtensionInstalled ? OpenChatGlobalAction.TITLE : defaultChat.name, icon: defaultChat.icon, }, undefined, undefined, undefined, undefined); - return instantiationService.createInstance( - DropdownWithPrimaryActionViewItem, - primaryAction, dropdownAction, action.actions, - '', - { - ...options, - skipTelemetry: true, // already handled by the workbench action bar - } - ); + this._dropdown = instantiationService.createInstance(DropdownWithPrimaryActionViewItem, primaryAction, dropdownAction, action.actions, '', { ...options, skipTelemetry: true }); + return this._dropdown; }, agentService.onDidChangeAgents)); } @@ -553,88 +587,108 @@ export class ChatCommandCenterRendering implements IWorkbenchContribution { } } -abstract class BaseInstallChatAction extends Action2 { +class InstallChatAction extends Action2 { - protected abstract getJustification(productService: IProductService): string | undefined; - - override async run(accessor: ServicesAccessor): Promise { - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - const productService = accessor.get(IProductService); - const telemetryService = accessor.get(ITelemetryService); - - 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.' }; - hasJustification: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of window error to understand the nature of the error better.' }; - }; - type InstallChatEvent = { - hasJustification: boolean; - installResult: 'installed' | 'cancelled' | 'failed'; - }; - - const justification = this.getJustification(productService); - - let installResult: 'installed' | 'cancelled' | 'failed'; - try { - await extensionsWorkbenchService.install(defaultChat.extensionId, { - justification, - enable: true, - installPreReleaseVersion: productService.quality !== 'stable' - }, ProgressLocation.Notification); - - installResult = 'installed'; - } catch (error) { - installResult = isCancellationError(error) ? 'cancelled' : 'failed'; - } - - telemetryService.publicLog2('commandCenter.chatInstall', { - installResult, - hasJustification: !!justification - }); - } -} - -class InstallChatWithPromptAction extends BaseInstallChatAction { - - static readonly ID = 'workbench.action.chat.installWithPrompt'; + static readonly ID = 'workbench.action.chat.install'; static readonly TITLE = localize2('installChat', "Install {0}", defaultChat.name); constructor() { super({ - id: InstallChatWithPromptAction.ID, - title: InstallChatWithPromptAction.TITLE, - icon: defaultChat.icon, - category: CHAT_CATEGORY - }); - } - - protected getJustification(productService: IProductService): string { - return localize('installChatGlobalAction.justification', "AI features in {0} require this extension. Your account already has access to {1}.", productService.nameShort, defaultChat.name); - } -} - -class InstallChatWithoutPromptAction extends BaseInstallChatAction { - - static readonly ID = 'workbench.action.chat.installWithoutPrompt'; - static readonly TITLE = localize2('installChat', "Install {0}", defaultChat.name); - - constructor() { - super({ - id: InstallChatWithoutPromptAction.ID, - title: InstallChatWithoutPromptAction.TITLE, + id: InstallChatAction.ID, + title: InstallChatAction.TITLE, category: CHAT_CATEGORY, menu: { id: MenuId.ChatCommandCenter, group: 'a_atfirst', order: 1, - when: ChatContextKeys.panelParticipantRegistered.negate() + when: ContextKeyExpr.and( + ChatContextKeys.panelParticipantRegistered.negate(), + ContextKeyExpr.or( + ChatContextKeys.ChatSetup.entitled, + ChatContextKeys.ChatSetup.signedIn + ) + ) } }); } - protected getJustification(): string | undefined { - return undefined; + override run(accessor: ServicesAccessor): Promise { + return InstallChatAction.install(accessor); + } + + static async install(accessor: ServicesAccessor) { + 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 setupRunningContextKey = ChatContextKeys.ChatSetup.running.bindTo(contextKeyService); + + 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.' }; + }; + type InstallChatEvent = { + installResult: 'installed' | 'cancelled' | 'failed'; + }; + + let installResult: 'installed' | 'cancelled' | 'failed'; + try { + setupRunningContextKey.set(true); + showChatView(viewsService); + + await extensionsWorkbenchService.install(defaultChat.extensionId, { + enable: true, + isMachineScoped: false, + installPreReleaseVersion: productService.quality !== 'stable' + }, CHAT_VIEW_ID); + + installResult = 'installed'; + } catch (error) { + installResult = isCancellationError(error) ? 'cancelled' : 'failed'; + } finally { + setupRunningContextKey.reset(); + } + + telemetryService.publicLog2('commandCenter.chatInstall', { + installResult + }); + } +} + +class SignInAndInstallChatAction extends Action2 { + + static readonly ID = 'workbench.action.chat.signInAndInstall'; + static readonly TITLE = localize2('signInAndInstallChat', "Sign in to use {0}", defaultChat.name); + + constructor() { + super({ + id: SignInAndInstallChatAction.ID, + title: SignInAndInstallChatAction.TITLE, + category: CHAT_CATEGORY, + menu: { + id: MenuId.ChatCommandCenter, + group: 'a_atfirst', + order: 1, + when: ContextKeyExpr.and( + ChatContextKeys.panelParticipantRegistered.negate(), + ChatContextKeys.ChatSetup.entitled.negate(), + ChatContextKeys.ChatSetup.signedIn.negate() + ) + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const authenticationService = accessor.get(IAuthenticationService); + const instantiationService = accessor.get(IInstantiationService); + + const session = await authenticationService.createSession(defaultChat.providerId, defaultChat.providerScopes); + if (session) { + instantiationService.invokeFunction(accessor => InstallChatAction.install(accessor)); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 4ef5da659d2..b65aa16fb60 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -118,6 +118,12 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for chat actions (requires {0}).", '`#window.commandCenter#`'), default: true }, + 'chat.experimental.offerSetup': { + type: 'boolean', + default: false, + markdownDescription: nls.localize('chat.experimental.offerSetup', "Controls whether setup is offered for Chat if not done already."), + tags: ['experimental', 'onExP'] + }, 'chat.editing.alwaysSaveWithGeneratedChanges': { type: 'boolean', scope: ConfigurationScope.APPLICATION, diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 1a07b8e8d35..51389415890 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -319,7 +319,11 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { order: 1 }, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), - when: ContextKeyExpr.or(ChatContextKeys.panelParticipantRegistered, ChatContextKeys.extensionInvalid) + when: ContextKeyExpr.or( + ChatContextKeys.panelParticipantRegistered, + ChatContextKeys.extensionInvalid, + ChatContextKeys.ChatSetup.running + ) }]; Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index 741dcbd6a13..31b95b07b3f 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -54,6 +54,16 @@ div.chat-welcome-view { font-size: 11px; } + & > .chat-welcome-view-progress { + display: flex; + gap: 6px; + color: var(--vscode-descriptionForeground); + text-align: center; + max-width: 350px; + padding: 0 20px; + margin-top: 20px; + } + & > .chat-welcome-view-message { color: var(--vscode-descriptionForeground); text-align: center; diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index d5992926d97..bae1465625e 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -17,6 +17,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ILogService } from '../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; +import { spinningLoading } from '../../../../../platform/theme/common/iconRegistry.js'; import { ChatAgentLocation } from '../../common/chatAgents.js'; import { chatViewsWelcomeRegistry, IChatViewsWelcomeDescriptor } from './chatViewsWelcome.js'; @@ -52,7 +53,7 @@ export class ChatViewWelcomeController extends Disposable { private update(force?: boolean): void { const enabled = this.delegate.shouldShowWelcome(); - if (this.enabled === enabled || force) { + if (this.enabled === enabled && !force) { return; } @@ -87,6 +88,7 @@ export class ChatViewWelcomeController extends Disposable { icon: enabledDescriptor.icon, title: enabledDescriptor.title, message: enabledDescriptor.content, + progress: enabledDescriptor.progress }; const welcomeView = this.renderDisposables.add(this.instantiationService.createInstance(ChatViewWelcomePart, content, { firstLinkToButton: true, location: this.location })); this.element!.appendChild(welcomeView.element); @@ -101,6 +103,7 @@ export interface IChatViewWelcomeContent { icon?: ThemeIcon; title: string; message: IMarkdownString; + progress?: string; tips?: IMarkdownString; } @@ -123,14 +126,23 @@ export class ChatViewWelcomePart extends Disposable { this.element = dom.$('.chat-welcome-view'); try { - const icon = dom.append(this.element!, $('.chat-welcome-view-icon')); - const title = dom.append(this.element!, $('.chat-welcome-view-title')); + const icon = dom.append(this.element, $('.chat-welcome-view-icon')); + const title = dom.append(this.element, $('.chat-welcome-view-title')); if (options?.location === ChatAgentLocation.EditingSession) { - const featureIndicator = dom.append(this.element!, $('.chat-welcome-view-indicator')); + const featureIndicator = dom.append(this.element, $('.chat-welcome-view-indicator')); featureIndicator.textContent = localize('preview', 'PREVIEW'); } - const message = dom.append(this.element!, $('.chat-welcome-view-message')); + + if (content.progress) { + const progress = dom.append(this.element, $('.chat-welcome-view-progress')); + progress.appendChild(renderIcon(spinningLoading)); + + const progressLabel = dom.append(progress, $('span')); + progressLabel.textContent = content.progress; + } + + const message = dom.append(this.element, $('.chat-welcome-view-message')); if (content.icon) { icon.appendChild(renderIcon(content.icon)); @@ -155,7 +167,7 @@ export class ChatViewWelcomePart extends Disposable { dom.append(message, messageResult.element); if (content.tips) { - const tips = dom.append(this.element!, $('.chat-welcome-view-tips')); + const tips = dom.append(this.element, $('.chat-welcome-view-tips')); const tipsResult = this._register(renderer.render(content.tips)); tips.appendChild(tipsResult.element); } diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts index cb630710521..9980a78c5dd 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts @@ -17,6 +17,7 @@ export interface IChatViewsWelcomeDescriptor { icon?: ThemeIcon; title: string; content: IMarkdownString; + progress?: string; // TODO@bpasero remove me if not used anymore when: ContextKeyExpression; } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 8dd8c552e0f..64d8dd41d77 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -41,7 +41,11 @@ 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 installEntitled = new RawContextKey('chatInstallEntitled', false, { type: 'boolean', description: localize('chatInstallEntitled', "True when the user is entitled for chat installation.") }); + export const ChatSetup = { + entitled: new RawContextKey('chatSetupEntitled', false, { type: 'boolean', description: localize('chatSetupEntitled', "True when chat setup is offered for a signed-in, entitled user.") }), + signedIn: new RawContextKey('chatSetupSignedIn', false, { type: 'boolean', description: localize('chatSetupSignedIn', "True when chat setup is offered for a signed-in user.") }), + running: new RawContextKey('chatSetupRunning', false, { type: 'boolean', description: localize('chatSetupRunning', "True when chat setup is running.") }) + }; export const shouldShowMovedViewWelcome = new RawContextKey('chatShouldShowMovedViewWelcome', false, { type: 'boolean', description: localize('chatShouldShowMovedViewWelcome', "True when the user should be shown the moved view welcome view.") }); } diff --git a/src/vs/workbench/contrib/chat/common/chatInstallEntitlement.contribution.ts b/src/vs/workbench/contrib/chat/common/chatSetup.contribution.ts similarity index 55% rename from src/vs/workbench/contrib/chat/common/chatInstallEntitlement.contribution.ts rename to src/vs/workbench/contrib/chat/common/chatSetup.contribution.ts index fb5352a0113..2a80213c7c3 100644 --- a/src/vs/workbench/contrib/chat/common/chatInstallEntitlement.contribution.ts +++ b/src/vs/workbench/contrib/chat/common/chatSetup.contribution.ts @@ -17,24 +17,26 @@ import { CancellationTokenSource } from '../../../../base/common/cancellation.js import { ChatContextKeys } from './chatContextKeys.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IRequestContext } from '../../../../base/parts/request/common/request.js'; +import { IGitHubEntitlement } from '../../../../base/common/product.js'; // TODO@bpasero revisit this flow -type ChatInstallEntitlementEnablementClassification = { - entitled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat install entitled' }; +type ChatSetupEntitlementEnablementClassification = { + entitled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat setup entitled' }; owner: 'bpasero'; - comment: 'Reporting if the user is chat install entitled'; + comment: 'Reporting if the user is chat setup entitled'; }; -type ChatInstallEntitlementEnablementEvent = { +type ChatSetupEntitlementEnablementEvent = { entitled: boolean; }; -class ChatInstallEntitlementContribution extends Disposable implements IWorkbenchContribution { +class ChatSetupContribution extends Disposable implements IWorkbenchContribution { private static readonly CHAT_EXTENSION_INSTALLED_KEY = 'chat.extensionInstalled'; - private readonly chatInstallEntitledContextKey = ChatContextKeys.installEntitled.bindTo(this.contextService); + private readonly chatSetupSignedInContextKey = ChatContextKeys.ChatSetup.signedIn.bindTo(this.contextService); + private readonly chatSetupEntitledContextKey = ChatContextKeys.ChatSetup.entitled.bindTo(this.contextService); private resolvedEntitlement: boolean | undefined = undefined; @@ -50,65 +52,91 @@ class ChatInstallEntitlementContribution extends Disposable implements IWorkbenc ) { super(); - if (!this.productService.gitHubEntitlement) { + const entitlement = this.productService.gitHubEntitlement; + if (!entitlement) { return; } - this.checkExtensionInstallation(); - this.registerListeners(); + this.checkExtensionInstallation(entitlement); + + this.registerEntitlementListeners(entitlement); + this.registerAuthListeners(entitlement); } - private async checkExtensionInstallation(): Promise { + private async checkExtensionInstallation(entitlement: IGitHubEntitlement): Promise { const extensions = await this.extensionManagementService.getInstalled(); - const installed = extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, this.productService.gitHubEntitlement?.extensionId)); + const installed = extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, entitlement.extensionId)); this.updateExtensionInstalled(installed ? true : false); } - private registerListeners(): void { + private registerEntitlementListeners(entitlement: IGitHubEntitlement): void { this._register(this.extensionService.onDidChangeExtensions(result => { for (const extension of result.removed) { - if (ExtensionIdentifier.equals(this.productService.gitHubEntitlement?.extensionId, extension.identifier)) { + if (ExtensionIdentifier.equals(entitlement.extensionId, extension.identifier)) { this.updateExtensionInstalled(false); break; } } for (const extension of result.added) { - if (ExtensionIdentifier.equals(this.productService.gitHubEntitlement?.extensionId, extension.identifier)) { + if (ExtensionIdentifier.equals(entitlement.extensionId, extension.identifier)) { this.updateExtensionInstalled(true); break; } } })); - this._register(this.authenticationService.onDidChangeSessions(async e => { - if (e.providerId === this.productService.gitHubEntitlement?.providerId) { + this._register(this.authenticationService.onDidChangeSessions(e => { + if (e.providerId === entitlement.providerId) { if (e.event.added?.length) { - this.resolveEntitlement(e.event.added[0]); + this.resolveEntitlement(entitlement, e.event.added[0]); } else if (e.event.removed?.length) { - this.chatInstallEntitledContextKey.set(false); + this.chatSetupEntitledContextKey.set(false); } } })); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(async e => { - if (e.id === this.productService.gitHubEntitlement?.providerId) { - this.resolveEntitlement((await this.authenticationService.getSessions(e.id))[0]); + if (e.id === entitlement.providerId) { + this.resolveEntitlement(entitlement, (await this.authenticationService.getSessions(e.id))[0]); } })); } - private async resolveEntitlement(session: AuthenticationSession | undefined): Promise { + private registerAuthListeners(entitlement: IGitHubEntitlement): void { + const hasProviderSessions = async () => { + const sessions = await this.authenticationService.getSessions(entitlement.providerId); + return sessions.length > 0; + }; + + const handleDeclaredAuthProviders = async () => { + if (this.authenticationService.declaredProviders.find(p => p.id === entitlement.providerId)) { + this.chatSetupSignedInContextKey.set(await hasProviderSessions()); + } + }; + this._register(this.authenticationService.onDidChangeDeclaredProviders(handleDeclaredAuthProviders)); + this._register(this.authenticationService.onDidRegisterAuthenticationProvider(handleDeclaredAuthProviders)); + + handleDeclaredAuthProviders(); + + this._register(this.authenticationService.onDidChangeSessions(async ({ providerId }) => { + if (providerId === entitlement.providerId) { + this.chatSetupSignedInContextKey.set(await hasProviderSessions()); + } + })); + } + + private async resolveEntitlement(entitlement: IGitHubEntitlement, session: AuthenticationSession | undefined): Promise { if (!session) { return; } - const entitled = await this.doResolveEntitlement(session); - this.chatInstallEntitledContextKey.set(entitled); + const entitled = await this.doResolveEntitlement(entitlement, session); + this.chatSetupEntitledContextKey.set(entitled); } - private async doResolveEntitlement(session: AuthenticationSession): Promise { + private async doResolveEntitlement(entitlement: IGitHubEntitlement, session: AuthenticationSession): Promise { if (typeof this.resolvedEntitlement === 'boolean') { return this.resolvedEntitlement; } @@ -120,7 +148,7 @@ class ChatInstallEntitlementContribution extends Disposable implements IWorkbenc try { context = await this.requestService.request({ type: 'GET', - url: this.productService.gitHubEntitlement!.entitlementUrl, + url: entitlement.entitlementUrl, headers: { 'Authorization': `Bearer ${session.accessToken}` } @@ -145,15 +173,15 @@ class ChatInstallEntitlementContribution extends Disposable implements IWorkbenc return false; //ignore } - this.resolvedEntitlement = Boolean(parsedResult[this.productService.gitHubEntitlement!.enablementKey]); - this.telemetryService.publicLog2('chatInstallEntitlement', { entitled: this.resolvedEntitlement }); + this.resolvedEntitlement = Boolean(parsedResult[entitlement.enablementKey]); + this.telemetryService.publicLog2('chatInstallEntitlement', { entitled: this.resolvedEntitlement }); return this.resolvedEntitlement; } private updateExtensionInstalled(isExtensionInstalled: boolean): void { - this.storageService.store(ChatInstallEntitlementContribution.CHAT_EXTENSION_INSTALLED_KEY, isExtensionInstalled, StorageScope.PROFILE, StorageTarget.MACHINE); + this.storageService.store(ChatSetupContribution.CHAT_EXTENSION_INSTALLED_KEY, isExtensionInstalled, StorageScope.PROFILE, StorageTarget.MACHINE); } } -registerWorkbenchContribution2('workbench.chat.installEntitlement', ChatInstallEntitlementContribution, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2('workbench.chat.setup', ChatSetupContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 1575be24d0d..48484d820f7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -2236,7 +2236,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return false; } - async install(arg: string | URI | IExtension, installOptions: InstallExtensionOptions = {}, progressLocation?: ProgressLocation): Promise { + async install(arg: string | URI | IExtension, installOptions: InstallExtensionOptions = {}, progressLocation?: ProgressLocation | string): Promise { let installable: URI | IGalleryExtension | IResourceExtension | undefined; let extension: IExtension | undefined; @@ -2607,7 +2607,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return extension; } - private doInstall(extension: IExtension | undefined, installTask: () => Promise, progressLocation?: ProgressLocation): Promise { + private doInstall(extension: IExtension | undefined, installTask: () => Promise, progressLocation?: ProgressLocation | string): Promise { const title = extension ? nls.localize('installing named extension', "Installing '{0}' extension....", extension.displayName) : nls.localize('installing extension', 'Installing extension....'); return this.withProgress({ location: progressLocation ?? ProgressLocation.Extensions, diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 0b880a40d55..f55dea181fb 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -134,9 +134,9 @@ export interface IExtensionsWorkbenchService { getExtensions(extensionInfos: IExtensionInfo[], options: IExtensionQueryOptions, token: CancellationToken): Promise; getResourceExtensions(locations: URI[], isWorkspaceScoped: boolean): Promise; canInstall(extension: IExtension): Promise; - install(id: string, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation): Promise; - install(vsix: URI, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation): Promise; - install(extension: IExtension, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation): Promise; + install(id: string, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise; + install(vsix: URI, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise; + install(extension: IExtension, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise; installInServer(extension: IExtension, server: IExtensionManagementServer): Promise; uninstall(extension: IExtension): Promise; reinstall(extension: IExtension): Promise; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 48a39d5fee3..07287eab1b0 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -171,7 +171,7 @@ import './contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.js'; // Chat import './contrib/chat/electron-sandbox/chat.contribution.js'; import './contrib/inlineChat/electron-sandbox/inlineChat.contribution.js'; -import './contrib/chat/common/chatInstallEntitlement.contribution.js'; +import './contrib/chat/common/chatSetup.contribution.js'; import './contrib/chat/browser/chatMovedView.contribution.js'; // Encryption