// ======================================== // TaskDrawer Component // ======================================== // Right-side task detail drawer with Overview/Flowchart/Files tabs import * as React from 'react'; import { useIntl } from 'react-intl'; import { X, FileText, GitBranch, Folder, CheckCircle, Circle, Loader2, XCircle } from 'lucide-react'; import { Flowchart } from './Flowchart'; import { Badge } from '../ui/Badge'; import { Button } from '../ui/Button'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/Tabs'; import type { NormalizedTask, LiteTask } from '@/lib/api'; import { buildFlowControl, normalizeTask } from '@/lib/api'; import type { TaskData } from '@/types/store'; // ========== Types ========== export interface TaskDrawerProps { task: NormalizedTask | TaskData | LiteTask | null; isOpen: boolean; onClose: () => void; } type TabValue = 'overview' | 'flowchart' | 'files'; // Status configuration const taskStatusConfig: Record }> = { pending: { label: 'sessionDetail.taskDrawer.status.pending', variant: 'secondary', icon: Circle, }, in_progress: { label: 'sessionDetail.taskDrawer.status.inProgress', variant: 'warning', icon: Loader2, }, completed: { label: 'sessionDetail.taskDrawer.status.completed', variant: 'success', icon: CheckCircle, }, blocked: { label: 'sessionDetail.taskDrawer.status.blocked', variant: 'destructive', icon: XCircle, }, skipped: { label: 'sessionDetail.taskDrawer.status.skipped', variant: 'default', icon: Circle, }, failed: { label: 'sessionDetail.taskDrawer.status.failed', variant: 'destructive', icon: XCircle, }, }; // ========== Component ========== export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) { const { formatMessage } = useIntl(); const [activeTab, setActiveTab] = React.useState('overview'); // Reset to overview when task changes React.useEffect(() => { if (task) { setActiveTab('overview'); } }, [task]); // ESC key to close React.useEffect(() => { const handleEsc = (e: KeyboardEvent) => { if (e.key === 'Escape' && isOpen) { onClose(); } }; window.addEventListener('keydown', handleEsc); return () => window.removeEventListener('keydown', handleEsc); }, [isOpen, onClose]); // Normalize task to unified flat format (handles old nested, new flat, and raw LiteTask/TaskData) // MUST be called before early return to satisfy React hooks rules const nt = React.useMemo( () => task ? normalizeTask(task as unknown as Record) : null, [task], ); if (!task || !isOpen || !nt) { return null; } const taskId = nt.task_id || 'N/A'; const taskTitle = nt.title || 'Untitled Task'; const taskDescription = nt.description; const taskStatus = nt.status; const flowControl = buildFlowControl(nt); // Normalized flat fields const acceptanceCriteria = nt.convergence?.criteria || []; const focusPaths = nt.focus_paths || []; const dependsOn = nt.depends_on || []; const preAnalysis = nt.pre_analysis || flowControl?.pre_analysis || []; const implSteps = nt.implementation || flowControl?.implementation_approach || []; const taskFiles = nt.files || flowControl?.target_files || []; const taskScope = nt.scope; // Detect if task supports status tracking (new format has explicit status/status_history) const rawData = nt._raw as Record | undefined; const sourceRaw = (rawData?._raw as Record) || rawData; const hasStatusTracking = sourceRaw ? (sourceRaw.status !== undefined || sourceRaw.status_history !== undefined) : false; const statusConfig = taskStatusConfig[taskStatus] || taskStatusConfig.pending; const StatusIcon = statusConfig.icon; const hasFlowchart = implSteps.length > 0; const hasFiles = taskFiles.length > 0; return ( <> {/* Overlay */}