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:
2026-10-02 14:19:02 +07:00
parent 5084334d81
commit c507d37baf
55 changed files with 851 additions and 1992 deletions
+24 -4
View File
@@ -1,14 +1,23 @@
/**
* @file AiToolIcon.tsx
* @description An AI tool's icon: the server's cached favicon
* (GET /api/ai-tools/:id/icon), or the label's first letter when there is
* none. Shared by the AI Tools modal and the Add-lane tool picker.
* @description An AI tool's icon: the mark shipped in `client/public/ai-tools/`
* when there is one (a light/dark pair where a mark needs one per theme), else
* the server's cached favicon (GET /api/ai-tools/:id/icon), else the label's
* first letter. Every tool icon in the app renders through here.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useState } from "react";
import { api, type AiTool } from "../lib/api";
/** Shipped marks by tool id; `[light, dark]` swaps with the theme (CSS only). */
const BUNDLED: Record<string, string | [string, string]> = {
claude: "/ai-tools/claude.svg",
codex: "/ai-tools/codex.svg",
openclaw: "/ai-tools/openclaw.svg",
hermes: ["/ai-tools/hermes-black.svg", "/ai-tools/hermes-white.svg"],
};
/**
* @param props.tool - The catalog entry.
* @param props.className - Size classes (default `h-8 w-8`).
@@ -22,13 +31,24 @@ export function AiToolIcon({
}) {
// Keyed by id: one instance showing another tool must retry its icon.
const [failedId, setFailedId] = useState<string | null>(null);
const box = `${className} shrink-0 rounded-md bg-surface-4 object-contain p-1`;
const bundled = BUNDLED[tool.id];
if (typeof bundled === "string") return <img src={bundled} alt="" className={box} />;
if (bundled) {
return (
<>
<img src={bundled[0]} alt="" className={`${box} dark:hidden`} />
<img src={bundled[1]} alt="" className={`${box} hidden dark:block`} />
</>
);
}
if (failedId !== tool.id) {
return (
<img
src={api.aiTools.iconUrl(tool.id)}
alt=""
onError={() => setFailedId(tool.id)}
className={`${className} shrink-0 rounded-md bg-surface-4 object-contain p-1`}
className={box}
/>
);
}