fix(terminal): psmux wheel scrolling, per-platform tmux upgrade advice, Windows-safe server tests

- Server tests on Windows no longer reach the user's real psmux: psmux
  ignores TMUX_TMPDIR (and 3.3.x PSMUX_DATA_DIR), so quit.test.js's Quit and
  the runner's kill-server ended every live lane pane — the pre-commit hook
  included. scripts/test-server.js now takes tmux off the suites' PATH there.
- Wheel scrolling on Windows: psmux never turns mouse tracking on for its
  client, so xterm.js never reported the wheel and a pane could not scroll.
  A Windows attach is told {type:"wheel"} and the browser sends the SGR wheel
  report itself; psmux scrolls or forwards it to the pane's app.
- tmux.upgradeAdvice(): the oldest tmux AgentHub asks for and how each
  platform upgrades (psmux 3.4.0 -> winget upgrade marlocarlo.psmux; tmux 3.7
  -> brew upgrade tmux / build the release on Linux). Used by the doctor,
  GET /api/run/tmux ({available, version, upgrade}), `agenthub doctor`, and a
  sticky toast on the Workspace page. Advice only: nothing installs tmux.

Committed with --no-verify (user's call): the pre-commit hook runs the full
backend suite, which has ~378 pre-existing failures on Windows (S3/Linux-only
tests: sudo, /proc, systemd, members). Every test touching this change passes:
pty-attach, tmux, run, plugin-doctor-tmux, TerminalView, Workspace, and the
client tests related to the changed files; tsc and the header audit are clean.
This commit is contained in:
Nguyễn Ngọc Trí Vĩ
2026-10-15 11:46:09 +07:00
parent 7a88b74016
commit cd427d218b
19 changed files with 218 additions and 45 deletions
+5
View File
@@ -136,6 +136,11 @@ function attach(ws, runId, { cols, rows, platform = process.platform }) {
group.viewers.add(me);
if (lead) tell(me, { type: "size", cols: lead.cols, rows: lead.rows });
else group.driver = me;
// psmux never turns mouse tracking on for its client (not even for a pane
// app that asked), so xterm.js would never report the wheel and the pane
// could not scroll. psmux does act on a wheel report it receives: the
// browser sends one itself (TerminalView).
if (platform === "win32") tell(me, { type: "wheel" });
// Backpressure: a slow viewer (a remote browser on a thin link) must not
// queue a chatty pane's output in this process without bound. Past 1 MB
+35
View File
@@ -695,6 +695,40 @@ function clientVersion() {
}
}
// The oldest tmux AgentHub asks for, and how each platform upgrades it.
// Windows' is psmux: before 3.4.0 a dashboard terminal stalled until a
// refresh, and fixes for frame push, writes and synchronized output landed in
// 3.3.6–3.4.0. Elsewhere 3.7, the first with pane scrollbars (enableMouse).
const UPGRADES = {
win32: {
min: [3, 4, 0],
name: "psmux",
how: "close every AgentHub terminal pane, then run: winget upgrade marlocarlo.psmux",
},
darwin: { min: [3, 7, 0], name: "tmux", how: "brew upgrade tmux" },
linux: {
min: [3, 7, 0],
name: "tmux",
how:
"distros ship older ones (Ubuntu 24.04 has 3.4), so build the release from " +
"https://github.com/tmux/tmux/releases (needs libevent-dev libncurses-dev bison: " +
"./configure && make && sudo make install)",
},
};
/** What to tell the user when `version` is older than their platform asks
* for; null when it is new enough or cannot be compared. Advice only —
* AgentHub never installs or restarts tmux. */
function upgradeAdvice(version = clientVersion(), platform = process.platform) {
const want = UPGRADES[platform];
const m = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(version || "");
if (!want || !m) return null;
const have = [m[1], m[2], m[3] || 0].map(Number);
const i = have.findIndex((n, k) => n !== want.min[k]);
if (i === -1 || have[i] > want.min[i]) return null;
return `${want.name} ${version} is older than ${want.min.join(".")} — ${want.how}`;
}
/** The RUNNING server's version, null when none runs. A server started before
* an upgrade keeps its old version until every tmux session has closed. */
function serverVersion() {
@@ -725,6 +759,7 @@ module.exports = {
isTmuxAvailable,
clientVersion,
serverVersion,
upgradeAdvice,
serverPid,
memberAttach,
__setExecImpl,