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:
@@ -59,6 +59,7 @@ import type {
|
||||
import type { Session, LaneTool } from "../../lib/types";
|
||||
import { useAiTools } from "../../hooks/useAiTools";
|
||||
import { Select } from "../Select";
|
||||
import { RouterPicker, routerArg, useSessionRouter } from "./RouterPicker";
|
||||
|
||||
export interface SlashCommand {
|
||||
name: string;
|
||||
@@ -409,6 +410,8 @@ export function RunSetup(props: RunSetupProps) {
|
||||
const canResume = !catalogTool || !!entry?.resume;
|
||||
// Per run, never remembered: the form starts unticked every time it mounts.
|
||||
const [bypass, setBypass] = useState(false);
|
||||
const [router, setRouter] = useSessionRouter();
|
||||
const routerSent = routerArg(entry?.routerCapable, router).router;
|
||||
const canBypass = props.tool !== "claude" && !!entry?.bypass;
|
||||
// Claude and Codex always take a model; a catalog tool only when its file names the flag.
|
||||
const hasModel = !catalogTool || !!entry?.modelFlag;
|
||||
@@ -450,6 +453,15 @@ export function RunSetup(props: RunSetupProps) {
|
||||
<ModelPicker value={props.model} onChange={props.onModelChange} tool={props.tool} />
|
||||
</Field>
|
||||
)}
|
||||
{/* A resume goes back to the router its session ran on (server-side). */}
|
||||
{!props.resumeSession && (
|
||||
<RouterPicker
|
||||
tool={props.tool}
|
||||
value={router}
|
||||
onChange={setRouter}
|
||||
wrap={(select) => <Field label={t("lanes:router.label")}>{select}</Field>}
|
||||
/>
|
||||
)}
|
||||
{catalogTool ? null : props.tool === "codex" ? (
|
||||
<>
|
||||
<Field label={t("fields.sandbox")}>
|
||||
@@ -536,7 +548,7 @@ export function RunSetup(props: RunSetupProps) {
|
||||
// update has not landed yet on this tick, so reading
|
||||
// props.resumeSession here would resume nothing.
|
||||
if (s && !props.busy)
|
||||
handleStart(props, s, bypass, hasModel, entry?.defaultModel);
|
||||
handleStart(props, s, bypass, hasModel, entry?.defaultModel, routerSent);
|
||||
}}
|
||||
// Fall back to the typed cwd when no lane is selected — otherwise
|
||||
// an unfiltered picker lists every session from every repo.
|
||||
@@ -564,7 +576,14 @@ export function RunSetup(props: RunSetupProps) {
|
||||
value={props.prompt}
|
||||
onChange={props.onPromptChange}
|
||||
onSubmit={() =>
|
||||
handleStart(props, undefined, canBypass && bypass, hasModel, entry?.defaultModel)
|
||||
handleStart(
|
||||
props,
|
||||
undefined,
|
||||
canBypass && bypass,
|
||||
hasModel,
|
||||
entry?.defaultModel,
|
||||
routerSent
|
||||
)
|
||||
}
|
||||
placeholder={t("fields.promptPlaceholderTerminal", { tool: aiTools.label(props.tool) })}
|
||||
rows={5}
|
||||
@@ -620,7 +639,14 @@ export function RunSetup(props: RunSetupProps) {
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleStart(props, undefined, canBypass && bypass, hasModel, entry?.defaultModel)
|
||||
handleStart(
|
||||
props,
|
||||
undefined,
|
||||
canBypass && bypass,
|
||||
hasModel,
|
||||
entry?.defaultModel,
|
||||
routerSent
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
!props.binaryFound ||
|
||||
@@ -655,7 +681,8 @@ function handleStart(
|
||||
session?: Session,
|
||||
bypass?: boolean,
|
||||
hasModel?: boolean,
|
||||
defaultModel?: string | null
|
||||
defaultModel?: string | null,
|
||||
router?: string
|
||||
) {
|
||||
const resume = session ?? props.resumeSession;
|
||||
const codex = props.tool === "codex";
|
||||
@@ -682,6 +709,8 @@ function handleStart(
|
||||
// asked for, so a resume carries no prompt at all.
|
||||
initialPrompt: resume ? undefined : props.prompt || undefined,
|
||||
bypass: bypass || undefined,
|
||||
// A resume names none: the server goes back to its session's own router.
|
||||
router: resume ? undefined : router,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user