From 8cc15b6043e844f7132adb1667e1d5786ccdfdde Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:23:11 -0700 Subject: [PATCH 1/3] chat: add Plugins to ChatSessionCustomizationType Adds a static Plugins instance so extensions can use ChatSessionCustomizationType.Plugins instead of constructing new ChatSessionCustomizationType('plugins') manually. Also adds explicit mapping in mainThread section conversion. --- src/vs/workbench/api/browser/mainThreadChatAgents2.ts | 1 + src/vs/workbench/api/common/extHostTypes.ts | 1 + .../vscode.proposed.chatSessionCustomizationProvider.d.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index a68b3c38e3dd1..63e4a83082d06 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -642,6 +642,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA case 'instructions': return AICustomizationManagementSection.Instructions; case 'prompt': return AICustomizationManagementSection.Prompts; case 'hook': return AICustomizationManagementSection.Hooks; + case 'plugins': return AICustomizationManagementSection.Plugins; default: return type; } }); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 5eb7131d80102..6b013cfa074d2 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3574,6 +3574,7 @@ export class ChatSessionCustomizationType { static readonly Instructions = new ChatSessionCustomizationType('instructions'); static readonly Prompt = new ChatSessionCustomizationType('prompt'); static readonly Hook = new ChatSessionCustomizationType('hook'); + static readonly Plugins = new ChatSessionCustomizationType('plugins'); constructor(public readonly id: string) { } } diff --git a/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts index b55537066e144..3c9becbaccf16 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts @@ -25,6 +25,8 @@ declare module 'vscode' { static readonly Prompt: ChatSessionCustomizationType; /** Hook customization (event-driven automation). */ static readonly Hook: ChatSessionCustomizationType; + /** Plugin customization (agent runtime plugins). */ + static readonly Plugins: ChatSessionCustomizationType; /** * The string identifier for this customization type. From aa8b6b5af06c5da9f909fd8f325fa7c32c84c96d Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:27:23 -0700 Subject: [PATCH 2/3] chat: rename unsupportedTypes to supportedTypes in customization provider metadata Inverts the semantics from a blacklist (types to hide) to a whitelist (types to show). More natural API: providers declare what they support rather than what they don't. When omitted, all sections are shown. --- .../api/browser/mainThreadChatAgents2.ts | 28 +++++++++++-------- .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/common/extHostChatAgents2.ts | 2 +- ...osed.chatSessionCustomizationProvider.d.ts | 8 +++--- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 63e4a83082d06..950231d956ac3 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -634,18 +634,22 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA }, }; - // Convert metadata to a harness descriptor - const hiddenSections = metadata.unsupportedTypes?.map(type => { - switch (type) { - case 'agent': return AICustomizationManagementSection.Agents; - case 'skill': return AICustomizationManagementSection.Skills; - case 'instructions': return AICustomizationManagementSection.Instructions; - case 'prompt': return AICustomizationManagementSection.Prompts; - case 'hook': return AICustomizationManagementSection.Hooks; - case 'plugins': return AICustomizationManagementSection.Plugins; - default: return type; - } - }); + // Convert supportedTypes whitelist to hiddenSections blacklist. + // Sections not in the supported list are hidden. When supportedTypes + // is omitted, all sections are shown. + const typeToSection: Record = { + 'agent': AICustomizationManagementSection.Agents, + 'skill': AICustomizationManagementSection.Skills, + 'instructions': AICustomizationManagementSection.Instructions, + 'prompt': AICustomizationManagementSection.Prompts, + 'hook': AICustomizationManagementSection.Hooks, + 'plugins': AICustomizationManagementSection.Plugins, + }; + let hiddenSections: string[] | undefined; + if (metadata.supportedTypes) { + const supportedSections = new Set(metadata.supportedTypes.map(t => typeToSection[t]).filter(Boolean)); + hiddenSections = Object.values(typeToSection).filter(section => !supportedSections.has(section)); + } const descriptor: IHarnessDescriptor = { id: chatSessionType, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4ced8585290a4..16d158aad71b9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1681,7 +1681,7 @@ export interface ISkillDto { export interface IChatSessionCustomizationProviderMetadataDto { readonly label: string; readonly iconId?: string; - readonly unsupportedTypes?: readonly string[]; + readonly supportedTypes?: readonly string[]; } export interface IChatSessionCustomizationItemDto { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index f6d282ea53cf8..8f5b62a9f12cd 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -671,7 +671,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const metadataDto: IChatSessionCustomizationProviderMetadataDto = { label: metadata.label, iconId: metadata.iconId, - unsupportedTypes: metadata.unsupportedTypes?.map(t => typeConvert.ChatSessionCustomizationType.from(t)), + supportedTypes: metadata.supportedTypes?.map(t => typeConvert.ChatSessionCustomizationType.from(t)), }; this._proxy.$registerChatSessionCustomizationProvider(handle, chatSessionType, metadataDto, extension.identifier); diff --git a/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts index 3c9becbaccf16..33027458d2c7c 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts @@ -58,11 +58,11 @@ declare module 'vscode' { readonly iconId?: string; /** - * Customization types that this provider does **not** support. - * The corresponding sections will be hidden in the management UI - * when this provider is active. + * Customization types that this provider supports. + * Only the corresponding sections will be shown in the management UI + * when this provider is active. When omitted, all sections are shown. */ - readonly unsupportedTypes?: readonly ChatSessionCustomizationType[]; + readonly supportedTypes?: readonly ChatSessionCustomizationType[]; } /** From bba778cd022d1d8c78ae870d6d599116e0a6e494 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:07:55 -0700 Subject: [PATCH 3/3] chat: log customization provider results to Agent Debug Logs Adds debug event logging for extension-contributed customization providers, following the same pattern as PromptsDebugContribution: - Logs each provideChatSessionCustomizations call with item count, type breakdown, and timing - Registers a resolve provider for expandable file list details - Uses category 'discovery' so events are filtered by the existing 'Chat Customization' toggle in Agent Debug Logs --- .../api/browser/mainThreadChatAgents2.ts | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 950231d956ac3..49c017ad7bf08 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -6,6 +6,7 @@ import { DeferredPromise } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../base/common/event.js'; +import { generateUuid } from '../../../base/common/uuid.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { Disposable, DisposableMap, IDisposable } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; @@ -47,6 +48,7 @@ import { isUntitledChatSession } from '../../contrib/chat/common/model/chatUri.j import { ICustomizationHarnessService, IExternalCustomizationItem, IExternalCustomizationItemProvider, IHarnessDescriptor } from '../../contrib/chat/common/customizationHarnessService.js'; import { AICustomizationManagementSection } from '../../contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; +import { IChatDebugService, IChatDebugResolvedEventContent, ChatDebugLogLevel } from '../../contrib/chat/common/chatDebugService.js'; interface AgentData { dispose: () => void; @@ -109,6 +111,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA private readonly _customizationProviders = this._register(new DisposableMap()); private readonly _customizationProviderEmitters = this._register(new DisposableMap>()); + /** Stored provider results for debug event resolution. */ + private readonly _customizationDebugDetails = new Map(); + private readonly _pendingProgress = new Map void; chatSession: IChatModel | undefined }>(); private readonly _proxy: ExtHostChatAgentsShape2; @@ -131,10 +136,19 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, @ICustomizationHarnessService private readonly _customizationHarnessService: ICustomizationHarnessService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @IChatDebugService private readonly _chatDebugService: IChatDebugService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); + // Register a resolve provider for customization discovery debug events. + this._register(this._chatDebugService.registerProvider({ + provideChatDebugLog: async () => undefined, + resolveChatDebugLogEvent: async (eventId) => { + return this._resolveCustomizationDebugEvent(eventId); + } + })); + // When the provider API kill-switch is toggled off, dispose all registered providers this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('chat.customizations.providerApi.enabled')) { @@ -618,11 +632,13 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA const itemProvider: IExternalCustomizationItemProvider = { onDidChange: emitter.event, provideChatSessionCustomizations: async (token) => { + const start = Date.now(); const items = await this._proxy.$provideChatSessionCustomizations(handle, token); + const durationInMillis = Date.now() - start; if (!items) { return undefined; } - return items.map((item: IChatSessionCustomizationItemDto): IExternalCustomizationItem => ({ + const mapped = items.map((item: IChatSessionCustomizationItemDto): IExternalCustomizationItem => ({ uri: URI.revive(item.uri), type: item.type, name: item.name, @@ -631,6 +647,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA badge: item.badge, badgeTooltip: item.badgeTooltip, })); + this._logCustomizationDiscovery(metadata.label, mapped, durationInMillis); + return mapped; }, }; @@ -679,6 +697,59 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA emitter.fire(); } } + + private _logCustomizationDiscovery(label: string, items: IExternalCustomizationItem[], durationInMillis: number): void { + const sessionResource = this._chatDebugService.activeSessionResource; + if (!sessionResource) { + return; + } + + const eventId = generateUuid(); + this._customizationDebugDetails.set(eventId, { items, durationInMillis }); + + // Evict oldest entries when the map grows too large. + if (this._customizationDebugDetails.size > 10_000) { + const first = this._customizationDebugDetails.keys().next().value; + if (first !== undefined) { + this._customizationDebugDetails.delete(first); + } + } + + // Group items by type for a concise summary. + const byType = new Map(); + for (const item of items) { + byType.set(item.type, (byType.get(item.type) ?? 0) + 1); + } + const typeSummary = [...byType.entries()].map(([type, count]) => `${count} ${type}`).join(', '); + const details = `${items.length} items (${typeSummary}) in ${durationInMillis.toFixed(1)}ms`; + + this._chatDebugService.log( + sessionResource, + `Customization Provider (${label})`, + details, + ChatDebugLogLevel.Info, + { id: eventId, category: 'discovery' }, + ); + } + + private _resolveCustomizationDebugEvent(eventId: string): IChatDebugResolvedEventContent | undefined { + const data = this._customizationDebugDetails.get(eventId); + if (!data) { + return undefined; + } + + return { + kind: 'fileList', + discoveryType: 'customization-provider', + durationInMillis: data.durationInMillis, + files: data.items.map(item => ({ + uri: item.uri, + name: item.name, + status: 'loaded' as const, + storage: item.groupKey, + })), + }; + } }