From 4de312c1704c4e0ae455b513643fcd1caa80da2d Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 17:44:00 +0200 Subject: [PATCH] fix: terminal resize propagation and redraw - Frontend: Add ResizeObserver with dimension tracking for accurate resize detection - Frontend: Fix cleanup function to properly disconnect ResizeObserver - Frontend: Use CSS grid for terminal wrapper layout - Backend: Add duplicate dimension check to avoid unnecessary resizes - Backend: Ensure stty command is sent correctly to container shell --- apps/api/src/services/terminal_session.py | 5 +++++ apps/web/src/components/terminal.tsx | 25 ++++++++++++++++------- apps/web/src/styles.css | 21 ++++++++----------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/apps/api/src/services/terminal_session.py b/apps/api/src/services/terminal_session.py index 755722d..396f9d1 100644 --- a/apps/api/src/services/terminal_session.py +++ b/apps/api/src/services/terminal_session.py @@ -142,6 +142,11 @@ class TerminalSession: if self._closed: logger.warning("Cannot resize: session is closed") return + + # Only resize if dimensions actually changed + if cols == self._cols and rows == self._rows: + return + self._cols = cols self._rows = rows logger.info(f"resize() called for session {self.session_id}: {cols}x{rows}") diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index 2defa46..68434e4 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -260,18 +260,29 @@ export const TerminalComponent: React.FC = ({ ws.send(data); }); - // Handle window resize with debounce - use rAF to ensure layout settled + // Handle container resize with ResizeObserver for accurate dimension tracking let resizeTimeout: ReturnType; - const handleResize = () => { + let lastWidth = 0; + let lastHeight = 0; + const resizeObserver = new ResizeObserver((entries) => { + const entry = entries[0]; + if (!entry) return; + + const { width, height } = entry.contentRect; + // Only trigger if dimensions actually changed + if (width === lastWidth && height === lastHeight) return; + lastWidth = width; + lastHeight = height; + clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { requestAnimationFrame(() => { + if (!container.isConnected) return; fitTerminal(); }); - }, 100); - }; - - window.addEventListener("resize", handleResize); + }, 50); + }); + resizeObserver.observe(container); // Refit after mobile header auto-hides (3s delay + 0.3s transition) const headerHideTimeout = setTimeout(() => { @@ -306,7 +317,7 @@ export const TerminalComponent: React.FC = ({ return () => { clearTimeout(resizeTimeout); clearTimeout(headerHideTimeout); - window.removeEventListener("resize", handleResize); + resizeObserver.disconnect(); document.removeEventListener("visibilitychange", handleVisibilityChange); if (ws) { ws.close(); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index aa7c0d2..eac9252 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -2506,8 +2506,8 @@ a.nav-item, } .terminal-wrapper { - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: auto 1fr; flex: 1; min-height: 0; border: 1px solid var(--border); @@ -2592,22 +2592,19 @@ a.nav-item, } .terminal-container { - flex: 1; - min-height: 0; - padding: var(--space-2); - display: flex; - flex-direction: column; - overflow: hidden; position: relative; width: 100%; height: 100%; + padding: var(--space-2); + overflow: hidden; } .terminal-container .xterm { - flex: 1; - min-height: 0; - width: 100%; - height: 100%; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; } .terminal-container canvas {