104 lines
4.3 KiB
Python
104 lines
4.3 KiB
Python
"""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)
|