Skip to content

Add revealRate reveal pacing to <Renderer> (#751)#752

Open
oakleaf wants to merge 2 commits into
thesysdev:mainfrom
oakleaf:feat/renderer-reveal-rate
Open

Add revealRate reveal pacing to <Renderer> (#751)#752
oakleaf wants to merge 2 commits into
thesysdev:mainfrom
oakleaf:feat/renderer-reveal-rate

Conversation

@oakleaf

@oakleaf oakleaf commented Jul 12, 2026

Copy link
Copy Markdown

Implements the reveal-pacing option proposed in #751.

Problem

<Renderer> renders progressively as the response arrives, so arrival rate is the reveal pace. When the DSL arrives bursty or whole — a fast tool-call generation, or a transport that coalesces writes (serverless response streaming buffers small writes, CDNs re-clump) — the components pop in all at once instead of assembling.

Approach

A new revealRate prop. While isStreaming, the Renderer feeds the streaming parser a growing prefix of the response on a clock, so completed statements stay stable (the parser already caches them) while the pending one fills in — the UI assembles the way it would over a smooth stream. It isn't fighting the parser; it's set() used as designed, paced on a timer instead of on network arrival.

<Renderer response={dsl} isStreaming revealRate />
<Renderer response={dsl} isStreaming revealRate={{ targetMs: 1100 }} />
  • Default offrevealRate omitted/false keeps the current behavior. Fully backward compatible.
  • Count-based and never resets mid-stream, so a non-append-only DSL change (a new element growing the root's child-ref list) keeps typing forward instead of blanking; the parser reconciles the shifted prefix by statementId.
  • Settled content and prefers-reduced-motion render immediately.

Structure

  • reveal.ts — pure pacing math (framework-free, unit-tested).
  • hooks/useRevealedResponse.ts — thin requestAnimationFrame + reduced-motion glue.
  • Renderer.tsx — new revealRate prop, paces response before useOpenUIState.
  • Docs: api-reference/react-lang.mdx gains the prop + a "Reveal pacing" section.

Tests

  • reveal.test.ts — the pacing math.
  • reveal-integration.test.ts — drives the real createStreamingParser with clock-paced prefixes and asserts the tree genuinely assembles (the root renders mid-stream, before every statement has completed) and never blanks a completed statement, landing exactly where a full parse does.
  • All of react-lang's checks pass (test, typecheck, lint:check, format:check, build). No new dependencies.

Happy to adjust the API shape (reveal="smooth", different defaults, statement- vs char-granularity) to match your preference.

Closes #751

oakleaf and others added 2 commits July 12, 2026 15:29
OpenUI Lang renders progressively as the response arrives, so the arrival
rate is the reveal pace. When the response arrives bursty or whole — a fast
tool-call generation, or a transport that coalesces writes (serverless
response streaming buffers small writes, CDNs re-clump) — the components pop
in all at once instead of assembling.

revealRate decouples paint from arrival: while isStreaming, the Renderer
feeds the streaming parser a growing prefix of the response on a clock, so
completed statements stay stable (the parser caches them) while the pending
one fills in and the UI assembles the way it would over a smooth stream.

The revealed length is count-based and never resets mid-stream, so a
non-append-only DSL change (a new element growing the root's child-ref list)
keeps typing forward instead of re-assembling from blank; the streaming
parser reconciles the shifted prefix by statement id. Settled content and
prefers-reduced-motion render immediately. Default off — fully backward
compatible.

Pure pacing math lives in reveal.ts (unit-tested); the rAF and
reduced-motion glue lives in useRevealedResponse.

Refs thesysdev#751

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drive createStreamingParser with clock-paced growing prefixes (the exact
sequence the Renderer feeds it) and assert the tree genuinely assembles: the
root renders mid-stream, before every statement has completed, and the
completed-statement count is monotonic — a completed component is never
un-rendered ("never blanks"). The paced stream lands exactly where a fresh
full parse does. Uses a ref-list-first DSL so the string is non-monotonic,
the shape a naive prefix reveal would blank-and-rebuild on.

Refs thesysdev#751

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: native reveal-rate / smoothing on <Renderer> to decouple paint from DSL arrival

1 participant