import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { useHive } from "../store"; import { closeAgent } from "../store/raw"; import { shortModel } from "../lib/format"; import { useFocusTrap } from "../hooks/useFocusTrap"; interface Part { type: "text" | "thinking" | "toolCall" | "toolResult"; text?: string; name?: string; args?: any; result?: string | null; resultError?: boolean; } interface Entry { kind: "message" | "meta"; role?: string; parts?: Part[]; text?: string; ts?: string; } interface Invoc { task: string; entries: Entry[] } interface RunRef { id: string; label: string; } function splitInvocations(entries: Entry[]): Invoc[] { const invs: Invoc[] = []; let cur: Invoc | null = null; let pending: Entry[] = []; const firstTextOf = (e: Entry) => e.parts?.find((p) => p.type === "text")?.text || ""; for (const e of entries) { if (e.kind === "message" && e.role === "user") { cur = { task: firstTextOf(e), entries: [...pending, e] }; pending = []; invs.push(cur); } else if (!cur) { pending.push(e); } else { cur.entries.push(e); } } if (!invs.length && pending.length) invs.push({ task: "", entries: pending }); return invs; } export default function AgentLog() { const openAgent = useHive((s) => s.openAgent); const [entries, setEntries] = useState([]); const [status, setStatus] = useState(""); const [loading, setLoading] = useState(true); const [exists, setExists] = useState(true); const [runs, setRuns] = useState([]); const [selectedRun, setSelectedRun] = useState("current"); const [bulk, setBulk] = useState<{ open: boolean; n: number }>({ open: false, n: 0 }); const invocations = useMemo(() => splitInvocations(entries), [entries]); const trapRef = useFocusTrap(!!openAgent); const offset = useRef(0); const timer = useRef | undefined>(undefined); const scroller = useRef(null); const selectedRunRef = useRef(selectedRun); selectedRunRef.current = selectedRun; const poll = useCallback(async (initial: boolean) => { const a = openAgent; if (!a) return; // Don't hit the network for a background tab's live tail; the visibility // handler re-polls once when the tab comes back to the foreground. if (!initial && typeof document !== "undefined" && document.visibilityState === "hidden") return; try { const run = selectedRunRef.current; const url = `/agent-log?session=${encodeURIComponent(a.sessionId)}&agent=${encodeURIComponent(a.name)}&offset=${offset.current}&run=${encodeURIComponent(run)}`; const res = await fetch(url); const data = await res.json(); setStatus(data.status || ""); setExists(!!data.exists); if (Array.isArray(data.runs)) setRuns(data.runs); if (data.offset != null) offset.current = data.offset; if (data.entries?.length) { const el = scroller.current; const atBottom = el ? el.scrollTop + el.clientHeight >= el.scrollHeight - 60 : true; setEntries((prev) => initial ? data.entries : [...prev, ...data.entries]); if (atBottom) queueMicrotask(() => scroller.current?.scrollTo({ top: scroller.current.scrollHeight })); } const shouldTail = data.running && selectedRunRef.current === "current"; if (shouldTail && !timer.current) timer.current = setInterval(() => poll(false), 1500); if (!shouldTail && timer.current) { clearInterval(timer.current); timer.current = undefined; } } catch { /* transient */ } setLoading(false); }, [openAgent]); function loadRun(runId: string) { if (timer.current) { clearInterval(timer.current); timer.current = undefined; } setSelectedRun(runId); selectedRunRef.current = runId; offset.current = 0; setEntries([]); setLoading(true); poll(true); } // (re)load when the target agent changes useEffect(() => { if (timer.current) { clearInterval(timer.current); timer.current = undefined; } offset.current = 0; setEntries([]); setLoading(true); setExists(true); setRuns([]); setSelectedRun("current"); selectedRunRef.current = "current"; if (openAgent) poll(true); return () => { if (timer.current) { clearInterval(timer.current); timer.current = undefined; } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [openAgent]); useEffect(() => { if (!openAgent) return; function onKey(e: KeyboardEvent) { if (e.key === "Escape") closeAgent(); } document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [openAgent]); // When the tab returns to the foreground, immediately catch up on any live // tail that was skipped while hidden. useEffect(() => { if (!openAgent) return; function onVisible() { if (document.visibilityState === "visible" && status === "running" && selectedRunRef.current === "current") poll(false); } document.addEventListener("visibilitychange", onVisible); return () => document.removeEventListener("visibilitychange", onVisible); }, [openAgent, status, poll]); if (!openAgent) return null; const a = openAgent; return createPortal(
e.stopPropagation()}>
{a.name} {status || a.status || "idle"} {a.model && {shortModel(a.model)}} {status === "running" && live}
{runs.length > 1 && (
Runs: {runs.map((r) => ( ))}
)}
{loading ?
Loading transcript…
: !exists ?
No transcript for this agent yet{status === "idle" ? " — it hasn't run." : "."}
: (<> {invocations.map((inv, i) => )} {!entries.length &&
Transcript is empty.
} )}
, document.body, ); } function Invocation(props: { inv: Invoc; index: number; total: number; bulk: { open: boolean; n: number } }) { const isLatest = props.index === props.total - 1; const [open, setOpen] = useState(props.total === 1 || isLatest); const taskPreview = props.inv.task ? props.inv.task.replace(/\s+/g, " ").slice(0, 90) : "(no task text)"; return (
setOpen(!open)}> {open ? "▾" : "▸"} {props.index === 0 ? "Invocation 1" : `↻ Invocation ${props.index + 1}`} {taskPreview} {props.inv.entries.length} msg{props.inv.entries.length === 1 ? "" : "s"}
{open && (
{props.inv.entries.map((e, i) => )}
)}
); } function LogEntry(props: { entry: Entry; bulk: { open: boolean; n: number } }) { const e = props.entry; if (e.kind === "meta") return
{e.text}
; return (
{e.role}
{e.parts?.map((p, i) => )}
); } function LogPart(props: { part: Part; bulk: { open: boolean; n: number } }) { const p = props.part; if (p.type === "text") return
{p.text}
; if (p.type === "thinking") return
💭 {p.text}
; if (p.type === "toolResult") { return ; } return ; } function ToolCard(props: { name: string; args?: any; result?: string; resultError?: boolean; bulk: { open: boolean; n: number } }) { const [open, setOpen] = useState(false); const lastBulk = useRef(props.bulk.n); useEffect(() => { if (props.bulk.n !== lastBulk.current) { lastBulk.current = props.bulk.n; setOpen(props.bulk.open); } }, [props.bulk]); const hasResult = props.result !== undefined && props.result !== null && props.result !== ""; return (
hasResult && setOpen(!open)}> {props.resultError ? "✗" : "⚙"} {props.name} {hasResult ? {open ? "hide result −" : "show result +"} : no result}
{props.args && Object.keys(props.args).length > 0 && (
{JSON.stringify(props.args, null, 2)}
)} {open && hasResult &&
{props.result}
}
); }