5fe5d51b1b
AI Proxy (was AI Router): - server/lib/ai-proxies.js, proxy-apply.js, routes/ai-proxies.js mounted at /api/ai-proxies and the old /api/ai-routers; `router` still accepted on the start routes; profiles move to <CLAUDE_HOME>/agenthub-proxies/ - dashboard_runs.router renamed in place to proxy: an older build on the same DB fails on that column, so dev and prod sharing one DB update together - a proxy's baseUrl stored as its root (a typed /v1 made /v1/v1/messages); Test probes Claude /v1/messages and Codex /v1/responses - Settings → AI Proxy (/settings/ai-router redirects), ProxyPicker Updates: - every tab reloads onto the new UI build when its WebSocket reconnects to a server of another version (lib/newBuildReload.ts, once per version) - Update now stops waiting on a plugin chain that failed or pulled nothing (the old server never goes down), and reloads as soon as a new version answers Also: - a pane whose command exits at once says so (EPANEEXITED) instead of "Command failed: tmux set-option … mouse on" - the pane drag preview draws where the pane actually lands
105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
/**
|
|
* @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ĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
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<AiProxy[] | null>(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 (
|
|
<div data-testid="proxy-picker">
|
|
<Select<string>
|
|
value={capable ? value : "native"}
|
|
onChange={onChange}
|
|
options={options}
|
|
disabled={!capable}
|
|
variant={variant}
|
|
label={t("proxy.label")}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|