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:
+7
-46
@@ -27,7 +27,7 @@ const laneFeatures = require("../lib/lane-features");
|
||||
const proofLib = require("../lib/proof");
|
||||
const { broadcast } = require("../websocket");
|
||||
const runs = require("../lib/pty-run");
|
||||
const { applyRouterToLane } = require("../lib/router-apply");
|
||||
const { resumeRouter } = require("../lib/router-apply");
|
||||
const { sameOriginGuard } = require("./run");
|
||||
const { writeLaneFile } = require("../lib/lane-file-write");
|
||||
const { preflight } = require("../lib/lane-preflight");
|
||||
@@ -735,48 +735,6 @@ router.post("/:id/mcp/sync", sameOriginGuard, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /:id/router/apply — point this lane at one AI Router profile (or back at
|
||||
* `native`). Body: `{ profile: "<name>" }`.
|
||||
*
|
||||
* The profile name is the only thing taken from the request: what gets written,
|
||||
* and where, is derived from the lane row (`server/lib/router-apply.js`). The
|
||||
* lane's `router_profile` column is set in the same call so a later spawn knows
|
||||
* which `-p` layer a codex pane needs.
|
||||
*/
|
||||
router.post("/:id/router/apply", sameOriginGuard, (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
const profile = (req.body || {}).profile;
|
||||
if (typeof profile !== "string" || !profile) {
|
||||
return res.status(400).json({ error: { code: "EBADREQ", message: "profile is required" } });
|
||||
}
|
||||
try {
|
||||
const result = applyRouterToLane(lane, profile);
|
||||
lanesLib.setRouterProfile(lane.id, profile);
|
||||
// A pane that is already running does NOT fully adopt the switch. Claude
|
||||
// Code re-applies an `env` block to a live session when the settings file
|
||||
// changes, but the session's model was resolved at startup — so the OLD
|
||||
// router's model name keeps being sent to the NEW endpoint, which answers
|
||||
// "model not found". (Switching to `native` is the same story: the router's
|
||||
// model name reaches api.anthropic.com.) Nothing here can fix that from the
|
||||
// outside; the pane has to be restarted. Report it instead of letting the
|
||||
// switch look clean and then fail on the user's next prompt.
|
||||
const runId = runs.runIdForLane(lane.id);
|
||||
const live = runs.getRun(runId)?.status === "running";
|
||||
// Every other mutating lane route broadcasts; without it the client's
|
||||
// cached row keeps the OLD router_profile, and the next lane switch
|
||||
// re-reads that stale row and flips the picker back.
|
||||
broadcastLane(lane.id);
|
||||
res.json({ ...result, restart_required: live, run_id: live ? runId : null });
|
||||
} catch (err) {
|
||||
const status =
|
||||
{ ENOTFOUND: 404, EBADJSON: 409, EBADNAME: 400, EUNSUPPORTED: 400 }[err.code] || 500;
|
||||
res.status(status).json({ error: { code: err.code || "ERUNTIME", message: err.message } });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Detect a Node.js project at this lane's OWN directory and scaffold
|
||||
* .agenthub/profile/ if one is found — the HTTP equivalent of
|
||||
@@ -1880,9 +1838,12 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
|
||||
// asked for the other CLI would otherwise get a pane that doesn't
|
||||
// match the lane's badge, its history, or its binary probe.
|
||||
tool: current.tool || "claude",
|
||||
// Same reasoning as `tool`: the router is the lane's fact, written
|
||||
// into its config when the lane was created.
|
||||
routerProfile: current.router_profile || null,
|
||||
// The SESSION's AI Router, by name; a resume that names none goes
|
||||
// back to the one its session last ran on.
|
||||
router:
|
||||
body.router !== undefined
|
||||
? body.router
|
||||
: resumeRouter(body.resumeSessionId || (body.resume ? current.session_id : null)),
|
||||
model: body.model,
|
||||
permissionMode: body.permissionMode,
|
||||
effort: body.effort,
|
||||
|
||||
Reference in New Issue
Block a user