Files
vscode/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts
cwebster-99 fea130899c polish
2026-03-27 15:41:55 -05:00

865 lines
36 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from '../../../../../base/browser/dom.js';
import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js';
import { IAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js';
import { Event } from '../../../../../base/common/event.js';
import { Lazy } from '../../../../../base/common/lazy.js';
import { Disposable, DisposableStore, markAsSingleton, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import Severity from '../../../../../base/common/severity.js';
import { equalsIgnoreCase } from '../../../../../base/common/strings.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js';
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
import { localize, localize2 } from '../../../../../nls.js';
import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
import { IsWebContext } from '../../../../../platform/contextkey/common/contextkeys.js';
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IMarkerService } from '../../../../../platform/markers/common/markers.js';
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
import product from '../../../../../platform/product/common/product.js';
import { IProductService } from '../../../../../platform/product/common/productService.js';
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
import { IWorkbenchContribution } from '../../../../common/contributions.js';
import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js';
import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService, isProUser } from '../../../../services/chat/common/chatEntitlementService.js';
import { EnablementState, IWorkbenchExtensionEnablementService } from '../../../../services/extensionManagement/common/extensionManagement.js';
import { ExtensionUrlHandlerOverrideRegistry, IExtensionUrlHandlerOverride } from '../../../../services/extensions/browser/extensionUrlHandler.js';
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
import { IHostService } from '../../../../services/host/browser/host.js';
import { IWorkbenchLayoutService, Parts } from '../../../../services/layout/browser/layoutService.js';
import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';
import { IExtension, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { IChatModeService } from '../../common/chatModes.js';
import { IChatSessionsService } from '../../common/chatSessionsService.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
import { CHAT_CATEGORY, CHAT_SETUP_ACTION_ID, CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../actions/chatActions.js';
import { ChatViewContainerId, IChatWidgetService } from '../chat.js';
import { chatViewsWelcomeRegistry } from '../viewsWelcome/chatViewsWelcome.js';
import { ChatSetupAnonymous } from './chatSetup.js';
import { ChatSetupController } from './chatSetupController.js';
import { GrowthSessionController, registerGrowthSession } from './chatSetupGrowthSession.js';
import { AICodeActionsHelper, AINewSymbolNamesProvider, ChatCodeActionsProvider, SetupAgent } from './chatSetupProviders.js';
import { ChatSetup } from './chatSetupRunner.js';
const defaultChat = {
chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '',
manageOveragesUrl: product.defaultChatAgent?.manageOverageUrl ?? '',
upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '',
completionsRefreshTokenCommand: product.defaultChatAgent?.completionsRefreshTokenCommand ?? '',
chatRefreshTokenCommand: product.defaultChatAgent?.chatRefreshTokenCommand ?? '',
};
const SIGN_IN_TITLE_BAR_ACTION_ID = 'workbench.action.chat.signInIndicator';
export class ChatSetupContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.chatSetup';
constructor(
@IActionViewItemService actionViewItemService: IActionViewItemService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IChatEntitlementService chatEntitlementService: ChatEntitlementService,
@ILogService private readonly logService: ILogService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionService private readonly extensionService: IExtensionService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
const context = chatEntitlementService.context?.value;
const requests = chatEntitlementService.requests?.value;
if (!context || !requests) {
return; // disabled
}
const controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, context, requests)));
this.registerSetupAgents(context, controller);
this.registerGrowthSession(chatEntitlementService);
this.registerActions(context, requests, controller);
this.registerSignInTitleBarEntry(actionViewItemService);
this.registerUrlLinkHandler();
this.checkExtensionInstallation(context);
}
private registerSetupAgents(context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): void {
const defaultAgentDisposables = markAsSingleton(new MutableDisposable()); // prevents flicker on window reload
const vscodeAgentDisposables = markAsSingleton(new MutableDisposable());
const renameProviderDisposables = markAsSingleton(new MutableDisposable());
const codeActionsProviderDisposables = markAsSingleton(new MutableDisposable());
const updateRegistration = () => {
// Agent + Tools
{
if (!context.state.hidden && !context.state.disabled) {
// Default Agents (always, even if installed to allow for speedy requests right on startup)
if (!defaultAgentDisposables.value) {
const disposables = defaultAgentDisposables.value = new DisposableStore();
// Panel Agents
const panelAgentDisposables = disposables.add(new DisposableStore());
for (const mode of [ChatModeKind.Ask, ChatModeKind.Edit, ChatModeKind.Agent]) {
const { agent, disposable } = SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Chat, mode, context, controller);
panelAgentDisposables.add(disposable);
panelAgentDisposables.add(agent.onUnresolvableError(() => {
const panelAgentHasGuidance = chatViewsWelcomeRegistry.get().some(descriptor => this.contextKeyService.contextMatchesRules(descriptor.when));
if (panelAgentHasGuidance) {
// An unresolvable error from our agent registrations means that
// Chat is unhealthy for some reason. We clear our panel
// registration to give Chat a chance to show a custom message
// to the user from the views and stop pretending as if there was
// a functional agent.
this.logService.error('[chat setup] Unresolvable error from Chat agent registration, clearing registration.');
panelAgentDisposables.dispose();
}
}));
}
// Inline Agents
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Terminal, ChatModeKind.Ask, context, controller).disposable);
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Notebook, ChatModeKind.Ask, context, controller).disposable);
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.EditorInline, ChatModeKind.Ask, context, controller).disposable);
}
// Built-In Agent + Tool (unless installed, signed-in and enabled)
if ((!context.state.installed || context.state.entitlement === ChatEntitlement.Unknown || context.state.entitlement === ChatEntitlement.Unresolved) && !vscodeAgentDisposables.value) {
const disposables = vscodeAgentDisposables.value = new DisposableStore();
disposables.add(SetupAgent.registerBuiltInAgents(this.instantiationService, context, controller));
}
} else {
defaultAgentDisposables.clear();
vscodeAgentDisposables.clear();
}
if (context.state.installed && !context.state.disabled) {
vscodeAgentDisposables.clear(); // we need to do this to prevent showing duplicate agent/tool entries in the list
}
}
// Rename Provider
{
if (!context.state.installed && !context.state.hidden && !context.state.disabled) {
if (!renameProviderDisposables.value) {
renameProviderDisposables.value = AINewSymbolNamesProvider.registerProvider(this.instantiationService, context, controller);
}
} else {
renameProviderDisposables.clear();
}
}
// Code Actions Provider
{
if (!context.state.installed && !context.state.hidden && !context.state.disabled) {
if (!codeActionsProviderDisposables.value) {
codeActionsProviderDisposables.value = ChatCodeActionsProvider.registerProvider(this.instantiationService);
}
} else {
codeActionsProviderDisposables.clear();
}
}
};
this._register(Event.runAndSubscribe(context.onDidChange, () => updateRegistration()));
}
private registerGrowthSession(chatEntitlementService: ChatEntitlementService): void {
const growthSessionDisposables = markAsSingleton(new MutableDisposable());
const updateGrowthSession = () => {
const experimentEnabled = this.configurationService.getValue<boolean>(ChatConfiguration.GrowthNotificationEnabled) === true;
// Show for users who don't have the Copilot extension installed yet.
// Additional conditions (e.g., anonymous, entitlement) can be layered here.
const shouldShow = experimentEnabled && !chatEntitlementService.sentiment.installed;
if (shouldShow && !growthSessionDisposables.value) {
const disposables = new DisposableStore();
const controller = disposables.add(this.instantiationService.createInstance(GrowthSessionController));
if (!controller.isDismissed) {
disposables.add(registerGrowthSession(this.chatSessionsService, controller));
// Fully unregister when dismissed to prevent cached session from
// appearing during filtered model updates from other providers.
disposables.add(controller.onDidDismiss(() => {
growthSessionDisposables.clear();
}));
growthSessionDisposables.value = disposables;
} else {
disposables.dispose();
}
} else if (!shouldShow) {
growthSessionDisposables.clear();
}
};
this._register(chatEntitlementService.onDidChangeSentiment(() => updateGrowthSession()));
updateGrowthSession();
}
private registerActions(context: ChatEntitlementContext, requests: ChatEntitlementRequests, controller: Lazy<ChatSetupController>): void {
//#region Global Chat Setup Actions
class ChatSetupTriggerAction extends Action2 {
static CHAT_SETUP_ACTION_LABEL = localize2('triggerChatSetup', "Use AI Features with Copilot for free...");
constructor() {
super({
id: CHAT_SETUP_ACTION_ID,
title: ChatSetupTriggerAction.CHAT_SETUP_ACTION_LABEL,
category: CHAT_CATEGORY,
f1: true,
precondition: ContextKeyExpr.or(
ChatContextKeys.Setup.hidden,
ChatContextKeys.Setup.disabled,
ChatContextKeys.Setup.untrusted,
ChatContextKeys.Setup.installed.negate(),
ChatContextKeys.Entitlement.canSignUp
)
});
}
override async run(accessor: ServicesAccessor, mode?: ChatModeKind | string, options?: { forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous; inputValue?: string; dialogIcon?: ThemeIcon; dialogTitle?: string }): Promise<boolean> {
const widgetService = accessor.get(IChatWidgetService);
const instantiationService = accessor.get(IInstantiationService);
const dialogService = accessor.get(IDialogService);
const commandService = accessor.get(ICommandService);
const lifecycleService = accessor.get(ILifecycleService);
const configurationService = accessor.get(IConfigurationService);
await context.update({ hidden: false });
configurationService.updateValue(ChatConfiguration.AIDisabled, false);
if (mode) {
const chatWidget = await widgetService.revealWidget();
chatWidget?.input.setChatMode(mode);
}
if (options?.inputValue) {
const chatWidget = await widgetService.revealWidget();
chatWidget?.input.showScrollbarUntilAccept();
chatWidget?.setInput(options.inputValue);
}
const setup = ChatSetup.getInstance(instantiationService, context, controller);
const { success } = await setup.run(options);
if (success === false && !lifecycleService.willShutdown) {
const { confirmed } = await dialogService.confirm({
type: Severity.Error,
message: localize('setupErrorDialog', "Chat setup failed. Would you like to try again?"),
primaryButton: localize('retry', "Retry"),
});
if (confirmed) {
return Boolean(await commandService.executeCommand(CHAT_SETUP_ACTION_ID, mode, options));
}
}
return Boolean(success);
}
}
class ChatSetupTriggerSupportAnonymousAction extends Action2 {
constructor() {
super({
id: CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID,
title: ChatSetupTriggerAction.CHAT_SETUP_ACTION_LABEL
});
}
override async run(accessor: ServicesAccessor, options?: { dialogIcon?: ThemeIcon; dialogTitle?: string }): Promise<unknown> {
const commandService = accessor.get(ICommandService);
const telemetryService = accessor.get(ITelemetryService);
const chatEntitlementService = accessor.get(IChatEntitlementService);
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'api' });
return commandService.executeCommand(CHAT_SETUP_ACTION_ID, undefined, {
forceAnonymous: chatEntitlementService.anonymous ? ChatSetupAnonymous.EnabledWithDialog : undefined,
...options
});
}
}
class ChatSetupTriggerForceSignInDialogAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.triggerSetupForceSignIn',
title: localize2('forceSignIn', "Sign in to use AI features")
});
}
override async run(accessor: ServicesAccessor): Promise<unknown> {
const commandService = accessor.get(ICommandService);
const telemetryService = accessor.get(ITelemetryService);
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'api' });
return commandService.executeCommand(CHAT_SETUP_ACTION_ID, undefined, { forceSignInDialog: true });
}
}
class ChatSetupTriggerAnonymousWithoutDialogAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.triggerSetupAnonymousWithoutDialog',
title: ChatSetupTriggerAction.CHAT_SETUP_ACTION_LABEL
});
}
override async run(accessor: ServicesAccessor): Promise<unknown> {
const commandService = accessor.get(ICommandService);
const telemetryService = accessor.get(ITelemetryService);
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'api' });
return commandService.executeCommand(CHAT_SETUP_ACTION_ID, undefined, { forceAnonymous: ChatSetupAnonymous.EnabledWithoutDialog });
}
}
class ChatSetupFromAccountsAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.triggerSetupFromAccounts',
title: localize2('triggerChatSetupFromAccounts', "Sign in to use AI features..."),
menu: {
id: MenuId.AccountsContext,
group: '2_copilot',
when: ContextKeyExpr.and(
ChatContextKeys.Setup.hidden.negate(),
ChatContextKeys.Setup.installed.negate(),
ChatContextKeys.Entitlement.signedOut
)
}
});
}
override async run(accessor: ServicesAccessor): Promise<void> {
const commandService = accessor.get(ICommandService);
const telemetryService = accessor.get(ITelemetryService);
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'accounts' });
return commandService.executeCommand(CHAT_SETUP_ACTION_ID);
}
}
class ChatSetupSignInTitleBarAction extends Action2 {
static readonly ID = SIGN_IN_TITLE_BAR_ACTION_ID;
constructor() {
super({
id: ChatSetupSignInTitleBarAction.ID,
title: localize('signInIndicatorTitleBarAction', 'Sign In'),
f1: false,
menu: [{
id: MenuId.TitleBarAdjacentCenter,
order: 0, // same position as the update button
when: ContextKeyExpr.and(
IsWebContext.negate(),
ContextKeyExpr.has(`config.${ChatConfiguration.SignInTitleBarEnabled}`),
ChatContextKeys.Entitlement.signedOut,
ChatContextKeys.Setup.hidden.negate(),
ContextKeyExpr.has('updateTitleBar').negate()
),
}]
});
}
override async run(accessor: ServicesAccessor): Promise<void> {
const commandService = accessor.get(ICommandService);
const telemetryService = accessor.get(ITelemetryService);
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'titlebar' });
return commandService.executeCommand(CHAT_SETUP_ACTION_ID);
}
}
const windowFocusListener = this._register(new MutableDisposable());
class UpgradePlanAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.upgradePlan',
title: localize2('managePlan', "Upgrade to GitHub Copilot Pro"),
category: localize2('chat.category', 'Chat'),
f1: true,
precondition: ContextKeyExpr.and(
ChatContextKeys.Setup.hidden.negate(),
ContextKeyExpr.or(
ChatContextKeys.Entitlement.canSignUp,
ChatContextKeys.Entitlement.planFree
)
),
menu: {
id: MenuId.ChatTitleBarMenu,
group: 'a_first',
order: 1,
when: ContextKeyExpr.and(
ChatContextKeys.Entitlement.planFree,
ContextKeyExpr.or(
ChatContextKeys.chatQuotaExceeded,
ChatContextKeys.completionsQuotaExceeded
)
)
}
});
}
override async run(accessor: ServicesAccessor): Promise<void> {
const openerService = accessor.get(IOpenerService);
const hostService = accessor.get(IHostService);
const commandService = accessor.get(ICommandService);
openerService.open(URI.parse(defaultChat.upgradePlanUrl));
const entitlement = context.state.entitlement;
if (!isProUser(entitlement)) {
// If the user is not yet Pro, we listen to window focus to refresh the token
// when the user has come back to the window assuming the user signed up.
windowFocusListener.value = hostService.onDidChangeFocus(focus => this.onWindowFocus(focus, commandService));
}
}
private async onWindowFocus(focus: boolean, commandService: ICommandService): Promise<void> {
if (focus) {
windowFocusListener.clear();
const entitlements = await requests.forceResolveEntitlement();
if (entitlements?.entitlement && isProUser(entitlements?.entitlement)) {
refreshTokens(commandService);
}
}
}
}
class EnableOveragesAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.manageOverages',
title: localize2('manageOverages', "Manage GitHub Copilot Overages"),
category: localize2('chat.category', 'Chat'),
f1: true,
precondition: ContextKeyExpr.and(
ChatContextKeys.Setup.hidden.negate(),
ContextKeyExpr.or(
ChatContextKeys.Entitlement.planPro,
ChatContextKeys.Entitlement.planProPlus,
ChatContextKeys.Entitlement.planEdu,
)
),
menu: {
id: MenuId.ChatTitleBarMenu,
group: 'a_first',
order: 1,
when: ContextKeyExpr.and(
ContextKeyExpr.or(
ChatContextKeys.Entitlement.planPro,
ChatContextKeys.Entitlement.planProPlus,
ChatContextKeys.Entitlement.planEdu,
),
ContextKeyExpr.or(
ChatContextKeys.chatQuotaExceeded,
ChatContextKeys.completionsQuotaExceeded
)
)
}
});
}
override async run(accessor: ServicesAccessor): Promise<void> {
const openerService = accessor.get(IOpenerService);
openerService.open(URI.parse(defaultChat.manageOveragesUrl));
}
}
registerAction2(ChatSetupTriggerAction);
registerAction2(ChatSetupTriggerForceSignInDialogAction);
registerAction2(ChatSetupFromAccountsAction);
registerAction2(ChatSetupSignInTitleBarAction);
registerAction2(ChatSetupTriggerAnonymousWithoutDialogAction);
registerAction2(ChatSetupTriggerSupportAnonymousAction);
registerAction2(UpgradePlanAction);
registerAction2(EnableOveragesAction);
//#endregion
//#region Editor Context Menu
function registerGenerateCodeCommand(coreCommand: 'chat.internal.explain' | 'chat.internal.fix' | 'chat.internal.review' | 'chat.internal.codeReview.run', actualCommand: string): void {
CommandsRegistry.registerCommand(coreCommand, async (accessor, ...args) => {
const commandService = accessor.get(ICommandService);
const codeEditorService = accessor.get(ICodeEditorService);
const markerService = accessor.get(IMarkerService);
switch (coreCommand) {
case 'chat.internal.explain':
case 'chat.internal.fix': {
const textEditor = codeEditorService.getActiveCodeEditor();
const uri = textEditor?.getModel()?.uri;
const range = textEditor?.getSelection();
if (!uri || !range) {
return;
}
const markers = AICodeActionsHelper.warningOrErrorMarkersAtRange(markerService, uri, range);
const actualCommand = coreCommand === 'chat.internal.explain'
? AICodeActionsHelper.explainMarkers(markers)
: AICodeActionsHelper.fixMarkers(markers, range);
await commandService.executeCommand(actualCommand.id, ...(actualCommand.arguments ?? []));
break;
}
case 'chat.internal.review': {
const result = await commandService.executeCommand(CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID);
if (result) {
await commandService.executeCommand(actualCommand);
}
break;
}
case 'chat.internal.codeReview.run': {
return commandService.executeCommand(actualCommand, ...args);
}
}
});
}
registerGenerateCodeCommand('chat.internal.explain', 'github.copilot.chat.explain');
registerGenerateCodeCommand('chat.internal.fix', 'github.copilot.chat.fix');
registerGenerateCodeCommand('chat.internal.review', 'github.copilot.chat.review');
registerGenerateCodeCommand('chat.internal.codeReview.run', 'github.copilot.chat.codeReview.run');
const internalGenerateCodeContext = ContextKeyExpr.and(
ChatContextKeys.Setup.hidden.negate(),
ChatContextKeys.Setup.disabled.negate(),
ChatContextKeys.Setup.installed.negate(),
);
MenuRegistry.appendMenuItem(MenuId.EditorContext, {
command: {
id: 'chat.internal.explain',
title: localize('explain', "Explain"),
},
group: '1_chat',
order: 4,
when: internalGenerateCodeContext
});
MenuRegistry.appendMenuItem(MenuId.EditorContext, {
command: {
id: 'chat.internal.fix',
title: localize('fix', "Fix"),
},
group: '1_chat',
order: 5,
when: ContextKeyExpr.and(
internalGenerateCodeContext,
EditorContextKeys.readOnly.negate()
)
});
MenuRegistry.appendMenuItem(MenuId.EditorContext, {
command: {
id: 'chat.internal.review',
title: localize('review', "Code Review"),
},
group: '1_chat',
order: 6,
when: internalGenerateCodeContext
});
}
private registerSignInTitleBarEntry(actionViewItemService: IActionViewItemService): void {
this._register(actionViewItemService.register(
MenuId.TitleBarAdjacentCenter,
SIGN_IN_TITLE_BAR_ACTION_ID,
(action, options) => new SignInTitleBarEntry(action, options)
));
}
private registerUrlLinkHandler(): void {
this._register(ExtensionUrlHandlerOverrideRegistry.registerHandler(this.instantiationService.createInstance(ChatSetupExtensionUrlHandler)));
}
private async checkExtensionInstallation(context: ChatEntitlementContext): Promise<void> {
// When developing extensions, await registration and then check
if (this.environmentService.isExtensionDevelopment) {
await this.extensionService.whenInstalledExtensionsRegistered();
if (this.extensionService.extensions.find(ext => ExtensionIdentifier.equals(ext.identifier, defaultChat.chatExtensionId))) {
context.update({ installed: true, disabled: false, untrusted: false });
return;
}
}
// Await extensions to be ready to be queried
await this.extensionsWorkbenchService.queryLocal();
// Listen to extensions change and process extensions once
this._register(Event.runAndSubscribe<IExtension | undefined>(this.extensionsWorkbenchService.onChange, e => {
if (e && !ExtensionIdentifier.equals(e.identifier.id, defaultChat.chatExtensionId)) {
return; // unrelated event
}
const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.chatExtensionId));
const installed = !!defaultChatExtension?.local;
let disabled: boolean;
let untrusted = false;
if (installed) {
disabled = !this.extensionEnablementService.isEnabled(defaultChatExtension.local);
if (disabled) {
const state = this.extensionEnablementService.getEnablementState(defaultChatExtension.local);
if (state === EnablementState.DisabledByTrustRequirement) {
disabled = false; // not disabled by user choice but
untrusted = true; // by missing workspace trust
}
}
} else {
disabled = false;
}
context.update({ installed, disabled, untrusted });
}));
}
}
class ChatSetupExtensionUrlHandler implements IExtensionUrlHandlerOverride {
constructor(
@IProductService private readonly productService: IProductService,
@ICommandService private readonly commandService: ICommandService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IChatModeService private readonly chatModeService: IChatModeService,
) { }
canHandleURL(url: URI): boolean {
return url.scheme === this.productService.urlProtocol && equalsIgnoreCase(url.authority, defaultChat.chatExtensionId);
}
async handleURL(url: URI): Promise<boolean> {
const params = new URLSearchParams(url.query);
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'url', detail: params.get('referrer') ?? undefined });
const agentParam = params.get('agent') ?? params.get('mode');
const inputParam = params.get('prompt');
if (!agentParam && !inputParam) {
return false;
}
const agentId = agentParam ? this.resolveAgentId(agentParam) : undefined;
await this.commandService.executeCommand(CHAT_SETUP_ACTION_ID, agentId, inputParam ? { inputValue: inputParam } : undefined);
return true;
}
private resolveAgentId(agentParam: string): string | undefined {
const agents = this.chatModeService.getModes();
const allAgents = [...agents.builtin, ...agents.custom];
const foundAgent = allAgents.find(agent => agent.id === agentParam);
if (foundAgent) {
return foundAgent.id;
}
const nameLower = agentParam.toLowerCase();
const agentByName = allAgents.find(agent => agent.name.get().toLowerCase() === nameLower);
return agentByName?.id;
}
}
export class ChatTeardownContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.chatTeardown';
constructor(
@IChatEntitlementService chatEntitlementService: ChatEntitlementService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService
) {
super();
const context = chatEntitlementService.context?.value;
if (!context) {
return; // disabled
}
this.registerListeners();
this.registerActions();
this.handleChatDisabled(false);
}
private handleChatDisabled(fromEvent: boolean): void {
const chatDisabled = this.configurationService.inspect(ChatConfiguration.AIDisabled);
if (chatDisabled.value === true) {
this.maybeEnableOrDisableExtension(typeof chatDisabled.workspaceValue === 'boolean' ? EnablementState.DisabledWorkspace : EnablementState.DisabledGlobally);
if (fromEvent) {
this.maybeHideAuxiliaryBar();
}
} else if (chatDisabled.value === false && fromEvent /* do not enable extensions unless its an explicit settings change */) {
this.maybeEnableOrDisableExtension(typeof chatDisabled.workspaceValue === 'boolean' ? EnablementState.EnabledWorkspace : EnablementState.EnabledGlobally);
}
}
private async registerListeners(): Promise<void> {
// Configuration changes
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (!e.affectsConfiguration(ChatConfiguration.AIDisabled)) {
return;
}
this.handleChatDisabled(true);
}));
// Extension installation
await this.extensionsWorkbenchService.queryLocal();
this._register(this.extensionsWorkbenchService.onChange(e => {
if (e && !ExtensionIdentifier.equals(e.identifier.id, defaultChat.chatExtensionId)) {
return; // unrelated event
}
const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.chatExtensionId));
if (defaultChatExtension?.local && this.extensionEnablementService.isEnabled(defaultChatExtension.local)) {
this.configurationService.updateValue(ChatConfiguration.AIDisabled, false);
}
}));
}
private async maybeEnableOrDisableExtension(state: EnablementState.EnabledGlobally | EnablementState.EnabledWorkspace | EnablementState.DisabledGlobally | EnablementState.DisabledWorkspace): Promise<void> {
const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.chatExtensionId));
if (!defaultChatExtension) {
return;
}
await this.extensionsWorkbenchService.setEnablement([defaultChatExtension], state);
await this.extensionsWorkbenchService.updateRunningExtensions(state === EnablementState.EnabledGlobally || state === EnablementState.EnabledWorkspace ? localize('restartExtensionHost.reason.enable', "Enabling AI features") : localize('restartExtensionHost.reason.disable', "Disabling AI features"));
}
private maybeHideAuxiliaryBar(): void {
const activeContainers = this.viewDescriptorService.getViewContainersByLocation(ViewContainerLocation.AuxiliaryBar).filter(
container => this.viewDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0
);
if (
(activeContainers.length === 0) || // chat view is already gone but we know it was there before
(activeContainers.length === 1 && activeContainers.at(0)?.id === ChatViewContainerId) // chat view is the only view which is going to go away
) {
this.layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); // hide if there are no views in the secondary sidebar
}
}
private registerActions(): void {
class ChatSetupHideAction extends Action2 {
static readonly ID = 'workbench.action.chat.hideSetup';
static readonly TITLE = localize2('hideChatSetup', "Learn How to Hide AI Features");
constructor() {
super({
id: ChatSetupHideAction.ID,
title: ChatSetupHideAction.TITLE,
f1: true,
category: CHAT_CATEGORY,
precondition: ChatContextKeys.Setup.hidden.negate(),
menu: {
id: MenuId.ChatTitleBarMenu,
group: 'z_hide',
order: 1,
when: ChatContextKeys.Setup.installed.negate()
}
});
}
override async run(accessor: ServicesAccessor): Promise<void> {
const preferencesService = accessor.get(IPreferencesService);
preferencesService.openSettings({ jsonEditor: false, query: `@id:${ChatConfiguration.AIDisabled}` });
}
}
registerAction2(ChatSetupHideAction);
}
}
//#endregion
export function refreshTokens(commandService: ICommandService): void {
// ugly, but we need to signal to the extension that entitlements changed
commandService.executeCommand(defaultChat.completionsRefreshTokenCommand);
commandService.executeCommand(defaultChat.chatRefreshTokenCommand);
}
/**
* Custom action view item that renders a "Sign In" button
* in the title bar with prominent button styling.
*/
class SignInTitleBarEntry extends BaseActionViewItem {
private label: HTMLElement | undefined;
constructor(
action: IAction,
options: IBaseActionViewItemOptions,
) {
super(undefined, action, options);
}
public override render(container: HTMLElement) {
super.render(container);
container.setAttribute('role', 'button');
container.setAttribute('aria-label', this.action.label);
const content = dom.append(container, dom.$('.update-indicator.prominent'));
this.label = dom.append(content, dom.$('.indicator-label'));
this.label.textContent = this.action.label;
}
protected override updateLabel(): void {
if (this.label) {
this.label.textContent = this.action.label;
}
if (this.element) {
this.element.setAttribute('aria-label', this.action.label);
}
}
protected override updateEnabled(): void {
if (this.element) {
this.element.classList.toggle('disabled', !this.action.enabled);
}
}
}