// ======================================== // LiteTasksPage Component // ======================================== // Lite-plan and lite-fix task list page with TaskDrawer import * as React from 'react'; import { useIntl } from 'react-intl'; import { ArrowLeft, Zap, Wrench, FileEdit, MessagesSquare, Calendar, XCircle, Activity, Repeat, MessageCircle, ChevronDown, ChevronRight, Search, SortAsc, SortDesc, ListFilter, Hash, ListChecks, Package, Loader2, Compass, Stethoscope, FolderOpen, FileText, CheckCircle2, Clock, AlertCircle, } from 'lucide-react'; import { useLiteTasks } from '@/hooks/useLiteTasks'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; import { Card, CardContent } from '@/components/ui/Card'; import { Tabs, TabsContent } from '@/components/ui/Tabs'; import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation'; import { TaskDrawer } from '@/components/shared/TaskDrawer'; import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext } from '@/lib/api'; import { useNavigate } from 'react-router-dom'; type LiteTaskTab = 'lite-plan' | 'lite-fix' | 'multi-cli-plan'; type SortField = 'date' | 'name' | 'tasks'; type SortOrder = 'asc' | 'desc'; /** * Get i18n text from label object (supports {en, zh} format) * Note: fallback should be provided dynamically from component context */ function getI18nText(label: string | { en?: string; zh?: string } | undefined): string | undefined { if (!label) return undefined; if (typeof label === 'string') return label; return label.en || label.zh; } type ExpandedTab = 'tasks' | 'context'; /** * ExpandedSessionPanel - Multi-tab panel shown when a lite session is expanded */ function ExpandedSessionPanel({ session, onTaskClick, }: { session: LiteTaskSession; onTaskClick: (task: LiteTask) => void; }) { const { formatMessage } = useIntl(); const [activeTab, setActiveTab] = React.useState('tasks'); const [contextData, setContextData] = React.useState(null); const [contextLoading, setContextLoading] = React.useState(false); const [contextError, setContextError] = React.useState(null); const tasks = session.tasks || []; const taskCount = tasks.length; // Load context data lazily when context tab is selected React.useEffect(() => { if (activeTab !== 'context') return; if (contextData || contextLoading) return; if (!session.path) { setContextError('No session path available'); return; } setContextLoading(true); fetchLiteSessionContext(session.path) .then((data) => { setContextData(data); setContextError(null); }) .catch((err) => { setContextError(err.message || 'Failed to load context'); }) .finally(() => { setContextLoading(false); }); }, [activeTab, session.path, contextData, contextLoading]); return (
{/* Quick Info Cards */}
{/* Tasks Tab */} {activeTab === 'tasks' && (
{tasks.map((task, index) => ( { e.stopPropagation(); onTaskClick(task); }} >
{task.task_id || `#${index + 1}`}

{task.title || formatMessage({ id: 'liteTasks.untitled' })}

{task.status && ( {task.status} )}
{task.description && (

{task.description}

)}
))}
)} {/* Context Tab */} {activeTab === 'context' && (
{contextLoading && (
{formatMessage({ id: 'liteTasks.contextPanel.loading' })}
)} {contextError && !contextLoading && (
{formatMessage({ id: 'liteTasks.contextPanel.error' })}: {contextError}
)} {!contextLoading && !contextError && contextData && ( )} {!contextLoading && !contextError && !contextData && !session.path && (

{formatMessage({ id: 'liteTasks.contextPanel.empty' })}

)}
)}
); } /** * ContextContent - Renders the context data sections */ function ContextContent({ contextData, session, }: { contextData: LiteSessionContext; session: LiteTaskSession; }) { const { formatMessage } = useIntl(); const plan = session.plan || {}; const hasExplorations = !!(contextData.explorations?.manifest); const hasDiagnoses = !!(contextData.diagnoses?.manifest || contextData.diagnoses?.items?.length); const hasContext = !!contextData.context; const hasFocusPaths = !!(plan.focus_paths as string[] | undefined)?.length; const hasSummary = !!(plan.summary as string | undefined); const hasAnyContent = hasExplorations || hasDiagnoses || hasContext || hasFocusPaths || hasSummary; if (!hasAnyContent) { return (

{formatMessage({ id: 'liteTasks.contextPanel.empty' })}

); } return (
{/* Explorations Section */} {hasExplorations && ( } title={formatMessage({ id: 'liteTasks.contextPanel.explorations' })} badge={ contextData.explorations?.manifest?.exploration_count ? formatMessage( { id: 'liteTasks.contextPanel.explorationsCount' }, { count: contextData.explorations.manifest.exploration_count as number } ) : undefined } >
{!!contextData.explorations?.manifest?.task_description && (
{formatMessage({ id: 'liteTasks.contextPanel.taskDescription' })}: {' '} {String(contextData.explorations.manifest.task_description)}
)} {!!contextData.explorations?.manifest?.complexity && (
{formatMessage({ id: 'liteTasks.contextPanel.complexity' })}: {' '} {String(contextData.explorations.manifest.complexity)}
)} {contextData.explorations?.data && (
{Object.keys(contextData.explorations.data).map((angle) => ( {angle.replace(/-/g, ' ')} ))}
)}
)} {/* Diagnoses Section */} {hasDiagnoses && ( } title={formatMessage({ id: 'liteTasks.contextPanel.diagnoses' })} badge={ contextData.diagnoses?.items?.length ? formatMessage( { id: 'liteTasks.contextPanel.diagnosesCount' }, { count: contextData.diagnoses.items.length } ) : undefined } > {contextData.diagnoses?.items?.map((item, i) => (
{(item.title as string) || (item.description as string) || `Diagnosis ${i + 1}`}
))}
)} {/* Context Package Section */} {hasContext && ( } title={formatMessage({ id: 'liteTasks.contextPanel.contextPackage' })} >
            {JSON.stringify(contextData.context, null, 2)}
          
)} {/* Focus Paths from Plan */} {hasFocusPaths && ( } title={formatMessage({ id: 'liteTasks.contextPanel.focusPaths' })} >
{(plan.focus_paths as string[]).map((p, i) => ( {p} ))}
)} {/* Plan Summary */} {hasSummary && ( } title={formatMessage({ id: 'liteTasks.contextPanel.summary' })} >

{plan.summary as string}

)}
); } /** * ContextSection - Collapsible section wrapper for context items */ function ContextSection({ icon, title, badge, children, }: { icon: React.ReactNode; title: string; badge?: string; children: React.ReactNode; }) { const [isOpen, setIsOpen] = React.useState(true); return ( e.stopPropagation()}> {isOpen && ( {children} )} ); } /** * LiteTasksPage component - Display lite-plan and lite-fix sessions with expandable tasks */ export function LiteTasksPage() { const navigate = useNavigate(); const { formatMessage } = useIntl(); const { litePlan, liteFix, multiCliPlan, isLoading, error, refetch } = useLiteTasks(); const [activeTab, setActiveTab] = React.useState('lite-plan'); const [expandedSessionId, setExpandedSessionId] = React.useState(null); const [selectedTask, setSelectedTask] = React.useState(null); const [searchQuery, setSearchQuery] = React.useState(''); const [sortField, setSortField] = React.useState('date'); const [sortOrder, setSortOrder] = React.useState('desc'); // Filter and sort sessions const filterAndSort = React.useCallback((sessions: LiteTaskSession[]) => { let filtered = sessions; // Apply search filter if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); filtered = sessions.filter(session => session.id.toLowerCase().includes(query) || session.tasks?.some(task => task.title?.toLowerCase().includes(query) || task.task_id?.toLowerCase().includes(query) ) ); } // Apply sort const sorted = [...filtered].sort((a, b) => { let comparison = 0; switch (sortField) { case 'date': comparison = new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime(); break; case 'name': comparison = a.id.localeCompare(b.id); break; case 'tasks': comparison = (a.tasks?.length || 0) - (b.tasks?.length || 0); break; } return sortOrder === 'asc' ? comparison : -comparison; }); return sorted; }, [searchQuery, sortField, sortOrder]); // Filtered data const filteredLitePlan = React.useMemo(() => filterAndSort(litePlan), [litePlan, filterAndSort]); const filteredLiteFix = React.useMemo(() => filterAndSort(liteFix), [liteFix, filterAndSort]); const filteredMultiCliPlan = React.useMemo(() => filterAndSort(multiCliPlan), [multiCliPlan, filterAndSort]); // Toggle sort const toggleSort = (field: SortField) => { if (sortField === field) { setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc'); } else { setSortField(field); setSortOrder('desc'); } }; const handleBack = () => { navigate('/sessions'); }; // Get status badge color const getStatusColor = (status?: string) => { const statusColors: Record = { decided: 'success', converged: 'success', plan_generated: 'success', completed: 'success', exploring: 'info', initialized: 'info', analyzing: 'warning', debating: 'warning', blocked: 'destructive', conflict: 'destructive', }; return statusColors[status || ''] || 'secondary'; }; // Render lite task card with expandable tasks const renderLiteTaskCard = (session: LiteTaskSession) => { const isLitePlan = session.type === 'lite-plan'; const taskCount = session.tasks?.length || 0; const isExpanded = expandedSessionId === session.id; // Calculate task status distribution const taskStats = React.useMemo(() => { const tasks = session.tasks || []; return { completed: tasks.filter((t) => t.status === 'completed').length, inProgress: tasks.filter((t) => t.status === 'in_progress').length, blocked: tasks.filter((t) => t.status === 'blocked').length, pending: tasks.filter((t) => !t.status || t.status === 'pending').length, }; }, [session.tasks]); const firstTask = session.tasks?.[0]; return (
setExpandedSessionId(isExpanded ? null : session.id)} >
{isExpanded ? ( ) : ( )}

{session.id}

{isLitePlan ? : } {formatMessage({ id: isLitePlan ? 'liteTasks.type.plan' : 'liteTasks.type.fix' })}
{/* Task preview - first task title */} {firstTask?.title && (

{firstTask.title}

)} {/* Task status distribution */}
{taskStats.completed > 0 && ( {taskStats.completed} {formatMessage({ id: 'liteTasks.status.completed' })} )} {taskStats.inProgress > 0 && ( {taskStats.inProgress} {formatMessage({ id: 'liteTasks.status.inProgress' })} )} {taskStats.blocked > 0 && ( {taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })} )} {taskStats.pending > 0 && ( {taskStats.pending} {formatMessage({ id: 'liteTasks.status.pending' })} )}
{/* Date and task count */}
{session.createdAt && ( {new Date(session.createdAt).toLocaleDateString()} )} {taskCount > 0 && ( {taskCount} {formatMessage({ id: 'liteTasks.tasksCount' })} )}
{/* Expanded tasks panel with tabs */} {isExpanded && session.tasks && session.tasks.length > 0 && ( )}
); }; // Render multi-cli plan card const renderMultiCliCard = (session: LiteTaskSession) => { const metadata = session.metadata || {}; const latestSynthesis = session.latestSynthesis || {}; const roundCount = (metadata.roundId as number) || session.roundCount || 1; const topicTitle = getI18nText( latestSynthesis.title as string | { en?: string; zh?: string } | undefined ) || formatMessage({ id: 'liteTasks.discussionTopic' }); const status = latestSynthesis.status || session.status || 'analyzing'; const createdAt = (metadata.timestamp as string) || session.createdAt || ''; // Calculate task status distribution const taskStats = React.useMemo(() => { const tasks = session.tasks || []; return { completed: tasks.filter((t) => t.status === 'completed').length, inProgress: tasks.filter((t) => t.status === 'in_progress').length, blocked: tasks.filter((t) => t.status === 'blocked').length, pending: tasks.filter((t) => !t.status || t.status === 'pending').length, total: tasks.length, }; }, [session.tasks]); return ( setExpandedSessionId(expandedSessionId === session.id ? null : session.id)} >
{expandedSessionId === session.id ? ( ) : ( )}

{session.id}

{formatMessage({ id: 'liteTasks.type.multiCli' })}
{topicTitle}
{/* Task status distribution for multi-cli */} {taskStats.total > 0 && (
{taskStats.completed > 0 && ( {taskStats.completed} {formatMessage({ id: 'liteTasks.status.completed' })} )} {taskStats.inProgress > 0 && ( {taskStats.inProgress} {formatMessage({ id: 'liteTasks.status.inProgress' })} )} {taskStats.blocked > 0 && ( {taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })} )} {taskStats.pending > 0 && ( {taskStats.pending} {formatMessage({ id: 'liteTasks.status.pending' })} )}
)}
{createdAt && ( {new Date(createdAt).toLocaleDateString()} )} {roundCount} {formatMessage({ id: 'liteTasks.rounds' })} {status}
); }; // Loading state if (isLoading) { return (
); } // Error state if (error) { return (

{formatMessage({ id: 'common.errors.loadFailed' })}

{error.message}

); } const totalSessions = litePlan.length + liteFix.length + multiCliPlan.length; return (
{/* Header */}

{formatMessage({ id: 'liteTasks.title' })}

{formatMessage({ id: 'liteTasks.subtitle' }, { count: totalSessions })}

{/* Tabs */} setActiveTab(v as LiteTaskTab)} tabs={[ { value: 'lite-plan', label: formatMessage({ id: 'liteTasks.type.plan' }), icon: , badge: {litePlan.length}, }, { value: 'lite-fix', label: formatMessage({ id: 'liteTasks.type.fix' }), icon: , badge: {liteFix.length}, }, { value: 'multi-cli-plan', label: formatMessage({ id: 'liteTasks.type.multiCli' }), icon: , badge: {multiCliPlan.length}, }, ]} /> {/* Search and Sort Toolbar */}
{/* Search */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 text-sm rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" />
{/* Sort Buttons */}
{formatMessage({ id: 'liteTasks.sortBy' })}:
{/* Lite Plan Tab */} {activeTab === 'lite-plan' && (
{litePlan.length === 0 ? (

{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-plan' })}

{formatMessage({ id: 'liteTasks.empty.message' })}

) : filteredLitePlan.length === 0 ? (

{formatMessage({ id: 'liteTasks.noResults.title' })}

{formatMessage({ id: 'liteTasks.noResults.message' })}

) : (
{filteredLitePlan.map(renderLiteTaskCard)}
)}
)} {/* Lite Fix Tab */} {activeTab === 'lite-fix' && (
{liteFix.length === 0 ? (

{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-fix' })}

{formatMessage({ id: 'liteTasks.empty.message' })}

) : filteredLiteFix.length === 0 ? (

{formatMessage({ id: 'liteTasks.noResults.title' })}

{formatMessage({ id: 'liteTasks.noResults.message' })}

) : (
{filteredLiteFix.map(renderLiteTaskCard)}
)}
)} {/* Multi-CLI Plan Tab */} {activeTab === 'multi-cli-plan' && (
{multiCliPlan.length === 0 ? (

{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'multi-cli-plan' })}

{formatMessage({ id: 'liteTasks.empty.message' })}

) : filteredMultiCliPlan.length === 0 ? (

{formatMessage({ id: 'liteTasks.noResults.title' })}

{formatMessage({ id: 'liteTasks.noResults.message' })}

) : (
{filteredMultiCliPlan.map(renderMultiCliCard)}
)}
)} {/* TaskDrawer */} setSelectedTask(null)} />
); } export default LiteTasksPage;