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).
8.1 KiB
Desktop parity — what the workspace still lacks next to Claude Desktop / Codex app
Status: written 2026-10-02. Phases 1–5 and 7 implemented 2026-10-02
(uncommitted); phase 6 waits on decision D4. Phases run in order, each one change-set on main.
Comes BEFORE docs/plans/multi-user-server/ — that plan's owner column hangs
off the session ownership built in phase 1 here.
Where we stand
The workspace-sessions plan gave AgentHub Claude Desktop's surface: a sidebar of workspaces that expand to sessions, a project home with a prompt box, Recents, Instructions / Memory / Context / Folder. Its data model is still "a workspace is a folder":
- Every session list (sidebar
WorkspaceSessionList.tsx, home Recents inWorkspaceHome.tsx, the activity bar'sWorkspaceSessionsTab.tsx) asksGET /api/sessions?cwd=<lane.cwd>&tool=, which filterss.cwd = ?exactly. A session typed into a terminal in that folder, or one from months before the workspace existed, is listed as the workspace's own; a session thatcd'd into a subfolder is not. - Nothing durable records which sessions a workspace started.
dashboard_runshaslane_id, but its key is the pane name (agenthub-lane-<id>[-slot]) andserver/lib/dashboard-runs.jswrites it withINSERT OR REPLACE, so each Start overwrites the last;bindRunSession(server/routes/hooks.js) keeps only the pane's CURRENT session, so a/clearforgets the previous one.
Claude Desktop (Code tab) and the Codex app both treat a project as the owner of the sessions/threads started in it, show each one's state, let one be archived, and give a session — not a project — its own worktree.
Decisions
| # | Decision | Value |
|---|---|---|
| D1 | Sessions in the folder that the workspace did not start | Not listed (user's call, 2026-10-02): a workspace shows only its own sessions; the rest stay on the Sessions page. The fold and ?unowned= / PATCH {lane_id} built first were removed again. |
| D2 | What "belongs to a workspace" means | sessions.lane_id = the lane whose pane held the session (stamped from the hook's run_id), or one the user added by hand. First writer wins; only the user moves it. |
| D3 | Old history | Not guessed. Only the sessions dashboard_runs still names are backfilled; everything older is not listed in any workspace. |
| D4 | Per-session worktree (phase 6) | A child workspace: a worktree lane with parent_lane_id, listed under its parent in the sidebar. Reuses every lane rule (git surface, destroy guard) instead of giving a session its own cwd. Confirm with the user before phase 6. |
| D5 | Tool chosen per session instead of per workspace | Out of scope here. lanes.tool stays immutable. Revisit with the user after phase 6. |
| D6 | Environments (SSH/cloud), Routines/Automations, cross-session messaging | Not built. AgentHub runs locally and does not orchestrate (CLAUDE.md). |
Phases
| # | Scope | Size |
|---|---|---|
| 1 | Session ownership: sessions.lane_id, GET /api/sessions?lane=, the "other sessions" fold, Add to workspace |
medium |
| 2 | A session row's state (running / needs you / error), rename, archive | medium |
| 3 | Drop the activity bar's Sessions tab (third copy of the same list) | small |
| 4 | Fork a Claude session into a free pane (--resume <id> --fork-session) |
small |
| 5 | Ctrl/Cmd+click a live session in the sidebar opens it in a second pane | small |
| 6 | Worktree per session = child workspace (D4) — ask first | large |
| 7 | Comment on a diff line → text pasted into the session's prompt | medium, optional |
Phase 1 — session ownership
Schema (server/db.js, probe-then-ALTER like every other column, inside the
migration lock):
sessions.lane_id INTEGER(nullable, no FK — a removed lane leaves its sessions, which are the user's history) +idx_sessions_lane.- Backfill once:
UPDATE sessions SET lane_id = r.lane_id FROM dashboard_runs r WHERE r.session_id = sessions.id AND sessions.lane_id IS NULL AND r.lane_id IS NOT NULL.
Stamping (server/routes/hooks.js):
bindRunSessionalready decides when a pane HOLDS a session (a nestedclaude -pthe agent runs through Bash inheritsAGENTHUB_RUN_IDbut does not take the pane over). Whenever the run's session is — or becomes — this session, stampsessions.lane_id = dashboard_runs.lane_idwhere it is null.- A session started from the dashboard with no hooks (Codex without hooks, any catalog tool) is not stamped, so no workspace lists it. Known limit.
API (server/routes/sessions.js):
GET /api/sessions?lane=<id>→s.lane_id = ?;?unowned=1→s.lane_id IS NULL(combined withcwd+toolfor the fold).PATCH /api/sessions/:id {lane_id}(number ornull),sameOriginGuard: the lane must exist and have the session'stool(EBADLANE). Existingname/statuspatching is unchanged.
Client:
WorkspaceSessionListand home Recents list?lane=<id>; below them a folded "Other sessions in this folder (N)" (?cwd=&tool=&unowned=1), each row with Add to workspace.SessionResumeButton/owningLaneunchanged (resume still needs the folder).
Tests first: server — a hook carrying run_id stamps the lane, a nested
claude -p session does not, the backfill, ?lane=, ?unowned=1, PATCH
validation. Client — the list asks lane=, the fold, Add to workspace.
Phase 2 — row state, rename, archive
- Row dot: live pane (green), the session
awaiting_input_sinceset (amber, "needs you"),status = 'error'(red), else hollow. Re-read when the lane'sneeds_actionmoves too. - Rename: double-click a session row →
PATCH /api/sessions/:id {name}.syncSessionNamealready lets only a Claude/rename(custom title) override a non-placeholder name. - Archive:
sessions.archived_at TEXT;PATCH {archived: true|false}; lists for a workspace skip archived rows unless?archived=1; the row's hover shows an archive button, the home Recents a "Show archived" toggle. Archiving deletes nothing.
Phase 3 — one session list fewer
WorkspaceSessionsTab.tsx and the sessions activity-bar tab go; a stored
agenthub.workspace.panelTab = "sessions" falls back to changes. Resume stays
on the transcript page (SessionResumeButton). CLAUDE.md's "Opening a
session never resumes it" rule loses the Sessions-tab mention.
Phase 4 — fork
buildClaudeArgvgainsforkSession→--fork-sessionafter--resume <id>(Claude only; refused elsewhere,EUNSUPPORTED).POST /api/lanes/:id/startpassesforkSession: truethrough likeresumeSessionId.- Transcript page: Fork beside Resume, same warning, same first-free-slot
logic (
freeSlot). The new session id arrives by hook and is stamped to the workspace by phase 1.
Phase 5 — open in a second pane
Ctrl/Cmd+click on a live session row → /workspace?lane=<id>&run=<runId>&pane=1;
Workspace.tsx switches a 1-pane layout to 2 and hands attachRequest to that
pane instead of pane 0. A plain click is unchanged.
Phase 6 — worktree per session (needs D4 confirmed)
lanes.parent_lane_id INTEGER(nullable).- Home prompt box: a Worktree toggle (git repos only). Run →
POST /api/lanes/worktreewith a generated branch +parent_lane_id, then start the prompt in the child's slot 0. - Sidebar: a child lane is listed under its parent's sessions, not as a top-level row. Archive on it = the existing Remove (record only — the worktree stays the user's, per the remove rule) plus a hint to delete it.
Phase 7 — diff comment to prompt (optional)
A Comment action on a diff line in LaneDiffView.tsx builds
<path>:<line> — <text> and pastes it into the pane the Changes panel belongs
to, through the same client-side term.paste() path the image drop uses. No
server route.
Ground rules
CLAUDE.md binds every phase: one console render site, opening a session never
resumes it, migrations probe-then-ALTER under the lock, headers on every
touched file, npm run test:server + npm run test:client (snapshots
regenerated only after reviewing the diff), docs + the CLAUDE.md marker in the
same change-set.