Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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({
Expand All @@ -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(
Expand All @@ -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],
Expand All @@ -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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Delay route jump until delete is committed

Calling navigateAway(workspaceId) before destroy(...) means users are redirected even when deletion fails in preflight/teardown (for example, dirty worktree conflict). In those cases the workspace still exists, but the current view has already been switched away, so canceling or retrying now happens from the wrong context and the user loses their place unexpectedly. The navigation should happen only after the delete has actually passed the commit point or succeeded.

Useful? React with 👍 / 👎.


setError(null);
onOpenChange(false);
markDeleting(workspaceId);
Expand All @@ -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;
Expand All @@ -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,
],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useNavigateAwayFromWorkspace } from "./useNavigateAwayFromWorkspace";
Original file line number Diff line number Diff line change
@@ -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: "/" });
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof v2UserPreferencesSchema>;
Expand All @@ -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,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Loading