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:
@@ -25,6 +25,17 @@ vi.mock("../../../lib/api", async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
api: {
|
||||
aiTools: {
|
||||
list: r({
|
||||
default: "claude",
|
||||
tools: [
|
||||
{ id: "claude", label: "Claude Code", resume: false, bypass: false },
|
||||
{ id: "codex", label: "Codex", resume: false, bypass: true },
|
||||
{ id: "antigravity", label: "Antigravity", resume: true, bypass: true },
|
||||
{ id: "gemini", label: "Gemini", resume: false, bypass: false },
|
||||
].map((x) => ({ ...x, installed: true, enabled: true, command: x.id })),
|
||||
}),
|
||||
},
|
||||
run: { files: r({ items: [] }) },
|
||||
sessions: { list: r({ sessions: [], total: 0, limit: 100, offset: 0 }) },
|
||||
ccConfig: {
|
||||
@@ -443,6 +454,41 @@ describe("RunSetup — codex lanes", () => {
|
||||
const { spies } = renderSetup({ tool: "codex" });
|
||||
fireEvent.click(screen.getByText(i18n.t("run:actions.start")));
|
||||
expect(spies.onStart).toHaveBeenCalledOnce();
|
||||
expect(spies.onStart.mock.calls[0][0].resumeLast).toBeUndefined();
|
||||
expect(spies.onStart.mock.calls[0]?.[0].resumeLast).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("RunSetup — catalog tools", () => {
|
||||
it("offers a per-run bypass switch with a warning, and sends it", async () => {
|
||||
const { spies } = renderSetup({ tool: "antigravity", prompt: "go" });
|
||||
const box = await screen.findByRole("checkbox", { name: i18n.t("run:fields.bypass") });
|
||||
expect(box).not.toBeChecked();
|
||||
expect(screen.queryByText(/without asking/)).toBeNull();
|
||||
fireEvent.click(box);
|
||||
expect(screen.getByText(/Antigravity will run every command/)).toBeTruthy();
|
||||
fireEvent.click(screen.getByText(i18n.t("run:actions.start")));
|
||||
expect(spies.onStart.mock.calls[0]?.[0].bypass).toBe(true);
|
||||
// Resume is offered because the catalog gives the tool a resume flag.
|
||||
expect(screen.getByText(i18n.t("run:resume.resumeOption"))).toBeTruthy();
|
||||
});
|
||||
|
||||
it("leaves out resume and bypass for a tool with neither", async () => {
|
||||
const { spies } = renderSetup({ tool: "gemini", prompt: "go" });
|
||||
expect(await screen.findByPlaceholderText("Ask Gemini anything…")).toBeTruthy();
|
||||
expect(screen.queryByRole("checkbox", { name: i18n.t("run:fields.bypass") })).toBeNull();
|
||||
expect(screen.queryByText(i18n.t("run:resume.resumeOption"))).toBeNull();
|
||||
fireEvent.click(screen.getByText(i18n.t("run:actions.start")));
|
||||
expect(spies.onStart.mock.calls[0]?.[0].bypass).toBeUndefined();
|
||||
});
|
||||
|
||||
it("gives Codex the switch but not Claude", async () => {
|
||||
renderSetup({ tool: "codex" });
|
||||
expect(await screen.findByRole("checkbox", { name: i18n.t("run:fields.bypass") })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("gives Claude no bypass switch (it has the bypassPermissions mode)", async () => {
|
||||
renderSetup({ tool: "claude" });
|
||||
await screen.findByPlaceholderText("Ask Claude Code anything…");
|
||||
expect(screen.queryByRole("checkbox", { name: i18n.t("run:fields.bypass") })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user