/** * SubagentGroupDisplay - Live/interactive subagent status display * * Used in the ACTIVE render area for subagents that may still be running. * Subscribes to external store and handles keyboard input - these hooks * require the component to stay "alive" and re-rendering. * * Features: * - Real-time updates via useSyncExternalStore * - Blinking dots for running agents * - Expand/collapse tool calls (ctrl+o) * - Shows "Running N subagents..." while active * * When agents complete, they get committed to Ink's area using * SubagentGroupStatic instead (a pure props-based snapshot with no hooks). */ import { Box, Text, useInput } from "ink"; import { memo, useSyncExternalStore } from "react"; import { formatStats, getTreeChars } from "../helpers/subagentDisplay.js"; import { getSnapshot, type SubagentState, subscribe, toggleExpanded, } from "../helpers/subagentState.js"; import { useTerminalWidth } from "../hooks/useTerminalWidth.js"; import { BlinkDot } from "./BlinkDot.js"; import { colors } from "./colors.js"; function formatToolArgs(argsStr: string): string { try { const args = JSON.parse(argsStr); const entries = Object.entries(args) .filter(([_, value]) => value !== undefined && value !== null) .slice(0, 2); if (entries.length === 0) return ""; return entries .map(([key, value]) => { let displayValue = String(value); if (displayValue.length > 50) { displayValue = `${displayValue.slice(0, 47)}...`; } return `${key}: "${displayValue}"`; }) .join(", "); } catch { return ""; } } // ============================================================================ // Subcomponents // ============================================================================ interface AgentRowProps { agent: SubagentState; isLast: boolean; expanded: boolean; } const AgentRow = memo(({ agent, isLast, expanded }: AgentRowProps) => { const { treeChar, continueChar } = getTreeChars(isLast); const columns = useTerminalWidth(); const gutterWidth = 6; // tree char (1) + " ⎿ " (5) const contentWidth = Math.max(0, columns - gutterWidth); const getDotElement = () => { switch (agent.status) { case "pending": return ; case "running": return ; case "completed": return ; case "error": return ; default: return ; } }; const isRunning = agent.status === "pending" || agent.status === "running"; const stats = formatStats( agent.toolCalls.length, agent.totalTokens, isRunning, ); const lastTool = agent.toolCalls[agent.toolCalls.length - 1]; return ( {/* Main row: tree char + description + type + model + stats */} {treeChar} {getDotElement()} {agent.description} · {agent.type.toLowerCase()} {agent.model && · {agent.model}} · {stats} {/* Subagent URL */} {agent.agentURL && ( {continueChar} {" ⎿ "} Subagent: {agent.agentURL} )} {/* Expanded: show all tool calls */} {expanded && agent.toolCalls.map((tc) => { const formattedArgs = formatToolArgs(tc.args); return ( {continueChar} {" "} {tc.name}({formattedArgs}) ); })} {/* Status line */} {agent.status === "completed" ? ( <> {continueChar} {" ⎿ Done"} ) : agent.status === "error" ? ( <> {continueChar} {" ⎿ "} {agent.error} ) : lastTool ? ( <> {continueChar} {" ⎿ "} {lastTool.name} ) : ( <> {continueChar} {" ⎿ Starting..."} )} ); }); AgentRow.displayName = "AgentRow"; interface GroupHeaderProps { count: number; allCompleted: boolean; hasErrors: boolean; expanded: boolean; } const GroupHeader = memo( ({ count, allCompleted, hasErrors, expanded }: GroupHeaderProps) => { const statusText = allCompleted ? `Ran ${count} subagent${count !== 1 ? "s" : ""}` : `Running ${count} subagent${count !== 1 ? "s" : ""}…`; const hint = expanded ? "(ctrl+o to collapse)" : "(ctrl+o to expand)"; // Use error color for dot if any subagent errored const dotColor = hasErrors ? colors.subagent.error : colors.subagent.completed; return ( {allCompleted ? ( ) : ( )} {statusText} {hint} ); }, ); GroupHeader.displayName = "GroupHeader"; // ============================================================================ // Main Component // ============================================================================ export const SubagentGroupDisplay = memo(() => { const { agents, expanded } = useSyncExternalStore(subscribe, getSnapshot); // Handle ctrl+o for expand/collapse useInput((input, key) => { if (key.ctrl && input === "o") { toggleExpanded(); } }); // Don't render if no agents if (agents.length === 0) { return null; } const allCompleted = agents.every( (a) => a.status === "completed" || a.status === "error", ); const hasErrors = agents.some((a) => a.status === "error"); return ( {agents.map((agent, index) => ( ))} ); }); SubagentGroupDisplay.displayName = "SubagentGroupDisplay";