Files
AgentHub/docs/plans/desktop-parity/README.md
T
nntrivi2001 a6bd33bcff 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).
2026-10-02 13:19:50 +07:00

8.1 KiB
Raw Blame History

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 in WorkspaceHome.tsx, the activity bar's WorkspaceSessionsTab.tsx) asks GET /api/sessions?cwd=<lane.cwd>&tool=, which filters s.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 that cd'd into a subfolder is not.
  • Nothing durable records which sessions a workspace started. dashboard_runs has lane_id, but its key is the pane name (agenthub-lane-<id>[-slot]) and server/lib/dashboard-runs.js writes it with INSERT OR REPLACE, so each Start overwrites the last; bindRunSession (server/routes/hooks.js) keeps only the pane's CURRENT session, so a /clear forgets 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):

  • bindRunSession already decides when a pane HOLDS a session (a nested claude -p the agent runs through Bash inherits AGENTHUB_RUN_ID but does not take the pane over). Whenever the run's session is — or becomes — this session, stamp sessions.lane_id = dashboard_runs.lane_id where 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 with cwd+tool for the fold).
  • PATCH /api/sessions/:id {lane_id} (number or null), sameOriginGuard: the lane must exist and have the session's tool (EBADLANE). Existing name/status patching is unchanged.

Client:

  • WorkspaceSessionList and 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/owningLane unchanged (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_since set (amber, "needs you"), status = 'error' (red), else hollow. Re-read when the lane's needs_action moves too.
  • Rename: double-click a session row → PATCH /api/sessions/:id {name}. syncSessionName already 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

  • buildClaudeArgv gains forkSession → --fork-session after --resume <id> (Claude only; refused elsewhere, EUNSUPPORTED). POST /api/lanes/:id/start passes forkSession: true through like resumeSessionId.
  • 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/worktree with 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.