diff --git a/apps/api/src/services/terminal_session.py b/apps/api/src/services/terminal_session.py index 396f9d1..f66632a 100644 --- a/apps/api/src/services/terminal_session.py +++ b/apps/api/src/services/terminal_session.py @@ -145,21 +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. - # 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. + # 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"stty -echo; stty cols {cols} rows {rows}; stty echo\n" + ).encode() + await self.write_input(stty_cmd) + 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.""" diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index 68434e4..6e13cad 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -216,17 +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; + + console.log('[Terminal] fitTerminal called, container dims:', container?.offsetWidth, 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); - } + 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); } }; @@ -284,6 +296,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(); @@ -316,8 +341,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(); 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 */