diff --git a/apps/api/src/services/terminal_session.py b/apps/api/src/services/terminal_session.py index a8c1f50..034b86f 100644 --- a/apps/api/src/services/terminal_session.py +++ b/apps/api/src/services/terminal_session.py @@ -61,15 +61,11 @@ class TerminalSession: logger.info(f"Starting terminal session {self.session_id} for container {self.container_id} with initial size {self._cols}x{self._rows}") # Start docker exec with the slave fd as stdin/stdout/stderr - # Using -i (interactive) but NOT -t (tty) because: - # 1. The slave fd IS a TTY - # 2. docker exec -t creates its OWN PTY inside the container - # 3. This prevents host PTY resize from propagating to the container shell - # By using only -i, docker exec uses our PTY slave directly + # Using -it because the slave fd IS a TTY self.process = await asyncio.create_subprocess_exec( "docker", "exec", - "-i", + "-it", "-e", "TERM=xterm", self.container_id, @@ -155,6 +151,16 @@ class TerminalSession: self._rows = rows logger.info(f"resize() called for session {self.session_id}: {cols}x{rows}") self._set_terminal_size(cols, rows) + + # Docker exec -it creates its own PTY inside the container, + # so host PTY resize doesn't propagate. We must send stty manually. + # To hide the command from the user: + # 1. \r moves cursor to start of current line (overwrites prompt) + # 2. stty command executes silently (no output on success) + # 3. \r moves cursor back to start, hiding the echoed command + stty_cmd = f"\rstty cols {cols} rows {rows}\r".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."""