fix: v2.5.7 — panes work on psmux, the Windows tmux

psmux (the WinGet `tmux` on Windows) has no `=name` exact-match targets: it
answered "no server running on session '=agenthub-lane-1'" for a live pane,
so every Windows start reported "the session's command exited as soon as it
started" while the session ran on, and the browser could not attach.

- server/lib/tmux.js: `session`/`pane` targets probe the first live session
  once — `=name` failing where the bare name answers means bare names from
  then on; real tmux keeps `=name` / `=name:` (it matches by prefix otherwise)
- pty-attach.js and cc-update-all-helper.js use the same targets
This commit is contained in:
2026-10-06 23:56:12 +07:00
parent fcf8d8010a
commit 4163f431bb
9 changed files with 88 additions and 13 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ function attach(ws, runId, { cols, rows }) {
} catch {
/* session gone or tmux unavailable — attach below will surface that */
}
const pty = spawnImpl(attachBinary(), ["attach-session", "-t", `=${runId}`], {
const pty = spawnImpl(attachBinary(), ["attach-session", "-t", tmux.session(runId)], {
name: "xterm-256color",
cols: cols || 80,
rows: rows || 24,
+44 -3
View File
@@ -26,19 +26,58 @@ const { execFileSync } = require("node:child_process");
const EXEC_OPTS = { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] };
let execImpl = (args) => execFileSync("tmux", args, EXEC_OPTS);
function __setExecImpl(fn) {
// Whether this tmux takes `=name` targets: true for tmux itself, false for
// psmux (the WinGet `tmux` on Windows), null until a live session has shown
// which. A fake exec in a test is real tmux unless it says otherwise.
let exactTargets = null;
function __setExecImpl(fn, { exact = true } = {}) {
execImpl = fn;
exactTargets = exact;
}
function __reset() {
execImpl = (args) => execFileSync("tmux", args, EXEC_OPTS);
exactTargets = null;
}
/**
* Probe once, on a session that exists: `=name` failing where the bare name
* answers means this tmux has no exact-match syntax (psmux answers
* "no server running on session '=name'"), so every target there is the bare
* name. Until a session exists to ask, tmux's own syntax.
*/
function exact() {
if (exactTargets !== null) return exactTargets;
let first;
try {
first = String(execImpl(["list-sessions", "-F", "#{session_name}"]))
.split("\n")
.map((x) => x.trim())
.find(Boolean);
} catch {
return true;
}
if (!first) return true;
const answers = (t) => {
try {
execImpl(["has-session", "-t", t]);
return true;
} catch {
return false;
}
};
if (answers(`=${first}`)) exactTargets = true;
else if (answers(first)) exactTargets = false;
return exactTargets ?? true;
}
// tmux resolves a bare `-t agenthub-lane-1` by PREFIX when no exact match
// exists, so it would hit `agenthub-lane-12` once lane 1's session is gone —
// and killSession would kill the wrong lane. `=` forces an exact match; pane
// and option targets need the trailing `:` (tmux 3.4 rejects `=name` there).
const session = (name) => `=${name}`;
const pane = (name) => `=${name}:`;
// psmux has neither form and names a session by its bare name.
const session = (name) => (exact() ? `=${name}` : name);
const pane = (name) => (exact() ? `=${name}:` : name);
function hasSession(name) {
try {
@@ -247,6 +286,8 @@ function serverVersion() {
}
module.exports = {
session,
pane,
hasSession,
newSession,
enableMouse,