feat(desktop,host-service): local-first projects — host.db owns projects, workspace cloud sync retired

Merged
KI
4mo ago
+2588-1537

Links

  • Design doc: plans/20260716-local-first-projects.md (in this PR, includes the decision log)

Summary

  • v2 goes fully local. Projects live in host.db only (name + updatedAt columns, migration 0010): create, rename, and delete commit locally with zero cloud involvement, so they work fully offline. #5649's silent my-app-2 duplicates die structurally — nothing ever walks a cloud slug.
  • Workspace cloud dual-write retires (decision 5 in the doc): workspace-cloud-sync.ts, tombstones, re-keying, and the reconciler are deleted; ensureHostRegistered was folded away (host registration via tunnel/connect.ts is untouched — relay access keeps working).
  • Desktop reads projects from a host fan-out (useHostProjects: per-host project.list + project:changed live events + IndexedDB last-seen snapshots, workspace-pattern settings). Sidebar, project settings, and the new-workspace modal are migrated off Electric v2Projects.

How It Works

  • Host project.list/update serve full identity rows; project:changed broadcasts on the existing event bus (new workspace-client payload type). A startup backfill fills name from the legacy cloud row when reachable, else the folder basename — never blocks boot.
  • project.setup gets a local-first fast path: an existing local row completes without any cloud call; the legacy cloud lookup only runs when adopting an old cloud-created project onto a new device. findByPath is local-only (a hit = this host already has it; no hit = create fresh).
  • project.remove flips its kill point: local deletes always run (offline-capable); the legacy cloud delete is best-effort cleanup for the mixed-version window.
  • Renames commit through the host everywhere (settings NameSection, sidebar inline rename) — the optimistic cloud rename is gone.

Manual QA — CDP-verified against the live dev app

Fresh dev host.db with 29 legacy projects; drills driven over CDP:

  • Migration 0010 + name backfill on boot: 28 rows filled from legacy cloud names; a transient-error row correctly kept its empty-name sentinel and filled on the next boot (retry-next-boot rule proven)
  • Create from folder: 171 ms local commit while fully online; cloud v2Project.get for the new id returns NOT_FOUND (no cloud row minted)
  • Full offline lifecycle with the API server SIGSTOP'd + navigator.onLine=false: create 179 ms, rename 18 ms, workspace+worktree create 379 ms, sidebar auto-updated in 101 ms — nothing hung, proving no hidden cloud awaits anywhere in the path
  • Rename → project:changed → sidebar DOM update in 130 ms (no refetch)
  • Settings: project list + detail render from host fan-out; NameSection rename through the real UI commits to host.db; repository field read-only ("No git remote detected")
  • Delete ×3: ~100 ms each, local rows + sidebar cleared; best-effort legacy cloud delete correctly logged "failed; continuing locally"
  • Zero cloud push failed / reconciler lines across the entire session log

Testing

  • bun test in packages/host-service — 885 tests green (3 tests asserting the retired cloud behavior updated to the new local contracts)
  • bunx tsc --noEmit in packages/host-service, packages/workspace-client, apps/desktop; bun run lint clean

Known Limitations / conscious trade-offs

  • Mobile loses workspace visibility until the future cloud layer (team cloud projects w/ sandboxes) returns — accepted in the decision walkthrough.
  • ~15 secondary desktop surfaces (task filters, command palette, history) still read the frozen Electric v2Projects collection; migrating them + the Electric v2Workspaces merge source is follow-up work before the cloud tables retire.
  • Custom project icons (cloud uploadIcon) are retired with the cloud row; thumbnails derive from the GitHub owner avatar. Repository URL in settings is read-only (derived from the git remote).

Rollout

  • Desktop-only in substance; no cloud/API deploy needed (cloud endpoints stay up for old clients). Nothing destructive; host sqlite migration 0010 is additive.

Summary by CodeRabbit

  • New Features

    • Projects are now managed locally on the host (create, update, rename, delete).
    • The project list aggregates results from available hosts, uses cached snapshots when hosts are unreachable, and stays in sync across devices in real time.
    • Deleting a project can be applied across reachable hosts, with warnings for unreachable ones.
    • Project settings now source and display host-based project/repo details.
  • Bug Fixes

    • Improved readiness gating to avoid flashing incomplete project settings.
    • Added one-time backfill for existing project names and timestamps.