(function() { const log = document.getElementById('live'); let placeholder = log.firstChild; function setPlaceholder(text) { log.innerHTML = ''; const span = document.createElement('div'); span.className = 'meta'; span.textContent = text; log.appendChild(span); placeholder = span; } function clearPlaceholder() { if (placeholder) { log.innerHTML = ''; placeholder = null; } } function row(cls, text) { clearPlaceholder(); const el = document.createElement('div'); el.className = 'row ' + (cls || ''); el.textContent = text; log.appendChild(el); log.scrollTop = log.scrollHeight; return el; } function trim(s, n) { return s.length > n ? s.slice(0, n) + '…' : s; } function renderStream(v) { if (v.type === 'system' && v.subtype === 'init') { row('sys', '· session init · tools=' + (v.tools||[]).length + ' model=' + (v.model || '?')); return; } if (v.type === 'rate_limit_event') { const u = Math.round((v.rate_limit_info?.utilization || 0) * 100); const s = v.rate_limit_info?.status || ''; row('sys', '· rate-limit util=' + u + '% (' + s + ')'); return; } if (v.type === 'assistant' && v.message && v.message.content) { for (const c of v.message.content) { if (c.type === 'text' && c.text && c.text.trim()) row('text', c.text); else if (c.type === 'thinking') row('thinking', '· thinking …'); else if (c.type === 'tool_use') row('tool-use', '→ ' + c.name + ' ' + trim(JSON.stringify(c.input || {}), 240)); } return; } if (v.type === 'user' && v.message && v.message.content) { for (const c of v.message.content) { if (c.type === 'tool_result') { const txt = Array.isArray(c.content) ? c.content.map(p => p.text || '').join(' ') : (c.content || ''); row('tool-result', '← ' + trim(txt, 300)); } } return; } if (v.type === 'result') { row('result', '✓ done · ' + (v.subtype || '') + (v.is_error ? ' [error]' : '')); return; } // Fallback: small one-liner for unknown events; don't spam. row('sys', '· ' + trim(JSON.stringify(v), 200)); } function handle(ev) { if (ev.kind === 'turn_start') { const block = row('turn-start', '◆ TURN ← ' + ev.from); const body = document.createElement('div'); body.className = 'turn-body'; body.textContent = ev.body; block.appendChild(body); return; } if (ev.kind === 'turn_end') { const cls = ev.ok ? 'turn-end-ok' : 'turn-end-fail'; const sym = ev.ok ? '✓' : '✗'; row(cls, sym + ' turn ' + (ev.ok ? 'ok' : 'fail') + (ev.note ? ' — ' + ev.note : '')); return; } if (ev.kind === 'note') { row('note', '· ' + ev.text); return; } if (ev.kind === 'stream') { const v = Object.assign({}, ev); delete v.kind; renderStream(v); return; } row('note', JSON.stringify(ev)); } const es = new EventSource('/events/stream'); es.onopen = function() { setPlaceholder('(connected — waiting for events)'); }; es.onmessage = function(e) { try { handle(JSON.parse(e.data)); } catch (err) { row('note', '[parse err] ' + e.data); } }; es.onerror = function() { if (es.readyState === EventSource.CONNECTING) setPlaceholder('(reconnecting…)'); else row('note', '[disconnected]'); }; })();