Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts
13405 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 * as l10n from '@vscode/l10n';6import * as vscode from 'vscode';7import { ICustomInstructionsService } from '../../../../platform/customInstructions/common/customInstructionsService';8import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';9import { ILogService } from '../../../../platform/log/common/logService';10import { IPromptsService } from '../../../../platform/promptFiles/common/promptsService';11import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';12import { CancellationToken } from '../../../../util/vs/base/common/cancellation';13import { isCancellationError } from '../../../../util/vs/base/common/errors';14import { Emitter } from '../../../../util/vs/base/common/event';15import { Disposable } from '../../../../util/vs/base/common/lifecycle';16import { basename } from '../../../../util/vs/base/common/resources';17import { URI } from '../../../../util/vs/base/common/uri';18import { ICopilotCLIAgents, isEnabledForCopilotCLI } from '../../copilotcli/node/copilotCli';1920export class CopilotCLICustomizationProvider extends Disposable implements vscode.ChatSessionCustomizationProvider {2122private readonly _onDidChange = this._register(new Emitter<void>());23readonly onDidChange = this._onDidChange.event;2425static get metadata(): vscode.ChatSessionCustomizationProviderMetadata {26return {27label: 'Copilot CLI',28iconId: 'copilot',29supportedTypes: [30vscode.ChatSessionCustomizationType.Agent,31vscode.ChatSessionCustomizationType.Skill,32vscode.ChatSessionCustomizationType.Instructions,33vscode.ChatSessionCustomizationType.Hook,34vscode.ChatSessionCustomizationType.Plugins,35].filter((t): t is vscode.ChatSessionCustomizationType => t !== undefined),36};37}3839constructor(40@ICopilotCLIAgents private readonly copilotCLIAgents: ICopilotCLIAgents,41@ICustomInstructionsService private readonly customInstructionsService: ICustomInstructionsService,42@IPromptsService private readonly promptsService: IPromptsService,43@ILogService private readonly logService: ILogService,44@IWorkspaceService private readonly workspaceService: IWorkspaceService,45@IFileSystemService private readonly fileSystemService: IFileSystemService,46) {47super();4849this._register(this.promptsService.onDidChangeCustomAgents(() => this._onDidChange.fire()));50this._register(this.promptsService.onDidChangeInstructions(() => this._onDidChange.fire()));51this._register(this.promptsService.onDidChangeSkills(() => this._onDidChange.fire()));52this._register(this.promptsService.onDidChangeHooks(() => this._onDidChange.fire()));53this._register(this.promptsService.onDidChangePlugins(() => this._onDidChange.fire()));54this._register(this.copilotCLIAgents.onDidChangeAgents(() => this._onDidChange.fire()));55}5657async provideChatSessionCustomizations(token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {58const [agents, instructions, skills, hooks, plugins] = await Promise.all([59this.getAgentItems(token),60this.getInstructionItems(token),61this.getSkillItems(token),62this.getHookItems(token),63this.getPluginItems(token),64].map(p => p.catch(err => {65if (isCancellationError(err) || token.isCancellationRequested) {66throw err;67}68this.logService.error(`[CopilotCLICustomizationProvider] failed to get customizations: ${err}`);69return [];70})));7172this.logService.debug(`[CopilotCLICustomizationProvider] agents (${agents.length}): ${agents.map(a => a.name).join(', ') || '(none)'}`);73this.logService.debug(`[CopilotCLICustomizationProvider] instructions (${instructions.length}): ${instructions.map(i => i.name).join(', ') || '(none)'}`);74this.logService.debug(`[CopilotCLICustomizationProvider] skills (${skills.length}): ${skills.map(s => s.name).join(', ') || '(none)'}`);75this.logService.debug(`[CopilotCLICustomizationProvider] hooks (${hooks.length}): ${hooks.map(h => h.name).join(', ') || '(none)'}`);7677this.logService.debug(`[CopilotCLICustomizationProvider] plugins (${plugins.length}): ${plugins.map(p => p.name).join(', ') || '(none)'}`);7879const items = [...agents, ...instructions, ...skills, ...hooks, ...plugins];80this.logService.debug(`[CopilotCLICustomizationProvider] total: ${items.length} items`);81return items;82}8384/**85* Builds agent items from ICopilotCLIAgents, which already merges SDK86* and prompt-file agents with source URIs.87*/88private async getAgentItems(_token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {89const agentInfos = await this.copilotCLIAgents.getAgents();90return agentInfos.map(({ agent, sourceUri, pluginUri, extensionId }) => ({91uri: sourceUri,92type: vscode.ChatSessionCustomizationType.Agent,93name: agent.displayName || agent.name,94description: agent.description,95extensionId,96pluginUri97}));98}99100/**101* Collects all instruction items from the prompt file service,102* categorizing them with groupKeys and badges matching the core103* implementation:104* - agent-instructions: AGENTS.md, CLAUDE.md, copilot-instructions.md105* - context-instructions: files with an applyTo pattern (badge = pattern)106* - on-demand-instructions: files without an applyTo pattern107*/108private async getInstructionItems(token: CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {109// Collect agent instruction URIs from customInstructionsService110// (copilot-instructions.md) plus workspace-root AGENTS.md and CLAUDE.md111const agentInstructionUriList = await this.customInstructionsService.getAgentInstructions();112const rootFileNames = ['AGENTS.md', 'CLAUDE.md'];113for (const folder of this.workspaceService.getWorkspaceFolders()) {114for (const fileName of rootFileNames) {115const uri = URI.joinPath(folder, fileName);116try {117await this.fileSystemService.stat(uri);118agentInstructionUriList.push(uri);119} catch {120// file doesn't exist121}122}123}124125const items: vscode.ChatSessionCustomizationItem[] = [];126const seenUris = new Set<string>();127128// Emit agent instruction files (AGENTS.md, CLAUDE.md, copilot-instructions.md)129// that come from customInstructionsService but may not appear in130// promptsService.getInstructions().131for (const uri of agentInstructionUriList) {132seenUris.add(uri.toString());133items.push({134uri,135type: vscode.ChatSessionCustomizationType.Instructions,136name: basename(uri),137description: undefined,138groupKey: 'agent-instructions',139extensionId: undefined,140pluginUri: undefined141});142}143144for (const instruction of await this.promptsService.getInstructions(token)) {145const uri = instruction.uri;146if (!isEnabledForCopilotCLI(instruction)) {147continue; // only include instructions that are relevant for copilotcli148}149150if (seenUris.has(uri.toString())) {151continue; // already emitted as agent instruction152}153154const name = instruction.name;155const pattern = instruction.pattern;156const description = instruction.description;157158if (pattern !== undefined) {159const badge = pattern === '**'160? l10n.t('always added')161: pattern;162const badgeTooltip = pattern === '**'163? l10n.t('This instruction is automatically included in every interaction.')164: l10n.t('This instruction is automatically included when files matching \'{0}\' are in context.', pattern);165items.push({166uri,167type: vscode.ChatSessionCustomizationType.Instructions,168name,169description,170groupKey: 'context-instructions',171badge,172badgeTooltip,173extensionId: instruction.extensionId,174pluginUri: instruction.pluginUri175});176} else {177items.push({178uri,179type: vscode.ChatSessionCustomizationType.Instructions,180name,181description,182groupKey: 'on-demand-instructions',183extensionId: instruction.extensionId,184pluginUri: instruction.pluginUri185});186}187}188189return items;190}191192/**193* Collects all skill items from the prompt file service.194*/195private async getSkillItems(token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {196return (await this.promptsService.getSkills(token)).filter(isEnabledForCopilotCLI).map(s => ({197uri: s.uri,198type: vscode.ChatSessionCustomizationType.Skill,199name: s.name,200description: s.description,201extensionId: s.extensionId,202pluginUri: s.pluginUri,203}));204}205206/**207* Collects all hook items from the prompt file service.208* Each item is a hook configuration file (JSON).209*/210private async getHookItems(token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {211return (await this.promptsService.getHooks(token)).filter(isEnabledForCopilotCLI).map(h => ({212uri: h.uri,213type: vscode.ChatSessionCustomizationType.Hook,214name: basename(h.uri).replace(/\.json$/i, ''),215description: undefined,216extensionId: h.extensionId,217pluginUri: h.pluginUri,218}));219}220221/**222* Collects all plugin items from the prompt file service.223*/224private async getPluginItems(token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {225return (await this.promptsService.getPlugins(token)).filter(isEnabledForCopilotCLI).map(p => ({226uri: p.uri,227type: vscode.ChatSessionCustomizationType.Plugins,228name: basename(p.uri),229description: undefined,230extensionId: undefined,231pluginUri: undefined,232}));233}234}235236237