Skip to content

fix(desktop): quickfix restore workspace deletion from disk#2908

Merged
AviPeltz merged 2 commits into
mainfrom
hotfix-workspace-deletion
Mar 26, 2026
Merged

fix(desktop): quickfix restore workspace deletion from disk#2908
AviPeltz merged 2 commits into
mainfrom
hotfix-workspace-deletion

Conversation

@AviPeltz
Copy link
Copy Markdown
Collaborator

@AviPeltz AviPeltz commented Mar 26, 2026

Summary

Fixes critical bug introduced in PR #2573 where workspace deletion only removed database records but left worktree files on disk, causing:

  • Disk space waste
  • UI showing workspace deleted but files remain
  • Inability to recreate workspaces with same branch name

Root Cause

listExternalWorktrees() was misnamed and returned ALL git worktrees instead of only external ones. The delete safety check found the worktree being deleted in this list and incorrectly skipped disk deletion.

Changes

  1. Renamed function: listExternalWorktreeslistAllGitWorktrees (accurate name)
  2. Created proper listExternalWorktrees(mainRepoPath, projectId):
    • Queries database for tracked worktrees
    • Returns only git worktrees NOT in database
    • Provides true "external worktrees" functionality
  3. Removed createdBySuperset flag gating:
    • Users can now delete imported external worktrees
    • Safety check via listExternalWorktrees prevents deleting untracked worktrees
  4. Updated all call sites to pass projectId parameter
  5. Extracted normalizePath helper to git.ts for reuse across modules
  6. Simplified git-status.ts to use new implementation (removed duplicate logic)

Testing

  • ✅ All typechecks pass
  • ✅ Existing tests updated (external-worktree-import.test.ts)
  • ✅ Logic verified: tracked worktrees deleted from disk, external worktrees preserved

Files Changed

  • apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts - Core changes
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts - Deletion logic fixed
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts - Updated caller
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts - Simplified
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-creation.ts - Updated caller
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/external-worktree-import.test.ts - Test updated

Summary by cubic

Restores on-disk deletion for Desktop workspaces and adds error handling to avoid stuck “deleting” states. Fix prevents leftover worktree files, frees disk space, and lets users recreate workspaces with the same branch.

  • Bug Fixes
    • Correct external worktree detection by adding listAllGitWorktrees(mainRepoPath) and a proper listExternalWorktrees(mainRepoPath, projectId) that excludes DB-tracked paths.
    • Remove createdBySuperset gating so DB-tracked (including imported) worktrees are deleted from disk; untracked externals are preserved with analytics logging.
    • Wrap deletion flows in try/catch to clear deleting status and return clear errors, preventing stuck “deleting” states.
    • Extract normalizePath to git.ts, update call sites to pass projectId, and simplify git-status to the new API. Tests updated.

Written for commit 4a21fb2. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Improved external worktree detection accuracy by adding project context.
    • Safer deletion flow: disk deletions now rely on actual on-disk detection and include error handling with clear failure responses and logging.
    • Consistent path comparisons to avoid mistaken mismatches.
  • Refactor

    • Streamlined worktree enumeration to avoid redundant lookups and improve performance.
    • Centralized path normalization for reliable matching across operations.

Fixes bug introduced in PR #2573 where workspace deletion only removed
database records but left worktree files on disk.

**Root cause:**
- `listExternalWorktrees()` was misnamed and returned ALL git worktrees
- Delete logic used this for safety checks, finding the worktree being
  deleted in the "external" list
- This caused disk deletion to be skipped incorrectly

**Changes:**
1. Renamed `listExternalWorktrees` → `listAllGitWorktrees` (accurate name)
2. Created proper `listExternalWorktrees(mainRepoPath, projectId)`:
   - Queries database for tracked worktrees
   - Returns only git worktrees NOT in database
   - Provides true "external worktrees" list
3. Removed `createdBySuperset` flag gating from deletion logic:
   - Users can now delete imported external worktrees
   - Safety check via `listExternalWorktrees` prevents deleting
     worktrees that haven't been imported
4. Updated all call sites to pass `projectId` parameter
5. Extracted `normalizePath` helper to git.ts for reuse
6. Simplified git-status.ts to use new implementation

**Testing:**
- All typechecks pass
- Existing tests updated (external-worktree-import.test.ts)
- Logic verified: tracked worktrees deleted, external preserved
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Refactors worktree discovery and matching: splits listing into all-vs-external functions, adds path normalization, changes callers to pass project.id for DB-aware filtering, and updates deletion logic to rely on external-detection + existence checks (removing prior createdBySuperset gating).

Changes

Cohort / File(s) Summary
Procedure Callsite Updates
apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts, apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts, apps/desktop/src/lib/trpc/routers/workspaces/procedures/workspace-creation.ts
Call sites updated to pass project.id as second arg to listExternalWorktrees; logic now relies on DB-aware external detection rather than local tracked-path exclusions.
Deletion Procedure Changes
apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
Removed file-local path normalization; now imports shared normalizePath. Deletion gating no longer uses createdBySuperset; uses isActuallyExternal and path existence. Added error handling around filesystem deletions and normalized path comparisons for detection.
Git/Worktree Utilities
apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
Introduced normalizePath; split prior function into listAllGitWorktrees (all on-disk worktrees) and listExternalWorktrees(mainRepoPath, projectId) (filters out DB-tracked paths). Adjusted logging/message wording.
Tests
apps/desktop/src/lib/trpc/routers/workspaces/procedures/external-worktree-import.test.ts
Test renamed/retargeted to call listAllGitWorktrees and assert against returned all-worktrees data instead of pre-filtered external list.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Procedure as Workspaces Procedure
  participant GitUtils as git utils
  participant DB as localDb
  participant FS as Filesystem

  Client->>Procedure: request (list/import/delete)
  Procedure->>GitUtils: listAllGitWorktrees(mainRepoPath)
  GitUtils->>FS: run `git worktree list --porcelain`
  GitUtils-->>Procedure: allWorktrees
  Procedure->>DB: query worktrees where projectId = project.id
  DB-->>Procedure: trackedWorktreePaths
  Procedure->>GitUtils: listExternalWorktrees(mainRepoPath, project.id)
  GitUtils-->>Procedure: externalWorktrees (allWorktrees - trackedPaths, normalized)
  alt Deletion requested
    Procedure->>FS: remove path (if isActuallyExternal && exists)
    FS-->>Procedure: success / error
    Procedure-->>Client: { success: bool, error? }
  else Import/list requested
    Procedure-->>Client: filtered externalWorktrees
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through paths both new and old,
Canonicalized each tale retold,
I sniffed the DB, then checked the tree,
Clean deletions and imports—joy for me! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(desktop): quickfix restore workspace deletion from disk' directly relates to the main change: fixing a critical bug where workspace deletion no longer deletes worktree files from disk. It clearly summarizes the primary fix.
Description check ✅ Passed The description is comprehensive and well-structured, including all required sections: summary of the bug, root cause analysis, detailed change list, testing status, and affected files. It clearly explains the problem, solution, and verification steps.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 hotfix-workspace-deletion

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.

@AviPeltz AviPeltz changed the title fix(desktop): restore workspace deletion from disk fix(desktop): quickfix restore workspace deletion from disk Mar 26, 2026
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.

1 issue found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts">

<violation number="1" location="apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts:259">
P1: Handle `listExternalWorktrees` failures here; otherwise a thrown git error can abort deletion after `markWorkspaceAsDeleting`, leaving the workspace stuck as "deleting".</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 26, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

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.

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/lib/trpc/routers/workspaces/procedures/external-worktree-import.test.ts (1)

115-132: ⚠️ Potential issue | 🟠 Major

Add test coverage for listExternalWorktrees with database filtering logic.

The new listExternalWorktrees(mainRepoPath, projectId) function—which filters worktrees by project ID and uses normalizePath for path comparison—lacks test coverage. This function contains the core DB-filtering logic introduced in this PR and should be tested to verify:

  1. It correctly returns only worktrees NOT in the database for the given projectId
  2. Path normalization handles edge cases (symlinks, relative paths)
  3. It filters external worktrees appropriately when some are tracked in the database

Additionally, consider using a static import in the test at line 120 instead of dynamic import to be consistent with other tests in git.test.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/lib/trpc/routers/workspaces/procedures/external-worktree-import.test.ts`
around lines 115 - 132, Add unit tests for listExternalWorktrees to cover the
DB-filtering and path-normalization behavior: write a test that uses the
existing test helper createExternalWorktree to create multiple external
worktrees, seed the test DB with one tracked worktree for the target projectId,
then call listExternalWorktrees(mainRepoPath, projectId) and assert it returns
only worktrees not present in the DB; add another test to exercise normalizePath
edge cases (symlink/relative path variants) to ensure comparisons treat
equivalent paths as equal; also replace the dynamic import of
listAllGitWorktrees with a static import (consistent with other tests) and
import listExternalWorktrees and listAllGitWorktrees from ../utils/git so tests
verify both listing and filtered results.
🧹 Nitpick comments (1)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts (1)

866-880: Redundant trackedPaths check after API update.

listExternalWorktrees now filters out tracked worktrees internally (by querying the DB for projectId). The local trackedPaths.has(wt.path) check at line 878 is now redundant since external worktrees returned by the function are guaranteed not to be in the DB.

However, since the filtering uses normalizePath internally but this check compares raw paths, keeping it provides a secondary safety net for edge cases where path formats differ.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts` around
lines 866 - 880, The local trackedPaths.has(wt.path) check in the
externalWorktrees filter is redundant because listExternalWorktrees already
excludes DB-tracked worktrees; remove the trackedPaths set and the
trackedPaths.has(wt.path) branch from the filter in the block that builds
externalWorktrees (referencing listExternalWorktrees, projectWorktrees,
externalWorktrees) so you rely on the updated API behavior and avoid duplicate
logic—if you want an extra safety net instead, compare normalized paths using
the same normalizePath logic used inside listExternalWorktrees rather than raw
wt.path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@apps/desktop/src/lib/trpc/routers/workspaces/procedures/external-worktree-import.test.ts`:
- Around line 115-132: Add unit tests for listExternalWorktrees to cover the
DB-filtering and path-normalization behavior: write a test that uses the
existing test helper createExternalWorktree to create multiple external
worktrees, seed the test DB with one tracked worktree for the target projectId,
then call listExternalWorktrees(mainRepoPath, projectId) and assert it returns
only worktrees not present in the DB; add another test to exercise normalizePath
edge cases (symlink/relative path variants) to ensure comparisons treat
equivalent paths as equal; also replace the dynamic import of
listAllGitWorktrees with a static import (consistent with other tests) and
import listExternalWorktrees and listAllGitWorktrees from ../utils/git so tests
verify both listing and filtered results.

---

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts`:
- Around line 866-880: The local trackedPaths.has(wt.path) check in the
externalWorktrees filter is redundant because listExternalWorktrees already
excludes DB-tracked worktrees; remove the trackedPaths set and the
trackedPaths.has(wt.path) branch from the filter in the block that builds
externalWorktrees (referencing listExternalWorktrees, projectWorktrees,
externalWorktrees) so you rely on the updated API behavior and avoid duplicate
logic—if you want an extra safety net instead, compare normalized paths using
the same normalizePath logic used inside listExternalWorktrees rather than raw
wt.path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae3e4b6b-26b2-4ee9-b3c9-e1cb8d5a4ed3

📥 Commits

Reviewing files that changed from the base of the PR and between a256303 and c1d826f.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/external-worktree-import.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-creation.ts

Wraps worktree deletion logic in try-catch to ensure workspace deleting
status is properly cleared if listExternalWorktrees() or other operations
fail.

**Problem:**
- If listExternalWorktrees() throws (git error, DB error, etc.), the
  workspace remains stuck in 'deleting' status
- User cannot retry deletion or interact with workspace
- Requires manual DB cleanup or app restart to recover

**Solution:**
- Wrap deletion section in outer try-catch
- Catch any errors from listExternalWorktrees and related operations
- Always call clearWorkspaceDeletingStatus() on error
- Return proper error response instead of crashing

**Changes:**
- workspace delete procedure: added error boundary around disk deletion
- deleteWorktree procedure: added error boundary for consistency

**Impact:**
- Prevents workspace from getting stuck in deleting state
- Provides clear error messages to user
- Allows user to retry deletion after fixing issues
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/lib/trpc/routers/workspaces/procedures/delete.ts (1)

546-560: Consider more granular error handling for DB operations.

If deleteWorktreeRecord or hideProjectIfNoWorkspaces throws after successful disk deletion, the catch block returns "Failed to delete worktree" which could be misleading—the worktree may already be removed from disk.

This is a minor edge case, but you could consider separating disk and DB error handling for clearer diagnostics:

💡 Optional: More specific error handling
         } finally {
             workspaceInitManager.releaseProjectLock(project.id);
         }

-        deleteWorktreeRecord(input.worktreeId);
-        hideProjectIfNoWorkspaces(worktree.projectId);
-
-        track("worktree_deleted", { worktree_id: input.worktreeId });
-
-        return { success: true };
+        try {
+            deleteWorktreeRecord(input.worktreeId);
+            hideProjectIfNoWorkspaces(worktree.projectId);
+            track("worktree_deleted", { worktree_id: input.worktreeId });
+            return { success: true };
+        } catch (dbError) {
+            console.error(
+                `[worktree/delete] DB cleanup failed after disk deletion:`,
+                dbError instanceof Error ? dbError.message : String(dbError),
+            );
+            // Return success since disk deletion worked, but log the DB issue
+            return { success: true, warning: "Worktree deleted but database cleanup encountered an error" };
+        }
     } catch (error) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts` around
lines 546 - 560, Separate disk and DB error handling so failures after disk
deletion are reported accurately: keep the existing disk-removal logic and track
call as-is, then wrap deleteWorktreeRecord and hideProjectIfNoWorkspaces in
their own try/catch (or check their results) so DB errors are logged and
returned with a distinct message (e.g., "disk_deleted_db_failed") instead of
"Failed to delete worktree"; ensure track("worktree_deleted", ...) is only
called after disk removal, and return success:true when disk removal succeeded
but include a field or error string describing DB-only failures from
deleteWorktreeRecord / hideProjectIfNoWorkspaces.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts`:
- Around line 546-560: Separate disk and DB error handling so failures after
disk deletion are reported accurately: keep the existing disk-removal logic and
track call as-is, then wrap deleteWorktreeRecord and hideProjectIfNoWorkspaces
in their own try/catch (or check their results) so DB errors are logged and
returned with a distinct message (e.g., "disk_deleted_db_failed") instead of
"Failed to delete worktree"; ensure track("worktree_deleted", ...) is only
called after disk removal, and return success:true when disk removal succeeded
but include a field or error string describing DB-only failures from
deleteWorktreeRecord / hideProjectIfNoWorkspaces.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91e88349-47c6-4cfd-b58e-3636977a4751

📥 Commits

Reviewing files that changed from the base of the PR and between c1d826f and 4a21fb2.

📒 Files selected for processing (1)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts

@AviPeltz AviPeltz requested a review from Kitenite March 26, 2026 20:22
@AviPeltz AviPeltz merged commit a2268c9 into main Mar 26, 2026
15 checks passed
AviPeltz added a commit that referenced this pull request Mar 26, 2026
AviPeltz added a commit that referenced this pull request Mar 27, 2026
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