---
name: ai-surface-patterns
description: Interaction and interface rules for generative AI product surfaces — chat and conversational UI, streaming output, agent steps and tool calls, citations and sources, latency and empty states, errors versus refusals, and human review and approval gates. Use this whenever building or reviewing any screen where a model produces output the person reads or acts on, including chat panels, inline assistants, agent run views, and anything with a loading state that waits on a model.
---

# AI surface patterns

Most of what goes wrong on a generative surface is not visual. It is that the
interface makes a claim about what the system is doing, and the claim is wrong.
A spinner says "wait"; a stream says "here, and there is more coming". A retry button
says "this failed"; a refusal is not a failure. Getting these mappings right is most
of the design work on an AI product.

`reference/state-matrix.md` has the full state table per surface. Read it before
building a new surface. The rules below apply to all of them.

## Never show a spinner for streaming output

A spinner communicates "unknown duration, nothing to show". Streaming output has
something to show almost immediately. The sequence is:

1. **Submitted** — the person's input is committed and visible, input cleared, send
   control disabled. This happens in under 100ms and is not a model state at all.
2. **Waiting for first token** — a shaped placeholder, not a spinner. Two or three
   skeleton lines at body-text metrics, so the layout does not jump when text
   arrives.
3. **Streaming** — text appears progressively. A stop control replaces send. Auto
   scroll follows the output *only while the person is already at the bottom*; the
   moment they scroll up, stop following and show a "jump to latest" affordance.
   Yanking someone's viewport away from the paragraph they are reading is the single
   most common defect on these surfaces.
4. **Complete** — stop control returns to send; response actions (copy, retry,
   feedback) appear now, not during streaming.

If first token has not arrived by roughly 5 seconds, the placeholder gains a
plain-language line about what is happening — "searching", "reading three sources".
If nothing truthful can be said, say nothing and let the skeleton stand. Never
invent a status.

Cursor or caret animation during streaming is optional and must respect
`prefers-reduced-motion`. Under reduced motion, text still streams — streaming is
information, not decoration — but the caret stops blinking.

## Agent steps and tool calls

The person needs to be able to answer two questions at a glance: *what is it doing
right now*, and *what did it already do*. Everything else is on demand.

- Each step renders as **one line, collapsed by default**: verb, object, outcome.
  "Searched the pricing docs — 4 results." Not "Executing tool: `search_docs`".
  Tool names are implementation; the person needs the intent.
- Expanding shows arguments and raw result. This is for debugging and for trust, and
  it must be there — a surface that only ever shows a summary is asking to be
  believed rather than checked.
- The **currently running** step is visible and distinguishable from completed ones.
  Use `color.ai.step.active` and a determinate signal if a duration is known,
  indeterminate if not. Do not fake a progress bar.
- A **failed** step stays in the list, marked, with its error expandable. Removing
  failed steps rewrites history and destroys the audit value of the list.
- Long runs collapse the middle: first steps, a "12 more steps" affordance, current
  step. Never truncate the current step or the last completed one.

If a run reaches roughly 15 steps without producing output, the surface should say so
plainly and offer to stop. Silent long-running work is where people lose confidence.

## Citations

A citation is a promise that a claim is checkable. Design it as a promise.

- Cite at the **claim**, not at the end of the message. A reference list at the
  bottom makes the person do the mapping work themselves.
- The marker is a real interactive target — at least 24×24 CSS px of hit area even
  when the visible mark is small (WCAG 2.2 target size). Superscript numerals fail
  this constantly; give them padding.
- Hover or focus reveals source title and a snippet. Activation opens the source.
  Never a bare numeral that resolves to nothing.
- If a source cannot be resolved, **show the citation as unresolved** rather than
  dropping it. A missing citation looks like an uncited claim, which is worse.
- Quoted text from a source is visually distinct from generated text. If the product
  ever surfaces verbatim source material, that distinction is doing legal work as
  well as design work.

## Errors versus refusals — these are different states

This is the distinction most teams get wrong, and it is worth being pedantic about.

| | Error | Refusal |
|---|---|---|
| What happened | The system failed | The system worked correctly |
| Tone | Apologetic, operational | Direct, non-judgemental |
| Affordance | Retry | Not retry — retrying is the wrong suggestion |
| Placement | Message-level, `feedback.danger` | Inside the response, `ai.refusal` |
| Persistence | Dismissable | Part of the transcript |

An error is a timeout, a rate limit, a dropped connection. It gets a retry, and the
person's input is preserved so retry actually costs nothing.

A refusal is the model declining. Presenting it in a red error container with a
"Try again" button tells the person the system is broken and invites them to bang on
it. It is not broken. Render a refusal as a normal response with a distinct,
non-alarming treatment, say what cannot be done, and where there is a legitimate
adjacent path, offer that instead. Never both — a refusal with a retry button is a
surface arguing with itself.

Partial failures — streamed output that stops midway — keep the partial text,
mark it clearly as incomplete, and offer continue rather than restart.

## Empty and first-run states

An empty chat surface is a design problem, not a blank canvas. It should answer:
what can this do, what is it connected to, and what does a good request look like.
Three concrete example prompts drawn from the product's actual capability beat any
amount of illustration. Rotate them; do not randomise them on every render, which
makes the surface feel unstable.

## Human review and approval

Any action with a side effect the person did not directly trigger gets a gate.

- State the action in the person's terms, with the specific object and destination.
  "Send this reply to Nadia" — not "Execute action".
- Show what will change and what will not. Reversibility is the key fact and is
  usually missing.
- **Approve and reject are not visually symmetric.** Approve is the primary action;
  reject is available and unstyled-as-danger, because rejecting is normal and safe.
- Never pre-select approve, never auto-approve on timeout, never let approve be the
  focus target on mount where a stray Enter confirms it.
- Batch approvals list every item. A single "approve all 14" with no itemisation is
  consent theatre.

## Latency budgets

These are what the states above are designed around. Measure against them.

| Threshold | Requirement |
|---|---|
| < 100 ms | Input commits. No state needed. |
| < 1 s | Placeholder appears. |
| ~5 s | Placeholder gains a truthful status line. |
| ~15 s | Offer to stop; explain what is still running. |
| ~60 s | Move to a background-run pattern; do not hold the person on a live view. |

## What to check before calling a surface done

- Reduced motion: streaming still streams, decoration stops.
- Keyboard only: send, stop, expand a step, open a citation, approve, reject.
- Screen reader: streamed text is announced in a polite live region, not assertive —
  assertive interrupts on every chunk and is unusable. Steps announce on completion,
  not per frame.
- Arabic: run the surface in RTL. Streaming direction, step chevrons and citation
  markers all have handedness. See the `rtl-bilingual` skill.
- Long output, empty output, refusal, mid-stream failure, and 20-step run — all
  designed, not just the happy path.
