Components/CodeStream

CodeStream

A code block with a filename header that streams code word by word as it arrives, or reveals it line by line, with copy and syntax highlighting built in.

VerifiedSince 0.3.9

Usage

Streaming

Pass the code received so far as code and set streaming: the component releases it word by word at a steady pace however unevenly it arrives. Each word comes in out of a light blur inside its own syntax colour, and the Tuff caret rides the end of the line being written. Setting streaming at all — true or false — is what selects this mode; revealedLines or diff take precedence over it. TxStreamCode is the same component under the streaming family's name.

Loading demo...

Line-by-Line Reveal

The host advances revealedLines; the component owns the transition and the caret. The cadence (400ms before the first line, 240ms per line, a 3200ms hold) stays in the demo layer.

Loading demo...

Full Listing

Omit revealedLines and the whole listing shows, with no caret drawn.

Full listing

The settled state: filename, language label, copy, and highlighting.

Loading demo...

Unified Diff

Passing diff switches from the listing to a diff: removed rows take a soft red tint with a hatched gutter marker, added rows a soft green tint with a solid one, and the header gains +N −N on the right. The hatching is not decoration — those two tints are exactly the pairing a red/green-blind reader cannot separate, so the marker's shape has to carry the distinction on its own.

A removed row and the added row replacing it share a gutter number (each is line 4 of its own revision), which is why numbers are given per row rather than counted by the component.

Every row, removed ones included, goes to Shiki together. The text is briefly not valid source — two versions of one statement in a row — but a highlighter recovers per line, whereas highlighting each row alone loses all surrounding context and mis-colours anything spanning lines.

The copy button always yields code, never the diff: a diff with its markers stripped is neither revision, so the host states which text is copyable.

Unified diff

One replaced line plus one addition; the gutter number repeats at the replacement.

Loading demo...

Best Practices

  • For code arriving from a model, pass it whole as code with streaming and let the component pace it. Use revealedLines when the host owns the cadence, such as a scripted replay or a log that advances line by line.
  • In streaming mode the box grows with its lines; set minHeight to the finished height when the page below must not move, or reserve when complete code plays back.
  • When the language is uncertain, prefer omitting lang: plain text is always correct, and a wrong id only costs a wasted highlight pass.
  • Pair long listings with an outer scroll container. The code area scrolls horizontally on its own, but its height follows content.
  • Use TxCodeBlock when a fence should match TxStreamMarkdown; use this component when you need a filename header, a gutter, and a line-by-line reveal.
  • In non-English UIs, override copyLabel and copiedLabel together — changing only one leaves mixed-language copy.

API Reference

Props

PropTypeDefaultDescription
codestring—The source. Required.
langstring''Shiki language id. Empty renders unhighlighted.
filenamestring—Header filename, in the mono face.
langLabelstring—Language label beside the filename, e.g. TypeScript.
revealedLinesnumber—How many lines are revealed. Omit or pass -1 for all.
streamingboolean—Streams code word by word while true. Setting it at all selects streaming mode unless revealedLines or diff is set; false shows the code at once and lets replay() play it back.
reveal'aurora' | 'hue' | 'blur' | 'languid' | 'none''aurora'Streaming mode: how a word enters. Code keeps its syntax colour throughout, so aurora and blur look alike here, and hue is a plain fade.
wordMsnumber24Streaming mode: release one word every wordMs.
maxLagMsnumber600Streaming mode: no word shows later than this after it arrived.
drainMsnumber320Streaming mode: once streaming turns false, release the rest within this.
pauseMsnumber400Streaming mode: report paused after this long without a new word.
reservebooleanfalseStreaming mode: hold the full height while complete code plays back. Ignored while streaming.
pacedbooleantrueStreaming mode: false shows each word the moment it arrives, still with its entrance. TxStreamElement paces its parts itself and passes false.
appearbooleanfalseStreaming mode: code present at mount enters too, instead of showing as it is. Read at mount.
caretbooleantrueThe caret: a still marker after the last revealed line in revealedLines mode, the Tuff caret while streaming mode is live. Switched off, the streaming caret goes at once; a stream that ends retracts it.
lineNumbersbooleantrueShows the line-number gutter.
theme'light' | 'dark' | 'auto''auto'Highlight theme; 'auto' follows the document root.
copyablebooleantrueRenders the copy button.
copyLabelstring'Copy'Copy button text.
copiedLabelstring'Copied'Text shown after a successful copy.
minHeightnumber | string—Floor for the code area. Defaults to the full listing's height.
diffCodeDiffRow[]—Unified diff rows. When present these render instead of code, and the header gains an added/removed tally.

CodeDiffRow

FieldTypeDescription
contentstringThe row's text, without any + / - marker — the gutter carries that
kind'context' | 'added' | 'removed'Defaults to 'context'
numbernumberGutter number. A removed row and the added row replacing it normally share one, which is why it is given per row rather than counted; omit to leave that gutter blank

Events

EventPayloadDescription
copy(code: string)Forwarded from TxCopyButton on a successful copy.
complete—Emitted once when revealedLines reaches the last line. Not emitted if it starts complete, nor in streaming mode (use done).
state-change(state: StreamState)Streaming mode: the stream moved to another state.
done—Streaming mode: once per play-through, when the last word is shown and the source has finished.

Slots

NameScopeDescription
header—Replaces the filename and language label pair.
actions—Inserts custom controls before the copy button.
caret{ state }Streaming mode: replaces the Tuff caret. Rendered only while the stream is live.

Exposed Methods

NameTypeDescription
stateStreamStateStreaming mode: where the stream is; idle in the other modes.
replay() => voidStreaming mode: plays the code again from the first word.
skip() => voidStreaming mode: shows everything now, without entrances.

Highlighting and Theming

Highlighting runs through the repository's existing shiki runtime — the same lazy singleton TxCodeBlock uses — as a pure async enhancement: the plain-text rendering is always correct, and colour arrives when it arrives. With no lang, nothing is highlighted and shiki is never loaded.

theme defaults to 'auto' and follows the document root's data-theme or .dark. Upstream is a single-theme demo; hardcoding light would render dark-on-light inside a dark host.

The component splits shiki's output back into lines and pairs each with a number and the reveal animation. It checks the line count before splitting: if the highlighted result does not have the same number of lines as the source, the whole block falls back to plain text rather than shifting every number by one.

Overview

  • revealedLines is clamped to [0, total lines]; out-of-range values are not an error.
  • Streaming mode keeps pace the way TxStreamText does. Words go out one every wordMs, faster when a burst would otherwise trail the source by more than maxLagMs, and whatever is left drains within drainMs once the source stops. A word carries the whitespace before it, so a line break and its indentation arrive with the line's first word, and the caret never waits alone on an empty line. States: idle → streaming → paused → draining → done; a rewrite re-releases from the first changed word.
  • Colour while streaming. Growing code is re-highlighted on a 120ms trailing timer, as TxCodeBlock does for an open fence. Each shown line takes its markup from the last pass: cut to the shown length, or with the newest characters plain until the next pass colours them — inside their entrance, where the change does not read.
  • Entrances survive re-highlighting. A line's markup is replaced whenever its text or colour changes; the entering characters are re-wrapped after every patch with a negative delay, so an entrance resumes where the patch cut it. Once done, the wrappers fold back into plain markup.
  • The line being written is one element. A new line moves the finished one out into a new row, so the caret inside it never remounts and keeps its orbit.
  • In revealedLines mode the caret appears only while revealing (0 < revealedLines < total), at the end of the last revealed line. It is still: upstream reserves the blinking caret for streaming prose, and the code caret is a position marker.
  • complete fires once, on crossing the end. Mounting with the listing already complete is not a crossing, so nothing is emitted.
  • The copy control reuses TxCopyButton, so it inherits the document.execCommand fallback and the polite live region; only its chrome is restyled to match.
  • The gutter's line-height: 1.86 against the code's 1.7 is deliberate: 10.5px × 1.86 and 11.5px × 1.7 both land near 19.5px, which is what puts a number on its line's baseline. Changing either side breaks the pairing.
  • The height floor reserves lines × 1.7em + 20px, so a reveal grows into space already held rather than pushing the page down. Upstream's hardcoded 137px is exactly six lines of its own sample. In streaming mode the floor follows the lines shown, unless reserve holds the full height while complete code plays back.
  • The code area is a <div>, not a <pre>: Vue's compiler preserves template whitespace inside <pre>, so the markup's own indentation would render as code. white-space: pre per line says the same thing.
  • Under reduced motion no line or word entrance plays: revealedLines still reveals, streaming mode shows code as it arrives, and its caret rests as a still arc. All motion is declared only under prefers-reduced-motion: no-preference.

Technologies

  • Component source: packages/tuffex/packages/components/src/code-stream/src/TxCodeStream.vue.
  • Types: packages/tuffex/packages/components/src/code-stream/src/types.ts.
  • Reused: packages/tuffex/packages/components/src/button/src/copy-button.vue, packages/tuffex/packages/components/src/stream-markdown/src/shiki-runtime.ts, and .../use-auto-theme.ts.
  • Streaming mode reuses the family's pieces: the clock stream-text/src/use-stream-pacer.ts, the caret stream-text/src/TxStreamCaret.vue, the reveal mixins in style/mixins.scss (their code variant, without the colour sweep) and stream-markdown/src/use-fresh-chunks.ts for the entrances.
  • Verified coverage: packages/tuffex/packages/components/src/code-stream/__tests__/code-stream.test.ts (22 cases) covers reveal clamping and reuse of already-revealed lines, caret conditions, complete firing once, header presence, splitting shiki output into lines, the plain-text fallback on a line-count mismatch, theme forwarding, and copy event forwarding. code-stream-live.test.ts (17) covers streaming mode: mode selection, code present at mount, code entering with appear, word-by-word streaming with indentation, spreading a burst, entrances inside highlighted markup and their folding back, the highlight cadence, the persistent line and caret, the caret retract, a caret switched off going at once, the caret slot and state events, replay and skip, rewrites, reserve, reduced motion, SSR and the alias. code-stream-style.test.ts (4) pins motion only without reduced motion, the line entrance outside streaming mode, the code variant of the reveal and a zero-width caret.
  • Adapted from Beautiful UI (https://www.beautifului.dev), © 2026 Shane Levine, MIT.
查看源码
packages/tuffex/packages/components/src/code-stream/index.ts
  • How it divides from TxCodeBlock: TxCodeBlock is the internal renderer TxStreamMarkdown uses for each fence and serves the markdown path. This component is a standalone "the agent is writing code" surface, adding a filename header, a gutter, the reveal, and the caret.
  • Known visual deviation: upstream hand-writes a five-colour token model (kw → --accent-ink, str → --green, dim → --ink-3); this uses shiki instead, so colours are not a pixel match for the reference screenshots. design.md §8.2 accepts this trade — the hand model would require every host to tokenize its own code, which cannot be a public API. To close the gap, configure a custom shiki theme mapping those five scopes onto --tx-bui-*.
  • Safety: only shiki-generated markup reaches v-html, with the code text already escaped; the unhighlighted branch is ordinary interpolation.