diff --git a/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts b/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts index 05876c34e3a..9d7b20bd21b 100644 --- a/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts +++ b/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts @@ -17,6 +17,7 @@ export interface V2UserPreferencesApi { setUrlLinks: (next: LinkTierMap) => void; setRightSidebarOpen: (next: boolean | ((prev: boolean) => boolean)) => void; setRightSidebarTab: (next: RightSidebarTab) => void; + setDeleteLocalBranch: (next: boolean) => void; } export function useV2UserPreferences(): V2UserPreferencesApi { @@ -103,11 +104,31 @@ export function useV2UserPreferences(): V2UserPreferencesApi { [collections], ); + const setDeleteLocalBranch = useCallback( + (next: boolean) => { + const existing = collections.v2UserPreferences.get( + V2_USER_PREFERENCES_ID, + ); + if (!existing) { + collections.v2UserPreferences.insert({ + ...DEFAULT_V2_USER_PREFERENCES, + deleteLocalBranch: next, + }); + return; + } + collections.v2UserPreferences.update(V2_USER_PREFERENCES_ID, (draft) => { + draft.deleteLocalBranch = next; + }); + }, + [collections], + ); + return { preferences, setFileLinks, setUrlLinks, setRightSidebarOpen, setRightSidebarTab, + setDeleteLocalBranch, }; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts index 10f39af2279..1480d5cdc0e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts @@ -4,6 +4,7 @@ import { type DestroyWorkspaceError, useDestroyWorkspace, } from "renderer/hooks/host-service/useDestroyWorkspace"; +import { useV2UserPreferences } from "renderer/hooks/useV2UserPreferences/useV2UserPreferences"; import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; @@ -27,7 +28,7 @@ interface UseDestroyDialogStateOptions { * - On error, `clearDeleting` runs in the `finally` block so the row * reappears. For decision-required errors (CONFLICT, TEARDOWN_FAILED) * we reopen the dialog in the matching error pane so the user can - * force-retry with full context. The branch opt-in is preserved. + * force-retry with full context. * - For unknown errors we just toast.error — no reopen. */ export function useDestroyDialogState({ @@ -40,16 +41,15 @@ export function useDestroyDialogState({ const { markDeleting, clearDeleting } = useDeletingWorkspaces(); const navigateAway = useNavigateAwayFromWorkspace(); - const [deleteBranch, setDeleteBranch] = useState(false); + const { preferences, setDeleteLocalBranch: setDeleteBranch } = + useV2UserPreferences(); + const deleteBranch = preferences.deleteLocalBranch; const [error, setError] = useState(null); const inFlight = useRef(false); const handleOpenChange = useCallback( (next: boolean) => { - if (!next) { - setDeleteBranch(false); - setError(null); - } + if (!next) setError(null); onOpenChange(next); }, [onOpenChange], @@ -69,8 +69,6 @@ export function useDestroyDialogState({ // and hiding the row were swallowing the nav otherwise. navigateAway(workspaceId); - // Optimistic close. `deleteBranch` preserved in case we re-open - // on a decision-required error. setError(null); onOpenChange(false); markDeleting(workspaceId); @@ -79,7 +77,6 @@ export function useDestroyDialogState({ try { const result = await destroy({ deleteBranch, force }); for (const warning of result.warnings) toast.warning(warning); - setDeleteBranch(false); onDeleted?.(); } catch (err) { const e = err as DestroyWorkspaceError; diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts index 4c5be6fd66d..dedfac4dd47 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts @@ -243,6 +243,7 @@ export const v2UserPreferencesSchema = z.object({ urlLinks: linkTierMapSchema.default(DEFAULT_LINK_TIER_MAP), rightSidebarOpen: z.boolean().default(true), rightSidebarTab: z.enum(["changes", "files"]).default("changes"), + deleteLocalBranch: z.boolean().default(false), }); export type V2UserPreferencesRow = z.infer; @@ -255,4 +256,5 @@ export const DEFAULT_V2_USER_PREFERENCES: V2UserPreferencesRow = { urlLinks: DEFAULT_LINK_TIER_MAP, rightSidebarOpen: true, rightSidebarTab: "changes", + deleteLocalBranch: false, }; diff --git a/packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts b/packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts index 350aefb5a15..db72093b3b5 100644 --- a/packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts +++ b/packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts @@ -23,8 +23,10 @@ export const workspaceCleanupRouter = router({ * Force semantics: * - skips preflight (step 0) * - skips teardown (step 1) - * - upgrades `git branch -d` to `-D` in step 3c * - step 3b always uses `--force` (we're past the commit point) + * - step 3c always uses `-D` regardless: the `deleteBranch` + * checkbox is the user's consent, so refusing unmerged branches + * would just silently drop the opt-in. * * Typed errors for the renderer: * - CONFLICT → dirty worktree; prompt force-retry @@ -147,7 +149,7 @@ export const workspaceCleanupRouter = router({ if (input.deleteBranch && local.branch) { try { - await git.raw(["branch", input.force ? "-D" : "-d", local.branch]); + await git.raw(["branch", "-D", local.branch]); branchDeleted = true; } catch (err) { const message = err instanceof Error ? err.message : String(err);