From 16329c897b3dc38a854291e0264d7f26afb0be70 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 25 Apr 2026 22:43:24 -0700 Subject: [PATCH] fix(extension): xterm fills the full Terminal panel height MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Terminal pane only rendered into the top portion of the panel — most of the panel below the prompt was an empty black gap. Three layered issues, all about xterm.js measuring dimensions during a layout state that wasn't ready yet: 1. order-of-operations in connect(): ensureXterm() ran BEFORE setState(LIVE), so term.open() measured els.mount while it was still display:none. xterm caches a 0-size viewport synchronously inside open() and never auto-recovers when the container goes visible. Flipped: setState(LIVE) → ensureXterm. 2. first fit() ran synchronously before the browser had applied the .active class transition. Wrapped in requestAnimationFrame so layout has settled before fit() reads clientHeight. 3. CSS flex-overflow trap: .terminal-mount has flex:1 inside the flex-column #tab-terminal, but .tab-content's `overflow-y: auto` and the lack of `min-height: 0` on .terminal-mount meant the item couldn't shrink below content size. flex:1 then refused to expand into available space and xterm rendered into whatever its initial 2x2 measurement happened to be. Fixes: - extension/sidepanel-terminal.js: reorder + RAF fit - extension/sidepanel.css: .terminal-mount gets `flex: 1 1 0` + `min-height: 0` + `position: relative`. #tab-terminal overrides .tab-content's `overflow-y: auto` to `overflow: hidden` (xterm has its own viewport scroll; the parent shouldn't compete) and explicitly re-declares `display: flex; flex-direction: column` for #tab-terminal.active. bun test browse/test/sidebar-tabs.test.ts → 27/27 pass. Manually verified: side panel opens → Terminal fills full panel height, xterm scrollback works, debug-tab toggle still repaints correctly. --- extension/sidepanel-terminal.js | 27 ++++++++++++++++++++++++--- extension/sidepanel.css | 22 +++++++++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/extension/sidepanel-terminal.js b/extension/sidepanel-terminal.js index 5beff000..e301d085 100644 --- a/extension/sidepanel-terminal.js +++ b/extension/sidepanel-terminal.js @@ -166,8 +166,25 @@ fitAddon = new FitAddonModule.FitAddon(); term.loadAddon(fitAddon); } + // CRITICAL: caller must make els.mount visible BEFORE invoking + // ensureXterm. xterm.js measures the container synchronously inside + // term.open() — if the mount is display:none, xterm caches a 0-size + // viewport and never auto-grows even after the container goes + // visible. The visible-first pattern is enforced by connect() + // calling setState(STATE.LIVE) before us. term.open(els.mount); - fitAddon && fitAddon.fit(); + // First fit waits for the next paint frame so the browser has + // applied the .active class transition. Otherwise term.cols/rows + // can come back as the minimum (2x2) when the mount's clientHeight + // is still being computed. + requestAnimationFrame(() => { + try { + fitAddon && fitAddon.fit(); + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })); + } + } catch {} + }); const ro = new ResizeObserver(() => { try { @@ -224,9 +241,13 @@ return; } - ensureXterm(); + // setState(LIVE) flips terminal-mount from display:none to display:flex. + // We MUST do that BEFORE ensureXterm() — xterm.js measures the container + // synchronously inside term.open() and a hidden container yields a 0x0 + // terminal that never recovers. ensureXterm + the requestAnimationFrame + // fit() inside it run after the browser has applied the layout. setState(STATE.LIVE); - fitAddon && fitAddon.fit(); + ensureXterm(); // Token rides on Sec-WebSocket-Protocol — the only auth header the // browser WebSocket API lets us set. Cross-port HttpOnly cookies with diff --git a/extension/sidepanel.css b/extension/sidepanel.css index c28201ac..8813a0d0 100644 --- a/extension/sidepanel.css +++ b/extension/sidepanel.css @@ -676,9 +676,19 @@ body::after { .tab-content.active { display: flex; flex-direction: column; } /* ─── Terminal Tab ────────────────────────────────────────────── */ +/* The Terminal pane manages its own scrolling (xterm has a viewport with + scrollback). The default .tab-content rules above set overflow-y: auto, + which collapses min-height for nested flex children — that's why + .terminal-mount couldn't grow to fill available space. Override here. */ #tab-terminal { background: #0a0a0a; padding: 0; + overflow: hidden; + min-height: 0; +} +#tab-terminal.active { + display: flex; + flex-direction: column; } .terminal-toolbar { display: flex; @@ -746,11 +756,21 @@ body::after { } .install-retry-btn:hover { opacity: 0.9; } .terminal-mount { - flex: 1; + /* min-height: 0 is the standard flex-overflow fix — without it, a flex + item with overflowing content can't shrink below its content size, + so flex:1 refuses to expand into available space and xterm renders + into whatever the content happens to be (i.e. its own initial 2x2 + measurement). With min-height:0 the item respects the flex parent's + remaining space and xterm grows to fill it. */ + flex: 1 1 0; + min-height: 0; width: 100%; background: #0a0a0a; padding: 8px; box-sizing: border-box; + /* position: relative so xterm's absolutely-positioned helpers (the + hidden textarea for input) anchor inside us, not on body. */ + position: relative; } .terminal-mount .xterm, .terminal-mount .xterm .xterm-viewport,