Command Palette

Search for a command to run...

Hectal
PHASE 8Advanced ~26 min· topic 4 of 9

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 useChat that 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.

Two tools in one stepdiagram
Rendering diagram…

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. 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. 2

    Swap the speeds

    In the first example make lookupOrder take 20 ms and returnPolicy take 120 ms.

    The console now shows lookupOrder finishing first. The list on the page keeps its order, because it follows the order of the parts, not the order of finishing.

Code & diagrams

Two tools at once: progress lines, cards and details

The quicker tool (returnPolicy) finishes first. Open a Details line on the page to see the raw data that the card hides.

Preview

Press Run to see the page.

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.
A failed tool with a way forward

The example clicks Try again by itself, but only when the button is on the page and enabled. It never guesses a delay.

Preview

Press Run to see the page.

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.

terminal
$ Console
── what you'll see ──
output-error | retry button: hidden
output-error | retry button: shown
answer: I could not reach the orders system.Your order is shipped.Your order is shipped.

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 shipped is a fact. A sentence the model wrote about the order is a claim.

Remember this

  1. 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. 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 respect prefers-reduced-motion.

  3. 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. 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. 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 calls regenerate(), which runs the last answer again. Hide or disable the button while a request is running, or two clicks start two attempts.

  6. 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 toolCallId as the key. Do not assume the order in which results arrive matches the order of the list.

  7. 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

01

What four things should a good tool UI show?

02

Two tools are called in the same step. In what order do they finish, and how does the UI cope?

03

A tool fails. What should the error message and the button do?

04

Why shouldn't the UI show the tool's code name or its raw JSON by default?

Practice

01

Add a spinner character to the progress lines that stops spinning for people who prefer reduced motion. Write only the CSS.

02

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.