Skip to content
zedkit
@zedtech/zedkit-ui

ToolCallRow

Render a live assistant log from collapsed tool-call rows, thinking text, finished markdown, and grouped consecutive tool calls.

alpha@zedtech/zedkit-ui

Wraps: lucide-react, react-markdown, rehype-sanitize

Checking open invoices.
listInvoicesDone1m 04s
{"status":"open"}
searchDocsRunning searchDocs…
query: refunds

Found three open invoices.

View props table
Who is it for?
App authors wiring a live assistant transcript from kit rows.
When should I use it?
When the caller already has a log of tool calls, thinking, and finished assistant turns and needs rows, not a host.
When should I pick something else?
For a prompt composer — use `Compose a chat prompt`. For a full chat shell — compose `Lay out an admin page (sidebar + topbar + header)` with `Follow a growing log to the latest`. For unsanitized HTML.

Use it

Examples import from the package. With copied source, import from @/components/ui/transcript-rows instead.

Simple

Checking open invoices.
listInvoicesDone1m 04s
{"status":"open"}
searchDocsRunning searchDocs…
query: refunds

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
query: refunds
readFile
path: refunds.md

Refunds take 5 to 7 days.

listRefunds
status: open
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?

You get
Every component from one import, with the theme tokens included. Updates arrive with your package manager.
You need
  • React 19 and Tailwind CSS 4, for example a Next.js app created with the Tailwind option.
  1. Install

    Terminal
    pnpm add @zedtech/zedkit-ui @fontsource-variable/inter @fontsource-variable/space-grotesk
  2. Load 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.*";
    
  3. 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 contents
    import "./globals.css";
    import "./fonts";
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en" data-identity="glass">
          <body>{children}</body>
        </html>
      );
    }
    
  4. Load the fonts

    The Glass look uses Inter and Space Grotesk; these imports self-host them.

    src/app/fonts.tsCreate the file
    import "@fontsource-variable/inter";
    import "@fontsource-variable/space-grotesk";
    
  5. Import and render

    src/app/page.tsxReplace the contents
    import { 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.

On this page