Skip to content

feat(desktop): delete local branch on workspace deletion#1255

Merged
Kitenite merged 8 commits into
mainfrom
kitenite/rocket
Feb 6, 2026
Merged

feat(desktop): delete local branch on workspace deletion#1255
Kitenite merged 8 commits into
mainfrom
kitenite/rocket

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Feb 6, 2026

Summary

  • Adds a "Also delete local branch" checkbox to the worktree delete confirmation dialog
  • Persists the user's checkbox preference in local settings so it remembers across deletions
  • Branch deletion is best-effort — failures are logged but don't block workspace removal

Test plan

  • Create a worktree workspace on a feature branch
  • Open delete dialog — checkbox should appear (unchecked by default)
  • Check the box and delete — verify git branch no longer lists the branch
  • Create another worktree — delete dialog should remember checkbox was checked
  • Uncheck and delete — branch should remain
  • Branch workspace delete dialog should NOT show the checkbox

Summary by CodeRabbit

  • New Features

    • Option to delete local git branches when removing a workspace.
    • "Also delete local branch" toggle added to the delete workspace dialog; preference is persisted.
    • New setting in Settings → Behavior with a toggle and searchable entry to control the default.
  • Chores

    • Database schema and migration updated to persist the new preference.

Add a checkbox to the worktree delete dialog that lets users also remove
the associated local git branch. The preference is persisted in settings
so it remembers the last-used choice. Branch deletion is best-effort and
won't block workspace removal if it fails.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

Adds a persisted "delete local branch" setting, UI controls to toggle it, TRPC procedures to get/set the setting, an optional deleteLocalBranch flag on workspace deletion, a git utility to delete local branches, and a DB migration adding the setting column.

Changes

Cohort / File(s) Summary
Database schema & migrations
packages/local-db/src/schema/schema.ts, packages/local-db/drizzle/0018_add_delete_local_branch_setting.sql, packages/local-db/drizzle/meta/0018_snapshot.json, packages/local-db/drizzle/meta/_journal.json
Added delete_local_branch column (mapped deleteLocalBranch) to settings, updated Drizzle snapshot and journal, and added migration (migration also drops telemetry_enabled).
Settings router
apps/desktop/src/lib/trpc/routers/settings/index.ts
Added getDeleteLocalBranch query and setDeleteLocalBranch mutation that read/upsert deleteLocalBranch on settings row (id: 1).
Workspace delete procedure
apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
Extended input schema with optional deleteLocalBranch boolean; after main deletion and lock release, performs non-blocking local branch deletion when requested (errors logged).
Git utilities
apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
Added deleteLocalBranch({ mainRepoPath, branch }) which runs git -C <path> branch -D <branch> with git env and 10s timeout; logs errors and surfaces failure context.
Delete workspace UI
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/.../DeleteWorkspaceDialog.tsx
Added "Also delete local branch" Switch, local state, server-driven default (get), persisted preference (set) and passes deleteLocalBranch to delete mutation.
Settings UI & search
apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
Exposed toggle in Behavior settings with optimistic update handlers and added search entry behavior-delete-local-branch to settings index.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as DeleteWorkspaceDialog
    participant Settings as Settings Router
    participant DeleteProc as Delete Procedure
    participant Git as Git Utilities
    participant DB as Local Database

    User->>UI: Open delete dialog
    UI->>Settings: getDeleteLocalBranch()
    Settings->>DB: SELECT settings (id:1)
    DB-->>Settings: { deleteLocalBranch }
    Settings-->>UI: return default

    User->>UI: Toggle checkbox, click Delete
    UI->>Settings: setDeleteLocalBranch({ enabled })
    Settings->>DB: UPSERT settings (id:1)
    DB-->>Settings: OK

    UI->>DeleteProc: delete({ id, deleteLocalBranch })
    DeleteProc->>DB: remove workspace & worktree records
    DB-->>DeleteProc: OK

    alt deleteLocalBranch == true and branch exists
        DeleteProc->>Git: deleteLocalBranch(mainRepoPath, branch)
        Git->>Git: run `git -C <path> branch -D <branch>`
        Git-->>DeleteProc: success / error (logged)
    end

    DeleteProc-->>UI: { success: true }
    UI-->>User: close dialog
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰
A little toggle in the glade,
I save the choice before it's made.
One hop, one clean, one gentle sweep,
Goodbye old branch — into mossy sleep. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature: adding local branch deletion capability to workspace deletion operations.
Description check ✅ Passed The description provided includes a clear summary of changes and a detailed test plan, though it deviates from the template structure by omitting standard sections like Related Issues and Type of Change.

✏️ 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 kitenite/rocket

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.

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

🤖 Fix all issues with AI agents
In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts`:
- Around line 636-661: In deleteLocalBranch, add a safety check to avoid
deleting the repository default (or current) branch: before running
execFileAsync in deleteLocalBranch({ mainRepoPath, branch }), call git to
determine the repo default branch (e.g., run git -C mainRepoPath symbolic-ref
--short refs/remotes/origin/HEAD or git -C mainRepoPath rev-parse --abbrev-ref
origin/HEAD) and also check the currently checked-out branch (git -C
mainRepoPath rev-parse --abbrev-ref HEAD); if branch equals the default branch
or the current branch, log and return early instead of running git branch -D;
keep using getGitEnv() and execFileAsync for these checks and preserve existing
error handling for other failures.

In `@packages/local-db/src/schema/schema.ts`:
- Line 158: The new schema added the deleteLocalBranch column
(delete_local_branch) in packages/local-db/src/schema/schema.ts but there is no
matching migration; create a migration file named
0018_add_delete_local_branch.sql in packages/local-db/drizzle/ containing a
single ALTER TABLE statement to add the integer column (`ALTER TABLE settings
ADD delete_local_branch integer;`) so the database schema and the
deleteLocalBranch field stay in sync; follow the existing migration naming/order
pattern used by files like 0015_add_notification_sounds_muted.sql and
0016_add_telemetry_enabled.sql.
🧹 Nitpick comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx (1)

45-49: Local checkbox state doesn't reset on dialog reopen.

If the user toggles the checkbox, then cancels (without deleting), the local deleteLocalBranch state retains the toggled value across dialog open/close cycles (since the component stays mounted). On the next open, the checkbox will show the locally-toggled value rather than the persisted server default.

Consider resetting the local state when the dialog opens:

Proposed fix
+import { useEffect, useState } from "react";
-import { useState } from "react";
 ...
 const [deleteLocalBranch, setDeleteLocalBranch] = useState<boolean | null>(
   null,
 );
+useEffect(() => {
+  if (open) {
+    setDeleteLocalBranch(null);
+  }
+}, [open]);
 const deleteLocalBranchChecked =
   deleteLocalBranch ?? deleteLocalBranchDefault ?? false;

Comment on lines +636 to +661
export async function deleteLocalBranch({
mainRepoPath,
branch,
}: {
mainRepoPath: string;
branch: string;
}): Promise<void> {
const env = await getGitEnv();

try {
await execFileAsync(
"git",
["-C", mainRepoPath, "branch", "-D", branch],
{ env, timeout: 10_000 },
);
console.log(
`[workspace/delete] Deleted local branch "${branch}"`,
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(
`[workspace/delete] Failed to delete local branch "${branch}": ${errorMessage}`,
);
throw new Error(`Failed to delete local branch "${branch}": ${errorMessage}`);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consider guarding against deletion of the default/current branch.

git branch -D will force-delete any branch, including main/master or the repo's default branch. If a user happens to create a worktree on the default branch and checks "Also delete local branch," this would delete it. While git would refuse to delete a currently checked-out branch, bare repos and edge cases could still be problematic.

Consider adding a safety check, either here or in the caller, to skip deletion when branch matches the project's default branch.

Proposed guard
 export async function deleteLocalBranch({
 	mainRepoPath,
 	branch,
+	defaultBranch,
 }: {
 	mainRepoPath: string;
 	branch: string;
+	defaultBranch?: string;
 }): Promise<void> {
+	const protectedBranches = ["main", "master", "develop"];
+	if (defaultBranch) protectedBranches.push(defaultBranch);
+	if (protectedBranches.includes(branch)) {
+		console.warn(
+			`[workspace/delete] Refusing to delete protected branch "${branch}"`,
+		);
+		return;
+	}
 	const env = await getGitEnv();
🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts` around lines 636 -
661, In deleteLocalBranch, add a safety check to avoid deleting the repository
default (or current) branch: before running execFileAsync in deleteLocalBranch({
mainRepoPath, branch }), call git to determine the repo default branch (e.g.,
run git -C mainRepoPath symbolic-ref --short refs/remotes/origin/HEAD or git -C
mainRepoPath rev-parse --abbrev-ref origin/HEAD) and also check the currently
checked-out branch (git -C mainRepoPath rev-parse --abbrev-ref HEAD); if branch
equals the default branch or the current branch, log and return early instead of
running git branch -D; keep using getGitEnv() and execFileAsync for these checks
and preserve existing error handling for other failures.

notificationSoundsMuted: integer("notification_sounds_muted", {
mode: "boolean",
}),
deleteLocalBranch: integer("delete_local_branch", { mode: "boolean" }),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat packages/local-db/src/schema/schema.ts | head -200 | tail -50

Repository: superset-sh/superset

Length of output: 1666


🏁 Script executed:

# Look for migration files related to this change
find . -type f \( -name "*migration*" -o -name "*migrate*" \) | grep -E "\.(ts|js|sql)$" | head -20

Repository: superset-sh/superset

Length of output: 112


🏁 Script executed:

# Search for references to delete_local_branch in the codebase
rg "delete_local_branch" --type ts --type js --type sql -l

Repository: superset-sh/superset

Length of output: 102


🏁 Script executed:

# Check the migration file to see if it includes delete_local_branch
cat packages/db/drizzle/0000_initial_migration.sql | grep -A 5 -B 5 "delete_local_branch"

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Also check if there are any other drizzle migration files
find packages -name "*.sql" -o -name "*migration*.ts" | sort

Repository: superset-sh/superset

Length of output: 2132


🏁 Script executed:

# Verify the full settings table schema definition
rg "export const settings = sqliteTable" -A 40 packages/local-db/src/schema/schema.ts

Repository: superset-sh/superset

Length of output: 1745


🏁 Script executed:

# Search for the migration file that adds delete_local_branch to local-db
rg "delete_local_branch" packages/local-db/drizzle/ -A 2 -B 2

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check the most recent local-db migration files
tail -5 packages/local-db/drizzle/*.sql | cat

Repository: superset-sh/superset

Length of output: 105


🏁 Script executed:

# List all local-db migration files with their content summary
for file in packages/local-db/drizzle/*.sql; do 
  echo "=== $(basename $file) ===" 
  head -3 "$file"
done | tail -40

Repository: superset-sh/superset

Length of output: 2628


Migration file missing for new delete_local_branch column.

The schema definition exists in schema.ts, but there is no corresponding migration file in packages/local-db/drizzle/. Following the pattern of recent migrations (e.g., 0015_add_notification_sounds_muted.sql, 0016_add_telemetry_enabled.sql), create migration file 0018_add_delete_local_branch.sql with:

ALTER TABLE `settings` ADD `delete_local_branch` integer;

Without this migration, the schema and database will be out of sync.

🤖 Prompt for AI Agents
In `@packages/local-db/src/schema/schema.ts` at line 158, The new schema added the
deleteLocalBranch column (delete_local_branch) in
packages/local-db/src/schema/schema.ts but there is no matching migration;
create a migration file named 0018_add_delete_local_branch.sql in
packages/local-db/drizzle/ containing a single ALTER TABLE statement to add the
integer column (`ALTER TABLE settings ADD delete_local_branch integer;`) so the
database schema and the deleteLocalBranch field stay in sync; follow the
existing migration naming/order pattern used by files like
0015_add_notification_sounds_muted.sql and 0016_add_telemetry_enabled.sql.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 6, 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

Add a switch in Features settings to control the default for deleting
local branches on workspace removal. Also generate drizzle migration
0018 for the new delete_local_branch column.
Use id/htmlFor association instead of wrapping Checkbox in label to
avoid Radix ref conflict that caused maximum update depth error.
Radix Checkbox causes maximum update depth error inside AlertDialog.
Switch component works without ref conflicts.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx`:
- Around line 96-98: The getTelemetryEnabled query in the BehaviorSettings
component (electronTrpc.settings.getTelemetryEnabled.useQuery) runs
unconditionally and will break after migration drops telemetry_enabled; disable
it by adding query options { enabled: false } (or remove the telemetry-related
lines entirely). Locate the useQuery call and change it to
electronTrpc.settings.getTelemetryEnabled.useQuery(undefined, { enabled: false
}) so the hook does not execute, and remove or keep the TODO comment about
removing telemetry once migrations are complete; also remove any code that reads
telemetryEnabled/isTelemetryLoading if you choose to delete telemetry support.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx`:
- Around line 45-49: The local toggle state deleteLocalBranch in
DeleteWorkspaceDialog is only initialized once and isn't reset when the dialog
reopens, so it can incorrectly override deleteLocalBranchDefault; update the
component to reset deleteLocalBranch to null whenever the dialog is opened
(i.e., when the open prop transitions true) — implement this by adding an effect
keyed on open (or handling open changes via onOpenChange) that sets
setDeleteLocalBranch(null) on open so the deleteLocalBranchChecked expression
(deleteLocalBranch ?? deleteLocalBranchDefault ?? false) will honor the server
default on each open.
- Around line 219-230: The "Also delete local branch" span in
DeleteWorkspaceDialog isn't associated with the Switch, so the label text
doesn't toggle the switch; update the JSX to replace or wrap the span with a
Label element that uses htmlFor matching an id prop on the Switch (e.g., give
the Switch an id like "delete-local-branch" and change the <span> to <Label
htmlFor="delete-local-branch">) and keep the same props wired to
deleteLocalBranchChecked and setDeleteLocalBranch so clicking the text toggles
the Switch.

Comment on lines 96 to 98
// TODO: remove telemetry query/mutation/handler once telemetry procedures are removed
const { data: telemetryEnabled, isLoading: isTelemetryLoading } =
electronTrpc.settings.getTelemetryEnabled.useQuery();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

getTelemetryEnabled query fires unconditionally despite the column being dropped in this PR's migration.

This useQuery executes on every render of BehaviorSettings regardless of the false && gate on line 281. After migration 0018 drops telemetry_enabled, this will likely produce runtime errors. Consider wrapping with enabled: false or removing the telemetry code entirely.

🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx`
around lines 96 - 98, The getTelemetryEnabled query in the BehaviorSettings
component (electronTrpc.settings.getTelemetryEnabled.useQuery) runs
unconditionally and will break after migration drops telemetry_enabled; disable
it by adding query options { enabled: false } (or remove the telemetry-related
lines entirely). Locate the useQuery call and change it to
electronTrpc.settings.getTelemetryEnabled.useQuery(undefined, { enabled: false
}) so the hook does not execute, and remove or keep the TODO comment about
removing telemetry once migrations are complete; also remove any code that reads
telemetryEnabled/isTelemetryLoading if you choose to delete telemetry support.

Comment on lines +45 to +49
const [deleteLocalBranch, setDeleteLocalBranch] = useState<boolean | null>(
null,
);
const deleteLocalBranchChecked =
deleteLocalBranch ?? deleteLocalBranchDefault ?? false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Local switch state is not reset when the dialog reopens.

deleteLocalBranch is initialized to null only on mount. Since DeleteWorkspaceDialog stays mounted in its parent WorkspaceListItem, re-opening the dialog (Cancel → reopen) keeps the previous local toggle value, which takes precedence over the server default via the ?? chain on line 48–49. This means a user who toggles the switch and cancels will see their unsaved toggle state next time, which is inconsistent with the "persisted preference" model.

Consider resetting local state when the dialog opens:

Proposed fix

Add a reset when the dialog opens, e.g. by using onOpenChange or an effect keyed on open:

// At top of component, after the useState:
const prevOpen = useRef(false);
if (open && !prevOpen.current) {
  setDeleteLocalBranch(null); // reset to server default on open
}
prevOpen.current = open;

Or, if acceptable, simply pass a key prop to force remount when open changes (though this re-fetches queries too).

🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx`
around lines 45 - 49, The local toggle state deleteLocalBranch in
DeleteWorkspaceDialog is only initialized once and isn't reset when the dialog
reopens, so it can incorrectly override deleteLocalBranchDefault; update the
component to reset deleteLocalBranch to null whenever the dialog is opened
(i.e., when the open prop transitions true) — implement this by adding an effect
keyed on open (or handling open changes via onOpenChange) that sets
setDeleteLocalBranch(null) on open so the deleteLocalBranchChecked expression
(deleteLocalBranch ?? deleteLocalBranchDefault ?? false) will honor the server
default on each open.

Comment on lines +219 to +230
{!isLoading && canDelete && (
<div className="flex items-center justify-between px-4 pb-2">
<span className="text-xs text-muted-foreground">
Also delete local branch
</span>
<Switch
checked={deleteLocalBranchChecked}
onCheckedChange={(checked) => setDeleteLocalBranch(checked)}
className="scale-75"
/>
</div>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing label association on the "Also delete local branch" switch.

The <span> text label isn't linked to the Switch (no id/htmlFor pairing), so clicking the text doesn't toggle the switch. Other settings toggles in this PR (e.g., BehaviorSettings.tsx line 208–220) use <Label htmlFor="..."> with a matching id on the Switch.

Proposed fix
 <div className="flex items-center justify-between px-4 pb-2">
-  <span className="text-xs text-muted-foreground">
+  <label htmlFor="delete-local-branch-switch" className="text-xs text-muted-foreground">
     Also delete local branch
-  </span>
+  </label>
   <Switch
+    id="delete-local-branch-switch"
     checked={deleteLocalBranchChecked}
     onCheckedChange={(checked) => setDeleteLocalBranch(checked)}
     className="scale-75"
   />
 </div>
📝 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
{!isLoading && canDelete && (
<div className="flex items-center justify-between px-4 pb-2">
<span className="text-xs text-muted-foreground">
Also delete local branch
</span>
<Switch
checked={deleteLocalBranchChecked}
onCheckedChange={(checked) => setDeleteLocalBranch(checked)}
className="scale-75"
/>
</div>
)}
{!isLoading && canDelete && (
<div className="flex items-center justify-between px-4 pb-2">
<label htmlFor="delete-local-branch-switch" className="text-xs text-muted-foreground">
Also delete local branch
</label>
<Switch
id="delete-local-branch-switch"
checked={deleteLocalBranchChecked}
onCheckedChange={(checked) => setDeleteLocalBranch(checked)}
className="scale-75"
/>
</div>
)}
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx`
around lines 219 - 230, The "Also delete local branch" span in
DeleteWorkspaceDialog isn't associated with the Switch, so the label text
doesn't toggle the switch; update the JSX to replace or wrap the span with a
Label element that uses htmlFor matching an id prop on the Switch (e.g., give
the Switch an id like "delete-local-branch" and change the <span> to <Label
htmlFor="delete-local-branch">) and keep the same props wired to
deleteLocalBranchChecked and setDeleteLocalBranch so clicking the text toggles
the Switch.

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