AI

Stream Inspector

A developer feed of the raw frames of one streamed model response — per-frame offsets and gaps, a TTFT marker line, header stats, stall marking with jump-to-next, delta folding, tail-follow with break-away and a pretty/raw toggle per row.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import {
  Activity,
  ArrowDownToLine,
  Brain,
  Braces,
  Check,
  ChevronRight,
  CircleAlert,
  Copy,
  CopyX,
  Flag,

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/stream-inspector.json

Prompt

Build a React + TypeScript + Tailwind "StreamInspector" component with zod and
lucide-react: a developer-facing feed of the RAW frames of ONE streamed model
response, the panel you open when a stream "felt slow" and you need to know
which frame is to blame.

Contract
- A zod schema (`streamInspectorEventSchema` in a sibling contract file) is the
  single source of truth for one frame: { id, kind, at, payload?: unknown,
  gapMs?, raw?, text?, tokens?, label?, timeLabel? }.
- `kind` is the normalised PROTOCOL vocabulary, nine values: "start" |
  "text-delta" | "reasoning-delta" | "tool-call" | "tool-args-delta" | "ping" |
  "usage" | "finish" | "error". Map your provider onto it once, in an adapter,
  and every downstream reading is comparable across providers. A keep-alive ping
  is a first-class row — during a long tool call it is the only evidence the
  socket is alive.
- `at` is a MONOTONIC OFFSET in ms from the moment the request was sent, never a
  wall clock. Offsets are what you compare, they survive being pasted into a bug
  report, and they render identically on the server and in the browser. A
  wall-clock reading is allowed only as a PRE-FORMATTED `timeLabel`; the
  component never calls Date.now() or toLocaleTimeString(), so it cannot hydrate
  differently from what the server rendered.
- `gapMs` is optional and derived when absent: `at - previous.at`, and `at`
  itself for the first frame (that one is the wait before the stream said
  anything). Out-of-order or non-finite frames clamp to 0 rather than render a
  negative gap.
- `payload` is `unknown` on purpose — a provider's frame shape is the provider's
  business. Never ask the caller to pre-stringify it.
- A separate envelope status — "loading" | "empty" | "error" | "ready" — is the
  panel's own render state. `status="error"` means the TRANSPORT failed (the
  connection dropped, the proxy answered text/html); a provider error that
  arrived as a frame is an ordinary `kind: "error"` row inside a `ready` feed,
  and the two must read differently. `empty` is not `loading`: the stream opened
  and closed without a single frame, which is its own bug.
- Props: events: Event[]; status; follow? + onFollowChange? (hybrid
  controlled/uncontrolled, default on); defaultMergeDeltas? (true); defaultView?
  ("pretty"); defaultTimeMode? ("offset"); stallMs? (400); maxRows? (200);
  maxHeight? (420); onStatsChange?(stats); onRetry?; emptyState?; errorMessage?;
  label? ("Stream events"); skeletonRows? (6); formatOffset?; className plus the
  rest of the div props, ref forwarded to the root.
- Every numeric prop is clamped (stallMs >= 1, maxRows >= 20, maxHeight >= 160):
  a stallMs of 0 would mark every row as a stall, a maxRows of 0 would render an
  empty feed that looks like a bug in the caller's transport.

Behavior
- HEADER STATS, all derived from the same events array, never passed in:
  TTFT = the offset of the first TOKEN-BEARING frame (text-delta,
  reasoning-delta, tool-call or tool-args-delta) — a response that opens with a
  tool call has a real TTFT even though it never writes a character.
  tokens/sec = tokens / (last token offset − TTFT), measured AFTER the first
  token on purpose: folding TTFT into throughput turns one slow queue into a
  permanently slow-looking model. It is null (rendered "—") when no frame
  reported tokens or the window is zero — never a fabricated number. Tokens
  reported by a `usage` frame are deliberately NOT summed: adding a running
  total to the deltas that produced it doubles the reading.
  Also: frame count (plus the folded row count), total duration, wire bytes, and
  a stall counter that doubles as a control.
- A TTFT MARKER LINE is drawn as its own row immediately above the first
  token-bearing row: everything above it is connection overhead, everything
  below it is output. It is a line across the timeline, not a badge on a row.
- STALLS: any gap >= stallMs is marked on its row (left edge + emphasised gap
  figure), counted in the header, and the counter is a BUTTON that walks the
  stalls one by one, centring each in the viewport and showing "3/7". If the
  next stall sits above the rendered window, the window budget is lifted first —
  a jump to a row that is not in the DOM would silently do nothing.
- DELTA FOLDING: consecutive runs of the three fragment kinds (text-delta,
  reasoning-delta, tool-args-delta) fold into one row carrying the concatenated
  text and a ×N badge; 400 rows that each say " the" bury the six rows that
  matter. The rule that makes folding safe: A STALL BREAKS THE RUN, so a gap you
  were meant to see can never be swallowed by the fold. Pings never fold — two
  keep-alives in a row are two separate facts about the connection.
- TAIL FOLLOW WITH BREAK-AWAY: while following, a layout effect pins scrollTop
  to scrollHeight after every content change, so the newest row is in place on
  the frame it appears instead of flashing half-clipped for one paint. Scrolling
  away from the bottom (beyond an 8px epsilon) breaks away; scrolling back
  resumes; the panel's own programmatic pins land at distance 0 and can only
  ever resume. While detached, a pill counts the frames you have not seen and
  jumps back.
- THE WINDOW FREEZES WHILE DETACHED. Only the last `maxRows` rows are kept in
  the DOM, but the top of that window is frozen at the moment you took over —
  otherwise rows would be unmounted above your cursor and the content under it
  would jump every time a frame landed. The banner states exactly how many
  earlier rows are not in the DOM and offers to render them all; a budget you
  can see is a budget you can trust.
- PRETTY / RAW PER ROW. The toolbar sets the default view and CLEARS the
  per-row overrides when it changes (a global switch that visibly does nothing
  on the rows you touched earlier is worse than no switch), and every open row
  can override it. Raw shows the exact `raw` frame; when a frame has none, the
  panel reconstructs `event: <kind>\ndata: <json>` and SAYS SO — a
  reconstruction passed off as the wire truth costs somebody an afternoon. A
  folded run prints at most 40 frames in raw and states how many it withheld.
  Copy writes exactly what the row is showing.
- Disclosure is a real <button aria-expanded> whose panel is UNMOUNTED when
  closed — a zero-height collapse would leave the view toggles and copy buttons
  of 200 rows reachable by Tab — and `aria-controls` is only set while open so
  it never points at an id that is not in the document. Payload bodies are only
  serialised for OPEN rows: stringifying 200 collapsed rows on every frame that
  lands turns a live stream into a slideshow.
- Arrow Up / Down / Home / End move focus between row headers when focus is
  already on one; 200 rows of Tab is not a keyboard path. Focusing a row scrolls
  it into view, which breaks the follow — correct, you took over.
- Screen readers hear ONE throttled status line ("Tail following paused, 42 new
  events below"), flushed at most once per ~1.5 s. A 60 frame/second stream in a
  live region would flood assistive tech into uselessness.
- JSON is serialised with an ancestor STACK, not a visited set, so an object
  used twice as a sibling still prints twice and only a true cycle becomes
  "[Circular]"; BigInt and functions get placeholders instead of throwing.
- All timers, the throttle and the copy reset are cleared on unmount; the panel
  starts no interval of its own — the stream belongs to the consumer.

Rendering & styling
- Semantic tokens only: bg-card / border / text-muted-foreground for chrome,
  bg-primary/10 + border-primary/50 for pressed toolbar chips and the TTFT line,
  bg-primary + text-primary-foreground for the jump-to-latest pill,
  text-destructive + bg-destructive/5 + a destructive left edge for error frames
  and stall figures, bg-muted/40 for payload blocks. No hard-coded colours.
- Kind chips separate by BORDER STYLE, TINT AND WEIGHT rather than a hue ramp
  (dashed for ping, filled for tool-call, destructive for error) plus a distinct
  icon each, so a monochrome theme and a colour-blind reader still read the
  feed.
- Rows are monospace with tabular-nums in the offset and gap columns so the
  numbers form a scannable column; the summary uses `wrap-anywhere` with
  `min-w-0` (NOT break-words — only overflow-wrap:anywhere lowers the
  min-content width) so a 200-character JSON fragment cannot widen the panel.
- Invisible characters are made visible: a newline renders as ↵, a tab as ⇥, a
  whitespace-only delta as ␣ glyphs. An empty-looking row is how you lose the
  delta that mattered.
- Only decorative motion is used (colour transitions, the chevron rotation, the
  skeleton pulse) and all of it is motion-reduce-guarded; scrolling is
  instant scrollTop arithmetic, never smooth scrollIntoView, which would also
  scroll every ancestor and yank the page the panel is embedded in.
- cn() merges the consumer's className into the root, which also spreads the
  remaining div props and forwards its ref.

Customization levers
- Thresholds: `stallMs` is the whole stall story — raise it to 800 for a mobile
  network, drop it to 150 when you are hunting micro-jitter; it re-marks rows,
  re-counts the header and re-decides where folds break, in that order.
- Density budget: `maxRows` (DOM rows) and `maxHeight` (viewport) are the two
  knobs for embedding — 120/300 in a side drawer, 500/640 in a full-page debug
  view. Both stay announced; neither ever truncates silently.
- Defaults for the three switches: `defaultMergeDeltas` (off for protocol-level
  fidelity, on for reading), `defaultView` ("raw" in a gateway inspector,
  "pretty" in a playground), `defaultTimeMode` ("clock" only when your frames
  carry `timeLabel`).
- Ownership: pass `follow` + `onFollowChange` to drive the tail from your own
  toolbar or to force-follow while a request is in flight; pass `onStatsChange`
  to feed TTFT / tok-s into a gauge elsewhere in your app instead of duplicating
  the maths.
- Units: `formatOffset` swaps the wording for both the offset and the gap
  columns (µs for a local model, "frame 12" for a video stream, percentages of
  the total).
- Chrome: `label` names the region, `emptyState` replaces the empty body,
  `errorMessage` + `onRetry` own the envelope failure, `skeletonRows` matches
  the loading block to the height of the panel it will become.
- Kinds are data, not UI: add a provider kind by extending the contract enum and
  giving it a chip class, an icon and a label — the folding, stall and TTFT
  rules are keyed off the two exported kind lists, so a new kind opts into them
  by joining a list rather than by touching the render.

Concepts

  • Offsets, not timestamps — every row is placed on a monotonic offset from the moment the request was sent, and the number you actually read is the GAP to the previous frame. A wall clock tells you when something happened; a gap column tells you where the stream stopped, which is the only question this panel exists to answer.
  • TTFT is a line, not a badge — the marker is drawn across the feed above the first token-bearing frame, so the connection overhead above it and the output below it are two visually separate regions. A tool-only response still crosses that line: the first tool-call frame is a token.
  • A stall breaks the fold — folding runs of deltas is what makes 1,200 frames readable, but a fold that could swallow a 900 ms gap would hide the one row you opened the panel for. Making the stall a hard boundary is what lets folding stay a reading preference instead of a fidelity trade.
  • Follow, break away, freeze — the tail pins itself until you scroll up; from that moment the window's TOP is frozen too, so rows are never unmounted above your cursor and the content under it never jumps. Coming back to the bottom resumes both, and the panel's own pins land at distance zero so they can only resume, never break away.
  • A budget you can see — only the last N rows live in the DOM, and the banner says exactly how many do not, with a control to render them all. The same discipline governs the raw view of a folded run: 40 frames, then the count of the rest. A silent cap makes you debug the wrong thing.
  • Reconstruction is labelled — a frame without its raw bytes still gets a raw view, rebuilt from the decoded payload and marked as rebuilt. Presenting a guess as the wire truth is how a missing comma gets debugged for an afternoon.
  • One throttled announcement — a live region on a 60 frame/second feed is a denial-of-service attack on a screen reader. The panel keeps a single status line and flushes it at most once per window, saying only what changed: following paused, and how many frames you have not seen.

On This Page