Skip to content

Scenario

Select a scenario and click Run to start.

SSE Parsing

Server-Sent Events use a simple text-based wire format: each event is a block of field lines (event:, data:, id:) separated by a blank line. The parser maintains an internal buffer to handle chunks that split mid-event across network boundaries. This is cheaper than WebSockets when you only need server-to-client streaming, and it works over standard HTTP with automatic reconnection built into the browser EventSource API.

event: text_delta
data: {"content":"Hello"}

event: done
data: {}

State Machine

The agent run state is modeled as a discriminated union on status: idle, running, awaiting_approval, completed, error, or cancelled. Each variant carries only the data relevant to that state, so impossible states are unrepresentable. A pure reducer handles all transitions, making every state change testable without React. This replaces the brittle pattern of multiple boolean flags (isLoading, isError, isWaiting) that can drift out of sync.

type AgentRunState =
  | { status: "idle" }
  | { status: "running"; steps: AgentStep[] }
  | { status: "awaiting_approval"; steps: AgentStep[]; pendingAction: ... }
  | { status: "completed"; steps: AgentStep[] }
  | { status: "error"; steps: AgentStep[]; error: string }
  | { status: "cancelled"; steps: AgentStep[] }

Streaming Text

Each text_delta token from the stream is tiny, often just a word or punctuation. Calling setState on every token would trigger a React render per token, which tanks performance. Instead, tokens accumulate in a ref buffer and flush to state via requestAnimationFrame, batching many tokens into a single render. This keeps the UI at 60fps even during fast streaming.

Tool Calls

Agent tool calls follow a start/result lifecycle: tool_use_start arrives with the tool name and input, then tool_result arrives later with the output. The timeline renders each tool call as an expandable card showing the function name, a status indicator (spinner while running, checkmark when done, X on error), and collapsible input/output JSON. This makes the agent's reasoning transparent and debuggable.

Approval Gates

Some actions are too dangerous for autonomous execution. The approval gate pattern pauses the stream when the agent requests human approval, renders the proposed action with Approve/Deny buttons, and only resumes streaming after explicit human confirmation. The stream itself pauses (the ReadableStream stops yielding chunks) rather than buffering events, so there's no race condition between approval and execution.

Auto-scroll

The timeline container auto-scrolls to keep new content visible, but only when the user is already near the bottom (within 100px). If the user has scrolled up to review earlier content, auto-scroll pauses so their position isn't disrupted. A passive scroll listener avoids blocking the main thread. This is the same pattern used by every chat interface, but getting the threshold right matters for UX.

Error Handling

Stream errors don't always mean total failure. The error_recovery scenario demonstrates a stream that interrupts mid-response, emits an error event, then resumes with recovery text. The state machine transitions to error status and preserves all partial steps, so the user never loses content they've already seen. The UI renders a red error banner inline in the timeline rather than replacing everything with an error page.

Anti-patterns

Common mistakes in agent UIs: polling an API instead of using streaming (wastes bandwidth, adds latency). Using dangerouslySetInnerHTML for markdown (XSS risk). No cancel button (user is trapped waiting). Replacing the entire UI on error instead of preserving partial content. Using multiple boolean flags instead of a state machine (leads to impossible states like isLoading && isError). Re-rendering on every token instead of batching (jank at 200+ tokens/second).