From 9606b0eb1a7797a548d0fd19fc6aca7630cd9846 Mon Sep 17 00:00:00 2001 From: Sauyon Lee Date: Sun, 5 Apr 2026 00:38:34 -0700 Subject: [PATCH] fix: preserve scroll position when new output arrives When the user has scrolled up to review earlier output, keep the viewport locked on the same content as new lines arrive instead of snapping back to the bottom. Save the scrollback length before each write and adjust `viewportY` by the delta afterward. Clamp to the current scrollback length in case old lines are dropped by the buffer limit. When the user is already at the bottom (`viewportY === 0`), the adjustment is skipped and the viewport naturally follows new output. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/terminal.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/terminal.ts b/lib/terminal.ts index eeb7acd..a5f701b 100644 --- a/lib/terminal.ts +++ b/lib/terminal.ts @@ -557,6 +557,14 @@ export class Terminal implements ITerminalCore { // preserve selection when new data arrives. Selection is cleared by user actions // like clicking or typing, not by incoming data. + // Save scroll state before writing. viewportY is relative to the + // bottom, so if new lines push content into scrollback we need to + // bump viewportY by the same amount to keep the viewport locked on + // the same content. + const savedViewportY = this.viewportY; + const savedScrollback = savedViewportY > 0 + ? this.wasmTerm!.getScrollbackLength() : 0; + // Write directly to WASM terminal (handles VT parsing internally) this.wasmTerm!.write(data); @@ -575,9 +583,14 @@ export class Terminal implements ITerminalCore { // Invalidate link cache (content changed) this.linkDetector?.invalidateCache(); - // Phase 2: Auto-scroll to bottom on new output (xterm.js behavior) - if (this.viewportY !== 0) { - this.scrollToBottom(); + // If the user had scrolled up, adjust viewportY so the viewport + // stays locked on the same content instead of drifting as new + // scrollback lines are added. Clamp to the current scrollback + // length in case old lines were dropped by the scrollback limit. + if (savedViewportY > 0) { + const newScrollback = this.wasmTerm!.getScrollbackLength(); + const delta = newScrollback - savedScrollback; + this.viewportY = Math.min(savedViewportY + Math.max(0, delta), newScrollback); } // Check for title changes (OSC 0, 1, 2 sequences)