History

Confirm project deletion and clean up orphaned worktrees

- Prompt before deleting a project with thread and linked worktree counts - Best-effort remove orphaned worktrees when project threads are deleted - Show toast with error details if project deletion fails

Julius Marminge committed 9 months ago

Commit f6a0b671 parent ebac7551 file · +46 −3Browse files

Unified · Split

apps/web/src/components/Sidebar.tsx+46−3
@@ -17,6 +17,7 @@ import { · 16 unmodified lines
1717 import { useNativeApi } from "../hooks/useNativeApi";
1818 import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery";
1919 import { serverConfigQueryOptions } from "../lib/serverReactQuery";
20+import { toastManager } from "./ui/toast";
2021 import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";
2122
2223 const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;
@@ -342,17 +343,59 @@ export default function Sidebar() { · 319 unmodified lines
342343 const project = state.projects.find((entry) => entry.id === projectId);
343344 if (!project) return;
344345
346+ const projectThreads = state.threads.filter((thread) => thread.projectId === projectId);
347+ const linkedWorktreeCount = new Set(
348+ projectThreads
349+ .map((thread) => thread.worktreePath?.trim() ?? "")
350+ .filter((worktreePath) => worktreePath.length > 0),
351+ ).size;
352+ const confirmed = await api.dialogs.confirm(
353+ [
354+ `Delete project "${project.name}"?`,
355+ `This will delete ${projectThreads.length} thread${projectThreads.length === 1 ? "" : "s"}.`,
356+ linkedWorktreeCount > 0
357+ ? `Linked worktrees to remove: ${linkedWorktreeCount}.`
358+ : "No linked worktrees detected.",
359+ ].join("\n"),
360+ );
361+ if (!confirmed) return;
362+
345363 if (isElectron) {
346364 try {
347365 await api.projects.remove({ id: projectId });
348− } catch {
366+ } catch (error) {
367+ const message =
368+ error instanceof Error ? error.message : "Unknown error deleting project.";
369+ console.error("Failed to remove project", { projectId, error });
370+ toastManager.add({
371+ type: "error",
372+ title: `Failed to delete "${project.name}"`,
373+ description: message,
374+ });
349375 return;
350376 }
351377 }
352378
353− const projectThreads = state.threads.filter((thread) => thread.projectId === projectId);
379+ const retainedThreads = state.threads.filter((thread) => thread.projectId !== projectId);
380+ const removedWorktreePaths = new Set<string>();
354381 await Promise.all(
355382 projectThreads.map(async (thread) => {
383+ const orphanedWorktreePath = getOrphanedWorktreePathForThread(
384+ [thread, ...retainedThreads],
385+ thread.id,
386+ );
387+ if (orphanedWorktreePath && !removedWorktreePaths.has(orphanedWorktreePath)) {
388+ removedWorktreePaths.add(orphanedWorktreePath);
389+ try {
390+ await removeWorktreeMutation.mutateAsync({
391+ cwd: project.cwd,
392+ path: orphanedWorktreePath,
393+ });
394+ } catch {
395+ // Worktree deletion is best-effort and should not block project deletion.
396+ }
397+ }
398+
356399 if (thread.session?.sessionId) {
357400 try {
358401 await api.providers.stopSession({
@@ -376,7 +419,7 @@ export default function Sidebar() { · 17 unmodified lines
376419
377420 dispatch({ type: "DELETE_PROJECT", projectId });
378421 },
379− [api, dispatch, state.projects, state.threads],
422+ [api, dispatch, removeWorktreeMutation, state.projects, state.threads],
380423 );
381424
382425 useEffect(() => {