/** * DocumentTaskStream — renders a running task's output as a flowing document * (assistant prose, tool blocks) instead of the boxy log style of TaskOutput. * * Key differences from TaskOutput: * - Document typography (serif-ish paragraphs, not monospace logs). * - Interleaved with subtle marginalia for tool calls and results. * - "Comment" footer interrupts the running task via sendTaskMessage — * same backend wire as the existing input bar, just framed as a comment. */ import { useCallback, useEffect, useRef, useState } from "react"; import { SimpleMarkdown } from "../SimpleMarkdown"; import { useTaskSubscription, type TaskOutputEvent, } from "../../hooks/useTaskSubscription"; import { getTaskOutput, sendTaskMessage } from "../../lib/api"; interface DocumentTaskStreamProps { taskId: string; /** Human label used as the document header (e.g. "orchestrator", step name) */ label: string; } export function DocumentTaskStream({ taskId, label }: DocumentTaskStreamProps) { const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [isStreaming, setIsStreaming] = useState(false); const [comment, setComment] = useState(""); const [sending, setSending] = useState(false); const [sendError, setSendError] = useState(null); const containerRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); // Load historical output when the selected task changes. useEffect(() => { let cancelled = false; setLoading(true); setEntries([]); setIsStreaming(false); getTaskOutput(taskId) .then((res) => { if (cancelled) return; const mapped: TaskOutputEvent[] = res.entries.map((e) => ({ taskId: e.taskId, messageType: e.messageType, content: e.content, toolName: e.toolName, toolInput: e.toolInput, isError: e.isError, costUsd: e.costUsd, durationMs: e.durationMs, isPartial: false, })); setEntries(mapped); }) .catch((err) => { if (cancelled) return; // eslint-disable-next-line no-console console.error("Failed to load task output history:", err); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [taskId]); const handleOutput = useCallback((event: TaskOutputEvent) => { if (event.isPartial) return; setEntries((prev) => [...prev, event]); setIsStreaming(true); }, []); const handleUpdate = useCallback((event: { status: string }) => { if ( event.status === "completed" || event.status === "failed" || event.status === "cancelled" ) { setIsStreaming(false); } else if (event.status === "running") { setIsStreaming(true); } }, []); useTaskSubscription({ taskId, subscribeOutput: true, onOutput: handleOutput, onUpdate: handleUpdate, }); // Auto-scroll while at bottom. useEffect(() => { if (autoScroll && containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } }, [entries, autoScroll]); const handleScroll = useCallback(() => { if (!containerRef.current) return; const { scrollTop, scrollHeight, clientHeight } = containerRef.current; const atBottom = scrollHeight - scrollTop - clientHeight < 80; setAutoScroll(atBottom); }, []); const submitComment = useCallback( async (e: React.FormEvent) => { e.preventDefault(); const trimmed = comment.trim(); if (!trimmed || sending) return; setSending(true); setSendError(null); // Show the comment immediately as a user-input entry. setEntries((prev) => [ ...prev, { taskId, messageType: "user_input", content: trimmed, isPartial: false, }, ]); try { await sendTaskMessage(taskId, trimmed); setComment(""); } catch (err) { setSendError( err instanceof Error ? err.message : "Failed to send comment", ); window.setTimeout(() => setSendError(null), 5000); } finally { setSending(false); } }, [comment, sending, taskId], ); return (
{/* Document body */}

{label}

{isStreaming && ( Live )}

Live transcript — comments below are sent to the task as input.

{loading && entries.length === 0 ? (

Loading transcript…

) : entries.length === 0 ? (

{isStreaming ? "Waiting for output…" : "No output yet."}

) : (
{entries.map((entry, idx) => ( ))} {isStreaming && ( )}
)}
{/* Comment / interrupt footer */}
{sendError && (
{sendError}
)}
Comment