"""Restore dashboard terminal modes and visible history on PTY reattachment.""" from __future__ import annotations import argparse from pathlib import Path def patch_sources(root: Path) -> None: """Patch the pinned upstream sources, rejecting drift before writing files.""" patches = { "hermes_cli/pty_bridge.py": ( ( " self._closed = False\n", " self._closed = False\n" " self._dashboard_redraw_on_attach = False\n", ), ( " return cls(proc)\n", " bridge = cls(proc)\n" " bridge._dashboard_redraw_on_attach = (\n" " spawn_env.get(\"HERMES_TUI_DASHBOARD\") == \"1\"\n" " and spawn_env.get(\"HERMES_TUI_INLINE\") == \"0\"\n" " )\n" " return bridge\n", ), ( " # -- I/O --------------------------------------------------------------\n", ''' def redraw_after_attach(self) -> None: """Ask an opted-in dashboard TUI to restore its screen without input.""" if not self._dashboard_redraw_on_attach or not self.is_alive(): return try: # Ink's SIGCONT handler restores alternate-screen/mouse modes and # paints the current viewport, even after replay bytes were evicted. os.kill(self.pid, signal.SIGCONT) except OSError: # The child may exit between the liveness check and the signal. pass # -- I/O -------------------------------------------------------------- ''', ), ), "hermes_cli/pty_session.py": ( ( " self.attached = False\n" " self.last_detached_at: Optional[float] = None\n", " self.attached = False\n" " self._has_attached = False\n" " self.last_detached_at: Optional[float] = None\n", ), ( " if snap:\n await ws.send_bytes(snap)\n", " if snap:\n await ws.send_bytes(snap)\n" " # A byte-bounded replay is not a complete terminal snapshot.\n" " # Restore modes and repaint only a reused dashboard TUI.\n" " if self._has_attached:\n" " redraw = getattr(self.bridge, \"redraw_after_attach\", None)\n" " if redraw is not None:\n" " redraw()\n" " self._has_attached = True\n", ), ), # The pinned image ships readable esbuild output without the TUI build # dependencies. Keep its source and shipped handler in sync, and reject # either anchor changing rather than resolving a new dependency graph. "ui-tui/packages/hermes-ink/src/ink/ink.tsx": ( ( " private handleResume = () => {\n" " if (!this.options.stdout.isTTY) {\n", " private handleResume = () => {\n" " if (!this.options.stdout.isTTY || this.isPaused) {\n", ), ), "ui-tui/dist/entry.js": ( ( " handleResume = () => {\n" " if (!this.options.stdout.isTTY) {\n", " handleResume = () => {\n" " if (!this.options.stdout.isTTY || this.isPaused) {\n", ), ), } prepared = {} for relative, replacements in patches.items(): path = root / relative source = path.read_text() for before, after in replacements: if source.count(before) != 1: raise ValueError(f"Hermes terminal replay patch context changed: {relative}") source = source.replace(before, after, 1) if path.suffix == ".py": compile(source, str(path), "exec") prepared[path] = source for path, source in prepared.items(): path.write_text(source) if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=Path, default=Path("/opt/hermes")) patch_sources(parser.parse_args().root)