Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 17 additions & 3 deletions apps/frontend/src/main/ipc-handlers/agent-events-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ export function registerAgenteventsHandlers(

taskStateManager.handleProcessExited(taskId, code, exitTask, exitProject);

// Fallback safety net: If XState failed to transition the task out of in_progress,
// force it to human_review after a short delay. This prevents tasks from getting stuck
// in in_progress state when the process exits without XState properly handling it.
setTimeout(() => {
const { task: checkTask } = findTaskAndProject(taskId, projectId);
if (checkTask && checkTask.status === 'in_progress') {
console.warn(`[agent-events-handlers] Task ${taskId} still in_progress 500ms after exit, forcing to human_review`);
taskStateManager.forceTransition(taskId, 'human_review', 'errors', checkTask, exitProject);

This comment was marked as outdated.

}
}, 500);
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

This fallback is a good safety net, but there are a couple of issues here:

  1. Critical: The method taskStateManager.forceTransition does not appear to be defined in TaskStateManager based on the provided file context. This will likely cause a runtime error. You may need to implement this method in apps/frontend/src/main/task-state-manager.ts to handle persisting and emitting the new task state.

  2. Medium: The timeout duration 500 is a magic number. It's better to define it as a named constant for readability and maintainability, for example: const STUCK_TASK_FALLBACK_TIMEOUT_MS = 500;.


// Send final plan state to renderer BEFORE unwatching
// This ensures the renderer has the final subtask data (fixes 0/0 subtask bug)
const finalPlan = fileWatcher.getCurrentPlan(taskId);
Expand Down Expand Up @@ -225,9 +236,12 @@ export function registerAgenteventsHandlers(
console.debug(`[agent-events-handlers] Skipping persistPlanPhaseSync for ${taskId}: XState in '${currentXState}', not overwriting with phase '${progress.phase}'`);
}

// Skip sending execution-progress to renderer when XState has settled.
// XState's emitPhaseFromState already sent the correct phase to the renderer.
if (xstateInTerminalState) {
// Skip sending execution-progress to renderer when XState has settled,
// UNLESS this is a final phase update (complete/failed).
// Final phase updates must still propagate to renderer even after XState settles,
// otherwise the UI never receives the final progress state.
const isFinalPhaseUpdate = progress.phase === 'complete' || progress.phase === 'failed';
if (xstateInTerminalState && !isFinalPhaseUpdate) {
console.debug(`[agent-events-handlers] Skipping execution-progress to renderer for ${taskId}: XState in '${currentXState}', ignoring phase '${progress.phase}'`);
return;
}
Expand Down
5 changes: 4 additions & 1 deletion apps/frontend/src/renderer/components/SortableTaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ export const SortableTaskCard = memo(function SortableTaskCard({ task, onClick,
transition,
isDragging,
isOver
} = useSortable({ id: task.id });
} = useSortable({
id: task.id,
disabled: task.status === 'in_progress' // Prevent dragging tasks that are currently running or stuck
});

const style = {
transform: CSS.Transform.toString(transform),
Expand Down
4 changes: 3 additions & 1 deletion apps/frontend/src/renderer/components/TaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@ export const TaskCard = memo(function TaskCard({

const handleStartStop = async (e: React.MouseEvent) => {
e.stopPropagation();
if (isRunning && !isStuck) {
if (isRunning) {
// Allow stopping both running and stuck tasks
// User should be able to force-stop a stuck task
stopTask(task.id);
} else {
const result = await startTaskOrQueue(task.id);
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/renderer/stores/task-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ export const useTaskStore = create<TaskState>((set, get) => ({
// When starting a task and no phase is set yet, default to planning
// This prevents the "no active phase" UI state during startup race condition
executionProgress = { phase: 'planning' as ExecutionPhase, phaseProgress: 0, overallProgress: 0 };
} else if (status === 'human_review' || status === 'error' || status === 'done' || status === 'pr_created') {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For better readability and maintainability, you could use an array with .includes() for this check. It makes it easier to manage the list of terminal statuses in the future.

Suggested change
} else if (status === 'human_review' || status === 'error' || status === 'done' || status === 'pr_created') {
} else if (['human_review', 'error', 'done', 'pr_created'].includes(status)) {

// Reset execution progress when task reaches terminal states
// This prevents stuck tasks from showing stale progress indicators
executionProgress = { phase: 'idle' as ExecutionPhase, phaseProgress: 0, overallProgress: 0 };
}

// Log status transitions to help diagnose flip-flop issues
Expand Down
Loading