From ca22e9c9d2fc730602f0ea0ae017c33fb0570cc6 Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 18:13:28 +0200 Subject: [PATCH 1/9] fix: reset xterm position to relative on mobile to prevent layout issues --- apps/web/src/styles.css | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index eac9252..ae3ddde 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -3220,21 +3220,15 @@ a.nav-item, position: relative; } -/* xterm fills container */ +/* xterm fills container - reset position for mobile */ .terminal-wrapper.mobile .terminal-container .xterm { + position: relative !important; + top: auto !important; + left: auto !important; + right: auto !important; + bottom: auto !important; width: 100%; height: 100%; - max-height: 100%; - display: flex; - flex-direction: column; -} - -.terminal-wrapper.mobile .terminal-container .xterm-viewport { - flex: 1; - width: 100% !important; - height: 100% !important; - max-height: 100% !important; - overflow-y: auto !important; } /* Special Keys Strip */ From 4a356fe88ecf42b40f9205926c86d81fb2396d67 Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 18:18:41 +0200 Subject: [PATCH 2/9] fix: add window resize listener and refresh on font size change - Add window resize listener to complement ResizeObserver - Clear window resize timeout on cleanup - Force term.refresh() after font size changes - Send resize message after font size change --- apps/web/src/components/terminal.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index 68434e4..b1203cd 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -284,6 +284,16 @@ export const TerminalComponent: React.FC = ({ }); resizeObserver.observe(container); + // Also handle window resize (ResizeObserver might miss viewport-only changes) + let windowResizeTimeout: ReturnType; + const handleWindowResize = () => { + clearTimeout(windowResizeTimeout); + windowResizeTimeout = setTimeout(() => { + requestAnimationFrame(() => fitTerminal()); + }, 100); + }; + window.addEventListener("resize", handleWindowResize); + // Refit after mobile header auto-hides (3s delay + 0.3s transition) const headerHideTimeout = setTimeout(() => { fitTerminal(); @@ -316,8 +326,10 @@ export const TerminalComponent: React.FC = ({ return () => { clearTimeout(resizeTimeout); + clearTimeout(windowResizeTimeout); clearTimeout(headerHideTimeout); resizeObserver.disconnect(); + window.removeEventListener("resize", handleWindowResize); document.removeEventListener("visibilitychange", handleVisibilityChange); if (ws) { ws.close(); @@ -356,6 +368,8 @@ export const TerminalComponent: React.FC = ({ try { fitAddonRef.current.fit(); const { cols, rows } = termRef.current; + // Force refresh after font size change + termRef.current.refresh(0, rows - 1); if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send( JSON.stringify({ From d931f3071d2f4b63ccdf4814654fa6398fbf09db Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 18:42:14 +0200 Subject: [PATCH 3/9] Revert "fix: add window resize listener and refresh on font size change" This reverts commit 4a356fe88ecf42b40f9205926c86d81fb2396d67. --- apps/web/src/components/terminal.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index b1203cd..68434e4 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -284,16 +284,6 @@ export const TerminalComponent: React.FC = ({ }); resizeObserver.observe(container); - // Also handle window resize (ResizeObserver might miss viewport-only changes) - let windowResizeTimeout: ReturnType; - const handleWindowResize = () => { - clearTimeout(windowResizeTimeout); - windowResizeTimeout = setTimeout(() => { - requestAnimationFrame(() => fitTerminal()); - }, 100); - }; - window.addEventListener("resize", handleWindowResize); - // Refit after mobile header auto-hides (3s delay + 0.3s transition) const headerHideTimeout = setTimeout(() => { fitTerminal(); @@ -326,10 +316,8 @@ export const TerminalComponent: React.FC = ({ return () => { clearTimeout(resizeTimeout); - clearTimeout(windowResizeTimeout); clearTimeout(headerHideTimeout); resizeObserver.disconnect(); - window.removeEventListener("resize", handleWindowResize); document.removeEventListener("visibilitychange", handleVisibilityChange); if (ws) { ws.close(); @@ -368,8 +356,6 @@ export const TerminalComponent: React.FC = ({ try { fitAddonRef.current.fit(); const { cols, rows } = termRef.current; - // Force refresh after font size change - termRef.current.refresh(0, rows - 1); if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send( JSON.stringify({ From 85f04447ea489f71f2872fcee11e904ab35c46c5 Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 18:47:27 +0200 Subject: [PATCH 4/9] fix: force xterm.js canvas redraw on resize via internal renderer --- apps/web/src/components/terminal.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index 68434e4..f418772 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -219,9 +219,15 @@ export const TerminalComponent: React.FC = ({ if (!fitAddonRef.current || !termRef.current) return; const oldCols = termRef.current.cols; const oldRows = termRef.current.rows; + + // Force layout recalculation before fit + if (container) { + void container.offsetWidth; + void container.offsetHeight; + } + fitAddonRef.current.fit(); const { cols, rows } = termRef.current; - // Force refresh if dimensions changed if (cols !== oldCols || rows !== oldRows) { termRef.current.refresh(0, rows - 1); } From 701bd57293c4e6734f59b8523d1e4fc37e18eade Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 19:07:37 +0200 Subject: [PATCH 5/9] fix: send stty resize on every resize, not just first time The _stty_sent guard prevented the container shell from updating its terminal size after the first resize. This caused visual mismatches where xterm.js displayed at the new size but the shell still wrapped output at the old size. Remove the guard so stty is sent on every resize event. --- apps/api/src/services/terminal_session.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/api/src/services/terminal_session.py b/apps/api/src/services/terminal_session.py index 396f9d1..e45f546 100644 --- a/apps/api/src/services/terminal_session.py +++ b/apps/api/src/services/terminal_session.py @@ -154,12 +154,10 @@ 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. - # 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}") + # Send on every resize so the container shell always matches the frontend. + stty_cmd = f"stty cols {cols} rows {rows}\n".encode() + await self.write_input(stty_cmd) + logger.debug(f"Sent stty resize 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.""" From 4d0c70de985513d9b45c037d529f474a0f4f5497 Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 19:17:54 +0200 Subject: [PATCH 6/9] fix: hide stty resize command from terminal output using ANSI escapes --- apps/api/src/services/terminal_session.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/api/src/services/terminal_session.py b/apps/api/src/services/terminal_session.py index e45f546..b87110f 100644 --- a/apps/api/src/services/terminal_session.py +++ b/apps/api/src/services/terminal_session.py @@ -155,7 +155,13 @@ 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. # Send on every resize so the container shell always matches the frontend. - stty_cmd = f"stty cols {cols} rows {rows}\n".encode() + # Use ANSI escape sequences to hide the command from view: + # - \x1b[?25l: hide cursor + # - \x1b[A\x1b[2K: move up and clear line + # - \x1b[?25h: show cursor + stty_cmd = ( + f"\x1b[?25lstty cols {cols} rows {rows}\n\x1b[A\x1b[2K\x1b[?25h" + ).encode() await self.write_input(stty_cmd) logger.debug(f"Sent stty resize to container for session {self.session_id}: {cols}x{rows}") From 738e01bb7c54724bc3eda13d2d16cc8f90c0efc1 Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 19:31:15 +0200 Subject: [PATCH 7/9] fix: add window resize fallback and delay refresh to next frame - Add window resize listener as fallback for ResizeObserver - Use 250ms debounce to avoid early layout reads - Delay term.refresh() to next animation frame so renderer can process resize before we force redraw - Clean up window resize listener on unmount --- apps/web/src/components/terminal.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index f418772..e126688 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -229,7 +229,10 @@ export const TerminalComponent: React.FC = ({ fitAddonRef.current.fit(); const { cols, rows } = termRef.current; if (cols !== oldCols || rows !== oldRows) { - termRef.current.refresh(0, rows - 1); + // Delay refresh to next frame so renderer can process resize first + requestAnimationFrame(() => { + termRef.current?.refresh(0, rows - 1); + }); } if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "resize", cols, rows })); @@ -290,6 +293,19 @@ export const TerminalComponent: React.FC = ({ }); resizeObserver.observe(container); + // Also listen for window resize as fallback (ResizeObserver might miss some cases) + let windowResizeTimeout: ReturnType; + const handleWindowResize = () => { + clearTimeout(windowResizeTimeout); + windowResizeTimeout = setTimeout(() => { + requestAnimationFrame(() => { + if (!container.isConnected) return; + fitTerminal(); + }); + }, 250); + }; + window.addEventListener("resize", handleWindowResize); + // Refit after mobile header auto-hides (3s delay + 0.3s transition) const headerHideTimeout = setTimeout(() => { fitTerminal(); @@ -322,8 +338,10 @@ export const TerminalComponent: React.FC = ({ return () => { clearTimeout(resizeTimeout); + clearTimeout(windowResizeTimeout); clearTimeout(headerHideTimeout); resizeObserver.disconnect(); + window.removeEventListener("resize", handleWindowResize); document.removeEventListener("visibilitychange", handleVisibilityChange); if (ws) { ws.close(); From 3092038e407dd246ace605b822f4e92aaea58a42 Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 19:44:21 +0200 Subject: [PATCH 8/9] debug: add console logging for terminal resize debugging --- apps/api/src/services/terminal_session.py | 13 +++++++++++ apps/web/src/components/terminal.tsx | 27 +++++++++++++---------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/apps/api/src/services/terminal_session.py b/apps/api/src/services/terminal_session.py index b87110f..4302a25 100644 --- a/apps/api/src/services/terminal_session.py +++ b/apps/api/src/services/terminal_session.py @@ -164,6 +164,19 @@ class TerminalSession: ).encode() await self.write_input(stty_cmd) logger.debug(f"Sent stty resize to container for session {self.session_id}: {cols}x{rows}") + + # Docker exec doesn't forward PTY resize to the container process, + # so we need to explicitly set the size inside the container shell. + # Send on every resize so the container shell always matches the frontend. + # Use ANSI escape sequences to hide the command from view: + # - \x1b[?25l: hide cursor + # - \x1b[A\x1b[2K: move up and clear line + # - \x1b[?25h: show cursor + stty_cmd = ( + f"\x1b[?25lstty cols {cols} rows {rows}\n\x1b[A\x1b[2K\x1b[?25h" + ).encode() + await self.write_input(stty_cmd) + logger.debug(f"Sent stty resize 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 e126688..6e13cad 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -216,26 +216,29 @@ export const TerminalComponent: React.FC = ({ // Fit terminal and notify backend const fitTerminal = () => { - if (!fitAddonRef.current || !termRef.current) return; + if (!fitAddonRef.current || !termRef.current) { + console.log('[Terminal] fitTerminal: refs not ready'); + return; + } const oldCols = termRef.current.cols; const oldRows = termRef.current.rows; - // Force layout recalculation before fit - if (container) { - void container.offsetWidth; - void container.offsetHeight; - } + console.log('[Terminal] fitTerminal called, container dims:', container?.offsetWidth, container?.offsetHeight); fitAddonRef.current.fit(); const { cols, rows } = termRef.current; - if (cols !== oldCols || rows !== oldRows) { - // Delay refresh to next frame so renderer can process resize first - requestAnimationFrame(() => { - termRef.current?.refresh(0, rows - 1); - }); - } + console.log('[Terminal] fitTerminal result:', cols, rows, '(was:', oldCols, oldRows + ')'); + + // Always refresh on initial load or when dimensions change + requestAnimationFrame(() => { + termRef.current?.refresh(0, rows - 1); + }); + if (ws.readyState === WebSocket.OPEN) { + console.log('[Terminal] Sending resize:', cols, rows); ws.send(JSON.stringify({ type: "resize", cols, rows })); + } else { + console.log('[Terminal] WebSocket not open, state:', ws.readyState); } }; From 9f29ac15da6a55fde57db7a7daa8c5be2c8d9a06 Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 19:52:00 +0200 Subject: [PATCH 9/9] fix: improve terminal resize with better stty command and logging --- apps/api/src/services/terminal_session.py | 27 +++++++---------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/apps/api/src/services/terminal_session.py b/apps/api/src/services/terminal_session.py index 4302a25..f66632a 100644 --- a/apps/api/src/services/terminal_session.py +++ b/apps/api/src/services/terminal_session.py @@ -145,38 +145,27 @@ class TerminalSession: # Only resize if dimensions actually changed if cols == self._cols and rows == self._rows: + logger.info(f"resize() skipped for session {self.session_id}: already {cols}x{rows}") return self._cols = cols self._rows = rows logger.info(f"resize() called for session {self.session_id}: {cols}x{rows}") + + # Set host PTY size self._set_terminal_size(cols, rows) # Docker exec doesn't forward PTY resize to the container process, # so we need to explicitly set the size inside the container shell. # Send on every resize so the container shell always matches the frontend. - # Use ANSI escape sequences to hide the command from view: - # - \x1b[?25l: hide cursor - # - \x1b[A\x1b[2K: move up and clear line - # - \x1b[?25h: show cursor + # Use stty -echo to hide command output, then re-enable echo + # Add small delay to ensure shell is ready to receive commands + await asyncio.sleep(0.1) stty_cmd = ( - f"\x1b[?25lstty cols {cols} rows {rows}\n\x1b[A\x1b[2K\x1b[?25h" + f"stty -echo; stty cols {cols} rows {rows}; stty echo\n" ).encode() await self.write_input(stty_cmd) - logger.debug(f"Sent stty resize to container for session {self.session_id}: {cols}x{rows}") - - # Docker exec doesn't forward PTY resize to the container process, - # so we need to explicitly set the size inside the container shell. - # Send on every resize so the container shell always matches the frontend. - # Use ANSI escape sequences to hide the command from view: - # - \x1b[?25l: hide cursor - # - \x1b[A\x1b[2K: move up and clear line - # - \x1b[?25h: show cursor - stty_cmd = ( - f"\x1b[?25lstty cols {cols} rows {rows}\n\x1b[A\x1b[2K\x1b[?25h" - ).encode() - await self.write_input(stty_cmd) - logger.debug(f"Sent stty resize to container for session {self.session_id}: {cols}x{rows}") + logger.info(f"Sent stty resize 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."""