/** * agents-coord.ts — SSE event stream for the Live Coordination page. * * Reads owner, repoSlug, and cursor from the #page-data JSON block and * connects to the /coord/watch SSE endpoint, appending rows to the stream panel. * * Registered as: window.MusePages['agents-coord'] */ interface AgentsCoordData { owner: string; repoSlug: string; cursor: number; } const MAX_ROWS = 60; const KIND_DOT_CLASS: Record = { reservation: 'agents-stream-dot--reservation', heartbeat: 'agents-stream-dot--heartbeat', claim: 'agents-stream-dot--claim', release: 'agents-stream-dot--release', task: 'agents-stream-dot--task', intent: 'agents-stream-dot--intent', }; function setStatus(state: 'live' | 'error' | 'connecting'): void { const dot = document.getElementById('sse-dot'); const label = document.getElementById('sse-label'); if (!dot || !label) return; if (state === 'live') { dot.style.background = 'var(--color-success, #3fb950)'; dot.style.boxShadow = '0 0 6px var(--color-success, #3fb950)'; label.textContent = 'LIVE'; } else if (state === 'error') { dot.style.background = 'var(--color-danger, #f85149)'; dot.style.boxShadow = 'none'; label.textContent = 'RECONNECTING'; } else { dot.style.background = 'var(--color-warning, #e3b341)'; dot.style.boxShadow = 'none'; label.textContent = 'CONNECTING'; } } function appendRow(body: Element, kind: string, detail: string): void { const row = document.createElement('div'); row.className = 'agents-stream-row'; const dotEl = document.createElement('span'); dotEl.className = 'agents-stream-dot ' + (KIND_DOT_CLASS[kind] ?? 'agents-stream-dot--default'); const kindEl = document.createElement('span'); kindEl.className = 'agents-stream-kind'; kindEl.textContent = kind; const detailEl = document.createElement('span'); detailEl.className = 'agents-stream-detail'; detailEl.textContent = detail; row.appendChild(dotEl); row.appendChild(kindEl); row.appendChild(detailEl); body.appendChild(row); while (body.children.length > MAX_ROWS) { body.removeChild(body.firstChild!); } (body as HTMLElement).scrollTop = (body as HTMLElement).scrollHeight; } function connect(owner: string, repoSlug: string, initialCursor: number): void { const body = document.getElementById('stream-body'); const cursorEl = document.getElementById('stream-cursor'); if (!body) return; let cursor = initialCursor; let watchUrl = `/${owner}/${repoSlug}/coord/watch?since_id=${cursor}`; setStatus('connecting'); const es = new EventSource(watchUrl); es.addEventListener('coord_record', (e: MessageEvent) => { setStatus('live'); try { const rec = JSON.parse(e.data as string) as Record; cursor = (rec['id'] as number) || cursor; if (cursorEl) cursorEl.textContent = `cursor: ${cursor}`; watchUrl = `/${owner}/${repoSlug}/coord/watch?since_id=${cursor}`; const kind = (rec['kind'] as string) || 'unknown'; const runId = ((rec['run_id'] as string) || '').substring(0, 22); const payload = (rec['payload'] as Record) || {}; let addr = ''; const addrs = payload['addresses'] as string[] | undefined; if (addrs && addrs.length) addr = addrs[0]; else if (payload['symbol_address']) addr = payload['symbol_address'] as string; else if (payload['task_id']) addr = 'task:' + ((payload['task_id'] as string).substring(0, 8)); const detail = runId + (addr ? ' · ' + addr.substring(0, 40) : ''); appendRow(body, kind, detail); } catch { /* ignore parse errors */ } }); es.onerror = () => { setStatus('error'); es.close(); setTimeout(() => connect(owner, repoSlug, cursor), 3000); }; } export function initAgentsCoord(data: Record): void { const cfg = data as unknown as AgentsCoordData; const owner = String(cfg.owner ?? ''); const repoSlug = String(cfg.repoSlug ?? ''); const cursor = Number(cfg.cursor ?? 0); if (!owner || !repoSlug) return; connect(owner, repoSlug, cursor); }