// ======================================== // QueuePanel Component // ======================================== // Queue list panel for the terminal dashboard with tab switching. // Tab 1 (Queue): Issue queue items from useIssueQueue() hook. // Tab 2 (Orchestrator): Active orchestration plans from orchestratorStore. // Integrates with issueQueueIntegrationStore for association chain. import { useState, useMemo, useCallback, memo } from 'react'; import { useIntl } from 'react-intl'; import { ListChecks, Loader2, AlertTriangle, ArrowDownToLine, Clock, CheckCircle, XCircle, Zap, Ban, Terminal, Workflow, Circle, CheckCircle2, SkipForward, Pause, Play, Square, RotateCcw, AlertCircle, } from 'lucide-react'; import { Badge } from '@/components/ui/Badge'; import { Button } from '@/components/ui/Button'; import { cn } from '@/lib/utils'; import { useIssueQueue } from '@/hooks/useIssues'; import { useIssueQueueIntegrationStore, selectAssociationChain, } from '@/stores/issueQueueIntegrationStore'; import { useQueueExecutionStore, selectByQueueItem, } from '@/stores/queueExecutionStore'; import { useOrchestratorStore, selectActivePlans, selectActivePlanCount, type OrchestrationRunState, } from '@/stores/orchestratorStore'; import type { StepStatus, OrchestrationStatus } from '@/types/orchestrator'; import type { QueueItem } from '@/lib/api'; // ========== Tab Type ========== type QueueTab = 'queue' | 'orchestrator'; // ========== Queue Tab: Status Config ========== type QueueItemStatus = QueueItem['status']; const STATUS_CONFIG: Record = { pending: { variant: 'secondary', icon: Clock, label: 'Pending' }, ready: { variant: 'info', icon: Zap, label: 'Ready' }, executing: { variant: 'warning', icon: Loader2, label: 'Executing' }, completed: { variant: 'success', icon: CheckCircle, label: 'Completed' }, failed: { variant: 'destructive', icon: XCircle, label: 'Failed' }, blocked: { variant: 'outline', icon: Ban, label: 'Blocked' }, }; // ========== Queue Tab: Item Row ========== function QueueItemRow({ item, isHighlighted, onSelect, }: { item: QueueItem; isHighlighted: boolean; onSelect: () => void; }) { const { formatMessage } = useIntl(); const config = STATUS_CONFIG[item.status] ?? STATUS_CONFIG.pending; const StatusIcon = config.icon; const executions = useQueueExecutionStore(selectByQueueItem(item.item_id)); const activeExec = executions.find((e) => e.status === 'running') ?? executions[0]; return ( ); } // ========== Queue Tab: Content ========== function QueueTabContent({ embedded = false }: { embedded?: boolean }) { const { formatMessage } = useIntl(); const queueQuery = useIssueQueue(); const associationChain = useIssueQueueIntegrationStore(selectAssociationChain); const buildAssociationChain = useIssueQueueIntegrationStore((s) => s.buildAssociationChain); const allItems = useMemo(() => { if (!queueQuery.data) return []; const grouped = queueQuery.data.grouped_items ?? {}; const items: QueueItem[] = []; for (const group of Object.values(grouped)) { items.push(...group); } items.sort((a, b) => a.execution_order - b.execution_order); return items; }, [queueQuery.data]); const handleSelect = useCallback( (queueItemId: string) => { buildAssociationChain(queueItemId, 'queue'); }, [buildAssociationChain] ); if (queueQuery.isLoading) { return (
); } if (queueQuery.error) { return (

{formatMessage({ id: 'terminalDashboard.queuePanel.error' })}

{queueQuery.error.message}

); } if (allItems.length === 0) { return (

{formatMessage({ id: 'terminalDashboard.queuePanel.noItems' })}

{formatMessage({ id: 'terminalDashboard.queuePanel.noItemsDesc' })}

); } return (
{allItems.map((item) => ( handleSelect(item.item_id)} /> ))}
); } // ========== Orchestrator Tab: Status Badge ========== const orchestratorStatusClass: Record = { pending: 'bg-muted text-muted-foreground border-border', running: 'bg-primary/10 text-primary border-primary/50', paused: 'bg-amber-500/10 text-amber-500 border-amber-500/50', completed: 'bg-green-500/10 text-green-500 border-green-500/50', failed: 'bg-destructive/10 text-destructive border-destructive/50', cancelled: 'bg-muted text-muted-foreground border-border', }; function OrchestratorStatusBadge({ status }: { status: OrchestrationStatus }) { const { formatMessage } = useIntl(); return ( {formatMessage({ id: `orchestrator.status.${status}` })} ); } // ========== Orchestrator Tab: Step Icon ========== function StepIcon({ status }: { status: StepStatus }) { switch (status) { case 'running': return ; case 'completed': return ; case 'failed': return ; case 'skipped': return ; case 'paused': return ; case 'cancelled': return ; default: return ; } } // ========== Orchestrator Tab: Plan Controls ========== function PlanControls({ planId, status, failedStepId }: { planId: string; status: OrchestrationStatus; failedStepId: string | null; }) { const pauseOrchestration = useOrchestratorStore((s) => s.pauseOrchestration); const resumeOrchestration = useOrchestratorStore((s) => s.resumeOrchestration); const stopOrchestration = useOrchestratorStore((s) => s.stopOrchestration); const retryStep = useOrchestratorStore((s) => s.retryStep); const skipStep = useOrchestratorStore((s) => s.skipStep); if (status === 'completed' || status === 'cancelled') return null; const isPausedOnError = status === 'paused' && failedStepId !== null; const isPausedByUser = status === 'paused' && failedStepId === null; return (
{status === 'running' && ( <> )} {isPausedByUser && ( <> )} {isPausedOnError && failedStepId && ( <> )}
); } // ========== Orchestrator Tab: Plan Card ========== const PlanCard = memo(function PlanCard({ runState }: { runState: OrchestrationRunState }) { const { plan, status, stepStatuses, currentStepIndex } = runState; const { completedCount, totalCount, progress } = useMemo(() => { const statuses = Object.values(stepStatuses); const total = statuses.length; const completed = statuses.filter((s) => s.status === 'completed' || s.status === 'skipped').length; return { completedCount: completed, totalCount: total, progress: total > 0 ? (completed / total) * 100 : 0 }; }, [stepStatuses]); const failedStepId = useMemo(() => { for (const [stepId, stepState] of Object.entries(stepStatuses)) { if (stepState.status === 'failed') return stepId; } return null; }, [stepStatuses]); return (

{plan.name}

{completedCount}/{totalCount}
{plan.steps.map((step, index) => { const stepState = stepStatuses[step.id]; if (!stepState) return null; const isCurrent = index === currentStepIndex && status === 'running'; return (
{step.name} {stepState.retryCount > 0 && ( ×{stepState.retryCount} )}
); })}
{failedStepId && stepStatuses[failedStepId]?.error && (
{stepStatuses[failedStepId].error}
)}
); }); // ========== Orchestrator Tab: Content ========== function OrchestratorTabContent() { const { formatMessage } = useIntl(); const activePlans = useOrchestratorStore(selectActivePlans); const planEntries = useMemo(() => Object.entries(activePlans), [activePlans]); if (planEntries.length === 0) { return (

{formatMessage({ id: 'terminalDashboard.orchestratorPanel.noPlans', defaultMessage: 'No active orchestrations' })}

{formatMessage({ id: 'terminalDashboard.orchestratorPanel.noPlansHint', defaultMessage: 'Run a flow from the Orchestrator to see progress here' })}

); } return (
{planEntries.map(([planId, runState]) => ( ))}
); } // ========== Main Component ========== export function QueuePanel({ embedded = false }: { embedded?: boolean }) { const { formatMessage } = useIntl(); const [activeTab, setActiveTab] = useState('queue'); const orchestratorCount = useOrchestratorStore(selectActivePlanCount); const queueQuery = useIssueQueue(); const queueActiveCount = useMemo(() => { if (!queueQuery.data) return 0; const grouped = queueQuery.data.grouped_items ?? {}; let count = 0; for (const items of Object.values(grouped)) { count += items.filter( (item) => item.status === 'pending' || item.status === 'ready' || item.status === 'executing' ).length; } return count; }, [queueQuery.data]); return (
{/* Tab bar */} {!embedded && (
)} {/* Tab content */} {activeTab === 'queue' ? ( ) : ( )}
); }