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
+8 -5
View File
@@ -331,19 +331,22 @@ describe("/api/lanes", () => {
ws.close();
});
it("starts a session on the router it names, and refuses one the store lacks", async () => {
it("starts a session on the proxy it names, and refuses one the store lacks", async () => {
const tmux = require("../lib/tmux");
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "lane-router-start-"));
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "lane-proxy-start-"));
const id = (await request("POST", "/api/lanes", { cwd })).body.lane.id;
try {
tmux.__setExecImpl((args) => {
if (args[0] === "has-session") throw new Error("no such session");
return "";
});
const bad = await request("POST", `/api/lanes/${id}/start`, { router: "nope" });
const bad = await request("POST", `/api/lanes/${id}/start`, { proxy: "nope" });
assert.equal(bad.status, 400);
assert.equal(bad.body.error.code, "EBADROUTER");
// The per-workspace route is gone: a router belongs to a session now.
assert.equal(bad.body.error.code, "EBADPROXY");
// The pre-rename field still names a proxy, for a client older than it.
const legacy = await request("POST", `/api/lanes/${id}/start`, { router: "nope" });
assert.equal(legacy.body.error.code, "EBADPROXY");
// The per-workspace route is gone: a proxy belongs to a session now.
const gone = await request("POST", `/api/lanes/${id}/router/apply`, { profile: "native" });
assert.equal(gone.status, 404);
} finally {