- 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
Unified · Split
| @@ -17,6 +17,7 @@ import { · 16 unmodified lines | ||
| 17 | 17 | import { useNativeApi } from "../hooks/useNativeApi"; |
| 18 | 18 | import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery"; |
| 19 | 19 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; |
| 20 | +import { toastManager } from "./ui/toast"; | |
| 20 | 21 | import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; |
| 21 | 22 | |
| 22 | 23 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; |
| @@ -342,17 +343,59 @@ export default function Sidebar() { · 319 unmodified lines | ||
| 342 | 343 | const project = state.projects.find((entry) => entry.id === projectId); |
| 343 | 344 | if (!project) return; |
| 344 | 345 | |
| 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 | 363 | if (isElectron) { |
| 346 | 364 | try { |
| 347 | 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 | 375 | return; |
| 350 | 376 | } |
| 351 | 377 | } |
| 352 | 378 | |
| 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 | 381 | await Promise.all( |
| 355 | 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 | 399 | if (thread.session?.sessionId) { |
| 357 | 400 | try { |
| 358 | 401 | await api.providers.stopSession({ |
| @@ -376,7 +419,7 @@ export default function Sidebar() { · 17 unmodified lines | ||
| 376 | 419 | |
| 377 | 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 | ); |
| 381 | 424 | |
| 382 | 425 | useEffect(() => { |