feat(workspace): a session in its own worktree, Claude's way
Desktop-parity phase 6, following Claude Desktop: - the workspace home's Worktree checkbox starts the pane with `claude --worktree=<name>`, so Claude Code makes, populates (.worktreeinclude, WorktreeCreate hook) and cleans up <repo>/.claude/worktrees/<name>; AgentHub only excludes /.claude/worktrees/ at the repo root (Claude does not) - server/lib/session-worktree.js resolves a session's worktree from the session row only (owned by the workspace, registered by git worktree list); every lane git route takes ?session= and answers ENOWORKTREE instead of falling back to the main folder - the session view's Changes panel works in the opened run's worktree (dashboard_runs.worktree, GitTarget in the client api) - Resume/Fork start in the session's worktree; a gone one is ENOWORKTREE - Archive of a worktree session removes the worktree after asking: git worktree remove without --force, git branch -d (unmerged work keeps its branch), a lock held by a live pid refused, a stale one cleared - listWorktrees reads `locked <reason>` locks - tests scrub GIT_* env so the pre-commit suite cannot act on this repo
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# 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
|
||||
_Status: written 2026-10-02. Phases 1–5 and 7 implemented and committed
|
||||
(a6bd33b); phase 6 implemented 2026-10-02 the Claude way (D4). Phases run in order, each one change-set on `main`.
|
||||
Comes BEFORE the multi-user server plan (now in the AgentHub-Server repo,
|
||||
https://git.smartgift.io.vn/Smartgift-AI/AgentHub-Server) — that plan's owner column hangs
|
||||
off the session ownership built in phase 1 here._
|
||||
|
||||
## Where we stand
|
||||
@@ -36,7 +37,7 @@ archived, and give a session — not a project — its own worktree.
|
||||
| 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. |
|
||||
| D4 | Per-session worktree (phase 6) | **Claude's model** (user's call, 2026-10-02): the pane runs `claude --worktree=<name>`, so Claude Code itself creates `<repo>/.claude/worktrees/<name>` on branch `worktree-<name>`, copies `.worktreeinclude`, runs the user's `WorktreeCreate` hook and cleans up on exit. The session stays the workspace's own (no child workspace). Archive removes a CLEAN worktree, like Claude Desktop. |
|
||||
| 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`). |
|
||||
|
||||
@@ -121,15 +122,48 @@ Ctrl/Cmd+click on a live session row → `/workspace?lane=<id>&run=<runId>&pane=
|
||||
`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)
|
||||
### Phase 6 — worktree per session, Claude's way
|
||||
|
||||
- `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.
|
||||
Verified 2026-10-02 on Claude Code 2.1.286: `claude -w probe1` makes
|
||||
`<repo>/.claude/worktrees/probe1` on `worktree-probe1` (from origin/HEAD, or
|
||||
`worktree.baseRef`), locks it with `claude session probe1 (pid …)`, and its
|
||||
hooks report that worktree as `cwd`. It does NOT exclude `.claude/worktrees/`,
|
||||
so the main repo shows `?? .claude/`.
|
||||
|
||||
- **Start:** the home prompt box gets a **Worktree** checkbox (Claude
|
||||
workspaces in a git repo). `POST /api/lanes/:id/start {slot, worktree: true}`
|
||||
→ `spawnRun` puts `--worktree=<name>` in the argv (`=` form: `-w [name]`
|
||||
takes an optional value and would eat the prompt as the name); the name is
|
||||
server-made (`ah-<lane>-<base36 time>`), never from the request. Claude only
|
||||
(`EUNSUPPORTED` elsewhere). `.claude/worktrees/` goes into the common git
|
||||
dir's `info/exclude` at the repo root (`excludeFromGit`).
|
||||
`dashboard_runs.worktree` = 1 marks the run.
|
||||
- **Which worktree a session is in:** `server/lib/session-worktree.js`
|
||||
`sessionWorktree(lane, sessionId)` → `{path, branch, locked}` only when the
|
||||
session is OWNED by the lane, its `cwd` is not the lane's own folder, and
|
||||
`git worktree list` of the lane's repo registers that path. Nothing else
|
||||
turns a request into a path.
|
||||
- **Git panel:** every `/:id/git*` route takes `?session=<id>`; with it the
|
||||
route works in that session's worktree, and answers `400 ENOWORKTREE`
|
||||
instead of falling back to the main folder (a commit must never land in the
|
||||
wrong tree). The session view passes the session of the attached run when
|
||||
that run is a worktree run.
|
||||
- **Resume / Fork** of a worktree session start the pane in its worktree;
|
||||
one whose worktree is gone is refused (`ENOWORKTREE`) — Claude finds a
|
||||
session by the folder it ran in.
|
||||
- **Sidebar / Recents:** a worktree session shows a branch icon and its
|
||||
branch in the tooltip.
|
||||
- **Archive = remove the worktree** (Claude Desktop): for a worktree session
|
||||
with no live pane the archive button asks first, then
|
||||
`POST /api/lanes/:id/session-worktree/remove {session, confirm: true}` runs
|
||||
`git worktree remove <path>` WITHOUT force (git refuses a dirty tree) and
|
||||
`git branch -d` (refuses unmerged work — the branch is then kept and
|
||||
reported); a lock held by a live process is refused (`ELOCKED`), a stale
|
||||
one (dead pid) unlocked first. Only after that succeeds is the session
|
||||
archived. The workspace's own folder and any worktree no session of this
|
||||
workspace ran in are never touched.
|
||||
- **Not built:** auto-archive on PR merge (no PR integration), a branch prefix
|
||||
setting (the CLI has none), worktrees for Codex.
|
||||
|
||||
### Phase 7 — diff comment to prompt (optional)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user