authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-15 14:19:35-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-15 16:35:17-08:00
log56a09ad1aa6334101047aa1f7da9882e38b837bf
tree1b80321523e48271f5ff98a51be1622fada14610
parentbe1c8c4656adad2592d718597018d0df120c58d3

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

1 files changed, 46 insertions(+), 3 deletions(-)

apps/web/src/components/Sidebar.tsx+46-3
...@@ -17,6 +17,7 @@ import {...@@ -17,6 +17,7 @@ import {
17import { useNativeApi } from "../hooks/useNativeApi";17import { useNativeApi } from "../hooks/useNativeApi";
18import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery";18import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery";
19import { serverConfigQueryOptions } from "../lib/serverReactQuery";19import { serverConfigQueryOptions } from "../lib/serverReactQuery";
20import { toastManager } from "./ui/toast";
20import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";21import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";
2122
22const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;23const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;
...@@ -342,17 +343,59 @@ export default function Sidebar() {...@@ -342,17 +343,59 @@ export default function Sidebar() {
342 const project = state.projects.find((entry) => entry.id === projectId);343 const project = state.projects.find((entry) => entry.id === projectId);
343 if (!project) return;344 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
345 if (isElectron) {363 if (isElectron) {
346 try {364 try {
347 await api.projects.remove({ id: projectId });365 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 });
349 return;375 return;
350 }376 }
351 }377 }
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>();
354 await Promise.all(381 await Promise.all(
355 projectThreads.map(async (thread) => {382 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
356 if (thread.session?.sessionId) {399 if (thread.session?.sessionId) {
357 try {400 try {
358 await api.providers.stopSession({401 await api.providers.stopSession({
...@@ -376,7 +419,7 @@ export default function Sidebar() {...@@ -376,7 +419,7 @@ export default function Sidebar() {
376419
377 dispatch({ type: "DELETE_PROJECT", projectId });420 dispatch({ type: "DELETE_PROJECT", projectId });
378 },421 },
379 [api, dispatch, state.projects, state.threads],422 [api, dispatch, removeWorktreeMutation, state.projects, state.threads],
380 );423 );
381424
382 useEffect(() => {425 useEffect(() => {