feat(desktop,host-service): tombstone workspace deletes + kanban board view for the workspaces page

Merged
KI
3mo ago
+3606-903

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:

  1. 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).
  2. 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

  • workspaces gains archivedAt/archiveReason; pullRequests gains a sticky mergedAt (migration 0020, 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 existing deleted event 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.list excludes archived rows by default; includeArchived: true opts in. Adopt-existing-worktree, branch search, worktree re-import, name backfill, the git watcher, and all PR refresh sweeps skip tombstones; getPullRequestsByWorkspaces deliberately 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

DecisionChoiceRationale
StorageSoft-delete in place, consumers updatedSingle source of truth; no unique-index collisions exist (worktreePath/branch are unindexed; one-main-per-project can't archive)
Commit pointAfter teardown, before physical cleanupBlocking teardown failure re-opens the delete dialog for force-retry; archiving first would unmount it mid-prompt
SessionsTombstone 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
RetentionKeep foreverRows 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 Electric githubPullRequests join, which is removed (local-first; also fixes sessions never matching). Live agent status comes from one host-wide terminalAgents.list query per host — never per workspace. Archived tombstones ride a dedicated query key in useHostWorkspacesSource so 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: validateSearch on 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.
  • DeletingWorkspacesProvider deleted: 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 0020 applied 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
  • queued PR 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 packages
  • bun run lint — exits 0
  • packages/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: deriveBoardColumn precedence 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 sticky mergedAt.
    • 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.list excludes tombstones by default (includeArchived: true to 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 honest hostReachable.
    • 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 deletingWorkspaces store; removed DeletingWorkspacesProvider.
  • Migration

    • Apply DB migration 0020 adding workspaces.archived_at, workspaces.archive_reason, and pull_requests.merged_at (additive, forward‑only; no backfill).

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.