Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/vscode-node/slashCommands/agentsCommand.ts
13406 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 vscode from 'vscode';6import { INativeEnvService } from '../../../../../platform/env/common/envService';7import { createDirectoryIfNotExists, IFileSystemService } from '../../../../../platform/filesystem/common/fileSystemService';8import { ILogService } from '../../../../../platform/log/common/logService';9import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';10import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';11import { DisposableStore } from '../../../../../util/vs/base/common/lifecycle';12import { URI } from '../../../../../util/vs/base/common/uri';13import { IClaudeSlashCommandHandler, registerClaudeSlashCommand } from './claudeSlashCommandRegistry';1415/**16* AGENTS CONFIGURATION WIZARD17* ===========================18*19* This wizard allows users to create and manage specialized Claude subagents that can be20* delegated to for specific tasks. Each agent has its own system prompt, tools, and model.21*22* ## MAIN MENU23* Shows a list of all agents with options to create new or manage existing ones.24*25* - Create new agent (always available)26* - Project agents (.claude/agents/) - listed with model27* - User/Personal agents (~/.claude/agents/) - listed with model28*29* ## CREATE FLOW30* 1. Choose location (Project .claude/agents/ or Personal ~/.claude/agents/)31* 2. Choose creation method:32* - Generate with Claude (recommended): Describe what the agent should do33* - Manual configuration: Enter type, system prompt, description manually34* 3. For "Generate with Claude":35* a. Enter description of what agent should do36* b. Wait for generation37* c. Select tools (with advanced options for individual tools/MCP)38* d. Select model39* e. File is saved and opened40* 4. For "Manual configuration":41* a. Enter agent type identifier (e.g., "test-runner")42* b. Enter system prompt43* c. Enter description (when Claude should use this agent)44* d. Select tools45* e. Select model46* f. File is saved and opened47*48* ## EDIT FLOW (selecting existing agent)49* 1. Choose action:50* - View agent (opens file)51* - Edit agent (shows edit menu)52* - Delete agent (with confirmation)53* - Back (returns to main menu)54* 2. Edit menu:55* - Open in editor56* - Edit tools (tool picker)57* - Edit model (model picker)58*59* ## AGENT FILE FORMAT60* Agents are stored as markdown files with YAML frontmatter:61* ```62* ---63* name: agent-name64* description: "When Claude should use this agent..."65* model: sonnet66* allowedTools:67* - Read68* - Grep69* - Glob70* ---71*72* System prompt content here...73* ```74*/7576/**77* Agent location type78*/79type AgentLocationType = 'project' | 'user';8081/**82* Agent file location83*/84interface AgentLocation {85type: AgentLocationType;86label: string;87agentsDir: URI;88workspaceFolder?: URI;89}9091/**92* Parsed agent configuration93*/94interface AgentConfig {95name: string;96description: string;97model: string;98allowedTools?: string[];99systemPrompt: string;100}101102/**103* Agent with its source location104*/105interface AgentWithSource {106config: AgentConfig;107location: AgentLocation;108filePath: URI;109}110111/**112* Available models for agents113*/114const AGENT_MODELS = [115{116id: 'sonnet',117label: 'Sonnet',118description: 'Balanced performance - best for most agents',119isDefault: true,120},121{122id: 'opus',123label: 'Opus',124description: 'Most capable for complex reasoning tasks',125},126{127id: 'haiku',128label: 'Haiku',129description: 'Fast and efficient for simple tasks',130},131{132id: 'inherit',133label: 'Inherit from parent',134description: 'Use the same model as the main conversation',135},136] as const;137138/**139* Tool categories for selection140*/141const TOOL_CATEGORIES = [142{ id: 'readonly', label: 'Read-only tools', tools: ['Read', 'Glob', 'Grep', 'WebFetch', 'WebSearch'] },143{ id: 'edit', label: 'Edit tools', tools: ['Edit', 'Write', 'NotebookEdit'] },144{ id: 'execution', label: 'Execution tools', tools: ['Bash'] },145{ id: 'mcp', label: 'MCP tools', tools: [] }, // Populated dynamically146{ id: 'other', label: 'Other tools', tools: ['Skill', 'Agent', 'Task', 'TodoWrite'] },147] as const;148149/**150* All individual tools151*/152const ALL_TOOLS = [153'Bash',154'Glob',155'Grep',156'Read',157'Edit',158'Write',159'NotebookEdit',160'WebFetch',161'WebSearch',162'Skill',163'Agent',164'Task',165'TodoWrite',166] as const;167168/**169* Slash command handler for managing Claude agents.170* Launches a QuickPick wizard to create, view, edit, and delete agents.171*/172export class AgentsSlashCommand implements IClaudeSlashCommandHandler {173readonly commandName = 'agents';174readonly description = 'Create and manage specialized Claude agents';175readonly commandId = 'copilot.claude.agents';176177constructor(178@IWorkspaceService private readonly workspaceService: IWorkspaceService,179@IFileSystemService private readonly fileSystemService: IFileSystemService,180@INativeEnvService private readonly envService: INativeEnvService,181@ILogService private readonly logService: ILogService,182) { }183184async handle(185_args: string,186stream: vscode.ChatResponseStream | undefined,187_token: CancellationToken188): Promise<vscode.ChatResult> {189stream?.markdown(vscode.l10n.t('Opening agents configuration...'));190191// Fire and forget - wizard runs in background192this._runWizard().catch(error => {193this.logService.error('[AgentsSlashCommand] Error running agents wizard:', error);194vscode.window.showErrorMessage(195vscode.l10n.t('Error configuring agent: {0}', error instanceof Error ? error.message : String(error))196);197});198199return {};200}201202private async _runWizard(): Promise<void> {203// Main menu - show agents list204const result = await this._showMainMenu();205if (!result) {206return;207}208209if (result.action === 'create') {210await this._runCreateFlow();211} else if (result.action === 'select' && result.agent) {212await this._runAgentActionMenu(result.agent);213}214}215216/**217* Shows the main agents list menu.218*/219private async _showMainMenu(): Promise<{ action: 'create' | 'select'; agent?: AgentWithSource } | undefined> {220const projectAgents = await this._loadProjectAgents();221const userAgents = await this._loadUserAgents();222223type AgentMenuItem = vscode.QuickPickItem & {224action: 'create' | 'select';225agent?: AgentWithSource;226};227228const items: (AgentMenuItem | vscode.QuickPickItem)[] = [];229230// Create new agent option231items.push({232label: '$(add) ' + vscode.l10n.t('Create new agent'),233action: 'create',234});235236// Project agents section237if (projectAgents.length > 0) {238items.push({239label: vscode.l10n.t('Project agents'),240kind: vscode.QuickPickItemKind.Separator,241});242243for (const agent of projectAgents) {244items.push({245label: agent.config.name,246description: `· ${agent.config.model}`,247action: 'select',248agent,249});250}251}252253// User/Personal agents section254if (userAgents.length > 0) {255items.push({256label: vscode.l10n.t('Personal agents'),257kind: vscode.QuickPickItemKind.Separator,258});259260for (const agent of userAgents) {261items.push({262label: agent.config.name,263description: `· ${agent.config.model}`,264action: 'select',265agent,266});267}268}269270// Show placeholder text if no custom agents271const placeholderText = projectAgents.length === 0 && userAgents.length === 0272? vscode.l10n.t('No agents found. Create specialized subagents that Claude can delegate to.')273: vscode.l10n.t('Select an agent to view, edit, or delete');274275const selected = await vscode.window.showQuickPick(items, {276title: vscode.l10n.t('Agents'),277placeHolder: placeholderText,278ignoreFocusOut: true,279}) as AgentMenuItem | undefined;280281if (!selected) {282return undefined;283}284285return { action: selected.action, agent: selected.agent };286}287288/**289* Runs the create agent flow.290*/291private async _runCreateFlow(): Promise<void> {292// Step 1: Choose location293const location = await this._selectLocation();294if (!location) {295return;296}297298// Step 2: Choose creation method299const method = await this._selectCreationMethod();300if (!method) {301return;302}303304if (method === 'generate') {305await this._runGenerateFlow(location);306} else {307await this._runManualFlow(location);308}309}310311/**312* Step 1: Select where to save the agent.313*/314private async _selectLocation(): Promise<AgentLocation | undefined> {315type LocationItem = vscode.QuickPickItem & { location: AgentLocation };316317const items: LocationItem[] = [];318319// Project location (first workspace folder)320const workspaceFolders = this.workspaceService.getWorkspaceFolders();321if (workspaceFolders.length > 0) {322const firstFolder = workspaceFolders[0];323items.push({324label: vscode.l10n.t('1. Project (.claude/agents/)'),325location: {326type: 'project',327label: vscode.l10n.t('Project'),328agentsDir: URI.joinPath(firstFolder, '.claude', 'agents'),329workspaceFolder: firstFolder,330},331});332}333334// Personal location335items.push({336label: vscode.l10n.t('2. Personal (~/.claude/agents/)'),337location: {338type: 'user',339label: vscode.l10n.t('Personal'),340agentsDir: URI.joinPath(this.envService.userHome, '.claude', 'agents'),341},342});343344const selected = await vscode.window.showQuickPick(items, {345title: vscode.l10n.t('Create new agent'),346placeHolder: vscode.l10n.t('Choose location'),347ignoreFocusOut: true,348});349350return selected?.location;351}352353/**354* Step 2: Select creation method.355*/356private async _selectCreationMethod(): Promise<'generate' | 'manual' | undefined> {357const items: (vscode.QuickPickItem & { method: 'generate' | 'manual' })[] = [358{359label: vscode.l10n.t('1. Generate with Claude (recommended)'),360method: 'generate',361},362{363label: vscode.l10n.t('2. Manual configuration'),364method: 'manual',365},366];367368const selected = await vscode.window.showQuickPick(items, {369title: vscode.l10n.t('Create new agent'),370placeHolder: vscode.l10n.t('Creation method'),371ignoreFocusOut: true,372});373374return selected?.method;375}376377/**378* Generate flow: describe agent, generate, select tools, select model.379*/380private async _runGenerateFlow(location: AgentLocation): Promise<void> {381// Step 3: Enter description382const description = await vscode.window.showInputBox({383title: vscode.l10n.t('Create new agent'),384prompt: vscode.l10n.t('Describe what this agent should do and when it should be used (be comprehensive for best results)'),385placeHolder: vscode.l10n.t('e.g., Help me write unit tests for my code...'),386ignoreFocusOut: true,387});388389if (!description) {390return;391}392393// Step 4: Generate agent with Claude394const generated = await vscode.window.withProgress({395location: vscode.ProgressLocation.Notification,396title: vscode.l10n.t('Generating agent from description...'),397cancellable: true,398}, async (_progress, token) => {399return this._generateAgentConfig(description, token);400});401402if (!generated) {403return;404}405406// Step 5: Select tools407const tools = await this._selectTools();408if (!tools) {409return;410}411412// Step 6: Select model413const model = await this._selectModel();414if (!model) {415return;416}417418// Build final config419const config: AgentConfig = {420name: generated.name,421description: generated.description,422model,423allowedTools: tools.length > 0 && !tools.includes('*') ? tools : undefined,424systemPrompt: generated.systemPrompt,425};426427// Save and open428const filePath = URI.joinPath(location.agentsDir, `${config.name}.md`);429await this._saveAgent(filePath, config);430await this._openAgentFile(filePath);431}432433/**434* Manual flow: enter type, system prompt, description, select tools, select model.435*/436private async _runManualFlow(location: AgentLocation): Promise<void> {437// Step 3: Enter agent type (identifier)438const name = await vscode.window.showInputBox({439title: vscode.l10n.t('Create new agent'),440prompt: vscode.l10n.t('Enter a unique identifier for your agent:'),441placeHolder: vscode.l10n.t('e.g., test-runner, tech-lead, etc'),442ignoreFocusOut: true,443validateInput: value => {444if (!value) {445return vscode.l10n.t('Agent name is required');446}447if (!/^[a-z0-9-]+$/.test(value)) {448return vscode.l10n.t('Use lowercase letters, numbers, and hyphens only');449}450return null;451},452});453454if (!name) {455return;456}457458// Step 4: Enter system prompt459const systemPrompt = await vscode.window.showInputBox({460title: vscode.l10n.t('Create new agent'),461prompt: vscode.l10n.t('Enter the system prompt for your agent:') + '\n' + vscode.l10n.t('Be comprehensive for best results'),462placeHolder: vscode.l10n.t('You are a helpful code reviewer who...'),463ignoreFocusOut: true,464});465466if (!systemPrompt) {467return;468}469470// Step 5: Enter description471const description = await vscode.window.showInputBox({472title: vscode.l10n.t('Create new agent'),473prompt: vscode.l10n.t('When should Claude use this agent?'),474placeHolder: vscode.l10n.t("e.g., use this agent after you're done writing code..."),475ignoreFocusOut: true,476});477478if (!description) {479return;480}481482// Step 6: Select tools483const tools = await this._selectTools();484if (!tools) {485return;486}487488// Step 7: Select model489const model = await this._selectModel();490if (!model) {491return;492}493494// Build config495const config: AgentConfig = {496name,497description,498model,499allowedTools: tools.length > 0 && !tools.includes('*') ? tools : undefined,500systemPrompt,501};502503// Save and open504const filePath = URI.joinPath(location.agentsDir, `${config.name}.md`);505await this._saveAgent(filePath, config);506await this._openAgentFile(filePath);507}508509/**510* Generate agent config using Claude.511*/512private async _generateAgentConfig(513description: string,514token: vscode.CancellationToken515): Promise<{ name: string; description: string; systemPrompt: string } | undefined> {516try {517const prompt = `Based on the following description, generate a Claude agent configuration.518519Description: ${description}520521Respond with a JSON object containing:5221. "name": A short, kebab-case identifier (e.g., "test-runner", "code-reviewer")5232. "description": A detailed description of when Claude should use this agent (include examples)5243. "systemPrompt": A comprehensive system prompt that defines the agent's behavior, expertise, and guidelines525526Keep the systemPrompt focused but thorough. Include specific instructions for how the agent should approach tasks.527528Respond ONLY with the JSON object, no markdown code blocks or other text.`;529530// Use claude-sonnet-4.5 for agent generation (fast and efficient for structured output)531let models = await vscode.lm.selectChatModels({ family: 'claude-sonnet-4.5', vendor: 'copilot' });532if (models.length === 0) {533// Fallback to any available model534models = await vscode.lm.selectChatModels({ vendor: 'copilot' });535// Get latest claude-sonnet- model536models = models537.filter(model => model.family.startsWith('claude-sonnet-'))538.sort((a, b) => b.family.localeCompare(a.family));539if (models.length === 0) {540vscode.window.showErrorMessage(vscode.l10n.t('No language model available for agent generation'));541return undefined;542}543}544545const response = await models[0].sendRequest(546[vscode.LanguageModelChatMessage.User(prompt)],547{},548token549);550551let responseText = '';552for await (const chunk of response.stream) {553if (chunk instanceof vscode.LanguageModelTextPart) {554responseText += chunk.value;555}556}557558// Strip markdown code blocks if present559let jsonText = responseText.trim();560const codeBlockMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)```/);561if (codeBlockMatch) {562jsonText = codeBlockMatch[1].trim();563}564565// Parse JSON response566const parsed = JSON.parse(jsonText);567return {568name: parsed.name,569description: parsed.description,570systemPrompt: parsed.systemPrompt,571};572} catch (error) {573this.logService.error('[AgentsSlashCommand] Failed to generate agent:', error);574vscode.window.showErrorMessage(575vscode.l10n.t('Failed to generate agent: {0}', error instanceof Error ? error.message : String(error))576);577return undefined;578}579}580581/**582* Select tools for the agent (multi-select with categories).583*/584private async _selectTools(): Promise<string[] | undefined> {585type ToolPickItem = vscode.QuickPickItem & {586categoryId?: string;587toolId?: string;588};589590// Toggle button for advanced options591const showAdvancedButton: vscode.QuickInputButton = {592iconPath: new vscode.ThemeIcon('chevron-down'),593tooltip: vscode.l10n.t('Show advanced options'),594};595const hideAdvancedButton: vscode.QuickInputButton = {596iconPath: new vscode.ThemeIcon('chevron-up'),597tooltip: vscode.l10n.t('Hide advanced options'),598};599600let showAdvanced = false;601let resolved = false;602603return new Promise<string[] | undefined>((resolve) => {604const disposables = new DisposableStore();605const quickPick = vscode.window.createQuickPick<ToolPickItem>();606disposables.add(quickPick);607quickPick.title = vscode.l10n.t('Create new agent');608quickPick.placeholder = vscode.l10n.t('Select tools');609quickPick.canSelectMany = true;610quickPick.ignoreFocusOut = true;611quickPick.buttons = [showAdvancedButton];612613const updateItems = () => {614const items: ToolPickItem[] = [];615616// Tool categories617for (const cat of TOOL_CATEGORIES) {618items.push({619label: cat.label,620categoryId: cat.id,621});622}623624// Advanced: individual tools625if (showAdvanced) {626items.push({627label: vscode.l10n.t('Individual Tools'),628kind: vscode.QuickPickItemKind.Separator,629});630631for (const tool of ALL_TOOLS) {632items.push({633label: tool,634toolId: tool,635});636}637}638639// Preserve selection when updating items640const previouslySelectedIds = new Set(641quickPick.selectedItems.map(item => item.categoryId || item.toolId)642);643644quickPick.items = items;645646// Restore selection647quickPick.selectedItems = items.filter(item => {648const id = item.categoryId || item.toolId;649return id && previouslySelectedIds.has(id);650});651};652653// Initialize with all categories selected654updateItems();655quickPick.selectedItems = quickPick.items.filter(item => item.categoryId);656657disposables.add(quickPick.onDidTriggerButton((button) => {658if (button === showAdvancedButton || button === hideAdvancedButton) {659showAdvanced = !showAdvanced;660quickPick.buttons = [showAdvanced ? hideAdvancedButton : showAdvancedButton];661updateItems();662}663}));664665disposables.add(quickPick.onDidAccept(() => {666if (resolved) {667return;668}669resolved = true;670671const selectedItems = quickPick.selectedItems;672disposables.dispose();673674// Check if all categories are selected - treat as "all tools"675const selectedCategoryIds = new Set(676selectedItems.filter(item => item.categoryId).map(item => item.categoryId)677);678const allCategoriesSelected = TOOL_CATEGORIES.every(cat => selectedCategoryIds.has(cat.id));679if (allCategoriesSelected) {680resolve(['*']);681return;682}683684// Collect selected tools from categories and individual tools685const tools = new Set<string>();686for (const item of selectedItems) {687if (item.categoryId) {688const cat = TOOL_CATEGORIES.find(c => c.id === item.categoryId);689if (cat) {690for (const tool of cat.tools) {691tools.add(tool);692}693}694} else if (item.toolId) {695tools.add(item.toolId);696}697}698699resolve(Array.from(tools));700}));701702disposables.add(quickPick.onDidHide(() => {703disposables.dispose();704if (!resolved) {705resolved = true;706resolve(undefined);707}708}));709710quickPick.show();711});712}713714/**715* Select model for the agent.716*/717private async _selectModel(): Promise<string | undefined> {718const items = AGENT_MODELS.map((model, index) => ({719label: `${index + 1}. ${model.label}${'isDefault' in model && model.isDefault ? ' $(check)' : ''}`,720description: model.description,721modelId: model.id,722}));723724const selected = await vscode.window.showQuickPick(items, {725title: vscode.l10n.t('Create new agent'),726placeHolder: vscode.l10n.t('Select model') + '\n' + vscode.l10n.t("Model determines the agent's reasoning capabilities and speed."),727ignoreFocusOut: true,728});729730return selected?.modelId;731}732733/**734* Shows the action menu for a selected agent.735*/736private async _runAgentActionMenu(agent: AgentWithSource): Promise<void> {737type ActionItem = vscode.QuickPickItem & { action: 'view' | 'edit' | 'delete' | 'back' };738739const items: ActionItem[] = [740{ label: vscode.l10n.t('1. View agent'), action: 'view' },741{ label: vscode.l10n.t('2. Edit agent'), action: 'edit' },742{ label: vscode.l10n.t('3. Delete agent'), action: 'delete' },743{ label: vscode.l10n.t('4. Back'), action: 'back' },744];745746const selected = await vscode.window.showQuickPick(items, {747title: agent.config.name,748placeHolder: vscode.l10n.t('Choose an action'),749ignoreFocusOut: true,750});751752if (!selected) {753return;754}755756switch (selected.action) {757case 'view':758await this._openAgentFile(agent.filePath);759break;760case 'edit':761await this._runEditMenu(agent);762break;763case 'delete':764await this._deleteAgent(agent);765break;766case 'back':767await this._runWizard();768break;769}770}771772/**773* Shows the edit menu for an agent.774*/775private async _runEditMenu(agent: AgentWithSource): Promise<void> {776type EditItem = vscode.QuickPickItem & { action: 'open' | 'tools' | 'model' };777778const items: EditItem[] = [779{ label: '$(edit) ' + vscode.l10n.t('Open in editor'), action: 'open' },780{ label: '$(tools) ' + vscode.l10n.t('Edit tools'), action: 'tools' },781{ label: '$(symbol-misc) ' + vscode.l10n.t('Edit model'), action: 'model' },782];783784const selected = await vscode.window.showQuickPick(items, {785title: vscode.l10n.t('Edit agent: {0}', agent.config.name),786placeHolder: vscode.l10n.t('Source: {0}', agent.location.label),787ignoreFocusOut: true,788});789790if (!selected) {791return;792}793794switch (selected.action) {795case 'open':796await this._openAgentFile(agent.filePath);797break;798case 'tools': {799const tools = await this._selectTools();800if (tools) {801const updatedConfig = {802...agent.config,803allowedTools: tools.includes('*') ? undefined : tools,804};805await this._saveAgent(agent.filePath, updatedConfig);806await this._openAgentFile(agent.filePath);807}808break;809}810case 'model': {811const model = await this._selectModel();812if (model) {813const updatedConfig = {814...agent.config,815model,816};817await this._saveAgent(agent.filePath, updatedConfig);818await this._openAgentFile(agent.filePath);819}820break;821}822}823}824825/**826* Delete an agent with confirmation.827*/828private async _deleteAgent(agent: AgentWithSource): Promise<void> {829const confirm = await vscode.window.showWarningMessage(830vscode.l10n.t('Are you sure you want to delete the agent "{0}"?', agent.config.name),831{ modal: true },832vscode.l10n.t('Delete')833);834835if (confirm === vscode.l10n.t('Delete')) {836await this.fileSystemService.delete(agent.filePath);837vscode.window.showInformationMessage(vscode.l10n.t('Agent "{0}" deleted', agent.config.name));838// Return to main menu839await this._runWizard();840}841}842843/**844* Load all project agents from .claude/agents/ directories.845*/846private async _loadProjectAgents(): Promise<AgentWithSource[]> {847const agents: AgentWithSource[] = [];848const workspaceFolders = this.workspaceService.getWorkspaceFolders();849850for (const folder of workspaceFolders) {851const agentsDir = URI.joinPath(folder, '.claude', 'agents');852const location: AgentLocation = {853type: 'project',854label: vscode.l10n.t('Project'),855agentsDir,856workspaceFolder: folder,857};858859const loaded = await this._loadAgentsFromDirectory(agentsDir, location);860agents.push(...loaded);861}862863return agents;864}865866/**867* Load all user/personal agents from ~/.claude/agents/.868*/869private async _loadUserAgents(): Promise<AgentWithSource[]> {870const agentsDir = URI.joinPath(this.envService.userHome, '.claude', 'agents');871const location: AgentLocation = {872type: 'user',873label: vscode.l10n.t('Personal'),874agentsDir,875};876877return this._loadAgentsFromDirectory(agentsDir, location);878}879880/**881* Load agents from a specific directory.882*/883private async _loadAgentsFromDirectory(dir: URI, location: AgentLocation): Promise<AgentWithSource[]> {884const agents: AgentWithSource[] = [];885886try {887const entries = await this.fileSystemService.readDirectory(dir);888889for (const [name, type] of entries) {890if (type === vscode.FileType.File && name.endsWith('.md')) {891const filePath = URI.joinPath(dir, name);892try {893const config = await this._parseAgentFile(filePath);894if (config) {895agents.push({ config, location, filePath });896}897} catch (error) {898this.logService.warn(`[AgentsSlashCommand] Failed to parse agent file ${filePath.fsPath}: ${error}`);899}900}901}902} catch {903// Directory doesn't exist or can't be read904}905906return agents;907}908909/**910* Parse an agent markdown file with YAML frontmatter.911*/912private async _parseAgentFile(filePath: URI): Promise<AgentConfig | undefined> {913try {914const content = await this.fileSystemService.readFile(filePath);915const text = new TextDecoder().decode(content);916917// Parse YAML frontmatter918const frontmatterMatch = text.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);919if (!frontmatterMatch) {920return undefined;921}922923const frontmatter = frontmatterMatch[1];924const systemPrompt = frontmatterMatch[2].trim();925926// Simple YAML parsing for the fields we need927const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);928const descMatch = frontmatter.match(/^description:\s*["']?([\s\S]*?)["']?$/m);929const modelMatch = frontmatter.match(/^model:\s*(.+)$/m);930931// Parse allowedTools if present932const toolsMatch = frontmatter.match(/^allowedTools:\s*\n((?:\s+-\s+.+\n?)+)/m);933let allowedTools: string[] | undefined;934if (toolsMatch) {935allowedTools = toolsMatch[1]936.split('\n')937.map(line => line.match(/^\s+-\s+(.+)$/)?.[1])938.filter((t): t is string => !!t);939}940941if (!nameMatch || !modelMatch) {942return undefined;943}944945return {946name: nameMatch[1].trim(),947description: descMatch ? descMatch[1].trim() : '',948model: modelMatch[1].trim(),949allowedTools,950systemPrompt,951};952} catch {953return undefined;954}955}956957/**958* Save an agent to a markdown file.959*/960private async _saveAgent(filePath: URI, config: AgentConfig): Promise<void> {961// Ensure directory exists962const dir = URI.joinPath(filePath, '..');963await createDirectoryIfNotExists(this.fileSystemService, dir);964965// Build the file content966let content = `---\nname: ${config.name}\ndescription: "${config.description.replace(/"/g, '\\"')}"\nmodel: ${config.model}\n`;967968if (config.allowedTools && config.allowedTools.length > 0) {969content += 'allowedTools:\n';970for (const tool of config.allowedTools) {971content += ` - ${tool}\n`;972}973}974975content += `---\n\n${config.systemPrompt}\n`;976977await this.fileSystemService.writeFile(filePath, new TextEncoder().encode(content));978}979980/**981* Open an agent file in the editor.982*/983private async _openAgentFile(filePath: URI): Promise<void> {984const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath.fsPath));985await vscode.window.showTextDocument(doc);986}987}988989// Self-register the agents command990registerClaudeSlashCommand(AgentsSlashCommand);991992993