#245
Terminal scrolls to empty space after clear when focused with deep scrollback
Summary
After focusing a terminal with deep scrollback, clear (ESC[3J ESC[H ESC[2J) leaves the terminal scrolled to empty space below the rows. Typing and new output keep it blank; the next focus (click) restores it.
Cause
The input textarea is absolutely positioned in scroll-content coordinates, but InputHandler.syncInputPosition() only moves it while composing or on touch devices. Otherwise it keeps the offset from the last focus() (positionTextarea).
- Focus with deep scrollback: the textarea is placed at the cursor, e.g.
top: 6800px. ESC[3Jdrops the scrollback, but the stale textarea still setsscrollHeight._doRenderis stuck to the bottom, so_scrollToBottom()scrolls to the textarea. The!hasScrollbackreset toscrollTop = 0is in theelse ifbranch and doesn't run.- Every later render scrolls to the same bottom. Only
focus()repositions the textarea.
Repro (@wterm/dom 0.5.4, @wterm/ghostty core, Chrome)
const el = document.createElement("div");
el.style.cssText = "position:fixed;inset:0 auto auto 0;width:800px;height:300px";
document.body.appendChild(el);
const term = new WTerm(el, { core: await GhosttyCore.load(), cols: 80, rows: 10 });
await term.init();
term.write(Array.from({ length: 300 }, (_, i) => `line ${i}`).join("\r\n"));
await new Promise(requestAnimationFrame);
term.focus();
term.write("\x1b[3J\x1b[H\x1b[2J$ ");
await new Promise(requestAnimationFrame);
await new Promise(requestAnimationFrame);
console.log(el.scrollTop); // ~6500; expected 0, "$ " is out of viewPossible fix
Position the textarea in syncInputPosition unconditionally (it already runs once per render), or before _scrollToBottom() reads scrollHeight. We work around it by replacing syncInputPosition with positionTextarea, which fixes it.
+1. Reproduced on
@wterm/dom0.5.5 in Chrome, using a custom libghostty-vtTerminalCore. Besidesclear, it also happens:ESC[3J. Pi (a coding agent TUI) does this on every resize, so it hits whenever a terminal is refit, for example when switching to it from another device.less, vim) from deep scrollback.Measured after
clearwith 100k lines of scrollback (800px viewport):scrollTopscrollHeighttopWe work around it from outside with a
ResizeObserveron.term-gridthat moves the textarea back to the cursor row whenever the grid height changes. The fix suggested in this issue looks like the right one inside wterm:One caveat:
positionTextarea()callsgetBoundingClientRect()twice, so this adds layout reads to every render. We haven't measured the cost.