From 57e647cb3a3d0557c0b5745a9d3dd419bb107569 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Mon, 1 Dec 2025 11:59:41 -0800 Subject: [PATCH 01/27] wip --- .../api/browser/mainThreadChatAgents2.ts | 44 ++++++++++++++++- .../workbench/api/common/extHost.api.impl.ts | 4 ++ .../workbench/api/common/extHost.protocol.ts | 6 ++- .../api/common/extHostChatAgents2.ts | 35 +++++++++++++- .../promptSyntax/service/promptsService.ts | 47 +++++++++++++++++++ .../service/promptsServiceImpl.ts | 44 ++++++++++++++++- .../chat/test/common/mockPromptsService.ts | 3 +- ...scode.proposed.chatParticipantPrivate.d.ts | 34 ++++++++++++++ 8 files changed, 212 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index ecaa0cc50aaab..8a67e14587ac6 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -26,7 +26,7 @@ import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIde import { IChatWidgetService } from '../../contrib/chat/browser/chat.js'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../contrib/chat/browser/contrib/chatDynamicVariables.js'; import { IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../contrib/chat/common/chatAgents.js'; -import { ICustomAgentQueryOptions, IPromptsService } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { ICustomAgentQueryOptions, IPromptsService, IInstructionQueryOptions } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/chatEditingService.js'; import { IChatModel } from '../../contrib/chat/common/chatModel.js'; import { ChatRequestAgentPart } from '../../contrib/chat/common/chatParserTypes.js'; @@ -98,6 +98,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA private readonly _customAgentsProviders = this._register(new DisposableMap()); private readonly _customAgentsProviderEmitters = this._register(new DisposableMap>()); + private readonly _instructionsProviders = this._register(new DisposableMap()); + private readonly _instructionsProviderEmitters = this._register(new DisposableMap>()); private readonly _pendingProgress = new Map void; chatSession: IChatModel | undefined }>(); private readonly _proxy: ExtHostChatAgentsShape2; @@ -468,6 +470,46 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA emitter.fire(); } } + + async $registerInstructionsProvider(handle: number, extensionId: ExtensionIdentifier): Promise { + const extension = await this._extensionService.getExtension(extensionId.value); + if (!extension) { + this._logService.error(`[MainThreadChatAgents2] Could not find extension for InstructionsProvider: ${extensionId.value}`); + return; + } + + const emitter = new Emitter(); + this._instructionsProviderEmitters.set(handle, emitter); + + const disposable = this._promptsService.registerInstructionsProvider(extension, { + onDidChangeInstructions: emitter.event, + provideInstructions: async (options: IInstructionQueryOptions, token: CancellationToken) => { + const instructions = await this._proxy.$provideInstructions(handle, options, token); + if (!instructions) { + return undefined; + } + // Convert UriComponents to URI + return instructions.map(instruction => ({ + ...instruction, + uri: URI.revive(instruction.uri) + })); + } + }); + + this._instructionsProviders.set(handle, disposable); + } + + $unregisterInstructionsProvider(handle: number): void { + this._instructionsProviders.deleteAndDispose(handle); + this._instructionsProviderEmitters.deleteAndDispose(handle); + } + + $onDidChangeInstructions(handle: number): void { + const emitter = this._instructionsProviderEmitters.get(handle); + if (emitter) { + emitter.fire(); + } + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ddb081274e5d9..1b1e3e7731b0b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1545,6 +1545,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatParticipantPrivate'); return extHostChatAgents2.registerCustomAgentsProvider(extension, provider); }, + registerInstructionsProvider(provider: vscode.InstructionsProvider): vscode.Disposable { + checkProposedApiEnabled(extension, 'chatParticipantPrivate'); + return extHostChatAgents2.registerInstructionsProvider(extension, provider); + }, }; // namespace: lm diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c34c4465f71ff..87b369dc4bfa5 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -65,7 +65,7 @@ import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariabl import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponsePart, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatSelector } from '../../contrib/chat/common/languageModels.js'; import { IPreparedToolInvocation, IToolInvocation, IToolInvocationPreparationContext, IToolProgressStep, IToolResult, ToolDataSource } from '../../contrib/chat/common/languageModelToolsService.js'; -import { ICustomAgentQueryOptions, IExternalCustomAgent } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { ICustomAgentQueryOptions, IExternalCustomAgent, IInstructionQueryOptions, IExternalInstruction } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js'; import * as notebookCommon from '../../contrib/notebook/common/notebookCommon.js'; @@ -1397,6 +1397,9 @@ export interface MainThreadChatAgentsShape2 extends IChatAgentProgressShape, IDi $registerCustomAgentsProvider(handle: number, extension: ExtensionIdentifier): void; $unregisterCustomAgentsProvider(handle: number): void; $onDidChangeCustomAgents(handle: number): void; + $registerInstructionsProvider(handle: number, extension: ExtensionIdentifier): void; + $unregisterInstructionsProvider(handle: number): void; + $onDidChangeInstructions(handle: number): void; $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void; $unregisterAgentCompletionsProvider(handle: number, id: string): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; @@ -1462,6 +1465,7 @@ export interface ExtHostChatAgentsShape2 { $detectChatParticipant(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise; $provideRelatedFiles(handle: number, request: Dto, token: CancellationToken): Promise[] | undefined>; $provideCustomAgents(handle: number, options: ICustomAgentQueryOptions, token: CancellationToken): Promise[] | undefined>; + $provideInstructions(handle: number, options: IInstructionQueryOptions, token: CancellationToken): Promise[] | undefined>; $setRequestTools(requestId: string, tools: UserSelectedTools): void; } export interface IChatParticipantMetadata { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 5aa9eaa33983b..ea2d83f419f43 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -35,7 +35,7 @@ import { ExtHostLanguageModels } from './extHostLanguageModels.js'; import { ExtHostLanguageModelTools } from './extHostLanguageModelTools.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; -import { ICustomAgentQueryOptions, IExternalCustomAgent } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { ICustomAgentQueryOptions, IExternalCustomAgent, IInstructionQueryOptions, IExternalInstruction } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js'; export class ChatAgentResponseStream { @@ -400,6 +400,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private static _customAgentsProviderIdPool = 0; private readonly _customAgentsProviders = new Map(); + private static _instructionsProviderIdPool = 0; + private readonly _instructionsProviders = new Map(); private readonly _sessionDisposables: DisposableResourceMap = this._register(new DisposableResourceMap()); private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); @@ -501,6 +503,28 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return disposables; } + registerInstructionsProvider(extension: IExtensionDescription, provider: vscode.InstructionsProvider): vscode.Disposable { + const handle = ExtHostChatAgents2._instructionsProviderIdPool++; + this._instructionsProviders.set(handle, { extension, provider }); + this._proxy.$registerInstructionsProvider(handle, extension.identifier); + + const disposables = new DisposableStore(); + + // Listen to provider change events and notify main thread + if (provider.onDidChangeInstructions) { + disposables.add(provider.onDidChangeInstructions(() => { + this._proxy.$onDidChangeInstructions(handle); + })); + } + + disposables.add(toDisposable(() => { + this._instructionsProviders.delete(handle); + this._proxy.$unregisterInstructionsProvider(handle); + })); + + return disposables; + } + async $provideRelatedFiles(handle: number, request: IChatRequestDraft, token: CancellationToken): Promise[] | undefined> { const provider = this._relatedFilesProviders.get(handle); if (!provider) { @@ -520,6 +544,15 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return await providerData.provider.provideCustomAgents(options, token) ?? undefined; } + async $provideInstructions(handle: number, options: IInstructionQueryOptions, token: CancellationToken): Promise { + const providerData = this._instructionsProviders.get(handle); + if (!providerData) { + return Promise.resolve(undefined); + } + + return await providerData.provider.provideInstructions(options, token) ?? undefined; + } + async $detectChatParticipant(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise { const detector = this._participantDetectionProviders.get(handle); if (!detector) { 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 3413f08847ecf..66afe5bd40ed6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -20,11 +20,21 @@ import { ResourceSet } from '../../../../../../base/common/map.js'; */ export const CUSTOM_AGENTS_PROVIDER_ACTIVATION_EVENT = 'onCustomAgentsProvider'; +/** + * Activation event for instructions providers. + */ +export const INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT = 'onInstructionsProvider'; + /** * Options for querying custom agents. */ export interface ICustomAgentQueryOptions { } +/** + * Options for querying instructions. + */ +export interface IInstructionQueryOptions { } + /** * Represents a custom agent resource from an external provider. */ @@ -50,6 +60,31 @@ export interface IExternalCustomAgent { readonly isEditable?: boolean; } +/** + * Represents an instruction resource from an external provider. + */ +export interface IExternalInstruction { + /** + * The unique identifier/name of the instruction resource. + */ + readonly name: string; + + /** + * A description of what the instruction resource does. + */ + readonly description: string; + + /** + * The URI to the instruction resource file. + */ + readonly uri: URI; + + /** + * Indicates whether the instruction resource is editable. Defaults to false. + */ + readonly isEditable?: boolean; +} + /** * Provides prompt services. */ @@ -327,6 +362,18 @@ export interface IPromptsService extends IDisposable { provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise; }): IDisposable; + /** + * Registers an InstructionsProvider that can provide instructions for repositories. + * This is part of the proposed API and requires the chatParticipantPrivate proposal. + * @param extension The extension registering the provider. + * @param provider The provider implementation with optional change event. + * @returns A disposable that unregisters the provider when disposed. + */ + registerInstructionsProvider(extension: IExtensionDescription, provider: { + onDidChangeInstructions?: Event; + provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; + }): IDisposable; + /** * Gets list of claude skills files. */ 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 4b3b29a0ad0c8..58292e97f4c13 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, CUSTOM_AGENTS_PROVIDER_ACTIVATION_EVENT } from './promptsService.js'; +import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IClaudeSkill, IUserPromptPath, PromptsStorage, ICustomAgentQueryOptions, IExternalCustomAgent, ExtensionAgentSourceType, CUSTOM_AGENTS_PROVIDER_ACTIVATION_EVENT, IInstructionQueryOptions, IExternalInstruction, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; @@ -169,6 +169,15 @@ export class PromptsService extends Disposable implements IPromptsService { provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise; }> = []; + /** + * Registry of InstructionsProvider instances. Extensions can register providers via the proposed API. + */ + private readonly instructionsProviders: Array<{ + extension: IExtensionDescription; + onDidChangeInstructions?: Event; + provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; + }> = []; + /** * Registers a CustomAgentsProvider. This will be called by the extension host bridge when * an extension registers a provider via vscode.chat.registerCustomAgentsProvider(). @@ -208,6 +217,39 @@ export class PromptsService extends Disposable implements IPromptsService { return disposables; } + /** + * Registers an InstructionsProvider. This will be called by the extension host bridge when + * an extension registers a provider via vscode.chat.registerInstructionsProvider(). + */ + public registerInstructionsProvider(extension: IExtensionDescription, provider: { + onDidChangeInstructions?: Event; + provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; + }): IDisposable { + const providerEntry = { extension, ...provider }; + this.instructionsProviders.push(providerEntry); + + const disposables = new DisposableStore(); + + // Listen to provider change events + if (provider.onDidChangeInstructions) { + disposables.add(provider.onDidChangeInstructions(() => { + // Invalidate instruction cache when providers change + // Instructions might be used for various purposes, trigger refresh + })); + } + + disposables.add({ + dispose: () => { + const index = this.instructionsProviders.findIndex((p) => p === providerEntry); + if (index >= 0) { + this.instructionsProviders.splice(index, 1); + } + } + }); + + return disposables; + } + private async listCustomAgentsFromProvider(token: CancellationToken): Promise { const result: IPromptPath[] = []; diff --git a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts index 177fdc35efb69..94c9d1a6d438f 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts @@ -11,7 +11,7 @@ import { ITextModel } from '../../../../../editor/common/model.js'; import { IExtensionDescription } from '../../../../../platform/extensions/common/extensions.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { ParsedPromptFile } from '../../common/promptSyntax/promptFileParser.js'; -import { IClaudeSkill, ICustomAgent, ICustomAgentQueryOptions, IExternalCustomAgent, IPromptPath, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; +import { IClaudeSkill, ICustomAgent, ICustomAgentQueryOptions, IExternalCustomAgent, IPromptPath, IPromptsService, PromptsStorage, IInstructionQueryOptions, IExternalInstruction } from '../../common/promptSyntax/service/promptsService.js'; import { ResourceSet } from '../../../../../base/common/map.js'; export class MockPromptsService implements IPromptsService { @@ -61,6 +61,7 @@ export class MockPromptsService implements IPromptsService { getDisabledPromptFiles(type: PromptsType): ResourceSet { throw new Error('Method not implemented.'); } setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void { throw new Error('Method not implemented.'); } registerCustomAgentsProvider(extension: IExtensionDescription, provider: { provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } + registerInstructionsProvider(extension: IExtensionDescription, provider: { provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } findClaudeSkills(token: CancellationToken): Promise { throw new Error('Method not implemented.'); } dispose(): void { } } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index c4a40a62c0014..0a5241cc12f49 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -370,6 +370,33 @@ declare module 'vscode' { provideCustomAgents(options: CustomAgentQueryOptions, token: CancellationToken): ProviderResult; } + // #endregion + + // #region InstructionsProvider + + /** + * Options for querying instructions. + */ + export interface InstructionQueryOptions { } + + /** + * A provider that supplies instruction resources for repositories. + */ + export interface InstructionsProvider { + /** + * An optional event to signal that instructions have changed. + */ + readonly onDidChangeInstructions?: Event; + + /** + * Provide the list of instruction resources available for a given repository. + * @param options Optional query parameters. + * @param token A cancellation token. + * @returns An array of instruction resources or a promise that resolves to such. + */ + provideInstructions(options: InstructionQueryOptions, token: CancellationToken): ProviderResult; + } + export namespace chat { /** * Register a provider for custom agents. @@ -377,6 +404,13 @@ declare module 'vscode' { * @returns A disposable that unregisters the provider when disposed. */ export function registerCustomAgentsProvider(provider: CustomAgentsProvider): Disposable; + + /** + * Register a provider for instructions. + * @param provider The instructions provider. + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerInstructionsProvider(provider: InstructionsProvider): Disposable; } // #endregion From 241e7757a8c93376eb1bb016961e6a659295e011 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Mon, 1 Dec 2025 12:17:14 -0800 Subject: [PATCH 02/27] updates --- .../computeAutomaticInstructions.ts | 41 ++++++++++++++- .../service/promptsServiceImpl.ts | 50 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index c8accbbf2f422..85e0b3df6debb 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -22,7 +22,7 @@ import { PromptsConfig } from './config/config.js'; import { isPromptOrInstructionsFile } from './config/promptFileLocations.js'; import { PromptsType } from './promptTypes.js'; import { ParsedPromptFile } from './promptFileParser.js'; -import { IPromptPath, IPromptsService } from './service/promptsService.js'; +import { IPromptPath, IPromptsService, PromptsStorage, ExtensionAgentSourceType } from './service/promptsService.js'; import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; import { ChatConfiguration } from '../constants.js'; @@ -88,6 +88,9 @@ export class ComputeAutomaticInstructions { const telemetryEvent: InstructionsCollectionEvent = newInstructionsCollectionEvent(); const context = this._getContext(variables); + // add instructions from providers (always included) + await this.addProviderInstructions(instructionFiles, context, variables, telemetryEvent, token); + // find instructions where the `applyTo` matches the attached context await this.addApplyingInstructions(instructionFiles, context, variables, telemetryEvent, token); @@ -112,6 +115,42 @@ export class ComputeAutomaticInstructions { this._telemetryService.publicLog2('instructionsCollected', telemetryEvent); } + /** public for testing */ + public async addProviderInstructions(instructionFiles: readonly IPromptPath[], context: { files: ResourceSet; instructions: ResourceSet }, variables: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise { + + for (const instructionPath of instructionFiles) { + // Only include instructions from extension providers (not contributed files) + if (instructionPath.storage !== PromptsStorage.extension) { + continue; + } + + // Check that the source is from a provider, not a contribution + if (instructionPath.source !== ExtensionAgentSourceType.provider) { + continue; + } + + const uri = instructionPath.uri; + + if (context.instructions.has(uri)) { + // the instruction file is already part of the input or has already been processed + this._logService.trace(`[InstructionsContextComputer] Skipping already processed provider instruction file: ${uri}`); + continue; + } + + const parsedFile = await this._parseInstructionsFile(uri, token); + if (!parsedFile) { + this._logService.trace(`[InstructionsContextComputer] Unable to read provider instruction: ${uri}`); + continue; + } + + this._logService.trace(`[InstructionsContextComputer] Adding provider instruction: ${uri}`); + + const reason = localize('instruction.file.reason.provider', 'Automatically attached from extension provider'); + variables.add(toPromptFileVariableEntry(uri, PromptFileVariableKind.Instruction, reason, true)); + telemetryEvent.applyingInstructionsCount++; + } + } + /** public for testing */ public async addApplyingInstructions(instructionFiles: readonly IPromptPath[], context: { files: ResourceSet; instructions: ResourceSet }, variables: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise { 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 58292e97f4c13..1660d1ebe4c73 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -296,6 +296,52 @@ export class PromptsService extends Disposable implements IPromptsService { return result; } + private async listInstructionsFromProvider(token: CancellationToken): Promise { + const result: IPromptPath[] = []; + + if (this.instructionsProviders.length === 0) { + return result; + } + + // Activate extensions that might provide instructions + await this.extensionService.activateByEvent(INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT); + + // Collect instructions from all providers + for (const providerEntry of this.instructionsProviders) { + try { + const instructions = await providerEntry.provideInstructions({}, token); + if (!instructions || token.isCancellationRequested) { + continue; + } + + for (const instruction of instructions) { + if (!instruction.isEditable) { + try { + await this.filesConfigService.updateReadonly(instruction.uri, true); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + this.logger.error(`[listInstructionsFromProvider] Failed to make instruction file readonly: ${instruction.uri}`, msg); + } + } + + result.push({ + uri: instruction.uri, + name: instruction.name, + description: instruction.description, + storage: PromptsStorage.extension, + type: PromptsType.instructions, + extension: providerEntry.extension, + source: ExtensionAgentSourceType.provider + } satisfies IExtensionPromptPath); + } + } catch (e) { + this.logger.error(`[listInstructionsFromProvider] Failed to get instructions from provider`, e instanceof Error ? e.message : String(e)); + } + } + + return result; + } + public async listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise { @@ -318,6 +364,10 @@ export class PromptsService extends Disposable implements IPromptsService { const providerAgents = await this.listCustomAgentsFromProvider(token); return [...contributedFiles, ...providerAgents]; } + if (type === PromptsType.instructions) { + const providerInstructions = await this.listInstructionsFromProvider(token); + return [...contributedFiles, ...providerInstructions]; + } return contributedFiles; } From 424a1dbcb9b08c392d9ef944ea25cbdfa17bbd5f Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Dec 2025 12:18:45 -0800 Subject: [PATCH 03/27] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../chat/common/promptSyntax/service/promptsServiceImpl.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 1660d1ebe4c73..45812f6782341 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -234,7 +234,10 @@ export class PromptsService extends Disposable implements IPromptsService { if (provider.onDidChangeInstructions) { disposables.add(provider.onDidChangeInstructions(() => { // Invalidate instruction cache when providers change - // Instructions might be used for various purposes, trigger refresh + this.cachedFileLocations[PromptsType.instructions] = undefined; + if (this.cachedInstructions && typeof this.cachedInstructions.refresh === 'function') { + this.cachedInstructions.refresh(); + } })); } From dc9551e567a60635ce0958f067d59b216e34a528 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Mon, 1 Dec 2025 12:19:01 -0800 Subject: [PATCH 04/27] test --- .../service/promptsService.test.ts | 119 +++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) 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 72a07f1235afc..8fedc17e8e1e1 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 @@ -36,7 +36,7 @@ import { ComputeAutomaticInstructions, newInstructionsCollectionEvent } from '.. import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js'; import { INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../common/promptSyntax/config/promptFileLocations.js'; import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; -import { ExtensionAgentSourceType, ICustomAgent, ICustomAgentQueryOptions, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ExtensionAgentSourceType, ICustomAgent, ICustomAgentQueryOptions, IInstructionQueryOptions, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; import { PromptsService } from '../../../../common/promptSyntax/service/promptsServiceImpl.js'; import { mockFiles } from '../testUtils/mockFilesystem.js'; import { InMemoryStorageService, IStorageService } from '../../../../../../../platform/storage/common/storage.js'; @@ -1202,6 +1202,123 @@ suite('PromptsService', () => { registered.dispose(); }); + + test('Instructions provider', async () => { + const instructionUri = URI.parse('file://extensions/my-extension/myInstruction.instructions.md'); + const extension = { + identifier: { value: 'test.my-extension' }, + enabledApiProposals: ['chatParticipantPrivate'] + } as unknown as IExtensionDescription; + + // Mock the instruction file content + await mockFiles(fileService, [ + { + path: instructionUri.path, + contents: [ + '# Test instruction content' + ] + } + ]); + + const provider = { + provideInstructions: async (_options: IInstructionQueryOptions, _token: CancellationToken) => { + return [ + { + name: 'testInstruction', + description: 'Test instruction from provider', + uri: instructionUri + } + ]; + } + }; + + const registered = service.registerInstructionsProvider(extension, provider); + + const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); + const providerInstruction = actual.find(i => i.name === 'testInstruction'); + + assert.ok(providerInstruction, 'Provider instruction should be found'); + assert.strictEqual(providerInstruction!.uri.toString(), instructionUri.toString()); + assert.strictEqual(providerInstruction!.name, 'testInstruction'); + assert.strictEqual(providerInstruction!.description, 'Test instruction from provider'); + assert.strictEqual(providerInstruction!.storage, PromptsStorage.extension); + assert.strictEqual(providerInstruction!.source, ExtensionAgentSourceType.provider); + + registered.dispose(); + + // After disposal, the instruction should no longer be listed + const actualAfterDispose = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); + const foundAfterDispose = actualAfterDispose.find(i => i.name === 'testInstruction'); + assert.strictEqual(foundAfterDispose, undefined); + }); + + test('Instructions provider with isEditable flag', async () => { + const readonlyInstructionUri = URI.parse('file://extensions/my-extension/readonly.instructions.md'); + const editableInstructionUri = URI.parse('file://extensions/my-extension/editable.instructions.md'); + const extension = { + identifier: { value: 'test.my-extension' }, + enabledApiProposals: ['chatParticipantPrivate'] + } as unknown as IExtensionDescription; + + // Mock the instruction file content + await mockFiles(fileService, [ + { + path: readonlyInstructionUri.path, + contents: [ + '# Readonly instruction content' + ] + }, + { + path: editableInstructionUri.path, + contents: [ + '# Editable instruction content' + ] + } + ]); + + const provider = { + provideInstructions: async (_options: IInstructionQueryOptions, _token: CancellationToken) => { + return [ + { + name: 'readonlyInstruction', + description: 'Readonly instruction from provider', + uri: readonlyInstructionUri, + isEditable: false + }, + { + name: 'editableInstruction', + description: 'Editable instruction from provider', + uri: editableInstructionUri, + isEditable: true + } + ]; + } + }; + + const registered = service.registerInstructionsProvider(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.instructions, CancellationToken.None); + + // Verify updateReadonly was called only for the non-editable instruction + assert.strictEqual(updateReadonlySpy.callCount, 1, 'updateReadonly should be called once'); + assert.ok(updateReadonlySpy.calledWith(readonlyInstructionUri, true), 'updateReadonly should be called with readonly instruction URI and true'); + + const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); + const readonlyInstruction = actual.find(i => i.name === 'readonlyInstruction'); + const editableInstruction = actual.find(i => i.name === 'editableInstruction'); + + assert.ok(readonlyInstruction, 'Readonly instruction should be found'); + assert.ok(editableInstruction, 'Editable instruction should be found'); + assert.strictEqual(readonlyInstruction!.description, 'Readonly instruction from provider'); + assert.strictEqual(editableInstruction!.description, 'Editable instruction from provider'); + + registered.dispose(); + }); }); suite('findClaudeSkills', () => { From 696ef50b12514d8883fc0464362b995c643bccc2 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Mon, 1 Dec 2025 12:23:51 -0800 Subject: [PATCH 05/27] dispose --- .../chat/common/promptSyntax/service/promptsServiceImpl.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 45812f6782341..a8636e3880b37 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -235,9 +235,6 @@ export class PromptsService extends Disposable implements IPromptsService { disposables.add(provider.onDidChangeInstructions(() => { // Invalidate instruction cache when providers change this.cachedFileLocations[PromptsType.instructions] = undefined; - if (this.cachedInstructions && typeof this.cachedInstructions.refresh === 'function') { - this.cachedInstructions.refresh(); - } })); } @@ -246,6 +243,7 @@ export class PromptsService extends Disposable implements IPromptsService { const index = this.instructionsProviders.findIndex((p) => p === providerEntry); if (index >= 0) { this.instructionsProviders.splice(index, 1); + this.cachedFileLocations[PromptsType.instructions] = undefined; } } }); From 24417460fba3aea670c8fee3749e6cbf99ab6ddc Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Mon, 1 Dec 2025 12:32:25 -0800 Subject: [PATCH 06/27] clean --- src/vs/workbench/api/common/extHost.protocol.ts | 6 +++--- src/vs/workbench/api/common/extHostChatAgents2.ts | 6 +++--- .../chat/common/promptSyntax/service/promptsService.ts | 6 +++--- .../common/promptSyntax/service/promptsServiceImpl.ts | 10 +++++----- .../contrib/chat/test/common/mockPromptsService.ts | 6 +++--- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 87b369dc4bfa5..fb5b628521d2a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -65,7 +65,7 @@ import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariabl import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponsePart, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatSelector } from '../../contrib/chat/common/languageModels.js'; import { IPreparedToolInvocation, IToolInvocation, IToolInvocationPreparationContext, IToolProgressStep, IToolResult, ToolDataSource } from '../../contrib/chat/common/languageModelToolsService.js'; -import { ICustomAgentQueryOptions, IExternalCustomAgent, IInstructionQueryOptions, IExternalInstruction } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { ICustomAgentQueryOptions, IExternalCustomAgentResource, IInstructionQueryOptions } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js'; import * as notebookCommon from '../../contrib/notebook/common/notebookCommon.js'; @@ -1464,8 +1464,8 @@ export interface ExtHostChatAgentsShape2 { $releaseSession(sessionResource: UriComponents): void; $detectChatParticipant(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise; $provideRelatedFiles(handle: number, request: Dto, token: CancellationToken): Promise[] | undefined>; - $provideCustomAgents(handle: number, options: ICustomAgentQueryOptions, token: CancellationToken): Promise[] | undefined>; - $provideInstructions(handle: number, options: IInstructionQueryOptions, token: CancellationToken): Promise[] | undefined>; + $provideCustomAgents(handle: number, options: ICustomAgentQueryOptions, token: CancellationToken): Promise[] | undefined>; + $provideInstructions(handle: number, options: IInstructionQueryOptions, token: CancellationToken): Promise[] | undefined>; $setRequestTools(requestId: string, tools: UserSelectedTools): void; } export interface IChatParticipantMetadata { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index ea2d83f419f43..dd45d910012ce 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -35,7 +35,7 @@ import { ExtHostLanguageModels } from './extHostLanguageModels.js'; import { ExtHostLanguageModelTools } from './extHostLanguageModelTools.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; -import { ICustomAgentQueryOptions, IExternalCustomAgent, IInstructionQueryOptions, IExternalInstruction } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { ICustomAgentQueryOptions, IExternalCustomAgentResource, IInstructionQueryOptions } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js'; export class ChatAgentResponseStream { @@ -535,7 +535,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return await provider.provider.provideRelatedFiles(extRequestDraft, token) ?? undefined; } - async $provideCustomAgents(handle: number, options: ICustomAgentQueryOptions, token: CancellationToken): Promise { + async $provideCustomAgents(handle: number, options: ICustomAgentQueryOptions, token: CancellationToken): Promise { const providerData = this._customAgentsProviders.get(handle); if (!providerData) { return Promise.resolve(undefined); @@ -544,7 +544,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return await providerData.provider.provideCustomAgents(options, token) ?? undefined; } - async $provideInstructions(handle: number, options: IInstructionQueryOptions, token: CancellationToken): Promise { + async $provideInstructions(handle: number, options: IInstructionQueryOptions, token: CancellationToken): Promise { const providerData = this._instructionsProviders.get(handle); if (!providerData) { return Promise.resolve(undefined); 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 66afe5bd40ed6..fef5c212297f1 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -38,7 +38,7 @@ export interface IInstructionQueryOptions { } /** * Represents a custom agent resource from an external provider. */ -export interface IExternalCustomAgent { +export interface IExternalCustomAgentResource { /** * The unique identifier/name of the custom agent resource. */ @@ -359,7 +359,7 @@ export interface IPromptsService extends IDisposable { */ registerCustomAgentsProvider(extension: IExtensionDescription, provider: { onDidChangeCustomAgents?: Event; - provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise; + provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise; }): IDisposable; /** @@ -371,7 +371,7 @@ export interface IPromptsService extends IDisposable { */ registerInstructionsProvider(extension: IExtensionDescription, provider: { onDidChangeInstructions?: Event; - provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; + provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; }): IDisposable; /** 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 a8636e3880b37..5f619bdb2f7c0 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, CUSTOM_AGENTS_PROVIDER_ACTIVATION_EVENT, IInstructionQueryOptions, IExternalInstruction, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT } from './promptsService.js'; +import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IClaudeSkill, IUserPromptPath, PromptsStorage, ICustomAgentQueryOptions, IExternalCustomAgentResource, ExtensionAgentSourceType, CUSTOM_AGENTS_PROVIDER_ACTIVATION_EVENT, IInstructionQueryOptions, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; @@ -166,7 +166,7 @@ export class PromptsService extends Disposable implements IPromptsService { private readonly customAgentsProviders: Array<{ extension: IExtensionDescription; onDidChangeCustomAgents?: Event; - provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise; + provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise; }> = []; /** @@ -175,7 +175,7 @@ export class PromptsService extends Disposable implements IPromptsService { private readonly instructionsProviders: Array<{ extension: IExtensionDescription; onDidChangeInstructions?: Event; - provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; + provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; }> = []; /** @@ -184,7 +184,7 @@ export class PromptsService extends Disposable implements IPromptsService { */ public registerCustomAgentsProvider(extension: IExtensionDescription, provider: { onDidChangeCustomAgents?: Event; - provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise; + provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise; }): IDisposable { const providerEntry = { extension, ...provider }; this.customAgentsProviders.push(providerEntry); @@ -223,7 +223,7 @@ export class PromptsService extends Disposable implements IPromptsService { */ public registerInstructionsProvider(extension: IExtensionDescription, provider: { onDidChangeInstructions?: Event; - provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; + provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; }): IDisposable { const providerEntry = { extension, ...provider }; this.instructionsProviders.push(providerEntry); diff --git a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts index 94c9d1a6d438f..3724501bf3fb5 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts @@ -11,7 +11,7 @@ import { ITextModel } from '../../../../../editor/common/model.js'; import { IExtensionDescription } from '../../../../../platform/extensions/common/extensions.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { ParsedPromptFile } from '../../common/promptSyntax/promptFileParser.js'; -import { IClaudeSkill, ICustomAgent, ICustomAgentQueryOptions, IExternalCustomAgent, IPromptPath, IPromptsService, PromptsStorage, IInstructionQueryOptions, IExternalInstruction } from '../../common/promptSyntax/service/promptsService.js'; +import { IClaudeSkill, ICustomAgent, ICustomAgentQueryOptions, IExternalCustomAgentResource, IPromptPath, IPromptsService, PromptsStorage, IInstructionQueryOptions } from '../../common/promptSyntax/service/promptsService.js'; import { ResourceSet } from '../../../../../base/common/map.js'; export class MockPromptsService implements IPromptsService { @@ -60,8 +60,8 @@ export class MockPromptsService implements IPromptsService { getAgentFileURIFromModeFile(oldURI: URI): URI | undefined { throw new Error('Not implemented'); } getDisabledPromptFiles(type: PromptsType): ResourceSet { throw new Error('Method not implemented.'); } setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void { throw new Error('Method not implemented.'); } - registerCustomAgentsProvider(extension: IExtensionDescription, provider: { provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } - registerInstructionsProvider(extension: IExtensionDescription, provider: { provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } + registerCustomAgentsProvider(extension: IExtensionDescription, provider: { provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } + registerInstructionsProvider(extension: IExtensionDescription, provider: { provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } findClaudeSkills(token: CancellationToken): Promise { throw new Error('Method not implemented.'); } dispose(): void { } } From 112dc09ed1fd1599aae616e628560b5a0f9b2530 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Mon, 1 Dec 2025 12:33:12 -0800 Subject: [PATCH 07/27] remove --- .../promptSyntax/service/promptsService.ts | 25 ------------------- 1 file changed, 25 deletions(-) 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 fef5c212297f1..8840bddb5dc58 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -60,31 +60,6 @@ export interface IExternalCustomAgentResource { readonly isEditable?: boolean; } -/** - * Represents an instruction resource from an external provider. - */ -export interface IExternalInstruction { - /** - * The unique identifier/name of the instruction resource. - */ - readonly name: string; - - /** - * A description of what the instruction resource does. - */ - readonly description: string; - - /** - * The URI to the instruction resource file. - */ - readonly uri: URI; - - /** - * Indicates whether the instruction resource is editable. Defaults to false. - */ - readonly isEditable?: boolean; -} - /** * Provides prompt services. */ From 25cd745e043a0df55e7e32e8b2f210232d301204 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Mon, 1 Dec 2025 14:40:06 -0800 Subject: [PATCH 08/27] invalidate --- .../chat/common/promptSyntax/service/promptsServiceImpl.ts | 3 +++ 1 file changed, 3 insertions(+) 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 5f619bdb2f7c0..ed57bcdc18e39 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -238,6 +238,9 @@ export class PromptsService extends Disposable implements IPromptsService { })); } + // Invalidate instruction cache when provider is registered + this.cachedFileLocations[PromptsType.instructions] = undefined; + disposables.add({ dispose: () => { const index = this.instructionsProviders.findIndex((p) => p === providerEntry); From 1600cdf21d354999d006934fa9e9ca029c4091bd Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 31 Dec 2025 12:19:47 -0800 Subject: [PATCH 09/27] wip --- .../api/browser/mainThreadChatAgents2.ts | 82 ++++-------- .../workbench/api/common/extHost.api.impl.ts | 8 +- .../workbench/api/common/extHost.protocol.ts | 14 +- .../api/common/extHostChatAgents2.ts | 63 +++------ .../promptSyntax/service/promptsService.ts | 43 ++---- .../service/promptsServiceImpl.ts | 125 +++++++----------- .../chat/test/common/mockPromptsService.ts | 5 +- .../service/promptsService.test.ts | 18 +-- ...scode.proposed.chatParticipantPrivate.d.ts | 48 +------ 9 files changed, 121 insertions(+), 285 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 655dfbb3a2a84..0388eb7294657 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -26,7 +26,8 @@ import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIde import { IChatWidgetService } from '../../contrib/chat/browser/chat.js'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../contrib/chat/browser/contrib/chatDynamicVariables.js'; import { IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../contrib/chat/common/chatAgents.js'; -import { ICustomAgentQueryOptions, IPromptsService, IInstructionQueryOptions } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IChatContributionQueryOptions, IPromptsService } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { isValidPromptType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/chatEditingService.js'; import { IChatModel } from '../../contrib/chat/common/chatModel.js'; import { ChatRequestAgentPart } from '../../contrib/chat/common/chatParserTypes.js'; @@ -96,10 +97,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA private readonly _chatRelatedFilesProviders = this._register(new DisposableMap()); - private readonly _customAgentsProviders = this._register(new DisposableMap()); - private readonly _customAgentsProviderEmitters = this._register(new DisposableMap>()); - private readonly _instructionsProviders = this._register(new DisposableMap()); - private readonly _instructionsProviderEmitters = this._register(new DisposableMap>()); + private readonly _contributionsProviders = this._register(new DisposableMap()); + private readonly _contributionsProviderEmitters = this._register(new DisposableMap>()); private readonly _pendingProgress = new Map void; chatSession: IChatModel | undefined }>(); private readonly _proxy: ExtHostChatAgentsShape2; @@ -437,81 +436,46 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._chatRelatedFilesProviders.deleteAndDispose(handle); } - async $registerCustomAgentsProvider(handle: number, extensionId: ExtensionIdentifier): Promise { + async $registerContributionsProvider(handle: number, type: string, extensionId: ExtensionIdentifier): Promise { const extension = await this._extensionService.getExtension(extensionId.value); if (!extension) { - this._logService.error(`[MainThreadChatAgents2] Could not find extension for CustomAgentsProvider: ${extensionId.value}`); + this._logService.error(`[MainThreadChatAgents2] Could not find extension for ChatContributionsProvider: ${extensionId.value}`); return; } - const emitter = new Emitter(); - this._customAgentsProviderEmitters.set(handle, emitter); - - const disposable = this._promptsService.registerCustomAgentsProvider(extension, { - onDidChangeCustomAgents: emitter.event, - provideCustomAgents: async (options: ICustomAgentQueryOptions, token: CancellationToken) => { - const agents = await this._proxy.$provideCustomAgents(handle, options, token); - if (!agents) { - return undefined; - } - // Convert UriComponents to URI - return agents.map(agent => ({ - ...agent, - uri: URI.revive(agent.uri) - })); - } - }); - - this._customAgentsProviders.set(handle, disposable); - } - - $unregisterCustomAgentsProvider(handle: number): void { - this._customAgentsProviders.deleteAndDispose(handle); - this._customAgentsProviderEmitters.deleteAndDispose(handle); - } - - $onDidChangeCustomAgents(handle: number): void { - const emitter = this._customAgentsProviderEmitters.get(handle); - if (emitter) { - emitter.fire(); - } - } - - async $registerInstructionsProvider(handle: number, extensionId: ExtensionIdentifier): Promise { - const extension = await this._extensionService.getExtension(extensionId.value); - if (!extension) { - this._logService.error(`[MainThreadChatAgents2] Could not find extension for InstructionsProvider: ${extensionId.value}`); + if (!isValidPromptType(type)) { + this._logService.error(`[MainThreadChatAgents2] Invalid contribution type: ${type}`); return; } const emitter = new Emitter(); - this._instructionsProviderEmitters.set(handle, emitter); + this._contributionsProviderEmitters.set(handle, emitter); - const disposable = this._promptsService.registerInstructionsProvider(extension, { - onDidChangeInstructions: emitter.event, - provideInstructions: async (options: IInstructionQueryOptions, token: CancellationToken) => { - const instructions = await this._proxy.$provideInstructions(handle, options, token); - if (!instructions) { + const disposable = this._promptsService.registerContributionsProvider(extension, type, { + onDidChangeContributions: emitter.event, + provideContributions: async (options: IChatContributionQueryOptions, token: CancellationToken) => { + const contributions = await this._proxy.$provideContributions(handle, options, token); + if (!contributions) { return undefined; } // Convert UriComponents to URI - return instructions.map(instruction => ({ - ...instruction, - uri: URI.revive(instruction.uri) + return contributions.map(c => ({ + ...c, + uri: URI.revive(c.uri) })); } }); - this._instructionsProviders.set(handle, disposable); + this._contributionsProviders.set(handle, disposable); } - $unregisterInstructionsProvider(handle: number): void { - this._instructionsProviders.deleteAndDispose(handle); - this._instructionsProviderEmitters.deleteAndDispose(handle); + $unregisterContributionsProvider(handle: number): void { + this._contributionsProviders.deleteAndDispose(handle); + this._contributionsProviderEmitters.deleteAndDispose(handle); } - $onDidChangeInstructions(handle: number): void { - const emitter = this._instructionsProviderEmitters.get(handle); + $onDidChangeContributions(handle: number): void { + const emitter = this._contributionsProviderEmitters.get(handle); if (emitter) { emitter.fire(); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d586560774a8b..c7e59f8ae0d9b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1541,13 +1541,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatContextProvider'); return extHostChatContext.registerChatContextProvider(selector ? checkSelector(selector) : undefined, `${extension.id}-${id}`, provider); }, - registerCustomAgentsProvider(provider: vscode.CustomAgentsProvider): vscode.Disposable { + registerContributionsProvider(type: string, provider: vscode.ChatContributionsProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatParticipantPrivate'); - return extHostChatAgents2.registerCustomAgentsProvider(extension, provider); - }, - registerInstructionsProvider(provider: vscode.InstructionsProvider): vscode.Disposable { - checkProposedApiEnabled(extension, 'chatParticipantPrivate'); - return extHostChatAgents2.registerInstructionsProvider(extension, provider); + return extHostChatAgents2.registerContributionsProvider(extension, type, provider); }, }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f26e85cbac836..04b7301f0c98b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -65,7 +65,7 @@ import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariabl import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponsePart, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatSelector } from '../../contrib/chat/common/languageModels.js'; import { IPreparedToolInvocation, IToolInvocation, IToolInvocationPreparationContext, IToolProgressStep, IToolResult, ToolDataSource } from '../../contrib/chat/common/languageModelToolsService.js'; -import { ICustomAgentQueryOptions, IExternalCustomAgentResource, IInstructionQueryOptions } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IChatContributionQueryOptions, IChatContributionResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js'; import * as notebookCommon from '../../contrib/notebook/common/notebookCommon.js'; @@ -1394,12 +1394,9 @@ export interface MainThreadChatAgentsShape2 extends IChatAgentProgressShape, IDi $unregisterChatParticipantDetectionProvider(handle: number): void; $registerRelatedFilesProvider(handle: number, metadata: IChatRelatedFilesProviderMetadata): void; $unregisterRelatedFilesProvider(handle: number): void; - $registerCustomAgentsProvider(handle: number, extension: ExtensionIdentifier): void; - $unregisterCustomAgentsProvider(handle: number): void; - $onDidChangeCustomAgents(handle: number): void; - $registerInstructionsProvider(handle: number, extension: ExtensionIdentifier): void; - $unregisterInstructionsProvider(handle: number): void; - $onDidChangeInstructions(handle: number): void; + $registerContributionsProvider(handle: number, type: string, extension: ExtensionIdentifier): void; + $unregisterContributionsProvider(handle: number): void; + $onDidChangeContributions(handle: number): void; $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void; $unregisterAgentCompletionsProvider(handle: number, id: string): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; @@ -1465,8 +1462,7 @@ export interface ExtHostChatAgentsShape2 { $releaseSession(sessionResource: UriComponents): void; $detectChatParticipant(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise; $provideRelatedFiles(handle: number, request: Dto, token: CancellationToken): Promise[] | undefined>; - $provideCustomAgents(handle: number, options: ICustomAgentQueryOptions, token: CancellationToken): Promise[] | undefined>; - $provideInstructions(handle: number, options: IInstructionQueryOptions, token: CancellationToken): Promise[] | undefined>; + $provideContributions(handle: number, options: IChatContributionQueryOptions, token: CancellationToken): Promise[] | undefined>; $setRequestTools(requestId: string, tools: UserSelectedTools): void; } export interface IChatParticipantMetadata { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 36ca770889472..37cf69d148370 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -35,7 +35,7 @@ import { ExtHostLanguageModels } from './extHostLanguageModels.js'; import { ExtHostLanguageModelTools } from './extHostLanguageModelTools.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; -import { ICustomAgentQueryOptions, IExternalCustomAgentResource, IInstructionQueryOptions } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IChatContributionQueryOptions, IChatContributionResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js'; export class ChatAgentResponseStream { @@ -398,10 +398,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private static _relatedFilesProviderIdPool = 0; private readonly _relatedFilesProviders = new Map(); - private static _customAgentsProviderIdPool = 0; - private readonly _customAgentsProviders = new Map(); - private static _instructionsProviderIdPool = 0; - private readonly _instructionsProviders = new Map(); + private static _contributionsProviderIdPool = 0; + private readonly _contributionsProviders = new Map(); private readonly _sessionDisposables: DisposableResourceMap = this._register(new DisposableResourceMap()); private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); @@ -481,45 +479,23 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS }); } - registerCustomAgentsProvider(extension: IExtensionDescription, provider: vscode.CustomAgentsProvider): vscode.Disposable { - const handle = ExtHostChatAgents2._customAgentsProviderIdPool++; - this._customAgentsProviders.set(handle, { extension, provider }); - this._proxy.$registerCustomAgentsProvider(handle, extension.identifier); + registerContributionsProvider(extension: IExtensionDescription, type: string, provider: vscode.ChatContributionsProvider): vscode.Disposable { + const handle = ExtHostChatAgents2._contributionsProviderIdPool++; + this._contributionsProviders.set(handle, { extension, provider }); + this._proxy.$registerContributionsProvider(handle, type, extension.identifier); const disposables = new DisposableStore(); // Listen to provider change events and notify main thread - if (provider.onDidChangeCustomAgents) { - disposables.add(provider.onDidChangeCustomAgents(() => { - this._proxy.$onDidChangeCustomAgents(handle); + if (provider.onDidChangeContributions) { + disposables.add(provider.onDidChangeContributions(() => { + this._proxy.$onDidChangeContributions(handle); })); } disposables.add(toDisposable(() => { - this._customAgentsProviders.delete(handle); - this._proxy.$unregisterCustomAgentsProvider(handle); - })); - - return disposables; - } - - registerInstructionsProvider(extension: IExtensionDescription, provider: vscode.InstructionsProvider): vscode.Disposable { - const handle = ExtHostChatAgents2._instructionsProviderIdPool++; - this._instructionsProviders.set(handle, { extension, provider }); - this._proxy.$registerInstructionsProvider(handle, extension.identifier); - - const disposables = new DisposableStore(); - - // Listen to provider change events and notify main thread - if (provider.onDidChangeInstructions) { - disposables.add(provider.onDidChangeInstructions(() => { - this._proxy.$onDidChangeInstructions(handle); - })); - } - - disposables.add(toDisposable(() => { - this._instructionsProviders.delete(handle); - this._proxy.$unregisterInstructionsProvider(handle); + this._contributionsProviders.delete(handle); + this._proxy.$unregisterContributionsProvider(handle); })); return disposables; @@ -535,22 +511,13 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return await provider.provider.provideRelatedFiles(extRequestDraft, token) ?? undefined; } - async $provideCustomAgents(handle: number, options: ICustomAgentQueryOptions, token: CancellationToken): Promise { - const providerData = this._customAgentsProviders.get(handle); - if (!providerData) { - return Promise.resolve(undefined); - } - - return await providerData.provider.provideCustomAgents(options, token) ?? undefined; - } - - async $provideInstructions(handle: number, options: IInstructionQueryOptions, token: CancellationToken): Promise { - const providerData = this._instructionsProviders.get(handle); + async $provideContributions(handle: number, options: IChatContributionQueryOptions, token: CancellationToken): Promise { + const providerData = this._contributionsProviders.get(handle); if (!providerData) { return Promise.resolve(undefined); } - return await providerData.provider.provideInstructions(options, token) ?? undefined; + return await providerData.provider.provideContributions(options, token) ?? undefined; } async $detectChatParticipant(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise { 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 aefc33ba42b55..b99a0d7573cd6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -16,29 +16,19 @@ import { IHandOff, ParsedPromptFile } from '../promptFileParser.js'; import { ResourceSet } from '../../../../../../base/common/map.js'; /** - * Activation event for custom agent providers. + * Activation event for chat contributions providers. */ -export const CUSTOM_AGENTS_PROVIDER_ACTIVATION_EVENT = 'onCustomAgentsProvider'; +export const CHAT_CONTRIBUTIONS_PROVIDER_ACTIVATION_EVENT = 'onChatContributionsProvider'; /** - * Activation event for instructions providers. + * Options for querying chat contributions. */ -export const INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT = 'onInstructionsProvider'; +export interface IChatContributionQueryOptions { } /** - * Options for querying custom agents. + * Represents a chat contribution resource from an external provider. */ -export interface ICustomAgentQueryOptions { } - -/** - * Options for querying instructions. - */ -export interface IInstructionQueryOptions { } - -/** - * Represents a custom agent resource from an external provider. - */ -export interface IExternalCustomAgentResource { +export interface IChatContributionResource { /** * The unique identifier/name of the custom agent resource. */ @@ -326,27 +316,16 @@ export interface IPromptsService extends IDisposable { setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void; /** - * Registers a CustomAgentsProvider that can provide custom agents for repositories. - * This is part of the proposed API and requires the chatParticipantPrivate proposal. - * @param extension The extension registering the provider. - * @param provider The provider implementation with optional change event. - * @returns A disposable that unregisters the provider when disposed. - */ - registerCustomAgentsProvider(extension: IExtensionDescription, provider: { - onDidChangeCustomAgents?: Event; - provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise; - }): IDisposable; - - /** - * Registers an InstructionsProvider that can provide instructions for repositories. + * Registers a ChatContributionsProvider that can provide chat contributions for repositories. * This is part of the proposed API and requires the chatParticipantPrivate proposal. * @param extension The extension registering the provider. + * @param type The type of contribution (e.g. 'instruction', 'custom agent', 'prompt'). * @param provider The provider implementation with optional change event. * @returns A disposable that unregisters the provider when disposed. */ - registerInstructionsProvider(extension: IExtensionDescription, provider: { - onDidChangeInstructions?: Event; - provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; + registerContributionsProvider(extension: IExtensionDescription, type: PromptsType, provider: { + onDidChangeContributions?: Event; + provideContributions: (options: IChatContributionQueryOptions, token: CancellationToken) => Promise; }): IDisposable; /** 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 df4381408428f..6c26f4a6c0f70 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -32,7 +32,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, IAgentSkill, IUserPromptPath, PromptsStorage, ICustomAgentQueryOptions, IExternalCustomAgentResource, ExtensionAgentSourceType, CUSTOM_AGENTS_PROVIDER_ACTIVATION_EVENT, IInstructionQueryOptions, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT } from './promptsService.js'; +import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CHAT_CONTRIBUTIONS_PROVIDER_ACTIVATION_EVENT, IChatContributionQueryOptions, IChatContributionResource } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; @@ -165,92 +165,59 @@ export class PromptsService extends Disposable implements IPromptsService { } /** - * Registry of CustomAgentsProvider instances. Extensions can register providers via the proposed API. + * Registry of ChatContributionsProvider instances. Extensions can register providers via the proposed API. */ - private readonly customAgentsProviders: Array<{ + private readonly contributionsProviders: Array<{ extension: IExtensionDescription; - onDidChangeCustomAgents?: Event; - provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise; + type: PromptsType; + onDidChangeContributions?: Event; + provideContributions: (options: IChatContributionQueryOptions, token: CancellationToken) => Promise; }> = []; /** - * Registry of InstructionsProvider instances. Extensions can register providers via the proposed API. + * Registers a ChatContributionsProvider. This will be called by the extension host bridge when + * an extension registers a provider via vscode.chat.registerContributionsProvider(). */ - private readonly instructionsProviders: Array<{ - extension: IExtensionDescription; - onDidChangeInstructions?: Event; - provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; - }> = []; - - /** - * Registers a CustomAgentsProvider. This will be called by the extension host bridge when - * an extension registers a provider via vscode.chat.registerCustomAgentsProvider(). - */ - public registerCustomAgentsProvider(extension: IExtensionDescription, provider: { - onDidChangeCustomAgents?: Event; - provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise; + public registerContributionsProvider(extension: IExtensionDescription, type: PromptsType, provider: { + onDidChangeContributions?: Event; + provideContributions: (options: IChatContributionQueryOptions, token: CancellationToken) => Promise; }): IDisposable { - const providerEntry = { extension, ...provider }; - this.customAgentsProviders.push(providerEntry); + const providerEntry = { extension, type, ...provider }; + this.contributionsProviders.push(providerEntry); const disposables = new DisposableStore(); // Listen to provider change events to rerun computeListPromptFiles - if (provider.onDidChangeCustomAgents) { - disposables.add(provider.onDidChangeCustomAgents(() => { - this.cachedFileLocations[PromptsType.agent] = undefined; - this.cachedCustomAgents.refresh(); - })); - } - - // Invalidate agent cache when providers change - this.cachedFileLocations[PromptsType.agent] = undefined; - this.cachedCustomAgents.refresh(); - - disposables.add({ - dispose: () => { - const index = this.customAgentsProviders.findIndex((p) => p === providerEntry); - if (index >= 0) { - this.customAgentsProviders.splice(index, 1); + if (provider.onDidChangeContributions) { + disposables.add(provider.onDidChangeContributions(() => { + if (type === PromptsType.agent) { this.cachedFileLocations[PromptsType.agent] = undefined; this.cachedCustomAgents.refresh(); + } else if (type === PromptsType.instructions) { + this.cachedFileLocations[PromptsType.instructions] = undefined; } - } - }); - - return disposables; - } - - /** - * Registers an InstructionsProvider. This will be called by the extension host bridge when - * an extension registers a provider via vscode.chat.registerInstructionsProvider(). - */ - public registerInstructionsProvider(extension: IExtensionDescription, provider: { - onDidChangeInstructions?: Event; - provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise; - }): IDisposable { - const providerEntry = { extension, ...provider }; - this.instructionsProviders.push(providerEntry); - - const disposables = new DisposableStore(); - - // Listen to provider change events - if (provider.onDidChangeInstructions) { - disposables.add(provider.onDidChangeInstructions(() => { - // Invalidate instruction cache when providers change - this.cachedFileLocations[PromptsType.instructions] = undefined; })); } - // Invalidate instruction cache when provider is registered - this.cachedFileLocations[PromptsType.instructions] = undefined; + // Invalidate cache when providers change + if (type === PromptsType.agent) { + this.cachedFileLocations[PromptsType.agent] = undefined; + this.cachedCustomAgents.refresh(); + } else if (type === PromptsType.instructions) { + this.cachedFileLocations[PromptsType.instructions] = undefined; + } disposables.add({ dispose: () => { - const index = this.instructionsProviders.findIndex((p) => p === providerEntry); + const index = this.contributionsProviders.findIndex((p) => p === providerEntry); if (index >= 0) { - this.instructionsProviders.splice(index, 1); - this.cachedFileLocations[PromptsType.instructions] = undefined; + this.contributionsProviders.splice(index, 1); + if (type === PromptsType.agent) { + this.cachedFileLocations[PromptsType.agent] = undefined; + this.cachedCustomAgents.refresh(); + } else if (type === PromptsType.instructions) { + this.cachedFileLocations[PromptsType.instructions] = undefined; + } } } }); @@ -261,17 +228,18 @@ export class PromptsService extends Disposable implements IPromptsService { private async listCustomAgentsFromProvider(token: CancellationToken): Promise { const result: IPromptPath[] = []; - if (this.customAgentsProviders.length === 0) { + // Activate extensions that might provide custom agents + await this.extensionService.activateByEvent(CHAT_CONTRIBUTIONS_PROVIDER_ACTIVATION_EVENT); + + const providers = this.contributionsProviders.filter(p => p.type === PromptsType.agent); + if (providers.length === 0) { 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) { + for (const providerEntry of providers) { try { - const agents = await providerEntry.provideCustomAgents({}, token); + const agents = await providerEntry.provideContributions({}, token); if (!agents || token.isCancellationRequested) { continue; } @@ -307,17 +275,18 @@ export class PromptsService extends Disposable implements IPromptsService { private async listInstructionsFromProvider(token: CancellationToken): Promise { const result: IPromptPath[] = []; - if (this.instructionsProviders.length === 0) { + // Activate extensions that might provide instructions + await this.extensionService.activateByEvent(CHAT_CONTRIBUTIONS_PROVIDER_ACTIVATION_EVENT); + + const providers = this.contributionsProviders.filter(p => p.type === PromptsType.instructions); + if (providers.length === 0) { return result; } - // Activate extensions that might provide instructions - await this.extensionService.activateByEvent(INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT); - // Collect instructions from all providers - for (const providerEntry of this.instructionsProviders) { + for (const providerEntry of providers) { try { - const instructions = await providerEntry.provideInstructions({}, token); + const instructions = await providerEntry.provideContributions({}, token); if (!instructions || token.isCancellationRequested) { continue; } diff --git a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts index 4deaeaa516c72..4037f16b39dc7 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts @@ -11,7 +11,7 @@ import { ITextModel } from '../../../../../editor/common/model.js'; import { IExtensionDescription } from '../../../../../platform/extensions/common/extensions.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { ParsedPromptFile } from '../../common/promptSyntax/promptFileParser.js'; -import { IAgentSkill, ICustomAgent, ICustomAgentQueryOptions, IExternalCustomAgentResource, IPromptPath, IPromptsService, PromptsStorage, IInstructionQueryOptions } from '../../common/promptSyntax/service/promptsService.js'; +import { IAgentSkill, ICustomAgent, IChatContributionQueryOptions, IChatContributionResource, IPromptPath, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { ResourceSet } from '../../../../../base/common/map.js'; export class MockPromptsService implements IPromptsService { @@ -60,8 +60,7 @@ export class MockPromptsService implements IPromptsService { getAgentFileURIFromModeFile(oldURI: URI): URI | undefined { throw new Error('Not implemented'); } getDisabledPromptFiles(type: PromptsType): ResourceSet { throw new Error('Method not implemented.'); } setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void { throw new Error('Method not implemented.'); } - registerCustomAgentsProvider(extension: IExtensionDescription, provider: { provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } - registerInstructionsProvider(extension: IExtensionDescription, provider: { provideInstructions: (options: IInstructionQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } + registerContributionsProvider(extension: IExtensionDescription, type: PromptsType, provider: { provideContributions: (options: IChatContributionQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } findAgentSkills(token: CancellationToken): Promise { throw new Error('Method not implemented.'); } dispose(): void { } } 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 4cea151ce96f8..c74a8f6478466 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 @@ -36,7 +36,7 @@ import { ComputeAutomaticInstructions, newInstructionsCollectionEvent } from '.. import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js'; import { INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../common/promptSyntax/config/promptFileLocations.js'; import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; -import { ExtensionAgentSourceType, ICustomAgent, ICustomAgentQueryOptions, IInstructionQueryOptions, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ExtensionAgentSourceType, ICustomAgent, IChatContributionQueryOptions, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; import { PromptsService } from '../../../../common/promptSyntax/service/promptsServiceImpl.js'; import { mockFiles } from '../testUtils/mockFilesystem.js'; import { InMemoryStorageService, IStorageService } from '../../../../../../../platform/storage/common/storage.js'; @@ -1103,7 +1103,7 @@ suite('PromptsService', () => { ]); const provider = { - provideCustomAgents: async (_options: ICustomAgentQueryOptions, _token: CancellationToken) => { + provideContributions: async (_options: IChatContributionQueryOptions, _token: CancellationToken) => { return [ { name: 'myAgent', @@ -1114,7 +1114,7 @@ suite('PromptsService', () => { } }; - const registered = service.registerCustomAgentsProvider(extension, provider); + const registered = service.registerContributionsProvider(extension, PromptsType.agent, provider); const actual = await service.getCustomAgents(CancellationToken.None); assert.strictEqual(actual.length, 1); @@ -1164,7 +1164,7 @@ suite('PromptsService', () => { ]); const provider = { - provideCustomAgents: async (_options: ICustomAgentQueryOptions, _token: CancellationToken) => { + provideContributions: async (_options: IChatContributionQueryOptions, _token: CancellationToken) => { return [ { name: 'readonlyAgent', @@ -1182,7 +1182,7 @@ suite('PromptsService', () => { } }; - const registered = service.registerCustomAgentsProvider(extension, provider); + const registered = service.registerContributionsProvider(extension, PromptsType.agent, provider); // Spy on updateReadonly to verify it's called correctly const filesConfigService = instaService.get(IFilesConfigurationService); @@ -1279,7 +1279,7 @@ suite('PromptsService', () => { ]); const provider = { - provideInstructions: async (_options: IInstructionQueryOptions, _token: CancellationToken) => { + provideContributions: async (_options: IChatContributionQueryOptions, _token: CancellationToken) => { return [ { name: 'testInstruction', @@ -1290,7 +1290,7 @@ suite('PromptsService', () => { } }; - const registered = service.registerInstructionsProvider(extension, provider); + const registered = service.registerContributionsProvider(extension, PromptsType.instructions, provider); const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); const providerInstruction = actual.find(i => i.name === 'testInstruction'); @@ -1335,7 +1335,7 @@ suite('PromptsService', () => { ]); const provider = { - provideInstructions: async (_options: IInstructionQueryOptions, _token: CancellationToken) => { + provideContributions: async (_options: IChatContributionQueryOptions, _token: CancellationToken) => { return [ { name: 'readonlyInstruction', @@ -1353,7 +1353,7 @@ suite('PromptsService', () => { } }; - const registered = service.registerInstructionsProvider(extension, provider); + const registered = service.registerContributionsProvider(extension, PromptsType.instructions, provider); // Spy on updateReadonly to verify it's called correctly const filesConfigService = instaService.get(IFilesConfigurationService); diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 0a5241cc12f49..bc3dd968e6498 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -320,12 +320,12 @@ declare module 'vscode' { // #endregion - // #region CustomAgentsProvider + // #region ChatContributionsProvider /** * Represents a custom agent resource file (e.g., .agent.md or .prompt.md) available for a repository. */ - export interface CustomAgentResource { + export interface ChatContributionResource { /** * The unique identifier/name of the custom agent resource. */ @@ -350,16 +350,16 @@ declare module 'vscode' { /** * Options for querying custom agents. */ - export interface CustomAgentQueryOptions { } + export interface ChatContributionQueryOptions { } /** * A provider that supplies custom agent resources (from .agent.md and .prompt.md files) for repositories. */ - export interface CustomAgentsProvider { + export interface ChatContributionsProvider { /** * An optional event to signal that custom agents have changed. */ - readonly onDidChangeCustomAgents?: Event; + readonly onDidChangeContributions?: Event; /** * Provide the list of custom agent resources available for a given repository. @@ -367,34 +367,7 @@ declare module 'vscode' { * @param token A cancellation token. * @returns An array of custom agent resources or a promise that resolves to such. */ - provideCustomAgents(options: CustomAgentQueryOptions, token: CancellationToken): ProviderResult; - } - - // #endregion - - // #region InstructionsProvider - - /** - * Options for querying instructions. - */ - export interface InstructionQueryOptions { } - - /** - * A provider that supplies instruction resources for repositories. - */ - export interface InstructionsProvider { - /** - * An optional event to signal that instructions have changed. - */ - readonly onDidChangeInstructions?: Event; - - /** - * Provide the list of instruction resources available for a given repository. - * @param options Optional query parameters. - * @param token A cancellation token. - * @returns An array of instruction resources or a promise that resolves to such. - */ - provideInstructions(options: InstructionQueryOptions, token: CancellationToken): ProviderResult; + provideContributions(options: ChatContributionQueryOptions, token: CancellationToken): ProviderResult; } export namespace chat { @@ -403,14 +376,7 @@ declare module 'vscode' { * @param provider The custom agents provider. * @returns A disposable that unregisters the provider when disposed. */ - export function registerCustomAgentsProvider(provider: CustomAgentsProvider): Disposable; - - /** - * Register a provider for instructions. - * @param provider The instructions provider. - * @returns A disposable that unregisters the provider when disposed. - */ - export function registerInstructionsProvider(provider: InstructionsProvider): Disposable; + export function registerContributionsProvider(type: string, provider: ChatContributionsProvider): Disposable; } // #endregion From 88bc61c9071b1998eedcf66e959b92e2d90348de Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 31 Dec 2025 12:20:55 -0800 Subject: [PATCH 10/27] pr --- .../computeAutomaticInstructions.ts | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index aed7db493f463..6a34125ba4a52 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -22,7 +22,7 @@ import { PromptsConfig } from './config/config.js'; import { isPromptOrInstructionsFile } from './config/promptFileLocations.js'; import { PromptsType } from './promptTypes.js'; import { ParsedPromptFile } from './promptFileParser.js'; -import { IPromptPath, IPromptsService, PromptsStorage, ExtensionAgentSourceType } from './service/promptsService.js'; +import { IPromptPath, IPromptsService } from './service/promptsService.js'; import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; import { ChatConfiguration } from '../constants.js'; @@ -88,9 +88,6 @@ export class ComputeAutomaticInstructions { const telemetryEvent: InstructionsCollectionEvent = newInstructionsCollectionEvent(); const context = this._getContext(variables); - // add instructions from providers (always included) - await this.addProviderInstructions(instructionFiles, context, variables, telemetryEvent, token); - // find instructions where the `applyTo` matches the attached context await this.addApplyingInstructions(instructionFiles, context, variables, telemetryEvent, token); @@ -115,42 +112,6 @@ export class ComputeAutomaticInstructions { this._telemetryService.publicLog2('instructionsCollected', telemetryEvent); } - /** public for testing */ - public async addProviderInstructions(instructionFiles: readonly IPromptPath[], context: { files: ResourceSet; instructions: ResourceSet }, variables: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise { - - for (const instructionPath of instructionFiles) { - // Only include instructions from extension providers (not contributed files) - if (instructionPath.storage !== PromptsStorage.extension) { - continue; - } - - // Check that the source is from a provider, not a contribution - if (instructionPath.source !== ExtensionAgentSourceType.provider) { - continue; - } - - const uri = instructionPath.uri; - - if (context.instructions.has(uri)) { - // the instruction file is already part of the input or has already been processed - this._logService.trace(`[InstructionsContextComputer] Skipping already processed provider instruction file: ${uri}`); - continue; - } - - const parsedFile = await this._parseInstructionsFile(uri, token); - if (!parsedFile) { - this._logService.trace(`[InstructionsContextComputer] Unable to read provider instruction: ${uri}`); - continue; - } - - this._logService.trace(`[InstructionsContextComputer] Adding provider instruction: ${uri}`); - - const reason = localize('instruction.file.reason.provider', 'Automatically attached from extension provider'); - variables.add(toPromptFileVariableEntry(uri, PromptFileVariableKind.Instruction, reason, true)); - telemetryEvent.applyingInstructionsCount++; - } - } - /** public for testing */ public async addApplyingInstructions(instructionFiles: readonly IPromptPath[], context: { files: ResourceSet; instructions: ResourceSet }, variables: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise { From d31d7ba013199a80a458d022f41edcd42319e501 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 31 Dec 2025 15:20:14 -0800 Subject: [PATCH 11/27] use enum --- src/vs/workbench/api/common/extHost.api.impl.ts | 2 +- src/vs/workbench/api/common/extHostChatAgents2.ts | 2 +- .../vscode.proposed.chatParticipantPrivate.d.ts | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index c7e59f8ae0d9b..2e1160a48f32d 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1541,7 +1541,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatContextProvider'); return extHostChatContext.registerChatContextProvider(selector ? checkSelector(selector) : undefined, `${extension.id}-${id}`, provider); }, - registerContributionsProvider(type: string, provider: vscode.ChatContributionsProvider): vscode.Disposable { + registerContributionsProvider(type: vscode.PromptsType, provider: vscode.ChatContributionsProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatParticipantPrivate'); return extHostChatAgents2.registerContributionsProvider(extension, type, provider); }, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 37cf69d148370..11e78eabeb2d9 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -479,7 +479,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS }); } - registerContributionsProvider(extension: IExtensionDescription, type: string, provider: vscode.ChatContributionsProvider): vscode.Disposable { + registerContributionsProvider(extension: IExtensionDescription, type: vscode.PromptsType, provider: vscode.ChatContributionsProvider): vscode.Disposable { const handle = ExtHostChatAgents2._contributionsProviderIdPool++; this._contributionsProviders.set(handle, { extension, provider }); this._proxy.$registerContributionsProvider(handle, type, extension.identifier); diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index bc3dd968e6498..d6ee950763450 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -322,6 +322,12 @@ declare module 'vscode' { // #region ChatContributionsProvider + export enum PromptsType { + instructions = 'instructions', + prompt = 'prompt', + agent = 'agent' + } + /** * Represents a custom agent resource file (e.g., .agent.md or .prompt.md) available for a repository. */ @@ -376,7 +382,7 @@ declare module 'vscode' { * @param provider The custom agents provider. * @returns A disposable that unregisters the provider when disposed. */ - export function registerContributionsProvider(type: string, provider: ChatContributionsProvider): Disposable; + export function registerContributionsProvider(type: PromptsType, provider: ChatContributionsProvider): Disposable; } // #endregion From 3ce36e753c9d28401f6c1a009b1c1e4cde2cc8ff Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 10:53:04 -0800 Subject: [PATCH 12/27] PR --- .../workbench/api/common/extHost.api.impl.ts | 12 +- .../api/common/extHostChatAgents2.ts | 36 +++- ...scode.proposed.chatParticipantPrivate.d.ts | 158 +++++++++++++++--- 3 files changed, 175 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 2e1160a48f32d..f82402f407439 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1541,9 +1541,17 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatContextProvider'); return extHostChatContext.registerChatContextProvider(selector ? checkSelector(selector) : undefined, `${extension.id}-${id}`, provider); }, - registerContributionsProvider(type: vscode.PromptsType, provider: vscode.ChatContributionsProvider): vscode.Disposable { + registerCustomAgentProvider(provider: vscode.CustomAgentProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatParticipantPrivate'); - return extHostChatAgents2.registerContributionsProvider(extension, type, provider); + return extHostChatAgents2.registerPromptFileProvider(extension, 'agent', provider); + }, + registerInstructionsProvider(provider: vscode.InstructionsProvider): vscode.Disposable { + checkProposedApiEnabled(extension, 'chatParticipantPrivate'); + return extHostChatAgents2.registerPromptFileProvider(extension, 'instructions', provider); + }, + registerPromptFileProvider(provider: vscode.PromptFileProvider): vscode.Disposable { + checkProposedApiEnabled(extension, 'chatParticipantPrivate'); + return extHostChatAgents2.registerPromptFileProvider(extension, 'prompt', provider); }, }; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index e131ccda8ba95..b614f6b655c78 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -399,7 +399,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private readonly _relatedFilesProviders = new Map(); private static _contributionsProviderIdPool = 0; - private readonly _contributionsProviders = new Map(); + private readonly _promptFileProviders = new Map(); private readonly _sessionDisposables: DisposableResourceMap = this._register(new DisposableResourceMap()); private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); @@ -479,22 +479,33 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS }); } - registerContributionsProvider(extension: IExtensionDescription, type: vscode.PromptsType, provider: vscode.ChatContributionsProvider): vscode.Disposable { + /** + * Internal method that handles all prompt file provider types. + * Routes custom agents, instructions, and prompt files to the unified internal implementation. + */ + registerPromptFileProvider(extension: IExtensionDescription, type: 'agent' | 'instructions' | 'prompt', provider: vscode.CustomAgentProvider | vscode.InstructionsProvider | vscode.PromptFileProvider): vscode.Disposable { const handle = ExtHostChatAgents2._contributionsProviderIdPool++; - this._contributionsProviders.set(handle, { extension, provider }); + this._promptFileProviders.set(handle, { extension, provider }); this._proxy.$registerContributionsProvider(handle, type, extension.identifier); const disposables = new DisposableStore(); // Listen to provider change events and notify main thread - if (provider.onDidChangeContributions) { - disposables.add(provider.onDidChangeContributions(() => { + // Check for the appropriate event based on the provider type + const changeEvent = type === 'agent' + ? (provider as vscode.CustomAgentProvider).onDidChangeCustomAgents + : type === 'instructions' + ? (provider as vscode.InstructionsProvider).onDidChangeInstructions + : (provider as vscode.PromptFileProvider).onDidChangePromptFiles; + + if (changeEvent) { + disposables.add(changeEvent(() => { this._proxy.$onDidChangeContributions(handle); })); } disposables.add(toDisposable(() => { - this._contributionsProviders.delete(handle); + this._promptFileProviders.delete(handle); this._proxy.$unregisterContributionsProvider(handle); })); @@ -512,12 +523,21 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } async $provideContributions(handle: number, options: IChatContributionQueryOptions, token: CancellationToken): Promise { - const providerData = this._contributionsProviders.get(handle); + const providerData = this._promptFileProviders.get(handle); if (!providerData) { return Promise.resolve(undefined); } - return await providerData.provider.provideContributions(options, token) ?? undefined; + const provider = providerData.provider; + // Call the appropriate method based on the provider type + if ('provideCustomAgents' in provider) { + return await provider.provideCustomAgents(options, token) ?? undefined; + } else if ('provideInstructions' in provider) { + return await provider.provideInstructions(options, token) ?? undefined; + } else if ('providePromptFiles' in provider) { + return await provider.providePromptFiles(options, token) ?? undefined; + } + return undefined; } async $detectChatParticipant(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise { diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index d6ee950763450..992657a5b8f62 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -320,35 +320,29 @@ declare module 'vscode' { // #endregion - // #region ChatContributionsProvider - - export enum PromptsType { - instructions = 'instructions', - prompt = 'prompt', - agent = 'agent' - } + // #region CustomAgentProvider /** - * Represents a custom agent resource file (e.g., .agent.md or .prompt.md) available for a repository. + * Represents a custom agent resource file (e.g., .agent.md) available for a repository. */ - export interface ChatContributionResource { + export interface CustomAgentResource { /** - * The unique identifier/name of the custom agent resource. + * The unique identifier/name of the custom agent. */ readonly name: string; /** - * A description of what the custom agent resource does. + * A description of what the custom agent does. */ readonly description: string; /** - * The URI to the agent or prompt resource file. + * The URI to the custom agent resource file. */ readonly uri: Uri; /** - * Indicates whether the custom agent resource is editable. Defaults to false. + * Indicates whether the custom agent is editable. Defaults to false. */ readonly isEditable?: boolean; } @@ -356,33 +350,155 @@ declare module 'vscode' { /** * Options for querying custom agents. */ - export interface ChatContributionQueryOptions { } + export interface CustomAgentQueryOptions { } /** - * A provider that supplies custom agent resources (from .agent.md and .prompt.md files) for repositories. + * A provider that supplies custom agent resources (from .agent.md files) for repositories. */ - export interface ChatContributionsProvider { + export interface CustomAgentProvider { /** * An optional event to signal that custom agents have changed. */ - readonly onDidChangeContributions?: Event; + readonly onDidChangeCustomAgents?: Event; /** - * Provide the list of custom agent resources available for a given repository. + * Provide the list of custom agents available. * @param options Optional query parameters. * @param token A cancellation token. * @returns An array of custom agent resources or a promise that resolves to such. */ - provideContributions(options: ChatContributionQueryOptions, token: CancellationToken): ProviderResult; + provideCustomAgents(options: CustomAgentQueryOptions, token: CancellationToken): ProviderResult; } + // #endregion + + // #region InstructionsProvider + + /** + * Represents an instructions resource file available for a repository. + */ + export interface InstructionsResource { + /** + * The unique identifier/name of the instructions. + */ + readonly name: string; + + /** + * A description of what the instructions provide. + */ + readonly description: string; + + /** + * The URI to the instructions resource file. + */ + readonly uri: Uri; + + /** + * Indicates whether the instructions are editable. Defaults to false. + */ + readonly isEditable?: boolean; + } + + /** + * Options for querying instructions. + */ + export interface InstructionsQueryOptions { } + + /** + * A provider that supplies instructions resources for repositories. + */ + export interface InstructionsProvider { + /** + * An optional event to signal that instructions have changed. + */ + readonly onDidChangeInstructions?: Event; + + /** + * Provide the list of instructions available. + * @param options Optional query parameters. + * @param token A cancellation token. + * @returns An array of instructions resources or a promise that resolves to such. + */ + provideInstructions(options: InstructionsQueryOptions, token: CancellationToken): ProviderResult; + } + + // #endregion + + // #region PromptFileProvider + + /** + * Represents a prompt file resource (e.g., .prompt.md) available for a repository. + */ + export interface PromptFileResource { + /** + * The unique identifier/name of the prompt file. + */ + readonly name: string; + + /** + * A description of what the prompt file does. + */ + readonly description: string; + + /** + * The URI to the prompt file resource. + */ + readonly uri: Uri; + + /** + * Indicates whether the prompt file is editable. Defaults to false. + */ + readonly isEditable?: boolean; + } + + /** + * Options for querying prompt files. + */ + export interface PromptFileQueryOptions { } + + /** + * A provider that supplies prompt file resources (from .prompt.md files) for repositories. + */ + export interface PromptFileProvider { + /** + * An optional event to signal that prompt files have changed. + */ + readonly onDidChangePromptFiles?: Event; + + /** + * Provide the list of prompt files available. + * @param options Optional query parameters. + * @param token A cancellation token. + * @returns An array of prompt file resources or a promise that resolves to such. + */ + providePromptFiles(options: PromptFileQueryOptions, token: CancellationToken): ProviderResult; + } + + // #endregion + + // #region Chat Provider Registration + export namespace chat { /** * Register a provider for custom agents. - * @param provider The custom agents provider. + * @param provider The custom agent provider. + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerCustomAgentProvider(provider: CustomAgentProvider): Disposable; + + /** + * Register a provider for instructions. + * @param provider The instructions provider. + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerInstructionsProvider(provider: InstructionsProvider): Disposable; + + /** + * Register a provider for prompt files. + * @param provider The prompt file provider. * @returns A disposable that unregisters the provider when disposed. */ - export function registerContributionsProvider(type: PromptsType, provider: ChatContributionsProvider): Disposable; + export function registerPromptFileProvider(provider: PromptFileProvider): Disposable; } // #endregion From 5a44074d634789b4f58943b97437226b5c072aca Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 10:57:32 -0800 Subject: [PATCH 13/27] clean --- .../api/browser/mainThreadChatAgents2.ts | 18 +++++++++--------- .../workbench/api/common/extHost.protocol.ts | 4 ++-- .../workbench/api/common/extHostChatAgents2.ts | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index b90fc25cf2477..694cc7e8cd3d9 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -97,8 +97,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA private readonly _chatRelatedFilesProviders = this._register(new DisposableMap()); - private readonly _contributionsProviders = this._register(new DisposableMap()); - private readonly _contributionsProviderEmitters = this._register(new DisposableMap>()); + private readonly _promptFileProviders = this._register(new DisposableMap()); + private readonly _promptFileProviderEmitters = this._register(new DisposableMap>()); private readonly _pendingProgress = new Map void; chatSession: IChatModel | undefined }>(); private readonly _proxy: ExtHostChatAgentsShape2; @@ -436,7 +436,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._chatRelatedFilesProviders.deleteAndDispose(handle); } - async $registerContributionsProvider(handle: number, type: string, extensionId: ExtensionIdentifier): Promise { + async $registerPromptFileProvider(handle: number, type: string, extensionId: ExtensionIdentifier): Promise { const extension = await this._extensionService.getExtension(extensionId.value); if (!extension) { this._logService.error(`[MainThreadChatAgents2] Could not find extension for ChatContributionsProvider: ${extensionId.value}`); @@ -449,7 +449,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA } const emitter = new Emitter(); - this._contributionsProviderEmitters.set(handle, emitter); + this._promptFileProviderEmitters.set(handle, emitter); const disposable = this._promptsService.registerContributionsProvider(extension, type, { onDidChangeContributions: emitter.event, @@ -466,16 +466,16 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA } }); - this._contributionsProviders.set(handle, disposable); + this._promptFileProviders.set(handle, disposable); } - $unregisterContributionsProvider(handle: number): void { - this._contributionsProviders.deleteAndDispose(handle); - this._contributionsProviderEmitters.deleteAndDispose(handle); + $unregisterPromptFileProvider(handle: number): void { + this._promptFileProviders.deleteAndDispose(handle); + this._promptFileProviderEmitters.deleteAndDispose(handle); } $onDidChangeContributions(handle: number): void { - const emitter = this._contributionsProviderEmitters.get(handle); + const emitter = this._promptFileProviderEmitters.get(handle); if (emitter) { emitter.fire(); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e33e5ed2997a1..d35bfbba438cd 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1394,8 +1394,8 @@ export interface MainThreadChatAgentsShape2 extends IChatAgentProgressShape, IDi $unregisterChatParticipantDetectionProvider(handle: number): void; $registerRelatedFilesProvider(handle: number, metadata: IChatRelatedFilesProviderMetadata): void; $unregisterRelatedFilesProvider(handle: number): void; - $registerContributionsProvider(handle: number, type: string, extension: ExtensionIdentifier): void; - $unregisterContributionsProvider(handle: number): void; + $registerPromptFileProvider(handle: number, type: string, extension: ExtensionIdentifier): void; + $unregisterPromptFileProvider(handle: number): void; $onDidChangeContributions(handle: number): void; $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void; $unregisterAgentCompletionsProvider(handle: number, id: string): void; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index b614f6b655c78..c1a1d7e962475 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -486,7 +486,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS registerPromptFileProvider(extension: IExtensionDescription, type: 'agent' | 'instructions' | 'prompt', provider: vscode.CustomAgentProvider | vscode.InstructionsProvider | vscode.PromptFileProvider): vscode.Disposable { const handle = ExtHostChatAgents2._contributionsProviderIdPool++; this._promptFileProviders.set(handle, { extension, provider }); - this._proxy.$registerContributionsProvider(handle, type, extension.identifier); + this._proxy.$registerPromptFileProvider(handle, type, extension.identifier); const disposables = new DisposableStore(); @@ -506,7 +506,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS disposables.add(toDisposable(() => { this._promptFileProviders.delete(handle); - this._proxy.$unregisterContributionsProvider(handle); + this._proxy.$unregisterPromptFileProvider(handle); })); return disposables; From 846facc37f4ac9f9442312d1614d399f6593b31c Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 11:07:00 -0800 Subject: [PATCH 14/27] more cleanup --- .../api/browser/mainThreadChatAgents2.ts | 10 ++++---- .../workbench/api/common/extHost.protocol.ts | 6 ++--- .../api/common/extHostChatAgents2.ts | 6 ++--- .../promptSyntax/service/promptsService.ts | 23 +++++++++++-------- .../service/promptsServiceImpl.ts | 21 +++++++++-------- .../service/mockPromptsService.ts | 4 ++-- .../service/promptsService.test.ts | 18 +++++++-------- 7 files changed, 47 insertions(+), 41 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 694cc7e8cd3d9..6c7c6a3af6746 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -26,7 +26,7 @@ import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIde import { IChatWidgetService } from '../../contrib/chat/browser/chat.js'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../contrib/chat/browser/attachments/chatDynamicVariables.js'; import { IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../contrib/chat/common/participants/chatAgents.js'; -import { IChatContributionQueryOptions, IPromptsService } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IPromptFileQueryOptions, IPromptsService } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { isValidPromptType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/editing/chatEditingService.js'; import { IChatModel } from '../../contrib/chat/common/model/chatModel.js'; @@ -439,7 +439,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA async $registerPromptFileProvider(handle: number, type: string, extensionId: ExtensionIdentifier): Promise { const extension = await this._extensionService.getExtension(extensionId.value); if (!extension) { - this._logService.error(`[MainThreadChatAgents2] Could not find extension for ChatContributionsProvider: ${extensionId.value}`); + this._logService.error(`[MainThreadChatAgents2] Could not find extension for prompt file provider: ${extensionId.value}`); return; } @@ -451,9 +451,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA const emitter = new Emitter(); this._promptFileProviderEmitters.set(handle, emitter); - const disposable = this._promptsService.registerContributionsProvider(extension, type, { + const disposable = this._promptsService.registerPromptFileProvider(extension, type, { onDidChangeContributions: emitter.event, - provideContributions: async (options: IChatContributionQueryOptions, token: CancellationToken) => { + provideContributions: async (options: IPromptFileQueryOptions, token: CancellationToken) => { const contributions = await this._proxy.$provideContributions(handle, options, token); if (!contributions) { return undefined; @@ -474,7 +474,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._promptFileProviderEmitters.deleteAndDispose(handle); } - $onDidChangeContributions(handle: number): void { + $onDidChangePromptFiles(handle: number): void { const emitter = this._promptFileProviderEmitters.get(handle); if (emitter) { emitter.fire(); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d35bfbba438cd..b300bf1cae121 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -65,7 +65,7 @@ import { IChatRequestVariableValue } from '../../contrib/chat/common/attachments import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponsePart, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatSelector } from '../../contrib/chat/common/languageModels.js'; import { IPreparedToolInvocation, IToolInvocation, IToolInvocationPreparationContext, IToolProgressStep, IToolResult, ToolDataSource } from '../../contrib/chat/common/tools/languageModelToolsService.js'; -import { IChatContributionQueryOptions, IChatContributionResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IPromptFileQueryOptions, IPromptFileResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js'; import * as notebookCommon from '../../contrib/notebook/common/notebookCommon.js'; @@ -1396,7 +1396,7 @@ export interface MainThreadChatAgentsShape2 extends IChatAgentProgressShape, IDi $unregisterRelatedFilesProvider(handle: number): void; $registerPromptFileProvider(handle: number, type: string, extension: ExtensionIdentifier): void; $unregisterPromptFileProvider(handle: number): void; - $onDidChangeContributions(handle: number): void; + $onDidChangePromptFiles(handle: number): void; $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void; $unregisterAgentCompletionsProvider(handle: number, id: string): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; @@ -1462,7 +1462,7 @@ export interface ExtHostChatAgentsShape2 { $releaseSession(sessionResource: UriComponents): void; $detectChatParticipant(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise; $provideRelatedFiles(handle: number, request: Dto, token: CancellationToken): Promise[] | undefined>; - $provideContributions(handle: number, options: IChatContributionQueryOptions, token: CancellationToken): Promise[] | undefined>; + $provideContributions(handle: number, options: IPromptFileQueryOptions, token: CancellationToken): Promise[] | undefined>; $setRequestTools(requestId: string, tools: UserSelectedTools): void; } export interface IChatParticipantMetadata { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index c1a1d7e962475..ac2271cce1310 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -35,7 +35,7 @@ import { ExtHostLanguageModels } from './extHostLanguageModels.js'; import { ExtHostLanguageModelTools } from './extHostLanguageModelTools.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; -import { IChatContributionQueryOptions, IChatContributionResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IPromptFileQueryOptions, IPromptFileResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js'; export class ChatAgentResponseStream { @@ -500,7 +500,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS if (changeEvent) { disposables.add(changeEvent(() => { - this._proxy.$onDidChangeContributions(handle); + this._proxy.$onDidChangePromptFiles(handle); })); } @@ -522,7 +522,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return await provider.provider.provideRelatedFiles(extRequestDraft, token) ?? undefined; } - async $provideContributions(handle: number, options: IChatContributionQueryOptions, token: CancellationToken): Promise { + async $provideContributions(handle: number, options: IPromptFileQueryOptions, token: CancellationToken): Promise { const providerData = this._promptFileProviders.get(handle); if (!providerData) { return Promise.resolve(undefined); 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 b99a0d7573cd6..21690e8c73064 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -16,19 +16,21 @@ import { IHandOff, ParsedPromptFile } from '../promptFileParser.js'; import { ResourceSet } from '../../../../../../base/common/map.js'; /** - * Activation event for chat contributions providers. + * Activation events for prompt file providers. */ -export const CHAT_CONTRIBUTIONS_PROVIDER_ACTIVATION_EVENT = 'onChatContributionsProvider'; +export const CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT = 'onCustomAgentProvider'; +export const INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT = 'onInstructionsProvider'; +export const PROMPT_FILE_PROVIDER_ACTIVATION_EVENT = 'onPromptFileProvider'; /** - * Options for querying chat contributions. + * Options for querying prompt files. */ -export interface IChatContributionQueryOptions { } +export interface IPromptFileQueryOptions { } /** - * Represents a chat contribution resource from an external provider. + * Represents a prompt file resource from an external provider. */ -export interface IChatContributionResource { +export interface IPromptFileResource { /** * The unique identifier/name of the custom agent resource. */ @@ -316,16 +318,17 @@ export interface IPromptsService extends IDisposable { setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void; /** - * Registers a ChatContributionsProvider that can provide chat contributions for repositories. + * Registers a prompt file provider (CustomAgentProvider, InstructionsProvider, or PromptFileProvider) + * that can provide prompt files for repositories. * This is part of the proposed API and requires the chatParticipantPrivate proposal. * @param extension The extension registering the provider. - * @param type The type of contribution (e.g. 'instruction', 'custom agent', 'prompt'). + * @param type The type of contribution (e.g. 'agent', 'instructions', 'prompt'). * @param provider The provider implementation with optional change event. * @returns A disposable that unregisters the provider when disposed. */ - registerContributionsProvider(extension: IExtensionDescription, type: PromptsType, provider: { + registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { onDidChangeContributions?: Event; - provideContributions: (options: IChatContributionQueryOptions, token: CancellationToken) => Promise; + provideContributions: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; }): IDisposable; /** 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 6c26f4a6c0f70..bb526f2a7df71 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -32,7 +32,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, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CHAT_CONTRIBUTIONS_PROVIDER_ACTIVATION_EVENT, IChatContributionQueryOptions, IChatContributionResource } from './promptsService.js'; +import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileQueryOptions, IPromptFileResource } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; @@ -165,22 +165,25 @@ export class PromptsService extends Disposable implements IPromptsService { } /** - * Registry of ChatContributionsProvider instances. Extensions can register providers via the proposed API. + * Registry of prompt file provider instances (custom agents, instructions, prompt files). + * Extensions can register providers via the proposed API. */ private readonly contributionsProviders: Array<{ extension: IExtensionDescription; type: PromptsType; onDidChangeContributions?: Event; - provideContributions: (options: IChatContributionQueryOptions, token: CancellationToken) => Promise; + provideContributions: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; }> = []; /** - * Registers a ChatContributionsProvider. This will be called by the extension host bridge when - * an extension registers a provider via vscode.chat.registerContributionsProvider(). + * Registers a prompt file provider (CustomAgentProvider, InstructionsProvider, or PromptFileProvider). + * This will be called by the extension host bridge when + * an extension registers a provider via vscode.chat.registerCustomAgentProvider(), + * registerInstructionsProvider(), or registerPromptFileProvider(). */ - public registerContributionsProvider(extension: IExtensionDescription, type: PromptsType, provider: { + public registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { onDidChangeContributions?: Event; - provideContributions: (options: IChatContributionQueryOptions, token: CancellationToken) => Promise; + provideContributions: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; }): IDisposable { const providerEntry = { extension, type, ...provider }; this.contributionsProviders.push(providerEntry); @@ -229,7 +232,7 @@ export class PromptsService extends Disposable implements IPromptsService { const result: IPromptPath[] = []; // Activate extensions that might provide custom agents - await this.extensionService.activateByEvent(CHAT_CONTRIBUTIONS_PROVIDER_ACTIVATION_EVENT); + await this.extensionService.activateByEvent(CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT); const providers = this.contributionsProviders.filter(p => p.type === PromptsType.agent); if (providers.length === 0) { @@ -276,7 +279,7 @@ export class PromptsService extends Disposable implements IPromptsService { const result: IPromptPath[] = []; // Activate extensions that might provide instructions - await this.extensionService.activateByEvent(CHAT_CONTRIBUTIONS_PROVIDER_ACTIVATION_EVENT); + await this.extensionService.activateByEvent(INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT); const providers = this.contributionsProviders.filter(p => p.type === PromptsType.instructions); if (providers.length === 0) { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts index a67a68b504bcd..a72ee4722de7c 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts @@ -11,7 +11,7 @@ import { ITextModel } from '../../../../../../../editor/common/model.js'; import { IExtensionDescription } from '../../../../../../../platform/extensions/common/extensions.js'; import { PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; import { ParsedPromptFile } from '../../../../common/promptSyntax/promptFileParser.js'; -import { IAgentSkill, ICustomAgent, IChatContributionQueryOptions, IChatContributionResource, IPromptPath, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { IAgentSkill, ICustomAgent, IPromptFileQueryOptions, IPromptFileResource, IPromptPath, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; import { ResourceSet } from '../../../../../../../base/common/map.js'; export class MockPromptsService implements IPromptsService { @@ -60,7 +60,7 @@ export class MockPromptsService implements IPromptsService { getAgentFileURIFromModeFile(oldURI: URI): URI | undefined { throw new Error('Not implemented'); } getDisabledPromptFiles(type: PromptsType): ResourceSet { throw new Error('Method not implemented.'); } setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void { throw new Error('Method not implemented.'); } - registerContributionsProvider(extension: IExtensionDescription, type: PromptsType, provider: { provideContributions: (options: IChatContributionQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } + registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { provideContributions: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } findAgentSkills(token: CancellationToken): Promise { throw new Error('Method not implemented.'); } dispose(): void { } } 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 f09ba4b5a3032..7757c08058505 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 @@ -36,7 +36,7 @@ import { ComputeAutomaticInstructions, newInstructionsCollectionEvent } from '.. import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js'; import { INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../common/promptSyntax/config/promptFileLocations.js'; import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; -import { ExtensionAgentSourceType, ICustomAgent, IChatContributionQueryOptions, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ExtensionAgentSourceType, ICustomAgent, IPromptFileQueryOptions, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; import { PromptsService } from '../../../../common/promptSyntax/service/promptsServiceImpl.js'; import { mockFiles } from '../testUtils/mockFilesystem.js'; import { InMemoryStorageService, IStorageService } from '../../../../../../../platform/storage/common/storage.js'; @@ -1103,7 +1103,7 @@ suite('PromptsService', () => { ]); const provider = { - provideContributions: async (_options: IChatContributionQueryOptions, _token: CancellationToken) => { + provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { return [ { name: 'myAgent', @@ -1114,7 +1114,7 @@ suite('PromptsService', () => { } }; - const registered = service.registerContributionsProvider(extension, PromptsType.agent, provider); + const registered = service.registerPromptFileProvider(extension, PromptsType.agent, provider); const actual = await service.getCustomAgents(CancellationToken.None); assert.strictEqual(actual.length, 1); @@ -1164,7 +1164,7 @@ suite('PromptsService', () => { ]); const provider = { - provideContributions: async (_options: IChatContributionQueryOptions, _token: CancellationToken) => { + provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { return [ { name: 'readonlyAgent', @@ -1182,7 +1182,7 @@ suite('PromptsService', () => { } }; - const registered = service.registerContributionsProvider(extension, PromptsType.agent, provider); + const registered = service.registerPromptFileProvider(extension, PromptsType.agent, provider); // Spy on updateReadonly to verify it's called correctly const filesConfigService = instaService.get(IFilesConfigurationService); @@ -1279,7 +1279,7 @@ suite('PromptsService', () => { ]); const provider = { - provideContributions: async (_options: IChatContributionQueryOptions, _token: CancellationToken) => { + provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { return [ { name: 'testInstruction', @@ -1290,7 +1290,7 @@ suite('PromptsService', () => { } }; - const registered = service.registerContributionsProvider(extension, PromptsType.instructions, provider); + const registered = service.registerPromptFileProvider(extension, PromptsType.instructions, provider); const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); const providerInstruction = actual.find(i => i.name === 'testInstruction'); @@ -1335,7 +1335,7 @@ suite('PromptsService', () => { ]); const provider = { - provideContributions: async (_options: IChatContributionQueryOptions, _token: CancellationToken) => { + provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { return [ { name: 'readonlyInstruction', @@ -1353,7 +1353,7 @@ suite('PromptsService', () => { } }; - const registered = service.registerContributionsProvider(extension, PromptsType.instructions, provider); + const registered = service.registerPromptFileProvider(extension, PromptsType.instructions, provider); // Spy on updateReadonly to verify it's called correctly const filesConfigService = instaService.get(IFilesConfigurationService); From de3f04c1209e0e093a2d7270804fb7ae72b82d44 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 11:15:52 -0800 Subject: [PATCH 15/27] more cleanup --- src/vs/workbench/api/browser/mainThreadChatAgents2.ts | 2 +- src/vs/workbench/api/common/extHost.api.impl.ts | 7 ++++--- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostChatAgents2.ts | 9 +++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 6c7c6a3af6746..b5366908a3f70 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -454,7 +454,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA const disposable = this._promptsService.registerPromptFileProvider(extension, type, { onDidChangeContributions: emitter.event, provideContributions: async (options: IPromptFileQueryOptions, token: CancellationToken) => { - const contributions = await this._proxy.$provideContributions(handle, options, token); + const contributions = await this._proxy.$providePromptFiles(handle, options, token); if (!contributions) { return undefined; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index f82402f407439..b78d5cf13401e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -23,6 +23,7 @@ import { getRemoteName } from '../../../platform/remote/common/remoteHosts.js'; import { TelemetryTrustedValue } from '../../../platform/telemetry/common/telemetryUtils.js'; import { EditSessionIdentityMatch } from '../../../platform/workspace/common/editSessions.js'; import { DebugConfigurationProviderTriggerKind } from '../../contrib/debug/common/debug.js'; +import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js'; import { UIKind } from '../../services/extensions/common/extensionHostProtocol.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; @@ -1543,15 +1544,15 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, registerCustomAgentProvider(provider: vscode.CustomAgentProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatParticipantPrivate'); - return extHostChatAgents2.registerPromptFileProvider(extension, 'agent', provider); + return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.agent, provider); }, registerInstructionsProvider(provider: vscode.InstructionsProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatParticipantPrivate'); - return extHostChatAgents2.registerPromptFileProvider(extension, 'instructions', provider); + return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.instructions, provider); }, registerPromptFileProvider(provider: vscode.PromptFileProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatParticipantPrivate'); - return extHostChatAgents2.registerPromptFileProvider(extension, 'prompt', provider); + return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.prompt, provider); }, }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b300bf1cae121..9f08196de038c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1462,7 +1462,7 @@ export interface ExtHostChatAgentsShape2 { $releaseSession(sessionResource: UriComponents): void; $detectChatParticipant(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise; $provideRelatedFiles(handle: number, request: Dto, token: CancellationToken): Promise[] | undefined>; - $provideContributions(handle: number, options: IPromptFileQueryOptions, token: CancellationToken): Promise[] | undefined>; + $providePromptFiles(handle: number, options: IPromptFileQueryOptions, token: CancellationToken): Promise[] | undefined>; $setRequestTools(requestId: string, tools: UserSelectedTools): void; } export interface IChatParticipantMetadata { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index ac2271cce1310..9bca45c2ec75f 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -36,6 +36,7 @@ import { ExtHostLanguageModelTools } from './extHostLanguageModelTools.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; import { IPromptFileQueryOptions, IPromptFileResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js'; export class ChatAgentResponseStream { @@ -483,7 +484,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS * Internal method that handles all prompt file provider types. * Routes custom agents, instructions, and prompt files to the unified internal implementation. */ - registerPromptFileProvider(extension: IExtensionDescription, type: 'agent' | 'instructions' | 'prompt', provider: vscode.CustomAgentProvider | vscode.InstructionsProvider | vscode.PromptFileProvider): vscode.Disposable { + registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: vscode.CustomAgentProvider | vscode.InstructionsProvider | vscode.PromptFileProvider): vscode.Disposable { const handle = ExtHostChatAgents2._contributionsProviderIdPool++; this._promptFileProviders.set(handle, { extension, provider }); this._proxy.$registerPromptFileProvider(handle, type, extension.identifier); @@ -492,9 +493,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS // Listen to provider change events and notify main thread // Check for the appropriate event based on the provider type - const changeEvent = type === 'agent' + const changeEvent = type === PromptsType.agent ? (provider as vscode.CustomAgentProvider).onDidChangeCustomAgents - : type === 'instructions' + : type === PromptsType.instructions ? (provider as vscode.InstructionsProvider).onDidChangeInstructions : (provider as vscode.PromptFileProvider).onDidChangePromptFiles; @@ -522,7 +523,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return await provider.provider.provideRelatedFiles(extRequestDraft, token) ?? undefined; } - async $provideContributions(handle: number, options: IPromptFileQueryOptions, token: CancellationToken): Promise { + async $providePromptFiles(handle: number, options: IPromptFileQueryOptions, token: CancellationToken): Promise { const providerData = this._promptFileProviders.get(handle); if (!providerData) { return Promise.resolve(undefined); From bc3dc9e407ea691e20e2d851eefaef6238118d75 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 11:21:29 -0800 Subject: [PATCH 16/27] more cleanup --- .../api/common/extHostChatAgents2.ts | 13 +- .../service/promptsServiceImpl.ts | 60 +++++++++ .../service/promptsService.test.ts | 117 ++++++++++++++++++ 3 files changed, 185 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 9bca45c2ec75f..85d2923092a95 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -493,11 +493,14 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS // Listen to provider change events and notify main thread // Check for the appropriate event based on the provider type - const changeEvent = type === PromptsType.agent - ? (provider as vscode.CustomAgentProvider).onDidChangeCustomAgents - : type === PromptsType.instructions - ? (provider as vscode.InstructionsProvider).onDidChangeInstructions - : (provider as vscode.PromptFileProvider).onDidChangePromptFiles; + let changeEvent: vscode.Event | undefined; + if ('onDidChangeCustomAgents' in provider) { + changeEvent = provider.onDidChangeCustomAgents; + } else if ('onDidChangeInstructions' in provider) { + changeEvent = provider.onDidChangeInstructions; + } else if ('onDidChangePromptFiles' in provider) { + changeEvent = provider.onDidChangePromptFiles; + } if (changeEvent) { disposables.add(changeEvent(() => { 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 bb526f2a7df71..71766c3ee7f33 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -198,6 +198,9 @@ export class PromptsService extends Disposable implements IPromptsService { this.cachedCustomAgents.refresh(); } else if (type === PromptsType.instructions) { this.cachedFileLocations[PromptsType.instructions] = undefined; + } else if (type === PromptsType.prompt) { + this.cachedFileLocations[PromptsType.prompt] = undefined; + this.cachedSlashCommands.refresh(); } })); } @@ -208,6 +211,9 @@ export class PromptsService extends Disposable implements IPromptsService { this.cachedCustomAgents.refresh(); } else if (type === PromptsType.instructions) { this.cachedFileLocations[PromptsType.instructions] = undefined; + } else if (type === PromptsType.prompt) { + this.cachedFileLocations[PromptsType.prompt] = undefined; + this.cachedSlashCommands.refresh(); } disposables.add({ @@ -220,6 +226,9 @@ export class PromptsService extends Disposable implements IPromptsService { this.cachedCustomAgents.refresh(); } else if (type === PromptsType.instructions) { this.cachedFileLocations[PromptsType.instructions] = undefined; + } else if (type === PromptsType.prompt) { + this.cachedFileLocations[PromptsType.prompt] = undefined; + this.cachedSlashCommands.refresh(); } } } @@ -322,6 +331,53 @@ export class PromptsService extends Disposable implements IPromptsService { return result; } + private async listPromptFilesFromProvider(token: CancellationToken): Promise { + const result: IPromptPath[] = []; + + // Activate extensions that might provide prompt files + await this.extensionService.activateByEvent('onPromptFile'); + + const providers = this.contributionsProviders.filter(p => p.type === PromptsType.prompt); + if (providers.length === 0) { + return result; + } + + // Collect prompt files from all providers + for (const providerEntry of providers) { + try { + const promptFiles = await providerEntry.provideContributions({}, token); + if (!promptFiles || token.isCancellationRequested) { + continue; + } + + for (const promptFile of promptFiles) { + if (!promptFile.isEditable) { + try { + await this.filesConfigService.updateReadonly(promptFile.uri, true); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + this.logger.error(`[listPromptFilesFromProvider] Failed to make prompt file readonly: ${promptFile.uri}`, msg); + } + } + + result.push({ + uri: promptFile.uri, + name: promptFile.name, + description: promptFile.description, + storage: PromptsStorage.extension, + type: PromptsType.prompt, + extension: providerEntry.extension, + source: ExtensionAgentSourceType.provider + } satisfies IExtensionPromptPath); + } + } catch (e) { + this.logger.error(`[listPromptFilesFromProvider] Failed to get prompt files from provider`, e instanceof Error ? e.message : String(e)); + } + } + + return result; + } + public async listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise { @@ -348,6 +404,10 @@ export class PromptsService extends Disposable implements IPromptsService { const providerInstructions = await this.listInstructionsFromProvider(token); return [...contributedFiles, ...providerInstructions]; } + if (type === PromptsType.prompt) { + const providerPromptFiles = await this.listPromptFilesFromProvider(token); + return [...contributedFiles, ...providerPromptFiles]; + } return contributedFiles; } 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 7757c08058505..069b224ebea10 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 @@ -1378,6 +1378,123 @@ suite('PromptsService', () => { registered.dispose(); }); + test('Prompt file provider', async () => { + const promptUri = URI.parse('file://extensions/my-extension/myPrompt.prompt.md'); + const extension = { + identifier: { value: 'test.my-extension' }, + enabledApiProposals: ['chatParticipantPrivate'] + } as unknown as IExtensionDescription; + + // Mock the prompt file content + await mockFiles(fileService, [ + { + path: promptUri.path, + contents: [ + '# Test prompt content' + ] + } + ]); + + const provider = { + provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + return [ + { + name: 'testPrompt', + description: 'Test prompt from provider', + uri: promptUri + } + ]; + } + }; + + const registered = service.registerPromptFileProvider(extension, PromptsType.prompt, provider); + + const actual = await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); + const providerPrompt = actual.find(i => i.name === 'testPrompt'); + + assert.ok(providerPrompt, 'Provider prompt should be found'); + assert.strictEqual(providerPrompt!.uri.toString(), promptUri.toString()); + assert.strictEqual(providerPrompt!.name, 'testPrompt'); + assert.strictEqual(providerPrompt!.description, 'Test prompt from provider'); + assert.strictEqual(providerPrompt!.storage, PromptsStorage.extension); + assert.strictEqual(providerPrompt!.source, ExtensionAgentSourceType.provider); + + registered.dispose(); + + // After disposal, the prompt should no longer be listed + const actualAfterDispose = await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); + const foundAfterDispose = actualAfterDispose.find(i => i.name === 'testPrompt'); + assert.strictEqual(foundAfterDispose, undefined); + }); + + test('Prompt file provider with isEditable flag', async () => { + const readonlyPromptUri = URI.parse('file://extensions/my-extension/readonly.prompt.md'); + const editablePromptUri = URI.parse('file://extensions/my-extension/editable.prompt.md'); + const extension = { + identifier: { value: 'test.my-extension' }, + enabledApiProposals: ['chatParticipantPrivate'] + } as unknown as IExtensionDescription; + + // Mock the prompt file content + await mockFiles(fileService, [ + { + path: readonlyPromptUri.path, + contents: [ + '# Readonly prompt content' + ] + }, + { + path: editablePromptUri.path, + contents: [ + '# Editable prompt content' + ] + } + ]); + + const provider = { + provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + return [ + { + name: 'readonlyPrompt', + description: 'Readonly prompt from provider', + uri: readonlyPromptUri, + isEditable: false + }, + { + name: 'editablePrompt', + description: 'Editable prompt from provider', + uri: editablePromptUri, + isEditable: true + } + ]; + } + }; + + const registered = service.registerPromptFileProvider(extension, PromptsType.prompt, 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.prompt, CancellationToken.None); + + // Verify updateReadonly was called only for the non-editable prompt + assert.strictEqual(updateReadonlySpy.callCount, 1, 'updateReadonly should be called once'); + assert.ok(updateReadonlySpy.calledWith(readonlyPromptUri, true), 'updateReadonly should be called with readonly prompt URI and true'); + + const actual = await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); + const readonlyPrompt = actual.find(i => i.name === 'readonlyPrompt'); + const editablePrompt = actual.find(i => i.name === 'editablePrompt'); + + assert.ok(readonlyPrompt, 'Readonly prompt should be found'); + assert.ok(editablePrompt, 'Editable prompt should be found'); + assert.strictEqual(readonlyPrompt!.description, 'Readonly prompt from provider'); + assert.strictEqual(editablePrompt!.description, 'Editable prompt from provider'); + + registered.dispose(); + }); + suite('findAgentSkills', () => { teardown(() => { sinon.restore(); From 50f719a046dcc9d5cdeae6ee3602b4db27908afe Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 11:31:26 -0800 Subject: [PATCH 17/27] more cleanup --- .../api/browser/mainThreadChatAgents2.ts | 4 +-- .../promptSyntax/service/promptsService.ts | 10 +++--- .../service/promptsServiceImpl.ts | 32 +++++++++---------- .../service/mockPromptsService.ts | 2 +- .../service/promptsService.test.ts | 19 ++++------- 5 files changed, 29 insertions(+), 38 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index b5366908a3f70..aa597ca86fcbd 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -452,8 +452,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._promptFileProviderEmitters.set(handle, emitter); const disposable = this._promptsService.registerPromptFileProvider(extension, type, { - onDidChangeContributions: emitter.event, - provideContributions: async (options: IPromptFileQueryOptions, token: CancellationToken) => { + onDidChangePromptFiles: emitter.event, + providePromptFiles: async (options: IPromptFileQueryOptions, token: CancellationToken) => { const contributions = await this._proxy.$providePromptFiles(handle, options, token); if (!contributions) { return undefined; 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 21690e8c73064..3b55e5b36644a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -318,17 +318,15 @@ export interface IPromptsService extends IDisposable { setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void; /** - * Registers a prompt file provider (CustomAgentProvider, InstructionsProvider, or PromptFileProvider) - * that can provide prompt files for repositories. - * This is part of the proposed API and requires the chatParticipantPrivate proposal. + * Registers a prompt file provider that can provide prompt files for repositories. * @param extension The extension registering the provider. - * @param type The type of contribution (e.g. 'agent', 'instructions', 'prompt'). + * @param type The type of contribution. * @param provider The provider implementation with optional change event. * @returns A disposable that unregisters the provider when disposed. */ registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { - onDidChangeContributions?: Event; - provideContributions: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; + onDidChangePromptFiles?: Event; + providePromptFiles: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; }): IDisposable; /** 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 71766c3ee7f33..cef595ec0351a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -168,11 +168,11 @@ export class PromptsService extends Disposable implements IPromptsService { * Registry of prompt file provider instances (custom agents, instructions, prompt files). * Extensions can register providers via the proposed API. */ - private readonly contributionsProviders: Array<{ + private readonly promptFileProviders: Array<{ extension: IExtensionDescription; type: PromptsType; - onDidChangeContributions?: Event; - provideContributions: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; + onDidChangePromptFiles?: Event; + providePromptFiles: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; }> = []; /** @@ -182,17 +182,17 @@ export class PromptsService extends Disposable implements IPromptsService { * registerInstructionsProvider(), or registerPromptFileProvider(). */ public registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { - onDidChangeContributions?: Event; - provideContributions: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; + onDidChangePromptFiles?: Event; + providePromptFiles: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; }): IDisposable { const providerEntry = { extension, type, ...provider }; - this.contributionsProviders.push(providerEntry); + this.promptFileProviders.push(providerEntry); const disposables = new DisposableStore(); // Listen to provider change events to rerun computeListPromptFiles - if (provider.onDidChangeContributions) { - disposables.add(provider.onDidChangeContributions(() => { + if (provider.onDidChangePromptFiles) { + disposables.add(provider.onDidChangePromptFiles(() => { if (type === PromptsType.agent) { this.cachedFileLocations[PromptsType.agent] = undefined; this.cachedCustomAgents.refresh(); @@ -218,9 +218,9 @@ export class PromptsService extends Disposable implements IPromptsService { disposables.add({ dispose: () => { - const index = this.contributionsProviders.findIndex((p) => p === providerEntry); + const index = this.promptFileProviders.findIndex((p) => p === providerEntry); if (index >= 0) { - this.contributionsProviders.splice(index, 1); + this.promptFileProviders.splice(index, 1); if (type === PromptsType.agent) { this.cachedFileLocations[PromptsType.agent] = undefined; this.cachedCustomAgents.refresh(); @@ -243,7 +243,7 @@ export class PromptsService extends Disposable implements IPromptsService { // Activate extensions that might provide custom agents await this.extensionService.activateByEvent(CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT); - const providers = this.contributionsProviders.filter(p => p.type === PromptsType.agent); + const providers = this.promptFileProviders.filter(p => p.type === PromptsType.agent); if (providers.length === 0) { return result; } @@ -251,7 +251,7 @@ export class PromptsService extends Disposable implements IPromptsService { // Collect agents from all providers for (const providerEntry of providers) { try { - const agents = await providerEntry.provideContributions({}, token); + const agents = await providerEntry.providePromptFiles({}, token); if (!agents || token.isCancellationRequested) { continue; } @@ -290,7 +290,7 @@ export class PromptsService extends Disposable implements IPromptsService { // Activate extensions that might provide instructions await this.extensionService.activateByEvent(INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT); - const providers = this.contributionsProviders.filter(p => p.type === PromptsType.instructions); + const providers = this.promptFileProviders.filter(p => p.type === PromptsType.instructions); if (providers.length === 0) { return result; } @@ -298,7 +298,7 @@ export class PromptsService extends Disposable implements IPromptsService { // Collect instructions from all providers for (const providerEntry of providers) { try { - const instructions = await providerEntry.provideContributions({}, token); + const instructions = await providerEntry.providePromptFiles({}, token); if (!instructions || token.isCancellationRequested) { continue; } @@ -337,7 +337,7 @@ export class PromptsService extends Disposable implements IPromptsService { // Activate extensions that might provide prompt files await this.extensionService.activateByEvent('onPromptFile'); - const providers = this.contributionsProviders.filter(p => p.type === PromptsType.prompt); + const providers = this.promptFileProviders.filter(p => p.type === PromptsType.prompt); if (providers.length === 0) { return result; } @@ -345,7 +345,7 @@ export class PromptsService extends Disposable implements IPromptsService { // Collect prompt files from all providers for (const providerEntry of providers) { try { - const promptFiles = await providerEntry.provideContributions({}, token); + const promptFiles = await providerEntry.providePromptFiles({}, token); if (!promptFiles || token.isCancellationRequested) { continue; } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts index a72ee4722de7c..b0aa60546f1d1 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts @@ -60,7 +60,7 @@ export class MockPromptsService implements IPromptsService { getAgentFileURIFromModeFile(oldURI: URI): URI | undefined { throw new Error('Not implemented'); } getDisabledPromptFiles(type: PromptsType): ResourceSet { throw new Error('Method not implemented.'); } setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void { throw new Error('Method not implemented.'); } - registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { provideContributions: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } + registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { providePromptFiles: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } findAgentSkills(token: CancellationToken): Promise { throw new Error('Method not implemented.'); } dispose(): void { } } 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 069b224ebea10..644107a825614 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 @@ -1103,7 +1103,7 @@ suite('PromptsService', () => { ]); const provider = { - provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { return [ { name: 'myAgent', @@ -1122,9 +1122,6 @@ suite('PromptsService', () => { assert.strictEqual(actual[0].description, 'My custom agent from provider'); assert.strictEqual(actual[0].uri.toString(), agentUri.toString()); assert.strictEqual(actual[0].source.storage, PromptsStorage.extension); - if (actual[0].source.storage === PromptsStorage.extension) { - assert.strictEqual(actual[0].source.type, ExtensionAgentSourceType.provider); - } registered.dispose(); @@ -1164,7 +1161,7 @@ suite('PromptsService', () => { ]); const provider = { - provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { return [ { name: 'readonlyAgent', @@ -1188,10 +1185,6 @@ suite('PromptsService', () => { 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'); @@ -1279,7 +1272,7 @@ suite('PromptsService', () => { ]); const provider = { - provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { return [ { name: 'testInstruction', @@ -1335,7 +1328,7 @@ suite('PromptsService', () => { ]); const provider = { - provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { return [ { name: 'readonlyInstruction', @@ -1396,7 +1389,7 @@ suite('PromptsService', () => { ]); const provider = { - provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { return [ { name: 'testPrompt', @@ -1452,7 +1445,7 @@ suite('PromptsService', () => { ]); const provider = { - provideContributions: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { return [ { name: 'readonlyPrompt', From d334ac127874a4215903ecb4838e66c76e99d600 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 11:37:04 -0800 Subject: [PATCH 18/27] more cleanup --- .../test/common/promptSyntax/service/promptsService.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 644107a825614..e543a07d565e2 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 @@ -1185,10 +1185,11 @@ suite('PromptsService', () => { const filesConfigService = instaService.get(IFilesConfigurationService); const updateReadonlySpy = sinon.spy(filesConfigService, 'updateReadonly'); + // Get custom agents to trigger the readonly check + const actual = await service.getCustomAgents(CancellationToken.None); + 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'); From 057840bbd2f53156f681ddf3daaceec64307bfe3 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 11:39:28 -0800 Subject: [PATCH 19/27] nit --- .../common/promptSyntax/service/promptsService.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 e543a07d565e2..ef13cb51a0ce7 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 @@ -1185,11 +1185,14 @@ suite('PromptsService', () => { const filesConfigService = instaService.get(IFilesConfigurationService); const updateReadonlySpy = sinon.spy(filesConfigService, 'updateReadonly'); - // Get custom agents to trigger the readonly check - const actual = await service.getCustomAgents(CancellationToken.None); + // 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'); From d283548160a58f04a26690800eb8623801e47fef Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 13:28:23 -0800 Subject: [PATCH 20/27] add optional metadata --- ...scode.proposed.chatParticipantPrivate.d.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 992657a5b8f62..edd8f510d00dd 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -322,6 +322,16 @@ declare module 'vscode' { // #region CustomAgentProvider + /** + * Private metadata for resource files. + */ + export interface ResourceMetadata { + /** + * The source to be displayed in the resource dropdown. + */ + readonly customSource?: string; + } + /** * Represents a custom agent resource file (e.g., .agent.md) available for a repository. */ @@ -347,6 +357,16 @@ declare module 'vscode' { readonly isEditable?: boolean; } + /** + * Private extension of CustomAgentResource with additional metadata. + */ + export interface CustomAgentResource { + /** + * Optional metadata for the custom agent resource. + */ + readonly metadata?: ResourceMetadata; + } + /** * Options for querying custom agents. */ @@ -399,6 +419,16 @@ declare module 'vscode' { readonly isEditable?: boolean; } + /** + * Private extension of InstructionsResource with additional metadata. + */ + export interface InstructionsResource { + /** + * Optional metadata for the instructions resource. + */ + readonly metadata?: ResourceMetadata; + } + /** * Options for querying instructions. */ @@ -451,6 +481,16 @@ declare module 'vscode' { readonly isEditable?: boolean; } + /** + * Private extension of PromptFileResource with additional metadata. + */ + export interface PromptFileResource { + /** + * Optional metadata for the prompt file resource. + */ + readonly metadata?: ResourceMetadata; + } + /** * Options for querying prompt files. */ From 36346df0334e21def708dbd72c22289aa517feb0 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 14:57:17 -0800 Subject: [PATCH 21/27] use new proposal --- .../common/extensionsApiProposals.ts | 4 + .../workbench/api/common/extHost.api.impl.ts | 6 +- ...scode.proposed.chatParticipantPrivate.d.ts | 223 ------------------ .../vscode.proposed.chatPromptFiles.d.ts | 207 ++++++++++++++++ 4 files changed, 214 insertions(+), 226 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 65bcacd26e622..8f1aa64f97b7d 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -53,6 +53,10 @@ const _allApiProposals = { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', version: 11 }, + chatPromptFiles: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts', + version: 1 + }, chatProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', version: 4 diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index b78d5cf13401e..a77a0079ee02e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1543,15 +1543,15 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostChatContext.registerChatContextProvider(selector ? checkSelector(selector) : undefined, `${extension.id}-${id}`, provider); }, registerCustomAgentProvider(provider: vscode.CustomAgentProvider): vscode.Disposable { - checkProposedApiEnabled(extension, 'chatParticipantPrivate'); + checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.agent, provider); }, registerInstructionsProvider(provider: vscode.InstructionsProvider): vscode.Disposable { - checkProposedApiEnabled(extension, 'chatParticipantPrivate'); + checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.instructions, provider); }, registerPromptFileProvider(provider: vscode.PromptFileProvider): vscode.Disposable { - checkProposedApiEnabled(extension, 'chatParticipantPrivate'); + checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.prompt, provider); }, }; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index edd8f510d00dd..6b6c670a5273e 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -319,227 +319,4 @@ declare module 'vscode' { } // #endregion - - // #region CustomAgentProvider - - /** - * Private metadata for resource files. - */ - export interface ResourceMetadata { - /** - * The source to be displayed in the resource dropdown. - */ - readonly customSource?: string; - } - - /** - * Represents a custom agent resource file (e.g., .agent.md) available for a repository. - */ - export interface CustomAgentResource { - /** - * The unique identifier/name of the custom agent. - */ - readonly name: string; - - /** - * A description of what the custom agent does. - */ - readonly description: string; - - /** - * The URI to the custom agent resource file. - */ - readonly uri: Uri; - - /** - * Indicates whether the custom agent is editable. Defaults to false. - */ - readonly isEditable?: boolean; - } - - /** - * Private extension of CustomAgentResource with additional metadata. - */ - export interface CustomAgentResource { - /** - * Optional metadata for the custom agent resource. - */ - readonly metadata?: ResourceMetadata; - } - - /** - * Options for querying custom agents. - */ - export interface CustomAgentQueryOptions { } - - /** - * A provider that supplies custom agent resources (from .agent.md files) for repositories. - */ - export interface CustomAgentProvider { - /** - * An optional event to signal that custom agents have changed. - */ - readonly onDidChangeCustomAgents?: Event; - - /** - * Provide the list of custom agents available. - * @param options Optional query parameters. - * @param token A cancellation token. - * @returns An array of custom agent resources or a promise that resolves to such. - */ - provideCustomAgents(options: CustomAgentQueryOptions, token: CancellationToken): ProviderResult; - } - - // #endregion - - // #region InstructionsProvider - - /** - * Represents an instructions resource file available for a repository. - */ - export interface InstructionsResource { - /** - * The unique identifier/name of the instructions. - */ - readonly name: string; - - /** - * A description of what the instructions provide. - */ - readonly description: string; - - /** - * The URI to the instructions resource file. - */ - readonly uri: Uri; - - /** - * Indicates whether the instructions are editable. Defaults to false. - */ - readonly isEditable?: boolean; - } - - /** - * Private extension of InstructionsResource with additional metadata. - */ - export interface InstructionsResource { - /** - * Optional metadata for the instructions resource. - */ - readonly metadata?: ResourceMetadata; - } - - /** - * Options for querying instructions. - */ - export interface InstructionsQueryOptions { } - - /** - * A provider that supplies instructions resources for repositories. - */ - export interface InstructionsProvider { - /** - * An optional event to signal that instructions have changed. - */ - readonly onDidChangeInstructions?: Event; - - /** - * Provide the list of instructions available. - * @param options Optional query parameters. - * @param token A cancellation token. - * @returns An array of instructions resources or a promise that resolves to such. - */ - provideInstructions(options: InstructionsQueryOptions, token: CancellationToken): ProviderResult; - } - - // #endregion - - // #region PromptFileProvider - - /** - * Represents a prompt file resource (e.g., .prompt.md) available for a repository. - */ - export interface PromptFileResource { - /** - * The unique identifier/name of the prompt file. - */ - readonly name: string; - - /** - * A description of what the prompt file does. - */ - readonly description: string; - - /** - * The URI to the prompt file resource. - */ - readonly uri: Uri; - - /** - * Indicates whether the prompt file is editable. Defaults to false. - */ - readonly isEditable?: boolean; - } - - /** - * Private extension of PromptFileResource with additional metadata. - */ - export interface PromptFileResource { - /** - * Optional metadata for the prompt file resource. - */ - readonly metadata?: ResourceMetadata; - } - - /** - * Options for querying prompt files. - */ - export interface PromptFileQueryOptions { } - - /** - * A provider that supplies prompt file resources (from .prompt.md files) for repositories. - */ - export interface PromptFileProvider { - /** - * An optional event to signal that prompt files have changed. - */ - readonly onDidChangePromptFiles?: Event; - - /** - * Provide the list of prompt files available. - * @param options Optional query parameters. - * @param token A cancellation token. - * @returns An array of prompt file resources or a promise that resolves to such. - */ - providePromptFiles(options: PromptFileQueryOptions, token: CancellationToken): ProviderResult; - } - - // #endregion - - // #region Chat Provider Registration - - export namespace chat { - /** - * Register a provider for custom agents. - * @param provider The custom agent provider. - * @returns A disposable that unregisters the provider when disposed. - */ - export function registerCustomAgentProvider(provider: CustomAgentProvider): Disposable; - - /** - * Register a provider for instructions. - * @param provider The instructions provider. - * @returns A disposable that unregisters the provider when disposed. - */ - export function registerInstructionsProvider(provider: InstructionsProvider): Disposable; - - /** - * Register a provider for prompt files. - * @param provider The prompt file provider. - * @returns A disposable that unregisters the provider when disposed. - */ - export function registerPromptFileProvider(provider: PromptFileProvider): Disposable; - } - - // #endregion } diff --git a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts new file mode 100644 index 0000000000000..ff16b2830a6f4 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// version: 1 + +declare module 'vscode' { + + // #region CustomAgentProvider + + /** + * Represents a custom agent resource file (e.g., .agent.md) available for a repository. + */ + export interface CustomAgentResource { + /** + * The unique identifier/name of the custom agent. + */ + readonly name: string; + + /** + * A description of what the custom agent does. + */ + readonly description: string; + + /** + * The URI to the custom agent resource file. + */ + readonly uri: Uri; + + /** + * Indicates whether the custom agent is editable. Defaults to false. + */ + readonly isEditable?: boolean; + + /** + * Indicates whether the editor should cache and use the URI on startup before activation. Defaults to true. + */ + readonly isCacheable?: boolean; + } + + /** + * Options for querying custom agents. + */ + export type CustomAgentQueryOptions = object; + + /** + * A provider that supplies custom agent resources (from .agent.md files) for repositories. + */ + export interface CustomAgentProvider { + /** + * An optional event to signal that custom agents have changed. + */ + readonly onDidChangeCustomAgents?: Event; + + /** + * Provide the list of custom agents available. + * @param options Optional query parameters. + * @param token A cancellation token. + * @returns An array of custom agent resources or a promise that resolves to such. + */ + provideCustomAgents(options: CustomAgentQueryOptions, token: CancellationToken): ProviderResult; + } + + // #endregion + + // #region InstructionsProvider + + /** + * Represents an instructions resource file available for a repository. + */ + export interface InstructionsResource { + /** + * The unique identifier/name of the instructions. + */ + readonly name: string; + + /** + * A description of what the instructions provide. + */ + readonly description: string; + + /** + * The URI to the instructions resource file. + */ + readonly uri: Uri; + + /** + * Indicates whether the instructions are editable. Defaults to false. + */ + readonly isEditable?: boolean; + + /** + * Indicates whether the editor should cache and use the URI on startup before activation. Defaults to true. + */ + readonly isCacheable?: boolean; + } + + /** + * Options for querying instructions. + */ + export type InstructionsQueryOptions = object; + + /** + * A provider that supplies instructions resources for repositories. + */ + export interface InstructionsProvider { + /** + * An optional event to signal that instructions have changed. + */ + readonly onDidChangeInstructions?: Event; + + /** + * Provide the list of instructions available. + * @param options Optional query parameters. + * @param token A cancellation token. + * @returns An array of instructions resources or a promise that resolves to such. + */ + provideInstructions(options: InstructionsQueryOptions, token: CancellationToken): ProviderResult; + } + + // #endregion + + // #region PromptFileProvider + + /** + * Represents a prompt file resource (e.g., .prompt.md) available for a repository. + */ + export interface PromptFileResource { + /** + * The unique identifier/name of the prompt file. + */ + readonly name: string; + + /** + * A description of what the prompt file does. + */ + readonly description: string; + + /** + * The URI to the prompt file resource. + */ + readonly uri: Uri; + + /** + * Indicates whether the prompt file is editable. Defaults to false. + */ + readonly isEditable?: boolean; + + /** + * Indicates whether the editor should cache and use and use the URI on startup before activation. Defaults to true. + */ + readonly isCacheable?: boolean; + } + + /** + * Options for querying prompt files. + */ + export type PromptFileQueryOptions = object; + + /** + * A provider that supplies prompt file resources (from .prompt.md files) for repositories. + */ + export interface PromptFileProvider { + /** + * An optional event to signal that prompt files have changed. + */ + readonly onDidChangePromptFiles?: Event; + + /** + * Provide the list of prompt files available. + * @param options Optional query parameters. + * @param token A cancellation token. + * @returns An array of prompt file resources or a promise that resolves to such. + */ + providePromptFiles(options: PromptFileQueryOptions, token: CancellationToken): ProviderResult; + } + + // #endregion + + // #region Chat Provider Registration + + export namespace chat { + /** + * Register a provider for custom agents. + * @param provider The custom agent provider. + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerCustomAgentProvider(provider: CustomAgentProvider): Disposable; + + /** + * Register a provider for instructions. + * @param provider The instructions provider. + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerInstructionsProvider(provider: InstructionsProvider): Disposable; + + /** + * Register a provider for prompt files. + * @param provider The prompt file provider. + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerPromptFileProvider(provider: PromptFileProvider): Disposable; + } + + // #endregion +} From 2617d682010f53affea470066dff8094c27dc7cc Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 15:26:19 -0800 Subject: [PATCH 22/27] clean --- .../service/promptsServiceImpl.ts | 161 +++++------------- 1 file changed, 39 insertions(+), 122 deletions(-) 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 cef595ec0351a..6ace470951ac1 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -32,7 +32,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, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileQueryOptions, IPromptFileResource } from './promptsService.js'; +import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileQueryOptions, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; @@ -237,141 +237,50 @@ export class PromptsService extends Disposable implements IPromptsService { return disposables; } - private async listCustomAgentsFromProvider(token: CancellationToken): Promise { - const result: IPromptPath[] = []; - - // Activate extensions that might provide custom agents - await this.extensionService.activateByEvent(CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT); - - const providers = this.promptFileProviders.filter(p => p.type === PromptsType.agent); - if (providers.length === 0) { - return result; - } - - // Collect agents from all providers - for (const providerEntry of providers) { - try { - const agents = await providerEntry.providePromptFiles({}, token); - if (!agents || token.isCancellationRequested) { - continue; - } - - for (const agent of agents) { - 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({ - uri: agent.uri, - name: agent.name, - description: agent.description, - storage: PromptsStorage.extension, - type: PromptsType.agent, - extension: providerEntry.extension, - source: ExtensionAgentSourceType.provider - } satisfies IExtensionPromptPath); - } - } catch (e) { - this.logger.error(`[listCustomAgentsFromProvider] Failed to get custom agents from provider`, e instanceof Error ? e.message : String(e)); - } - } - - return result; - } - - private async listInstructionsFromProvider(token: CancellationToken): Promise { - const result: IPromptPath[] = []; - - // Activate extensions that might provide instructions - await this.extensionService.activateByEvent(INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT); - - const providers = this.promptFileProviders.filter(p => p.type === PromptsType.instructions); - if (providers.length === 0) { - return result; - } - - // Collect instructions from all providers - for (const providerEntry of providers) { - try { - const instructions = await providerEntry.providePromptFiles({}, token); - if (!instructions || token.isCancellationRequested) { - continue; - } - - for (const instruction of instructions) { - if (!instruction.isEditable) { - try { - await this.filesConfigService.updateReadonly(instruction.uri, true); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - this.logger.error(`[listInstructionsFromProvider] Failed to make instruction file readonly: ${instruction.uri}`, msg); - } - } - - result.push({ - uri: instruction.uri, - name: instruction.name, - description: instruction.description, - storage: PromptsStorage.extension, - type: PromptsType.instructions, - extension: providerEntry.extension, - source: ExtensionAgentSourceType.provider - } satisfies IExtensionPromptPath); - } - } catch (e) { - this.logger.error(`[listInstructionsFromProvider] Failed to get instructions from provider`, e instanceof Error ? e.message : String(e)); - } - } - - return result; - } - - private async listPromptFilesFromProvider(token: CancellationToken): Promise { + /** + * Shared helper to list prompt files from registered providers for a given type. + */ + private async listFromProviders(type: PromptsType, activationEvent: string, token: CancellationToken): Promise { const result: IPromptPath[] = []; - // Activate extensions that might provide prompt files - await this.extensionService.activateByEvent('onPromptFile'); + // Activate extensions that might provide files for this type + await this.extensionService.activateByEvent(activationEvent); - const providers = this.promptFileProviders.filter(p => p.type === PromptsType.prompt); + const providers = this.promptFileProviders.filter(p => p.type === type); if (providers.length === 0) { return result; } - // Collect prompt files from all providers + // Collect files from all providers for (const providerEntry of providers) { try { - const promptFiles = await providerEntry.providePromptFiles({}, token); - if (!promptFiles || token.isCancellationRequested) { + const files = await providerEntry.providePromptFiles({}, token); + if (!files || token.isCancellationRequested) { continue; } - for (const promptFile of promptFiles) { - if (!promptFile.isEditable) { + for (const file of files) { + if (!file.isEditable) { try { - await this.filesConfigService.updateReadonly(promptFile.uri, true); + await this.filesConfigService.updateReadonly(file.uri, true); } catch (e) { const msg = e instanceof Error ? e.message : String(e); - this.logger.error(`[listPromptFilesFromProvider] Failed to make prompt file readonly: ${promptFile.uri}`, msg); + this.logger.error(`[listFromProviders] Failed to make file readonly: ${file.uri}`, msg); } } result.push({ - uri: promptFile.uri, - name: promptFile.name, - description: promptFile.description, + uri: file.uri, + name: file.name, + description: file.description, storage: PromptsStorage.extension, - type: PromptsType.prompt, + type, extension: providerEntry.extension, source: ExtensionAgentSourceType.provider } satisfies IExtensionPromptPath); } } catch (e) { - this.logger.error(`[listPromptFilesFromProvider] Failed to get prompt files from provider`, e instanceof Error ? e.message : String(e)); + this.logger.error(`[listFromProviders] Failed to get ${type} files from provider`, e instanceof Error ? e.message : String(e)); } } @@ -396,21 +305,29 @@ export class PromptsService extends Disposable implements IPromptsService { private async getExtensionPromptFiles(type: PromptsType, token: CancellationToken): Promise { await this.extensionService.whenInstalledExtensionsRegistered(); const contributedFiles = await Promise.all(this.contributedFiles[type].values()); - if (type === PromptsType.agent) { - const providerAgents = await this.listCustomAgentsFromProvider(token); - return [...contributedFiles, ...providerAgents]; - } - if (type === PromptsType.instructions) { - const providerInstructions = await this.listInstructionsFromProvider(token); - return [...contributedFiles, ...providerInstructions]; - } - if (type === PromptsType.prompt) { - const providerPromptFiles = await this.listPromptFilesFromProvider(token); - return [...contributedFiles, ...providerPromptFiles]; + + const activationEvent = this.getProviderActivationEvent(type); + if (activationEvent) { + const providerFiles = await this.listFromProviders(type, activationEvent, token); + return [...contributedFiles, ...providerFiles]; } + return contributedFiles; } + private getProviderActivationEvent(type: PromptsType): string | undefined { + switch (type) { + case PromptsType.agent: + return CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT; + case PromptsType.instructions: + return INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT; + case PromptsType.prompt: + return PROMPT_FILE_PROVIDER_ACTIVATION_EVENT; + default: + return undefined; + } + } + public getSourceFolders(type: PromptsType): readonly IPromptPath[] { const result: IPromptPath[] = []; From 44f55015ee3bd3088465ff7d0567f69bb36f45c4 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 15:29:14 -0800 Subject: [PATCH 23/27] clean --- .../vscode.proposed.chatPromptFiles.d.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts index ff16b2830a6f4..90e33a55a226c 100644 --- a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts +++ b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts @@ -32,11 +32,6 @@ declare module 'vscode' { * Indicates whether the custom agent is editable. Defaults to false. */ readonly isEditable?: boolean; - - /** - * Indicates whether the editor should cache and use the URI on startup before activation. Defaults to true. - */ - readonly isCacheable?: boolean; } /** @@ -89,11 +84,6 @@ declare module 'vscode' { * Indicates whether the instructions are editable. Defaults to false. */ readonly isEditable?: boolean; - - /** - * Indicates whether the editor should cache and use the URI on startup before activation. Defaults to true. - */ - readonly isCacheable?: boolean; } /** @@ -146,11 +136,6 @@ declare module 'vscode' { * Indicates whether the prompt file is editable. Defaults to false. */ readonly isEditable?: boolean; - - /** - * Indicates whether the editor should cache and use and use the URI on startup before activation. Defaults to true. - */ - readonly isCacheable?: boolean; } /** From f4e9bc45294cc3618a1e7182595608891e65b45a Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 7 Jan 2026 15:33:20 -0800 Subject: [PATCH 24/27] nit --- .../chat/common/promptSyntax/service/promptsServiceImpl.ts | 2 ++ 1 file changed, 2 insertions(+) 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 6ace470951ac1..3c6735313eb98 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -310,6 +310,8 @@ export class PromptsService extends Disposable implements IPromptsService { if (activationEvent) { const providerFiles = await this.listFromProviders(type, activationEvent, token); return [...contributedFiles, ...providerFiles]; + } else { + this.logger.warn(`[getExtensionPromptFiles] No activation event found for prompt type: ${type}`); } return contributedFiles; From a5eec28cfb6292b823961c57192d7874fcbe3eaa Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 8 Jan 2026 09:25:15 -0800 Subject: [PATCH 25/27] Update src/vs/workbench/api/common/extHostChatAgents2.ts Co-authored-by: Martin Aeschlimann --- src/vs/workbench/api/common/extHostChatAgents2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 85d2923092a95..dd9a5ae6cdcaa 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -529,7 +529,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS async $providePromptFiles(handle: number, options: IPromptFileQueryOptions, token: CancellationToken): Promise { const providerData = this._promptFileProviders.get(handle); if (!providerData) { - return Promise.resolve(undefined); + return undefined; } const provider = providerData.provider; From c50fcb8418637b3d61afdcfaa634018878d53a0d Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Thu, 8 Jan 2026 09:43:37 -0800 Subject: [PATCH 26/27] PR --- .../api/browser/mainThreadChatAgents2.ts | 6 +- .../workbench/api/common/extHost.protocol.ts | 4 +- .../api/common/extHostChatAgents2.ts | 10 +-- .../promptSyntax/service/promptsService.ts | 16 +---- .../service/promptsServiceImpl.ts | 22 ++---- .../service/mockPromptsService.ts | 4 +- .../service/promptsService.test.ts | 56 ++++----------- .../vscode.proposed.chatPromptFiles.d.ts | 69 ++++++++----------- 8 files changed, 63 insertions(+), 124 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index aa597ca86fcbd..39dc483db83e2 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -26,7 +26,7 @@ import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIde import { IChatWidgetService } from '../../contrib/chat/browser/chat.js'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../contrib/chat/browser/attachments/chatDynamicVariables.js'; import { IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../contrib/chat/common/participants/chatAgents.js'; -import { IPromptFileQueryOptions, IPromptsService } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IPromptFileContext, IPromptsService } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { isValidPromptType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/editing/chatEditingService.js'; import { IChatModel } from '../../contrib/chat/common/model/chatModel.js'; @@ -453,8 +453,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA const disposable = this._promptsService.registerPromptFileProvider(extension, type, { onDidChangePromptFiles: emitter.event, - providePromptFiles: async (options: IPromptFileQueryOptions, token: CancellationToken) => { - const contributions = await this._proxy.$providePromptFiles(handle, options, token); + providePromptFiles: async (context: IPromptFileContext, token: CancellationToken) => { + const contributions = await this._proxy.$providePromptFiles(handle, context, token); if (!contributions) { return undefined; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9f08196de038c..29474a4281b56 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -65,7 +65,7 @@ import { IChatRequestVariableValue } from '../../contrib/chat/common/attachments import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponsePart, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatSelector } from '../../contrib/chat/common/languageModels.js'; import { IPreparedToolInvocation, IToolInvocation, IToolInvocationPreparationContext, IToolProgressStep, IToolResult, ToolDataSource } from '../../contrib/chat/common/tools/languageModelToolsService.js'; -import { IPromptFileQueryOptions, IPromptFileResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IPromptFileContext, IPromptFileResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js'; import * as notebookCommon from '../../contrib/notebook/common/notebookCommon.js'; @@ -1462,7 +1462,7 @@ export interface ExtHostChatAgentsShape2 { $releaseSession(sessionResource: UriComponents): void; $detectChatParticipant(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise; $provideRelatedFiles(handle: number, request: Dto, token: CancellationToken): Promise[] | undefined>; - $providePromptFiles(handle: number, options: IPromptFileQueryOptions, token: CancellationToken): Promise[] | undefined>; + $providePromptFiles(handle: number, context: IPromptFileContext, token: CancellationToken): Promise[] | undefined>; $setRequestTools(requestId: string, tools: UserSelectedTools): void; } export interface IChatParticipantMetadata { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 85d2923092a95..55baeb3792515 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -35,7 +35,7 @@ import { ExtHostLanguageModels } from './extHostLanguageModels.js'; import { ExtHostLanguageModelTools } from './extHostLanguageModelTools.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; -import { IPromptFileQueryOptions, IPromptFileResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IPromptFileContext, IPromptFileResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js'; @@ -526,7 +526,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return await provider.provider.provideRelatedFiles(extRequestDraft, token) ?? undefined; } - async $providePromptFiles(handle: number, options: IPromptFileQueryOptions, token: CancellationToken): Promise { + async $providePromptFiles(handle: number, context: IPromptFileContext, token: CancellationToken): Promise { const providerData = this._promptFileProviders.get(handle); if (!providerData) { return Promise.resolve(undefined); @@ -535,11 +535,11 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const provider = providerData.provider; // Call the appropriate method based on the provider type if ('provideCustomAgents' in provider) { - return await provider.provideCustomAgents(options, token) ?? undefined; + return await provider.provideCustomAgents(context, token) ?? undefined; } else if ('provideInstructions' in provider) { - return await provider.provideInstructions(options, token) ?? undefined; + return await provider.provideInstructions(context, token) ?? undefined; } else if ('providePromptFiles' in provider) { - return await provider.providePromptFiles(options, token) ?? undefined; + return await provider.providePromptFiles(context, token) ?? undefined; } return undefined; } 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 3b55e5b36644a..7d7f205164543 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -23,24 +23,14 @@ export const INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT = 'onInstructionsProvider'; export const PROMPT_FILE_PROVIDER_ACTIVATION_EVENT = 'onPromptFileProvider'; /** - * Options for querying prompt files. + * Context for querying prompt files. */ -export interface IPromptFileQueryOptions { } +export interface IPromptFileContext { } /** * Represents a prompt file resource from an external provider. */ export interface IPromptFileResource { - /** - * The unique identifier/name of the custom agent resource. - */ - readonly name: string; - - /** - * A description of what the custom agent resource does. - */ - readonly description: string; - /** * The URI to the agent or prompt resource file. */ @@ -326,7 +316,7 @@ export interface IPromptsService extends IDisposable { */ registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { onDidChangePromptFiles?: Event; - providePromptFiles: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; + providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise; }): IDisposable; /** 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 3c6735313eb98..63a78454b07db 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -32,7 +32,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, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileQueryOptions, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT } from './promptsService.js'; +import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; @@ -172,7 +172,7 @@ export class PromptsService extends Disposable implements IPromptsService { extension: IExtensionDescription; type: PromptsType; onDidChangePromptFiles?: Event; - providePromptFiles: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; + providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise; }> = []; /** @@ -183,7 +183,7 @@ export class PromptsService extends Disposable implements IPromptsService { */ public registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { onDidChangePromptFiles?: Event; - providePromptFiles: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise; + providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise; }): IDisposable { const providerEntry = { extension, type, ...provider }; this.promptFileProviders.push(providerEntry); @@ -271,8 +271,6 @@ export class PromptsService extends Disposable implements IPromptsService { result.push({ uri: file.uri, - name: file.name, - description: file.description, storage: PromptsStorage.extension, type, extension: providerEntry.extension, @@ -307,17 +305,11 @@ export class PromptsService extends Disposable implements IPromptsService { const contributedFiles = await Promise.all(this.contributedFiles[type].values()); const activationEvent = this.getProviderActivationEvent(type); - if (activationEvent) { - const providerFiles = await this.listFromProviders(type, activationEvent, token); - return [...contributedFiles, ...providerFiles]; - } else { - this.logger.warn(`[getExtensionPromptFiles] No activation event found for prompt type: ${type}`); - } - - return contributedFiles; + const providerFiles = await this.listFromProviders(type, activationEvent, token); + return [...contributedFiles, ...providerFiles]; } - private getProviderActivationEvent(type: PromptsType): string | undefined { + private getProviderActivationEvent(type: PromptsType): string { switch (type) { case PromptsType.agent: return CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT; @@ -325,8 +317,6 @@ export class PromptsService extends Disposable implements IPromptsService { return INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT; case PromptsType.prompt: return PROMPT_FILE_PROVIDER_ACTIVATION_EVENT; - default: - return undefined; } } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts index b0aa60546f1d1..f71149b121891 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts @@ -11,7 +11,7 @@ import { ITextModel } from '../../../../../../../editor/common/model.js'; import { IExtensionDescription } from '../../../../../../../platform/extensions/common/extensions.js'; import { PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; import { ParsedPromptFile } from '../../../../common/promptSyntax/promptFileParser.js'; -import { IAgentSkill, ICustomAgent, IPromptFileQueryOptions, IPromptFileResource, IPromptPath, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { IAgentSkill, ICustomAgent, IPromptFileContext, IPromptFileResource, IPromptPath, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; import { ResourceSet } from '../../../../../../../base/common/map.js'; export class MockPromptsService implements IPromptsService { @@ -60,7 +60,7 @@ export class MockPromptsService implements IPromptsService { getAgentFileURIFromModeFile(oldURI: URI): URI | undefined { throw new Error('Not implemented'); } getDisabledPromptFiles(type: PromptsType): ResourceSet { throw new Error('Method not implemented.'); } setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void { throw new Error('Method not implemented.'); } - registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { providePromptFiles: (options: IPromptFileQueryOptions, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } + registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } findAgentSkills(token: CancellationToken): Promise { throw new Error('Method not implemented.'); } dispose(): void { } } 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 ef13cb51a0ce7..55845c7bcc19a 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 @@ -36,7 +36,7 @@ import { ComputeAutomaticInstructions, newInstructionsCollectionEvent } from '.. import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js'; import { INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../common/promptSyntax/config/promptFileLocations.js'; import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; -import { ExtensionAgentSourceType, ICustomAgent, IPromptFileQueryOptions, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ExtensionAgentSourceType, ICustomAgent, IPromptFileContext, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; import { PromptsService } from '../../../../common/promptSyntax/service/promptsServiceImpl.js'; import { mockFiles } from '../testUtils/mockFilesystem.js'; import { InMemoryStorageService, IStorageService } from '../../../../../../../platform/storage/common/storage.js'; @@ -1103,11 +1103,9 @@ suite('PromptsService', () => { ]); const provider = { - providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { return [ { - name: 'myAgent', - description: 'My custom agent from provider', uri: agentUri } ]; @@ -1161,17 +1159,13 @@ suite('PromptsService', () => { ]); const provider = { - providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_context: IPromptFileContext, _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 } @@ -1276,11 +1270,9 @@ suite('PromptsService', () => { ]); const provider = { - providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { return [ { - name: 'testInstruction', - description: 'Test instruction from provider', uri: instructionUri } ]; @@ -1290,12 +1282,10 @@ suite('PromptsService', () => { const registered = service.registerPromptFileProvider(extension, PromptsType.instructions, provider); const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - const providerInstruction = actual.find(i => i.name === 'testInstruction'); + const providerInstruction = actual.find(i => i.uri.toString() === instructionUri.toString()); assert.ok(providerInstruction, 'Provider instruction should be found'); assert.strictEqual(providerInstruction!.uri.toString(), instructionUri.toString()); - assert.strictEqual(providerInstruction!.name, 'testInstruction'); - assert.strictEqual(providerInstruction!.description, 'Test instruction from provider'); assert.strictEqual(providerInstruction!.storage, PromptsStorage.extension); assert.strictEqual(providerInstruction!.source, ExtensionAgentSourceType.provider); @@ -1303,7 +1293,7 @@ suite('PromptsService', () => { // After disposal, the instruction should no longer be listed const actualAfterDispose = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - const foundAfterDispose = actualAfterDispose.find(i => i.name === 'testInstruction'); + const foundAfterDispose = actualAfterDispose.find(i => i.uri.toString() === instructionUri.toString()); assert.strictEqual(foundAfterDispose, undefined); }); @@ -1332,17 +1322,13 @@ suite('PromptsService', () => { ]); const provider = { - providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { return [ { - name: 'readonlyInstruction', - description: 'Readonly instruction from provider', uri: readonlyInstructionUri, isEditable: false }, { - name: 'editableInstruction', - description: 'Editable instruction from provider', uri: editableInstructionUri, isEditable: true } @@ -1364,13 +1350,11 @@ suite('PromptsService', () => { assert.ok(updateReadonlySpy.calledWith(readonlyInstructionUri, true), 'updateReadonly should be called with readonly instruction URI and true'); const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - const readonlyInstruction = actual.find(i => i.name === 'readonlyInstruction'); - const editableInstruction = actual.find(i => i.name === 'editableInstruction'); + const readonlyInstruction = actual.find(i => i.uri.toString() === readonlyInstructionUri.toString()); + const editableInstruction = actual.find(i => i.uri.toString() === editableInstructionUri.toString()); assert.ok(readonlyInstruction, 'Readonly instruction should be found'); assert.ok(editableInstruction, 'Editable instruction should be found'); - assert.strictEqual(readonlyInstruction!.description, 'Readonly instruction from provider'); - assert.strictEqual(editableInstruction!.description, 'Editable instruction from provider'); registered.dispose(); }); @@ -1393,11 +1377,9 @@ suite('PromptsService', () => { ]); const provider = { - providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { return [ { - name: 'testPrompt', - description: 'Test prompt from provider', uri: promptUri } ]; @@ -1407,12 +1389,10 @@ suite('PromptsService', () => { const registered = service.registerPromptFileProvider(extension, PromptsType.prompt, provider); const actual = await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); - const providerPrompt = actual.find(i => i.name === 'testPrompt'); + const providerPrompt = actual.find(i => i.uri.toString() === promptUri.toString()); assert.ok(providerPrompt, 'Provider prompt should be found'); assert.strictEqual(providerPrompt!.uri.toString(), promptUri.toString()); - assert.strictEqual(providerPrompt!.name, 'testPrompt'); - assert.strictEqual(providerPrompt!.description, 'Test prompt from provider'); assert.strictEqual(providerPrompt!.storage, PromptsStorage.extension); assert.strictEqual(providerPrompt!.source, ExtensionAgentSourceType.provider); @@ -1420,7 +1400,7 @@ suite('PromptsService', () => { // After disposal, the prompt should no longer be listed const actualAfterDispose = await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); - const foundAfterDispose = actualAfterDispose.find(i => i.name === 'testPrompt'); + const foundAfterDispose = actualAfterDispose.find(i => i.uri.toString() === promptUri.toString()); assert.strictEqual(foundAfterDispose, undefined); }); @@ -1449,17 +1429,13 @@ suite('PromptsService', () => { ]); const provider = { - providePromptFiles: async (_options: IPromptFileQueryOptions, _token: CancellationToken) => { + providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { return [ { - name: 'readonlyPrompt', - description: 'Readonly prompt from provider', uri: readonlyPromptUri, isEditable: false }, { - name: 'editablePrompt', - description: 'Editable prompt from provider', uri: editablePromptUri, isEditable: true } @@ -1481,13 +1457,11 @@ suite('PromptsService', () => { assert.ok(updateReadonlySpy.calledWith(readonlyPromptUri, true), 'updateReadonly should be called with readonly prompt URI and true'); const actual = await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); - const readonlyPrompt = actual.find(i => i.name === 'readonlyPrompt'); - const editablePrompt = actual.find(i => i.name === 'editablePrompt'); + const readonlyPrompt = actual.find(i => i.uri.toString() === readonlyPromptUri.toString()); + const editablePrompt = actual.find(i => i.uri.toString() === editablePromptUri.toString()); assert.ok(readonlyPrompt, 'Readonly prompt should be found'); assert.ok(editablePrompt, 'Editable prompt should be found'); - assert.strictEqual(readonlyPrompt!.description, 'Readonly prompt from provider'); - assert.strictEqual(editablePrompt!.description, 'Editable prompt from provider'); registered.dispose(); }); diff --git a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts index 90e33a55a226c..8a9017558070c 100644 --- a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts +++ b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts @@ -13,16 +13,6 @@ declare module 'vscode' { * Represents a custom agent resource file (e.g., .agent.md) available for a repository. */ export interface CustomAgentResource { - /** - * The unique identifier/name of the custom agent. - */ - readonly name: string; - - /** - * A description of what the custom agent does. - */ - readonly description: string; - /** * The URI to the custom agent resource file. */ @@ -35,14 +25,19 @@ declare module 'vscode' { } /** - * Options for querying custom agents. + * Context for querying custom agents. */ - export type CustomAgentQueryOptions = object; + export type CustomAgentContext = object; /** * A provider that supplies custom agent resources (from .agent.md files) for repositories. */ export interface CustomAgentProvider { + /** + * A human-readable label for this provider. + */ + readonly label: string; + /** * An optional event to signal that custom agents have changed. */ @@ -50,11 +45,11 @@ declare module 'vscode' { /** * Provide the list of custom agents available. - * @param options Optional query parameters. + * @param context Context for the query. * @param token A cancellation token. * @returns An array of custom agent resources or a promise that resolves to such. */ - provideCustomAgents(options: CustomAgentQueryOptions, token: CancellationToken): ProviderResult; + provideCustomAgents(context: CustomAgentContext, token: CancellationToken): ProviderResult; } // #endregion @@ -65,16 +60,6 @@ declare module 'vscode' { * Represents an instructions resource file available for a repository. */ export interface InstructionsResource { - /** - * The unique identifier/name of the instructions. - */ - readonly name: string; - - /** - * A description of what the instructions provide. - */ - readonly description: string; - /** * The URI to the instructions resource file. */ @@ -87,14 +72,19 @@ declare module 'vscode' { } /** - * Options for querying instructions. + * Context for querying instructions. */ - export type InstructionsQueryOptions = object; + export type InstructionsContext = object; /** * A provider that supplies instructions resources for repositories. */ export interface InstructionsProvider { + /** + * A human-readable label for this provider. + */ + readonly label: string; + /** * An optional event to signal that instructions have changed. */ @@ -102,11 +92,11 @@ declare module 'vscode' { /** * Provide the list of instructions available. - * @param options Optional query parameters. + * @param context Context for the query. * @param token A cancellation token. * @returns An array of instructions resources or a promise that resolves to such. */ - provideInstructions(options: InstructionsQueryOptions, token: CancellationToken): ProviderResult; + provideInstructions(context: InstructionsContext, token: CancellationToken): ProviderResult; } // #endregion @@ -117,16 +107,6 @@ declare module 'vscode' { * Represents a prompt file resource (e.g., .prompt.md) available for a repository. */ export interface PromptFileResource { - /** - * The unique identifier/name of the prompt file. - */ - readonly name: string; - - /** - * A description of what the prompt file does. - */ - readonly description: string; - /** * The URI to the prompt file resource. */ @@ -139,14 +119,19 @@ declare module 'vscode' { } /** - * Options for querying prompt files. + * Context for querying prompt files. */ - export type PromptFileQueryOptions = object; + export type PromptFileContext = object; /** * A provider that supplies prompt file resources (from .prompt.md files) for repositories. */ export interface PromptFileProvider { + /** + * A human-readable label for this provider. + */ + readonly label: string; + /** * An optional event to signal that prompt files have changed. */ @@ -154,11 +139,11 @@ declare module 'vscode' { /** * Provide the list of prompt files available. - * @param options Optional query parameters. + * @param context Context for the query. * @param token A cancellation token. * @returns An array of prompt file resources or a promise that resolves to such. */ - providePromptFiles(options: PromptFileQueryOptions, token: CancellationToken): ProviderResult; + providePromptFiles(context: PromptFileContext, token: CancellationToken): ProviderResult; } // #endregion From 1c07b6e32fff90efbe9ad4049f4c7592921d0348 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 9 Jan 2026 12:22:00 +0100 Subject: [PATCH 27/27] use type instead of 'in' checks --- .../api/browser/mainThreadChatAgents2.ts | 2 +- .../workbench/api/common/extHost.protocol.ts | 3 +- .../api/common/extHostChatAgents2.ts | 33 ++++++++++--------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 39dc483db83e2..6444ca9c12c12 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -454,7 +454,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA const disposable = this._promptsService.registerPromptFileProvider(extension, type, { onDidChangePromptFiles: emitter.event, providePromptFiles: async (context: IPromptFileContext, token: CancellationToken) => { - const contributions = await this._proxy.$providePromptFiles(handle, context, token); + const contributions = await this._proxy.$providePromptFiles(handle, type, context, token); if (!contributions) { return undefined; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 29474a4281b56..d742bfe00dea1 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -99,6 +99,7 @@ import { ISaveProfileResult } from '../../services/userDataProfile/common/userDa import { IExtHostDocumentSaveDelegate } from './extHostDocumentData.js'; import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; import * as tasks from './shared/tasks.js'; +import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; export type IconPathDto = | UriComponents @@ -1462,7 +1463,7 @@ export interface ExtHostChatAgentsShape2 { $releaseSession(sessionResource: UriComponents): void; $detectChatParticipant(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise; $provideRelatedFiles(handle: number, request: Dto, token: CancellationToken): Promise[] | undefined>; - $providePromptFiles(handle: number, context: IPromptFileContext, token: CancellationToken): Promise[] | undefined>; + $providePromptFiles(handle: number, type: PromptsType, context: IPromptFileContext, token: CancellationToken): Promise[] | undefined>; $setRequestTools(requestId: string, tools: UserSelectedTools): void; } export interface IChatParticipantMetadata { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index e7b504f9b1119..493723aa848ed 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -494,12 +494,16 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS // Listen to provider change events and notify main thread // Check for the appropriate event based on the provider type let changeEvent: vscode.Event | undefined; - if ('onDidChangeCustomAgents' in provider) { - changeEvent = provider.onDidChangeCustomAgents; - } else if ('onDidChangeInstructions' in provider) { - changeEvent = provider.onDidChangeInstructions; - } else if ('onDidChangePromptFiles' in provider) { - changeEvent = provider.onDidChangePromptFiles; + switch (type) { + case PromptsType.agent: + changeEvent = (provider as vscode.CustomAgentProvider).onDidChangeCustomAgents; + break; + case PromptsType.instructions: + changeEvent = (provider as vscode.InstructionsProvider).onDidChangeInstructions; + break; + case PromptsType.prompt: + changeEvent = (provider as vscode.PromptFileProvider).onDidChangePromptFiles; + break; } if (changeEvent) { @@ -526,22 +530,21 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return await provider.provider.provideRelatedFiles(extRequestDraft, token) ?? undefined; } - async $providePromptFiles(handle: number, context: IPromptFileContext, token: CancellationToken): Promise { + async $providePromptFiles(handle: number, type: PromptsType, context: IPromptFileContext, token: CancellationToken): Promise { const providerData = this._promptFileProviders.get(handle); if (!providerData) { return undefined; } const provider = providerData.provider; - // Call the appropriate method based on the provider type - if ('provideCustomAgents' in provider) { - return await provider.provideCustomAgents(context, token) ?? undefined; - } else if ('provideInstructions' in provider) { - return await provider.provideInstructions(context, token) ?? undefined; - } else if ('providePromptFiles' in provider) { - return await provider.providePromptFiles(context, token) ?? undefined; + switch (type) { + case PromptsType.agent: + return await (provider as vscode.CustomAgentProvider).provideCustomAgents(context, token) ?? undefined; + case PromptsType.instructions: + return await (provider as vscode.InstructionsProvider).provideInstructions(context, token) ?? undefined; + case PromptsType.prompt: + return await (provider as vscode.PromptFileProvider).providePromptFiles(context, token) ?? undefined; } - return undefined; } async $detectChatParticipant(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise {