| ... | ... | @@ -17,6 +17,7 @@ import { |
| 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() { |
| 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() { |
| 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(() => { |