/** * @file ProxyPicker.tsx * @description The AI Proxy a NEW session starts on — a session's choice, not * its workspace's (`proxy` on `POST /api/lanes/:id/start`, `server/lib/proxy-apply.js`): * Native or one of the saved profiles (`useAiProxies`, which the caller also * reads to list the proxy's models). A tool that cannot use a proxy (only * Claude and Codex can) shows Native, disabled. The last pick is remembered * for the next session (`agenthub.session.proxy`; the pre-rename * `agenthub.session.router` is still read when that is unset). * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { api, type AiProxy } from "../../lib/api"; import { useAiTools } from "../../hooks/useAiTools"; import { Select } from "../Select"; const KEY = "agenthub.session.proxy"; const LEGACY_KEY = "agenthub.session.router"; /** The remembered pick, `"native"` by default. */ export function useSessionProxy(): [string, (v: string) => void] { const [value, setValue] = useState(() => { try { return localStorage.getItem(KEY) || localStorage.getItem(LEGACY_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 useAiProxies(): AiProxy[] | null { const [proxies, setProxies] = useState(null); useEffect(() => { let alive = true; api.aiProxies .list() .then((r) => alive && setProxies(r.items)) .catch(() => alive && setProxies([])); return () => { alive = false; }; }, []); return proxies; } /** What to send as `proxy` for `tool`: nothing for native or a tool that has none. */ export function proxyArg(proxyCapable: boolean | undefined, proxy: string): { proxy?: string } { return proxyCapable && proxy && proxy !== "native" ? { proxy } : {}; } export function ProxyPicker({ tool, proxies, value, onChange, variant, }: { variant?: "field" | "ghost"; tool: string; /** From `useAiProxies`. */ proxies: AiProxy[] | null; value: string; onChange: (v: string) => void; }) { const { t } = useTranslation(["lanes"]); const aiTools = useAiTools(); const capable = aiTools.byId(tool)?.proxyCapable ?? (tool === "claude" || tool === "codex"); const native = t("proxy.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 proxy the user cannot see. useEffect(() => { if (proxies && value !== "native" && !proxies.some((r) => r.name === value)) onChange("native"); }, [proxies, value, onChange]); const options = capable && proxies?.length ? proxies.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("proxy.label")} />
); }