import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ArrowDown, Pause, Play, AlertCircle, Square } from "lucide-react"; import { trpc } from "@/lib/trpc"; import { useSubscriptionWithErrorHandling } from "@/hooks"; import { type ParsedMessage, 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 containerRef = useRef(null); // Accumulate raw JSONL: initial query data + live subscription chunks const rawBufferRef = 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); rawBufferRef.current += chunk; setMessages(parseAgentOutput(rawBufferRef.current)); }, onError: (error) => { console.error('Agent output subscription error:', error); }, autoReconnect: true, maxReconnectAttempts: 3, } ); // Set initial output when query loads useEffect(() => { if (outputQuery.data) { rawBufferRef.current = outputQuery.data; setMessages(parseAgentOutput(outputQuery.data)); } }, [outputQuery.data]); // Reset output when agent changes useEffect(() => { rawBufferRef.current = ''; setMessages([]); setFollow(true); }, [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; return (
{/* Header */}
{agentName ? `Output: ${agentName}` : "Agent Output"} {subscription.error && (
Connection error
)} {subscription.isConnecting && ( Connecting... )}
{onStop && (status === "running" || status === "waiting_for_input") && ( )} {!follow && ( )}
{/* Output content */}
{isLoading ? (
Loading output...
) : !hasOutput ? (
No output yet...
) : (
{messages.map((message, index) => (
{message.type === 'system' && (
System {message.content}
)} {message.type === 'text' && (
{message.content}
)} {message.type === 'tool_call' && (
{message.meta?.toolName}
{message.content}
)} {message.type === 'tool_result' && (
Result
{message.content}
)} {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 )}
)}
))}
)}
); }