From cec8e65ee814bcffd1e54c82727e5c53ef7dc1df Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Mon, 9 Feb 2026 11:22:23 +0100 Subject: [PATCH] fix(worktree): remove auto-commit on deletion and add uncommitted changes warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worktree deletion was failing with ETIMEDOUT because git add/commit scanned massive node_modules directories (Electron.app bundles). The auto-commit step was unnecessary — the Python backend never does it, and users explicitly choose to delete. Changes: - Remove auto-commit step from worktree-cleanup.ts and all call sites - Add /bin/rm -rf fallback when Node.js rm() fails on macOS .app bundles - Add TASK_CHECK_WORKTREE_CHANGES IPC to detect uncommitted changes - Show amber warning in delete dialog when worktree has uncommitted files - Add deleteDialog i18n keys for en/fr Co-Authored-By: Claude Opus 4.6 --- .../main/ipc-handlers/task/crud-handlers.ts | 50 +++++++++-- .../ipc-handlers/task/worktree-handlers.ts | 13 +-- .../terminal/worktree-handlers.ts | 1 - .../src/main/utils/worktree-cleanup.ts | 87 +++++-------------- apps/frontend/src/preload/api/task-api.ts | 7 ++ .../components/task-detail/TaskActions.tsx | 35 ++++++-- .../task-detail/TaskDetailModal.tsx | 29 +++++-- .../task-detail/hooks/useTaskDetail.ts | 21 +++++ .../frontend/src/renderer/lib/browser-mock.ts | 6 ++ .../src/renderer/lib/mocks/task-mock.ts | 6 ++ apps/frontend/src/shared/constants/ipc.ts | 1 + .../src/shared/i18n/locales/en/tasks.json | 11 +++ .../src/shared/i18n/locales/fr/tasks.json | 11 +++ apps/frontend/src/shared/types/ipc.ts | 3 + 14 files changed, 185 insertions(+), 96 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/task/crud-handlers.ts b/apps/frontend/src/main/ipc-handlers/task/crud-handlers.ts index b906a581..8f68ee88 100644 --- a/apps/frontend/src/main/ipc-handlers/task/crud-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/task/crud-handlers.ts @@ -2,6 +2,7 @@ import { ipcMain, nativeImage } from 'electron'; import { IPC_CHANNELS, AUTO_BUILD_PATHS, getSpecsDir } from '../../../shared/constants'; import type { IPCResult, Task, TaskMetadata } from '../../../shared/types'; import path from 'path'; +import { execFileSync } from 'child_process'; import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, Dirent } from 'fs'; import { projectStore } from '../../project-store'; import { titleGenerator } from '../../title-generator'; @@ -10,6 +11,8 @@ import { findTaskAndProject } from './shared'; import { findAllSpecPaths, isValidTaskId } from '../../utils/spec-path-helpers'; import { isPathWithinBase, findTaskWorktree } from '../../worktree-paths'; import { cleanupWorktree } from '../../utils/worktree-cleanup'; +import { getToolPath } from '../../cli-tool-manager'; +import { getIsolatedGitEnv } from '../../utils/git-isolation'; import { taskStateManager } from '../../task-state-manager'; /** @@ -284,7 +287,6 @@ export function registerTaskCRUDHandlers(agentManager: AgentManager): void { worktreePath, projectPath: project.path, specId: task.specId, - commitMessage: 'Auto-save before task deletion', logPrefix: '[TASK_DELETE]', deleteBranch: true }); @@ -293,13 +295,8 @@ export function registerTaskCRUDHandlers(agentManager: AgentManager): void { console.error(`[TASK_DELETE] Worktree cleanup failed:`, cleanupResult.warnings); hasErrors = true; errors.push(`Worktree cleanup: ${cleanupResult.warnings.join('; ')}`); - } else { - if (cleanupResult.autoCommitted) { - console.warn(`[TASK_DELETE] Auto-committed uncommitted work before deletion`); - } - if (cleanupResult.warnings.length > 0) { - console.warn(`[TASK_DELETE] Cleanup warnings:`, cleanupResult.warnings); - } + } else if (cleanupResult.warnings.length > 0) { + console.warn(`[TASK_DELETE] Cleanup warnings:`, cleanupResult.warnings); } } @@ -650,4 +647,41 @@ export function registerTaskCRUDHandlers(agentManager: AgentManager): void { } } ); + + /** + * Check if a task's worktree has uncommitted changes + * Used by the UI before showing the delete confirmation dialog + */ + ipcMain.handle( + IPC_CHANNELS.TASK_CHECK_WORKTREE_CHANGES, + async (_, taskId: string): Promise> => { + const { task, project } = findTaskAndProject(taskId); + if (!task || !project) { + return { success: true, data: { hasChanges: false } }; + } + + const worktreePath = findTaskWorktree(project.path, task.specId); + if (!worktreePath) { + return { success: true, data: { hasChanges: false } }; + } + + try { + const status = execFileSync(getToolPath('git'), ['status', '--porcelain'], { + cwd: worktreePath, + encoding: 'utf-8', + env: getIsolatedGitEnv(), + timeout: 5000 + }).trim(); + + const changedFiles = status ? status.split('\n').length : 0; + return { + success: true, + data: { hasChanges: changedFiles > 0, worktreePath, changedFileCount: changedFiles } + }; + } catch { + // On error/timeout, return false as fail-safe (don't block deletion) + return { success: true, data: { hasChanges: false, worktreePath } }; + } + } + ); } 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 356fff78..57fedbdf 100644 --- a/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts @@ -2310,7 +2310,6 @@ export function registerWorktreeHandlers( worktreePath, projectPath: project.path, specId: task.specId, - commitMessage: 'Auto-save before merge cleanup', logPrefix: '[TASK_WORKTREE_MERGE]', deleteBranch: true }); @@ -2753,7 +2752,6 @@ export function registerWorktreeHandlers( worktreePath, projectPath: project.path, specId: task.specId, - commitMessage: 'Auto-save before discard', logPrefix: '[TASK_WORKTREE_DISCARD]', deleteBranch: true }); @@ -2770,9 +2768,6 @@ export function registerWorktreeHandlers( if (cleanupResult.warnings.length > 0) { console.warn('[TASK_WORKTREE_DISCARD] Cleanup warnings:', cleanupResult.warnings); } - if (cleanupResult.autoCommitted) { - console.warn('[TASK_WORKTREE_DISCARD] Auto-committed uncommitted work before discard'); - } // Only send status change to backlog if not skipped @@ -2844,13 +2839,11 @@ export function registerWorktreeHandlers( }; } - // Use cleanupWorktree which auto-commits any uncommitted changes before deletion - // This preserves work in git history (recoverable via reflog for ~90 days) + // Use cleanupWorktree for robust, cross-platform worktree deletion const cleanupResult = await cleanupWorktree({ worktreePath, projectPath: project.path, specId: specName, - commitMessage: 'Auto-save before orphaned worktree deletion', logPrefix: '[ORPHAN_CLEANUP]', deleteBranch: true }); @@ -2866,9 +2859,7 @@ export function registerWorktreeHandlers( success: true, data: { success: true, - message: cleanupResult.autoCommitted - ? 'Orphaned worktree deleted (uncommitted changes were auto-saved)' - : 'Orphaned worktree deleted successfully' + message: 'Orphaned worktree deleted successfully' } }; } catch (error) { diff --git a/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts b/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts index 97eaeaf8..dba7a965 100644 --- a/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts @@ -789,7 +789,6 @@ async function removeTerminalWorktree( logPrefix: '[TerminalWorktree]', deleteBranch: deleteBranch && config.hasGitBranch, branchName: config.branchName || undefined, - commitMessage: 'Auto-save before terminal worktree deletion', }); if (!cleanupResult.success) { diff --git a/apps/frontend/src/main/utils/worktree-cleanup.ts b/apps/frontend/src/main/utils/worktree-cleanup.ts index e28d62d6..6c5be7b1 100644 --- a/apps/frontend/src/main/utils/worktree-cleanup.ts +++ b/apps/frontend/src/main/utils/worktree-cleanup.ts @@ -7,10 +7,10 @@ * The standard `git worktree remove --force` fails on Windows when the worktree * contains untracked files (node_modules, build artifacts, etc.). This utility: * - * 1. Auto-commits any uncommitted work (preserves in git history for ~90 days via reflog) - * 2. Manually deletes the worktree directory with retry logic for Windows file locks - * 3. Prunes git's internal worktree references - * 4. Optionally deletes the associated branch + * 1. Manually deletes the worktree directory with retry logic for file locks + * (falls back to shell `rm -rf` on Unix when Node.js rm() fails) + * 2. Prunes git's internal worktree references + * 3. Optionally deletes the associated branch * * Related issue: https://github.com/AndyMik90/Auto-Claude/issues/1539 */ @@ -32,8 +32,6 @@ export interface WorktreeCleanupOptions { projectPath: string; /** Spec ID for generating branch name (e.g., "001-my-feature") */ specId: string; - /** Custom commit message for auto-commit (default: "Auto-save before deletion") */ - commitMessage?: string; /** Log prefix for console messages (e.g., "[TASK_DELETE]") */ logPrefix?: string; /** Whether to delete the associated branch (default: true) */ @@ -56,8 +54,6 @@ export interface WorktreeCleanupResult { success: boolean; /** The branch that was deleted (if deleteBranch was true) */ branch?: string; - /** Whether uncommitted changes were auto-committed */ - autoCommitted?: boolean; /** Warnings that occurred during cleanup (non-fatal issues) */ warnings: string[]; } @@ -133,7 +129,18 @@ async function deleteDirectoryWithRetry( } } - // All retries exhausted + // All retries exhausted - try shell rm -rf as fallback on Unix + // Node's rm() can fail with ENOTEMPTY on macOS .app bundles + if (process.platform !== 'win32') { + try { + console.warn(`${logPrefix} Node.js rm() failed, trying /bin/rm -rf fallback...`); + execFileSync('/bin/rm', ['-rf', dirPath], { timeout: 60000 }); + return; + } catch { + // Fall through to throw original error + } + } + throw lastError || new Error('Failed to delete directory after retries'); } @@ -143,10 +150,10 @@ async function deleteDirectoryWithRetry( * This function handles the Windows-specific issue where `git worktree remove --force` * fails when the worktree contains untracked files. The approach is: * - * 1. Auto-commit any uncommitted changes (preserves work in git history) - * 2. Manually delete the directory with retry logic for file locks - * 3. Run `git worktree prune` to clean up git's internal references - * 4. Optionally delete the associated branch + * 1. Manually delete the directory with retry logic for file locks + * (falls back to shell `rm -rf` on Unix when Node.js rm() fails) + * 2. Run `git worktree prune` to clean up git's internal references + * 3. Optionally delete the associated branch * * All errors except directory deletion are logged but don't fail the operation. * @@ -164,9 +171,6 @@ async function deleteDirectoryWithRetry( * * if (result.success) { * console.log('Cleanup successful'); - * if (result.autoCommitted) { - * console.log('Note: Uncommitted work was auto-saved'); - * } * } * ``` */ @@ -175,7 +179,6 @@ export async function cleanupWorktree(options: WorktreeCleanupOptions): Promise< worktreePath, projectPath, specId, - commitMessage = 'Auto-save before deletion', logPrefix = '[WORKTREE_CLEANUP]', deleteBranch = true, branchName, @@ -185,7 +188,6 @@ export async function cleanupWorktree(options: WorktreeCleanupOptions): Promise< } = options; const warnings: string[] = []; - let autoCommitted = false; // Security: Validate that worktreePath is within the expected worktree directories // This prevents path traversal attacks and accidental deletion of wrong directories @@ -209,48 +211,7 @@ export async function cleanupWorktree(options: WorktreeCleanupOptions): Promise< console.warn(`${logPrefix} Associated branch: ${branch}`); } - // 2. Auto-commit any uncommitted changes to preserve work - // This ensures the user can recover their work via `git reflog` for ~90 days - if (existsSync(worktreePath)) { - try { - // Check if there are any changes to commit - const status = execFileSync(getToolPath('git'), ['status', '--porcelain'], { - cwd: worktreePath, - encoding: 'utf-8', - env: getIsolatedGitEnv(), - timeout - }); - - if (status.trim()) { - // There are uncommitted changes - commit them before deletion - console.warn(`${logPrefix} Found uncommitted changes, auto-committing...`); - - execFileSync(getToolPath('git'), ['add', '-A'], { - cwd: worktreePath, - encoding: 'utf-8', - env: getIsolatedGitEnv(), - timeout - }); - - execFileSync(getToolPath('git'), ['commit', '-m', commitMessage], { - cwd: worktreePath, - encoding: 'utf-8', - env: getIsolatedGitEnv(), - timeout - }); - - console.warn(`${logPrefix} Auto-committed changes before deletion`); - autoCommitted = true; - } - } catch (commitError) { - // Non-critical - log and continue with deletion - const msg = commitError instanceof Error ? commitError.message : String(commitError); - console.warn(`${logPrefix} Failed to auto-commit changes (non-critical): ${msg}`); - warnings.push(`Auto-commit failed: ${msg}`); - } - } - - // 3. Delete the worktree directory manually + // 2. Delete the worktree directory manually // This is required because `git worktree remove --force` fails on Windows // when the directory contains untracked files (node_modules, build artifacts, etc.) if (existsSync(worktreePath)) { @@ -265,7 +226,6 @@ export async function cleanupWorktree(options: WorktreeCleanupOptions): Promise< return { success: false, branch: branch || undefined, - autoCommitted, warnings: [...warnings, `Directory deletion failed: ${msg}`] }; } @@ -273,7 +233,7 @@ export async function cleanupWorktree(options: WorktreeCleanupOptions): Promise< console.warn(`${logPrefix} Worktree directory already deleted`); } - // 4. Prune git's internal worktree references + // 3. Prune git's internal worktree references // After manual deletion, git still thinks the worktree exists in .git/worktrees/ // Running prune cleans up these stale references try { @@ -291,7 +251,7 @@ export async function cleanupWorktree(options: WorktreeCleanupOptions): Promise< warnings.push(`Worktree prune failed: ${msg}`); } - // 5. Delete the branch if requested + // 4. Delete the branch if requested if (deleteBranch && branch) { try { execFileSync(getToolPath('git'), ['branch', '-D', branch], { @@ -313,7 +273,6 @@ export async function cleanupWorktree(options: WorktreeCleanupOptions): Promise< return { success: true, branch: branch || undefined, - autoCommitted, warnings }; } diff --git a/apps/frontend/src/preload/api/task-api.ts b/apps/frontend/src/preload/api/task-api.ts index 960ed433..857422fa 100644 --- a/apps/frontend/src/preload/api/task-api.ts +++ b/apps/frontend/src/preload/api/task-api.ts @@ -53,6 +53,9 @@ export interface TaskAPI { checkTaskRunning: (taskId: string) => Promise>; resumePausedTask: (taskId: string) => Promise; + // Worktree Change Detection + checkWorktreeChanges: (taskId: string) => Promise>; + // Image Operations loadImageThumbnail: (projectPath: string, specId: string, imagePath: string) => Promise>; @@ -148,6 +151,10 @@ export const createTaskAPI = (): TaskAPI => ({ resumePausedTask: (taskId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.TASK_RESUME_PAUSED, taskId), + // Worktree Change Detection + checkWorktreeChanges: (taskId: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.TASK_CHECK_WORKTREE_CHANGES, taskId), + // Image Operations loadImageThumbnail: (projectPath: string, specId: string, imagePath: string): Promise> => ipcRenderer.invoke(IPC_CHANNELS.TASK_LOAD_IMAGE_THUMBNAIL, projectPath, specId, imagePath), diff --git a/apps/frontend/src/renderer/components/task-detail/TaskActions.tsx b/apps/frontend/src/renderer/components/task-detail/TaskActions.tsx index 33ce06ed..4d1a3947 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskActions.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskActions.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import { Play, Square, CheckCircle2, RotateCcw, Trash2, Loader2, AlertTriangle } from 'lucide-react'; import { Button } from '../ui/button'; import { @@ -21,6 +22,8 @@ interface TaskActionsProps { showDeleteDialog: boolean; isDeleting: boolean; deleteError: string | null; + worktreeChangesInfo: { hasChanges: boolean; worktreePath?: string; changedFileCount?: number } | null; + isCheckingChanges: boolean; onStartStop: () => void; onRecover: () => void; onDelete: () => void; @@ -36,11 +39,14 @@ export function TaskActions({ showDeleteDialog, isDeleting, deleteError, + worktreeChangesInfo, + isCheckingChanges, onStartStop, onRecover, onDelete, onShowDeleteDialog }: TaskActionsProps) { + const { t } = useTranslation(['tasks']); return ( <>
@@ -117,15 +123,32 @@ export function TaskActions({ - Delete Task + {t('tasks:deleteDialog.title')}

- Are you sure you want to delete "{task.title}"? + {t('tasks:deleteDialog.confirmMessage')} "{task.title}"?

+ {isCheckingChanges && ( +
+ + {t('tasks:deleteDialog.checkingChanges')} +
+ )} + {worktreeChangesInfo?.hasChanges && ( +
+

+ + {t('tasks:deleteDialog.uncommittedChanges', { count: worktreeChangesInfo.changedFileCount })} +

+

+ {t('tasks:deleteDialog.uncommittedChangesHint')} +

+
+ )}

- This action cannot be undone. All task files, including the spec, implementation plan, and any generated code will be permanently deleted from the project. + {t('tasks:deleteDialog.destructiveWarning')}

{deleteError && (

@@ -136,7 +159,7 @@ export function TaskActions({ - Cancel + {t('tasks:deleteDialog.cancel')} { e.preventDefault(); @@ -148,12 +171,12 @@ export function TaskActions({ {isDeleting ? ( <> - Deleting... + {t('tasks:deleteDialog.deleting')} ) : ( <> - Delete Permanently + {t('tasks:deleteDialog.deletePermanently')} )} diff --git a/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx b/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx index 9460998b..b88591c7 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskDetailModal.tsx @@ -617,15 +617,32 @@ function TaskDetailModalContent({ open, task, onOpenChange, onSwitchToTerminals, - Delete Task + {t('tasks:deleteDialog.title')}

- Are you sure you want to delete "{task.title}"? + {t('tasks:deleteDialog.confirmMessage')} "{task.title}"?

+ {state.isCheckingChanges && ( +
+ + {t('tasks:deleteDialog.checkingChanges')} +
+ )} + {state.worktreeChangesInfo?.hasChanges && ( +
+

+ + {t('tasks:deleteDialog.uncommittedChanges', { count: state.worktreeChangesInfo.changedFileCount })} +

+

+ {t('tasks:deleteDialog.uncommittedChangesHint')} +

+
+ )}

- This action cannot be undone. All task files, including the spec, implementation plan, and any generated code will be permanently deleted from the project. + {t('tasks:deleteDialog.destructiveWarning')}

{state.deleteError && (

@@ -636,7 +653,7 @@ function TaskDetailModalContent({ open, task, onOpenChange, onSwitchToTerminals, - Cancel + {t('tasks:deleteDialog.cancel')} { e.preventDefault(); @@ -648,12 +665,12 @@ function TaskDetailModalContent({ open, task, onOpenChange, onSwitchToTerminals, {state.isDeleting ? ( <> - Deleting... + {t('tasks:deleteDialog.deleting')} ) : ( <> - Delete Permanently + {t('tasks:deleteDialog.deletePermanently')} )} 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 ca501c39..85098920 100644 --- a/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts +++ b/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts @@ -61,6 +61,8 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [deleteError, setDeleteError] = useState(null); + const [worktreeChangesInfo, setWorktreeChangesInfo] = useState<{ hasChanges: boolean; worktreePath?: string; changedFileCount?: number } | null>(null); + const [isCheckingChanges, setIsCheckingChanges] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [worktreeStatus, setWorktreeStatus] = useState(null); const [worktreeDiff, setWorktreeDiff] = useState(null); @@ -133,6 +135,21 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { return () => clearInterval(intervalId); }, [task.id, isActiveTask]); + // Check for uncommitted worktree changes when delete dialog opens + useEffect(() => { + if (showDeleteDialog && task) { + setIsCheckingChanges(true); + window.electronAPI.checkWorktreeChanges(task.id).then((result) => { + if (result.success && result.data) { + setWorktreeChangesInfo(result.data); + } + setIsCheckingChanges(false); + }).catch(() => setIsCheckingChanges(false)); + } else { + setWorktreeChangesInfo(null); + } + }, [showDeleteDialog, task]); + // Handle scroll events in logs to detect if user scrolled away from anchor const handleLogsScroll = (e: React.UIEvent) => { const target = e.target as HTMLDivElement; @@ -486,6 +503,8 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { showDeleteDialog, isDeleting, deleteError, + worktreeChangesInfo, + isCheckingChanges, isEditDialogOpen, worktreeStatus, worktreeDiff, @@ -530,6 +549,8 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { setShowDeleteDialog, setIsDeleting, setDeleteError, + setWorktreeChangesInfo, + setIsCheckingChanges, setIsEditDialogOpen, setWorktreeStatus, setWorktreeDiff, diff --git a/apps/frontend/src/renderer/lib/browser-mock.ts b/apps/frontend/src/renderer/lib/browser-mock.ts index 72343df9..abe46ad0 100644 --- a/apps/frontend/src/renderer/lib/browser-mock.ts +++ b/apps/frontend/src/renderer/lib/browser-mock.ts @@ -306,6 +306,12 @@ const browserMockAPI: ElectronAPI = { data: { path: cliPath } }), + // Worktree Change Detection + checkWorktreeChanges: async () => ({ + success: true, + data: { hasChanges: false, changedFileCount: 0 } + }), + // Terminal Worktree Operations createTerminalWorktree: async () => ({ success: false, diff --git a/apps/frontend/src/renderer/lib/mocks/task-mock.ts b/apps/frontend/src/renderer/lib/mocks/task-mock.ts index 13d21d8b..8b1a368f 100644 --- a/apps/frontend/src/renderer/lib/mocks/task-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/task-mock.ts @@ -76,6 +76,12 @@ export const taskMock = { resumePausedTask: async () => ({ success: true }), + // Worktree change detection + checkWorktreeChanges: async (_taskId: string) => ({ + success: true as const, + data: { hasChanges: false } + }), + // Image operations loadImageThumbnail: async (_projectPath: string, _specId: string, _imagePath: string) => ({ success: false, diff --git a/apps/frontend/src/shared/constants/ipc.ts b/apps/frontend/src/shared/constants/ipc.ts index 1488a4f4..2727a3b7 100644 --- a/apps/frontend/src/shared/constants/ipc.ts +++ b/apps/frontend/src/shared/constants/ipc.ts @@ -33,6 +33,7 @@ export const IPC_CHANNELS = { TASK_CHECK_RUNNING: 'task:checkRunning', TASK_RESUME_PAUSED: 'task:resumePaused', // Resume a rate-limited or auth-paused task TASK_LOAD_IMAGE_THUMBNAIL: 'task:loadImageThumbnail', + TASK_CHECK_WORKTREE_CHANGES: 'task:checkWorktreeChanges', // Workspace management (for human review) // Per-spec architecture: Each spec has its own worktree at .worktrees/{spec-name}/ diff --git a/apps/frontend/src/shared/i18n/locales/en/tasks.json b/apps/frontend/src/shared/i18n/locales/en/tasks.json index c34ad63f..3421fa08 100644 --- a/apps/frontend/src/shared/i18n/locales/en/tasks.json +++ b/apps/frontend/src/shared/i18n/locales/en/tasks.json @@ -330,6 +330,17 @@ "hint": "Use an external screenshot tool and paste directly into the task description with {{shortcut}}." } }, + "deleteDialog": { + "title": "Delete Task", + "confirmMessage": "Are you sure you want to delete", + "destructiveWarning": "This action cannot be undone. All task files, including the spec, implementation plan, and any generated code will be permanently deleted from the project.", + "checkingChanges": "Checking for uncommitted changes...", + "uncommittedChanges": "This task's worktree has {{count}} uncommitted file(s)", + "uncommittedChangesHint": "These changes have not been committed or merged. Deleting this task will permanently discard all uncommitted work in the worktree.", + "cancel": "Cancel", + "deletePermanently": "Delete Permanently", + "deleting": "Deleting..." + }, "referenceImages": { "title": "Reference Images (optional)", "description": "Add visual references like screenshots or designs to help the AI understand your requirements." diff --git a/apps/frontend/src/shared/i18n/locales/fr/tasks.json b/apps/frontend/src/shared/i18n/locales/fr/tasks.json index 5212a239..16eb651b 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/tasks.json +++ b/apps/frontend/src/shared/i18n/locales/fr/tasks.json @@ -329,6 +329,17 @@ "hint": "Utilisez un outil de capture d'écran externe et collez directement dans la description de la tâche avec {{shortcut}}." } }, + "deleteDialog": { + "title": "Supprimer la t\u00e2che", + "confirmMessage": "\u00cates-vous s\u00fbr de vouloir supprimer", + "destructiveWarning": "Cette action est irr\u00e9versible. Tous les fichiers de t\u00e2che, y compris la sp\u00e9cification, le plan d'impl\u00e9mentation et tout code g\u00e9n\u00e9r\u00e9 seront d\u00e9finitivement supprim\u00e9s du projet.", + "checkingChanges": "V\u00e9rification des changements non valid\u00e9s...", + "uncommittedChanges": "Le worktree de cette t\u00e2che contient {{count}} fichier(s) non valid\u00e9(s)", + "uncommittedChangesHint": "Ces changements n'ont pas \u00e9t\u00e9 valid\u00e9s ni fusionn\u00e9s. La suppression de cette t\u00e2che supprimera d\u00e9finitivement tout le travail non valid\u00e9 dans le worktree.", + "cancel": "Annuler", + "deletePermanently": "Supprimer d\u00e9finitivement", + "deleting": "Suppression..." + }, "referenceImages": { "title": "Images de référence (facultatif)", "description": "Ajoutez des références visuelles comme des captures d'écran ou des conceptions pour aider l'IA à comprendre vos exigences." diff --git a/apps/frontend/src/shared/types/ipc.ts b/apps/frontend/src/shared/types/ipc.ts index 10cb22a7..b1fc2c4b 100644 --- a/apps/frontend/src/shared/types/ipc.ts +++ b/apps/frontend/src/shared/types/ipc.ts @@ -209,6 +209,9 @@ export interface ElectronAPI { // Image operations loadImageThumbnail: (projectPath: string, specId: string, imagePath: string) => Promise>; + // Worktree change detection + checkWorktreeChanges: (taskId: string) => Promise>; + // Workspace management (for human review) // Per-spec architecture: Each spec has its own worktree at .worktrees/{spec-name}/ getWorktreeStatus: (taskId: string) => Promise>;