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
+41 -2
View File
@@ -196,6 +196,40 @@ describe("pty-run", () => {
assert.ok(newSessionArgv.includes("abc12345"));
});
it("spawnRun forks a resumed Claude session into a new id, and refuses a fork elsewhere", () => {
let newSessionArgv = null;
tmux.__setExecImpl((args) => {
if (args[0] === "has-session") {
const e = new Error("gone");
e.status = 1;
throw e;
}
if (args[0] === "new-session") newSessionArgv = args;
return "";
});
pty.spawnRun({ laneId: 31, cwd: "/tmp/repo", resumeSessionId: "abc12345", forkSession: true });
const at = newSessionArgv.indexOf("--resume");
assert.deepEqual(newSessionArgv.slice(at, at + 3), ["--resume", "abc12345", "--fork-session"]);
// The pane holds the FORK, whose id only its first hook reports.
const dashboardRuns = require("../lib/dashboard-runs");
assert.equal(dashboardRuns.getRun("agenthub-lane-31").session_id, null);
assert.throws(
() => pty.spawnRun({ laneId: 32, cwd: "/tmp/repo", forkSession: true }),
(e) => e.code === "EBADSESSION"
);
assert.throws(
() =>
pty.spawnRun({
laneId: 33,
cwd: "/tmp/repo",
tool: "codex",
resumeSessionId: "abc12345",
forkSession: true,
}),
(e) => e.code === "EUNSUPPORTED"
);
});
it("spawnRun offers bypass in the pane's shift+tab cycle without starting in it", () => {
let newSessionArgv = null;
tmux.__setExecImpl((args) => {
@@ -427,11 +461,16 @@ describe("pty-run", () => {
}
savedPath = process.env.PATH;
process.env.PATH = `${BIN}:/usr/bin:/bin`;
aiTools.refresh();
// The built-in catalog plus three test-only tools for the prompt modes
// and bypassEnv no built-in tool uses any more (custom files may).
aiTools.__setCatalogDirs([
path.join(__dirname, "..", "data", "ai-tools"),
path.join(__dirname, "fixtures", "ai-tools"),
]);
});
after(() => {
process.env.PATH = savedPath;
aiTools.refresh();
aiTools.__setCatalogDirs(null);
});
const at = (name) => path.join(BIN, name);
const envOf = (call, key) => {