fix(cli): reduce terminal redraw cursor movement#3381
Conversation
wenshao
left a comment
There was a problem hiding this comment.
Previous review attempt used a stale local base branch and should be ignored. Re-running against the correct remote base now. — gpt-5.4 via Qwen Code /review
Collapse Ink multiline erase sequences into a single relative cursor move plus erase-down operation. This avoids excessive repeated cursor-up writes during streaming interactive renders while preserving normal TTY behavior. Screen reader mode and non-TTY output are left unchanged, with a legacy env fallback available.
…ing. Collapse repeated cursor-up movement while preserving bounded line clearing, so redraws avoid excessive upward cursor jumps without erasing unrelated terminal output below the frame. Non-TTY output, screen reader mode, and non-string writes are unchanged.
864aa2a to
5365ceb
Compare
|
Good catch, you're right. The ESC[J version widened the erase semantics and could clear unrelated output below the Ink frame. |
| ) { | ||
| const version = await getCliVersion(); | ||
| setWindowTitle(basename(workspaceRoot), settings); | ||
| const restoreTerminalRedrawOptimizer = |
There was a problem hiding this comment.
[Suggestion] installTerminalRedrawOptimizer(process.stdout) is applied before startup finishes, but the restore function is only registered later in registerCleanup(). If render(...) or another later startup step throws first, process.stdout.write stays monkey-patched for the rest of the process.
| const restoreTerminalRedrawOptimizer = | |
| const restoreTerminalRedrawOptimizer = | |
| process.stdout.isTTY && !config.getScreenReader() | |
| ? installTerminalRedrawOptimizer(process.stdout) | |
| : () => {}; | |
| try { |
— gpt-5.4 via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
Verified against HEAD 5365ceb:
- The
ESC[J]erase-down concern is fully addressed — optimizer now emits onlyESC[2Kper line; explicit test assertsnot.toContain('[J') - Cursor-position equivalence traced for a 3-line frame: original and optimized sequences both end at top-left with all frame lines cleared
cursorUpCount <= 1early-return keeps small frames untouched (no regression for 1-2 line cases)- Cleanup path restores original
stdout.writeat unmount with a defensive identity check QWEN_CODE_LEGACY_ERASE_LINES=1fallback works
Non-blocking nits for later:
- Regex is tightly coupled to
ansi-escapes.eraseLines()output — worth a brief comment noting the dependency so a future Ink/ansi-escapes change doesn't cause silent degradation - If Ink ever splits an erase sequence across two
write()calls, the optimizer won't match; not a concern today sincelog-updaterenders atomically
LGTM.
…#125) Backport of the core foundation pieces from upstream QwenLM#3591 (TUI flicker foundation fixes). Two interventions on stdout, both no-op outside a TTY or under a screen reader: 1. terminalRedrawOptimizer (from upstream QwenLM#3381 → QwenLM#3591): wraps stdout.write to collapse Ink's per-line {ERASE_LINE, CURSOR_UP_ONE} sequences into a single {CURSOR_UP_N, erase_at_each, CURSOR_UP_N, CURSOR_LEFT}. Eliminates the scrollback-bouncing during streaming renders. Bypass via PROTO_LEGACY_ERASE_LINES=1. 2. synchronizedOutput (from QwenLM#3591): wraps each render frame in BSU/ESU escape codes (\\e[?2026h / \\e[?2026l) on supporting terminals (Kitty, WezTerm, iTerm) so Ink frames are committed atomically. Terminal allowlist with auto-detect; opt-out via PROTO_DISABLE_SYNCHRONIZED_OUTPUT=1, force on via PROTO_FORCE_SYNCHRONIZED_OUTPUT=1. Both installed in startInteractiveUI() before the Ink render call; both restored in the registerCleanup callback. Deferred from upstream QwenLM#3591 (too entangled with our fork's useGeminiStream.ts / ToolMessage.tsx divergence; revisit later): - main-stream event buffering with flush timer - tool output pre-slicing by visual height - shell soft-wrap-only rerender suppression The two installed pieces cover the user-visible streaming flicker case directly; the deferred pieces are about huge-tool-output and narrow-terminal soft-wrap edge cases. Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): reduce terminal redraw cursor movement Collapse Ink multiline erase sequences into a single relative cursor move plus erase-down operation. This avoids excessive repeated cursor-up writes during streaming interactive renders while preserving normal TTY behavior. Screen reader mode and non-TTY output are left unchanged, with a legacy env fallback available. * Optimize Ink multiline erase sequences during interactive TTY rendering. Collapse repeated cursor-up movement while preserving bounded line clearing, so redraws avoid excessive upward cursor jumps without erasing unrelated terminal output below the frame. Non-TTY output, screen reader mode, and non-string writes are unchanged.
TLDR
Summary
This change reduces terminal viewport jumping during interactive streaming output by optimizing the multiline redraw sequence emitted by Ink/log-update.
Instead of repeatedly writing clear-line + cursor-up for every previous rendered line, the CLI now collapses that exact multiline erase pattern into one relative cursor-up plus erase-down operation.
The optimizer is only installed for interactive TTY output and is skipped for screen reader mode. A compatibility fallback is available via:
Changes
Screenshots / Video Demo
Manual Verification
Ran an interactive streaming prompt with proxy environment variables removed:
Before the fix, the recording contained thousands of repeated cursor-up and clear-line sequences:
ESC[1A count: 13753
ESC[2K count: 14756
max consecutive ESC[1A before one redraw: 37
After the fix, the old repeated redraw sequence is gone and the output remains complete:
ESC[1A count: 0
ESC[2K count: 0
ESC[2J count: 0
ESC[H count: 0
max consecutive old ESC[1A redraw: 0
output complete: yes
Dive Deeper
Reviewer Test Plan
Testing Matrix
Linked issues / bugs
Fixes #3144