From d695a1b8abc5c363559819f2e1d7b52e957bc981 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 23 Apr 2026 10:50:28 -0700 Subject: [PATCH 1/3] fix(desktop): persist "also delete local branch" checkbox in v2 delete dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the v2 delete-workspace dialog into the existing `settings.getDeleteLocalBranch` / `setDeleteLocalBranch` tRPC pair (already used by v1 and the Settings → Git page) so the user's last choice is remembered across deletes instead of always defaulting to unchecked. --- .../useDestroyDialogState.ts | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) 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 413a8489ee7..00e33344fb3 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 { electronTrpc } from "renderer/lib/electron-trpc"; import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; interface UseDestroyDialogStateOptions { @@ -38,14 +39,26 @@ export function useDestroyDialogState({ const { destroy } = useDestroyWorkspace(workspaceId); const { markDeleting, clearDeleting } = useDeletingWorkspaces(); - const [deleteBranch, setDeleteBranch] = useState(false); + const { data: persistedDeleteBranch } = + electronTrpc.settings.getDeleteLocalBranch.useQuery(); + const setPersistedDeleteBranch = + electronTrpc.settings.setDeleteLocalBranch.useMutation(); + + const [deleteBranchOverride, setDeleteBranchOverride] = useState< + boolean | null + >(null); + const deleteBranch = deleteBranchOverride ?? persistedDeleteBranch ?? false; const [error, setError] = useState(null); const inFlight = useRef(false); + const setDeleteBranch = useCallback((next: boolean) => { + setDeleteBranchOverride(next); + }, []); + const handleOpenChange = useCallback( (next: boolean) => { if (!next) { - setDeleteBranch(false); + setDeleteBranchOverride(null); setError(null); } onOpenChange(next); @@ -69,10 +82,12 @@ export function useDestroyDialogState({ onOpenChange(false); markDeleting(workspaceId); + setPersistedDeleteBranch.mutate({ enabled: deleteBranch }); + try { const result = await destroy({ deleteBranch, force }); for (const warning of result.warnings) toast.warning(warning); - setDeleteBranch(false); + setDeleteBranchOverride(null); onDeleted?.(); } catch (err) { const e = err as DestroyWorkspaceError; @@ -90,6 +105,7 @@ export function useDestroyDialogState({ [ destroy, deleteBranch, + setPersistedDeleteBranch, workspaceName, workspaceId, onOpenChange, From 0caa200b71f51191ee58e0354da95e50785525b0 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 23 Apr 2026 11:17:02 -0700 Subject: [PATCH 2/3] fix(desktop): persist v2 delete-branch choice via v2UserPreferences; always force MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move the v2 delete dialog's "Also delete local branch" opt-in off the v1 tRPC settings singleton and onto `v2UserPreferences` — the same collection used for rightSidebar/link-tier prefs. The choice is now persisted the instant the checkbox toggles (optimistic update via @tanstack/db) and shared across every workspace's delete dialog, not per-instance. - Drop the per-hook `deleteBranchOverride` state; read straight from the singleton preferences row. - In host-service workspace-cleanup, always use `git branch -D` when `deleteBranch` is on — the checkbox is the user's consent, so silently refusing unmerged branches (the old `-d`/`-D` gate on `force`) just dropped the opt-in and produced confusing warnings. --- .../useV2UserPreferences.ts | 21 +++++++++++++ .../useDestroyDialogState.ts | 31 +++++-------------- .../dashboardSidebarLocal/schema.ts | 2 ++ .../workspace-cleanup/workspace-cleanup.ts | 6 ++-- 4 files changed, 35 insertions(+), 25 deletions(-) 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 00e33344fb3..ca311c6cb85 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,7 +4,7 @@ import { type DestroyWorkspaceError, useDestroyWorkspace, } from "renderer/hooks/host-service/useDestroyWorkspace"; -import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useV2UserPreferences } from "renderer/hooks/useV2UserPreferences/useV2UserPreferences"; import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; interface UseDestroyDialogStateOptions { @@ -39,28 +39,19 @@ export function useDestroyDialogState({ const { destroy } = useDestroyWorkspace(workspaceId); const { markDeleting, clearDeleting } = useDeletingWorkspaces(); - const { data: persistedDeleteBranch } = - electronTrpc.settings.getDeleteLocalBranch.useQuery(); - const setPersistedDeleteBranch = - electronTrpc.settings.setDeleteLocalBranch.useMutation(); - - const [deleteBranchOverride, setDeleteBranchOverride] = useState< - boolean | null - >(null); - const deleteBranch = deleteBranchOverride ?? persistedDeleteBranch ?? false; + const { preferences, setDeleteLocalBranch } = useV2UserPreferences(); + const deleteBranch = preferences.deleteLocalBranch; const [error, setError] = useState(null); const inFlight = useRef(false); - const setDeleteBranch = useCallback((next: boolean) => { - setDeleteBranchOverride(next); - }, []); + const setDeleteBranch = useCallback( + (next: boolean) => setDeleteLocalBranch(next), + [setDeleteLocalBranch], + ); const handleOpenChange = useCallback( (next: boolean) => { - if (!next) { - setDeleteBranchOverride(null); - setError(null); - } + if (!next) setError(null); onOpenChange(next); }, [onOpenChange], @@ -76,18 +67,13 @@ export function useDestroyDialogState({ if (inFlight.current) return; inFlight.current = true; - // Optimistic close. State (deleteBranch) preserved in case we re-open - // on a decision-required error. setError(null); onOpenChange(false); markDeleting(workspaceId); - setPersistedDeleteBranch.mutate({ enabled: deleteBranch }); - try { const result = await destroy({ deleteBranch, force }); for (const warning of result.warnings) toast.warning(warning); - setDeleteBranchOverride(null); onDeleted?.(); } catch (err) { const e = err as DestroyWorkspaceError; @@ -105,7 +91,6 @@ export function useDestroyDialogState({ [ destroy, deleteBranch, - setPersistedDeleteBranch, workspaceName, workspaceId, onOpenChange, 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 bf3fa1433cb..548f1b570fa 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 @@ -238,6 +238,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; @@ -250,4 +251,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); From 1a20b87cd433181903dd212e9f0508a32c7851c7 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 23 Apr 2026 11:19:32 -0700 Subject: [PATCH 3/3] chore(desktop): drop redundant setDeleteBranch wrapper in destroy hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename destructured `setDeleteLocalBranch` → `setDeleteBranch` at the call site instead of wrapping it in a useCallback that did nothing. Also trim a docstring line that's trivially true now that the opt-in is a global singleton. --- .../useDestroyDialogState/useDestroyDialogState.ts | 10 +++------- bun.lock | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) 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 ca311c6cb85..7f3aeb53b2d 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 @@ -27,7 +27,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({ @@ -39,16 +39,12 @@ export function useDestroyDialogState({ const { destroy } = useDestroyWorkspace(workspaceId); const { markDeleting, clearDeleting } = useDeletingWorkspaces(); - const { preferences, setDeleteLocalBranch } = useV2UserPreferences(); + const { preferences, setDeleteLocalBranch: setDeleteBranch } = + useV2UserPreferences(); const deleteBranch = preferences.deleteLocalBranch; const [error, setError] = useState(null); const inFlight = useRef(false); - const setDeleteBranch = useCallback( - (next: boolean) => setDeleteLocalBranch(next), - [setDeleteLocalBranch], - ); - const handleOpenChange = useCallback( (next: boolean) => { if (!next) setError(null); diff --git a/bun.lock b/bun.lock index c96140813ad..9c318eb014e 100644 --- a/bun.lock +++ b/bun.lock @@ -110,7 +110,7 @@ }, "apps/desktop": { "name": "@superset/desktop", - "version": "1.5.6", + "version": "1.5.9", "dependencies": { "@ai-sdk/anthropic": "^3.0.43", "@ai-sdk/openai": "3.0.36",