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 {