Skip to content

fix(cli): reduce terminal redraw cursor movement#3381

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
reidliu41:fix-terminal-redraw-jump
Apr 18, 2026
Merged

fix(cli): reduce terminal redraw cursor movement#3381
wenshao merged 2 commits into
QwenLM:mainfrom
reidliu41:fix-terminal-redraw-jump

Conversation

@reidliu41

@reidliu41 reidliu41 commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

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:

QWEN_CODE_LEGACY_ERASE_LINES=1

Changes

  • Add a terminal redraw optimizer for Ink multiline erase sequences.
  • Install it only for interactive TTY sessions.
  • Restore the original stdout writer during cleanup.
  • Add unit tests for optimization behavior, passthrough writes, restore behavior, and fallback disabling.

Screenshots / Video Demo

Manual Verification

Ran an interactive streaming prompt with proxy environment variables removed:

  timeout 90s script -q -c "\
  stty rows 44 cols 179; \
  env -u NO_COLOR \
    qwen -y -i '请用中文输出30行编号,每行短句。不要调用工具。'" \
  /tmp/qwen-3144-fixed-host-no-proxy-44x179.typescript

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

🍏 🪟 🐧
npm run
npx
Docker
Podman - -
Seatbelt - -

Linked issues / bugs

Fixes #3144

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/cli/src/ui/utils/terminalRedrawOptimizer.ts Outdated
  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.
@reidliu41 reidliu41 force-pushed the fix-terminal-redraw-jump branch from 864aa2a to 5365ceb Compare April 17, 2026 23:37
@reidliu41

Copy link
Copy Markdown
Contributor Author

Good catch, you're right. The ESC[J version widened the erase semantics and could clear unrelated output below the Ink frame.
I've updated the implementation to avoid erase-down entirely. It now clears only the previous frame's lines, preserving Ink's bounded eraseLines() behavior while still avoiding repeated cursor-up operations.
I also added tests covering the no-ESC[J behavior and leaving small/two-line erase sequences unchanged.

) {
const version = await getCliVersion();
setWindowTitle(basename(workspaceRoot), settings);
const restoreTerminalRedrawOptimizer =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
const restoreTerminalRedrawOptimizer =
const restoreTerminalRedrawOptimizer =
process.stdout.isTTY && !config.getScreenReader()
? installTerminalRedrawOptimizer(process.stdout)
: () => {};
try {

— gpt-5.4 via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against HEAD 5365ceb:

  • The ESC[J] erase-down concern is fully addressed — optimizer now emits only ESC[2K per line; explicit test asserts not.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 <= 1 early-return keeps small frames untouched (no regression for 1-2 line cases)
  • Cleanup path restores original stdout.write at unmount with a defensive identity check
  • QWEN_CODE_LEGACY_ERASE_LINES=1 fallback 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 since log-update renders atomically

LGTM.

@wenshao wenshao merged commit 418acc5 into QwenLM:main Apr 18, 2026
13 checks passed
@tanzhenxin tanzhenxin added the TBD To Be Discussed label Apr 22, 2026
mabry1985 added a commit to protoLabsAI/protoCLI that referenced this pull request Apr 26, 2026
…#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#3381QwenLM#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>
xaelistic pushed a commit to xaelistic/qwen-code that referenced this pull request Jun 7, 2026
xaelistic pushed a commit to xaelistic/qwen-code that referenced this pull request Jun 7, 2026
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TBD To Be Discussed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Terminal scroll jumps up/down rapidly during agent execution and response streaming

4 participants