feat: optional input-smoothing utility (./smoothing)#89
Merged
Conversation
Ship an opt-in input smoother that steadies chunky token arrival into a
smooth character-cadence reveal by throttling the string fed to
`renderer.update()` (via requestAnimationFrame) rather than animating output.
It lives behind `@copse/streaming-markdown/smoothing`, is never re-exported
from the main entry, and adds zero bytes to the main bundle unless imported —
the default emitter path stays byte-for-byte unchanged.
- `createInputSmoother({ update, charsPerSecond, disabled, respectReducedMotion })`
with `push`/`flush`/`dispose` and an `enabled` flag.
- Honours `prefers-reduced-motion` (disable -> immediate pass-through); flushes
the full target on stream end with no completion lag.
- Convergence preserved: after flush the DOM equals a single un-smoothed
`update(fullText)` (asserted against a direct full-string render).
- Environment access (raf/caf/now/matchMedia) is guarded behind defaulted,
injectable seams so it runs and unit-tests under node/jsdom.
- Tests: incremental reveal on a fake clock, flush, dispose, reduced-motion,
UTF-16 surrogate safety, default-env fallbacks, and an esbuild bundle check
proving no leakage into the main entry. Docs in LAZY-LOADING.md + README,
covering the input-smoothing-vs-output-animation decision. Coverage ratcheted
to 99.76%.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehmok4AzyTQDNkRzW3g8z8
# Conflicts: # README.md # coverage-baseline.json # docs/LAZY-LOADING.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #84.
An opt-in reveal-smoothing utility behind a new
./smoothingsubpath — off by default, zero bytes in the main bundle unless imported, and the incremental DOM emitter's default behavior is byte-for-byte unchanged.Approach: input-smoothing, not output-animation
The smoother throttles the input string fed to
renderer.update()— releasing a growing prefix a few characters per frame viarequestAnimationFrame— rather than animating newly-added DOM nodes. Every released value is a prefix of the accumulated text, i.e. exactly the streaming contract the existing pending-state machinery and DOM morph already converge on, so it composes with them instead of introducing a second animation system that could fight the morph. Lower-risk and framework-/theme-agnostic.CSS entrance animations are deliberately left to the host (documented): style the emitter's existing
.stream-pending/.stream-completehooks with your own transitions.API
createInputSmoother({ update, charsPerSecond=600, disabled, respectReducedMotion=true, + injectable now/requestFrame/cancelFrame/matchMedia seams })→{ push, flush, dispose, enabled }.Guarantees
prefers-reduced-motion→ smoothing disabled, immediate pass-through.flush()DOM equals a single un-smoothedupdate(fullText)(asserted against a direct full-string render) — convergence preserved.Verification
npm run typecheck,npm run build(dist/smoothing.*emitted) — passnpm test— 786 pass / 0 fail / 1 pre-existing skip (incl. 19 new smoothing tests)npm run check:conformance/npm run check:gfm-conformance— passnpm run coverage:ci— gate passes;smoothing.ts100% lines/funcs; baseline ratcheted 99.75 → 99.76Caveats
prefers-reduced-motionis evaluated once at creation (documented).🤖 Generated with Claude Code
https://claude.ai/code/session_01Ehmok4AzyTQDNkRzW3g8z8
Generated by Claude Code