import React, { useState, useEffect, useMemo } from 'react' import { Box, Text } from 'ink' import { useI18n } from '../i18n-context' import { getEventBus } from '../workflow/event-bus.js' import type { WorkflowEvent } from '../workflow/event-bus.js' interface AgentState { label: string phase: string status: 'running' | 'done' | 'failed' durationMs: number } interface WorkflowProgressProps { /** If provided, only show progress for this runId. Otherwise auto-detect. */ runId?: string } export function WorkflowProgress({ runId }: WorkflowProgressProps) { const { t } = useI18n() const [activeRunId, setActiveRunId] = useState(null) const [phases, setPhases] = useState>([]) const [agents, setAgents] = useState>(new Map()) const [elapsed, setElapsed] = useState(0) const [done, setDone] = useState(false) const [_totalAgents, setTotalAgents] = useState(0) const [cacheHits, setCacheHits] = useState(0) useEffect(() => { const bus = getEventBus() const handleEvent = (event: WorkflowEvent) => { switch (event.type) { case 'phase:start': setPhases((prev) => [...prev, { name: event.phase, done: false }]) setActiveRunId(bus.getActiveRunId()) break case 'agent:start': setAgents((prev) => { const next = new Map(prev) next.set(event.agentId, { label: event.label, phase: event.phase, status: 'running', durationMs: 0, }) return next }) break case 'agent:end': setAgents((prev) => { const next = new Map(prev) const existing = next.get(event.agentId) if (existing) { next.set(event.agentId, { ...existing, status: event.success ? 'done' : 'failed', durationMs: event.durationMs, }) // Mark phase as done if all agents in it are done const phaseName = existing.phase const allInPhase = Array.from(next.values()).filter((a) => a.phase === phaseName) const allDone = allInPhase.every((a) => a.status !== 'running') if (allDone) { setPhases((prev) => prev.map((p) => (p.name === phaseName ? { ...p, done: true } : p)), ) } } return next }) break case 'done': setDone(true) setTotalAgents(event.totalAgents) setCacheHits(event.cacheHits) break case 'error': break } } // Subscribe to all event types const types = [ 'phase:start', 'agent:start', 'agent:end', 'agent:result', 'log', 'error', 'done', ] for (const type of types) { bus.on(type, handleEvent) } // Elapsed timer (ticks every 100ms) const timer = setInterval(() => { setElapsed((prev) => prev + 100) }, 100) return () => { for (const type of types) { bus.off(type, handleEvent) } clearInterval(timer) } }, []) const agentList = useMemo(() => Array.from(agents.values()), [agents]) if (!activeRunId && !runId) return null if (done && agentList.length === 0) return null const runningCount = agentList.filter((a) => a.status === 'running').length const doneCount = agentList.filter((a) => a.status === 'done').length const failedCount = agentList.filter((a) => a.status === 'failed').length const formatDuration = (ms: number) => { if (ms < 1000) return `${ms}ms` return `${(ms / 1000).toFixed(1)}s` } const elapsedStr = formatDuration(elapsed) return ( {t('ui.workflow.title', { runId: activeRunId ?? runId ?? '' })} {/* Phases */} {phases.map((phase, i) => { const phaseAgents = agentList.filter((a) => a.phase === phase.name) return ( {phase.done ? '●' : '◌'} {t('ui.workflow.phase', { name: phase.name })} {phaseAgents.length > 0 && t('ui.workflow.done_count', { done: String(phaseAgents.filter((a) => a.status === 'done').length), total: String(phaseAgents.length), })} {phaseAgents.map((agent, j) => ( {agent.status === 'done' ? '✓' : agent.status === 'failed' ? '✗' : '⏳'}{' '} {agent.label} {agent.status !== 'running' && ` ${formatDuration(agent.durationMs)}`} ))} ) })} {/* Summary footer */} {t('ui.workflow.footer', { time: elapsedStr, done: String(doneCount), total: String(agentList.length), })} {runningCount > 0 && t('ui.workflow.running', { n: String(runningCount) })} {failedCount > 0 && t('ui.workflow.failed', { n: String(failedCount) })} {done && t('ui.workflow.cache_hits', { n: String(cacheHits) })} {done && t('ui.workflow.done_marker')} {!done && t('ui.workflow.running_marker')} ) }