/** * Message components — one entry of the transcript. * * Assistant messages render as: thinking (collapsible) → tool chips → text. * Thinking is streamed live in the session view (expanded while streaming, * collapses when the message lands) and persisted in the entry, so it * survives reloads and resumes. */ import React, { useEffect, useState } from "react"; import type { ClientToolCall } from "../../../src/shared/protocol.ts"; import { Markdown } from "./Markdown.tsx"; // ─── thinking ─────────────────────────────────────────────────────────────── export function ThinkingBlock({ text, streaming }: { text: string; streaming?: boolean }) { const [open, setOpen] = useState(false); // Auto-collapse when the stream finishes (expanded-while-live pattern). useEffect(() => { if (streaming) setOpen(true); else setOpen(false); }, [streaming]); if (!text) return null; return (
{open &&
{text}
}
); } // ─── tool calls ───────────────────────────────────────────────────────────── const TOOL_LABELS: Record = { quiz: "asked a quiz", ask_user_question: "asked a question", }; export function ToolChips({ toolCalls }: { toolCalls: ClientToolCall[] }) { if (toolCalls.length === 0) return null; return (
{toolCalls.map((tc, i) => (
{TOOL_LABELS[tc.name] ?? tc.name} {typeof tc.args.question === "string" && tc.args.question && ( {tc.args.question} )}
))}
); } // ─── user ─────────────────────────────────────────────────────────────────── export function UserMessage({ text, status }: { text: string; status?: "sending" | "failed" }) { return (
{text}
{status === "failed" &&
failed to send
} {status === "sending" &&
sending…
}
); } // ─── assistant ────────────────────────────────────────────────────────────── export function AssistantMessage({ text, thinking, toolCalls, }: { text: string; thinking?: string; toolCalls: ClientToolCall[]; }) { return (
{text && }
); }