fix(cli): use tmux-safe dots spinner to reduce redraw pressure#3903
Conversation
Inside tmux, ink-spinner's high-frequency frame loop is rewritten by
the multiplexer's pane buffer on every tick. The visible effect on
some terminals is spinner residue / pane-scroll churn during long
streaming responses.
When TMUX env var is set, swap to a fixed-width 750ms three-frame
dots spinner ('. ' / '.. ' / '...') so each tick is a single-cell
write that tmux can absorb without rewriting surrounding state.
Outside tmux the existing ink-spinner path is unchanged.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
6bd99ec to
0b57207
Compare
|
Force-pushed an amended commit ( Bug: tmux-mode branch returned Fix:
This is the bug that #3663's original commit Verified locally:
|
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
wenshao
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI still running. — gpt-5.5 via Qwen Code /review
…M#3903) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Inside tmux, ink-spinner's high-frequency frame loop is rewritten by
the multiplexer's pane buffer on every tick. The visible effect on
some terminals is spinner residue / pane-scroll churn during long
streaming responses.
When TMUX env var is set, swap to a fixed-width 750ms three-frame
dots spinner ('. ' / '.. ' / '...') so each tick is a single-cell
write that tmux can absorb without rewriting surrounding state.
Outside tmux the existing ink-spinner path is unchanged.
Generated with AI
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…M#3903) Inside tmux, ink-spinner's high-frequency frame loop is rewritten by the multiplexer's pane buffer on every tick. The visible effect on some terminals is spinner residue / pane-scroll churn during long streaming responses. When TMUX env var is set, swap to a fixed-width 750ms three-frame dots spinner ('. ' / '.. ' / '...') so each tick is a single-cell write that tmux can absorb without rewriting surrounding state. Outside tmux the existing ink-spinner path is unchanged. Generated with AI Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Summary
Inside tmux,
ink-spinnerruns at the cli-spinners default frame rate (~80 ms per frame = ~12.5 frames/s). Each frame is a single-cell rewrite, but the multiplexer's pane buffer churns on every tick — visible on some tmux/terminal combinations as spinner residue, pane-scroll churn, or pane buffer activity that crowds out useful streaming content.When the
TMUXenvironment variable is set, this PR swaps to a fixed-width 750 ms three-frame dots spinner ('. '/'.. '/'...'):main(ink-spinner default)That is a ~10× reduction in spinner-driven pane writes during streaming. Outside tmux the existing
ink-spinnerpath is unchanged.Why split out
Extracted from #3663 (umbrella TUI flicker / streaming stability PR). This piece touches a single isolated component (
GeminiRespondingSpinner.tsx+ a small new test), is gated by an env-var branch (process.env.TMUX), and uses only common ink primitives (Text,useIsScreenReaderEnabled) — so it is forward-compatible with the in-flight ink 7 upgrade.Bug found and fixed during pre-flight (force-pushed)
The original commit from #3663 returned
<Box width={3}><Text>...</Text></Box>in the tmux branch.GeminiSpinneris rendered inside a<Text>inFooter.tsx:87(<Text>...<GeminiSpinner /> {configInitMessage}</Text>), and Ink forbids<Box>nested inside<Text>— the CLI threw<Box> can't be nested inside <Text> componenton every tmux startup that hit the Footer'sconfigInitMessagepath.This PR's fix:
<Box>wrapper in the tmux branch — return plain<Text>directly. The 3-char fixed-width frames already give stable layout without an explicit width container.Boximport.<Text><GeminiSpinner /> ...</Text>and asserts no throw — directly catches the Footer-context bug that the original PR's isolated-render test missed.The original umbrella #3663 still carries this bug; this split surfaces and fixes it.
Issues this addresses
Test evidence
packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx— 2 deterministic tests:```ts
it('uses a low-frequency fixed-width indicator inside tmux', () => {
vi.stubEnv('TMUX', '/tmp/tmux-1000/default,12345,0');
const { lastFrame } = render();
expect(lastFrame()).toContain('.');
});
// Regression: Footer.tsx renders inside a wrapper.
// Ink forbids nested inside , so the tmux branch must return a
// , not a -wrapped one — otherwise the CLI throws on startup.
it('renders without throwing when nested inside a (Footer context)', () => {
vi.stubEnv('TMUX', '/tmp/tmux-1000/default,12345,0');
expect(() =>
render(
startup message
,
),
).not.toThrow();
});
```
The first test pins behaviour (tmux env → dots indicator). The second test is the regression test that would fail against the original
<Box>-wrapped commit and passes after dropping the Box.Verification
cd packages/cli && npx vitest run src/ui/components/GeminiRespondingSpinner.test.tsx— 2 / 2 pass.npm run typecheck— clean.npm run lint— clean.src/config/config.integration.test.ts(vite resolveId / channel-registry — verified to fail identically on un-modifiedorigin/main); spinner-touched paths all pass.<Box>-in-<Text>error, spinner ticks every 750 ms.Follow-ups
This is one of a series of small PRs splitting out independent fixes from #3663. Sibling splits already up: