Path: blob/main/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.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 { MarkdownString } from '../../../../../base/common/htmlContent.js';7import { applyEdits, removeProperty } from '../../../../../base/common/jsonEdit.js';8import { Disposable } from '../../../../../base/common/lifecycle.js';9import { Schemas } from '../../../../../base/common/network.js';10import { isMacintosh, isWindows } from '../../../../../base/common/platform.js';11import { basename, dirname, isEqualOrParent } from '../../../../../base/common/resources.js';12import { URI } from '../../../../../base/common/uri.js';13import { VSBuffer } from '../../../../../base/common/buffer.js';14import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js';15import { localize, localize2 } from '../../../../../nls.js';16import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js';17import { Action2, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';18import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';19import { ICommandService } from '../../../../../platform/commands/common/commands.js';20import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';21import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';22import { FileSystemProviderCapabilities, IFileService } from '../../../../../platform/files/common/files.js';23import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';24import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';25import { Registry } from '../../../../../platform/registry/common/platform.js';26import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';27import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../../browser/editor.js';28import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js';29import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from '../../../../common/editor.js';30import { EditorInput } from '../../../../common/editor/editorInput.js';31import { IEditorService } from '../../../../services/editor/common/editorService.js';32import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';33import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js';34import { ICustomizationHarnessService } from '../../common/customizationHarnessService.js';35import { getChatSessionType } from '../../common/model/chatUri.js';36import { IAgentPluginService } from '../../common/plugins/agentPluginService.js';37import { PromptsType } from '../../common/promptSyntax/promptTypes.js';38import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';39import { CHAT_CATEGORY } from '../actions/chatActions.js';40import { IChatWidgetService } from '../chat.js';41import { AgentPluginItemKind } from '../agentPluginEditor/agentPluginItems.js';42import {43AI_CUSTOMIZATION_ITEM_DISABLED_KEY,44AI_CUSTOMIZATION_ITEM_STORAGE_KEY,45AI_CUSTOMIZATION_ITEM_TYPE_KEY,46AI_CUSTOMIZATION_ITEM_URI_KEY,47AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID,48AI_CUSTOMIZATION_MANAGEMENT_EDITOR_INPUT_ID,49AICustomizationManagementCommands,50AICustomizationManagementItemMenuId,51AICustomizationManagementSection,52BUILTIN_STORAGE,53} from './aiCustomizationManagement.js';54import { AICustomizationManagementEditor } from './aiCustomizationManagementEditor.js';55import { AICustomizationManagementEditorInput } from './aiCustomizationManagementEditorInput.js';5657//#region Telemetry5859type CustomizationEditorDeleteItemEvent = {60promptType: string;61storage: string;62};6364type CustomizationEditorDeleteItemClassification = {65promptType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of customization being deleted.' };66storage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The storage location of the deleted item.' };67owner: 'joshspicer';68comment: 'Tracks item deletion in the Agent Customizations editor.';69};7071//#endregion7273//#region Editor Registration7475Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(76EditorPaneDescriptor.create(77AICustomizationManagementEditor,78AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID,79localize('aiCustomizationManagementEditor', "Agent Customizations Editor")80),81[82// Note: Using the class directly since we use a singleton pattern83new SyncDescriptor(AICustomizationManagementEditorInput as unknown as { new(): AICustomizationManagementEditorInput })84]85);8687//#endregion8889//#region Editor Serializer9091class AICustomizationManagementEditorInputSerializer implements IEditorSerializer {9293canSerialize(editorInput: EditorInput): boolean {94return editorInput instanceof AICustomizationManagementEditorInput;95}9697serialize(input: AICustomizationManagementEditorInput): string {98return '';99}100101deserialize(instantiationService: IInstantiationService): AICustomizationManagementEditorInput {102return AICustomizationManagementEditorInput.getOrCreate();103}104}105106Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer(107AI_CUSTOMIZATION_MANAGEMENT_EDITOR_INPUT_ID,108AICustomizationManagementEditorInputSerializer109);110111//#endregion112113//#region Context Menu Actions114115/**116* Type for context passed to actions from list context menus.117* Handles both direct URI arguments and serialized context objects.118*/119type AICustomizationContext = {120uri: URI | string;121name?: string;122promptType?: PromptsType;123storage?: PromptsStorage;124[key: string]: unknown;125} | URI | string;126127/**128* Extracts a URI from various context formats.129*/130function extractURI(context: AICustomizationContext): URI {131if (URI.isUri(context)) {132return context;133}134if (typeof context === 'string') {135return URI.parse(context);136}137if (URI.isUri(context.uri)) {138return context.uri;139}140return URI.parse(context.uri as string);141}142143/**144* Extracts storage type from context.145*/146function extractStorage(context: AICustomizationContext): PromptsStorage | undefined {147if (URI.isUri(context) || typeof context === 'string') {148return undefined;149}150return context.storage;151}152153/**154* Extracts prompt type from context.155*/156function extractPromptType(context: AICustomizationContext): PromptsType | undefined {157if (URI.isUri(context) || typeof context === 'string') {158return undefined;159}160return context.promptType;161}162163/**164* Extracts the parent plugin URI from context, if present.165*/166function extractPluginUri(context: AICustomizationContext): URI | undefined {167if (URI.isUri(context) || typeof context === 'string') {168return undefined;169}170const raw = context.pluginUri;171if (!raw) {172return undefined;173}174return URI.isUri(raw) ? raw : typeof raw === 'string' ? URI.parse(raw) : undefined;175}176177178/**179* Extracts the item ID from context (used for identifying individual hooks within a file).180*/181function extractItemId(context: AICustomizationContext): string | undefined {182if (URI.isUri(context) || typeof context === 'string') {183return undefined;184}185return typeof context.itemId === 'string' ? context.itemId : undefined;186}187188/**189* Parses a hook item ID to extract the original hook type ID and array index.190* Hook item IDs have the format: `fileUri#originalId[index]`191* Returns undefined if the ID does not match this format.192*/193function parseHookItemId(itemId: string): { originalId: string; index: number } | undefined {194const hashIndex = itemId.lastIndexOf('#');195if (hashIndex < 0) {196return undefined;197}198const fragment = itemId.substring(hashIndex + 1);199const match = /^([^[]+)\[(\d+)\]$/.exec(fragment);200if (!match) {201return undefined;202}203return { originalId: match[1], index: parseInt(match[2], 10) };204}205206// Open file action207const OPEN_AI_CUSTOMIZATION_MGMT_FILE_ID = 'aiCustomizationManagement.openFile';208registerAction2(class extends Action2 {209constructor() {210super({211id: OPEN_AI_CUSTOMIZATION_MGMT_FILE_ID,212title: localize2('open', "Open"),213icon: Codicon.goToFile,214});215}216async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {217const editorService = accessor.get(IEditorService);218const storage = extractStorage(context);219220const editorPane = await editorService.openEditor({221resource: extractURI(context)222});223224const codeEditor = getCodeEditor(editorPane?.getControl());225if (codeEditor && (storage === PromptsStorage.extension || storage === PromptsStorage.plugin)) {226codeEditor.updateOptions({227readOnly: true,228readOnlyMessage: new MarkdownString(localize('readonlyPluginFile', "This file is provided by a plugin or extension and cannot be edited.")),229});230}231}232});233234235// Run prompt action236const RUN_PROMPT_MGMT_ID = 'aiCustomizationManagement.runPrompt';237registerAction2(class extends Action2 {238constructor() {239super({240id: RUN_PROMPT_MGMT_ID,241title: localize2('runPrompt', "Run Prompt"),242icon: Codicon.play,243});244}245async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {246const commandService = accessor.get(ICommandService);247await commandService.executeCommand('workbench.action.chat.run.prompt.current', extractURI(context));248}249});250251// Reveal in Finder/Explorer action252const REVEAL_IN_OS_LABEL = isWindows253? localize2('revealInWindows', "Reveal in File Explorer")254: isMacintosh255? localize2('revealInMac', "Reveal in Finder")256: localize2('openContainer', "Open Containing Folder");257258const REVEAL_AI_CUSTOMIZATION_IN_OS_ID = 'aiCustomizationManagement.revealInOS';259registerAction2(class extends Action2 {260constructor() {261super({262id: REVEAL_AI_CUSTOMIZATION_IN_OS_ID,263title: REVEAL_IN_OS_LABEL,264icon: Codicon.folderOpened,265});266}267async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {268const commandService = accessor.get(ICommandService);269const uri = extractURI(context);270// Use existing reveal command271await commandService.executeCommand('revealFileInOS', uri);272}273});274275// Delete action276const DELETE_AI_CUSTOMIZATION_ID = 'aiCustomizationManagement.delete';277registerAction2(class extends Action2 {278constructor() {279super({280id: DELETE_AI_CUSTOMIZATION_ID,281title: localize2('delete', "Delete"),282icon: Codicon.trash,283});284}285async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {286const fileService = accessor.get(IFileService);287const dialogService = accessor.get(IDialogService);288const telemetryService = accessor.get(ITelemetryService);289const workspaceService = accessor.get(IAICustomizationWorkspaceService);290const editorService = accessor.get(IEditorService);291292const uri = extractURI(context);293const storage = extractStorage(context);294const promptType = extractPromptType(context);295const itemId = extractItemId(context);296const isSkill = promptType === PromptsType.skill;297const isHook = promptType === PromptsType.hook;298// For skills, use the parent folder name since skills are structured as <skillname>/SKILL.md.299const fileName = isSkill ? basename(dirname(uri)) : basename(uri);300301// Plugin-provided files: offer to uninstall the plugin302if (storage === PromptsStorage.plugin) {303const agentPluginService = accessor.get(IAgentPluginService);304const plugin = agentPluginService.plugins.get().find(p => isEqualOrParent(uri, p.uri));305if (plugin) {306const result = await dialogService.confirm({307message: localize('cannotDeletePluginItem', "This item is provided by the plugin '{0}'", plugin.label),308detail: localize('cannotDeletePluginItemDetail', "Individual components from a plugin cannot be removed separately. Would you like to uninstall the entire plugin?"),309primaryButton: localize('uninstallPlugin', "Uninstall Plugin"),310type: 'question',311});312if (result.confirmed) {313plugin.remove();314}315}316return;317}318319// Extension and built-in files cannot be deleted320if (storage === PromptsStorage.extension || storage === BUILTIN_STORAGE) {321await dialogService.info(322localize('cannotDeleteExtension', "Cannot Delete Extension File"),323localize('cannotDeleteExtensionDetail', "Files provided by extensions cannot be deleted. You can disable the extension if you no longer want to use this customization.")324);325return;326}327328// Confirm deletion329const hookInfo = isHook && itemId ? parseHookItemId(itemId) : undefined;330const hookName = typeof context !== 'string' && !URI.isUri(context) ? context.name : undefined;331const message = isSkill332? localize('confirmDeleteSkill', "Are you sure you want to delete skill '{0}' and its folder?", fileName)333: hookInfo && hookName334? localize('confirmDeleteHook', "Are you sure you want to delete the '{0}' hook?", hookName)335: localize('confirmDelete', "Are you sure you want to delete '{0}'?", fileName);336const confirmation = await dialogService.confirm({337message,338detail: localize('confirmDeleteDetail', "This action cannot be undone."),339primaryButton: localize('delete', "Delete"),340type: 'warning',341});342343if (confirmation.confirmed) {344try {345telemetryService.publicLog2<CustomizationEditorDeleteItemEvent, CustomizationEditorDeleteItemClassification>('chatCustomizationEditor.deleteItem', {346promptType: promptType ?? '',347storage: storage ?? '',348});349} catch {350// Telemetry must not block deletion351}352353// For hooks with a specific hook ID, remove only that entry from the file.354// Uses JSONC edits to preserve user comments and formatting.355if (hookInfo) {356try {357const content = await fileService.readFile(uri);358const text = content.value.toString();359const edits = removeProperty(text, ['hooks', hookInfo.originalId, hookInfo.index], { tabSize: 1, insertSpaces: false });360if (edits.length > 0) {361const updated = applyEdits(text, edits);362await fileService.writeFile(uri, VSBuffer.fromString(updated));363if (storage === PromptsStorage.local) {364const projectRoot = workspaceService.getActiveProjectRoot();365if (projectRoot) {366await workspaceService.commitFiles(projectRoot, [uri]);367}368}369}370} catch {371await dialogService.error(372localize('deleteHookItemFailed', "Unable to delete this hook entry because the file contents have changed."),373localize('deleteHookItemFailedDetail', "Refresh the view and try again."),374);375}376return;377}378379// For skills, delete the parent folder (e.g. .github/skills/my-skill/)380// since each skill is a folder containing SKILL.md.381const deleteTarget = isSkill ? dirname(uri) : uri;382const useTrash = fileService.hasCapability(deleteTarget, FileSystemProviderCapabilities.Trash);383await fileService.del(deleteTarget, { useTrash, recursive: isSkill });384385// Commit the deletion to git (sessions: main repo + worktree)386if (storage === PromptsStorage.local) {387const projectRoot = workspaceService.getActiveProjectRoot();388if (projectRoot) {389await workspaceService.deleteFiles(projectRoot, [deleteTarget]);390}391}392393// Refresh the list to remove the deleted item immediately394// (provider's onDidChange may not fire if it doesn't watch the filesystem)395const activeEditor = editorService.activeEditorPane;396if (activeEditor instanceof AICustomizationManagementEditor) {397activeEditor.refreshList();398}399}400}401});402403// Copy path action404const COPY_AI_CUSTOMIZATION_PATH_ID = 'aiCustomizationManagement.copyPath';405registerAction2(class extends Action2 {406constructor() {407super({408id: COPY_AI_CUSTOMIZATION_PATH_ID,409title: localize2('copyPath', "Copy Path"),410icon: Codicon.clippy,411});412}413async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {414const clipboardService = accessor.get(IClipboardService);415const uri = extractURI(context);416const textToCopy = uri.scheme === 'file' ? uri.fsPath : uri.toString(true);417await clipboardService.writeText(textToCopy);418}419});420421/**422* When clause that hides an action for read-only (extension, plugin, built-in) items.423*/424const WHEN_ITEM_IS_DELETABLE = ContextKeyExpr.and(425ContextKeyExpr.notEquals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, PromptsStorage.extension),426ContextKeyExpr.notEquals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, PromptsStorage.plugin),427ContextKeyExpr.notEquals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, BUILTIN_STORAGE),428);429430/**431* When clause that shows an action only for plugin items.432*/433const WHEN_ITEM_IS_PLUGIN = ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, PromptsStorage.plugin);434435// Register context menu items436437// Inline hover actions (shown as icon buttons on hover)438MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {439command: { id: COPY_AI_CUSTOMIZATION_PATH_ID, title: localize('copyPath', "Copy Path"), icon: Codicon.clippy },440group: 'inline',441order: 1,442});443444MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {445command: { id: DELETE_AI_CUSTOMIZATION_ID, title: localize('delete', "Delete"), icon: Codicon.trash },446group: 'inline',447order: 10,448when: WHEN_ITEM_IS_DELETABLE,449});450451// Context menu items (shown on right-click)452MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {453command: { id: OPEN_AI_CUSTOMIZATION_MGMT_FILE_ID, title: localize('open', "Open") },454group: '1_open',455order: 1,456});457458MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {459command: { id: RUN_PROMPT_MGMT_ID, title: localize('runPrompt', "Run Prompt"), icon: Codicon.play },460group: '2_run',461order: 1,462when: ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_TYPE_KEY, PromptsType.prompt),463});464465MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {466command: { id: REVEAL_AI_CUSTOMIZATION_IN_OS_ID, title: REVEAL_IN_OS_LABEL.value },467group: '3_file',468order: 1,469when: ContextKeyExpr.or(470ContextKeyExpr.regex(AI_CUSTOMIZATION_ITEM_URI_KEY, new RegExp(`^${Schemas.file}:`)),471ContextKeyExpr.regex(AI_CUSTOMIZATION_ITEM_URI_KEY, new RegExp(`^${Schemas.vscodeUserData}:`))472),473});474475MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {476command: { id: DELETE_AI_CUSTOMIZATION_ID, title: localize('delete', "Delete") },477group: '4_modify',478order: 1,479when: WHEN_ITEM_IS_DELETABLE,480});481482// Uninstall Plugin action - shown for plugin-provided items483const UNINSTALL_PLUGIN_AI_CUSTOMIZATION_ID = 'aiCustomizationManagement.uninstallPlugin';484registerAction2(class extends Action2 {485constructor() {486super({487id: UNINSTALL_PLUGIN_AI_CUSTOMIZATION_ID,488title: localize2('uninstallPlugin', "Uninstall Plugin"),489icon: Codicon.trash,490});491}492async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {493const agentPluginService = accessor.get(IAgentPluginService);494const dialogService = accessor.get(IDialogService);495496const uri = extractURI(context);497const plugin = agentPluginService.plugins.get().find(p => isEqualOrParent(uri, p.uri));498if (!plugin) {499return;500}501502const result = await dialogService.confirm({503message: localize('confirmUninstallPlugin', "This item is provided by the plugin '{0}'", plugin.label),504detail: localize('confirmUninstallPluginDetail', "Individual components from a plugin cannot be removed separately. Would you like to uninstall the entire plugin?"),505primaryButton: localize('uninstallPluginBtn', "Uninstall Plugin"),506type: 'question',507});508if (result.confirmed) {509plugin.remove();510}511}512});513514MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {515command: { id: UNINSTALL_PLUGIN_AI_CUSTOMIZATION_ID, title: localize('uninstallPlugin', "Uninstall Plugin"), icon: Codicon.trash },516group: 'inline',517order: 10,518when: WHEN_ITEM_IS_PLUGIN,519});520521MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {522command: { id: UNINSTALL_PLUGIN_AI_CUSTOMIZATION_ID, title: localize('uninstallPlugin', "Uninstall Plugin") },523group: '4_modify',524order: 1,525when: WHEN_ITEM_IS_PLUGIN,526});527528// Show Plugin action - navigates to the parent plugin detail page529const SHOW_PLUGIN_AI_CUSTOMIZATION_ID = 'aiCustomizationManagement.showPlugin';530registerAction2(class extends Action2 {531constructor() {532super({533id: SHOW_PLUGIN_AI_CUSTOMIZATION_ID,534title: localize2('showPlugin', "Show Plugin"),535});536}537async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {538const agentPluginService = accessor.get(IAgentPluginService);539const editorService = accessor.get(IEditorService);540541const pluginUri = extractPluginUri(context);542if (!pluginUri) {543return;544}545const plugin = agentPluginService.plugins.get().find(p => p.uri.toString() === pluginUri.toString());546if (!plugin) {547return;548}549550const item = {551kind: AgentPluginItemKind.Installed as const,552name: plugin.label,553description: plugin.fromMarketplace?.description ?? '',554marketplace: plugin.fromMarketplace?.marketplace,555plugin,556};557558// Try to show within the active AI Customization editor (with back navigation)559const input = AICustomizationManagementEditorInput.getOrCreate();560const pane = await editorService.openEditor(input, { pinned: true });561if (pane instanceof AICustomizationManagementEditor) {562await pane.showPluginDetail(item);563}564}565});566567MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {568command: { id: SHOW_PLUGIN_AI_CUSTOMIZATION_ID, title: localize('showPlugin', "Show Plugin") },569group: '1_open',570order: 2,571when: WHEN_ITEM_IS_PLUGIN,572});573574// Disable item action575const DISABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID = 'aiCustomizationManagement.disableItem';576registerAction2(class extends Action2 {577constructor() {578super({579id: DISABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID,580title: localize2('disable', "Disable"),581icon: Codicon.eyeClosed,582});583}584async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {585const promptsService = accessor.get(IPromptsService);586const uri = extractURI(context);587const promptType = extractPromptType(context);588if (!promptType) {589return;590}591592const disabled = promptsService.getDisabledPromptFiles(promptType);593disabled.add(uri);594promptsService.setDisabledPromptFiles(promptType, disabled);595}596});597598// Enable item action599const ENABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID = 'aiCustomizationManagement.enableItem';600registerAction2(class extends Action2 {601constructor() {602super({603id: ENABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID,604title: localize2('enable', "Enable"),605icon: Codicon.eye,606});607}608async run(accessor: ServicesAccessor, context: AICustomizationContext): Promise<void> {609const promptsService = accessor.get(IPromptsService);610const uri = extractURI(context);611const promptType = extractPromptType(context);612if (!promptType) {613return;614}615616const disabled = promptsService.getDisabledPromptFiles(promptType);617disabled.delete(uri);618promptsService.setDisabledPromptFiles(promptType, disabled);619}620});621622// Context menu: Disable (shown when builtin item is enabled)623MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {624command: { id: DISABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID, title: localize('disable', "Disable") },625group: '5_toggle',626order: 1,627when: ContextKeyExpr.and(628ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_DISABLED_KEY, false),629ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, BUILTIN_STORAGE),630ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_TYPE_KEY, PromptsType.skill),631),632});633634// Context menu: Enable (shown when builtin item is disabled)635MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {636command: { id: ENABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID, title: localize('enable', "Enable") },637group: '5_toggle',638order: 1,639when: ContextKeyExpr.and(640ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_DISABLED_KEY, true),641ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, BUILTIN_STORAGE),642ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_TYPE_KEY, PromptsType.skill),643),644});645646// Inline hover: Disable (shown when builtin item is enabled)647MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {648command: { id: DISABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID, title: localize('disable', "Disable"), icon: Codicon.eyeClosed },649group: 'inline',650order: 5,651when: ContextKeyExpr.and(652ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_DISABLED_KEY, false),653ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, BUILTIN_STORAGE),654ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_TYPE_KEY, PromptsType.skill),655),656});657658// Inline hover: Enable (shown when builtin item is disabled)659MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {660command: { id: ENABLE_AI_CUSTOMIZATION_MGMT_ITEM_ID, title: localize('enable', "Enable"), icon: Codicon.eye },661group: 'inline',662order: 5,663when: ContextKeyExpr.and(664ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_DISABLED_KEY, true),665ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, BUILTIN_STORAGE),666ContextKeyExpr.equals(AI_CUSTOMIZATION_ITEM_TYPE_KEY, PromptsType.skill),667),668});669670//#endregion671672//#region Actions673674class AICustomizationManagementActionsContribution extends Disposable implements IWorkbenchContribution {675676static readonly ID = 'workbench.contrib.aiCustomizationManagementActions';677678constructor() {679super();680this.registerActions();681}682683private registerActions(): void {684// Open AI Customizations Editor685this._register(registerAction2(class extends Action2 {686constructor() {687super({688id: AICustomizationManagementCommands.OpenEditor,689title: localize2('openAICustomizations', "Open Customizations"),690shortTitle: localize2('aiCustomizations', "Customizations"),691category: CHAT_CATEGORY,692precondition: ChatContextKeys.enabled,693f1: true,694});695}696697async run(accessor: ServicesAccessor, section?: AICustomizationManagementSection): Promise<void> {698const editorService = accessor.get(IEditorService);699const chatWidgetService = accessor.get(IChatWidgetService);700const harnessService = accessor.get(ICustomizationHarnessService);701702// Detect the active chat session type and switch the harness703// so the customization editor opens in the matching context.704const sessionResource = chatWidgetService.lastFocusedWidget?.viewModel?.sessionResource;705if (sessionResource) {706const sessionType = getChatSessionType(sessionResource);707const harness = harnessService.findHarnessById(sessionType);708if (harness) {709harnessService.setActiveHarness(sessionType);710}711}712713const input = AICustomizationManagementEditorInput.getOrCreate();714const pane = await editorService.openEditor(input, { pinned: true });715if (section && pane instanceof AICustomizationManagementEditor) {716pane.selectSectionById(section);717}718}719}));720721// Open Marketplace (hidden command for deep-linking into browse mode)722this._register(registerAction2(class extends Action2 {723constructor() {724super({725id: AICustomizationManagementCommands.OpenMarketplace,726title: localize2('openMarketplace', "Open Marketplace"),727category: CHAT_CATEGORY,728precondition: ChatContextKeys.enabled,729});730}731732async run(accessor: ServicesAccessor, section?: AICustomizationManagementSection): Promise<void> {733const editorService = accessor.get(IEditorService);734const input = AICustomizationManagementEditorInput.getOrCreate();735const pane = await editorService.openEditor(input, { pinned: true });736if (pane instanceof AICustomizationManagementEditor) {737const targetSection = section ?? AICustomizationManagementSection.McpServers;738pane.selectSectionById(targetSection, { showMarketplace: true });739}740}741}));742743// Generate Debug Report744this._register(registerAction2(class extends Action2 {745constructor() {746super({747id: AICustomizationManagementCommands.GenerateDebugReport,748title: localize2('generateDebugReport', "Generate Customization Debug Report"),749category: Categories.Developer,750precondition: ChatContextKeys.enabled,751f1: true,752});753}754755async run(accessor: ServicesAccessor): Promise<void> {756const editorService = accessor.get(IEditorService);757// Open the customizations editor if not already open758const input = AICustomizationManagementEditorInput.getOrCreate();759const pane = await editorService.openEditor(input, { pinned: true });760if (!(pane instanceof AICustomizationManagementEditor)) {761return;762}763const report = await pane.generateDebugReport();764await editorService.openEditor({765resource: undefined,766contents: report,767languageId: 'plaintext',768});769}770}));771772}773}774775registerWorkbenchContribution2(776AICustomizationManagementActionsContribution.ID,777AICustomizationManagementActionsContribution,778WorkbenchPhase.AfterRestored779);780781//#endregion782783784