import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation, type TabProps } from "@termix/plugin-sdk/frontend"; import { Button, Input } from "@termix/plugin-sdk/ui"; import { Bot, Plus, Square, Play } from "lucide-react"; import { useAgentStream } from "./useAgentStream"; import { SessionTools } from "./SessionTools"; import { AgentComposer } from "./AgentComposer"; import { InstallRuntime } from "./InstallRuntime"; import { aiApp } from "../app-ref"; import { AiMessage } from "../AiMessage"; import { getAiProviders, type AiProvider } from "../ai-api"; import { AGENTS, compatible, type AgentEvent, type AgentKind, type AgentSession, } from "../../backend/agents/types"; const names: Record = { pi: "Pi", opencode: "OpenCode", claude: "Claude Code", codex: "Codex", }; function message(error: unknown): string { const e = error as { response?: { data?: { error?: string } }; message?: string; }; return e.response?.data?.error || e.message || "Agent request failed"; } export function AgentPanel({ host, sshHost }: TabProps) { const { t } = useTranslation(); const target = host ?? sshHost; const hostId = Number(target?.id); const [providers, setProviders] = useState([]), [sessions, setSessions] = useState([]); const [agent, setAgent] = useState("pi"), [providerId, setProviderId] = useState(0), [model, setModel] = useState(""); const [cwd, setCwd] = useState("/tmp"), [executable, setExecutable] = useState(""); const [active, setActive] = useState(null), [events, setEvents] = useState([]); const [error, setError] = useState(""), [busy, setBusy] = useState(false); const [answers, setAnswers] = useState>({}), [answered, setAnswered] = useState>(new Set()); const [generation, setGeneration] = useState(0); const bottom = useRef(null); const composing = useRef(false); const [search, setSearch] = useState(""); const [showArchived, setShowArchived] = useState(false); const refresh = useCallback(async () => { const [p, s] = await Promise.all([ getAiProviders(), aiApp().api.get<{ sessions: AgentSession[] }>("/agents"), ]); setProviders(p.filter((x) => x.enabled)); setSessions(s.data.sessions.filter((x) => x.hostId === hostId)); }, [hostId]); useEffect(() => { void refresh().catch((e) => setError(message(e))); }, [refresh]); useEffect(() => { bottom.current?.scrollIntoView({ block: "end" }); }, [events]); const activeId = active?.id; const connection = useAgentStream( activeId, generation, (s) => { setActive((old) => (old?.id === s.id ? s : old)); setEvents((old) => [ ...s.events, ...old.filter((e) => e.seq > (s.events.at(-1)?.seq ?? 0)), ].slice(-2000), ); }, (e) => { setEvents((old) => old.some((x) => x.seq === e.seq) ? old : [...old, e].slice(-2000), ); if (e.kind === "status") setActive((old) => old ? { ...old, status: e.text as AgentSession["status"] } : old, ); if (e.kind === "error") setError(e.text); }, ); async function action(fn: () => Promise) { setBusy(true); setError(""); try { await fn(); } catch (e) { setError(message(e)); } finally { setBusy(false); } } async function select(id: string) { await action(async () => { const r = await aiApp().api.get(`/agents/${id}`); setActive(r.data); setEvents(r.data.events); setAnswered(new Set()); }); } async function input(body: unknown) { if (active) await aiApp().api.post(`/agents/${active.id}/input`, body); } const transcript: AgentEvent[] = []; for (const e of events) { const last = transcript.at(-1); if (e.kind === "text" && last?.kind === "text") last.text += e.text; else transcript.push({ ...e }); } return (
{ if ( e.key !== "Escape" || e.defaultPrevented || e.repeat || e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229 || composing.current || busy || active?.status !== "running" ) return; e.preventDefault(); e.stopPropagation(); void action(() => input({ type: "cancel" })); }} >
{t("agents.title")} {String(target?.name ?? target?.ip ?? "")}
{error && (
{error}
)}
{!active ? (
{ e.preventDefault(); void action(async () => { const r = await aiApp().api.post<{ id: string }>("/agents", { hostId, agent, providerId, model, cwd, executable: executable || undefined, }); const s = await aiApp().api.get( `/agents/${r.data.id}`, ); setActive(s.data); setEvents(s.data.events); await refresh(); }); }} >

{t("agents.description")}

{t("agents.requirements")}

{agent === "codex" && (

{t("agents.responsesRequired")}

)} {agent === "pi" && (

{t("agents.piPermissions")}

)} ) : (
{connection !== "connected" && (

{t(`agents.${connection}`)}

)} { setCwd(path); setAgent(active.agent); setProviderId(active.providerId); setModel(active.model); setExecutable(active.executable); setActive(null); setEvents([]); }} key={`tools-${active.id}`} session={active} busy={busy} action={action} onUpdated={async (s) => { setActive(s); await refresh(); }} />
{names[active.agent]} · {active.model} · {active.cwd} ·{" "} {t(`agents.status.${active.status}`)}
{transcript .filter((e) => e.kind !== "state") .map((e) => (
{e.kind === "text" || e.kind === "user" ? ( ) : e.kind === "permission" ? (
{t("agents.approval")}
                          {e.text}
                        
{e.choices?.length ? ( ) : ( setAnswers((a) => ({ ...a, [e.requestId!]: v.target.value, })) } /> )} {[true, false].map((allow) => ( ))}
) : e.kind === "tool" ? (
{t("agents.tool")}
                          {e.text}
                        
) : (

{e.kind === "status" ? t(`agents.status.${e.text}`) : e.text}

)}
))}
{ composing.current = value; }} onUpdated={setActive} />
)}
); }