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

148 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.