fix(worktree): remove auto-commit on deletion and add uncommitted changes warning

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 <noreply@anthropic.com>
This commit is contained in:
AndyMik90
2026-02-09 11:22:23 +01:00
parent 48d5f7a321
commit cec8e65ee8
14 changed files with 185 additions and 96 deletions
@@ -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<IPCResult<{ hasChanges: boolean; worktreePath?: string; changedFileCount?: number }>> => {
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 } };
}
}
);
}
@@ -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) {
@@ -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) {
@@ -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
};
}
@@ -53,6 +53,9 @@ export interface TaskAPI {
checkTaskRunning: (taskId: string) => Promise<IPCResult<boolean>>;
resumePausedTask: (taskId: string) => Promise<IPCResult>;
// Worktree Change Detection
checkWorktreeChanges: (taskId: string) => Promise<IPCResult<{ hasChanges: boolean; worktreePath?: string; changedFileCount?: number }>>;
// Image Operations
loadImageThumbnail: (projectPath: string, specId: string, imagePath: string) => Promise<IPCResult<string>>;
@@ -148,6 +151,10 @@ export const createTaskAPI = (): TaskAPI => ({
resumePausedTask: (taskId: string): Promise<IPCResult> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_RESUME_PAUSED, taskId),
// Worktree Change Detection
checkWorktreeChanges: (taskId: string): Promise<IPCResult<{ hasChanges: boolean; worktreePath?: string; changedFileCount?: number }>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_CHECK_WORKTREE_CHANGES, taskId),
// Image Operations
loadImageThumbnail: (projectPath: string, specId: string, imagePath: string): Promise<IPCResult<string>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_LOAD_IMAGE_THUMBNAIL, projectPath, specId, imagePath),
@@ -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 (
<>
<div className="p-4">
@@ -117,15 +123,32 @@ export function TaskActions({
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-destructive" />
Delete Task
{t('tasks:deleteDialog.title')}
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="text-sm text-muted-foreground space-y-3">
<p>
Are you sure you want to delete <strong className="text-foreground">"{task.title}"</strong>?
{t('tasks:deleteDialog.confirmMessage')} <strong className="text-foreground">"{task.title}"</strong>?
</p>
{isCheckingChanges && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
{t('tasks:deleteDialog.checkingChanges')}
</div>
)}
{worktreeChangesInfo?.hasChanges && (
<div className="bg-amber-500/10 border border-amber-500/30 px-3 py-2 rounded-lg text-sm space-y-1">
<p className="font-medium text-amber-600 dark:text-amber-400 flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4" />
{t('tasks:deleteDialog.uncommittedChanges', { count: worktreeChangesInfo.changedFileCount })}
</p>
<p className="text-muted-foreground text-xs">
{t('tasks:deleteDialog.uncommittedChangesHint')}
</p>
</div>
)}
<p className="text-destructive">
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')}
</p>
{deleteError && (
<p className="text-destructive bg-destructive/10 px-3 py-2 rounded-lg text-sm">
@@ -136,7 +159,7 @@ export function TaskActions({
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={isDeleting}>{t('tasks:deleteDialog.cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
@@ -148,12 +171,12 @@ export function TaskActions({
{isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting...
{t('tasks:deleteDialog.deleting')}
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete Permanently
{t('tasks:deleteDialog.deletePermanently')}
</>
)}
</AlertDialogAction>
@@ -617,15 +617,32 @@ function TaskDetailModalContent({ open, task, onOpenChange, onSwitchToTerminals,
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-destructive" />
Delete Task
{t('tasks:deleteDialog.title')}
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="text-sm text-muted-foreground space-y-3">
<p>
Are you sure you want to delete <strong className="text-foreground">"{task.title}"</strong>?
{t('tasks:deleteDialog.confirmMessage')} <strong className="text-foreground">"{task.title}"</strong>?
</p>
{state.isCheckingChanges && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
{t('tasks:deleteDialog.checkingChanges')}
</div>
)}
{state.worktreeChangesInfo?.hasChanges && (
<div className="bg-amber-500/10 border border-amber-500/30 px-3 py-2 rounded-lg text-sm space-y-1">
<p className="font-medium text-amber-600 dark:text-amber-400 flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4" />
{t('tasks:deleteDialog.uncommittedChanges', { count: state.worktreeChangesInfo.changedFileCount })}
</p>
<p className="text-muted-foreground text-xs">
{t('tasks:deleteDialog.uncommittedChangesHint')}
</p>
</div>
)}
<p className="text-destructive">
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')}
</p>
{state.deleteError && (
<p className="text-destructive bg-destructive/10 px-3 py-2 rounded-lg text-sm">
@@ -636,7 +653,7 @@ function TaskDetailModalContent({ open, task, onOpenChange, onSwitchToTerminals,
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={state.isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={state.isDeleting}>{t('tasks:deleteDialog.cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
@@ -648,12 +665,12 @@ function TaskDetailModalContent({ open, task, onOpenChange, onSwitchToTerminals,
{state.isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting...
{t('tasks:deleteDialog.deleting')}
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete Permanently
{t('tasks:deleteDialog.deletePermanently')}
</>
)}
</AlertDialogAction>
@@ -61,6 +61,8 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) {
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [deleteError, setDeleteError] = useState<string | null>(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<WorktreeStatus | null>(null);
const [worktreeDiff, setWorktreeDiff] = useState<WorktreeDiff | null>(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<HTMLDivElement>) => {
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,
@@ -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,
@@ -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,
@@ -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}/
@@ -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."
@@ -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."
+3
View File
@@ -209,6 +209,9 @@ export interface ElectronAPI {
// Image operations
loadImageThumbnail: (projectPath: string, specId: string, imagePath: string) => Promise<IPCResult<string>>;
// Worktree change detection
checkWorktreeChanges: (taskId: string) => Promise<IPCResult<{ hasChanges: boolean; worktreePath?: string; changedFileCount?: number }>>;
// Workspace management (for human review)
// Per-spec architecture: Each spec has its own worktree at .worktrees/{spec-name}/
getWorktreeStatus: (taskId: string) => Promise<IPCResult<WorktreeStatus>>;