Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.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 { MarkdownString } from '../../../../../base/common/htmlContent.js';
8
import { applyEdits, removeProperty } from '../../../../../base/common/jsonEdit.js';
9
import { Disposable } from '../../../../../base/common/lifecycle.js';
10
import { Schemas } from '../../../../../base/common/network.js';
11
import { isMacintosh, isWindows } from '../../../../../base/common/platform.js';
12
import { basename, dirname, isEqualOrParent } from '../../../../../base/common/resources.js';
13
import { URI } from '../../../../../base/common/uri.js';
14
import { VSBuffer } from '../../../../../base/common/buffer.js';
15
import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
16
import { localize, localize2 } from '../../../../../nls.js';
17
import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js';
18
import { Action2, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
19
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
20
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
21
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
22
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
23
import { FileSystemProviderCapabilities, IFileService } from '../../../../../platform/files/common/files.js';
24
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
25
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
26
import { Registry } from '../../../../../platform/registry/common/platform.js';
27
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
28
import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../../browser/editor.js';
29
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js';
30
import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from '../../../../common/editor.js';
31
import { EditorInput } from '../../../../common/editor/editorInput.js';
32
import { IEditorService } from '../../../../services/editor/common/editorService.js';
33
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
34
import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js';
35
import { ICustomizationHarnessService } from '../../common/customizationHarnessService.js';
36
import { getChatSessionType } from '../../common/model/chatUri.js';
37
import { IAgentPluginService } from '../../common/plugins/agentPluginService.js';
38
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
39
import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
40
import { CHAT_CATEGORY } from '../actions/chatActions.js';
41
import { IChatWidgetService } from '../chat.js';
42
import { AgentPluginItemKind } from '../agentPluginEditor/agentPluginItems.js';
43
import {
44
AI_CUSTOMIZATION_ITEM_DISABLED_KEY,
45
AI_CUSTOMIZATION_ITEM_STORAGE_KEY,
46
AI_CUSTOMIZATION_ITEM_TYPE_KEY,
47
AI_CUSTOMIZATION_ITEM_URI_KEY,
48
AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID,
49
AI_CUSTOMIZATION_MANAGEMENT_EDITOR_INPUT_ID,
50
AICustomizationManagementCommands,
51
AICustomizationManagementItemMenuId,
52
AICustomizationManagementSection,
53
BUILTIN_STORAGE,
54
} from './aiCustomizationManagement.js';
55
import { AICustomizationManagementEditor } from './aiCustomizationManagementEditor.js';
56
import { AICustomizationManagementEditorInput } from './aiCustomizationManagementEditorInput.js';
57
58
//#region Telemetry
59
60
type CustomizationEditorDeleteItemEvent = {
61
promptType: string;
62
storage: string;
63
};
64
65
type CustomizationEditorDeleteItemClassification = {
66
promptType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of customization being deleted.' };
67
storage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The storage location of the deleted item.' };
68
owner: 'joshspicer';
69
comment: 'Tracks item deletion in the Agent Customizations editor.';
70
};
71
72
//#endregion
73
74
//#region Editor Registration
75
76
Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
77
EditorPaneDescriptor.create(
78
AICustomizationManagementEditor,
79
AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID,
80
localize('aiCustomizationManagementEditor', "Agent Customizations Editor")
81
),
82
[
83
// Note: Using the class directly since we use a singleton pattern
84
new SyncDescriptor(AICustomizationManagementEditorInput as unknown as { new(): AICustomizationManagementEditorInput })
85
]
86
);
87
88
//#endregion
89
90
//#region Editor Serializer
91
92
class AICustomizationManagementEditorInputSerializer implements IEditorSerializer {
93
94
canSerialize(editorInput: EditorInput): boolean {
95
return editorInput instanceof AICustomizationManagementEditorInput;
96
}
97
98
serialize(input: AICustomizationManagementEditorInput): string {
99
return '';
100
}
101
102
deserialize(instantiationService: IInstantiationService): AICustomizationManagementEditorInput {
103
return AICustomizationManagementEditorInput.getOrCreate();
104
}
105
}
106
107
Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer(
108
AI_CUSTOMIZATION_MANAGEMENT_EDITOR_INPUT_ID,
109
AICustomizationManagementEditorInputSerializer
110
);
111
112
//#endregion
113
114
//#region Context Menu Actions
115
116
/**
117
* Type for context passed to actions from list context menus.
118
* Handles both direct URI arguments and serialized context objects.
119
*/
120
type AICustomizationContext = {
121
uri: URI | string;
122
name?: string;
123
promptType?: PromptsType;
124
storage?: PromptsStorage;
125
[key: string]: unknown;
126
} | URI | string;
127
128
/**
129
* Extracts a URI from various context formats.
130
*/
131
function extractURI(context: AICustomizationContext): URI {
132
if (URI.isUri(context)) {
133
return context;
134
}
135
if (typeof context === 'string') {
136
return URI.parse(context);
137
}
138
if (URI.isUri(context.uri)) {
139
return context.uri;
140
}
141
return URI.parse(context.uri as string);
142
}
143
144
/**
145
* Extracts storage type from context.
146
*/
147
function extractStorage(context: AICustomizationContext): PromptsStorage | undefined {
148
if (URI.isUri(context) || typeof context === 'string') {
149
return undefined;
150
}
151
return context.storage;
152
}
153
154
/**
155
* Extracts prompt type from context.
156
*/
157
function extractPromptType(context: AICustomizationContext): PromptsType | undefined {
158
if (URI.isUri(context) || typeof context === 'string') {
159
return undefined;
160
}
161
return context.promptType;
162
}
163
164
/**
165
* Extracts the parent plugin URI from context, if present.
166
*/
167
function extractPluginUri(context: AICustomizationContext): URI | undefined {
168
if (URI.isUri(context) || typeof context === 'string') {
169
return undefined;
170
}
171
const raw = context.pluginUri;
172
if (!raw) {
173
return undefined;
174
}
175
return URI.isUri(raw) ? raw : typeof raw === 'string' ? URI.parse(raw) : undefined;
176
}
177
178
179
/**
180
* Extracts the item ID from context (used for identifying individual hooks within a file).
181
*/
182
function extractItemId(context: AICustomizationContext): string | undefined {
183
if (URI.isUri(context) || typeof context === 'string') {
184
return undefined;
185
}
186
return typeof context.itemId === 'string' ? context.itemId : undefined;
187
}
188
189
/**
190
* Parses a hook item ID to extract the original hook type ID and array index.
191
* Hook item IDs have the format: `fileUri#originalId[index]`
192
* Returns undefined if the ID does not match this format.
193
*/
194
function parseHookItemId(itemId: string): { originalId: string; index: number } | undefined {
195
const hashIndex = itemId.lastIndexOf('#');
196
if (hashIndex < 0) {
197
return undefined;
198
}
199
const fragment = itemId.substring(hashIndex + 1);
200
const match = /^([^[]+)\[(\d+)\]$/.exec(fragment);
201
if (!match) {
202
return undefined;
203
}
204
return { originalId: match[1], index: parseInt(match[2], 10) };
205
}
206
207
// Open file action
208
const OPEN_AI_CUSTOMIZATION_MGMT_FILE_ID = 'aiCustomizationManagement.openFile';
209
registerAction2(class extends Action2 {
210
constructor() {
211
super({
212
id: OPEN_AI_CUSTOMIZATION_MGMT_FILE_ID,
213
title: localize2('open', "Open"),
214
icon: Codicon.goToFile,
215
});
216
}
217
async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {
218
const editorService = accessor.get(IEditorService);
219
const storage = extractStorage(context);
220
221
const editorPane = await editorService.openEditor({
222
resource: extractURI(context)
223
});
224
225
const codeEditor = getCodeEditor(editorPane?.getControl());
226
if (codeEditor && (storage === PromptsStorage.extension || storage === PromptsStorage.plugin)) {
227
codeEditor.updateOptions({
228
readOnly: true,
229
readOnlyMessage: new MarkdownString(localize('readonlyPluginFile', "This file is provided by a plugin or extension and cannot be edited.")),
230
});
231
}
232
}
233
});
234
235
236
// Run prompt action
237
const RUN_PROMPT_MGMT_ID = 'aiCustomizationManagement.runPrompt';
238
registerAction2(class extends Action2 {
239
constructor() {
240
super({
241
id: RUN_PROMPT_MGMT_ID,
242
title: localize2('runPrompt', "Run Prompt"),
243
icon: Codicon.play,
244
});
245
}
246
async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {
247
const commandService = accessor.get(ICommandService);
248
await commandService.executeCommand('workbench.action.chat.run.prompt.current', extractURI(context));
249
}
250
});
251
252
// Reveal in Finder/Explorer action
253
const REVEAL_IN_OS_LABEL = isWindows
254
? localize2('revealInWindows', "Reveal in File Explorer")
255
: isMacintosh
256
? localize2('revealInMac', "Reveal in Finder")
257
: localize2('openContainer', "Open Containing Folder");
258
259
const REVEAL_AI_CUSTOMIZATION_IN_OS_ID = 'aiCustomizationManagement.revealInOS';
260
registerAction2(class extends Action2 {
261
constructor() {
262
super({
263
id: REVEAL_AI_CUSTOMIZATION_IN_OS_ID,
264
title: REVEAL_IN_OS_LABEL,
265
icon: Codicon.folderOpened,
266
});
267
}
268
async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {
269
const commandService = accessor.get(ICommandService);
270
const uri = extractURI(context);
271
// Use existing reveal command
272
await commandService.executeCommand('revealFileInOS', uri);
273
}
274
});
275
276
// Delete action
277
const DELETE_AI_CUSTOMIZATION_ID = 'aiCustomizationManagement.delete';
278
registerAction2(class extends Action2 {
279
constructor() {
280
super({
281
id: DELETE_AI_CUSTOMIZATION_ID,
282
title: localize2('delete', "Delete"),
283
icon: Codicon.trash,
284
});
285
}
286
async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {
287
const fileService = accessor.get(IFileService);
288
const dialogService = accessor.get(IDialogService);
289
const telemetryService = accessor.get(ITelemetryService);
290
const workspaceService = accessor.get(IAICustomizationWorkspaceService);
291
const editorService = accessor.get(IEditorService);
292
293
const uri = extractURI(context);
294
const storage = extractStorage(context);
295
const promptType = extractPromptType(context);
296
const itemId = extractItemId(context);
297
const isSkill = promptType === PromptsType.skill;
298
const isHook = promptType === PromptsType.hook;
299
// For skills, use the parent folder name since skills are structured as <skillname>/SKILL.md.
300
const fileName = isSkill ? basename(dirname(uri)) : basename(uri);
301
302
// Plugin-provided files: offer to uninstall the plugin
303
if (storage === PromptsStorage.plugin) {
304
const agentPluginService = accessor.get(IAgentPluginService);
305
const plugin = agentPluginService.plugins.get().find(p => isEqualOrParent(uri, p.uri));
306
if (plugin) {
307
const result = await dialogService.confirm({
308
message: localize('cannotDeletePluginItem', "This item is provided by the plugin '{0}'", plugin.label),
309
detail: localize('cannotDeletePluginItemDetail', "Individual components from a plugin cannot be removed separately. Would you like to uninstall the entire plugin?"),
310
primaryButton: localize('uninstallPlugin', "Uninstall Plugin"),
311
type: 'question',
312
});
313
if (result.confirmed) {
314
plugin.remove();
315
}
316
}
317
return;
318
}
319
320
// Extension and built-in files cannot be deleted
321
if (storage === PromptsStorage.extension || storage === BUILTIN_STORAGE) {
322
await dialogService.info(
323
localize('cannotDeleteExtension', "Cannot Delete Extension File"),
324
localize('cannotDeleteExtensionDetail', "Files provided by extensions cannot be deleted. You can disable the extension if you no longer want to use this customization.")
325
);
326
return;
327
}
328
329
// Confirm deletion
330
const hookInfo = isHook && itemId ? parseHookItemId(itemId) : undefined;
331
const hookName = typeof context !== 'string' && !URI.isUri(context) ? context.name : undefined;
332
const message = isSkill
333
? localize('confirmDeleteSkill', "Are you sure you want to delete skill '{0}' and its folder?", fileName)
334
: hookInfo && hookName
335
? localize('confirmDeleteHook', "Are you sure you want to delete the '{0}' hook?", hookName)
336
: localize('confirmDelete', "Are you sure you want to delete '{0}'?", fileName);
337
const confirmation = await dialogService.confirm({
338
message,
339
detail: localize('confirmDeleteDetail', "This action cannot be undone."),
340
primaryButton: localize('delete', "Delete"),
341
type: 'warning',
342
});
343
344
if (confirmation.confirmed) {
345
try {
346
telemetryService.publicLog2<CustomizationEditorDeleteItemEvent, CustomizationEditorDeleteItemClassification>('chatCustomizationEditor.deleteItem', {
347
promptType: promptType ?? '',
348
storage: storage ?? '',
349
});
350
} catch {
351
// Telemetry must not block deletion
352
}
353
354
// For hooks with a specific hook ID, remove only that entry from the file.
355
// Uses JSONC edits to preserve user comments and formatting.
356
if (hookInfo) {
357
try {
358
const content = await fileService.readFile(uri);
359
const text = content.value.toString();
360
const edits = removeProperty(text, ['hooks', hookInfo.originalId, hookInfo.index], { tabSize: 1, insertSpaces: false });
361
if (edits.length > 0) {
362
const updated = applyEdits(text, edits);
363
await fileService.writeFile(uri, VSBuffer.fromString(updated));
364
if (storage === PromptsStorage.local) {
365
const projectRoot = workspaceService.getActiveProjectRoot();
366
if (projectRoot) {
367
await workspaceService.commitFiles(projectRoot, [uri]);
368
}
369
}
370
}
371
} catch {
372
await dialogService.error(
373
localize('deleteHookItemFailed', "Unable to delete this hook entry because the file contents have changed."),
374
localize('deleteHookItemFailedDetail', "Refresh the view and try again."),
375
);
376
}
377
return;
378
}
379
380
// For skills, delete the parent folder (e.g. .github/skills/my-skill/)
381
// since each skill is a folder containing SKILL.md.
382
const deleteTarget = isSkill ? dirname(uri) : uri;
383
const useTrash = fileService.hasCapability(deleteTarget, FileSystemProviderCapabilities.Trash);
384
await fileService.del(deleteTarget, { useTrash, recursive: isSkill });
385
386
// Commit the deletion to git (sessions: main repo + worktree)
387
if (storage === PromptsStorage.local) {
388
const projectRoot = workspaceService.getActiveProjectRoot();
389
if (projectRoot) {
390
await workspaceService.deleteFiles(projectRoot, [deleteTarget]);
391
}
392
}
393
394
// Refresh the list to remove the deleted item immediately
395
// (provider's onDidChange may not fire if it doesn't watch the filesystem)
396
const activeEditor = editorService.activeEditorPane;
397
if (activeEditor instanceof AICustomizationManagementEditor) {
398
activeEditor.refreshList();
399
}
400
}
401
}
402
});
403
404
// Copy path action
405
const COPY_AI_CUSTOMIZATION_PATH_ID = 'aiCustomizationManagement.copyPath';
406
registerAction2(class extends Action2 {
407
constructor() {
408
super({
409
id: COPY_AI_CUSTOMIZATION_PATH_ID,
410
title: localize2('copyPath', "Copy Path"),
411
icon: Codicon.clippy,
412
});
413
}
414
async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {
415
const clipboardService = accessor.get(IClipboardService);
416
const uri = extractURI(context);
417
const textToCopy = uri.scheme === 'file' ? uri.fsPath : uri.toString(true);
418
await clipboardService.writeText(textToCopy);
419
}
420
});
421
422
/**
423
* When clause that hides an action for read-only (extension, plugin, built-in) items.
424
*/
425
const WHEN_ITEM_IS_DELETABLE = ContextKeyExpr.and(
426
ContextKeyExpr.notEquals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, PromptsStorage.extension),
427
ContextKeyExpr.notEquals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, PromptsStorage.plugin),
428
ContextKeyExpr.notEquals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, BUILTIN_STORAGE),
429
);
430
431
/**
432
* When clause that shows an action only for plugin items.
433
*/
434
const WHEN_ITEM_IS_PLUGIN = ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, PromptsStorage.plugin);
435
436
// Register context menu items
437
438
// Inline hover actions (shown as icon buttons on hover)
439
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
440
command: { id: COPY_AI_CUSTOMIZATION_PATH_ID, title: localize('copyPath', "Copy Path"), icon: Codicon.clippy },
441
group: 'inline',
442
order: 1,
443
});
444
445
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
446
command: { id: DELETE_AI_CUSTOMIZATION_ID, title: localize('delete', "Delete"), icon: Codicon.trash },
447
group: 'inline',
448
order: 10,
449
when: WHEN_ITEM_IS_DELETABLE,
450
});
451
452
// Context menu items (shown on right-click)
453
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
454
command: { id: OPEN_AI_CUSTOMIZATION_MGMT_FILE_ID, title: localize('open', "Open") },
455
group: '1_open',
456
order: 1,
457
});
458
459
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
460
command: { id: RUN_PROMPT_MGMT_ID, title: localize('runPrompt', "Run Prompt"), icon: Codicon.play },
461
group: '2_run',
462
order: 1,
463
when: ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_TYPE_KEY, PromptsType.prompt),
464
});
465
466
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
467
command: { id: REVEAL_AI_CUSTOMIZATION_IN_OS_ID, title: REVEAL_IN_OS_LABEL.value },
468
group: '3_file',
469
order: 1,
470
when: ContextKeyExpr.or(
471
ContextKeyExpr.regex(AI_CUSTOMIZATION_ITEM_URI_KEY, new RegExp(`^${Schemas.file}:`)),
472
ContextKeyExpr.regex(AI_CUSTOMIZATION_ITEM_URI_KEY, new RegExp(`^${Schemas.vscodeUserData}:`))
473
),
474
});
475
476
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
477
command: { id: DELETE_AI_CUSTOMIZATION_ID, title: localize('delete', "Delete") },
478
group: '4_modify',
479
order: 1,
480
when: WHEN_ITEM_IS_DELETABLE,
481
});
482
483
// Uninstall Plugin action - shown for plugin-provided items
484
const UNINSTALL_PLUGIN_AI_CUSTOMIZATION_ID = 'aiCustomizationManagement.uninstallPlugin';
485
registerAction2(class extends Action2 {
486
constructor() {
487
super({
488
id: UNINSTALL_PLUGIN_AI_CUSTOMIZATION_ID,
489
title: localize2('uninstallPlugin', "Uninstall Plugin"),
490
icon: Codicon.trash,
491
});
492
}
493
async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {
494
const agentPluginService = accessor.get(IAgentPluginService);
495
const dialogService = accessor.get(IDialogService);
496
497
const uri = extractURI(context);
498
const plugin = agentPluginService.plugins.get().find(p => isEqualOrParent(uri, p.uri));
499
if (!plugin) {
500
return;
501
}
502
503
const result = await dialogService.confirm({
504
message: localize('confirmUninstallPlugin', "This item is provided by the plugin '{0}'", plugin.label),
505
detail: localize('confirmUninstallPluginDetail', "Individual components from a plugin cannot be removed separately. Would you like to uninstall the entire plugin?"),
506
primaryButton: localize('uninstallPluginBtn', "Uninstall Plugin"),
507
type: 'question',
508
});
509
if (result.confirmed) {
510
plugin.remove();
511
}
512
}
513
});
514
515
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
516
command: { id: UNINSTALL_PLUGIN_AI_CUSTOMIZATION_ID, title: localize('uninstallPlugin', "Uninstall Plugin"), icon: Codicon.trash },
517
group: 'inline',
518
order: 10,
519
when: WHEN_ITEM_IS_PLUGIN,
520
});
521
522
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
523
command: { id: UNINSTALL_PLUGIN_AI_CUSTOMIZATION_ID, title: localize('uninstallPlugin', "Uninstall Plugin") },
524
group: '4_modify',
525
order: 1,
526
when: WHEN_ITEM_IS_PLUGIN,
527
});
528
529
// Show Plugin action - navigates to the parent plugin detail page
530
const SHOW_PLUGIN_AI_CUSTOMIZATION_ID = 'aiCustomizationManagement.showPlugin';
531
registerAction2(class extends Action2 {
532
constructor() {
533
super({
534
id: SHOW_PLUGIN_AI_CUSTOMIZATION_ID,
535
title: localize2('showPlugin', "Show Plugin"),
536
});
537
}
538
async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {
539
const agentPluginService = accessor.get(IAgentPluginService);
540
const editorService = accessor.get(IEditorService);
541
542
const pluginUri = extractPluginUri(context);
543
if (!pluginUri) {
544
return;
545
}
546
const plugin = agentPluginService.plugins.get().find(p => p.uri.toString() === pluginUri.toString());
547
if (!plugin) {
548
return;
549
}
550
551
const item = {
552
kind: AgentPluginItemKind.Installed as const,
553
name: plugin.label,
554
description: plugin.fromMarketplace?.description ?? '',
555
marketplace: plugin.fromMarketplace?.marketplace,
556
plugin,
557
};
558
559
// Try to show within the active AI Customization editor (with back navigation)
560
const input = AICustomizationManagementEditorInput.getOrCreate();
561
const pane = await editorService.openEditor(input, { pinned: true });
562
if (pane instanceof AICustomizationManagementEditor) {
563
await pane.showPluginDetail(item);
564
}
565
}
566
});
567
568
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
569
command: { id: SHOW_PLUGIN_AI_CUSTOMIZATION_ID, title: localize('showPlugin', "Show Plugin") },
570
group: '1_open',
571
order: 2,
572
when: WHEN_ITEM_IS_PLUGIN,
573
});
574
575
// Disable item action
576
const DISABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID = 'aiCustomizationManagement.disableItem';
577
registerAction2(class extends Action2 {
578
constructor() {
579
super({
580
id: DISABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID,
581
title: localize2('disable', "Disable"),
582
icon: Codicon.eyeClosed,
583
});
584
}
585
async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {
586
const promptsService = accessor.get(IPromptsService);
587
const uri = extractURI(context);
588
const promptType = extractPromptType(context);
589
if (!promptType) {
590
return;
591
}
592
593
const disabled = promptsService.getDisabledPromptFiles(promptType);
594
disabled.add(uri);
595
promptsService.setDisabledPromptFiles(promptType, disabled);
596
}
597
});
598
599
// Enable item action
600
const ENABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID = 'aiCustomizationManagement.enableItem';
601
registerAction2(class extends Action2 {
602
constructor() {
603
super({
604
id: ENABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID,
605
title: localize2('enable', "Enable"),
606
icon: Codicon.eye,
607
});
608
}
609
async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {
610
const promptsService = accessor.get(IPromptsService);
611
const uri = extractURI(context);
612
const promptType = extractPromptType(context);
613
if (!promptType) {
614
return;
615
}
616
617
const disabled = promptsService.getDisabledPromptFiles(promptType);
618
disabled.delete(uri);
619
promptsService.setDisabledPromptFiles(promptType, disabled);
620
}
621
});
622
623
// Context menu: Disable (shown when builtin item is enabled)
624
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
625
command: { id: DISABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID, title: localize('disable', "Disable") },
626
group: '5_toggle',
627
order: 1,
628
when: ContextKeyExpr.and(
629
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_DISABLED_KEY, false),
630
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, BUILTIN_STORAGE),
631
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_TYPE_KEY, PromptsType.skill),
632
),
633
});
634
635
// Context menu: Enable (shown when builtin item is disabled)
636
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
637
command: { id: ENABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID, title: localize('enable', "Enable") },
638
group: '5_toggle',
639
order: 1,
640
when: ContextKeyExpr.and(
641
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_DISABLED_KEY, true),
642
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, BUILTIN_STORAGE),
643
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_TYPE_KEY, PromptsType.skill),
644
),
645
});
646
647
// Inline hover: Disable (shown when builtin item is enabled)
648
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
649
command: { id: DISABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID, title: localize('disable', "Disable"), icon: Codicon.eyeClosed },
650
group: 'inline',
651
order: 5,
652
when: ContextKeyExpr.and(
653
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_DISABLED_KEY, false),
654
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, BUILTIN_STORAGE),
655
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_TYPE_KEY, PromptsType.skill),
656
),
657
});
658
659
// Inline hover: Enable (shown when builtin item is disabled)
660
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
661
command: { id: ENABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID, title: localize('enable', "Enable"), icon: Codicon.eye },
662
group: 'inline',
663
order: 5,
664
when: ContextKeyExpr.and(
665
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_DISABLED_KEY, true),
666
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, BUILTIN_STORAGE),
667
ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_TYPE_KEY, PromptsType.skill),
668
),
669
});
670
671
//#endregion
672
673
//#region Actions
674
675
class AICustomizationManagementActionsContribution extends Disposable implements IWorkbenchContribution {
676
677
static readonly ID = 'workbench.contrib.aiCustomizationManagementActions';
678
679
constructor() {
680
super();
681
this.registerActions();
682
}
683
684
private registerActions(): void {
685
// Open AI Customizations Editor
686
this._register(registerAction2(class extends Action2 {
687
constructor() {
688
super({
689
id: AICustomizationManagementCommands.OpenEditor,
690
title: localize2('openAICustomizations', "Open Customizations"),
691
shortTitle: localize2('aiCustomizations', "Customizations"),
692
category: CHAT_CATEGORY,
693
precondition: ChatContextKeys.enabled,
694
f1: true,
695
});
696
}
697
698
async run(accessor: ServicesAccessor, section?: AICustomizationManagementSection): Promise<void> {
699
const editorService = accessor.get(IEditorService);
700
const chatWidgetService = accessor.get(IChatWidgetService);
701
const harnessService = accessor.get(ICustomizationHarnessService);
702
703
// Detect the active chat session type and switch the harness
704
// so the customization editor opens in the matching context.
705
const sessionResource = chatWidgetService.lastFocusedWidget?.viewModel?.sessionResource;
706
if (sessionResource) {
707
const sessionType = getChatSessionType(sessionResource);
708
const harness = harnessService.findHarnessById(sessionType);
709
if (harness) {
710
harnessService.setActiveHarness(sessionType);
711
}
712
}
713
714
const input = AICustomizationManagementEditorInput.getOrCreate();
715
const pane = await editorService.openEditor(input, { pinned: true });
716
if (section && pane instanceof AICustomizationManagementEditor) {
717
pane.selectSectionById(section);
718
}
719
}
720
}));
721
722
// Open Marketplace (hidden command for deep-linking into browse mode)
723
this._register(registerAction2(class extends Action2 {
724
constructor() {
725
super({
726
id: AICustomizationManagementCommands.OpenMarketplace,
727
title: localize2('openMarketplace', "Open Marketplace"),
728
category: CHAT_CATEGORY,
729
precondition: ChatContextKeys.enabled,
730
});
731
}
732
733
async run(accessor: ServicesAccessor, section?: AICustomizationManagementSection): Promise<void> {
734
const editorService = accessor.get(IEditorService);
735
const input = AICustomizationManagementEditorInput.getOrCreate();
736
const pane = await editorService.openEditor(input, { pinned: true });
737
if (pane instanceof AICustomizationManagementEditor) {
738
const targetSection = section ?? AICustomizationManagementSection.McpServers;
739
pane.selectSectionById(targetSection, { showMarketplace: true });
740
}
741
}
742
}));
743
744
// Generate Debug Report
745
this._register(registerAction2(class extends Action2 {
746
constructor() {
747
super({
748
id: AICustomizationManagementCommands.GenerateDebugReport,
749
title: localize2('generateDebugReport', "Generate Customization Debug Report"),
750
category: Categories.Developer,
751
precondition: ChatContextKeys.enabled,
752
f1: true,
753
});
754
}
755
756
async run(accessor: ServicesAccessor): Promise<void> {
757
const editorService = accessor.get(IEditorService);
758
// Open the customizations editor if not already open
759
const input = AICustomizationManagementEditorInput.getOrCreate();
760
const pane = await editorService.openEditor(input, { pinned: true });
761
if (!(pane instanceof AICustomizationManagementEditor)) {
762
return;
763
}
764
const report = await pane.generateDebugReport();
765
await editorService.openEditor({
766
resource: undefined,
767
contents: report,
768
languageId: 'plaintext',
769
});
770
}
771
}));
772
773
}
774
}
775
776
registerWorkbenchContribution2(
777
AICustomizationManagementActionsContribution.ID,
778
AICustomizationManagementActionsContribution,
779
WorkbenchPhase.AfterRestored
780
);
781
782
//#endregion
783
784