| 50 | |
| 51 | /** Build a reactive store from the bus: subscribe to the bus, reduce events, notify React subscribers. */ |
| 52 | export function createProgressStoreFromBus(bus: ProgressBus): ProgressStore { |
| 53 | const byId = new Map<string, RunProgress>() |
| 54 | let snapshot: RunProgress[] = [] |
| 55 | const listeners = new Set<() => void>() |
| 56 | |
| 57 | const notify = (): void => { |
| 58 | snapshot = [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt) |
| 59 | for (const fn of listeners) fn() |
| 60 | } |
| 61 | |
| 62 | const ensure = (runId: string, workflowName: string): RunProgress => { |
| 63 | let p = byId.get(runId) |
| 64 | if (!p) { |
| 65 | p = { |
| 66 | runId, |
| 67 | workflowName, |
| 68 | status: 'running', |
| 69 | phases: [], |
| 70 | declaredPhases: [], |
| 71 | currentPhase: null, |
| 72 | agents: [], |
| 73 | agentCount: 0, |
| 74 | startedAt: Date.now(), |
| 75 | updatedAt: Date.now(), |
| 76 | } |
| 77 | byId.set(runId, p) |
| 78 | } |
| 79 | return p |
| 80 | } |
| 81 | |
| 82 | const apply = (event: ProgressEvent): void => { |
| 83 | // log produces no visible state change (panel has no log view): early exit to avoid pointless snapshot rebuild and React re-render |
| 84 | if (event.type === 'log') return |
| 85 | const runId = event.runId |
| 86 | const p = ensure( |
| 87 | runId, |
| 88 | 'workflowName' in event ? event.workflowName : 'workflow', |
| 89 | ) |
| 90 | p.updatedAt = Date.now() |
| 91 | switch (event.type) { |
| 92 | case 'run_started': |
| 93 | p.workflowName = event.workflowName |
| 94 | p.status = 'running' |
| 95 | p.declaredPhases = event.meta?.phases?.map(ph => ph.title) ?? [] |
| 96 | p.description = event.meta?.description ?? undefined |
| 97 | break |
| 98 | case 'phase_started': |
| 99 | if (!p.phases.some(ph => ph.title === event.phase)) { |
| 100 | p.phases.push({ title: event.phase, status: 'running' }) |
| 101 | } |
| 102 | p.currentPhase = event.phase |
| 103 | break |
| 104 | case 'phase_done': |
| 105 | for (const ph of p.phases) |
| 106 | if (ph.title === event.phase) ph.status = 'done' |
| 107 | if (p.currentPhase === event.phase) p.currentPhase = null |
| 108 | break |
| 109 | case 'agent_started': { |