From 8edd662c607dfd480629d5c2c57c431a436fa6b9 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Dec 2025 11:44:17 -0800 Subject: [PATCH] Custom agent provider cleanups (#280391) --- .../workbench/api/common/extHost.api.impl.ts | 1 - src/vs/workbench/api/common/extHostTypes.ts | 5 -- .../promptSyntax/service/promptsService.ts | 19 ++--- .../service/promptsServiceImpl.ts | 17 ++-- .../service/promptsService.test.ts | 81 ++++++++++++++++++- ...scode.proposed.chatParticipantPrivate.d.ts | 20 ++--- 6 files changed, 105 insertions(+), 38 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index b0dcecef9f8..ddb081274e5 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1947,7 +1947,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I McpStdioServerDefinition2: extHostTypes.McpStdioServerDefinition, McpToolAvailability: extHostTypes.McpToolAvailability, SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind, - CustomAgentTarget: extHostTypes.CustomAgentTarget, }; }; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index e73600ecae0..f41e201c1cf 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3493,11 +3493,6 @@ export enum ChatErrorLevel { Error = 2 } -export enum CustomAgentTarget { - GitHubCopilot = 'github-copilot', - VSCode = 'vscode', -} - export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage { static User(content: string | (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart)[], name?: string): LanguageModelChatMessage { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index c7023ba9e57..3413f08847e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -16,22 +16,14 @@ import { IHandOff, ParsedPromptFile } from '../promptFileParser.js'; import { ResourceSet } from '../../../../../../base/common/map.js'; /** - * Target environment for custom agents. + * Activation event for custom agent providers. */ -export enum CustomAgentTarget { - GitHubCopilot = 'github-copilot', - VSCode = 'vscode', -} +export const CUSTOM_AGENTS_PROVIDER_ACTIVATION_EVENT = 'onCustomAgentsProvider'; /** * Options for querying custom agents. */ -export interface ICustomAgentQueryOptions { - /** - * Filter agents by target environment. - */ - readonly target?: CustomAgentTarget; -} +export interface ICustomAgentQueryOptions { } /** * Represents a custom agent resource from an external provider. @@ -51,6 +43,11 @@ export interface IExternalCustomAgent { * The URI to the agent or prompt resource file. */ readonly uri: URI; + + /** + * Indicates whether the custom agent resource is editable. Defaults to false. + */ + readonly isEditable?: boolean; } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 5149ab7406f..4b3b29a0ad0 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -30,7 +30,7 @@ import { getCleanPromptName } from '../config/promptFileLocations.js'; import { PROMPT_LANGUAGE_ID, PromptsType, getPromptsTypeForLanguageId } from '../promptTypes.js'; import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; import { PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../promptFileParser.js'; -import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IClaudeSkill, IUserPromptPath, PromptsStorage, ICustomAgentQueryOptions, IExternalCustomAgent, ExtensionAgentSourceType } from './promptsService.js'; +import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IClaudeSkill, IUserPromptPath, PromptsStorage, ICustomAgentQueryOptions, IExternalCustomAgent, ExtensionAgentSourceType, CUSTOM_AGENTS_PROVIDER_ACTIVATION_EVENT } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; @@ -215,6 +215,9 @@ export class PromptsService extends Disposable implements IPromptsService { return result; } + // Activate extensions that might provide custom agents + await this.extensionService.activateByEvent(CUSTOM_AGENTS_PROVIDER_ACTIVATION_EVENT); + // Collect agents from all providers for (const providerEntry of this.customAgentsProviders) { try { @@ -224,11 +227,13 @@ export class PromptsService extends Disposable implements IPromptsService { } for (const agent of agents) { - try { - await this.filesConfigService.updateReadonly(agent.uri, true); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - this.logger.error(`[listCustomAgentsFromProvider] Failed to make agent file readonly: ${agent.uri}`, msg); + if (!agent.isEditable) { + try { + await this.filesConfigService.updateReadonly(agent.uri, true); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + this.logger.error(`[listCustomAgentsFromProvider] Failed to make agent file readonly: ${agent.uri}`, msg); + } } result.push({ diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index f9e0ef2b007..72a07f1235a 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -73,7 +73,10 @@ suite('PromptsService', () => { instaService.stub(IUserDataProfileService, new TestUserDataProfileService()); instaService.stub(ITelemetryService, NullTelemetryService); instaService.stub(IStorageService, InMemoryStorageService); - instaService.stub(IExtensionService, { whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); + instaService.stub(IExtensionService, { + whenInstalledExtensionsRegistered: () => Promise.resolve(true), + activateByEvent: () => Promise.resolve() + }); fileService = disposables.add(instaService.createInstance(FileService)); instaService.stub(IFileService, fileService); @@ -1123,6 +1126,82 @@ suite('PromptsService', () => { const actualAfterDispose = await service.getCustomAgents(CancellationToken.None); assert.strictEqual(actualAfterDispose.length, 0); }); + + test('Custom agent provider with isEditable', async () => { + const readonlyAgentUri = URI.parse('file://extensions/my-extension/readonlyAgent.agent.md'); + const editableAgentUri = URI.parse('file://extensions/my-extension/editableAgent.agent.md'); + const extension = { + identifier: { value: 'test.my-extension' }, + enabledApiProposals: ['chatParticipantPrivate'] + } as unknown as IExtensionDescription; + + // Mock the agent file content + await mockFiles(fileService, [ + { + path: readonlyAgentUri.path, + contents: [ + '---', + 'description: \'Readonly agent from provider\'', + '---', + 'I am a readonly agent.', + ] + }, + { + path: editableAgentUri.path, + contents: [ + '---', + 'description: \'Editable agent from provider\'', + '---', + 'I am an editable agent.', + ] + } + ]); + + const provider = { + provideCustomAgents: async (_options: ICustomAgentQueryOptions, _token: CancellationToken) => { + return [ + { + name: 'readonlyAgent', + description: 'Readonly agent from provider', + uri: readonlyAgentUri, + isEditable: false + }, + { + name: 'editableAgent', + description: 'Editable agent from provider', + uri: editableAgentUri, + isEditable: true + } + ]; + } + }; + + const registered = service.registerCustomAgentsProvider(extension, provider); + + // Spy on updateReadonly to verify it's called correctly + const filesConfigService = instaService.get(IFilesConfigurationService); + const updateReadonlySpy = sinon.spy(filesConfigService, 'updateReadonly'); + + // List prompt files to trigger the readonly check + await service.listPromptFiles(PromptsType.agent, CancellationToken.None); + + // Verify updateReadonly was called only for the non-editable agent + assert.strictEqual(updateReadonlySpy.callCount, 1, 'updateReadonly should be called once'); + assert.ok(updateReadonlySpy.calledWith(readonlyAgentUri, true), 'updateReadonly should be called with readonly agent URI and true'); + + const actual = await service.getCustomAgents(CancellationToken.None); + assert.strictEqual(actual.length, 2); + + const readonlyAgent = actual.find(a => a.name === 'readonlyAgent'); + const editableAgent = actual.find(a => a.name === 'editableAgent'); + + assert.ok(readonlyAgent, 'Readonly agent should be found'); + assert.ok(editableAgent, 'Editable agent should be found'); + assert.strictEqual(readonlyAgent!.description, 'Readonly agent from provider'); + assert.strictEqual(editableAgent!.description, 'Editable agent from provider'); + + registered.dispose(); + }); }); suite('findClaudeSkills', () => { diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index a2b1f7cc32d..c4a40a62c00 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -340,25 +340,17 @@ declare module 'vscode' { * The URI to the agent or prompt resource file. */ readonly uri: Uri; - } - /** - * Target environment for custom agents. - */ - export enum CustomAgentTarget { - GitHubCopilot = 'github-copilot', - VSCode = 'vscode', + /** + * Indicates whether the custom agent resource is editable. Defaults to false. + */ + readonly isEditable?: boolean; } /** * Options for querying custom agents. */ - export interface CustomAgentQueryOptions { - /** - * Filter agents by target environment. - */ - readonly target?: CustomAgentTarget; - } + export interface CustomAgentQueryOptions { } /** * A provider that supplies custom agent resources (from .agent.md and .prompt.md files) for repositories. @@ -367,7 +359,7 @@ declare module 'vscode' { /** * An optional event to signal that custom agents have changed. */ - onDidChangeCustomAgents?: Event; + readonly onDidChangeCustomAgents?: Event; /** * Provide the list of custom agent resources available for a given repository.