diff --git a/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts b/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts index 9545d8ea0f3..9d7b20bd21b 100644 --- a/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts +++ b/apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts @@ -9,10 +9,15 @@ 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; 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 +62,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..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 @@ -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({ @@ -43,8 +45,10 @@ export function useDestroyDialogState({ }: UseDestroyDialogStateOptions) { 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 +67,7 @@ export function useDestroyDialogState({ const handleOpenChange = useCallback( (next: boolean) => { - if (!next) { - setDeleteBranch(false); - setError(null); - } + if (!next) setError(null); onOpenChange(next); }, [onOpenChange], @@ -77,8 +78,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 +104,6 @@ export function useDestroyDialogState({ } } for (const warning of result.warnings) toast.warning(warning); - setDeleteBranch(false); onDeleted?.(); } catch (err) { const e = err as DestroyWorkspaceError; @@ -122,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: "/" }); + } + }; +} 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);