- return Array(8).fill(0).map((_, i) => {
- const widths = [['55%','25%'], ['65%','20%'], ['45%','30%'], ['70%','15%'], ['50%','22%'], ['60%','18%'], ['42%','28%'], ['68%','12%']];
- const [w1, w2] = widths[i % widths.length];
- return `
- |
- |
- |
- |
- |
-
`;
- }).join('');
- }
-
- function agentsSkeleton() {
- return `
- ${Array(4).fill('
').join('')}
-
`;
- }
-
- function infrastructureSkeleton() {
- return `
- ${Array(6).fill('
').join('')}
-
`;
- }
-
- function extractEnvelope(record) {
- if (record && typeof record === 'object' && record.payload && record.payload.event && record.payload.schema) {
- return record.payload;
- }
- return record || {};
- }
-
- function getEnvelopeEvent(record) {
- const envelope = extractEnvelope(record);
- return envelope.event || envelope.Event || {};
- }
-
- function getEnvelopeType(record) {
- return record?.type || getEnvelopeEvent(record).type || '';
- }
-
- function getEnvelopeTS(record) {
- return record?.ts || getEnvelopeEvent(record).ts || '';
- }
-
- function getEnvelopeSource(record) {
- return getEnvelopeEvent(record).source || {};
- }
-
- function getEnvelopePayload(record) {
- const envelope = extractEnvelope(record);
- return envelope.payload || envelope.Payload || {};
- }
-
- function getEnvelopeAttributes(record) {
- const envelope = extractEnvelope(record);
- return envelope.attributes || envelope.Attributes || {};
- }
-
- function getEnvelopeCorrelation(record) {
- const envelope = extractEnvelope(record);
- return envelope.correlation || envelope.Correlation || {};
- }
-
- function getRecordID(record) {
- return record?.event_id || getEnvelopeEvent(record).id || '';
- }
-
- function isCurrentPath(prefix) {
- return window.location.pathname.startsWith(prefix);
- }
-
- function groupSessionsByDate(sessions) {
- const now = new Date();
- const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
- const yesterdayStart = new Date(todayStart);
- yesterdayStart.setDate(yesterdayStart.getDate() - 1);
- const weekStart = new Date(todayStart);
- weekStart.setDate(weekStart.getDate() - 6);
- const groups = [
- { label: 'Today', items: [] },
- { label: 'Yesterday', items: [] },
- { label: 'This Week', items: [] },
- { label: 'Older', items: [] },
- ];
- for (const s of sessions) {
- const d = new Date(s.started_at);
- if (d >= todayStart) groups[0].items.push(s);
- else if (d >= yesterdayStart) groups[1].items.push(s);
- else if (d >= weekStart) groups[2].items.push(s);
- else groups[3].items.push(s);
- }
- return groups.filter(g => g.items.length > 0);
- }
-
- function getSessionBackendKey(s) {
- const framework = s.framework || 'unknown';
- const backendID = s.client_id || s.host || 'unknown';
- return `${framework}|${backendID}`;
- }
-
- function sessionActivityTS(s) {
- const raw = s._lastActivityTS || Date.parse(s.started_at);
- return Number.isFinite(raw) ? raw : 0;
- }
-
- function recomputeActiveSessionByBackend() {
- const next = {};
- const bestTS = {};
- sessionsState.sessions.forEach(s => {
- if (!isSessionActive(s)) return;
- const key = getSessionBackendKey(s);
- const ts = sessionActivityTS(s);
- if (!next[key] || ts > bestTS[key]) {
- next[key] = s.session_id;
- bestTS[key] = ts;
- }
- });
- sessionsState.activeSessionByBackend = next;
- }
-
- function sessionDotState(s) {
- if (!isSessionActive(s)) return 'ended';
- const key = getSessionBackendKey(s);
- const activeSessionID = sessionsState.activeSessionByBackend[key];
- return activeSessionID === s.session_id ? 'active' : 'idle';
- }
-
- function touchSessionActivity(sessionID, ts, source) {
- const session = sessionsState.sessions.find(s => s.session_id === sessionID);
- if (!session) return null;
-
- const parsedTS = Date.parse(ts || '');
- const activityTS = Number.isFinite(parsedTS) ? parsedTS : Date.now();
- session._lastActivityTS = Math.max(session._lastActivityTS || 0, activityTS);
-
- if (source && typeof source === 'object') {
- if (source.framework) session.framework = source.framework;
- if (source.host) session.host = source.host;
- if (source.client_id) session.client_id = source.client_id;
- }
-
- const key = getSessionBackendKey(session);
- sessionsState.activeSessionByBackend[key] = session.session_id;
- return session;
- }
-
- function refreshSessionsTable() {
- const tbody = document.getElementById('sessions-body');
- if (!tbody) return;
-
- // Update pill counts based on full unfiltered sessions list
- const all = sessionsState.sessions;
- const activeCount = all.filter(s => isSessionActive(s)).length;
- const endedCount = all.filter(s => !isSessionActive(s)).length;
- const erroredCount = all.filter(s => (s._errorCount || 0) > 0).length;
- const pillDefs = [
- { filter: 'all', count: all.length },
- { filter: 'active', count: activeCount },
- { filter: 'ended', count: endedCount },
- { filter: 'errored', count: erroredCount },
- ];
- pillDefs.forEach(({ filter, count }) => {
- const btn = document.querySelector(`#session-pills [data-filter="${filter}"]`);
- if (!btn) return;
- let countEl = btn.querySelector('.pill-count');
- if (!countEl) {
- countEl = document.createElement('span');
- countEl.className = 'pill-count';
- btn.appendChild(countEl);
- }
- countEl.textContent = count;
- });
-
- // Apply filter
- let filtered = sessionsState.sessions;
- if (sessionFilterMode === 'active') {
- filtered = filtered.filter(s => isSessionActive(s));
- } else if (sessionFilterMode === 'ended') {
- filtered = filtered.filter(s => !isSessionActive(s));
- } else if (sessionFilterMode === 'errored') {
- filtered = filtered.filter(s => (s._errorCount || 0) > 0);
- }
-
- const groups = groupSessionsByDate(filtered);
- if (groups.length === 0) {
- tbody.innerHTML = '| No sessions found |
';
- return;
- }
- const allFiltered = groups.flatMap(g => g.items);
- const maxDuration = Math.max(...allFiltered.map(s => {
- const start = new Date(s.started_at).getTime();
- const end = s.ended_at ? new Date(s.ended_at).getTime() : Date.now();
- return end - start;
- }), 1);
- tbody.innerHTML = groups.map(group => {
- const rows = group.items.map(s => {
- const fw = s.framework || 'unknown';
- const fwClass = fw.replace(/[^a-z0-9-]/g, '-');
- const active = isSessionActive(s);
- const dotState = sessionDotState(s);
- const dotTitle = dotState === 'active'
- ? 'Currently active session'
- : (active ? 'Open session' : 'Session ended');
- const rowClass = active ? 'clickable active-session' : 'clickable';
- const start = new Date(s.started_at).getTime();
- const end = s.ended_at ? new Date(s.ended_at).getTime() : Date.now();
- const duration = end - start;
- const barWidth = Math.max(4, (duration / maxDuration) * 80);
- const durationBar = ``;
- return `
-
- | ${escapeHTML(s.session_id.substring(0, 12))}…${renderCopyButton(s.session_id)} |
- ${escapeHTML(fw)} |
- ${escapeHTML(s.host || '-')} |
- ${s.run_count} |
- ${escapeHTML(relativeTime(s.started_at))}${durationBar} |
-
`;
- }).join('');
- return `| ${escapeHTML(group.label)} |
${rows}`;
- }).join('');
- tbody.querySelectorAll('tr.clickable').forEach(row => {
- row.addEventListener('click', () => navigate('/sessions/' + row.dataset.session));
- });
- }
-
- async function renderSessions() {
- // Reset filter mode on each page visit
- sessionFilterMode = 'all';
-
- app.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- | Session |
- Framework |
- Host |
- Runs |
- Time |
-
-
- ${sessionsSkeleton()}
-
-
-
- `;
-
- // Wire up filter pill click handlers
- document.querySelectorAll('#session-pills .filter-pill').forEach(btn => {
- btn.addEventListener('click', () => {
- document.querySelectorAll('#session-pills .filter-pill').forEach(b => b.classList.remove('active'));
- btn.classList.add('active');
- sessionFilterMode = btn.dataset.filter;
- refreshSessionsTable();
- });
- });
-
- api('/v1/stats/summary').then(data => {
- const sel = document.getElementById('filter-framework');
- if (!sel || !data.by_framework) return;
- for (const fw of Object.keys(data.by_framework).sort()) {
- const opt = document.createElement('option');
- opt.value = fw;
- opt.textContent = fw;
- sel.appendChild(opt);
- }
- }).catch(() => {});
-
- // Restore filters from URL
- const urlParams = new URLSearchParams(window.location.search);
- if (urlParams.get('from')) document.getElementById('filter-from').value = urlParams.get('from');
- if (urlParams.get('to')) document.getElementById('filter-to').value = urlParams.get('to');
- if (urlParams.get('framework')) document.getElementById('filter-framework').value = urlParams.get('framework');
- if (urlParams.get('host')) document.getElementById('filter-host').value = urlParams.get('host');
-
- ['from', 'to', 'framework'].forEach(f => {
- document.getElementById('filter-' + f).addEventListener('change', () => {
- sessionsState.sessions = [];
- sessionsState.cursor = null;
- loadSessions();
- });
- });
- let _hostDebounce = null;
- document.getElementById('filter-host').addEventListener('input', () => {
- clearTimeout(_hostDebounce);
- _hostDebounce = setTimeout(() => {
- sessionsState.sessions = [];
- sessionsState.cursor = null;
- loadSessions();
- }, 400);
- });
-
- document.getElementById('load-more').addEventListener('click', loadSessions);
-
- sessionsState = { sessions: [], cursor: null, timerInterval: null, activeSessionByBackend: {} };
- await loadSessions();
-
- sessionsState.timerInterval = setInterval(updateSessionTimers, 30000);
- sessionsUnsubscribe = subscribeWS(handleSessionsWS);
- }
-
- function isSessionActive(s) { return !s.ended_at; }
-
- function renderSessionRow(s) {
- const fw = s.framework || 'unknown';
- const fwClass = fw.replace(/[^a-z0-9-]/g, '-');
- const active = isSessionActive(s);
- const dotState = sessionDotState(s);
- const dotTitle = dotState === 'active'
- ? 'Currently active session'
- : (active ? 'Open session' : 'Session ended');
- return `
- ${escapeHTML(s.session_id.substring(0, 12))}…${renderCopyButton(s.session_id)} |
- ${escapeHTML(fw)} |
- ${escapeHTML(s.host || '-')} |
- ${s.run_count} |
- ${escapeHTML(relativeTime(s.started_at))} |
- `;
- }
-
- function updateSessionTimers() {
- const tbody = document.getElementById('sessions-body');
- if (!tbody) return;
- sessionsState.sessions.forEach(s => {
- const row = tbody.querySelector(`[data-session="${s.session_id}"]`);
- if (row) {
- const td = row.cells[4];
- if (td) {
- // Update only the text node, preserving the duration bar span
- const bar = td.querySelector('.session-duration-bar');
- td.textContent = relativeTime(s.started_at);
- if (bar) td.appendChild(bar);
- }
- }
- });
- }
-
- function handleSessionsWS(msg) {
- if (msg.type !== 'message') return;
- const eventType = getEnvelopeType(msg.data);
- const correlation = getEnvelopeCorrelation(msg.data);
- const source = getEnvelopeSource(msg.data);
- const ts = getEnvelopeTS(msg.data);
- const sessionId = correlation?.session_id || msg.data.event?.id;
-
- if (eventType === 'session.start') {
- const newSession = {
- session_id: sessionId,
- started_at: ts || new Date().toISOString(),
- framework: source.framework || 'unknown',
- client_id: source.client_id || '',
- host: source.host || '-',
- run_count: 1,
- _lastActivityTS: Date.parse(ts || '') || Date.now(),
- };
- sessionsState.sessions.unshift(newSession);
- const backendKey = getSessionBackendKey(newSession);
- sessionsState.activeSessionByBackend[backendKey] = newSession.session_id;
- refreshSessionsTable();
- return;
- }
-
- const tbody = document.getElementById('sessions-body');
- if (!tbody) return;
-
- if (sessionId) {
- touchSessionActivity(sessionId, ts, source);
- }
-
- if (eventType === 'run.start' && sessionId) {
- const session = sessionsState.sessions.find(s => s.session_id === sessionId);
- if (session) {
- session.run_count = (session.run_count || 0) + 1;
- const row = tbody.querySelector(`[data-session="${sessionId}"]`);
- if (row && row.cells[3]) row.cells[3].textContent = session.run_count;
- }
- }
-
- if (eventType === 'session.end' && sessionId) {
- const session = sessionsState.sessions.find(s => s.session_id === sessionId);
- if (session) {
- session.ended_at = new Date().toISOString();
- recomputeActiveSessionByBackend();
- }
- }
-
- if (eventType === 'error' && sessionId) {
- const session = sessionsState.sessions.find(s => s.session_id === sessionId);
- if (session) {
- session._errorCount = (session._errorCount || 0) + 1;
- }
- }
-
- refreshSessionsTable();
- }
-
- async function loadSessions() {
- const params = new URLSearchParams();
- const from = document.getElementById('filter-from').value;
- const to = document.getElementById('filter-to').value;
- const framework = document.getElementById('filter-framework').value;
- const host = document.getElementById('filter-host').value;
-
- // Sync filters to URL
- const filterParams = new URLSearchParams();
- if (from) filterParams.set('from', from);
- if (to) filterParams.set('to', to);
- if (framework) filterParams.set('framework', framework);
- if (host) filterParams.set('host', host);
- const filterQS = filterParams.toString();
- const newURL = '/sessions' + (filterQS ? '?' + filterQS : '');
- if (window.location.pathname + window.location.search !== newURL) {
- history.replaceState(null, '', newURL);
- }
-
- if (from) params.set('from', from);
- if (to) params.set('to', to);
- if (framework) params.set('framework', framework);
- if (host) params.set('host', host);
- if (sessionsState.cursor) params.set('cursor', sessionsState.cursor);
-
- const data = await api('/v1/sessions?' + params.toString());
- const incoming = (data.sessions || []).map(s => ({
- ...s,
- _lastActivityTS: Date.parse(s.started_at || '') || Date.now(),
- }));
- sessionsState.sessions = sessionsState.sessions.concat(incoming);
- sessionsState.cursor = data.next_cursor;
- recomputeActiveSessionByBackend();
-
- refreshSessionsTable();
- document.getElementById('load-more').style.display = sessionsState.cursor ? 'block' : 'none';
- }
-
- async function renderSession(sessionID) {
- const data = await api('/v1/sessions/' + sessionID);
- 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';
-
- app.innerHTML = `
- ← Back to Sessions
-
- Runs ${runs.length}
-
-
-
-
- | Run ID |
- Status |
- Model |
- Tools |
- Spans |
- Duration |
- Started |
-
-
-
- ${renderSessionRunsRows(runs)}
-
-
-
- `;
-
- bindSessionRunRows();
-
- document.querySelector('.back-link').addEventListener('click', e => {
+ // Cmd+K or Ctrl+K — command palette
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
- navigate('/sessions');
- });
-
- sessionsUnsubscribe = subscribeWS((msg) => handleSessionWS(sessionID, msg));
- }
-
- let _sessionReloadTimer = null;
- 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;
- }
-
- 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 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 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();
- }
- });
- });
- }
-
- // Track active spans for the run detail live ops panel
- let runLiveOps = {}; // spanID → { name, kind, startedAt, promptPreview, inputPreview }
- let _runReloadTimer = null;
- let _dashFeedRenderTimer = null;
-
- async function renderRun(runID) {
- app.innerHTML = '' + '
| Name | Kind | Status | Duration |
' + skeletonRows(5, 4) + '
';
- runLiveOps = {};
- let data;
- try {
- data = await api('/v1/runs/' + runID);
- } catch (e) {
- app.innerHTML = `Error loading run: ${escapeHTML(e.message)}
`;
+ if (isCommandPaletteOpen()) closeCommandPalette();
+ else openCommandPalette();
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';
-
- app.innerHTML = `
- ← Back to Session
-
- ${!r.ended_at ? '' : ''}
-
- Spans ${spans.length}
- ${!r.ended_at ? 'Live' : ''}
-
-
-
-
-
- | Name |
- Kind |
- Status |
- Duration |
-
-
-
- ${renderRunSpansRows(spans)}
-
-
-
- `;
-
- bindRunSpanRows();
-
- document.querySelector('.back-link').addEventListener('click', e => {
+ // '/' to focus search
+ if (e.key === '/' && !isCommandPaletteOpen()) {
e.preventDefault();
- navigate('/sessions/' + r.session_id);
- });
-
- if (!r.ended_at) {
- sessionsUnsubscribe = subscribeWS((msg) => handleRunWS(runID, msg));
- }
- }
-
- 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 tbody = document.getElementById('spans-body');
- if (!tbody) return;
-
- // Preserve expanded rows
- const openIndices = new Set();
- document.querySelectorAll('tr.span-detail-row').forEach(row => {
- if (row.style.display !== 'none') openIndices.add(row.dataset.index);
- });
-
- tbody.innerHTML = renderRunSpansRows(spans);
-
- // Restore expanded rows
- document.querySelectorAll('tr.span-detail-row').forEach(row => {
- if (openIndices.has(row.dataset.index)) {
- row.style.display = 'table-row';
- const hdr = document.querySelector(`tr.run-span-row[data-index="${row.dataset.index}"]`);
- if (hdr) {
- const icon = hdr.querySelector('.expand-icon');
- if (icon) icon.style.transform = 'rotate(45deg)';
- }
- }
- });
-
- bindRunSpanRows();
-
- 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 (sessionsUnsubscribe) { sessionsUnsubscribe(); sessionsUnsubscribe = null; }
- const liveSpan = document.querySelector('.section-title .live-indicator');
- if (liveSpan) liveSpan.remove();
- }
- } catch (e) {
- console.error('Failed to reload run detail:', e);
- }
- }
-
- 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();
- }
- });
- });
- }
-
- async function renderInfrastructure() {
- app.innerHTML = `${infrastructureSkeleton()}
`;
-
- infraUnsubscribe = subscribeWS(handleInfraWS);
-
- try {
- const [ocData, swarmData] = await Promise.all([
- api('/v1/events?event_type=openclaw.snapshot&limit=100'),
- api('/v1/events?event_type=swarm.snapshot&limit=10').catch(() => ({ events: [] })),
- ]);
-
- mergeOpenClawEvents(ocData.events || []);
- for (const evt of swarmData.events || []) mergeSwarmSnapshot(evt);
-
- if (isCurrentPath('/infrastructure')) {
- renderInfraGrid();
- }
- } catch (e) {
- if (isCurrentPath('/infrastructure')) {
- app.innerHTML = `Error: ${escapeHTML(e.message)}
`;
- }
- }
- }
-
- function handleInfraWS(msg) {
- if (msg.type !== 'message') return;
-
- const eventType = getEnvelopeType(msg.data);
-
- if (eventType === 'openclaw.snapshot') {
- mergeOpenClawEvents([msg.data]);
- if (isCurrentPath('/infrastructure')) renderInfraGrid();
- if (isCurrentPath('/agents')) renderAgentVMStrip();
+ const si = document.getElementById('global-search');
+ if (si) si.focus();
return;
}
- if (eventType === 'swarm.snapshot') {
- mergeSwarmSnapshot(msg.data);
- if (isCurrentPath('/infrastructure')) renderInfraGrid();
+ // Escape closes palette
+ if (e.key === 'Escape' && isCommandPaletteOpen()) {
+ closeCommandPalette();
return;
}
- if (eventType === 'swarm.service.snapshot') {
- mergeSwarmServiceSnapshot(msg.data);
- if (isCurrentPath('/infrastructure')) renderInfraGrid();
- return;
- }
- }
-
- function mergeOpenClawEvents(events) {
- for (const evt of events) {
- const payload = getEnvelopePayload(evt);
- const instance = payload.instance || {};
- if (!instance.name) {
- continue;
- }
-
- const existing = openclawState.instances[instance.name];
- const nextTS = new Date(getEnvelopeTS(evt) || 0).getTime();
- const currentTS = existing ? new Date(getEnvelopeTS(existing) || 0).getTime() : 0;
- if (!existing || nextTS >= currentTS) {
- openclawState.instances[instance.name] = evt;
- }
- }
- }
-
- function mergeSwarmSnapshot(evt) {
- const payload = getEnvelopePayload(evt);
- const services = payload.services || [];
- for (const svc of services) {
- if (svc.name) swarmState.services[svc.name] = svc;
- }
- }
-
- function mergeSwarmServiceSnapshot(evt) {
- const payload = getEnvelopePayload(evt);
- const svc = payload.service;
- if (svc && svc.name) swarmState.services[svc.name] = svc;
- }
-
- function renderInfraGrid() {
- const vmNames = Object.keys(openclawState.instances).sort();
- const allServices = Object.values(swarmState.services);
- const agentmonServices = allServices.filter(s => s.group === 'agentmon');
- const swarmServices = allServices.filter(s => s.group !== 'agentmon');
- const homelabServices = getK8sHomelabServices();
-
- app.innerHTML = `
-
-
-
-
VMs
- ${vmNames.length === 0
- ? '
No VM data
'
- : `
${vmNames.map(name => renderVMCard(name)).join('')}
`
- }
-
-
-
-
Swarm Services
- ${swarmServices.length === 0
- ? '
No swarm service data
'
- : `
${swarmServices.map(svc => renderServiceCard(svc)).join('')}
`
- }
-
-
-
-
K8s Homelab
- ${homelabServices.length === 0
- ? '
No k8s homelab service data
'
- : `
${homelabServices.map(svc => renderHomelabServiceCard(svc)).join('')}
`
- }
-
-
-
-
Agentmon
- ${agentmonServices.length === 0
- ? '
No agentmon service data
'
- : `
${agentmonServices.map(svc => renderServiceCard(svc)).join('')}
`
- }
-
- `;
-
- // Start freshness timer — update "Updated X ago" text every 10s
- if (_infraTimerInterval) clearInterval(_infraTimerInterval);
- _infraTimerInterval = setInterval(() => {
- document.querySelectorAll('.freshness-timer[data-ts]').forEach(el => {
- el.textContent = 'Updated ' + relativeTime(el.dataset.ts);
- });
- }, 10000);
- }
-
- function renderVMCard(name) {
- const evt = openclawState.instances[name];
- const payload = getEnvelopePayload(evt);
- const inst = payload.instance || {};
- const host = payload.host || {};
- const guest = payload.guest;
- const issues = payload.issues;
-
- return `
-
-
-
Updated ${escapeHTML(relativeTime(getEnvelopeTS(evt)))}
-
- | Host | ${escapeHTML(inst.host || '-')} |
- | Domain | ${escapeHTML(inst.domain || '-')} |
- | vCPUs | ${host.vcpus || '-'} |
- | Memory | ${escapeHTML(formatBytes(host.memory_kib ? host.memory_kib * 1024 : 0) || '-')} |
- | Disk | ${escapeHTML(formatBytes(host.disk_actual_bytes) || '-')} |
- | Autostart | ${host.autostart ? 'Yes' : 'No'} |
-
- ${guest ? `
-
-
- | Gateway | ${guest.service_active ? 'Active' : 'Inactive'} |
- | HTTP | ${guest.http_status || 'N/A'} |
- | Version | ${escapeHTML(guest.version || '-')} |
- | Guest Mem | ${guest.memory_percent !== undefined ? guest.memory_percent.toFixed(1) : '-'}% |
- | Guest Disk | ${guest.disk_percent !== undefined ? guest.disk_percent.toFixed(1) : '-'}% |
- | Load | ${guest.load_average !== undefined ? guest.load_average.toFixed(2) : '-'} |
- | Uptime | ${escapeHTML(guest.service_uptime || '-')} |
-
- ` : ''}
- ${issues && Object.values(issues).some(Boolean) ? `
-
-
Issues
-
- ${Object.entries(issues).filter(([, value]) => value).map(([key]) => `
- ${escapeHTML(key.replace(/_/g, ' '))}
- `).join('')}
-
- ` : ''}
-
- `;
- }
-
- function renderServiceCard(svc) {
- const role = svc.role || 'unknown';
- switch (role) {
- case 'llm-proxy': return renderLLMProxyCard(svc);
- case 'db': return renderDBCard(svc);
- case 'search': return renderSearchCard(svc);
- case 'mcp': return renderMCPCard(svc);
- case 'voice': return renderVoiceCard(svc);
- case 'automation':return renderAutomationCard(svc);
- case 'api':
- case 'web': return renderAPICard(svc);
- case 'worker':
- case 'queue': return renderWorkerCard(svc);
- default: return renderGenericServiceCard(svc);
- }
- }
-
- function serviceCardHeader(svc) {
- const uptimeBadge = getUptimeBadge(svc.uptime_sec);
- return `
-
- `;
- }
-
- function serviceStatRow(label, value, valueClass) {
- return `
-
- ${escapeHTML(label)}
- ${value}
-
- `;
- }
-
- // ── Infrastructure Uptime & Freshness ───────────────────
- function getUptimeBadge(uptimeSec) {
- if (!uptimeSec) return '';
- const hours = uptimeSec / 3600;
- const pct = Math.min(100, (hours / 24) * 100);
- const cls = pct >= 99 ? 'good' : pct >= 90 ? 'warn' : 'bad';
- return `${pct.toFixed(0)}% / 24h`;
- }
-
- function formatUptime(sec) {
- if (!sec) return '-';
- if (sec < 60) return sec + 's';
- if (sec < 3600) return Math.floor(sec / 60) + 'm';
- if (sec < 86400) return Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm';
- return Math.floor(sec / 86400) + 'd ' + Math.floor((sec % 86400) / 3600) + 'h';
- }
-
- function renderLLMProxyCard(svc) {
- const extra = svc.extra || {};
- const modelCount = extra.model_count;
- const cooldowns = extra.cooldown_count || 0;
- const httpStatus = svc.http_status;
- const httpClass = httpStatus === 200 ? 'ok' : httpStatus ? 'bad' : '';
-
- return `
-
- ${serviceCardHeader(svc)}
-
- ${modelCount !== undefined ? modelCount : '-'}
- models
-
- ${cooldowns > 0 ? `
⚠ ${cooldowns} model${cooldowns > 1 ? 's' : ''} in cooldown
` : ''}
-
- ${serviceStatRow('HTTP', httpStatus ? String(httpStatus) : '-', httpClass)}
- ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
- ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')}
-
-
- `;
- }
-
- function renderDBCard(svc) {
- const healthClass = svc.health_state === 'healthy' ? 'ok' : svc.health_state === 'unhealthy' ? 'bad' : '';
- return `
-
- ${serviceCardHeader(svc)}
-
- ${serviceStatRow('Health', escapeHTML(svc.health_state || 'none'), healthClass)}
- ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
- ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')}
-
-
- `;
- }
-
- function renderSearchCard(svc) {
- const extra = svc.extra || {};
- const ms = extra.response_ms;
- const httpStatus = svc.http_status;
- const httpClass = httpStatus === 200 ? 'ok' : httpStatus ? 'bad' : '';
- return `
-
- ${serviceCardHeader(svc)}
-
- ${serviceStatRow('HTTP', httpStatus ? String(httpStatus) : '-', httpClass)}
- ${ms !== undefined ? serviceStatRow('Response', ms + 'ms', ms < 500 ? 'ok' : 'warn') : ''}
- ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
-
-
- `;
- }
-
- function renderMCPCard(svc) {
- const extra = svc.extra || {};
- const reachable = extra.port_reachable;
- return `
-
- ${serviceCardHeader(svc)}
-
- ${reachable !== undefined ? serviceStatRow('Port', reachable ? 'reachable' : 'unreachable', reachable ? 'ok' : 'bad') : ''}
- ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')}
- ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
-
-
- `;
- }
-
- function renderVoiceCard(svc) {
- const healthClass = svc.health_state === 'healthy' ? 'ok' : svc.health_state === 'unhealthy' ? 'bad' : '';
- return `
-
- ${serviceCardHeader(svc)}
-
- ${serviceStatRow('Health', escapeHTML(svc.health_state || 'none'), healthClass)}
- ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')}
- ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
-
-
- `;
- }
-
- function renderAutomationCard(svc) {
- const healthClass = svc.health_state === 'healthy' ? 'ok' : svc.health_state === 'unhealthy' ? 'bad' : '';
- return `
-
- ${serviceCardHeader(svc)}
-
- ${serviceStatRow('Health', escapeHTML(svc.health_state || 'none'), healthClass)}
- ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')}
- ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
-
-
- `;
- }
-
- function renderAPICard(svc) {
- const httpStatus = svc.http_status;
- const httpClass = httpStatus === 200 ? 'ok' : httpStatus ? 'bad' : '';
- return `
-
- ${serviceCardHeader(svc)}
-
- ${serviceStatRow('HTTP', httpStatus ? String(httpStatus) : '-', httpClass)}
- ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
- ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')}
-
-
- `;
- }
-
- function renderWorkerCard(svc) {
- return `
-
- ${serviceCardHeader(svc)}
-
- ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')}
- ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
-
-
- `;
- }
-
- function renderGenericServiceCard(svc) {
- return `
-
- ${serviceCardHeader(svc)}
-
- ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')}
- ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
-
-
- `;
- }
-
- function getK8sHomelabServices() {
- const services = [];
- for (const [name, evt] of Object.entries(openclawState.instances)) {
- const payload = getEnvelopePayload(evt);
- if (!payload.minio) continue;
-
- const minio = payload.minio;
- services.push({
- name: 'minio-storage',
- role: 'storage',
- category: 'k8s homelab',
- sourceInstance: name,
- status: minio.reachable ? 'healthy' : 'down',
- endpoint: minio.endpoint || '',
- bucket: minio.bucket || '',
- prefix: minio.prefix || '',
- objectCount: minio.object_count,
- totalBytes: minio.total_bytes,
- latestBackup: minio.latest_backup || '',
- httpStatus: minio.http_status,
- error: minio.error || '',
- });
- }
- return services;
- }
-
- function renderHomelabServiceCard(svc) {
- const httpClass = svc.httpStatus === 200 ? 'ok' : svc.httpStatus ? 'bad' : '';
- return `
-
- ${serviceCardHeader(svc)}
-
- ${serviceStatRow('Endpoint', escapeHTML(svc.endpoint || '-'), '')}
- ${serviceStatRow('Bucket', escapeHTML(svc.bucket ? `${svc.bucket}/${svc.prefix || ''}` : '-'), '')}
- ${serviceStatRow('Usage', escapeHTML(formatBytes(svc.totalBytes) || '-'), '')}
- ${serviceStatRow('Objects', escapeHTML(svc.objectCount !== undefined ? String(svc.objectCount) : '-'), '')}
- ${serviceStatRow('HTTP', svc.httpStatus ? String(svc.httpStatus) : '-', httpClass)}
- ${serviceStatRow('Source', escapeHTML(svc.sourceInstance || '-'), '')}
- ${serviceStatRow('Latest', escapeHTML(svc.latestBackup ? relativeTime(svc.latestBackup) : '-'), '')}
- ${svc.error ? serviceStatRow('Error', escapeHTML(svc.error), 'bad') : ''}
-
-
- `;
- }
-
- function createAgentsState() {
- return {
- agents: {},
- stats: { messages: 0, tools: 0, errors: 0, toolCounts: {} },
- dbStats: { messages: 0, tools: 0, errors: 0 },
- viewMode: 'overview',
- selectedAgentKey: '',
- timerInterval: null,
- };
- }
-
- function getVMStatus() {
- const names = Object.keys(openclawState.instances).sort();
- return names.map(name => {
- const snapshot = openclawState.instances[name];
- const payload = snapshot ? getEnvelopePayload(snapshot) : {};
- const host = payload.host || {};
- return {
- name,
- active: host.state === 'running',
- };
- });
- }
-
- function normalizeAgentKey(value) {
- return String(value || '')
- .trim()
- .toLowerCase()
- .replace(/[^a-z0-9._-]+/g, '-');
- }
-
- function getAgentIdentity(evt) {
- const source = getEnvelopeSource(evt);
- const correlation = getEnvelopeCorrelation(evt);
- const framework = source.framework || evt.source_framework || 'unknown';
- const host = source.host || '';
- const clientID = source.client_id || '';
- const sessionID = correlation.session_id || '';
- const name = clientID || host || framework || sessionID || 'unknown';
- const key = normalizeAgentKey(clientID || host || sessionID || framework || 'unknown');
- return {
- key,
- name,
- framework,
- host,
- clientID,
- sessionID,
- };
- }
-
- function ensureAgentBucket(evt) {
- const identity = getAgentIdentity(evt);
- if (!identity.key) {
- return null;
- }
-
- if (!agentsState.agents[identity.key]) {
- agentsState.agents[identity.key] = {
- key: identity.key,
- name: identity.name,
- framework: identity.framework,
- host: identity.host,
- clientID: identity.clientID,
- sessions: {},
- operations: {},
- events: [],
- eventIDs: new Set(),
- lastSeenAt: 0,
- liveLoaded: false,
- liveLoading: false,
- };
- }
-
- const agent = agentsState.agents[identity.key];
- agent.name = identity.name || agent.name || identity.key;
- agent.framework = identity.framework || agent.framework;
- agent.host = identity.host || agent.host;
- agent.clientID = identity.clientID || agent.clientID;
- return agent;
- }
-
- function getSortedAgentKeys() {
- return Object.keys(agentsState.agents).sort((a, b) => {
- const left = agentsState.agents[a];
- const right = agentsState.agents[b];
- const leftOnline = isAgentOnline(left);
- const rightOnline = isAgentOnline(right);
-
- if (leftOnline !== rightOnline) {
- return leftOnline ? -1 : 1;
- }
- return (left.name || left.key).localeCompare(right.name || right.key);
- });
- }
-
- function ensureSelectedAgentKey() {
- const keys = getSortedAgentKeys();
- if (keys.length === 0) {
- agentsState.selectedAgentKey = '';
- return '';
- }
- if (!agentsState.selectedAgentKey || !agentsState.agents[agentsState.selectedAgentKey]) {
- agentsState.selectedAgentKey = keys[0];
- }
- return agentsState.selectedAgentKey;
- }
-
- function setAgentsViewMode(mode) {
- agentsState.viewMode = mode === 'live' ? 'live' : 'overview';
- renderAgentsContent();
- if (agentsState.viewMode === 'live') {
- void loadSelectedAgentLiveData();
- }
- }
-
- function selectAgent(key, nextMode) {
- if (!key || !agentsState.agents[key]) return;
- agentsState.selectedAgentKey = key;
- if (nextMode) {
- agentsState.viewMode = nextMode;
- }
- renderAgentsContent();
- if (agentsState.viewMode === 'live') {
- void loadSelectedAgentLiveData();
- }
- }
-
- function isOpenClawVM(agent) {
- const key = normalizeAgentKey(agent && agent.name);
- return !!openclawState.instances[key];
- }
-
- function isAgentOnline(agent) {
- if (!agent) {
- return false;
- }
-
- if (isOpenClawVM(agent)) {
- const vmStatus = getVMStatus().find(v => v.name === normalizeAgentKey(agent.name));
- if (vmStatus) {
- return vmStatus.active;
- }
- }
-
- const hasSessions = Object.keys(agent.sessions).length > 0;
- const hasOps = Object.keys(agent.operations).length > 0;
- const seenRecently = agent.lastSeenAt > 0 && (Date.now() - agent.lastSeenAt) < 300000;
- return hasSessions || hasOps || seenRecently;
- }
-
- function getAgentBucket(evt) {
- return ensureAgentBucket(evt);
- }
-
- function processAgentEvent(evt) {
- const agent = getAgentBucket(evt);
- if (!agent) return;
-
- const eventType = getEnvelopeType(evt);
- const correlation = getEnvelopeCorrelation(evt);
- const attrs = getEnvelopeAttributes(evt);
- const ts = new Date(getEnvelopeTS(evt)).getTime();
- agent.lastSeenAt = Number.isFinite(ts) ? ts : Date.now();
-
- if (eventType === 'session.start' && correlation.session_id) {
- agent.sessions[correlation.session_id] = { ts: getEnvelopeTS(evt) };
- }
- if (eventType === 'session.end' && correlation.session_id) {
- delete agent.sessions[correlation.session_id];
- }
-
- if (eventType === 'span.start' && correlation.span_id) {
- const payload = getEnvelopePayload(evt);
- agent.operations['s:' + correlation.span_id] = {
- type: 'span',
- name: attrs.name || attrs.span_kind || 'unknown',
- kind: attrs.span_kind || '',
- subType: attrs.type || '',
- startedAt: new Date(getEnvelopeTS(evt)).getTime() || Date.now(),
- promptPreview: payload.prompt_preview || '',
- inputPreview: payload.input ? (typeof payload.input === 'string' ? payload.input : JSON.stringify(payload.input)) : '',
- spanID: correlation.span_id,
- runID: correlation.run_id || '',
- };
- }
- if (eventType === 'span.end' && correlation.span_id) {
- const op = agent.operations['s:' + correlation.span_id];
- if (op) {
- const payload = getEnvelopePayload(evt);
- op.resultPreview = payload.result_preview || '';
- op.status = payload.status || '';
- op.durationMS = payload.duration_ms || 0;
- op.endedAt = new Date(getEnvelopeTS(evt)).getTime() || Date.now();
- op.usage = payload.usage || null;
- // Keep completed ops briefly for display, then remove and refresh stream
- setTimeout(() => {
- delete agent.operations['s:' + correlation.span_id];
- refreshThinkingStream(agent);
- }, 3000);
- }
- }
-
- if (eventType === 'run.start' && correlation.run_id) {
- const payload = getEnvelopePayload(evt);
- agent.operations['r:' + correlation.run_id] = {
- type: 'run',
- name: 'Thinking…',
- kind: 'run',
- startedAt: new Date(getEnvelopeTS(evt)).getTime() || Date.now(),
- promptPreview: payload.prompt_preview || payload.message_preview || payload.message || '',
- runID: correlation.run_id,
- };
- }
- if (eventType === 'run.end' && correlation.run_id) {
- const op = agent.operations['r:' + correlation.run_id];
- if (op) {
- const payload = getEnvelopePayload(evt);
- op.endedAt = new Date(getEnvelopeTS(evt)).getTime() || Date.now();
- op.status = payload.status || '';
- op.usage = payload.usage || null;
- op.model = payload.model || '';
- op.thinkingTokens = (payload.usage && payload.usage.thinking_tokens) || 0;
- setTimeout(() => {
- delete agent.operations['r:' + correlation.run_id];
- refreshThinkingStream(agent);
- }, 2000);
- }
- }
-
- const id = getRecordID(evt);
- if (id && !agent.eventIDs.has(id)) {
- agent.eventIDs.add(id);
- agent.events.push(evt);
- while (agent.events.length > 100) {
- const removed = agent.events.shift();
- agent.eventIDs.delete(getRecordID(removed));
- }
- }
- }
-
- function getAgentDisplayOps(agent) {
- const now = Date.now();
- const ops = Object.values(agent.operations).filter(op => (now - op.startedAt) < 300000);
- const hasSpecificSpans = ops.some(op => op.kind && op.kind !== 'run');
- return hasSpecificSpans ? ops.filter(op => op.kind && op.kind !== 'run') : ops;
- }
-
- function isAgentTimelineEvent(evt) {
- const eventType = getEnvelopeType(evt);
- return [
- 'session.start',
- 'session.end',
- 'run.start',
- 'run.end',
- 'span.start',
- 'span.end',
- 'error',
- ].includes(eventType);
- }
-
- function isDashboardFeedEvent(evt) {
- const eventType = getEnvelopeType(evt);
- return isAgentTimelineEvent(evt) || eventType === 'metric.snapshot';
- }
-
- function getDashboardInfraPill() {
- const services = Object.values(swarmState.services);
- if (services.length === 0) {
- return {
- className: 'inactive',
- name: 'infra',
- label: 'unknown',
- };
- }
-
- const unhealthy = services.filter(svc => svc.status !== 'healthy');
- if (unhealthy.length === 0) {
- return {
- className: 'active',
- name: 'infra',
- label: 'all running',
- };
- }
-
- const degradedOnly = unhealthy.every(svc => svc.status === 'degraded');
- return {
- className: degradedOnly ? 'degraded' : 'inactive',
- name: 'infra',
- label: degradedOnly ? 'degraded' : `${unhealthy.length} issue${unhealthy.length === 1 ? '' : 's'}`,
- };
- }
-
- async function renderAgents() {
- agentsState = createAgentsState();
-
- app.innerHTML = `
-
-
-
- ${agentsSkeleton()}
- `;
-
- bindAgentViewToggle();
-
- try {
- const [snapshots, events, summaryData] = await Promise.all([
- api('/v1/events?event_type=openclaw.snapshot&limit=100').catch(() => ({ events: [] })),
- api('/v1/events?limit=300'),
- api('/v1/stats/summary').catch(() => null),
- ]);
-
- if (!isCurrentPath('/agents')) return;
-
- if (summaryData) {
- agentsState.dbStats.messages = summaryData.runs_today || 0;
- agentsState.dbStats.tools = summaryData.tool_calls_today || 0;
- agentsState.dbStats.errors = summaryData.errors_today || 0;
- }
-
- mergeOpenClawEvents(snapshots.events || []);
- addAgentEvents((events.events || []).filter(isAgentTimelineEvent).slice().reverse());
- renderAgentsContent();
- } catch (e) {
- document.getElementById('agents-content').innerHTML =
- `Error loading agent activity: ${escapeHTML(e.message)}
`;
- }
-
- agentsState.timerInterval = setInterval(updateAgentTimers, 1000);
- agentsUnsubscribe = subscribeWS(handleAgentsWS);
- }
-
- function bindAgentViewToggle() {
- const root = document.getElementById('agents-view-toggle');
- if (!root) return;
- root.querySelectorAll('[data-mode]').forEach(button => {
- button.addEventListener('click', () => {
- setAgentsViewMode(button.dataset.mode || 'overview');
- });
- });
- }
-
- function updateAgentViewToggle() {
- const root = document.getElementById('agents-view-toggle');
- if (!root) return;
- root.querySelectorAll('[data-mode]').forEach(button => {
- button.classList.toggle('active', button.dataset.mode === agentsState.viewMode);
- });
- }
-
- function renderAgentsContent() {
- renderAgentSummary();
- updateAgentViewToggle();
- if (agentsState.viewMode === 'live') {
- renderAgentsLive();
- return;
- }
- renderAgentLanes();
- }
-
- async function loadSelectedAgentLiveData() {
- const selectedKey = ensureSelectedAgentKey();
- if (!selectedKey) return;
-
- const agent = agentsState.agents[selectedKey];
- if (!agent || agent.liveLoaded || agent.liveLoading || !agent.clientID || !agent.framework) {
+ // 'g' prefix for goto shortcuts
+ if (e.key === 'g' && !_pendingGoto) {
+ _pendingGoto = true;
+ setTimeout(() => { _pendingGoto = false; }, 800);
return;
}
- agent.liveLoading = true;
- try {
- const params = new URLSearchParams();
- params.set('client_id', agent.clientID);
- params.set('framework', agent.framework);
- params.set('limit', '250');
- const data = await api('/v1/agents/live?' + params.toString());
- addAgentEvents((data.events || []).slice().reverse());
- agent.liveLoaded = true;
- } catch (err) {
- console.error('Failed to load live agent context:', err);
- } finally {
- agent.liveLoading = false;
- if (isCurrentPath('/agents') && agentsState.viewMode === 'live' && agentsState.selectedAgentKey === selectedKey) {
- renderAgentsContent();
- }
- }
- }
-
- // ── Agent Lane Sparklines ───────────────────────────────
- function buildAgentActivityBars(agent, bucketCount) {
- const events = agent.events || [];
- if (events.length === 0) return '';
- const count = bucketCount || 20;
- const now = Date.now();
- const windowMS = 3600000; // 1 hour
- const bucketMS = windowMS / count;
- const buckets = new Array(count).fill(0);
-
- for (const evt of events) {
- const ts = new Date(getEnvelopeTS(evt)).getTime();
- const age = now - ts;
- if (age > windowMS || age < 0) continue;
- const idx = Math.min(count - 1, Math.floor((windowMS - age) / bucketMS));
- buckets[idx]++;
- }
-
- const max = Math.max(...buckets, 1);
- return `${buckets.map(b => {
- const pct = (b / max * 100).toFixed(0);
- return `
`;
- }).join('')}
`;
- }
-
- function renderAgentLanes() {
- const contentEl = document.getElementById('agents-content');
- if (!contentEl) return;
- contentEl.innerHTML = '';
-
- const lanesEl = document.getElementById('agents-lanes');
- if (!lanesEl) return;
-
- const agentKeys = getSortedAgentKeys();
-
- if (agentKeys.length === 0) {
- lanesEl.innerHTML = 'No recent agent activity
';
+ if (_pendingGoto) {
+ _pendingGoto = false;
+ if (e.key === 'd') navigate('/');
+ else if (e.key === 's') navigate('/sessions');
+ else if (e.key === 'a') navigate('/agents');
+ else if (e.key === 'i') navigate('/infrastructure');
+ else if (e.key === 'p') navigate('/settings');
+ else if (e.key === 'u') navigate('/usage');
return;
}
+ });
- lanesEl.innerHTML = agentKeys.map(key => {
- const agent = agentsState.agents[key];
- const isOnline = isAgentOnline(agent);
- const sessionCount = Object.keys(agent.sessions).length;
- const ops = getAgentDisplayOps(agent);
- const subagentCount = ops.filter(op => op.kind === 'agent' || op.subType === 'subagent').length;
-
- const statusClass = sessionCount > 0 ? ' has-sessions' : '';
- const statusText = !isOnline ? 'offline'
- : subagentCount > 0 ? subagentCount + ' subagent' + (subagentCount > 1 ? 's' : '')
- : sessionCount > 0 ? sessionCount + ' session' + (sessionCount > 1 ? 's' : '')
- : 'idle';
-
- const opsHTML = ops.length > 0 ? `${ops.map(op => {
- const elapsed = Math.floor((Date.now() - op.startedAt) / 1000);
- const stale = elapsed > 300;
- const kindClass = op.kind === 'agent' || op.subType === 'subagent' ? ' subagent' : '';
- return `
-
-
- ${escapeHTML(op.name)}
- ${formatElapsed(elapsed)}
- ${stale ? '(stale?)' : ''}
-
`;
- }).join('')}
` : '';
-
- const recent = agent.events.slice(-40).reverse();
- const eventsHTML = recent.length > 0 ? recent.map(evt => {
- const eventType = getEnvelopeType(evt);
- const details = getEventDetails(evt);
- const detailHTML = details ? `${escapeHTML(details)}
` : '';
- const expandHTML = details ? '' : '';
-
- return `
-
-
- ${getEventBody(evt)}
- ${expandHTML}
- ${detailHTML}
-
`;
- }).join('') : 'No recent activity
';
-
- return `
-
-
- ${opsHTML}
-
${eventsHTML}
-
`;
- }).join('');
-
- lanesEl.querySelectorAll('.agent-lane[data-agent-key]').forEach(lane => {
- lane.addEventListener('click', () => {
- selectAgent(lane.dataset.agentKey || '', 'live');
- });
- });
- lanesEl.querySelectorAll('.timeline-expand-hint').forEach(button => {
- button.addEventListener('click', event => {
- event.stopPropagation();
- button.parentElement.classList.toggle('expanded');
- });
- });
- }
-
- function formatElapsed(seconds) {
- if (seconds < 60) return seconds + 's';
- if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's';
- return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm';
- }
-
- function renderAgentSummary() {
- const el = document.getElementById('agents-summary');
- if (!el) return;
- const s = agentsState.dbStats;
- const liveAgents = getSortedAgentKeys().filter(key => isAgentOnline(agentsState.agents[key])).length;
- const liveSubagents = getSortedAgentKeys().reduce((count, key) => {
- const agent = agentsState.agents[key];
- return count + Object.values(agent.operations).filter(op => op.kind === 'agent' || op.subType === 'subagent').length;
- }, 0);
- el.innerHTML = `
- Live Agents ${liveAgents}
- Active Subagents ${liveSubagents}
- Runs Today ${s.messages}
- Tool Calls ${s.tools}
- Errors ${s.errors}
- `;
- }
-
- function getAgentLabel(agent) {
- if (!agent) return 'Unknown';
- return agent.name || agent.host || agent.framework || agent.key || 'Unknown';
- }
-
- function getAgentLiveSummary(agent) {
- const recent = agent.events.slice().reverse();
- const activeOps = getAgentDisplayOps(agent);
- const sessionIDs = Object.keys(agent.sessions);
- const live = {
- sessionIDs,
- activeOps,
- activeSubagents: activeOps.filter(op => op.kind === 'agent' || op.subType === 'subagent'),
- activeTools: activeOps.filter(op => op.kind === 'tool'),
- latestPrompt: '',
- latestRunStatus: '',
- latestModel: '',
- latestError: '',
- latestUsage: null,
- latestContextWindow: null,
- };
-
- for (const evt of recent) {
- const eventType = getEnvelopeType(evt);
- const payload = getEnvelopePayload(evt);
- if (!live.latestPrompt && eventType === 'run.start') {
- live.latestPrompt = payload.prompt_preview || payload.message_preview || payload.message || '';
- }
- if (!live.latestRunStatus && eventType === 'run.end') {
- live.latestRunStatus = payload.status || '';
- live.latestModel = payload.model || '';
- live.latestUsage = payload.usage || null;
- live.latestContextWindow = payload.context_window || null;
- }
- if (!live.latestUsage && eventType === 'metric.snapshot' && payload.metrics) {
- live.latestUsage = payload.metrics.usage || null;
- live.latestModel = live.latestModel || payload.metrics.model || '';
- }
- if (!live.latestError && eventType === 'error') {
- const errPayload = payload.error || {};
- live.latestError = errPayload.message || payload.message || '';
- }
- if (live.latestPrompt && live.latestRunStatus && live.latestError) {
- break;
- }
- }
-
- return live;
- }
-
- function formatCount(value) {
- if (value === undefined || value === null || value === '') return '-';
- return String(value);
- }
-
- function formatCost(value) {
- if (value === undefined || value === null || value === '') return '-';
- const num = Number(value);
- if (!Number.isFinite(num)) return String(value);
- return '$' + num.toFixed(4);
- }
-
- function formatTokenCount(value) {
- if (value === undefined || value === null || value === '') return '-';
- const n = Number(value);
- if (!Number.isFinite(n)) return String(value);
- if (n === 0) return '0';
- if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
- if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
- return String(n);
- }
-
- function buildLiveEventContext(evt) {
- const eventType = getEnvelopeType(evt);
- const payload = getEnvelopePayload(evt);
- const attrs = getEnvelopeAttributes(evt);
- const correlation = getEnvelopeCorrelation(evt);
- const parts = [];
-
- if ((eventType === 'span.start' || eventType === 'span.end') && attrs.span_kind === 'tool') {
- if (payload.input) {
- parts.push(`input${escapeHTML(typeof payload.input === 'string' ? payload.input : JSON.stringify(payload.input))}
`);
- }
- if (payload.result_preview) {
- parts.push(`result${escapeHTML(String(payload.result_preview))}
`);
- }
- }
- if ((eventType === 'span.start' || eventType === 'span.end') && (attrs.span_kind === 'agent' || attrs.type === 'subagent')) {
- if (payload.prompt_preview) {
- parts.push(`prompt${escapeHTML(String(payload.prompt_preview))}
`);
- }
- if (payload.usage && payload.usage.total_tokens !== undefined) {
- parts.push(`tokens${escapeHTML(formatCount(payload.usage.total_tokens))}
`);
- }
- if (payload.usage && payload.usage.total_cost !== undefined) {
- parts.push(`cost${escapeHTML(formatCost(payload.usage.total_cost))}
`);
- }
- }
- if (eventType === 'run.start') {
- const preview = payload.prompt_preview || payload.message_preview || payload.message || '';
- if (preview) {
- parts.push(`prompt${escapeHTML(String(preview))}
`);
- }
- }
- if (eventType === 'run.end') {
- if (payload.model) {
- parts.push(`model${escapeHTML(String(payload.model))}
`);
- }
- if (payload.usage && payload.usage.total_tokens !== undefined) {
- parts.push(`tokens${escapeHTML(formatCount(payload.usage.total_tokens))}
`);
- }
- if (payload.duration_ms !== undefined) {
- parts.push(`duration${escapeHTML(formatDuration(payload.duration_ms))}
`);
- }
- }
- if (eventType === 'metric.snapshot' && payload.metrics) {
- if (payload.metrics.model) {
- parts.push(`model${escapeHTML(String(payload.metrics.model))}
`);
- }
- if (payload.metrics.usage && payload.metrics.usage.total_tokens !== undefined) {
- parts.push(`tokens${escapeHTML(formatCount(payload.metrics.usage.total_tokens))}
`);
- }
- if (payload.metrics.usage && payload.metrics.usage.total_cost !== undefined) {
- parts.push(`cost${escapeHTML(formatCost(payload.metrics.usage.total_cost))}
`);
- }
- }
- if (eventType === 'error') {
- const errPayload = payload.error || {};
- if (errPayload.type) {
- parts.push(`type${escapeHTML(String(errPayload.type))}
`);
- }
- }
-
- const ids = [];
- if (correlation.session_id) ids.push(`session ${correlation.session_id}`);
- if (correlation.run_id) ids.push(`run ${correlation.run_id}`);
- if (correlation.span_id) ids.push(`span ${correlation.span_id}`);
- if (ids.length > 0) {
- parts.push(`ids${escapeHTML(ids.join(' · '))}
`);
- }
-
- return parts.join('');
- }
-
- function getRunGroupLabel(runID, events) {
- const runStart = events.find(evt => getEnvelopeType(evt) === 'run.start');
- if (!runStart) {
- return runID ? `Run ${runID.slice(0, 12)}...` : 'Session activity';
- }
- const payload = getEnvelopePayload(runStart);
- const preview = payload.prompt_preview || payload.message_preview || payload.message || '';
- if (preview) {
- return preview.length > 72 ? preview.slice(0, 72) + '...' : preview;
- }
- return runID ? `Run ${runID.slice(0, 12)}...` : 'Session activity';
- }
-
- function groupAgentEventsByRun(events) {
- const groups = [];
- const byRun = new Map();
-
- for (const evt of events) {
- const correlation = getEnvelopeCorrelation(evt);
- const runID = correlation.run_id || '';
- const key = runID || `session:${correlation.session_id || 'unknown'}`;
- if (!byRun.has(key)) {
- const group = {
- key,
- runID,
- sessionID: correlation.session_id || '',
- events: [],
- subagents: new Set(),
- tools: new Set(),
- };
- byRun.set(key, group);
- groups.push(group);
- }
-
- const group = byRun.get(key);
- group.events.push(evt);
- const attrs = getEnvelopeAttributes(evt);
- if (attrs.span_kind === 'agent' || attrs.type === 'subagent') {
- group.subagents.add(attrs.name || 'unknown');
- }
- if (attrs.span_kind === 'tool' && attrs.name) {
- group.tools.add(attrs.name);
- }
- }
-
- return groups;
- }
-
- function refreshThinkingStream(agent) {
- if (!agent) return;
- const selectedKey = agentsState.selectedAgentKey;
- if (agent.key !== selectedKey) return;
- const streamEl = document.getElementById('thinking-stream-' + selectedKey);
- if (streamEl) {
- streamEl.innerHTML = renderThinkingStream(agent);
- }
- }
-
- function renderThinkingStream(agent) {
- const now = Date.now();
- const ops = Object.values(agent.operations).filter(op => {
- // Show ended ops only if recently ended (within 3s)
- if (op.endedAt) return (now - op.endedAt) < 3000;
- return (now - op.startedAt) < 300000;
- });
-
- if (ops.length === 0) {
- return 'Idle — waiting for activity
';
- }
-
- return ops.map(op => {
- const elapsed = op.endedAt
- ? Math.floor((op.endedAt - op.startedAt) / 1000)
- : Math.floor((now - op.startedAt) / 1000);
- const isEnded = !!op.endedAt;
- const isSubagent = op.kind === 'agent' || op.subType === 'subagent';
- const isRun = op.kind === 'run';
- const isTool = op.kind === 'tool';
-
- let icon, kindLabel, kindClass;
- if (isRun) {
- icon = isEnded ? '✓' : '◌';
- kindLabel = isEnded ? (op.status === 'success' ? 'Done' : op.status || 'Done') : 'Thinking';
- kindClass = 'thinking-op-run' + (isEnded ? ' ended' : ' active');
- } else if (isSubagent) {
- icon = isEnded ? '✓' : '◎';
- kindLabel = isEnded ? (op.status === 'success' ? 'Subagent done' : 'Subagent ' + (op.status || 'done')) : 'Subagent';
- kindClass = 'thinking-op-subagent' + (isEnded ? ' ended' : ' active');
- } else if (isTool) {
- icon = isEnded ? '✓' : '▸';
- kindLabel = isEnded ? (op.status === 'success' ? 'Tool done' : 'Tool ' + (op.status || 'done')) : 'Tool';
- kindClass = 'thinking-op-tool' + (isEnded ? ' ended' : ' active');
- } else {
- icon = '·';
- kindLabel = op.name;
- kindClass = 'thinking-op-other' + (isEnded ? ' ended' : ' active');
- }
-
- const preview = op.promptPreview || op.inputPreview || '';
- const result = op.resultPreview || '';
- const usage = op.usage || {};
- const thinkingToks = op.thinkingTokens || usage.thinking_tokens || 0;
- const totalToks = usage.total_tokens || 0;
-
- const navigableRunID = isRun ? op.runID : (isSubagent ? op.runID : '');
- const clickable = navigableRunID ? ` clickable" data-run-id="${escapeHTML(navigableRunID)}` : '';
-
- return `
-
-
- ${preview ? `
${escapeHTML(preview.length > 180 ? preview.slice(0, 180) + '…' : preview)}
` : ''}
- ${result ? `
${escapeHTML(result.length > 180 ? result.slice(0, 180) + '…' : result)}
` : ''}
- ${(thinkingToks || totalToks) ? `
${thinkingToks ? `🧠 ${formatTokenCount(thinkingToks)} thinking` : ''}${totalToks ? `⚡ ${formatTokenCount(totalToks)} total` : ''}
` : ''}
-
`;
- }).join('');
- }
-
- function renderAgentsLive() {
- const contentEl = document.getElementById('agents-content');
- if (!contentEl) return;
-
- const agentKeys = getSortedAgentKeys();
- const selectedKey = ensureSelectedAgentKey();
- if (!selectedKey || agentKeys.length === 0) {
- contentEl.innerHTML = 'No recent agent activity
';
- return;
- }
-
- const selected = agentsState.agents[selectedKey];
- const summary = getAgentLiveSummary(selected);
- const recent = selected.events.slice(-80).reverse();
- const runGroups = groupAgentEventsByRun(recent);
-
- contentEl.innerHTML = `
-
-
-
-
-
-
-
- Live Operations
- ${summary.activeOps.length > 0 ? `${summary.activeOps.length} active` : ''}
-
-
- ${renderThinkingStream(selected)}
-
-
-
-
Last Run
-
Status${escapeHTML(summary.latestRunStatus || '—')}
-
Model${escapeHTML(summary.latestModel || '—')}
-
Tokens${escapeHTML(formatTokenCount(summary.latestUsage ? summary.latestUsage.total_tokens : null))}
-
Thinking${escapeHTML(formatTokenCount(summary.latestUsage ? summary.latestUsage.thinking_tokens : null))}
-
Cost${escapeHTML(formatCost(summary.latestUsage ? summary.latestUsage.total_cost : null))}
- ${summary.latestError ? `
Error${escapeHTML(summary.latestError)}
` : ''}
-
-
-
Context Window
-
Input${escapeHTML(formatTokenCount(summary.latestContextWindow ? summary.latestContextWindow.input_tokens : null))}
-
Output${escapeHTML(formatTokenCount(summary.latestContextWindow ? summary.latestContextWindow.output_tokens : null))}
-
Used${escapeHTML(formatTokenCount(summary.latestContextWindow ? summary.latestContextWindow.used_tokens : null))}
-
Remaining${escapeHTML(formatTokenCount(summary.latestContextWindow ? summary.latestContextWindow.tokens_remaining : null))}
- ${summary.latestContextWindow && summary.latestContextWindow.max_tokens ? `
-
` : ''}
-
-
-
- ${runGroups.length > 0 ? runGroups.map(group => `
-
-
-
- ${group.events.map(evt => `
-
-
- ${getEventBody(evt)}
-
${buildLiveEventContext(evt)}
-
`).join('')}
-
-
- `).join('') : '
No recent activity
'}
-
-
-
- `;
-
- contentEl.querySelectorAll('[data-agent-key]').forEach(button => {
- button.addEventListener('click', () => selectAgent(button.dataset.agentKey || '', 'live'));
- });
-
- // Delegate thinking-op clicks — bound once on stable container, survives 1s stream refresh
- const mainSection = contentEl.querySelector('.agents-live-main');
- if (mainSection) {
- mainSection.addEventListener('click', e => {
- const op = e.target.closest('.thinking-op[data-run-id]');
- if (op && op.dataset.runId) {
- navigate('/runs/' + op.dataset.runId);
- }
- });
- }
- }
-
- function renderAgentVMStrip() {
- // VM online/offline state is shown in each lane header via getVMStatus().
- // Re-render lanes to pick up the updated openclawState.
- renderAgentsContent();
- }
-
- let _agentsRenderTimer = null;
-
- function scheduleAgentsRender() {
- if (_agentsRenderTimer) return;
- _agentsRenderTimer = requestAnimationFrame(() => {
- _agentsRenderTimer = null;
- renderAgentsContent();
- });
- }
-
- function handleAgentsWS(msg) {
- if (msg.type !== 'message') return;
-
- const eventType = getEnvelopeType(msg.data);
- if (eventType === 'openclaw.snapshot') {
- mergeOpenClawEvents([msg.data]);
- scheduleAgentsRender();
- return;
- }
- if (!isAgentTimelineEvent(msg.data)) return;
-
- if (eventType === 'run.start') agentsState.dbStats.messages++;
- else if (eventType === 'span.end') {
- const attrs = getEnvelopeAttributes(msg.data);
- if (attrs.span_kind === 'tool') agentsState.dbStats.tools++;
- } else if (eventType === 'error') agentsState.dbStats.errors++;
-
- addAgentEvents([msg.data]);
- scheduleAgentsRender();
- }
-
- function updateAgentTimers() {
- document.querySelectorAll('.active-op-time[data-start]').forEach(el => {
- const start = parseInt(el.dataset.start, 10);
- if (!start) return;
- const elapsed = Math.floor((Date.now() - start) / 1000);
- el.textContent = formatElapsed(elapsed);
-
- const op = el.closest('.active-op');
- if (op && elapsed > 300 && !op.classList.contains('stale')) {
- op.classList.add('stale');
- if (!op.querySelector('.active-op-stale')) {
- op.insertAdjacentHTML('beforeend', '(stale?)');
- }
- }
- });
-
- }
-
- function addAgentEvents(events) {
- let changed = false;
-
- for (const evt of events) {
- const id = getRecordID(evt);
- const agent = getAgentBucket(evt);
- if (!id || !agent || agent.eventIDs.has(id)) continue;
- processAgentEvent(evt);
- changed = true;
- }
-
- if (changed) {
- for (const agent of Object.values(agentsState.agents)) {
- agent.events.sort((a, b) => new Date(getEnvelopeTS(a)).getTime() - new Date(getEnvelopeTS(b)).getTime());
- }
- recomputeAgentStats();
- }
- }
-
- function recomputeAgentStats() {
- const stats = { messages: 0, tools: 0, errors: 0, toolCounts: {} };
-
- for (const agent of Object.values(agentsState.agents)) {
- for (const evt of agent.events) {
- const eventType = getEnvelopeType(evt);
- const attrs = getEnvelopeAttributes(evt);
-
- if (eventType === 'run.start' || eventType === 'run.end') stats.messages++;
- if (eventType === 'span.end' && attrs.span_kind === 'tool') {
- stats.tools++;
- const toolName = attrs.name || 'unknown';
- stats.toolCounts[toolName] = (stats.toolCounts[toolName] || 0) + 1;
- }
- if (eventType === 'error') stats.errors++;
- }
- }
-
- agentsState.stats = stats;
- }
-
- function getEventIcon(eventType) {
- switch (eventType) {
- case 'run.start':
- return '↓
';
- case 'run.end':
- return '↑
';
- case 'span.start':
- case 'span.end':
- return '⚙
';
- case 'error':
- return '!
';
- case 'session.start':
- case 'session.end':
- return '○
';
- default:
- return '·
';
- }
- }
-
- function getEventLabel(eventType) {
- const labels = {
- 'session.start': 'Session Started',
- 'session.end': 'Session Ended',
- 'run.start': 'Message Received',
- 'run.end': 'Response Sent',
- 'span.start': 'Span Started',
- 'span.end': 'Span Completed',
- 'error': 'Error',
- 'metric.snapshot': 'Metric',
- };
- return labels[eventType] || eventType;
- }
-
- function getVMName(evt) {
- return getAgentIdentity(evt).name || 'unknown';
- }
-
- function getVMClassName(vmName) {
- const normalized = String(vmName || 'unknown').toLowerCase();
- return ['zap', 'orb', 'sun'].includes(normalized) ? normalized : 'unknown';
- }
-
- function getEventBody(evt) {
- const eventType = getEnvelopeType(evt);
- const payload = getEnvelopePayload(evt);
- const attrs = getEnvelopeAttributes(evt);
- const correlation = getEnvelopeCorrelation(evt);
-
- if (eventType === 'span.start' || eventType === 'span.end') {
- const name = attrs.name || attrs.span_kind || 'unknown span';
- const duration = payload.duration_ms !== undefined && payload.duration_ms !== null
- ? ` ${escapeHTML(formatDuration(payload.duration_ms))}`
- : '';
- const detailClass = attrs.span_kind === 'agent' || attrs.type === 'subagent' ? ' subagent-name' : ' tool-name';
- const prefix = attrs.span_kind === 'agent' || attrs.type === 'subagent' ? 'subagent ' : '';
- return `${escapeHTML(prefix + name)}${duration}
`;
- }
-
- if (eventType === 'run.start') {
- const preview = payload.prompt_preview || payload.message_preview || payload.message || '';
- if (!preview) {
- return '';
- }
- const trimmed = preview.length > 140 ? preview.slice(0, 140) + '...' : preview;
- return `"${escapeHTML(trimmed)}"
`;
- }
-
- if (eventType === 'run.end') {
- return `${statusIcon(payload.status || 'unknown')}
`;
- }
-
- if (eventType === 'error') {
- const errPayload = payload.error || {};
- const errType = errPayload.type || 'error';
- const message = errPayload.message || payload.message || 'unknown';
- return `${escapeHTML(errType + ': ' + message)}
`;
- }
-
- if (eventType === 'session.start' || eventType === 'session.end') {
- return correlation.session_id
- ? `session ${escapeHTML(correlation.session_id)}
`
- : '';
- }
-
- return '';
- }
-
- function getEventDetails(evt) {
- const details = {};
- const correlation = getEnvelopeCorrelation(evt);
- const attributes = getEnvelopeAttributes(evt);
- const payload = getEnvelopePayload(evt);
-
- if (Object.keys(correlation).length > 0) {
- details.correlation = correlation;
- }
- if (Object.keys(attributes).length > 0) {
- details.attributes = attributes;
- }
- if (Object.keys(payload).length > 0) {
- details.payload = payload;
- }
-
- if (Object.keys(details).length === 0) {
- return '';
- }
-
- return JSON.stringify(details, null, 2);
- }
-
- function persistDashboardRecentEvents() {
- if (!dashboardState) return;
- localStorage.setItem(
- DASH_RECENT_EVENTS_STORAGE_KEY,
- JSON.stringify(dashboardState.recentEvents.slice(-DASH_RECENT_EVENTS_LIMIT)),
- );
- }
-
- function addDashboardRecentEvent(evt) {
- if (!dashboardState || !isDashboardFeedEvent(evt)) {
- return false;
- }
-
- const id = getRecordID(evt);
- if (id && dashboardState.recentEventIDs.has(id)) {
- return false;
- }
-
- if (id) {
- dashboardState.recentEventIDs.add(id);
- }
- dashboardState.recentEvents.push(evt);
-
- while (dashboardState.recentEvents.length > DASH_RECENT_EVENTS_LIMIT) {
- const removed = dashboardState.recentEvents.shift();
- const removedID = getRecordID(removed);
- if (removedID) {
- dashboardState.recentEventIDs.delete(removedID);
- }
- }
-
- persistDashboardRecentEvents();
- return true;
- }
-
- async function renderDashboard() {
- clearErrorBadge();
- dashboardState = {
- summary: null,
- timeseries: null,
- window: '1h',
- chartMode: getDashboardChartMode(),
- chartCursorIndex: null,
- recentEvents: [],
- recentEventIDs: new Set(),
- toolCounts: {},
- modelCounts: {},
- rightPanelMode: localStorage.getItem('agentmon:dash:right-panel') || 'framework',
- };
-
- app.innerHTML = `
-
-
-
-
◉Active Sessions
-
-
-
-
-
-
-
-
-
-
- Tokens today
- -
-
-
- Cost today
- -
-
-
- Avg run duration
- -
-
-
- Error rate
- -
-
-
- Infrastructure
-
-
-
- `;
-
- document.querySelectorAll('.window-btn').forEach(btn => {
- btn.addEventListener('click', () => {
- document.querySelectorAll('.window-btn').forEach(b => b.classList.remove('active'));
- btn.classList.add('active');
- dashboardState.window = btn.dataset.w;
- loadTimeseries();
- });
- });
-
- document.querySelectorAll('.mode-btn').forEach(btn => {
- btn.addEventListener('click', () => {
- const nextMode = btn.dataset.mode;
- if (dashboardState.chartMode === nextMode) return;
- document.querySelectorAll('.mode-btn').forEach(b => b.classList.toggle('active', b === btn));
- dashboardState.chartMode = nextMode;
- localStorage.setItem('agentmon:dash:chart-mode', nextMode);
- if (dashboardChart) {
- dashboardChart.destroy();
- dashboardChart = null;
- }
- renderTimeseriesChart();
- });
- });
-
- document.querySelectorAll('.right-panel-tab').forEach(btn => {
- btn.addEventListener('click', () => {
- const panel = btn.dataset.panel;
- if (dashboardState.rightPanelMode === panel) return;
- document.querySelectorAll('.right-panel-tab').forEach(b => b.classList.toggle('active', b === btn));
- dashboardState.rightPanelMode = panel;
- localStorage.setItem('agentmon:dash:right-panel', panel);
- renderRightPanel();
- });
- });
-
- renderDashVMStrip();
-
- const cachedRecentEvents = tryParseJSON(localStorage.getItem(DASH_RECENT_EVENTS_STORAGE_KEY));
- if (Array.isArray(cachedRecentEvents)) {
- for (const evt of cachedRecentEvents) {
- addDashboardRecentEvent(evt);
- }
- renderDashFeed();
- }
-
- // Render cached data immediately while the API call is in-flight
- const cachedSummary = tryParseJSON(localStorage.getItem('agentmon:dash:summary'));
- const cachedTS = tryParseJSON(localStorage.getItem('agentmon:dash:ts:' + dashboardState.window));
- if (cachedSummary) { dashboardState.summary = cachedSummary; renderSummaryCards(); }
- if (cachedTS) { dashboardState.timeseries = cachedTS; renderTimeseriesChart(); renderDashSparklines(); renderRightPanel(); }
-
- try {
- const [summaryData, tsData, recentData, snapshots, swarmSnaps, topToolsData, topModelsData] = await Promise.all([
- api('/v1/stats/summary'),
- api('/v1/stats/timeseries?window=1h'),
- api('/v1/events?limit=10'),
- api('/v1/events?event_type=openclaw.snapshot&limit=100').catch(() => ({ events: [] })),
- api('/v1/events?event_type=swarm.snapshot&limit=10').catch(() => ({ events: [] })),
- api('/v1/stats/top-tools').catch(() => ({ tools: [] })),
- api('/v1/stats/top-models').catch(() => ({ models: [] })),
- ]);
-
- if (!isCurrentPath('/')) return;
-
- mergeOpenClawEvents(snapshots.events || []);
- for (const evt of swarmSnaps.events || []) mergeSwarmSnapshot(evt);
- renderDashVMStrip();
-
- dashboardState.summary = summaryData;
- dashboardState.timeseries = tsData;
- localStorage.setItem('agentmon:dash:summary', JSON.stringify(summaryData));
- localStorage.setItem('agentmon:dash:ts:' + dashboardState.window, JSON.stringify(tsData));
- renderSummaryCards();
- renderTimeseriesChart();
- renderDashSparklines();
- renderRightPanel();
-
- // Seed tool counts from the dedicated top-tools endpoint
- for (const t of (topToolsData.tools || [])) {
- dashboardState.toolCounts[t.name] = t.count;
- }
- for (const m of (topModelsData.models || [])) {
- dashboardState.modelCounts[m.name] = m.count;
- }
-
- const events = (recentData.events || [])
- .filter(isDashboardFeedEvent)
- .slice()
- .reverse();
- for (const evt of events) {
- addDashboardRecentEvent(evt);
- }
- renderDashFeed();
- renderDashTopTools();
- renderDashTopModels();
- } catch (e) {
- console.error('Dashboard load error:', e);
- }
-
- dashboardUnsubscribe = subscribeWS(handleDashboardWS);
- }
-
- function renderDashVMStrip() {
- const strip = document.getElementById('dash-vm-strip');
- if (!strip) return;
- const vms = getVMStatus();
- const infra = getDashboardInfraPill();
- strip.innerHTML = [
- ...vms.map(vm => `
-
-
- ${escapeHTML(vm.name)}
- ${vm.active ? 'online' : 'offline'}
-
- `),
- `
-
-
- ${escapeHTML(infra.name)}
- ${escapeHTML(infra.label)}
-
- `,
- ].join('');
- }
-
- function handleDashboardWS(msg) {
- if (msg.type !== 'message') return;
-
- const eventType = getEnvelopeType(msg.data);
-
- if (eventType === 'openclaw.snapshot') {
- mergeOpenClawEvents([msg.data]);
- renderDashVMStrip();
- return;
- }
- if (eventType === 'swarm.snapshot') {
- mergeSwarmSnapshot(msg.data);
- renderDashVMStrip();
- return;
- }
- if (eventType === 'swarm.service.snapshot') {
- mergeSwarmServiceSnapshot(msg.data);
- renderDashVMStrip();
- return;
- }
-
- if (dashboardState.summary) {
- if (eventType === 'session.start') dashboardState.summary.active_sessions++;
- if (eventType === 'session.end') dashboardState.summary.active_sessions = Math.max(0, dashboardState.summary.active_sessions - 1);
- if (eventType === 'run.start') dashboardState.summary.runs_today++;
- if (eventType === 'error') dashboardState.summary.errors_today++;
- if (eventType === 'span.end') {
- const attrs = getEnvelopeAttributes(msg.data);
- if (attrs.span_kind === 'tool') dashboardState.summary.tool_calls_today++;
- }
- if (eventType === 'run.end') {
- const payload = getEnvelopePayload(msg.data);
- const usage = payload.usage || {};
- dashboardState.summary.tokens_today = (dashboardState.summary.tokens_today || 0) + (usage.total_tokens || 0);
- dashboardState.summary.cost_today = (dashboardState.summary.cost_today || 0) + (usage.total_cost || 0);
- // Update rolling avg duration
- if (payload.duration_ms) {
- const runs = dashboardState.summary.runs_today || 1;
- const prev = dashboardState.summary.avg_duration_ms || 0;
- dashboardState.summary.avg_duration_ms = prev + (payload.duration_ms - prev) / runs;
- }
- }
- renderSummaryCards();
- }
-
- if (!isDashboardFeedEvent(msg.data)) {
- if (dashboardState.timeseries && dashboardState.window === '1h') {
- appendToCurrentBucket(msg.data);
- }
- return;
- }
-
- if (addDashboardRecentEvent(msg.data)) {
- tallyTool(msg.data);
- tallyModel(msg.data);
-
- if (!_dashFeedRenderTimer) {
- _dashFeedRenderTimer = requestAnimationFrame(() => {
- _dashFeedRenderTimer = null;
- renderDashFeed();
- renderDashTopTools();
- renderDashTopModels();
- });
- }
- }
-
- if (dashboardState.timeseries && dashboardState.window === '1h') {
- appendToCurrentBucket(msg.data);
- }
- }
-
- function tallyTool(evt) {
- const eventType = getEnvelopeType(evt);
- if (eventType === 'span.end') {
- const attrs = getEnvelopeAttributes(evt);
- if (attrs.span_kind === 'tool') {
- const name = attrs.name || 'unknown';
- dashboardState.toolCounts[name] = (dashboardState.toolCounts[name] || 0) + 1;
- }
- }
- }
-
- function tallyModel(evt) {
- const eventType = getEnvelopeType(evt);
- const payload = getEnvelopePayload(evt);
-
- if (eventType === 'run.end' && payload.model) {
- const name = String(payload.model);
- dashboardState.modelCounts[name] = (dashboardState.modelCounts[name] || 0) + 1;
- return;
- }
-
- if (eventType === 'metric.snapshot' && payload.metrics && payload.metrics.model) {
- const name = String(payload.metrics.model);
- if (!dashboardState.modelCounts[name]) {
- dashboardState.modelCounts[name] = 1;
- }
- }
- }
-
- function renderSummaryCards() {
- const s = dashboardState.summary;
- if (!s) return;
-
- animateCounter('dash-active', s.active_sessions);
- animateCounter('dash-runs', s.runs_today);
- animateCounter('dash-tools', s.tool_calls_today);
- animateCounter('dash-errors', s.errors_today);
-
- // Sub-line: framework breakdown for active sessions
- const fws = Object.keys(s.by_framework || {});
- if (fws.length > 0) {
- const sub = document.getElementById('dash-active-sub');
- if (sub) sub.textContent = fws.map(f => `${f} ${(s.by_framework[f].runs || 0)}`).join(' · ');
- }
-
- const errEl = document.getElementById('dash-errors');
- if (errEl) {
- errEl.classList.toggle('has-errors', s.errors_today > 0);
- }
-
- // Metrics strip
- animateCounter('dash-tokens-today', formatTokenCount(s.tokens_today || 0));
- animateCounter('dash-cost-today', s.cost_today ? formatCost(s.cost_today) : '$0.0000');
- animateCounter('dash-avg-duration', s.avg_duration_ms ? formatDuration(s.avg_duration_ms) : '-');
-
- const errorRateEl = document.getElementById('dash-error-rate');
- if (errorRateEl) {
- const totalOps = (s.runs_today || 0) + (s.tool_calls_today || 0);
- const rate = totalOps > 0 ? ((s.errors_today || 0) / totalOps * 100) : 0;
- animateCounter('dash-error-rate', rate.toFixed(1) + '%');
- errorRateEl.classList.toggle('alert', rate > 5);
- }
- }
-
- async function loadTimeseries() {
- try {
- // Destroy chart so it's recreated with new window scale
- if (dashboardChart) {
- dashboardChart.destroy();
- dashboardChart = null;
- }
- dashboardState.chartCursorIndex = null;
- const cachedWin = tryParseJSON(localStorage.getItem('agentmon:dash:ts:' + dashboardState.window));
- if (cachedWin) { dashboardState.timeseries = cachedWin; renderTimeseriesChart(); renderDashSparklines(); renderRightPanel(); }
- const data = await api('/v1/stats/timeseries?window=' + dashboardState.window);
- if (!isCurrentPath('/')) return;
- dashboardState.timeseries = data;
- localStorage.setItem('agentmon:dash:ts:' + dashboardState.window, JSON.stringify(data));
- renderTimeseriesChart();
- renderDashSparklines();
- renderRightPanel();
- } catch (e) {
- console.error('Failed to load timeseries:', e);
- }
- }
-
- function getDashboardBucketIntervalMS() {
- const bucket = dashboardState && dashboardState.timeseries ? dashboardState.timeseries.bucket : '';
- switch (bucket) {
- case '1m': return 60 * 1000;
- case '5m': return 5 * 60 * 1000;
- case '15m': return 15 * 60 * 1000;
- case '1h': return 60 * 60 * 1000;
- default: return 60 * 1000;
- }
- }
-
- function formatBucketLabel(ts) {
- const start = new Date(ts);
- if (Number.isNaN(start.getTime())) return '-';
- const end = new Date(start.getTime() + getDashboardBucketIntervalMS());
- const sameDay = start.toLocaleDateString() === end.toLocaleDateString();
- const startLabel = start.toLocaleString([], {
- month: 'short',
- day: 'numeric',
- hour: 'numeric',
- minute: '2-digit',
- });
- const endLabel = end.toLocaleString([], sameDay
- ? { hour: 'numeric', minute: '2-digit' }
- : { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
- return startLabel + ' to ' + endLabel;
- }
-
- function getDashboardChartStats() {
- const ts = dashboardState.timeseries;
- if (!ts || !ts.series || ts.series.length === 0) return null;
-
- const totals = ts.series.map(b => (b.runs || 0) + (b.tools || 0) + (b.errors || 0));
- const sum = values => values.reduce((acc, value) => acc + value, 0);
-
- let peakIndex = 0;
- for (let i = 1; i < totals.length; i++) {
- if (totals[i] > totals[peakIndex]) peakIndex = i;
- }
-
- return {
- totalRuns: sum(ts.series.map(b => b.runs || 0)),
- totalTools: sum(ts.series.map(b => b.tools || 0)),
- totalErrors: sum(ts.series.map(b => b.errors || 0)),
- totalEvents: sum(totals),
- peakIndex,
- peakTotal: totals[peakIndex] || 0,
- bucketCount: ts.series.length,
- };
- }
-
- function buildChartData() {
- const ts = dashboardState.timeseries;
- if (!ts || !ts.series || ts.series.length === 0) return null;
-
- const timestamps = ts.series.map(b => Math.floor(new Date(b.ts).getTime() / 1000));
- const runs = ts.series.map(b => b.runs || 0);
- const tools = ts.series.map(b => b.tools || 0);
- const errors = ts.series.map(b => b.errors || 0);
- const totals = ts.series.map((b, i) => runs[i] + tools[i] + errors[i]);
-
- if (dashboardState.chartMode === 'lines') {
- return [timestamps, totals, runs, tools, errors];
- }
-
- const stackedTools = tools.map((value, i) => value + errors[i]);
- return [timestamps, totals, stackedTools, errors];
- }
-
- function renderDashboardChartInsights() {
- const container = document.getElementById('dash-chart-insights');
- if (!container) return;
-
- const stats = getDashboardChartStats();
- if (!stats) {
- container.innerHTML = '';
- return;
- }
-
- const peakBucket = dashboardState.timeseries.series[stats.peakIndex];
- container.innerHTML = `
- window total${escapeHTML(formatCount(stats.totalEvents))}
- peak bucket${escapeHTML(formatCount(stats.peakTotal))}${escapeHTML(formatBucketLabel(peakBucket.ts))}
- mix${escapeHTML(formatCount(stats.totalRuns))}r / ${escapeHTML(formatCount(stats.totalTools))}t / ${escapeHTML(formatCount(stats.totalErrors))}e
- bucket${escapeHTML(dashboardState.timeseries.bucket || '-')}${escapeHTML(String(stats.bucketCount))} points
- `;
- }
-
- function renderDashboardChartHover(idx) {
- const container = document.getElementById('dash-chart-hover');
- if (!container) return;
-
- const ts = dashboardState.timeseries;
- if (!ts || !ts.series || ts.series.length === 0) {
- container.innerHTML = '';
- return;
- }
-
- const safeIdx = Number.isInteger(idx) && idx >= 0 && idx < ts.series.length ? idx : ts.series.length - 1;
- const bucket = ts.series[safeIdx];
- const prev = safeIdx > 0 ? ts.series[safeIdx - 1] : null;
- const total = (bucket.runs || 0) + (bucket.tools || 0) + (bucket.errors || 0);
- const prevTotal = prev ? (prev.runs || 0) + (prev.tools || 0) + (prev.errors || 0) : 0;
- const delta = total - prevTotal;
- const deltaLabel = (delta > 0 ? '+' : '') + delta;
- const bucketLabel = safeIdx === ts.series.length - 1 ? 'Latest bucket' : 'Selected bucket';
-
- container.innerHTML = `
-
-
-
${escapeHTML(bucketLabel)}
-
${escapeHTML(formatBucketLabel(bucket.ts))}
-
-
- Total
- ${escapeHTML(formatCount(total))}
-
-
-
-
Runs${escapeHTML(formatCount(bucket.runs || 0))}
-
Tools${escapeHTML(formatCount(bucket.tools || 0))}
-
Errors${escapeHTML(formatCount(bucket.errors || 0))}
-
Delta${escapeHTML(deltaLabel)}
-
- `;
- }
-
- function renderTimeseriesChart() {
- const container = document.getElementById('dash-chart');
- if (!container || !dashboardState.timeseries) return;
-
- const data = buildChartData();
- renderDashboardChartInsights();
- renderDashboardChartHover(dashboardState.chartCursorIndex);
- if (!data) {
- container.innerHTML = 'No data for this window
';
- return;
- }
-
- // If chart already exists, just update the data
- if (dashboardChart) {
- dashboardChart.setData(data);
- return;
- }
-
- container.innerHTML = '';
-
- const width = container.clientWidth || 600;
- const height = 200;
-
- const commonSeries = [
- {},
- {
- label: 'Total',
- stroke: '#f8fafc',
- width: 1.5,
- dash: [6, 4],
- points: { show: false },
- },
- ];
-
- const lineSeries = [
- ...commonSeries,
- {
- label: 'Runs',
- stroke: '#34d399',
- width: 1.75,
- fill: 'rgba(52, 211, 153, 0.08)',
- },
- {
- label: 'Tools',
- stroke: '#22d3ee',
- width: 1.75,
- fill: 'rgba(34, 211, 238, 0.08)',
- },
- {
- label: 'Errors',
- stroke: '#f87171',
- width: 1.75,
- fill: 'rgba(248, 113, 113, 0.08)',
- },
- ];
-
- const stackedSeries = [
- ...commonSeries,
- {
- label: 'Tools+Errors',
- stroke: 'rgba(34, 211, 238, 0.85)',
- width: 1.25,
- points: { show: false },
- },
- {
- label: 'Errors',
- stroke: '#f87171',
- width: 1.25,
- points: { show: false },
- fill: 'rgba(248, 113, 113, 0.18)',
- },
- ];
-
- const opts = {
- width,
- height,
- cursor: { show: true },
- hooks: {
- setCursor: [
- u => {
- dashboardState.chartCursorIndex = Number.isInteger(u.cursor.idx) ? u.cursor.idx : null;
- renderDashboardChartHover(dashboardState.chartCursorIndex);
- },
- ],
- },
- scales: {
- x: { time: true },
- y: { auto: true, min: 0 },
- },
- axes: [
- {
- stroke: '#4e6070',
- grid: { stroke: 'rgba(28, 38, 55, 0.6)', width: 1 },
- ticks: { stroke: 'rgba(28, 38, 55, 0.6)', width: 1 },
- font: '11px Fira Code',
- },
- {
- stroke: '#4e6070',
- grid: { stroke: 'rgba(28, 38, 55, 0.6)', width: 1 },
- ticks: { stroke: 'rgba(28, 38, 55, 0.6)', width: 1 },
- font: '11px Fira Code',
- size: 50,
- },
- ],
- series: dashboardState.chartMode === 'lines' ? lineSeries : stackedSeries,
- bands: dashboardState.chartMode === 'lines'
- ? []
- : [
- { series: [1, 2], fill: 'rgba(52, 211, 153, 0.18)' },
- { series: [2, 3], fill: 'rgba(34, 211, 238, 0.18)' },
- ],
- };
-
- dashboardChart = new uPlot(opts, data, container);
-
- if (dashboardResizeObserver) {
- dashboardResizeObserver.disconnect();
- }
- dashboardResizeObserver = new ResizeObserver(entries => {
- for (const entry of entries) {
- if (dashboardChart) {
- dashboardChart.setSize({ width: entry.contentRect.width, height: 200 });
- }
- }
- });
- dashboardResizeObserver.observe(container);
- }
-
- function appendToCurrentBucket(evt) {
- const ts = dashboardState.timeseries;
- if (!ts || !ts.series || ts.series.length === 0) return;
-
- const now = Math.floor(Date.now() / 60000) * 60000;
- const last = ts.series[ts.series.length - 1];
- const lastTs = new Date(last.ts).getTime();
-
- let bucket;
- if (Math.abs(now - lastTs) < 60000) {
- bucket = last;
- } else {
- bucket = { ts: new Date(now).toISOString(), runs: 0, tools: 0, errors: 0, tokens: 0, input_tokens: 0, output_tokens: 0, cost: 0, avg_duration_ms: 0 };
- ts.series.push(bucket);
- }
-
- const eventType = getEnvelopeType(evt);
- if (eventType === 'run.start') bucket.runs++;
- if (eventType === 'error') bucket.errors++;
- if (eventType === 'span.end') {
- const attrs = getEnvelopeAttributes(evt);
- if (attrs.span_kind === 'tool') bucket.tools++;
- }
- if (eventType === 'run.end') {
- const payload = getEnvelopePayload(evt);
- const usage = payload.usage || {};
- bucket.tokens = (bucket.tokens || 0) + (usage.total_tokens || 0);
- bucket.input_tokens = (bucket.input_tokens || 0) + (usage.input_tokens || 0);
- bucket.output_tokens = (bucket.output_tokens || 0) + (usage.output_tokens || 0);
- bucket.cost = (bucket.cost || 0) + (usage.total_cost || 0);
- if (payload.duration_ms) {
- const runCount = bucket.runs || 1;
- const prev = bucket.avg_duration_ms || 0;
- bucket.avg_duration_ms = prev + (payload.duration_ms - prev) / runCount;
- }
- }
-
- dashboardState.chartCursorIndex = ts.series.length - 1;
- renderTimeseriesChart();
- renderDashSparklines();
- }
-
- function renderRightPanel() {
- const mode = dashboardState && dashboardState.rightPanelMode;
- if (mode === 'tokens') {
- renderTokenPanel();
- } else if (mode === 'latency') {
- renderLatencyPanel();
- } else {
- renderFrameworkBars();
- }
- }
-
- function renderTokenPanel() {
- const container = document.getElementById('dash-right-panel');
- if (!container) return;
- const s = dashboardState.summary;
- const ts = dashboardState.timeseries;
-
- const totalTokens = s ? (s.tokens_today || 0) : 0;
- const inputTokens = ts && ts.series ? ts.series.reduce((acc, b) => acc + (b.input_tokens || 0), 0) : 0;
- const outputTokens = ts && ts.series ? ts.series.reduce((acc, b) => acc + (b.output_tokens || 0), 0) : 0;
- const totalCost = s ? (s.cost_today || 0) : 0;
- const maxIO = Math.max(inputTokens, outputTokens, 1);
-
- container.innerHTML = `
-
-
-
Total tokens today
-
${escapeHTML(formatTokenCount(totalTokens))}
-
-
-
-
Input
-
-
${escapeHTML(formatTokenCount(inputTokens))}
-
-
-
Output
-
-
${escapeHTML(formatTokenCount(outputTokens))}
-
-
-
- Est. cost today
- ${escapeHTML(totalCost ? formatCost(totalCost) : '$0.0000')}
-
-
- `;
- }
-
- function renderLatencyPanel() {
- const container = document.getElementById('dash-right-panel');
- if (!container) return;
- const ts = dashboardState.timeseries;
-
- if (!ts || !ts.series || ts.series.length === 0) {
- container.innerHTML = 'No latency data
';
- return;
- }
-
- const durSeries = ts.series.map(b => b.avg_duration_ms || 0).filter(v => v > 0);
- if (durSeries.length === 0) {
- container.innerHTML = 'No run latency recorded yet
';
- return;
- }
-
- const avg = durSeries.reduce((a, b) => a + b, 0) / durSeries.length;
- const min = Math.min(...durSeries);
- const max = Math.max(...durSeries);
- const maxBar = max || 1;
-
- container.innerHTML = `
-
-
-
- Min
- ${escapeHTML(formatDuration(min))}
-
-
- Avg
- ${escapeHTML(formatDuration(avg))}
-
-
- Max
- ${escapeHTML(formatDuration(max))}
-
-
-
- ${durSeries.map((v, i) => {
- const pct = (v / maxBar * 100).toFixed(1);
- const label = ts.series.filter(b => b.avg_duration_ms > 0)[i];
- const title = label ? formatBucketLabel(label.ts) + ': ' + formatDuration(v) : formatDuration(v);
- return `
`;
- }).join('')}
-
-
Avg run duration per bucket (${escapeHTML(ts.bucket || '-')})
-
- `;
- }
-
- function renderFrameworkBars() {
- const container = document.getElementById('dash-right-panel');
- if (!container || !dashboardState.summary) return;
-
- const byFw = dashboardState.summary.by_framework || {};
- const entries = Object.entries(byFw).sort((a, b) => {
- const totalA = a[1].runs + a[1].tools + a[1].errors;
- const totalB = b[1].runs + b[1].tools + b[1].errors;
- return totalB - totalA;
- });
-
- if (entries.length === 0) {
- container.innerHTML = 'No framework data
';
- return;
- }
-
- const maxTotal = Math.max(...entries.map(([, s]) => s.runs + s.tools + s.errors));
-
- container.innerHTML = '' + entries.map(([name, stats]) => {
- const total = stats.runs + stats.tools + stats.errors;
- const pct = maxTotal > 0 ? (total / maxTotal * 100) : 0;
- const cssClass = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
- return `
-
-
- ${escapeHTML(name)}
- ${total} events
-
-
-
- `;
- }).join('') + '
';
- }
-
- function renderDashFeedItem(evt) {
- const eventType = getEnvelopeType(evt);
- const correlation = getEnvelopeCorrelation(evt);
- const vmName = getVMName(evt);
- const vmClass = getVMClassName(vmName);
- const source = getEnvelopeSource(evt);
- const framework = source.framework || '';
- const tag = framework
- ? `${escapeHTML(framework)}`
- : '';
- const sessionID = correlation.session_id || '';
- const clickableClass = sessionID ? ' timeline-event-link' : '';
- const attrs = sessionID
- ? ` role="link" tabindex="0" data-session-id="${escapeHTML(sessionID)}"`
- : '';
-
- return `
-
-
- ${getEventBody(evt)}
-
- `;
- }
-
- function renderDashFeed() {
- const feed = document.getElementById('dash-feed');
- if (!feed) return;
-
- const recent = dashboardState.recentEvents.slice(-DASH_RECENT_EVENTS_LIMIT).reverse();
- if (recent.length === 0) {
- feed.innerHTML = 'Waiting for events...
';
- return;
- }
- feed.innerHTML = recent.map(renderDashFeedItem).join('');
- feed.querySelectorAll('.timeline-event-link').forEach(item => {
- const sessionID = item.dataset.sessionId || '';
- if (!sessionID) return;
- item.addEventListener('click', () => navigate('/sessions/' + sessionID));
- item.addEventListener('keydown', event => {
- if (event.key !== 'Enter' && event.key !== ' ') {
- return;
- }
- event.preventDefault();
- navigate('/sessions/' + sessionID);
- });
- });
- }
-
- function renderDashTopTools() {
- const list = document.getElementById('dash-top-tools');
- if (!list) return;
-
- const topTools = Object.entries(dashboardState.toolCounts)
- .sort((a, b) => b[1] - a[1])
- .slice(0, 10);
-
- if (topTools.length === 0) {
- list.innerHTML = 'No tool data yet';
- return;
- }
-
- const maxCount = topTools[0]?.[1] || 1;
- list.innerHTML = topTools.map(([name, count]) => {
- const pct = (count / maxCount * 100).toFixed(1);
- return `
-
-
-
-
- `;
- }).join('');
- }
-
- function renderDashTopModels() {
- const list = document.getElementById('dash-top-models');
- if (!list) return;
-
- const topModels = Object.entries(dashboardState.modelCounts)
- .sort((a, b) => b[1] - a[1])
- .slice(0, 10);
-
- if (topModels.length === 0) {
- list.innerHTML = 'No model data yet';
- return;
- }
-
- const maxCount = topModels[0]?.[1] || 1;
- list.innerHTML = topModels.map(([name, count]) => {
- const pct = (count / maxCount * 100).toFixed(1);
- return `
-
-
-
-
- `;
- }).join('');
- }
-
+ connectWS();
+ updateWSIndicator();
route();
-})();
+});
diff --git a/cmd/web-ui/static/index.html b/cmd/web-ui/static/index.html
index c144bb6..b7365a5 100644
--- a/cmd/web-ui/static/index.html
+++ b/cmd/web-ui/static/index.html
@@ -17,14 +17,12 @@
-
+