// ── 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 eventKindClass(eventType) {
if (eventType === 'run.start' || eventType === 'run.end') return 'evt-run';
if (eventType === 'span.start' || eventType === 'span.end') return 'evt-span';
if (eventType === 'error') return 'evt-error';
if (eventType === 'session.start' || eventType === 'session.end') return 'evt-session';
return 'evt-other';
}
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'
: op.kind === 'run' ? ' run' : '';
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 `
`;
}).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 `
${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'));
});
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;
}
}