feat(tui): spinner breathes instead of ticking - #1421
Merged
Conversation
Six frames at a flat 120ms is a full rotation every 720ms with every frame weighted the same, which reads as a mechanical tick. The frame order also jumped from the heaviest glyph straight to the lightest, a stutter at any speed. Same six glyphs, reordered by weight and ping-ponged into a pulse, with a per-step hold table so advance is gated rather than one-per-tick. The extremes hold 360ms and the middle passes in 120ms; a cycle is now about 2.16s. The uneven hold is the easing — a flat cadence is exactly what made it feel mechanical. The tick rate stays at 120ms on purpose: the same tick redraws the elapsed-time counter, so slowing the timer would slow the clock. Closes #1420 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
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 #1420
The complaint
After #1415 stopped the word rotating, the spinner still felt wrong: "it still seems like it spins too fast", "slow it down so that it breaths."
The remaining problem was the glyph, and I had left it alone on a bad assumption — that because our six glyphs match the set independently reported for Claude Code, the glyph layer was already right. Matching their glyphs is not matching their cadence, and the research explicitly could not find their frame interval.
Two things were wrong:
Speed. Six frames at a flat 120ms is a full rotation every 720ms. For something the eye rests on during a long wait that is fast; Charm's own presets run 83–333ms and ours sat near the fast end.
No shape. Every frame held exactly as long as every other, so the cycle had no contour. And the order
✶ · ✻ ✽ ✳ ✢was not monotonic in visual weight — it jumped from the heaviest glyph straight to the lightest and back, which reads as a stutter at any speed.What it does now
The same six glyphs, reordered by weight and ping-ponged so the cycle grows and shrinks, with a per-step hold table:
It lingers at the top and bottom of the breath and moves quickly through the middle. That unevenness is the easing — a flat cadence is precisely what made it read as mechanical. Three times slower than before, and symmetric.
The tick rate deliberately stays at 120ms. The same tick redraws the elapsed-time counter, so slowing the timer would slow the clock. Advance is gated instead, which slows only the glyph. The glyph set is unchanged; it was never the problem.
Verification
Red first, all three:
TestSpinnerCycleTakesAboutTwoSeconds— a cycle was 6 ticks, must be 18.TestSpinnerEasesAtTheExtremes— every frame held exactly one tick, so no strict inequality between extremes and middle could exist.TestSpinnerPulseGrowsThenShrinks— the old order made exactly the heaviest-to-lightest jump this forbids.TestSpinnerStillAnimatesUnderEasingis the control: a hold table that never released would satisfy "slower" while freezing the spinner outright.TestTUI024_SpinnerCyclesFramespinned one-frame-per-tick, the behavior being replaced, so it becameTestTUI024_SpinnerAdvancesThroughThePulse. The #1415 glyph guard widened its bound to the longest hold.Full TUI suite green under
-race. Snapshots regenerated and reviewed — glyph changes only,✽to✢, as expected from the new step ordering.What is not yet verified
The hold table is a considered starting point, not a proven result. "Breathes" is a perceptual claim and no unit test can judge it — the numbers above are what I chose, and whether they feel right is the owner's call after watching a live run. If 2.16s reads as sluggish rather than calm, the table is one line to retune.
Also worth stating plainly: the claim that Claude Code eases its frames comes from third parties hand-timing screen recordings, not from reading source. This is our own easing, informed by that description rather than reproducing a measurement.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5