Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/pluginUrlHandler.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { mainWindow } from '../../../../base/browser/window.js';
7
import { decodeBase64 } from '../../../../base/common/buffer.js';
8
import { Codicon } from '../../../../base/common/codicons.js';
9
import { Disposable } from '../../../../base/common/lifecycle.js';
10
import { URI } from '../../../../base/common/uri.js';
11
import { localize } from '../../../../nls.js';
12
import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
13
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
14
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
15
import { ILogService } from '../../../../platform/log/common/log.js';
16
import { IURLHandler, IURLService } from '../../../../platform/url/common/url.js';
17
import { IEditorService } from '../../../services/editor/common/editorService.js';
18
import { IHostService } from '../../../services/host/browser/host.js';
19
import { IWorkbenchContribution } from '../../../common/contributions.js';
20
import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
21
import { AgentPluginEditorInput } from './agentPluginEditor/agentPluginEditorInput.js';
22
import { AgentPluginItemKind, IMarketplacePluginItem } from './agentPluginEditor/agentPluginItems.js';
23
import { ChatConfiguration } from '../common/constants.js';
24
import { MarketplaceReferenceKind, parseMarketplaceReference, parseMarketplaceReferences } from '../common/plugins/marketplaceReference.js';
25
import { IPluginInstallService } from '../common/plugins/pluginInstallService.js';
26
27
/**
28
* Handles `vscode://chat-plugin/install?source=<base64>[&plugin=<base64>]` and
29
* `vscode://chat-plugin/add-marketplace?ref=<base64>` URLs.
30
*
31
* The `source` / `ref` query parameter is a base64-encoded `owner/repo` or
32
* git clone URL. When `plugin` is provided on the `/install` route, the handler
33
* targets that specific plugin within the marketplace, installs it, and opens
34
* its details in the editor. Otherwise, a confirmation dialog is shown before
35
* any action.
36
*/
37
export class PluginUrlHandler extends Disposable implements IWorkbenchContribution, IURLHandler {
38
39
static readonly ID = 'workbench.contrib.pluginUrlHandler';
40
41
constructor(
42
@IURLService urlService: IURLService,
43
@IPluginInstallService private readonly _pluginInstallService: IPluginInstallService,
44
@IDialogService private readonly _dialogService: IDialogService,
45
@IConfigurationService private readonly _configurationService: IConfigurationService,
46
@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
47
@IHostService private readonly _hostService: IHostService,
48
@ILogService private readonly _logService: ILogService,
49
@IEditorService private readonly _editorService: IEditorService,
50
@IInstantiationService private readonly _instantiationService: IInstantiationService,
51
) {
52
super();
53
this._register(urlService.registerHandler(this));
54
}
55
56
async handleURL(uri: URI): Promise<boolean> {
57
if (uri.authority !== 'chat-plugin') {
58
return false;
59
}
60
61
switch (uri.path) {
62
case '/install':
63
return this._handleInstall(uri);
64
case '/add-marketplace':
65
return this._handleAddMarketplace(uri);
66
default:
67
return false;
68
}
69
}
70
71
// --- install a plugin from source ---
72
73
private async _handleInstall(uri: URI): Promise<boolean> {
74
const source = this._decodeQueryParam(uri, 'source');
75
if (!source) {
76
this._logService.warn('[PluginUrlHandler] Missing or invalid "source" query parameter');
77
return true;
78
}
79
80
const ref = parseMarketplaceReference(source);
81
if (!ref) {
82
this._logService.warn(`[PluginUrlHandler] Invalid plugin source: ${source}`);
83
return true;
84
}
85
86
if (ref.kind === MarketplaceReferenceKind.LocalFileUri) {
87
this._logService.warn('[PluginUrlHandler] Local file URIs are not supported for install');
88
return true;
89
}
90
91
await this._hostService.focus(mainWindow);
92
93
const pluginName = this._decodeStringParam(uri, 'plugin');
94
if (pluginName) {
95
return this._handleInstallTargetedPlugin(source, ref.displayLabel, pluginName);
96
}
97
98
const { confirmed } = await this._dialogService.confirm({
99
type: 'question',
100
message: localize('confirmInstallPlugin', "Install Plugin from '{0}'?", ref.displayLabel),
101
detail: 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),
102
primaryButton: localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install"),
103
custom: { icon: Codicon.shield },
104
});
105
106
if (!confirmed) {
107
return true;
108
}
109
110
await this._pluginInstallService.installPluginFromSource(source);
111
this._extensionsWorkbenchService.openSearch(`@agentPlugins ${ref.displayLabel}`);
112
return true;
113
}
114
115
/**
116
* Handles the case where a specific plugin is targeted within a
117
* marketplace. Delegates trust and discovery to the install service,
118
* then opens the plugin details in a modal editor.
119
*/
120
private async _handleInstallTargetedPlugin(source: string, displayLabel: string, pluginName: string): Promise<boolean> {
121
const result = await this._pluginInstallService.installPluginFromValidatedSource(source, { plugin: pluginName });
122
123
if (!result.success) {
124
if (result.message) {
125
this._logService.warn(`[PluginUrlHandler] ${result.message}`);
126
}
127
this._extensionsWorkbenchService.openSearch(`@agentPlugins ${displayLabel}`);
128
return true;
129
}
130
131
if (!result.matchedPlugin) {
132
this._extensionsWorkbenchService.openSearch(`@agentPlugins ${displayLabel}`);
133
return true;
134
}
135
136
const plugin = result.matchedPlugin;
137
const item: IMarketplacePluginItem = {
138
kind: AgentPluginItemKind.Marketplace,
139
name: plugin.name,
140
description: plugin.description,
141
source: plugin.source,
142
sourceDescriptor: plugin.sourceDescriptor,
143
marketplace: plugin.marketplace,
144
marketplaceReference: plugin.marketplaceReference,
145
marketplaceType: plugin.marketplaceType,
146
readmeUri: plugin.readmeUri,
147
};
148
149
const input = this._instantiationService.createInstance(AgentPluginEditorInput, item);
150
await this._editorService.openEditor(input);
151
152
return true;
153
}
154
155
// --- add a marketplace ---
156
157
private async _handleAddMarketplace(uri: URI): Promise<boolean> {
158
const refValue = this._decodeQueryParam(uri, 'ref');
159
if (!refValue) {
160
this._logService.warn('[PluginUrlHandler] Missing or invalid "ref" query parameter');
161
return true;
162
}
163
164
const ref = parseMarketplaceReference(refValue);
165
if (!ref) {
166
this._logService.warn(`[PluginUrlHandler] Invalid marketplace reference: ${refValue}`);
167
return true;
168
}
169
170
await this._hostService.focus(mainWindow);
171
172
const { confirmed } = await this._dialogService.confirm({
173
type: 'question',
174
message: localize('confirmAddMarketplace', "Add Plugin Marketplace '{0}'?", ref.displayLabel),
175
detail: 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),
176
primaryButton: localize({ key: 'addMarketplaceButton', comment: ['&& denotes a mnemonic'] }, "&&Add Marketplace"),
177
custom: { icon: Codicon.shield },
178
});
179
180
if (!confirmed) {
181
return true;
182
}
183
184
const existing = this._configurationService.getValue<string[]>(ChatConfiguration.PluginMarketplaces) ?? [];
185
const existingRefs = parseMarketplaceReferences(existing);
186
if (!existingRefs.some(e => e.canonicalId === ref.canonicalId)) {
187
await this._configurationService.updateValue(
188
ChatConfiguration.PluginMarketplaces,
189
[...existing, refValue],
190
ConfigurationTarget.USER,
191
);
192
}
193
194
this._extensionsWorkbenchService.openSearch(`@agentPlugins ${ref.displayLabel}`);
195
return true;
196
}
197
198
// --- helpers ---
199
200
/**
201
* Reads a query parameter and attempts to parse it as a marketplace
202
* reference. Tries base64-decoding first, then falls back to the raw
203
* value so that plain-text `owner/repo` values also work in URLs.
204
*/
205
private _decodeQueryParam(uri: URI, key: string): string | undefined {
206
const params = new URLSearchParams(uri.query);
207
const raw = params.get(key);
208
if (!raw) {
209
return undefined;
210
}
211
212
const decoded = this._tryBase64Decode(raw);
213
if (decoded && parseMarketplaceReference(decoded)) {
214
return decoded;
215
}
216
return parseMarketplaceReference(raw) ? raw : undefined;
217
}
218
219
/**
220
* Reads a query parameter and decodes it. Tries base64-decoding first,
221
* then falls back to the raw value.
222
*/
223
private _decodeStringParam(uri: URI, key: string): string | undefined {
224
const params = new URLSearchParams(uri.query);
225
return params.get(key) ?? undefined;
226
}
227
228
private _tryBase64Decode(raw: string): string | undefined {
229
try {
230
const decoded = decodeBase64(raw).toString();
231
return decoded || undefined;
232
} catch {
233
return undefined;
234
}
235
}
236
}
237
238