feat(workspace): AI tool per session, one place to start a session, ended sessions open like Claude Desktop
- A session picks its own AI tool: `tool` on POST /api/lanes/:id/start (a catalog id, known + enabled, else 400 EBADTOOL/EDISABLED); a resume keeps the tool its session ran on (`sessions.tool`). The workspace's `lanes.tool` is now only the default. - The workspace home's prompt box is the one place a session starts. Its fields run in order: AI → AI Router (Native or a profile) → Model (the tool's own list, or the router's models) → Permission (Claude mode / Codex sandbox+approval / catalog bypass) → Effort. - The pane's run form (RunSetup) is gone: New run, a single pane's exit and an empty split pane's New session all go to the home. RunSetup.tsx keeps PromptEditor, CwdAutocomplete and ModelPicker (new `choices` prop). - The tool icon moved from the sidebar's workspace row to each session row (right edge, `session.tool`); the split-pane header shows the run's tool. - An ended session opens in its workspace (`&session=`, SessionReplay.tsx): its conversation read-only plus a prompt box; only a SENT message resumes it (resumeSessionId + prompt, first free slot). The console is only hidden behind it. - Analyst button beside Files/Changes in the right panel opens /sessions/:id for the opened session. - Gemini CLI ships its own icon (client/public/ai-tools/gemini.svg, from zonalogo.com). - RouterPicker always shows (Native disabled for tools without router support); useAiRouters() exported. - Tests: server start-with-tool cases; SessionReplay, home field order/tool/ router models, empty pane, Analyst; dropped the cwd-field suites (Run.defaultCwd, Workspace.laneCwd). Docs: CLAUDE.md rules, API.md, LANES.md, README, ARCHITECTURE, INSTALL.
This commit is contained in:
@@ -1318,4 +1318,43 @@ describe("lane ensure, start mode, lane_id and releasing a finished run", () =>
|
||||
assert.equal(after.run_id, null);
|
||||
assert.equal(after.status, "idle");
|
||||
});
|
||||
|
||||
it("starts a session on the AI tool the request names, not the workspace's", async () => {
|
||||
const lane = await adoptedLane("session-tool");
|
||||
assert.equal(lane.tool, "claude");
|
||||
// A fake `codex` on PATH, so the pane runs this script, not a real CLI.
|
||||
const bin = path.join(ROOT, "session-tool-bin");
|
||||
fs.mkdirSync(bin, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(bin, "codex"),
|
||||
"#!/usr/bin/env node\nprocess.on('SIGTERM', () => process.exit(0));\nsetInterval(() => {}, 1000);\n"
|
||||
);
|
||||
fs.chmodSync(path.join(bin, "codex"), 0o755);
|
||||
const originalPath = process.env.PATH;
|
||||
process.env.PATH = `${bin}${path.delimiter}${originalPath}`;
|
||||
try {
|
||||
const started = await request("POST", `/api/lanes/${lane.id}/start`, {
|
||||
prompt: "hi",
|
||||
tool: "codex",
|
||||
});
|
||||
assert.equal(started.status, 200, JSON.stringify(started.body));
|
||||
const run = require("../lib/dashboard-runs").getRun(started.body.run_id);
|
||||
assert.equal(run.tool, "codex");
|
||||
// The workspace's own tool is untouched.
|
||||
assert.equal(started.body.lane.tool, "claude");
|
||||
} finally {
|
||||
await request("POST", `/api/lanes/${lane.id}/stop`, {});
|
||||
process.env.PATH = originalPath;
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses a start naming a tool the catalog does not know", async () => {
|
||||
const lane = await adoptedLane("session-tool-bad");
|
||||
for (const tool of ["no-such-cli", 42]) {
|
||||
const r = await request("POST", `/api/lanes/${lane.id}/start`, { prompt: "hi", tool });
|
||||
assert.equal(r.status, 400, JSON.stringify(r.body));
|
||||
assert.equal(r.body.error.code, "EBADTOOL");
|
||||
}
|
||||
assert.equal(require("../lib/lanes").getLane(lane.id).run_id, null);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user