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
+17 -17
View File
@@ -31,7 +31,7 @@
*/
const tmux = require("./tmux");
const { resolveRouter, routerForSpawn } = require("./router-apply");
const { resolveProxy, proxyForSpawn } = require("./proxy-apply");
const { desktopEnv } = require("./desktop-env");
const { getCodexHome } = require("./codex-home");
const aiTools = require("./ai-tools");
@@ -131,7 +131,7 @@ function buildClaudeArgv({
initialPrompt,
addDirs = [],
systemPromptFile = null,
routerArgv = [],
proxyArgv = [],
}) {
const argv = ["claude"];
// `--add-dir` is VARIADIC (`<directories...>`): it swallows every following
@@ -139,8 +139,8 @@ function buildClaudeArgv({
// always ends it — after the prompt it would eat the prompt as a directory.
if (addDirs.length) argv.push("--add-dir", ...addDirs);
argv.push("--permission-mode", permissionMode || "auto");
// The session's AI Router: `--settings=<file>` (router-apply.js).
argv.push(...routerArgv);
// The session's AI Proxy: `--settings=<file>` (proxy-apply.js).
argv.push(...proxyArgv);
// Claude Code makes the worktree itself (`.claude/worktrees/<name>`, branch
// `worktree-<name>`, `.worktreeinclude`, the user's WorktreeCreate hook).
// `=` form: `-w [name]` takes an OPTIONAL value and would otherwise swallow
@@ -182,7 +182,7 @@ function buildCodexArgv({
resumeLast,
bypass,
initialPrompt,
routerArgv = [],
proxyArgv = [],
addDirs = [],
systemPrompt = null,
systemPromptFile = null,
@@ -191,7 +191,7 @@ function buildCodexArgv({
// Before the subcommand and before the prompt: `-p` layers
// `$CODEX_HOME/<name>.config.toml` over the base config, and an option that
// lands after the positional prompt is not an option any more.
argv.push(...routerArgv);
argv.push(...proxyArgv);
if (resumeSessionId) argv.push("resume", resumeSessionId);
else if (resumeLast) argv.push("resume", "--last");
if (model) argv.push("-m", model);
@@ -266,7 +266,7 @@ function buildArgv(args, entry) {
* @param {boolean} [args.bypass] the user ticked "bypass permissions" for THIS
* run; appends the catalog's bypass args/env. Claude has no catalog bypass.
* @param {string} [args.initialPrompt]
* @param {string} [args.router] AI Router profile THIS session runs on — a
* @param {string} [args.proxy] AI Proxy profile THIS session runs on — a
* name only, its key and endpoint come from the store; null/"native" = the
* vendor's own API. Claude and Codex only (`EUNSUPPORTED`).
*/
@@ -288,7 +288,7 @@ function spawnRun(args) {
resumeLast,
bypass,
initialPrompt,
router,
proxy,
} = args || {};
if (typeof laneId !== "number" || !Number.isInteger(laneId)) {
throw makeErr("EBADLANE", "laneId must be an integer");
@@ -345,7 +345,7 @@ function spawnRun(args) {
if (worktree === true && entry.launcher !== "claude") {
throw makeErr("EUNSUPPORTED", `${tool} cannot start a session in its own worktree`);
}
const routerProfile = resolveRouter(tool, router); // ENOTFOUND, EUNSUPPORTED
const proxyProfile = resolveProxy(tool, proxy); // ENOTFOUND, EUNSUPPORTED
if (forkSession === true) {
if (!resumeSessionId) throw makeErr("EBADSESSION", "a fork needs resumeSessionId");
if (entry.launcher !== "claude") {
@@ -382,7 +382,7 @@ function spawnRun(args) {
laneId,
tool
);
const routerSpawn = routerForSpawn(tool, routerProfile);
const proxySpawn = proxyForSpawn(tool, proxyProfile);
// A new Claude session gets its id from us, so the run knows the session it
// holds from the start — a hook that lost AGENTHUB_RUN_ID (an older hook
// handler) still finds its pane by it (routes/hooks.js → bindRunSession).
@@ -406,7 +406,7 @@ function spawnRun(args) {
resumeLast,
bypass: bypass === true,
initialPrompt,
routerArgv: routerSpawn.argv,
proxyArgv: proxySpawn.argv,
},
entry
);
@@ -432,7 +432,7 @@ function spawnRun(args) {
effort: effort || null,
resumeSessionId: resumeSessionId || null,
worktree: worktree === true || inWorktree === true,
router: routerProfile ? routerProfile.name : null,
proxy: proxyProfile ? proxyProfile.name : null,
prompt: initialPrompt || "",
status: "running",
startedAt,
@@ -446,9 +446,9 @@ function spawnRun(args) {
// from inside it then resolves the lane exactly, instead of matching on cwd —
// which is ambiguous now that one directory can back both a `claude` lane and
// a `codex` lane (see bin/agenthub.js's resolveLaneArg).
// A codex session on an AI Router also needs the key its layer's `env_key`
// A codex session on an AI Proxy also needs the key its layer's `env_key`
// names. A claude one needs nothing here — its `--settings` file carries the
// router — and a session on the vendor's own API needs nothing either.
// proxy — and a session on the vendor's own API needs nothing either.
// The graphical session's own DISPLAY/WAYLAND_DISPLAY goes in too: the
// server normally runs as a systemd user service with neither, so a pane it
// starts has no clipboard at all — pasting an image into `claude` there
@@ -466,14 +466,14 @@ function spawnRun(args) {
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
...desktopEnv(),
// A codex pane must read the same home the dashboard reads — its rollout
// journals and the router layer `-p <name>` loads live there — and the
// journals and the proxy layer `-p <name>` loads live there — and the
// tmux server's own env may predate, or never have had, CODEX_HOME.
...(tool === "codex" ? { CODEX_HOME: getCodexHome() } : {}),
// No CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN: an inline claude duplicated its
// output whenever the pane narrowed (tmux re-wraps the full-width rules and
// pushes lines into history its redraw cannot clear). The user's own
// `"tui"` setting decides; under fullscreen the pane scrolls in Claude.
...routerSpawn.env,
...proxySpawn.env,
...(bypass === true && entry.bypassEnv ? entry.bypassEnv : {}),
};
@@ -571,7 +571,7 @@ function spawnShell({ laneId, slot = 0, cwd }) {
effort: null,
resumeSessionId: null,
worktree: false,
router: null,
proxy: null,
prompt: "",
status: "running",
startedAt: Date.now(),