diff --git a/.vscode/settings.json b/.vscode/settings.json index 75cbca16260..63733b35c08 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -168,6 +168,5 @@ }, "css.format.spaceAroundSelectorSeparator": true, "typescript.enablePromptUseWorkspaceTsdk": true, - "chat.commandCenter.enabled": true, "eslint.useFlatConfig": true } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 1f58ce0fee8..f4cfba7a1aa 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -194,6 +194,14 @@ export interface IProductConfiguration { readonly chatParticipantRegistry?: string; readonly emergencyAlertUrl?: string; + + readonly defaultChatAgent?: { + readonly extensionId: string; + readonly name: string; + readonly icon: string; + readonly documentationUrl: string; + readonly gettingStartedCommand: string; + }; } export interface ITunnelApplicationConfig { diff --git a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts index 1f5cf56907e..24e79fddb5b 100644 --- a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts +++ b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts @@ -27,6 +27,8 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { isWeb } from '../../../../base/common/platform.js'; +// TODO@bpasero remove this experiment eventually + const accountsBadgeConfigKey = 'workbench.accounts.experimental.showEntitlements'; type EntitlementEnablementClassification = { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 15ac2950674..c285a26b65d 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -18,18 +18,23 @@ 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 { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js'; import { IInstantiationService } 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'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; +import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; -import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_QUICK_CHAT } from '../../common/chatContextKeys.js'; +import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INSTALL_ENTITLED, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_QUICK_CHAT } from '../../common/chatContextKeys.js'; import { extractAgentAndCommand } from '../../common/chatParserTypes.js'; import { IChatDetail, IChatService } from '../../common/chatService.js'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js'; @@ -40,6 +45,8 @@ import { ChatEditorInput } from '../chatEditorInput.js'; import { ChatViewPane } from '../chatViewPane.js'; import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js'; import { clearChatEditor } from './chatClear.js'; +import product from '../../../../../platform/product/common/product.js'; +import { URI } from '../../../../../base/common/uri.js'; import { IHostService } from '../../../../services/host/browser/host.js'; export const CHAT_CATEGORY = localize2('chat.category', 'Chat'); @@ -70,6 +77,14 @@ export interface IChatViewOpenRequestEntry { response: string; } +const defaultChat = { + extensionId: product.defaultChatAgent?.extensionId ?? '', + name: product.defaultChatAgent?.name ?? '', + icon: Codicon[product.defaultChatAgent?.icon as keyof typeof Codicon ?? 'commentDiscussion'], + documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', + gettingStartedCommand: product.defaultChatAgent?.gettingStartedCommand ?? '', +}; + class OpenChatGlobalAction extends Action2 { static readonly TITLE = localize2('openChat', "Open Chat"); @@ -78,8 +93,9 @@ class OpenChatGlobalAction extends Action2 { super({ id: CHAT_OPEN_ACTION_ID, title: OpenChatGlobalAction.TITLE, - icon: Codicon.commentDiscussion, + icon: defaultChat.icon, f1: true, + precondition: CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED, category: CHAT_CATEGORY, keybinding: { weight: KeybindingWeight.WorkbenchContrib, @@ -422,6 +438,10 @@ export function registerChatActions() { widgetService.lastFocusedWidget?.focusInput(); } }); + + registerAction2(InstallChatWithPromptAction); + registerAction2(InstallChatWithoutPromptAction); + registerAction2(LearnMoreChatAction); } export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string { @@ -438,14 +458,25 @@ export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewMod MenuRegistry.appendMenuItem(MenuId.CommandCenter, { submenu: MenuId.ChatCommandCenter, title: localize('title4', "Chat"), - icon: Codicon.commentDiscussion, - when: ContextKeyExpr.and(CONTEXT_CHAT_ENABLED, ContextKeyExpr.has('config.chat.commandCenter.enabled')), + icon: defaultChat.icon, + when: ContextKeyExpr.and( + ContextKeyExpr.has('config.chat.commandCenter.enabled'), + ContextKeyExpr.or(CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED, CONTEXT_CHAT_INSTALL_ENTITLED) + ), order: 10001, }); registerAction2(class ToggleChatControl extends ToggleTitleBarConfigAction { constructor() { - super('chat.commandCenter.enabled', localize('toggle.chatControl', 'Chat Controls'), localize('toggle.chatControlsDescription', "Toggle visibility of the Chat Controls in title bar"), 3, false, ContextKeyExpr.and(CONTEXT_CHAT_ENABLED, ContextKeyExpr.has('config.window.commandCenter'))); + super( + 'chat.commandCenter.enabled', + localize('toggle.chatControl', 'Chat Controls'), + localize('toggle.chatControlsDescription', "Toggle visibility of the Chat Controls in title bar"), 3, false, + ContextKeyExpr.and( + ContextKeyExpr.has('config.window.commandCenter'), + ContextKeyExpr.or(CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED, CONTEXT_CHAT_INSTALL_ENTITLED) + ) + ); } }); @@ -463,25 +494,22 @@ export class ChatCommandCenterRendering implements IWorkbenchContribution { this._store.add(actionViewItemService.register(MenuId.CommandCenter, MenuId.ChatCommandCenter, (action, options) => { - const agent = agentService.getDefaultAgent(ChatAgentLocation.Panel); - if (!agent?.metadata.themeIcon) { - return undefined; - } - if (!(action instanceof SubmenuItemAction)) { return undefined; } const dropdownAction = toAction({ - id: agent.id, + id: 'chat.commandCenter.more', label: localize('more', "More..."), run() { } }); + const chatExtensionInstalled = agentService.getAgents().some(agent => agent.isDefault); + const primaryAction = instantiationService.createInstance(MenuItemAction, { - id: CHAT_OPEN_ACTION_ID, - title: OpenChatGlobalAction.TITLE, - icon: agent.metadata.themeIcon, + id: chatExtensionInstalled ? CHAT_OPEN_ACTION_ID : InstallChatWithPromptAction.ID, + title: chatExtensionInstalled ? OpenChatGlobalAction.TITLE : InstallChatWithPromptAction.TITLE, + icon: defaultChat.icon, }, undefined, undefined, undefined, undefined); return instantiationService.createInstance( @@ -501,3 +529,97 @@ export class ChatCommandCenterRendering implements IWorkbenchContribution { this._store.dispose(); } } + +abstract class BaseInstallChatAction extends Action2 { + + protected abstract getJustification(): string | undefined; + + override async run(accessor: ServicesAccessor): Promise { + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const commandService = accessor.get(ICommandService); + const productService = accessor.get(IProductService); + + await extensionsWorkbenchService.install(defaultChat.extensionId, { + justification: this.getJustification(), + enable: true, + installPreReleaseVersion: productService.quality !== 'stable' + }, ProgressLocation.Notification); + + await commandService.executeCommand(CHAT_OPEN_ACTION_ID); + } +} + +class InstallChatWithPromptAction extends BaseInstallChatAction { + + static readonly ID = 'workbench.action.chat.installWithPrompt'; + 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(): string { + return localize('installChatGlobalAction.justification', "AI support requires this extension."); + } +} + +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, + category: CHAT_CATEGORY, + menu: { + id: MenuId.ChatCommandCenter, + group: 'a_atfirst', + order: 1, + when: CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED.negate() + } + }); + } + + protected getJustification(): string | undefined { + return undefined; + } +} + +class LearnMoreChatAction extends Action2 { + + static readonly ID = 'workbench.action.chat.learnMore'; + static readonly TITLE = localize2('learnMore', "Learn More"); + + constructor() { + super({ + id: LearnMoreChatAction.ID, + title: LearnMoreChatAction.TITLE, + category: CHAT_CATEGORY, + menu: [{ + id: MenuId.ChatCommandCenter, + group: 'a_atfirst', + order: 2, + when: CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED.negate() + }, { + id: MenuId.ChatCommandCenter, + group: 'z_atlast', + order: 1, + when: CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED + }] + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const openerService = accessor.get(IOpenerService); + if (defaultChat.documentationUrl) { + openerService.open(URI.parse(defaultChat.documentationUrl)); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 83549846bfc..fd63dabcd71 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -36,7 +36,7 @@ import { SearchView } from '../../../search/browser/searchView.js'; import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../search/browser/symbolsQuickAccess.js'; import { SearchContext } from '../../../search/common/constants.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; -import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT } from '../../common/chatContextKeys.js'; +import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT } from '../../common/chatContextKeys.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; @@ -166,7 +166,7 @@ class AttachFileAction extends Action2 { title: localize2('workbench.action.chat.attachFile.label', "Add File to Chat"), category: CHAT_CATEGORY, f1: false, - precondition: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), + precondition: ContextKeyExpr.and(CONTEXT_CHAT_ENABLED, ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor')), menu: { id: MenuId.ChatCommandCenter, group: 'a_chat', @@ -197,7 +197,7 @@ class AttachSelectionAction extends Action2 { title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"), category: CHAT_CATEGORY, f1: false, - precondition: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), + precondition: ContextKeyExpr.and(CONTEXT_CHAT_ENABLED, ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor')), menu: { id: MenuId.ChatCommandCenter, group: 'a_chat', diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 2b1327f9de3..3827f0b9370 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -19,10 +19,9 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; +import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js'; import { IEditorResolverService, RegisteredEditorPriority } from '../../../services/editor/common/editorResolverService.js'; -import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { ChatAgentLocation, ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from '../common/chatAgents.js'; import { CodeMapperService, ICodeMapperService } from '../common/chatCodeMapperService.js'; import '../common/chatColors.js'; @@ -114,9 +113,8 @@ configurationRegistry.registerConfiguration({ }, 'chat.commandCenter.enabled': { type: 'boolean', - tags: ['experimental'], markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for chat actions (requires {0}).", '`#window.commandCenter#`'), - default: false + default: true }, 'chat.editing.alwaysSaveWithGeneratedChanges': { type: 'boolean', @@ -196,6 +194,8 @@ AccessibleViewRegistry.register(new QuickChatAccessibilityHelp()); class ChatSlashStaticSlashCommandsContribution extends Disposable { + static readonly ID = 'workbench.contrib.chatSlashStaticSlashCommands'; + constructor( @IChatSlashCommandService slashCommandService: IChatSlashCommandService, @ICommandService commandService: ICommandService, @@ -286,11 +286,10 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { })); } } - -const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); -registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribution, WorkbenchPhase.BlockStartup); -workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlashCommandsContribution, LifecyclePhase.Eventually); Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); + +registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribution, WorkbenchPhase.BlockStartup); +registerWorkbenchContribution2(ChatSlashStaticSlashCommandsContribution.ID, ChatSlashStaticSlashCommandsContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 1f75a0e7ae4..f4f0f48db5a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -7,6 +7,7 @@ import { coalesce, isNonEmptyArray } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { Event } from '../../../../base/common/event.js'; +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import * as strings from '../../../../base/common/strings.js'; import { localize, localize2 } from '../../../../nls.js'; @@ -289,6 +290,16 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { storageId: viewContainerId, hideIfEmpty: true, order: 100, + openCommandActionDescriptor: { + id: viewContainerId, + keybindings: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI, + mac: { + primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI + } + }, + order: 100 + }, }, ViewContainerLocation.Sidebar); return viewContainer; diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 52b3b0ada1a..fefcc829bb0 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -37,3 +37,5 @@ export const CONTEXT_IN_QUICK_CHAT = new RawContextKey('quickChatHasFoc export const CONTEXT_CHAT_HAS_FILE_ATTACHMENTS = new RawContextKey('chatHasFileAttachments', false, { type: 'boolean', description: localize('chatHasFileAttachments', "True when the chat has file attachments.") }); export const CONTEXT_LANGUAGE_MODELS_ARE_USER_SELECTABLE = new RawContextKey('chatModelsAreUserSelectable', false, { type: 'boolean', description: localize('chatModelsAreUserSelectable', "True when the chat model can be selected manually by the user.") }); + +export const CONTEXT_CHAT_INSTALL_ENTITLED = new RawContextKey('chatInstallEntitled', false, { type: 'boolean', description: localize('chatInstallEntitled', "True when the user is entitled for chat installation.") }); diff --git a/src/vs/workbench/contrib/chat/common/chatInstallEntitlement.contribution.ts b/src/vs/workbench/contrib/chat/common/chatInstallEntitlement.contribution.ts new file mode 100644 index 00000000000..27f2819c816 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatInstallEntitlement.contribution.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ITelemetryService } 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'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { IRequestService, asText } from '../../../../platform/request/common/request.js'; +import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { CONTEXT_CHAT_INSTALL_ENTITLED } from './chatContextKeys.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; + +// TODO@bpasero revisit this flow + +type ChatInstallEntitlementEnablementClassification = { + entitled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat install entitled' }; + owner: 'bpasero'; + comment: 'Reporting if the user is chat install entitled'; +}; + +type ChatInstallEntitlementEnablementEvent = { + entitled: boolean; +}; + +class ChatInstallEntitlementContribution extends Disposable implements IWorkbenchContribution { + + private static readonly CHAT_EXTENSION_INSTALLED_KEY = 'chat.extensionInstalled'; + + private readonly chatInstallEntitledContextKey = CONTEXT_CHAT_INSTALL_ENTITLED.bindTo(this.contextService); + private readonly listeners = this._register(new DisposableStore()); + + private didResolveEntitlement = false; + + constructor( + @IContextKeyService private readonly contextService: 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, + @IRequestService private readonly requestService: IRequestService, + @IStorageService private readonly storageService: IStorageService + ) { + super(); + + if (!this.productService.gitHubEntitlement) { + return; + } + + this.init(); + } + + private async init(): Promise { + const extensions = await this.extensionManagementService.getInstalled(); + + const installed = extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, this.productService.gitHubEntitlement?.extensionId)); + if (!installed) { + this.registerListeners(); + } else { + this.disableEntitlement(true); + } + } + + private registerListeners(): void { + this.listeners.add(this.extensionService.onDidChangeExtensions(result => { + for (const extension of result.added) { + if (ExtensionIdentifier.equals(this.productService.gitHubEntitlement?.extensionId, extension.identifier)) { + this.disableEntitlement(true); + return; + } + } + })); + + this.listeners.add(this.authenticationService.onDidChangeSessions(async e => { + if (e.providerId === this.productService.gitHubEntitlement?.providerId && e.event.added?.length) { + await this.resolveEntitlement(e.event.added[0]); + } else if (e.providerId === this.productService.gitHubEntitlement?.providerId && e.event.removed?.length) { + this.disableEntitlement(false); + } + })); + + this.listeners.add(this.authenticationService.onDidRegisterAuthenticationProvider(async e => { + if (e.id === this.productService.gitHubEntitlement?.providerId) { + this.resolveEntitlement((await this.authenticationService.getSessions(e.id))[0]); + } + })); + } + + private async resolveEntitlement(session: AuthenticationSession | undefined) { + if (!session) { + return; + } + + const installedExtensions = await this.extensionManagementService.getInstalled(); + const installed = installedExtensions.find(value => ExtensionIdentifier.equals(value.identifier.id, this.productService.gitHubEntitlement?.extensionId)); + if (installed) { + this.disableEntitlement(true); + return; + } + + const entitled = await this.doResolveEntitlement(session); + this.chatInstallEntitledContextKey.set(entitled); + } + + private async doResolveEntitlement(session: AuthenticationSession): Promise { + if (this.didResolveEntitlement) { + return false; + } + + const cts = new CancellationTokenSource(); + this._register(toDisposable(() => cts.dispose(true))); + + const context = await this.requestService.request({ + type: 'GET', + url: this.productService.gitHubEntitlement!.entitlementUrl, + headers: { + 'Authorization': `Bearer ${session.accessToken}` + } + }, cts.token); + + if (context.res.statusCode && context.res.statusCode !== 200) { + return false; + } + + const result = await asText(context); + if (!result) { + return false; + } + + let parsedResult: any; + try { + parsedResult = JSON.parse(result); + } catch (err) { + return false; //ignore + } + + this.didResolveEntitlement = true; + + const entitled = Boolean(parsedResult[this.productService.gitHubEntitlement!.enablementKey]); + this.telemetryService.publicLog2('chatInstallEntitlement', { entitled }); + + return entitled; + } + + private disableEntitlement(isExtensionInstalled: boolean): void { + if (isExtensionInstalled) { + this.storageService.store(ChatInstallEntitlementContribution.CHAT_EXTENSION_INSTALLED_KEY, true, StorageScope.PROFILE, StorageTarget.MACHINE); + } + this.chatInstallEntitledContextKey.set(false); + this.listeners.dispose(); + } +} + +registerWorkbenchContribution2('workbench.chat.installEntitlement', ChatInstallEntitlementContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 91144f957bc..73de190f4cc 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -395,6 +395,15 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc }>('window.commandCenter', { settingValue: this.getValueToReport(key, target), source }); return; + case 'chat.commandCenter.enabled': + this.telemetryService.publicLog2('chat.commandCenter.enabled', { settingValue: this.getValueToReport(key, target), source }); + return; + case 'window.customTitleBarVisibility': this.telemetryService.publicLog2