Skip to content

fix(desktop): hide v2 workspace rows while destroy is in flight#3621

Merged
Kitenite merged 1 commit into
mainfrom
avoid-toast.promise-for-workspace-deletion-perform
Apr 21, 2026
Merged

fix(desktop): hide v2 workspace rows while destroy is in flight#3621
Kitenite merged 1 commit into
mainfrom
avoid-toast.promise-for-workspace-deletion-perform

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Apr 21, 2026

Summary

  • workspaceCleanup.destroy can take 10–20s. The previous toast.loading → success sat onscreen the whole time and the sidebar row stayed put, so clicking Delete felt like nothing was happening.
  • On confirm we now optimistically hide the row and drop the loading toast entirely. The hidden row is the feedback. On success, onDeleted removes the v2WorkspaceLocalState entry and the row unmounts naturally. On error the row reappears.
  • Error panes (Conflict / Teardown-failed / Unknown) are preserved — the dialog lives as a sibling of the hidden wrapper, so decision-required errors still reopen into the matching pane for force-retry.

Mechanism: a new DeletingWorkspacesProvider tracks in-flight deletions with { isDeleting, markDeleting, clearDeleting }. useDestroyDialogState marks on confirm and clears in finally; DashboardSidebarWorkspaceItem wraps its visible content in <div hidden={isDeleting}>.

Test plan

  • Delete a v2 workspace from the sidebar context menu → row disappears instantly, no loading toast, workspace gone after destroy completes.
  • Delete with uncommitted changes / unpushed commits → conflict pane reopens, row reappears behind it, force-retry works.
  • Delete a workspace whose teardown script fails → teardown-failed pane reopens, row reappears, force-retry works.
  • Delete an unknown-error case (e.g. host-service down) → toast.error names the workspace, row reappears.
  • Delete the currently-active workspace → navigates away immediately (existing handleDeleted flow, unchanged).
  • Warnings emitted by destroy still surface as individual toast.warnings.

Summary by cubic

Optimistically hide v2 workspace rows during deletion and remove the loading toast for faster, clearer feedback. Rows reappear on error, and the delete dialog reopens for conflict/teardown cases.

  • Bug Fixes
    • Added DeletingWorkspacesProvider with isDeleting, markDeleting, and clearDeleting to track in-flight destroys.
    • Updated DashboardSidebarWorkspaceItem to hide visible content with hidden={isDeleting}; keep the delete dialog outside the wrapper so errors can reopen correctly.
    • Changed useDestroyDialogState to mark/clear deleting, drop toast.loading, keep toast.warning for destroy warnings, and show toast.error for unknown errors; on success, the row unmounts via existing state.
    • Wrapped the app with DeletingWorkspacesProvider in _authenticated/layout.tsx.

Written for commit bd0edda. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Enhanced workspace deletion experience: the sidebar item is now hidden during deletion processing and reappears if deletion fails.
    • Deletion dialog closes immediately after confirming for instant feedback.
  • User Experience

    • Reduced notification volume during workspace deletion operations.

`workspaceCleanup.destroy` can take 10–20s; the old loading toast sat
there the whole time and the row stayed in the sidebar. Now the row
hides optimistically on confirm and the toast is gone. On error
(conflict / teardown-failed) the row reappears and the dialog reopens
into the matching error pane for force-retry.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1071030b-d3bc-4a91-bcd7-1b6e5c1ee41b

📥 Commits

Reviewing files that changed from the base of the PR and between f175be4 and bd0edda.

📒 Files selected for processing (5)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx
  • apps/desktop/src/renderer/routes/_authenticated/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/DeletingWorkspacesProvider.tsx
  • apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/index.ts

📝 Walkthrough

Walkthrough

A new context-based provider system tracks workspace deletion states across the authenticated layout. The DeletingWorkspacesProvider maintains a set of IDs undergoing deletion and exposes query/mutation functions. The sidebar integrates this to hide items during deletion, while the delete dialog hook manages removal operations with optimistic UI updates.

Changes

Cohort / File(s) Summary
New Provider Infrastructure
apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/DeletingWorkspacesProvider.tsx, apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/index.ts
Introduces DeletingWorkspacesProvider component and useDeletingWorkspaces() hook to manage a ReadonlySet of workspace IDs marked for deletion, with memoized context value and immutable state updates.
Provider Integration
apps/desktop/src/renderer/routes/_authenticated/layout.tsx
Wraps WorkerPoolContextProvider and its descendants with DeletingWorkspacesProvider to establish the context layer for authenticated routes.
Delete Dialog Hook
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts
Integrates with provider via markDeleting() and clearDeleting(), removes loading/success toast flow in favor of optimistic sidebar hiding, ensures state cleanup in finally block, and updates dependency list.
Sidebar Component
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx
Calls useDeletingWorkspaces() to check deletion state and hides workspace item UI with hidden={isDeleting} while keeping delete dialog visible outside the wrapper.

Sequence Diagram

sequenceDiagram
    participant User
    participant DeleteDialog as Delete Dialog
    participant Provider as DeletingWorkspacesProvider
    participant Sidebar as Sidebar Component

    User->>DeleteDialog: Confirm delete workspace
    DeleteDialog->>Provider: markDeleting(workspaceId)
    Provider->>Provider: Add ID to deletion set
    DeleteDialog->>DeleteDialog: Close dialog
    Sidebar->>Provider: Poll isDeleting(workspaceId)
    Provider-->>Sidebar: true
    Sidebar->>Sidebar: Hide item UI
    DeleteDialog->>DeleteDialog: Execute destroy operation
    alt Success
        DeleteDialog->>DeleteDialog: Call onDeleted()
    else Error (conflict/teardown)
        DeleteDialog->>DeleteDialog: Reopen with error state
    else Other Error
        DeleteDialog->>DeleteDialog: Show error toast
    end
    DeleteDialog->>Provider: clearDeleting(workspaceId) in finally
    Provider->>Provider: Remove ID from deletion set
    Sidebar->>Provider: Poll isDeleting(workspaceId)
    Provider-->>Sidebar: false
    Sidebar->>Sidebar: Show item UI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


🐰 A provider springs to life,
Hiding sidebars from strife—
Deletion marked, then cleared with care,
Context flows through desktop air! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% 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: hiding v2 workspace rows while deletion is in progress, which is the primary UX improvement across all modified files.
Description check ✅ Passed The description is comprehensive, covering the problem statement, implementation mechanism, and test plan with specific scenarios. All required template sections are present and well-populated.
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 avoid-toast.promise-for-workspace-deletion-perform

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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 21, 2026

Greptile Summary

This PR improves the UX of v2 workspace deletion by replacing the long-lived toast.loading → success pattern with an optimistic "hide the row immediately" approach, eliminating the awkward 10–20 second wait where the sidebar appeared frozen.

Key changes:

  • DeletingWorkspacesProvider — new lightweight React context that maintains a Set<string> of workspace IDs whose destroy calls are in flight. Exposes isDeleting, markDeleting, and clearDeleting with stable callbacks and correct functional setState updates.
  • useDestroyDialogState — on confirm, calls markDeleting then runs destroy silently. clearDeleting fires in finally so the row reappears on any error. Decision-required errors (conflict / teardown-failed) still reopen the dialog in the correct error pane.
  • DashboardSidebarWorkspaceItem — wraps the visible row in <div hidden={isDeleting}>. The DashboardSidebarDeleteDialog is intentionally kept outside this wrapper so error-pane reopening works while the row is visually hidden.
  • layout.tsx — mounts DeletingWorkspacesProvider correctly scoped to the authenticated layout tree.

Confidence Score: 5/5

Safe to merge — all error paths restore the row and the implementation is correct under React 18 automatic batching.

The provider is implemented correctly (functional setState, stable callbacks, proper memoization). The hidden wrapper pattern keeps the dialog accessible for error-pane reopening. The only note is that clearDeleting runs in finally on the success path, but React 18 automatic batching ensures the two state updates are coalesced into one render, making this safe in practice.

No files require special attention.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/DeletingWorkspacesProvider.tsx New context provider tracking in-flight workspace deletions with a Set; correctly uses functional setState updates and memoization.
apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/index.ts Barrel export for the new provider — no issues.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts Replaces loading-toast pattern with optimistic hide; success-path clearDeleting in finally could theoretically cause a single-frame flash if onDeleted is async, but React 18 batching makes this safe in practice.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx Wraps visible content in <div hidden={isDeleting}>; correctly keeps DashboardSidebarDeleteDialog outside the hidden wrapper so error panes can still reopen.
apps/desktop/src/renderer/routes/_authenticated/layout.tsx Wraps WorkerPoolContextProvider and descendants in the new DeletingWorkspacesProvider — correct placement in the component tree.

Sequence Diagram

sequenceDiagram
    actor User
    participant Dialog as DeleteDialog
    participant Hook as useDestroyDialogState
    participant Provider as DeletingWorkspacesProvider
    participant Row as SidebarWorkspaceItem
    participant API as workspaceCleanup.destroy

    User->>Dialog: Click Delete → Confirm
    Dialog->>Hook: run(force=false)
    Hook->>Provider: markDeleting(workspaceId)
    Provider-->>Row: isDeleting → true → hidden
    Hook->>Dialog: onOpenChange(false)
    Hook->>API: destroy()

    alt Success
        API-->>Hook: result
        Hook->>Row: onDeleted() → unmount
        Hook->>Provider: clearDeleting() [finally]
    else Conflict / Teardown-failed
        API-->>Hook: throw error
        Hook->>Provider: clearDeleting() [finally]
        Provider-->>Row: row reappears
        Hook->>Dialog: reopen error pane
    else Unknown error
        API-->>Hook: throw error
        Hook->>Hook: toast.error
        Hook->>Provider: clearDeleting() [finally]
        Provider-->>Row: row reappears
    end
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts
Line: 85-88

Comment:
**`clearDeleting` on success path may briefly flash the row**

On the success path, `onDeleted?.()` (which removes the workspace from sidebar state) is called in the `try` block and `clearDeleting(workspaceId)` runs afterwards in `finally`. In React 18 with automatic batching these two state updates will almost always be coalesced into one render — so the row would unmount without ever becoming visible again.

However, if `onDeleted` performs an asynchronous step before actually updating sidebar state (e.g. an async removal from a store), the `clearDeleting` call could land first in a separate render tick. At that instant `isDeleting(id)` returns `false` while the workspace is still present in the sidebar list, causing the row to flash visible for one frame before the sidebar update unmounts it.

If `onDeleted` is always synchronous this is a non-issue, but it may be worth moving `clearDeleting` inside the `try` block (before `onDeleted`) so the sequence is deterministically: row hidden → mark not-deleting → unmount due to onDeleted. The current code is safe as long as `onDeleted` only calls synchronous state setters.

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

Reviews (1): Last reviewed commit: "fix(desktop): hide v2 workspace rows whi..." | Re-trigger Greptile

Comment on lines 85 to 88
} finally {
clearDeleting(workspaceId);
inFlight.current = 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 clearDeleting on success path may briefly flash the row

On the success path, onDeleted?.() (which removes the workspace from sidebar state) is called in the try block and clearDeleting(workspaceId) runs afterwards in finally. In React 18 with automatic batching these two state updates will almost always be coalesced into one render — so the row would unmount without ever becoming visible again.

However, if onDeleted performs an asynchronous step before actually updating sidebar state (e.g. an async removal from a store), the clearDeleting call could land first in a separate render tick. At that instant isDeleting(id) returns false while the workspace is still present in the sidebar list, causing the row to flash visible for one frame before the sidebar update unmounts it.

If onDeleted is always synchronous this is a non-issue, but it may be worth moving clearDeleting inside the try block (before onDeleted) so the sequence is deterministically: row hidden → mark not-deleting → unmount due to onDeleted. The current code is safe as long as onDeleted only calls synchronous state setters.

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

Comment:
**`clearDeleting` on success path may briefly flash the row**

On the success path, `onDeleted?.()` (which removes the workspace from sidebar state) is called in the `try` block and `clearDeleting(workspaceId)` runs afterwards in `finally`. In React 18 with automatic batching these two state updates will almost always be coalesced into one render — so the row would unmount without ever becoming visible again.

However, if `onDeleted` performs an asynchronous step before actually updating sidebar state (e.g. an async removal from a store), the `clearDeleting` call could land first in a separate render tick. At that instant `isDeleting(id)` returns `false` while the workspace is still present in the sidebar list, causing the row to flash visible for one frame before the sidebar update unmounts it.

If `onDeleted` is always synchronous this is a non-issue, but it may be worth moving `clearDeleting` inside the `try` block (before `onDeleted`) so the sequence is deterministically: row hidden → mark not-deleting → unmount due to onDeleted. The current code is safe as long as `onDeleted` only calls synchronous state setters.

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

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 5 files

@Kitenite Kitenite merged commit 62e1a77 into main Apr 21, 2026
6 of 7 checks passed
@Kitenite Kitenite deleted the avoid-toast.promise-for-workspace-deletion-perform branch April 21, 2026 21:29
@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

MocA-Love added a commit to MocA-Love/superset that referenced this pull request Apr 23, 2026
upstream commit 3902e3b (superset-sh#3621) が layout.tsx から MainWindowEffects wrapper
を外して AgentHooks / ScheduleFireToasts / WorkspaceInitEffects を直接 mount
するように変更していた。この変更は fork の tearoff window 前提を壊す:
tearoff でも authenticated tree が描画されるため、これら singleton エフェクト
(useDevicePresence / useCommandWatcher / tRPC subscription / workspace init
subscription) が main window と tearoff で二重に走り、agent orchestration
および workspace init が多重起動するリスクがある。

MainWindowEffects コンポーネント自体はリポジトリに残っており
(isTearoffWindow + window.shouldOwnSingletonEffects でガードする実装)、
単に layout.tsx の呼び出しだけが置き換えられていた。直接 mount を
<MainWindowEffects /> に戻すことで upstream 変更を巻き戻す。

Refs: CodeRabbit on PR #388 (Major).
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