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:
@@ -31,7 +31,7 @@ vi.mock("../../../lib/api", () => ({
|
||||
profileInit: vi.fn(),
|
||||
mcpSync: vi.fn(),
|
||||
},
|
||||
aiRouters: { list: vi.fn() },
|
||||
aiProxies: { list: vi.fn() },
|
||||
aiTools: { list: vi.fn(), iconUrl: (id: string) => `/api/ai-tools/${id}/icon` },
|
||||
run: { start: vi.fn() },
|
||||
},
|
||||
@@ -50,7 +50,7 @@ const tool = (id: string, label: string, over: Partial<AiTool> = {}): AiTool =>
|
||||
prompt: { mode: "argv" },
|
||||
resume: false,
|
||||
bypass: false,
|
||||
routerCapable: false,
|
||||
proxyCapable: false,
|
||||
install: null,
|
||||
update: null,
|
||||
modelFlag: false,
|
||||
@@ -62,8 +62,8 @@ const tool = (id: string, label: string, over: Partial<AiTool> = {}): AiTool =>
|
||||
const TOOLS: AiToolsView = {
|
||||
default: "claude",
|
||||
tools: [
|
||||
tool("claude", "Claude Code", { launcher: "claude", routerCapable: true, prompt: null }),
|
||||
tool("codex", "Codex", { launcher: "codex", routerCapable: true, prompt: null }),
|
||||
tool("claude", "Claude Code", { launcher: "claude", proxyCapable: true, prompt: null }),
|
||||
tool("codex", "Codex", { launcher: "codex", proxyCapable: true, prompt: null }),
|
||||
tool("gemini", "Gemini"),
|
||||
tool("aider", "Aider", { installed: false, path: null }),
|
||||
tool("grok", "Grok", { enabled: false }),
|
||||
@@ -158,7 +158,7 @@ beforeEach(() => {
|
||||
.mockReset()
|
||||
.mockResolvedValue({ scaffolded: false, reason: "no detectable Node.js project" });
|
||||
vi.mocked(api.lanes.mcpSync).mockReset().mockResolvedValue({ servers: [], profilesSeeded: [] });
|
||||
vi.mocked(api.aiRouters.list).mockReset();
|
||||
vi.mocked(api.aiProxies.list).mockReset();
|
||||
});
|
||||
|
||||
describe("AddLaneModal — picking an existing branch (issue #34)", () => {
|
||||
@@ -639,12 +639,12 @@ describe("AddLaneModal — create/update readme", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("AddLaneModal — no router here", () => {
|
||||
it("asks for no AI Router: a new session picks its own", async () => {
|
||||
describe("AddLaneModal — no proxy here", () => {
|
||||
it("asks for no AI Proxy: a new session picks its own", async () => {
|
||||
renderModal({});
|
||||
await screen.findByRole("button", { name: /Claude Code.*Default/ });
|
||||
expect(screen.queryByRole("radio", { name: "AI Router" })).toBeNull();
|
||||
expect(api.aiRouters.list).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole("radio", { name: "AI Proxy" })).toBeNull();
|
||||
expect(api.aiProxies.list).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ vi.mock("../../../lib/api", () => ({
|
||||
locks: {
|
||||
list: vi.fn(),
|
||||
},
|
||||
aiRouters: {
|
||||
aiProxies: {
|
||||
list: vi.fn(),
|
||||
},
|
||||
run: {
|
||||
@@ -45,10 +45,10 @@ beforeEach(() => {
|
||||
// stack, so the runtime strip and its button stay absent unless a test opts in.
|
||||
vi.mocked(api.lanes.runtime).mockReset();
|
||||
vi.mocked(api.lanes.runtime).mockResolvedValue({ available: false });
|
||||
// Only the built-in profile by default: a user who never opened the AI Router
|
||||
// Only the built-in profile by default: a user who never opened the AI Proxy
|
||||
// tab has nothing to switch between, so the row stays out of every other test.
|
||||
vi.mocked(api.aiRouters.list).mockReset();
|
||||
vi.mocked(api.aiRouters.list).mockResolvedValue({ items: [] });
|
||||
vi.mocked(api.aiProxies.list).mockReset();
|
||||
vi.mocked(api.aiProxies.list).mockResolvedValue({ items: [] });
|
||||
});
|
||||
vi.mocked(api.locks.list).mockReset();
|
||||
vi.mocked(api.locks.list).mockResolvedValue({ locks: [] });
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* @file WorkspaceHome.test.tsx
|
||||
* @description The workspace home: a new session starts in the first free
|
||||
* pane slot and opens it, on the AI tool picked for it (the workspace's by
|
||||
* default), AI and Model in the box and AI Router / Permission / Worktree
|
||||
* under it (the router's models on a router), all-busy is refused, Recents open a live pane or the
|
||||
* default), AI and Model in the box and AI Proxy / Permission / Worktree
|
||||
* under it (the proxy's models on a proxy), all-busy is refused, Recents open a live pane or the
|
||||
* transcript (never a resume), instructions save, context files upload and
|
||||
* remove, a folder is added with the repo's untracked worktrees suggested
|
||||
* (minus hidden sources), an unsupported tool is said so, and memory opens
|
||||
@@ -117,7 +117,7 @@ vi.mock("../../../lib/api", async (importOriginal) => {
|
||||
})),
|
||||
},
|
||||
aiTools: { list: vi.fn(async () => ({ tools: [], default: "claude" })), iconUrl: () => "" },
|
||||
aiRouters: { list: vi.fn(async () => ({ items: [] })) },
|
||||
aiProxies: { list: vi.fn(async () => ({ items: [] })) },
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -241,7 +241,7 @@ describe("WorkspaceHome", () => {
|
||||
await waitFor(() => expect(localStorage.getItem("agenthub.workspace.draft.1")).toBeNull());
|
||||
});
|
||||
|
||||
it("keeps AI and Model (with effort) in the box, Router/Permission/Worktree under it", async () => {
|
||||
it("keeps AI and Model (with effort) in the box, Proxy/Permission/Worktree under it", async () => {
|
||||
renderHome();
|
||||
await screen.findByTestId("workspace-recent-s-live");
|
||||
const labels = (id: string) =>
|
||||
@@ -252,7 +252,7 @@ describe("WorkspaceHome", () => {
|
||||
].map((x) => x.getAttribute("aria-label"));
|
||||
expect(labels("workspace-home-composer")).toEqual(["AI", "model"]);
|
||||
expect(labels("workspace-home-options")).toEqual([
|
||||
"AI Router",
|
||||
"AI Proxy",
|
||||
"Permission mode",
|
||||
expect.stringContaining("own git worktree"),
|
||||
]);
|
||||
@@ -263,8 +263,8 @@ describe("WorkspaceHome", () => {
|
||||
vi.mocked(api.aiTools.list).mockResolvedValue({
|
||||
default: "claude",
|
||||
tools: [
|
||||
{ id: "claude", label: "Claude Code", launcher: "claude", routerCapable: true },
|
||||
{ id: "codex", label: "Codex", launcher: "codex", routerCapable: true, bypass: true },
|
||||
{ id: "claude", label: "Claude Code", launcher: "claude", proxyCapable: true },
|
||||
{ id: "codex", label: "Codex", launcher: "codex", proxyCapable: true, bypass: true },
|
||||
{ id: "cursor", label: "Cursor CLI", installed: false, enabled: true },
|
||||
].map((x) => ({ installed: true, enabled: true, ...x })),
|
||||
} as never);
|
||||
@@ -289,9 +289,9 @@ describe("WorkspaceHome", () => {
|
||||
__resetAiTools();
|
||||
});
|
||||
|
||||
it("offers the router's models on a router, and sends the router", async () => {
|
||||
localStorage.setItem("agenthub.session.router", "glm");
|
||||
vi.mocked(api.aiRouters.list).mockResolvedValueOnce({
|
||||
it("offers the proxy's models on a proxy, and sends the proxy", async () => {
|
||||
localStorage.setItem("agenthub.session.proxy", "glm");
|
||||
vi.mocked(api.aiProxies.list).mockResolvedValueOnce({
|
||||
items: [
|
||||
{ name: "native", label: "Native", builtin: true, models: {} },
|
||||
{
|
||||
@@ -316,7 +316,7 @@ describe("WorkspaceHome", () => {
|
||||
expect(api.lanes.action).toHaveBeenCalledWith(
|
||||
1,
|
||||
"start",
|
||||
expect.objectContaining({ router: "glm" })
|
||||
expect.objectContaining({ proxy: "glm" })
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user