Showing What the System Is Doing
Observable states — searching, retrieving, calling, processing — reported honestly. Not a dramatisation of thinking the system cannot actually show you.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What do I show during the thirty seconds when the model is not producing text?
Someone asked a question that requires the system to go and look things up. They want to know it is working, roughly what it is doing, and whether it is stuck.
Show a spinner. If it feels too blank, add a rotating series of messages like "Thinking…", "Pondering…", "Consulting the archives…".
A spinner conveys "busy" and nothing else, so a run that takes thirty seconds is indistinguishable from one that has hung.
- A spinner conveys "busy" and nothing else, so a run that takes thirty seconds is indistinguishable from one that has hung.
- Invented status messages are not reporting anything. They are a loading animation with extra words, and once a user notices they are decorative, every status message you show loses credibility — including the true ones.
- A rotating message that says "Consulting the archives" while the system is actually waiting on a failed tool is actively misleading.
- None of it survives a screen reader: a spinner with no accessible name and no announced state is silence.
- When something does go wrong, there is nowhere to put the information, because the UI has no vocabulary for tool states.
What is actually happening
In the browser, not in the framework.
- The agent backend genuinely knows a sequence of observable facts: which tool was invoked, with what broad intent, whether it returned, and whether it succeeded. Those are reportable (What the Frontend Owns in an Agent Product).
- It does not know, and cannot faithfully show, why the model chose that tool. Presenting a narrative of reasoning is presenting something the system does not have.
- Tool events arrive interleaved with tokens on the same stream, so progress is part of the same state machine rather than a separate loading flag.
- A run can call several tools, sequentially or in parallel, and any of them can fail while the run continues (Citations and Answers With Holes In).
What this makes the browser do
And which of it is avoidable.
- Rendering a small, frequently-changing status region — cheap, unless it is inside the same reactive scope as the transcript (What a Component Costs to Render).
- Animating a progress indicator, which should be compositor-driven so it keeps moving even when the main thread is busy (Cheap and Expensive Animation).
- Retaining tool results for the transcript, which is where an agent UI's memory growth usually comes from.
Report the observable, not the imagined
The line to hold is simple: show states the system emitted. A tool started; it returned; it failed; the run finished. These are facts, they are useful, and they degrade gracefully — if you cannot report one, you show less rather than showing something invented.
The alternative — a narration of thought — is tempting because it fills the silence attractively. But it describes something the system does not expose, so it can be wrong at exactly the moments the user most needs it to be right, and it teaches users to discount every status message you show.
- 1Queued
The run has been accepted and has not started.
fails by Being skipped, so a busy backend looks like a dead one.
- 2Searching / Retrieving
Names the tool in the user's terms and shows it is outstanding.
fails by Naming the internal service instead, which tells the user nothing.
- 3Tool returned
States the outcome, including a count where one exists.
fails by Reporting success before the backend confirmed it.
- 4Tool failed
Says so in place, and what it costs the answer.
fails by Swallowing the failure, leaving a silently degraded answer.
- 5Processing result
Covers the gap between a tool returning and tokens resuming.
fails by Leaving the gap blank, which reads as a hang (Streaming a Response Without Melting the Device).
- 6Completed / Cancelled / Truncated
Gives the run a terminal state that can be announced.
fails by Only ever having one ending, so an interrupted run looks finished.
| Show this | Not this | Why |
|---|---|---|
Searching your documents | Thinking about your question… | The first names an event that occurred; the second describes an internal process the system cannot observe |
Retrieved 12 results | Found some interesting things! | A count is checkable and sets expectations; enthusiasm is not information |
Calendar unavailable — answering without it | silence | The gap in the answer has a cause; hiding it makes the answer look more complete than it is |
Calling calendar (14s) | Almost there… | Elapsed time distinguishes slow from stuck; reassurance does not |
Completed | the spinner just disappearing | A terminal state is announceable and tells a screen-reader user the run has ended |
The announcement contract
Progress is a live-region problem, and live regions are easy to get wrong in both directions — too quiet and the run is invisible, too eager and it is unusable. The spec below is the shape that works for a stepped, long-running operation.
semantics A role="status" region (implicitly aria-live="polite", aria-atomic="true") holding a short text description of the current state. The step list itself is ordinary content — a list, not a live region — so it can be read at leisure. The stop control is a real button outside the live region.
| Tab | Reaches the stop control at any point during the run; it is never the last thing in the order. |
| Enter / Space | Activates stop, which announces Stopping and then a terminal state. |
| Escape | Optional shortcut for stop; if provided it must be discoverable and not the only way. |
- — Focus does not move when a step starts or completes — the run is not asking for attention.
- — Focus moves only when the run genuinely needs a decision, such as a confirmation for a destructive action (A Suggestion Is Not an Authorization).
- — On completion focus stays where the user left it; completion is announced, not focused.
- — The run starting: "Generating response".
- — Each meaningful step transition, once: "Searching your documents", "Retrieved 12 results".
- — A tool failure, with its consequence: "Calendar unavailable, answering without it".
- — The terminal state: "Response complete", "Response stopped", "Response ended early".
usually broken by Putting the streaming answer, or a ticking elapsed-seconds counter, inside the same live region — which converts a helpful status announcement into a continuous interruption and makes the whole interface unusable with a screen reader.
When a step fails but the run does not
Partial tool failure is the normal case, not an edge case, and it is where the honesty principle earns its keep. If the calendar lookup failed and the answer was assembled without it, the user is holding an answer with a known gap — and only the interface can tell them that.
Rendering the failure in place, attached to the step that failed, is better than a banner: it keeps the cause next to the consequence, and it survives the user scrolling back later to work out why the answer looked thin.
Searching your documents… ✓ Calling calendar… ✓ "You have no meetings this week."
Searching your documents ✓ 12 results
Calling calendar ✗ unavailable
"Based on your documents only — I could not reach
your calendar, so this may be missing meetings."
[ Retry calendar ]The first version states something false with full confidence: it did not check the calendar, so it does not know there are no meetings. The second keeps the claim inside what was actually verified, tells the user why it is limited, and offers the specific recovery — which is also the only version a user can safely act on.
How to build it
Most important first.
- Report states you can verify:
Searching documents,Retrieving 12 results,Calling calendar,Processing result,Completed. Every one of these corresponds to an event the backend actually emitted. - Name the tool in the user's terms, not yours. "Searching your documents" beats "invoking
vector_search_v2". - Keep completed steps visible so the user can see the shape of what happened, collapsed by default once the run finishes.
- Show failures as failures, in place, without ending the run — and say what it means for the answer.
- Give long-running steps an elapsed indication so a slow tool is distinguishable from a stuck one.
- Never claim a step the system did not take. This is a credibility asset that is spent once.
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Tool progress is exactly the kind of change a
statuslive region is for, and exactly the kind that is easy to overdo. Announce meaningful transitions — the step started, the step failed, the run completed — not every intermediate update (Live Regions and Announcement). - Use
role="status"(polite) rather thanrole="alert"for progress. Progress is not an emergency and should not interrupt what the user is reading; a tool *failure* may justify a more assertive announcement. - A purely visual spinner conveys nothing. Every indicator needs a text equivalent that describes the state, and that text is what should be announced.
- If a step needs the user (a permission prompt, a confirmation), that is a focus event, not just a visual one — it must be reachable and announced, and it must not appear so abruptly that it captures a keystroke meant for something else (Focus Management).
- Elapsed-time indicators that tick every second are an announcement flood if they live inside a live region. Keep the ticking counter out of it and announce only when the state changes.
What can go wrong
- Progress that only appears after the slow part finishes, which is when it is no longer needed.
- A tool that fails silently, so the answer is missing information and nothing indicates why.
- Nested or parallel tool calls flattened into one line, so the user cannot tell what is still outstanding.
- A status region that updates so often it becomes visual noise, or announces so often it becomes audible noise.
- Progress state that never resolves because the terminal event was dropped, leaving a permanent "Processing…".
- Parallel tool calls complete out of order, so a UI that assumes sequential completion mislabels which step finished.
- A terminal event can be lost on a flaky connection, leaving a step permanently in progress unless the client times it out itself.
- Tool names and parameters can leak internal structure. Report what the user needs to understand, not your service topology.
- A tool result rendered into the transcript is untrusted content from a document the agent retrieved, and must be sanitized like any other model output (Sanitization and Trusted HTML).
- Do not show a tool as completed until the backend confirms it. A UI that reports success optimistically is asserting something about a side effect it does not know occurred (A Suggestion Is Not an Authorization).
- "Users want to see the reasoning." They want to know what happened and whether to trust it. A narrative of reasoning that the system cannot verify is a story, and it is the fastest way to lose the credibility that makes real status messages useful.
- "A spinner is enough." It is enough for something short. It is not enough for anything with steps, and it is nothing at all without a text equivalent.
- "More detail is always better." Detail the user cannot act on is noise, and in a live region it is worse than noise.
- "Progress is cosmetic." It is the main mechanism by which a user decides whether to keep waiting or cancel.
Measuring it, and what changes in the field
- Duration per tool, from the client's view, which includes the queueing the server-side view often omits.
- Failure rate per tool — surfaced in the UI as well as in telemetry, because a tool that fails a fifth of the time is degrading answers invisibly (Frontend Error Tracking).
- Abandonment during long tool steps: if users cancel disproportionately while a particular tool runs, that step needs either a better indication or a different design.
- When tools are fast, progress detail is noise and a simple pending state is better.
- When tools are slow, progress detail is the difference between waiting and giving up.
- On a screen reader, the same information needs a different cadence — fewer, more meaningful announcements.
- Detailed progress builds trust and exposes internal structure that becomes an implicit contract users notice when it changes.
- Honest progress sometimes means showing an unglamorous truth — that the system is waiting on a slow third party. That is better than a fabricated narrative, and it also tells you what to fix.
- Keeping completed steps in the transcript aids understanding and grows the DOM and memory over a long session.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALReporting verifiable states rather than inferred reasoning is a product and honesty constraint, not a platform one, so it holds regardless of framework or vendor.
- SPEC-EVOLVINGWhat a backend can report about a run depends on the tool-calling and streaming interfaces available, which differ between providers and change often. Design your own progress vocabulary and map vendor events onto it, rather than surfacing a vendor's event names directly.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Distributed Systems — a run spanning several tools is a distributed operation, and partial failure with a continuing whole is its defining condition.