Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 88 additions & 12 deletions src/vs/workbench/api/browser/mainThreadChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -109,6 +111,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
private readonly _customizationProviders = this._register(new DisposableMap<number, IDisposable>());
private readonly _customizationProviderEmitters = this._register(new DisposableMap<number, Emitter<void>>());

/** Stored provider results for debug event resolution. */
private readonly _customizationDebugDetails = new Map<string, { items: IExternalCustomizationItem[]; durationInMillis: number }>();

private readonly _pendingProgress = new Map<string, { progress: (parts: IChatProgress[]) => void; chatSession: IChatModel | undefined }>();
private readonly _proxy: ExtHostChatAgentsShape2;

Expand All @@ -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')) {
Expand Down Expand Up @@ -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;
Comment on lines +635 to +637
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timing is measured with Date.now() but later formatted with toFixed(1), which will always yield *.0ms and is sensitive to system clock changes. Prefer StopWatch.create(true) (or performance.now) for high-resolution, monotonic timing so the debug logs show accurate durations.

Copilot uses AI. Check for mistakes.
if (!items) {
return undefined;
Comment on lines +635 to 639
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This early return means discovery events aren’t logged when a provider returns undefined (unavailable) even though the PR description says each provideChatSessionCustomizations call is logged. Consider logging a 0/"unavailable" result (and duration) before returning so the debug log shows calls consistently.

Copilot uses AI. Check for mistakes.
}
return items.map((item: IChatSessionCustomizationItemDto): IExternalCustomizationItem => ({
const mapped = items.map((item: IChatSessionCustomizationItemDto): IExternalCustomizationItem => ({
uri: URI.revive(item.uri),
type: item.type,
name: item.name,
Expand All @@ -631,20 +647,27 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
badge: item.badge,
badgeTooltip: item.badgeTooltip,
}));
this._logCustomizationDiscovery(metadata.label, mapped, durationInMillis);
return mapped;
},
};

// 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;
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<string, string> = {
'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,
Expand Down Expand Up @@ -674,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<string, number>();
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`;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a provider returns an empty array, typeSummary becomes an empty string and the log renders as 0 items () .... Handle the empty case to avoid the stray parentheses (and consider omitting the breakdown when there are no items).

Suggested change
const details = `${items.length} items (${typeSummary}) in ${durationInMillis.toFixed(1)}ms`;
const details = typeSummary
? `${items.length} items (${typeSummary}) in ${durationInMillis.toFixed(1)}ms`
: `${items.length} items in ${durationInMillis.toFixed(1)}ms`;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug

@Agent``


this._chatDebugService.log(
sessionResource,
`Customization Provider (${label})`,
details,
ChatDebugLogLevel.Info,
{ id: eventId, category: 'discovery' },
Comment on lines +726 to +731
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug event title/details strings here are user-visible in the Agent Debug Logs UI but are not localized. Other discovery/debug log entries (e.g. prompt discovery) use localize(...); please externalize these strings as well and avoid manual pluralization in the raw template string.

Copilot uses AI. Check for mistakes.
);
}

private _resolveCustomizationDebugEvent(eventId: string): IChatDebugResolvedEventContent | undefined {
const data = this._customizationDebugDetails.get(eventId);
if (!data) {
return undefined;
}

return {
kind: 'fileList',
discoveryType: 'customization-provider',
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discoveryType: 'customization-provider' will render in the discovery UI as "Customization-provider" (simple first-letter capitalization) and won’t map to any known discovery settings key. If this should appear as a first-class discovery type, consider either using a renderer-friendly identifier (or adding a special-case label/settings mapping in the discovery renderer) so the UI output is readable and consistent.

Suggested change
discoveryType: 'customization-provider',
discoveryType: 'customizationProvider',

Copilot uses AI. Check for mistakes.
durationInMillis: data.durationInMillis,
files: data.items.map(item => ({
uri: item.uri,
name: item.name,
status: 'loaded' as const,
storage: item.groupKey,
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

storage in IChatDebugFileEntry is meant to represent the storage/source label (and is used for grouping in the discovery renderer). Setting it to item.groupKey is misleading because groupKey is just a UI grouping hint for the customizations editor. Consider omitting storage here (or populating extensionId / a real source label) and, if you want to surface groupKey, include it in the file name/details instead.

Suggested change
storage: item.groupKey,

Copilot uses AI. Check for mistakes.
})),
};
}
}


Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/common/extHostChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/common/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -56,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[];
}

/**
Expand Down
Loading