Chapter 142 · Interface and adoption
Streaming UX, and what to show while it thinks
Twelve seconds of a spinner against twelve seconds of visible work. What to reveal from the agent loop, the honesty constraint on progress messages, and the two things never to stream.
7 min read
Chapter 133 measured the effect and left the design open: streaming made total time 200 ms worse and satisfaction twelve points better. This chapter is what to show, and it goes well beyond streaming tokens.
The distinction that organises it: streaming the answer helps a 3-second run; showing the work is what rescues a 15-second one. A spinner for fifteen seconds and a visible sequence of steps for fifteen seconds are different products.
Three phases, three treatments
// A multi-step run has three phases and users need different things
// in each. Treating the whole run as one "loading" state is the mistake.
const PHASES = {
preToken: { typical: '0.6-2 s', show: 'what it is doing' },
generating: { typical: '1-3 s', show: 'the tokens' },
betweenSteps: { typical: '0.3-2 s each', show: 'the step that completed' },
}// The step feed. It is the chapter 89 trace, filtered to the things a
// non-engineer can read, rendered as it happens.
<StepFeed>
<Step done>Searching policies and contracts</Step>
<Step done>Found 4 relevant passages</Step>
<Step active>Checking the orders system</Step>
</StepFeed>Atlas's abandonment on runs over 10 seconds, by treatment:
| Treatment | Abandonment |
|---|---|
| Spinner | 12% |
| "Thinking..." with a timer | 11% |
| Generic step names ("Step 2 of 4") | 9% |
| Named steps, as above | 4% |
Generic step counts barely help, which is the finding worth carrying. "Step 2 of 4" tells the user nothing they can evaluate; "checking the orders system" tells them which dependency is slow and whether the system understood the question — and a user who can see it understood the question will wait.
The honesty constraint
// Progress messages are derived from what is actually happening, never
// written to fill the silence. Two rules.
// 1. Every message corresponds to a real span (chapter 128). If no
// span is open, show nothing rather than something reassuring.
// 2. No invented progress. A progress bar that advances on a timer
// rather than on work is a lie the user eventually catches, and the
// trust cost outlives the latency it was hiding.
const message = SPAN_LABELS[currentSpan.name] ?? null // null renders nothing// The labels, which are the only authored part. Written for a rep, not
// an engineer, and deliberately naming the external system where there
// is one — chapter 133 found that naming the slow dependency is what
// moved abandonment.
const SPAN_LABELS: Record<string, string> = {
rewrite_query: 'Understanding the question',
retrieve: 'Searching policies and contracts',
'tool:lookup_order': 'Checking the orders system',
'tool:check_stock': 'Checking inventory',
generate: 'Writing the answer',
validate_response: 'Checking the sources',
}The last label is worth keeping visible rather than hiding as technical detail. A user who sees "checking the sources" learns, once per run, that the system verifies its own citations — which is chapter 147's work happening for free.
Two things never to stream
// 1. Tool calls that have not been confirmed. Streaming "applying a
// credit of $400" while a confirmation dialog is pending reads as
// done, and chapter 72's whole point is that it is not.
if (step.kind === 'tool' && step.requiresConfirmation) suppressUntilConfirmed()// 2. The model's own reasoning text, raw. It contains dead ends,
// reconsiderations and occasionally the content of documents the
// answer will not cite. Atlas streamed it for two weeks and stopped:
// users read an abandoned line of reasoning as a conclusion, and
// three support questions came from reasoning text that said
// something the final answer correctly did not.The second is a real tension. Showing reasoning builds confidence and it is the raw output of a process whose intermediate states are wrong on purpose. Atlas's compromise is the step feed above — derived, labelled, and incapable of containing an uncited claim.
Streaming and validation
// Chapter 122's problem: a streamed token cannot be unsent, and
// citation integrity is only checkable when the answer is complete.
const STRATEGY = {
text: 'stream, sentence by sentence',
citations: 'render after completion, as the validated set',
structured: 'validate on completion; replace visibly on failure',
}// Which produces a specific visual sequence, and the ordering is the
// design: text appears, then citations attach to it.
{streaming ? <Text value={partial} /> : <AnswerWithCitations answer={final} />}A response failing validation after streaming is replaced with an error. Visible and jarring, and far better than the alternative — Atlas's rate is 0.1%, and leaving an invalid answer on screen because removing it looks bad is the wrong trade at any rate.
Cancellation
// A user who can stop a run is a user who will start one. Without a
// stop button, a slow run is a trap, and the measured behaviour is
// that people close the tab — which also loses the session.
<button onClick={() => controller.abort()}>Stop</button>// Three things a cancel must do, and the third is the one that gets
// missed.
async function onAbort(run: Run) {
await run.cancel() // 1. stop the work
await showPartial(run.partialAnswer) // 2. keep what exists
await audit.record('run.cancelled', { runId, atStep: run.step }) // 3. record it
}The record matters because a rising cancellation rate is the earliest adoption signal there is — it is a user telling you the answer was not worth the wait, before they stop coming back. Atlas alerts when it exceeds 3%.
Showing the partial answer rather than discarding it was worth a point of satisfaction on its own: a cancelled run that had already found the order status has done useful work, and throwing it away makes cancelling feel like a penalty.
Mobile and narrow screens
// The step feed is the first thing to break at 375px, because it grows
// downward while the answer is trying to appear above it.
const NARROW = {
stepFeed: 'collapse to the current step only, one line',
citations: 'collapsed by default; chapter 143 expands in place',
followUps: 'horizontal scroll rather than wrap',
}Atlas's mobile share is 22% — warehouse and field staff — and the step feed collapsing to one line was the difference between a usable screen and a page of progress messages with the answer below the fold.
Key points
- Streaming the answer helps a 3-second run. Showing the work rescues a 15-second one.
- Treat the three phases differently: before the first token, during generation, and between steps.
- Named steps cut abandonment on long runs from 12% to 4%. Generic step counts took it only to 9%.
- Derive every progress message from a real span. If no span is open, show nothing rather than something reassuring.
- Never advance a progress indicator on a timer. The trust cost outlives the latency it hides.
- Write the labels for the reader, and name the external system. "Checking the orders system" is what moved the number.
- Never stream an unconfirmed tool call — it reads as done — or raw reasoning text, which contains dead ends users read as conclusions.
- Stream text, attach citations after validation, and replace a failing response visibly. 0.1% is not a reason to leave an invalid answer on screen.
- Give people a stop button, keep the partial answer, and record the cancellation. A rising cancel rate is the earliest adoption signal there is.
- On narrow screens, collapse the step feed to the current step. Atlas's mobile share is 22%.
Next: showing sources so an answer can be checked — the interface half of chapter 62.