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:
@@ -292,6 +292,47 @@ describe("Sessions API", () => {
|
||||
assert.ok(claude.body.sessions.every((s) => s.tool === "claude"));
|
||||
});
|
||||
|
||||
it("lists only the sessions a workspace owns, not the rest of its folder", async () => {
|
||||
const laneId = db
|
||||
.prepare("INSERT INTO lanes (cwd, tool) VALUES ('/tmp/own-ws', 'claude')")
|
||||
.run().lastInsertRowid;
|
||||
const add = (id, cwd, lane) =>
|
||||
db
|
||||
.prepare(
|
||||
"INSERT INTO sessions (id, name, status, cwd, lane_id, started_at, updated_at) VALUES (?, ?, 'active', ?, ?, '2026-09-29T00:00:00Z', '2026-09-29T00:00:00Z')"
|
||||
)
|
||||
.run(id, id, cwd, lane);
|
||||
add("own-1", "/tmp/own-ws", laneId);
|
||||
add("own-sub", "/tmp/own-ws/pkg", laneId); // cd'd into a subfolder — still its own
|
||||
add("stray-1", "/tmp/own-ws", null); // a terminal session in the same folder
|
||||
const own = await fetch(`/api/sessions?lane=${laneId}`);
|
||||
assert.deepEqual(own.body.sessions.map((s) => s.id).sort(), ["own-1", "own-sub"]);
|
||||
assert.equal((await fetch("/api/sessions?lane=abc")).status, 400);
|
||||
});
|
||||
|
||||
it("archives a session out of a workspace's list and back, deleting nothing", async () => {
|
||||
const laneId = stmts.getSession.get("own-1").lane_id;
|
||||
let res = await patch("/api/sessions/own-1", { archived: true });
|
||||
assert.equal(res.status, 200);
|
||||
assert.ok(res.body.session.archived_at);
|
||||
const live = await fetch(`/api/sessions?lane=${laneId}&archived=0`);
|
||||
assert.deepEqual(
|
||||
live.body.sessions.map((s) => s.id),
|
||||
["own-sub"]
|
||||
);
|
||||
const gone = await fetch(`/api/sessions?lane=${laneId}&archived=1`);
|
||||
assert.deepEqual(
|
||||
gone.body.sessions.map((s) => s.id),
|
||||
["own-1"]
|
||||
);
|
||||
// Without the parameter every session is listed, as before.
|
||||
assert.equal((await fetch(`/api/sessions?lane=${laneId}`)).body.sessions.length, 2);
|
||||
res = await patch("/api/sessions/own-1", { archived: "yes" });
|
||||
assert.equal(res.status, 400);
|
||||
res = await patch("/api/sessions/own-1", { archived: false });
|
||||
assert.equal(res.body.session.archived_at, null);
|
||||
});
|
||||
|
||||
it("should paginate sessions", async () => {
|
||||
const res = await fetch("/api/sessions?limit=1&offset=0");
|
||||
assert.equal(res.body.sessions.length, 1);
|
||||
|
||||
Reference in New Issue
Block a user