diff --git a/src/textual_webterm/local_server.py b/src/textual_webterm/local_server.py index c5e8b20..e62414c 100644 --- a/src/textual_webterm/local_server.py +++ b/src/textual_webterm/local_server.py @@ -445,6 +445,9 @@ class LocalServer: if session is None or not session.is_running(): self.session_manager.on_session_end(session_id) session_id = None + elif hasattr(session, "force_redraw"): + # Force redraw on reconnect to refresh tmux/screen displays + await session.force_redraw() session_created = session_id is not None diff --git a/src/textual_webterm/terminal_session.py b/src/textual_webterm/terminal_session.py index 760f440..7ba7dd9 100644 --- a/src/textual_webterm/terminal_session.py +++ b/src/textual_webterm/terminal_session.py @@ -56,6 +56,9 @@ class TerminalSession(Session): self._screen = pyte.Screen(DEFAULT_SCREEN_WIDTH, DEFAULT_SCREEN_HEIGHT) self._stream = pyte.Stream(self._screen) self._screen_lock = asyncio.Lock() + # Track last known terminal size for reconnection + self._last_width = DEFAULT_SCREEN_WIDTH + self._last_height = DEFAULT_SCREEN_HEIGHT super().__init__() def __rich_repr__(self) -> rich.repr.Result: @@ -64,6 +67,9 @@ class TerminalSession(Session): async def open(self, width: int = 80, height: int = 24) -> None: log.info("Opening terminal session %s with command: %s", self.session_id, self.command) + # Track the initial size + self._last_width = width + self._last_height = height # Initialize pyte screen with the requested size (under lock to prevent races) async with self._screen_lock: self._screen = pyte.Screen(width, height) @@ -100,6 +106,9 @@ class TerminalSession(Session): fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, buf) async def set_terminal_size(self, width: int, height: int) -> None: + # Track the size for reconnection + self._last_width = width + self._last_height = height # First resize the PTY (blocking call in executor) loop = asyncio.get_running_loop() await loop.run_in_executor(None, self._set_terminal_size, width, height) @@ -107,6 +116,17 @@ class TerminalSession(Session): async with self._screen_lock: self._screen.resize(height, width) + async def force_redraw(self) -> None: + """Force a terminal redraw by re-sending the current size. + + This triggers a SIGWINCH to the child process, causing applications + like tmux to redraw their display. + """ + loop = asyncio.get_running_loop() + await loop.run_in_executor( + None, self._set_terminal_size, self._last_width, self._last_height + ) + async def _add_to_replay_buffer(self, data: bytes) -> None: """Add data to replay buffer, maintaining size limit.""" async with self._replay_lock: