import { app, navigate, isRouteCurrent } from '../router.js'; import { api } from '../api.js'; import { escapeHTML, formatDuration, formatTokenCount, formatCost, formatElapsed, getEnvelopeCorrelation, getEnvelopeType, getEnvelopeAttributes, getEnvelopePayload, isCurrentPath, renderCopyButton, statusIcon, skeletonRows, extractRunUsage, } from '../utils.js'; import { subscribeWS } from '../ws.js'; let runDetailUnsubscribe = null; let runLiveOps = {}; // spanID → { name, kind, startedAt, promptPreview, inputPreview } let _runReloadTimer = null; let runSpansViewMode = 'table'; export function cleanup() { if (runDetailUnsubscribe) { runDetailUnsubscribe(); runDetailUnsubscribe = null; } clearTimeout(_runReloadTimer); _runReloadTimer = null; runLiveOps = {}; } function extractPromptPreview(spans) { for (const sp of spans) { const inner = (sp.payload || {}).payload || {}; if (inner.prompt_preview) return inner.prompt_preview; if (inner.message_preview) return inner.message_preview; } return null; } function renderSpanPayload(sp) { const outer = sp.payload || {}; const inner = outer.payload || {}; const parts = []; if (sp.kind === 'tool') { if (inner.input !== undefined) { const inputStr = typeof inner.input === 'object' ? JSON.stringify(inner.input, null, 2) : String(inner.input); parts.push(`
Input
${escapeHTML(inputStr)}
`); } if (inner.result_preview !== undefined) { parts.push(`
Result
${escapeHTML(String(inner.result_preview))}
`); } } else if (sp.kind === 'agent') { if (inner.prompt_preview) { parts.push(`
Prompt
${escapeHTML(String(inner.prompt_preview))}
`); } if (inner.usage) { const u = inner.usage; const tokens = [ u.total_tokens != null ? `${u.total_tokens} total` : null, u.input_tokens != null ? `${u.input_tokens} in` : null, u.output_tokens != null ? `${u.output_tokens} out` : null, ].filter(Boolean).join(' · '); if (tokens) parts.push(`
Tokens${escapeHTML(tokens)}
`); if (u.total_cost != null) { parts.push(`
Cost${escapeHTML(formatCost(u.total_cost))}
`); } } if (inner.model) { parts.push(`
Model${escapeHTML(String(inner.model))}
`); } } else { const raw = Object.keys(inner).length > 0 ? inner : (Object.keys(outer).length > 0 ? outer : null); if (raw) { parts.push(`
${escapeHTML(JSON.stringify(raw, null, 2))}
`); } } if (sp.duration_ms != null) { parts.push(`
Duration${escapeHTML(formatDuration(sp.duration_ms))}
`); } return parts.length > 0 ? parts.join('') : 'No payload data'; } function renderTimescale(totalMS) { const ticks = 5; return Array.from({ length: ticks + 1 }, (_, i) => { const pct = (i / ticks * 100).toFixed(0); return `${escapeHTML(formatDuration(totalMS * i / ticks))}`; }).join(''); } function renderSpanWaterfall(spans, runStartedAt, runDurationMS) { if (!spans || spans.length === 0) return '

No spans

'; const runStart = new Date(runStartedAt).getTime(); const totalMS = runDurationMS || Math.max(...spans.map(sp => { const s = new Date(sp.started_at || runStartedAt).getTime(); return (s - runStart) + (sp.duration_ms || 0); }), 1); return `
Span
${renderTimescale(totalMS)}
${spans.map(sp => { const spStart = sp.started_at ? new Date(sp.started_at).getTime() - runStart : 0; const spDur = sp.duration_ms || 0; const leftPct = Math.max(0, (spStart / totalMS * 100)).toFixed(2); const widthPct = Math.max(0.5, (spDur / totalMS * 100)).toFixed(2); const kindClass = sp.kind || 'unknown'; const statusClass = sp.status === 'error' ? ' wf-error' : sp.status === 'success' ? ' wf-success' : ''; return `
${escapeHTML(sp.kind || '?')} ${escapeHTML((sp.name || '(unnamed)').slice(0, 40))}
${spDur > totalMS * 0.05 ? escapeHTML(formatDuration(spDur)) : ''}
`; }).join('')}
`; } function renderRunSpansRows(spans) { if (!spans || spans.length === 0) { return 'No spans'; } return spans.map((sp, i) => { const kindClass = sp.kind || 'unknown'; return ` ${escapeHTML(sp.kind || '?')} ${escapeHTML(sp.name || '(unnamed)')} ${escapeHTML(sp.kind || '-')} ${statusIcon(sp.status)} ${escapeHTML(formatDuration(sp.duration_ms))}
${renderSpanPayload(sp)}
`; }).join(''); } function renderRunSpansTable(spans) { return `
${renderRunSpansRows(spans)}
Name Kind Status Duration
`; } function captureOpenSpanIndices() { const openIndices = new Set(); document.querySelectorAll('tr.span-detail-row').forEach(row => { if (row.style.display !== 'none') openIndices.add(row.dataset.index); }); return openIndices; } function restoreOpenSpanIndices(openIndices) { if (!openIndices || openIndices.size === 0) return; document.querySelectorAll('tr.span-detail-row').forEach(row => { if (!openIndices.has(row.dataset.index)) return; row.style.display = 'table-row'; const hdr = document.querySelector(`tr.run-span-row[data-index="${row.dataset.index}"]`); const icon = hdr?.querySelector('.expand-icon'); if (icon) icon.style.transform = 'rotate(45deg)'; }); } function updateSpanViewButtons() { document.getElementById('spans-view-table')?.classList.toggle('active', runSpansViewMode === 'table'); document.getElementById('spans-view-waterfall')?.classList.toggle('active', runSpansViewMode === 'waterfall'); } function renderSpansView(spans, run, openIndices) { const container = document.getElementById('spans-container'); if (!container) return; if (runSpansViewMode === 'waterfall') { container.innerHTML = renderSpanWaterfall( spans, run.started_at, run.ended_at ? new Date(run.ended_at) - new Date(run.started_at) : null, ); } else { container.innerHTML = renderRunSpansTable(spans); bindRunSpanRows(); restoreOpenSpanIndices(openIndices); } updateSpanViewButtons(); } function bindSpanViewToggles(spans, run) { const tableBtn = document.getElementById('spans-view-table'); const waterfallBtn = document.getElementById('spans-view-waterfall'); if (tableBtn) { tableBtn.onclick = () => { runSpansViewMode = 'table'; renderSpansView(spans, run, captureOpenSpanIndices()); }; } if (waterfallBtn) { waterfallBtn.onclick = () => { runSpansViewMode = 'waterfall'; renderSpansView(spans, run, captureOpenSpanIndices()); }; } } function bindRunSpanRows() { document.querySelectorAll('tr.run-span-row').forEach(row => { row.addEventListener('click', () => { const idx = row.dataset.index; const detailRow = document.querySelector(`tr.span-detail-row[data-index="${idx}"]`); const icon = row.querySelector('.expand-icon'); if (!detailRow) return; const isOpen = detailRow.style.display !== 'none'; detailRow.style.display = isOpen ? 'none' : 'table-row'; if (icon) icon.style.transform = isOpen ? '' : 'rotate(45deg)'; }); row.setAttribute('tabindex', '0'); row.setAttribute('role', 'button'); row.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); row.click(); } }); }); } function renderRunLiveOps() { const el = document.getElementById('run-live-ops'); if (!el) return; const ops = Object.values(runLiveOps); if (ops.length === 0) { el.innerHTML = ''; return; } el.innerHTML = `
${ops.map(op => { const elapsed = Math.floor((Date.now() - op.startedAt) / 1000); const isSubagent = op.kind === 'agent' || op.subType === 'subagent'; const icon = isSubagent ? '◎' : op.kind === 'run' ? '◌' : '▸'; const label = isSubagent ? 'subagent' : op.kind === 'run' ? 'thinking' : 'tool'; const preview = op.promptPreview || op.inputPreview || ''; return `
${icon} ${escapeHTML(op.name)} ${preview ? `${escapeHTML(preview.length > 60 ? preview.slice(0, 60) + '…' : preview)}` : ''} ${formatElapsed(elapsed)}
`; }).join('')}
`; } function handleRunWS(runID, msg) { if (msg.type !== 'message') return; const correlation = getEnvelopeCorrelation(msg.data); if (correlation?.run_id !== runID) return; // Track live ops from WS without full reload const eventType = getEnvelopeType(msg.data); const attrs = getEnvelopeAttributes(msg.data); const payload = getEnvelopePayload(msg.data); const spanID = correlation.span_id; if (eventType === 'span.start' && spanID) { runLiveOps[spanID] = { name: attrs.name || attrs.span_kind || 'span', kind: attrs.span_kind || '', subType: attrs.type || '', startedAt: Date.now(), promptPreview: payload.prompt_preview || '', inputPreview: payload.input ? (typeof payload.input === 'string' ? payload.input.slice(0, 100) : '') : '', }; renderRunLiveOps(); } if (eventType === 'span.end' && spanID) { delete runLiveOps[spanID]; renderRunLiveOps(); } if (eventType === 'run.start') { runLiveOps['__run__'] = { name: 'Thinking…', kind: 'run', startedAt: Date.now(), promptPreview: payload.prompt_preview || payload.message_preview || payload.message || '', inputPreview: '', }; renderRunLiveOps(); } if (eventType === 'run.end') { delete runLiveOps['__run__']; runLiveOps = {}; renderRunLiveOps(); } clearTimeout(_runReloadTimer); _runReloadTimer = setTimeout(() => loadRunDetailData(runID), 500); } async function loadRunDetailData(runID) { if (!isCurrentPath('/runs/' + runID)) return; try { const data = await api('/v1/runs/' + runID); const spans = data.spans || []; const r = data.run; const openIndices = captureOpenSpanIndices(); renderSpansView(spans, r, openIndices); bindSpanViewToggles(spans, r); const countEl = document.getElementById('run-detail-span-count'); if (countEl) countEl.textContent = spans.length; if (r.ended_at) { const durEl = document.getElementById('run-detail-duration'); if (durEl) durEl.textContent = formatDuration(new Date(r.ended_at) - new Date(r.started_at)); if (runDetailUnsubscribe) { runDetailUnsubscribe(); runDetailUnsubscribe = null; } const liveSpan = document.querySelector('.section-title .live-indicator'); if (liveSpan) liveSpan.remove(); } } catch (e) { console.error('Failed to reload run detail:', e); } } export async function renderRun(runID, routeToken) { app.innerHTML = '
' + '
' + skeletonRows(5, 4) + '
NameKindStatusDuration
'; runLiveOps = {}; runSpansViewMode = 'table'; let data; try { data = await api('/v1/runs/' + runID); } catch (e) { if (routeToken && !isRouteCurrent(routeToken)) return; app.innerHTML = `

Error loading run: ${escapeHTML(e.message)}

`; return; } if (routeToken && !isRouteCurrent(routeToken)) return; const r = data.run; const spans = data.spans || []; const duration = r.ended_at ? formatDuration(new Date(r.ended_at) - new Date(r.started_at)) : 'ongoing'; const runUsage = extractRunUsage(spans); const promptPreview = extractPromptPreview(spans); app.innerHTML = ` ← Back to Session ${!r.ended_at ? '
' : ''} ${promptPreview ? `
Prompt
${escapeHTML(promptPreview)}
` : ''}
Spans ${spans.length} ${!r.ended_at ? 'Live' : ''}
${renderRunSpansTable(spans)}
`; bindRunSpanRows(); bindSpanViewToggles(spans, r); document.querySelector('.back-link').addEventListener('click', e => { e.preventDefault(); navigate('/sessions/' + r.session_id); }); if (!r.ended_at) { runDetailUnsubscribe = subscribeWS((msg) => handleRunWS(runID, msg)); } }