Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Codicon } from '../../../../../base/common/codicons.js';6import { DisposableStore } from '../../../../../base/common/lifecycle.js';7import { localize, localize2 } from '../../../../../nls.js';8import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';9import { ICommandService } from '../../../../../platform/commands/common/commands.js';10import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';11import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';12import { IFileService } from '../../../../../platform/files/common/files.js';13import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';14import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';15import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';16import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';17import { ChatConfiguration } from '../../common/constants.js';18import { IAgentPluginRepositoryService } from '../../common/plugins/agentPluginRepositoryService.js';19import { IPluginInstallService } from '../../common/plugins/pluginInstallService.js';20import { type IMarketplaceReference, MarketplaceReferenceKind, parseMarketplaceReference, parseMarketplaceReferences } from '../../common/plugins/pluginMarketplaceService.js';21import { InstalledAgentPluginsViewId } from '../chat.js';22import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from './chatActions.js';2324export class ManagePluginsAction extends Action2 {25static readonly ID = 'workbench.action.chat.managePlugins';2627constructor() {28super({29id: ManagePluginsAction.ID,30title: localize2('plugins', 'Plugins'),31category: CHAT_CATEGORY,32precondition: ChatContextKeys.enabled,33menu: [{34id: CHAT_CONFIG_MENU_ID,35group: '2_plugins',36}],37f1: true38});39}4041async run(accessor: ServicesAccessor): Promise<void> {42accessor.get(IExtensionsWorkbenchService).openSearch('@agentPlugins ');43}44}4546class InstallFromSourceAction extends Action2 {47static readonly ID = 'workbench.action.chat.installPluginFromSource';4849constructor() {50super({51id: InstallFromSourceAction.ID,52title: localize2('installPluginFromSource', 'Install Plugin from Source'),53category: CHAT_CATEGORY,54icon: Codicon.add,55precondition: ChatContextKeys.enabled,56f1: true,57menu: [{58id: MenuId.ViewTitle,59when: ContextKeyExpr.and(60ContextKeyExpr.equals('view', InstalledAgentPluginsViewId),61ChatContextKeys.Setup.hidden.negate(),62ChatContextKeys.Setup.disabledInWorkspace.negate(),63),64group: 'navigation',65order: 1,66}],67});68}6970async run(accessor: ServicesAccessor): Promise<void> {71const quickInputService = accessor.get(IQuickInputService);72const pluginInstallService = accessor.get(IPluginInstallService);73const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);7475const store = new DisposableStore();76const inputBox = store.add(quickInputService.createInputBox());77inputBox.placeholder = localize('pluginSourcePlaceholder', "owner/repo or git clone URL");78inputBox.prompt = localize('pluginSourcePrompt', "Enter a GitHub repository or git URL to install a plugin from");79inputBox.ignoreFocusOut = true;80inputBox.show();8182store.add(inputBox.onDidChangeValue(() => {83inputBox.validationMessage = undefined;84}));8586let installing = false;87store.add(inputBox.onDidHide(() => {88if (!installing) {89store.dispose();90}91}));9293store.add(inputBox.onDidAccept(async () => {94const source = inputBox.value.trim();95if (!source) {96return;97}9899// Quick format validation keeps the input box open for correction.100const validationError = pluginInstallService.validatePluginSource(source);101if (validationError) {102inputBox.validationMessage = validationError;103return;104}105106// Show busy state and prevent concurrent installs.107inputBox.busy = true;108inputBox.enabled = false;109installing = true;110try {111// Hide the input box so it doesn't conflict with trust/progress dialogs.112inputBox.hide();113114const result = await pluginInstallService.installPluginFromValidatedSource(source);115if (!result.success) {116if (result.message) {117// Re-open with the error so the user can correct their input.118inputBox.validationMessage = result.message;119}120inputBox.show();121} else {122const ref = parseMarketplaceReference(source);123if (ref) {124extensionsWorkbenchService.openSearch(`@agentPlugins ${ref.displayLabel}`);125}126store.dispose();127}128} finally {129installing = false;130if (!store.isDisposed) {131inputBox.busy = false;132inputBox.enabled = true;133}134}135}));136}137}138139interface IMarketplaceQuickPickItem extends IQuickPickItem {140readonly reference: IMarketplaceReference;141}142143class ManagePluginMarketplacesAction extends Action2 {144static readonly ID = 'workbench.action.chat.managePluginMarketplaces';145146constructor() {147super({148id: ManagePluginMarketplacesAction.ID,149title: localize2('managePluginMarketplaces', 'Manage Plugin Marketplaces'),150icon: Codicon.globe,151category: CHAT_CATEGORY,152precondition: ChatContextKeys.enabled,153f1: true,154menu: [{155id: MenuId.ViewTitle,156when: ContextKeyExpr.and(157ContextKeyExpr.equals('view', InstalledAgentPluginsViewId),158ChatContextKeys.Setup.hidden.negate(),159ChatContextKeys.Setup.disabledInWorkspace.negate(),160),161group: 'navigation',162order: 2,163}],164});165}166167async run(accessor: ServicesAccessor): Promise<void> {168const quickInputService = accessor.get(IQuickInputService);169const configurationService = accessor.get(IConfigurationService);170const pluginRepositoryService = accessor.get(IAgentPluginRepositoryService);171const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);172const commandService = accessor.get(ICommandService);173const fileService = accessor.get(IFileService);174175const configuredRefs = configurationService.getValue<unknown[]>(ChatConfiguration.PluginMarketplaces) ?? [];176const refs = parseMarketplaceReferences(configuredRefs);177178if (refs.length === 0) {179quickInputService.pick([], { placeHolder: localize('noMarketplaces', "No plugin marketplaces configured") });180return;181}182183// Step 1: pick a marketplace184const items: IMarketplaceQuickPickItem[] = refs.map(ref => ({185label: ref.displayLabel,186description: ref.kind === MarketplaceReferenceKind.LocalFileUri187? localize('localMarketplace', "Local")188: ref.cloneUrl,189reference: ref,190}));191192const selected = await quickInputService.pick(items, {193placeHolder: localize('selectMarketplace', "Select a plugin marketplace"),194});195196if (!selected) {197return;198}199200const ref = selected.reference;201202// Step 2: pick an action for the selected marketplace203const actionItems: IQuickPickItem[] = [204{ id: 'showPlugins', label: localize('showPlugins', "Show Plugins") },205];206207// "Open Folder" only for cloned/local repos208const repoUri = pluginRepositoryService.getRepositoryUri(ref);209const repoExists = await fileService.exists(repoUri);210if (repoExists) {211actionItems.push({ id: 'openDirectory', label: localize('openMarketplaceDirectory', "Open Folder") });212}213214actionItems.push({ id: 'removeMarketplace', label: localize('removeMarketplace', "Remove Marketplace") });215216const action = await quickInputService.pick(actionItems, {217placeHolder: localize('selectMarketplaceAction', "Select an action for '{0}'", ref.displayLabel),218});219220if (!action) {221return;222}223224switch (action.id) {225case 'showPlugins':226extensionsWorkbenchService.openSearch(`@agentPlugins ${ref.displayLabel}`);227break;228case 'openDirectory':229await commandService.executeCommand('revealFileInOS', repoUri);230break;231case 'removeMarketplace': {232const currentValues = configurationService.getValue<unknown[]>(ChatConfiguration.PluginMarketplaces) ?? [];233const updated = currentValues.filter(v => typeof v === 'string' && v.trim() !== ref.rawValue);234await configurationService.updateValue(ChatConfiguration.PluginMarketplaces, updated);235break;236}237}238}239}240241export function registerChatPluginActions() {242registerAction2(ManagePluginsAction);243registerAction2(InstallFromSourceAction);244registerAction2(ManagePluginMarketplacesAction);245}246247248