add ICustomizationHarnessService.getCustomAgent#312353
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support in the chat customization harness layer for retrieving custom agents (in addition to slash commands), and threads through source-provenance fields needed to attribute customizations to extensions/plugins.
Changes:
- Extend
ICustomizationHarnessServicewithgetCustomAgents()andonDidChangeCustomAgents, and implement provider-backed + prompts-service fallback resolution. - Refactor custom agent construction in
PromptsServiceinto a shared helper (CustomAgent.fromParsedPromptFile) and simplifyIAgentSourceserialization (removetype). - Add
extensionId/pluginUriprovenance fields to customization items and update providers/tests accordingly.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/contrib/chat/common/customizationHarnessService.ts | Adds getCustomAgents() + onDidChangeCustomAgents and adapts provider items into ICustomAgents. |
| src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts | Refactors agent parsing into CustomAgent.fromParsedPromptFile and adjusts hook parsing flow. |
| src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts | Simplifies IAgentSource by removing the extension type discriminator. |
| src/vs/workbench/contrib/chat/common/chatModes.ts | Updates (de)serialization for the simplified IAgentSource. |
| src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts | Populates extensionId/pluginUri on provided customization items. |
| src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemSource.ts | Preserves provenance when expanding hook file items / emitting synthetic items. |
| src/vs/workbench/api/common/extHostChatAgents2.ts | Tightens DTO mapping with satisfies for customization items returned from extensions. |
| src/vs/workbench/api/common/extHost.protocol.ts | Minor formatting change in customization item DTO definition. |
| src/vs/workbench/api/browser/mainThreadChatAgents2.ts | Ensures customization items include the new required provenance fields (set to undefined). |
| src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts | Ensures remote agent harness items include provenance fields (set to undefined). |
| extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts | Forwards extensionId/pluginUri for Copilot CLI skill customizations. |
| extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts | Extends CLI agent info model with optional provenance fields. |
| src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts | Adds coverage for onDidChangeCustomAgents forwarding and getCustomAgents() behavior; updates item shapes for new fields. |
Copilot's findings
- Files reviewed: 13/13 changed files
- Comments generated: 5
| this._providerListeners.push(provider.onDidChange(() => this._onDidChangeSlashCommands.fire({ sessionType: harness.id }))); | ||
| this._providerListeners.push(provider.onDidChange(() => this._onDidChangeCustomAgents.fire({ sessionType: harness.id }))); |
There was a problem hiding this comment.
_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.
| 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 }); | |
| })); |
| 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; |
There was a problem hiding this comment.
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.
| 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); | |
| })); |
| 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)); |
There was a problem hiding this comment.
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.
| 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; | |
| } |
| const listener = store.add(service.onDidChangeCustomAgents(e => firedSessionType = e.sessionType)); | ||
| store.add(listener); |
There was a problem hiding this comment.
The listener disposable is already added to store via const listener = store.add(...); adding it again with store.add(listener) is redundant and can lead to double-dispose. Remove the second store.add(listener).
| const extra = { | ||
| sessionTypes: promptPath.sessionTypes, | ||
| hooks, | ||
| name: promptPath.name, | ||
| description: promptPath.description, | ||
| source: IAgentSource.fromPromptPath(promptPath) | ||
| }; | ||
| const agent = CustomAgent.fromParsedPromptFile(ast, extra); | ||
| return { status: 'loaded', promptPath: this.withPromptPathMetadata(promptPath, agent.name, agent.description), agent }; |
There was a problem hiding this comment.
computeAgentDiscoveryInfo no longer forwards the contributed when clause from promptPath.when into the resolved ICustomAgent. As a result, extension-contributed agents will lose their when gating (and may appear/enabled when they shouldn’t). Pass the original promptPath.when through (e.g. via extra.when) so CustomAgent.fromParsedPromptFile can deserialize it and preserve the condition.
No description provided.