Skip to content

feat(desktop): optimistic v2 workspace.create#4120

Merged
saddlepaddle merged 3 commits into
mainfrom
optimistic-updates-create
May 6, 2026
Merged

feat(desktop): optimistic v2 workspace.create#4120
saddlepaddle merged 3 commits into
mainfrom
optimistic-updates-create

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented May 6, 2026

Summary

  • Routes new v2 workspaces through collections.v2Workspaces.insert so the row appears optimistically across every live query (sidebar, list page, detail page) and resolves via $synced once Electric confirms the host-service insert.
  • Threads Postgres txid from API → host-service → renderer onInsert so Electric matches the optimistic upsert against the synced shape.
  • Replaces the in-flight zustand store with a failures-only store. Successful creates touch nothing; rollbacks persist a snapshot keyed by workspaceId for retry / dismiss from the detail page.
  • New workspaces land at the top of the sidebar by computing the local-state row's tabOrder via getPrependTabOrder(...) instead of hardcoding 0.

Behavior

Submit. useWorkspaceCreates.submit builds an optimistic SelectV2Workspace (placeholder name/branch when the user didn't supply them) plus a WorkspaceCreateMeta sidecar (host URL, PR number, base branch, agent launches), then calls collections.v2Workspaces.insert(row, { metadata }). The row appears immediately with $synced=false.

onInsert handler (collections.ts) reads the metadata, calls workspaces.create on the host service via the resolved client, and:

  • Returns { txid } on a fresh write so Electric awaits the synced shape stream.
  • Throws WorkspaceAlreadyExistsAtDifferentIdError(canonicalId) when the host returns an existing workspace at a different id — Electric rolls back the optimistic row and the submit hook surfaces the canonical id to the caller.
  • Mirrors the post-create paneLayout setup that previously lived in the renderer dispatch (insert v2WorkspaceLocalState with prepend tabOrder + paneLayout).

Sidebar (useDashboardSidebarData) selects synced: workspaces.$synced in the local-machine candidates query and auto-includes type=worktree rows where synced === false, alongside the existing type=main auto-include. creationStatus: "creating" is attached on the row, read directly in the iteration loop. Failed creates inject from the failures store with creationStatus: "failed".

Detail page triggers WorkspaceCreatingState when workspace && !workspace.$synced, WorkspaceCreateErrorState when !workspace && failureEntry. ensureWorkspaceInSidebar only fires after sync confirmation, so a rolled-back create doesn't leave a phantom local-state row.

Failures store is a Record<workspaceId, FailedWorkspaceCreate> zustand store. submit writes on rollback, retry re-runs dispatch with the saved snapshot, dismiss clears.

API

v2Workspace.create now returns { ...row, txid?: number }. txid is set on fresh inserts and main-workspace patches via getCurrentTxid(tx). Idempotent return paths (existing row, no patch) omit txid — those rows are already synced and don't need an optimistic match.

workspaces.create on the host service threads the txid through registerCloudAndLocal and the adoptExistingWorktree paths and surfaces it in the procedure response.

Test plan

  • Create a v2 workspace from the modal — row appears in sidebar/list immediately with spinner, transitions to confirmed once host-service finishes
  • Create from the task properties sidebar (OpenInWorkspaceV2) — same flow
  • Create from the task popover (RunInWorkspacePopoverV2) — same flow
  • Create from the branch picker (useBranchPickerController) — same flow
  • Workspace lands at top of the project's sidebar lane after sync
  • Force a host-service failure (e.g. invalid PR number) → row rolls back, sidebar shows failed indicator, detail page shows WorkspaceCreateErrorState
  • Retry from the failed-state detail page → re-runs and succeeds
  • Dismiss from the failed-state detail page → entry cleared, navigates to /v2-workspaces
  • Reload the desktop app mid-create — optimistic state is in-memory only, expected behavior
  • Existing v2Workspace.update and other consumers of the create response still work (tests pass)

Summary by cubic

Adds optimistic v2 workspace creates via collections.v2Workspaces.insert so new workspaces show up immediately and settle via $synced. Replaces the in-flight store with a failures-only store and keeps the create hook to just submit.

  • New Features

    • onInsert calls host workspaces.create with a metadata sidecar and returns { txid } on fresh writes; throws WorkspaceAlreadyExistsAtDifferentIdError to roll back and surface the canonical id.
    • Sidebar auto-includes local type=worktree rows with $synced=false as "creating"; failed creates inject from the failures store. Detail page and sidebar read $synced to show creating until confirmed; ensureWorkspaceInSidebar runs only after sync.
    • v2Workspace.create returns { ...row, txid? }; host-service threads txid through registerCloudAndLocal and adoptExistingWorktree so optimistic inserts match the synced row.
    • Pane layout is set during insert and new workspaces are prepended using getPrependTabOrder(...).
  • Bug Fixes

    • Keep the creating spinner visible in the sidebar for pinned rows until $synced is true, ensuring sidebar and detail page stay in sync.
    • Guard local sidebar-state writes in onInsert with try/catch so a storage failure doesn’t roll back a successful remote create; the next view re-syncs local state.

Written for commit 089f71d. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Sidebar shows workspace creation status (creating / failed) and surfaces failed creates as sidebar items.
    • Local workspace candidates are auto-included in the sidebar; newly-created workspaces are pinned only after confirmation.
    • New tab-order utilities keep sidebar ordering consistent.
  • Bug Fixes

    • Improved handling of workspace creation failures with retry and dismiss flows and clearer feedback.

Wires `collections.v2Workspaces.insert` as the entry point for new v2
workspaces. The optimistic row appears immediately in every live query
(sidebar, list page, detail page) and resolves once Electric syncs the
host-service insert. On failure, Electric rolls back the row and the
renderer persists a failure record keyed by workspace id so the user
can retry from the detail page.

API: `v2Workspace.create` returns Postgres `txid` on writes (omitted on
idempotent reuse) so Electric can match the synced shape. Host-service
threads the txid back through `workspaces.create` and the
`adoptExistingWorktree` paths.

Renderer: `onInsert` on the v2_workspaces collection invokes
`workspaces.create` on the host service via a metadata sidecar (host
URL plus the create args that don't live on the row). Throws a typed
`WorkspaceAlreadyExistsAtDifferentIdError` when the host returns a
canonical id different from the optimistic one — Electric rolls back
the row and the submit hook redirects to the canonical id.

Sidebar reads `$synced` directly off the v2_workspaces row to flag
optimistic worktrees and renders them with the existing
`creationStatus: "creating"` indicator. Detail page transitions
between `WorkspaceCreatingState` and the actual workspace via the same
flag. Failed creates render via the failures store.

New workspaces land at the top of the sidebar by inserting their local
state row at `getPrependTabOrder(...)` instead of a hardcoded `0`.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

Replaces renderer in-flight workspace-create tracking with a failure-only store, adds optimistic local inserts for workspace creation, propagates cloud transaction IDs (txid) through creation APIs, and updates sidebar/dashboard UI to surface creating/failed states and retry/dismiss flows.

Changes

Workspace Creation Refactor: Failure Store + Optimistic Inserts

Layer / File(s) Summary
Store Architecture
apps/desktop/src/renderer/stores/workspace-creates/store.ts, apps/desktop/src/renderer/stores/workspace-creates/index.ts
Removes in-flight constructs and adds FailedWorkspaceCreate, WorkspaceCreateFailuresState, useWorkspaceCreateFailuresStore, WorkspaceCreateMeta, and WorkspaceAlreadyExistsAtDifferentIdError.
Optimistic Creation Hook
apps/desktop/src/renderer/stores/workspace-creates/useWorkspaceCreates.ts
Reworks submit to perform optimistic collections.v2Workspaces.insert with WorkspaceCreateMeta; records failures in the new failure store; public API now exposes only submit.
Collections Integration
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts
Adds onInsert handler for v2_workspaces to call host-service, surface WorkspaceAlreadyExistsAtDifferentIdError, mirror pane layout into local state, and initialize tabOrder for new local rows.
API / Backend: txid propagation
packages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.ts, packages/host-service/src/trpc/router/workspaces/workspaces.ts, packages/trpc/src/router/v2-workspace/v2-workspace.ts
Propagates cloud transaction ID (txid) through adopt/create flows and returns txid alongside created/resolved workspace rows.
Dashboard Sidebar Data
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts
Adds localWorkspaceCandidates live query, a synced flag, maps failures from failure store into failureSidebarRows, injects failed-create rows with creationStatus: "failed", and replaces prior in-flight logic.
Sidebar Item Dismissal
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx
Replaces dismissal of in-flight entries with clearing failures via useWorkspaceCreateFailuresStore.getState().clear(id).
Workspace Create UI
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreateErrorState/WorkspaceCreateErrorState.tsx
Renders WorkspaceCreatingState for unsynced optimistic rows, WorkspaceCreateErrorState when a failure exists; retry reads failure snapshot and calls submit; dismiss clears failure and navigates away.
Manager / Cleanup
apps/desktop/src/renderer/stores/workspace-creates/Manager.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx
Removes WorkspaceCreatesManager component and its JSX usage from dashboard layout.
Tab Order Utilities
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/tabOrder.ts, .../index.ts, apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts
Extracts getPrependTabOrder and getNextTabOrder into tabOrder.ts and re-exports; callers import the helpers instead of defining locally.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as Dashboard UI
    participant Collections as Collections DB
    participant HostService as Host Service
    participant FailureStore as Failure Store

    rect rgba(100, 150, 255, 0.5)
    Note over User,HostService: Optimistic creation + sync flow
    User->>UI: Submit workspace create
    UI->>Collections: Insert optimistic v2_workspace with metadata
    Collections->>Collections: onInsert handler runs
    Collections->>HostService: Request create/adopt worktree
    HostService-->>Collections: Success (includes txid)
    Collections->>Collections: Mirror pane layout, init local state
    UI->>Collections: Observe $synced flag
    UI->>UI: Render creating state until synced
    end

    rect rgba(255, 100, 100, 0.5)
    Note over UI,FailureStore: Failure and recovery
    Collections->>FailureStore: Record failed create (snapshot + error)
    FailureStore-->>UI: Failure visible in sidebar
    UI->>UI: Render error state (retry/dismiss)
    alt Retry
        UI->>Collections: Re-insert using failure snapshot via submit
        Collections->>HostService: Retry create
    else Dismiss
        UI->>FailureStore: Clear failure
        FailureStore-->>UI: Remove failed row
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I burrowed through code, both bright and spare,
Swapped in-flight ghosts for failures kept with care.
Optimistic sprouts in the sidebar grow,
Txids shining where the cloud winds blow.
Retry, dismiss — the workflow hops with flair!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: implementing optimistic creation for v2 workspaces, which is the central feature of this PR.
Description check ✅ Passed The description is comprehensive, well-structured, and covers all required template sections including summary, related behavior details, API changes, and a detailed test plan.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch optimistic-updates-create

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 6, 2026

Greptile Summary

This PR replaces a renderer-side in-flight Zustand store with Electric's native $synced optimistic flag for workspace creates, moving the host-service call into an onInsert handler so the row appears in all live queries immediately. A txid is threaded from the Postgres write through the host service and back to the renderer so Electric can match the optimistic row against the synced shape stream.

  • Optimistic v2Workspaces rows are inserted with WorkspaceCreateMeta metadata; the onInsert handler calls the host service, inserts local sidebar state on success, and either returns { txid } or throws WorkspaceAlreadyExistsAtDifferentIdError to trigger Electric rollback.
  • The old WorkspaceCreatesManager polling component is deleted; the sidebar now auto-includes type=worktree rows with $synced === false as "creating" entries and the failures store is narrowed to rollback-only snapshots.
  • getPrependTabOrder / getNextTabOrder are extracted to a shared tabOrder.ts module and used in onInsert to ensure new workspaces land at the top of their project lane.

Confidence Score: 3/5

The core optimistic-insert architecture is sound, but two gaps in sidebar-to-detail-page consistency and the unguarded metadata access in onInsert warrant attention before shipping.

After onInsert writes the local state row and returns { txid }, the workspace moves from the autoIncluded creating-indicator path into sidebarWithSyncMeta, which hardcodes creationStatus: undefined. Electric then waits for the shape stream to confirm the txid before flipping $synced, producing a real window where the sidebar shows no spinner but the detail page still renders WorkspaceCreatingState. Separately, the onInsert handler accesses transaction.mutations[0] and casts metadata without any guard; a caller that omits metadata would produce an opaque TypeError rather than a clear failure.

useDashboardSidebarData.ts (the sidebarWithSyncMeta mapping that drops creationStatus without checking $synced) and collections.ts (the onInsert handler's unguarded mutations[0] / metadata cast).

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts Adds onInsert handler to v2Workspaces that calls the host service, manages local state, and returns txid — but accesses transaction.mutations[0] and casts metadata without defensive guards.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts Replaces in-flight store with live $synced query for creating status, but sidebarWithSyncMeta loses the creating indicator before Electric confirms sync after local state is inserted.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx Switches creating/error state detection from in-flight store to workspace.$synced and failures store; isSynced defaults to false for null workspace which is a semantic mismatch but harmless today.
apps/desktop/src/renderer/stores/workspace-creates/useWorkspaceCreates.ts Refactored to build an optimistic row and use collections.v2Workspaces.insert with metadata; failures are now recorded only on rollback/error. Retry and dismiss wire up correctly to the new store.
apps/desktop/src/renderer/stores/workspace-creates/store.ts Clean redesign — replaces the in-flight multi-state entries array with a narrow failures-only Record<id, FailedWorkspaceCreate> store; WorkspaceAlreadyExistsAtDifferentIdError is a well-scoped custom error class.
packages/trpc/src/router/v2-workspace/v2-workspace.ts Returns { ...row, txid } from all v2Workspace.create code paths — fresh inserts get a real txid from getCurrentTxid, idempotent/reuse paths return txid: undefined.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts:307-311
**Sidebar spinner dropped before Electric confirms `$synced`**

After `onInsert` completes its host-service call and inserts the `v2WorkspaceLocalState` row, the workspace moves from `autoIncluded` (where `creationStatus: "creating"` is set) into `sidebarWithSyncMeta`. Because `rawSidebarWorkspaces` does not select `$synced`, `sidebarWithSyncMeta` unconditionally sets `synced: true` / `creationStatus: undefined`. From that point until Electric's shape stream confirms the txid and flips `$synced = true`, the sidebar shows no creating indicator while the detail page still renders `WorkspaceCreatingState` (which reads `workspace?.$synced` directly). The window is bounded by Electric's shape-stream round-trip — typically short, but observable during slow or congested sync.

### Issue 2 of 3
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts:408-409
**No guard against missing `metadata` or empty `mutations` array**

`transaction.mutations[0]` is accessed without checking that the array has at least one element, and `metadata` is immediately cast to `WorkspaceCreateMeta` without validation. If anything calls `v2Workspaces.insert` without a `metadata` object (e.g., a future code path or a test), `onInsert` will throw `TypeError: Cannot read properties of undefined` when destructuring `modified` and `metadata`, which Electric would surface as an unhandled rejection and roll back the row without a user-visible error message.

### Issue 3 of 3
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx:39
**`?? false` default for `isSynced` is semantically inverted**

When `workspace` is `null` (not found in the collection), `isSynced` resolves to `false`. The `?? false` default is harmless today because `isSynced` is only meaningful when `workspace` is truthy (the `if (workspace && !isSynced)` guard prevents misuse), but it risks confusion if a future branch reads `isSynced` without first asserting `workspace` is non-null. `?? true` would be the safer semantic default.

```suggestion
	const isSynced = workspace?.$synced ?? true;
```

Reviews (1): Last reviewed commit: "feat(desktop): optimistic v2 workspace.c..." | Re-trigger Greptile

Comment on lines +307 to +311
const sidebarWithSyncMeta = sidebarWorkspaces.map((workspace) => ({
...workspace,
synced: true,
creationStatus: undefined,
}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Sidebar spinner dropped before Electric confirms $synced

After onInsert completes its host-service call and inserts the v2WorkspaceLocalState row, the workspace moves from autoIncluded (where creationStatus: "creating" is set) into sidebarWithSyncMeta. Because rawSidebarWorkspaces does not select $synced, sidebarWithSyncMeta unconditionally sets synced: true / creationStatus: undefined. From that point until Electric's shape stream confirms the txid and flips $synced = true, the sidebar shows no creating indicator while the detail page still renders WorkspaceCreatingState (which reads workspace?.$synced directly). The window is bounded by Electric's shape-stream round-trip — typically short, but observable during slow or congested sync.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts
Line: 307-311

Comment:
**Sidebar spinner dropped before Electric confirms `$synced`**

After `onInsert` completes its host-service call and inserts the `v2WorkspaceLocalState` row, the workspace moves from `autoIncluded` (where `creationStatus: "creating"` is set) into `sidebarWithSyncMeta`. Because `rawSidebarWorkspaces` does not select `$synced`, `sidebarWithSyncMeta` unconditionally sets `synced: true` / `creationStatus: undefined`. From that point until Electric's shape stream confirms the txid and flips `$synced = true`, the sidebar shows no creating indicator while the detail page still renders `WorkspaceCreatingState` (which reads `workspace?.$synced` directly). The window is bounded by Electric's shape-stream round-trip — typically short, but observable during slow or congested sync.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +408 to +409
const { modified, metadata } = transaction.mutations[0];
const meta = metadata as WorkspaceCreateMeta;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 No guard against missing metadata or empty mutations array

transaction.mutations[0] is accessed without checking that the array has at least one element, and metadata is immediately cast to WorkspaceCreateMeta without validation. If anything calls v2Workspaces.insert without a metadata object (e.g., a future code path or a test), onInsert will throw TypeError: Cannot read properties of undefined when destructuring modified and metadata, which Electric would surface as an unhandled rejection and roll back the row without a user-visible error message.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts
Line: 408-409

Comment:
**No guard against missing `metadata` or empty `mutations` array**

`transaction.mutations[0]` is accessed without checking that the array has at least one element, and `metadata` is immediately cast to `WorkspaceCreateMeta` without validation. If anything calls `v2Workspaces.insert` without a `metadata` object (e.g., a future code path or a test), `onInsert` will throw `TypeError: Cannot read properties of undefined` when destructuring `modified` and `metadata`, which Electric would surface as an unhandled rejection and roll back the row without a user-visible error message.

How can I resolve this? If you propose a fix, please make it concise.

: undefined,
// Read `$synced` straight off the row — useLiveQuery returns rows enriched
// with virtual props, and changes to optimistic state retrigger the query.
const isSynced = workspace?.$synced ?? false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 ?? false default for isSynced is semantically inverted

When workspace is null (not found in the collection), isSynced resolves to false. The ?? false default is harmless today because isSynced is only meaningful when workspace is truthy (the if (workspace && !isSynced) guard prevents misuse), but it risks confusion if a future branch reads isSynced without first asserting workspace is non-null. ?? true would be the safer semantic default.

Suggested change
const isSynced = workspace?.$synced ?? false;
const isSynced = workspace?.$synced ?? true;
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx
Line: 39

Comment:
**`?? false` default for `isSynced` is semantically inverted**

When `workspace` is `null` (not found in the collection), `isSynced` resolves to `false`. The `?? false` default is harmless today because `isSynced` is only meaningful when `workspace` is truthy (the `if (workspace && !isSynced)` guard prevents misuse), but it risks confusion if a future branch reads `isSynced` without first asserting `workspace` is non-null. `?? true` would be the safer semantic default.

```suggestion
	const isSynced = workspace?.$synced ?? true;
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx (1)

144-147: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a failure-specific accessible label here.

creationStatus === "failed" still announces Creating workspace: … in collapsed mode, so screen readers get the wrong state.

Suggested fix
 					creationStatus={creationStatus}
 					aria-label={
-						creationStatus ? `Creating workspace: ${name}` : undefined
+						creationStatus === "creating"
+							? `Creating workspace: ${name}`
+							: creationStatus === "failed"
+								? `Workspace creation failed: ${name}`
+								: undefined
 					}
 				/>
🤖 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/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx`
around lines 144 - 147, The aria-label currently always says "Creating
workspace: {name}" when creationStatus is truthy, which miscommunicates the
state when creationStatus === "failed"; update the aria-label logic inside
DashboardSidebarWorkspaceItem to return a failure-specific label (e.g., `Failed
to create workspace: ${name}`) when creationStatus === "failed", `Creating
workspace: ${name}` when creationStatus === "creating" (or other in-progress
value), and undefined otherwise so screen readers receive the correct state for
the name prop.
🤖 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/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts`:
- Around line 307-310: The code currently forces every sidebar row to synced:
true in the sidebarWithSyncMeta mapping which hides optimistic "creating" state;
instead, stop overriding the real sync flag — in useDashboardSidebarData replace
the hardcoded synced: true with the actual sync state from the workspace (or the
rawSidebarWorkspaces / v2Workspaces.$synced lookup) so rows backed by
v2WorkspaceLocalState keep their true $synced value; only clear creationStatus
(creationStatus: undefined) when that resolved/synced value is true so
optimistic rows remain in "creating" until onInsert / v2Workspaces.$synced
confirms the insert.

In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts`:
- Around line 411-479: The remote create (client.workspaces.create.mutate) must
not be coupled to subsequent local-state work (appendLaunchesToPaneLayout,
v2WorkspaceLocalState.update/insert) so that failures in mirroring don't roll
back an already-created workspace; after calling client.workspaces.create.mutate
and handling the WorkspaceAlreadyExistsAtDifferentIdError case as now, perform
the paneLayout computation and v2WorkspaceLocalState.update/insert inside a
try/catch that does not throw back to the caller on failure—log or surface a
repairable local-state error separately (e.g., emit to a local error reporter)
and still return the successful result.txid; ensure only the canonical-id
conflict still throws by keeping the existing throw of
WorkspaceAlreadyExistsAtDifferentIdError for result.alreadyExists with
mismatched id.

In `@apps/desktop/src/renderer/stores/workspace-creates/store.ts`:
- Around line 58-71: The current useWorkspaceCreateFailuresStore
(WorkspaceCreateFailuresState) stores failures in-memory so records made by
record(workspaceId, entry) and cleared by clear(workspaceId) are lost on reload;
wrap the create(...) with Zustand's persist middleware (or the app's existing
persisted store helper) to persist the failures map under a stable key (e.g.,
"workspace-create-failures") to localStorage / electron store, ensure
rehydration on startup, and keep the same API (record/clear) while updating
types if necessary and avoiding persisting non-serializable fields.

---

Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx`:
- Around line 144-147: The aria-label currently always says "Creating workspace:
{name}" when creationStatus is truthy, which miscommunicates the state when
creationStatus === "failed"; update the aria-label logic inside
DashboardSidebarWorkspaceItem to return a failure-specific label (e.g., `Failed
to create workspace: ${name}`) when creationStatus === "failed", `Creating
workspace: ${name}` when creationStatus === "creating" (or other in-progress
value), and undefined otherwise so screen readers receive the correct state for
the name prop.
🪄 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: a1edd7cf-689d-4d0a-b1db-78297473c837

📥 Commits

Reviewing files that changed from the base of the PR and between 5c35219 and 3decb62.

📒 Files selected for processing (15)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/tabOrder.ts
  • apps/desktop/src/renderer/stores/workspace-creates/Manager.tsx
  • apps/desktop/src/renderer/stores/workspace-creates/index.ts
  • apps/desktop/src/renderer/stores/workspace-creates/store.ts
  • apps/desktop/src/renderer/stores/workspace-creates/useWorkspaceCreates.ts
  • packages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.ts
  • packages/host-service/src/trpc/router/workspaces/workspaces.ts
  • packages/trpc/src/router/v2-workspace/v2-workspace.ts
💤 Files with no reviewable changes (2)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx
  • apps/desktop/src/renderer/stores/workspace-creates/Manager.tsx

Comment on lines +58 to +71
export const useWorkspaceCreateFailuresStore =
create<WorkspaceCreateFailuresState>((set) => ({
failures: {},
record: (workspaceId, entry) =>
set((state) => ({
entries: state.entries.filter(
(entry) => entry.snapshot.id !== workspaceId,
),
failures: { ...state.failures, [workspaceId]: entry },
})),
}),
);
clear: (workspaceId) =>
set((state) => {
if (!(workspaceId in state.failures)) return state;
const { [workspaceId]: _, ...rest } = state.failures;
return { failures: rest };
}),
}));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist rollback entries instead of keeping them renderer-memory only.

This plain Zustand create() store is wiped on refresh/relaunch, so a failed create loses its retry/dismiss snapshot as soon as the renderer reloads. That makes the failure-only flow unreliable once the optimistic row has already rolled back.

Suggested direction
 import { create } from "zustand";
+import { createJSONStorage, persist } from "zustand/middleware";
 ...
-export const useWorkspaceCreateFailuresStore =
-	create<WorkspaceCreateFailuresState>((set) => ({
-		failures: {},
-		record: (workspaceId, entry) =>
-			set((state) => ({
-				failures: { ...state.failures, [workspaceId]: entry },
-			})),
-		clear: (workspaceId) =>
-			set((state) => {
-				if (!(workspaceId in state.failures)) return state;
-				const { [workspaceId]: _, ...rest } = state.failures;
-				return { failures: rest };
-			}),
-	}));
+export const useWorkspaceCreateFailuresStore =
+	create<WorkspaceCreateFailuresState>()(
+		persist(
+			(set) => ({
+				failures: {},
+				record: (workspaceId, entry) =>
+					set((state) => ({
+						failures: { ...state.failures, [workspaceId]: entry },
+					})),
+				clear: (workspaceId) =>
+					set((state) => {
+						if (!(workspaceId in state.failures)) return state;
+						const { [workspaceId]: _, ...rest } = state.failures;
+						return { failures: rest };
+					}),
+			}),
+			{
+				name: "workspace-create-failures",
+				storage: createJSONStorage(() => localStorage),
+			},
+		),
+	);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const useWorkspaceCreateFailuresStore =
create<WorkspaceCreateFailuresState>((set) => ({
failures: {},
record: (workspaceId, entry) =>
set((state) => ({
entries: state.entries.filter(
(entry) => entry.snapshot.id !== workspaceId,
),
failures: { ...state.failures, [workspaceId]: entry },
})),
}),
);
clear: (workspaceId) =>
set((state) => {
if (!(workspaceId in state.failures)) return state;
const { [workspaceId]: _, ...rest } = state.failures;
return { failures: rest };
}),
}));
export const useWorkspaceCreateFailuresStore =
create<WorkspaceCreateFailuresState>()(
persist(
(set) => ({
failures: {},
record: (workspaceId, entry) =>
set((state) => ({
failures: { ...state.failures, [workspaceId]: entry },
})),
clear: (workspaceId) =>
set((state) => {
if (!(workspaceId in state.failures)) return state;
const { [workspaceId]: _, ...rest } = state.failures;
return { failures: rest };
}),
}),
{
name: "workspace-create-failures",
storage: createJSONStorage(() => localStorage),
},
),
);
🤖 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/stores/workspace-creates/store.ts` around lines 58
- 71, The current useWorkspaceCreateFailuresStore (WorkspaceCreateFailuresState)
stores failures in-memory so records made by record(workspaceId, entry) and
cleared by clear(workspaceId) are lost on reload; wrap the create(...) with
Zustand's persist middleware (or the app's existing persisted store helper) to
persist the failures map under a stable key (e.g., "workspace-create-failures")
to localStorage / electron store, ensure rehydration on startup, and keep the
same API (record/clear) while updating types if necessary and avoiding
persisting non-serializable fields.

Drop `retry` and `dismiss` from the hook's API — they were thin wrappers
around `useWorkspaceCreateFailuresStore`. `WorkspaceCreateErrorState`
now reads/writes the store directly for retry/dismiss, matching the
precedent already set by `DashboardSidebarWorkspaceItem`.

Collapse the two early-return validation branches (no org, no host URL)
into throws inside the same try/catch that handles host-service
failures, so all failure paths flow through one record-to-store path.

Dismiss now navigates via `useNavigateAwayFromWorkspace` (same hook
the destroy flow uses) — lands on the top sidebar workspace, falling
back to "/" if the sidebar is empty, instead of always sending the
user back to the v2 workspaces list.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/desktop/src/renderer/stores/workspace-creates/useWorkspaceCreates.ts (1)

99-99: ⚡ Quick win

Double cast bypasses metadata type-checking.

meta as unknown as Record<string, unknown> discards the WorkspaceCreateMeta type contract at the boundary, so a future drift between this sidecar and collections.ts:onInsert's reader will not be caught by the compiler. If the cast exists only because optional props don't widen to Record<string, unknown> cleanly, prefer typing the insert metadata generic (or accepting WorkspaceCreateMeta directly in the collection definition) so both ends share one type.

🤖 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/stores/workspace-creates/useWorkspaceCreates.ts` at
line 99, The code is double-casting meta with "meta as unknown as Record<string,
unknown>" which defeats compile-time checks; remove the double-cast and make the
insert metadata strongly typed by sharing WorkspaceCreateMeta across
boundary—either change the collection definition (the insert metadata generic in
collections.ts/onInsert) to accept WorkspaceCreateMeta or call insert with the
properly typed generic so useWorkspaceCreates (the meta variable) is passed as
WorkspaceCreateMeta instead of casting to Record<string, unknown>.
🤖 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.

Nitpick comments:
In `@apps/desktop/src/renderer/stores/workspace-creates/useWorkspaceCreates.ts`:
- Line 99: The code is double-casting meta with "meta as unknown as
Record<string, unknown>" which defeats compile-time checks; remove the
double-cast and make the insert metadata strongly typed by sharing
WorkspaceCreateMeta across boundary—either change the collection definition (the
insert metadata generic in collections.ts/onInsert) to accept
WorkspaceCreateMeta or call insert with the properly typed generic so
useWorkspaceCreates (the meta variable) is passed as WorkspaceCreateMeta instead
of casting to Record<string, unknown>.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 676372ec-4b64-4156-b541-ebc3406f94f1

📥 Commits

Reviewing files that changed from the base of the PR and between 3decb62 and 6f6174f.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreateErrorState/WorkspaceCreateErrorState.tsx
  • apps/desktop/src/renderer/stores/workspace-creates/index.ts
  • apps/desktop/src/renderer/stores/workspace-creates/useWorkspaceCreates.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/stores/workspace-creates/index.ts

Two PR-review fixes for the optimistic v2 workspace.create path.

1. Sidebar spinner gap. `rawSidebarWorkspaces` now selects
   `synced: workspaces.\$synced`, and `sidebarWithSyncMeta` derives
   `creationStatus` from the actual flag instead of hardcoding
   `synced: true`. Without this, the moment `onInsert` wrote
   `v2WorkspaceLocalState`, the workspace flipped from the
   optimistic-only injection into the local-state-backed query and
   lost its spinner — while the detail page (which reads
   `\$synced` directly off the row) kept rendering
   `WorkspaceCreatingState` until Electric confirmed the txid.
   Now both surfaces stay in lockstep until sync.

2. Local-state write coupling. The post-create paneLayout setup
   (`appendLaunchesToPaneLayout` + `v2WorkspaceLocalState`
   insert/update) was inside `onInsert` but unguarded — a
   localStorage failure after a successful remote create would
   throw back to Electric, roll the optimistic row back, and the
   user would see a false "create failed" state even though the
   host already wrote the workspace. Wrap that section in
   try/catch + console.warn so local-state failures stay local;
   the detail page's `ensureWorkspaceInSidebar` mount effect
   re-syncs sidebar state on next view.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts (1)

487-527: 💤 Low value

Stabilize createdAt/updatedAt for failed-item rows to preserve project fingerprint stability.

new Date() is invoked inside computedGroups, which re-runs whenever any of its many dependencies change (machineId, sidebarProjects, sidebarSections, visibleSidebarWorkspaces, pullRequestsByWorkspaceId, localStateWorkspaceIds, etc.) — not only when failures change. Each rerun produces fresh Date instances for every failed item, which propagate into the project's JSON fingerprint computed by getDashboardSidebarProjectFingerprint (line 60). The result: useStableDashboardSidebarProjects will always treat any project containing a failed row as "changed", partially defeating the stability layer.

Hoist the timestamps into failureSidebarRows (which only recomputes when failuresMap actually changes), or pull them from entry.snapshot.createdAt/updatedAt if the snapshot already carries them.

♻️ Suggested fix
 	const failureSidebarRows = useMemo(
 		() =>
 			Object.entries(failuresMap).map(([id, entry]) => ({
 				id,
 				projectId: entry.snapshot.projectId,
 				hostId: entry.hostId,
 				name: entry.snapshot.name ?? "New workspace",
 				branchName:
 					entry.snapshot.branch ?? entry.snapshot.name ?? "New workspace",
+				createdAt: entry.snapshot.createdAt ?? new Date(),
+				updatedAt: entry.snapshot.updatedAt ?? new Date(),
 			})),
 		[failuresMap],
 	);
@@
-				createdAt: new Date(),
-				updatedAt: new Date(),
+				createdAt: failure.createdAt,
+				updatedAt: failure.updatedAt,
 				creationStatus: "failed",
🤖 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/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts`
around lines 487 - 527, The computedGroups loop creates new Date() for
createdAt/updatedAt on every recompute which destabilizes project fingerprints;
instead derive stable timestamps from the failure snapshot or compute them once
when failureSidebarRows is built so they only change when failuresMap
changes—update useDashboardSidebarData so the failedItem createdAt/updatedAt
come from entry.snapshot.createdAt/updatedAt or from a timestamp stored on
failureSidebarRows (constructed where failureSidebarRows is produced) rather
than calling new Date() inside computedGroups, ensuring
getDashboardSidebarProjectFingerprint and useStableDashboardSidebarProjects see
stable values.
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts (1)

408-410: ⚡ Quick win

Guard the WorkspaceCreateMeta contract before dereferencing it.

This callback now hard-depends on metadata, but the boundary is still just an unchecked cast. If a future caller inserts into v2Workspaces without the expected metadata, or the payload shape drifts, this will fail with a generic property-access error instead of a diagnosable create failure.

🛡️ Minimal guard
 onInsert: async ({ transaction }) => {
   const { modified, metadata } = transaction.mutations[0];
+  if (
+    !metadata ||
+    typeof metadata !== "object" ||
+    !("hostUrl" in metadata)
+  ) {
+    throw new Error(
+      "v2Workspaces.insert requires WorkspaceCreateMeta metadata",
+    );
+  }
   const meta = metadata as WorkspaceCreateMeta;
   const client = getHostServiceClientByUrl(meta.hostUrl);
🤖 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/providers/CollectionsProvider/collections.ts`
around lines 408 - 410, The code assumes transaction.mutations[0].metadata
conforms to WorkspaceCreateMeta and directly casts it; guard that contract first
by checking the metadata exists and has the expected shape (e.g., metadata &&
typeof metadata.hostUrl === 'string' and any other required fields) before
assigning meta and calling getHostServiceClientByUrl(meta.hostUrl); if the guard
fails, log or throw a clear, diagnosable error (including the offending
metadata) and return early instead of dereferencing invalid properties inside
the WorkspaceCreateMeta handling path.
🤖 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.

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts`:
- Around line 487-527: The computedGroups loop creates new Date() for
createdAt/updatedAt on every recompute which destabilizes project fingerprints;
instead derive stable timestamps from the failure snapshot or compute them once
when failureSidebarRows is built so they only change when failuresMap
changes—update useDashboardSidebarData so the failedItem createdAt/updatedAt
come from entry.snapshot.createdAt/updatedAt or from a timestamp stored on
failureSidebarRows (constructed where failureSidebarRows is produced) rather
than calling new Date() inside computedGroups, ensuring
getDashboardSidebarProjectFingerprint and useStableDashboardSidebarProjects see
stable values.

In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts`:
- Around line 408-410: The code assumes transaction.mutations[0].metadata
conforms to WorkspaceCreateMeta and directly casts it; guard that contract first
by checking the metadata exists and has the expected shape (e.g., metadata &&
typeof metadata.hostUrl === 'string' and any other required fields) before
assigning meta and calling getHostServiceClientByUrl(meta.hostUrl); if the guard
fails, log or throw a clear, diagnosable error (including the offending
metadata) and return early instead of dereferencing invalid properties inside
the WorkspaceCreateMeta handling path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e15b3be-96c4-459b-877e-02d0ce748524

📥 Commits

Reviewing files that changed from the base of the PR and between 6f6174f and 089f71d.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts

@saddlepaddle saddlepaddle merged commit 3b6615d into main May 6, 2026
17 checks passed
saddlepaddle added a commit that referenced this pull request May 6, 2026
saddlepaddle added a commit that referenced this pull request May 6, 2026
The revert of #4120 (#4135) restored a hardcoded `tabOrder: 0` in the
v2 workspace.create path. Existing rows default to `tabOrder: 0` too,
so new workspaces tied with them and ordering was non-deterministic.

Restores the `getPrependTabOrder` helper from #4120 as a shared util in
`dashboardSidebarLocal/`, and uses it from `useWorkspaceCreates` so new
rows land strictly above every existing top-level item — matching what
the pending-row injection in `useDashboardSidebarData` already assumes.
MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request May 8, 2026
)

The revert of superset-sh#4120 (superset-sh#4135) restored a hardcoded `tabOrder: 0` in the
v2 workspace.create path. Existing rows default to `tabOrder: 0` too,
so new workspaces tied with them and ordering was non-deterministic.

Restores the `getPrependTabOrder` helper from superset-sh#4120 as a shared util in
`dashboardSidebarLocal/`, and uses it from `useWorkspaceCreates` so new
rows land strictly above every existing top-level item — matching what
the pending-row injection in `useDashboardSidebarData` already assumes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant