atlas-iac/dockerfiles/patch-hermes-dashboard-wheel.js

93 lines
3.0 KiB
JavaScript

/** Route only validated xterm wheel reports to an alternate-screen dashboard PTY. */
const fs = require("node:fs");
const path = process.env.HERMES_CHAT_PAGE_PATH || "/opt/hermes/web/src/pages/ChatPage.tsx";
let source = fs.readFileSync(path, "utf8");
function replaceOnce(before, after, label) {
const count = source.split(before).length - 1;
if (count !== 1) {
throw new Error(`${label} patch context changed: expected 1, found ${count}`);
}
source = source.replace(before, after);
}
replaceOnce(
'import { api } from "@/lib/api";\n',
'import { api } from "@/lib/api";\n' +
'import { isSgrWheelReport } from "@/lib/dashboard-terminal-input";\n',
"dashboard wheel input import",
);
replaceOnce(
` // Dashboard chat should scroll the browser-side transcript, not send
// mouse-wheel protocol bytes through the PTY.
term.attachCustomWheelEventHandler((ev) => {
const delta = ev.deltaY;
if (!delta) {
return false;
}
const step = Math.max(1, Math.round(Math.abs(delta) / 50));
term.scrollLines(delta > 0 ? step : -step);
ev.preventDefault();
ev.stopPropagation();
return false;
});
`,
"",
"browser-local wheel handler",
);
replaceOnce(
` // Browser-embedded chat runs the TUI in inline mode. Keep transcript
// history in xterm.js so the browser wheel can scroll it directly.
scrollback: 5000,
`,
` // Alternate-screen PTYs keep their own visible history. Retain a
// modest xterm scrollback only for terminal protocol compatibility.
scrollback: 5000,
`,
"dashboard alternate-screen scrollback comment",
);
const inputCommentStart = source.indexOf(" // Keystrokes → PTY.");
const inputCommentEnd = source.indexOf(
" // eslint-disable-next-line no-control-regex",
inputCommentStart,
);
if (inputCommentStart < 0 || inputCommentEnd < 0) {
throw new Error("dashboard PTY input comment patch context changed");
}
source =
source.slice(0, inputCommentStart) +
` // Keystrokes and paste retain their existing PTY path. Mouse reports
// are fail-closed below: only a bounded SGR wheel report can pass.
` +
source.slice(inputCommentEnd);
replaceOnce(
String.raw` // eslint-disable-next-line no-control-regex -- intentional ESC byte in xterm SGR mouse report parser
const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/;
onDataDisposable = term.onData((data) => {
if (ws.readyState !== WebSocket.OPEN) return;
if (SGR_MOUSE_RE.test(data)) {
return;
}
ws.send(data);
});
`,
String.raw` onDataDisposable = term.onData((data) => {
if (ws.readyState !== WebSocket.OPEN) return;
if (data.startsWith("\x1b[<") && !isSgrWheelReport(data)) return;
if (data.startsWith("\x1b[M")) return;
ws.send(data);
});
`,
"SGR wheel relay",
);
if (source.includes("attachCustomWheelEventHandler")) {
throw new Error("dashboard wheel interception remained after patch");
}
fs.writeFileSync(path, source);