Root cause analysis: terminal scrolls to top during agent execution (Ink cursorUp+eraseLines)#34798
Closed
cruzlauroiii wants to merge 10 commits intoanthropics:mainfrom
Closed
Root cause analysis: terminal scrolls to top during agent execution (Ink cursorUp+eraseLines)#34798cruzlauroiii wants to merge 10 commits intoanthropics:mainfrom
cruzlauroiii wants to merge 10 commits intoanthropics:mainfrom
Conversation
Traced 5 specific triggers in cli.js v2.1.76 that cause terminal viewport to jump to top: 1. Ru6() full reset → LH8() → \x1B[2J\x1B[3J\x1B[H (cursor home) 2. enterAlternateScreen() → \x1B[2J\x1B[H 3. exitAlternateScreen() → \x1B[2J\x1B[H 4. handleResume() SIGCONT → \x1B[2J\x1B[H 5. repaint() → resets frame buffers → triggers Ru6() All flow through SH8() output writer. The \x1B[H (cursor home to row 1, col 1) is the direct cause in all cases. Source: cli.js v2.1.76 SHA-256: 38b8fd29d0817e5f75202b2bb211fe959d4b6a4f2224b8118dabf876e503b50b Build: 2026-03-14T00:12:49Z Fixes #34794 Related: #34400, #34765, #33814, #34052, #34503, #33624
ede7024 to
ae76572
Compare
patches/cli.js.scroll-fix.patch: Removes \x1B[H (cursor home) from 3 locations in cli.js: 1. LH8() - clearTerminal function (removes +HK6) 2. exitAlternateScreen() - leaving alt screen buffer 3. handleResume() - SIGCONT handler Keeps \x1B[2J (erase screen) intact. scripts/fix-scroll-to-top.ps1: PowerShell script that applies all 3 patches to npm cli.js. Supports -Uninstall to revert. Idempotent (detects already-patched). Source hashes (with bridge fix from PR #34789 already applied): Before: 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c After: fd47ae1e5cc7686f1b10d4d04e12eed9ce8ce7fdfdd4a4b6529672a61249ac80 Diff: -20 bytes (3x removal of \x1B[H = 4 chars + surrounding quotes) Fixes #34794
Renamed cli.js.scroll-fix.patch → scroll-to-top.patch Now includes actual diff format (-/+ lines) for all 3 fixes, plus SHA-256 hashes for original, bridge-patched, and fully-patched states. Fixes #34794
v1 only patched specific functions (LH8, exitAlternateScreen, handleResume). This didn't fix the issue because cursor-up also comes from: - Ink's hV7() clear function (line-by-line erase) - Inquirer prompt renderer (eraseLines + cursorUp per line) - Other code paths outside sync blocks v2 injects a global stdout.write interceptor that: - Tracks net cursor-up movement across ALL writes - Clamps total upward movement to terminal height - Resets counter on sync block end and cursor home - The cursor can never go above the viewport Hashes (with bridge fix from PR #34789): Before scroll fix: 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c After scroll v2: 31d7b99667a46ef3df3a0958e937582365acb5d295ae846eeeb623f598c8175a Fixes #34794
This was referenced Mar 16, 2026
The cursor-up clamping (v2) alone doesn't prevent scroll-to-top because Windows Terminal exits scroll mode on ANY stdout write, not just cursor-up. Two additional patches: - Render throttle 16ms→200ms: reduces viewport resets from 60fps to 5fps - Disable sync update mode (\x1b[?2026h/l): lets Windows Terminal's native auto-scroll prevention work per-write instead of batching everything into one viewport-resetting sync block All 3 patches combined: SHA-256: 96ed31561b6f04a48f67b583d0491fb3584b7d8a1ae185809569f47e8aaed838 Also documents that chrome-native-host.bat gets regenerated on startup, and native messaging manifest must NOT have UTF-8 BOM (PowerShell's default encoding adds BOM which breaks Chrome's JSON parser). Fixes #34794
Root cause identified: Windows Terminal bug microsoft/terminal#14774 SetConsoleCursorPosition always scrolls viewport to cursor position, even when cursor is already visible. Every Ink re-render triggers this. Previous approaches that failed: - v1: patching LH8/exitAltScreen/handleResume (only edge cases) - v2: stdout.write interceptor clamping cursor-up (WT bug triggers on ANY cursor positioning, not just cursor-up) - Disabling synchronized update mode (causes flickering) - All combined (flickering + still scrolls) v3 approach: aggressive render throttle (1fps) gives 1 full second of uninterrupted reading between viewport resets. Sync update stays enabled (no flickering). Trade-off: streaming text in ~1s chunks. The real fix must come from Microsoft (terminal#14774) or Anthropic (PTY proxy / append-only rendering mode). SHA-256 (bridge + throttle): e985041bdd17e85dba04ec605346714aa21e4d7be16889e06774a32e1bd1e410 Fixes #34794
Previous approaches (v1-v4) all failed because Windows Terminal bug microsoft/terminal#14774 triggers viewport scroll on ANY cursor positioning — not just cursor-up, not just sync blocks. v5 takes a fundamentally different approach: - Buffer ALL Ink renders (sync blocks) — screen stays frozen during work - Flush only when user types (stdin data) or after 5s of no renders - User input means they're at the prompt (bottom) — viewport follows naturally - 5s idle means Claude finished — show final state - Non-sync writes pass through immediately This eliminates viewport jumping entirely during active work. Trade-off: screen is frozen while Claude works (spinner/progress not visible). SHA-256 (bridge + scroll v5): 580641c8f9c762b31fce20a9fcfc85b158b1ca52dded5d5495de29c7ec234f2c Fixes #34794
Strictly: if user has not typed, ZERO screen updates. Screen updates ONLY when user types (stdin data = at prompt = bottom). No timer. No auto-flush. No exceptions. Removes the 5s auto-flush timer from v5 which still caused a viewport jump when the user was scrolled up reading. SHA-256 (bridge + scroll v6): 626975ae7894859cfe301c761272c147a61f20a320ab2427807df063e64cfbf1 Fixes #34794
v6 only kept the LAST frame. Since each frame is a diff against the previous, flushing just the last frame showed garbled/outdated content (diff chain was broken). v7 keeps ALL frames and replays the entire chain when user types. The diff chain is preserved so the screen shows the correct latest state. SHA-256 (bridge + scroll v7): a57e8bc8904aeeccbcabae1656543c95b54d2140e8a9afe91829bf14d527e52e Fixes #34794
Ctrl+6 toggles screen freeze on/off. Frozen: Ink renders buffered, screen stays still, user can scroll freely. Unfrozen: replays all buffered frames, resumes live output. Terminal title shows [FROZEN] state. Removed scroll-to-top-analysis.md and fix-scroll-to-top.ps1. SHA-256 (bridge + freeze): 85906e42ba628058519f727ac42fb0dac9c8c2ea1304b9d9ae0e5dad51793ebd Fixes #34794
|
Pretty funny adding this hack as a PR good work |
Author
|
Superseded by plugin-based approach in PR #35683. The scroll-to-top fix is now packaged as a Claude Code plugin in plugins/scroll-fix/ with automatic cursor-up clamping, Ctrl+6 freeze toggle (from this PR), and portable install/uninstall scripts. |
This was referenced Mar 19, 2026
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.
Summary
Ctrl+6 freeze toggle for terminal scroll-to-top issue on Windows Terminal.
How it works
[FROZEN])Root cause
microsoft/terminal#14774 —
SetConsoleCursorPositionalways scrolls viewport to cursor. Every Ink re-render triggers this. ConPTY does not expose scroll position to the process. Cannot be fixed automatically — user toggle is the cleanest solution.Patch
Single line injected before
import{in cli.js. Interceptsprocess.stdout.writeto buffer Ink sync blocks when frozen. Listens onprocess.stdinfor\x1e(Ctrl+6) to toggle.Hashes
38b8fd29d0817e5f75202b2bb211fe959d4b6a4f2224b8118dabf876e503b50b6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c85906e42ba628058519f727ac42fb0dac9c8c2ea1304b9d9ae0e5dad51793ebdcli.js v2.1.76, build 2026-03-14T00:12:49Z
Fixes #34794
Upstream: microsoft/terminal#14774