fix(hermes): preserve paused editors during terminal reattachment

This commit is contained in:
jenkins 2026-09-14 00:36:31 -05:00
parent 15c79c6da4
commit cfcd932065
4 changed files with 59 additions and 2 deletions

View File

@ -1623,9 +1623,12 @@ RUN case "${HERMES_KANIKO_HEREDOC_COMPAT}" in 0) ;; 1) python /tmp/hermes-kaniko
# alternate screen and current viewport after a bounded replay.
COPY dockerfiles/patch-hermes-terminal-replay.py /tmp/patch-hermes-terminal-replay.py
COPY dockerfiles/hermes-terminal-replay-regression.py /tmp/hermes-terminal-replay-regression.py
COPY dockerfiles/hermes-terminal-resume-regression.js /tmp/hermes-terminal-resume-regression.js
RUN /opt/hermes/.venv/bin/python /tmp/patch-hermes-terminal-replay.py \
&& HERMES_SOURCE_ROOT=/opt/hermes /opt/hermes/.venv/bin/python \
/tmp/hermes-terminal-replay-regression.py
/tmp/hermes-terminal-replay-regression.py \
&& node /tmp/hermes-terminal-resume-regression.js \
&& node --check /opt/hermes/ui-tui/dist/entry.js
# The dashboard keeps keyboard and paste handling unchanged, but forwards only
# bounded SGR wheel reports when the TUI has explicitly enabled mouse tracking.

View File

@ -7,6 +7,7 @@
!dockerfiles/hermes-session-migrate.py
!dockerfiles/patch-hermes-terminal-replay.py
!dockerfiles/hermes-terminal-replay-regression.py
!dockerfiles/hermes-terminal-resume-regression.js
!dockerfiles/patch-hermes-dashboard-wheel.js
!dockerfiles/hermes-dashboard-terminal-input.ts
!dockerfiles/hermes-dashboard-terminal-input.test.ts

View File

@ -0,0 +1,33 @@
/** Exercise the pinned source and shipped SIGCONT handlers without starting Hermes. */
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const root = process.env.HERMES_SOURCE_ROOT || "/opt/hermes";
const handlers = [
["ui-tui/packages/hermes-ink/src/ink/ink.tsx", /^ private handleResume = \(\) => \{[\s\S]*?^ }/m],
["ui-tui/dist/entry.js", /^ handleResume = \(\) => \{[\s\S]*?^ };/m],
];
for (const [file, pattern] of handlers) {
const source = fs.readFileSync(path.join(root, file), "utf8");
const match = source.match(pattern);
assert.ok(match, `missing pinned SIGCONT handler: ${file}`);
// Evaluate only the reviewed handler, with renderer effects replaced by spies.
const Renderer = new Function(`return class { ${match[0].replace("private ", "")} }`)();
for (const [isTTY, isPaused, expected] of [
[true, false, 1],
[true, true, 0],
[false, false, 0],
]) {
const renderer = new Renderer();
let repaints = 0;
renderer.options = { stdout: { isTTY } };
renderer.isPaused = isPaused;
renderer.altScreenActive = true;
renderer.reenterAltScreen = () => { repaints++; };
renderer.handleResume();
assert.equal(repaints, expected, `${file}: TTY=${isTTY}, paused=${isPaused}`);
}
}
console.log("dashboard SIGCONT source and shipped handlers passed");

View File

@ -62,6 +62,25 @@ def patch_sources(root: Path) -> None:
" 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():
@ -71,6 +90,7 @@ def patch_sources(root: Path) -> None:
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():