AI
Streaming Code
A code panel that renders while the model is still writing it — caret on the last line, frozen finished lines, regex tinting, copy-what-arrived and auto-scroll the reader can break.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/streaming-code.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "StreamingCode" component: a code panel
whose content is still being written. No Shiki / Prism / highlight.js and no
dangerouslySetInnerHTML — ship a small regex tokenizer so the incremental
strategy can be built into it.
Contract
- forwardRef<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>
minus children and onCopy.
- code: string — everything received so far; the caller appends to it.
- language?: string — "ts" | "tsx" | "js" | "jsx" | "python" | "json" | "bash"
| "css" plus aliases (typescript, py, sh, shell, zsh, scss). Unknown values
render as plain text, never as an error.
- status?: "streaming" | "complete" | "interrupted" (default "streaming").
- filename?: string, showLineNumbers = true, maxHeight: number | string =
"20rem", wrap = true, caret = true, autoScroll = true, copyable = true,
detectFence = true.
- followLabel = "Resume follow", endLabel = "Jump to end",
incompleteLabel = "Stopped — file incomplete".
- onCopy?: (code: string) => void — fires with the exact snapshot written to
the clipboard.
Behavior — tokenizing an append-only file
- scanLine(line, config, state, collect) cuts ONE line into { text, type }
tokens where type is keyword | type | string | number | function | comment |
punct | plain. Re-joining every token.text must reproduce the line exactly:
that invariant is what makes "what is copied is what was received" true.
- The scanner is line-based but NOT line-independent. It takes the state the
previous line left open — "code" | "block-comment" | "template" |
"doc-double" | "doc-single" — and returns the state it leaves open, so a
/* … */ block, a backtick template literal or a Python triple-quoted string
opened forty lines ago still colours the new line correctly.
- Two passes, one scanner. collect=false skips every branch that cannot change
the state (identifiers, numbers, punctuation) and is run over the whole file
each chunk to chain the states; collect=true builds tokens and runs only
inside the memoised line that actually changed.
- Render one <CodeLine> per line, React.memo'd, taking ONLY primitive props
(text, entering state, languageKey, lineNumber, gutter, wrap, active, caret)
and tokenising internally with useMemo. Under append-only growth every line
but the last has byte-identical props, so the shallow compare freezes it: no
re-tokenise, no re-render, and its DOM text nodes — with any selection inside
them — survive to the end of the stream.
- Half-written syntax must look written: a quote with no closer on its line is
tinted as a string to the end of the line (a plain quote cannot span lines,
so the state stays "code"), an unterminated /* opens a comment that keeps
running, an unterminated backtick opens a template.
- Keyword lookup wins over the "followed by ( → function" heuristic, otherwise
if / for / while / switch / catch / return get painted as calls.
- JSON only: a string followed by ":" is a property key, tinted as a type.
- Shell only: $VAR / ${VAR} / $1 is one identifier token.
Behavior — fence adoption
- detectFence strips the Markdown fence a model wraps code in and adopts its
info string as the language: the writer just told you what it is, so the
fence wins over the `language` prop.
- The info string is only final once the opening line's newline lands —
"```ts" can still become "```tsx" — so render nothing until then instead of
flipping the highlighter twice.
- A lone ` or `` at the very end is a closer that has not finished arriving:
hide it, or the last line flickers. Everything after a complete closing
fence is prose about the code, not code — drop it.
Behavior — follow, detach, resume
- Keep the viewport pinned to the bottom while code arrives. Scroll instantly,
never smoothly: a smooth scroll retargeted every chunk never arrives and
fights the reader's own wheel.
- On scroll, measure scrollHeight - scrollTop - clientHeight and treat <= 24px
as pinned. Appending content does NOT fire a scroll event, so this is only
ever measured after a real scroll — the reader's or the component's own.
- Scrolling up detaches: auto-scroll stops immediately and a pill appears over
the bottom of the viewport ("Resume follow" while streaming, "Jump to end"
once settled) carrying "+n", the number of lines written since the reader
broke away. Scrolling back to the bottom re-pins on its own, no click needed.
- Auto-scroll follows GROWTH, not renders: compare the received length with the
previous one and skip when it did not grow, so mounting a finished file opens
it at line 1 instead of yanking the reader to the end.
Behavior — copy, status, cleanup
- Copy snapshots the received text synchronously BEFORE awaiting the clipboard,
so the reader gets exactly what was on screen when they clicked, not the few
hundred characters that landed during the await. While streaming the button's
accessible name says so ("Copy the code received so far").
- navigator.clipboard with a hidden-textarea + execCommand fallback; a failed
copy renders a visible destructive line ("select the code manually"), never a
silent no-op. The 2s "copied" reset timer is cleared on unmount.
- status="interrupted" keeps every character received, drops the caret and adds
one muted line under the panel; nothing is ever cleared, because a stopped
file is still the only file the user has.
- The wrap toggle is real state with aria-pressed; `wrap` is only its initial
value.
Rendering & styling
- Semantic tokens only: bg-card, bg-muted/40, bg-muted, border, text-foreground,
text-muted-foreground, bg-primary/5 for the active line, bg-primary +
text-primary-foreground for the "+n" pill, text-destructive + bg-destructive/10
for the copy error, ring for focus. No hex, no rgb(), no oklch().
- Token colours are the five chart tokens — keyword var(--chart-1), string
var(--chart-2), number var(--chart-3), type var(--chart-4), function
var(--chart-5) — with comments on text-muted-foreground italic and
punctuation muted. Hue carries the token type; those five are the only ramp
guaranteed to clear 3:1 against a card in both schemes.
- Real <pre><code> around flex rows: the semantic pair means copy/paste keeps
the line breaks and assistive tech announces it as code, while the rows still
host a gutter and the active-line tint. Rows get min-h-6 so an empty line
keeps its height and can hold the caret.
- The caret is an EMPTY inline <span> with border-r-2 border-current, never an
inline-block: an atomic inline is a UAX#14 CB, so with wrapping on the line
breaker may drop the caret onto a line of its own. Its keyframes ship in a
React 19 hoisted <style href precedence> and the blink is
motion-reduce:[animation:none] — solid, visible, still correct.
- Accessibility: the code carries aria-busy while arriving and NO live region;
a separate sr-only role="status" region stays silent during the stream and
speaks once when it settles ("34 lines of tsx generated" / the interrupted
label). The viewport is tabbable with role="group" and an aria-label, so a
keyboard user can scroll it and every control has a focus-visible ring.
Customization levers
- TOKEN_CLASS is one map: repoint it at your own editor palette, or collapse
the five hues to two (keyword + string) for a quieter panel.
- Language table: each entry is { keywords, types, lineComment?, blockComment?,
template?, triple?, dollarVars?, keyStrings? } — adding Go, Rust or SQL is
one object, not a new code path.
- Density: text-[13px] / leading-6 / min-h-6 on the row and w-12 on the gutter
are the only spacing knobs; maxHeight sets how much of the file is on screen
("none" removes the internal scroller, which also removes the follow pill).
- Pin slop (24px): raise it if your rows are tall, lower it for a stricter
"only truly at the bottom counts as following".
- Chrome: drop the wrap toggle, the line counter or the language badge from the
header independently; set copyable={false} for a read-only panel, caret={false}
when a sibling already owns the "generating" affordance, autoScroll={false}
for a panel the reader alone controls.
- Fence policy: detectFence={false} when the caller already extracts the code
block and the `language` prop is authoritative.Concepts
- Carried scan state — each line is tokenized with the state the line above left open (block comment, template literal, docstring), so a construct opened forty lines earlier keeps colouring new lines without anything above being touched.
- Frozen lines — every line component takes only primitive props, so under append-only growth React's shallow compare skips all but the last one: finished lines are never re-tokenized, never re-rendered, and any text selection inside them survives the whole stream.
- Follow and detach — the viewport sticks to the bottom until the reader scrolls up, which hands control over immediately; a pill counts the lines written behind their back and hands it back, and scrolling to the bottom re-pins with no click at all.
- Growth-gated auto-scroll — the jump to the bottom is triggered by the received text getting longer, not by a render, so mounting an already-finished file opens it at line 1 instead of yanking the reader to the end.
- Fence adoption — the triple-backtick fence a model wraps code in is markup, not line 1 of the file: it is stripped, and its info string becomes the language because the writer just declared it — but only once the opening line is complete, since "ts" can still become "tsx".
- Copy snapshot — the clipboard write captures the received text synchronously before awaiting, so a partial copy is exactly the prefix that was on screen at click time, never a longer one that arrived during the await.
Streaming Table
A table that renders while its rows are still arriving — the header locks the column geometry on frame one, cells still in flight shimmer in place, and the footer count settles into a total.
Model Compare
A side-by-side arena for two answers to one prompt — per-side streaming states, derived speed, optional blind names, and a vote that fires exactly once.