Topic 8.4
Designing Tool UI
In one line
Tool activity needs its own small design: a progress line while it runs, a result card when it is done, details tucked away for the curious, and an error message that says what to do next. Several tools in one answer run side by side and finish in any order.
Think of it like this
A courier's tracking page. While the parcel moves you see one friendly line ("Out for delivery"). When it arrives you see a clear card with the time and a photo. If you want more, you can open the full scan history. If it fails, the page says "We missed you. Rebook delivery" and gives you a button. A tool in a chat deserves the same four things: progress, result, details, and a way forward when it fails.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Progress line
- A short plain-words line shown while a tool runs, for example "Looking up your order…".
- Result card
- A small, designed block showing the useful facts from a tool's output.
- <details>
- A built-in HTML element that shows a summary and hides the rest until the user opens it.
- regenerate()
- A function from
useChatthat asks the server for the last assistant answer again. - Parallel tool calls
- Several tool calls asked for in the same step. The SDK runs them at the same time.
- toolCallId
- A unique id for one tool call. Use it as a React key so the part keeps its DOM while its state changes.
- prefers-reduced-motion
- A CSS setting that tells you the person wants less animation. A spinner should stop or slow down for them.
Step by step
01Two tools in one step
In the first example the stand-in model asks for lookupOrder and returnPolicy in the same step. returnPolicy takes 40 ms and lookupOrder takes 90 ms. Read the console: first both parts are input-available, then returnPolicy finishes while lookupOrder is still working, then lookupOrder finishes.
The page shows one line per part. A line is either the progress text or the result card with a <details> for the raw data.
02One component per tool
Keep a small component for each tool's states, and one switch that picks the component from the tool name. Adding a tool then means adding one component and one line. The example's ToolLine does both jobs in a few lines, which is fine for two tools.
03When a tool fails, offer the way forward
The second example makes the first lookup fail and the second succeed. The error message names the order, says nothing was changed, and offers Try again. The button is only shown after the answer has finished, and it is disabled while the retry runs.
After the retry, the new answer replaces the failed one in place (regenerate() replaces the last assistant message), so the user does not see two answers.
04The page is driven by events, not by guessing time
Notice how the second example clicks Try again: it waits until the button exists and is enabled, and then clicks. A fixed delay would work on a fast machine and fail on a slow one. Use the same habit in your tests: wait for the thing you need to see, not for a number of milliseconds.
Try it yourself
- 1
Open the details
Run the first example, then click Details under the order line on the page.
You see the raw output,
{"orderId":"ORD-1042","item":"Masala chai 250 g","status":"shipped"}. The card shows what a customer needs and the details show what support needs. - 2
Swap the speeds
In the first example make
lookupOrdertake 20 ms andreturnPolicytake 120 ms.The console now shows
lookupOrderfinishing first. The list on the page keeps its order, because it follows the order of the parts, not the order of finishing.
Code & diagrams
The quicker tool (returnPolicy) finishes first. Open a Details line on the page to see the raw data that the card hides.
Preview
Console
Nothing logged yet.Expected console output
lookupOrder: input-available | returnPolicy: input-available
lookupOrder: input-available | returnPolicy: output-available
lookupOrder: output-available | returnPolicy: output-available
page: Order ORD-1042: Masala chai 250 g (shipped) / Details / Returns accepted for 7 days / Details || Your chai has shipped, and you can return it within 7 days.The example clicks Try again by itself, but only when the button is on the page and enabled. It never guesses a delay.
Preview
Console
Nothing logged yet.Expected console output
output-error | retry button: hidden
output-error | retry button: shown
output-available | retry button: hidden
answer: Your order is shipped.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Leave the retry button enabled
In the second example change the retry button to <button id="retry" onClick={() => { regenerate(); regenerate(); }}>Try again</button> (no disabled) and press Run.
Myth vs fact
Myth
Showing the raw JSON is the honest thing to do.
Fact
Honesty means the user can find out how an answer was made. A card with a closed Details section does that. Raw JSON in the main flow just hides the useful part.
Myth
Tool parts arrive in the order I listed the tools.
Fact
Parallel tools finish in any order. The list order follows the model's request, and each part has its own state.
Myth
A retry button should re-run only the failed tool.
Fact
regenerate() re-runs the whole answer. That is simple and usually right, since the model needs the result to write the text. Re-running one tool alone needs a design of your own.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Add
aria-live="polite"to the region that holds tool lines, so a screen reader announces "Looking up your order…" once and the result when it arrives. Without it, blind users hear nothing while the assistant works. - ▸
Collapse finished tool lines into one "Used 2 tools" summary after the text starts, and keep the lines available. A long chat of tool cards is noise.
- ▸
Plan for results that are too big or too odd for the card: a list of 200 orders, a name in another script, a very long unbroken id. Show the first few rows with a count ("Showing 5 of 200"), let long values wrap (
overflow-wrap: anywhere), and keep the full data inside the closed Details section with its own scroll. The card must never push the composer off the screen. - ▸
Do not show a spinner forever. If a tool has not finished after your own limit (say 15 seconds), say so and offer to stop (Phase 3). The server should also enforce a timeout inside
execute. - ▸
Make cards from data your code trusts, not from words the model wrote. A card that says "Order shipped" because the tool returned
shippedis a fact. A sentence the model wrote about the order is a claim.
Remember this
- 1
The problem this solves: The default way to show a tool part is
JSON.stringify(part.output)in a grey box. That is fine for debugging and bad for customers: they see field names, curly braces and nothing they can act on. Good tool UI turns data into a sentence or card, hides the technical part, and never leaves the user guessing. - 2
Progress line. While the state is
input-available, show one line in plain words that names what is happening: "Looking up your order…", "Checking the return policy…". Keep a map from tool name to words in one place. Never show the tool's code name (lookupOrder) to a customer. Add a visible change (a small spinner) so it does not look frozen, and respectprefers-reduced-motion. - 3
Result card. In
output-available, render the data the person cares about, not the raw object: "Order ORD-1042: Masala chai 250 g (shipped)". Use the same card component everywhere the order appears. Decide what the card does if a field is missing, because tools return{ error: … }objects for expected problems (such as "no such order"). - 4
Collapsed details. Put the raw input and output behind a
<details>element with the summary "Details". Curious users and your support team can open it. The default is closed. A native<details>works without JavaScript and is keyboard-accessible. - 5
Errors people can act on. In
output-error, say what *did not* happen, what *is* safe, and what to do: "I could not look up ORD-1042. Nothing was changed. [Try again]". The button callsregenerate(), which runs the last answer again. Hide or disable the button while a request is running, or two clicks start two attempts. - 6
Several tools in one answer. A model can ask for two tools in one step. The SDK runs them at the same time, so the message holds two tool parts that finish in either order: the slower one is last. Render a list, one line per part, each in its own state. Use
toolCallIdas the key. Do not assume the order in which results arrive matches the order of the list. - 7
Where the text goes. The model's words come after the tool parts in the same message. Show tool lines above the text, and consider collapsing them into a single "Used 2 tools" line once the answer is complete. Keep them reachable: people trust an answer more when they can see how it was found.
Explain it without notes
What four things should a good tool UI show?
Two tools are called in the same step. In what order do they finish, and how does the UI cope?
A tool fails. What should the error message and the button do?
Why shouldn't the UI show the tool's code name or its raw JSON by default?
Practice
Add a spinner character to the progress lines that stops spinning for people who prefer reduced motion. Write only the CSS.
Change the first example so that, once the answer text has started, the tool lines collapse into one line "Used 2 tools" with the original lines inside a <details>.
Trade-offs
- ↔
Showing every tool step builds trust and costs screen space. Show steps while they run, then fold them away.
- ↔
A custom card per tool looks best and costs the most code. Start with one generic result card and special-case the tools your users see every day.
Done when you can
I can show a plain-words progress line for a running tool.
I can render a result card and keep the raw data in a closed details element.
I can handle two tools finishing in any order.
I can write an error message with a button that does not allow double clicks.
I wait for events in scripted demos instead of guessing delays.