| ... | ... | @@ -344,19 +344,17 @@ export default function Sidebar() { |
| 344 | 344 | if (!project) return; |
| 345 | 345 | |
| 346 | 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; |
| 347 | if (projectThreads.length > 0) { |
| 348 | toastManager.add({ |
| 349 | type: "warning", |
| 350 | title: "Project is not empty", |
| 351 | description: "Delete all threads in this project before deleting it.", |
| 352 | }); |
| 353 | return; |
| 354 | } |
| 355 | |
| 352 | 356 | 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"), |
| 357 | [`Delete project "${project.name}"?`, "This action cannot be undone."].join("\n"), |
| 360 | 358 | ); |
| 361 | 359 | if (!confirmed) return; |
| 362 | 360 | |
| ... | ... | @@ -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 | 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 | 382 | useEffect(() => { |