From c8b76d287f8285d3fefffc5c0fc359779efdc102 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 6 Feb 2026 10:11:22 -0800 Subject: [PATCH 1/7] feat(desktop): optionally delete local branch on workspace deletion 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. --- .../src/lib/trpc/routers/settings/index.ts | 20 ++++++++ .../routers/workspaces/procedures/delete.ts | 17 ++++++- .../lib/trpc/routers/workspaces/utils/git.ts | 27 ++++++++++ .../DeleteWorkspaceDialog.tsx | 49 ++++++++++++++++--- packages/local-db/src/schema/schema.ts | 1 + 5 files changed, 107 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index d04925039a9..ba0dcfa465e 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -365,6 +365,26 @@ export const createSettingsRouter = () => { }; }), + getDeleteLocalBranch: publicProcedure.query(() => { + const row = getSettings(); + return row.deleteLocalBranch ?? false; + }), + + setDeleteLocalBranch: publicProcedure + .input(z.object({ enabled: z.boolean() })) + .mutation(({ input }) => { + localDb + .insert(settings) + .values({ id: 1, deleteLocalBranch: input.enabled }) + .onConflictDoUpdate({ + target: settings.id, + set: { deleteLocalBranch: input.enabled }, + }) + .run(); + + return { success: true }; + }), + getNotificationSoundsMuted: publicProcedure.query(() => { const row = getSettings(); return row.notificationSoundsMuted ?? false; diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts index bf8b60b42a2..c99546dcc85 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts @@ -17,6 +17,7 @@ import { updateActiveWorkspaceIfRemoved, } from "../utils/db-helpers"; import { + deleteLocalBranch, hasUncommittedChanges, hasUnpushedCommits, worktreeExists, @@ -148,7 +149,7 @@ export const createDeleteProcedures = () => { }), delete: publicProcedure - .input(z.object({ id: z.string() })) + .input(z.object({ id: z.string(), deleteLocalBranch: z.boolean().optional() })) .mutation(async ({ input }) => { const workspace = getWorkspace(input.id); @@ -247,6 +248,20 @@ export const createDeleteProcedures = () => { } finally { workspaceInitManager.releaseProjectLock(project.id); } + + if (input.deleteLocalBranch && workspace.branch) { + try { + await deleteLocalBranch({ + mainRepoPath: project.mainRepoPath, + branch: workspace.branch, + }); + } catch (error) { + console.error( + `[workspace/delete] Branch cleanup failed (non-blocking):`, + error instanceof Error ? error.message : String(error), + ); + } + } } deleteWorkspace(input.id); diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts index 75231c64c4e..85c06c2fb75 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts @@ -633,6 +633,33 @@ export async function createWorktreeFromExistingBranch({ } } +export async function deleteLocalBranch({ + mainRepoPath, + branch, +}: { + mainRepoPath: string; + branch: string; +}): Promise { + 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}`); + } +} + export async function removeWorktree( mainRepoPath: string, worktreePath: string, diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx index 8d919f1d8ee..5d9f30729cd 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { AlertDialog, AlertDialogContent, @@ -7,6 +8,7 @@ import { AlertDialogTitle, } from "@superset/ui/alert-dialog"; import { Button } from "@superset/ui/button"; +import { Checkbox } from "@superset/ui/checkbox"; import { toast } from "@superset/ui/sonner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { electronTrpc } from "renderer/lib/electron-trpc"; @@ -33,6 +35,18 @@ export function DeleteWorkspaceDialog({ const isBranch = workspaceType === "branch"; const deleteWorkspace = useDeleteWorkspace(); const closeWorkspace = useCloseWorkspace(); + const setDeleteLocalBranchSetting = + electronTrpc.settings.setDeleteLocalBranch.useMutation(); + + const { data: deleteLocalBranchDefault } = + electronTrpc.settings.getDeleteLocalBranch.useQuery(undefined, { + enabled: open && !isBranch, + }); + const [deleteLocalBranch, setDeleteLocalBranch] = useState( + null, + ); + const deleteLocalBranchChecked = + deleteLocalBranch ?? deleteLocalBranchDefault ?? false; const { data: gitStatusData, isLoading: isLoadingGitStatus } = electronTrpc.workspaces.canDelete.useQuery( @@ -85,13 +99,22 @@ export function DeleteWorkspaceDialog({ const handleDelete = () => { onOpenChange(false); + setDeleteLocalBranchSetting.mutate({ + enabled: deleteLocalBranchChecked, + }); + toast.promise( - deleteWorkspace.mutateAsync({ id: workspaceId }).then((result) => { - if (!result.success) { - throw new Error(result.error ?? "Failed to delete"); - } - return result; - }), + deleteWorkspace + .mutateAsync({ + id: workspaceId, + deleteLocalBranch: deleteLocalBranchChecked, + }) + .then((result) => { + if (!result.success) { + throw new Error(result.error ?? "Failed to delete"); + } + return result; + }), { loading: `Deleting "${workspaceName}"...`, success: (result) => { @@ -193,6 +216,20 @@ export function DeleteWorkspaceDialog({ )} + {!isLoading && canDelete && ( +
+ +
+ )} +