Skip to content

Root cause analysis: terminal scrolls to top during agent execution (Ink cursorUp+eraseLines)#34798

Closed
cruzlauroiii wants to merge 10 commits intoanthropics:mainfrom
cruzlauroiii:fix/windows-scroll-to-top
Closed

Root cause analysis: terminal scrolls to top during agent execution (Ink cursorUp+eraseLines)#34798
cruzlauroiii wants to merge 10 commits intoanthropics:mainfrom
cruzlauroiii:fix/windows-scroll-to-top

Conversation

@cruzlauroiii
Copy link
Copy Markdown

@cruzlauroiii cruzlauroiii commented Mar 16, 2026

Summary

Ctrl+6 freeze toggle for terminal scroll-to-top issue on Windows Terminal.

How it works

  • Ctrl+6 → freeze screen (renders buffered, tab title shows [FROZEN])
  • Ctrl+6 again → unfreeze (replays all buffered frames, live output resumes)

Root cause

microsoft/terminal#14774SetConsoleCursorPosition always 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. Intercepts process.stdout.write to buffer Ink sync blocks when frozen. Listens on process.stdin for \x1e (Ctrl+6) to toggle.

Hashes

State SHA-256
Original 38b8fd29d0817e5f75202b2bb211fe959d4b6a4f2224b8118dabf876e503b50b
+ bridge (#34789) 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c
+ freeze toggle 85906e42ba628058519f727ac42fb0dac9c8c2ea1304b9d9ae0e5dad51793ebd

cli.js v2.1.76, build 2026-03-14T00:12:49Z

Fixes #34794
Upstream: microsoft/terminal#14774

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
@cruzlauroiii cruzlauroiii force-pushed the fix/windows-scroll-to-top branch from ede7024 to ae76572 Compare March 16, 2026 01:21
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
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
@cruzlauroiii cruzlauroiii closed this by deleting the head repository Mar 16, 2026
@gherghett
Copy link
Copy Markdown

Pretty funny adding this hack as a PR good work

@cruzlauroiii
Copy link
Copy Markdown
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Terminal scrolls to top during agent execution (Windows Terminal + PowerShell)

3 participants