| ... | @@ -344,19 +344,17 @@ export default function Sidebar() { | ... | @@ -344,19 +344,17 @@ export default function Sidebar() { |
| 344 | if (!project) return; | 344 | if (!project) return; |
| 345 | | 345 | |
| 346 | const projectThreads = state.threads.filter((thread) => thread.projectId === projectId); | 346 | const projectThreads = state.threads.filter((thread) => thread.projectId === projectId); |
| 347 | const linkedWorktreeCount = new Set( | 347 | if (projectThreads.length > 0) { |
| 348 | projectThreads | 348 | toastManager.add({ |
| 349 | .map((thread) => thread.worktreePath?.trim() ?? "") | 349 | type: "warning", |
| 350 | .filter((worktreePath) => worktreePath.length > 0), | 350 | title: "Project is not empty", |
| 351 | ).size; | 351 | description: "Delete all threads in this project before deleting it.", |
| | 352 | }); |
| | 353 | return; |
| | 354 | } |
| | 355 | |
| 352 | const confirmed = await api.dialogs.confirm( | 356 | const confirmed = await api.dialogs.confirm( |
| 353 | [ | 357 | [`Delete project "${project.name}"?`, "This action cannot be undone."].join("\n"), |
| 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 | ); | 358 | ); |
| 361 | if (!confirmed) return; | 359 | if (!confirmed) return; |
| 362 | | 360 | |
| ... | @@ -376,50 +374,9 @@ export default function Sidebar() { | ... | @@ -376,50 +374,9 @@ export default function Sidebar() { |
| 376 | } | 374 | } |
| 377 | } | 375 | } |
| 378 | | 376 | |
| 379 | const retainedThreads = state.threads.filter((thread) => thread.projectId !== projectId); | | |
| 380 | const removedWorktreePaths = new Set<string>(); | | |
| 381 | await Promise.all( | | |
| 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 | | | |
| 399 | if (thread.session?.sessionId) { | | |
| 400 | try { | | |
| 401 | await api.providers.stopSession({ | | |
| 402 | sessionId: thread.session.sessionId, | | |
| 403 | }); | | |
| 404 | } catch { | | |
| 405 | // Session may already be stopped | | |
| 406 | } | | |
| 407 | } | | |
| 408 | | | |
| 409 | try { | | |
| 410 | await api.terminal.close({ | | |
| 411 | threadId: thread.id, | | |
| 412 | deleteHistory: true, | | |
| 413 | }); | | |
| 414 | } catch { | | |
| 415 | // Terminal may already be closed | | |
| 416 | } | | |
| 417 | }), | | |
| 418 | ); | | |
| 419 | | | |
| 420 | dispatch({ type: "DELETE_PROJECT", projectId }); | 377 | dispatch({ type: "DELETE_PROJECT", projectId }); |
| 421 | }, | 378 | }, |
| 422 | [api, dispatch, removeWorktreeMutation, state.projects, state.threads], | 379 | [api, dispatch, state.projects, state.threads], |
| 423 | ); | 380 | ); |
| 424 | | 381 | |
| 425 | useEffect(() => { | 382 | useEffect(() => { |