From 5de599983cab192e2053d809c38c335806a226c7 Mon Sep 17 00:00:00 2001 From: Kiet <31864905+Kitenite@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:21:30 -0700 Subject: [PATCH 1/2] fix(desktop): persist "also delete local branch" checkbox in v2 delete dialog (#3681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(desktop): persist "also delete local branch" checkbox in v2 delete dialog 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. * fix(desktop): persist v2 delete-branch choice via v2UserPreferences; always force - 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. * chore(desktop): drop redundant setDeleteBranch wrapper in destroy hook 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. --- .../useV2UserPreferences.ts | 73 ++++++++++++++++++- .../useDestroyDialogState.ts | 23 +++--- .../dashboardSidebarLocal/schema.ts | 6 ++ .../workspace-cleanup/workspace-cleanup.ts | 6 +- 4 files changed, 96 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts b/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts index 9545d8ea0f3..665b6354902 100644 --- a/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts +++ b/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts @@ -13,6 +13,9 @@ export interface V2UserPreferencesApi { preferences: V2UserPreferencesRow; setFileLinks: (next: LinkTierMap) => void; setUrlLinks: (next: LinkTierMap) => void; + setRightSidebarOpen: (next: boolean | ((prev: boolean) => boolean)) => void; + setRightSidebarTab: (next: RightSidebarTab) => void; + setDeleteLocalBranch: (next: boolean) => void; } export function useV2UserPreferences(): V2UserPreferencesApi { @@ -57,5 +60,73 @@ export function useV2UserPreferences(): V2UserPreferencesApi { [upsertTierMap], ); - return { preferences, setFileLinks, setUrlLinks }; + const setRightSidebarOpen = useCallback( + (next: boolean | ((prev: boolean) => boolean)) => { + const existing = collections.v2UserPreferences.get( + V2_USER_PREFERENCES_ID, + ); + const prev = + existing?.rightSidebarOpen ?? + DEFAULT_V2_USER_PREFERENCES.rightSidebarOpen; + const value = typeof next === "function" ? next(prev) : next; + if (!existing) { + collections.v2UserPreferences.insert({ + ...DEFAULT_V2_USER_PREFERENCES, + rightSidebarOpen: value, + }); + return; + } + collections.v2UserPreferences.update(V2_USER_PREFERENCES_ID, (draft) => { + draft.rightSidebarOpen = value; + }); + }, + [collections], + ); + + const setRightSidebarTab = useCallback( + (next: RightSidebarTab) => { + const existing = collections.v2UserPreferences.get( + V2_USER_PREFERENCES_ID, + ); + if (!existing) { + collections.v2UserPreferences.insert({ + ...DEFAULT_V2_USER_PREFERENCES, + rightSidebarTab: next, + }); + return; + } + collections.v2UserPreferences.update(V2_USER_PREFERENCES_ID, (draft) => { + draft.rightSidebarTab = next; + }); + }, + [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 a81609a6eeb..85b9292f296 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 @@ -5,7 +5,9 @@ import { type DestroyWorkspaceError, useDestroyWorkspace, } from "renderer/hooks/host-service/useDestroyWorkspace"; +import { useV2UserPreferences } from "renderer/hooks/useV2UserPreferences/useV2UserPreferences"; import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; const STATUS_STALE_TIME_MS = 5_000; @@ -31,7 +33,7 @@ interface UseDestroyDialogStateOptions { * - On error, `clearDeleting` runs in the `finally` block so the row * reappears. For decision-required errors (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({ @@ -44,7 +46,10 @@ export function useDestroyDialogState({ const { destroy } = useDestroyWorkspace(workspaceId); const { markDeleting, clearDeleting } = useDeletingWorkspaces(); - const [deleteBranch, setDeleteBranch] = useState(false); + const navigateAway = useNavigateAwayFromWorkspace(); + const { preferences, setDeleteLocalBranch: setDeleteBranch } = + useV2UserPreferences(); + const deleteBranch = preferences.deleteLocalBranch; const { data: canDeleteData, isPending: isCheckingStatus } = electronTrpc.workspaces.canDelete.useQuery( @@ -63,10 +68,7 @@ export function useDestroyDialogState({ const handleOpenChange = useCallback( (next: boolean) => { - if (!next) { - setDeleteBranch(false); - setError(null); - } + if (!next) setError(null); onOpenChange(next); }, [onOpenChange], @@ -77,8 +79,12 @@ 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. + // Navigate off the doomed workspace FIRST. Closing the dialog + // and hiding the row were swallowing the nav otherwise. + // State (deleteBranch) preserved in case we re-open on a + // decision-required error. + navigateAway(workspaceId); + setError(null); onOpenChange(false); markDeleting(workspaceId); @@ -99,7 +105,6 @@ export function useDestroyDialogState({ } } 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 82e4cc4e3f7..0d5ab69822b 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 @@ -242,6 +242,9 @@ export const v2UserPreferencesSchema = z.object({ id: z.literal("preferences"), fileLinks: linkTierMapSchema.default(DEFAULT_LINK_TIER_MAP), 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; @@ -252,4 +255,7 @@ export const DEFAULT_V2_USER_PREFERENCES: V2UserPreferencesRow = { id: V2_USER_PREFERENCES_ID, fileLinks: DEFAULT_LINK_TIER_MAP, 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 eed175611e6fb7dc9e76977892028b7139b2142a Mon Sep 17 00:00:00 2001 From: MocA-Love Date: Fri, 24 Apr 2026 18:51:53 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(desktop):=20amend=20PR=20#3681=20?= =?UTF-8?q?=E2=80=94=20supply=20useNavigateAwayFromWorkspace=20+=20RightSi?= =?UTF-8?q?debarTab=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cherry-pick 時に不足していた部分を補完: - apps/desktop/.../useNavigateAwayFromWorkspace/ hook を upstream から追加 (PR #2b 取り込み時の取りこぼし) - useV2UserPreferences に RightSidebarTab type export を復元 - useDestroyDialogState で hook を使って navigateAway を呼ぶ typecheck / lint 全 pass。 --- .../useV2UserPreferences.ts | 2 ++ .../useDestroyDialogState.ts | 7 +++-- .../useNavigateAwayFromWorkspace/index.ts | 1 + .../useNavigateAwayFromWorkspace.ts | 27 +++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts diff --git a/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts b/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts index 665b6354902..9d7b20bd21b 100644 --- a/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts +++ b/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts @@ -9,6 +9,8 @@ import { type V2UserPreferencesRow, } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema"; +export type RightSidebarTab = V2UserPreferencesRow["rightSidebarTab"]; + export interface V2UserPreferencesApi { preferences: V2UserPreferencesRow; setFileLinks: (next: LinkTierMap) => void; 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 85b9292f296..5910740b82d 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 @@ -45,7 +45,6 @@ export function useDestroyDialogState({ }: UseDestroyDialogStateOptions) { const { destroy } = useDestroyWorkspace(workspaceId); const { markDeleting, clearDeleting } = useDeletingWorkspaces(); - const navigateAway = useNavigateAwayFromWorkspace(); const { preferences, setDeleteLocalBranch: setDeleteBranch } = useV2UserPreferences(); @@ -127,7 +126,11 @@ export function useDestroyDialogState({ onOpenChange, onDeleted, markDeleting, - clearDeleting, + clearDeleting, // Navigate off the doomed workspace FIRST. Closing the dialog + // and hiding the row were swallowing the nav otherwise. + // State (deleteBranch) preserved in case we re-open on a + // decision-required error. + navigateAway, ], ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts new file mode 100644 index 00000000000..debafab68e8 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts @@ -0,0 +1 @@ +export { useNavigateAwayFromWorkspace } from "./useNavigateAwayFromWorkspace"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts new file mode 100644 index 00000000000..6915878a0f6 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts @@ -0,0 +1,27 @@ +import { useNavigate, useParams } from "@tanstack/react-router"; +import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { getFlattenedV2WorkspaceIds } from "../../utils/getFlattenedV2WorkspaceIds"; + +/** + * If the user is viewing the workspace about to be removed, jump to the + * next visible sidebar sibling (or home). No-op otherwise. Called + * directly at the callsite — not via a callback prop — because + * plumbing this through dialog onDeleting was silently dropping the nav. + */ +export function useNavigateAwayFromWorkspace() { + const navigate = useNavigate(); + const params = useParams({ strict: false }); + const collections = useCollections(); + + return (workspaceId: string) => { + if (params.workspaceId !== workspaceId) return; + const ids = getFlattenedV2WorkspaceIds(collections); + const next = ids.find((id) => id !== workspaceId); + if (next) { + void navigateToV2Workspace(next, navigate); + } else { + void navigate({ to: "/" }); + } + }; +}