import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ArrowDown, Pause, Play, AlertCircle, Square, ChevronRight, ChevronDown, CheckCircle2, Loader2, Circle } from "lucide-react"; import { trpc } from "@/lib/trpc"; import { useSubscriptionWithErrorHandling } from "@/hooks"; import { type ParsedMessage, type TimestampedChunk, getMessageStyling, parseAgentOutput, } from "@/lib/parse-agent-output"; interface AgentOutputViewerProps { agentId: string; agentName?: string; status?: string; onStop?: (id: string) => void; } export function AgentOutputViewer({ agentId, agentName, status, onStop }: AgentOutputViewerProps) { const [messages, setMessages] = useState([]); const [follow, setFollow] = useState(true); const [expandedResults, setExpandedResults] = useState>(new Set()); const containerRef = useRef(null); // Accumulate timestamped chunks: initial query data + live subscription chunks const chunksRef = useRef([]); // Load initial/historical output const outputQuery = trpc.getAgentOutput.useQuery( { id: agentId }, { refetchOnWindowFocus: false, } ); // Subscribe to live output with error handling const subscription = useSubscriptionWithErrorHandling( () => trpc.onAgentOutput.useSubscription({ agentId }), { onData: (event: any) => { // TrackedEnvelope shape: { id, data: { agentId, data: string } } const raw = event?.data?.data ?? event?.data; const chunk = typeof raw === 'string' ? raw : JSON.stringify(raw); chunksRef.current = [...chunksRef.current, { content: chunk, createdAt: new Date().toISOString() }]; setMessages(parseAgentOutput(chunksRef.current)); }, onError: (error) => { console.error('Agent output subscription error:', error); }, autoReconnect: true, maxReconnectAttempts: 3, } ); // Set initial output when query loads useEffect(() => { if (outputQuery.data) { chunksRef.current = outputQuery.data; setMessages(parseAgentOutput(outputQuery.data)); } }, [outputQuery.data]); // Reset output when agent changes useEffect(() => { chunksRef.current = []; setMessages([]); setFollow(true); setExpandedResults(new Set()); }, [agentId]); // Auto-scroll to bottom when following useEffect(() => { if (follow && containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } }, [messages, follow]); // Handle scroll to detect user scrolling up function handleScroll() { if (!containerRef.current) return; const { scrollTop, scrollHeight, clientHeight } = containerRef.current; const isAtBottom = scrollHeight - scrollTop - clientHeight < 50; if (!isAtBottom && follow) { setFollow(false); } } // Jump to bottom function scrollToBottom() { if (containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; setFollow(true); } } const isLoading = outputQuery.isLoading; const hasOutput = messages.length > 0; const lastTodoWrite = [...messages] .reverse() .find(m => m.type === "tool_call" && m.meta?.toolName === "TodoWrite"); const currentTodos = lastTodoWrite ? (lastTodoWrite.meta?.toolInput as { todos: Array<{ content: string; status: string; activeForm: string }> } | undefined)?.todos ?? [] : []; return (
{/* Header */}
{agentName ? `Output: ${agentName}` : "Agent Output"} {subscription.error && (
Connection error
)} {subscription.isConnecting && ( Connecting... )}
{onStop && (status === "running" || status === "waiting_for_input") && ( )} {!follow && ( )}
{/* Todo strip */} {currentTodos.length > 0 && (
TASKS
{currentTodos.slice(0, 5).map((todo, i) => (
{todo.status === "completed" && } {todo.status === "in_progress" && } {todo.status === "pending" && } {todo.content}
))} {currentTodos.length > 5 && (
+ {currentTodos.length - 5} more
)}
)} {/* Output content */}
{isLoading ? (
Loading output...
) : !hasOutput ? (
No output yet...
) : (
{messages.map((message, index) => (
{message.type === 'system' && (
{message.content}
)} {message.type === 'text' && ( <>
{message.content}
)} {message.type === 'tool_call' && (
{message.meta?.toolName}
{message.content}
)} {message.type === 'tool_result' && (
setExpandedResults(prev => { const next = new Set(prev); if (next.has(index)) next.delete(index); else next.add(index); return next; })} > {expandedResults.has(index) ? ( <> Result
{message.content}
) : ( <> {message.meta?.toolName === "Task" ? `${(message.meta.toolInput as any)?.subagent_type ?? "Subagent"} result` : message.content.substring(0, 80)} )}
)} {message.type === 'error' && (
Error
{message.content}
)} {message.type === 'session_end' && (
{message.content} {message.meta?.cost && ( ${message.meta.cost.toFixed(4)} )} {message.meta?.duration && ( {(message.meta.duration / 1000).toFixed(1)}s )}
)}
))}
)}
); } function formatTime(date: Date): string { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); } function Timestamp({ date }: { date?: Date }) { if (!date) return null; return ( {formatTime(date)} ); }