import { app, navigate, isRouteCurrent } from '../router.js'; import { api } from '../api.js'; import { escapeHTML, formatDuration, formatTokenCount, formatCost, getEnvelopeCorrelation, getEnvelopeType, isCurrentPath, renderCopyButton, statusIcon, extractRunUsage, } from '../utils.js'; import { subscribeWS } from '../ws.js'; let sessionDetailUnsubscribe = null; let _sessionReloadTimer = null; export function cleanup() { if (sessionDetailUnsubscribe) { sessionDetailUnsubscribe(); sessionDetailUnsubscribe = null; } clearTimeout(_sessionReloadTimer); _sessionReloadTimer = null; } function renderSessionRunsRows(runs) { if (!runs || runs.length === 0) { return 'No runs'; } return runs.map((r, i) => { const runDuration = r.ended_at ? formatDuration(new Date(r.ended_at) - new Date(r.started_at)) : '-'; const modelLabel = r.model ? escapeHTML(r.model.replace(/^claude-/, '')) : '-'; const spans = r.spans || []; const spansHTML = spans.length > 0 ? `
${spans.map(sp => { const body = getSessionSpanSummary(sp); return `
${escapeHTML(sp.name || sp.kind || 'span')} ${escapeHTML(body)}
`; }).join('')}
` : '
No spans yet
'; return ` ${escapeHTML(r.run_id.substring(0, 12))}...${renderCopyButton(r.run_id)} ${statusIcon(r.status)} ${modelLabel} ${r.tool_count || 0} ${r.span_count} ${escapeHTML(runDuration)} ${escapeHTML(new Date(r.started_at).toLocaleTimeString())}
Spans ${spans.length}
${spansHTML}
`; }).join(''); } function getSessionSpanSummary(sp) { const payload = sp.payload || {}; const innerPayload = payload.payload || {}; if (sp.kind === 'tool') { const result = innerPayload.result_preview || ''; const duration = sp.duration_ms !== undefined && sp.duration_ms !== null ? formatDuration(sp.duration_ms) : '-'; return result ? `${duration} · ${String(result).slice(0, 80)}` : duration; } if (sp.kind === 'agent') { const usage = innerPayload.usage || {}; const totalTokens = usage.total_tokens !== undefined ? `${usage.total_tokens} tok` : ''; const duration = sp.duration_ms !== undefined && sp.duration_ms !== null ? formatDuration(sp.duration_ms) : '-'; return totalTokens ? `${duration} · ${totalTokens}` : duration; } return sp.duration_ms !== undefined && sp.duration_ms !== null ? formatDuration(sp.duration_ms) : '-'; } function bindSessionRunRows() { document.querySelectorAll('tr.expandable-run').forEach(row => { row.addEventListener('click', event => { if (event.metaKey || event.ctrlKey) { navigate('/runs/' + row.dataset.run); return; } 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; if (detailRow.style.display === 'none') { detailRow.style.display = 'table-row'; if (icon) icon.style.transform = 'rotate(45deg)'; } else { detailRow.style.display = 'none'; if (icon) icon.style.transform = ''; } }); row.addEventListener('dblclick', () => navigate('/runs/' + row.dataset.run)); row.setAttribute('tabindex', '0'); row.setAttribute('role', 'button'); row.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); row.click(); } }); }); } function handleSessionWS(sessionID, msg) { if (msg.type !== 'message') return; const correlation = getEnvelopeCorrelation(msg.data); if (correlation?.session_id !== sessionID) return; const eventType = getEnvelopeType(msg.data); if (!['run.start', 'run.end', 'span.start', 'span.end', 'session.end', 'error'].includes(eventType)) return; clearTimeout(_sessionReloadTimer); _sessionReloadTimer = setTimeout(() => loadSessionData(sessionID), 300); } async function loadSessionData(sessionID) { if (!isCurrentPath('/sessions/' + sessionID)) return; const data = await api('/v1/sessions/' + sessionID); const runs = data.runs || []; const tbody = document.getElementById('session-runs-body'); if (!tbody) return; tbody.innerHTML = renderSessionRunsRows(runs); bindSessionRunRows(); const countSpan = document.querySelector('.section-title .count'); if (countSpan) countSpan.textContent = runs.length; } export async function renderSession(sessionID, routeToken) { const data = await api('/v1/sessions/' + sessionID); if (routeToken && !isRouteCurrent(routeToken)) return; const s = data.session; const runs = data.runs || []; const active = !s.ended_at; const duration = s.ended_at ? formatDuration(new Date(s.ended_at) - new Date(s.started_at)) : 'ongoing'; // Aggregate token/cost/tool data from runs' spans let sessionTotalTokens = 0, sessionTotalCost = 0, sessionTotalTools = 0; runs.forEach(r => { const usage = extractRunUsage(r.spans || []); if (usage) { sessionTotalTokens += usage.totalTokens; sessionTotalCost += usage.totalCost; } sessionTotalTools += (r.tool_count || 0); }); app.innerHTML = ` ← Back to Sessions
Runs ${runs.length}
${renderSessionRunsRows(runs)}
Run ID Status Model Tools Spans Duration Started
`; bindSessionRunRows(); document.querySelector('.back-link').addEventListener('click', e => { e.preventDefault(); navigate('/sessions'); }); sessionDetailUnsubscribe = subscribeWS((msg) => handleSessionWS(sessionID, msg)); }