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:
2026-10-02 12:05:00 +07:00
parent 5b269698f6
commit a6bd33bcff
121 changed files with 2699 additions and 1552 deletions
+12 -12
View File
@@ -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);