// ======================================== // RecentSessionsWidget Component // ======================================== // Widget showing recent sessions across different task types (workflow, lite, orchestrator) import * as React from 'react'; import { useNavigate } from 'react-router-dom'; import { useIntl } from 'react-intl'; import { FolderKanban, Workflow, Zap, Clock, CheckCircle2, XCircle, PauseCircle, FileEdit, Wrench, GitBranch, Tag, Loader2, } from 'lucide-react'; import { Card } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { Button } from '@/components/ui/Button'; import { Progress } from '@/components/ui/Progress'; import { useSessions } from '@/hooks/useSessions'; import { useLiteTasks } from '@/hooks/useLiteTasks'; import { cn } from '@/lib/utils'; export interface RecentSessionsWidgetProps { className?: string; maxItems?: number; } // Task type definitions type TaskType = 'all' | 'workflow' | 'lite'; // Unified task item for display interface UnifiedTaskItem { id: string; name: string; type: TaskType; subType?: string; status: string; statusKey: string; // i18n key for status createdAt: string; description?: string; tags?: string[]; progress?: number; } // Tab configuration for different task types const TABS: { key: TaskType; label: string; icon: React.ElementType }[] = [ { key: 'all', label: 'home.tabs.allTasks', icon: FolderKanban }, { key: 'workflow', label: 'home.tabs.workflow', icon: Workflow }, { key: 'lite', label: 'home.tabs.liteTasks', icon: Zap }, ]; // Status icon mapping const statusIcons: Record = { in_progress: Loader2, running: Loader2, planning: FileEdit, completed: CheckCircle2, failed: XCircle, paused: PauseCircle, pending: Clock, cancelled: XCircle, idle: Clock, initializing: Loader2, ready: CheckCircle2, }; // Status color mapping const statusColors: Record = { in_progress: 'bg-warning/20 text-warning border-warning/30', running: 'bg-warning/20 text-warning border-warning/30', planning: 'bg-violet-500/20 text-violet-600 border-violet-500/30', completed: 'bg-success/20 text-success border-success/30', failed: 'bg-destructive/20 text-destructive border-destructive/30', paused: 'bg-slate-400/20 text-slate-500 border-slate-400/30', pending: 'bg-muted text-muted-foreground border-border', cancelled: 'bg-destructive/20 text-destructive border-destructive/30', idle: 'bg-muted text-muted-foreground border-border', initializing: 'bg-info/20 text-info border-info/30', ready: 'bg-success/20 text-success border-success/30', }; // Status to i18n key mapping const statusI18nKeys: Record = { in_progress: 'inProgress', running: 'running', planning: 'planning', completed: 'completed', failed: 'failed', paused: 'paused', pending: 'pending', cancelled: 'cancelled', idle: 'idle', initializing: 'initializing', ready: 'ready', }; // Lite task sub-type icons const liteTypeIcons: Record = { 'lite-plan': FileEdit, 'lite-fix': Wrench, 'multi-cli-plan': GitBranch, }; // Task type colors const typeColors: Record = { all: 'bg-muted text-muted-foreground', workflow: 'bg-primary/20 text-primary', lite: 'bg-amber-500/20 text-amber-600', }; function TaskItemCard({ item, onClick }: { item: UnifiedTaskItem; onClick: () => void }) { const { formatMessage } = useIntl(); const StatusIcon = statusIcons[item.status] || Clock; const TypeIcon = item.subType ? (liteTypeIcons[item.subType] || Zap) : item.type === 'workflow' ? Workflow : Zap; const isAnimated = item.status === 'in_progress' || item.status === 'running' || item.status === 'initializing'; return ( ); } function TaskItemSkeleton() { return (
); } function RecentSessionsWidgetComponent({ className, maxItems = 6, }: RecentSessionsWidgetProps) { const { formatMessage } = useIntl(); const navigate = useNavigate(); const [activeTab, setActiveTab] = React.useState('all'); // Fetch workflow sessions const { activeSessions, isLoading: sessionsLoading } = useSessions({ filter: { location: 'active' }, }); // Fetch lite tasks const { allSessions: liteSessions, isLoading: liteLoading } = useLiteTasks(); // Format relative time with fallback const formatRelativeTime = React.useCallback((dateStr: string | undefined): string => { if (!dateStr) return formatMessage({ id: 'common.time.justNow' }); const date = new Date(dateStr); if (isNaN(date.getTime())) return formatMessage({ id: 'common.time.justNow' }); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return formatMessage({ id: 'common.time.justNow' }); if (diffMins < 60) return formatMessage({ id: 'common.time.minutesAgo' }, { count: diffMins }); if (diffHours < 24) return formatMessage({ id: 'common.time.hoursAgo' }, { count: diffHours }); return formatMessage({ id: 'common.time.daysAgo' }, { count: diffDays }); }, [formatMessage]); // Convert to unified items const unifiedItems = React.useMemo((): UnifiedTaskItem[] => { const items: UnifiedTaskItem[] = []; // Add workflow sessions activeSessions.forEach((session) => { const status = session.status || 'pending'; items.push({ id: session.session_id, name: session.title || session.description || session.session_id, type: 'workflow', status, statusKey: statusI18nKeys[status] || status, createdAt: formatRelativeTime(session.created_at), description: session.description || `Session: ${session.session_id}`, tags: [], progress: undefined, }); }); // Add lite tasks liteSessions.forEach((session) => { const status = session.status || 'pending'; const sessionId = session.session_id || session.id; items.push({ id: sessionId, name: session.title || sessionId, type: 'lite', subType: session._type, status, statusKey: statusI18nKeys[status] || status, createdAt: formatRelativeTime(session.createdAt), description: session.description || `${session._type} task`, tags: [], progress: undefined, }); }); // Sort by most recent (use original date for sorting, not formatted string) return items; }, [activeSessions, liteSessions, formatRelativeTime]); // Filter items by tab const filteredItems = React.useMemo(() => { if (activeTab === 'all') return unifiedItems.slice(0, maxItems); return unifiedItems.filter((item) => item.type === activeTab).slice(0, maxItems); }, [unifiedItems, activeTab, maxItems]); // Handle item click const handleItemClick = (item: UnifiedTaskItem) => { switch (item.type) { case 'workflow': navigate(`/sessions/${item.id}`); break; case 'lite': navigate(`/lite-tasks/${item.subType}/${item.id}`); break; } }; const handleViewAll = () => { navigate('/sessions'); }; const isLoading = sessionsLoading || liteLoading; return (

{formatMessage({ id: 'home.sections.recentTasks' })}

{/* Tabs */}
{TABS.map((tab) => { const TabIcon = tab.icon; const count = tab.key === 'all' ? unifiedItems.length : unifiedItems.filter((i) => i.type === tab.key).length; return ( ); })}
{/* Task items */}
{isLoading ? (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) : filteredItems.length === 0 ? (

{formatMessage({ id: 'home.emptyState.noTasks.message' })}

) : (
{filteredItems.map((item) => ( handleItemClick(item)} /> ))}
)}
); } export const RecentSessionsWidget = React.memo(RecentSessionsWidgetComponent); export default RecentSessionsWidget;