Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ export interface CLIAgentInfo {
readonly agent: Readonly<SweCustomAgent>;
/** File URI for prompt-file agents, synthetic `copilotcli:` URI for SDK-only agents. */
readonly sourceUri: URI;
readonly extensionId?: string;
readonly pluginUri?: URI;
}

export interface ICopilotCLIAgents {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod
uri: s.uri,
type: vscode.ChatSessionCustomizationType.Skill,
name: s.name,
extensionId: s.extensionId,
pluginUri: s.pluginUri,
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements
status: toStatusString(sc.status),
statusMessage: sc.statusMessage,
enabled: sc.enabled,
extensionId: undefined,
pluginUri: undefined
}));
}

Expand All @@ -99,6 +101,8 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements
type: 'plugin',
name: ref.displayName,
description: ref.description,
extensionId: undefined,
pluginUri: undefined
}));
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/browser/mainThreadChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
groupKey: item.groupKey,
badge: item.badge,
badgeTooltip: item.badgeTooltip,
extensionId: undefined,
pluginUri: undefined
}));
},
};
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1738,6 +1738,7 @@ export interface IChatSessionCustomizationItemDto {
readonly description?: string;
readonly groupKey?: string;
readonly badge?: string;

readonly badgeTooltip?: string;
}
export interface IChatParticipantMetadata {
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/common/extHostChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -830,8 +830,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
description: item.description,
groupKey: item.groupKey,
badge: item.badge,
badgeTooltip: item.badgeTooltip,
}));
badgeTooltip: item.badgeTooltip
} satisfies IChatSessionCustomizationItemDto));
} catch (err) {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ export async function expandHookFileItems(
enabled: item.enabled,
groupKey: item.groupKey,
storage: item.storage,
extensionId: item.extensionId,
pluginUri: item.pluginUri
});
}
}
Expand Down Expand Up @@ -449,6 +451,8 @@ export class ProviderCustomizationItemSource implements IAICustomizationItemSour
enabled: !disabledPromptFiles.has(p.uri),
badge: uiTooltip ? uiIntegrationBadge : undefined,
badgeTooltip: uiTooltip,
extensionId: undefined,
pluginUri: undefined
};
appended.push(this.itemNormalizer.normalizeItem(builtinItem, promptType, uriUseCounts));
}
Expand Down Expand Up @@ -484,6 +488,8 @@ export class ProviderCustomizationItemSource implements IAICustomizationItemSour
name: getFriendlyName(basename(file.uri)),
groupKey: 'sync-local',
enabled: true,
extensionId: undefined,
pluginUri: undefined
}));

return this.itemNormalizer.normalizeItems(providerItems, promptType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt
description: agent.description,
storage: agent.source.storage,
enabled: !disabledUris.has(agent.uri),
extensionId: agent.source.storage === PromptsStorage.extension ? agent.source.extensionId.value : undefined,
pluginUri: agent.source.storage === PromptsStorage.plugin ? agent.source.pluginUri : undefined
});
if (agent.source.storage === PromptsStorage.extension && !extensionInfoByUri.has(agent.uri)) {
extensionInfoByUri.set(agent.uri, { id: agent.source.extensionId });
Expand Down Expand Up @@ -104,6 +106,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt
enabled: true,
badge: uiTooltip ? localize('uiIntegrationBadge', "UI Integration") : undefined,
badgeTooltip: uiTooltip,
extensionId: skill.extension?.identifier.value,
pluginUri: skill.pluginUri
});
}
if (disabledUris.size > 0) {
Expand All @@ -121,6 +125,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt
enabled: false,
badge: uiTooltip ? localize('uiIntegrationBadge', "UI Integration") : undefined,
badgeTooltip: uiTooltip,
extensionId: file.extension?.identifier.value,
pluginUri: file.pluginUri
});
}
}
Expand All @@ -138,6 +144,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt
description: command.description,
storage: command.storage,
enabled: !disabledUris.has(command.uri),
extensionId: command.extension?.identifier.value,
pluginUri: command.pluginUri
});
if (command.extension) {
extensionInfoByUri.set(command.uri, { id: command.extension.identifier, displayName: command.extension.displayName });
Expand Down Expand Up @@ -166,6 +174,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt
name: f.name || getFriendlyName(basename(f.uri)),
storage: f.storage,
enabled: !disabledUris.has(f.uri),
extensionId: f.extension?.identifier.value,
pluginUri: f.pluginUri
});
}

Expand Down Expand Up @@ -193,6 +203,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt
storage: agent.source.storage,
groupKey: 'agents',
enabled: !disabledUris.has(agent.uri),
extensionId: agent.source.storage === PromptsStorage.extension ? agent.source.extensionId.value : undefined,
pluginUri: agent.source.storage === PromptsStorage.plugin ? agent.source.pluginUri : undefined
});
}
}
Expand All @@ -219,10 +231,12 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt
storage,
groupKey: 'agent-instructions',
enabled: !disabledUris.has(file.uri),
extensionId: undefined,
pluginUri: undefined
});
}

for (const { uri, pattern, name, description, storage } of instructionFiles) {
for (const { uri, pattern, name, description, storage, extension, pluginUri } of instructionFiles) {
if (agentInstructionUris.has(uri)) {
continue;
}
Expand All @@ -246,6 +260,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt
storage,
groupKey: 'context-instructions',
enabled: !disabledUris.has(uri),
extensionId: extension?.identifier.value,
pluginUri
});
} else {
items.push({
Expand All @@ -256,6 +272,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt
storage,
groupKey: 'on-demand-instructions',
enabled: !disabledUris.has(uri),
extensionId: extension?.identifier.value,
pluginUri
});
}
}
Expand Down
11 changes: 2 additions & 9 deletions src/vs/workbench/contrib/chat/common/chatModes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ function serializeChatModeSource(source: IAgentSource | undefined): IChatModeSou
return undefined;
}
if (source.storage === PromptsStorage.extension) {
return { storage: PromptsStorage.extension, extensionId: source.extensionId.value, type: source.type };
return { storage: PromptsStorage.extension, extensionId: source.extensionId.value };
}
if (source.storage === PromptsStorage.plugin) {
return { storage: PromptsStorage.plugin, pluginUri: source.pluginUri };
Expand All @@ -507,14 +507,7 @@ function reviveChatModeSource(data: IChatModeSourceData | undefined): IAgentSour
return undefined;
}
if (data.storage === PromptsStorage.extension) {
// Migrate old ExtensionAgentSourceType values ('contribution'/'provider') to PromptFileSource values
let type: PromptFileSource.ExtensionContribution | PromptFileSource.ExtensionAPI;
if (data.type === 'provider' as string /* old type value */ || data.type === PromptFileSource.ExtensionAPI) {
type = PromptFileSource.ExtensionAPI;
} else {
type = PromptFileSource.ExtensionContribution;
}
return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId), type };
return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId) };
}
if (data.storage === PromptsStorage.plugin) {
return { storage: PromptsStorage.plugin, pluginUri: URI.revive(data.pluginUri) };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
import { AICustomizationManagementSection, IStorageSourceFilter } from './aiCustomizationWorkspaceService.js';
import { PromptsType } from './promptSyntax/promptTypes.js';
import { AGENT_MD_FILENAME } from './promptSyntax/config/promptFileLocations.js';
import { IChatPromptSlashCommand, IPromptsService, IResolvedChatPromptSlashCommand, matchesSessionType, PromptsStorage } from './promptSyntax/service/promptsService.js';
import { IAgentSource, IChatPromptSlashCommand, ICustomAgent, IPromptsService, IResolvedChatPromptSlashCommand, matchesSessionType, PromptsStorage } from './promptSyntax/service/promptsService.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { SessionType } from './chatSessionsService.js';
import { CustomAgent } from './promptSyntax/service/promptsServiceImpl.js';
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';

export const ICustomizationHarnessService = createDecorator<ICustomizationHarnessService>('customizationHarnessService');

Expand Down Expand Up @@ -146,6 +148,10 @@ export interface ICustomizationItem {
readonly storage?: PromptsStorage;
/** Display name of the contributing extension (e.g. "GitHub Copilot Chat"). */
readonly extensionLabel?: string;
/** The extension identifier that contributed this customization, if any. */
readonly extensionId: string | undefined;
/** The URI of the plugin that contributed this customization, if any. */
readonly pluginUri: URI | undefined;
/** Server-reported loading status for this customization. */
readonly status?: 'loading' | 'loaded' | 'degraded' | 'error';
/** Human-readable status detail (e.g. error message or warning). */
Expand Down Expand Up @@ -265,13 +271,25 @@ export interface ICustomizationHarnessService {
*/
readonly onDidChangeSlashCommands: Event<{ readonly sessionType: string }>;

/**
* Fires when one of the provided custom agents changes.
*/
readonly onDidChangeCustomAgents: Event<{ readonly sessionType: string }>;

/**
* Returns the prompt and skill slash commands for the given session type.
* Provider-backed harnesses contribute their own items directly; the default
* VS Code harness falls back to the core prompts service.
*/
getSlashCommands(sessionType: string, token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]>;

/**
* Returns the custom agents for the given session type.
* Provider-backed harnesses select items via their own provider and resolve
* details via the core prompts service.
*/
getCustomAgents(sessionType: string, token: CancellationToken): Promise<readonly ICustomAgent[]>;

/**
* Resolves a slash command to its full metadata, including the parsed prompt file for prompt commands.
* Provider-backed harnesses resolve their own items directly; the default VS Code harness falls back to the core prompts service.
Expand Down Expand Up @@ -481,7 +499,10 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer
declare readonly _serviceBrand: undefined;
private readonly _onDidChangeSlashCommands = new Emitter<{ readonly sessionType: string }>();
readonly onDidChangeSlashCommands = this._onDidChangeSlashCommands.event;
private readonly _onDidChangeCustomAgents = new Emitter<{ readonly sessionType: string }>();
readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event;
private readonly _providerListeners: IDisposable[] = [];
private _isDisposed = false;

private readonly _activeHarness: ISettableObservable<string>;
readonly activeHarness: IObservable<string>;
Expand Down Expand Up @@ -516,6 +537,9 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer
}

private _refreshAvailableHarnesses(): void {
if (this._isDisposed) {
return;
}
this._availableHarnesses.set(this._getAllHarnesses(), undefined);
this._rebindProviderListeners();
}
Expand All @@ -529,25 +553,32 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer
const provider = harness.itemProvider;
if (!provider) {
this._providerListeners.push(this.promptsService.onDidChangeSlashCommands(() => this._onDidChangeSlashCommands.fire({ sessionType: harness.id })));
this._providerListeners.push(this.promptsService.onDidChangeCustomAgents(() => this._onDidChangeCustomAgents.fire({ sessionType: harness.id })));
} else {
this._providerListeners.push(provider.onDidChange(() => this._onDidChangeSlashCommands.fire({ sessionType: harness.id })));
this._providerListeners.push(provider.onDidChange(() => this._onDidChangeCustomAgents.fire({ sessionType: harness.id })));
Comment on lines 558 to +559
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

_rebindProviderListeners subscribes to provider.onDidChange twice per harness (once for slash commands and once for custom agents). This duplicates listener registrations/disposals and can add avoidable overhead. Consider using a single onDidChange subscription per provider that fires both _onDidChangeSlashCommands and _onDidChangeCustomAgents for the harness id.

Suggested change
this._providerListeners.push(provider.onDidChange(() => this._onDidChangeSlashCommands.fire({ sessionType: harness.id })));
this._providerListeners.push(provider.onDidChange(() => this._onDidChangeCustomAgents.fire({ sessionType: harness.id })));
this._providerListeners.push(provider.onDidChange(() => {
this._onDidChangeSlashCommands.fire({ sessionType: harness.id });
this._onDidChangeCustomAgents.fire({ sessionType: harness.id });
}));

Copilot uses AI. Check for mistakes.
}
}
}

dispose(): void {
this._isDisposed = true;
for (const listener of this._providerListeners) {
listener.dispose();
}
this._providerListeners.length = 0;
this._onDidChangeSlashCommands.dispose();
this._onDidChangeCustomAgents.dispose();
}

registerExternalHarness(descriptor: IHarnessDescriptor): IDisposable {
this._externalHarnesses.push(descriptor);
this._refreshAvailableHarnesses();
return {
dispose: () => {
if (this._isDisposed) {
return;
}
const idx = this._externalHarnesses.indexOf(descriptor);
if (idx >= 0) {
this._externalHarnesses.splice(idx, 1);
Expand Down Expand Up @@ -624,6 +655,47 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer
return result;
}

async getCustomAgents(sessionType: string, token: CancellationToken): Promise<readonly ICustomAgent[]> {
const harness = this.findHarnessById(sessionType);
if (!harness || !harness.itemProvider) {
const allAgents = await this.promptsService.getCustomAgents(token);
return allAgents.filter(agent => matchesSessionType(agent.sessionTypes, sessionType));
}

const items = await harness.itemProvider.provideChatSessionCustomizations(token);
if (!items) {
return [];
}

const getSource = (item: ICustomizationItem): IAgentSource => {
if (item.storage === PromptsStorage.extension && item.extensionId) {
return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(item.extensionId) };
} else if (item.storage === PromptsStorage.plugin && item.pluginUri) {
return { storage: PromptsStorage.plugin, pluginUri: item.pluginUri };
} else if (item.storage === PromptsStorage.user) {
return { storage: PromptsStorage.user };
}
return { storage: PromptsStorage.local };
};

const result: ICustomAgent[] = [];
for (const item of items) {
if ((item.enabled !== false) && item.type === PromptsType.agent) {
const promptFile = await this.promptsService.parseNew(item.uri, token);
const extra = {
name: item.name,
description: item.description,
sessionTypes: [sessionType],
hooks: undefined,
source: getSource(item),
type: PromptsType.agent,
};
result.push(CustomAgent.fromParsedPromptFile(promptFile, extra));
Comment on lines +684 to +693
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

getCustomAgents will reject the entire call if parseNew throws for any single provider item URI (missing file, parse error, etc.). Since these URIs come from an external provider, one bad entry could break the whole agents list. Consider handling errors per item (skip and optionally surface a status) so other valid agents still load.

Suggested change
const promptFile = await this.promptsService.parseNew(item.uri, token);
const extra = {
name: item.name,
description: item.description,
sessionTypes: [sessionType],
hooks: undefined,
source: getSource(item),
type: PromptsType.agent,
};
result.push(CustomAgent.fromParsedPromptFile(promptFile, extra));
try {
const promptFile = await this.promptsService.parseNew(item.uri, token);
const extra = {
name: item.name,
description: item.description,
sessionTypes: [sessionType],
hooks: undefined,
source: getSource(item),
type: PromptsType.agent,
};
result.push(CustomAgent.fromParsedPromptFile(promptFile, extra));
} catch {
continue;
}

Copilot uses AI. Check for mistakes.
}
}
return result;
Comment on lines +681 to +696
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

getCustomAgents parses provider items sequentially with await inside a loop. If a harness provides many agent items, this will unnecessarily serialize I/O and slow down the UI. Consider resolving agents in parallel (e.g. Promise.all) while still honoring cancellation, and preserving the original item ordering if needed.

Suggested change
const result: ICustomAgent[] = [];
for (const item of items) {
if ((item.enabled !== false) && item.type === PromptsType.agent) {
const promptFile = await this.promptsService.parseNew(item.uri, token);
const extra = {
name: item.name,
description: item.description,
sessionTypes: [sessionType],
hooks: undefined,
source: getSource(item),
type: PromptsType.agent,
};
result.push(CustomAgent.fromParsedPromptFile(promptFile, extra));
}
}
return result;
const agentItems = items.filter(item => (item.enabled !== false) && item.type === PromptsType.agent);
return Promise.all(agentItems.map(async item => {
const promptFile = await this.promptsService.parseNew(item.uri, token);
const extra = {
name: item.name,
description: item.description,
sessionTypes: [sessionType],
hooks: undefined,
source: getSource(item),
type: PromptsType.agent,
};
return CustomAgent.fromParsedPromptFile(promptFile, extra);
}));

Copilot uses AI. Check for mistakes.
}

public async resolvePromptSlashCommand(name: string, sessionType: string, token: CancellationToken): Promise<IResolvedChatPromptSlashCommand | undefined> {
const harness = this.findHarnessById(sessionType);
if (!harness || !harness.itemProvider) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ export interface IPluginPromptPath extends IPromptPathBase {
export type IAgentSource = {
readonly storage: PromptsStorage.extension;
readonly extensionId: ExtensionIdentifier;
readonly type: PromptFileSource.ExtensionContribution | PromptFileSource.ExtensionAPI;
} | {
readonly storage: PromptsStorage.local | PromptsStorage.user;
} | {
Expand Down
Loading
Loading