feat: AI Router per session; Settings at the sidebar's foot; shipped tool icons
AI Router is a session's choice, not a workspace's: - an AI Router select beside the model picker in the workspace home and a pane's run form (RouterPicker.tsx; hidden off Claude/Codex or with only Native; last pick remembered in agenthub.session.router) - `router` on POST /api/lanes/:id/start and POST /api/run; Claude gets --settings=<data dir>/router-settings/<name>.json (0600, outranks project and user settings, key never in argv), Codex -p <name> + key via tmux -e - dashboard_runs.router; a resume naming none goes back to it - profile PATCH rewrites the session files, DELETE removes them - gone: POST /api/lanes/:id/router/apply, LaneRouterRow, the Add-workspace router radio, the claude-/codex-home `routers` answers; clearLaneRouters takes the old keys out of each workspace's settings.local.json at start Sidebar: Settings pinned to the foot, the collapse toggle beside it (stacked on the rail) instead of in the brand's corner. Icons: shipped SVG marks for claude, codex, openclaw and hermes (a black/white pair swapped by theme) in client/public/ai-tools/, used by AiToolIcon before the server's favicon.
This commit is contained in:
@@ -69,12 +69,8 @@ beforeEach(() => {
|
||||
.mockReset()
|
||||
.mockResolvedValue({ items: [native] });
|
||||
vi.mocked(api.aiRouters.create).mockReset().mockResolvedValue(nineRouter);
|
||||
vi.mocked(api.aiRouters.update)
|
||||
.mockReset()
|
||||
.mockResolvedValue({ ...nineRouter, lanes: { updated: 0, failed: [] } });
|
||||
vi.mocked(api.aiRouters.remove)
|
||||
.mockReset()
|
||||
.mockResolvedValue({ removed: "9router", lanes: { updated: 0, failed: [] } });
|
||||
vi.mocked(api.aiRouters.update).mockReset().mockResolvedValue(nineRouter);
|
||||
vi.mocked(api.aiRouters.remove).mockReset().mockResolvedValue({ removed: "9router" });
|
||||
vi.mocked(api.aiRouters.test)
|
||||
.mockReset()
|
||||
.mockResolvedValue({ ok: true, status: 200, message: "Highest" });
|
||||
@@ -149,25 +145,6 @@ describe("AiRouterConfig", () => {
|
||||
expect(call?.[1]).not.toHaveProperty("apiKey");
|
||||
});
|
||||
|
||||
it("warns when a rotated key did not reach every lane using the profile", async () => {
|
||||
// The whole point of pushing an edit out to the lanes is that a revoked key
|
||||
// stops being used. A lane that silently kept the old one must be named,
|
||||
// or the user believes a rotation is complete when it is not.
|
||||
vi.mocked(api.aiRouters.list).mockResolvedValue({ items: [native, nineRouter] });
|
||||
vi.mocked(api.aiRouters.update).mockResolvedValue({
|
||||
...nineRouter,
|
||||
lanes: { updated: 1, failed: ["7 (checkout): EBADJSON"] },
|
||||
});
|
||||
render(<AiRouterConfig />);
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.click(await screen.findByLabelText("aiRouter.edit 9Router"));
|
||||
await user.type(screen.getByLabelText("aiRouter.fieldApiKey"), "sk-rotated");
|
||||
await user.click(screen.getByRole("button", { name: "aiRouter.save" }));
|
||||
|
||||
expect(await screen.findByText("aiRouter.lanesFailed")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("surfaces a server error instead of closing the form", async () => {
|
||||
vi.mocked(api.aiRouters.create).mockRejectedValue(
|
||||
new Error("baseUrl must be http:// or https://")
|
||||
|
||||
Reference in New Issue
Block a user