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..10f39af2279 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 { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; interface UseDestroyDialogStateOptions { @@ -17,11 +18,11 @@ interface UseDestroyDialogStateOptions { * Drives the delete flow for `DashboardSidebarDeleteDialog`. * * UX pattern: - * - On confirm, close the dialog immediately, mark the workspace as - * deleting (sidebar row hides optimistically), and run destroy in - * the background silently. No loading toast — destroy can take - * 10–20s and a persistent toast across that window feels bad. The - * hidden row is the feedback. + * - On confirm, navigate off the workspace first (if viewing it), + * close the dialog, mark the workspace deleting (row hides + * optimistically), fire a one-shot "Deleting..." toast, and let + * destroy run in the background. A loading toast across the 10–20s + * teardown feels worse than fire-and-forget + hidden row. * - On success, `onDeleted` removes the row from sidebar state. * - On error, `clearDeleting` runs in the `finally` block so the row * reappears. For decision-required errors (CONFLICT, TEARDOWN_FAILED) @@ -37,6 +38,7 @@ export function useDestroyDialogState({ }: UseDestroyDialogStateOptions) { const { destroy } = useDestroyWorkspace(workspaceId); const { markDeleting, clearDeleting } = useDeletingWorkspaces(); + const navigateAway = useNavigateAwayFromWorkspace(); const [deleteBranch, setDeleteBranch] = useState(false); const [error, setError] = useState(null); @@ -63,11 +65,16 @@ export function useDestroyDialogState({ if (inFlight.current) return; inFlight.current = true; - // Optimistic close. State (deleteBranch) preserved in case we re-open + // Navigate off the doomed workspace FIRST. Closing the dialog + // 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); + toast(`Deleting "${workspaceName}"...`); try { const result = await destroy({ deleteBranch, force }); @@ -96,6 +103,7 @@ export function useDestroyDialogState({ onDeleted, markDeleting, clearDeleting, + navigateAway, ], ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index 750c00c58c8..0ba0972232c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -43,11 +43,11 @@ export function DashboardSidebarWorkspaceItem({ handleCreateSection, handleDeleted, handleOpenInFinder, + handleRemoveFromSidebar, isActive, isDeleteDialogOpen, isRenaming, moveWorkspaceToSection, - removeWorkspaceFromSidebar, renameValue, setIsDeleteDialogOpen, setRenameValue, @@ -124,7 +124,7 @@ export function DashboardSidebarWorkspaceItem({ onOpenInFinder={handleOpenInFinder} onCopyPath={handleCopyPath} onCopyBranchName={handleCopyBranchName} - onRemoveFromSidebar={() => removeWorkspaceFromSidebar(id)} + onRemoveFromSidebar={handleRemoveFromSidebar} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} > @@ -189,7 +189,7 @@ export function DashboardSidebarWorkspaceItem({ onOpenInFinder={handleOpenInFinder} onCopyPath={handleCopyPath} onCopyBranchName={handleCopyBranchName} - onRemoveFromSidebar={() => removeWorkspaceFromSidebar(id)} + onRemoveFromSidebar={handleRemoveFromSidebar} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} > diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index 3eb9f4f059f..7c0dc64ce0e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -5,11 +5,8 @@ import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; import { apiTrpcClient } from "renderer/lib/api-trpc-client"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { electronTrpcClient } from "renderer/lib/trpc-client"; -import { getDeleteFocusTargetWorkspaceId } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId"; -import { getFlattenedV2WorkspaceIds } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds"; -import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; +import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; interface UseDashboardSidebarWorkspaceItemActionsOptions { @@ -27,7 +24,7 @@ export function useDashboardSidebarWorkspaceItemActions({ }: UseDashboardSidebarWorkspaceItemActionsOptions) { const navigate = useNavigate(); const matchRoute = useMatchRoute(); - const collections = useCollections(); + const navigateAway = useNavigateAwayFromWorkspace(); const { activeHostUrl } = useLocalHostService(); const { copyToClipboard } = useCopyToClipboard(); const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = @@ -77,27 +74,13 @@ export function useDashboardSidebarWorkspaceItemActions({ } }; - /** - * Runs after `workspaceCleanup.destroy` succeeds. Removes the row from - * the sidebar and, if we were viewing the deleted workspace, navigates - * to the next sibling or home. - */ const handleDeleted = () => { - const focusTargetId = isActive - ? getDeleteFocusTargetWorkspaceId( - getFlattenedV2WorkspaceIds(collections), - workspaceId, - ) - : null; - removeWorkspaceFromSidebar(workspaceId); + }; - if (!isActive) return; - if (focusTargetId) { - void navigateToV2Workspace(focusTargetId, navigate); - } else { - void navigate({ to: "/" }); - } + const handleRemoveFromSidebar = () => { + navigateAway(workspaceId); + removeWorkspaceFromSidebar(workspaceId); }; const handleCreateSection = () => { @@ -169,11 +152,11 @@ export function useDashboardSidebarWorkspaceItemActions({ handleCreateSection, handleDeleted, handleOpenInFinder, + handleRemoveFromSidebar, isActive, isDeleteDialogOpen, isRenaming, moveWorkspaceToSection, - removeWorkspaceFromSidebar, renameValue, setIsDeleteDialogOpen, setRenameValue, 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: "/" }); + } + }; +}