fix: v2.5.6 — Windows panes without sh, UI build without node on PATH
- Windows panes: a WinGet tmux runs native programs and there is no `sh`, so
every pane's `sh -c` wrapper died at once ("Command failed: tmux set-option
… mouse on"). paneArgv runs the CLI itself on win32, and a Terminal gets
tmux's own default shell (newSession drops the `--` for an empty argv).
- Plugin UI build: client deps install with --ignore-scripts and tsc/vite run
by the running node instead of `npm run build`. On Windows esbuild's
postinstall went through cmd.exe with no `node` on the server's PATH, both
installs failed, and Update now kept the previous UI. A test keeps the two
build steps equal to client/package.json's build script.
- Update all: Atomics.wait instead of a `sleep` binary Windows lacks.
This commit is contained in:
@@ -7233,7 +7233,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
|||||||
<p
|
<p
|
||||||
class="text-[10px] text-status-warning/90 mt-1"
|
class="text-[10px] text-status-warning/90 mt-1"
|
||||||
>
|
>
|
||||||
UI build v2.5.5 (rebuild client to match)
|
UI build v2.5.6 (rebuild client to match)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
+2
-2
@@ -129,7 +129,7 @@ waits on an install:
|
|||||||
5. **Install** — `npm install --omit=dev --ignore-scripts` into the runtime dir, run as the npm bundled with the node running the bootstrap (`npmCommand`: its `npm-cli.js` beside `process.execPath`, no shell, that node's directory first on PATH; `npm` on PATH only when that node ships none) — the update chain runs with the dashboard server's environment, whose PATH need not reach `npm.cmd` on Windows. The UI build below and the doctor's npm row use the same npm (never into the plugin cache, which is garbage-collected and replaced on every update). `--ignore-scripts` keeps the root `postinstall` from pulling the whole Vite client toolchain.
|
5. **Install** — `npm install --omit=dev --ignore-scripts` into the runtime dir, run as the npm bundled with the node running the bootstrap (`npmCommand`: its `npm-cli.js` beside `process.execPath`, no shell, that node's directory first on PATH; `npm` on PATH only when that node ships none) — the update chain runs with the dashboard server's environment, whose PATH need not reach `npm.cmd` on Windows. The UI build below and the doctor's npm row use the same npm (never into the plugin cache, which is garbage-collected and replaced on every update). `--ignore-scripts` keeps the root `postinstall` from pulling the whole Vite client toolchain.
|
||||||
6. **Legacy hook cleanup** — see above.
|
6. **Legacy hook cleanup** — see above.
|
||||||
7. **CLI** — write the `~/.local/bin/agenthub` launcher, never clobbering a `agenthub` the bootstrap did not write. It pins the node that ran the bootstrap and falls back to `node` on `PATH` when that binary is gone (a Homebrew Cellar or nvm path deleted by an upgrade).
|
7. **CLI** — write the `~/.local/bin/agenthub` launcher, never clobbering a `agenthub` the bootstrap did not write. It pins the node that ran the bootstrap and falls back to `node` on `PATH` when that binary is gone (a Homebrew Cellar or nvm path deleted by an upgrade).
|
||||||
8. **UI build** — `client/` is copied into the runtime dir and built there (`npm ci --include=dev`, falling back to `npm install --include=dev`, then `npm run build`), landing in `runtime/client-dist` — the same directory the server serves from. `--include=dev` is required, not tidy: the entire build toolchain (`vite`, `typescript`, `@vitejs/plugin-react`, `tailwindcss`, `@types/*`) is a devDependency, and npm reads an inherited `NODE_ENV=production` as `--omit=dev`. Every bootstrap descended from the running server carries that env — the self-update chain, and any `SessionStart` fired by a `claude` inside a tmux pane the server started — so without the flag the install drops `vite` and the build dies on missing types. Skipped when a bundle for the current version already exists; a failure here does not fail the bootstrap (the API still works, and `/agenthub-open` can retry) but does mean `/run` 404s until it's fixed — and when a bundle from the PREVIOUS version is still on disk the failure is quieter still: the dashboard keeps serving the old UI against the new API, visible only as the Settings page's `UI build vX (rebuild client to match)` line.
|
8. **UI build** — `client/` is copied into the runtime dir and built there (`npm ci --include=dev --ignore-scripts`, falling back to `npm install` with the same flags, then `tsc -b` and `vite build` run by the bootstrap's own `node` — the client's `build` script without `npm run`), landing in `runtime/client-dist` — the same directory the server serves from. Nothing in it looks `node` up on PATH: on Windows an install script or a `.bin/*.cmd` shim runs through cmd.exe, and the server's PATH may have no `node` (esbuild's postinstall failed there with "'node' is not recognized" and the update kept the old UI); esbuild, the only dependency with an install script, finds its binary without it. `--include=dev` is required, not tidy: the entire build toolchain (`vite`, `typescript`, `@vitejs/plugin-react`, `tailwindcss`, `@types/*`) is a devDependency, and npm reads an inherited `NODE_ENV=production` as `--omit=dev`. Every bootstrap descended from the running server carries that env — the self-update chain, and any `SessionStart` fired by a `claude` inside a tmux pane the server started — so without the flag the install drops `vite` and the build dies on missing types. Skipped when a bundle for the current version already exists; a failure here does not fail the bootstrap (the API still works, and `/agenthub-open` can retry) but does mean `/run` 404s until it's fixed — and when a bundle from the PREVIOUS version is still on disk the failure is quieter still: the dashboard keeps serving the old UI against the new API, visible only as the Settings page's `UI build vX (rebuild client to match)` line.
|
||||||
9. **Server** — when the user has turned **Start AgentHub automatically** on
|
9. **Server** — when the user has turned **Start AgentHub automatically** on
|
||||||
(Settings → About; OFF by default), install/refresh the
|
(Settings → About; OFF by default), install/refresh the
|
||||||
`agenthub.service` unit on Linux — on macOS with a GUI login, the
|
`agenthub.service` unit on Linux — on macOS with a GUI login, the
|
||||||
@@ -149,7 +149,7 @@ waits on an install:
|
|||||||
The first run takes a few minutes — most of it is the UI build, which is what
|
The first run takes a few minutes — most of it is the UI build, which is what
|
||||||
makes client-only routes (`http://localhost:4820/run`, and every other page)
|
makes client-only routes (`http://localhost:4820/run`, and every other page)
|
||||||
work the moment `claude` starts, with no manual `/agenthub-open` step. Progress
|
work the moment `claude` starts, with no manual `/agenthub-open` step. Progress
|
||||||
goes to `~/.claude/agenthub/runtime/bootstrap.log`; `npm run build`'s own
|
goes to `~/.claude/agenthub/runtime/bootstrap.log`; the UI build's own
|
||||||
output goes to `client-build.log` next to it; the server's own output goes to
|
output goes to `client-build.log` next to it; the server's own output goes to
|
||||||
`server.log`.
|
`server.log`.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: Agent Dashboard for Claude Code API
|
title: Agent Dashboard for Claude Code API
|
||||||
version: 2.5.5
|
version: 2.5.6
|
||||||
description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.
|
description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.
|
||||||
contact:
|
contact:
|
||||||
name: Nguyễn Ngọc Trí Vĩ
|
name: Nguyễn Ngọc Trí Vĩ
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.5.5",
|
"version": "2.5.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.5.5",
|
"version": "2.5.6",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.5.5",
|
"version": "2.5.6",
|
||||||
"bin": {
|
"bin": {
|
||||||
"agenthub": "bin/agenthub.js"
|
"agenthub": "bin/agenthub.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -187,7 +187,8 @@ function reloadPanes() {
|
|||||||
// `/reload-plugins /reload-skills` — see SETTLE_MS in
|
// `/reload-plugins /reload-skills` — see SETTLE_MS in
|
||||||
// server/lib/cc-reload.js. Blocking is fine here: this worker is
|
// server/lib/cc-reload.js. Blocking is fine here: this worker is
|
||||||
// detached and serves no requests.
|
// detached and serves no requests.
|
||||||
if (!first) execFileSync("sleep", ["1"], { timeout: 15_000 });
|
// Atomics.wait, not `sleep`: Windows has no sleep binary.
|
||||||
|
if (!first) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
|
||||||
first = false;
|
first = false;
|
||||||
execFileSync("tmux", ["send-keys", "-t", `=${name}:`, "-l", command], { timeout: 15_000 });
|
execFileSync("tmux", ["send-keys", "-t", `=${name}:`, "-l", command], { timeout: 15_000 });
|
||||||
execFileSync("tmux", ["send-keys", "-t", `=${name}:`, "Enter"], { timeout: 15_000 });
|
execFileSync("tmux", ["send-keys", "-t", `=${name}:`, "Enter"], { timeout: 15_000 });
|
||||||
|
|||||||
+23
-4
@@ -30,9 +30,12 @@ const { resolveDashboardPort } = require("../server/lib/server-info");
|
|||||||
|
|
||||||
const PLUGIN_ROOT = path.resolve(__dirname, "..");
|
const PLUGIN_ROOT = path.resolve(__dirname, "..");
|
||||||
const NPM = "npm";
|
const NPM = "npm";
|
||||||
|
const NODE = "node";
|
||||||
|
|
||||||
/** `cmd` is always NPM: run through `boot.npmCommand`, never a PATH lookup. */
|
/** NPM runs through `boot.npmCommand`, NODE is the running node itself —
|
||||||
function run(_cmd, args, cwd, stdio) {
|
* neither is ever a PATH lookup. */
|
||||||
|
function run(cmd, args, cwd, stdio) {
|
||||||
|
if (cmd === NODE) return spawnSync(process.execPath, args, { cwd, stdio }).status === 0;
|
||||||
const npm = boot.npmCommand(args);
|
const npm = boot.npmCommand(args);
|
||||||
const res = spawnSync(npm.file, npm.args, { cwd, stdio, env: npm.env, shell: npm.shell });
|
const res = spawnSync(npm.file, npm.args, { cwd, stdio, env: npm.env, shell: npm.shell });
|
||||||
return res.status === 0;
|
return res.status === 0;
|
||||||
@@ -89,14 +92,30 @@ function buildClient({
|
|||||||
// `npm run build` failed, and the PREVIOUS bundle stayed in place: the
|
// `npm run build` failed, and the PREVIOUS bundle stayed in place: the
|
||||||
// dashboard kept serving the old version's UI while the API was already the
|
// dashboard kept serving the old version's UI while the API was already the
|
||||||
// new one. An explicit CLI flag outranks the env, which is the point.
|
// new one. An explicit CLI flag outranks the env, which is the point.
|
||||||
const npmFlags = ["--no-audit", "--no-fund", "--include=dev"];
|
//
|
||||||
|
// `--ignore-scripts`, and the build run by node itself below rather than
|
||||||
|
// `npm run build`: a lifecycle script and a `.bin/*.cmd` shim both go
|
||||||
|
// through cmd.exe and look `node` up on PATH, and on Windows the server's
|
||||||
|
// PATH did not have it — esbuild's postinstall died with "'node' is not
|
||||||
|
// recognized", both installs failed, and the update kept the old UI.
|
||||||
|
// esbuild is the only dependency with an install script, and it finds its
|
||||||
|
// platform binary without it.
|
||||||
|
const npmFlags = ["--no-audit", "--no-fund", "--include=dev", "--ignore-scripts"];
|
||||||
if (
|
if (
|
||||||
!exec(NPM, ["ci", ...npmFlags], src, stdio) &&
|
!exec(NPM, ["ci", ...npmFlags], src, stdio) &&
|
||||||
!exec(NPM, ["install", ...npmFlags], src, stdio)
|
!exec(NPM, ["install", ...npmFlags], src, stdio)
|
||||||
) {
|
) {
|
||||||
return "failed";
|
return "failed";
|
||||||
}
|
}
|
||||||
if (!exec(NPM, ["run", "build"], src, stdio)) return "failed";
|
// = client/package.json's "build" script (`tsc -b && vite build`); a test
|
||||||
|
// keeps the two in step.
|
||||||
|
const bin = (...p) => path.join(src, "node_modules", ...p);
|
||||||
|
if (
|
||||||
|
!exec(NODE, [bin("typescript", "bin", "tsc"), "-b"], src, stdio) ||
|
||||||
|
!exec(NODE, [bin("vite", "bin", "vite.js"), "build"], src, stdio)
|
||||||
|
) {
|
||||||
|
return "failed";
|
||||||
|
}
|
||||||
|
|
||||||
fs.rmSync(dist, { recursive: true, force: true });
|
fs.rmSync(dist, { recursive: true, force: true });
|
||||||
fs.cpSync(path.join(src, "dist"), dist, { recursive: true });
|
fs.cpSync(path.join(src, "dist"), dist, { recursive: true });
|
||||||
|
|||||||
@@ -455,14 +455,18 @@ describe("buildClient dependency install", () => {
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Records every npm invocation; `fails` names the subcommands that exit non-zero.
|
// Records every invocation — npm's as its args, node's as `node <script> …`;
|
||||||
// A successful "run build" writes the dist/ the real Vite build would produce.
|
// `fails` names the npm subcommands that exit non-zero. A successful vite
|
||||||
|
// build writes the dist/ the real one would produce.
|
||||||
function fakeExec(calls, fails = []) {
|
function fakeExec(calls, fails = []) {
|
||||||
return (_cmd, args, cwd) => {
|
return (cmd, args, cwd) => {
|
||||||
|
if (cmd === "node") {
|
||||||
|
calls.push(`node ${path.basename(args[0])} ${args.slice(1).join(" ")}`);
|
||||||
|
if (args[1] === "build") fs.mkdirSync(path.join(cwd, "dist"), { recursive: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
calls.push(args.join(" "));
|
calls.push(args.join(" "));
|
||||||
if (fails.includes(args[0])) return false;
|
return !fails.includes(args[0]);
|
||||||
if (args[0] === "run") fs.mkdirSync(path.join(cwd, "dist"), { recursive: true });
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,10 +493,12 @@ describe("buildClient dependency install", () => {
|
|||||||
const result = buildClient({ rt: RT, root: fakeRoot(), exec: fakeExec(calls, ["ci"]) });
|
const result = buildClient({ rt: RT, root: fakeRoot(), exec: fakeExec(calls, ["ci"]) });
|
||||||
|
|
||||||
assert.equal(result, "built");
|
assert.equal(result, "built");
|
||||||
assert.deepEqual(
|
assert.deepEqual(calls, [
|
||||||
calls.map((c) => c.split(" ")[0]),
|
"ci --no-audit --no-fund --include=dev --ignore-scripts",
|
||||||
["ci", "install", "run"]
|
"install --no-audit --no-fund --include=dev --ignore-scripts",
|
||||||
);
|
"node tsc -b",
|
||||||
|
"node vite.js build",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("installs devDependencies explicitly, because NODE_ENV=production omits them", () => {
|
it("installs devDependencies explicitly, because NODE_ENV=production omits them", () => {
|
||||||
@@ -521,11 +527,28 @@ describe("buildClient dependency install", () => {
|
|||||||
|
|
||||||
assert.equal(result, "failed");
|
assert.equal(result, "failed");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
calls.some((c) => c.startsWith("run")),
|
calls.some((c) => c.startsWith("node")),
|
||||||
false,
|
false,
|
||||||
"the build must not run on a broken dependency tree"
|
"the build must not run on a broken dependency tree"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds with the running node, no lifecycle script and no PATH lookup (Windows)", () => {
|
||||||
|
// On Windows a script or a .bin shim runs through cmd.exe and needs `node`
|
||||||
|
// on PATH; the server's PATH lacked it, so esbuild's postinstall failed and
|
||||||
|
// every update kept the previous UI. The build here must equal the
|
||||||
|
// client's own "build" script, run without npm.
|
||||||
|
assert.equal(
|
||||||
|
JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "client", "package.json")))
|
||||||
|
.scripts.build,
|
||||||
|
"tsc -b && vite build"
|
||||||
|
);
|
||||||
|
resetRuntime();
|
||||||
|
const calls = [];
|
||||||
|
buildClient({ rt: RT, root: fakeRoot(), exec: fakeExec(calls) });
|
||||||
|
assert.deepEqual(calls.slice(-2), ["node tsc -b", "node vite.js build"]);
|
||||||
|
assert.ok(calls.every((c) => !c.startsWith("run")));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("supersededBy — an old cache directory must not act", () => {
|
describe("supersededBy — an old cache directory must not act", () => {
|
||||||
|
|||||||
@@ -499,6 +499,15 @@ describe("pty-run", () => {
|
|||||||
return argv.slice(4);
|
return argv.slice(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it("runs a Windows pane's command itself — no sh there — and a Terminal as tmux's default shell", () => {
|
||||||
|
const argv = ["C:\\Users\\u\\.local\\bin\\claude.exe", "--model", "opus"];
|
||||||
|
assert.deepEqual(pty.paneArgv(argv, "win32"), argv);
|
||||||
|
assert.deepEqual(pty.paneArgv(null, "win32"), []);
|
||||||
|
assert.equal(pty.paneArgv(argv, "linux")[0], "sh");
|
||||||
|
assert.deepEqual(pty.paneArgv(argv, "linux").slice(-3), argv);
|
||||||
|
assert.match(pty.paneArgv(null, "linux")[2], /exec "\$SHELL" -l$/);
|
||||||
|
});
|
||||||
|
|
||||||
it("spawnRun starts the CLI only once the pane has its real size, argv untouched", () => {
|
it("spawnRun starts the CLI only once the pane has its real size, argv untouched", () => {
|
||||||
// A claude drawn at tmux's birth size duplicated its banner on the first
|
// A claude drawn at tmux's birth size duplicated its banner on the first
|
||||||
// resize; the argv rides as positional parameters, never script text.
|
// resize; the argv rides as positional parameters, never script text.
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ describe("tmux wrapper", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("newSession with no argv leaves tmux its default shell (no trailing --)", () => {
|
||||||
|
const calls = [];
|
||||||
|
tmux.__setExecImpl((args) => {
|
||||||
|
calls.push(args);
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
tmux.newSession({ name: "agenthub-lane-1", cwd: "C:\\repo", argv: [] });
|
||||||
|
assert.deepEqual(calls[0], ["new-session", "-d", "-s", "agenthub-lane-1", "-c", "C:\\repo"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("newSession builds the correct argv", () => {
|
it("newSession builds the correct argv", () => {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
tmux.__setExecImpl((args) => {
|
tmux.__setExecImpl((args) => {
|
||||||
|
|||||||
+20
-13
@@ -77,6 +77,23 @@ const WAIT_FOR_CLIENT =
|
|||||||
'i=0; while [ "$(tmux display -p -t "$TMUX_PANE" \'#{session_attached}\' 2>/dev/null)" = 0 ]' +
|
'i=0; while [ "$(tmux display -p -t "$TMUX_PANE" \'#{session_attached}\' 2>/dev/null)" = 0 ]' +
|
||||||
" && [ $i -lt 20 ]; do sleep 0.1; i=$((i+1)); done;";
|
" && [ $i -lt 20 ]; do sleep 0.1; i=$((i+1)); done;";
|
||||||
const CLIENT_WAIT_MS = 2000;
|
const CLIENT_WAIT_MS = 2000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pane's tmux command: `argv` behind {@link WAIT_FOR_CLIENT}, or — with no
|
||||||
|
* `argv` — the login shell behind it. Windows has no `sh` (a WinGet tmux runs
|
||||||
|
* native programs), and an `sh -c` there died at once and took the session
|
||||||
|
* with it, so it gets the command itself, or nothing for tmux's own default
|
||||||
|
* shell.
|
||||||
|
* ponytail: no wait for the attach on Windows, so a pane may first draw at
|
||||||
|
* 80×24 (issue #55); port the wait to PowerShell if that shows.
|
||||||
|
*/
|
||||||
|
function paneArgv(argv, platform = process.platform) {
|
||||||
|
if (platform === "win32") return argv || [];
|
||||||
|
// argv rides as sh's positional parameters, never inside the script text.
|
||||||
|
return argv
|
||||||
|
? ["sh", "-c", `${WAIT_FOR_CLIENT} exec "$@"`, "sh", ...argv]
|
||||||
|
: ["sh", "-c", `${WAIT_FOR_CLIENT} exec "$SHELL" -l`];
|
||||||
|
}
|
||||||
const CODEX_SANDBOXES = new Set(["read-only", "workspace-write", "danger-full-access"]);
|
const CODEX_SANDBOXES = new Set(["read-only", "workspace-write", "danger-full-access"]);
|
||||||
const CODEX_APPROVALS = new Set(["on-request", "never"]);
|
const CODEX_APPROVALS = new Set(["on-request", "never"]);
|
||||||
// Codex has no `--effort` flag; reasoning effort is a config override.
|
// Codex has no `--effort` flag; reasoning effort is a config override.
|
||||||
@@ -493,13 +510,7 @@ function spawnRun(args) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!tmux.hasSession(id)) {
|
if (!tmux.hasSession(id)) {
|
||||||
// argv rides as sh's positional parameters, never inside the script text.
|
tmux.newSession({ name: id, cwd, argv: paneArgv(argv), env });
|
||||||
tmux.newSession({
|
|
||||||
name: id,
|
|
||||||
cwd,
|
|
||||||
argv: ["sh", "-c", `${WAIT_FOR_CLIENT} exec "$@"`, "sh", ...argv],
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
record();
|
record();
|
||||||
schedulePaste();
|
schedulePaste();
|
||||||
} else if (SHELL_COMMANDS.has(tmux.paneCommand(id) || "")) {
|
} else if (SHELL_COMMANDS.has(tmux.paneCommand(id) || "")) {
|
||||||
@@ -554,12 +565,7 @@ function spawnShell({ laneId, slot = 0, cwd }) {
|
|||||||
};
|
};
|
||||||
// Started once the pane has its real size (WAIT_FOR_CLIENT, issue #55), as
|
// Started once the pane has its real size (WAIT_FOR_CLIENT, issue #55), as
|
||||||
// the login shell tmux would have run (tmux sets SHELL in the pane).
|
// the login shell tmux would have run (tmux sets SHELL in the pane).
|
||||||
tmux.newSession({
|
tmux.newSession({ name: id, cwd, argv: paneArgv(null), env });
|
||||||
name: id,
|
|
||||||
cwd,
|
|
||||||
argv: ["sh", "-c", `${WAIT_FOR_CLIENT} exec "$SHELL" -l`],
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
dashboardRuns?.recordRun({
|
dashboardRuns?.recordRun({
|
||||||
id,
|
id,
|
||||||
sessionId: null,
|
sessionId: null,
|
||||||
@@ -636,4 +642,5 @@ module.exports = {
|
|||||||
runIdForLane,
|
runIdForLane,
|
||||||
MAX_PANE_SLOT,
|
MAX_PANE_SLOT,
|
||||||
SHELL_COMMANDS,
|
SHELL_COMMANDS,
|
||||||
|
paneArgv,
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-2
@@ -76,7 +76,9 @@ function envFlags(env) {
|
|||||||
* guessing from cwd — see resolveLaneArg in bin/agenthub.js.
|
* guessing from cwd — see resolveLaneArg in bin/agenthub.js.
|
||||||
*/
|
*/
|
||||||
function newSession({ name, cwd, argv, env }) {
|
function newSession({ name, cwd, argv, env }) {
|
||||||
execImpl(["new-session", "-d", "-s", name, "-c", cwd, ...envFlags(env), "--", ...argv]);
|
// No argv = tmux's own default shell (a Windows Terminal pane).
|
||||||
|
const cmd = argv.length ? ["--", ...argv] : [];
|
||||||
|
execImpl(["new-session", "-d", "-s", name, "-c", cwd, ...envFlags(env), ...cmd]);
|
||||||
try {
|
try {
|
||||||
enableMouse(name);
|
enableMouse(name);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -85,7 +87,9 @@ function newSession({ name, cwd, argv, env }) {
|
|||||||
// "Command failed: tmux set-option …" that hid the real problem.
|
// "Command failed: tmux set-option …" that hid the real problem.
|
||||||
if (hasSession(name)) throw err;
|
if (hasSession(name)) throw err;
|
||||||
throw Object.assign(
|
throw Object.assign(
|
||||||
new Error(`the session's command exited as soon as it started: ${argv[0]}`),
|
new Error(
|
||||||
|
`the session's command exited as soon as it started: ${argv[0] || "the default shell"}`
|
||||||
|
),
|
||||||
{ code: "EPANEEXITED" }
|
{ code: "EPANEEXITED" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user