import { useEffect, useState, useMemo } from "react"; import { useTranslation } from "@/lib/i18n"; import { useConfigStore } from "@/store/config-store"; import { Badge } from "@/components/ui/Badge"; import { EmptyState } from "@/components/ui/EmptyState"; import { formatTokens } from "@/lib/utils"; import type { AgentDef, ChainDef, ChainStep, RunRecord, SubagentsData } from "@/types"; import { Brain, GitBranch, History, Box, Users, FileCode, CheckCircle2, XCircle, Loader2, Search, ExternalLink, Pencil, Check, X, } from "lucide-react"; const API_BASE = "/api/pi"; type Tab = "agents" | "chains" | "history"; export function SubagentsPage() { const { t } = useTranslation(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [tab, setTab] = useState("agents"); const [search, setSearch] = useState(""); const loadData = async () => { setLoading(true); setError(null); try { const res = await fetch(`${API_BASE}/subagents`); if (!res.ok) throw new Error(`API /subagents: ${res.status}`); const json = await res.json(); setData(json); } catch (e: any) { setError(e.message || "Failed to load subagents data"); } finally { setLoading(false); } }; useEffect(() => { loadData(); }, []); const q = search.trim().toLowerCase(); const filteredAgents = data?.agents.filter( (a) => !q || a.name.toLowerCase().includes(q) || a.description.toLowerCase().includes(q) ); const filteredChains = data?.chains.filter( (c) => !q || c.name.toLowerCase().includes(q) || c.description.toLowerCase().includes(q) ); const filteredHistory = data?.runHistory.filter( (r) => !q || r.agent.toLowerCase().includes(q) || r.status.toLowerCase().includes(q) ); if (loading && !data) { return (
); } if (error && !data) { return (

{error}

); } return (
{/* Header */}

{t("nav.subagents")}

{data && t("subagents.summary", String(data.agents.length), String(data.chains.length))}

{/* Tabs */}
{(["agents", "chains", "history"] as Tab[]).map((tKey) => ( ))} {data && (data.agents.length > 5 || data.chains.length > 5) && (
setSearch(e.target.value)} placeholder={t("models.search_placeholder")} className="w-48 rounded-lg border border-gray-700 bg-gray-800 py-2 pl-9 pr-3 text-sm text-white" />
)}
{/* Tab Content */}
{tab === "agents" && ( )} {tab === "chains" && ( )} {tab === "history" && ( )}
); } // ─── Agent List ──────────────────────────────────────────── function AgentList({ agents, onRefresh, searchActive, }: { agents: AgentDef[]; onRefresh: () => void; searchActive: boolean; }) { const { t } = useTranslation(); const [selectedFile, setSelectedFile] = useState(null); // Resolve the selected agent from the live list so it reflects saved edits // after a refresh (matching by fileName), instead of a stale captured object. const selected = agents.find((a) => a.fileName === selectedFile) ?? null; if (agents.length === 0) { return ( } title={t("subagents.no_agents")} description={t("subagents.no_agents_desc")} /> ); } return (
{/* Left: Agent cards */}
{agents.map((agent) => ( ))}
{/* Right: Agent detail */}
{selected ? ( ) : (
{t("providers_models.select_hint")}
)}
); } function AgentDetail({ agent, onSaved }: { agent: AgentDef; onSaved: () => void }) { const { t } = useTranslation(); const { allModels, allProviders } = useConfigStore(); // Eligible = custom providers + built-in providers with an API key saved. const eligibleModels = useMemo(() => { const usable = new Set( allProviders.filter((p) => p.type === "custom" || p.hasAuth).map((p) => p.id) ); return allModels.filter((m) => usable.has(m.providerId)); }, [allModels, allProviders]); const [editing, setEditing] = useState(false); const [model, setModel] = useState(agent.model ?? ""); const [thinking, setThinking] = useState(agent.thinking ?? ""); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null); // Reset the draft whenever a different agent is selected. useEffect(() => { setModel(agent.model ?? ""); setThinking(agent.thinking ?? ""); setEditing(false); setMsg(null); }, [agent.fileName]); const handleSave = async () => { setSaving(true); setMsg(null); try { const res = await fetch(`${API_BASE}/subagents/update-agent`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fileName: agent.fileName, model: model.trim(), thinking: thinking.trim() }), }); const { success } = (await res.json()) as { success: boolean }; if (success) { setMsg({ ok: true, text: t("subagents.saved") }); setEditing(false); onSaved(); } else { setMsg({ ok: false, text: t("subagents.save_failed") }); } } catch { setMsg({ ok: false, text: t("subagents.save_failed") }); } finally { setSaving(false); } }; return (

{agent.name}

{agent.package} {agent.package === "custom" && !editing && ( )}

{agent.description}

{/* Model — editable for custom agents */}
{t("subagents.model")} {editing ? ( ) : (

{agent.model || t("subagents.model_default")}

)}
{/* Thinking — editable for custom agents */}
{t("subagents.thinking")} {editing ? ( ) : (

{agent.thinking || t("subagents.thinking_default")}

)}
{editing && (
{msg && ( {msg.text} )}
)} {!editing && msg && (
{msg.text}
)} {agent.tools && (
{t("subagents.tools")}
{agent.tools.map((tool) => ( {tool} ))}
)}
{t("subagents.system_prompt_mode")}

{agent.systemPromptMode || "replace"}

{t("subagents.input")}

{(agent.input ?? ["text"]).join(", ")}

{/* Body (system prompt preview) */}
{t("subagents.system_prompt")}
          {agent.body || t("subagents.empty_prompt")}
        
); } // ─── Chain List ──────────────────────────────────────────── function ChainList({ chains, searchActive, }: { chains: ChainDef[]; searchActive: boolean; }) { const { t } = useTranslation(); const [selected, setSelected] = useState(null); if (chains.length === 0) { return ( } title={t("subagents.no_chains")} description={t("subagents.no_chains_desc")} /> ); } return (
{chains.map((chain) => ( ))}
{selected ? ( ) : (
{t("providers_models.select_hint")}
)}
); } function StepIcon({ agent }: { agent: string }) { const isParallel = agent.includes("|"); if (isParallel) return ; return ; } function ChainDetail({ chain }: { chain: ChainDef }) { const { t } = useTranslation(); return (

{chain.name}

{chain.description}

{t("subagents.pipeline")}
{/* Vertical line connector */}
{chain.steps.map((step, i) => (

{step.agent.split("|").map((a, j) => ( {j > 0 && |} {a.trim()} ))}

{step.phase && {t("subagents.phase")}: {step.phase}} {step.label && {t("subagents.label")}: {step.label}} {step.output && {t("subagents.output")}: {step.output}}
#{i + 1}
))}
); } // ─── Run History ─────────────────────────────────────────── function RunHistoryList({ records, searchActive, }: { records: RunRecord[]; searchActive: boolean; }) { const { t } = useTranslation(); if (records.length === 0) { return ( } title={t("subagents.no_history")} description={t("subagents.no_history_desc")} /> ); } return (
{records.map((r, i) => ( ))}
{t("subagents.agent")} {t("subagents.time")} {t("subagents.status")} {t("subagents.duration")}
{r.agent} {formatTimestamp(r.ts)} {r.status === "ok" ? ( {t("subagents.status_ok")} ) : ( {t("subagents.status_error")} {r.exit != null && (exit {r.exit})} )} {r.duration != null ? formatDuration(r.duration) : "—"}
{records.length >= 100 && (

{t("subagents.showing_recent")}

)}
); } // ─── Helpers ─────────────────────────────────────────────── function formatTimestamp(ts: number): string { const d = new Date(ts * 1000); const now = new Date(); const diffMs = now.getTime() - d.getTime(); const diffMin = Math.floor(diffMs / 60000); if (diffMin < 1) return "just now"; if (diffMin < 60) return `${diffMin}m ago`; const diffHr = Math.floor(diffMin / 60); if (diffHr < 24) return `${diffHr}h ago`; return d.toLocaleDateString(); } function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; const sec = Math.floor(ms / 1000); if (sec < 60) return `${sec}s`; const min = Math.floor(sec / 60); const rem = sec % 60; return `${min}m ${rem}s`; }