Skip to content

feat(text): pace the stream fade to how fast chunks arrive - #7

Merged
trancong12102 merged 1 commit into
mainfrom
feat/paced-stream-fade
Oct 3, 2026
Merged

trancong12102 merged 1 commit into
mainfrom
feat/paced-stream-fade

Conversation

@trancong12102

Copy link
Copy Markdown

What

The stream fade runs for a fixed duration. On a fast stream it pulses chunk by chunk, and on a slow one it finishes long before the next chunk lands, so the text arrives in visible steps.

TextViewMotion::with_stream_fade_pacing(min, max) paces the fade to the stream instead:

  • Duration: each update fades over 3 times the running average of the gaps between updates (EMA with weight 0.3), clamped to [min, max].
  • First update: stream_fade still turns the fade on and sets the duration of the first update, when there is no gap yet to go by.
  • Pauses: each gap is clamped to [min/3, max/3] before it is averaged, so a pause (a model thinking) counts as the longest gap the bounds can follow. The pace comes back within a few updates once the stream resumes.
  • Backlog: an update parsed while others are already queued behind it (revision - committed revision) fades 1.3 times faster per waiting update, up to 8, so a backlog catches up.
  • Stagger: the stagger is now compressed or dropped against the paced fade, not the set one.
  • Replaced text: starts its pace afresh.
  • Reduced motion: still drops the fade.

The idea comes from Waku's veil (GPL). It was rewritten from a description of its behaviour, and none of its code was read or copied. The existing tracker already re-veils only the changed suffix after a reparse, so that part was not needed.

Each fade segment now keeps its own duration and is sampled directly, without building a Timing.

Docs: website/base/text-view.md and its zh-CN twin.

Numbers

paced_fade_frame_bench: 300 words mid-fade, release build with debug assertions (gpui-fast bb5991c fails to build in plain release), median of 9 rounds.

sampling a frame fixed paced
main (one shared Timing) 21.17 µs 21.13 µs
a Timing per segment (first try) 31.24 µs 31.21 µs
sampled directly (this PR) 12.14 µs 12.10 µs

Tests

Eight tracker-level tests in stream_fade.rs (tests::pacing), on real parsed documents with chosen timestamps:

  • the first update fades over the set fade;
  • the fade lasts three gaps (150 ms after a 50 ms gap, then 195 ms as the average moves);
  • a fast stream fades at min and a slow one at max;
  • a pause moves the fade by one step (to 204 ms), and the pace returns within 8 updates;
  • waiting updates speed the fade (300 to 178 ms with two waiting; capped at 1.3^8);
  • a replaced text starts afresh;
  • the stagger is dropped against a 120 ms paced fade, where it would have been kept against the set 280;
  • reduced motion drops a paced fade.

Checks:

  • cargo test -p gpui-base --lib: 1338 passed.
  • fmt is clean.
  • cargo clippy -p gpui-base reports nothing in the touched files. Its existing too_many_arguments errors elsewhere are unchanged from main.

Not in this PR:

  • The 33 ms fade tick stays. Moving it to request_animation_frame waits on a frame-time measurement on a long transcript.
  • The styled TextView::stream_fade(true) keeps its fixed timing. Slopty passes .motion(..) with pacing.

Upstream checked first: longbridge/gpui-kit has no open stream-fade PR.

🤖 Generated with Claude Code

A fixed fade pulsed chunk by chunk on a fast stream and finished long
before the next chunk on a slow one. With
`TextViewMotion::with_stream_fade_pacing(min, max)`:
- each update fades over three times the running average of the gaps
  between updates (weight 0.3), kept between `min` and `max`;
- `stream_fade` still turns the fade on and times the first update;
- a pause counts as the longest gap the bounds follow, so the pace returns
  within a few updates;
- an update parsed with others already waiting fades 1.3 times faster for
  each (up to eight), so a backlog catches up;
- the stagger compresses against the paced fade, and a replaced text
  starts its pace afresh.

The idea follows Waku's veil as described, rewritten without its (GPL)
code. Reduced motion still drops the fade. Each segment keeps its own
duration and is sampled directly rather than through a `Timing`:
`paced_fade_frame_bench` (300 words mid-fade, release, median of 9) goes
from 21.2 to 12.1 us per frame, paced or not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trancong12102
trancong12102 merged commit add2f41 into main Oct 3, 2026
8 of 12 checks passed
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.

1 participant