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:
@@ -3,9 +3,10 @@
|
||||
* an install its browser run console cannot work, because every run the
|
||||
* dashboard starts is a tmux session (server/lib/tmux.js). The row must WARN
|
||||
* and never FAIL: sessions, analytics, lanes and the CLI all work without the
|
||||
* binary, so failing a doctor run over it would cry wolf. A tmux older than 3.7
|
||||
* (no pane scrollbars) also warns, as does a new binary still talking to an
|
||||
* older running server. tmux is faked through the module's own exec seam, so
|
||||
* binary, so failing a doctor run over it would cry wolf. A tmux older than its
|
||||
* platform asks for (tmux.upgradeAdvice: psmux 3.4.0 on Windows, tmux 3.7
|
||||
* elsewhere) also warns, as does a new binary still talking to an older
|
||||
* running server. tmux is faked through the module's own exec seam, so
|
||||
* the test never depends on the host having it.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
@@ -56,18 +57,18 @@ describe("plugin-doctor: tmux row", () => {
|
||||
assert.equal(rowWithTmux("next-3.8", "3.8-rc3").level, "ok");
|
||||
});
|
||||
|
||||
it("warns, with the upgrade path, when tmux is older than 3.7", () => {
|
||||
const row = rowWithTmux("3.4", "3.4");
|
||||
it("warns, with this platform's upgrade path, when tmux is older than it asks for", () => {
|
||||
// 2.9 is too old everywhere; the advice (minimum, command) is per platform.
|
||||
const row = rowWithTmux("2.9", "2.9");
|
||||
|
||||
assert.equal(row.level, "warn");
|
||||
assert.match(row.detail, /3\.7/);
|
||||
assert.match(row.detail, /brew upgrade tmux/);
|
||||
assert.ok(row.detail.includes(tmux.upgradeAdvice("2.9")), row.detail);
|
||||
});
|
||||
|
||||
it("warns when a new tmux is still talking to an older running server", () => {
|
||||
const row = rowWithTmux("3.7c", "3.4");
|
||||
const row = rowWithTmux("3.7c", "2.9");
|
||||
|
||||
assert.equal(row.level, "warn");
|
||||
assert.match(row.detail, /server is 3\.4/);
|
||||
assert.match(row.detail, /server is 2\.9/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,7 +116,8 @@ describe("pty-attach", () => {
|
||||
|
||||
it("wires PTY data to binary WS frames and WS binary frames to PTY writes", () => {
|
||||
const ws = makeFakeWs();
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
|
||||
// Linux: a Windows attach also says `wheel` first (its own test below).
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24, platform: "linux" });
|
||||
|
||||
fakePty.__emitter.emit("data", "hello from claude");
|
||||
assert.equal(ws.__sent.length, 1);
|
||||
@@ -269,4 +270,21 @@ describe("attachBinary on Windows", () => {
|
||||
}
|
||||
tmux.__reset();
|
||||
});
|
||||
|
||||
it("tells a Windows attach to send the wheel itself (psmux never tracks the mouse)", () => {
|
||||
for (const [platform, told] of [
|
||||
["win32", true],
|
||||
["linux", false],
|
||||
]) {
|
||||
const ws = makeFakeWs();
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24, platform });
|
||||
const said = ws.__sent.filter((m) => !m.binary).map((m) => JSON.parse(m.data));
|
||||
assert.equal(
|
||||
said.some((m) => m.type === "wheel"),
|
||||
told,
|
||||
platform
|
||||
);
|
||||
ptyAttach.__resetGroups();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -190,6 +190,15 @@ describe("/api/run", () => {
|
||||
tmux.__setExecImpl(() => "tmux 3.4");
|
||||
const { body } = await fetchJson("/api/run/tmux");
|
||||
assert.equal(body.available, true);
|
||||
// The version and this platform's upgrade advice (null when new enough).
|
||||
assert.equal(body.version, "3.4");
|
||||
assert.equal(body.upgrade, tmux.upgradeAdvice("3.4"));
|
||||
});
|
||||
|
||||
it("GET /tmux says to upgrade a version older than the platform asks for", async () => {
|
||||
tmux.__setExecImpl(() => "tmux 2.9");
|
||||
const { body } = await fetchJson("/api/run/tmux");
|
||||
assert.match(body.upgrade, /older than/);
|
||||
});
|
||||
|
||||
it("GET /cwds still returns suggested directories (unchanged behavior)", async () => {
|
||||
|
||||
@@ -326,4 +326,21 @@ describe("tmux wrapper", () => {
|
||||
});
|
||||
assert.equal(tmux.isTmuxAvailable(), false);
|
||||
});
|
||||
it("upgradeAdvice asks each platform for its own minimum, with its own command", () => {
|
||||
// Windows: psmux before 3.4.0 (the WinGet `tmux`).
|
||||
assert.match(
|
||||
tmux.upgradeAdvice("3.3.2", "win32"),
|
||||
/psmux 3\.3\.2 .*3\.4\.0.*winget upgrade marlocarlo\.psmux/
|
||||
);
|
||||
assert.equal(tmux.upgradeAdvice("3.4.0", "win32"), null);
|
||||
assert.equal(tmux.upgradeAdvice("3.10.1", "win32"), null);
|
||||
// macOS / Linux: tmux before 3.7 (pane scrollbars).
|
||||
assert.match(tmux.upgradeAdvice("3.4", "darwin"), /brew upgrade tmux/);
|
||||
assert.match(tmux.upgradeAdvice("3.4", "linux"), /github\.com\/tmux\/tmux\/releases/);
|
||||
assert.equal(tmux.upgradeAdvice("3.7c", "linux"), null);
|
||||
assert.equal(tmux.upgradeAdvice("next-3.8", "darwin"), null);
|
||||
// Nothing to compare: no advice rather than a wrong one.
|
||||
assert.equal(tmux.upgradeAdvice(null, "linux"), null);
|
||||
assert.equal(tmux.upgradeAdvice("3.3.2", "freebsd"), null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -423,7 +423,14 @@ router.get("/binary", (req, res) => {
|
||||
});
|
||||
|
||||
router.get("/tmux", (_req, res) => {
|
||||
res.json({ available: tmux.isTmuxAvailable() });
|
||||
// `upgrade`: this platform's advice when the binary is older than AgentHub
|
||||
// asks for (tmux.upgradeAdvice), else null. The Workspace shows it.
|
||||
const version = tmux.clientVersion();
|
||||
res.json({
|
||||
available: version !== null,
|
||||
version,
|
||||
upgrade: version ? tmux.upgradeAdvice(version) : null,
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/", (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user