Path: blob/main/src/vs/sessions/contrib/chat/browser/promptsService.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationToken } from '../../../../base/common/cancellation.js';6import { FileAccess } from '../../../../base/common/network.js';7import { basename, dirname, joinPath } from '../../../../base/common/resources.js';8import { SKILL_FILENAME } from '../../../../workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.js';9import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';10import { IAgentSkill, IPromptPath, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';11import { PromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.js';12import { BUILTIN_STORAGE, IBuiltinPromptPath } from '../common/builtinPromptsStorage.js';1314/** URI root for built-in skills bundled with the Agents app. */15export const BUILTIN_SKILLS_URI = FileAccess.asFileUri('vs/sessions/skills');1617/**18* Sessions-specific PromptsService that additionally discovers built-in skills19* bundled at `vs/sessions/skills/{folder}/SKILL.md`.20*21* Built-in skills are merged into `findAgentSkills()` and exposed via22* `listPromptFilesForStorage(skill, BUILTIN_STORAGE)` so that the existing23* AI customization UI (groups, badges, overrides) picks them up naturally.24*25* User/workspace skills with the same folder name take precedence (built-ins26* are appended last and filtered when overridden).27*/28export class AgenticPromptsService extends PromptsService {2930private _builtinSkillsCache: Promise<readonly IAgentSkill[]> | undefined;3132private async getBuiltinSkills(): Promise<readonly IAgentSkill[]> {33if (!this._builtinSkillsCache) {34this._builtinSkillsCache = this.discoverBuiltinSkills();35}36return this._builtinSkillsCache;37}3839private async discoverBuiltinSkills(): Promise<readonly IAgentSkill[]> {40try {41const stat = await this.fileService.resolve(BUILTIN_SKILLS_URI);42if (!stat.children) {43return [];44}4546const skills: IAgentSkill[] = [];47for (const child of stat.children) {48if (!child.isDirectory) {49continue;50}51const skillFileUri = joinPath(child.resource, SKILL_FILENAME);52try {53const parsed = await this.parseNew(skillFileUri, CancellationToken.None);54const rawName = parsed.header?.name;55const rawDescription = parsed.header?.description;56if (!rawName || !rawDescription) {57continue;58}59const name = sanitizeSkillText(rawName, 64);60const description = sanitizeSkillText(rawDescription, 1024);61const folderName = basename(child.resource);62if (name !== folderName) {63continue;64}65skills.push({66uri: skillFileUri,67storage: BUILTIN_STORAGE as PromptsStorage,68name,69description,70disableModelInvocation: parsed.header?.disableModelInvocation === true,71userInvocable: parsed.header?.userInvocable !== false,72});73} catch (e) {74this.logger.warn(`[AgenticPromptsService] Failed to parse built-in skill: ${skillFileUri}`, e instanceof Error ? e.message : String(e));75}76}77return skills;78} catch {79return [];80}81}8283private async getBuiltinSkillPaths(): Promise<readonly IBuiltinPromptPath[]> {84const skills = await this.getBuiltinSkills();85return skills.map(s => ({86uri: s.uri,87storage: BUILTIN_STORAGE,88type: PromptsType.skill,89name: s.name,90description: s.description,91}));92}9394public override async findAgentSkills(token: CancellationToken): Promise<IAgentSkill[] | undefined> {95const baseResult = await super.findAgentSkills(token);96if (baseResult === undefined) {97return undefined;98}99100const builtinSkills = await this.getBuiltinSkills();101if (builtinSkills.length === 0) {102return baseResult;103}104105const existingNames = new Set(106baseResult107.filter(s => s.storage === PromptsStorage.local || s.storage === PromptsStorage.user)108.map(s => s.name)109);110const disabledSkills = this.getDisabledPromptFiles(PromptsType.skill);111const nonOverridden = builtinSkills.filter(s => !existingNames.has(s.name) && !disabledSkills.has(s.uri));112if (nonOverridden.length === 0) {113return baseResult;114}115116return [...baseResult, ...nonOverridden];117}118119public override async listPromptFiles(type: PromptsType, token: CancellationToken): Promise<readonly IPromptPath[]> {120const baseResults = await super.listPromptFiles(type, token);121122if (type !== PromptsType.skill) {123return baseResults;124}125126const builtinItems = await this.getBuiltinSkillPaths();127if (builtinItems.length === 0) {128return baseResults;129}130131// Filter out built-ins overridden by user/workspace skills of the same folder name.132const overriddenNames = new Set<string>();133for (const p of baseResults) {134if (p.storage === PromptsStorage.local || p.storage === PromptsStorage.user) {135overriddenNames.add(basename(dirname(p.uri)));136}137}138const nonOverridden = builtinItems.filter(p => !overriddenNames.has(basename(dirname(p.uri))));139140// Built-in items use BUILTIN_STORAGE ('builtin') which is not in the core141// IPromptPath union but is handled by the sessions UI layer.142return [...baseResults, ...nonOverridden] as readonly IPromptPath[];143}144145public override async listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise<readonly IPromptPath[]> {146if ((storage as PromptsStorage | typeof BUILTIN_STORAGE) === BUILTIN_STORAGE) {147if (type === PromptsType.skill) {148return this.getBuiltinSkillPaths() as Promise<readonly IPromptPath[]>;149}150return [];151}152return super.listPromptFilesForStorage(type, storage, token);153}154}155156/**157* Strips XML tags and truncates to the given max length.158* Matches the sanitization applied by PromptsService for other skill sources.159*/160function sanitizeSkillText(text: string, maxLength: number): string {161const sanitized = text.replace(/<[^>]+>/g, '');162return sanitized.length > maxLength ? sanitized.substring(0, maxLength) : sanitized;163}164165166