93 lines
3.0 KiB
JavaScript
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);
|