mirror of
https://github.com/microsoft/vscode.git
synced 2026-01-09 06:31:23 +08:00
Offer chat install experience for entitled users (#231287)
This commit is contained in:
parent
d72a0f0af6
commit
42b425bc8e
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -168,6 +168,5 @@
|
||||
},
|
||||
"css.format.spaceAroundSelectorSeparator": true,
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"chat.commandCenter.enabled": true,
|
||||
"eslint.useFlatConfig": true
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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<void> {
|
||||
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<void> {
|
||||
const openerService = accessor.get(IOpenerService);
|
||||
if (defaultChat.documentationUrl) {
|
||||
openerService.open(URI.parse(defaultChat.documentationUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
|
||||
registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribution, WorkbenchPhase.BlockStartup);
|
||||
workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlashCommandsContribution, LifecyclePhase.Eventually);
|
||||
Registry.as<IEditorFactoryRegistry>(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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -37,3 +37,5 @@ export const CONTEXT_IN_QUICK_CHAT = new RawContextKey<boolean>('quickChatHasFoc
|
||||
export const CONTEXT_CHAT_HAS_FILE_ATTACHMENTS = new RawContextKey<boolean>('chatHasFileAttachments', false, { type: 'boolean', description: localize('chatHasFileAttachments', "True when the chat has file attachments.") });
|
||||
|
||||
export const CONTEXT_LANGUAGE_MODELS_ARE_USER_SELECTABLE = new RawContextKey<boolean>('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<boolean>('chatInstallEntitled', false, { type: 'boolean', description: localize('chatInstallEntitled', "True when the user is entitled for chat installation.") });
|
||||
|
||||
@ -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<void> {
|
||||
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<boolean> {
|
||||
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<ChatInstallEntitlementEnablementEvent, ChatInstallEntitlementEnablementClassification>('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);
|
||||
@ -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<UpdatedSettingEvent, {
|
||||
owner: 'bpasero';
|
||||
comment: 'This is used to know if command center chat menu is enabled or not';
|
||||
settingValue: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'value of the setting' };
|
||||
source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'source of the setting' };
|
||||
}>('chat.commandCenter.enabled', { settingValue: this.getValueToReport(key, target), source });
|
||||
return;
|
||||
|
||||
case 'window.customTitleBarVisibility':
|
||||
this.telemetryService.publicLog2<UpdatedSettingEvent, {
|
||||
owner: 'benibenj';
|
||||
|
||||
@ -171,6 +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';
|
||||
|
||||
// Encryption
|
||||
import './contrib/encryption/electron-sandbox/encryption.contribution.js';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user