import React, { useState, useEffect, useMemo, useCallback, useReducer, Fragment } from 'react' import { Box, Text, useInput } from 'ink' import InkTextInput from 'ink-text-input' import { getActiveAgents, clearAgentCache } from '../utils/agentLoader' import { AgentConfig } from '../utils/agentLoader' import { writeFileSync, unlinkSync, mkdirSync, existsSync, readFileSync, renameSync } from 'fs' import { join } from 'path' import * as path from 'path' import { homedir } from 'os' import * as os from 'os' import { getCwd } from '../utils/state' import { getTheme } from '../utils/theme' import matter from 'gray-matter' import { exec, spawn } from 'child_process' import { promisify } from 'util' import { watch, FSWatcher } from 'fs' import { getMCPTools } from '../services/mcpClient' import { getModelManager } from '../utils/model' import { randomUUID } from 'crypto' const execAsync = promisify(exec) // Core constants aligned with Claude Code architecture const AGENT_LOCATIONS = { USER: "user", PROJECT: "project", BUILT_IN: "built-in", ALL: "all" } as const const UI_ICONS = { pointer: "❯", checkboxOn: "☑", checkboxOff: "☐", warning: "⚠", separator: "─", loading: "◐◑◒◓" } as const const FOLDER_CONFIG = { FOLDER_NAME: ".claude", AGENTS_DIR: "agents" } as const // Tool categories for sophisticated selection const TOOL_CATEGORIES = { read: ['Read', 'Glob', 'Grep', 'LS'], edit: ['Edit', 'MultiEdit', 'Write', 'NotebookEdit'], execution: ['Bash', 'BashOutput', 'KillBash'], web: ['WebFetch', 'WebSearch'], other: ['TodoWrite', 'ExitPlanMode', 'Task'] } as const type AgentLocation = typeof AGENT_LOCATIONS[keyof typeof AGENT_LOCATIONS] // Models will be listed dynamically from ModelManager // Comprehensive mode state for complete UI flow type ModeState = { mode: 'list-agents' | 'create-location' | 'create-method' | 'create-generate' | 'create-type' | 'create-description' | 'create-tools' | 'create-model' | 'create-color' | 'create-prompt' | 'create-confirm' | 'agent-menu' | 'view-agent' | 'edit-agent' | 'edit-tools' | 'edit-model' | 'edit-color' | 'delete-confirm' location?: AgentLocation selectedAgent?: AgentConfig previousMode?: ModeState [key: string]: any } // State for agent creation flow type CreateState = { location: AgentLocation | null agentType: string method: 'generate' | 'manual' | null generationPrompt: string whenToUse: string selectedTools: string[] selectedModel: string | null // null for inherit, or model profile modelName selectedColor: string | null systemPrompt: string isGenerating: boolean wasGenerated: boolean isAIGenerated: boolean error: string | null warnings: string[] // Cursor positions for text inputs agentTypeCursor: number whenToUseCursor: number promptCursor: number generationPromptCursor: number } type Tool = { name: string description?: string | (() => Promise) } // Map a stored model identifier to a display name via ModelManager function getDisplayModelName(modelId?: string | null): string { // null/undefined means inherit from parent (task model) if (!modelId) return 'Inherit' try { const profiles = getModelManager().getActiveModelProfiles() const profile = profiles.find((p: any) => p.modelName === modelId || p.name === modelId) return profile ? profile.name : `Custom (${modelId})` } catch (error) { console.warn('Failed to get model profiles:', error) return modelId ? `Custom (${modelId})` : 'Inherit' } } // AI Generation response type type GeneratedAgent = { identifier: string whenToUse: string systemPrompt: string } // AI generation function (use main pointer model) async function generateAgentWithClaude(prompt: string): Promise { // Import Claude service dynamically to avoid circular dependencies const { queryModel } = await import('../services/claude') const systemPrompt = `You are an expert at creating AI agent configurations. Based on the user's description, generate a specialized agent configuration. Return your response as a JSON object with exactly these fields: - identifier: A short, kebab-case identifier for the agent (e.g., "code-reviewer", "security-auditor") - whenToUse: A clear description of when this agent should be used (50-200 words) - systemPrompt: A comprehensive system prompt that defines the agent's role, capabilities, and behavior (200-500 words) Make the agent highly specialized and effective for the described use case.` try { const messages = [ { type: 'user', uuid: randomUUID(), message: { role: 'user', content: prompt }, }, ] as any const response = await queryModel('main', messages, [systemPrompt]) // Get the text content from the response - handle both string and object responses let responseText = '' if (typeof response.message?.content === 'string') { responseText = response.message.content } else if (Array.isArray(response.message?.content)) { const textContent = response.message.content.find((c: any) => c.type === 'text') responseText = textContent?.text || '' } else if (response.message?.content?.[0]?.text) { responseText = response.message.content[0].text } if (!responseText) { throw new Error('No text content in Claude response') } // 安全限制 const MAX_JSON_SIZE = 100_000 // 100KB const MAX_FIELD_LENGTH = 10_000 if (responseText.length > MAX_JSON_SIZE) { throw new Error('Response too large') } // 安全的JSON提取和解析 let parsed: any try { // 首先尝试直接解析整个响应 parsed = JSON.parse(responseText.trim()) } catch { // 如果失败,提取第一个JSON对象,限制搜索范围 const startIdx = responseText.indexOf('{') const endIdx = responseText.lastIndexOf('}') if (startIdx === -1 || endIdx === -1 || startIdx >= endIdx) { throw new Error('No valid JSON found in Claude response') } const jsonStr = responseText.substring(startIdx, endIdx + 1) if (jsonStr.length > MAX_JSON_SIZE) { throw new Error('JSON content too large') } try { parsed = JSON.parse(jsonStr) } catch (parseError) { throw new Error(`Invalid JSON format: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`) } } // 深度验证和安全清理 const identifier = String(parsed.identifier || '').slice(0, 100).trim() const whenToUse = String(parsed.whenToUse || '').slice(0, MAX_FIELD_LENGTH).trim() const agentSystemPrompt = String(parsed.systemPrompt || '').slice(0, MAX_FIELD_LENGTH).trim() // 验证必填字段 if (!identifier || !whenToUse || !agentSystemPrompt) { throw new Error('Invalid response structure: missing required fields (identifier, whenToUse, systemPrompt)') } // 清理危险字符(控制字符和非打印字符) const sanitize = (str: string) => str.replace(/[\x00-\x1F\x7F-\x9F]/g, '') // 验证identifier格式(只允许字母、数字、连字符) const cleanIdentifier = sanitize(identifier) if (!/^[a-zA-Z0-9-]+$/.test(cleanIdentifier)) { throw new Error('Invalid identifier format: only letters, numbers, and hyphens allowed') } return { identifier: cleanIdentifier, whenToUse: sanitize(whenToUse), systemPrompt: sanitize(agentSystemPrompt) } } catch (error) { console.error('AI generation failed:', error) // Fallback to a reasonable default based on the prompt const fallbackId = prompt.toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .slice(0, 30) return { identifier: fallbackId || 'custom-agent', whenToUse: `Use this agent when you need assistance with: ${prompt}`, systemPrompt: `You are a specialized assistant focused on helping with ${prompt}. Provide expert-level assistance in this domain.` } } } // Comprehensive validation system function validateAgentType(agentType: string, existingAgents: AgentConfig[] = []): { isValid: boolean errors: string[] warnings: string[] } { const errors: string[] = [] const warnings: string[] = [] if (!agentType) { errors.push("Agent type is required") return { isValid: false, errors, warnings } } if (!/^[a-zA-Z]/.test(agentType)) { errors.push("Agent type must start with a letter") } if (!/^[a-zA-Z0-9-]+$/.test(agentType)) { errors.push("Agent type can only contain letters, numbers, and hyphens") } if (agentType.length < 3) { errors.push("Agent type must be at least 3 characters long") } if (agentType.length > 50) { errors.push("Agent type must be less than 50 characters") } // Check for reserved names const reserved = ['help', 'exit', 'quit', 'agents', 'task'] if (reserved.includes(agentType.toLowerCase())) { errors.push("This name is reserved") } // Check for duplicates const duplicate = existingAgents.find(a => a.agentType === agentType) if (duplicate) { errors.push(`An agent with this name already exists in ${duplicate.location}`) } // Warnings if (agentType.includes('--')) { warnings.push("Consider avoiding consecutive hyphens") } return { isValid: errors.length === 0, errors, warnings } } function validateAgentConfig(config: Partial, existingAgents: AgentConfig[] = []): { isValid: boolean errors: string[] warnings: string[] } { const errors: string[] = [] const warnings: string[] = [] // Validate agent type if (config.agentType) { const typeValidation = validateAgentType(config.agentType, existingAgents) errors.push(...typeValidation.errors) warnings.push(...typeValidation.warnings) } // Validate description if (!config.whenToUse) { errors.push("Description is required") } else if (config.whenToUse.length < 10) { warnings.push("Description should be more descriptive (at least 10 characters)") } // Validate system prompt if (!config.systemPrompt) { errors.push("System prompt is required") } else if (config.systemPrompt.length < 20) { warnings.push("System prompt might be too short for effective agent behavior") } // Validate tools if (!config.selectedTools || config.selectedTools.length === 0) { warnings.push("No tools selected - agent will have limited capabilities") } return { isValid: errors.length === 0, errors, warnings } } // File system operations with Claude Code alignment function getAgentDirectory(location: AgentLocation): string { if (location === AGENT_LOCATIONS.BUILT_IN || location === AGENT_LOCATIONS.ALL) { throw new Error(`Cannot get directory path for ${location} agents`) } if (location === AGENT_LOCATIONS.USER) { return join(homedir(), FOLDER_CONFIG.FOLDER_NAME, FOLDER_CONFIG.AGENTS_DIR) } else { return join(getCwd(), FOLDER_CONFIG.FOLDER_NAME, FOLDER_CONFIG.AGENTS_DIR) } } function getAgentFilePath(agent: AgentConfig): string { if (agent.location === 'built-in') { throw new Error('Cannot get file path for built-in agents') } const dir = getAgentDirectory(agent.location as AgentLocation) return join(dir, `${agent.agentType}.md`) } function ensureDirectoryExists(location: AgentLocation): string { const dir = getAgentDirectory(location) if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }) } return dir } // Generate agent file content function generateAgentFileContent( agentType: string, description: string, tools: string[] | '*', systemPrompt: string, model?: string, color?: string ): string { // Use YAML multi-line string for description to avoid escaping issues const descriptionLines = description.split('\n') const formattedDescription = descriptionLines.length > 1 ? `|\n ${descriptionLines.join('\n ')}` : JSON.stringify(description) const lines = [ '---', `name: ${agentType}`, `description: ${formattedDescription}` ] if (tools) { if (tools === '*') { lines.push(`tools: "*"`) } else if (Array.isArray(tools) && tools.length > 0) { lines.push(`tools: [${tools.map(t => `"${t}"`).join(', ')}]`) } } if (model) { lines.push(`model: ${model}`) } if (color) { lines.push(`color: ${color}`) } lines.push('---', '', systemPrompt) return lines.join('\n') } // Save agent to file async function saveAgent( location: AgentLocation, agentType: string, description: string, tools: string[], systemPrompt: string, model?: string, color?: string, throwIfExists: boolean = true ): Promise { if (location === AGENT_LOCATIONS.BUILT_IN) { throw new Error('Cannot save built-in agents') } ensureDirectoryExists(location) const filePath = join(getAgentDirectory(location), `${agentType}.md`) const tempFile = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substr(2, 9)}` // Ensure tools is properly typed for file saving const toolsForFile: string[] | '*' = Array.isArray(tools) && tools.length === 1 && tools[0] === '*' ? '*' : tools const content = generateAgentFileContent(agentType, description, toolsForFile, systemPrompt, model, color) try { // 先写入临时文件,使用 'wx' 确保不覆盖现有文件 writeFileSync(tempFile, content, { encoding: 'utf-8', flag: 'wx' }) // 检查目标文件是否存在(原子性检查) if (throwIfExists && existsSync(filePath)) { // 清理临时文件 try { unlinkSync(tempFile) } catch {} throw new Error(`Agent file already exists: ${filePath}`) } // 原子性重命名(在大多数文件系统上,rename是原子操作) renameSync(tempFile, filePath) } catch (error) { // 确保清理临时文件 try { if (existsSync(tempFile)) { unlinkSync(tempFile) } } catch (cleanupError) { console.warn('Failed to cleanup temp file:', cleanupError) } throw error } } // Delete agent file async function deleteAgent(agent: AgentConfig): Promise { if (agent.location === 'built-in') { throw new Error('Cannot delete built-in agents') } const filePath = getAgentFilePath(agent) unlinkSync(filePath) } // Open file in system editor - 安全版本,防止命令注入 async function openInEditor(filePath: string): Promise { // 安全验证:确保路径在允许的目录内 const resolvedPath = path.resolve(filePath) const projectDir = process.cwd() const homeDir = os.homedir() const isSub = (base: string, target: string) => { const path = require('path') const rel = path.relative(path.resolve(base), path.resolve(target)) if (!rel || rel === '') return true if (rel.startsWith('..')) return false if (path.isAbsolute(rel)) return false return true } if (!isSub(projectDir, resolvedPath) && !isSub(homeDir, resolvedPath)) { throw new Error('Access denied: File path outside allowed directories') } // 验证文件扩展名 if (!resolvedPath.endsWith('.md')) { throw new Error('Invalid file type: Only .md files are allowed') } return new Promise((resolve, reject) => { const platform = process.platform let command: string let args: string[] // 使用spawn而不是exec,避免shell注入 switch (platform) { case 'darwin': // macOS command = 'open' args = [resolvedPath] break case 'win32': // Windows command = 'cmd' args = ['/c', 'start', '', resolvedPath] break default: // Linux and others command = 'xdg-open' args = [resolvedPath] break } // 使用spawn替代exec,避免shell解释 const child = spawn(command, args, { detached: true, stdio: 'ignore', // 确保没有shell解释 shell: false }) child.unref() // 允许父进程退出 child.on('error', (error) => { reject(new Error(`Failed to open editor: ${error.message}`)) }) child.on('exit', (code) => { if (code === 0) { resolve() } else { reject(new Error(`Editor exited with code ${code}`)) } }) }) } // Update existing agent async function updateAgent( agent: AgentConfig, description: string, tools: string[] | '*', systemPrompt: string, color?: string, model?: string ): Promise { if (agent.location === 'built-in') { throw new Error('Cannot update built-in agents') } const toolsForFile = tools.length === 1 && tools[0] === '*' ? '*' : tools const content = generateAgentFileContent(agent.agentType, description, toolsForFile, systemPrompt, model, color) const filePath = getAgentFilePath(agent) writeFileSync(filePath, content, { encoding: 'utf-8', flag: 'w' }) } // Enhanced UI Components with Claude Code alignment interface HeaderProps { title: string subtitle?: string step?: number totalSteps?: number children?: React.ReactNode } function Header({ title, subtitle, step, totalSteps, children }: HeaderProps) { const theme = getTheme() return ( {title} {subtitle && ( {step && totalSteps ? `Step ${step}/${totalSteps}: ` : ''}{subtitle} )} {children} ) } interface InstructionBarProps { instructions?: string } function InstructionBar({ instructions = "Press ↑↓ to navigate · Enter to select · Esc to go back" }: InstructionBarProps) { const theme = getTheme() return ( {instructions} ) } interface SelectListProps { options: Array<{ label: string; value: string }> selectedIndex: number onChange: (value: string) => void onCancel?: () => void numbered?: boolean } function SelectList({ options, selectedIndex, onChange, onCancel, numbered = true }: SelectListProps) { const theme = getTheme() useInput((input, key) => { if (key.escape && onCancel) { onCancel() } else if (key.return) { onChange(options[selectedIndex].value) } }) return ( {options.map((option, idx) => ( {idx === selectedIndex ? `${UI_ICONS.pointer} ` : " "} {numbered ? `${idx + 1}. ` : ''}{option.label} ))} ) } // Multiline text input component with better UX interface MultilineTextInputProps { value: string onChange: (value: string) => void placeholder?: string onSubmit?: () => void focus?: boolean rows?: number error?: string | null } function MultilineTextInput({ value, onChange, placeholder = '', onSubmit, focus = true, rows = 5, error }: MultilineTextInputProps) { const theme = getTheme() const [internalValue, setInternalValue] = useState(value) const [cursorBlink, setCursorBlink] = useState(true) // Sync with external value changes useEffect(() => { setInternalValue(value) }, [value]) // Cursor blink animation useEffect(() => { if (!focus) return const timer = setInterval(() => { setCursorBlink(prev => !prev) }, 500) return () => clearInterval(timer) }, [focus]) // Calculate display metrics const lines = internalValue.split('\n') const lineCount = lines.length const charCount = internalValue.length const isEmpty = !internalValue.trim() const hasContent = !isEmpty // Format lines for display with word wrapping const formatLines = (text: string): string[] => { if (!text && placeholder) { return [placeholder] } const maxWidth = 70 // Maximum characters per line const result: string[] = [] const textLines = text.split('\n') textLines.forEach(line => { if (line.length <= maxWidth) { result.push(line) } else { // Word wrap long lines let remaining = line while (remaining.length > 0) { result.push(remaining.slice(0, maxWidth)) remaining = remaining.slice(maxWidth) } } }) return result.length > 0 ? result : [''] } const displayLines = formatLines(internalValue) const visibleLines = displayLines.slice(Math.max(0, displayLines.length - rows)) // Handle submit const handleSubmit = () => { if (internalValue.trim() && onSubmit) { onSubmit() } } return ( {/* Modern card-style input container */} {/* Input area */} {/* Use ink-text-input for better input handling */} { setInternalValue(val) onChange(val) }} onSubmit={handleSubmit} focus={focus} placeholder={placeholder} /> {/* Show cursor indicator when focused */} {focus && cursorBlink && hasContent && ( _ )} {/* Status bar */} {hasContent ? ( ✓ {charCount} chars • {lineCount} line{lineCount !== 1 ? 's' : ''} ) : ( ○ Type to begin... )} {error ? ( ⚠ {error} ) : ( {hasContent ? 'Ready' : 'Waiting'} )} {/* Instructions */} Press Enter to submit · Shift+Enter for new line ) } // Loading spinner component interface LoadingSpinnerProps { text?: string } function LoadingSpinner({ text }: LoadingSpinnerProps) { const theme = getTheme() const [frame, setFrame] = useState(0) useEffect(() => { const interval = setInterval(() => { setFrame(prev => (prev + 1) % UI_ICONS.loading.length) }, 100) return () => clearInterval(interval) }, []) return ( {UI_ICONS.loading[frame]} {text && {text}} ) } // Complete agents UI with comprehensive state management interface AgentsUIProps { onExit: (message?: string) => void } function AgentsUI({ onExit }: AgentsUIProps) { const theme = getTheme() // Core state management const [modeState, setModeState] = useState({ mode: "list-agents", location: "all" as AgentLocation }) const [agents, setAgents] = useState([]) const [changes, setChanges] = useState([]) const [refreshKey, setRefreshKey] = useState(0) const [loading, setLoading] = useState(true) const [tools, setTools] = useState([]) // Creation state using reducer for complex flow management const [createState, setCreateState] = useReducer( (state: CreateState, action: any) => { switch (action.type) { case 'RESET': return { location: null, agentType: '', method: null, generationPrompt: '', whenToUse: '', selectedTools: [], selectedModel: null, selectedColor: null, systemPrompt: '', isGenerating: false, wasGenerated: false, isAIGenerated: false, error: null, warnings: [], agentTypeCursor: 0, whenToUseCursor: 0, promptCursor: 0, generationPromptCursor: 0 } case 'SET_LOCATION': return { ...state, location: action.value } case 'SET_METHOD': return { ...state, method: action.value } case 'SET_AGENT_TYPE': return { ...state, agentType: action.value, error: null } case 'SET_GENERATION_PROMPT': return { ...state, generationPrompt: action.value } case 'SET_WHEN_TO_USE': return { ...state, whenToUse: action.value, error: null } case 'SET_SELECTED_TOOLS': return { ...state, selectedTools: action.value } case 'SET_SELECTED_MODEL': return { ...state, selectedModel: action.value } case 'SET_SELECTED_COLOR': return { ...state, selectedColor: action.value } case 'SET_SYSTEM_PROMPT': return { ...state, systemPrompt: action.value } case 'SET_IS_GENERATING': return { ...state, isGenerating: action.value } case 'SET_WAS_GENERATED': return { ...state, wasGenerated: action.value } case 'SET_IS_AI_GENERATED': return { ...state, isAIGenerated: action.value } case 'SET_ERROR': return { ...state, error: action.value } case 'SET_WARNINGS': return { ...state, warnings: action.value } case 'SET_CURSOR': return { ...state, [action.field]: action.value } default: return state } }, { location: null, agentType: '', method: null, generationPrompt: '', whenToUse: '', selectedTools: [], selectedModel: null, selectedColor: null, systemPrompt: '', isGenerating: false, wasGenerated: false, isAIGenerated: false, error: null, warnings: [], agentTypeCursor: 0, whenToUseCursor: 0, promptCursor: 0, generationPromptCursor: 0 } ) // Load agents and tools dynamically const loadAgents = useCallback(async () => { setLoading(true) clearAgentCache() // 创建取消令牌以防止竞态条件 const abortController = new AbortController() const loadingId = Date.now() // 用于标识这次加载 try { const result = await getActiveAgents() // 检查是否仍然是当前的加载请求 if (abortController.signal.aborted) { return // 组件已卸载或新的加载已开始 } setAgents(result) // Update selectedAgent if there's one currently selected (for live reload) if (modeState.selectedAgent) { const freshSelectedAgent = result.find(a => a.agentType === modeState.selectedAgent!.agentType) if (freshSelectedAgent) { setModeState(prev => ({ ...prev, selectedAgent: freshSelectedAgent })) } } // Load available tools dynamically from tool registry const availableTools: Tool[] = [] // Core built-in tools let coreTools = [ { name: 'Read', description: 'Read files from filesystem' }, { name: 'Write', description: 'Write files to filesystem' }, { name: 'Edit', description: 'Edit existing files' }, { name: 'MultiEdit', description: 'Make multiple edits to files' }, { name: 'NotebookEdit', description: 'Edit Jupyter notebooks' }, { name: 'Bash', description: 'Execute bash commands' }, { name: 'Glob', description: 'Find files matching patterns' }, { name: 'Grep', description: 'Search file contents' }, { name: 'LS', description: 'List directory contents' }, { name: 'WebFetch', description: 'Fetch web content' }, { name: 'WebSearch', description: 'Search the web' }, { name: 'TodoWrite', description: 'Manage task lists' } ] // Hide agent orchestration/self-control tools for subagent configs coreTools = coreTools.filter(t => t.name !== 'Task' && t.name !== 'ExitPlanMode') availableTools.push(...coreTools) // Try to load MCP tools dynamically try { const mcpTools = await getMCPTools() if (Array.isArray(mcpTools) && mcpTools.length > 0) { availableTools.push(...mcpTools) } } catch (error) { console.warn('Failed to load MCP tools:', error) } if (!abortController.signal.aborted) { setTools(availableTools) } } catch (error) { if (!abortController.signal.aborted) { console.error('Failed to load agents:', error) } } finally { if (!abortController.signal.aborted) { setLoading(false) } } // 返回取消函数供useEffect使用 return () => abortController.abort() }, []) // Remove mock MCP loader; real MCP tools are loaded via getMCPTools() useEffect(() => { let cleanup: (() => void) | undefined const load = async () => { cleanup = await loadAgents() } load() return () => { if (cleanup) { cleanup() } } }, [refreshKey, loadAgents]) // Local file watcher removed; rely on global watcher started in CLI. // Global keyboard handling: ESC 逐级返回 useInput((input, key) => { if (!key.escape) return const changesSummary = changes.length > 0 ? `Agent changes:\n${changes.join('\n')}` : undefined const current = modeState.mode if (current === 'list-agents') { onExit(changesSummary) return } // Hierarchical back navigation switch (current) { case 'create-location': setModeState({ mode: 'list-agents', location: 'all' as AgentLocation }) break case 'create-method': setModeState({ mode: 'create-location', location: modeState.location }) break case 'create-generate': setModeState({ mode: 'create-location', location: modeState.location }) break case 'create-type': setModeState({ mode: 'create-generate', location: modeState.location }) break case 'create-prompt': setModeState({ mode: 'create-type', location: modeState.location }) break case 'create-description': setModeState({ mode: 'create-prompt', location: modeState.location }) break case 'create-tools': setModeState({ mode: 'create-description', location: modeState.location }) break case 'create-model': setModeState({ mode: 'create-tools', location: modeState.location }) break case 'create-color': setModeState({ mode: 'create-model', location: modeState.location }) break case 'create-confirm': setModeState({ mode: 'create-color', location: modeState.location }) break case 'agent-menu': setModeState({ mode: 'list-agents', location: 'all' as AgentLocation }) break case 'view-agent': setModeState({ mode: 'agent-menu', selectedAgent: modeState.selectedAgent }) break case 'edit-agent': setModeState({ mode: 'agent-menu', selectedAgent: modeState.selectedAgent }) break case 'edit-tools': case 'edit-model': case 'edit-color': setModeState({ mode: 'edit-agent', selectedAgent: modeState.selectedAgent }) break case 'delete-confirm': setModeState({ mode: 'agent-menu', selectedAgent: modeState.selectedAgent }) break default: setModeState({ mode: 'list-agents', location: 'all' as AgentLocation }) break } }) // Event handlers const handleAgentSelect = useCallback((agent: AgentConfig) => { setModeState({ mode: "agent-menu", location: modeState.location, selectedAgent: agent }) }, [modeState]) const handleCreateNew = useCallback(() => { console.log('=== STARTING AGENT CREATION FLOW ===') console.log('Current mode state:', modeState) setCreateState({ type: 'RESET' }) console.log('Reset create state') setModeState({ mode: "create-location" }) console.log('Set mode to create-location') console.log('=== CREATE NEW HANDLER COMPLETED ===') }, [modeState]) const handleAgentCreated = useCallback((message: string) => { setChanges(prev => [...prev, message]) setRefreshKey(prev => prev + 1) setModeState({ mode: "list-agents", location: "all" as AgentLocation }) }, []) const handleAgentDeleted = useCallback((message: string) => { setChanges(prev => [...prev, message]) setRefreshKey(prev => prev + 1) setModeState({ mode: "list-agents", location: "all" as AgentLocation }) }, []) if (loading) { return (
) } // Render based on current mode switch (modeState.mode) { case "list-agents": return ( onExit()} onSelect={handleAgentSelect} onCreateNew={handleCreateNew} changes={changes} /> ) case "create-location": return ( ) case "create-method": return ( ) case "create-generate": return ( ) case "create-type": return ( ) case "create-description": return ( ) case "create-tools": return ( ) case "create-model": return ( ) case "create-color": return ( ) case "create-prompt": return ( ) case "create-confirm": return ( ) case "agent-menu": return ( ) case "view-agent": return ( ) case "edit-agent": return ( ) case "edit-tools": return ( { setChanges(prev => [...prev, message]) setRefreshKey(prev => prev + 1) setModeState({ mode: "agent-menu", selectedAgent: updated }) }} /> ) case "edit-model": return ( { setChanges(prev => [...prev, message]) setRefreshKey(prev => prev + 1) setModeState({ mode: "agent-menu", selectedAgent: updated }) }} /> ) case "edit-color": return ( { setChanges(prev => [...prev, message]) setRefreshKey(prev => prev + 1) setModeState({ mode: "agent-menu", selectedAgent: updated }) }} /> ) case "delete-confirm": return ( ) default: return (
Mode: {modeState.mode} (Not implemented yet) Press Esc to go back
) } } interface AgentListProps { location: AgentLocation agents: AgentConfig[] allAgents: AgentConfig[] onBack: () => void onSelect: (agent: AgentConfig) => void onCreateNew?: () => void changes: string[] } function AgentListView({ location, agents, allAgents, onBack, onSelect, onCreateNew, changes }: AgentListProps) { const theme = getTheme() const allAgentsList = allAgents || agents const customAgents = allAgentsList.filter(a => a.location !== "built-in") const builtInAgents = allAgentsList.filter(a => a.location === "built-in") const [selectedAgent, setSelectedAgent] = useState(null) const [onCreateOption, setOnCreateOption] = useState(true) const [currentLocation, setCurrentLocation] = useState(location) const [inLocationTabs, setInLocationTabs] = useState(false) const [selectedLocationTab, setSelectedLocationTab] = useState(0) const locationTabs = [ { label: "All", value: "all" as AgentLocation }, { label: "Personal", value: "user" as AgentLocation }, { label: "Project", value: "project" as AgentLocation } ] const activeMap = useMemo(() => { const map = new Map() agents.forEach(a => map.set(a.agentType, a)) return map }, [agents]) const checkOverride = (agent: AgentConfig) => { const active = activeMap.get(agent.agentType) const isOverridden = !!(active && active.location !== agent.location) return { isOverridden, overriddenBy: isOverridden ? active.location : null } } const renderCreateOption = () => ( {onCreateOption ? `${UI_ICONS.pointer} ` : " "} ✨ Create new agent ) const renderAgent = (agent: AgentConfig, isBuiltIn = false) => { const isSelected = !isBuiltIn && !onCreateOption && selectedAgent?.agentType === agent.agentType && selectedAgent?.location === agent.location const { isOverridden, overriddenBy } = checkOverride(agent) const dimmed = isBuiltIn || isOverridden const color = !isBuiltIn && isSelected ? theme.primary : undefined // Extract model from agent metadata const agentModel = (agent as any).model || null const modelDisplay = getDisplayModelName(agentModel) return ( {isBuiltIn ? "" : isSelected ? `${UI_ICONS.pointer} ` : " "} {agent.agentType} {" · "}{modelDisplay} {overriddenBy && ( {UI_ICONS.warning} overridden by {overriddenBy} )} ) } const displayAgents = useMemo(() => { if (currentLocation === "all") { return [ ...customAgents.filter(a => a.location === "user"), ...customAgents.filter(a => a.location === "project") ] } else if (currentLocation === "user" || currentLocation === "project") { return customAgents.filter(a => a.location === currentLocation) } return customAgents }, [customAgents, currentLocation]) // 更新当前选中的标签索引 useEffect(() => { const tabIndex = locationTabs.findIndex(tab => tab.value === currentLocation) if (tabIndex !== -1) { setSelectedLocationTab(tabIndex) } }, [currentLocation, locationTabs]) // 确保当有agents时,初始化选择状态 useEffect(() => { if (displayAgents.length > 0 && !selectedAgent && !onCreateOption) { setOnCreateOption(true) // 默认选择创建选项 } }, [displayAgents.length, selectedAgent, onCreateOption]) useInput((input, key) => { if (key.escape) { if (inLocationTabs) { setInLocationTabs(false) return } onBack() return } if (key.return) { if (inLocationTabs) { setCurrentLocation(locationTabs[selectedLocationTab].value) setInLocationTabs(false) return } if (onCreateOption && onCreateNew) { onCreateNew() } else if (selectedAgent) { onSelect(selectedAgent) } return } // Tab键进入/退出标签导航 if (key.tab) { setInLocationTabs(!inLocationTabs) return } // 在标签导航模式 if (inLocationTabs) { if (key.leftArrow) { setSelectedLocationTab(prev => prev > 0 ? prev - 1 : locationTabs.length - 1) } else if (key.rightArrow) { setSelectedLocationTab(prev => prev < locationTabs.length - 1 ? prev + 1 : 0) } return } // 键盘导航 - 这是关键缺失的功能 if (key.upArrow || key.downArrow) { const allNavigableItems = [] // 添加创建选项 if (onCreateNew) { allNavigableItems.push({ type: 'create', agent: null }) } // 添加可导航的agents displayAgents.forEach(agent => { const { isOverridden } = checkOverride(agent) if (!isOverridden) { // 只显示未被覆盖的agents allNavigableItems.push({ type: 'agent', agent }) } }) if (allNavigableItems.length === 0) return if (key.upArrow) { if (onCreateOption) { // 从创建选项向上到最后一个agent const lastAgent = allNavigableItems[allNavigableItems.length - 1] if (lastAgent.type === 'agent') { setSelectedAgent(lastAgent.agent) setOnCreateOption(false) } } else if (selectedAgent) { const currentIndex = allNavigableItems.findIndex( item => item.type === 'agent' && item.agent?.agentType === selectedAgent.agentType && item.agent?.location === selectedAgent.location ) if (currentIndex > 0) { const prevItem = allNavigableItems[currentIndex - 1] if (prevItem.type === 'create') { setOnCreateOption(true) setSelectedAgent(null) } else { setSelectedAgent(prevItem.agent) } } else { // 到达顶部,回到创建选项 if (onCreateNew) { setOnCreateOption(true) setSelectedAgent(null) } } } } else if (key.downArrow) { if (onCreateOption) { // 从创建选项向下到第一个agent const firstAgent = allNavigableItems.find(item => item.type === 'agent') if (firstAgent) { setSelectedAgent(firstAgent.agent) setOnCreateOption(false) } } else if (selectedAgent) { const currentIndex = allNavigableItems.findIndex( item => item.type === 'agent' && item.agent?.agentType === selectedAgent.agentType && item.agent?.location === selectedAgent.location ) if (currentIndex < allNavigableItems.length - 1) { const nextItem = allNavigableItems[currentIndex + 1] if (nextItem.type === 'agent') { setSelectedAgent(nextItem.agent) } } else { // 到达底部,回到创建选项 if (onCreateNew) { setOnCreateOption(true) setSelectedAgent(null) } } } } } }) // 特殊的键盘输入处理组件用于空状态 const EmptyStateInput = () => { useInput((input, key) => { if (key.escape) { onBack() return } if (key.return && onCreateNew) { onCreateNew() return } }) return null } if (!agents.length || (currentLocation !== "built-in" && !customAgents.length)) { return (
{onCreateNew && ( {renderCreateOption()} )} 💭 What are agents? Specialized AI assistants that Claude can delegate to for specific tasks. Each agent has its own context, prompt, and tools. 💡 Popular agent ideas: • 🔍 Code Reviewer - Reviews PRs for best practices • 🔒 Security Auditor - Finds vulnerabilities • ⚡ Performance Optimizer - Improves code speed • 🧑‍💼 Tech Lead - Makes architecture decisions • 🎨 UX Expert - Improves user experience {currentLocation !== "built-in" && builtInAgents.length > 0 && ( <> {UI_ICONS.separator.repeat(40)} Built-in (always available): {builtInAgents.map(a => renderAgent(a, true))} )}
) } return (
{changes.length > 0 && ( {changes[changes.length - 1]} )} {/* Fancy location tabs */} {locationTabs.map((tab, idx) => { const isActive = currentLocation === tab.value const isSelected = inLocationTabs && idx === selectedLocationTab return ( {isSelected ? '▶ ' : isActive ? '◉ ' : '○ '} {tab.label} {idx < locationTabs.length - 1 && | } ) })} {currentLocation === 'all' ? 'Showing all agents' : currentLocation === 'user' ? 'Personal agents (~/.claude/agents)' : 'Project agents (.claude/agents)'} {onCreateNew && ( {renderCreateOption()} )} {currentLocation === "all" ? ( <> {customAgents.filter(a => a.location === "user").length > 0 && ( <> Personal: {customAgents.filter(a => a.location === "user").map(a => renderAgent(a))} )} {customAgents.filter(a => a.location === "project").length > 0 && ( <> a.location === "user").length > 0 ? 1 : 0}> Project: {customAgents.filter(a => a.location === "project").map(a => renderAgent(a))} )} {builtInAgents.length > 0 && ( <> 0 ? 1 : 0}> {UI_ICONS.separator.repeat(40)} Built-in: {builtInAgents.map(a => renderAgent(a, true))} )} ) : ( <> {displayAgents.map(a => renderAgent(a))} {currentLocation !== "built-in" && builtInAgents.length > 0 && ( <> {UI_ICONS.separator.repeat(40)} Built-in: {builtInAgents.map(a => renderAgent(a, true))} )} )}
) } // Common interface for creation step props interface StepProps { createState: CreateState setCreateState: React.Dispatch setModeState: (state: ModeState) => void } // Step 3: AI Generation interface GenerateStepProps extends StepProps { existingAgents: AgentConfig[] } function GenerateStep({ createState, setCreateState, setModeState, existingAgents }: GenerateStepProps) { const handleSubmit = async () => { if (createState.generationPrompt.trim()) { setCreateState({ type: 'SET_IS_GENERATING', value: true }) setCreateState({ type: 'SET_ERROR', value: null }) try { const generated = await generateAgentWithClaude(createState.generationPrompt) // Validate the generated identifier doesn't conflict const validation = validateAgentType(generated.identifier, existingAgents) let finalIdentifier = generated.identifier if (!validation.isValid) { // Add a suffix to make it unique let counter = 1 while (true) { const testId = `${generated.identifier}-${counter}` const testValidation = validateAgentType(testId, existingAgents) if (testValidation.isValid) { finalIdentifier = testId break } counter++ if (counter > 10) { finalIdentifier = `custom-agent-${Date.now()}` break } } } setCreateState({ type: 'SET_AGENT_TYPE', value: finalIdentifier }) setCreateState({ type: 'SET_WHEN_TO_USE', value: generated.whenToUse }) setCreateState({ type: 'SET_SYSTEM_PROMPT', value: generated.systemPrompt }) setCreateState({ type: 'SET_WAS_GENERATED', value: true }) setCreateState({ type: 'SET_IS_GENERATING', value: false }) setModeState({ mode: 'create-tools', location: createState.location }) } catch (error) { console.error('Generation failed:', error) setCreateState({ type: 'SET_ERROR', value: 'Failed to generate agent. Please try again or use manual configuration.' }) setCreateState({ type: 'SET_IS_GENERATING', value: false }) } } } return (
{createState.isGenerating ? ( {createState.generationPrompt} ) : ( setCreateState({ type: 'SET_GENERATION_PROMPT', value })} placeholder="An expert that reviews pull requests for best practices, security issues, and suggests improvements..." onSubmit={handleSubmit} error={createState.error} rows={3} /> )}
) } // Step 4: Manual type input (for manual method) interface TypeStepProps extends StepProps { existingAgents: AgentConfig[] } function TypeStep({ createState, setCreateState, setModeState, existingAgents }: TypeStepProps) { const handleSubmit = () => { const validation = validateAgentType(createState.agentType, existingAgents) if (validation.isValid) { setModeState({ mode: 'create-prompt', location: createState.location }) } else { setCreateState({ type: 'SET_ERROR', value: validation.errors[0] }) } } return (
setCreateState({ type: 'SET_AGENT_TYPE', value })} placeholder="e.g. code-reviewer, tech-lead" onSubmit={handleSubmit} /> {createState.error && ( ⚠ {createState.error} )}
) } // Step 5: Description input function DescriptionStep({ createState, setCreateState, setModeState }: StepProps) { const handleSubmit = () => { if (createState.whenToUse.trim()) { setModeState({ mode: 'create-tools', location: createState.location }) } } return (
setCreateState({ type: 'SET_WHEN_TO_USE', value })} placeholder="Use this agent when you need to review code for best practices, security issues..." onSubmit={handleSubmit} error={createState.error} rows={4} />
) } // Step 6: Tools selection interface ToolsStepProps extends StepProps { tools: Tool[] } function ToolsStep({ createState, setCreateState, setModeState, tools }: ToolsStepProps) { const [selectedIndex, setSelectedIndex] = useState(0) // Default to all tools selected initially const initialSelection = createState.selectedTools.length > 0 ? new Set(createState.selectedTools) : new Set(tools.map(t => t.name)) // Select all tools by default const [selectedTools, setSelectedTools] = useState>(initialSelection) const [showAdvanced, setShowAdvanced] = useState(false) const [selectedCategory, setSelectedCategory] = useState('all') // Categorize tools const categorizedTools = useMemo(() => { const categories: Record = { read: [], edit: [], execution: [], web: [], mcp: [], other: [] } tools.forEach(tool => { let categorized = false // Check MCP tools first if (tool.name.startsWith('mcp__')) { categories.mcp.push(tool) categorized = true } else { // Check built-in categories for (const [category, toolNames] of Object.entries(TOOL_CATEGORIES)) { if (Array.isArray(toolNames) && toolNames.includes(tool.name)) { categories[category as keyof typeof categories]?.push(tool) categorized = true break } } } if (!categorized) { categories.other.push(tool) } }) return categories }, [tools]) const displayTools = useMemo(() => { if (selectedCategory === 'all') { return tools } return categorizedTools[selectedCategory] || [] }, [selectedCategory, tools, categorizedTools]) const allSelected = selectedTools.size === tools.length && tools.length > 0 const categoryOptions = [ { id: 'all', label: `All (${tools.length})` }, { id: 'read', label: `Read (${categorizedTools.read.length})` }, { id: 'edit', label: `Edit (${categorizedTools.edit.length})` }, { id: 'execution', label: `Execution (${categorizedTools.execution.length})` }, { id: 'web', label: `Web (${categorizedTools.web.length})` }, { id: 'mcp', label: `MCP (${categorizedTools.mcp.length})` }, { id: 'other', label: `Other (${categorizedTools.other.length})` } ].filter(cat => cat.id === 'all' || categorizedTools[cat.id]?.length > 0) // Calculate category selections const readSelected = categorizedTools.read.every(tool => selectedTools.has(tool.name)) const editSelected = categorizedTools.edit.every(tool => selectedTools.has(tool.name)) const execSelected = categorizedTools.execution.every(tool => selectedTools.has(tool.name)) const webSelected = categorizedTools.web.every(tool => selectedTools.has(tool.name)) const options: Array<{ id: string label: string isContinue?: boolean isAll?: boolean isTool?: boolean isCategory?: boolean isAdvancedToggle?: boolean isSeparator?: boolean }> = [ { id: 'continue', label: 'Save', isContinue: true }, { id: 'separator1', label: '────────────────────────────────────', isSeparator: true }, { id: 'all', label: `${allSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} All tools`, isAll: true }, { id: 'read', label: `${readSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Read-only tools`, isCategory: true }, { id: 'edit', label: `${editSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Edit tools`, isCategory: true }, { id: 'execution', label: `${execSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Execution tools`, isCategory: true }, { id: 'separator2', label: '────────────────────────────────────', isSeparator: true }, { id: 'advanced', label: `[ ${showAdvanced ? 'Hide' : 'Show'} advanced options ]`, isAdvancedToggle: true }, ...(showAdvanced ? displayTools.map(tool => ({ id: tool.name, label: `${selectedTools.has(tool.name) ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} ${tool.name}`, isTool: true })) : []) ] const handleSelect = () => { const option = options[selectedIndex] as any // Type assertion for union type if (!option) return if (option.isSeparator) return if (option.isContinue) { const result = allSelected ? ['*'] : Array.from(selectedTools) setCreateState({ type: 'SET_SELECTED_TOOLS', value: result }) setModeState({ mode: 'create-model', location: createState.location }) } else if (option.isAdvancedToggle) { setShowAdvanced(!showAdvanced) } else if (option.isAll) { if (allSelected) { setSelectedTools(new Set()) } else { setSelectedTools(new Set(tools.map(t => t.name))) } } else if (option.isCategory) { const categoryName = option.id as keyof typeof categorizedTools const categoryTools = categorizedTools[categoryName] || [] const newSelected = new Set(selectedTools) const categorySelected = categoryTools.every(tool => selectedTools.has(tool.name)) if (categorySelected) { // Unselect all tools in this category categoryTools.forEach(tool => newSelected.delete(tool.name)) } else { // Select all tools in this category categoryTools.forEach(tool => newSelected.add(tool.name)) } setSelectedTools(newSelected) } else if (option.isTool) { const newSelected = new Set(selectedTools) if (newSelected.has(option.id)) { newSelected.delete(option.id) } else { newSelected.add(option.id) } setSelectedTools(newSelected) } } useInput((input, key) => { if (key.return) { handleSelect() } else if (key.upArrow) { setSelectedIndex(prev => { let newIndex = prev > 0 ? prev - 1 : options.length - 1 // Skip separators when going up while (options[newIndex] && (options[newIndex] as any).isSeparator) { newIndex = newIndex > 0 ? newIndex - 1 : options.length - 1 } return newIndex }) } else if (key.downArrow) { setSelectedIndex(prev => { let newIndex = prev < options.length - 1 ? prev + 1 : 0 // Skip separators when going down while (options[newIndex] && (options[newIndex] as any).isSeparator) { newIndex = newIndex < options.length - 1 ? newIndex + 1 : 0 } return newIndex }) } }) return (
{options.map((option, idx) => { const isSelected = idx === selectedIndex const isContinue = option.isContinue const isAdvancedToggle = option.isAdvancedToggle const isSeparator = option.isSeparator return ( {isSeparator ? option.label : `${isSelected ? `${UI_ICONS.pointer} ` : ' '}${isContinue || isAdvancedToggle ? `${option.label}` : option.label}` } {option.isTool && isSelected && tools.find(t => t.name === option.id)?.description && ( {tools.find(t => t.name === option.id)?.description} )} ) })} {allSelected ? 'All tools selected' : `${selectedTools.size} of ${tools.length} tools selected`} {selectedCategory !== 'all' && ( Filtering: {selectedCategory} tools )}
) } // Step 6: Model selection (clean design like /models) function ModelStep({ createState, setCreateState, setModeState }: StepProps) { const theme = getTheme() const manager = getModelManager() const profiles = manager.getActiveModelProfiles() // Group models by provider const groupedModels = profiles.reduce((acc: any, profile: any) => { const provider = profile.provider || 'Default' if (!acc[provider]) acc[provider] = [] acc[provider].push(profile) return acc }, {}) // Flatten with inherit option const modelOptions = [ { id: null, name: '◈ Inherit from parent', provider: 'System', modelName: 'default' }, ...Object.entries(groupedModels).flatMap(([provider, models]: any) => models.map((p: any) => ({ id: p.modelName, name: p.name, provider: provider, modelName: p.modelName })) ) ] const [selectedIndex, setSelectedIndex] = useState(() => { const idx = modelOptions.findIndex(m => m.id === createState.selectedModel) return idx >= 0 ? idx : 0 }) const handleSelect = (modelId: string | null) => { setCreateState({ type: 'SET_SELECTED_MODEL', value: modelId }) setModeState({ mode: 'create-color', location: createState.location }) } useInput((input, key) => { if (key.return) { handleSelect(modelOptions[selectedIndex].id) } else if (key.upArrow) { setSelectedIndex(prev => (prev > 0 ? prev - 1 : modelOptions.length - 1)) } else if (key.downArrow) { setSelectedIndex(prev => (prev < modelOptions.length - 1 ? prev + 1 : 0)) } }) return (
{modelOptions.map((model, index) => { const isSelected = index === selectedIndex const isInherit = model.id === null return ( {isSelected ? UI_ICONS.pointer : ' '} {model.name} {!isInherit && ( {model.provider} • {model.modelName} )} ) })}
) } // Step 7: Color selection (using hex colors for display) function ColorStep({ createState, setCreateState, setModeState }: StepProps) { const theme = getTheme() const [selectedIndex, setSelectedIndex] = useState(0) // Color options without red/green due to display issues const colors = [ { label: 'Default', value: null, displayColor: null }, { label: 'Yellow', value: 'yellow', displayColor: 'yellow' }, { label: 'Blue', value: 'blue', displayColor: 'blue' }, { label: 'Magenta', value: 'magenta', displayColor: 'magenta' }, { label: 'Cyan', value: 'cyan', displayColor: 'cyan' }, { label: 'Gray', value: 'gray', displayColor: 'gray' }, { label: 'White', value: 'white', displayColor: 'white' } ] const handleSelect = (value: string | null) => { setCreateState({ type: 'SET_SELECTED_COLOR', value: value }) setModeState({ mode: 'create-confirm', location: createState.location }) } useInput((input, key) => { if (key.return) { handleSelect(colors[selectedIndex].value) } else if (key.upArrow) { setSelectedIndex(prev => prev > 0 ? prev - 1 : colors.length - 1) } else if (key.downArrow) { setSelectedIndex(prev => prev < colors.length - 1 ? prev + 1 : 0) } }) return (
Choose how your agent appears in the list: {colors.map((color, idx) => { const isSelected = idx === selectedIndex return ( {isSelected ? '❯ ' : ' '} {color.label} ) })} Preview: {createState.agentType || 'your-agent'}
) } // Step 8: System prompt function PromptStep({ createState, setCreateState, setModeState }: StepProps) { const handleSubmit = () => { if (createState.systemPrompt.trim()) { setModeState({ mode: 'create-description', location: createState.location }) } } return (
setCreateState({ type: 'SET_SYSTEM_PROMPT', value })} placeholder="You are a helpful assistant that specializes in..." onSubmit={handleSubmit} error={createState.error} rows={5} />
) } // Step 9: Confirmation interface ConfirmStepProps extends StepProps { tools: Tool[] onAgentCreated: (message: string) => void } function ConfirmStep({ createState, setCreateState, setModeState, tools, onAgentCreated }: ConfirmStepProps) { const [isCreating, setIsCreating] = useState(false) const theme = getTheme() const handleConfirm = async () => { setIsCreating(true) try { await saveAgent( createState.location!, createState.agentType, createState.whenToUse, createState.selectedTools, createState.systemPrompt, createState.selectedModel, createState.selectedColor || undefined ) onAgentCreated(`Created agent: ${createState.agentType}`) } catch (error) { setCreateState({ type: 'SET_ERROR', value: (error as Error).message }) setIsCreating(false) } } const validation = validateAgentConfig(createState) const toolNames = createState.selectedTools.includes('*') ? 'All tools' : createState.selectedTools.length > 0 ? createState.selectedTools.join(', ') : 'No tools' const handleEditInEditor = async () => { const filePath = createState.location === 'project' ? path.join(process.cwd(), '.claude', 'agents', `${createState.agentType}.md`) : path.join(os.homedir(), '.claude', 'agents', `${createState.agentType}.md`) try { // First, save the agent file await saveAgent( createState.location!, createState.agentType, createState.whenToUse, createState.selectedTools, createState.systemPrompt, createState.selectedModel, createState.selectedColor || undefined ) // Then open it in editor const command = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open' await execAsync(`${command} "${filePath}"`) onAgentCreated(`Created agent: ${createState.agentType}`) } catch (error) { setCreateState({ type: 'SET_ERROR', value: (error as Error).message }) } } useInput((input, key) => { if (isCreating) return if ((key.return || input === 's') && !isCreating) { handleConfirm() } else if (input === 'e') { handleEditInEditor() } else if (key.escape) { setModeState({ mode: "create-color", location: createState.location! }) } }) return (
📋 Configuration Agent ID: {createState.agentType} Location: {createState.location === 'project' ? 'Project' : 'Personal'} Tools: {toolNames.length > 50 ? toolNames.slice(0, 50) + '...' : toolNames} Model: {getDisplayModelName(createState.selectedModel)} {createState.selectedColor && ( Color: {createState.selectedColor} )} 📝 Purpose {createState.whenToUse} {validation.warnings.length > 0 && ( Warnings: {validation.warnings.map((warning, idx) => ( • {warning} ))} )} {createState.error && ( ✗ {createState.error} )} {isCreating ? ( ) : null}
) } // Step 1: Location selection interface LocationSelectProps { createState: CreateState setCreateState: React.Dispatch setModeState: (state: ModeState) => void } function LocationSelect({ createState, setCreateState, setModeState }: LocationSelectProps) { const theme = getTheme() const [selectedIndex, setSelectedIndex] = useState(0) const options = [ { label: "📁 Project", value: "project", desc: ".claude/agents/" }, { label: "🏠 Personal", value: "user", desc: "~/.claude/agents/" } ] const handleChange = (value: string) => { setCreateState({ type: 'SET_LOCATION', value: value as AgentLocation }) setCreateState({ type: 'SET_METHOD', value: 'generate' }) // Always use generate method setModeState({ mode: "create-generate", location: value as AgentLocation }) } const handleCancel = () => { setModeState({ mode: "list-agents", location: "all" as AgentLocation }) } useInput((input, key) => { if (key.escape) { handleCancel() } else if (key.return) { handleChange(options[selectedIndex].value) } else if (key.upArrow) { setSelectedIndex(prev => prev > 0 ? prev - 1 : options.length - 1) } else if (key.downArrow) { setSelectedIndex(prev => prev < options.length - 1 ? prev + 1 : 0) } }) return (
{options.map((opt, idx) => ( {idx === selectedIndex ? '❯ ' : ' '}{opt.label} {opt.desc} ))}
) } // Step 2: Method selection interface MethodSelectProps { createState: CreateState setCreateState: React.Dispatch setModeState: (state: ModeState) => void } function MethodSelect({ createState, setCreateState, setModeState }: MethodSelectProps) { const [selectedIndex, setSelectedIndex] = useState(0) const options = [ { label: "Generate with Claude (recommended)", value: "generate" }, { label: "Manual configuration", value: "manual" } ] const handleChange = (value: string) => { setCreateState({ type: 'SET_METHOD', value: value as 'generate' | 'manual' }) if (value === "generate") { setCreateState({ type: 'SET_IS_AI_GENERATED', value: true }) setModeState({ mode: "create-generate", location: createState.location }) } else { setCreateState({ type: 'SET_IS_AI_GENERATED', value: false }) setModeState({ mode: "create-type", location: createState.location }) } } const handleCancel = () => { setModeState({ mode: "create-location" }) } useInput((input, key) => { if (key.escape) { handleCancel() } else if (key.return) { handleChange(options[selectedIndex].value) } else if (key.upArrow) { setSelectedIndex(prev => prev > 0 ? prev - 1 : options.length - 1) } else if (key.downArrow) { setSelectedIndex(prev => prev < options.length - 1 ? prev + 1 : 0) } }) return (
) } // Agent menu for agent operations interface AgentMenuProps { agent: AgentConfig setModeState: (state: ModeState) => void } function AgentMenu({ agent, setModeState }: AgentMenuProps) { const [selectedIndex, setSelectedIndex] = useState(0) const options = [ { label: "View details", value: "view" }, { label: "Edit agent", value: "edit", disabled: agent.location === 'built-in' }, { label: "Delete agent", value: "delete", disabled: agent.location === 'built-in' } ] const availableOptions = options.filter(opt => !opt.disabled) const handleSelect = (value: string) => { switch (value) { case "view": setModeState({ mode: "view-agent", selectedAgent: agent }) break case "edit": setModeState({ mode: "edit-agent", selectedAgent: agent }) break case "delete": setModeState({ mode: "delete-confirm", selectedAgent: agent }) break } } useInput((input, key) => { if (key.return) { handleSelect(availableOptions[selectedIndex].value) } else if (key.upArrow) { setSelectedIndex(prev => prev > 0 ? prev - 1 : availableOptions.length - 1) } else if (key.downArrow) { setSelectedIndex(prev => prev < availableOptions.length - 1 ? prev + 1 : 0) } }) return (
) } // Edit menu for agent editing options interface EditMenuProps { agent: AgentConfig setModeState: (state: ModeState) => void } function EditMenu({ agent, setModeState }: EditMenuProps) { const [selectedIndex, setSelectedIndex] = useState(0) const [isOpening, setIsOpening] = useState(false) const theme = getTheme() const options = [ { label: "Open in editor", value: "open-editor" }, { label: "Edit tools", value: "edit-tools" }, { label: "Edit model", value: "edit-model" }, { label: "Edit color", value: "edit-color" } ] const handleSelect = async (value: string) => { switch (value) { case "open-editor": setIsOpening(true) try { const filePath = getAgentFilePath(agent) await openInEditor(filePath) setModeState({ mode: "agent-menu", selectedAgent: agent }) } catch (error) { console.error('Failed to open editor:', error) // TODO: Show error to user } finally { setIsOpening(false) } break case "edit-tools": setModeState({ mode: "edit-tools", selectedAgent: agent }) break case "edit-model": setModeState({ mode: "edit-model", selectedAgent: agent }) break case "edit-color": setModeState({ mode: "edit-color", selectedAgent: agent }) break } } const handleBack = () => { setModeState({ mode: "agent-menu", selectedAgent: agent }) } useInput((input, key) => { if (key.escape) { handleBack() } else if (key.return && !isOpening) { handleSelect(options[selectedIndex].value) } else if (key.upArrow) { setSelectedIndex(prev => prev > 0 ? prev - 1 : options.length - 1) } else if (key.downArrow) { setSelectedIndex(prev => prev < options.length - 1 ? prev + 1 : 0) } }) if (isOpening) { return (
) } return (
) } // Edit tools step interface EditToolsStepProps { agent: AgentConfig tools: Tool[] setModeState: (state: ModeState) => void onAgentUpdated: (message: string, updated: AgentConfig) => void } function EditToolsStep({ agent, tools, setModeState, onAgentUpdated }: EditToolsStepProps) { const [selectedIndex, setSelectedIndex] = useState(0) // Initialize selected tools based on agent.tools const initialTools = Array.isArray(agent.tools) ? agent.tools : agent.tools === '*' ? tools.map(t => t.name) : [] const [selectedTools, setSelectedTools] = useState>(new Set(initialTools)) const [showAdvanced, setShowAdvanced] = useState(false) const [isUpdating, setIsUpdating] = useState(false) // Categorize tools const categorizedTools = useMemo(() => { const categories: Record = { read: [], edit: [], execution: [], web: [], other: [] } tools.forEach(tool => { let categorized = false // Check built-in categories for (const [category, toolNames] of Object.entries(TOOL_CATEGORIES)) { if (Array.isArray(toolNames) && toolNames.includes(tool.name)) { categories[category as keyof typeof categories]?.push(tool) categorized = true break } } if (!categorized) { categories.other.push(tool) } }) return categories }, [tools]) const allSelected = selectedTools.size === tools.length && tools.length > 0 const readSelected = categorizedTools.read.every(tool => selectedTools.has(tool.name)) && categorizedTools.read.length > 0 const editSelected = categorizedTools.edit.every(tool => selectedTools.has(tool.name)) && categorizedTools.edit.length > 0 const execSelected = categorizedTools.execution.every(tool => selectedTools.has(tool.name)) && categorizedTools.execution.length > 0 const options = [ { id: 'continue', label: 'Save', isContinue: true }, { id: 'separator1', label: '────────────────────────────────────', isSeparator: true }, { id: 'all', label: `${allSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} All tools`, isAll: true }, { id: 'read', label: `${readSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Read-only tools`, isCategory: true }, { id: 'edit', label: `${editSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Edit tools`, isCategory: true }, { id: 'execution', label: `${execSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Execution tools`, isCategory: true }, { id: 'separator2', label: '────────────────────────────────────', isSeparator: true }, { id: 'advanced', label: `[ ${showAdvanced ? 'Hide' : 'Show'} advanced options ]`, isAdvancedToggle: true }, ...(showAdvanced ? tools.map(tool => ({ id: tool.name, label: `${selectedTools.has(tool.name) ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} ${tool.name}`, isTool: true })) : []) ] const handleSave = async () => { setIsUpdating(true) try { // Type-safe tools conversion for updateAgent const toolsArray: string[] | '*' = allSelected ? '*' : Array.from(selectedTools) await updateAgent(agent, agent.whenToUse, toolsArray, agent.systemPrompt, agent.color, (agent as any).model) // Clear cache and reload fresh agent data from file system clearAgentCache() const freshAgents = await getActiveAgents() const updatedAgent = freshAgents.find(a => a.agentType === agent.agentType) if (updatedAgent) { onAgentUpdated(`Updated tools for agent: ${agent.agentType}`, updatedAgent) setModeState({ mode: "edit-agent", selectedAgent: updatedAgent }) } else { console.error('Failed to find updated agent after save') // Fallback to manual update const fallbackAgent: AgentConfig = { ...agent, tools: toolsArray.length === 1 && toolsArray[0] === '*' ? '*' : toolsArray, } onAgentUpdated(`Updated tools for agent: ${agent.agentType}`, fallbackAgent) setModeState({ mode: "edit-agent", selectedAgent: fallbackAgent }) } } catch (error) { console.error('Failed to update agent tools:', error) // TODO: Show error to user } finally { setIsUpdating(false) } } const handleSelect = () => { const option = options[selectedIndex] as any // Type assertion for union type if (!option) return if (option.isSeparator) return if (option.isContinue) { handleSave() } else if (option.isAdvancedToggle) { setShowAdvanced(!showAdvanced) } else if (option.isAll) { if (allSelected) { setSelectedTools(new Set()) } else { setSelectedTools(new Set(tools.map(t => t.name))) } } else if (option.isCategory) { const categoryName = option.id as keyof typeof categorizedTools const categoryTools = categorizedTools[categoryName] || [] const newSelected = new Set(selectedTools) const categorySelected = categoryTools.every(tool => selectedTools.has(tool.name)) if (categorySelected) { categoryTools.forEach(tool => newSelected.delete(tool.name)) } else { categoryTools.forEach(tool => newSelected.add(tool.name)) } setSelectedTools(newSelected) } else if (option.isTool) { const newSelected = new Set(selectedTools) if (newSelected.has(option.id)) { newSelected.delete(option.id) } else { newSelected.add(option.id) } setSelectedTools(newSelected) } } useInput((input, key) => { if (key.escape) { setModeState({ mode: "edit-agent", selectedAgent: agent }) } else if (key.return && !isUpdating) { handleSelect() } else if (key.upArrow) { setSelectedIndex(prev => { let newIndex = prev > 0 ? prev - 1 : options.length - 1 // Skip separators when going up while (options[newIndex] && (options[newIndex] as any).isSeparator) { newIndex = newIndex > 0 ? newIndex - 1 : options.length - 1 } return newIndex }) } else if (key.downArrow) { setSelectedIndex(prev => { let newIndex = prev < options.length - 1 ? prev + 1 : 0 // Skip separators when going down while (options[newIndex] && (options[newIndex] as any).isSeparator) { newIndex = newIndex < options.length - 1 ? newIndex + 1 : 0 } return newIndex }) } }) if (isUpdating) { return (
) } return (
{options.map((option, idx) => { const isSelected = idx === selectedIndex const isContinue = 'isContinue' in option && option.isContinue const isAdvancedToggle = (option as any).isAdvancedToggle const isSeparator = (option as any).isSeparator return ( {isSeparator ? option.label : `${isSelected ? `${UI_ICONS.pointer} ` : ' '}${isContinue || isAdvancedToggle ? option.label : option.label}` } {(option as any).isTool && isSelected && tools.find(t => t.name === option.id)?.description && ( {tools.find(t => t.name === option.id)?.description} )} ) })} {allSelected ? 'All tools selected' : `${selectedTools.size} of ${tools.length} tools selected`}
) } // Edit model step interface EditModelStepProps { agent: AgentConfig setModeState: (state: ModeState) => void onAgentUpdated: (message: string, updated: AgentConfig) => void } function EditModelStep({ agent, setModeState, onAgentUpdated }: EditModelStepProps) { const manager = getModelManager() const profiles = manager.getActiveModelProfiles() const currentModel = (agent as any).model || null // Build model options array const modelOptions = [ { id: null, name: 'Inherit from parent', description: 'Use the model from task configuration' }, ...profiles.map((p: any) => ({ id: p.modelName, name: p.name, description: `${p.provider || 'provider'} · ${p.modelName}` })) ] // Find the index of current model const defaultIndex = modelOptions.findIndex(m => m.id === currentModel) const [selectedIndex, setSelectedIndex] = useState(defaultIndex >= 0 ? defaultIndex : 0) const [isUpdating, setIsUpdating] = useState(false) const handleSave = async (modelId: string | null) => { setIsUpdating(true) try { const modelValue = modelId === null ? undefined : modelId await updateAgent(agent, agent.whenToUse, agent.tools, agent.systemPrompt, agent.color, modelValue) // Clear cache and reload fresh agent data from file system clearAgentCache() const freshAgents = await getActiveAgents() const updatedAgent = freshAgents.find(a => a.agentType === agent.agentType) if (updatedAgent) { onAgentUpdated(`Updated model for agent: ${agent.agentType}`, updatedAgent) setModeState({ mode: 'edit-agent', selectedAgent: updatedAgent }) } else { console.error('Failed to find updated agent after save') // Fallback to manual update const fallbackAgent: AgentConfig = { ...agent } if (modelValue) { (fallbackAgent as any).model = modelValue } else { delete (fallbackAgent as any).model } onAgentUpdated(`Updated model for agent: ${agent.agentType}`, fallbackAgent) setModeState({ mode: 'edit-agent', selectedAgent: fallbackAgent }) } } catch (error) { console.error('Failed to update agent model:', error) } finally { setIsUpdating(false) } } useInput((input, key) => { if (key.escape) { setModeState({ mode: 'edit-agent', selectedAgent: agent }) } else if (key.return && !isUpdating) { handleSave(modelOptions[selectedIndex].id) } else if (key.upArrow) { setSelectedIndex(prev => (prev > 0 ? prev - 1 : modelOptions.length - 1)) } else if (key.downArrow) { setSelectedIndex(prev => (prev < modelOptions.length - 1 ? prev + 1 : 0)) } }) if (isUpdating) { return (
) } return (
({ label: `${i + 1}. ${m.name}${m.description ? `\n${m.description}` : ''}`, value: m.id }))} selectedIndex={selectedIndex} onChange={(val) => handleSave(val)} numbered={false} />
) } // Edit color step interface EditColorStepProps { agent: AgentConfig setModeState: (state: ModeState) => void onAgentUpdated: (message: string, updated: AgentConfig) => void } function EditColorStep({ agent, setModeState, onAgentUpdated }: EditColorStepProps) { const currentColor = agent.color || null // Define color options (removed red/green due to display issues) const colors = [ { label: 'Automatic color', value: null }, { label: 'Yellow', value: 'yellow' }, { label: 'Blue', value: 'blue' }, { label: 'Magenta', value: 'magenta' }, { label: 'Cyan', value: 'cyan' }, { label: 'Gray', value: 'gray' }, { label: 'White', value: 'white' } ] // Find current color index const defaultIndex = colors.findIndex(color => color.value === currentColor) const [selectedIndex, setSelectedIndex] = useState(defaultIndex >= 0 ? defaultIndex : 0) const [isUpdating, setIsUpdating] = useState(false) const handleSave = async (color: string | null) => { setIsUpdating(true) try { const colorValue = color === null ? undefined : color await updateAgent(agent, agent.whenToUse, agent.tools, agent.systemPrompt, colorValue, (agent as any).model) // Clear cache and reload fresh agent data from file system clearAgentCache() const freshAgents = await getActiveAgents() const updatedAgent = freshAgents.find(a => a.agentType === agent.agentType) if (updatedAgent) { onAgentUpdated(`Updated color for agent: ${agent.agentType}`, updatedAgent) setModeState({ mode: "edit-agent", selectedAgent: updatedAgent }) } else { console.error('Failed to find updated agent after save') // Fallback to manual update const fallbackAgent: AgentConfig = { ...agent, ...(colorValue ? { color: colorValue } : { color: undefined }) } onAgentUpdated(`Updated color for agent: ${agent.agentType}`, fallbackAgent) setModeState({ mode: "edit-agent", selectedAgent: fallbackAgent }) } } catch (error) { console.error('Failed to update agent color:', error) // TODO: Show error to user } finally { setIsUpdating(false) } } useInput((input, key) => { if (key.escape) { setModeState({ mode: "edit-agent", selectedAgent: agent }) } else if (key.return && !isUpdating) { handleSave(colors[selectedIndex].value) } else if (key.upArrow) { setSelectedIndex(prev => prev > 0 ? prev - 1 : colors.length - 1) } else if (key.downArrow) { setSelectedIndex(prev => prev < colors.length - 1 ? prev + 1 : 0) } }) if (isUpdating) { return (
) } const selectedColor = colors[selectedIndex] const previewColor = selectedColor.value || undefined return (
{colors.map((color, index) => { const isSelected = index === selectedIndex const isCurrent = color.value === currentColor return ( {isSelected ? '❯ ' : ' '} {' '}{color.label} {isCurrent && ( )} ) })} Preview: {agent.agentType}
) } // View agent details interface ViewAgentProps { agent: AgentConfig tools: Tool[] setModeState: (state: ModeState) => void } function ViewAgent({ agent, tools, setModeState }: ViewAgentProps) { const theme = getTheme() const agentTools = Array.isArray(agent.tools) ? agent.tools : [] const hasAllTools = agent.tools === "*" || agentTools.includes("*") const locationPath = agent.location === 'user' ? `~/.claude/agents/${agent.agentType}.md` : agent.location === 'project' ? `.claude/agents/${agent.agentType}.md` : '(built-in)' const displayModel = getDisplayModelName((agent as any).model || null) const allowedTools = useMemo(() => { if (hasAllTools) return tools return tools.filter(tool => agentTools.some(allowedTool => { if (allowedTool.includes("*")) { const prefix = allowedTool.replace("*", "") return tool.name.startsWith(prefix) } return tool.name === allowedTool }) ) }, [tools, agentTools, hasAllTools]) return (
Type: {agent.agentType} Location: {agent.location} {locationPath !== '(built-in)' ? `· ${locationPath}` : ''} Description: {agent.whenToUse} Model: {displayModel} Color: {agent.color || 'auto'} Tools: {hasAllTools ? ( All tools ({tools.length} available) ) : ( {allowedTools.map(tool => ( • {tool.name} ))} )} System Prompt: {agent.systemPrompt}
) } // Edit agent component interface EditAgentProps { agent: AgentConfig tools: Tool[] setModeState: (state: ModeState) => void onAgentUpdated: (message: string) => void } function EditAgent({ agent, tools, setModeState, onAgentUpdated }: EditAgentProps) { const theme = getTheme() const [currentStep, setCurrentStep] = useState<'description' | 'tools' | 'prompt' | 'confirm'>('description') const [isUpdating, setIsUpdating] = useState(false) // 编辑状态 const [editedDescription, setEditedDescription] = useState(agent.whenToUse) const [editedTools, setEditedTools] = useState( Array.isArray(agent.tools) ? agent.tools : agent.tools === '*' ? ['*'] : [] ) const [editedPrompt, setEditedPrompt] = useState(agent.systemPrompt) const [error, setError] = useState(null) const handleSave = async () => { setIsUpdating(true) try { await updateAgent(agent, editedDescription, editedTools, editedPrompt, agent.color) clearAgentCache() onAgentUpdated(`Updated agent: ${agent.agentType}`) } catch (error) { setError((error as Error).message) setIsUpdating(false) } } const renderStepContent = () => { switch (currentStep) { case 'description': return ( Edit Description: setCurrentStep('tools')} error={error} rows={4} /> ) case 'tools': return ( Edit Tools: { if (action.type === 'SET_SELECTED_TOOLS') { setEditedTools(action.value) setCurrentStep('prompt') } }} setModeState={() => {}} tools={tools} /> ) case 'prompt': return ( Edit System Prompt: setCurrentStep('confirm')} error={error} rows={5} /> ) case 'confirm': const validation = validateAgentConfig({ agentType: agent.agentType, whenToUse: editedDescription, systemPrompt: editedPrompt, selectedTools: editedTools }) return ( Confirm Changes: Agent: {agent.agentType} Description: {editedDescription} Tools: {editedTools.includes('*') ? 'All tools' : editedTools.join(', ')} System Prompt: {editedPrompt.slice(0, 100)}{editedPrompt.length > 100 ? '...' : ''} {validation.warnings.length > 0 && ( {validation.warnings.map((warning, idx) => ( ⚠ {warning} ))} )} {error && ( ✗ {error} )} {isUpdating ? ( ) : ( Press Enter to save changes )} ) } } useInput((input, key) => { if (key.escape) { if (currentStep === 'description') { setModeState({ mode: "agent-menu", selectedAgent: agent }) } else { // 返回上一步 const steps: Array = ['description', 'tools', 'prompt', 'confirm'] const currentIndex = steps.indexOf(currentStep) if (currentIndex > 0) { setCurrentStep(steps[currentIndex - 1]) } } return } if (key.return && currentStep === 'confirm' && !isUpdating) { handleSave() } }) return (
{renderStepContent()}
) } // Delete confirmation interface DeleteConfirmProps { agent: AgentConfig setModeState: (state: ModeState) => void onAgentDeleted: (message: string) => void } function DeleteConfirm({ agent, setModeState, onAgentDeleted }: DeleteConfirmProps) { const [isDeleting, setIsDeleting] = useState(false) const [selected, setSelected] = useState(false) // false = No, true = Yes const handleConfirm = async () => { if (selected) { setIsDeleting(true) try { await deleteAgent(agent) clearAgentCache() onAgentDeleted(`Deleted agent: ${agent.agentType}`) } catch (error) { console.error('Failed to delete agent:', error) setIsDeleting(false) // TODO: Show error to user } } else { setModeState({ mode: "agent-menu", selectedAgent: agent }) } } useInput((input, key) => { if (key.return) { handleConfirm() } else if (key.leftArrow || key.rightArrow || key.tab) { setSelected(!selected) } }) if (isDeleting) { return (
) } return (
This action cannot be undone. The agent file will be permanently deleted. {!selected ? `${UI_ICONS.pointer} ` : ' '}No {selected ? `${UI_ICONS.pointer} ` : ' '}Yes, delete
) } export default { name: 'agents', description: 'Manage agent configurations', type: 'local-jsx' as const, isEnabled: true, isHidden: false, async call(onExit: (message?: string) => void) { return }, userFacingName() { return 'agents' } }