Skip to content

Delete orphaned worktree when removing a thread#23

Merged
juliusmarminge merged 6 commits intomainfrom
codething/b0e2dc22
Feb 13, 2026
Merged

Delete orphaned worktree when removing a thread#23
juliusmarminge merged 6 commits intomainfrom
codething/b0e2dc22

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 13, 2026

Summary

  • Prompt for worktree deletion when deleting a thread that is the sole reference to that worktree path.
  • Keep thread deletion non-blocking by making worktree removal best-effort and swallowing removal errors.
  • Add getOrphanedWorktreePathForThread helper to detect whether a worktree is shared by other threads.
  • Add unit tests covering missing thread, missing worktree path, shared worktree, and unique worktree scenarios.

Testing

  • Added apps/web/src/worktreeCleanup.test.ts with 5 Vitest cases for orphaned worktree detection behavior.
  • Not run: project lint scripts.
  • Not run: test suite in local environment.

Open with Devin

Summary by CodeRabbit

  • New Features

    • Thread deletion now offers confirmation and will attempt best-effort removal of orphaned worktrees.
    • Desktop confirmation dialogs added for destructive actions, available via the app bridge with browser fallback.
    • Worktree paths are now formatted for clearer display in the UI.
  • Tests

    • Added tests for orphaned worktree detection and desktop confirmation dialog behavior.

- Prompt to remove the worktree when a deleted thread is its only reference
- Best-effort cleanup via `api.git.removeWorktree` after thread deletion
- Add `getOrphanedWorktreePathForThread` helper with unit tests
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 13, 2026

Walkthrough

Adds utilities to detect orphaned Git worktree paths and format them, integrates a desktop confirmation dialog (IPC, preload bridge, main handler), updates renderer native API, uses confirmation and best-effort worktree removal when deleting threads, and adds tests for utilities and the dialog.

Changes

Cohort / File(s) Summary
Worktree cleanup utility & tests
apps/web/src/worktreeCleanup.ts, apps/web/src/worktreeCleanup.test.ts
New exports getOrphanedWorktreePathForThread and formatWorktreePathForDisplay with internal normalization; tests cover missing thread/worktree, uniqueness, shared paths, path formatting (unix/windows/outside paths, trailing slashes).
Sidebar integration (web)
apps/web/src/components/Sidebar.tsx
Imports cleanup helpers; on thread delete computes threadProject, orphanedWorktreePath, displayWorktreePath, derives canDeleteWorktree/shouldDeleteWorktree with user confirmation, calls git.removeWorktree best-effort (errors ignored). Extends dependency arrays to include state.projects.
Desktop confirm dialog & tests
apps/desktop/src/confirmDialog.ts, apps/desktop/src/confirmDialog.test.ts
Adds showDesktopConfirmDialog(message, ownerWindow) that trims/validates message and shows a No/Yes dialog; tests mock dialog.showMessageBox for empty message, focused window, and no-window scenarios.
Main IPC & preload bridge
apps/desktop/src/main.ts, apps/desktop/src/preload.ts
Registers IPC channel desktop:confirm in main, delegating to showDesktopConfirmDialog; exposes desktopBridge.confirm(message): Promise<boolean> via preload.
Renderer/native API surface & contracts
apps/web/src/wsNativeApi.ts, apps/web/src/vite-env.d.ts, packages/contracts/src/ipc.ts
Adds dialogs.confirm(message): Promise<boolean> to contracts and DesktopBridge typing; wsNativeApi delegates to window.desktopBridge.confirm when available, else falls back to window.confirm.
Other
package.json
Minor manifest change recorded.

Sequence Diagram(s)

sequenceDiagram
  participant Renderer as Renderer (Sidebar)
  participant Utils as Worktree Utils
  participant Native as NativeApi (wsNativeApi)
  participant Preload as Preload (desktopBridge)
  participant Main as Main (IPC handler)
  participant Dialog as Electron Dialog
  participant Git as Git API

  Renderer->>Utils: getOrphanedWorktreePathForThread(threads, threadId)
  Utils-->>Renderer: orphanedPath (string|null)
  alt orphanedPath exists
    Renderer->>Native: dialogs.confirm("Delete worktree?")
    Native->>Preload: window.desktopBridge.confirm(message)
    Preload->>Main: IPC "desktop:confirm" with message
    Main->>Dialog: showDesktopConfirmDialog(message, ownerWindow)
    Dialog-->>Main: userChoice (Cancel|Confirm)
    Main-->>Preload: boolean result
    Preload-->>Native: boolean result
    Native-->>Renderer: confirmed?
    alt confirmed
      Renderer->>Git: git.removeWorktree({cwd: projectCwd, path: orphanedPath})
      Git-->>Renderer: success|error (ignored)
    end
  end
  Renderer->>Renderer: delete thread (always proceeds)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (7 files):

⚔️ apps/desktop/src/main.ts (content)
⚔️ apps/desktop/src/preload.ts (content)
⚔️ apps/web/src/components/ChatView.tsx (content)
⚔️ apps/web/src/components/Sidebar.tsx (content)
⚔️ apps/web/src/vite-env.d.ts (content)
⚔️ apps/web/src/wsNativeApi.ts (content)
⚔️ packages/contracts/src/ipc.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Delete orphaned worktree when removing a thread' accurately summarizes the primary change: implementing deletion of unused worktrees when threads are removed.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/b0e2dc22
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch codething/b0e2dc22
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉


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

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Feb 13, 2026

Prompt to delete an orphaned worktree when a thread is deleted and add desktopBridge.confirm with IPC-backed native confirm dialog in Sidebar.tsx and desktop app

Add dialogs.confirm(message) across desktop and web, implement native confirm via desktop:confirm IPC, and use it in thread deletion to optionally remove a uniquely-referenced worktree. Core logic lives in showDesktopConfirmDialog in confirmDialog.ts, IPC wiring in main.ts and preload.ts, and worktree checks in worktreeCleanup.ts.

📍Where to Start

Start with Sidebar.handleThreadContextMenu in Sidebar.tsx, then review getOrphanedWorktreePathForThread in worktreeCleanup.ts and showDesktopConfirmDialog in confirmDialog.ts.


Macroscope summarized e5b69a3.

Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/components/Sidebar.tsx`:
- Around line 305-312: The current call to api.git.removeWorktree uses
orphanedWorktreePath as a fallback for cwd which is invalid because git worktree
remove expects a repository root or another worktree; change the logic so you
only call api.git.removeWorktree when threadProject is present and use
threadProject.cwd as the cwd, and if threadProject is undefined either skip the
removal attempt or log a warning (e.g., use console.warn/processLogger.warn)
instead of passing orphanedWorktreePath as cwd; keep the existing try/catch
around the removal to ensure deletion remains best-effort.
🧹 Nitpick comments (2)
apps/web/src/worktreeCleanup.test.ts (1)

48-52: Consider adding a test for whitespace normalization.

Since normalizeWorktreePath trims input, a test verifying that paths differing only by whitespace are treated as equal would strengthen coverage.

🧪 Optional test case
+  it("treats whitespace-padded paths as equal", () => {
+    const threads = [
+      makeThread({ id: "thread-1", worktreePath: "/tmp/repo/worktrees/feature-a" }),
+      makeThread({ id: "thread-2", worktreePath: "  /tmp/repo/worktrees/feature-a  " }),
+    ];
+    const result = getOrphanedWorktreePathForThread(threads, "thread-1");
+    expect(result).toBeNull();
+  });
apps/web/src/components/Sidebar.tsx (1)

266-277: Blocking UI with window.confirm during an async handler.

Using window.confirm is functional but blocks the main thread and provides a dated UX. This is acceptable for an MVP but consider replacing with a non-blocking modal in the future for better user experience.

Comment thread apps/web/src/components/Sidebar.tsx
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 13, 2026

Greptile Overview

Greptile Summary

This PR adds automatic cleanup of orphaned git worktrees when deleting threads. When a thread is the only reference to a worktree, the user is prompted to delete the worktree along with the thread.

The implementation adds a getOrphanedWorktreePathForThread helper that checks if other threads share the same worktree path, preventing accidental deletion of shared worktrees. Thread deletion remains non-blocking by making worktree removal best-effort (errors are swallowed).

Key changes:

  • Added worktree orphan detection logic with path normalization
  • Integrated confirmation dialog into thread deletion flow in Sidebar
  • Added comprehensive unit tests covering all edge cases
  • Made worktree deletion best-effort to avoid blocking thread deletion

Issues found:

  • Potential edge case when threadProject is undefined could cause worktree removal to fail or behave unexpectedly

Confidence Score: 3/5

  • This PR is mostly safe but has an edge case that should be addressed before merging
  • The core logic for detecting orphaned worktrees is solid and well-tested. However, there's a logical issue in Sidebar.tsx where an undefined threadProject could cause problems with worktree removal. The best-effort error handling partially mitigates this, but the edge case should be handled explicitly to avoid attempting git operations with an invalid cwd.
  • Pay close attention to apps/web/src/components/Sidebar.tsx - specifically the handling of undefined threadProject in the worktree removal logic

Important Files Changed

Filename Overview
apps/web/src/worktreeCleanup.ts Adds helper to detect orphaned worktrees. Implementation is clean with proper path normalization and edge case handling.
apps/web/src/worktreeCleanup.test.ts Comprehensive test coverage for getOrphanedWorktreePathForThread with 5 test cases covering all edge cases.
apps/web/src/components/Sidebar.tsx Integrates worktree cleanup into thread deletion flow. Has potential issue with undefined threadProject handling.

Sequence Diagram

sequenceDiagram
    participant User
    participant Sidebar
    participant getOrphanedWorktreePathForThread
    participant API
    participant Git

    User->>Sidebar: Right-click thread, select Delete
    Sidebar->>Sidebar: Find thread in state.threads
    Sidebar->>Sidebar: Find threadProject in state.projects
    Sidebar->>getOrphanedWorktreePathForThread: Check if worktree is orphaned
    getOrphanedWorktreePathForThread->>getOrphanedWorktreePathForThread: Find target thread
    getOrphanedWorktreePathForThread->>getOrphanedWorktreePathForThread: Check if worktreePath exists
    getOrphanedWorktreePathForThread->>getOrphanedWorktreePathForThread: Check if other threads share same worktreePath
    getOrphanedWorktreePathForThread-->>Sidebar: Return orphanedWorktreePath or null
    
    alt orphanedWorktreePath exists
        Sidebar->>User: Show confirmation dialog
        User-->>Sidebar: Confirm or cancel
    end
    
    Sidebar->>API: providers.stopSession (if session active)
    Sidebar->>API: terminal.close (if terminal open)
    Sidebar->>Sidebar: dispatch DELETE_THREAD
    
    alt shouldDeleteWorktree is true
        Sidebar->>API: git.removeWorktree(cwd, path)
        API->>Git: Execute git worktree remove
        Git-->>API: Success or error
        Note over Sidebar: Errors are swallowed (best-effort)
    end
Loading

Last reviewed commit: 6d9a467

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated
juliusmarminge and others added 2 commits February 12, 2026 22:42
- add `dialogs.confirm` to native IPC/contracts and desktop preload/main bridge
- switch sidebar worktree deletion prompt from `window.confirm` to native API
- add `showDesktopConfirmDialog` helper with unit tests for dialog behavior
Co-authored-by: codex <codex@users.noreply.github.com>
Copy link
Copy Markdown

@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: 2

🤖 Fix all issues with AI agents
In `@apps/desktop/src/main.ts`:
- Around line 147-155: Wrap the ipcMain handler for CONFIRM_CHANNEL so any
rejection from showDesktopConfirmDialog is caught and results in a safe false
return; specifically, update the ipcMain.handle callback (which currently checks
message type and calls showDesktopConfirmDialog with owner resolved via
BrowserWindow.getFocusedWindow() ?? mainWindow) to use try/catch around the
await call and return false on error, ensuring non-string messages still return
false as before.

In `@apps/web/src/wsNativeApi.ts`:
- Around line 63-68: The confirm method currently forwards to
window.desktopBridge.confirm but will reject the returned promise if the IPC
call fails; wrap the desktopBridge.confirm call in a try/catch inside the
confirm async function (the confirm export) and on error fallback to calling
window.confirm(message) so callers always get a resolved boolean (or fallback
result) instead of a propagated rejection.

Comment thread apps/desktop/src/main.ts
Comment on lines +147 to +155
ipcMain.removeHandler(CONFIRM_CHANNEL);
ipcMain.handle(CONFIRM_CHANNEL, async (_event, message: unknown) => {
if (typeof message !== "string") {
return false;
}

const owner = BrowserWindow.getFocusedWindow() ?? mainWindow;
return showDesktopConfirmDialog(message, owner);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard confirm IPC against dialog failures.

If showDesktopConfirmDialog rejects (e.g., owner window closed), the IPC call rejects and can surface as a UI error. Consider returning false on failure to keep the flow best-effort.

🔧 Suggested change
 ipcMain.handle(CONFIRM_CHANNEL, async (_event, message: unknown) => {
   if (typeof message !== "string") {
     return false;
   }

   const owner = BrowserWindow.getFocusedWindow() ?? mainWindow;
-  return showDesktopConfirmDialog(message, owner);
+  try {
+    return await showDesktopConfirmDialog(message, owner);
+  } catch {
+    return false;
+  }
 });
🤖 Prompt for AI Agents
In `@apps/desktop/src/main.ts` around lines 147 - 155, Wrap the ipcMain handler
for CONFIRM_CHANNEL so any rejection from showDesktopConfirmDialog is caught and
results in a safe false return; specifically, update the ipcMain.handle callback
(which currently checks message type and calls showDesktopConfirmDialog with
owner resolved via BrowserWindow.getFocusedWindow() ?? mainWindow) to use
try/catch around the await call and return false on error, ensuring non-string
messages still return false as before.

Comment on lines +63 to +68
confirm: async (message) => {
if (window.desktopBridge) {
return window.desktopBridge.confirm(message);
}
return window.confirm(message);
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fallback if the desktop confirm IPC rejects.

If the IPC call fails, callers will see a rejected promise. Consider a try/catch fallback to keep confirmation best-effort.

🔧 Suggested change
       confirm: async (message) => {
         if (window.desktopBridge) {
-          return window.desktopBridge.confirm(message);
+          try {
+            return await window.desktopBridge.confirm(message);
+          } catch {
+            // fall through to browser confirm
+          }
         }
         return window.confirm(message);
       },
📝 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
confirm: async (message) => {
if (window.desktopBridge) {
return window.desktopBridge.confirm(message);
}
return window.confirm(message);
},
confirm: async (message) => {
if (window.desktopBridge) {
try {
return await window.desktopBridge.confirm(message);
} catch {
// fall through to browser confirm
}
}
return window.confirm(message);
},
🤖 Prompt for AI Agents
In `@apps/web/src/wsNativeApi.ts` around lines 63 - 68, The confirm method
currently forwards to window.desktopBridge.confirm but will reject the returned
promise if the IPC call fails; wrap the desktopBridge.confirm call in a
try/catch inside the confirm async function (the confirm export) and on error
fallback to calling window.confirm(message) so callers always get a resolved
boolean (or fallback result) instead of a propagated rejection.

Co-authored-by: codex <codex@users.noreply.github.com>
Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/components/Sidebar.tsx`:
- Around line 268-277: The confirmation dialog is shown even when deletion
cannot proceed because threadProject may be undefined; change the logic around
shouldDeleteWorktree so the api.dialogs.confirm prompt is only shown when
orphanedWorktreePath is non-null AND threadProject (or the project/worker that
indicates the worktree can be deleted) is defined/owned; update the condition
that computes shouldDeleteWorktree (referencing shouldDeleteWorktree,
orphanedWorktreePath, threadProject, and api.dialogs.confirm) so the confirm
dialog is skipped when threadProject is undefined, and ensure the subsequent
deletion branch still checks the same threadProject existence before calling the
worktree delete code.

Comment thread apps/web/src/components/Sidebar.tsx
- Format orphaned worktree paths to display only the final segment
- Apply formatting in Sidebar delete prompt for cleaner UX
- Add tests for Unix, Windows, trailing slash, and fallback cases
- Require both orphaned worktree path and thread project before prompting delete
- Prevents delete confirmation when project metadata is unavailable
@juliusmarminge juliusmarminge merged commit 293f7b1 into main Feb 13, 2026
3 checks passed
DavidIlie added a commit to DavidIlie/t3code that referenced this pull request Mar 13, 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