From 8915500eacc5baa91d6d5bcd0e4f6190b5bbb334 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Thu, 16 Apr 2026 00:00:00 +0100 Subject: [PATCH] =?UTF-8?q?Fix=20terminal=20size=20on=20connect=20?= =?UTF-8?q?=E2=80=94=20send=20dimensions=20after=20handshake?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: The PTY is allocated at 80x24 during the SSH handshake. The frontend xterm.js fits correctly to the container (e.g. 120x35), but term.onResize fires before the backend connection is ready, so the ResizeSession call is silently rejected. When the connection completes, fitAddon.fit() sees the terminal already has the right cols/rows and does NOT fire another onResize event. The PTY stays at 80x24. Fix: On 'session:state → connected', explicitly read the current terminal cols/rows and send them to the backend via ResizeSession, bypassing the onResize event dependency. Also fix empty-state ordering in addTab() — hide the empty-state BEFORE activating the panel so the panel-container gets the full workspace height immediately (empty-state has flex:1 and was stealing half the height during initial layout). Simplify createTerminal() fit logic — force a synchronous reflow via offsetHeight access, then fit immediately. --- frontend/src/js/app.js | 21 ++++++++++++++++----- frontend/src/js/tabs.js | 4 +++- frontend/src/js/terminal.js | 24 ++++++++---------------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index 26c5b49..81dcb02 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -134,13 +134,24 @@ class App { window.runtime.EventsOn('session:state', (payload) => { this.tabs.updateSessionState(payload.sessionID, payload.state, payload.error); this.updateStatusBar(); - // Re-fit the terminal once connected so it picks up the final container size. - // Use multiple delays because the panel layout may still be settling. + // Once connected, ensure the backend PTY has the correct dimensions. + // The initial fit happens before the SSH handshake completes, so the + // onResize callback fires when the backend isn't ready. By the time + // the connection is established, xterm already has the right cols/rows + // and fitAddon.fit() won't trigger a new resize event. We must + // explicitly send the current terminal size to the backend. if (payload.state === 'connected') { + const entry = this.terminals.terminals.get(payload.sessionID); + if (entry) { + const { cols, rows } = entry.term; + try { + window.go.api.Service.ResizeSession(payload.sessionID, cols, rows); + } catch (_) { /* noop */ } + } + // Also re-fit to catch any layout changes that occurred while connecting. const fit = () => this.terminals.fitSession(payload.sessionID); - setTimeout(fit, 30); - setTimeout(fit, 150); - setTimeout(fit, 500); + setTimeout(fit, 50); + setTimeout(fit, 300); } }); diff --git a/frontend/src/js/tabs.js b/frontend/src/js/tabs.js index 29d4a7e..ff7630c 100644 --- a/frontend/src/js/tabs.js +++ b/frontend/src/js/tabs.js @@ -47,8 +47,10 @@ class TabManager { panel.style.display = 'none'; this.panelContainer.appendChild(panel); - this.setActive(sessionInfo.id); + // Hide the empty-state BEFORE activating the panel so the + // panel-container gets the full workspace height immediately. this._updateEmptyState(); + this.setActive(sessionInfo.id); } /** diff --git a/frontend/src/js/terminal.js b/frontend/src/js/terminal.js index f859de1..6630eec 100644 --- a/frontend/src/js/terminal.js +++ b/frontend/src/js/terminal.js @@ -67,23 +67,15 @@ class TerminalManager { const container = document.getElementById(containerId); term.open(container); - // Fit the terminal once the container has real dimensions. - // The panel may have just transitioned from display:none, so we - // poll until the container reports a non-trivial size. + // Force a synchronous layout reflow so the container reports its + // true dimensions, then fit the terminal to fill the space. + // eslint-disable-next-line no-unused-expressions + container.offsetHeight; const doFit = () => { try { fitAddon.fit(); } catch (_) { /* noop */ } }; - let fitAttempts = 0; - const tryFit = () => { - fitAttempts++; - const rect = container.getBoundingClientRect(); - if (rect.width > 10 && rect.height > 10) { - doFit(); - } else if (fitAttempts < 20) { - // Container still has no size — retry - setTimeout(tryFit, 50); - return; - } - }; - requestAnimationFrame(tryFit); + doFit(); + // Belt-and-braces: retry once after a paint in case the initial + // reflow dimensions were still stale. + requestAnimationFrame(doFit); // Forward user keystrokes to the backend term.onData((data) => {