Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts
13406 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 { Codicon } from '../../../../../base/common/codicons.js';
7
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
8
import { localize, localize2 } from '../../../../../nls.js';
9
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
10
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
11
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
12
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
13
import { IFileService } from '../../../../../platform/files/common/files.js';
14
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
15
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
16
import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
17
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
18
import { ChatConfiguration } from '../../common/constants.js';
19
import { IAgentPluginRepositoryService } from '../../common/plugins/agentPluginRepositoryService.js';
20
import { IPluginInstallService } from '../../common/plugins/pluginInstallService.js';
21
import { type IMarketplaceReference, MarketplaceReferenceKind, parseMarketplaceReference, parseMarketplaceReferences } from '../../common/plugins/pluginMarketplaceService.js';
22
import { InstalledAgentPluginsViewId } from '../chat.js';
23
import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from './chatActions.js';
24
25
export class ManagePluginsAction extends Action2 {
26
static readonly ID = 'workbench.action.chat.managePlugins';
27
28
constructor() {
29
super({
30
id: ManagePluginsAction.ID,
31
title: localize2('plugins', 'Plugins'),
32
category: CHAT_CATEGORY,
33
precondition: ChatContextKeys.enabled,
34
menu: [{
35
id: CHAT_CONFIG_MENU_ID,
36
group: '2_plugins',
37
}],
38
f1: true
39
});
40
}
41
42
async run(accessor: ServicesAccessor): Promise<void> {
43
accessor.get(IExtensionsWorkbenchService).openSearch('@agentPlugins ');
44
}
45
}
46
47
class InstallFromSourceAction extends Action2 {
48
static readonly ID = 'workbench.action.chat.installPluginFromSource';
49
50
constructor() {
51
super({
52
id: InstallFromSourceAction.ID,
53
title: localize2('installPluginFromSource', 'Install Plugin from Source'),
54
category: CHAT_CATEGORY,
55
icon: Codicon.add,
56
precondition: ChatContextKeys.enabled,
57
f1: true,
58
menu: [{
59
id: MenuId.ViewTitle,
60
when: ContextKeyExpr.and(
61
ContextKeyExpr.equals('view', InstalledAgentPluginsViewId),
62
ChatContextKeys.Setup.hidden.negate(),
63
ChatContextKeys.Setup.disabledInWorkspace.negate(),
64
),
65
group: 'navigation',
66
order: 1,
67
}],
68
});
69
}
70
71
async run(accessor: ServicesAccessor): Promise<void> {
72
const quickInputService = accessor.get(IQuickInputService);
73
const pluginInstallService = accessor.get(IPluginInstallService);
74
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
75
76
const store = new DisposableStore();
77
const inputBox = store.add(quickInputService.createInputBox());
78
inputBox.placeholder = localize('pluginSourcePlaceholder', "owner/repo or git clone URL");
79
inputBox.prompt = localize('pluginSourcePrompt', "Enter a GitHub repository or git URL to install a plugin from");
80
inputBox.ignoreFocusOut = true;
81
inputBox.show();
82
83
store.add(inputBox.onDidChangeValue(() => {
84
inputBox.validationMessage = undefined;
85
}));
86
87
let installing = false;
88
store.add(inputBox.onDidHide(() => {
89
if (!installing) {
90
store.dispose();
91
}
92
}));
93
94
store.add(inputBox.onDidAccept(async () => {
95
const source = inputBox.value.trim();
96
if (!source) {
97
return;
98
}
99
100
// Quick format validation keeps the input box open for correction.
101
const validationError = pluginInstallService.validatePluginSource(source);
102
if (validationError) {
103
inputBox.validationMessage = validationError;
104
return;
105
}
106
107
// Show busy state and prevent concurrent installs.
108
inputBox.busy = true;
109
inputBox.enabled = false;
110
installing = true;
111
try {
112
// Hide the input box so it doesn't conflict with trust/progress dialogs.
113
inputBox.hide();
114
115
const result = await pluginInstallService.installPluginFromValidatedSource(source);
116
if (!result.success) {
117
if (result.message) {
118
// Re-open with the error so the user can correct their input.
119
inputBox.validationMessage = result.message;
120
}
121
inputBox.show();
122
} else {
123
const ref = parseMarketplaceReference(source);
124
if (ref) {
125
extensionsWorkbenchService.openSearch(`@agentPlugins ${ref.displayLabel}`);
126
}
127
store.dispose();
128
}
129
} finally {
130
installing = false;
131
if (!store.isDisposed) {
132
inputBox.busy = false;
133
inputBox.enabled = true;
134
}
135
}
136
}));
137
}
138
}
139
140
interface IMarketplaceQuickPickItem extends IQuickPickItem {
141
readonly reference: IMarketplaceReference;
142
}
143
144
class ManagePluginMarketplacesAction extends Action2 {
145
static readonly ID = 'workbench.action.chat.managePluginMarketplaces';
146
147
constructor() {
148
super({
149
id: ManagePluginMarketplacesAction.ID,
150
title: localize2('managePluginMarketplaces', 'Manage Plugin Marketplaces'),
151
icon: Codicon.globe,
152
category: CHAT_CATEGORY,
153
precondition: ChatContextKeys.enabled,
154
f1: true,
155
menu: [{
156
id: MenuId.ViewTitle,
157
when: ContextKeyExpr.and(
158
ContextKeyExpr.equals('view', InstalledAgentPluginsViewId),
159
ChatContextKeys.Setup.hidden.negate(),
160
ChatContextKeys.Setup.disabledInWorkspace.negate(),
161
),
162
group: 'navigation',
163
order: 2,
164
}],
165
});
166
}
167
168
async run(accessor: ServicesAccessor): Promise<void> {
169
const quickInputService = accessor.get(IQuickInputService);
170
const configurationService = accessor.get(IConfigurationService);
171
const pluginRepositoryService = accessor.get(IAgentPluginRepositoryService);
172
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
173
const commandService = accessor.get(ICommandService);
174
const fileService = accessor.get(IFileService);
175
176
const configuredRefs = configurationService.getValue<unknown[]>(ChatConfiguration.PluginMarketplaces) ?? [];
177
const refs = parseMarketplaceReferences(configuredRefs);
178
179
if (refs.length === 0) {
180
quickInputService.pick([], { placeHolder: localize('noMarketplaces', "No plugin marketplaces configured") });
181
return;
182
}
183
184
// Step 1: pick a marketplace
185
const items: IMarketplaceQuickPickItem[] = refs.map(ref => ({
186
label: ref.displayLabel,
187
description: ref.kind === MarketplaceReferenceKind.LocalFileUri
188
? localize('localMarketplace', "Local")
189
: ref.cloneUrl,
190
reference: ref,
191
}));
192
193
const selected = await quickInputService.pick(items, {
194
placeHolder: localize('selectMarketplace', "Select a plugin marketplace"),
195
});
196
197
if (!selected) {
198
return;
199
}
200
201
const ref = selected.reference;
202
203
// Step 2: pick an action for the selected marketplace
204
const actionItems: IQuickPickItem[] = [
205
{ id: 'showPlugins', label: localize('showPlugins', "Show Plugins") },
206
];
207
208
// "Open Folder" only for cloned/local repos
209
const repoUri = pluginRepositoryService.getRepositoryUri(ref);
210
const repoExists = await fileService.exists(repoUri);
211
if (repoExists) {
212
actionItems.push({ id: 'openDirectory', label: localize('openMarketplaceDirectory', "Open Folder") });
213
}
214
215
actionItems.push({ id: 'removeMarketplace', label: localize('removeMarketplace', "Remove Marketplace") });
216
217
const action = await quickInputService.pick(actionItems, {
218
placeHolder: localize('selectMarketplaceAction', "Select an action for '{0}'", ref.displayLabel),
219
});
220
221
if (!action) {
222
return;
223
}
224
225
switch (action.id) {
226
case 'showPlugins':
227
extensionsWorkbenchService.openSearch(`@agentPlugins ${ref.displayLabel}`);
228
break;
229
case 'openDirectory':
230
await commandService.executeCommand('revealFileInOS', repoUri);
231
break;
232
case 'removeMarketplace': {
233
const currentValues = configurationService.getValue<unknown[]>(ChatConfiguration.PluginMarketplaces) ?? [];
234
const updated = currentValues.filter(v => typeof v === 'string' && v.trim() !== ref.rawValue);
235
await configurationService.updateValue(ChatConfiguration.PluginMarketplaces, updated);
236
break;
237
}
238
}
239
}
240
}
241
242
export function registerChatPluginActions() {
243
registerAction2(ManagePluginsAction);
244
registerAction2(InstallFromSourceAction);
245
registerAction2(ManagePluginMarketplacesAction);
246
}
247
248