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:

TreatmentAbandonment
Spinner12%
"Thinking..." with a timer11%
Generic step names ("Step 2 of 4")9%
Named steps, as above4%

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.