Skip to content

fix(desktop): prevent workspace from reappearing during deletion#702

Closed
Kitenite wants to merge 0 commit intomainfrom
deleting-with-open-threads
Closed

fix(desktop): prevent workspace from reappearing during deletion#702
Kitenite wants to merge 0 commit intomainfrom
deleting-with-open-threads

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Jan 10, 2026

Summary

  • Fix bug where workspaces would briefly disappear then reappear in the UI during deletion
  • Add deletingAt timestamp field to immediately hide workspaces when deletion starts
  • Simplify optimistic update logic from 148 lines to 56 lines

Problem

When deleting a workspace:

  1. Optimistic update removed it from the UI cache
  2. Slow git operations (teardown scripts, worktree removal) ran on server
  3. A refetch occurred (window focus, polling, etc.)
  4. Workspace reappeared because it still existed in the database
  5. Finally disappeared when deletion completed

Solution

Added a deletingAt timestamp field that is set immediately when deletion starts, before any slow operations. All workspace queries now filter out entries with deletingAt set, so even if refetches occur mid-deletion, the workspace stays hidden.

Changes

File Change
packages/local-db/src/schema/schema.ts Added deletingAt field
packages/local-db/drizzle/0010_*.sql Migration for new field
procedures/delete.ts Mark as deleting immediately; clear on failure
procedures/query.ts Filter out deleting workspaces
utils/db-helpers.ts Add helper functions; update queries to exclude deleting
useDeleteWorkspace.ts Simplified optimistic update (148 → 56 lines)

Test plan

  • Delete a workspace and verify it disappears immediately and doesn't reappear
  • Delete the active workspace and verify the app switches to another workspace
  • Force a deletion failure (e.g., lock a worktree file) and verify workspace reappears
  • Test with multiple workspaces being deleted in quick succession

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Workspaces now immediately hide from the UI when deletion begins, providing clearer feedback during long-running deletion operations. If deletion fails, the workspace automatically reappears.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 10, 2026

📝 Walkthrough

Walkthrough

This PR introduces a soft-delete mechanism for workspaces using a deletingAt timestamp field. Workspaces are marked as deleting immediately before long-running removal operations, filtered from queries, and the deleting status is cleared if operations fail. Database schema, server procedures, and client logic are updated to support this hiding mechanism.

Changes

Cohort / File(s) Summary
Database Schema
packages/local-db/src/schema/schema.ts, packages/local-db/drizzle/0010_add_workspace_deleting_at.sql, packages/local-db/drizzle/meta/_journal.json, packages/local-db/drizzle/meta/0010_snapshot.json
Adds deletingAt integer column to workspaces table with migration; records metadata snapshot and journal entry for version 6 of schema
Server Delete Procedure
apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
Marks workspace as deleting immediately after validation to hide from UI during long operations; clears deleting status if teardown fails for error recovery
Server Query Procedures
apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
Updates getAll, getAllGrouped, getActive procedures to filter results where deletingAt is null, preventing deleting workspaces from appearing in UI queries
Server DB Utilities
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
Adds markWorkspaceAsDeleting and clearWorkspaceDeletingStatus helpers; updates existing helpers (getMaxWorkspaceTabOrder, hideProjectIfNoWorkspaces, selectNextActiveWorkspace, getBranchWorkspace) to exclude deleting workspaces
Client Delete Hook
apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
Simplifies optimistic update logic by replacing complex per-field state handling with lightweight cache mutations; stores rollback context for error recovery via onError handler

Sequence Diagram

sequenceDiagram
    participant UI as User/UI
    participant Client as React Query Hook
    participant Server as Delete Procedure
    participant DB as Database
    
    UI->>Client: Request workspace delete
    Client->>Client: Cancel related caches (onMutate)
    Client->>Client: Remove workspace from local cache
    
    Client->>Server: Delete RPC call
    activate Server
    
    Server->>DB: Validate workspace exists
    DB-->>Server: Workspace found
    
    Server->>DB: Mark workspace as deleting (set deletingAt)
    DB-->>Server: Updated
    
    Note over Server: Long-running operations<br/>(teardown, worktree cleanup)
    
    alt Teardown Success
        Server->>DB: Remove workspace/worktree records
        DB-->>Server: Deleted
        Server-->>Client: Success
        Client->>Client: onSettled: invalidate caches
    else Teardown Fails
        Server->>DB: Clear deleting status (reset deletingAt)
        DB-->>Server: Updated
        Server-->>Client: Error
        Client->>Client: onError: restore previous cache state
        Client->>UI: Display error, workspace reappears
    end
    
    deactivate Server
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A workspace fades with gentle grace,
No sudden vanish from this place—
A deletingAt timestamp true,
Marks the path when work is through!
If errors strike, we simply heal,
And bring the workspace back—what's real? ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main fix: preventing workspace reappearance during deletion, which is the core problem being solved.
Description check ✅ Passed The PR description follows the template structure with clear sections: summary, problem statement, solution with changes table, and test plan. All required sections are adequately filled with relevant information.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

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 Jan 10, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Fly.io Electric (Fly.io) View App
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

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: 0

Caution

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

⚠️ Outside diff range comments (2)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts (1)

145-256: Prevent “stuck hidden” workspaces: clear deletingAt on any deletion failure, not just worktree removal.
Right now markWorkspaceAsDeleting() (Line 155) happens early, but clearWorkspaceDeletingStatus() is only called in the removeWorktree catch (Lines 219-220). Any other exception/throw after Line 155 can leave the row present with deletingAt != null, and your updated queries will permanently filter it out.

Proposed fix (wrap mutation in try/catch and clear deleting status on failure)
 		delete: publicProcedure
 			.input(z.object({ id: z.string() }))
 			.mutation(async ({ input }) => {
 				const workspace = getWorkspace(input.id);

 				if (!workspace) {
 					return { success: false, error: "Workspace not found" };
 				}

 				// Mark workspace as deleting IMMEDIATELY so it's hidden from UI.
 				// This prevents the workspace from reappearing if a refetch occurs
 				// during the slow git operations below.
 				markWorkspaceAsDeleting(input.id);
+
+				try {
 					// Cancel any ongoing initialization and wait for it to complete
 					// This ensures we don't race with init's git operations
 					if (workspaceInitManager.isInitializing(input.id)) {
 						console.log(
 							`[workspace/delete] Cancelling init for ${input.id}, waiting for completion...`,
 						);
 						workspaceInitManager.cancel(input.id);
 						// Wait for init to finish (up to 30s) - it will see cancellation and exit
 						await workspaceInitManager.waitForInit(input.id, 30000);
 					}
 
 					// Kill all terminal processes in this workspace first
 					const terminalResult = await terminalManager.killByWorkspaceId(
 						input.id,
 					);
 
 					const project = getProject(workspace.projectId);
 
 					let worktree: SelectWorktree | undefined;
 
 					// Branch workspaces don't have worktrees - skip worktree operations
 					if (workspace.type === "worktree" && workspace.worktreeId) {
 						worktree = getWorktree(workspace.worktreeId);
 
 						if (worktree && project) {
 							// Acquire project lock before any git operations
 							// This prevents racing with any concurrent init operations
 							await workspaceInitManager.acquireProjectLock(project.id);
 
 							try {
 								// Run teardown scripts before removing worktree
 								const exists = await worktreeExists(
 									project.mainRepoPath,
 									worktree.path,
 								);
 
 								if (exists) {
 									const teardownResult = await runTeardown(
 										project.mainRepoPath,
 										worktree.path,
 										workspace.name,
 									);
 									if (!teardownResult.success) {
 										console.error(
 											`Teardown failed for workspace ${workspace.name}:`,
 											teardownResult.error,
 										);
 									}
 								}
 
 								try {
 									if (exists) {
 										await removeWorktree(project.mainRepoPath, worktree.path);
 									} else {
 										console.warn(
 											`Worktree ${worktree.path} not found in git, skipping removal`,
 										);
 									}
 								} catch (error) {
 									const errorMessage =
 										error instanceof Error ? error.message : String(error);
 									console.error("Failed to remove worktree:", errorMessage);
 									// Clear deleting status so workspace reappears in UI
 									clearWorkspaceDeletingStatus(input.id);
 									return {
 										success: false,
 										error: `Failed to remove worktree: ${errorMessage}`,
 									};
 								}
 							} finally {
 								workspaceInitManager.releaseProjectLock(project.id);
 							}
 						}
 					}
 
 					// Proceed with DB cleanup
 					deleteWorkspace(input.id);
 
 					if (worktree) {
 						deleteWorktreeRecord(worktree.id);
 					}
 
 					if (project) {
 						hideProjectIfNoWorkspaces(workspace.projectId);
 					}
 
 					updateActiveWorkspaceIfRemoved(input.id);
 
 					const terminalWarning =
 						terminalResult.failed > 0
 							? `${terminalResult.failed} terminal process(es) may still be running`
 							: undefined;
 
 					track("workspace_deleted", { workspace_id: input.id });
 
 					// Clear init job state only after all cleanup is complete
 					// This ensures cancellation signals remain visible during cleanup
 					workspaceInitManager.clearJob(input.id);
 
 					return { success: true, terminalWarning };
+				} catch (error) {
+					// Best effort: ensure workspace becomes visible again if anything fails
+					clearWorkspaceDeletingStatus(input.id);
+					throw error;
+				}
 			}),
apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts (1)

15-43: Don’t drop options.onMutate (and its returned context).
Because you spread ...options and then override onMutate, any consumer-supplied onMutate won’t run anymore, and its context won’t flow into onError/onSettled.

Proposed fix (call + return user onMutate context)
 		onMutate: async ({ id }) => {
 			// Cancel outgoing refetches to avoid overwriting optimistic update
 			await Promise.all([
 				utils.workspaces.getAll.cancel(),
 				utils.workspaces.getAllGrouped.cancel(),
 				utils.workspaces.getActive.cancel(),
 			]);
 
+			// Preserve user onMutate behavior/context
+			const userContext = await options?.onMutate?.({ id } as never);
+
 			// Optimistically remove workspace from caches for instant feedback
 			utils.workspaces.getAll.setData(undefined, (old) =>
 				old?.filter((w) => w.id !== id),
 			);
 
 			utils.workspaces.getAllGrouped.setData(undefined, (old) =>
 				old
 					?.map((group) => ({
 						...group,
 						workspaces: group.workspaces.filter((w) => w.id !== id),
 					}))
 					.filter((group) => group.workspaces.length > 0),
 			);
 
 			// Clear active if it's the one being deleted - server will select next
 			utils.workspaces.getActive.setData(undefined, (old) =>
 				old?.id === id ? null : old,
 			);
+
+			return userContext;
 		},
🧹 Nitpick comments (4)
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (3)

28-43: Consider selecting only needed columns (avoids full-row scans).
getMaxWorkspaceTabOrder() only needs tabOrder, but selects entire workspace rows. Same logic, smaller payload.


90-105: Consider LIMIT 1 existence checks for “has any workspaces”.
hideProjectIfNoWorkspaces() only needs to know whether at least one non-deleting workspace exists.


214-237: New helpers are fine; consider returning affected-row count for observability.
Right now these are fire-and-forget void. Returning .run() result (or checking changes) can help detect “id not found” / unexpected states when debugging deletion issues.

apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts (1)

44-54: Invalidate-on-settled is a solid simplification; consider narrowing invalidation scope (optional).
utils.workspaces.invalidate() may refetch more than needed vs invalidating getAll/getAllGrouped/getActive.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca47593 and 87e2f33.

📒 Files selected for processing (8)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
  • packages/local-db/drizzle/0010_add_workspace_deleting_at.sql
  • packages/local-db/drizzle/meta/0010_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/src/schema/schema.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • packages/local-db/src/schema/schema.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • packages/local-db/src/schema/schema.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
🧠 Learnings (3)
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to packages/db/src/**/*.ts : Schema definitions must be in packages/db/src/ using Drizzle ORM

Applied to files:

  • packages/local-db/src/schema/schema.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/api/**/*.{ts,tsx} : Extract business logic from tRPC procedures into utility functions when logic exceeds ~50 lines, is used by multiple procedures, or needs independent testing

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/**/*.{ts,tsx} : Use Drizzle ORM for all database operations - never use raw SQL

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
🧬 Code graph analysis (3)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts (1)
packages/local-db/src/schema/schema.ts (1)
  • workspaces (80-121)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts (1)
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (2)
  • markWorkspaceAsDeleting (219-225)
  • clearWorkspaceDeletingStatus (231-237)
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (82-82)
packages/local-db/src/schema/schema.ts (1)
  • workspaces (80-121)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Deploy Admin
  • GitHub Check: Deploy Docs
  • GitHub Check: Deploy Marketing
  • GitHub Check: Deploy API
  • GitHub Check: Deploy Web
  • GitHub Check: Build
🔇 Additional comments (12)
packages/local-db/drizzle/0010_add_workspace_deleting_at.sql (1)

1-1: Migration looks correct for nullable “deletion in progress” sentinel. deleting_at being nullable aligns with the IS NULL filters. Consider (optional) indexing only if the table is expected to grow large.

packages/local-db/drizzle/meta/_journal.json (1)

74-81: Drizzle journal entry looks consistent (idx/tag progression).

packages/local-db/src/schema/schema.ts (1)

105-109: Schema addition is aligned (nullable deletingAt timestamp). This should work cleanly with isNull(workspaces.deletingAt) filtering and rollback-on-failure by clearing the column.

apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts (3)

1-3: Import change is fine; avoid unused operators drift. and/isNull are now used; just ensure isNotNull is still needed elsewhere in this module after the change.


24-31: Filtering out deleting workspaces should fix the “reappears during deletion” refetch. Looks correct using isNull(workspaces.deletingAt) in both list endpoints.

Also applies to: 89-95


121-143: No action required—null handling is already well-implemented across consumers and the get/getActive asymmetry is intentional.

Consumers of getActive handle null safely via optional chaining throughout the codebase (e.g., activeWorkspace?.id, activeWorkspace?.projectId). The useCloseWorkspace mutation explicitly expects and sets getActive to null as a valid state. No regressions detected.

The filtering difference between getActive (excludes deleting workspaces) and get/getWorkspace (includes deleting) is correct by design: getActive is UI-facing and must never return a mid-deletion workspace, while get is backend-facing and correctly allows the delete procedure to query the workspace state during the deletion flow (after markWorkspaceAsDeleting is called).

packages/local-db/drizzle/meta/0010_snapshot.json (1)

749-836: Snapshot correctly reflects nullable workspaces.deleting_at. As long as this file is generated by your migration tooling, this looks consistent with the schema + migration.

apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts (1)

7-17: Imports look consistent with the new deletingAt flow.

apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (3)

10-10: Good: isNull(workspaces.deletingAt) filtering is now available where needed.


107-120: Next-active selection excluding deleting workspaces matches the new semantics.


253-272: Branch workspace query correctly excludes deleting rows.

apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts (1)

3-9: Doc comment matches the new server-driven deletingAt approach.

@Kitenite Kitenite force-pushed the deleting-with-open-threads branch from 87e2f33 to 4398f7b Compare January 10, 2026 00:48
@Kitenite Kitenite closed this Jan 10, 2026
@Kitenite Kitenite force-pushed the deleting-with-open-threads branch from f4665ac to ca47593 Compare January 10, 2026 01:39
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: 0

🧹 Nitpick comments (5)
apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts (2)

14-37: Consider returning previous state from onMutate as a React Query best practice.

The optimistic updates correctly remove the workspace from all three caches. However, React Query convention suggests capturing and returning the previous state from onMutate as context, even if the current rollback strategy relies on invalidation. This improves debuggability and provides flexibility for future enhancements.

♻️ Suggested enhancement
 onMutate: async ({ id }) => {
   await Promise.all([
     utils.workspaces.getAll.cancel(),
     utils.workspaces.getAllGrouped.cancel(),
     utils.workspaces.getActive.cancel(),
   ]);

+  const previousAll = utils.workspaces.getAll.getData();
+  const previousGrouped = utils.workspaces.getAllGrouped.getData();
+  const previousActive = utils.workspaces.getActive.getData();
+
   utils.workspaces.getAll.setData(undefined, (old) =>
     old?.filter((w) => w.id !== id),
   );

   utils.workspaces.getAllGrouped.setData(undefined, (old) =>
     old
       ?.map((group) => ({
         ...group,
         workspaces: group.workspaces.filter((w) => w.id !== id),
       }))
       .filter((group) => group.workspaces.length > 0),
   );

   utils.workspaces.getActive.setData(undefined, (old) =>
     old?.id === id ? null : old,
   );
+
+  return { previousAll, previousGrouped, previousActive };
 },

45-47: Consider adding error logging for debugging failed deletions.

The error handler correctly delegates to the user's callback, and rollback occurs via invalidation in onSettled. For improved observability when troubleshooting deletion failures, consider adding a prefixed console log before delegating.

♻️ Suggested enhancement
 onError: async (...args) => {
+  const [error, variables] = args;
+  console.error('[workspaces/delete] Failed to delete workspace:', {
+    id: variables.id,
+    error,
+  });
   await options?.onError?.(...args);
 },
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (3)

28-43: deletingAt filter is correct; consider selecting only tabOrder to avoid pulling full rows.
On Line 33-39, you can limit the payload to { tabOrder: workspaces.tabOrder } (and/or a DB-side max) since only tabOrder is used.


107-120: Filter is correct; optional: select only id and add a stable tiebreaker.
Line 113-119 can be reduced to selecting workspaces.id only; optionally add a second orderBy (e.g., desc(workspaces.updatedAt) or desc(workspaces.id)) for deterministic results on timestamp ties.


214-230: Good helpers; consider making markWorkspaceAsDeleting idempotent (don’t overwrite existing deletingAt).
If you want deletingAt to mean “when deletion started”, gate the update with isNull(workspaces.deletingAt) on Line 219.

Proposed diff (idempotent mark)
 export function markWorkspaceAsDeleting(workspaceId: string): void {
 	localDb
 		.update(workspaces)
 		.set({ deletingAt: Date.now() })
-		.where(eq(workspaces.id, workspaceId))
+		.where(and(eq(workspaces.id, workspaceId), isNull(workspaces.deletingAt)))
 		.run();
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55579de and f4665ac.

📒 Files selected for processing (2)
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
🧰 Additional context used
📓 Path-based instructions (5)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
🧠 Learnings (2)
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/**/*.{ts,tsx} : Use Drizzle ORM for all database operations - never use raw SQL

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to packages/db/src/**/*.ts : Schema definitions must be in packages/db/src/ using Drizzle ORM

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
🧬 Code graph analysis (1)
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (82-82)
packages/local-db/src/schema/schema.ts (1)
  • workspaces (80-119)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Deploy API
  • GitHub Check: Deploy Admin
  • GitHub Check: Deploy Web
  • GitHub Check: Deploy Docs
  • GitHub Check: Deploy Marketing
  • GitHub Check: Build
🔇 Additional comments (4)
apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts (1)

4-5: Simplified rollback approach works, but AI summary is inconsistent.

The comment correctly describes the server-side deletingAt mechanism. However, the AI summary claims this file "uses stored rollback context to restore prior cache states" in onError, but the actual implementation relies on query invalidation in onSettled for rollback—no explicit context restoration occurs. The simplified approach (relying on utils.workspaces.invalidate() to trigger refetches after the server clears deletingAt on error) is valid and reduces complexity as intended.

apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (3)

10-10: Import update looks correct (isNull used for deletingAt filters).
This stays within Drizzle (no raw SQL), consistent with repo guidelines/learnings.


246-265: Branch workspace query now correctly excludes deleting rows.
This matches the “hide during deletion” model and should prevent branch workspace flicker during slow teardown.


90-105: This review comment is based on code that has been reverted and is no longer in the codebase.

The code snippet shown in the review includes isNull(workspaces.deletingAt) filter, but the current implementation of hideProjectIfNoWorkspaces (lines 92-101) contains no such filter. Additionally, the workspaces table schema does not have a deletingAt column.

Git history shows commit 55579de added deletingAt logic and deletion rollback handling, but this was reverted by commit f4665ac. The current codebase has neither the deletingAt column nor the filtering logic, so the concern about projects staying hidden after a failed deletion rollback is not applicable.

Likely an incorrect or invalid review comment.

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