diff --git a/apps/frontend/src/renderer/components/KanbanBoard.tsx b/apps/frontend/src/renderer/components/KanbanBoard.tsx
index 25e37d56a7..711871e8d9 100644
--- a/apps/frontend/src/renderer/components/KanbanBoard.tsx
+++ b/apps/frontend/src/renderer/components/KanbanBoard.tsx
@@ -29,7 +29,7 @@ import { SortableTaskCard } from './SortableTaskCard';
import { QueueSettingsModal } from './QueueSettingsModal';
import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants';
import { cn } from '../lib/utils';
-import { persistTaskStatus, forceCompleteTask, archiveTasks, deleteTasks, useTaskStore, isQueueAtCapacity, DEFAULT_MAX_PARALLEL_TASKS } from '../stores/task-store';
+import { persistTaskStatus, forceCompleteTask, archiveTasks, unarchiveTasks, deleteTasks, useTaskStore, isQueueAtCapacity, DEFAULT_MAX_PARALLEL_TASKS } from '../stores/task-store';
import { updateProjectSettings, useProjectStore } from '../stores/project-store';
import { useKanbanSettingsStore, DEFAULT_COLUMN_WIDTH, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH, COLLAPSED_COLUMN_WIDTH_REM, MIN_COLUMN_WIDTH_REM, MAX_COLUMN_WIDTH_REM, BASE_FONT_SIZE, pxToRem } from '../stores/kanban-settings-store';
import { useToast } from '../hooks/use-toast';
@@ -986,6 +986,19 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
});
}
}
+
+ // Auto-unarchive: moving an archived task to another state implies the user wants it active
+ if (result.success && task?.metadata?.archivedAt && projectId) {
+ const unarchiveResult = await unarchiveTasks(projectId, [taskId]);
+ if (!unarchiveResult.success) {
+ toast({
+ title: t('common:errors.operationFailed'),
+ description: unarchiveResult.error,
+ variant: 'destructive',
+ });
+ }
+ }
+
// Note: queue auto-promotion when a task leaves in_progress is handled by the
// useEffect task status change listener (registerTaskStatusChangeListener), so
// no explicit processQueue() call is needed here.
diff --git a/apps/frontend/src/renderer/components/TaskCard.tsx b/apps/frontend/src/renderer/components/TaskCard.tsx
index 837b7df58f..6e6e492a9f 100644
--- a/apps/frontend/src/renderer/components/TaskCard.tsx
+++ b/apps/frontend/src/renderer/components/TaskCard.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw, Archive, GitPullRequest, MoreVertical } from 'lucide-react';
+import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw, Archive, ArchiveRestore, GitPullRequest, MoreVertical } from 'lucide-react';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
@@ -31,7 +31,7 @@ import {
JSON_ERROR_PREFIX,
JSON_ERROR_TITLE_SUFFIX
} from '../../shared/constants';
-import { stopTask, checkTaskRunning, recoverStuckTask, isIncompleteHumanReview, archiveTasks, hasRecentActivity, startTaskOrQueue } from '../stores/task-store';
+import { stopTask, checkTaskRunning, recoverStuckTask, isIncompleteHumanReview, archiveTasks, unarchiveTasks, hasRecentActivity, startTaskOrQueue } from '../stores/task-store';
import { useToast } from '../hooks/use-toast';
import type { Task, TaskCategory, ReviewReason, TaskStatus } from '../../shared/types';
@@ -265,6 +265,18 @@ export const TaskCard = memo(function TaskCard({
}
};
+ const handleUnarchive = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const result = await unarchiveTasks(task.projectId, [task.id]);
+ if (!result.success) {
+ toast({
+ title: t('tasks:actions.unarchive'),
+ description: result.error,
+ variant: 'destructive',
+ });
+ }
+ };
+
const handleViewPR = (e: React.MouseEvent) => {
e.stopPropagation();
if (task.metadata?.prUrl && window.electronAPI?.openExternal) {
@@ -571,7 +583,17 @@ export const TaskCard = memo(function TaskCard({
)}
- {!task.metadata?.archivedAt && (
+ {task.metadata?.archivedAt ? (
+
+ ) : (