Skip to content

fix(webui): match the TUI's per-character playbook run shimmer - #1297

Merged
edwin-zvs merged 3 commits into
mainfrom
webui-playbook-text-shimmer
Aug 21, 2026
Merged

fix(webui): match the TUI's per-character playbook run shimmer#1297
edwin-zvs merged 3 commits into
mainfrom
webui-playbook-text-shimmer

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

What

The web Playbook editor now shimmers pending blocks with the same wave the TUI paints, instead of its own unrelated highlight band.

The TUI behavior I matched

crates/cli/src/ui.rsapply_playbook_shimmer re-emits every character of a pending block with a brightness drawn from a travelling sine wave:

TUI
temporal speed PLAYBOOK_SHIMMER_SPEED = 4.2 rad/s
spatial frequency PLAYBOOK_SHIMMER_DENSITY = 0.18 rad/char
brightness smoothstep(0.5 + 0.5·sin(phase − gidx·D)), blended theme.muted → theme.text
continuity one character index (gidx) advances across all pending lines, so the band runs continuously down the block
spans with a background left intact, but still advance the wave

That works out to a 34.907ch wavelength (2π/0.18) travelling one wavelength per 1.496s (2π/4.2) — ~23.3 chars/sec, left to right and on down the block.

The web editor was doing something else entirely: a translucent --accent band swept across each pending row independently on a 1.6s cadence, restarting per line.

The change

crates/daemon/assets/index.html only (CSS + the existing playbookApplyShimmer):

  • .playbook-line.is-running paints a 17-stop repeating gradient sampling that same smoothstep, sized 34.907ch, clipped to the glyphs with background-clip: text, and translated one wavelength per 1.496s.
  • playbookApplyShimmer sets a per-line --pb-shim-lag (negative animation-delay) from the running character offset. The wave is periodic, so lagging a line by chars / 23.3 seconds reproduces the TUI's single gidx — the crest carries on across lines instead of restarting per row. It is anchored to the run's own start time, so a block that joins a run late stays in phase.
  • Chips and inline code carry their own background — the TUI overlay skips those — so their colors are pinned back against the inherited transparent text fill. Fenced-code rows re-issue their block background as a border-box layer so background-clip: text can't clip it away.
  • prefers-reduced-motion gets a static accent tint at full contrast instead.

Source of truth is unchanged: the daemon's run pending set (pending_block_refs), exactly as before. No local timers, no heuristics.

No per-frame JS and no idle repaint loop — it's a CSS animation that only exists while a line carries .is-running. With everything settled: 0 running lines, 0 leftover custom properties, document.getAnimations() reports 0 shimmer animations.

Known deviation

The TUI also sets BOLD when eased > 0.85. CSS can't vary font weight per character inside one text run, so the crest peaks at full --fg without the extra weight.

Evidence

Isolated daemon from this worktree (CONSTRUCT_ASSETS_DIR hot-reload), a real playbook run with 4 pending blocks declared over IPC, driven in Chrome over CDP.

TUI (the reference, unchanged):

tui

Web, dark theme:

web dark

Web, light theme:

web light

Two frames ~370ms apart, showing the crest travelling right and carrying across the two Notes: lines:

frame 1 frame 2
f1 f2

Also verified in the live page:

  • Typing inside a shimmering line — inserted ZQX mid-line: text lands at the caret, caret offset advances correctly, line keeps .is-running and its animation; backspacing restores it.
  • Selection stays legible (Chrome keeps painting the clipped gradient through the selection highlight).
  • Settling stops promptly — settling 2 of 4 blocks over playbook.update cleared .is-running and --pb-shim-lag on exactly those lines and re-phased the rest; settling all cleared everything.
  • prefers-reduced-motion: animation-name: none, no gradient, -webkit-text-fill-color back to rgb(236,236,241).

Testing

  • cargo build — clean.
  • cargo test --workspace --no-fail-fast — all green, including crates/e2e/tests/web_smoke.rs against a freshly built target/debug/construct (verified the new CSS is baked into the binary). One earlier run tripped pty_render::tests::many_small_events_stay_fast_as_history_grows, a load-sensitive perf test unrelated to this diff; it passes in isolation and in the full re-run.

Note for the reviewer

Another branch is concurrently editing this same file (C-space selection fix). This diff is confined to the shimmer CSS block and the body of playbookApplyShimmer.

The media above lives in a temporary .pr-media/ commit that is removed by the next commit on this branch; the links are pinned to 65ab37db5ed19b26aef683c2b333e2dfb0cb5a97 and stay resolvable via refs/pull.

The TUI paints a pending playbook block by re-styling every character with
a brightness taken from a travelling sine wave (crates/cli/src/ui.rs,
`apply_playbook_shimmer`): PLAYBOOK_SHIMMER_SPEED 4.2 rad/s,
PLAYBOOK_SHIMMER_DENSITY 0.18 rad/char, foreground blended muted -> text
through a smoothstep so the region rests dim and the crest pops, with one
character index running across every pending line so the band stays
continuous down the block.

The web editor instead swept a translucent accent band across each pending
row, on its own 1.6s cadence — a different effect entirely. Reproduce the
TUI wave: a 34.907ch (2*pi/0.18) repeating gradient sampling the same
smoothstep, clipped to the glyphs and translated one wavelength per 1.496s
(2*pi/4.2). playbookApplyShimmer sets a per-line negative animation-delay
from the running character offset, so the crest carries on across lines the
way the TUI's `gidx` does, and lines joining a run late stay in phase.

Still driven by the same source of truth as before — the daemon's run
pending set — so nothing shimmers once blocks settle, and the animation is
pure CSS: no per-character DOM, no per-frame JS, no idle repaint loop.

Notes:
- Chips and inline code carry their own background, which the TUI overlay
  skips; their colors are pinned back against the inherited transparent
  text fill. Fenced-code rows re-issue their block background as a
  border-box layer so `background-clip: text` cannot clip it away.
- The TUI also bolds the crest, which CSS cannot express per character;
  the crest peaks at full foreground instead.
- prefers-reduced-motion gets a static accent tint at full contrast.
@edwin-zvs
edwin-zvs merged commit cd4d17e into main Aug 21, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the webui-playbook-text-shimmer branch August 21, 2026 16:29
@edwin-zvs edwin-zvs mentioned this pull request Aug 22, 2026
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