Path: blob/main/src/vs/workbench/contrib/chat/browser/pluginUrlHandler.ts
13401 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 { mainWindow } from '../../../../base/browser/window.js';6import { decodeBase64 } from '../../../../base/common/buffer.js';7import { Codicon } from '../../../../base/common/codicons.js';8import { Disposable } from '../../../../base/common/lifecycle.js';9import { URI } from '../../../../base/common/uri.js';10import { localize } from '../../../../nls.js';11import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';12import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';13import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';14import { ILogService } from '../../../../platform/log/common/log.js';15import { IURLHandler, IURLService } from '../../../../platform/url/common/url.js';16import { IEditorService } from '../../../services/editor/common/editorService.js';17import { IHostService } from '../../../services/host/browser/host.js';18import { IWorkbenchContribution } from '../../../common/contributions.js';19import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';20import { AgentPluginEditorInput } from './agentPluginEditor/agentPluginEditorInput.js';21import { AgentPluginItemKind, IMarketplacePluginItem } from './agentPluginEditor/agentPluginItems.js';22import { ChatConfiguration } from '../common/constants.js';23import { MarketplaceReferenceKind, parseMarketplaceReference, parseMarketplaceReferences } from '../common/plugins/marketplaceReference.js';24import { IPluginInstallService } from '../common/plugins/pluginInstallService.js';2526/**27* Handles `vscode://chat-plugin/install?source=<base64>[&plugin=<base64>]` and28* `vscode://chat-plugin/add-marketplace?ref=<base64>` URLs.29*30* The `source` / `ref` query parameter is a base64-encoded `owner/repo` or31* git clone URL. When `plugin` is provided on the `/install` route, the handler32* targets that specific plugin within the marketplace, installs it, and opens33* its details in the editor. Otherwise, a confirmation dialog is shown before34* any action.35*/36export class PluginUrlHandler extends Disposable implements IWorkbenchContribution, IURLHandler {3738static readonly ID = 'workbench.contrib.pluginUrlHandler';3940constructor(41@IURLService urlService: IURLService,42@IPluginInstallService private readonly _pluginInstallService: IPluginInstallService,43@IDialogService private readonly _dialogService: IDialogService,44@IConfigurationService private readonly _configurationService: IConfigurationService,45@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,46@IHostService private readonly _hostService: IHostService,47@ILogService private readonly _logService: ILogService,48@IEditorService private readonly _editorService: IEditorService,49@IInstantiationService private readonly _instantiationService: IInstantiationService,50) {51super();52this._register(urlService.registerHandler(this));53}5455async handleURL(uri: URI): Promise<boolean> {56if (uri.authority !== 'chat-plugin') {57return false;58}5960switch (uri.path) {61case '/install':62return this._handleInstall(uri);63case '/add-marketplace':64return this._handleAddMarketplace(uri);65default:66return false;67}68}6970// --- install a plugin from source ---7172private async _handleInstall(uri: URI): Promise<boolean> {73const source = this._decodeQueryParam(uri, 'source');74if (!source) {75this._logService.warn('[PluginUrlHandler] Missing or invalid "source" query parameter');76return true;77}7879const ref = parseMarketplaceReference(source);80if (!ref) {81this._logService.warn(`[PluginUrlHandler] Invalid plugin source: ${source}`);82return true;83}8485if (ref.kind === MarketplaceReferenceKind.LocalFileUri) {86this._logService.warn('[PluginUrlHandler] Local file URIs are not supported for install');87return true;88}8990await this._hostService.focus(mainWindow);9192const pluginName = this._decodeStringParam(uri, 'plugin');93if (pluginName) {94return this._handleInstallTargetedPlugin(source, ref.displayLabel, pluginName);95}9697const { confirmed } = await this._dialogService.confirm({98type: 'question',99message: localize('confirmInstallPlugin', "Install Plugin from '{0}'?", ref.displayLabel),100detail: localize('confirmInstallPluginDetail', "An external application wants to install a plugin from this source. Plugins can run code on your machine. Only install plugins from sources you trust.\n\nSource: {0}", ref.rawValue),101primaryButton: localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install"),102custom: { icon: Codicon.shield },103});104105if (!confirmed) {106return true;107}108109await this._pluginInstallService.installPluginFromSource(source);110this._extensionsWorkbenchService.openSearch(`@agentPlugins ${ref.displayLabel}`);111return true;112}113114/**115* Handles the case where a specific plugin is targeted within a116* marketplace. Delegates trust and discovery to the install service,117* then opens the plugin details in a modal editor.118*/119private async _handleInstallTargetedPlugin(source: string, displayLabel: string, pluginName: string): Promise<boolean> {120const result = await this._pluginInstallService.installPluginFromValidatedSource(source, { plugin: pluginName });121122if (!result.success) {123if (result.message) {124this._logService.warn(`[PluginUrlHandler] ${result.message}`);125}126this._extensionsWorkbenchService.openSearch(`@agentPlugins ${displayLabel}`);127return true;128}129130if (!result.matchedPlugin) {131this._extensionsWorkbenchService.openSearch(`@agentPlugins ${displayLabel}`);132return true;133}134135const plugin = result.matchedPlugin;136const item: IMarketplacePluginItem = {137kind: AgentPluginItemKind.Marketplace,138name: plugin.name,139description: plugin.description,140source: plugin.source,141sourceDescriptor: plugin.sourceDescriptor,142marketplace: plugin.marketplace,143marketplaceReference: plugin.marketplaceReference,144marketplaceType: plugin.marketplaceType,145readmeUri: plugin.readmeUri,146};147148const input = this._instantiationService.createInstance(AgentPluginEditorInput, item);149await this._editorService.openEditor(input);150151return true;152}153154// --- add a marketplace ---155156private async _handleAddMarketplace(uri: URI): Promise<boolean> {157const refValue = this._decodeQueryParam(uri, 'ref');158if (!refValue) {159this._logService.warn('[PluginUrlHandler] Missing or invalid "ref" query parameter');160return true;161}162163const ref = parseMarketplaceReference(refValue);164if (!ref) {165this._logService.warn(`[PluginUrlHandler] Invalid marketplace reference: ${refValue}`);166return true;167}168169await this._hostService.focus(mainWindow);170171const { confirmed } = await this._dialogService.confirm({172type: 'question',173message: localize('confirmAddMarketplace', "Add Plugin Marketplace '{0}'?", ref.displayLabel),174detail: localize('confirmAddMarketplaceDetail', "An external application wants to add a plugin marketplace. Plugins from this marketplace will appear in the plugin catalog and can be installed.\n\nSource: {0}", ref.rawValue),175primaryButton: localize({ key: 'addMarketplaceButton', comment: ['&& denotes a mnemonic'] }, "&&Add Marketplace"),176custom: { icon: Codicon.shield },177});178179if (!confirmed) {180return true;181}182183const existing = this._configurationService.getValue<string[]>(ChatConfiguration.PluginMarketplaces) ?? [];184const existingRefs = parseMarketplaceReferences(existing);185if (!existingRefs.some(e => e.canonicalId === ref.canonicalId)) {186await this._configurationService.updateValue(187ChatConfiguration.PluginMarketplaces,188[...existing, refValue],189ConfigurationTarget.USER,190);191}192193this._extensionsWorkbenchService.openSearch(`@agentPlugins ${ref.displayLabel}`);194return true;195}196197// --- helpers ---198199/**200* Reads a query parameter and attempts to parse it as a marketplace201* reference. Tries base64-decoding first, then falls back to the raw202* value so that plain-text `owner/repo` values also work in URLs.203*/204private _decodeQueryParam(uri: URI, key: string): string | undefined {205const params = new URLSearchParams(uri.query);206const raw = params.get(key);207if (!raw) {208return undefined;209}210211const decoded = this._tryBase64Decode(raw);212if (decoded && parseMarketplaceReference(decoded)) {213return decoded;214}215return parseMarketplaceReference(raw) ? raw : undefined;216}217218/**219* Reads a query parameter and decodes it. Tries base64-decoding first,220* then falls back to the raw value.221*/222private _decodeStringParam(uri: URI, key: string): string | undefined {223const params = new URLSearchParams(uri.query);224return params.get(key) ?? undefined;225}226227private _tryBase64Decode(raw: string): string | undefined {228try {229const decoded = decodeBase64(raw).toString();230return decoded || undefined;231} catch {232return undefined;233}234}235}236237238