pyb-ts
Version:
PYB-CLI - Minimal AI Agent with multi-model support and CLI interface
1,165 lines (1,163 loc) • 105 kB
JavaScript
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