From ae765725a56ed481aa6ee39d0aab730ebd8bc10c Mon Sep 17 00:00:00 2001 From: LAURO III CRUZ Date: Mon, 16 Mar 2026 09:21:38 +0800 Subject: [PATCH 01/10] Add root cause analysis for scroll-to-top bug during agent execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- patches/scroll-to-top-analysis.md | 168 ++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 patches/scroll-to-top-analysis.md diff --git a/patches/scroll-to-top-analysis.md b/patches/scroll-to-top-analysis.md new file mode 100644 index 0000000000..d460874a82 --- /dev/null +++ b/patches/scroll-to-top-analysis.md @@ -0,0 +1,168 @@ +# Scroll-to-Top Bug — Full Root Cause Analysis + +## Source +- **cli.js version:** 2.1.76 +- **Build:** 2026-03-14T00:12:49Z +- **SHA-256:** `38b8fd29d0817e5f75202b2bb211fe959d4b6a4f2224b8118dabf876e503b50b` + +## Architecture + +Claude Code uses a custom **Ink** (React for CLI) renderer with a dual frame-buffer diffing system: + +``` +React component tree → Yoga layout → screen buffer → diff vs previous buffer → emit render ops → SH8() writes to stdout +``` + +## 5 Scroll-to-Top Triggers + +### 1. `Ru6()` — Full Screen Reset (line ~767) + +Called when the diff engine determines screen state has diverged too much: + +```js +function Ru6(A, q, K) { + let Y = new Kj8({x:0, y:0}, A.viewport.width); + return qd3(Y, A, K), + [{type: "clearTerminal", reason: q}, ...Y.diff]; +} +``` + +Emits `{type: "clearTerminal"}` which triggers `LH8()`: + +```js +function LH8() { + // All platforms: \x1B[2J (erase screen) + \x1B[3J (clear scrollback) + \x1B[H (CURSOR HOME) + if (process.platform === "win32") + if (HU3()) return jO1 + $H8 + HK6; // \x1B[2J\x1B[3J\x1B[H + else return jO1 + wU3; // \x1B[2J\x1B[0f + return jO1 + $H8 + HK6; // \x1B[2J\x1B[3J\x1B[H +} +``` + +**`\x1B[H` = cursor to row 1, column 1 = TOP OF TERMINAL.** This is the primary trigger. + +Triggered by: +- Viewport resize +- Scrollback changes +- Content changes above visible region +- Cursor past screen height + content shrink + +### 2. `enterAlternateScreen()` (line ~780) + +```js +enterAlternateScreen() { + this.options.stdout.write( + "\x1B[?1049h" // enter alt screen buffer + + "\x1B[?1004l" // disable focus events + + "\x1B[0m" // reset styles + + "\x1B[?25h" // show cursor + + "\x1B[2J\x1B[H" // ERASE SCREEN + CURSOR HOME ← scroll-to-top + ); +} +``` + +### 3. `exitAlternateScreen()` (line ~780) + +```js +exitAlternateScreen() { + this.options.stdout.write( + "\x1B[2J\x1B[H" // ERASE SCREEN + CURSOR HOME ← scroll-to-top + + "\x1B[?1049l" // leave alt screen buffer + + "\x1B[?25l" // hide cursor + ); +} +``` + +Both are used during tool execution (e.g., thinkback animation). + +### 4. `handleResume()` — SIGCONT handler (line ~780) + +```js +handleResume = () => { + if (this.altScreenActive) { + this.options.stdout.write( + "\x1B[?1049h" // enter alt screen + + "\x1B[2J\x1B[H" // ERASE SCREEN + CURSOR HOME ← scroll-to-top + ); + this.resetFramesForAltScreen(); + return; + } + // Non-alt-screen: resets frames, calls repaint() +} +``` + +### 5. `repaint()` — Frame buffer reset (line ~780) + +```js +repaint() { + this.frontFrame = js(...); // fresh empty screen buffer + this.backFrame = js(...); // fresh empty screen buffer + this.log.reset(); +} +``` + +Resets both frame buffers to empty. Next `onRender()` detects everything changed → triggers `Ru6()` (trigger #1) → `clearTerminal` → `\x1B[H`. + +### Output writer: `SH8()` (line ~755) + +All triggers flow through this function: + +```js +function SH8(A, q, K = false) { + let Y = !K, z = Y ? kk7 : ""; // synchronized update begin \x1B[?2026h + for (let _ of q) switch (_.type) { + case "clearTerminal": z += LH8(); break; // ← SCROLL-TO-TOP + case "cursorMove": z += RV7(_.x, _.y); break; + case "cursorTo": z += yV7(_.col); break; + // ... + } + if (Y) z += Ek7; // synchronized update end \x1B[?2026l + A.stdout.write(z); +} +``` + +### ANSI constants (line ~755) + +```js +jO1 = "\x1B[2J" // erase screen +$H8 = "\x1B[3J" // clear scrollback +HK6 = "\x1B[H" // cursor home (TOP-LEFT) — the scroll-to-top culprit +``` + +## Suggested Fixes + +### Fix A: Remove `\x1B[H` from `LH8()` on non-alt-screen renders + +The cursor-home sequence is unnecessary when the diff engine already positions the cursor correctly. Only use it in alt-screen mode. + +```js +function LH8() { + // Don't include \x1B[H — let the diff engine handle cursor positioning + return jO1 + $H8; // erase screen + clear scrollback, but NO cursor home +} +``` + +### Fix B: Guard `Ru6()` against triggering during streaming + +Add a condition to suppress full resets while content is actively streaming: + +```js +function Ru6(A, q, K) { + if (A.isStreaming) return; // don't full-reset during streaming + // ...existing code... +} +``` + +### Fix C: Use scroll regions to contain cursor movement + +``` +\x1B[;r — set scroll region (isolates cursor movement) +\x1B[r — reset scroll region +``` + +### Fix D: Replace `clearTerminal` with incremental diff + +Instead of clearing everything and redrawing, only update changed lines. + +## Related issues +#34794, #34400, #34765, #33814, #34052, #34503, #33624 From 13490a341c2d2c4a6f2fb51737914d62710a7078 Mon Sep 17 00:00:00 2001 From: LAURO III CRUZ Date: Mon, 16 Mar 2026 09:27:11 +0800 Subject: [PATCH 02/10] Add cli.js patch and PowerShell fix script for scroll-to-top bug 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 --- patches/cli.js.scroll-fix.patch | 41 ++++++++++++ scripts/fix-scroll-to-top.ps1 | 110 ++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 patches/cli.js.scroll-fix.patch create mode 100644 scripts/fix-scroll-to-top.ps1 diff --git a/patches/cli.js.scroll-fix.patch b/patches/cli.js.scroll-fix.patch new file mode 100644 index 0000000000..8d55a55af2 --- /dev/null +++ b/patches/cli.js.scroll-fix.patch @@ -0,0 +1,41 @@ +# Patch: Remove \x1B[H (cursor home) from terminal clear sequences +# Target: @anthropic-ai/claude-code@2.1.76 cli.js +# +# Before patch (with bridge fix applied): +# SHA-256: 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c +# After patch: +# SHA-256: fd47ae1e5cc7686f1b10d4d04e12eed9ce8ce7fdfdd4a4b6529672a61249ac80 +# +# What this does: +# Removes \x1B[H (cursor home = row 1, col 1) from 3 locations: +# 1. LH8() - clearTerminal function (primary trigger) +# 2. exitAlternateScreen() - leaving alt screen buffer +# 3. handleResume() - SIGCONT handler +# +# The \x1B[2J (erase screen) is kept — only the cursor-home is removed. +# The diff engine already handles cursor positioning correctly. +# +# Why: +# \x1B[H moves the cursor to the top-left corner of the terminal. +# Windows Terminal, iTerm2, and Terminal.app follow the cursor position, +# snapping the viewport to wherever the cursor lands — which is the TOP. +# This happens on every re-render during agent execution, making it +# impossible to scroll up and read previous output. +# +# Fix 1: LH8() — clearTerminal +# Remove +HK6 (which is \x1B[H) from return values +# +# - function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8+HK6;else return jO1+wU3;return jO1+$H8+HK6} +# + function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8;else return jO1+wU3;return jO1+$H8} +# +# Fix 2: exitAlternateScreen() +# Remove \x1B[H from the stdout.write call +# +# - exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J\x1B[H"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l") +# + exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l") +# +# Fix 3: handleResume() — SIGCONT +# Remove \x1B[H from the stdout.write call +# +# - this.options.stdout.write(kH8+"\x1B[2J\x1B[H"+(this.altScreenMouseTracking?NO1:"")) +# + this.options.stdout.write(kH8+"\x1B[2J"+(this.altScreenMouseTracking?NO1:"")) diff --git a/scripts/fix-scroll-to-top.ps1 b/scripts/fix-scroll-to-top.ps1 new file mode 100644 index 0000000000..7026ed8b31 --- /dev/null +++ b/scripts/fix-scroll-to-top.ps1 @@ -0,0 +1,110 @@ +# fix-scroll-to-top.ps1 +# Fixes terminal scroll-to-top during Claude Code agent execution. +# +# Root cause: Ink's rendering emits \x1B[H (cursor home = row 1, col 1) +# in clearTerminal, exitAlternateScreen, and handleResume. Terminal +# emulators follow the cursor, snapping viewport to the top. +# +# This patch removes \x1B[H from 3 locations, keeping \x1B[2J (erase +# screen) intact. The diff engine handles cursor positioning correctly +# without cursor-home. +# +# Usage: +# powershell -ExecutionPolicy Bypass -File fix-scroll-to-top.ps1 +# +# Requires: npm-installed Claude Code (npm install -g @anthropic-ai/claude-code) +# Re-run after every npm update. +# +# Related: https://github.com/anthropics/claude-code/issues/34794 +# https://github.com/anthropics/claude-code/pull/34798 + +param( + [switch]$Uninstall +) + +$ErrorActionPreference = "Stop" +$cliPath = "$env:APPDATA\npm\node_modules\@anthropic-ai\claude-code\cli.js" + +if (-not (Test-Path $cliPath)) { + Write-Host "ERROR: Claude Code npm install not found at $cliPath" -ForegroundColor Red + Write-Host "Run: npm install -g @anthropic-ai/claude-code" -ForegroundColor Yellow + exit 1 +} + +$code = [System.IO.File]::ReadAllText($cliPath, [System.Text.Encoding]::UTF8) + +# --- Patch definitions (3 fixes) --- + +# Fix 1: LH8() - clearTerminal: remove +HK6 (cursor home) +$fix1_orig = 'function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8+HK6;else return jO1+wU3;return jO1+$H8+HK6}' +$fix1_new = 'function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8;else return jO1+wU3;return jO1+$H8}' + +# Fix 2: exitAlternateScreen() - remove \x1B[H +$fix2_orig = 'exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J\x1B[H"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l")' +$fix2_new = 'exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l")' + +# Fix 3: handleResume() SIGCONT - remove \x1B[H +$fix3_orig = 'this.options.stdout.write(kH8+"\x1B[2J\x1B[H"+(this.altScreenMouseTracking?NO1:""))' +$fix3_new = 'this.options.stdout.write(kH8+"\x1B[2J"+(this.altScreenMouseTracking?NO1:""))' + +# --- Uninstall --- + +if ($Uninstall) { + $reverted = 0 + if ($code.Contains($fix1_new)) { $code = $code.Replace($fix1_new, $fix1_orig); $reverted++ } + if ($code.Contains($fix2_new)) { $code = $code.Replace($fix2_new, $fix2_orig); $reverted++ } + if ($code.Contains($fix3_new)) { $code = $code.Replace($fix3_new, $fix3_orig); $reverted++ } + if ($reverted -gt 0) { + [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) + Write-Host "Reverted $reverted patch(es)." -ForegroundColor Green + } else { + Write-Host "No patches to revert." -ForegroundColor Yellow + } + exit 0 +} + +# --- Apply patches --- + +$applied = 0 +$skipped = 0 + +# Fix 1 +if ($code.Contains($fix1_new)) { + Write-Host "[Fix 1/3] LH8() already patched." -ForegroundColor Green; $skipped++ +} elseif ($code.Contains($fix1_orig)) { + $code = $code.Replace($fix1_orig, $fix1_new); $applied++ + Write-Host "[Fix 1/3] LH8() patched - removed cursor home from clearTerminal." -ForegroundColor Green +} else { + Write-Host "[Fix 1/3] LH8() pattern not found - version may have changed." -ForegroundColor Red +} + +# Fix 2 +if ($code.Contains($fix2_new)) { + Write-Host "[Fix 2/3] exitAlternateScreen() already patched." -ForegroundColor Green; $skipped++ +} elseif ($code.Contains($fix2_orig)) { + $code = $code.Replace($fix2_orig, $fix2_new); $applied++ + Write-Host "[Fix 2/3] exitAlternateScreen() patched - removed cursor home." -ForegroundColor Green +} else { + Write-Host "[Fix 2/3] exitAlternateScreen() pattern not found." -ForegroundColor Red +} + +# Fix 3 +if ($code.Contains($fix3_new)) { + Write-Host "[Fix 3/3] handleResume() already patched." -ForegroundColor Green; $skipped++ +} elseif ($code.Contains($fix3_orig)) { + $code = $code.Replace($fix3_orig, $fix3_new); $applied++ + Write-Host "[Fix 3/3] handleResume() patched - removed cursor home from SIGCONT." -ForegroundColor Green +} else { + Write-Host "[Fix 3/3] handleResume() pattern not found." -ForegroundColor Red +} + +# --- Write --- + +if ($applied -gt 0) { + [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) +} + +Write-Host "" +Write-Host "Done: $applied applied, $skipped already patched." -ForegroundColor Cyan +Write-Host "Run Claude Code via: $env:APPDATA\npm\claude.cmd" -ForegroundColor White +Write-Host "To revert: powershell -File $PSCommandPath -Uninstall" -ForegroundColor Gray From cdb1de132756c9f36d0e2970c12f0b25c32b1341 Mon Sep 17 00:00:00 2001 From: LAURO III CRUZ Date: Mon, 16 Mar 2026 09:38:34 +0800 Subject: [PATCH 03/10] Rename patch, add actual diff with before/after lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- patches/cli.js.scroll-fix.patch | 41 --------------------------------- patches/scroll-to-top.patch | 30 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 41 deletions(-) delete mode 100644 patches/cli.js.scroll-fix.patch create mode 100644 patches/scroll-to-top.patch diff --git a/patches/cli.js.scroll-fix.patch b/patches/cli.js.scroll-fix.patch deleted file mode 100644 index 8d55a55af2..0000000000 --- a/patches/cli.js.scroll-fix.patch +++ /dev/null @@ -1,41 +0,0 @@ -# Patch: Remove \x1B[H (cursor home) from terminal clear sequences -# Target: @anthropic-ai/claude-code@2.1.76 cli.js -# -# Before patch (with bridge fix applied): -# SHA-256: 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c -# After patch: -# SHA-256: fd47ae1e5cc7686f1b10d4d04e12eed9ce8ce7fdfdd4a4b6529672a61249ac80 -# -# What this does: -# Removes \x1B[H (cursor home = row 1, col 1) from 3 locations: -# 1. LH8() - clearTerminal function (primary trigger) -# 2. exitAlternateScreen() - leaving alt screen buffer -# 3. handleResume() - SIGCONT handler -# -# The \x1B[2J (erase screen) is kept — only the cursor-home is removed. -# The diff engine already handles cursor positioning correctly. -# -# Why: -# \x1B[H moves the cursor to the top-left corner of the terminal. -# Windows Terminal, iTerm2, and Terminal.app follow the cursor position, -# snapping the viewport to wherever the cursor lands — which is the TOP. -# This happens on every re-render during agent execution, making it -# impossible to scroll up and read previous output. -# -# Fix 1: LH8() — clearTerminal -# Remove +HK6 (which is \x1B[H) from return values -# -# - function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8+HK6;else return jO1+wU3;return jO1+$H8+HK6} -# + function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8;else return jO1+wU3;return jO1+$H8} -# -# Fix 2: exitAlternateScreen() -# Remove \x1B[H from the stdout.write call -# -# - exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J\x1B[H"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l") -# + exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l") -# -# Fix 3: handleResume() — SIGCONT -# Remove \x1B[H from the stdout.write call -# -# - this.options.stdout.write(kH8+"\x1B[2J\x1B[H"+(this.altScreenMouseTracking?NO1:"")) -# + this.options.stdout.write(kH8+"\x1B[2J"+(this.altScreenMouseTracking?NO1:"")) diff --git a/patches/scroll-to-top.patch b/patches/scroll-to-top.patch new file mode 100644 index 0000000000..d16ddfdf50 --- /dev/null +++ b/patches/scroll-to-top.patch @@ -0,0 +1,30 @@ +--- a/cli.js ++++ b/cli.js +@@ Fix 1: LH8() — clearTerminal removes cursor home (HK6 = \x1B[H) +-function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8+HK6;else return jO1+wU3;return jO1+$H8+HK6} ++function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8;else return jO1+wU3;return jO1+$H8} + +@@ Fix 2: exitAlternateScreen() — remove \x1B[H from stdout.write +-exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J\x1B[H"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l") ++exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l") + +@@ Fix 3: handleResume() SIGCONT — remove \x1B[H from stdout.write +-this.options.stdout.write(kH8+"\x1B[2J\x1B[H"+(this.altScreenMouseTracking?NO1:"")) ++this.options.stdout.write(kH8+"\x1B[2J"+(this.altScreenMouseTracking?NO1:"")) + +# Target: @anthropic-ai/claude-code@2.1.76 cli.js +# Build: 2026-03-14T00:12:49Z +# +# Original (unpatched) SHA-256: +# 38b8fd29d0817e5f75202b2bb211fe959d4b6a4f2224b8118dabf876e503b50b +# +# With bridge fix (PR #34789) applied: +# 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c +# +# With both bridge + scroll fixes applied: +# fd47ae1e5cc7686f1b10d4d04e12eed9ce8ce7fdfdd4a4b6529672a61249ac80 +# +# What: Removes \x1B[H (cursor home = move to row 1, col 1) from 3 places. +# Keeps \x1B[2J (erase screen). The diff engine positions the cursor. +# +# Why: \x1B[H makes the terminal viewport snap to the top on every re-render. From 4c37ee03639fae33e01f4c41d689378930ecba90 Mon Sep 17 00:00:00 2001 From: LAURO III CRUZ Date: Mon, 16 Mar 2026 11:13:38 +0800 Subject: [PATCH 04/10] Update scroll fix to v2: stateful stdout.write interceptor 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 --- patches/scroll-to-top.patch | 28 +++++------ scripts/fix-scroll-to-top.ps1 | 90 +++++++++++------------------------ 2 files changed, 41 insertions(+), 77 deletions(-) diff --git a/patches/scroll-to-top.patch b/patches/scroll-to-top.patch index d16ddfdf50..906b9318f6 100644 --- a/patches/scroll-to-top.patch +++ b/patches/scroll-to-top.patch @@ -1,16 +1,9 @@ --- a/cli.js +++ b/cli.js -@@ Fix 1: LH8() — clearTerminal removes cursor home (HK6 = \x1B[H) --function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8+HK6;else return jO1+wU3;return jO1+$H8+HK6} -+function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8;else return jO1+wU3;return jO1+$H8} -@@ Fix 2: exitAlternateScreen() — remove \x1B[H from stdout.write --exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J\x1B[H"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l") -+exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l") +# Inject before the first `import{` statement in cli.js: -@@ Fix 3: handleResume() SIGCONT — remove \x1B[H from stdout.write --this.options.stdout.write(kH8+"\x1B[2J\x1B[H"+(this.altScreenMouseTracking?NO1:"")) -+this.options.stdout.write(kH8+"\x1B[2J"+(this.altScreenMouseTracking?NO1:"")) ++/* SCROLL_FIX_v2 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _gr=function(){return process.stdout.rows||24};var _up=0;process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);var dn=0,m,re=/\x1b\[(\d+)B/g;while(m=re.exec(s))dn+=parseInt(m[1],10);var nl=0;for(var i=0;imx){var a=Math.max(0,v-(_up-mx));_up=mx;if(a<=0)return"";return"\x1b["+a+"A"}return m});if(s.indexOf("\x1b[?2026l")!==-1)_up=0;if(s.indexOf("\x1b[H")!==-1)_up=0;return _ow(s,e,c)};})(); # Target: @anthropic-ai/claude-code@2.1.76 cli.js # Build: 2026-03-14T00:12:49Z @@ -21,10 +14,17 @@ # With bridge fix (PR #34789) applied: # 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c # -# With both bridge + scroll fixes applied: -# fd47ae1e5cc7686f1b10d4d04e12eed9ce8ce7fdfdd4a4b6529672a61249ac80 +# With both bridge + scroll v2 fixes applied: +# 31d7b99667a46ef3df3a0958e937582365acb5d295ae846eeeb623f598c8175a # -# What: Removes \x1B[H (cursor home = move to row 1, col 1) from 3 places. -# Keeps \x1B[2J (erase screen). The diff engine positions the cursor. +# Approach: Stateful stdout.write interceptor that tracks net cursor-up +# movement across ALL writes (not just sync blocks). Clamps total upward +# movement to terminal height. Resets on sync block end and cursor home. # -# Why: \x1B[H makes the terminal viewport snap to the top on every re-render. +# This catches cursor-up from: +# - Ink's SH8 renderer (sync blocks) +# - Inquirer prompt renderer (eraseLines + cursorUp per line) +# - Any other code path that moves the cursor up +# +# The cursor can never go more than (terminal rows - 1) lines above its +# lowest point, keeping it within the viewport. diff --git a/scripts/fix-scroll-to-top.ps1 b/scripts/fix-scroll-to-top.ps1 index 7026ed8b31..655235080e 100644 --- a/scripts/fix-scroll-to-top.ps1 +++ b/scripts/fix-scroll-to-top.ps1 @@ -1,13 +1,13 @@ -# fix-scroll-to-top.ps1 +# fix-scroll-to-top.ps1 (v2) # Fixes terminal scroll-to-top during Claude Code agent execution. # -# Root cause: Ink's rendering emits \x1B[H (cursor home = row 1, col 1) -# in clearTerminal, exitAlternateScreen, and handleResume. Terminal -# emulators follow the cursor, snapping viewport to the top. +# Root cause: Ink's rendering and the prompt renderer emit cursor-up (\x1B[nA) +# sequences that move the cursor above the viewport. Terminal emulators follow +# the cursor, snapping the viewport to the top. # -# This patch removes \x1B[H from 3 locations, keeping \x1B[2J (erase -# screen) intact. The diff engine handles cursor positioning correctly -# without cursor-home. +# This patch injects a stateful stdout.write interceptor into cli.js that +# tracks net cursor-up movement across ALL writes and clamps it to the +# terminal height. The cursor can never leave the viewport. # # Usage: # powershell -ExecutionPolicy Bypass -File fix-scroll-to-top.ps1 @@ -33,78 +33,42 @@ if (-not (Test-Path $cliPath)) { $code = [System.IO.File]::ReadAllText($cliPath, [System.Text.Encoding]::UTF8) -# --- Patch definitions (3 fixes) --- +$marker = '/* SCROLL_FIX_v2 */' -# Fix 1: LH8() - clearTerminal: remove +HK6 (cursor home) -$fix1_orig = 'function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8+HK6;else return jO1+wU3;return jO1+$H8+HK6}' -$fix1_new = 'function LH8(){if(process.platform==="win32")if(HU3())return jO1+$H8;else return jO1+wU3;return jO1+$H8}' - -# Fix 2: exitAlternateScreen() - remove \x1B[H -$fix2_orig = 'exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J\x1B[H"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l")' -$fix2_new = 'exitAlternateScreen(){if(this.options.stdout.write("\x1B[2J"+(this.altScreenActive?NO1:"\x1B[?1049l")+"\x1B[?25l")' - -# Fix 3: handleResume() SIGCONT - remove \x1B[H -$fix3_orig = 'this.options.stdout.write(kH8+"\x1B[2J\x1B[H"+(this.altScreenMouseTracking?NO1:""))' -$fix3_new = 'this.options.stdout.write(kH8+"\x1B[2J"+(this.altScreenMouseTracking?NO1:""))' +# The interceptor code (single line, injected before first import) +$interceptor = '/* SCROLL_FIX_v2 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _gr=function(){return process.stdout.rows||24};var _up=0;process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);var dn=0,m,re=/\x1b\[(\d+)B/g;while(m=re.exec(s))dn+=parseInt(m[1],10);var nl=0;for(var i=0;imx){var a=Math.max(0,v-(_up-mx));_up=mx;if(a<=0)return"";return"\x1b["+a+"A"}return m});if(s.indexOf("\x1b[?2026l")!==-1)_up=0;if(s.indexOf("\x1b[H")!==-1)_up=0;return _ow(s,e,c)};})();' # --- Uninstall --- if ($Uninstall) { - $reverted = 0 - if ($code.Contains($fix1_new)) { $code = $code.Replace($fix1_new, $fix1_orig); $reverted++ } - if ($code.Contains($fix2_new)) { $code = $code.Replace($fix2_new, $fix2_orig); $reverted++ } - if ($code.Contains($fix3_new)) { $code = $code.Replace($fix3_new, $fix3_orig); $reverted++ } - if ($reverted -gt 0) { + if ($code.Contains($marker)) { + $start = $code.IndexOf($marker) + $end = $code.IndexOf('})();', $start) + 5 + $code = $code.Substring(0, $start) + $code.Substring($end + 1) [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) - Write-Host "Reverted $reverted patch(es)." -ForegroundColor Green + Write-Host "Reverted scroll fix." -ForegroundColor Green } else { - Write-Host "No patches to revert." -ForegroundColor Yellow + Write-Host "No scroll fix to revert." -ForegroundColor Yellow } exit 0 } -# --- Apply patches --- - -$applied = 0 -$skipped = 0 - -# Fix 1 -if ($code.Contains($fix1_new)) { - Write-Host "[Fix 1/3] LH8() already patched." -ForegroundColor Green; $skipped++ -} elseif ($code.Contains($fix1_orig)) { - $code = $code.Replace($fix1_orig, $fix1_new); $applied++ - Write-Host "[Fix 1/3] LH8() patched - removed cursor home from clearTerminal." -ForegroundColor Green -} else { - Write-Host "[Fix 1/3] LH8() pattern not found - version may have changed." -ForegroundColor Red -} +# --- Apply --- -# Fix 2 -if ($code.Contains($fix2_new)) { - Write-Host "[Fix 2/3] exitAlternateScreen() already patched." -ForegroundColor Green; $skipped++ -} elseif ($code.Contains($fix2_orig)) { - $code = $code.Replace($fix2_orig, $fix2_new); $applied++ - Write-Host "[Fix 2/3] exitAlternateScreen() patched - removed cursor home." -ForegroundColor Green -} else { - Write-Host "[Fix 2/3] exitAlternateScreen() pattern not found." -ForegroundColor Red +if ($code.Contains($marker)) { + Write-Host "Already patched." -ForegroundColor Green + exit 0 } -# Fix 3 -if ($code.Contains($fix3_new)) { - Write-Host "[Fix 3/3] handleResume() already patched." -ForegroundColor Green; $skipped++ -} elseif ($code.Contains($fix3_orig)) { - $code = $code.Replace($fix3_orig, $fix3_new); $applied++ - Write-Host "[Fix 3/3] handleResume() patched - removed cursor home from SIGCONT." -ForegroundColor Green -} else { - Write-Host "[Fix 3/3] handleResume() pattern not found." -ForegroundColor Red +$insertAt = $code.IndexOf('import{') +if ($insertAt -eq -1) { + Write-Host "ERROR: Could not find import statement in cli.js" -ForegroundColor Red + exit 1 } -# --- Write --- - -if ($applied -gt 0) { - [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) -} +$code = $code.Substring(0, $insertAt) + $interceptor + "`n" + $code.Substring($insertAt) +[System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) -Write-Host "" -Write-Host "Done: $applied applied, $skipped already patched." -ForegroundColor Cyan +Write-Host "Applied scroll fix v2 to cli.js" -ForegroundColor Green Write-Host "Run Claude Code via: $env:APPDATA\npm\claude.cmd" -ForegroundColor White Write-Host "To revert: powershell -File $PSCommandPath -Uninstall" -ForegroundColor Gray From 01374bbcff023e5802b5dea5f39cc39f05bd743c Mon Sep 17 00:00:00 2001 From: LAURO III CRUZ Date: Mon, 16 Mar 2026 13:07:19 +0800 Subject: [PATCH 05/10] Update scroll fix: add render throttle + disable synchronized update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- patches/scroll-to-top.patch | 32 +++++++------- scripts/fix-scroll-to-top.ps1 | 78 ++++++++++++++++++++--------------- 2 files changed, 61 insertions(+), 49 deletions(-) diff --git a/patches/scroll-to-top.patch b/patches/scroll-to-top.patch index 906b9318f6..31643a0365 100644 --- a/patches/scroll-to-top.patch +++ b/patches/scroll-to-top.patch @@ -1,10 +1,24 @@ --- a/cli.js +++ b/cli.js -# Inject before the first `import{` statement in cli.js: +# === Patch 1: Inject before the first `import{` statement === +# Stateful stdout.write interceptor — clamps cursor-up across all writes +/* SCROLL_FIX_v2 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _gr=function(){return process.stdout.rows||24};var _up=0;process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);var dn=0,m,re=/\x1b\[(\d+)B/g;while(m=re.exec(s))dn+=parseInt(m[1],10);var nl=0;for(var i=0;imx){var a=Math.max(0,v-(_up-mx));_up=mx;if(a<=0)return"";return"\x1b["+a+"A"}return m});if(s.indexOf("\x1b[?2026l")!==-1)_up=0;if(s.indexOf("\x1b[H")!==-1)_up=0;return _ow(s,e,c)};})(); +# === Patch 2: Increase render throttle from 16ms to 200ms === +# Reduces re-renders from 60fps to 5fps — less frequent viewport resets + +-var SK6=16; ++var SK6=200; + +# === Patch 3: Disable synchronized update mode === +# Lets Windows Terminal's native auto-scroll prevention work per-write +# instead of batching everything into one viewport-resetting sync block + +-kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE) ++kk7="",Ek7="" + # Target: @anthropic-ai/claude-code@2.1.76 cli.js # Build: 2026-03-14T00:12:49Z # @@ -14,17 +28,5 @@ # With bridge fix (PR #34789) applied: # 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c # -# With both bridge + scroll v2 fixes applied: -# 31d7b99667a46ef3df3a0958e937582365acb5d295ae846eeeb623f598c8175a -# -# Approach: Stateful stdout.write interceptor that tracks net cursor-up -# movement across ALL writes (not just sync blocks). Clamps total upward -# movement to terminal height. Resets on sync block end and cursor home. -# -# This catches cursor-up from: -# - Ink's SH8 renderer (sync blocks) -# - Inquirer prompt renderer (eraseLines + cursorUp per line) -# - Any other code path that moves the cursor up -# -# The cursor can never go more than (terminal rows - 1) lines above its -# lowest point, keeping it within the viewport. +# With all scroll fixes (v2 + throttle + sync disable) applied: +# 96ed31561b6f04a48f67b583d0491fb3584b7d8a1ae185809569f47e8aaed838 diff --git a/scripts/fix-scroll-to-top.ps1 b/scripts/fix-scroll-to-top.ps1 index 655235080e..e2a5bd726e 100644 --- a/scripts/fix-scroll-to-top.ps1 +++ b/scripts/fix-scroll-to-top.ps1 @@ -1,13 +1,10 @@ -# fix-scroll-to-top.ps1 (v2) +# fix-scroll-to-top.ps1 (v2 + throttle + sync disable) # Fixes terminal scroll-to-top during Claude Code agent execution. # -# Root cause: Ink's rendering and the prompt renderer emit cursor-up (\x1B[nA) -# sequences that move the cursor above the viewport. Terminal emulators follow -# the cursor, snapping the viewport to the top. -# -# This patch injects a stateful stdout.write interceptor into cli.js that -# tracks net cursor-up movement across ALL writes and clamps it to the -# terminal height. The cursor can never leave the viewport. +# 3 patches: +# 1. Stateful stdout.write interceptor — clamps cursor-up across all writes +# 2. Render throttle 16ms→200ms — reduces viewport resets from 60fps to 5fps +# 3. Disable synchronized update mode — lets terminal's native auto-scroll prevention work # # Usage: # powershell -ExecutionPolicy Bypass -File fix-scroll-to-top.ps1 @@ -18,57 +15,70 @@ # Related: https://github.com/anthropics/claude-code/issues/34794 # https://github.com/anthropics/claude-code/pull/34798 -param( - [switch]$Uninstall -) +param([switch]$Uninstall) $ErrorActionPreference = "Stop" $cliPath = "$env:APPDATA\npm\node_modules\@anthropic-ai\claude-code\cli.js" if (-not (Test-Path $cliPath)) { - Write-Host "ERROR: Claude Code npm install not found at $cliPath" -ForegroundColor Red - Write-Host "Run: npm install -g @anthropic-ai/claude-code" -ForegroundColor Yellow + Write-Host "ERROR: npm install not found. Run: npm install -g @anthropic-ai/claude-code" -ForegroundColor Red exit 1 } $code = [System.IO.File]::ReadAllText($cliPath, [System.Text.Encoding]::UTF8) $marker = '/* SCROLL_FIX_v2 */' - -# The interceptor code (single line, injected before first import) $interceptor = '/* SCROLL_FIX_v2 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _gr=function(){return process.stdout.rows||24};var _up=0;process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);var dn=0,m,re=/\x1b\[(\d+)B/g;while(m=re.exec(s))dn+=parseInt(m[1],10);var nl=0;for(var i=0;imx){var a=Math.max(0,v-(_up-mx));_up=mx;if(a<=0)return"";return"\x1b["+a+"A"}return m});if(s.indexOf("\x1b[?2026l")!==-1)_up=0;if(s.indexOf("\x1b[H")!==-1)_up=0;return _ow(s,e,c)};})();' # --- Uninstall --- - if ($Uninstall) { + $reverted = 0 if ($code.Contains($marker)) { - $start = $code.IndexOf($marker) - $end = $code.IndexOf('})();', $start) + 5 - $code = $code.Substring(0, $start) + $code.Substring($end + 1) - [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) - Write-Host "Reverted scroll fix." -ForegroundColor Green - } else { - Write-Host "No scroll fix to revert." -ForegroundColor Yellow + $s = $code.IndexOf($marker); $e = $code.IndexOf('})();', $s) + 5 + $code = $code.Substring(0, $s) + $code.Substring($e + 1); $reverted++ } + $code = $code.Replace('var SK6=200;', 'var SK6=16;') + $code = $code.Replace('kk7="",Ek7=""', 'kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE)') + [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) + Write-Host "Reverted all scroll patches." -ForegroundColor Green exit 0 } # --- Apply --- +$applied = 0 +# Patch 1: stdout interceptor if ($code.Contains($marker)) { - Write-Host "Already patched." -ForegroundColor Green - exit 0 + Write-Host "[1/3] Interceptor already applied." -ForegroundColor Green +} else { + $i = $code.IndexOf('import{') + $code = $code.Substring(0, $i) + $interceptor + "`n" + $code.Substring($i) + $applied++; Write-Host "[1/3] Applied stdout.write interceptor." -ForegroundColor Green } -$insertAt = $code.IndexOf('import{') -if ($insertAt -eq -1) { - Write-Host "ERROR: Could not find import statement in cli.js" -ForegroundColor Red - exit 1 +# Patch 2: render throttle +if ($code.Contains('var SK6=200;')) { + Write-Host "[2/3] Render throttle already applied." -ForegroundColor Green +} elseif ($code.Contains('var SK6=16;')) { + $code = $code.Replace('var SK6=16;', 'var SK6=200;') + $applied++; Write-Host "[2/3] Applied render throttle (16ms -> 200ms)." -ForegroundColor Green +} else { + Write-Host "[2/3] Render throttle pattern not found." -ForegroundColor Red +} + +# Patch 3: disable sync update +if ($code.Contains('kk7="",Ek7=""')) { + Write-Host "[3/3] Sync disable already applied." -ForegroundColor Green +} elseif ($code.Contains('kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE)')) { + $code = $code.Replace('kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE)', 'kk7="",Ek7=""') + $applied++; Write-Host "[3/3] Disabled synchronized update mode." -ForegroundColor Green +} else { + Write-Host "[3/3] Sync update pattern not found." -ForegroundColor Red } -$code = $code.Substring(0, $insertAt) + $interceptor + "`n" + $code.Substring($insertAt) -[System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) +if ($applied -gt 0) { + [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) +} -Write-Host "Applied scroll fix v2 to cli.js" -ForegroundColor Green -Write-Host "Run Claude Code via: $env:APPDATA\npm\claude.cmd" -ForegroundColor White -Write-Host "To revert: powershell -File $PSCommandPath -Uninstall" -ForegroundColor Gray +Write-Host "`nDone. Run: $env:APPDATA\npm\claude.cmd" -ForegroundColor Cyan +Write-Host "Revert: powershell -File $PSCommandPath -Uninstall" -ForegroundColor Gray From 22a7821468ee23fd98c8c0b96d941813dff8c2df Mon Sep 17 00:00:00 2001 From: LAURO III CRUZ Date: Mon, 16 Mar 2026 13:15:49 +0800 Subject: [PATCH 06/10] =?UTF-8?q?v3:=20simplify=20to=20render=20throttle?= =?UTF-8?q?=20only=20(16ms=20=E2=86=92=201000ms)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- patches/scroll-to-top.patch | 43 +++++++++++-------- scripts/fix-scroll-to-top.ps1 | 81 ++++++++++++----------------------- 2 files changed, 53 insertions(+), 71 deletions(-) diff --git a/patches/scroll-to-top.patch b/patches/scroll-to-top.patch index 31643a0365..ff4750fe55 100644 --- a/patches/scroll-to-top.patch +++ b/patches/scroll-to-top.patch @@ -1,23 +1,30 @@ --- a/cli.js +++ b/cli.js -# === Patch 1: Inject before the first `import{` statement === -# Stateful stdout.write interceptor — clamps cursor-up across all writes - -+/* SCROLL_FIX_v2 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _gr=function(){return process.stdout.rows||24};var _up=0;process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);var dn=0,m,re=/\x1b\[(\d+)B/g;while(m=re.exec(s))dn+=parseInt(m[1],10);var nl=0;for(var i=0;imx){var a=Math.max(0,v-(_up-mx));_up=mx;if(a<=0)return"";return"\x1b["+a+"A"}return m});if(s.indexOf("\x1b[?2026l")!==-1)_up=0;if(s.indexOf("\x1b[H")!==-1)_up=0;return _ow(s,e,c)};})(); - -# === Patch 2: Increase render throttle from 16ms to 200ms === -# Reduces re-renders from 60fps to 5fps — less frequent viewport resets +# === Single patch: Increase render throttle from 16ms to 1000ms === +# +# Root cause: Windows Terminal bug #14774 — SetConsoleCursorPosition always +# scrolls the viewport to the cursor position, even when the cursor is +# already visible. This is triggered by ANY cursor positioning ANSI sequence. +# Open since Feb 2023, Priority-1, UNRESOLVED. +# +# Since Ink's renderer uses cursor positioning on every re-render (to update +# the display via diff), every render triggers a viewport reset in WT. +# +# The only mitigation without a PTY proxy is to REDUCE render frequency. +# At 1000ms (1fps), the user gets 1 full second of uninterrupted reading +# between viewport resets. Streaming text appears in ~1 second chunks. +# +# Previous approaches that did NOT work: +# - Cursor-up clamping: WT bug triggers on ANY cursor positioning, not just up +# - Disabling synchronized update: causes flickering (worse than scroll) +# - Both combined: flickering + still scrolls +# +# Trade-off: streaming text is slightly chunky (1s batches) but readable +# and scroll position is mostly stable. -var SK6=16; -+var SK6=200; - -# === Patch 3: Disable synchronized update mode === -# Lets Windows Terminal's native auto-scroll prevention work per-write -# instead of batching everything into one viewport-resetting sync block - --kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE) -+kk7="",Ek7="" ++var SK6=1000; # Target: @anthropic-ai/claude-code@2.1.76 cli.js # Build: 2026-03-14T00:12:49Z @@ -28,5 +35,7 @@ # With bridge fix (PR #34789) applied: # 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c # -# With all scroll fixes (v2 + throttle + sync disable) applied: -# 96ed31561b6f04a48f67b583d0491fb3584b7d8a1ae185809569f47e8aaed838 +# With bridge + render throttle 1000ms: +# e985041bdd17e85dba04ec605346714aa21e4d7be16889e06774a32e1bd1e410 +# +# Upstream fix needed: microsoft/terminal#14774 diff --git a/scripts/fix-scroll-to-top.ps1 b/scripts/fix-scroll-to-top.ps1 index e2a5bd726e..f76d298566 100644 --- a/scripts/fix-scroll-to-top.ps1 +++ b/scripts/fix-scroll-to-top.ps1 @@ -1,19 +1,23 @@ -# fix-scroll-to-top.ps1 (v2 + throttle + sync disable) -# Fixes terminal scroll-to-top during Claude Code agent execution. +# fix-scroll-to-top.ps1 (v3 — render throttle only) # -# 3 patches: -# 1. Stateful stdout.write interceptor — clamps cursor-up across all writes -# 2. Render throttle 16ms→200ms — reduces viewport resets from 60fps to 5fps -# 3. Disable synchronized update mode — lets terminal's native auto-scroll prevention work +# Root cause: Windows Terminal bug microsoft/terminal#14774 +# SetConsoleCursorPosition always scrolls viewport to cursor, even when visible. +# Every Ink re-render triggers cursor positioning → viewport jumps. +# +# Fix: increase render throttle from 16ms (60fps) to 1000ms (1fps). +# Gives 1 full second of uninterrupted reading between viewport resets. +# Trade-off: streaming text appears in ~1 second chunks. +# +# Previous approaches that did NOT work: +# - Cursor-up clamping (stdout interceptor): WT bug triggers on ANY cursor move +# - Disabling synchronized update mode: causes flickering # # Usage: # powershell -ExecutionPolicy Bypass -File fix-scroll-to-top.ps1 # -# Requires: npm-installed Claude Code (npm install -g @anthropic-ai/claude-code) -# Re-run after every npm update. -# # Related: https://github.com/anthropics/claude-code/issues/34794 # https://github.com/anthropics/claude-code/pull/34798 +# https://github.com/microsoft/terminal/issues/14774 param([switch]$Uninstall) @@ -27,58 +31,27 @@ if (-not (Test-Path $cliPath)) { $code = [System.IO.File]::ReadAllText($cliPath, [System.Text.Encoding]::UTF8) -$marker = '/* SCROLL_FIX_v2 */' -$interceptor = '/* SCROLL_FIX_v2 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _gr=function(){return process.stdout.rows||24};var _up=0;process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);var dn=0,m,re=/\x1b\[(\d+)B/g;while(m=re.exec(s))dn+=parseInt(m[1],10);var nl=0;for(var i=0;imx){var a=Math.max(0,v-(_up-mx));_up=mx;if(a<=0)return"";return"\x1b["+a+"A"}return m});if(s.indexOf("\x1b[?2026l")!==-1)_up=0;if(s.indexOf("\x1b[H")!==-1)_up=0;return _ow(s,e,c)};})();' - -# --- Uninstall --- if ($Uninstall) { - $reverted = 0 - if ($code.Contains($marker)) { - $s = $code.IndexOf($marker); $e = $code.IndexOf('})();', $s) + 5 - $code = $code.Substring(0, $s) + $code.Substring($e + 1); $reverted++ + if ($code.Contains('var SK6=1000;')) { + $code = $code.Replace('var SK6=1000;', 'var SK6=16;') + [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) + Write-Host "Reverted render throttle to 16ms." -ForegroundColor Green + } else { + Write-Host "No patch to revert." -ForegroundColor Yellow } - $code = $code.Replace('var SK6=200;', 'var SK6=16;') - $code = $code.Replace('kk7="",Ek7=""', 'kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE)') - [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) - Write-Host "Reverted all scroll patches." -ForegroundColor Green exit 0 } -# --- Apply --- -$applied = 0 - -# Patch 1: stdout interceptor -if ($code.Contains($marker)) { - Write-Host "[1/3] Interceptor already applied." -ForegroundColor Green -} else { - $i = $code.IndexOf('import{') - $code = $code.Substring(0, $i) + $interceptor + "`n" + $code.Substring($i) - $applied++; Write-Host "[1/3] Applied stdout.write interceptor." -ForegroundColor Green -} - -# Patch 2: render throttle -if ($code.Contains('var SK6=200;')) { - Write-Host "[2/3] Render throttle already applied." -ForegroundColor Green +if ($code.Contains('var SK6=1000;')) { + Write-Host "Already patched (1000ms throttle)." -ForegroundColor Green } elseif ($code.Contains('var SK6=16;')) { - $code = $code.Replace('var SK6=16;', 'var SK6=200;') - $applied++; Write-Host "[2/3] Applied render throttle (16ms -> 200ms)." -ForegroundColor Green -} else { - Write-Host "[2/3] Render throttle pattern not found." -ForegroundColor Red -} - -# Patch 3: disable sync update -if ($code.Contains('kk7="",Ek7=""')) { - Write-Host "[3/3] Sync disable already applied." -ForegroundColor Green -} elseif ($code.Contains('kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE)')) { - $code = $code.Replace('kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE)', 'kk7="",Ek7=""') - $applied++; Write-Host "[3/3] Disabled synchronized update mode." -ForegroundColor Green -} else { - Write-Host "[3/3] Sync update pattern not found." -ForegroundColor Red -} - -if ($applied -gt 0) { + $code = $code.Replace('var SK6=16;', 'var SK6=1000;') [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) + Write-Host "Applied render throttle: 16ms -> 1000ms (1fps)." -ForegroundColor Green +} else { + Write-Host "ERROR: SK6 pattern not found — version may have changed." -ForegroundColor Red + exit 1 } -Write-Host "`nDone. Run: $env:APPDATA\npm\claude.cmd" -ForegroundColor Cyan +Write-Host "Run: $env:APPDATA\npm\claude.cmd" -ForegroundColor Cyan Write-Host "Revert: powershell -File $PSCommandPath -Uninstall" -ForegroundColor Gray From 11f409e3a4be06dc7136e048a8eef7bb551c35d4 Mon Sep 17 00:00:00 2001 From: LAURO III CRUZ Date: Mon, 16 Mar 2026 14:47:05 +0800 Subject: [PATCH 07/10] v5: buffer renders during active work, flush on user input or idle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- patches/scroll-to-top.patch | 53 +++++++++++++++-------------- scripts/fix-scroll-to-top.ps1 | 63 ++++++++++++++++++++--------------- 2 files changed, 62 insertions(+), 54 deletions(-) diff --git a/patches/scroll-to-top.patch b/patches/scroll-to-top.patch index ff4750fe55..030d03906e 100644 --- a/patches/scroll-to-top.patch +++ b/patches/scroll-to-top.patch @@ -1,30 +1,10 @@ --- a/cli.js +++ b/cli.js -# === Single patch: Increase render throttle from 16ms to 1000ms === -# -# Root cause: Windows Terminal bug #14774 — SetConsoleCursorPosition always -# scrolls the viewport to the cursor position, even when the cursor is -# already visible. This is triggered by ANY cursor positioning ANSI sequence. -# Open since Feb 2023, Priority-1, UNRESOLVED. -# -# Since Ink's renderer uses cursor positioning on every re-render (to update -# the display via diff), every render triggers a viewport reset in WT. -# -# The only mitigation without a PTY proxy is to REDUCE render frequency. -# At 1000ms (1fps), the user gets 1 full second of uninterrupted reading -# between viewport resets. Streaming text appears in ~1 second chunks. -# -# Previous approaches that did NOT work: -# - Cursor-up clamping: WT bug triggers on ANY cursor positioning, not just up -# - Disabling synchronized update: causes flickering (worse than scroll) -# - Both combined: flickering + still scrolls -# -# Trade-off: streaming text is slightly chunky (1s batches) but readable -# and scroll position is mostly stable. +# === Inject before the first `import{` statement === +# Buffer Ink renders during active work, flush only on user input or idle --var SK6=16; -+var SK6=1000; ++/* SCROLL_FIX_v5 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=null;var _timer=null;var _userInput=false;var _IDLE=5000;process.stdin.on("data",function(){_userInput=true});function _flush(){if(_timer){clearTimeout(_timer);_timer=null}if(!_buf)return;var b=_buf;_buf=null;_ow(b.s,b.e)}process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;return _ow(s,e,c)}_buf={s:s,e:e};if(_timer)clearTimeout(_timer);_timer=setTimeout(_flush,_IDLE);if(c)c();return true}return _ow(s,e,c)};})(); # Target: @anthropic-ai/claude-code@2.1.76 cli.js # Build: 2026-03-14T00:12:49Z @@ -35,7 +15,26 @@ # With bridge fix (PR #34789) applied: # 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c # -# With bridge + render throttle 1000ms: -# e985041bdd17e85dba04ec605346714aa21e4d7be16889e06774a32e1bd1e410 -# -# Upstream fix needed: microsoft/terminal#14774 +# With bridge + scroll v5: +# 580641c8f9c762b31fce20a9fcfc85b158b1ca52dded5d5495de29c7ec234f2c +# +# How it works: +# - Intercepts process.stdout.write +# - Ink renders (sync blocks with \x1b[?2026h) are BUFFERED, not written +# - Only the LATEST frame is kept (previous frames discarded) +# - Flush triggers: +# (a) User types (stdin data) → next render writes immediately +# (user is at the prompt/bottom, viewport follows naturally) +# (b) 5 seconds of no new renders → auto-flush +# (Claude finished, show final state) +# - Non-sync writes pass through immediately +# +# Why this works: +# Windows Terminal bug microsoft/terminal#14774 causes viewport to jump +# on ANY cursor positioning. By not writing to stdout during active work, +# the viewport stays wherever the user scrolled. When the user types +# (returning to the prompt at the bottom), rendering resumes normally. +# +# Trade-off: +# Screen is frozen during active Claude work. User sees the result +# when Claude pauses or they type. Spinner/progress not visible during work. diff --git a/scripts/fix-scroll-to-top.ps1 b/scripts/fix-scroll-to-top.ps1 index f76d298566..920ceafb4c 100644 --- a/scripts/fix-scroll-to-top.ps1 +++ b/scripts/fix-scroll-to-top.ps1 @@ -1,16 +1,12 @@ -# fix-scroll-to-top.ps1 (v3 — render throttle only) +# fix-scroll-to-top.ps1 (v5 — idle-flush) # -# Root cause: Windows Terminal bug microsoft/terminal#14774 -# SetConsoleCursorPosition always scrolls viewport to cursor, even when visible. -# Every Ink re-render triggers cursor positioning → viewport jumps. -# -# Fix: increase render throttle from 16ms (60fps) to 1000ms (1fps). -# Gives 1 full second of uninterrupted reading between viewport resets. -# Trade-off: streaming text appears in ~1 second chunks. +# Buffers all Ink renders during active Claude work. Screen stays frozen +# while working (no viewport jumping). Flushes only when: +# (a) User types (stdin) — they're at the prompt, viewport follows naturally +# (b) 5 seconds of no new renders — Claude finished, show final state # -# Previous approaches that did NOT work: -# - Cursor-up clamping (stdout interceptor): WT bug triggers on ANY cursor move -# - Disabling synchronized update mode: causes flickering +# Root cause: Windows Terminal bug microsoft/terminal#14774 +# Any cursor positioning scrolls viewport, even when cursor is visible. # # Usage: # powershell -ExecutionPolicy Bypass -File fix-scroll-to-top.ps1 @@ -30,28 +26,41 @@ if (-not (Test-Path $cliPath)) { } $code = [System.IO.File]::ReadAllText($cliPath, [System.Text.Encoding]::UTF8) +$marker = '/* SCROLL_FIX_v5 */' if ($Uninstall) { - if ($code.Contains('var SK6=1000;')) { - $code = $code.Replace('var SK6=1000;', 'var SK6=16;') + if ($code.Contains($marker)) { + $s = $code.IndexOf($marker) + $e = $code.IndexOf('})();', $s) + 5 + $code = $code.Substring(0, $s) + $code.Substring($e) + if ($code[$s] -eq "`n") { $code = $code.Substring(0, $s) + $code.Substring($s + 1) } [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) - Write-Host "Reverted render throttle to 16ms." -ForegroundColor Green - } else { - Write-Host "No patch to revert." -ForegroundColor Yellow - } + Write-Host "Reverted scroll fix." -ForegroundColor Green + } else { Write-Host "No patch to revert." -ForegroundColor Yellow } exit 0 } -if ($code.Contains('var SK6=1000;')) { - Write-Host "Already patched (1000ms throttle)." -ForegroundColor Green -} elseif ($code.Contains('var SK6=16;')) { - $code = $code.Replace('var SK6=16;', 'var SK6=1000;') - [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) - Write-Host "Applied render throttle: 16ms -> 1000ms (1fps)." -ForegroundColor Green -} else { - Write-Host "ERROR: SK6 pattern not found — version may have changed." -ForegroundColor Red - exit 1 +if ($code.Contains($marker)) { + Write-Host "Already patched." -ForegroundColor Green + exit 0 } +# Remove older versions +foreach ($old in @('/* SCROLL_FIX_v2 */', '/* SCROLL_FIX_v4 */')) { + if ($code.Contains($old)) { + $s = $code.IndexOf($old); $e = $code.IndexOf('})();', $s) + 5 + $code = $code.Substring(0, $s) + $code.Substring($e) + } +} +# Revert throttle/sync changes from older versions +$code = $code.Replace('var SK6=200;', 'var SK6=16;').Replace('var SK6=1000;', 'var SK6=16;') +$code = $code.Replace('kk7="",Ek7=""', 'kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE)') + +$fix = '/* SCROLL_FIX_v5 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=null;var _timer=null;var _userInput=false;var _IDLE=5000;process.stdin.on("data",function(){_userInput=true});function _flush(){if(_timer){clearTimeout(_timer);_timer=null}if(!_buf)return;var b=_buf;_buf=null;_ow(b.s,b.e)}process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;return _ow(s,e,c)}_buf={s:s,e:e};if(_timer)clearTimeout(_timer);_timer=setTimeout(_flush,_IDLE);if(c)c();return true}return _ow(s,e,c)};})();' + +$i = $code.IndexOf('import{') +$code = $code.Substring(0, $i) + $fix + "`n" + $code.Substring($i) +[System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) + +Write-Host "Applied scroll fix v5 (idle-flush)." -ForegroundColor Green Write-Host "Run: $env:APPDATA\npm\claude.cmd" -ForegroundColor Cyan -Write-Host "Revert: powershell -File $PSCommandPath -Uninstall" -ForegroundColor Gray From 97f8f20a19bc8b213bc312045f0851dc7e8245fe Mon Sep 17 00:00:00 2001 From: LAURO III CRUZ Date: Mon, 16 Mar 2026 14:57:43 +0800 Subject: [PATCH 08/10] v6: stdin-only flush, no timer, no auto-flush 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 --- patches/scroll-to-top.patch | 39 ++++++++--------------------- scripts/fix-scroll-to-top.ps1 | 46 ++++++++++++----------------------- 2 files changed, 25 insertions(+), 60 deletions(-) diff --git a/patches/scroll-to-top.patch b/patches/scroll-to-top.patch index 030d03906e..ae9de1c2a3 100644 --- a/patches/scroll-to-top.patch +++ b/patches/scroll-to-top.patch @@ -2,39 +2,20 @@ +++ b/cli.js # === Inject before the first `import{` statement === -# Buffer Ink renders during active work, flush only on user input or idle +# Buffer Ink renders, flush ONLY on user input (stdin). No timer. No auto-flush. -+/* SCROLL_FIX_v5 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=null;var _timer=null;var _userInput=false;var _IDLE=5000;process.stdin.on("data",function(){_userInput=true});function _flush(){if(_timer){clearTimeout(_timer);_timer=null}if(!_buf)return;var b=_buf;_buf=null;_ow(b.s,b.e)}process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;return _ow(s,e,c)}_buf={s:s,e:e};if(_timer)clearTimeout(_timer);_timer=setTimeout(_flush,_IDLE);if(c)c();return true}return _ow(s,e,c)};})(); ++/* SCROLL_FIX_v6 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=null;var _userInput=false;process.stdin.on("data",function(){_userInput=true});process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;return _ow(s,e,c)}_buf={s:s,e:e};if(c)c();return true}return _ow(s,e,c)};})(); # Target: @anthropic-ai/claude-code@2.1.76 cli.js # Build: 2026-03-14T00:12:49Z # -# Original (unpatched) SHA-256: -# 38b8fd29d0817e5f75202b2bb211fe959d4b6a4f2224b8118dabf876e503b50b +# Original SHA-256: 38b8fd29d0817e5f75202b2bb211fe959d4b6a4f2224b8118dabf876e503b50b +# + bridge fix: 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c +# + scroll v6: 626975ae7894859cfe301c761272c147a61f20a320ab2427807df063e64cfbf1 # -# With bridge fix (PR #34789) applied: -# 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c +# Rule: if user has not typed (not at bottom), ZERO screen updates. +# Flush ONLY when user types (stdin data = at prompt = at bottom). +# No timer. No auto-flush. No exceptions. # -# With bridge + scroll v5: -# 580641c8f9c762b31fce20a9fcfc85b158b1ca52dded5d5495de29c7ec234f2c -# -# How it works: -# - Intercepts process.stdout.write -# - Ink renders (sync blocks with \x1b[?2026h) are BUFFERED, not written -# - Only the LATEST frame is kept (previous frames discarded) -# - Flush triggers: -# (a) User types (stdin data) → next render writes immediately -# (user is at the prompt/bottom, viewport follows naturally) -# (b) 5 seconds of no new renders → auto-flush -# (Claude finished, show final state) -# - Non-sync writes pass through immediately -# -# Why this works: -# Windows Terminal bug microsoft/terminal#14774 causes viewport to jump -# on ANY cursor positioning. By not writing to stdout during active work, -# the viewport stays wherever the user scrolled. When the user types -# (returning to the prompt at the bottom), rendering resumes normally. -# -# Trade-off: -# Screen is frozen during active Claude work. User sees the result -# when Claude pauses or they type. Spinner/progress not visible during work. +# Root cause: Windows Terminal bug microsoft/terminal#14774 +# SetConsoleCursorPosition always scrolls viewport to cursor. diff --git a/scripts/fix-scroll-to-top.ps1 b/scripts/fix-scroll-to-top.ps1 index 920ceafb4c..e632b6c1ab 100644 --- a/scripts/fix-scroll-to-top.ps1 +++ b/scripts/fix-scroll-to-top.ps1 @@ -1,66 +1,50 @@ -# fix-scroll-to-top.ps1 (v5 — idle-flush) +# fix-scroll-to-top.ps1 (v6 — stdin-only flush) # -# Buffers all Ink renders during active Claude work. Screen stays frozen -# while working (no viewport jumping). Flushes only when: -# (a) User types (stdin) — they're at the prompt, viewport follows naturally -# (b) 5 seconds of no new renders — Claude finished, show final state +# Buffers ALL Ink renders. Screen updates ONLY when user types. +# No timer. No auto-flush. Zero viewport jumping. # # Root cause: Windows Terminal bug microsoft/terminal#14774 -# Any cursor positioning scrolls viewport, even when cursor is visible. -# -# Usage: -# powershell -ExecutionPolicy Bypass -File fix-scroll-to-top.ps1 # +# Usage: powershell -ExecutionPolicy Bypass -File fix-scroll-to-top.ps1 # Related: https://github.com/anthropics/claude-code/issues/34794 -# https://github.com/anthropics/claude-code/pull/34798 -# https://github.com/microsoft/terminal/issues/14774 param([switch]$Uninstall) - $ErrorActionPreference = "Stop" $cliPath = "$env:APPDATA\npm\node_modules\@anthropic-ai\claude-code\cli.js" if (-not (Test-Path $cliPath)) { - Write-Host "ERROR: npm install not found. Run: npm install -g @anthropic-ai/claude-code" -ForegroundColor Red - exit 1 + Write-Host "ERROR: Run: npm install -g @anthropic-ai/claude-code" -ForegroundColor Red; exit 1 } $code = [System.IO.File]::ReadAllText($cliPath, [System.Text.Encoding]::UTF8) -$marker = '/* SCROLL_FIX_v5 */' +$marker = '/* SCROLL_FIX_v6 */' if ($Uninstall) { if ($code.Contains($marker)) { - $s = $code.IndexOf($marker) - $e = $code.IndexOf('})();', $s) + 5 + $s = $code.IndexOf($marker); $e = $code.IndexOf('})();', $s) + 5 $code = $code.Substring(0, $s) + $code.Substring($e) if ($code[$s] -eq "`n") { $code = $code.Substring(0, $s) + $code.Substring($s + 1) } [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) - Write-Host "Reverted scroll fix." -ForegroundColor Green - } else { Write-Host "No patch to revert." -ForegroundColor Yellow } + Write-Host "Reverted." -ForegroundColor Green + } else { Write-Host "No patch found." -ForegroundColor Yellow } exit 0 } -if ($code.Contains($marker)) { - Write-Host "Already patched." -ForegroundColor Green - exit 0 -} +if ($code.Contains($marker)) { Write-Host "Already patched." -ForegroundColor Green; exit 0 } # Remove older versions -foreach ($old in @('/* SCROLL_FIX_v2 */', '/* SCROLL_FIX_v4 */')) { +foreach ($old in @('/* SCROLL_FIX_v2 */', '/* SCROLL_FIX_v4 */', '/* SCROLL_FIX_v5 */')) { if ($code.Contains($old)) { $s = $code.IndexOf($old); $e = $code.IndexOf('})();', $s) + 5 $code = $code.Substring(0, $s) + $code.Substring($e) } } -# Revert throttle/sync changes from older versions -$code = $code.Replace('var SK6=200;', 'var SK6=16;').Replace('var SK6=1000;', 'var SK6=16;') -$code = $code.Replace('kk7="",Ek7=""', 'kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE)') +$code = $code.Replace('var SK6=200;','var SK6=16;').Replace('var SK6=1000;','var SK6=16;') +$code = $code.Replace('kk7="",Ek7=""','kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE)') -$fix = '/* SCROLL_FIX_v5 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=null;var _timer=null;var _userInput=false;var _IDLE=5000;process.stdin.on("data",function(){_userInput=true});function _flush(){if(_timer){clearTimeout(_timer);_timer=null}if(!_buf)return;var b=_buf;_buf=null;_ow(b.s,b.e)}process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;return _ow(s,e,c)}_buf={s:s,e:e};if(_timer)clearTimeout(_timer);_timer=setTimeout(_flush,_IDLE);if(c)c();return true}return _ow(s,e,c)};})();' +$fix = '/* SCROLL_FIX_v6 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=null;var _userInput=false;process.stdin.on("data",function(){_userInput=true});process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;return _ow(s,e,c)}_buf={s:s,e:e};if(c)c();return true}return _ow(s,e,c)};})();' $i = $code.IndexOf('import{') $code = $code.Substring(0, $i) + $fix + "`n" + $code.Substring($i) [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) - -Write-Host "Applied scroll fix v5 (idle-flush)." -ForegroundColor Green -Write-Host "Run: $env:APPDATA\npm\claude.cmd" -ForegroundColor Cyan +Write-Host "Applied scroll fix v6." -ForegroundColor Green From 493e85c03dc7defbf84a3ffdf892ec4c91be57fa Mon Sep 17 00:00:00 2001 From: LAURO III CRUZ Date: Mon, 16 Mar 2026 15:24:14 +0800 Subject: [PATCH 09/10] v7: replay ALL buffered frames on flush (fixes outdated screens) 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 --- patches/scroll-to-top.patch | 15 ++++++--------- scripts/fix-scroll-to-top.ps1 | 17 +++++++---------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/patches/scroll-to-top.patch b/patches/scroll-to-top.patch index ae9de1c2a3..dcf51f6826 100644 --- a/patches/scroll-to-top.patch +++ b/patches/scroll-to-top.patch @@ -2,20 +2,17 @@ +++ b/cli.js # === Inject before the first `import{` statement === -# Buffer Ink renders, flush ONLY on user input (stdin). No timer. No auto-flush. +# Buffer Ink renders, replay ALL on user input (stdin). No timer. -+/* SCROLL_FIX_v6 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=null;var _userInput=false;process.stdin.on("data",function(){_userInput=true});process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;return _ow(s,e,c)}_buf={s:s,e:e};if(c)c();return true}return _ow(s,e,c)};})(); ++/* SCROLL_FIX_v7 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=[];var _userInput=false;process.stdin.on("data",function(){_userInput=true});function _flush(){if(_buf.length===0)return;var all="";for(var i=0;i<_buf.length;i++)all+=_buf[i];_buf=[];_ow(all)}process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;_flush();return _ow(s,e,c)}_buf.push(s);if(c)c();return true}return _ow(s,e,c)};})(); -# Target: @anthropic-ai/claude-code@2.1.76 cli.js -# Build: 2026-03-14T00:12:49Z +# Target: @anthropic-ai/claude-code@2.1.76 cli.js (build 2026-03-14T00:12:49Z) # # Original SHA-256: 38b8fd29d0817e5f75202b2bb211fe959d4b6a4f2224b8118dabf876e503b50b # + bridge fix: 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c -# + scroll v6: 626975ae7894859cfe301c761272c147a61f20a320ab2427807df063e64cfbf1 +# + scroll v7: a57e8bc8904aeeccbcabae1656543c95b54d2140e8a9afe91829bf14d527e52e # -# Rule: if user has not typed (not at bottom), ZERO screen updates. -# Flush ONLY when user types (stdin data = at prompt = at bottom). -# No timer. No auto-flush. No exceptions. +# v6 only kept the LAST frame → outdated screens on flush (diff chain broken). +# v7 keeps ALL frames and replays the entire chain on flush → correct output. # # Root cause: Windows Terminal bug microsoft/terminal#14774 -# SetConsoleCursorPosition always scrolls viewport to cursor. diff --git a/scripts/fix-scroll-to-top.ps1 b/scripts/fix-scroll-to-top.ps1 index e632b6c1ab..bc123b1910 100644 --- a/scripts/fix-scroll-to-top.ps1 +++ b/scripts/fix-scroll-to-top.ps1 @@ -1,12 +1,10 @@ -# fix-scroll-to-top.ps1 (v6 — stdin-only flush) +# fix-scroll-to-top.ps1 (v7 — replay all frames on stdin flush) # -# Buffers ALL Ink renders. Screen updates ONLY when user types. -# No timer. No auto-flush. Zero viewport jumping. +# Buffers ALL Ink renders. Replays entire chain when user types. +# No timer. No auto-flush. Correct diff chain (no outdated screens). # # Root cause: Windows Terminal bug microsoft/terminal#14774 -# # Usage: powershell -ExecutionPolicy Bypass -File fix-scroll-to-top.ps1 -# Related: https://github.com/anthropics/claude-code/issues/34794 param([switch]$Uninstall) $ErrorActionPreference = "Stop" @@ -17,7 +15,7 @@ if (-not (Test-Path $cliPath)) { } $code = [System.IO.File]::ReadAllText($cliPath, [System.Text.Encoding]::UTF8) -$marker = '/* SCROLL_FIX_v6 */' +$marker = '/* SCROLL_FIX_v7 */' if ($Uninstall) { if ($code.Contains($marker)) { @@ -32,8 +30,7 @@ if ($Uninstall) { if ($code.Contains($marker)) { Write-Host "Already patched." -ForegroundColor Green; exit 0 } -# Remove older versions -foreach ($old in @('/* SCROLL_FIX_v2 */', '/* SCROLL_FIX_v4 */', '/* SCROLL_FIX_v5 */')) { +foreach ($old in @('/* SCROLL_FIX_v2 */','/* SCROLL_FIX_v4 */','/* SCROLL_FIX_v5 */','/* SCROLL_FIX_v6 */')) { if ($code.Contains($old)) { $s = $code.IndexOf($old); $e = $code.IndexOf('})();', $s) + 5 $code = $code.Substring(0, $s) + $code.Substring($e) @@ -42,9 +39,9 @@ foreach ($old in @('/* SCROLL_FIX_v2 */', '/* SCROLL_FIX_v4 */', '/* SCROLL_FIX_ $code = $code.Replace('var SK6=200;','var SK6=16;').Replace('var SK6=1000;','var SK6=16;') $code = $code.Replace('kk7="",Ek7=""','kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE)') -$fix = '/* SCROLL_FIX_v6 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=null;var _userInput=false;process.stdin.on("data",function(){_userInput=true});process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;return _ow(s,e,c)}_buf={s:s,e:e};if(c)c();return true}return _ow(s,e,c)};})();' +$fix = '/* SCROLL_FIX_v7 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=[];var _userInput=false;process.stdin.on("data",function(){_userInput=true});function _flush(){if(_buf.length===0)return;var all="";for(var i=0;i<_buf.length;i++)all+=_buf[i];_buf=[];_ow(all)}process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;_flush();return _ow(s,e,c)}_buf.push(s);if(c)c();return true}return _ow(s,e,c)};})();' $i = $code.IndexOf('import{') $code = $code.Substring(0, $i) + $fix + "`n" + $code.Substring($i) [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) -Write-Host "Applied scroll fix v6." -ForegroundColor Green +Write-Host "Applied scroll fix v7." -ForegroundColor Green From def6f1d6501c16fd6d2640b8696fa320a10227f9 Mon Sep 17 00:00:00 2001 From: LAURO III CRUZ Date: Mon, 16 Mar 2026 16:17:17 +0800 Subject: [PATCH 10/10] Final: Ctrl+6 freeze toggle (replaces v1-v10 scroll fixes) 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 --- patches/scroll-to-top-analysis.md | 168 ------------------------------ patches/scroll-to-top.patch | 15 +-- scripts/fix-scroll-to-top.ps1 | 47 --------- 3 files changed, 4 insertions(+), 226 deletions(-) delete mode 100644 patches/scroll-to-top-analysis.md delete mode 100644 scripts/fix-scroll-to-top.ps1 diff --git a/patches/scroll-to-top-analysis.md b/patches/scroll-to-top-analysis.md deleted file mode 100644 index d460874a82..0000000000 --- a/patches/scroll-to-top-analysis.md +++ /dev/null @@ -1,168 +0,0 @@ -# Scroll-to-Top Bug — Full Root Cause Analysis - -## Source -- **cli.js version:** 2.1.76 -- **Build:** 2026-03-14T00:12:49Z -- **SHA-256:** `38b8fd29d0817e5f75202b2bb211fe959d4b6a4f2224b8118dabf876e503b50b` - -## Architecture - -Claude Code uses a custom **Ink** (React for CLI) renderer with a dual frame-buffer diffing system: - -``` -React component tree → Yoga layout → screen buffer → diff vs previous buffer → emit render ops → SH8() writes to stdout -``` - -## 5 Scroll-to-Top Triggers - -### 1. `Ru6()` — Full Screen Reset (line ~767) - -Called when the diff engine determines screen state has diverged too much: - -```js -function Ru6(A, q, K) { - let Y = new Kj8({x:0, y:0}, A.viewport.width); - return qd3(Y, A, K), - [{type: "clearTerminal", reason: q}, ...Y.diff]; -} -``` - -Emits `{type: "clearTerminal"}` which triggers `LH8()`: - -```js -function LH8() { - // All platforms: \x1B[2J (erase screen) + \x1B[3J (clear scrollback) + \x1B[H (CURSOR HOME) - if (process.platform === "win32") - if (HU3()) return jO1 + $H8 + HK6; // \x1B[2J\x1B[3J\x1B[H - else return jO1 + wU3; // \x1B[2J\x1B[0f - return jO1 + $H8 + HK6; // \x1B[2J\x1B[3J\x1B[H -} -``` - -**`\x1B[H` = cursor to row 1, column 1 = TOP OF TERMINAL.** This is the primary trigger. - -Triggered by: -- Viewport resize -- Scrollback changes -- Content changes above visible region -- Cursor past screen height + content shrink - -### 2. `enterAlternateScreen()` (line ~780) - -```js -enterAlternateScreen() { - this.options.stdout.write( - "\x1B[?1049h" // enter alt screen buffer - + "\x1B[?1004l" // disable focus events - + "\x1B[0m" // reset styles - + "\x1B[?25h" // show cursor - + "\x1B[2J\x1B[H" // ERASE SCREEN + CURSOR HOME ← scroll-to-top - ); -} -``` - -### 3. `exitAlternateScreen()` (line ~780) - -```js -exitAlternateScreen() { - this.options.stdout.write( - "\x1B[2J\x1B[H" // ERASE SCREEN + CURSOR HOME ← scroll-to-top - + "\x1B[?1049l" // leave alt screen buffer - + "\x1B[?25l" // hide cursor - ); -} -``` - -Both are used during tool execution (e.g., thinkback animation). - -### 4. `handleResume()` — SIGCONT handler (line ~780) - -```js -handleResume = () => { - if (this.altScreenActive) { - this.options.stdout.write( - "\x1B[?1049h" // enter alt screen - + "\x1B[2J\x1B[H" // ERASE SCREEN + CURSOR HOME ← scroll-to-top - ); - this.resetFramesForAltScreen(); - return; - } - // Non-alt-screen: resets frames, calls repaint() -} -``` - -### 5. `repaint()` — Frame buffer reset (line ~780) - -```js -repaint() { - this.frontFrame = js(...); // fresh empty screen buffer - this.backFrame = js(...); // fresh empty screen buffer - this.log.reset(); -} -``` - -Resets both frame buffers to empty. Next `onRender()` detects everything changed → triggers `Ru6()` (trigger #1) → `clearTerminal` → `\x1B[H`. - -### Output writer: `SH8()` (line ~755) - -All triggers flow through this function: - -```js -function SH8(A, q, K = false) { - let Y = !K, z = Y ? kk7 : ""; // synchronized update begin \x1B[?2026h - for (let _ of q) switch (_.type) { - case "clearTerminal": z += LH8(); break; // ← SCROLL-TO-TOP - case "cursorMove": z += RV7(_.x, _.y); break; - case "cursorTo": z += yV7(_.col); break; - // ... - } - if (Y) z += Ek7; // synchronized update end \x1B[?2026l - A.stdout.write(z); -} -``` - -### ANSI constants (line ~755) - -```js -jO1 = "\x1B[2J" // erase screen -$H8 = "\x1B[3J" // clear scrollback -HK6 = "\x1B[H" // cursor home (TOP-LEFT) — the scroll-to-top culprit -``` - -## Suggested Fixes - -### Fix A: Remove `\x1B[H` from `LH8()` on non-alt-screen renders - -The cursor-home sequence is unnecessary when the diff engine already positions the cursor correctly. Only use it in alt-screen mode. - -```js -function LH8() { - // Don't include \x1B[H — let the diff engine handle cursor positioning - return jO1 + $H8; // erase screen + clear scrollback, but NO cursor home -} -``` - -### Fix B: Guard `Ru6()` against triggering during streaming - -Add a condition to suppress full resets while content is actively streaming: - -```js -function Ru6(A, q, K) { - if (A.isStreaming) return; // don't full-reset during streaming - // ...existing code... -} -``` - -### Fix C: Use scroll regions to contain cursor movement - -``` -\x1B[;r — set scroll region (isolates cursor movement) -\x1B[r — reset scroll region -``` - -### Fix D: Replace `clearTerminal` with incremental diff - -Instead of clearing everything and redrawing, only update changed lines. - -## Related issues -#34794, #34400, #34765, #33814, #34052, #34503, #33624 diff --git a/patches/scroll-to-top.patch b/patches/scroll-to-top.patch index dcf51f6826..235412f1e2 100644 --- a/patches/scroll-to-top.patch +++ b/patches/scroll-to-top.patch @@ -1,18 +1,11 @@ --- a/cli.js +++ b/cli.js -# === Inject before the first `import{` statement === -# Buffer Ink renders, replay ALL on user input (stdin). No timer. +# Inject before the first `import{` statement: -+/* SCROLL_FIX_v7 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=[];var _userInput=false;process.stdin.on("data",function(){_userInput=true});function _flush(){if(_buf.length===0)return;var all="";for(var i=0;i<_buf.length;i++)all+=_buf[i];_buf=[];_ow(all)}process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;_flush();return _ow(s,e,c)}_buf.push(s);if(c)c();return true}return _ow(s,e,c)};})(); ++/* FREEZE_TOGGLE */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _frozen=false;var _buf=[];setTimeout(function(){try{process.stdin.on("data",function(d){if(d.toString().indexOf("\x1e")!==-1){_frozen=!_frozen;if(_frozen){_ow("\x1b]0;Claude Code [FROZEN - Ctrl+6 to resume]\x07")}else{if(_buf.length>0){var a="";for(var i=0;i<_buf.length;i++)a+=_buf[i];_buf=[];_ow(a)}_ow("\x1b]0;Claude Code\x07")}}})}catch(e){}},2000);process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(_frozen&&s.indexOf("\x1b[?2026h")!==-1){_buf.push(s);if(c)c();return true}return _ow(s,e,c)};})(); -# Target: @anthropic-ai/claude-code@2.1.76 cli.js (build 2026-03-14T00:12:49Z) -# +# cli.js v2.1.76 (build 2026-03-14T00:12:49Z) # Original SHA-256: 38b8fd29d0817e5f75202b2bb211fe959d4b6a4f2224b8118dabf876e503b50b # + bridge fix: 6ea2a57ddd49c3f0869e77e027f3de0c2116c390a0d338963f70bec9b92b537c -# + scroll v7: a57e8bc8904aeeccbcabae1656543c95b54d2140e8a9afe91829bf14d527e52e -# -# v6 only kept the LAST frame → outdated screens on flush (diff chain broken). -# v7 keeps ALL frames and replays the entire chain on flush → correct output. -# -# Root cause: Windows Terminal bug microsoft/terminal#14774 +# + freeze toggle: 85906e42ba628058519f727ac42fb0dac9c8c2ea1304b9d9ae0e5dad51793ebd diff --git a/scripts/fix-scroll-to-top.ps1 b/scripts/fix-scroll-to-top.ps1 deleted file mode 100644 index bc123b1910..0000000000 --- a/scripts/fix-scroll-to-top.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -# fix-scroll-to-top.ps1 (v7 — replay all frames on stdin flush) -# -# Buffers ALL Ink renders. Replays entire chain when user types. -# No timer. No auto-flush. Correct diff chain (no outdated screens). -# -# Root cause: Windows Terminal bug microsoft/terminal#14774 -# Usage: powershell -ExecutionPolicy Bypass -File fix-scroll-to-top.ps1 - -param([switch]$Uninstall) -$ErrorActionPreference = "Stop" -$cliPath = "$env:APPDATA\npm\node_modules\@anthropic-ai\claude-code\cli.js" - -if (-not (Test-Path $cliPath)) { - Write-Host "ERROR: Run: npm install -g @anthropic-ai/claude-code" -ForegroundColor Red; exit 1 -} - -$code = [System.IO.File]::ReadAllText($cliPath, [System.Text.Encoding]::UTF8) -$marker = '/* SCROLL_FIX_v7 */' - -if ($Uninstall) { - if ($code.Contains($marker)) { - $s = $code.IndexOf($marker); $e = $code.IndexOf('})();', $s) + 5 - $code = $code.Substring(0, $s) + $code.Substring($e) - if ($code[$s] -eq "`n") { $code = $code.Substring(0, $s) + $code.Substring($s + 1) } - [System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) - Write-Host "Reverted." -ForegroundColor Green - } else { Write-Host "No patch found." -ForegroundColor Yellow } - exit 0 -} - -if ($code.Contains($marker)) { Write-Host "Already patched." -ForegroundColor Green; exit 0 } - -foreach ($old in @('/* SCROLL_FIX_v2 */','/* SCROLL_FIX_v4 */','/* SCROLL_FIX_v5 */','/* SCROLL_FIX_v6 */')) { - if ($code.Contains($old)) { - $s = $code.IndexOf($old); $e = $code.IndexOf('})();', $s) + 5 - $code = $code.Substring(0, $s) + $code.Substring($e) - } -} -$code = $code.Replace('var SK6=200;','var SK6=16;').Replace('var SK6=1000;','var SK6=16;') -$code = $code.Replace('kk7="",Ek7=""','kk7=qs(XO.SYNCHRONIZED_UPDATE),Ek7=Ks(XO.SYNCHRONIZED_UPDATE)') - -$fix = '/* SCROLL_FIX_v7 */;(function(){var _ow=process.stdout.write.bind(process.stdout);var _buf=[];var _userInput=false;process.stdin.on("data",function(){_userInput=true});function _flush(){if(_buf.length===0)return;var all="";for(var i=0;i<_buf.length;i++)all+=_buf[i];_buf=[];_ow(all)}process.stdout.write=function(d,e,c){if(typeof e==="function"){c=e;e=void 0}var s=typeof d==="string"?d:Buffer.isBuffer(d)?d.toString("utf-8"):String(d);if(s.indexOf("\x1b[?2026h")!==-1){if(_userInput){_userInput=false;_flush();return _ow(s,e,c)}_buf.push(s);if(c)c();return true}return _ow(s,e,c)};})();' - -$i = $code.IndexOf('import{') -$code = $code.Substring(0, $i) + $fix + "`n" + $code.Substring($i) -[System.IO.File]::WriteAllText($cliPath, $code, [System.Text.Encoding]::UTF8) -Write-Host "Applied scroll fix v7." -ForegroundColor Green