"use client"; // INFRA → Herdr sessions: browse the Herdr session snapshot from every // online node. One card per node with per-workspace agent-state pills // + a "Open in browser" button that spawns an xterm attached to that // node's Herdr TUI (same LivePane mechanism the MissionCanvas uses). import { useCallback, useEffect, useMemo, useState } from "react"; import { RefreshCw, Sparkles, TerminalSquare } from "lucide-react"; import type { FleetNode } from "./fleet/FleetPanels"; import { nodeHerdrConnector, useResilientTerminal, type TermMode, } from "@/components/computer/apps/terminal/core"; import "@xterm/xterm/css/xterm.css"; const mono = "'JetBrains Mono', ui-monospace, monospace"; type AgentStatus = "working" | "blocked" | "done" | "idle" | "unknown"; interface Workspace { workspace_id: string; label?: string; number?: number; agent_status?: AgentStatus; pane_count?: number; tab_count?: number; } interface Snapshot { workspaces?: Workspace[]; error?: string; } const STATUS_COLOR: Record = { working: "#5ec8d8", blocked: "#e8b465", done: "#5fd08a", idle: "#8a8a92", unknown: "#6a6a72", }; export function HerdrSessions() { const [nodes, setNodes] = useState([]); const [snapshots, setSnapshots] = useState>({}); const [openNode, setOpenNode] = useState(null); const [tick, setTick] = useState(0); useEffect(() => { let alive = true; (async () => { try { const r = await fetch("/api/nodes"); if (!r.ok) return; const data = (await r.json()) as { nodes?: FleetNode[] }; if (alive) setNodes((data.nodes ?? []).filter((n) => n.status === "online")); } catch { /* non-fatal */ } })(); return () => { alive = false; }; }, [tick]); useEffect(() => { let alive = true; (async () => { const results = await Promise.all( nodes.map(async (n) => { try { const r = await fetch(`/api/nodes/${n.id}/herdr/session`); if (!r.ok) return [n.id, "error" as const] as const; const raw = (await r.json()) as { error?: string; result?: { workspaces?: Workspace[] }; workspaces?: Workspace[]; }; if (raw.error) return [n.id, { error: raw.error }] as const; const workspaces = raw.result?.workspaces ?? raw.workspaces ?? []; return [n.id, { workspaces }] as const; } catch { return [n.id, "error" as const] as const; } }), ); if (!alive) return; const next: Record = {}; for (const [id, s] of results) next[id] = s; setSnapshots(next); })(); return () => { alive = false; }; }, [nodes, tick]); const refresh = useCallback(() => setTick((n) => n + 1), []); if (nodes.length === 0) { return (
No online nodes. Connect a host from the Local hardware panel first.
); } return (
Herdr sessions · {nodes.length} online node{nodes.length === 1 ? "" : "s"}

Every online fleet node is running a persistent Herdr daemon. This view shows their live workspaces + agent states. Click Open to render the node's Herdr TUI in your browser (WebRTC direct where possible).

{nodes.map((n) => { const snap = snapshots[n.id]; return (
{n.hostname ?? n.name}
{n.localIp ?? n.tailscaleIp ?? ""}
); })}
{openNode && (
{nodes.find((n) => n.id === openNode)?.hostname ?? nodes.find((n) => n.id === openNode)?.name ?? openNode}{" "} · Herdr TUI
)}
); } function SessionSummary({ snap }: { snap: Snapshot | "loading" | "error" | undefined }) { if (snap === undefined) return
Loading…
; if (snap === "loading") return
Loading…
; if (snap === "error") return
Snapshot failed
; if (typeof snap === "object" && snap.error) return
{snap.error}
; const workspaces = (snap as Snapshot).workspaces ?? []; if (workspaces.length === 0) return
No active workspaces on this node.
; return (
{workspaces.slice(0, 8).map((w) => { const status = (w.agent_status ?? "unknown") as AgentStatus; return (
{w.label ?? `w${w.number}`} {status} {w.pane_count ?? 0}p
); })} {workspaces.length > 8 && (
+{workspaces.length - 8} more workspaces
)}
); } function HerdrTerminal({ nodeId }: { nodeId: string }) { const [mode, setMode] = useState("connecting"); const connector = useMemo(() => nodeHerdrConnector(nodeId, setMode), [nodeId]); const { hostRef } = useResilientTerminal({ connect: connector, autoFocus: true }, [nodeId]); return (
{mode === "direct" ? "direct" : mode === "relayed" ? "relayed" : "connecting…"}
); } const hint: React.CSSProperties = { fontSize: 12, color: "#6a6a72", marginTop: 4, };