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:
@@ -244,10 +244,23 @@ describe("AiToolsSettings — install and update", () => {
|
||||
describe("AiToolIcon", () => {
|
||||
it("retries the icon when the same instance shows another tool", async () => {
|
||||
const { AiToolIcon } = await import("../../AiToolIcon");
|
||||
const { container, rerender } = render(<AiToolIcon tool={{ id: "claude", label: "Claude" }} />);
|
||||
const { container, rerender } = render(<AiToolIcon tool={{ id: "gemini", label: "Gemini" }} />);
|
||||
fireEvent.error(container.querySelector("img")!);
|
||||
expect(screen.getByText("C")).toBeInTheDocument();
|
||||
rerender(<AiToolIcon tool={{ id: "codex", label: "Codex" }} />);
|
||||
expect(container.querySelector('img[src="/api/ai-tools/codex/icon"]')).toBeTruthy();
|
||||
expect(screen.getByText("G")).toBeInTheDocument();
|
||||
rerender(<AiToolIcon tool={{ id: "cursor", label: "Cursor CLI" }} />);
|
||||
expect(container.querySelector('img[src="/api/ai-tools/cursor/icon"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses the shipped mark, and a light/dark pair for Hermes", async () => {
|
||||
const { AiToolIcon } = await import("../../AiToolIcon");
|
||||
const { container, rerender } = render(<AiToolIcon tool={{ id: "claude", label: "Claude" }} />);
|
||||
expect(container.querySelector('img[src="/ai-tools/claude.svg"]')).toBeTruthy();
|
||||
rerender(<AiToolIcon tool={{ id: "hermes", label: "Hermes" }} />);
|
||||
expect(container.querySelector('img[src="/ai-tools/hermes-black.svg"]')?.className).toMatch(
|
||||
/dark:hidden/
|
||||
);
|
||||
expect(container.querySelector('img[src="/ai-tools/hermes-white.svg"]')?.className).toMatch(
|
||||
/hidden dark:block/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user