diff --git a/emain/emain-window.ts b/emain/emain-window.ts index d3b7f4849e..cfccbe8256 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -433,7 +433,7 @@ export class WaveBrowserWindow extends BaseWindow { await Promise.all([p1, p2]); } else { console.log("reusing an existing tab, calling wave-init", tabView.waveTabId); - const p1 = this.repositionTabsSlowly(35); + const p1 = this.repositionTabsSlowly(35, true); const p2 = tabView.webContents.send("wave-init", tabView.savedInitOpts); // reinit await Promise.all([p1, p2]); } @@ -452,13 +452,16 @@ export class WaveBrowserWindow extends BaseWindow { }, 30); } - private async repositionTabsSlowly(delayMs: number) { + private async repositionTabsSlowly(delayMs: number, skipIntermediate: boolean = false) { const activeTabView = this.activeTabView; const winBounds = this.getContentBounds(); if (activeTabView == null) { return; } - if (activeTabView.isOnScreen()) { + if (skipIntermediate || activeTabView.isOnScreen()) { + // For already-initialized tabs (skipIntermediate=true) or tabs already on screen, + // go directly to final bounds to avoid an intermediate resize that causes + // xterm.js buffer reflow and scroll position loss. activeTabView.setBounds({ x: 0, y: 0, diff --git a/frontend/app/view/term/osc-handlers.ts b/frontend/app/view/term/osc-handlers.ts index e066faafa0..40ef491cb7 100644 --- a/frontend/app/view/term/osc-handlers.ts +++ b/frontend/app/view/term/osc-handlers.ts @@ -318,6 +318,12 @@ export function handleOsc16162Command(data: string, blockId: string, loaded: boo case "R": globalStore.set(termWrap.shellIntegrationStatusAtom, null); if (terminal.buffer.active.type === "alternate") { + // Save scroll position before alternate buffer exit (only if user scrolled up) + const normalBuffer = terminal.buffer.normal; + const isAtBottom = normalBuffer.baseY >= normalBuffer.length - terminal.rows; + if (!isAtBottom) { + termWrap.savedBaseY = normalBuffer.baseY; + } terminal.write("\x1b[?1049l"); } break; diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 45ba48351c..c133ad0ea7 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -104,6 +104,9 @@ export class TermWrap { lastPasteData: string = ""; lastPasteTime: number = 0; + // Scroll position preservation across buffer swaps (alt screen exit) + savedBaseY: number | null = null; + constructor( tabId: string, blockId: string, @@ -197,6 +200,19 @@ export class TermWrap { this.heldData = []; this.handleResize_debounced = debounce(50, this.handleResize.bind(this)); this.terminal.open(this.connectElem); + + // Restore scroll position after alternate buffer exit (buffer swap) + this.toDispose.push( + this.terminal.buffer.onBufferChange(() => { + if (this.savedBaseY !== null) { + const restoreY = this.savedBaseY; + this.savedBaseY = null; + requestAnimationFrame(() => { + this.terminal.scrollToLine(restoreY); + }); + } + }) + ); this.handleResize(); const pasteHandler = this.pasteHandler.bind(this); this.connectElem.addEventListener("paste", pasteHandler, true); @@ -465,8 +481,19 @@ export class TermWrap { handleResize() { const oldRows = this.terminal.rows; const oldCols = this.terminal.cols; + // Save scroll position before fit (only if user scrolled up from bottom) + const buffer = this.terminal.buffer.active; + const wasAtBottom = buffer.baseY >= buffer.length - this.terminal.rows; + const fitSavedY = wasAtBottom ? null : buffer.baseY; this.fitAddon.fit(); - if (oldRows !== this.terminal.rows || oldCols !== this.terminal.cols) { + // Only restore scroll if rows/cols actually changed (reflow happened) + const dimsChanged = oldRows !== this.terminal.rows || oldCols !== this.terminal.cols; + if (fitSavedY !== null && dimsChanged) { + requestAnimationFrame(() => { + this.terminal.scrollToLine(fitSavedY); + }); + } + if (dimsChanged) { const termSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols }; RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, termsize: termSize }); }