feat: workspaces own their sessions; desktop parity; sidebar and settings cleanup
Workspace sessions (docs/plans/desktop-parity, phases 1-5 and 7): - sessions.lane_id: a workspace lists only the sessions its panes held, stamped by bindRunSession (first writer wins, nested `claude -p` excluded); GET /api/sessions?lane=&archived= - session row state dot, double-click rename, archive (sessions.archived_at, hides only); the activity bar's duplicate Sessions tab removed - Fork beside Resume (--fork-session, Claude only), Ctrl/Cmd+click a live session opens it in a second pane, a diff line's comment is pasted into the workspace's last-focused pane (lib/panePaste.ts) dashboard_runs stability: - a pane's previous run is archived as <pane>@<started_at> instead of being overwritten by INSERT OR REPLACE - a run is flagged abandoned only once its tmux session is gone, at boot and on the run-heal beat (panes outlive a server restart) UI: - sidebar: no footer, language or theme block; small collapse button in the brand corner; version + update check in the connection details modal, a status-bar dot while a release is out; language in Settings -> General - one Switch component for every switch, the themed Checkbox for every checkbox (no native checkboxes) Also: AI Tools catalog trimmed to 10 built-ins (custom-file prompt modes tested through fixtures), and the workspace plans under docs/plans/ (the multi-user server plan lives in the AgentHub-Server repo).
This commit is contained in:
@@ -135,21 +135,21 @@ describe("lanes on catalog tools", () => {
|
||||
let r = await request("POST", "/api/lanes", { cwd: dir("a"), title: "a", tool: "gemini" });
|
||||
assert.equal(r.status, 201);
|
||||
assert.equal(r.body.lane.tool, "gemini");
|
||||
r = await request("POST", "/api/lanes", { cwd: dir("b"), title: "b", tool: "aider" });
|
||||
r = await request("POST", "/api/lanes", { cwd: dir("b"), title: "b", tool: "omp" });
|
||||
assert.equal(r.status, 201, "a tool that is not installed may still get a lane");
|
||||
r = await request("POST", "/api/lanes", { cwd: dir("c"), title: "c", tool: "nope" });
|
||||
assert.equal(r.body.error.code, "EBADTOOL");
|
||||
await request("PUT", "/api/ai-tools/aider", { enabled: false });
|
||||
r = await request("POST", "/api/lanes", { cwd: dir("d"), title: "d", tool: "aider" });
|
||||
await request("PUT", "/api/ai-tools/omp", { enabled: false });
|
||||
r = await request("POST", "/api/lanes", { cwd: dir("d"), title: "d", tool: "omp" });
|
||||
assert.equal(r.status, 400);
|
||||
assert.equal(r.body.error.code, "EDISABLED");
|
||||
r = await request("POST", "/api/lanes/ensure", { cwd: dir("e"), tool: "aider" });
|
||||
r = await request("POST", "/api/lanes/ensure", { cwd: dir("e"), tool: "omp" });
|
||||
assert.equal(r.body.error.code, "EDISABLED");
|
||||
r = await request("POST", "/api/lanes/ensure", { cwd: dir("b"), tool: "aider" });
|
||||
r = await request("POST", "/api/lanes/ensure", { cwd: dir("b"), tool: "omp" });
|
||||
assert.equal(r.status, 200, "an existing lane on a disabled tool is still found");
|
||||
r = await request("POST", "/api/lanes/worktree", { sourceRepo: dir("f"), tool: "aider" });
|
||||
r = await request("POST", "/api/lanes/worktree", { sourceRepo: dir("f"), tool: "omp" });
|
||||
assert.equal(r.body.error.code, "EDISABLED");
|
||||
await request("PUT", "/api/ai-tools/aider", { enabled: true });
|
||||
await request("PUT", "/api/ai-tools/omp", { enabled: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -190,23 +190,23 @@ describe("install / update / icons", () => {
|
||||
it("starts an install job and reports it", async () => {
|
||||
jobs.__reset();
|
||||
assert.equal((await request("GET", "/api/ai-tools/job")).body, null);
|
||||
const r = await request("POST", "/api/ai-tools/qwen-code/install");
|
||||
const r = await request("POST", "/api/ai-tools/copilot/install");
|
||||
assert.equal(r.status, 202);
|
||||
assert.equal(r.body.job.tool, "qwen-code");
|
||||
assert.equal(r.body.job.tool, "copilot");
|
||||
const done = await settle();
|
||||
assert.equal(done.status, "ok");
|
||||
assert.deepEqual(spawned.at(-1), [
|
||||
path.join(BIN, "npm"),
|
||||
"install",
|
||||
"-g",
|
||||
"@qwen-code/qwen-code@latest",
|
||||
"@github/copilot@latest",
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps refusals to statuses", async () => {
|
||||
jobs.__reset();
|
||||
assert.equal((await request("POST", "/api/ai-tools/nope/install")).status, 404);
|
||||
const noInstall = await request("POST", "/api/ai-tools/aider/install");
|
||||
const noInstall = await request("POST", "/api/ai-tools/antigravity/install");
|
||||
assert.equal(noInstall.status, 400);
|
||||
assert.equal(noInstall.body.error.code, "ENOINSTALL");
|
||||
const notInstalled = await request("POST", "/api/ai-tools/codex/update");
|
||||
@@ -216,7 +216,7 @@ describe("install / update / icons", () => {
|
||||
const notNpm = await request("POST", "/api/ai-tools/gemini/update");
|
||||
assert.equal(notNpm.status, 409);
|
||||
assert.equal(notNpm.body.error.code, "ENOTNPM");
|
||||
const cross = await request("POST", "/api/ai-tools/qwen-code/install", undefined, {
|
||||
const cross = await request("POST", "/api/ai-tools/copilot/install", undefined, {
|
||||
Origin: "https://evil.test",
|
||||
});
|
||||
assert.equal(cross.status, 403);
|
||||
|
||||
Reference in New Issue
Block a user