/** * Agents right-panel surface: the fleet view over the native subagent fold, * and the ONLY place the roster renders (the chat carries one CTA row per * spawn batch). * * Visualization rules (from live-test feedback): * - Spawn order is stable. Activity and completion update rows in place. * - Agent rows reserve three fixed lines for identity, activity, and metrics; * changing data must never change their height. * - Workflow expansion is presentation state. A live run stays expanded when * it settles; older collapsed runs can still be opened at run granularity. * - Static status dots, DOM-write elapsed timers, plain token counters. */ import { useAtomValue } from "@effect/atom-react"; import type { AgentPanelModel, AgentPanelWorkflowGroup, RuntimeSubagent, } from "@t3tools/client-runtime/state/subagentRuntime"; import { formatSubagentModelLabel, formatSubagentTokenCount, } from "@t3tools/client-runtime/state/subagentRuntime"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { Bot, Braces, Check, ChevronDown, ChevronRight, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { cn } from "~/lib/utils"; import { orchestrationEnvironment } from "~/state/orchestration"; import { ScrollArea } from "~/components/ui/scroll-area"; /** * In-flight states all present as Working (one steady state, per the * monitoring-pill design: detail belongs in the activity sub-line, and a * stalled/waiting/queued subagent is still the fleet doing its job, not a * user problem). Only settled states differentiate. */ const STATUS_VISUALS: Record = { pending: { dotClass: "bg-info", label: "Working" }, running: { dotClass: "bg-info", label: "Working" }, waiting: { dotClass: "bg-info", label: "Working" }, // Idle reads as settled (muted, not sky): a resting Codex child looks done // unless resumed — live-test: sky idle dots read as stuck in-progress. idle: { dotClass: "bg-muted-foreground/50", label: "Idle · resumable" }, completed: { dotClass: "bg-success", label: "Completed" }, failed: { dotClass: "bg-destructive", label: "Failed" }, cancelled: { dotClass: "bg-muted-foreground/60", label: "Stopped" }, interrupted: { dotClass: "bg-muted-foreground/60", label: "Stopped" }, }; function StatusDot({ status }: { status: RuntimeSubagent["status"] }) { return ( ); } function formatElapsedSeconds(totalSeconds: number): string { const seconds = Math.max(0, Math.floor(totalSeconds)); const minutes = Math.floor(seconds / 60); if (minutes === 0) { return `${seconds}s`; } const hours = Math.floor(minutes / 60); if (hours === 0) { return `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`; } return `${hours}h ${String(minutes % 60).padStart(2, "0")}m`; } function elapsedBetween(startedAt: string, endIso: string | null): string { const start = Date.parse(startedAt); const end = endIso ? Date.parse(endIso) : Date.now(); if (Number.isNaN(start) || Number.isNaN(end)) { return ""; } return formatElapsedSeconds((end - start) / 1000); } /** * Elapsed time for the current activation. Live agents self-tick via DOM * writes (zero React commits per tick); settled agents freeze at completedAt. */ function AgentElapsed({ agent }: { agent: RuntimeSubagent }) { const textRef = useRef(null); const live = agent.status === "running" || agent.status === "waiting"; const startedAt = agent.startedAt; useEffect(() => { if (!live || !startedAt) { return; } const update = () => { if (textRef.current) { textRef.current.textContent = elapsedBetween(startedAt, null); } }; update(); const id = setInterval(update, 1000); return () => clearInterval(id); }, [live, startedAt]); if (!startedAt) { return null; } return ( {elapsedBetween(startedAt, live ? null : agent.completedAt)} ); } /** * Status-dependent activity line. Live rows lead with what is happening now; * settled rows lead with the outcome. Errors are the only inline previews on * failed rows because they explain a red row at a glance. */ function agentActivityText(agent: RuntimeSubagent): string | null { const live = agent.status === "running" || agent.status === "pending" || agent.status === "waiting"; if (live) { return ( agent.progress ?? (agent.lastToolName ? `▸ ${agent.lastToolName}` : null) ?? agent.result ?? agent.error ); } return ( agent.error ?? agent.result ?? agent.progress ?? (agent.lastToolName ? `▸ ${agent.lastToolName}` : null) ); } /** Flat, non-interactive agent status line. No unfold. */ function AgentRow({ agent }: { agent: RuntimeSubagent }) { const visuals = STATUS_VISUALS[agent.status]; const activity = agentActivityText(agent); const modelLabel = formatSubagentModelLabel(agent.model, agent.effort); const role = agent.role?.trim().toLocaleLowerCase() === agent.title.trim().toLocaleLowerCase() ? null : agent.role; const metadata = [ modelLabel, agent.usage ? `${formatSubagentTokenCount(agent.usage.totalTokens)} tok` : "— tok", agent.usage?.toolUses !== undefined ? `${agent.usage.toolUses} tools` : null, agent.activationCount > 1 ? `run ${agent.activationCount}` : null, ].filter((value): value is string => value !== null); return (
{agent.title} {role ? ( {role} ) : null} {agent.status === "completed" ? ( ) : null} {activity ?? visuals.label} {metadata.join(" · ")} {visuals.label}
); } function workflowIsLive(group: AgentPanelWorkflowGroup): boolean { const status = group.workflow.status; return ( status !== "completed" && status !== "failed" && status !== "cancelled" && status !== "interrupted" ); } function workflowMembers(group: AgentPanelWorkflowGroup): ReadonlyArray { return [...group.phases.flatMap((phase) => phase.members), ...group.unphasedMembers]; } /** * Phase rail: the run's shape at a glance. One segment per phase in order, * separated by chevrons; each segment shows title + one dot per member. * The whole arc (done → live → pending) is visible without scrolling the * member list. */ function PhaseRail({ group }: { group: AgentPanelWorkflowGroup }) { if (group.phases.length === 0) { return null; } return (
{group.phases.map((phase, index) => (
{index > 0 ? ( ) : null}
{phase.state === "done" ? "✓ " : ""} {phase.title} {phase.members.length === 0 ? ( – ) : ( phase.members.map((member) => ) )}
))}
); } /** * Read-only workflow script viewer, fetched through the contained * getWorkflowScript RPC (never a raw filesystem read from the client). */ function WorkflowScriptView({ environmentId, threadId, scriptPath, onClose, }: { environmentId: EnvironmentId; threadId: ThreadId; scriptPath: string; onClose: () => void; }) { const result = useAtomValue( orchestrationEnvironment.workflowScript({ environmentId, input: { threadId, scriptPath } }), ); return (
{scriptPath.split("/").at(-1)}
{result._tag === "Success" ? (
            {result.value.contents}
            {result.value.truncated ? "\n… (truncated)" : ""}
          
) : result._tag === "Failure" ? (

Could not load the script.

) : (

Loading…

)}
); } /** * Collapsible phase section. A phase opens when it becomes active, then keeps * that shape as it settles so completion never yanks rows out from under the * user. Manual toggles stick until a later activation begins. */ function PhaseSection({ phase, defaultOpen = false, }: { phase: AgentPanelWorkflowGroup["phases"][number]; defaultOpen?: boolean; }) { const [open, setOpen] = useState(defaultOpen || phase.state === "running"); const previousState = useRef(phase.state); useEffect(() => { if (previousState.current !== "running" && phase.state === "running") { setOpen(true); } previousState.current = phase.state; }, [phase.state]); return (
{open ? phase.members.map((member) => ) : null}
); } /** Expanded workflow: phase rail + full phase tree. */ function ExpandedWorkflowSection({ group, environmentId, threadId, onCollapse, }: { group: AgentPanelWorkflowGroup; environmentId: EnvironmentId | null; threadId: ThreadId | null; onCollapse: () => void; }) { const [scriptOpen, setScriptOpen] = useState(false); const members = workflowMembers(group); const settled = members.filter( (member) => member.status === "completed" || member.status === "failed" || member.status === "cancelled" || member.status === "interrupted", ).length; const scriptPath = group.workflow.runHandles?.scriptPath; const canShowScript = scriptPath !== undefined && environmentId !== null && threadId !== null; return (
{group.workflow.workflowName ?? group.workflow.title} {canShowScript ? ( ) : null} {settled}/{members.length} settled
{scriptOpen && canShowScript ? ( setScriptOpen(false)} /> ) : null} {group.phases.map((phase) => ( ))} {group.unphasedMembers.map((member) => ( ))} {group.phases.length === 0 && group.unphasedMembers.length === 0 ? ( ) : null}
); } /** * Collapsed workflow: one summary line. The parent owns expansion so a live * workflow keeps its shape when it settles. */ function CollapsedWorkflowSection({ group, onExpand, }: { group: AgentPanelWorkflowGroup; onExpand: () => void; }) { const members = workflowMembers(group); const failed = members.filter((member) => member.status === "failed").length; // Coordinator usage may already aggregate members (panel-footer rule): // count it only when there are no member rows to sum. const totalTokens = members.reduce( (sum, member) => sum + (member.usage?.totalTokens ?? 0), members.length === 0 ? (group.workflow.usage?.totalTokens ?? 0) : 0, ); const elapsed = group.workflow.startedAt && group.workflow.completedAt ? elapsedBetween(group.workflow.startedAt, group.workflow.completedAt) : null; return (
); } /** A workflow's open state is presentation state, not a status derivative. */ function WorkflowSection({ group, environmentId, threadId, }: { group: AgentPanelWorkflowGroup; environmentId: EnvironmentId | null; threadId: ThreadId | null; }) { const [open, setOpen] = useState(() => workflowIsLive(group)); return open ? ( setOpen(false)} /> ) : ( setOpen(true)} /> ); } export function AgentsPanel({ model, environmentId = null, threadId = null, }: { model: AgentPanelModel; environmentId?: EnvironmentId | null; threadId?: ThreadId | null; }) { if (!model.hasAgents) { return (

No agents yet

When this thread spawns subagents or runs a workflow, they show up here with live status, activity, and token usage.

); } return (
{model.workflows.map((group) => ( ))} {model.directAgents.length > 0 ? (
Direct spawns
{model.directAgents.map((agent) => ( ))}
) : null}
); }