Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 84 additions & 4 deletions apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2588,7 +2588,7 @@ export function registerWorktreeHandlers(
*/
ipcMain.handle(
IPC_CHANNELS.TASK_WORKTREE_DISCARD,
async (_, taskId: string): Promise<IPCResult<WorktreeDiscardResult>> => {
async (_, taskId: string, skipStatusChange?: boolean): Promise<IPCResult<WorktreeDiscardResult>> => {
try {
const { task, project } = findTaskAndProject(taskId);
if (!task || !project) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<IPCResult<{ cleared: boolean }>> => {
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
Expand Down
10 changes: 7 additions & 3 deletions apps/frontend/src/preload/api/task-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export interface TaskAPI {
getWorktreeDiff: (taskId: string) => Promise<IPCResult<import('../../shared/types').WorktreeDiff>>;
mergeWorktree: (taskId: string, options?: { noCommit?: boolean }) => Promise<IPCResult<import('../../shared/types').WorktreeMergeResult>>;
mergeWorktreePreview: (taskId: string) => Promise<IPCResult<import('../../shared/types').WorktreeMergeResult>>;
discardWorktree: (taskId: string) => Promise<IPCResult<import('../../shared/types').WorktreeDiscardResult>>;
discardWorktree: (taskId: string, skipStatusChange?: boolean) => Promise<IPCResult<import('../../shared/types').WorktreeDiscardResult>>;
clearStagedState: (taskId: string) => Promise<IPCResult<{ cleared: boolean }>>;
listWorktrees: (projectId: string) => Promise<IPCResult<import('../../shared/types').WorktreeListResult>>;
worktreeOpenInIDE: (worktreePath: string, ide: SupportedIDE, customPath?: string) => Promise<IPCResult<{ opened: boolean }>>;
worktreeOpenInTerminal: (worktreePath: string, terminal: SupportedTerminal, customPath?: string) => Promise<IPCResult<{ opened: boolean }>>;
Expand Down Expand Up @@ -142,8 +143,11 @@ export const createTaskAPI = (): TaskAPI => ({
mergeWorktreePreview: (taskId: string): Promise<IPCResult<import('../../shared/types').WorktreeMergeResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_MERGE_PREVIEW, taskId),

discardWorktree: (taskId: string): Promise<IPCResult<import('../../shared/types').WorktreeDiscardResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_DISCARD, taskId),
discardWorktree: (taskId: string, skipStatusChange?: boolean): Promise<IPCResult<import('../../shared/types').WorktreeDiscardResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_DISCARD, taskId, skipStatusChange),

clearStagedState: (taskId: string): Promise<IPCResult<{ cleared: boolean }>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_CLEAR_STAGED_STATE, taskId),

listWorktrees: (projectId: string): Promise<IPCResult<import('../../shared/types').WorktreeListResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_LIST_WORKTREES, projectId),
Expand Down
157 changes: 140 additions & 17 deletions apps/frontend/src/renderer/components/KanbanBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -336,6 +344,11 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
const [overColumnId, setOverColumnId] = useState<string | null>(null);
const { showArchived, toggleShowArchived } = useViewState();

// Worktree cleanup dialog state
const [worktreeDialogOpen, setWorktreeDialogOpen] = useState(false);
const [pendingDoneTask, setPendingDoneTask] = useState<Task | null>(null);
const [isCleaningUp, setIsCleaningUp] = useState(false);

// Calculate archived count for Done column button
const archivedCount = useMemo(() =>
tasks.filter(t => t.metadata?.archivedAt).length,
Expand Down Expand Up @@ -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<boolean> => {
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);
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignoring status persist failure after worktree deletion

Medium Severity

In handleMoveToDone, the return value of persistTaskStatus is ignored. Since persistTaskStatus returns false on failure (rather than throwing), if the status persist fails after the worktree was successfully deleted, the dialog closes without showing any error. The worktree is gone, local UI state shows the task as done, but the file on disk has the original status. On app restart, the task reappears in its original column but the worktree data is permanently lost.

Fix in Cursor Fix in Web

Comment on lines +466 to +485
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider improving error handling clarity.

The error handling in handleMoveToDone logs the failure but continues to mark the task as done anyway (line 479). While the comment "Continue anyway - user can clean up manually" explains this behavior, it might be better to inform the user of the failure explicitly.

Consider adding a toast notification or updating the UI to show that worktree deletion failed but the task was still marked as done. This provides better user feedback about the partial success.

Example approach:

if (!result.success) {
  console.error('Failed to delete worktree:', result.error);
  // Show toast or set error state to inform user
  toast.warning('Task marked as done, but worktree deletion failed. You may need to clean it up manually.');
}
🤖 Prompt for AI Agents
In @apps/frontend/src/renderer/components/KanbanBoard.tsx around lines 466 -
485, The cleanup path in handleMoveToDone swallows worktree deletion failures
and still marks the task done without user-visible feedback; update the error
branch after calling window.electronAPI.discardWorktree(task.id, true) to
surface the failure to the user (e.g., call a toast warning or set an error
state) while continuing to call persistTaskStatus(task.id, 'done'), and ensure
existing cleanup finally-block behavior (setIsCleaningUp(false),
setWorktreeDialogOpen(false), setPendingDoneTask(null)) remains unchanged so UI
state is consistent.


const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
setActiveTask(null);
setOverColumnId(null);
Expand All @@ -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;
}
Comment on lines +516 to 524
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Potential UX issue: drag appears to complete but task stays in place.

When moving to "done" triggers the worktree dialog, the function returns early at line 513 without updating the task status. The user sees the drag complete visually (card snaps back), then the dialog appears. This could be confusing.

Consider either:

  1. Adding a brief visual indicator that an action is pending, or
  2. Documenting this as expected behavior in the dialog description

This is minor since the dialog explains the situation, but worth noting for UX polish.

🤖 Prompt for AI Agents
In @apps/frontend/src/renderer/components/KanbanBoard.tsx around lines 506 -
514, Dragging a card to "done" can appear to complete then snap back because the
handler returns early when checkTaskHasWorktree(task.id) is true; update the UI
to reflect a pending state before returning by setting a short-lived visual
indicator or optimistic pending status (e.g., call setPendingDoneTask(task) and
set a "pending" flag on that task or global state like
setWorktreeDialogOpen(true) plus setTaskPending(task.id)) so the card shows an
explicit "awaiting confirmation" state while the worktree dialog is open; ensure
you use the existing symbols checkTaskHasWorktree, setPendingDoneTask,
setWorktreeDialogOpen and the task.id/targetStatus logic so the drag UX no
longer looks like an unexpected rollback.

}

// Normal status change
persistTaskStatus(activeTaskId, targetStatus);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rapid drags to done column can lose tasks

Low Severity

Making handleDragEnd async introduces a race condition when dragging tasks to the "done" column. If a user quickly drags two tasks with worktrees to "done", both async checkTaskHasWorktree calls run concurrently. When they complete, each call sets setPendingDoneTask and setWorktreeDialogOpen, causing the second task to overwrite the first in pendingDoneTask. The dialog displays only the last task, and the first task's drag is silently lost - it never gets moved to "done" and the user receives no indication that their action was ignored.

Fix in Cursor Fix in Web

};

return (
Expand Down Expand Up @@ -524,6 +580,73 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
) : null}
</DragOverlay>
</DndContext>

{/* Worktree cleanup confirmation dialog */}
<AlertDialog open={worktreeDialogOpen} onOpenChange={setWorktreeDialogOpen}>
<AlertDialogContent className="max-w-md">
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<FolderCheck className="h-5 w-5 text-primary" />
{t('kanban.worktreeCleanupTitle', 'Worktree Cleanup')}
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="text-left space-y-2">
{pendingDoneTask?.stagedInMainProject ? (
<p>
{t('kanban.worktreeCleanupStaged', 'This task has been staged and has a worktree. Would you like to clean up the worktree?')}
</p>
) : (
<p>
{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.')}
</p>
)}
{pendingDoneTask && (
<p className="text-sm font-medium text-foreground/80 bg-muted/50 rounded px-2 py-1.5">
{pendingDoneTask.title}
</p>
)}
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="flex-col sm:flex-row gap-2">
<Button
variant="outline"
onClick={() => {
setWorktreeDialogOpen(false);
setPendingDoneTask(null);
}}
disabled={isCleaningUp}
>
{t('common:cancel', 'Cancel')}
</Button>
{/* Only show "Keep Worktree" option if task is staged */}
{pendingDoneTask?.stagedInMainProject && (
<Button
variant="secondary"
onClick={() => pendingDoneTask && handleMoveToDone(pendingDoneTask, false)}
disabled={isCleaningUp}
className="gap-2"
>
<FolderCheck className="h-4 w-4" />
{t('kanban.keepWorktree', 'Keep Worktree')}
</Button>
)}
<Button
variant={pendingDoneTask?.stagedInMainProject ? 'default' : 'destructive'}
onClick={() => pendingDoneTask && handleMoveToDone(pendingDoneTask, true)}
disabled={isCleaningUp}
className="gap-2"
>
{isCleaningUp ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
{t('kanban.deleteWorktree', 'Delete Worktree & Mark Done')}
Comment on lines +590 to +645
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the translation files in the repository
find . -type f -name "*.json" | grep -E "(locale|i18n|translation|lang)" | head -20

Repository: AndyMik90/Auto-Claude

Length of output: 1198


🏁 Script executed:

# Let's search for kanban keys in the translation files
echo "=== Searching for kanban keys in translation files ==="
rg "kanban|worktreeCleanup|keepWorktree|deleteWorktree" apps/frontend/src/shared/i18n/locales/ --type json

echo ""
echo "=== Checking if kanban.json file exists ==="
ls -la apps/frontend/src/shared/i18n/locales/en/ | grep -i kanban
ls -la apps/frontend/src/shared/i18n/locales/fr/ | grep -i kanban

echo ""
echo "=== All JSON files in en directory ==="
ls apps/frontend/src/shared/i18n/locales/en/

echo ""
echo "=== Checking common.json for cancel key ==="
rg "cancel" apps/frontend/src/shared/i18n/locales/en/common.json -C2

Repository: AndyMik90/Auto-Claude

Length of output: 1029


🏁 Script executed:

# Let's examine the kanban object in tasks.json to see what keys are defined
echo "=== Content of kanban object in en/tasks.json ==="
cat apps/frontend/src/shared/i18n/locales/en/tasks.json | jq '.kanban' 2>/dev/null || cat apps/frontend/src/shared/i18n/locales/en/tasks.json | grep -A 50 '"kanban"'

echo ""
echo "=== Content of kanban object in fr/tasks.json ==="
cat apps/frontend/src/shared/i18n/locales/fr/tasks.json | jq '.kanban' 2>/dev/null || cat apps/frontend/src/shared/i18n/locales/fr/tasks.json | grep -A 50 '"kanban"'

Repository: AndyMik90/Auto-Claude

Length of output: 1797


🏁 Script executed:

# Let's verify that these keys are definitely missing and count the files that need updates
echo "=== Verifying missing keys in en/tasks.json ==="
grep -c "worktreeCleanupTitle\|worktreeCleanupStaged\|worktreeCleanupNotStaged\|keepWorktree\|deleteWorktree" apps/frontend/src/shared/i18n/locales/en/tasks.json || echo "No matches found (confirmed missing)"

echo ""
echo "=== Verifying missing keys in fr/tasks.json ==="
grep -c "worktreeCleanupTitle\|worktreeCleanupStaged\|worktreeCleanupNotStaged\|keepWorktree\|deleteWorktree" apps/frontend/src/shared/i18n/locales/fr/tasks.json || echo "No matches found (confirmed missing)"

echo ""
echo "=== Confirming common:cancel key exists ==="
grep -c "\"cancel\"" apps/frontend/src/shared/i18n/locales/en/common.json

Repository: AndyMik90/Auto-Claude

Length of output: 286


Add missing kanban i18n translation keys to both en and fr language files.

The code uses translation keys (kanban.worktreeCleanupTitle, kanban.worktreeCleanupStaged, kanban.worktreeCleanupNotStaged, kanban.keepWorktree, kanban.deleteWorktree) that are not defined in the translation files. These keys must be added to both apps/frontend/src/shared/i18n/locales/en/tasks.json and apps/frontend/src/shared/i18n/locales/fr/tasks.json to ensure proper localization instead of relying on fallback strings.

🤖 Prompt for AI Agents
In @apps/frontend/src/renderer/components/KanbanBoard.tsx around lines 580 -
635, The noted translation keys used in KanbanBoard
(kanban.worktreeCleanupTitle, kanban.worktreeCleanupStaged,
kanban.worktreeCleanupNotStaged, kanban.keepWorktree, kanban.deleteWorktree) are
missing from the tasks locale files; add these keys to both the English and
French tasks locale JSONs with the appropriate localized strings (English values
should match the current fallback strings like "Worktree Cleanup", "This task
has been staged and has a worktree. Would you like to clean up the worktree?",
"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.", "Keep
Worktree", "Delete Worktree & Mark Done"; provide equivalent French translations
for each key) so the UI uses proper i18n instead of fallback text.

</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Comment on lines +583 to +649
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the locale file structure
find apps/frontend/src/shared/i18n/locales -type f -name "*.json" | head -20

Repository: AndyMik90/Auto-Claude

Length of output: 1158


🏁 Script executed:

# Search for worktreeCleanup keys in the codebase
rg "worktreeCleanup|keepWorktree|deleteWorktree" apps/frontend/src/shared/i18n/locales/

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

# Check the structure of en locale files
ls -la apps/frontend/src/shared/i18n/locales/en/

Repository: AndyMik90/Auto-Claude

Length of output: 815


🏁 Script executed:

# Search for any kanban keys in existing locale files
rg "kanban" apps/frontend/src/shared/i18n/locales/en/

Repository: AndyMik90/Auto-Claude

Length of output: 217


🏁 Script executed:

# Check tasks.json and dialogs.json for structure
cat apps/frontend/src/shared/i18n/locales/en/tasks.json | head -40

Repository: AndyMik90/Auto-Claude

Length of output: 1012


🏁 Script executed:

# Get the full kanban section from tasks.json
rg -A 50 '"kanban"' apps/frontend/src/shared/i18n/locales/en/tasks.json

Repository: AndyMik90/Auto-Claude

Length of output: 1672


🏁 Script executed:

# Check fr locale for kanban keys
rg -A 50 '"kanban"' apps/frontend/src/shared/i18n/locales/fr/tasks.json

Repository: AndyMik90/Auto-Claude

Length of output: 1875


Add translation keys for the worktree cleanup dialog to locale files.

The code introduces five new user-facing strings without adding translation keys to the locale files:

  • kanban.worktreeCleanupTitle
  • kanban.worktreeCleanupStaged
  • kanban.worktreeCleanupNotStaged
  • kanban.keepWorktree
  • kanban.deleteWorktree

These keys must be added to both apps/frontend/src/shared/i18n/locales/en/tasks.json and apps/frontend/src/shared/i18n/locales/fr/tasks.json within the kanban section. Using fallback strings is not sufficient—per project requirements, new user-facing text must have proper translation entries in all required language files.

🤖 Prompt for AI Agents
In @apps/frontend/src/renderer/components/KanbanBoard.tsx around lines 583 -
649, The new Kanban worktree dialog strings (kanban.worktreeCleanupTitle,
kanban.worktreeCleanupStaged, kanban.worktreeCleanupNotStaged,
kanban.keepWorktree, kanban.deleteWorktree) are missing from the locale
files—add these keys under the "kanban" section in the English and French
tasks.json locale files with appropriate translations (use the current fallback
strings as the values if needed), so the AlertDialog text in KanbanBoard.tsx
displays localized content.

</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
26 changes: 17 additions & 9 deletions apps/frontend/src/renderer/components/task-detail/TaskReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface TaskReviewProps {
onClose?: () => void;
onSwitchToTerminals?: () => void;
onOpenInbuiltTerminal?: (id: string, cwd: string) => void;
onReviewAgain?: () => void;
// PR creation
showPRDialog: boolean;
isCreatingPR: boolean;
Expand Down Expand Up @@ -90,6 +91,7 @@ export function TaskReview({
onClose,
onSwitchToTerminals,
onOpenInbuiltTerminal,
onReviewAgain,
showPRDialog,
isCreatingPR,
onShowPRDialog,
Expand All @@ -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 ? (
<LoadingMessage />
) : 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 */
<StagedInProjectMessage
task={task}
projectPath={stagedProjectPath}
hasWorktree={worktreeStatus?.exists || false}
onClose={onClose}
onReviewAgain={onReviewAgain}
/>
) : worktreeStatus?.exists ? (
/* Worktree exists but not yet staged - show staging UI */
<WorkspaceStatus
worktreeStatus={worktreeStatus}
workspaceError={workspaceError}
Expand All @@ -132,13 +147,6 @@ export function TaskReview({
onSwitchToTerminals={onSwitchToTerminals}
onOpenInbuiltTerminal={onOpenInbuiltTerminal}
/>
) : task.stagedInMainProject && !stagedSuccess ? (
<StagedInProjectMessage
task={task}
projectPath={stagedProjectPath}
hasWorktree={worktreeStatus?.exists || false}
onClose={onClose}
/>
) : (
<NoWorkspaceMessage task={task} onClose={onClose} />
)}
Expand Down
Loading
Loading