Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
59 changes: 38 additions & 21 deletions apps/frontend/src/renderer/components/KanbanBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import { QueueSettingsModal } from './QueueSettingsModal';
import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants';
import { cn, shallowEqual } from '../lib/utils';
import { persistTaskStatus, forceCompleteTask, archiveTasks, useTaskStore } from '../stores/task-store';
import { persistTaskStatus, forceCompleteTask, archiveTasks, useTaskStore, createOptimisticTaskAction } from '../stores/task-store';
import { updateProjectSettings, useProjectStore } from '../stores/project-store';
import { useKanbanSettingsStore, COLLAPSED_COLUMN_WIDTH, DEFAULT_COLUMN_WIDTH, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH } from '../stores/kanban-settings-store';
import { useToast } from '../hooks/use-toast';
Expand Down Expand Up @@ -990,27 +990,44 @@
*/
const handleStatusChange = async (taskId: string, newStatus: TaskStatus, providedTask?: Task) => {
const task = providedTask || tasks.find(t => t.id === taskId);
const result = await persistTaskStatus(taskId, newStatus);
if (!task) return;

if (!result.success) {
if (result.worktreeExists) {
// Show the worktree cleanup dialog
setWorktreeCleanupDialog({
open: true,
taskId: taskId,
taskTitle: task?.title || t('tasks:untitled'),
worktreePath: result.worktreePath,
isProcessing: false,
error: undefined
});
} else {
// Show error toast for other failures
toast({
title: t('common:errors.operationFailed'),
description: result.error || t('common:errors.unknownError'),
variant: 'destructive'
});
}
const previousStatus = task.status;

Check warning on line 995 in apps/frontend/src/renderer/components/KanbanBoard.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless assignment to variable "previousStatus".

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ0Kaw8RKYsfbtn9Wj0K&open=AZ0Kaw8RKYsfbtn9Wj0K&pullRequest=142

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable previousStatus.

Copilot Autofix

AI 3 months ago

In general, unused variables should be removed to keep the code clear and avoid misleading future readers into thinking the value is important. Here, previousStatus is assigned from task.status but never used, so the best fix is simply to delete that declaration.

Concretely, in apps/frontend/src/renderer/components/KanbanBoard.tsx, within the handleStatusChange function around line 995, remove the line const previousStatus = task.status;. No additional imports, methods, or definitions are required, and no other code within the snippet needs to be adjusted because previousStatus is not referenced anywhere.

Suggested changeset 1
apps/frontend/src/renderer/components/KanbanBoard.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/frontend/src/renderer/components/KanbanBoard.tsx b/apps/frontend/src/renderer/components/KanbanBoard.tsx
--- a/apps/frontend/src/renderer/components/KanbanBoard.tsx
+++ b/apps/frontend/src/renderer/components/KanbanBoard.tsx
@@ -992,8 +992,6 @@
     const task = providedTask || tasks.find(t => t.id === taskId);
     if (!task) return;
 
-    const previousStatus = task.status;
-
     try {
       // Optimistic update: immediately update UI
       await createOptimisticTaskAction(
EOF
@@ -992,8 +992,6 @@
const task = providedTask || tasks.find(t => t.id === taskId);
if (!task) return;

const previousStatus = task.status;

try {
// Optimistic update: immediately update UI
await createOptimisticTaskAction(
Copilot is powered by AI and may make mistakes. Always verify output.

try {
// Optimistic update: immediately update UI
await createOptimisticTaskAction(
taskId,
(t) => ({ status: newStatus }),
async () => {
// Call backend API
const result = await persistTaskStatus(taskId, newStatus);
if (!result.success) {
if (result.worktreeExists) {
// Show worktree cleanup dialog
setWorktreeCleanupDialog({
open: true,
taskId: taskId,
taskTitle: task?.title || t('tasks:untitled'),
worktreePath: result.worktreePath,
isProcessing: false,
error: undefined
});
} else {
// Throw to trigger rollback
throw new Error(result.error || 'Failed to update task status');
}
}
}
);
} catch (error) {
// Rollback handled by createOptimisticTaskAction
// Show error toast
toast({
title: t('common:errors.operationFailed'),
description: error instanceof Error ? error.message : t('common:errors.unknownError'),
variant: 'destructive'
});
}
};

Expand Down
62 changes: 52 additions & 10 deletions apps/frontend/src/renderer/components/TaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
JSON_ERROR_PREFIX,
JSON_ERROR_TITLE_SUFFIX
} from '../../shared/constants';
import { startTask, stopTask, checkTaskRunning, recoverStuckTask, isIncompleteHumanReview, archiveTasks } from '../stores/task-store';
import { startTask, stopTask, checkTaskRunning, recoverStuckTask, isIncompleteHumanReview, archiveTasks, archiveTaskOptimistic } from '../stores/task-store';

Check warning on line 35 in apps/frontend/src/renderer/components/TaskCard.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import of 'archiveTasks'.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ0Kaw_OKYsfbtn9Wj0L&open=AZ0Kaw_OKYsfbtn9Wj0L&pullRequest=142

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import archiveTasks.

Copilot Autofix

AI 3 months ago

In general, the correct fix for an unused import is to remove it from the import list so that only actually used bindings remain. This improves readability and avoids confusion about which APIs are in use.

For this specific case, we should modify the import statement on line 35 in apps/frontend/src/renderer/components/TaskCard.tsx to drop archiveTasks while leaving all other imported symbols intact. No additional methods, definitions, or imports are needed; we are only cleaning up the import list. Functionality will remain unchanged because archiveTasks was not referenced anywhere in this file.

Suggested changeset 1
apps/frontend/src/renderer/components/TaskCard.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/frontend/src/renderer/components/TaskCard.tsx b/apps/frontend/src/renderer/components/TaskCard.tsx
--- a/apps/frontend/src/renderer/components/TaskCard.tsx
+++ b/apps/frontend/src/renderer/components/TaskCard.tsx
@@ -32,7 +32,7 @@
   JSON_ERROR_PREFIX,
   JSON_ERROR_TITLE_SUFFIX
 } from '../../shared/constants';
-import { startTask, stopTask, checkTaskRunning, recoverStuckTask, isIncompleteHumanReview, archiveTasks, archiveTaskOptimistic } from '../stores/task-store';
+import { startTask, stopTask, checkTaskRunning, recoverStuckTask, isIncompleteHumanReview, archiveTaskOptimistic } from '../stores/task-store';
 import { useToast } from '../hooks/use-toast';
 import type { Task, TaskCategory, ReviewReason, TaskStatus } from '../../shared/types';
 
EOF
@@ -32,7 +32,7 @@
JSON_ERROR_PREFIX,
JSON_ERROR_TITLE_SUFFIX
} from '../../shared/constants';
import { startTask, stopTask, checkTaskRunning, recoverStuckTask, isIncompleteHumanReview, archiveTasks, archiveTaskOptimistic } from '../stores/task-store';
import { startTask, stopTask, checkTaskRunning, recoverStuckTask, isIncompleteHumanReview, archiveTaskOptimistic } from '../stores/task-store';
import { useToast } from '../hooks/use-toast';
import type { Task, TaskCategory, ReviewReason, TaskStatus } from '../../shared/types';

Copilot is powered by AI and may make mistakes. Always verify output.
import { useToast } from '../hooks/use-toast';
import type { Task, TaskCategory, ReviewReason, TaskStatus } from '../../shared/types';

// Module-level visibility change singleton — one listener for all TaskCard instances
Expand Down Expand Up @@ -169,6 +170,7 @@
onToggleSelect
}: TaskCardProps) {
const { t } = useTranslation(['tasks', 'errors']);
const { toast } = useToast();
const [isStuck, setIsStuck] = useState(false);
const [isRecovering, setIsRecovering] = useState(false);
const stuckCheckRef = useRef<{ timeout: NodeJS.Timeout | null; interval: NodeJS.Timeout | null }>({
Expand Down Expand Up @@ -308,14 +310,44 @@
};
}, [isRunning, performStuckCheck]);

const handleStartStop = (e: React.MouseEvent) => {
const handleStartStop = useCallback(async (e: React.MouseEvent) => {
e.stopPropagation();
if (isRunning && !isStuck) {
stopTask(task.id);
} else {
startTask(task.id);

// Capture previous status for revert
const previousStatus = task.status;

// Determine new status optimistically
const newStatus: TaskStatus = (isRunning && !isStuck) ? 'backlog' : 'in_progress';

// Apply optimistic update by calling onStatusChange if available
// This provides immediate UI feedback
if (onStatusChange) {
onStatusChange(newStatus);
}
};

try {
// Actually start/stop the task on the backend
if (isRunning && !isStuck) {
await stopTask(task.id);

Check failure on line 331 in apps/frontend/src/renderer/components/TaskCard.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected `await` of a non-Promise (non-"Thenable") value.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ0Kaw_OKYsfbtn9Wj0M&open=AZ0Kaw_OKYsfbtn9Wj0M&pullRequest=142
} else {
await startTask(task.id);

Check failure on line 333 in apps/frontend/src/renderer/components/TaskCard.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected `await` of a non-Promise (non-"Thenable") value.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ0Kaw_OKYsfbtn9Wj0N&open=AZ0Kaw_OKYsfbtn9Wj0N&pullRequest=142
}
// If successful, the optimistic update is now the actual state
// The task store will emit updates that sync everything
} catch (error) {
// Revert optimistic update on failure
console.error('[TaskCard] Failed to start/stop task, reverting:', error);
if (onStatusChange && previousStatus !== newStatus) {
onStatusChange(previousStatus);
}
// Show error toast
toast({
title: 'Action Failed',
description: `Could not ${isRunning ? 'stop' : 'start'} task. Please try again.`,
variant: 'destructive'
});
}
}, [task.id, task.status, isRunning, isStuck, onStatusChange]);

const handleRecover = async (e: React.MouseEvent) => {
e.stopPropagation();
Expand All @@ -330,9 +362,19 @@

const handleArchive = async (e: React.MouseEvent) => {
e.stopPropagation();
const result = await archiveTasks(task.projectId, [task.id]);
if (!result.success) {
console.error('[TaskCard] Failed to archive task:', task.id, result.error);
// Optimistic archive - immediately updates UI with archivedAt timestamp
// The archiveTaskOptimistic function handles rollback on error
try {
await archiveTaskOptimistic(task.projectId, task.id);
} catch (error) {
// Error is already logged by archiveTaskOptimistic
console.error('[TaskCard] Archive failed, UI reverted:', error);
// Show error toast
toast({
title: 'Archive Failed',
description: 'Could not archive task. Please try again.',
variant: 'destructive'
});
}
};

Expand Down
209 changes: 209 additions & 0 deletions apps/frontend/src/renderer/stores/task-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,120 @@
}
}

/**
* Creates an optimistic update action for instant UI feedback with automatic revert on error
*
* This helper follows the pattern from UsageIndicator.tsx:
* 1. Capture previous state for potential revert
* 2. Apply optimistic update immediately for instant feedback
* 3. Execute async operation
* 4. Revert to previous state if operation fails
*
* @param getState - Function to get current state
* @param setState - Function to update state
* @param optimisticUpdate - Function to calculate optimistic state from current state
* @param asyncOperation - The async operation to perform (e.g., API call)
* @param onError - Optional custom error handler (default: console.error)
* @returns Promise with the result of the async operation
*
* @example
* ```typescript
* await createOptimisticAction(
* () => useTaskStore.getState(),
* (state) => useTaskStore.setState(state),
* (state) => ({
* ...state,
* tasks: state.tasks.map(t => t.id === taskId ? { ...t, status: 'done' } : t)
* }),
* async () => {
* await window.electronAPI.updateTaskStatus(taskId, 'done');
* }
* );
* ```
*/
export async function createOptimisticAction<T>(
getState: () => T,
setState: (state: T | Partial<T>) => void,
optimisticUpdate: (currentState: T) => T | Partial<T>,
asyncOperation: () => Promise<void>,
onError?: (error: Error) => void
): Promise<void> {
// Capture previous state for revert (before any changes)
const previousState = getState();

try {
// Calculate and apply optimistic update immediately
const optimisticState = optimisticUpdate(previousState);
setState(optimisticState);

// Execute the actual async operation
await asyncOperation();
} catch (error) {
// Revert to captured previous state on error
setState(previousState);

// Call custom error handler or default to console.error
if (onError) {
onError(error as Error);
} else {
console.error('[createOptimisticAction] Operation failed, reverted state:', error);
}

// Re-throw for caller to handle if needed
throw error;
}
}

/**
* Creates an optimistic action specifically for task updates
* Convenience wrapper around createOptimisticAction for task-store operations
*
* @param taskId - The task ID to update
* @param optimisticUpdate - Function to calculate optimistic task state
* @param asyncOperation - The async operation to perform
* @param onError - Optional custom error handler
* @returns Promise that resolves when operation completes
*
* @example
* ```typescript
* await createOptimisticTaskAction(
* taskId,
* (task) => ({ ...task, status: 'done' }),
* async () => {
* await window.electronAPI.completeTask(taskId);
* }
* );
* ```
*/
export async function createOptimisticTaskAction(
taskId: string,
optimisticUpdate: (task: Task) => Partial<Task>,
asyncOperation: () => Promise<void>,
onError?: (error: Error) => void
): Promise<void> {
await createOptimisticAction(
() => useTaskStore.getState(),
(state) => useTaskStore.setState(state),
(state) => {
const taskIndex = findTaskIndex(state.tasks, taskId);
if (taskIndex === -1) {
debugLog('[createOptimisticTaskAction] Task not found:', taskId);
return state;
}

const updatedTasks = updateTaskAtIndex(
state.tasks,
taskIndex,
(task) => ({ ...task, ...optimisticUpdate(task) })
);

return { ...state, tasks: updatedTasks };
},
asyncOperation,
onError
);
}

export const useTaskStore = create<TaskState>((set, get) => ({
tasks: [],
selectedTaskId: null,
Expand Down Expand Up @@ -829,6 +943,64 @@
window.electronAPI.stopTask(taskId);
}

/**
* Start a task with optimistic UI update
* Immediately updates the task status to 'in_progress' before server confirmation,
* with automatic rollback on error.
*
* @param taskId - The task ID to start
* @param options - Optional configuration (parallel execution, worker count)
* @throws Error if task not found or start operation fails
*/
export async function startTaskOptimistic(
taskId: string,
options?: { parallel?: boolean; workers?: number }
): Promise<void> {
await createOptimisticTaskAction(
taskId,
(task) => {
// Optimistic update: change status to 'in_progress'
return {
status: 'in_progress' as const,
};
},
async () => {
// Call the backend API to start the task
await window.electronAPI.startTask(taskId, options);

Check failure on line 969 in apps/frontend/src/renderer/stores/task-store.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected `await` of a non-Promise (non-"Thenable") value.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ0Kaw_jKYsfbtn9Wj0O&open=AZ0Kaw_jKYsfbtn9Wj0O&pullRequest=142

Check warning on line 969 in apps/frontend/src/renderer/stores/task-store.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ0Kaw_jKYsfbtn9Wj0P&open=AZ0Kaw_jKYsfbtn9Wj0P&pullRequest=142
},
(error) => {
console.error('[startTaskOptimistic] Failed to start task:', error);
}
);
}

/**
* Stop a task with optimistic UI update
* Immediately updates the task status back to 'backlog'
* before server confirmation, with automatic rollback on error.
*
* @param taskId - The task ID to stop
* @throws Error if task not found or stop operation fails
*/
export async function stopTaskOptimistic(taskId: string): Promise<void> {
await createOptimisticTaskAction(
taskId,
(task) => {
// Optimistic update: revert to 'backlog' (most common pre-start status)
return {
status: 'backlog' as const,
};
},
async () => {
// Call the backend API to stop the task
await window.electronAPI.stopTask(taskId);

Check failure on line 996 in apps/frontend/src/renderer/stores/task-store.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected `await` of a non-Promise (non-"Thenable") value.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ0Kaw_jKYsfbtn9Wj0Q&open=AZ0Kaw_jKYsfbtn9Wj0Q&pullRequest=142

Check warning on line 996 in apps/frontend/src/renderer/stores/task-store.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ0Kaw_jKYsfbtn9Wj0R&open=AZ0Kaw_jKYsfbtn9Wj0R&pullRequest=142
},
(error) => {
console.error('[stopTaskOptimistic] Failed to stop task:', error);
}
);
}

/**
* Submit review for a task
*/
Expand Down Expand Up @@ -1061,6 +1233,43 @@
// Task Creation Draft Management
// ============================================

/**
* Archive a single task with optimistic UI update
* Immediately adds archivedAt timestamp before server confirmation,
* with automatic rollback on error.
*
* @param projectId - The project ID
* @param taskId - The task ID to archive
* @throws Error if task not found or archive operation fails
*/
export async function archiveTaskOptimistic(
projectId: string,
taskId: string
): Promise<void> {
await createOptimisticTaskAction(
taskId,
(task) => {
// Optimistic update: add archivedAt timestamp
return {
metadata: {
...task.metadata,
archivedAt: new Date().toISOString(),
},
};
},
async () => {
// Call the backend API to archive the task
const result = await window.electronAPI.archiveTasks(projectId, [taskId]);

Check warning on line 1262 in apps/frontend/src/renderer/stores/task-store.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ0Kaw_jKYsfbtn9Wj0S&open=AZ0Kaw_jKYsfbtn9Wj0S&pullRequest=142
if (!result.success) {
throw new Error(result.error || 'Failed to archive task');
}
},
(error) => {
console.error('[archiveTaskOptimistic] Failed to archive task:', error);
}
);
}

const DRAFT_KEY_PREFIX = 'task-creation-draft';

/**
Expand Down
Loading
Loading