feat(ui): sidebar lists workspaces with their sessions, Claude Desktop style

The sidebar's project tree becomes a flat workspace list (WorkspaceTree):
one row per workspace in the stored order, each expanding to its sessions.
A session that still has a live pane opens /workspace?lane=<id>&run=<runId>,
which only attaches pane 0 to that tmux session; any other session opens
its read-only transcript. Nothing in the sidebar starts or resumes a run.
This commit is contained in:
2026-10-01 22:23:01 +07:00
parent 744e9c6cda
commit 2e051e23b6
16 changed files with 616 additions and 538 deletions
+5 -4
View File
@@ -389,13 +389,14 @@ A missing source `.env` falls back to `.env.example` with a loud warning, never
The dashboard web UI merges workspaces and runs into a single **Workspace** page accessible at `/run` (the old `/lanes` route now redirects here). Top to bottom:
- **No page header.** The page title and the four counters (`lanes`, `running`, `needs you`, `dead`) are gone: the sidebar's project tree shows every workspace with its own status dot, and the status bar at the bottom of every page counts running and need-you workspaces. `GET /api/lanes` still returns `counts` for other callers.
- **The sidebar's project tree picks the workspace** — on every page, between the nav and the footer (`client/src/components/ProjectTree.tsx`). Workspaces are grouped under **Projects**: one per git repository, keyed by the repo's MAIN worktree root, with every workspace outside a repo under **Folders** (last). The grouping comes from `GET /api/projects` (see `docs/API.md`), read when the set of workspaces changes or on the header's Refresh — never on every status frame, so it shells out to git only then, and caches what it read. A row (`WorkspaceRow.tsx`) shows a status dot (green = active, pulsing while running; amber row = needs you; red = dead or error; grey = idle), the title or folder name, a `primary` badge on the repo's own root, the branch (or the path relative to the repo), the tool's icon and the id as `#23` — the number the CLI addresses it by. Clicking a row opens `/workspace?lane=<id>`: the URL is the selection, so a notification link, F5 and the back button all land on the same workspace, and a bare `/workspace` reopens the last one shown (`agenthub.workspace.selected`) or the first. The project's `+` opens the Add-workspace modal prefilled with that repo; the PROJECTS header has Refresh and a plain `+`. In the collapsed rail the tree is a single icon that opens the sidebar.
- **No page header.** The page title and the four counters (`lanes`, `running`, `needs you`, `dead`) are gone: the sidebar's workspace list shows every workspace with its own status dot, and the status bar at the bottom of every page counts running and need-you workspaces. `GET /api/lanes` still returns `counts` for other callers.
- **The sidebar's workspace list picks the workspace** — on every page, between the nav and the footer (`client/src/components/WorkspaceTree.tsx`), shaped like Claude Desktop's project list: every workspace on ONE level (no repo grouping, no worktree rows), in your stored order. A row (`WorkspaceRow.tsx`) is a chevron, a status dot (green = active, pulsing while running; amber row = needs you; red = dead or error; grey = idle), the title or folder name and the tool's icon, with `+` (open it for a new session) and `⋯` on hover. Clicking the name opens `/workspace?lane=<id>` and expands the row: the URL is the selection, so a notification link, F5 and the back button all land on the same workspace, and a bare `/workspace` reopens the last one shown (`agenthub.workspace.selected`) or the first. The header has search (workspace names and paths, plus the names of the sessions listed) and `+` (Add workspace). In the collapsed rail the list is a single icon that opens the sidebar.
- **A workspace's sessions** — the chevron expands the row to its sessions (`WorkspaceSessionList.tsx`: `GET /api/sessions?cwd=&tool=`, the newest 20, re-read when the workspace's status or session moves; which rows are expanded is kept in `agenthub.sidebar.projects` → `expanded`). A filled dot marks a session that still has a live pane (a `GET /api/run` handle carrying its session id): clicking it opens `/workspace?lane=<id>&run=<runId>`, which ATTACHES pane 0 to that tmux session (`attachRequest` on `LaneConsolePane`) — it starts nothing and resumes nothing. Any other session opens its read-only transcript at `/sessions/<id>`; resuming it is a separate, explicit action there, because a resume re-reads the whole transcript into context.
- **Row menu** — right-click a row, or its `⋯` button: Start/Stop (slot 0), Rename, the Model (AI Router) select, Create/update readme file (Claude only), New workspace here…, Copy path, Reset worktree… (managed only) and Delete workspace…, each the call the rest of the UI already makes. Delete is record-only exactly as before (see "Removing a workspace").
- **Untracked worktrees** — a repo's worktrees that no workspace sits on (from `git worktree list`) are listed dimmed under the project, with **Adopt** (`POST /api/lanes/ensure {exact: true}` with the default AI tool, then selected). Shown by default; the tree's preferences (`agenthub.sidebar.projects` in `localStorage`: collapsed projects, `showUntracked`, hidden sources `claude` = `<repo>/.claude/worktrees/*` and `agenthub` = `LANES_ROOT`) can hide them — Settings → Git → **Worktrees** (`/settings/worktrees`) for every project, and a project's own page (`/settings/projects/<encoded root>`) to override the sources for that project only (`projectHideSources`) — and the project row then says "N hidden" — a click shows them for this visit.
- **Untracked worktrees** are no longer listed in the sidebar; the settings that hid them (`showUntracked`, hidden sources `claude` = `<repo>/.claude/worktrees/*` and `agenthub` = `LANES_ROOT`, Settings → Git → **Worktrees** and a project's own page) are kept for the workspace page's folder suggestions.
- **A project's settings page** — Settings → Projects → <repo> shows the repository's root, main branch and worktree count, links each of its workspaces, holds the per-project worktree override above, and offers **Forget all workspaces…**: after one confirm listing them it runs the same `remove` as the row menu for each (preflight, then `POST /api/lanes/:id/remove` with those facts as `expect`) and stops at the first refusal. Forgetting drops AgentHub's records only — nothing on disk is deleted.
- **No workspaces yet** — the page shows a single banner (`lane-empty`): icon, one line of what a workspace is, the CLI equivalent (`agenthub lanes add --cwd $(pwd)`) and an **Add workspace** affordance. The whole banner IS the button — clicking anywhere on it opens the Add-workspace modal. The banner stays compact on purpose: the run setup form below it is what needs the height.
- **Reordering** — drag a row onto another within its project, or press **Alt+↑/↓** with a row focused. The order is a **per-viewer preference**, not a workspace fact: it persists to `localStorage` (`agenthub.workspace.laneOrder`) and writes nothing server-side — `GET /api/lanes` and `agenthub lanes` keep answering in id order, and the order does not follow you to another browser. A workspace the stored order has never seen lands at the END of its project; a stored id whose workspace is gone is skipped.
- **Reordering** — drag a row onto another, or press **Alt+↑/↓** with a row focused. The order is a **per-viewer preference**, not a workspace fact: it persists to `localStorage` (`agenthub.workspace.laneOrder`) and writes nothing server-side — `GET /api/lanes` and `agenthub lanes` keep answering in id order, and the order does not follow you to another browser. A workspace the stored order has never seen lands at the END; a stored id whose workspace is gone is skipped.
- **Renaming a workspace** — double-click its sidebar row (or Rename in the row menu) to edit in place. **Enter** saves via `PATCH /api/lanes/:id` with `{title}`, **Escape** and clicking away both cancel, and saving an empty name clears the title, which puts the folder name back in its place. Renaming is display-only — a managed workspace's branch name was slugified from its title once, at creation.
- **Workspace-detail header row** — one row, and every per-workspace control lives on it: liveness + status, the workspace's name (read-only here — renaming is on the sidebar row; no "Workspace 23 ·" prefix — the id is on the row), time-on-stage, the pipeline picker, the Model (AI Router) select, the feature picker (time-on-stage and both pickers only while a pipeline template is installed; there are no stage chips here — the declared stage and the `auto: <stage>` detection both render in the PipelineMap directly below, so the chips were a second, smaller copy of the same two facts), and on the right the **Tools** menu (mcp sync / sync-base check / create-update readme, hidden when the workspace declares no runtime profile) and the split-view toggle. The detail block under it (the workspace card, the PipelineMap, the proof gallery) is always shown — there is no info toggle any more — and Delete lives in the sidebar row menu. Tools sits here rather than in Settings because every one of its actions runs against one workspace's `cwd` — in Settings it would need a workspace picker just to say which. **Create/update readme file** (Claude workspaces only — Codex has no slash commands) starts the workspace's terminal run with `/sg-all:sg-md-writing` as its first message, so the workspace opens already writing/refreshing its `CLAUDE.md`/`AGENTS.md` set; it needs the `sg-all` plugin installed in Claude Code, and without it the session just reports an unknown command. It used to be a checkbox in the Add-workspace modal: creating a workspace and writing its docs are separate decisions, and the second is worth re-running long after the first.
- **Detail panel** — the selected workspace's declared stage, its inferred stage when detection leads, a full-width pipeline map, and a legend naming all five node states plus the dashed-amber inferred treatment Hidden while no pipeline template is installed (see "Pipeline stages and the five node states").