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
+10 -9
View File
@@ -28,7 +28,7 @@ const laneFeatures = require("../lib/lane-features");
const proofLib = require("../lib/proof");
const { broadcast } = require("../websocket");
const runs = require("../lib/pty-run");
const { resumeRouter } = require("../lib/router-apply");
const { resumeProxy } = require("../lib/proxy-apply");
const { sameOriginGuard } = require("./run");
const { writeLaneFile } = require("../lib/lane-file-write");
const { pickPath } = require("../lib/native-dialog");
@@ -927,7 +927,7 @@ router.get("/:id/git/file", async (req, res) => {
);
res.json({ available: true, ...file });
} catch (err) {
// A secret file (the router's API key) is refused at every rev.
// A secret file (where a proxy's API key once lived) is refused at every rev.
if (err.code === "ESECRET") {
return res.status(403).json({ error: { code: err.code, message: err.message } });
}
@@ -2002,12 +2002,13 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
// A NEW session in a worktree of its own (`claude --worktree`).
worktree: body.worktree === true && !body.resumeSessionId,
tool: sessionTool || current.tool || "claude",
// The SESSION's AI Router, by name; a resume that names none goes
// back to the one its session last ran on.
router:
body.router !== undefined
? body.router
: resumeRouter(body.resumeSessionId || (body.resume ? current.session_id : null)),
// The SESSION's AI Proxy, by name; a resume that names none goes
// back to the one its session last ran on. `router` is the
// pre-rename field, still taken from an older client.
proxy:
(body.proxy ?? body.router) !== undefined
? (body.proxy ?? body.router)
: resumeProxy(body.resumeSessionId || (body.resume ? current.session_id : null)),
model: body.model,
permissionMode: body.permissionMode,
effort: body.effort,
@@ -2020,7 +2021,7 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
});
startedRunId = handle.id;
// An extra pane's session is not the lane's run: `run_id` stays the
// pane-0 session every other path (reset, router restart) names.
// pane-0 session every other path (reset, stop) names.
if (!slot) lanesLib.updateLane(current.id, { run_id: handle.id, status: "running" });
return {};
});