feat: v2.5.5 — AI Router renamed AI Proxy, reload onto new builds, update no longer spins

AI Proxy (was AI Router):
- server/lib/ai-proxies.js, proxy-apply.js, routes/ai-proxies.js mounted at
  /api/ai-proxies and the old /api/ai-routers; `router` still accepted on the
  start routes; profiles move to <CLAUDE_HOME>/agenthub-proxies/
- dashboard_runs.router renamed in place to proxy: an older build on the same
  DB fails on that column, so dev and prod sharing one DB update together
- a proxy's baseUrl stored as its root (a typed /v1 made /v1/v1/messages);
  Test probes Claude /v1/messages and Codex /v1/responses
- Settings → AI Proxy (/settings/ai-router redirects), ProxyPicker

Updates:
- every tab reloads onto the new UI build when its WebSocket reconnects to a
  server of another version (lib/newBuildReload.ts, once per version)
- Update now stops waiting on a plugin chain that failed or pulled nothing
  (the old server never goes down), and reloads as soon as a new version answers

Also:
- a pane whose command exits at once says so (EPANEEXITED) instead of
  "Command failed: tmux set-option … mouse on"
- the pane drag preview draws where the pane actually lands
This commit is contained in:
2026-10-06 22:48:09 +07:00
parent dd9b23106c
commit 5fe5d51b1b
82 changed files with 1792 additions and 1412 deletions
+13 -13
View File
@@ -213,7 +213,7 @@ describe("pty-run", () => {
});
pty.spawnRun({ laneId: 12, cwd: "/tmp/repo", tool: "codex" });
// Through argv, never as a shell assignment typed into the pane: this is
// also the path an AI Router's API key takes, and a typed line would be
// also the path an AI Proxy's API key takes, and a typed line would be
// rendered to every attached browser and land in the shell history.
const at = respawn.indexOf("-e");
assert.ok(at > -1, "expected a tmux -e environment flag");
@@ -309,7 +309,7 @@ describe("pty-run", () => {
}
});
it("spawnRun starts a session on ITS router: Claude by --settings, Codex by -p", () => {
it("spawnRun starts a session on ITS proxy: Claude by --settings, Codex by -p", () => {
let newSessionArgv = null;
tmux.__setExecImpl((args) => {
if (args[0] === "has-session") {
@@ -320,7 +320,7 @@ describe("pty-run", () => {
if (args[0] === "new-session") newSessionArgv = args;
return "";
});
// The router store and the files a spawn writes live under these homes.
// The proxy store and the files a spawn writes live under these homes.
const saved = ["CLAUDE_HOME", "CODEX_HOME", "DASHBOARD_DATA_DIR"].map((k) => [
k,
process.env[k],
@@ -329,30 +329,30 @@ describe("pty-run", () => {
process.env.CODEX_HOME = path.join(TMP, "codex");
process.env.DASHBOARD_DATA_DIR = path.join(TMP, "data");
try {
const { name } = require("../lib/ai-routers").createRouter({
label: "Router",
const { name } = require("../lib/ai-proxies").createProxy({
label: "Proxy",
baseUrl: "http://localhost:20128/v1",
apiKey: "sk-secret-value",
model: "M",
});
pty.spawnRun({ laneId: 51, cwd: "/tmp/repo", router: name, initialPrompt: "hi" });
pty.spawnRun({ laneId: 51, cwd: "/tmp/repo", proxy: name, initialPrompt: "hi" });
const flag = newSessionArgv.find((a) => a.startsWith("--settings="));
assert.equal(flag, `--settings=${path.join(TMP, "data", "router-settings", `${name}.json`)}`);
assert.equal(flag, `--settings=${path.join(TMP, "data", "proxy-settings", `${name}.json`)}`);
assert.ok(!newSessionArgv.join(" ").includes("sk-secret-value"));
assert.equal(require("../lib/dashboard-runs").getRun("agenthub-lane-51").router, name);
assert.equal(require("../lib/dashboard-runs").getRun("agenthub-lane-51").proxy, name);
pty.spawnRun({ laneId: 52, cwd: "/tmp/repo", tool: "codex", router: name });
pty.spawnRun({ laneId: 52, cwd: "/tmp/repo", tool: "codex", proxy: name });
const at = newSessionArgv.indexOf("-p");
assert.deepEqual(newSessionArgv.slice(at, at + 2), ["-p", name]);
// The key rides tmux's -e, never the command line.
assert.ok(newSessionArgv.includes("AGENTHUB_ROUTER_KEY=sk-secret-value"));
assert.ok(newSessionArgv.includes("AGENTHUB_PROXY_KEY=sk-secret-value"));
pty.spawnRun({ laneId: 53, cwd: "/tmp/repo" });
assert.ok(!newSessionArgv.some((a) => a.startsWith("--settings=")));
assert.equal(require("../lib/dashboard-runs").getRun("agenthub-lane-53").router, null);
assert.equal(require("../lib/dashboard-runs").getRun("agenthub-lane-53").proxy, null);
assert.throws(
() => pty.spawnRun({ laneId: 54, cwd: "/tmp/repo", router: "nope" }),
(e) => e.code === "EBADROUTER"
() => pty.spawnRun({ laneId: 54, cwd: "/tmp/repo", proxy: "nope" }),
(e) => e.code === "EBADPROXY"
);
} finally {
for (const [k, v] of saved) {