feat(tui): spinner says what the run is doing - #1418
Merged
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
The spinner rotated fifteen near-synonyms for "thinking" every ~960ms. The word changed and the meaning did not, which reads as a stuck animation rather than a live one. Research across 139 published claude-code npm versions puts their list at ~90 varied words plus a runtime-fetched set, rotating at roughly the same ~1s. So speed was never what made ours feel canned — vocabulary was. Matching that would mean maintaining a list at a scale we cannot verify, from a secondary source, to reproduce an effect that depends on a dynamic set we do not have. opencode, which we can read directly, uses no randomized words at all. So the label now states what is actually happening, most specific first: a running tool, then streaming text, then reasoning, then the model being waited on. It never returns empty, which is what makes a decorative pool unnecessary. The glyph still animates at 120ms so the line reads as live; only the word holds still until the state changes. Truthful labels are longer than "Computing...", and at 40 columns right-truncation ate the cancel hint. The label now yields instead: the hint is the only actionable part of that line. TUIConfig.SpinnerSeed is removed. It existed only to make random verbs deterministic for snapshots, and would otherwise have become a setting that no longer did anything. Closes #1415 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
dennisonbertram
force-pushed
the
feat/1415-truthful-spinner
branch
from
September 8, 2026 11:49
898ef56 to
cb14d69
Compare
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 #1415
The problem
The spinner rotated fifteen near-synonyms for "thinking":
A random one was re-picked every 8 ticks at 120ms — about once a second. The word changed and the meaning did not, which reads as a stuck animation rather than a live one.
A comment above that list claimed these were "the same verbs Claude Code uses in its thinking indicator." That was false, and the research to check it is what settled the design.
Why not just get better words
Extraction across 139 published
@anthropic-ai/claude-codenpm versions (levindixon/tengu_spinner_words) reports ~90 varied words — Flibbertigibbeting, Reticulating, Schlepping, Spelunking — merged at runtime with a set fetched from a Statsig dynamic config. Their rotation is also roughly 1s.So our rotation was never faster than theirs (~960ms vs ~1000ms). Speed was not the differentiator; vocabulary was. Ninety varied words make a 1s rotation feel playful. Fifteen synonyms make the same rotation feel broken.
Matching that would mean maintaining a list at a scale we cannot verify, from a secondary source, to reproduce an effect that partly depends on a server-side set we do not have. opencode — which we can read directly, so a primary source — uses no randomized words at all.
Both the ~90 count and the ~1000ms interval are third-party static analysis of a minified bundle. Nobody has pulled the literal
setIntervalout of it. The engineering log records them as hypotheses rather than facts, so the limitation does not quietly become received wisdom in our own docs.What it does now
Most specific true thing wins:
Running bashWriting responseThinkingWaiting for gpt-4.1-minicurrentSpinnerAction()never returns"", which is what makes a decorative pool unnecessary rather than merely unfashionable. The glyph still animates at 120ms so the line reads as live; only the word holds still until state changes. The trailing...is gone — "Running bash" is a fact, not a vague one.TUIConfig.SpinnerSeedandspinnerSeed()are removed. They existed solely to make random verb selection deterministic for snapshots; rendering is now deterministic by construction, so keeping the field would have left a setting that no longer did anything.A regression I introduced, and caught by looking
Truthful labels are longer than
Computing.... At 40 columns, right-truncation ate the cancel hint:The hint is the only actionable part of that line. The label yields instead:
Two tests pin it, including the degenerate case where not even a stub of a label fits and the hint alone survives.
Verification
Red first:
TestCurrentSpinnerActionLadderfailed on four of its five states (currentSpinnerAction() = "", want "Waiting for gpt-4.1-mini"), andTestSpinnerLabelDoesNotRotateOnTicksfailed against the rotating model.TestSpinnerGlyphStillAnimatesis the deliberate control: freezing the entire line would satisfy "the label does not change" while making the spinner look hung.All TUI packages green under
-race. Snapshots regenerated from✽ Computing... (esc to interrupt)to✽ Working (esc to interrupt).Live TUI, and what it could not show. I drove the real TUI through a pty against the fake provider on this branch's binaries. Observed verbatim:
ThinkingandWriting responsenever appeared as distinct frames, and the reason is worth stating rather than glossing: the fake provider's turns-file format (fakeProviderTurnJSONincmd/harnessd/main.go) exposes no delay field, thoughinternal/fakeprovider.TurnhasDelayandInterDeltaDelay. With zero delay, a scripted turn's reasoning delta, content delta, and tool call all land inside one sub-millisecondComplete()call, while the label refreshes on the 120ms tick. Those two states exist for microseconds.With a real model, streaming takes seconds and both states will be visible — but I have not observed them, and the unit tests are the only evidence for them. Filed as a follow-up: the fake provider should be able to inject a delay, so TUI states like these are observable in tests at all.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5