feat(desktop,host-service): tombstone workspace deletes + kanban board view for the workspaces page
Links
- ExecPlan:
plans/done/20260809-1015-workspace-tombstones-kanban-board.md(decision log + deviations recorded there)
Summary
This PR bundles two related features, decided in a design walkthrough with Kiet:
- Workspace tombstoning — deleting a workspace soft-deletes its host.db row (
archivedAt+archiveReason: "merged" | "deleted") instead of erasing it. Sessions tombstone too (reason is always"deleted"— they have no PR link). - Kanban board view — the workspaces page gains a Linear-style board layout behind a list ↔ board toggle, with Merged/Deleted history columns fed by the tombstones, plus URL-synced multi-select filters shared by both layouts.
Part 1: Tombstoning (host-service)
Why
Merged and deleted workspaces vanished without a trace — the row delete was documented as "the commit point and the only record." History is the raw material for the board's Merged/Deleted columns.
What / How
workspacesgainsarchivedAt/archiveReason;pullRequestsgains a stickymergedAt(migration0020, additive only, generated with drizzle-kit — not hand-edited).- The destroy pipeline archives at a new commit point after the teardown script (step 1.5): reason is
"merged"when the linked PR was observed merged, else"deleted". Any later cleanup failure un-archives so the workspace stays visible and retryable; the archive broadcast reuses the existingdeletedevent shape so no consumer needs a wire migration. runArchivedWorkspaceReconcile(startup sweep) finishes crash-interrupted deletes — archived rows whose worktree still exists — with force + best-effort teardown, guarded so it never touches a path a live row owns (worktree paths get reused when a branch is re-created).workspace.listexcludes archived rows by default;includeArchived: trueopts in. Adopt-existing-worktree, branch search, worktree re-import, name backfill, the git watcher, and all PR refresh sweeps skip tombstones;getPullRequestsByWorkspacesdeliberately still serves archived ids so ghost cards keep their frozen PR pill.- Retention: keep forever (visibility is bounded by the board's archived window instead).
Key Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Storage | Soft-delete in place, consumers updated | Single source of truth; no unique-index collisions exist (worktreePath/branch are unindexed; one-main-per-project can't archive) |
| Commit point | After teardown, before physical cleanup | Blocking teardown failure re-opens the delete dialog for force-retry; archiving first would unmount it mid-prompt |
| Sessions | Tombstone too (originally exempt; reversed in review) | "Essentially workspaces with a little missing data" — simplifies the pipeline and gives sessions Deleted-column history. Folder-name claiming counts tombstones, so paths never reuse |
| Retention | Keep forever | Rows are small metadata; "Show archived" window bounds the UI |
Part 2: Board + filters (desktop)
Why
The page was a flat table with a substring search and two single-select dropdowns — no way to see work by state. Linear's board-is-a-layout model (researched against Linear's docs/Mobbin) fits: columns are derived state, filters are layout-independent, done items stay accessible without cluttering.
What / How
- Data layer first: the page now keeps the host's full 5-valued PR state (incl.
queued) instead of discarding it and re-deriving a worse 4-valued state from the cloud ElectricgithubPullRequestsjoin, which is removed (local-first; also fixes sessions never matching). Live agent status comes from one host-wideterminalAgents.listquery per host — never per workspace. Archived tombstones ride a dedicated query key inuseHostWorkspacesSourceso the shared live cache and its IndexedDB snapshots never see them. - Board: fixed workflow-ordered columns — Idle · Working · Needs attention · Needs review · Merged · Deleted — derived in a unit-tested pure util (
deriveBoardColumn, first match wins: archived reason → live merged PR → permission/failed → working → review/open PR → idle). Archived cards render ghosted with a reason badge and are non-clickable. No drag-and-drop in v1 (columns are derived state; nothing settable to drag to). "Show archived: none / past week / past month / all" (default past week) scopes tombstones. - Filters:
validateSearchon the route —q,device,projects(multi,__sessions__sentinel),pr,agent,view,archived. URL hydrates the store once per mount, then the store drives the URL (replace). Copying the URL reproduces the exact view, including board mode. DeletingWorkspacesProviderdeleted: the durable archive mark replaces the renderer-memory in-flight set (which other devices never saw anyway). The v2 row's delete dialog stays mounted via a mounted-once-opened latch so the teardown-failure force-retry still re-opens.
Manual QA Checklist (all verified via CDP on the dev app from this worktree, screenshots captured)
Tombstoning
- Destroy through the real pipeline archives with reason
deleted; worktree removed, row kept (verified in host.db) - Destroy of a workspace linked to a merged PR archives with reason
merged - Migration
0020applied cleanly to an existing dev host.db on boot - Archived rows absent from the list view and sidebar; live board gains the ghost card without a refresh (event-driven invalidation)
Board / filters
- List ↔ board toggle; URL carries
?view=board - All six columns render with correct counts against real data (82 workspaces)
- PR-state multi-select filters the board; URL becomes
?pr=merged&view=board - Full
location.reload()on a filtered URL restores board mode + active filters from the URL alone -
queuedPR state renders (amber) end-to-end now that host state is used
Not manually exercised (covered by tests / low-risk reasoning)
- Blocking-teardown failure un-archive (integration-tested; not reproduced in the live app)
- Startup reconciler resume (unit-tested selection logic; destroy path is the same code the live QA exercised)
- Remote-host board (single dev host available)
Testing
bun run typecheck— clean, all packagesbun run lint— exits 0packages/host-service:bun test— 1072 pass / 0 fail (new: archive semantics integration tests, merged-reason test, session tombstone tests, reconciler stranded-selection tests, commit-point failure tests)apps/desktop:bun test— 2630 pass / 0 fail (new:deriveBoardColumnprecedence tests)
Known Limitations
- The PR hover card no longer shows +/- line counts or "updated X ago" — those were cloud-only fields; the host store doesn't track diff stats. Can return if the host PR fetch starts carrying them.
- During the teardown-script window a workspace mid-delete looks normal in lists (no dimmed row anymore); it vanishes at the archive commit point seconds later. A second delete attempt gets the existing in-progress CONFLICT toast.
- Removing a project cascades away that project's tombstones (existing FK behavior, judged acceptable).
Follow-ups (deliberately out of scope)
- Drag-and-drop column semantics; board group-by picker (columns are fixed to status)
- Time-windowed "merged in last N days" for live merged workspaces using the new
mergedAt - Saved views / filter pills with operators (Linear-style)
Rollout / Rollback
- Migration is additive (two nullable columns + index, one nullable column); old builds ignore the new columns. Forward-only, no backfill needed.
- Rollback: revert the code; existing tombstone rows are inert under old code except they reappear as live workspaces in lists (old list has no archived filter) with missing worktrees — acceptable for a short rollback window, or clear with
delete from workspaces where archived_at is not null.
Summary by cubic
Soft-delete all workspaces (including sessions) in packages/host-service and add a Kanban board view with URL‑synced filters in apps/desktop. This update also tightens safety and UX: archived rows are fetched only for the board, navigation ignores in‑flight deletes, and the reconciler adds extra guards.
-
New Features
- Tombstoning: archive workspaces with
archivedAt/archiveReason("merged"or"deleted"); sessions also tombstone ("deleted"); PRs get stickymergedAt. - Safe cleanup: archive after teardown; failures un‑archive; startup reconciler finishes crash‑interrupted deletes and re‑checks live path ownership; refresh/update paths guard on
archivedAt IS NULL; project removal sweeps stranded tombstone worktrees; deletion telemetry fires at the true commit. - API/UI:
workspace.listexcludes tombstones by default (includeArchived: trueto opt in); creation/search/backfill/watchers skip archived; tombstones fetched under a separate query key; archived polling runs only in board view; archived rows report honesthostReachable. - Board view: fixed columns (Idle, Working, Needs attention, Needs review, Merged, Deleted); archived cards render ghosted with reason; shared workspace context menu reused by list rows and board cards (ghosts have no actions).
- Filters: multi‑select projects (incl. sessions), PR states (incl. queued), agent statuses, device, archived window; all URL‑synced with list↔board toggle; URL hydration applies only present params.
- Data: PR state and agent status sourced from the host; shared checks dot indicator; navigation/shortcuts exclude mid‑teardown rows via a module‑scope
deletingWorkspacesstore; removedDeletingWorkspacesProvider.
- Tombstoning: archive workspaces with
-
Migration
- Apply DB migration 0020 adding
workspaces.archived_at,workspaces.archive_reason, andpull_requests.merged_at(additive, forward‑only; no backfill).
- Apply DB migration 0020 adding
Summary by CodeRabbit
- New Features
- Added a Kanban board view for workspaces with workflow-based columns.
- Added list/board toggles, archived-workspace viewing, and URL-synchronized filters.
- Added multi-select filters for projects, pull-request states, and agent status.
- Added queued pull-request status indicators and richer workspace cards.
- Added shared workspace actions for opening, pinning, copying branches, and deleting.
- Bug Fixes
- Workspace deletion now preserves recoverable archive records and improves cleanup retry behavior.
- Archived workspaces are excluded from active workspace operations by default.