/** * ChatThread renders the conversation: user prompts, streamed agent answers * (markdown) and inline errors, auto-scrolled to the latest entry. * * @example * */ import { useEffect, useRef } from "react"; import type { ThreadItem } from "../reducer"; import { Markdown } from "./Markdown"; import { ToolCallCard } from "./ToolCallCard"; import { WorkingIndicator } from "./WorkingIndicator"; interface ChatThreadProps { thread: ThreadItem[]; turnActive: boolean; } export function ChatThread({ thread, turnActive }: ChatThreadProps) { const endRef = useRef(null); // biome-ignore lint/correctness/useExhaustiveDependencies: thread and turnActive intentionally retrigger the scroll-to-bottom on every conversation change. useEffect(() => { // Optional call: test DOMs (jsdom) do not implement scrollIntoView. endRef.current?.scrollIntoView?.({ behavior: "smooth" }); }, [thread, turnActive]); return (
{thread.length === 0 && (

Send a prompt to start working with the agent.

)} {thread.map((item, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: the thread is append-only (never reordered or spliced), so the index is a stable identity. ))} {turnActive && }
); } function ThreadEntry({ item }: { item: ThreadItem }) { switch (item.kind) { case "user": return
{item.text}
; case "agent": return (
); case "thought": return ; case "tool": return ; case "error": return (
{item.text}
); } } /** * ThoughtEntry renders one thinking block: while the thought streams its * content stays visible; once the turn ends it collapses into a reopenable * summary. */ function ThoughtEntry({ text, closed }: { text: string; closed: boolean }) { if (!closed) { return (

💭 Thinking…

); } return (
💭 Thought for a moment
); }