UNPKG

pyb-ts

Version:

PYB-CLI - Minimal AI Agent with multi-model support and CLI interface

1,165 lines (1,163 loc) 105 kB
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 { writeFileSync, unlinkSync, mkdirSync, existsSync, renameSync } from "fs"; import { join } from "path"; import * as path from "path"; import { homedir } from "os"; import * as os from "os"; import { getTheme } from "@utils/theme"; import { exec, spawn } from "child_process"; import { promisify } from "util"; import { getMCPTools } from "@services/mcpClient"; import { getModelManager } from "@utils/model"; import { randomUUID } from "crypto"; const execAsync = promisify(exec); const AGENT_LOCATIONS = { USER: "user", PROJECT: "project", BUILT_IN: "built-in", ALL: "all" }; const UI_ICONS = { pointer: "\u276F", checkboxOn: "\u2611", checkboxOff: "\u2610", warning: "\u26A0", separator: "\u2500", loading: "\u25D0\u25D1\u25D2\u25D3" }; const FOLDER_CONFIG = { FOLDER_NAME: ".claude", AGENTS_DIR: "agents" }; const TOOL_CATEGORIES = { read: ["Read", "Glob", "Grep", "LS", "Read Memory"], edit: ["Edit", "MultiEdit", "Write", "NotebookEdit", "Write Memory"], execution: ["Bash", "BashOutput", "KillBash"], web: ["WebFetch", "WebSearch"], other: ["TodoWrite", "ExitPlanMode", "Task"] }; function getDisplayModelName(modelId) { if (!modelId) return "Inherit"; try { const profiles = getModelManager().getActiveModelProfiles(); const profile = profiles.find((p) => 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"; } } async function generateAgentWithClaude(prompt) { 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 } } ]; const response = await queryModel("main", messages, [systemPrompt]); 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) => 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 = 1e5; const MAX_FIELD_LENGTH = 1e4; if (responseText.length > MAX_JSON_SIZE) { throw new Error("Response too large"); } let parsed; try { parsed = JSON.parse(responseText.trim()); } catch { 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) => str.replace(/[\x00-\x1F\x7F-\x9F]/g, ""); 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); 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.` }; } } function validateAgentType(agentType, existingAgents = []) { const errors = []; const warnings = []; 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"); } const reserved = ["help", "exit", "quit", "agents", "task"]; if (reserved.includes(agentType.toLowerCase())) { errors.push("This name is reserved"); } const duplicate = existingAgents.find((a) => a.agentType === agentType); if (duplicate) { errors.push(`An agent with this name already exists in ${duplicate.location}`); } if (agentType.includes("--")) { warnings.push("Consider avoiding consecutive hyphens"); } return { isValid: errors.length === 0, errors, warnings }; } function validateAgentConfig(config, existingAgents = []) { const errors = []; const warnings = []; if (config.agentType) { const typeValidation = validateAgentType(config.agentType, existingAgents); errors.push(...typeValidation.errors); warnings.push(...typeValidation.warnings); } 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)"); } 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"); } if (!config.selectedTools || config.selectedTools.length === 0) { warnings.push("No tools selected - agent will have limited capabilities"); } return { isValid: errors.length === 0, errors, warnings }; } function ensureMemoryTools(tools) { const memoryTools = ["Read Memory", "Write Memory"]; const result = [...tools]; for (const tool of memoryTools) { if (!result.includes(tool)) { result.push(tool); } } return result; } function getAgentDirectory(location) { 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(process.cwd(), FOLDER_CONFIG.FOLDER_NAME, FOLDER_CONFIG.AGENTS_DIR); } } function getAgentFilePath(agent) { if (agent.location === "built-in") { throw new Error("Cannot get file path for built-in agents"); } const dir = getAgentDirectory(agent.location); return join(dir, `${agent.agentType}.md`); } function ensureDirectoryExists(location) { const dir = getAgentDirectory(location); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } return dir; } function generateAgentFileContent(agentType, description, tools, systemPrompt, model, color) { const descriptionLines = description.split("\n"); const formattedDescription = descriptionLines.length > 1 ? `| ${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"); } async function saveAgent(location, agentType, description, tools, systemPrompt, model, color, throwIfExists = true) { 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)}`; const toolsForFile = Array.isArray(tools) && tools.length === 1 && tools[0] === "*" ? "*" : tools; const content = generateAgentFileContent(agentType, description, toolsForFile, systemPrompt, model, color); try { writeFileSync(tempFile, content, { encoding: "utf-8", flag: "wx" }); if (throwIfExists && existsSync(filePath)) { try { unlinkSync(tempFile); } catch { } throw new Error(`Agent file already exists: ${filePath}`); } renameSync(tempFile, filePath); } catch (error) { try { if (existsSync(tempFile)) { unlinkSync(tempFile); } } catch (cleanupError) { console.warn("Failed to cleanup temp file:", cleanupError); } throw error; } } async function deleteAgent(agent) { if (agent.location === "built-in") { throw new Error("Cannot delete built-in agents"); } const filePath = getAgentFilePath(agent); unlinkSync(filePath); } async function openInEditor(filePath) { const resolvedPath = path.resolve(filePath); const projectDir = process.cwd(); const homeDir = os.homedir(); const isSub = (base, target) => { const path2 = require("path"); const rel = path2.relative(path2.resolve(base), path2.resolve(target)); if (!rel || rel === "") return true; if (rel.startsWith("..")) return false; if (path2.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; let args; switch (platform) { case "darwin": command = "open"; args = [resolvedPath]; break; case "win32": command = "cmd"; args = ["/c", "start", "", resolvedPath]; break; default: command = "xdg-open"; args = [resolvedPath]; break; } 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}`)); } }); }); } async function updateAgent(agent, description, tools, systemPrompt, color, model) { 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" }); } function Header({ title, subtitle, step, totalSteps, children }) { const theme = getTheme(); return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: theme.primary }, title), subtitle && /* @__PURE__ */ React.createElement(Text, { color: theme.secondary }, step && totalSteps ? `Step ${step}/${totalSteps}: ` : "", subtitle), children); } function InstructionBar({ instructions = "Press \u2191\u2193 to navigate \xB7 Enter to select \xB7 Esc to go back" }) { const theme = getTheme(); return /* @__PURE__ */ React.createElement(Box, { marginTop: 2 }, /* @__PURE__ */ React.createElement(Box, { borderStyle: "round", borderColor: theme.secondary, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: theme.secondary }, instructions))); } function SelectList({ options, selectedIndex, onChange, onCancel, numbered = true }) { const theme = getTheme(); useInput((input, key) => { if (key.escape && onCancel) { onCancel(); } else if (key.return) { onChange(options[selectedIndex].value); } }); return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, options.map((option, idx) => /* @__PURE__ */ React.createElement(Box, { key: option.value }, /* @__PURE__ */ React.createElement(Text, { color: idx === selectedIndex ? theme.primary : void 0 }, idx === selectedIndex ? `${UI_ICONS.pointer} ` : " ", numbered ? `${idx + 1}. ` : "", option.label)))); } function MultilineTextInput({ value, onChange, placeholder = "", onSubmit, focus = true, rows = 5, error }) { const theme = getTheme(); const [internalValue, setInternalValue] = useState(value); const [cursorBlink, setCursorBlink] = useState(true); useEffect(() => { setInternalValue(value); }, [value]); useEffect(() => { if (!focus) return; const timer = setInterval(() => { setCursorBlink((prev) => !prev); }, 500); return () => clearInterval(timer); }, [focus]); const lines = internalValue.split("\n"); const lineCount = lines.length; const charCount = internalValue.length; const isEmpty = !internalValue.trim(); const hasContent = !isEmpty; const formatLines = (text) => { if (!text && placeholder) { return [placeholder]; } const maxWidth = 70; const result = []; const textLines = text.split("\n"); textLines.forEach((line) => { if (line.length <= maxWidth) { result.push(line); } else { 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)); const handleSubmit = () => { if (internalValue.trim() && onSubmit) { onSubmit(); } }; return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: "100%" }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement( Box, { borderStyle: "round", borderColor: focus ? theme.primary : "gray", paddingX: 2, paddingY: 1, minHeight: rows + 2 }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement( InkTextInput, { value: internalValue, onChange: (val) => { setInternalValue(val); onChange(val); }, onSubmit: handleSubmit, focus, placeholder } ), focus && cursorBlink && hasContent && /* @__PURE__ */ React.createElement(Text, { color: theme.primary }, "_")) ), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Box, null, hasContent ? /* @__PURE__ */ React.createElement(Text, { color: theme.success }, "\u2713 ", charCount, " chars \u2022 ", lineCount, " line", lineCount !== 1 ? "s" : "") : /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u25CB Type to begin...")), /* @__PURE__ */ React.createElement(Box, null, error ? /* @__PURE__ */ React.createElement(Text, { color: theme.error }, "\u26A0 ", error) : /* @__PURE__ */ React.createElement(Text, { dimColor: true }, hasContent ? "Ready" : "Waiting")))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press Enter to submit \xB7 Shift+Enter for new line"))); } function LoadingSpinner({ text }) { 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 /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: theme.primary }, UI_ICONS.loading[frame]), text && /* @__PURE__ */ React.createElement(Text, { color: theme.secondary }, " ", text)); } function AgentsUI({ onExit }) { const theme = getTheme(); const [modeState, setModeState] = useState({ mode: "list-agents", location: "all" }); const [agents, setAgents] = useState([]); const [changes, setChanges] = useState([]); const [refreshKey, setRefreshKey] = useState(0); const [loading, setLoading] = useState(true); const [tools, setTools] = useState([]); const [createState, setCreateState] = useReducer( (state, action) => { 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 } ); const loadAgents = useCallback(async (currentSelectedAgent) => { setLoading(true); clearAgentCache(); const abortController = new AbortController(); const loadingId = Date.now(); try { const result = await getActiveAgents(); if (abortController.signal.aborted) { return; } console.log("\u{1F50D} Loaded agents:", result.length, result.map((a) => a.agentType)); setAgents(result); if (currentSelectedAgent) { const freshSelectedAgent = result.find((a) => a.agentType === currentSelectedAgent.agentType); if (freshSelectedAgent) { setModeState((prev) => ({ ...prev, selectedAgent: freshSelectedAgent })); } } const availableTools = []; 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" }, { name: "Read Memory", description: "Read from agent memory" }, { name: "Write Memory", description: "Write to agent memory" } ]; coreTools = coreTools.filter((t) => t.name !== "Task" && t.name !== "ExitPlanMode"); availableTools.push(...coreTools); 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); } } return () => abortController.abort(); }, []); useEffect(() => { let cleanup; const load = async () => { const shouldUpdateSelected = refreshKey > 0; cleanup = await loadAgents(shouldUpdateSelected ? modeState.selectedAgent : void 0); }; load(); return () => { if (cleanup) { cleanup(); } }; }, [refreshKey, loadAgents]); useInput((input, key) => { if (!key.escape) return; const changesSummary = changes.length > 0 ? `Agent changes: ${changes.join("\n")}` : void 0; const current = modeState.mode; if (current === "list-agents") { onExit(changesSummary); return; } switch (current) { case "create-location": setModeState({ mode: "list-agents", location: "all" }); 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" }); 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" }); break; } }); const handleAgentSelect = useCallback((agent) => { 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) => { setChanges((prev) => [...prev, message]); setRefreshKey((prev) => prev + 1); setModeState({ mode: "list-agents", location: "all" }); }, []); const handleAgentDeleted = useCallback((message) => { setChanges((prev) => [...prev, message]); setRefreshKey((prev) => prev + 1); setModeState({ mode: "list-agents", location: "all" }); }, []); if (loading) { return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Header, { title: "Agents" }, /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(LoadingSpinner, { text: "Loading agents..." }))), /* @__PURE__ */ React.createElement(InstructionBar, null)); } switch (modeState.mode) { case "list-agents": if (loading) { return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Header, { title: "\u{1F916} Agents" }, /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(LoadingSpinner, { text: "Loading agents..." }))), /* @__PURE__ */ React.createElement(InstructionBar, { instructions: "Loading agents..." })); } return /* @__PURE__ */ React.createElement( AgentListView, { location: modeState.location || "all", agents, allAgents: agents, onBack: () => onExit(), onSelect: handleAgentSelect, onCreateNew: handleCreateNew, changes } ); case "create-location": return /* @__PURE__ */ React.createElement( LocationSelect, { createState, setCreateState, setModeState } ); case "create-method": return /* @__PURE__ */ React.createElement( MethodSelect, { createState, setCreateState, setModeState } ); case "create-generate": return /* @__PURE__ */ React.createElement( GenerateStep, { createState, setCreateState, setModeState, existingAgents: agents } ); case "create-type": return /* @__PURE__ */ React.createElement( TypeStep, { createState, setCreateState, setModeState, existingAgents: agents } ); case "create-description": return /* @__PURE__ */ React.createElement( DescriptionStep, { createState, setCreateState, setModeState } ); case "create-tools": return /* @__PURE__ */ React.createElement( ToolsStep, { createState, setCreateState, setModeState, tools } ); case "create-model": return /* @__PURE__ */ React.createElement( ModelStep, { createState, setCreateState, setModeState } ); case "create-color": return /* @__PURE__ */ React.createElement( ColorStep, { createState, setCreateState, setModeState } ); case "create-prompt": return /* @__PURE__ */ React.createElement( PromptStep, { createState, setCreateState, setModeState } ); case "create-confirm": return /* @__PURE__ */ React.createElement( ConfirmStep, { createState, setCreateState, setModeState, tools, onAgentCreated: handleAgentCreated } ); case "agent-menu": return /* @__PURE__ */ React.createElement( AgentMenu, { agent: modeState.selectedAgent, setModeState } ); case "view-agent": return /* @__PURE__ */ React.createElement( ViewAgent, { agent: modeState.selectedAgent, tools, setModeState } ); case "edit-agent": return /* @__PURE__ */ React.createElement( EditMenu, { agent: modeState.selectedAgent, setModeState } ); case "edit-tools": return /* @__PURE__ */ React.createElement( EditToolsStep, { agent: modeState.selectedAgent, tools, setModeState, onAgentUpdated: (message, updated) => { setChanges((prev) => [...prev, message]); setRefreshKey((prev) => prev + 1); setModeState({ mode: "agent-menu", selectedAgent: updated }); } } ); case "edit-model": return /* @__PURE__ */ React.createElement( EditModelStep, { agent: modeState.selectedAgent, setModeState, onAgentUpdated: (message, updated) => { setChanges((prev) => [...prev, message]); setRefreshKey((prev) => prev + 1); setModeState({ mode: "agent-menu", selectedAgent: updated }); } } ); case "edit-color": return /* @__PURE__ */ React.createElement( EditColorStep, { agent: modeState.selectedAgent, setModeState, onAgentUpdated: (message, updated) => { setChanges((prev) => [...prev, message]); setRefreshKey((prev) => prev + 1); setModeState({ mode: "agent-menu", selectedAgent: updated }); } } ); case "delete-confirm": return /* @__PURE__ */ React.createElement( DeleteConfirm, { agent: modeState.selectedAgent, setModeState, onAgentDeleted: handleAgentDeleted } ); default: return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Header, { title: "Agents" }, /* @__PURE__ */ React.createElement(Text, null, "Mode: ", modeState.mode, " (Not implemented yet)"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Press Esc to go back"))), /* @__PURE__ */ React.createElement(InstructionBar, { instructions: "Esc to go back" })); } } function AgentListView({ location, agents, allAgents, onBack, onSelect, onCreateNew, changes }) { 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" }, { label: "Personal", value: "user" }, { label: "Project", value: "project" } ]; const activeMap = useMemo(() => { const map = /* @__PURE__ */ new Map(); agents.forEach((a) => map.set(a.agentType, a)); return map; }, [agents]); const checkOverride = (agent) => { const active = activeMap.get(agent.agentType); const isOverridden = !!(active && active.location !== agent.location); return { isOverridden, overriddenBy: isOverridden ? active.location : null }; }; const renderCreateOption = () => /* @__PURE__ */ React.createElement(Box, { flexDirection: "row", gap: 1 }, /* @__PURE__ */ React.createElement(Text, { color: onCreateOption ? theme.primary : void 0 }, onCreateOption ? `${UI_ICONS.pointer} ` : " "), /* @__PURE__ */ React.createElement(Text, { bold: true, color: onCreateOption ? theme.primary : void 0 }, "\u2728 Create new agent")); const renderAgent = (agent, 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 : void 0; const agentModel = agent.model || null; const modelDisplay = getDisplayModelName(agentModel); return /* @__PURE__ */ React.createElement(Box, { key: `${agent.agentType}-${agent.location}`, flexDirection: "row", alignItems: "center" }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "row", alignItems: "center", minWidth: 3 }, /* @__PURE__ */ React.createElement(Text, { dimColor: dimmed && !isSelected, color }, isBuiltIn ? "" : isSelected ? `${UI_ICONS.pointer} ` : " ")), /* @__PURE__ */ React.createElement(Box, { flexDirection: "row", alignItems: "center", flexGrow: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: dimmed && !isSelected, color }, agent.agentType), /* @__PURE__ */ React.createElement(Text, { dimColor: true, color: dimmed ? void 0 : "gray" }, " \xB7 ", modelDisplay)), overriddenBy && /* @__PURE__ */ React.createElement(Box, { marginLeft: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: !isSelected, color: isSelected ? "yellow" : "gray" }, 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]); 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; } 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 }); } displayAgents.forEach((agent) => { const { isOverridden } = checkOverride(agent); if (!isOverridden) { allNavigableItems.push({ type: "agent", agent }); } }); if (allNavigableItems.length === 0) return; if (key.upArrow) { if (onCreateOption) { 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) { 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 /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(EmptyStateInput, null), /* @__PURE__ */ React.createElement(Header, { title: "\u{1F916} Agents", subtitle: "" }, onCreateNew && /* @__PURE__ */ React.createElement(Box, { marginY: 1 }, renderCreateOption()), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: theme.primary }, "\u{1F4AD} What are agents?")), /* @__PURE__ */ React.createElement(Text, null, "Specialized AI assistants that Kode can delegate to for specific tasks, compatible with Claude Code `.claude` agent packs."), /* @__PURE__ */ React.createElement(Text, null, "Each agent has its own context, prompt, and tools."), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: theme.primary }, "\u{1F4A1} Popular agent ideas:")), /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, null, "\u2022 \u{1F50D} Code Reviewer - Reviews PRs for best practices"), /* @__PURE__ */ React.createElement(Text, null, "\u2022 \u{1F512} Security Auditor - Finds vulnerabilities"), /* @__PURE__ */ React.createElement(Text, null, "\u2022 \u26A1 Performance Optimizer - Improves code speed"), /* @__PURE__ */ React.createElement(Text, null, "\u2022 \u{1F9D1}\u200D\u{1F4BC} Tech Lead - Makes architecture decisions"), /* @__PURE__ */ React.createElement(Text, null, "\u2022 \u{1F3A8} UX Expert - Improves user experience"))), currentLocation !== "built-in" && builtInAgents.length > 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, UI_ICONS.separator.repeat(40))), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: theme.secondary }, "Built-in (always available):"), builtInAgents.map((a) => renderAgent(a, true))))), /* @__PURE__ */ React.createElement(InstructionBar, { instructions: "Press Enter to create new agent \xB7 Esc to go back" })); } return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Header, { title: "\u{1F916} Agents", subtitle: "" }, changes.length > 0 && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, changes[changes.length - 1])), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "row", gap: 2 }, locationTabs.map((tab, idx) => { const isActive = currentLocation === tab.value; const isSelected = inLocationTabs && idx === selectedLocationTab; return /* @__PURE__ */ React.createElement(Box, { key: tab.value, flexDirection: "row" }, /* @__PURE__ */ React.createElement( Text, { color: isSelected || isActive ? theme.primary : void 0, bold: isActive, dimColor: !isActive && !isSelected }, isSelected ? "\u25B6 " : isActive ? "\u25C9 " : "\u25CB ", tab.label ), idx < locationTabs.length - 1 && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " | ")); })), /* @__PURE__ */ React.createElement(Box, { marginTop: 0 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, currentLocation === "all" ? "Showing all agents" : currentLocation === "user" ? "Personal agents (~/.claude/agents)" : "Project agents (.claude/agents)"))), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, onCreateNew && /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, renderCreateOption()), currentLocation === "all" ? /* @__PURE__ */ React.createElement(React.Fragment, null, customAgents.filter((a) => a.location === "user").length > 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { bold: true, color: theme.secondary }, "Personal:"), customAgents.filter((a) => a.location === "user").map((a) => renderAgent(a))), customAgents.filter((a) => a.location === "project").length > 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Box, { marginTop: customAgents.filter((a) => a.location === "user").length > 0 ? 1 : 0 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: theme.secondary }, "Project:")), customAgents.filter((a) => a.location === "project").map((a) => renderAgent(a))), builtInAgents.length > 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Box, { marginTop: customAgents.length > 0 ? 1 : 0 }, /* @__PURE__ */ React.createElement(Text, null, UI_ICONS.separator.repeat(40))), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: theme.secondary }, "Built-in:"), builtInAgents.map((a) => renderAgent(a, true))))) : /* @__PURE__ */ React.createElement(React.Fragment, null, displayAgents.map((a) => renderAgent(a)), currentLocation !== "built-in" && builtInAgents.length > 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, UI_ICONS.separator.repeat(40))), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: theme.secondary }, "Built-in:"), builtInAgents.map((a) => renderAgent(a, true))))))), /* @__PURE__ */ React.createElement( InstructionBar, { instructions: inLocationTabs ? "\u2190\u2192 Switch tabs \u2022 Enter Select \u2022 Tab Exit tabs" : "\u2191\u2193 Navigate \u2022 Tab Location \u2022 Enter Select" } )); } function GenerateStep({ createState, setCreateState, setModeState, existingAgents }) { 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); const validation = validateAgentType(generated.identifier, existingAgents); let finalIdentifier = generated.identifier; if (!validation.isValid) { 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 /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Header, { title: "\u2728 New Agent", subtitle: "What should it do?", step: 2, totalSteps: 8 }, /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, createState.isGenerating ? /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, createState.generationPrompt), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(LoadingSpinner, { text: "Generating agent configuration..." }))) : /* @__PURE__ */ React.createElement( MultilineTextInput, { value: createState.generationPrompt, onChange: (value) => 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 } ))), /* @__PURE__ */ React.createElement(InstructionBar, null)); } function TypeStep({ createState, setCreateState, setModeState, existingAgents }) { const handleSubmit = () => { const validation = validateAgentType(createState.agentType, existingAgents); if (validation.isValid) { setModeState({ mode: "create-prompt", location: createState.location }); } else