/** * @file RouterPicker.tsx * @description The AI Router a NEW session starts on — a session's choice, not * its workspace's (`router` on `POST /api/lanes/:id/start`, `server/lib/router-apply.js`): * Native or one of the saved profiles (`useAiRouters`, which the caller also * reads to list the router's models). A tool that cannot use a router (only * Claude and Codex can) shows Native, disabled. The last pick is remembered * for the next session (`agenthub.session.router`). * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { api, type AiRouter } from "../../lib/api"; import { useAiTools } from "../../hooks/useAiTools"; import { Select } from "../Select"; const KEY = "agenthub.session.router"; /** The remembered pick, `"native"` by default. */ export function useSessionRouter(): [string, (v: string) => void] { const [value, setValue] = useState(() => { try { return localStorage.getItem(KEY) || "native"; } catch { return "native"; } }); const set = (v: string) => { setValue(v); try { localStorage.setItem(KEY, v); } catch { /* storage off — the pick still holds for this page */ } }; return [value, set]; } /** The saved profiles (Native included), null until they arrive. */ export function useAiRouters(): AiRouter[] | null { const [routers, setRouters] = useState(null); useEffect(() => { let alive = true; api.aiRouters .list() .then((r) => alive && setRouters(r.items)) .catch(() => alive && setRouters([])); return () => { alive = false; }; }, []); return routers; } /** What to send as `router` for `tool`: nothing for native or a tool that has none. */ export function routerArg(routerCapable: boolean | undefined, router: string): { router?: string } { return routerCapable && router && router !== "native" ? { router } : {}; } export function RouterPicker({ tool, routers, value, onChange, variant, }: { variant?: "field" | "ghost"; tool: string; /** From `useAiRouters`. */ routers: AiRouter[] | null; value: string; onChange: (v: string) => void; }) { const { t } = useTranslation(["lanes"]); const aiTools = useAiTools(); const capable = aiTools.byId(tool)?.routerCapable ?? (tool === "claude" || tool === "codex"); const native = t("router.native", { tool: aiTools.label(tool) }); // A remembered profile that has since been deleted falls back to native, // or the start would be refused for a router the user cannot see. useEffect(() => { if (routers && value !== "native" && !routers.some((r) => r.name === value)) onChange("native"); }, [routers, value, onChange]); const options = capable && routers?.length ? routers.map((r) => ({ value: r.name, label: r.builtin ? native : r.label })) : [{ value: "native", label: native }]; return (
value={capable ? value : "native"} onChange={onChange} options={options} disabled={!capable} variant={variant} label={t("router.label")} />
); }