feat(ai-tools): Settings → AI Tools, Default/Other lane picker, per-run bypass switch (#40 A1)

Settings → Dashboard gains an AI Tools section (installed tools with
Enabled/Disabled and Set default, available ones with a homepage link,
Refresh). useAiTools shares one catalog copy across the app and replaces
LANE_TOOL_LABEL. The Add-lane modal picks Default (<tool>) or Other…; the
router row reads Native (<tool>) and disables AI Router profiles for tools
that cannot use one. The run form gives catalog tools resume and a red
per-run Bypass permissions switch (Codex too). Claude Code / Codex settings
tabs hide while that tool is disabled. The Workspace probes each lane tool's
CLI fresh.

Docs: CLAUDE.md tool/bypass rule, AGENTS.md, LANES.md, API.md, CLI.md,
server/README.md, OpenAPI fragment + regenerated openapi.yaml.
This commit is contained in:
2026-09-28 11:19:30 +07:00
parent 77917203fd
commit fcfebe44f1
33 changed files with 1306 additions and 153 deletions
+44 -2
View File
@@ -1939,6 +1939,18 @@ export const api = {
{ method: "DELETE" }
),
},
aiTools: {
/** GET /api/ai-tools - the catalog, what is installed, enabled, and the default. */
list: () => request<AiToolsView>("/ai-tools"),
/** POST /api/ai-tools/refresh - re-read the login shell's PATH and re-detect. */
refresh: () => request<AiToolsView>("/ai-tools/refresh", { method: "POST" }),
/** PUT /api/ai-tools/:id - enable/disable a tool, or make it the default. */
update: (id: string, body: { enabled?: boolean; default?: true }) =>
request<AiToolsView>(`/ai-tools/${encodeURIComponent(id)}`, {
method: "PUT",
body: JSON.stringify(body),
}),
},
aiRouters: {
/** GET /api/ai-routers - every profile, the built-in `native` one first. */
list: () => request<{ items: AiRouter[] }>("/ai-routers"),
@@ -1987,7 +1999,7 @@ export const api = {
if (options?.laneId !== undefined) qs.set("laneId", String(options.laneId));
return request<{ items: DashboardRunHistoryItem[] }>(`/run/history?${qs.toString()}`);
},
/** GET /api/run/binary - whether the lane's CLI (`claude` or `codex`) was found on PATH. */
/** GET /api/run/binary - whether the lane tool's CLI was found on PATH (fresh look). */
binary: (tool: LaneTool = "claude") =>
request<{ found: boolean; path: string | null }>(`/run/binary?tool=${tool}`),
/** GET /api/run/tmux - whether the `tmux` binary was found on PATH. */
@@ -2963,6 +2975,34 @@ export interface GitAuthNeeded {
rejected: boolean;
}
/** One AI coding CLI from the server's AI Tools catalog (GET /api/ai-tools). */
export interface AiTool {
id: string;
label: string;
/** The command that was found (or would be looked for) on PATH. */
command: string | null;
homepageUrl: string | null;
installed: boolean;
path: string | null;
enabled: boolean;
source: "builtin" | "custom";
/** `claude` / `codex` keep their own run form; null for every other tool. */
launcher: "claude" | "codex" | null;
prompt: { mode: "argv" | "flag" | "after-start" } | null;
/** Offers "Resume last session". */
resume: boolean;
/** Offers the per-run "Bypass permissions" switch. */
bypass: boolean;
/** Can use an AI Router profile (Claude, Codex); others are native only. */
routerCapable: boolean;
}
export interface AiToolsView {
tools: AiTool[];
/** The effective default tool id, or null when nothing is installed and enabled. */
default: string | null;
}
export interface AiRouter {
/** Identifier, derived from the label. Stable: a lane stores this. */
name: string;
@@ -3667,8 +3707,10 @@ export interface RunStartArgs {
sandbox?: CodexSandbox;
/** Codex only: `-a`. Ignored by a claude lane. */
approval?: CodexApproval;
/** Codex only: `codex resume --last` instead of picking a session id. */
/** Continue the tool's last session: Codex `resume --last`, a catalog tool's `resume` args. */
resumeLast?: boolean;
/** Turn off the tool's permission prompts for THIS run (its catalog bypass args). */
bypass?: boolean;
}
/** A lane's tmux-backed terminal run — one per lane, id is the tmux session
+6 -9
View File
@@ -2409,15 +2409,12 @@ export interface ProofFeature {
mtime: number;
}
/** Which CLI drives a lane's pane. Fixed at lane creation and never patched. */
export type LaneTool = "claude" | "codex" | "antigravity";
/** How each CLI is named on screen (strip tag, console title, prompt box). */
export const LANE_TOOL_LABEL: Record<LaneTool, string> = {
claude: "Claude",
codex: "Codex",
antigravity: "Antigravity",
};
/**
* Which CLI drives a lane's pane: an AI Tools catalog id (`claude`, `codex`,
* `antigravity`, `gemini`, …). Fixed at lane creation and never patched. Its
* label comes from `useAiTools().label(id)`.
*/
export type LaneTool = string;
export interface Lane {
id: number;