a6bd33bcff
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).
148 lines
8.1 KiB
Markdown
148 lines
8.1 KiB
Markdown
# 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.
|