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,