ToolCallRow
Render a live assistant log from collapsed tool-call rows, thinking text, finished markdown, and grouped consecutive tool calls.
Wraps: lucide-react, react-markdown, rehype-sanitize
listInvoices1m 04s
searchDocs
Found three open invoices.
Use it
Examples import from the package. With copied source, import from @/components/ui/transcript-rows instead.
Simple
listInvoices1m 04s
searchDocs
Found three open invoices.
import { AssistantMessage, StatusLabel, ThinkingRow, ToolCallRow } from "@zedtech/zedkit-ui";
<ThinkingRow text="Checking open invoices." />
<ToolCallRow name="listInvoices" status={<StatusLabel tone="saved">Done</StatusLabel>} duration="1m 04s">
{`{"status":"open"}`}
</ToolCallRow>
<ToolCallRow name="searchDocs" status={<StatusLabel tone="loading">Running searchDocs…</StatusLabel>}>
{`query: refunds`}
</ToolCallRow>
<AssistantMessage>{"Found three open invoices."}</AssistantMessage>Burst
searchDocs
readFile
Refunds take 5 to 7 days.
listRefunds
import { AssistantMessage, ToolCallRow, TranscriptBurst, groupToolCallBursts } from "@zedtech/zedkit-ui";
const items = [
{ kind: "tool", id: "searchDocs", name: "searchDocs", body: "query: refunds" },
{ kind: "tool", id: "readFile", name: "readFile", body: "path: refunds.md" },
{ kind: "assistant", id: "reply", body: "Refunds take 5 to 7 days." },
{ kind: "tool", id: "listRefunds", name: "listRefunds", body: "status: open" },
];
{groupToolCallBursts(items).map((group) =>
group.type === "burst" ? (
<TranscriptBurst key={group.items.map((row) => row.id).join("-")} aria-label="Tool calls">
{group.items.map((row) => <ToolCallRow key={row.id} name={row.name}>{row.body}</ToolCallRow>)}
</TranscriptBurst>
) : group.item.kind === "tool" ? (
<ToolCallRow key={group.item.id} name={group.item.name}>{group.item.body}</ToolCallRow>
) : (
<AssistantMessage key={group.item.id}>{group.item.body}</AssistantMessage>
),
)}Kit not installed yet?
Install
Terminal pnpm add @zedtech/zedkit-ui @fontsource-variable/inter @fontsource-variable/space-groteskLoad the styles
Tailwind does not scan node_modules, so the @source line points it at the kit's class names. The path is relative to this file.
src/app/globals.cssReplace the contents@import "tailwindcss"; @import "@zedtech/zedkit-ui/styles.css"; @source "../../node_modules/@zedtech/zedkit-ui/src/**/*.{ts,tsx}"; @source not "../../node_modules/@zedtech/zedkit-ui/src/**/*.test.*";Pick a look
Set data-identity="glass" on <html> so the finished app matches the site. Fresh app: replace src/app/layout.tsx. Existing app: merge, and keep ThemeProvider and other providers.
src/app/layout.tsxReplace the contentsimport "./globals.css"; import "./fonts"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" data-identity="glass"> <body>{children}</body> </html> ); }Load the fonts
The Glass look uses Inter and Space Grotesk; these imports self-host them.
src/app/fonts.tsCreate the fileimport "@fontsource-variable/inter"; import "@fontsource-variable/space-grotesk";Import and render
src/app/page.tsxReplace the contentsimport { AssistantMessage, StatusLabel, ThinkingRow, ToolCallRow } from "@zedtech/zedkit-ui"; export default function Page() { return ( <main className="px-page-gutter py-section-sm"> <ThinkingRow text="Checking open invoices." /> <ToolCallRow name="listInvoices" status={<StatusLabel tone="saved">Done</StatusLabel>} duration="1m 04s"> {`{"status":"open"}`} </ToolCallRow> <ToolCallRow name="searchDocs" status={<StatusLabel tone="loading">Running searchDocs…</StatusLabel>}> {`query: refunds`} </ToolCallRow> <AssistantMessage>{"Found three open invoices."}</AssistantMessage> </main> ); }
Package version 2.0.1.
Props
Prop
Type
Show inherited HTML attributes
Standard attributes from the underlying element. See MDN.
Prop
Type
Accessibility
ToolCallRow is native details/summary (Enter/Space, find-in-page), collapsed by default. The tool name is the summary's accessible name; status and duration stay in the tree so a nested StatusLabel role=status can announce. Summary is min-h-target (44px on a phone). The chevron is aria-hidden. Truncation uses title={name} for sighted hover. ThinkingRow reuses StreamText (polite live region, aria-atomic=false); pass live={false} when a parent already announces. AssistantMessage is sanitized Markdown (same-tab info links). TranscriptBurst is a presentational role=group slot matching Stack gap="sm"; pass aria-label to name the group.
Troubleshooting
A parent live log already announces — pass `live={false}` on ThinkingRow (Simple leaves the default). Status and duration are slots; this module does not import StatusLabel, Badge, or formatDuration. `duration` is a string (for example `formatDuration` output). Consecutive `kind === "tool"` items group only at length ≥ 2; a single tool stays an item. `kind` is case-sensitive (`"Tool"` is not a tool). Key rows by a stable `id`, not `name`. Pass a non-empty `name`; empty still renders an unnamed disclosure. `name` is the summary label, not HTML `details` accordion grouping. No kit chrome strings — the caller owns the tool name. Do not add a border on TranscriptBurst.