// ── agents.js — Agents page ─────────────────────────────── import { escapeHTML, formatDuration, formatCount, formatCost, formatTokenCount, formatElapsed, getEnvelopeType, getEnvelopePayload, getEnvelopeAttributes, getEnvelopeCorrelation, getEnvelopeTS, getRecordID, getEventIcon, getEventLabel, getEventBody, getEventDetails, isAgentTimelineEvent, isCurrentPath, getAgentIdentity, normalizeAgentKey, agentsSkeleton, } from '../utils.js'; import { subscribeWS } from '../ws.js'; import { agentsState, resetAgentsState, mergeOpenClawEvents, getVMStatus, isOpenClawVM, isAgentOnline, openclawState, } from '../state.js'; import { app, navigate, renderBreadcrumbs, isRouteCurrent } from '../router.js'; import { api } from '../api.js'; // ── Module-level state ─────────────────────────────────── let agentsUnsubscribe = null; let _agentsRenderTimer = null; // ── Private helpers ────────────────────────────────────── 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 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; 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 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

'; 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 `
${getEventIcon(eventType)} ${escapeHTML(getEventLabel(eventType))} ${escapeHTML(new Date(getEnvelopeTS(evt)).toLocaleTimeString())}
${getEventBody(evt)} ${expandHTML} ${detailHTML}
`; }).join('') : '

No recent activity

'; return `
${escapeHTML(agent.name || key)}
${escapeHTML(agent.framework || 'unknown')}${agent.host && agent.host !== agent.name ? ' · ' + escapeHTML(agent.host) : ''}
${buildAgentActivityBars(agent)}
${statusText}
${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 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 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 => { 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 `
${icon} ${escapeHTML(kindLabel)} ${escapeHTML(op.name)} ${isEnded ? formatElapsed(elapsed) : `${formatElapsed(elapsed)}`} ${navigableRunID ? '' : ''}
${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 = `
${escapeHTML(getAgentLabel(selected))}
${escapeHTML(selected.framework || 'unknown')}${selected.host && selected.host !== selected.name ? ' · ' + escapeHTML(selected.host) : ''}
${summary.sessionIDs.length} sessions ${summary.activeSubagents.length} subagents ${summary.activeTools.length} tools
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 => `
${escapeHTML(getRunGroupLabel(group.runID, group.events))}
${escapeHTML(group.runID ? `run ${group.runID.slice(0, 12)}...` : 'session-only')} ${escapeHTML(group.subagents.size > 0 ? `${group.subagents.size} subagents` : '0 subagents')} ${escapeHTML(group.tools.size > 0 ? `${group.tools.size} tools` : '0 tools')}
${group.events.map(evt => `
${getEventIcon(getEnvelopeType(evt))} ${escapeHTML(getEventLabel(getEnvelopeType(evt)))} ${escapeHTML(new Date(getEnvelopeTS(evt)).toLocaleTimeString())}
${getEventBody(evt)}
${buildLiveEventContext(evt)}
`).join('')}
`).join('') : '

No recent activity

'}
`; contentEl.querySelectorAll('[data-agent-key]').forEach(button => { button.addEventListener('click', () => selectAgent(button.dataset.agentKey || '', 'live')); }); 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 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 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) { 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(); } } } // ── Exports ────────────────────────────────────────────── export async function renderAgents(initialKey, routeToken) { resetAgentsState(); 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 ((routeToken && !isRouteCurrent(routeToken)) || !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()); if (initialKey && agentsState.agents[initialKey]) { agentsState.selectedAgentKey = initialKey; renderBreadcrumbs(); } renderAgentsContent(); } catch (e) { if (routeToken && !isRouteCurrent(routeToken)) return; document.getElementById('agents-content').innerHTML = `

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

`; } if (!routeToken || isRouteCurrent(routeToken)) { agentsState.timerInterval = setInterval(updateAgentTimers, 1000); agentsUnsubscribe = subscribeWS(handleAgentsWS); } } export function selectAgent(key, nextMode) { if (!key || !agentsState.agents[key]) return; agentsState.selectedAgentKey = key; if (nextMode) { agentsState.viewMode = nextMode; } const newPath = '/agents/' + encodeURIComponent(key); if (window.location.pathname !== newPath) { history.pushState(null, '', newPath); renderBreadcrumbs(); } renderAgentsContent(); if (agentsState.viewMode === 'live') { void loadSelectedAgentLiveData(); } } export function cleanup() { if (agentsState.timerInterval) { clearInterval(agentsState.timerInterval); agentsState.timerInterval = null; } if (agentsUnsubscribe) { agentsUnsubscribe(); agentsUnsubscribe = null; } if (_agentsRenderTimer) { cancelAnimationFrame(_agentsRenderTimer); _agentsRenderTimer = null; } }