|
| 1 | +# Workspaces Overview |
| 2 | + |
| 3 | +- Purpose: Describe the full-screen "Workspaces" overlay that lists every workspace this device tracks — local + every remote host it has pushed to — with filters, search, and per-row push/pull/open actions. |
| 4 | +- Audience: Anyone touching the sidebar, the workspace push/pull flow, or the overview UI itself. |
| 5 | +- Authority: Canonical feature doc for the overview surface. |
| 6 | +- Update when: The sidebar entry, the overview layout, the filter set, or the per-row actions change. |
| 7 | +- Read next: `docs/features/remote-hosts.md` for the push/pull pipeline underneath, `docs/features/workspace-creation.md` for the new-workspace flow. |
| 8 | + |
| 9 | +## What This Feature Is |
| 10 | + |
| 11 | +A single pane that answers "where do all my workspaces live?" — across this device and every host it has pushed to. Reached from the left sidebar (the `Workspaces` button under `Automations`), opens as a full-screen overlay with the same chrome as Settings and Automations. |
| 12 | + |
| 13 | +Before this feature the sidebar only showed workspaces grouped by project on the current device. Once you pushed a workspace to a remote host the cloud icon told you it was remote, but there was no surface for "show me everything I have, everywhere." The overview is that surface. |
| 14 | + |
| 15 | +## Current Model |
| 16 | + |
| 17 | +### Entry point |
| 18 | + |
| 19 | +- `src/components/layout/sidebar-action-row.tsx` renders the `Workspaces` button under `Automations`. Click → `useUIStore.setShowWorkspacesOverview(true)`. |
| 20 | +- `src/components/layout/app-shell.tsx` early-returns `<WorkspacesOverviewView />` whenever `showWorkspacesOverview` is true — same pattern as `showAutomations` and `showSettings`. |
| 21 | +- Escape and the back button both close the overlay. |
| 22 | + |
| 23 | +### Data source |
| 24 | + |
| 25 | +- Workspaces come from `useAppStore((s) => s.appState?.workspaces)`. Every `WorkspaceSnapshot` already carries `host_id?: number | null` (added in the cloud-push series — see `remote-hosts.md`). `null` means local; a number references the local `hosts` table row id. |
| 26 | +- Host metadata comes from `useHostsStore()` — the same shared cache the `DevicePicker` and workspace context menu use, so the overview pays one IPC round-trip on first mount and reads from cache thereafter. |
| 27 | +- Project names + the basename / "Home" disambiguation reuse `groupWorkspacesByProject(workspaces, homeDir)` from `src/stores/app-store.ts`, so projects appear with the exact labels the sidebar gives them. |
| 28 | + |
| 29 | +### Layout |
| 30 | + |
| 31 | +- Sticky filter bar at the top: search, project, device, status, sort. |
| 32 | +- Result-count row underneath with a `New workspace` shortcut. |
| 33 | +- Body groups workspaces into **device sections**, in this order: |
| 34 | + 1. `This device` (the local bucket — emerald accent + custom laptop glyph) |
| 35 | + 2. Each configured host in the order they appear in `Settings → Hosts` (sky-blue accent + cloud icon) |
| 36 | + 3. Any `Removed host` orphan section, if a workspace still references a deleted host id (so rows never silently disappear) |
| 37 | +- Each device section header shows the host name + ssh target, a workspace count, and a "filtered" pill if any of the bucket's workspaces are hidden by the active filter. |
| 38 | +- Workspaces render as compact two-column cards (single column under `md`): status dot · title · project name · branch · git stats · hover-reveal action menu. |
| 39 | + |
| 40 | +### Status semantics |
| 41 | + |
| 42 | +The status dot to the left of each workspace title has two precedence layers. **Live agent status wins** when present — sourced from `getWorkspaceStatus(workspace.surfaces, appState.pane_statuses)`, the same path the sidebar uses, so when a pane flips between idle / working / waiting-on-input / review the overview row repaints automatically without polling: |
| 43 | + |
| 44 | +| Live status | Treatment | |
| 45 | +|---|---| |
| 46 | +| `working` | Amber pulsing dot via `<StatusIndicator>`; meta line shows `· agent working` | |
| 47 | +| `permission` | Red pulsing dot; meta line shows `· needs input` | |
| 48 | +| `review` | Emerald dot; meta line shows `· ready to review` | |
| 49 | + |
| 50 | +When no live status is set, the static dot falls back to: |
| 51 | + |
| 52 | +| Color | Meaning | |
| 53 | +|---|---| |
| 54 | +| Emerald | Currently open in this app (matches `appState.active_workspace_id`) | |
| 55 | +| Sky blue | Lives on a remote host | |
| 56 | +| Violet | OpenFlow workspace | |
| 57 | +| Amber | Push or pull in flight (replaces the action menu with a spinner) | |
| 58 | +| Muted | Local, not currently attached | |
| 59 | + |
| 60 | +The attached card also gets an emerald border + soft ring so it's findable at a glance even when the dot scrolls off. |
| 61 | + |
| 62 | +### Filters |
| 63 | + |
| 64 | +- **Search** — case-insensitive substring match against title, branch, or project name. Clear button appears once non-empty. |
| 65 | +- **Project** — exact-path match using the project paths that `groupWorkspacesByProject` derives. |
| 66 | +- **Device** — `All`, `This device`, or a specific host. |
| 67 | +- **Status** — `Any status`, `Currently open`, `On a remote host`, `Has uncommitted work`. |
| 68 | +- **Sort** — `Recently active` (notifications + dirty-git proxy → name tie-break), `Name`, `Branch`. |
| 69 | +- A `Clear` chip appears whenever any non-default filter is set. |
| 70 | + |
| 71 | +### Per-row actions |
| 72 | + |
| 73 | +The trailing `⋯` menu (hover- and focus-revealed) holds: |
| 74 | + |
| 75 | +- **Open workspace** — calls `activateWorkspace(workspaceId)`, closes the overlay, lands the user in the workspace. |
| 76 | +- **Copy branch name** — clipboard copy, disabled when no branch. |
| 77 | +- **Rename…** — `window.prompt` + `renameWorkspace`. |
| 78 | +- **Push to host…** — submenu of every configured host. Each entry calls `workspacePushToHost(workspaceId, hostId)` (reuses the same Tauri command the sidebar context menu uses). Disabled with a tooltip when zero hosts are configured. |
| 79 | +- **Pull back to this device** — only appears for workspaces with a non-null `host_id`. Calls `workspacePullBack(workspaceId)`. |
| 80 | +- **Delete worktree… / Close workspace…** — destructive, gated by `window.confirm`. Worktrees call `closeWorkspaceWithWorktree` with the same flags as the sidebar's remove dialog. |
| 81 | + |
| 82 | +Whole card is also clickable — clicking opens. The action menu stops event propagation so it never accidentally opens the workspace while you're navigating the submenu. |
| 83 | + |
| 84 | +The push/pull flight indicator lives on the shared `useAppStore.workspacePushPullInFlight` slot, so the same workspace shows a spinner in the sidebar **and** the overview at the same time — there's exactly one in-flight push or pull per workspace. |
| 85 | + |
| 86 | +## What Works Today |
| 87 | + |
| 88 | +- Sidebar button under Automations opens the full-screen overview; Escape and back button close it. |
| 89 | +- Every workspace this device tracks is listed, grouped by device. |
| 90 | +- Pushed workspaces visibly migrate to the matching device bucket once the push succeeds. |
| 91 | +- Filters: search (title / branch / project), project, device, status, sort. |
| 92 | +- Per-row actions: open, copy branch, rename, push to any configured host, pull back, delete. |
| 93 | +- Empty states for "no workspaces yet" (offers `New workspace`) and "filters hide everything" (offers `Clear filters`). |
| 94 | +- "Removed host" orphan bucket prevents workspaces from silently disappearing when a host row is deleted. |
| 95 | +- One push/pull spinner per workspace, shared with the sidebar. |
| 96 | +- Live agent status (working / needs-input / ready-to-review) reflected on every row in real time — same source as the sidebar's `StatusIndicator`, no polling. |
| 97 | +- First-run welcome banner (`WelcomeBanner` component) with three state-aware variants — brand-new (no devices, no siblings, with `Add a device` CTA), device-configured (nudges first push), has-siblings (counts visible cross-device workspaces with pull instructions). Dismissable; persists in localStorage and never re-shows. |
| 98 | + |
| 99 | +## Current Constraints |
| 100 | + |
| 101 | +- **Sibling-device adoption is deferred.** The overview SHOWS workspaces from other devices of the same account (via the cross-device workspace sync — see `docs/features/workspaces-sync.md`). The "Pull to this device" action on sibling rows is disabled in v1; the affordance is visible with a "coming soon" tooltip and will land in a follow-up. Until then, sibling-device rows are read-only — you can see them and their metadata, but you adopt them by visiting the device they live on. |
| 102 | +- **No created-at field** on `WorkspaceSnapshot` yet, so the "Created within" time filter UI is intentionally **not** rendered — would be misleading without the data. The `Recently active` sort uses a proxy (notification count + dirty-git heuristic + name tie-break) until a real `last_active_at` is plumbed through. |
| 103 | +- **No bulk actions.** Push N workspaces in one go isn't supported; each row pushes individually. |
| 104 | +- **Window-prompt rename.** Matches the sidebar context menu; if/when that switches to an inline edit the overview should follow. |
| 105 | +- **Delete uses window.confirm**, not the richer `RemoveWorkspaceDialog` the sidebar pops. Fine for now — the overview is the "manage many at once" surface where a heavyweight modal per row would be noisy — but if `RemoveWorkspaceDialog` grows more functionality we should reconsider. |
| 106 | + |
| 107 | +## Important Touch Points |
| 108 | + |
| 109 | +- `src/components/workspaces-overview/workspaces-overview-view.tsx` — full-screen overlay shell (WindowChrome + back button + Escape). |
| 110 | +- `src/components/workspaces-overview/workspaces-overview-section.tsx` — filter bar, device bucketing, sort. |
| 111 | +- `src/components/workspaces-overview/workspace-overview-row.tsx` — card component + action menu + push/pull wiring. |
| 112 | +- `src/components/layout/sidebar-action-row.tsx` — sidebar entry point. |
| 113 | +- `src/components/layout/app-shell.tsx` — overlay mount. |
| 114 | +- `src/stores/ui-store.ts` — `showWorkspacesOverview` boolean + setter. |
| 115 | +- `src/stores/app-store.ts` — `workspaces`, `active_workspace_id`, `workspacePushPullInFlight`, `groupWorkspacesByProject`. |
| 116 | +- `src/stores/hosts-store.ts` — shared `useHostsStore` cache. |
| 117 | +- `src/tauri/commands.ts` — `workspacePushToHost`, `workspacePullBack`, `activateWorkspace`, `renameWorkspace`, `closeWorkspace`, `closeWorkspaceWithWorktree`. |
| 118 | +- `src/tauri/types.ts` — `WorkspaceSnapshot.host_id` (the field the overview keys off). |
| 119 | + |
| 120 | +## Notes |
| 121 | + |
| 122 | +- The overview is read-only with regard to the workspace data model — it does not introduce new persistence, new Tauri commands, or new sync paths. Every action it surfaces was already shipping in the sidebar context menu; the overview just makes them discoverable from one place. |
| 123 | +- When cross-device workspace sync ships, the only change here should be that `useAppStore.workspaces` includes the synced remote rows. The bucketing already groups by `host_id` correctly, so no UI rework is expected. |
| 124 | +- Status colors and density follow `docs/features/automations.md`'s reference implementation so the two pane types feel like the same family. |
0 commit comments