From f37813a3179aacfdd3f93130bee3c49d8f9d19db Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 16:54:05 +0200 Subject: [PATCH] revert: remove ResizeObserver and stty-on-every-resize to fix infinite loop The ResizeObserver detected size changes caused by the stty command output appearing in the terminal, creating an infinite resize loop: 1. Resize detected -> fit() -> send resize to backend 2. Backend sends stty command through PTY 3. stty text appears in terminal output 4. ResizeObserver detects content height change 5. fit() calculates new rows -> send resize 6. Loop continues forever Reverted to: - Window resize event instead of ResizeObserver - stty command only sent once on first resize This means the container shell stays at the initial size and won't dynamically resize when the browser window changes, but prevents the infinite loop. --- apps/api/src/services/terminal_session.py | 14 ++++++-------- apps/web/src/components/terminal.tsx | 13 +++++++------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/api/src/services/terminal_session.py b/apps/api/src/services/terminal_session.py index 6b5d349..755722d 100644 --- a/apps/api/src/services/terminal_session.py +++ b/apps/api/src/services/terminal_session.py @@ -142,11 +142,6 @@ class TerminalSession: if self._closed: logger.warning("Cannot resize: session is closed") return - - # Only resize if dimensions actually changed - if self._cols == cols and self._rows == rows: - return - self._cols = cols self._rows = rows logger.info(f"resize() called for session {self.session_id}: {cols}x{rows}") @@ -154,9 +149,12 @@ class TerminalSession: # Docker exec doesn't forward PTY resize to the container process, # so we need to explicitly set the size inside the container shell. - stty_cmd = f"stty cols {cols} rows {rows}\n".encode() - await self.write_input(stty_cmd) - logger.info(f"Sent stty command to container for session {self.session_id}: {cols}x{rows}") + # Only do this on the first resize to avoid interfering with user input. + if not getattr(self, '_stty_sent', False): + self._stty_sent = True + stty_cmd = f"stty cols {cols} rows {rows}\n".encode() + await self.write_input(stty_cmd) + logger.info(f"Sent stty command to container for session {self.session_id}: {cols}x{rows}") async def reset(self) -> None: """Reset the session by killing the process and clearing state.""" diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index 2235ca9..38710d7 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -254,15 +254,16 @@ export const TerminalComponent: React.FC = ({ ws.send(data); }); - // Handle container resize with ResizeObserver (more reliable than window resize) + // Handle window resize with debounce let resizeTimeout: ReturnType; - const resizeObserver = new ResizeObserver(() => { + const handleResize = () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { fitTerminal(); - }, 100); - }); - resizeObserver.observe(container); + }, 250); + }; + + window.addEventListener("resize", handleResize); // Refit after mobile header auto-hides (3s delay + 0.3s transition) const headerHideTimeout = setTimeout(() => { @@ -297,7 +298,7 @@ export const TerminalComponent: React.FC = ({ return () => { clearTimeout(resizeTimeout); clearTimeout(headerHideTimeout); - resizeObserver.disconnect(); + window.removeEventListener("resize", handleResize); document.removeEventListener("visibilitychange", handleVisibilityChange); if (ws) { ws.close();