- 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 | import { useNativeApi } from "../hooks/useNativeApi"; | 17 | import { useNativeApi } from "../hooks/useNativeApi"; |
| 18 | import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery"; | 18 | import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery"; |
| 19 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; | 19 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; |
| 20 | import { toastManager } from "./ui/toast"; | ||
| 20 | import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; | 21 | import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; |
| 21 | 22 | ||
| 22 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; | 23 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; |
| @@ -342,17 +343,59 @@ export default function Sidebar() { · 319 unmodified lines | |||
| 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; |
| 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 | 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 | } |
| 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 | 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() { · 17 unmodified lines | |||
| 376 | 419 | ||
| 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 | ); |
| 381 | 424 | ||
| 382 | useEffect(() => { | 425 | useEffect(() => { |