diff --git a/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts b/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts index 30e8ca520d..6a10e345c7 100644 --- a/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts @@ -2588,7 +2588,7 @@ export function registerWorktreeHandlers( */ ipcMain.handle( IPC_CHANNELS.TASK_WORKTREE_DISCARD, - async (_, taskId: string): Promise> => { + async (_, taskId: string, skipStatusChange?: boolean): Promise> => { try { const { task, project } = findTaskAndProject(taskId); if (!task || !project) { @@ -2631,9 +2631,13 @@ export function registerWorktreeHandlers( // Branch might already be deleted or not exist } - const mainWindow = getMainWindow(); - if (mainWindow) { - mainWindow.webContents.send(IPC_CHANNELS.TASK_STATUS_CHANGE, taskId, 'backlog'); + // Only send status change to backlog if not skipped + // (skip when caller will set a different status, e.g., 'done') + if (!skipStatusChange) { + const mainWindow = getMainWindow(); + if (mainWindow) { + mainWindow.webContents.send(IPC_CHANNELS.TASK_STATUS_CHANGE, taskId, 'backlog'); + } } return { @@ -2844,6 +2848,82 @@ export function registerWorktreeHandlers( } ); + /** + * Clear the staged state for a task + * This allows the user to re-stage changes if needed + */ + ipcMain.handle( + IPC_CHANNELS.TASK_CLEAR_STAGED_STATE, + async (_, taskId: string): Promise> => { + try { + const { task, project } = findTaskAndProject(taskId); + if (!task || !project) { + return { success: false, error: 'Task not found' }; + } + + const specsBaseDir = getSpecsDir(project.autoBuildPath); + const specDir = path.join(project.path, specsBaseDir, task.specId); + const planPath = path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN); + + // Use EAFP pattern (try/catch) instead of LBYL (existsSync check) to avoid TOCTOU race conditions + const { promises: fsPromises } = require('fs'); + const isFileNotFound = (err: unknown): boolean => + !!(err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT'); + + // Read, update, and write the plan file + let planContent: string; + try { + planContent = await fsPromises.readFile(planPath, 'utf-8'); + } catch (readErr) { + if (isFileNotFound(readErr)) { + return { success: false, error: 'Implementation plan not found' }; + } + throw readErr; + } + + const plan = JSON.parse(planContent); + + // Clear the staged state flags + delete plan.stagedInMainProject; + delete plan.stagedAt; + plan.updated_at = new Date().toISOString(); + + await fsPromises.writeFile(planPath, JSON.stringify(plan, null, 2)); + + // Also update worktree plan if it exists + const worktreePath = findTaskWorktree(project.path, task.specId); + if (worktreePath) { + const worktreePlanPath = path.join(worktreePath, specsBaseDir, task.specId, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN); + try { + const worktreePlanContent = await fsPromises.readFile(worktreePlanPath, 'utf-8'); + const worktreePlan = JSON.parse(worktreePlanContent); + delete worktreePlan.stagedInMainProject; + delete worktreePlan.stagedAt; + worktreePlan.updated_at = new Date().toISOString(); + await fsPromises.writeFile(worktreePlanPath, JSON.stringify(worktreePlan, null, 2)); + } catch (e) { + // Non-fatal - worktree plan update is best-effort + // ENOENT is expected when worktree has no plan file + if (!isFileNotFound(e)) { + console.warn('[CLEAR_STAGED_STATE] Failed to update worktree plan:', e); + } + } + } + + // Invalidate tasks cache to force reload + projectStore.invalidateTasksCache(project.id); + + return { success: true, data: { cleared: true } }; + } catch (error) { + console.error('Failed to clear staged state:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to clear staged state' + }; + } + } + ); + /** * Create a Pull Request from the worktree branch * Pushes the branch to origin and creates a GitHub PR using gh CLI diff --git a/apps/frontend/src/preload/api/task-api.ts b/apps/frontend/src/preload/api/task-api.ts index 27e55dd1d8..167e39f0ff 100644 --- a/apps/frontend/src/preload/api/task-api.ts +++ b/apps/frontend/src/preload/api/task-api.ts @@ -52,7 +52,8 @@ export interface TaskAPI { getWorktreeDiff: (taskId: string) => Promise>; mergeWorktree: (taskId: string, options?: { noCommit?: boolean }) => Promise>; mergeWorktreePreview: (taskId: string) => Promise>; - discardWorktree: (taskId: string) => Promise>; + discardWorktree: (taskId: string, skipStatusChange?: boolean) => Promise>; + clearStagedState: (taskId: string) => Promise>; listWorktrees: (projectId: string) => Promise>; worktreeOpenInIDE: (worktreePath: string, ide: SupportedIDE, customPath?: string) => Promise>; worktreeOpenInTerminal: (worktreePath: string, terminal: SupportedTerminal, customPath?: string) => Promise>; @@ -142,8 +143,11 @@ export const createTaskAPI = (): TaskAPI => ({ mergeWorktreePreview: (taskId: string): Promise> => ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_MERGE_PREVIEW, taskId), - discardWorktree: (taskId: string): Promise> => - ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_DISCARD, taskId), + discardWorktree: (taskId: string, skipStatusChange?: boolean): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_DISCARD, taskId, skipStatusChange), + + clearStagedState: (taskId: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.TASK_CLEAR_STAGED_STATE, taskId), listWorktrees: (projectId: string): Promise> => ipcRenderer.invoke(IPC_CHANNELS.TASK_LIST_WORKTREES, projectId), diff --git a/apps/frontend/src/renderer/components/KanbanBoard.tsx b/apps/frontend/src/renderer/components/KanbanBoard.tsx index 69a4612c50..56f797b8ab 100644 --- a/apps/frontend/src/renderer/components/KanbanBoard.tsx +++ b/apps/frontend/src/renderer/components/KanbanBoard.tsx @@ -19,10 +19,18 @@ import { sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable'; -import { Plus, Inbox, Loader2, Eye, CheckCircle2, Archive, RefreshCw } from 'lucide-react'; +import { Plus, Inbox, Loader2, Eye, CheckCircle2, Archive, RefreshCw, Trash2, FolderCheck } from 'lucide-react'; import { ScrollArea } from './ui/scroll-area'; import { Button } from './ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from './ui/alert-dialog'; import { TaskCard } from './TaskCard'; import { SortableTaskCard } from './SortableTaskCard'; import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants'; @@ -336,6 +344,11 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR const [overColumnId, setOverColumnId] = useState(null); const { showArchived, toggleShowArchived } = useViewState(); + // Worktree cleanup dialog state + const [worktreeDialogOpen, setWorktreeDialogOpen] = useState(false); + const [pendingDoneTask, setPendingDoneTask] = useState(null); + const [isCleaningUp, setIsCleaningUp] = useState(false); + // Calculate archived count for Done column button const archivedCount = useMemo(() => tasks.filter(t => t.metadata?.archivedAt).length, @@ -439,7 +452,39 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR } }; - const handleDragEnd = (event: DragEndEvent) => { + // Check if a task has a worktree (async check) + const checkTaskHasWorktree = async (taskId: string): Promise => { + try { + const result = await window.electronAPI.getWorktreeStatus(taskId); + return result.success && result.data?.exists === true; + } catch { + return false; + } + }; + + // Handle moving task to done with worktree cleanup option + const handleMoveToDone = async (task: Task, deleteWorktree: boolean) => { + setIsCleaningUp(true); + try { + if (deleteWorktree) { + // Delete worktree first, skip automatic status change to backlog + // since we're about to set status to 'done' + const result = await window.electronAPI.discardWorktree(task.id, true); + if (!result.success) { + console.error('Failed to delete worktree:', result.error); + // Continue anyway - user can clean up manually + } + } + // Mark as done + await persistTaskStatus(task.id, 'done'); + } finally { + setIsCleaningUp(false); + setWorktreeDialogOpen(false); + setPendingDoneTask(null); + } + }; + + const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; setActiveTask(null); setOverColumnId(null); @@ -449,27 +494,38 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR const activeTaskId = active.id as string; const overId = over.id as string; - // Check if dropped on a column - if (isValidDropColumn(overId)) { - const newStatus = overId; - const task = tasks.find((t) => t.id === activeTaskId); + // Determine target status + let targetStatus: TaskStatus | null = null; - if (task && task.status !== newStatus) { - // Persist status change to file and update local state - persistTaskStatus(activeTaskId, newStatus); + if (isValidDropColumn(overId)) { + targetStatus = overId; + } else { + // Dropped on another task - get its column + const overTask = tasks.find((t) => t.id === overId); + if (overTask) { + targetStatus = overTask.status; } - return; } - // Check if dropped on another task - move to that task's column - const overTask = tasks.find((t) => t.id === overId); - if (overTask) { - const task = tasks.find((t) => t.id === activeTaskId); - if (task && task.status !== overTask.status) { - // Persist status change to file and update local state - persistTaskStatus(activeTaskId, overTask.status); + if (!targetStatus) return; + + const task = tasks.find((t) => t.id === activeTaskId); + if (!task || task.status === targetStatus) return; + + // Special handling for moving to "done" - check for worktree + if (targetStatus === 'done') { + const hasWorktree = await checkTaskHasWorktree(task.id); + + if (hasWorktree) { + // Show dialog asking about worktree cleanup + setPendingDoneTask(task); + setWorktreeDialogOpen(true); + return; } } + + // Normal status change + persistTaskStatus(activeTaskId, targetStatus); }; return ( @@ -524,6 +580,73 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR ) : null} + + {/* Worktree cleanup confirmation dialog */} + + + + + + {t('kanban.worktreeCleanupTitle', 'Worktree Cleanup')} + + +
+ {pendingDoneTask?.stagedInMainProject ? ( +

+ {t('kanban.worktreeCleanupStaged', 'This task has been staged and has a worktree. Would you like to clean up the worktree?')} +

+ ) : ( +

+ {t('kanban.worktreeCleanupNotStaged', 'This task has a worktree with changes that have not been merged. Delete the worktree to mark as done, or cancel to review the changes first.')} +

+ )} + {pendingDoneTask && ( +

+ {pendingDoneTask.title} +

+ )} +
+
+
+ + + {/* Only show "Keep Worktree" option if task is staged */} + {pendingDoneTask?.stagedInMainProject && ( + + )} + + +
+
); } diff --git a/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx b/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx index 3b41168a2b..e51fb2e72c 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx @@ -526,6 +526,7 @@ function TaskDetailModalContent({ open, task, onOpenChange, onSwitchToTerminals, onClose={handleClose} onSwitchToTerminals={onSwitchToTerminals} onOpenInbuiltTerminal={onOpenInbuiltTerminal} + onReviewAgain={state.handleReviewAgain} showPRDialog={state.showPRDialog} isCreatingPR={state.isCreatingPR} onShowPRDialog={state.setShowPRDialog} diff --git a/apps/frontend/src/renderer/components/task-detail/TaskReview.tsx b/apps/frontend/src/renderer/components/task-detail/TaskReview.tsx index de60d8d017..d27a168931 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskReview.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskReview.tsx @@ -43,6 +43,7 @@ interface TaskReviewProps { onClose?: () => void; onSwitchToTerminals?: () => void; onOpenInbuiltTerminal?: (id: string, cwd: string) => void; + onReviewAgain?: () => void; // PR creation showPRDialog: boolean; isCreatingPR: boolean; @@ -90,6 +91,7 @@ export function TaskReview({ onClose, onSwitchToTerminals, onOpenInbuiltTerminal, + onReviewAgain, showPRDialog, isCreatingPR, onShowPRDialog, @@ -108,10 +110,23 @@ export function TaskReview({ /> )} - {/* Workspace Status - hide if staging was successful (worktree is deleted after staging) */} + {/* Workspace Status - priority: loading > fresh staging success > already staged (persisted) > worktree exists > no workspace */} {isLoadingWorktree ? ( - ) : worktreeStatus?.exists && !stagedSuccess ? ( + ) : stagedSuccess ? ( + /* Fresh staging just completed - StagedSuccessMessage is rendered above */ + null + ) : task.stagedInMainProject ? ( + /* Task was previously staged (persisted state) - show even if worktree still exists */ + + ) : worktreeStatus?.exists ? ( + /* Worktree exists but not yet staged - show staging UI */ - ) : task.stagedInMainProject && !stagedSuccess ? ( - ) : ( )} diff --git a/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts b/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts index df5330b278..ea310269d7 100644 --- a/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts +++ b/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts @@ -1,6 +1,6 @@ import { useState, useRef, useEffect, useCallback } from 'react'; import { useProjectStore } from '../../../stores/project-store'; -import { checkTaskRunning, isIncompleteHumanReview, getTaskProgress, useTaskStore } from '../../../stores/task-store'; +import { checkTaskRunning, isIncompleteHumanReview, getTaskProgress, useTaskStore, loadTasks } from '../../../stores/task-store'; import type { Task, TaskLogs, TaskLogPhase, WorktreeStatus, WorktreeDiff, MergeConflict, MergeStats, GitConflictInfo } from '../../../../shared/types'; /** @@ -282,6 +282,46 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { } }, [task.id]); + // Handle "Review Again" - clears staged state and reloads worktree info + const handleReviewAgain = useCallback(async () => { + // Clear staged success state if it was set in this session + setStagedSuccess(null); + setStagedProjectPath(undefined); + setSuggestedCommitMessage(undefined); + + // Reset merge preview to force re-check + setMergePreview(null); + hasLoadedPreviewRef.current = null; + + // Reset workspace error state + setWorkspaceError(null); + + // Reload worktree status + setIsLoadingWorktree(true); + try { + const [statusResult, diffResult] = await Promise.all([ + window.electronAPI.getWorktreeStatus(task.id), + window.electronAPI.getWorktreeDiff(task.id) + ]); + if (statusResult.success && statusResult.data) { + setWorktreeStatus(statusResult.data); + } + if (diffResult.success && diffResult.data) { + setWorktreeDiff(diffResult.data); + } + + // Reload task data from store to reflect cleared staged state + // (clearStagedState IPC already invalidated the cache) + if (selectedProject) { + await loadTasks(selectedProject.id); + } + } catch (err) { + console.error('Failed to reload worktree info:', err); + } finally { + setIsLoadingWorktree(false); + } + }, [task.id, selectedProject]); + // NOTE: Merge preview is NO LONGER auto-loaded on modal open. // User must click "Check for Conflicts" button to trigger the expensive preview operation. // This improves modal open performance significantly (avoids 1-30+ second Python subprocess). @@ -442,6 +482,7 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { handleLogsScroll, togglePhase, loadMergePreview, + handleReviewAgain, reloadPlanForIncompleteTask, }; } diff --git a/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceMessages.tsx b/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceMessages.tsx index 81daa813e7..e2b33dc904 100644 --- a/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceMessages.tsx +++ b/apps/frontend/src/renderer/components/task-detail/task-review/WorkspaceMessages.tsx @@ -1,4 +1,4 @@ -import { AlertCircle, GitMerge, Loader2, Trash2, Check } from 'lucide-react'; +import { AlertCircle, GitMerge, Loader2, Check, RotateCcw } from 'lucide-react'; import { useState } from 'react'; import { Button } from '../../ui/button'; import { persistTaskStatus } from '../../../stores/task-store'; @@ -89,13 +89,16 @@ interface StagedInProjectMessageProps { projectPath?: string; hasWorktree?: boolean; onClose?: () => void; + onReviewAgain?: () => void; } /** * Displays message when changes have already been staged in the main project */ -export function StagedInProjectMessage({ task, projectPath, hasWorktree = false, onClose }: StagedInProjectMessageProps) { +export function StagedInProjectMessage({ task, projectPath, hasWorktree = false, onClose, onReviewAgain }: StagedInProjectMessageProps) { const [isDeleting, setIsDeleting] = useState(false); + const [isMarkingDone, setIsMarkingDone] = useState(false); + const [isResetting, setIsResetting] = useState(false); const [error, setError] = useState(null); const handleDeleteWorktreeAndMarkDone = async () => { @@ -124,6 +127,46 @@ export function StagedInProjectMessage({ task, projectPath, hasWorktree = false, } }; + const handleMarkDoneOnly = async () => { + setIsMarkingDone(true); + setError(null); + + try { + await persistTaskStatus(task.id, 'done'); + onClose?.(); + } catch (err) { + console.error('Error marking task as done:', err); + setError(err instanceof Error ? err.message : 'Failed to mark as done'); + } finally { + setIsMarkingDone(false); + } + }; + + const handleReviewAgain = async () => { + if (!onReviewAgain) return; + + setIsResetting(true); + setError(null); + + try { + // Clear the staged flag via IPC + const result = await window.electronAPI.clearStagedState(task.id); + + if (!result.success) { + setError(result.error || 'Failed to reset staged state'); + return; + } + + // Trigger re-render by calling parent callback + onReviewAgain(); + } catch (err) { + console.error('Error resetting staged state:', err); + setError(err instanceof Error ? err.message : 'Failed to reset staged state'); + } finally { + setIsResetting(false); + } + }; + return (

@@ -143,12 +186,13 @@ export function StagedInProjectMessage({ task, projectPath, hasWorktree = false,

{/* Action buttons */} - {hasWorktree && ( -
-
+
+
+ {/* Primary action: Mark Done or Delete Worktree & Mark Done */} + {hasWorktree ? ( -
- {error && ( -

{error}

+ ) : ( + + )} +
+ + {/* Secondary actions row */} +
+ {/* Mark Done Only (when worktree exists) - allows keeping worktree */} + {hasWorktree && ( + + )} + + {/* Review Again button - only show if worktree exists and callback provided */} + {hasWorktree && onReviewAgain && ( + )} +
+ + {error && ( +

{error}

+ )} + + {hasWorktree && (

- This will delete the isolated workspace and mark the task as complete. + "Delete Worktree & Mark Done" cleans up the isolated workspace. "Mark Done Only" keeps it for reference.

-
- )} + )} +
); } diff --git a/apps/frontend/src/renderer/lib/mocks/workspace-mock.ts b/apps/frontend/src/renderer/lib/mocks/workspace-mock.ts index c3ef1a8429..838410e8b1 100644 --- a/apps/frontend/src/renderer/lib/mocks/workspace-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/workspace-mock.ts @@ -70,6 +70,11 @@ export const workspaceMock = { } }), + clearStagedState: async () => ({ + success: true, + data: { cleared: true } + }), + listWorktrees: async () => ({ success: true, data: { diff --git a/apps/frontend/src/shared/constants/ipc.ts b/apps/frontend/src/shared/constants/ipc.ts index 6229d70b8f..ddbb1c6042 100644 --- a/apps/frontend/src/shared/constants/ipc.ts +++ b/apps/frontend/src/shared/constants/ipc.ts @@ -42,6 +42,7 @@ export const IPC_CHANNELS = { TASK_LIST_WORKTREES: 'task:listWorktrees', TASK_ARCHIVE: 'task:archive', TASK_UNARCHIVE: 'task:unarchive', + TASK_CLEAR_STAGED_STATE: 'task:clearStagedState', // Task events (main -> renderer) TASK_PROGRESS: 'task:progress', diff --git a/apps/frontend/src/shared/i18n/locales/en/tasks.json b/apps/frontend/src/shared/i18n/locales/en/tasks.json index 9b4b8c4263..1e5b5cc2f6 100644 --- a/apps/frontend/src/shared/i18n/locales/en/tasks.json +++ b/apps/frontend/src/shared/i18n/locales/en/tasks.json @@ -76,7 +76,12 @@ "addTaskAriaLabel": "Add new task to backlog", "closeTaskDetailsAriaLabel": "Close task details", "editTask": "Edit task", - "cannotEditWhileRunning": "Cannot edit while task is running" + "cannotEditWhileRunning": "Cannot edit while task is running", + "worktreeCleanupTitle": "Worktree Cleanup", + "worktreeCleanupStaged": "This task has been staged and has a worktree. Would you like to clean up the worktree?", + "worktreeCleanupNotStaged": "This task has a worktree with changes that have not been merged. Delete the worktree to mark as done, or cancel to review the changes first.", + "keepWorktree": "Keep Worktree", + "deleteWorktree": "Delete Worktree & Mark Done" }, "execution": { "phases": { diff --git a/apps/frontend/src/shared/i18n/locales/fr/tasks.json b/apps/frontend/src/shared/i18n/locales/fr/tasks.json index 2cab8d9d62..cb0ea8446f 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/tasks.json +++ b/apps/frontend/src/shared/i18n/locales/fr/tasks.json @@ -76,7 +76,12 @@ "addTaskAriaLabel": "Ajouter une nouvelle tâche au backlog", "closeTaskDetailsAriaLabel": "Fermer les détails de la tâche", "editTask": "Modifier la tâche", - "cannotEditWhileRunning": "Impossible de modifier pendant l'exécution" + "cannotEditWhileRunning": "Impossible de modifier pendant l'exécution", + "worktreeCleanupTitle": "Nettoyage du Worktree", + "worktreeCleanupStaged": "Cette tâche a été préparée et possède un worktree. Voulez-vous nettoyer le worktree ?", + "worktreeCleanupNotStaged": "Cette tâche possède un worktree avec des changements non fusionnés. Supprimez le worktree pour marquer comme terminé, ou annulez pour réviser les changements d'abord.", + "keepWorktree": "Garder le Worktree", + "deleteWorktree": "Supprimer le Worktree & Marquer Terminé" }, "execution": { "phases": { diff --git a/apps/frontend/src/shared/types/ipc.ts b/apps/frontend/src/shared/types/ipc.ts index 783291042f..48789df94c 100644 --- a/apps/frontend/src/shared/types/ipc.ts +++ b/apps/frontend/src/shared/types/ipc.ts @@ -170,7 +170,8 @@ export interface ElectronAPI { mergeWorktree: (taskId: string, options?: { noCommit?: boolean }) => Promise>; mergeWorktreePreview: (taskId: string) => Promise>; createWorktreePR: (taskId: string, options?: WorktreeCreatePROptions) => Promise>; - discardWorktree: (taskId: string) => Promise>; + discardWorktree: (taskId: string, skipStatusChange?: boolean) => Promise>; + clearStagedState: (taskId: string) => Promise>; listWorktrees: (projectId: string) => Promise>; worktreeOpenInIDE: (worktreePath: string, ide: SupportedIDE, customPath?: string) => Promise>; worktreeOpenInTerminal: (worktreePath: string, terminal: SupportedTerminal, customPath?: string) => Promise>;