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 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 `
${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 `
| Name |
Kind |
Status |
Duration |
${renderRunSpansRows(spans)}
`;
}
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 = '' + '
| Name | Kind | Status | Duration |
' + skeletonRows(5, 4) + '
';
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);
app.innerHTML = `
← Back to Session
${!r.ended_at ? '' : ''}
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));
}
}