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 ? ( + + ) : ( ) : task.status === 'done' && !task.metadata?.archivedAt ? (