Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Building UI with AI agents
  4. AI Agent Tool Call UI: Steps, Approvals and Errors
Auf Deutsch lesen
  1. Blog
  2. Building UI with AI agents
  3. AI Agent Tool Call UI: Steps, Approvals and Errors
Building UI with AI agents
Building UI with AI agents

AI Agent Tool Call UI: Steps, Approvals and Errors

SR
Serban Rusu · Founder of Wingo UI
Oct 16, 2026 · 11 min read

On this page

0%
  1. What states does an AI agent tool call need?
  2. How do you make tool inputs and outputs readable?
  3. How do you build a tool call UI in React with the AI SDK?
  4. How should human in the loop approval UI work?
  5. Where do tool calls go: a chat message, a panel or an agent page?
  6. How do you show an agent run's overall status?
  7. How do reasoning and tool calls fit together?
  8. What makes a tool call UI accessible on a phone?
  9. When is AI Elements the better choice?
  10. What should you try next?
  11. FAQ
    1. What states does a tool call UI need?
    2. How do I add human in the loop approval to an AI SDK tool?
    3. Should tool call details be open or collapsed by default?
    4. How do I show tool calls inside a chat message?
    5. Are the Wingo UI agent components free?

TL;DR

A good AI agent tool call UI shows every call as one row with a status (queued, running, done, failed, waiting for approval), a human title, the tool's id, how long it took, and its input and output as readable JSON on demand. Failed calls open by themselves on their error, calls that need a person show Approve and Reject next to the exact arguments that will run, and each status change is announced once to screen readers. Map your AI SDK tool part states to those statuses and the same rows work in a chat message, a side panel or an agent page.

Published Oct 16, 2026

An AI agent tool call UI has one job: let a person see what the agent did, step by step, and step in when it matters. Most first versions fall short in the same places. A running call renders nothing, so the screen looks idle while the agent works. A failed call vanishes into a generic "Something went wrong". An approval prompt says "Send email?" without showing the address it will go to. This guide covers the states a tool call UI needs, how to make inputs and outputs readable, how to wire approvals to the AI SDK, and where the rows go in a chat, a side panel or an agent page. The examples use the agent components of Wingo UI, the library we build, so we are not neutral; outside facts link to their sources.

What states does an AI agent tool call need?

Six: queued, running, done, failed, canceled and waiting for approval. Each one needs a look of its own, because a person scanning a run reads the markers before the words.

If you use the AI SDK, its tool parts already carry a state. The chatbot tool usage docs (opens in a new tab) list input-streaming, input-available, approval-requested, approval-responded, output-available, output-error and output-denied (as of October 2026). Here is how they map to what a person should see:

AI SDK stateStatusWhat the row shows
input-streamingpendinga dashed ring, "Queued"
input-availablerunninga spinner, a shimmering title, a time that ticks
approval-requestedawaiting-approvala hand that pings, "Needs approval", Reject and Approve
approval-responded (approved)runningthe spinner again while the tool runs
output-availablecompletea check that pops once, the final time
output-errorerrora cross, the row opened on its error, Retry when you pass onRetry
output-deniedcanceleda gray marker, "Canceled"

The middle column is the ToolCallStatus type from lib/ai-chat, which Tool Call Timeline and Agent Status Card share. Marker colors come from the status alone, so the tone prop only paints the Approve button and the header: running is info blue, done is green, failed is red and waiting is amber.

Two rules make these states hold up in any tool call UI in React. Show a running call the moment its input arrives, so real work never looks like a frozen screen. And keep failed calls in the list with their error, because a failure that disappears is the hardest one to debug.

How do you make tool inputs and outputs readable?

Lead with a human title, keep the tool's id one tap away, and show the JSON only when someone asks for it.

Each row in the timeline reads like a sentence: "Finding overdue invoices", then search_invoices in a small mono chip, then "0.8 s". Click or tap the row (or press Enter or Space on it) and it opens the input the model sent and the output the tool returned as highlighted JSON. Text results wrap instead of turning into one long string. Every block has a copy button, and a block taller than codeMaxHeight (240px by default) scrolls inside itself, so a 200-line response does not push the rest of the run off screen.

When JSON is the wrong view, swap it. renderOutput and renderInput take the call and return any React node: invoices as a small table, an email draft as text, a map. Return undefined to keep the JSON and null to hide the section.

Watch the agent find invoices, fetch contacts and draft the email, then stop at Sending the email. Open a row to read its input and output, then press Approve or Reject
$ npx wingo-ui@latest add tool-call-timeline
ProTool Call Timeline docs

The shape behind each row is plain data:

ts
import type { ToolCall } from "@/components/ui/tool-call-timeline";
export const search: ToolCall = {
id: "call-search",
name: "search_invoices",
title: "Finding overdue invoices",
status: "complete",
input: { status: "overdue", dueFrom: "2026-09-01", dueTo: "2026-09-24" },
output: { total: 3, sum: 22775.95, currency: "USD" },
durationMs: 820,
};

startedAt and endedAt work too; durationMs wins when your server already measured the call. steps holds sub-steps, which render indented under their parent, and logs holds plain log lines.

How do you build a tool call UI in React with the AI SDK?

Convert each assistant message's tool parts into ToolCall objects, pass them to the timeline, and send approvals back with addToolApprovalResponse. That adapter is all an AI SDK tool UI needs on top of the component.

bash
npx wingo-ui@latest add tool-call-timeline

The adapter keeps the AI SDK on one side and the component on the other. The structural ToolPart type keeps it independent of your tool definitions:

ts
// lib/agent-tool-calls.ts
import type { UIMessage } from "ai";
import type { ToolCall, ToolCallStatus } from "@/components/ui/tool-call-timeline";
type ToolPart = {
type: string;
toolCallId: string;
toolName?: string;
state: string;
input?: unknown;
output?: unknown;
errorText?: string;
approval?: { id: string; approved?: boolean; isAutomatic?: boolean };
};
const STATUS: Record<string, ToolCallStatus> = {
"input-streaming": "pending",
"input-available": "running",
"approval-requested": "awaiting-approval",
"approval-responded": "running",
"output-available": "complete",
"output-error": "error",
"output-denied": "canceled",
};
// what people read; the tool id stays in the chip
const TITLES: Record<string, string> = {
searchInvoices: "Finding overdue invoices",
sendEmail: "Sending the email",
};
function toolParts(message: UIMessage): ToolPart[] {
return message.parts
.filter((part) => part.type.startsWith("tool-") || part.type === "dynamic-tool")
.map((part) => part as unknown as ToolPart);
}
export function toToolCalls(message: UIMessage): ToolCall[] {
return toolParts(message).map((part) => {
const name = part.type === "dynamic-tool" ? (part.toolName ?? "tool") : part.type.slice(5);
const denied = part.state === "approval-responded" && part.approval?.approved === false;
// a server-side policy decided: nothing for a person to press
const automatic = part.state === "approval-requested" && part.approval?.isAutomatic === true;
return {
id: part.toolCallId,
name,
title: TITLES[name],
status: denied ? "canceled" : automatic ? "running" : (STATUS[part.state] ?? "pending"),
input: part.input,
output: part.output,
error: part.errorText,
};
});
}
export function approvalIdFor(message: UIMessage, callId: string): string | undefined {
return toolParts(message).find((part) => part.toolCallId === callId)?.approval?.id;
}

Then render one timeline per assistant message:

tsx
// app/agent/agent-run.tsx
"use client";
import { useMemo } from "react";
import { useChat } from "@ai-sdk/react";
import { lastAssistantMessageIsCompleteWithApprovalResponses, type UIMessage } from "ai";
import { ToolCallTimeline, type ToolCall } from "@/components/ui/tool-call-timeline";
import { approvalIdFor, toToolCalls } from "@/lib/agent-tool-calls";
function RunSteps({
message,
respond,
}: {
message: UIMessage;
respond: (call: ToolCall, approved: boolean) => void;
}) {
const calls = useMemo(() => toToolCalls(message), [message]);
if (calls.length === 0) return null;
return (
<ToolCallTimeline
variant="card"
title="Collections agent"
calls={calls}
onApprove={(call) => respond(call, true)}
onReject={(call) => respond(call, false)}
locale="en-US"
/>
);
}
export function AgentRun() {
const { messages, addToolApprovalResponse } = useChat({
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
});
return (
<div className="flex flex-col gap-4">
{messages
.filter((message) => message.role === "assistant")
.map((message) => (
<RunSteps
key={message.id}
message={message}
respond={(call, approved) => {
const id = approvalIdFor(message, call.id);
if (id) addToolApprovalResponse({ id, approved });
}}
/>
))}
</div>
);
}

useChat hands you a new object for the message that is streaming, so useMemo rebuilds the calls of that one message while older runs keep theirs. Your chat input stays as it is. The route handler in the next section adds the approval gate.

One pitfall: an approval-requested part is not always a question for a person. When a server-side policy approves or denies a call on its own, the AI SDK sets approval.isAutomatic to true and expects no response. The adapter shows those calls as running, so nobody gets Approve and Reject buttons that do nothing.

How should human in the loop approval UI work?

Pause before the side effect, show the exact arguments that will run, and keep the decision one tap away. A human in the loop approval UI that hides the input asks people to approve something they cannot see.

On the server, the gate goes on streamText. The chatbot tool usage docs (opens in a new tab) set a tool to "user-approval" in the toolApproval option and mark the older needsApproval tool property as deprecated (as of October 2026). The streamText reference (opens in a new tab) also accepts a function per tool that receives the input, so only risky calls stop. The tool keeps its execute; the SDK waits for the answer before running it:

ts
// lib/agent-tools.ts
import { tool } from "ai";
import { z } from "zod";
import { invoices } from "@/lib/invoices"; // your data layer
import { mailer } from "@/lib/mailer"; // your mail client
export const searchInvoices = tool({
description: "Find invoices by status and due date",
inputSchema: z.object({ status: z.enum(["open", "overdue", "paid"]), dueFrom: z.string(), dueTo: z.string() }),
execute: async (input) => invoices.search(input),
});
export const sendEmail = tool({
description: "Send a payment reminder to a client",
inputSchema: z.object({ to: z.string().email(), subject: z.string() }),
execute: async ({ to, subject }) => mailer.send({ to, subject }),
});
ts
// app/api/chat/route.ts
import { convertToModelMessages, streamText, type UIMessage } from "ai";
import { model } from "@/lib/ai"; // your provider's model
import { searchInvoices, sendEmail } from "@/lib/agent-tools";
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model,
system: "When a tool execution is not approved by the user, do not retry it.",
messages: await convertToModelMessages(messages),
tools: { searchInvoices, sendEmail },
toolApproval: { sendEmail: "user-approval" },
});
return result.toUIMessageStreamResponse();
}

The AI SDK's human in the loop recipe (opens in a new tab) still shows needsApproval: true on the tool. Both paths produce the same approval-requested parts, so the client code above does not change, and the streamText reference says toolApproval wins when both are set. Tool keys become part types (tool-sendEmail), which is why the adapter's TITLES map uses the same camelCase names.

On the client, a call with status awaiting-approval shows Reject and Approve under its title, and opening the row shows the arguments right above them. To open waiting calls by themselves, control expandedKeys and add their ids as they arrive. If onApprove returns a promise, Approve spins and Reject stays disabled until it settles; when the buttons go away, focus moves back to the row, so a keyboard user never lands on the page body. On a phone the two buttons are 44px tall and share the row's width.

Rejecting is a real outcome. Show the call as canceled (the adapter above does it from approval.approved === false). Keep the system line from the route above: the AI SDK recipe adds that instruction so the model does not retry a denied call and ask the person the same question twice.

Where do tool calls go: a chat message, a panel or an agent page?

All three, with a different variant for each place.

  • In a chat message, use variant="inline": a one-line summary like "Used 4 tools, 2.8 s" that opens while a call runs or waits for approval and closes 1.2 s after everything finished. It stays open if the person opened it or focus is inside it. Chat Message renders this on its own when the message has toolCalls, and toolCallProps passes onApprove and the rest through.
  • In a side panel, use variant="timeline" with size="sm". Rows lay out by their own width, so in a narrow column the tool id moves under the title.
  • On an agent page, use variant="card". The header shows the run's state in words, its progress while it runs ("2 of 5 done"), the total time and a button that opens or closes every row.

How do you show an agent run's overall status?

Put a status card above the agent steps timeline. When an agent runs in the background, people need an answer to "is it still working?" before anyone opens a row.

Agent Status Card takes the same ToolCall[] as steps and derives the progress ("Step 3 of 5"), the current step and the elapsed time from them. Only the controls that matter right now appear: Pause and Stop while it runs, Approve and Reject in an amber box while it waits, Retry in a red box when it failed, View result when it is done. Stop asks first in a dialog, so mount AlertDialogProvider once.

tsx
import { AlertDialogProvider } from "@/components/ui/alert-dialog";
import { AgentStatusCard, type ToolCall } from "@/components/ui/agent-status-card";
export function CollectionsAgent({ steps, onPause, onStop }: {
steps: ToolCall[];
onPause: () => Promise<void>;
onStop: () => Promise<void>;
}) {
return (
<>
<AlertDialogProvider />
<AgentStatusCard
name="Collections agent"
task="Reminders for September's overdue invoices"
status="running"
steps={steps}
startedAt={new Date(2026, 8, 24, 16, 38, 2)}
logs={[]}
onPause={onPause}
onStop={onStop}
/>
</>
);
}
The run advances step by step. Open Steps and Logs, press Pause, then try Stop to see the confirmation dialog
$ npx wingo-ui@latest add agent-status-card
ProAgent Status Card docs

Use variant="compact" for a grid of agents and variant="row" for a 64px list line with a progress ring. Below 400px the orb moves above the text and the buttons share the width.

How do reasoning and tool calls fit together?

Reasoning comes before the first call and between calls, and it should fold the same way. Reasoning Indicator shows a shimmering label and a live timer while the model thinks, then settles on "Thought for 12 s" that opens on a tap. Its label prop replaces the text during a phase, for example "Searching the web", so a thinking step and a tool step read alike. Chat Message stacks the reasoning row above the tool summary, then the answer below both.

What makes a tool call UI accessible on a phone?

It needs three things: one announcement per status change, 44px touch targets and long content that scrolls inside its own block.

  • Announce once per change. The list is not a live region; a screen reader reading every ticking duration is unusable. A separate polite region says "Sending the email: Needs approval" once when the status changes.
  • Say the status in words. Failed and waiting calls say their status next to the title. The others carry it in the marker and a screen-reader-only word.
  • Targets. On touch screens rows grow to 44px, and small rows keep their look with a 44px hit area.
  • Keep scrolling inside the row. Code blocks scroll sideways inside themselves, so a long URL in a JSON output never widens the page at 360px. Agent logs stop at 40% of the screen height.
  • Reduced motion. Springs become quick fades, and the shimmer and the ping stop.

When is AI Elements the better choice?

AI Elements is the better choice when your app already runs on the AI SDK and you want free components under the Apache 2.0 license (opens in a new tab). Vercel's AI Elements has a Tool (opens in a new tab) component whose header, input and output take the fields of the SDK's tool part (its state, input, output and errorText) and a Confirmation (opens in a new tab) component that takes a part's approval and state, all built on shadcn/ui. You skip the adapter above. Its docs (opens in a new tab) also say the project is moving to shadcn/ui and the current site will be deprecated at the end of November 2026, so new projects should install it from there (checked October 16, 2026). Wingo UI's timeline fits when you want the rail, durations, sub-steps, retry, the run-level card and the phone layouts built in, or when your agent does not use the AI SDK at all, since ToolCall is plain data. For the chat around it, see our guide to the React AI chat UI.

What should you try next?

Open the Tool Call Timeline docs, switch between the timeline, card and inline variants in the playground, then install it with npx wingo-ui@latest add tool-call-timeline. It and the other two components in this guide are part of Wingo UI Pro. If a coding agent builds your screens, the component library MCP guide shows how to let it read these props directly, and more posts like this one are in Building UI with AI agents.

Components in this post

  • Tool Call Timeline

    A React tool call timeline for AI agents: each step with its status, time, input and output, plus approval and retry when a call needs them.

    Pro
  • Agent Status Card

    A React AI agent status card: orb, name, status, current step, progress, elapsed time, steps, logs and the controls that matter right now.

    Pro
  • Reasoning Indicator

    A React reasoning indicator for AI chat: a shimmering live timer while the model thinks, then a quiet "Thought for 12 s" row to expand.

    Pro

FAQ

What states does a tool call UI need?

Six cover real agents: queued, running, done, failed, canceled and waiting for approval. The AI SDK's tool part states (input-streaming, input-available, approval-requested, approval-responded, output-available, output-error, output-denied) map onto them one to one or two to one.

How do I add human in the loop approval to an AI SDK tool?

Set the tool to 'user-approval' in the toolApproval option of streamText (the older needsApproval tool property is marked deprecated in the AI SDK docs as of October 2026). On the client, render Approve and Reject when the tool part's state is approval-requested and call addToolApprovalResponse with the approval id and approved true or false. Pass lastAssistantMessageIsCompleteWithApprovalResponses as sendAutomaticallyWhen so the chat continues once every approval is answered.

Should tool call details be open or collapsed by default?

Collapsed for calls that worked and open for calls that failed. People read a successful call's title and time; they need the error, the input and the logs of a failed one right away.

How do I show tool calls inside a chat message?

Use a one-line summary such as Used 4 tools, 2.8 s that opens while a call runs or waits for approval and folds away after the run finishes. Wingo UI's Chat Message does this when you pass toolCalls on the message.

Are the Wingo UI agent components free?

No. Tool Call Timeline, Agent Status Card and Reasoning Indicator are part of Wingo UI Pro. Free items they build on, such as Button, Badge, Code Block and Progress, install without a paid plan.

  • AI agents
  • Tool calls
  • Human in the loop
  • React
  • Accessibility

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Building UI with AI agents

AI Elements Alternative: 5 React AI Chat Kits Compared

Looking for an AI Elements alternative before its November 2026 deprecation? Five React AI chat kits compared on streaming, tool calls, phones and lock-in.

SRSerban Rusu·Oct 16, 2026·11 min read
Building UI with AI agents

React AI Chat UI: Streaming, Auto-Scroll and Mobile

How a production React AI chat UI handles streaming, auto-scroll, IME input and the phone keyboard, part by part, with code for the Next.js App Router.

SRSerban Rusu·Oct 9, 2026·10 min read
Building UI with AI agents

Claude Code Design System: Make 50 Screens Match

Set up a Claude Code design system: tokens, size tables and component rules the agent reads, plus the checks that prove 50 generated screens still match.

SRSerban Rusu·Oct 16, 2026·12 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI