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:
+10
-39
@@ -1042,19 +1042,16 @@ export const api = {
|
||||
/**
|
||||
* PUT /api/settings/claude-home - repoint the server at a new Claude home.
|
||||
* @param path New absolute `~/.claude` root the server should read from.
|
||||
* @returns `{ ok, claude_home, routers }` — the accepted path, and the
|
||||
* lanes whose AI Router profile the new home's store does not have.
|
||||
* @returns `{ ok, claude_home }` — the accepted path.
|
||||
*/
|
||||
set: (path: string) =>
|
||||
request<{
|
||||
ok: boolean;
|
||||
claude_home: string;
|
||||
env_owned?: boolean;
|
||||
routers: { missing: string[] };
|
||||
}>("/settings/claude-home", { method: "PUT", body: JSON.stringify({ path }) }),
|
||||
},
|
||||
/** The Codex home: same shape as `claudeHome`, plus the AI Router layers
|
||||
* a PUT re-wrote into the new home for codex lanes. */
|
||||
/** The Codex home: same shape as `claudeHome`. */
|
||||
codexHome: {
|
||||
/** GET /api/settings/codex-home */
|
||||
get: () => request<{ codex_home: string; env_owned: boolean }>("/settings/codex-home"),
|
||||
@@ -1064,7 +1061,6 @@ export const api = {
|
||||
ok: boolean;
|
||||
codex_home: string;
|
||||
env_owned: boolean;
|
||||
routers: { updated: number; failed: string[] };
|
||||
}>("/settings/codex-home", { method: "PUT", body: JSON.stringify({ path }) }),
|
||||
},
|
||||
/** AgentHub's hooks in Codex's `$CODEX_HOME/hooks.json` — changed only on a
|
||||
@@ -2084,7 +2080,7 @@ export const api = {
|
||||
* display a key, so it has none to send back.
|
||||
*/
|
||||
update: (name: string, body: Partial<AiRouterInput>) =>
|
||||
request<AiRouter & { lanes: AiRouterLaneSync }>(`/ai-routers/${encodeURIComponent(name)}`, {
|
||||
request<AiRouter>(`/ai-routers/${encodeURIComponent(name)}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
@@ -2098,14 +2094,13 @@ export const api = {
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
/**
|
||||
* DELETE /api/ai-routers/:name - also resets every lane using it back to
|
||||
* native, on disk as well as in the DB.
|
||||
* DELETE /api/ai-routers/:name - also removes the session files that
|
||||
* copy its key.
|
||||
*/
|
||||
remove: (name: string) =>
|
||||
request<{ removed: string; lanes: AiRouterLaneSync }>(
|
||||
`/ai-routers/${encodeURIComponent(name)}`,
|
||||
{ method: "DELETE" }
|
||||
),
|
||||
request<{ removed: string }>(`/ai-routers/${encodeURIComponent(name)}`, {
|
||||
method: "DELETE",
|
||||
}),
|
||||
},
|
||||
|
||||
// ────────────────────────────────── Run API ─────────────────────────────────
|
||||
@@ -2895,23 +2890,6 @@ export const api = {
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
},
|
||||
/**
|
||||
* POST /lanes/:id/router/apply - point this lane at an AI Router profile,
|
||||
* or back at `native`. Writes the lane's own config on disk; only the
|
||||
* profile NAME comes from here.
|
||||
*/
|
||||
routerApply: (id: number, profile: string) =>
|
||||
request<{
|
||||
tool: string;
|
||||
file: string | null;
|
||||
profile: string;
|
||||
/** A pane is live and keeps the old models until it restarts. */
|
||||
restart_required: boolean;
|
||||
run_id: string | null;
|
||||
}>(`/lanes/${id}/router/apply`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ profile }),
|
||||
}),
|
||||
mcpSync: (id: number) =>
|
||||
request<{ servers: string[]; profilesSeeded: string[] }>(`/lanes/${id}/mcp/sync`, {
|
||||
method: "POST",
|
||||
@@ -3253,15 +3231,6 @@ export interface AiRouterTestInput {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/** How an edit or a delete landed on the lanes already using that profile.
|
||||
* Attaching copies a profile, so the store alone is not the whole truth: a
|
||||
* lane listed in `failed` still carries the OLD key and endpoint. */
|
||||
export interface AiRouterLaneSync {
|
||||
updated: number;
|
||||
/** One `"<id> (<title>): <code>"` line per lane that could not be rewritten. */
|
||||
failed: string[];
|
||||
}
|
||||
|
||||
/** Body accepted when creating or updating a profile. */
|
||||
export interface AiRouterInput {
|
||||
label: string;
|
||||
@@ -3926,6 +3895,8 @@ export interface RunStartArgs {
|
||||
resumeLast?: boolean;
|
||||
/** Turn off the tool's permission prompts for THIS run (its catalog bypass args). */
|
||||
bypass?: boolean;
|
||||
/** AI Router profile THIS session runs on; omitted = native, or a resume's own. */
|
||||
router?: string;
|
||||
}
|
||||
|
||||
/** A lane's tmux-backed terminal run — one per lane, id is the tmux session
|
||||
|
||||
Reference in New Issue
Block a user