Repository navigation
feat(desktop,host-service): local-first projects — host.db owns projects, workspace cloud sync retired - #5731
Conversation
|
Ready to review this PR? Stage has broken it down into 8 individual chapters for you: Chapters generated by Stage for commit af5a4dd on Jul 19, 2026 8:13pm UTC. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughLocal-first project ownership is implemented across host persistence, project lifecycle APIs, workspace synchronization, realtime events, cached host fan-out, and desktop project surfaces. Cloud writes are removed from new local flows, while legacy cloud cleanup remains best-effort. ChangesLocal-first project ownership
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@plans/20260716-local-first-projects.md`:
- Around line 30-35: Update the host startup lifecycle in app.ts to remove or
disable the runWorkspaceBackfill, runMainWorkspaceSweep, and
startWorkspaceCloudSync call sites so no retired workspace sync or backfill can
perform cloud writes during the mixed-version window. Remove any now-unused
imports or startup wiring while preserving the remaining host initialization
flow.
- Around line 20-22: The project name backfill described in the Host schema
migration must not finalize a folder-basename fallback when the cloud row is
unreachable. Persist the backfill as incomplete and retry it on a later
connected startup, or define and consistently apply one authoritative precedence
so the resulting name is independent of transient connectivity.
- Around line 37-41: Update the cloud-freeze plan to require concrete
replacement migrations for every listed cloud-row consumer—mobile
home/filter/new-chat screens, the automations router, task-to-workspace
dispatch, and web workspace views—before the freeze is enabled. Define the local
or relay-backed data source and behavior for each consumer, and mark the freeze
as blocked until all migrations are completed; retain mobile workspace
visibility loss only as the explicitly accepted trade-off.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ec55ce72-f765-4507-b2f3-65f1aa20ebad
📒 Files selected for processing (1)
plans/20260716-local-first-projects.md
| - **Host schema**: `projects` gains `name`, `updatedAt` (drizzle-kit). | ||
| One-time backfill: name from cloud row if reachable, else folder basename; | ||
| never blocks startup. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Make the name backfill deterministic and retryable.
If startup cannot reach the cloud, this one-time backfill permanently chooses the folder basename; a later connected startup will then retain a different name than the cloud row. Persist the fallback as incomplete or define a single authoritative precedence so project names do not depend on transient network state.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plans/20260716-local-first-projects.md` around lines 20 - 22, The project
name backfill described in the Host schema migration must not finalize a
folder-basename fallback when the cloud row is unreachable. Persist the backfill
as incomplete and retry it on a later connected startup, or define and
consistently apply one authoritative precedence so the resulting name is
independent of transient connectivity.
| - **Workspace sync retires**: `workspace-cloud-sync.ts` + backfill stop | ||
| pushing; Electric `v2Workspaces`/`v2Projects` merge sources removed from | ||
| desktop. Local `cloudSyncedAt`-style fields don't get added at all. | ||
| - **Cloud tables freeze**: `v2_projects` / `v2_workspaces` and their | ||
| endpoints stay up for old clients during the mixed-version window, take no | ||
| new writes from new clients, and are retired on adoption telemetry. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Include the host startup lifecycle in the sync-retirement change.
The supplied packages/host-service/src/app.ts still invokes runWorkspaceBackfill, runMainWorkspaceSweep, and startWorkspaceCloudSync. Unless those call sites are removed or made provably non-writing, the retired sync can continue creating cloud writes during the mixed-version window.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plans/20260716-local-first-projects.md` around lines 30 - 35, Update the host
startup lifecycle in app.ts to remove or disable the runWorkspaceBackfill,
runMainWorkspaceSweep, and startWorkspaceCloudSync call sites so no retired
workspace sync or backfill can perform cloud writes during the mixed-version
window. Remove any now-unused imports or startup wiring while preserving the
remaining host initialization flow.
| Consumers currently reading cloud rows that need a migration story before the | ||
| freeze bites (audit before build): mobile home/filter/new-chat screens, | ||
| automations router, task→workspace dispatch, web workspace views. Mobile | ||
| loses workspace visibility until either relay-backed host fan-out or the | ||
| future cloud layer — accepted trade-off of this decision. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Gate the cloud freeze on concrete migrations for every listed consumer.
The document freezes cloud tables while acknowledging that automations, task dispatch, and web workspace views still read cloud rows. New local writes will not reach those consumers, so they can become stale or nonfunctional even if mobile visibility loss is accepted. Define and complete each consumer’s replacement behavior before enabling the freeze.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plans/20260716-local-first-projects.md` around lines 37 - 41, Update the
cloud-freeze plan to require concrete replacement migrations for every listed
cloud-row consumer—mobile home/filter/new-chat screens, the automations router,
task-to-workspace dispatch, and web workspace views—before the freeze is
enabled. Define the local or relay-backed data source and behavior for each
consumer, and mark the freeze as blocked until all migrations are completed;
retain mobile workspace visibility loss only as the explicitly accepted
trade-off.
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
There was a problem hiding this comment.
3 issues found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="plans/20260716-local-first-projects.md">
<violation number="1" location="plans/20260716-local-first-projects.md:21">
P2: The one-time backfill as described is non-deterministic: if the first startup after migration happens offline, the project permanently gets the folder basename; if online, it gets the cloud name. This means the same user on the same machine gets a different project name depending on transient network state at upgrade time. Consider either marking the offline-derived name as provisional (re-attempted on next connected startup) or defining a single authoritative source (e.g., always folder basename, ignoring cloud) so the result is idempotent regardless of connectivity.</violation>
<violation number="2" location="plans/20260716-local-first-projects.md:24">
P1: New local-only projects cannot be targeted by the current automation and cloud workspace flows because those routers still require a matching `v2_projects` row. The plan should define a relay/host lookup or an explicit deferred/disabled behavior and acceptance for these consumers before freezing the cloud tables, rather than leaving the migration as an audit item.</violation>
<violation number="3" location="plans/20260716-local-first-projects.md:30">
P2: The retirement plan covers `workspace-cloud-sync.ts` and the Electric merge sources, but doesn't explicitly address the host startup lifecycle (`app.ts`) which currently invokes `runWorkspaceBackfill`, `runMainWorkspaceSweep`, and `startWorkspaceCloudSync`. If those call sites aren't removed or gated, the retired sync can still produce cloud writes during the mixed-version window. Consider adding the `app.ts` call-site removal to the "Now" checklist.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as Desktop Client
participant HostDB as host.db (SQLite)
participant HostRouter as Host Router
participant Renderer as Renderer (Sidebar/Settings)
participant Electric as Electric SQL (Cloud)
participant CloudDB as Cloud DB (v2_projects)
Note over Client,CloudDB: CURRENT ARCHITECTURE (pre-v2 local-first)
Client->>HostDB: persistFromResolved() - create project
HostDB->>CloudDB: blocking cloud create + rollback
CloudDB-->>HostDB: cloud row created
HostDB-->>Client: project created (dual-write)
Client->>Electric: subscribe v2Projects (workspace mode)
Electric->>CloudDB: query cloud projects
CloudDB-->>Electric: project data
Electric-->>Renderer: project list (Electric source)
alt #5649 occurs
Client->>HostDB: create project offline/partial
HostDB->>CloudDB: cloud create fails or duplicates
CloudDB-->>Client: my-app-2 duplicate
end
Renderer->>HostRouter: project.list (current bus)
HostRouter->>HostDB: local query
HostDB-->>HostRouter: local projects
HostRouter-->>Renderer: local projects (for fallback)
Note over Client,CloudDB: Mobile/automations still read cloud rows directly
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| One-time backfill: name from cloud row if reachable, else folder basename; | ||
| never blocks startup. | ||
| - **Create path**: `persistFromResolved` drops the blocking cloud create + | ||
| rollback — local insert is the commit point. Import = local create, always. |
There was a problem hiding this comment.
P1: New local-only projects cannot be targeted by the current automation and cloud workspace flows because those routers still require a matching v2_projects row. The plan should define a relay/host lookup or an explicit deferred/disabled behavior and acceptance for these consumers before freezing the cloud tables, rather than leaving the migration as an audit item.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plans/20260716-local-first-projects.md, line 24:
<comment>New local-only projects cannot be targeted by the current automation and cloud workspace flows because those routers still require a matching `v2_projects` row. The plan should define a relay/host lookup or an explicit deferred/disabled behavior and acceptance for these consumers before freezing the cloud tables, rather than leaving the migration as an audit item.</comment>
<file context>
@@ -0,0 +1,79 @@
+ One-time backfill: name from cloud row if reachable, else folder basename;
+ never blocks startup.
+- **Create path**: `persistFromResolved` drops the blocking cloud create +
+ rollback — local insert is the commit point. Import = local create, always.
+- **Host router**: `project.list` / `project.update` / `project:changed` on
+ the existing bus. Bare `protectedProcedure`; relay stays the authz boundary.
</file context>
| - **Renderer**: `useHostProjects` fan-out (workspace pattern: `networkMode: | ||
| "always"`, event-patched cache, 30s heal). Sidebar + settings move off | ||
| Electric `v2Projects`. | ||
| - **Workspace sync retires**: `workspace-cloud-sync.ts` + backfill stop |
There was a problem hiding this comment.
P2: The retirement plan covers workspace-cloud-sync.ts and the Electric merge sources, but doesn't explicitly address the host startup lifecycle (app.ts) which currently invokes runWorkspaceBackfill, runMainWorkspaceSweep, and startWorkspaceCloudSync. If those call sites aren't removed or gated, the retired sync can still produce cloud writes during the mixed-version window. Consider adding the app.ts call-site removal to the "Now" checklist.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plans/20260716-local-first-projects.md, line 30:
<comment>The retirement plan covers `workspace-cloud-sync.ts` and the Electric merge sources, but doesn't explicitly address the host startup lifecycle (`app.ts`) which currently invokes `runWorkspaceBackfill`, `runMainWorkspaceSweep`, and `startWorkspaceCloudSync`. If those call sites aren't removed or gated, the retired sync can still produce cloud writes during the mixed-version window. Consider adding the `app.ts` call-site removal to the "Now" checklist.</comment>
<file context>
@@ -0,0 +1,79 @@
+- **Renderer**: `useHostProjects` fan-out (workspace pattern: `networkMode:
+ "always"`, event-patched cache, 30s heal). Sidebar + settings move off
+ Electric `v2Projects`.
+- **Workspace sync retires**: `workspace-cloud-sync.ts` + backfill stop
+ pushing; Electric `v2Workspaces`/`v2Projects` merge sources removed from
+ desktop. Local `cloudSyncedAt`-style fields don't get added at all.
</file context>
| ## Now | ||
|
|
||
| - **Host schema**: `projects` gains `name`, `updatedAt` (drizzle-kit). | ||
| One-time backfill: name from cloud row if reachable, else folder basename; |
There was a problem hiding this comment.
P2: The one-time backfill as described is non-deterministic: if the first startup after migration happens offline, the project permanently gets the folder basename; if online, it gets the cloud name. This means the same user on the same machine gets a different project name depending on transient network state at upgrade time. Consider either marking the offline-derived name as provisional (re-attempted on next connected startup) or defining a single authoritative source (e.g., always folder basename, ignoring cloud) so the result is idempotent regardless of connectivity.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plans/20260716-local-first-projects.md, line 21:
<comment>The one-time backfill as described is non-deterministic: if the first startup after migration happens offline, the project permanently gets the folder basename; if online, it gets the cloud name. This means the same user on the same machine gets a different project name depending on transient network state at upgrade time. Consider either marking the offline-derived name as provisional (re-attempted on next connected startup) or defining a single authoritative source (e.g., always folder basename, ignoring cloud) so the result is idempotent regardless of connectivity.</comment>
<file context>
@@ -0,0 +1,79 @@
+## Now
+
+- **Host schema**: `projects` gains `name`, `updatedAt` (drizzle-kit).
+ One-time backfill: name from cloud row if reachable, else folder basename;
+ never blocks startup.
+- **Create path**: `persistFromResolved` drops the blocking cloud create +
</file context>
… local create/rename/delete, retire workspace cloud sync
…igrate sidebar, settings, new-workspace modal off Electric v2Projects
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx (1)
25-41: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winWait for host-project readiness before falling back to v1.
An empty
v2Matchwhile fan-out is pending does not prove this is a v1 project, so the page can render the wrong settings component. Returnnullwhen there are no cached matches andisReadyis false.As per coding guidelines, “Use readiness only when there is no data: show loading/skeleton/null when not ready, and empty/not-found when ready.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/renderer/routes/_authenticated/settings/projects/`$projectId/page.tsx around lines 25 - 41, Use the readiness state from useHostProjects in the routing decision: when v2Match is empty and host-project data is not ready, return null instead of falling back to ProjectSettings. Preserve the existing V2ProjectSettings path for matches and the v1 fallback once readiness is true.Source: Coding guidelines
packages/host-service/src/trpc/router/project/handlers.ts (1)
20-31: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winReject
.and..as generated directory names.Both values survive this sanitizer but resolve to the selected directory or its parent rather than a new child project directory. Explicitly reject these path-navigation components.
Proposed fix
- if (!slug) { + if (!slug || slug === "." || slug === "..") { throw new TRPCError({ code: "BAD_REQUEST", message: "Project name must produce a non-empty directory name",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/host-service/src/trpc/router/project/handlers.ts` around lines 20 - 31, Update dirNameForEmpty to reject generated slugs equal to "." or ".." in addition to empty slugs, throwing the existing BAD_REQUEST TRPCError before returning the directory name. Preserve the current sanitization behavior for all other valid names.packages/host-service/src/trpc/router/project/project.ts (1)
698-713: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftDo not report success when the authoritative local deletion fails.
The catch logs any workspace/project deletion failure, but Line 713 still returns success. This can leave a partially deleted project while consumers assume removal completed. Make the local deletes atomic, emit events after commit, and propagate failure to the caller.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/host-service/src/trpc/router/project/project.ts` around lines 698 - 713, Update the local deletion flow in the project removal handler around deleteLocalWorkspace and the projects delete so all deletes execute atomically in a transaction, with change events emitted only after the transaction commits. If the transaction fails, propagate the error to the caller instead of logging and returning success; retain the success response only after the authoritative local deletion completes.
🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/components/NameSection/NameSection.tsx (1)
33-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShow a user-visible error toast on rename failure.
When a project rename fails, resetting the input and logging a warning to the console does not inform the user of the failure. Consider displaying a toast notification, similar to how failures are handled in the sidebar's rename action.
♻️ Proposed refactor
First, add the import at the top of the file:
+import { toast } from "`@superset/ui/sonner`";Then, update the
catchblock to display the toast:void getHostServiceClientByUrl(hostUrl) .project.update.mutate({ projectId, name: trimmed }) .catch((err) => { console.warn("[project-rename] host commit failed", err); + toast.error(`Rename failed: ${err instanceof Error ? err.message : String(err)}`); setValue(currentName); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/`$projectId/components/V2ProjectSettings/components/NameSection/NameSection.tsx around lines 33 - 43, Update the rename failure handling in NameSection’s project.update.mutate catch block to display the existing user-facing toast used by the sidebar rename action, while preserving the current input reset and warning log behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts`:
- Around line 128-139: Update the host-projects cache update around setQueryData
and applyProjectChangedEvent so deletion events are handled when rows is
undefined. Remove the deleted project from the persisted snapshot or
invalidate/refetch the host-projects query, ensuring stale snapshot data cannot
restore it.
- Around line 49-57: Gate the v2/v1 settings split on host-project readiness: in
apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts:49-57,
preserve the useLiveQuery host lookup while ensuring its completion drives
isReady; in
apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts:171-177,
keep that readiness exposed by useHostProjects; and in
apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx:25-41,
use useHostProjects().isReady before selecting the v2 settings page, falling
back only after discovery is ready.
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx`:
- Around line 31-40: Preserve the preferred serving-host identity through both
project navigation paths. In ProjectsSettingsSidebar.tsx lines 31-40, add the
preferred hostId to each v2 project row; at lines 81-85, pass it in the link
search parameters. In page.tsx lines 17-24, retain hostId in the mapped project;
at lines 44-50, include it in automatic-navigation search parameters.
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/`$projectId/components/V2ProjectSettings/V2ProjectSettings.tsx:
- Around line 141-150: Update the host-specific settings around NameSection and
RepositorySection to use hostProject.name and hostProject.repoUrl instead of the
merged project fields. Keep these values scoped strictly to targetHostUrl, with
no cross-host fallback for rename or repository setup operations.
In `@packages/host-service/drizzle/meta/0010_snapshot.json`:
- Around line 329-336: Update the projects repo-path index definition in the
Drizzle schema to enforce uniqueness for repo_path, add a migration that safely
handles or removes existing duplicate rows before applying the constraint, and
regenerate the 0010_snapshot metadata so projects_repo_path_idx is marked
unique.
In `@packages/host-service/src/projects/local-project-store.ts`:
- Around line 14-24: Update toProjectSnapshot so the fallback project name uses
the platform path helper basename(row.repoPath) instead of manually splitting on
"/"; preserve the existing row.name and row.id fallback order.
In `@packages/host-service/src/runtime/project-backfill.ts`:
- Around line 41-73: Make the update in the pending-project sweep conditional on
both projects.id matching row.id and projects.name still being empty, preventing
stale cloud data from overwriting a concurrent rename. Use the conditional
update result to emit the “updated” event and increment filled only when a row
was actually changed; retain the existing lookup and fallback behavior.
In `@packages/host-service/src/trpc/router/project/project.ts`:
- Around line 658-670: Update the project removal flow around the local project
lookup and legacy cloud deletion so it loads the local row first and returns
immediately when no local project exists. Only invoke
ctx.api.v2Project.delete.mutate and set _cloudDeleted after confirming local
ownership, while preserving the existing local removal behavior for present
projects.
---
Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/projects/`$projectId/page.tsx:
- Around line 25-41: Use the readiness state from useHostProjects in the routing
decision: when v2Match is empty and host-project data is not ready, return null
instead of falling back to ProjectSettings. Preserve the existing
V2ProjectSettings path for matches and the v1 fallback once readiness is true.
In `@packages/host-service/src/trpc/router/project/handlers.ts`:
- Around line 20-31: Update dirNameForEmpty to reject generated slugs equal to
"." or ".." in addition to empty slugs, throwing the existing BAD_REQUEST
TRPCError before returning the directory name. Preserve the current sanitization
behavior for all other valid names.
In `@packages/host-service/src/trpc/router/project/project.ts`:
- Around line 698-713: Update the local deletion flow in the project removal
handler around deleteLocalWorkspace and the projects delete so all deletes
execute atomically in a transaction, with change events emitted only after the
transaction commits. If the transaction fails, propagate the error to the caller
instead of logging and returning success; retain the success response only after
the authoritative local deletion completes.
---
Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/`$projectId/components/V2ProjectSettings/components/NameSection/NameSection.tsx:
- Around line 33-43: Update the rename failure handling in NameSection’s
project.update.mutate catch block to display the existing user-facing toast used
by the sidebar rename action, while preserving the current input reset and
warning log behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 95d3a90d-be87-457c-b53c-7643bf6c476f
📒 Files selected for processing (44)
apps/desktop/src/renderer/hooks/host-projects/useHostProjects/index.tsapps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.tsapps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.utils.tsapps/desktop/src/renderer/hooks/useEnsureV2Project/useEnsureV2Project.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/buildDashboardSidebarProjects.test.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/buildDashboardSidebarProjects.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.tsapps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsxapps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsxapps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsxapps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsxapps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/V2ProjectSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/components/NameSection/NameSection.tsxapps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/components/RepositorySection/RepositorySection.tsxpackages/host-service/drizzle/0010_project_identity_fields.sqlpackages/host-service/drizzle/meta/0010_snapshot.jsonpackages/host-service/drizzle/meta/_journal.jsonpackages/host-service/src/app.tspackages/host-service/src/db/schema.tspackages/host-service/src/events/event-bus.tspackages/host-service/src/events/types.tspackages/host-service/src/projects/local-project-store.tspackages/host-service/src/runtime/project-backfill.tspackages/host-service/src/runtime/workspace-backfill.tspackages/host-service/src/runtime/workspace-cloud-sync.tspackages/host-service/src/trpc/router/project/handlers.tspackages/host-service/src/trpc/router/project/project.tspackages/host-service/src/trpc/router/project/utils/ensure-main-workspace.tspackages/host-service/src/trpc/router/project/utils/persist-project.tspackages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.tspackages/host-service/src/trpc/router/workspace-creation/procedures/adopt.tspackages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.tspackages/host-service/src/trpc/router/workspace-creation/utils/ai-workspace-names.tspackages/host-service/src/trpc/router/workspace/workspace.tspackages/host-service/src/trpc/router/workspaces/workspaces.tspackages/host-service/src/workspaces/local-workspace-store.tspackages/host-service/test/integration/project.integration.test.tspackages/host-service/test/integration/workspace-cleanup.integration.test.tspackages/host-service/test/integration/workspace-create-pr.integration.test.tspackages/host-service/test/workspace-cloud-sync.test.tspackages/workspace-client/src/index.tspackages/workspace-client/src/lib/eventBus.ts
💤 Files with no reviewable changes (8)
- packages/host-service/test/workspace-cloud-sync.test.ts
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/buildDashboardSidebarProjects.test.ts
- packages/host-service/src/runtime/workspace-backfill.ts
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts
- packages/host-service/src/runtime/workspace-cloud-sync.ts
- packages/host-service/src/trpc/router/workspace/workspace.ts
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/buildDashboardSidebarProjects.ts
- packages/host-service/src/trpc/router/workspace-creation/procedures/adopt.ts
| const { data: hosts = [] } = useLiveQuery( | ||
| (q) => | ||
| q.from({ hosts: collections.v2Hosts }).select(({ hosts }) => ({ | ||
| organizationId: hosts.organizationId, | ||
| machineId: hosts.machineId, | ||
| isOnline: hosts.isOnline, | ||
| })), | ||
| [collections], | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== useHostProjects.ts ==\n'
sed -n '1,260p' apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts
printf '\n== project settings page ==\n'
sed -n '1,220p' apps/desktop/src/renderer/routes/_authenticated/settings/projects/\$projectId/page.tsx
printf '\n== related host project usages ==\n'
rg -n "useHostProjects\\(|isReady|v2Match|ProjectSettings|V2ProjectSettings|hostUrl === null|snapshots.has" apps/desktop/src/renderer -g '!**/dist/**'Repository: superset-sh/superset
Length of output: 22955
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,260p' apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts
sed -n '1,220p' apps/desktop/src/renderer/routes/_authenticated/settings/projects/\$projectId/page.tsxRepository: superset-sh/superset
Length of output: 7137
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== useHostProjects.ts ==\n'
cat -n apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts | sed -n '1,260p'
printf '\n== page.tsx ==\n'
cat -n apps/desktop/src/renderer/routes/_authenticated/settings/projects/\$projectId/page.tsx | sed -n '1,220p'Repository: superset-sh/superset
Length of output: 8733
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "export function useHostProjects|const isReady =|useLiveQuery\\(" apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts apps/desktop/src/renderer/routes/_authenticated/settings/projects/\$projectId/page.tsxRepository: superset-sh/superset
Length of output: 546
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== useHostProjects.utils.ts ==\n'
cat -n apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.utils.ts | sed -n '1,260p'
printf '\n== settings/projects/page.tsx ==\n'
cat -n apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx | sed -n '1,220p'
printf '\n== V2ProjectSettings.tsx ==\n'
cat -n apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/\$projectId/components/V2ProjectSettings/V2ProjectSettings.tsx | sed -n '1,220p'Repository: superset-sh/superset
Length of output: 19755
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat -n apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.utils.ts | sed -n '1,260p'
echo '---'
cat -n apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx | sed -n '1,220p'
echo '---'
cat -n apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/\$projectId/components/V2ProjectSettings/V2ProjectSettings.tsx | sed -n '1,220p'Repository: superset-sh/superset
Length of output: 19668
Gate the v2/v1 split on host-project readiness. apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx#L25-L41 can fall back to <ProjectSettings /> before host discovery finishes, so a v2 project may render the wrong settings page on first load. Use useHostProjects().isReady here, and keep that readiness flowing from the useLiveQuery host lookup in apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts#L49-L57, L171-L177.
📍 Affects 2 files
apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts#L49-L57(this comment)apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts#L171-L177apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx#L25-L41
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts`
around lines 49 - 57, Gate the v2/v1 settings split on host-project readiness:
in
apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts:49-57,
preserve the useLiveQuery host lookup while ensuring its completion drives
isReady; in
apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.ts:171-177,
keep that readiness exposed by useHostProjects; and in
apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx:25-41,
use useHostProjects().isReady before selecting the v2 settings page, falling
back only after discovery is ready.
Source: Coding guidelines
| const v2Projects = useMemo( | ||
| () => | ||
| hostProjects.map((project) => ({ | ||
| id: project.projectKey, | ||
| name: project.name, | ||
| iconUrl: project.repoOwner | ||
| ? `https://github.com/${project.repoOwner}.png?size=64` | ||
| : null, | ||
| })), | ||
| [hostProjects], |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Keep serving-host identity when navigating to local-first projects. Both navigation paths reduce HostProjectItem to project identity, causing remote-only projects to open against the local host.
apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx#L31-L40: retain a preferredhostIdin each v2 row.apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx#L81-L85: include that host in the link search parameters.apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx#L17-L24: retain a preferredhostIdin the mapped project.apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx#L44-L50: include that host in automatic navigation.
📍 Affects 2 files
apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx#L31-L40(this comment)apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx#L81-L85apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx#L17-L24apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx#L44-L50
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx`
around lines 31 - 40, Preserve the preferred serving-host identity through both
project navigation paths. In ProjectsSettingsSidebar.tsx lines 31-40, add the
preferred hostId to each v2 project row; at lines 81-85, pass it in the link
search parameters. In page.tsx lines 17-24, retain hostId in the mapped project;
at lines 44-50, include it in automatic-navigation search parameters.
| "indexes": { | ||
| "projects_repo_path_idx": { | ||
| "name": "projects_repo_path_idx", | ||
| "columns": [ | ||
| "repo_path" | ||
| ], | ||
| "isUnique": false | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Enforce one local project per repository path in SQLite.
This index remains non-unique, although project.setup assumes each on-disk path maps to at most one project. Concurrent imports can both pass application checks and persist duplicate projects. Add a unique constraint with migration handling for existing duplicates, then regenerate this snapshot.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/host-service/drizzle/meta/0010_snapshot.json` around lines 329 -
336, Update the projects repo-path index definition in the Drizzle schema to
enforce uniqueness for repo_path, add a migration that safely handles or removes
existing duplicate rows before applying the constraint, and regenerate the
0010_snapshot metadata so projects_repo_path_idx is marked unique.
There was a problem hiding this comment.
7 issues found across 44 files (changes from recent commits).
You’re at about 90% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/host-service/src/db/schema.ts">
<violation number="1" location="packages/host-service/src/db/schema.ts:75">
P2: The startup backfill contradicts this comment's “single indexed query” claim: every host boot scans the entire `projects` table for `name = ''`. Adding an index on `projects.name` (or changing the backfill strategy) keeps this migration from becoming an avoidable full-table scan.</violation>
<violation number="2" location="packages/host-service/src/db/schema.ts:77">
P2: `updatedAt` does not actually track all writes to a project row: the existing `setWorktreeBaseDir` and `setBranchPrefix` mutations leave it unchanged. That makes the new timestamp stale for consumers that use it to order or detect local project changes; routing these mutations through the timestamped project store or setting `updatedAt: Date.now()` here would keep the field consistent.</violation>
</file>
<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts">
<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts:23">
P2: `useHostProjects()` returns an `isReady` flag, but it's not used here. Before host discovery finishes, `hostProjects` is empty, so `v2Match` resolves to `[]` — causing a v2 project to briefly render the legacy `<ProjectSettings />` component on first load. Destructure and check `isReady` before falling back, or return a loading state until host projects are resolved.</violation>
</file>
<file name="apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx">
<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx:30">
P2: Clicking a project that exists only on a remote host opens its settings against the local host, showing “Not set up on this device” and disabling host actions until the user manually switches hosts. Since this hook now supplies projects from every host, retaining a serving host ID in each row and passing it as `search.hostId` would preserve the selected project's origin.</violation>
</file>
<file name="packages/host-service/src/trpc/router/project/project.ts">
<violation number="1" location="packages/host-service/src/trpc/router/project/project.ts:702">
P2: When the legacy cloud delete fails, removing each local workspace through this call does not record its cloud ID for retry, so old cloud workspace mirrors can remain orphaned permanently. The delete path could enqueue tombstones when `_cloudDeleted` is false, or remove the legacy cleanup contract and its misleading retry documentation.</violation>
</file>
<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts">
<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts:185">
P3: Sidebar project metadata is now duplicated from `DashboardNewWorkspaceModalContent` instead of using a shared mapper, including the GitHub avatar URL construction. A shared host-project presentation mapper would keep the sidebar and project picker consistent as these fields evolve.</violation>
</file>
<file name="packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts">
<violation number="1" location="packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts:341">
P2: Successful deletes now leave a cloud-delete tombstone behind: `deleteLocalWorkspace(...)` still queues one by default, but the success path no longer clears it. Keeping the clear after a successful legacy cleanup (or explicitly disabling tombstone queuing for this local-only path) prevents the reconciler from repeatedly deleting an already-deleted cloud row.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| })), | ||
| [collections, activeOrganizationId], | ||
| // Projects are fully local — identity comes from the host fan-out. | ||
| const { projects: hostProjects } = useHostProjects(); |
There was a problem hiding this comment.
P2: Clicking a project that exists only on a remote host opens its settings against the local host, showing “Not set up on this device” and disabling host actions until the user manually switches hosts. Since this hook now supplies projects from every host, retaining a serving host ID in each row and passing it as search.hostId would preserve the selected project's origin.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx, line 30:
<comment>Clicking a project that exists only on a remote host opens its settings against the local host, showing “Not set up on this device” and disabling host actions until the user manually switches hosts. Since this hook now supplies projects from every host, retaining a serving host ID in each row and passing it as `search.hostId` would preserve the selected project's origin.</comment>
<file context>
@@ -28,29 +23,21 @@ interface ProjectsSettingsSidebarProps {
- })),
- [collections, activeOrganizationId],
+ // Projects are fully local — identity comes from the host fan-out.
+ const { projects: hostProjects } = useHostProjects();
+ const v2Projects = useMemo(
+ () =>
</file context>
| // tombstones the id. The cloud delete is a best-effort mirror push — | ||
| // unreachable cloud means the reconciler replays the tombstone later. | ||
| // ─── Step 3: Local delete (authoritative) ───────────────────── | ||
| // The local row is the commit point and the only record. The cloud |
There was a problem hiding this comment.
P2: Successful deletes now leave a cloud-delete tombstone behind: deleteLocalWorkspace(...) still queues one by default, but the success path no longer clears it. Keeping the clear after a successful legacy cleanup (or explicitly disabling tombstone queuing for this local-only path) prevents the reconciler from repeatedly deleting an already-deleted cloud row.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts, line 341:
<comment>Successful deletes now leave a cloud-delete tombstone behind: `deleteLocalWorkspace(...)` still queues one by default, but the success path no longer clears it. Keeping the clear after a successful legacy cleanup (or explicitly disabling tombstone queuing for this local-only path) prevents the reconciler from repeatedly deleting an already-deleted cloud row.</comment>
<file context>
@@ -340,18 +337,17 @@ async function runDestroy(
- // tombstones the id. The cloud delete is a best-effort mirror push —
- // unreachable cloud means the reconciler replays the tombstone later.
+ // ─── Step 3: Local delete (authoritative) ─────────────────────
+ // The local row is the commit point and the only record. The cloud
+ // delete is best-effort legacy cleanup for rows mirrored before
+ // workspaces went fully local.
</file context>
| const navigate = useNavigate(); | ||
| const { v2Projects: projectActions } = useOptimisticCollectionActions(); | ||
| // Renames commit on a host serving the project — host.db owns the name. | ||
| const { projects: hostProjects } = useHostProjects(); |
There was a problem hiding this comment.
P2: useHostProjects() returns an isReady flag, but it's not used here. Before host discovery finishes, hostProjects is empty, so v2Match resolves to [] — causing a v2 project to briefly render the legacy <ProjectSettings /> component on first load. Destructure and check isReady before falling back, or return a loading state until host projects are resolved.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts, line 23:
<comment>`useHostProjects()` returns an `isReady` flag, but it's not used here. Before host discovery finishes, `hostProjects` is empty, so `v2Match` resolves to `[]` — causing a v2 project to briefly render the legacy `<ProjectSettings />` component on first load. Destructure and check `isReady` before falling back, or return a loading state until host projects are resolved.</comment>
<file context>
@@ -17,7 +19,17 @@ export function useDashboardSidebarProjectSectionActions({
const navigate = useNavigate();
- const { v2Projects: projectActions } = useOptimisticCollectionActions();
+ // Renames commit on a host serving the project — host.db owns the name.
+ const { projects: hostProjects } = useHostProjects();
+ const servingHostId = useMemo(
+ () =>
</file context>
| name: project.name, | ||
| githubOwner: project.repoOwner, | ||
| githubRepoName: project.repoName, | ||
| iconUrl: project.repoOwner |
There was a problem hiding this comment.
P3: Sidebar project metadata is now duplicated from DashboardNewWorkspaceModalContent instead of using a shared mapper, including the GitHub avatar URL construction. A shared host-project presentation mapper would keep the sidebar and project picker consistent as these fields evolve.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts, line 185:
<comment>Sidebar project metadata is now duplicated from `DashboardNewWorkspaceModalContent` instead of using a shared mapper, including the GitHub avatar URL construction. A shared host-project presentation mapper would keep the sidebar and project picker consistent as these fields evolve.</comment>
<file context>
@@ -150,44 +150,48 @@ export function useDashboardSidebarData() {
+ name: project.name,
+ githubOwner: project.repoOwner,
+ githubRepoName: project.repoName,
+ iconUrl: project.repoOwner
+ ? `https://github.com/${project.repoOwner}.png?size=64`
+ : null,
</file context>
…move ownership check, richer project events, snapshot delete cleanup
|
Review triage — fixed in 1f90ad4:
Rejected (with reasons):
Deferred to the tracked follow-up: unique index on |
Links
plans/20260716-local-first-projects.md(in this PR, includes the decision log)Summary
name+updatedAtcolumns, migration 0010): create, rename, and delete commit locally with zero cloud involvement, so they work fully offline. [bug] Opening a non-GitHub repo on a second machine silently creates a duplicate project instead of joining the existing one #5649's silentmy-app-2duplicates die structurally — nothing ever walks a cloud slug.workspace-cloud-sync.ts, tombstones, re-keying, and the reconciler are deleted;ensureHostRegisteredwas folded away (host registration viatunnel/connect.tsis untouched — relay access keeps working).useHostProjects: per-hostproject.list+project:changedlive events + IndexedDB last-seen snapshots, workspace-pattern settings). Sidebar, project settings, and the new-workspace modal are migrated off Electricv2Projects.How It Works
project.list/updateserve full identity rows;project:changedbroadcasts on the existing event bus (newworkspace-clientpayload type). A startup backfill fillsnamefrom the legacy cloud row when reachable, else the folder basename — never blocks boot.project.setupgets 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.findByPathis local-only (a hit = this host already has it; no hit = create fresh).project.removeflips its kill point: local deletes always run (offline-capable); the legacy cloud delete is best-effort cleanup for the mixed-version window.Manual QA — CDP-verified against the live dev app
Fresh dev host.db with 29 legacy projects; drills driven over CDP:
v2Project.getfor the new id returns NOT_FOUND (no cloud row minted)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 pathproject:changed→ sidebar DOM update in 130 ms (no refetch)cloud push failed/ reconciler lines across the entire session logTesting
bun testinpackages/host-service— 885 tests green (3 tests asserting the retired cloud behavior updated to the new local contracts)bunx tsc --noEmitinpackages/host-service,packages/workspace-client,apps/desktop;bun run lintcleanKnown Limitations / conscious trade-offs
v2Projectscollection; migrating them + the Electricv2Workspacesmerge source is follow-up work before the cloud tables retire.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
Summary by CodeRabbit
New Features
Bug Fixes