How to show tool calls and their results in an AI chat

· 3 min read

Show each tool call as one row in the message, above the answer it fed: a past-tense summary ("Searched 14 tickets"), the tool's code name, and a live timer while it runs. Fold the arguments and the result behind the row as copyable JSON. Group several calls under one line that counts them, and show errors in place with Retry.

What people need from a tool call#

When an agent calls tools, the person reading the chat wants three answers, in this order:

  1. What did it do? A sentence, not a function signature. "Read 3 open invoices in Stripe" beats stripe.invoices.list({status:"open"}).
  2. Is it still working? A timer that counts while the call runs, and a clear finished or failed state.
  3. Can I check it? The exact input and output, when they want them, without scrolling past JSON on every message.

Most chat UIs get this backwards. They either hide tool calls entirely, so the answer appears from nowhere, or they print raw JSON into the thread, so the answer is buried under it.

The row#

  • Summary first. While running, use the present tense of the intent ("Searching tickets"). When done, switch to past tense with the evidence ("Searched 14 tickets about refunds"). Write these in your tool handler, where you know the counts.
  • Tool name in mono. zendesk.search_tickets tells a developer exactly what ran, and it is an identifier, so it belongs in a monospaced face.
  • Timer, then duration. A live count while running; the final duration (0.8s) once done. Slow calls explain slow answers.
  • Fold-out with input and output. Formatted JSON, each with a Copy button. Closed by default.
  • Errors in place. The row turns to an error state with the message in one sentence and a Retry, instead of a toast that disappears.

Many calls#

An agent answering one question often makes four to ten calls. Do not stack ten rows above every answer. Wrap them in a group with one summary line ("Used 4 tools", or "2 of 4 tools done" while running) that unfolds to the rows. The group's state follows its slowest and worst row: still running if any row runs, failed if any failed.

Where rows go in the thread#

Put tool rows inside the assistant message, before the text they produced, in the order they ran. If the agent calls tools between paragraphs, keep that order; the reader sees the evidence just before the claim. For long multi-step runs that last minutes, a run timeline is a better container than the chat message.

Safety#

Tool output can contain anything: user data, secrets from an API, HTML. Render it as text inside a <pre>, never as HTML. Redact known secret fields (tokens, keys, passwords) before they reach the client, not in the UI. If a call needs a person's sign-off before it runs, that is an approval step, not a tool row.

With PixilUI#

ToolCallRow is the row; ToolCallRowGroup sums several. Map your SDK's tool parts onto them.

tsx
import { ToolCallRow, ToolCallRowGroup, type ToolCallRowValue } from "@/components/pixilui/tool-call-row";

type Call = { id: string; tool: string; state: "running" | "done" | "error"; summary: string; startedAt: number; durationMs?: number; input: ToolCallRowValue; output?: ToolCallRowValue; error?: string };

export function ToolCalls({ calls, onRetry }: { calls: Call[]; onRetry: (id: string) => Promise<void> }) {
  return (
    <ToolCallRowGroup>
      {calls.map((call) => (
        <ToolCallRow
          key={call.id}
          name={call.tool}
          status={call.state}
          summary={call.summary}
          startedAt={call.startedAt}
          durationMs={call.durationMs}
          input={call.input}
          output={call.output}
          error={call.error}
          onRetry={() => onRetry(call.id)}
        />
      ))}
    </ToolCallRowGroup>
  );
}

Install it with npx shadcn@latest add @pixilui/tool-call-row. Return a promise from onRetry and the row shows its own pending state.

When not to use this#

  • One-shot answers with no tools. If your model never calls tools, a row has nothing to say.
  • Calls the person should not see. Internal retrieval or ranking steps can be noise. Show the step that changed the answer, hide plumbing.
  • Long autonomous runs. Twenty calls over five minutes read better as a run timeline with steps than as rows in one message.
  • Debugging. Developers tracing latency and cost need a span waterfall with tokens per call, not a chat row.