Lesson 162·M19 AI Application Engineering
Streaming UI
OpenLearning Objectives
By the end of this lesson you should be able to:
- Explain the streaming UI's job: make the stream felt — TTFT visible, progress shown, states clear (L145)
- Render streaming markdown: incremental, without breaking the formatting
- Show tool progress in the UI (L164): pending → executed, as part of the conversation
- Implement cancellation (L145): a Stop that aborts the upstream call
- Handle the end states: done, length (L149), errored (L168) — as designed UX
One-Line Definition
The streaming UI is the product layer that renders the model's stream as it arrives — incremental markdown, visible tool progress, real cancellation, and clear states — turning the parts-and-states of L161 into the felt-quality of a conversation.
The one-sentence interview answer: "The streaming UI is where the stream becomes the product: text parts render as incremental markdown (L161), tool-call parts show pending → executed progress (L164), a Stop button aborts the upstream call (L145), and the states — streaming, tool-running, done, errored — are always explicit. The user never waits in silence, never wonders what's happening, and never sees a truncated answer treated as complete (L149)."
Mental Model
Think of the streaming UI as watching a skilled person think out loud — instead of a messenger who disappears and returns with the whole answer.
Non-streaming UI Streaming UI (this lesson)
┌────────────────────┐ ┌──────────────────────────┐
│ [spinner] … 3s … │ │ "A closure is a function │ ← streaming
│ [spinner] … 8s … │ │ that remembers…" │
│ [the whole answer] │ │ │
└────────────────────┘ │ [using get_stock…] ✓ │ ← tool progress (L164)
│ "$212.40 — up 1.2%." │
feels broken after 2s │ [Stop] │ ← cancellation (L145)
└──────────────────────────┘
feels like a conversation
The mental model is progress replaces waiting: every moment of the stream is visible, every state is explained, and the user is never left with a spinner and a prayer.
Visual Flow — The Streaming UI State Machine
user sends
│
▼
┌─────────────────────────────┐
│ streaming │ ← text parts render as markdown (L161)
│ │ │
│ ▼ (tool-call part) │
│ tool-running │ ← "using get_stock…" (L164)
│ │ │
│ ▼ (tool-result part) │
│ streaming (continues) │ ← the answer resumes
│ │ │
│ ▼ (finish part) │
├─────────────────────────────┤
│ done stop │ → the answer stands
│ done length │ → "continued?" (L149) — never silent
│ errored error │ → recovery + partial kept (L168)
└─────────────────────────────┘
any state → [Stop] aborts the upstream call (L145)
The state machine is L161's, made visible: every state has a rendering, and the two end states that need honesty — length and error — are designed UX, not surprises.
How It Works — The Streaming UI's Four Jobs
- Render incremental markdown. Text parts (L161) arrive as deltas; the UI accumulates them and renders markdown. The tricky part is incremental correctness: the markdown is incomplete mid-stream — a code fence is half-open, a list is mid-item. The renderer must handle partial markdown without flashing broken formatting (a common approach: render the raw text progressively and parse only at safe points, or accept minor transient formatting flicker — the senior trade-off).
- Show tool progress (L164). A tool-call part renders as "using get_stock…", and the tool-result part flips it to the result — pending → executed, as part of the conversation. The user sees why the model paused.
- Make cancellation real (L145). The Stop button calls
abort()on the client; the server aborts the upstream provider call. Cancellation that only hides the text (while the provider keeps billing tokens) is a fake Stop. - Handle the end states honestly.
finish: stop→ done.finish: length(L149) → the answer was truncated — render "continued?" rather than a normal-looking half-answer.error(L168) → recovery with the partial kept.
Note
The honesty rule of the streaming UI. The two states that decide whether the UI is a product or a demo are length (truncation) and error (failure). A demo treats both as "done"; a product tells the truth — "this was cut off, continue?" and "this failed, retry or keep the partial." That honesty is the felt-quality people can't name but absolutely feel.
Real Project Usage
- Chat products. Incremental markdown, tool progress, Stop — the ChatGPT/Claude/Gemini feel, built from L161's parts.
- Copilots and assistants. The streaming UI shows the tool loop (L164) as visible steps — the user watches the assistant check, run, and answer.
- Agents (L200). Tool-call parts become agent steps; the UI renders the sequence — a primitive agent observability (L213) that doubles as user trust.
- Structured output (L163).
streamObjectparts render a typed result as it forms — a form that fills in, validated live by the schema (L143). - Any human-facing AI output. Summaries, code, reports — rendered as they're produced, with a Stop that works.
The through-line: the streaming UI is where AI products earn their feel — progress instead of waiting, explanation instead of mystery, and honesty about limits.
Interview Explanation
Say it in four moves:
- The frame. "The streaming UI renders the stream as it arrives — text, tool progress, cancellation, and honest end states — turning L161's parts into the product's feel."
- The rendering. "Text parts render as incremental markdown; tool-call parts show pending → executed (L164); a Stop aborts the upstream call (L145)."
- The honesty. "The end states tell the truth:
lengthrenders 'continued?' not a silent cut (L149), anderrorshows recovery with the partial kept (L168)." - The why. "Progress replaces waiting — the user never stares at a spinner, never wonders what's happening, and never trusts a truncated answer as complete."
Senior-Level Insights
- The streaming UI is the product layer of latency (L145, L151). TTFT is engineering; the streaming UI is what the user feels of it. A 300ms TTFT rendered as a spinner feels worse than a 1s TTFT that streams. The UI is a latency lever.
- Tool progress is trust (L164, L213). Showing "checking the database…" explains the pause; hiding it makes the model feel unpredictable. The pending → executed states are the seed of agent observability (L213) and user confidence.
- Cancellation is a cost control (L145, L150). A Stop that aborts upstream stops token billing mid-stream (L149) — it's not just UX, it's cost. A fake Stop is a hidden cost leak.
- Incremental markdown is a rendering trade (L151). Perfect markdown needs the full text; streaming needs incremental. The senior answer names the trade — progressive rendering with transient formatting flicker, or plain-text-until-stable — and picks per product.
- The state machine is the testable core (L341). A UI that's a pure function of the stream's parts is testable: feed parts, assert states. The streaming UI's quality is an eval (L343), not a vibe.
Common Mistakes
- Rendering when done. Waiting for the whole stream (L145) — the spinner that kills the feel, and the demo's tell.
- Fake Stop. Hiding the text but not aborting the upstream call (L145) — tokens keep billing (L150), and the user's "stop" was a lie.
- Tool calls invisible. No pending state (L164) — the pause is unexplained, the model feels random.
lengthtreated as done. The truncated answer, normal-looking (L149) — the honesty rule, broken.- Markdown flicker, unfixed. Mid-stream code fences flashing broken — a rendering trade handled badly (or ignored).
- No errored state. A mid-stream failure as a blank UI (L168) — the stream died and the user got nothing.
Best Practices
- Stream everything a human reads (L145) — render the parts as they arrive, never wait for the whole.
- Model the states explicitly (L161): streaming, tool-running, done, errored — each with a rendering.
- Show tool progress (L164): "using X…" → the result, as conversation.
- Make Stop real (L145): abort the upstream call, and surface it as a cost control (L150).
- Handle
lengthhonestly (L149): "continued?" — never a silent cut. - Handle
errorwith recovery (L168): keep the partial, offer retry. - Decide the markdown trade deliberately — progressive rendering, accepted flicker, or plain-text-until-stable.
Interview Questions
Q: What makes a streaming UI good?
A: Progress replaces waiting. Text renders incrementally (L161), tool calls show pending → executed (L164), Stop actually aborts the upstream call (L145), and the end states tell the truth —
lengthrenders "continued?" (L149),errorkeeps the partial and offers recovery (L168). The user never stares at a spinner and never trusts a truncated answer as complete.
Q: How do you render markdown that's still streaming?
A: It's a trade (L151). The markdown is incomplete mid-stream — a code fence is half-open. I either render progressively and accept transient formatting flicker, or render plain text until a safe parse point. The senior answer names the trade and picks deliberately per product — perfect markdown needs the full text; streaming needs incremental.
Q: Why is a real Stop button important?
A: Two reasons. UX — the user must be able to stop a generation they don't want. And cost (L150) — a Stop that only hides the text keeps billing tokens mid-stream (L149). A fake Stop is a hidden cost leak; a real one aborts the upstream call.
Q: How do the states make the UI honest?
A: Every state has a rendering and tells the truth. Streaming shows the answer forming. Tool-running shows why the model paused (L164). Done means done.
lengthmeans "this was cut off — continue?" (L149). Error means "this failed — keep the partial, retry" (L168). A demo treatslengthanderroras done; a product tells the truth.
Follow-Up Questions
- How does the streaming UI compose with structured output (L163)?
- How do agent steps render from tool-call parts (L200, L213)?
- What's the markdown trade, and how do you decide (L151)?
- How does cancellation interact with the token budget (L145, L149)?
- How do you test the streaming UI's states (L341)?
Comparison Table — Spinner vs Streaming UI
| Spinner-then-render | Streaming UI (this lesson) | |
|---|---|---|
| Waiting | invisible | progress (L145) |
| Tool calls | invisible | pending → executed (L164) |
| Stop | none / fake | real abort (L145) |
| Truncation | silent | "continued?" (L149) |
| Failure | blank | recovery + partial (L168) |
| The feel | a loader | a conversation |
The senior read: the table is the product spec — the streaming UI is the difference between "the AI is loading" and "the AI is thinking, with me watching."
Code Example — The Streaming UI, States and All
// The streaming UI: parts, states, Stop, and honest end states.
import { useChat } from 'ai/react';
export function Chat() {
const {
messages, input, handleInputChange, handleSubmit,
isLoading, stop, error, // ← stop (L145) · error state (L168)
} = useChat();
// The markdown trade (L151): render incrementally; the message
// body is the accumulated deltas, parsed at safe points.
return (
<div className="chat">
{messages.map((m) => (
<MessageBubble key={m.id} message={m} />
))}
{/* THE STATES — every one has a rendering (L161) */}
{isLoading && <StreamingIndicator />} {/* streaming / tool-running */}
{error && ( {/* errored — honest (L168) */}
<div className="error">
The stream failed. <button onClick={retry}>Retry</button>
<button onClick={clearError}>Keep partial</button>
</div>
)}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
{isLoading ? (
<button type="button" onClick={stop}>Stop</button> {/* real abort (L145) */}
) : (
<button type="submit">Send</button>
)}
</form>
</div>
);
}
What the reader must SEE — the states are the design:
isLoading → StreamingIndicator (text streaming / tool-running)
error → recovery, partial kept (L168)
Stop → real abort of the upstream call (L145)
length → handled in the message renderer as "continued?" (L149)
The UI is a state machine with a rendering for every state.
Tip
The error and stop lines are the ones that make it a product: an errored state with recovery, and a Stop that really stops. Everything else is rendering; those two are honesty and control — the felt-quality of AI UIs.
Performance Notes
- The streaming UI is the felt layer of TTFT (L151). Rendering deltas as they arrive makes the first token feel like the start of the answer — the UI is a latency lever, not just a renderer.
- Incremental markdown costs render passes (L151). Re-parsing the growing text each delta can be hot; throttle parsing or render plain text between safe points.
- Real Stop saves tokens (L150). Aborting upstream stops billing mid-stream (L149) — cancellation is a cost control with a UI face.
- Tool progress covers execution latency (L164, L151). The pending state turns a tool round trip into visible progress instead of dead air.
Debugging Scenarios
| Symptom | Likely cause | First thing to check |
|---|---|---|
| Text appears all at once | Streaming not wired; UI waits for full (L145) | Render parts as they arrive |
| Stop does nothing | Cancellation not aborting upstream (L145) | Wire stop() to the abort path |
| Tool pause unexplained | No tool-pending state (L164) | Render the pending → executed states |
| "Continued?" never shows | length not handled (L149) | Handle the finish reason in the renderer |
| Blank on failure | No errored state (L168) | Add recovery + partial-keep |
Quick Revision Notes
- The streaming UI = progress replaces waiting — L161's parts, made visible.
- Render incremental markdown (a deliberate trade, L151); show tool progress (L164).
- Real Stop (L145) aborts upstream — a cost control (L150), not just UX.
- Honest end states:
length→ "continued?" (L149);error→ recovery + partial (L168). - The UI is a state machine with a rendering for every state (L161).
- The streaming UI is the product layer of latency (L145, L151).
Cheat Sheet
STREAMING UI = the stream, made felt (L161 → product)
FOUR JOBS
render incremental markdown (the trade, L151)
show tool progress pending → executed (L164)
make Stop real abort upstream (L145) — a cost control (L150)
handle end states honestly length → "continued?" (L149) · error → recover (L168)
THE STATES (every one has a rendering)
streaming the answer forming
tool-running "using get_stock…" (L164)
done the answer stands
errored recovery + partial kept (L168)
THE MARKDOWN TRADE (L151)
progressive + transient flicker OR plain text until a safe point
name it, pick deliberately
RULES
never render when done — stream it (L145)
never fake the Stop (L145, L150)
never treat length or error as "done" (L149, L168)
the state machine is testable (L341)
INTERVIEW, 4 MOVES
1 frame "progress replaces waiting"
2 rendering "incremental markdown + tool progress (L164)"
3 honesty "length → 'continued?' · error → recover (L149, L168)"
4 control "real Stop — a cost control (L145, L150)"Key Takeaways
Recap
- The streaming UI is the product layer of the stream — L161's parts and states, made felt
- It renders incremental markdown (a deliberate trade, L151) and tool progress (pending → executed, L164)
- Stop is real (L145) — it aborts the upstream call, which makes it a cost control (L150), not just UX
- The end states are honest:
lengthrenders "continued?" (L149),errorkeeps the partial and offers recovery (L168) — a demo treats both as done - The UI is a state machine with a rendering for every state (L161) — and that machine is testable (L341)
- Progress replaces waiting — the user never stares at a spinner, never wonders what's happening, and never trusts a truncated answer as complete
Check your understanding
Answer each one out loud. Then rate yourself honestly.
0/8What are the streaming UI's four jobs?
What is the markdown trade, and how do you decide (L151)?
How do tool calls render as progress (L164)?
Why is a real Stop a cost control (L145, L150)?
What does length render, and why (L149)?
What does the errored state render, and why (L168)?
How is the streaming UI the "product layer of latency" (L145)?
Why is the state machine testable (L341)?
A Closing Note — The Feel of the Product
You now hold the layer that makes AI products feel alive: incremental markdown, tool progress, a real Stop, and honest end states. It's the difference between "the AI is loading" and "the AI is thinking, with me watching" — and it's the layer the next lessons slot around: structured generation in the stream (L163), the full tool loop (L164), and the state model that holds it all (L165).