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
@@ -222,6 +222,40 @@ describe("UpdateNotifier — Update now", () => {
});
});
describe("UpdateNotifier — plugin chain verdict", () => {
it("stops Restarting when the chain fails and the old server stays up", async () => {
const PLUGIN: UpdateStatusPayload = {
...BASE_STATUS,
situation: "plugin_managed",
install_kind: "plugin",
plugin_marketplace: "sg",
plugin_name: "agenthub",
commits_behind: 0,
local_version: "1.5.0",
release_check: "ok",
latest_release: "v1.6.0",
};
statusMock.mockResolvedValue(PLUGIN);
applyMock.mockResolvedValue({ ...PLUGIN, applied: true, mode: "plugin" });
render(<UpdateNotifier />);
await userEvent.click(await screen.findByText(i18n.t("updates:updateNow")));
await screen.findAllByText(i18n.t("updates:pluginUpdating"));
// The old server answers again, now carrying this chain's failed verdict.
statusMock.mockResolvedValue({
...PLUGIN,
last_plugin_update: {
ok: false,
step: "plugin_update",
error: "boom",
finished_at: new Date().toISOString(),
},
});
expect(await screen.findByText(/plugin_update/, {}, { timeout: 4000 })).toBeInTheDocument();
expect(screen.queryByText(i18n.t("updates:pluginUpdating"))).not.toBeInTheDocument();
});
});
describe("UpdateNotifier — closing vs skipping", () => {
it("Escape only closes for now; Skip this version is the persistent dismiss", async () => {
const { unmount } = render(<UpdateNotifier />);
@@ -88,7 +88,7 @@ vi.mock("../../lib/api", async (importOriginal) => {
list: vi.fn(async () => ({ tools: [], default: "claude" })),
iconUrl: () => "",
},
aiRouters: { list: vi.fn(async () => ({ routers: [] })) },
aiProxies: { list: vi.fn(async () => ({ items: [] })) },
},
};
});