Add live agent views and improve Codex monitoring

This commit is contained in:
William Valentin
2026-03-20 13:59:51 -07:00
parent a87bbc6983
commit 687a7aa79d
10 changed files with 1408 additions and 184 deletions
+659 -86
View File
@@ -475,6 +475,7 @@
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';
@@ -483,6 +484,10 @@
<a href="/sessions" class="back-link">&larr; Back to Sessions</a>
<div class="page-header">
<h2>Session <span style="font-family:var(--font-mono);font-size:1.1rem;color:var(--accent)">${escapeHTML(sessionID.substring(0, 16))}...</span></h2>
<div class="session-status-line">
<span class="fw-dot ${escapeHTML((s.framework || 'unknown').replace(/[^a-z0-9-]/g, '-'))} ${active ? 'active' : 'ended'}"></span>
<span class="session-status-text">${active ? 'Active' : 'Ended'}</span>
</div>
<div class="meta-tiles">
<div class="meta-tile">
<div class="meta-tile-label">Started</div>
@@ -516,32 +521,14 @@
<th>Started</th>
</tr>
</thead>
<tbody>
${runs.map(r => {
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-/, '')) : '-';
return `
<tr class="clickable" data-run="${escapeHTML(r.run_id)}">
<td class="id-cell">${escapeHTML(r.run_id.substring(0, 12))}...</td>
<td>${statusIcon(r.status)}</td>
<td><span class="model-badge">${modelLabel}</span></td>
<td>${r.tool_count || 0}</td>
<td>${r.span_count}</td>
<td>${escapeHTML(runDuration)}</td>
<td>${escapeHTML(new Date(r.started_at).toLocaleTimeString())}</td>
</tr>
`;
}).join('') || '<tr><td colspan="7" class="empty-state">No runs</td></tr>'}
<tbody id="session-runs-body">
${renderSessionRunsRows(runs)}
</tbody>
</table>
</div>
`;
document.querySelectorAll('tr.clickable').forEach(row => {
row.addEventListener('click', () => navigate('/runs/' + row.dataset.run));
});
bindSessionRunRows();
document.querySelector('.back-link').addEventListener('click', e => {
e.preventDefault();
@@ -564,30 +551,11 @@
const data = await api('/v1/sessions/' + sessionID);
const runs = data.runs || [];
const tbody = document.querySelector('#app table tbody');
const tbody = document.getElementById('session-runs-body');
if (!tbody) return;
tbody.innerHTML = runs.map(r => {
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-/, '')) : '-';
return `
<tr class="clickable" data-run="${escapeHTML(r.run_id)}">
<td class="id-cell">${escapeHTML(r.run_id.substring(0, 12))}...</td>
<td>${statusIcon(r.status)}</td>
<td><span class="model-badge">${modelLabel}</span></td>
<td>${r.tool_count || 0}</td>
<td>${r.span_count}</td>
<td>${escapeHTML(runDuration)}</td>
<td>${escapeHTML(new Date(r.started_at).toLocaleTimeString())}</td>
</tr>
`;
}).join('') || '<tr><td colspan="7" class="empty-state">No runs</td></tr>';
tbody.querySelectorAll('tr.clickable').forEach(row => {
row.addEventListener('click', () => navigate('/runs/' + row.dataset.run));
});
tbody.innerHTML = renderSessionRunsRows(runs);
bindSessionRunRows();
const countSpan = document.querySelector('.section-title .count');
if (countSpan) countSpan.textContent = runs.length;
@@ -667,6 +635,96 @@
});
}
function renderSessionRunsRows(runs) {
if (!runs || runs.length === 0) {
return '<tr><td colspan="7" class="empty-state">No runs</td></tr>';
}
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 ? `
<div class="session-run-spans">
${spans.map(sp => {
const body = getSessionSpanSummary(sp);
return `
<div class="session-span-pill ${escapeHTML(sp.kind || 'unknown')}">
<span class="session-span-name">${escapeHTML(sp.name || sp.kind || 'span')}</span>
<span class="session-span-meta">${escapeHTML(body)}</span>
</div>
`;
}).join('')}
</div>
` : '<div class="empty-state" style="padding:0.5rem 0">No spans yet</div>';
return `
<tr class="clickable expandable-run" data-run="${escapeHTML(r.run_id)}" data-index="${i}">
<td class="id-cell"><span class="expand-icon"></span>${escapeHTML(r.run_id.substring(0, 12))}...</td>
<td>${statusIcon(r.status)}</td>
<td><span class="model-badge">${modelLabel}</span></td>
<td>${r.tool_count || 0}</td>
<td>${r.span_count}</td>
<td>${escapeHTML(runDuration)}</td>
<td>${escapeHTML(new Date(r.started_at).toLocaleTimeString())}</td>
</tr>
<tr class="span-detail-row" data-index="${i}" style="display:none">
<td colspan="7">
<div class="session-run-detail">
<div class="section-title" style="margin-bottom:0.5rem">Spans <span class="count">${spans.length}</span></div>
${spansHTML}
</div>
</td>
</tr>
`;
}).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));
});
}
async function renderInfrastructure() {
app.innerHTML = '<div class="page-header"><h2>Infrastructure</h2></div><p class="empty-state">Loading...</p>';
@@ -1020,13 +1078,12 @@
}
function createAgentsState() {
function agentBucket() {
return { sessions: {}, operations: {}, events: [], eventIDs: new Set() };
}
return {
agents: { zap: agentBucket(), orb: agentBucket(), sun: agentBucket() },
agents: {},
stats: { messages: 0, tools: 0, errors: 0, toolCounts: {} },
dbStats: { messages: 0, tools: 0, errors: 0 },
viewMode: 'overview',
selectedAgentKey: '',
timerInterval: null,
};
}
@@ -1044,9 +1101,134 @@
});
}
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 ['zap', 'orb', 'sun'].includes(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) {
const name = getVMName(evt).toLowerCase();
return agentsState.agents[name] || null;
return ensureAgentBucket(evt);
}
function processAgentEvent(evt) {
@@ -1056,6 +1238,8 @@
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) };
@@ -1069,6 +1253,7 @@
type: 'span',
name: attrs.name || attrs.span_kind || 'unknown',
kind: attrs.span_kind || '',
subType: attrs.type || '',
startedAt: new Date(getEnvelopeTS(evt)).getTime() || Date.now(),
};
}
@@ -1102,8 +1287,21 @@
function getAgentDisplayOps(agent) {
const now = Date.now();
const ops = Object.values(agent.operations).filter(op => (now - op.startedAt) < 300000);
const hasTools = ops.some(op => op.kind === 'tool');
return hasTools ? ops.filter(op => op.kind === 'tool') : ops;
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);
}
async function renderAgents() {
@@ -1113,36 +1311,38 @@
<div class="page-header">
<h2>Agents <span class="live-indicator"><span class="live-dot"></span>Live</span></h2>
</div>
<div class="agents-summary-row" id="agents-summary"></div>
<div class="agent-lanes" id="agents-lanes">
<div class="agent-lane"><div class="agent-lane-header"><div class="agent-lane-name">ZAP</div></div><div class="agent-lane-events"><p class="empty-state">Loading...</p></div></div>
<div class="agent-lane"><div class="agent-lane-header"><div class="agent-lane-name">ORB</div></div><div class="agent-lane-events"><p class="empty-state">Loading...</p></div></div>
<div class="agent-lane"><div class="agent-lane-header"><div class="agent-lane-name">SUN</div></div><div class="agent-lane-events"><p class="empty-state">Loading...</p></div></div>
<div class="agents-toolbar">
<div class="view-toggle" id="agents-view-toggle">
<button class="view-toggle-btn active" data-mode="overview" type="button">Overview</button>
<button class="view-toggle-btn" data-mode="live" type="button">Live</button>
</div>
</div>
<div class="agents-summary-row" id="agents-summary"></div>
<div id="agents-content"><p class="empty-state">Loading...</p></div>
`;
bindAgentViewToggle();
try {
const [snapshots, events, summaryData] = await Promise.all([
api('/v1/events?event_type=openclaw.snapshot&limit=100').catch(() => ({ events: [] })),
api('/v1/events?framework=openclaw&limit=200'),
api('/v1/events?limit=300'),
api('/v1/stats/summary').catch(() => null),
]);
if (!isCurrentPath('/agents')) return;
if (summaryData) {
const fw = (summaryData.by_framework || {}).openclaw || {};
agentsState.dbStats.messages = fw.runs || 0;
agentsState.dbStats.tools = fw.tools || 0;
agentsState.dbStats.errors = fw.errors || 0;
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 || []).slice().reverse());
renderAgentLanes();
renderAgentSummary();
addAgentEvents((events.events || []).filter(isAgentTimelineEvent).slice().reverse());
renderAgentsContent();
} catch (e) {
document.getElementById('agents-lanes').innerHTML =
document.getElementById('agents-content').innerHTML =
`<p class="empty-state">Error loading agent activity: ${escapeHTML(e.message)}</p>`;
}
@@ -1150,29 +1350,96 @@
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) {
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();
}
}
}
function renderAgentLanes() {
const contentEl = document.getElementById('agents-content');
if (!contentEl) return;
contentEl.innerHTML = '<div class="agent-lanes" id="agents-lanes"></div>';
const lanesEl = document.getElementById('agents-lanes');
if (!lanesEl) return;
const vmNames = ['zap', 'orb', 'sun'];
const agentKeys = getSortedAgentKeys();
lanesEl.innerHTML = vmNames.map(name => {
const agent = agentsState.agents[name];
const vmStatus = getVMStatus().find(v => v.name === name);
const isOnline = vmStatus && vmStatus.active;
if (agentKeys.length === 0) {
lanesEl.innerHTML = '<p class="empty-state">No recent agent activity</p>';
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 ? `<div class="active-ops">${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 `
<div class="active-op${stale ? ' stale' : ''}">
<div class="active-op${stale ? ' stale' : ''}${kindClass}">
<span class="active-op-dot"></span>
<span class="active-op-name">${escapeHTML(op.name)}</span>
<span class="active-op-time" data-start="${op.startedAt}">${formatElapsed(elapsed)}</span>
@@ -1183,7 +1450,6 @@
const recent = agent.events.slice(-40).reverse();
const eventsHTML = recent.length > 0 ? recent.map(evt => {
const eventType = getEnvelopeType(evt);
const vmClass = getVMClassName(name);
const details = getEventDetails(evt);
const detailHTML = details ? `<div class="timeline-detail">${escapeHTML(details)}</div>` : '';
const expandHTML = details ? '<button class="timeline-expand-hint" type="button">details</button>' : '';
@@ -1202,11 +1468,14 @@
}).join('') : '<p class="empty-state">No recent activity</p>';
return `
<div class="agent-lane">
<div class="agent-lane" data-agent-key="${escapeHTML(key)}">
<div class="agent-lane-header">
<div class="agent-lane-name">
<div>
<div class="agent-lane-name">
<span class="agent-lane-dot ${isOnline ? 'online' : 'offline'}"></span>
${escapeHTML(name.toUpperCase())}
${escapeHTML(agent.name || key)}
</div>
<div class="agent-lane-meta">${escapeHTML(agent.framework || 'unknown')}${agent.host && agent.host !== agent.name ? ' · ' + escapeHTML(agent.host) : ''}</div>
</div>
<span class="agent-lane-status${statusClass}">${statusText}</span>
</div>
@@ -1215,8 +1484,14 @@
</div>`;
}).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', () => {
button.addEventListener('click', event => {
event.stopPropagation();
button.parentElement.classList.toggle('expanded');
});
});
@@ -1232,17 +1507,316 @@
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 = `
<div class="agents-summary-stat">Live Agents <span class="value">${liveAgents}</span></div>
<div class="agents-summary-stat">Active Subagents <span class="value">${liveSubagents}</span></div>
<div class="agents-summary-stat">Runs Today <span class="value">${s.messages}</span></div>
<div class="agents-summary-stat">Tool Calls <span class="value">${s.tools}</span></div>
<div class="agents-summary-stat">Errors <span class="value">${s.errors}</span></div>
`;
}
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 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(`<div class="live-detail-row"><span class="k">input</span><span class="v">${escapeHTML(typeof payload.input === 'string' ? payload.input : JSON.stringify(payload.input))}</span></div>`);
}
if (payload.result_preview) {
parts.push(`<div class="live-detail-row"><span class="k">result</span><span class="v">${escapeHTML(String(payload.result_preview))}</span></div>`);
}
}
if ((eventType === 'span.start' || eventType === 'span.end') && (attrs.span_kind === 'agent' || attrs.type === 'subagent')) {
if (payload.prompt_preview) {
parts.push(`<div class="live-detail-row"><span class="k">prompt</span><span class="v">${escapeHTML(String(payload.prompt_preview))}</span></div>`);
}
if (payload.usage && payload.usage.total_tokens !== undefined) {
parts.push(`<div class="live-detail-row"><span class="k">tokens</span><span class="v">${escapeHTML(formatCount(payload.usage.total_tokens))}</span></div>`);
}
if (payload.usage && payload.usage.total_cost !== undefined) {
parts.push(`<div class="live-detail-row"><span class="k">cost</span><span class="v">${escapeHTML(formatCost(payload.usage.total_cost))}</span></div>`);
}
}
if (eventType === 'run.start') {
const preview = payload.prompt_preview || payload.message_preview || payload.message || '';
if (preview) {
parts.push(`<div class="live-detail-row"><span class="k">prompt</span><span class="v">${escapeHTML(String(preview))}</span></div>`);
}
}
if (eventType === 'run.end') {
if (payload.model) {
parts.push(`<div class="live-detail-row"><span class="k">model</span><span class="v">${escapeHTML(String(payload.model))}</span></div>`);
}
if (payload.usage && payload.usage.total_tokens !== undefined) {
parts.push(`<div class="live-detail-row"><span class="k">tokens</span><span class="v">${escapeHTML(formatCount(payload.usage.total_tokens))}</span></div>`);
}
if (payload.duration_ms !== undefined) {
parts.push(`<div class="live-detail-row"><span class="k">duration</span><span class="v">${escapeHTML(formatDuration(payload.duration_ms))}</span></div>`);
}
}
if (eventType === 'metric.snapshot' && payload.metrics) {
if (payload.metrics.model) {
parts.push(`<div class="live-detail-row"><span class="k">model</span><span class="v">${escapeHTML(String(payload.metrics.model))}</span></div>`);
}
if (payload.metrics.usage && payload.metrics.usage.total_tokens !== undefined) {
parts.push(`<div class="live-detail-row"><span class="k">tokens</span><span class="v">${escapeHTML(formatCount(payload.metrics.usage.total_tokens))}</span></div>`);
}
if (payload.metrics.usage && payload.metrics.usage.total_cost !== undefined) {
parts.push(`<div class="live-detail-row"><span class="k">cost</span><span class="v">${escapeHTML(formatCost(payload.metrics.usage.total_cost))}</span></div>`);
}
}
if (eventType === 'error') {
const errPayload = payload.error || {};
if (errPayload.type) {
parts.push(`<div class="live-detail-row"><span class="k">type</span><span class="v">${escapeHTML(String(errPayload.type))}</span></div>`);
}
}
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(`<div class="live-detail-row ids"><span class="k">ids</span><span class="v">${escapeHTML(ids.join(' · '))}</span></div>`);
}
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 renderAgentsLive() {
const contentEl = document.getElementById('agents-content');
if (!contentEl) return;
const agentKeys = getSortedAgentKeys();
const selectedKey = ensureSelectedAgentKey();
if (!selectedKey || agentKeys.length === 0) {
contentEl.innerHTML = '<p class="empty-state">No recent agent activity</p>';
return;
}
const selected = agentsState.agents[selectedKey];
const summary = getAgentLiveSummary(selected);
const recent = selected.events.slice(-80).reverse();
const runGroups = groupAgentEventsByRun(recent);
contentEl.innerHTML = `
<div class="agents-live-layout">
<aside class="agents-live-sidebar">
<div class="section-title">Agents</div>
<div class="agent-picker" id="agent-picker">
${agentKeys.map(key => {
const agent = agentsState.agents[key];
const active = key === selectedKey ? ' active' : '';
const online = isAgentOnline(agent) ? 'online' : 'offline';
const sessions = Object.keys(agent.sessions).length;
const ops = getAgentDisplayOps(agent).length;
return `
<button class="agent-picker-item${active}" data-agent-key="${escapeHTML(key)}" type="button">
<span class="agent-picker-dot ${online}"></span>
<span class="agent-picker-main">
<span class="agent-picker-name">${escapeHTML(getAgentLabel(agent))}</span>
<span class="agent-picker-meta">${escapeHTML(agent.framework || 'unknown')} · ${sessions} sessions · ${ops} ops</span>
</span>
</button>`;
}).join('')}
</div>
</aside>
<section class="agents-live-main">
<div class="agents-live-header">
<div>
<div class="agent-lane-name"><span class="agent-lane-dot ${isAgentOnline(selected) ? 'online' : 'offline'}"></span>${escapeHTML(getAgentLabel(selected))}</div>
<div class="agent-lane-meta">${escapeHTML(selected.framework || 'unknown')}${selected.host && selected.host !== selected.name ? ' · ' + escapeHTML(selected.host) : ''}</div>
</div>
<div class="agents-live-badges">
<span class="agents-live-badge">${summary.sessionIDs.length} sessions</span>
<span class="agents-live-badge">${summary.activeSubagents.length} subagents</span>
<span class="agents-live-badge">${summary.activeTools.length} tools</span>
</div>
</div>
<div class="agents-live-cards">
<div class="agents-live-card">
<div class="agents-live-card-title">Current State</div>
<div class="live-kv"><span>Active subagents</span><strong>${escapeHTML(summary.activeSubagents.map(op => op.name).join(', ') || '-')}</strong></div>
<div class="live-kv"><span>Active tools</span><strong>${escapeHTML(summary.activeTools.map(op => op.name).join(', ') || '-')}</strong></div>
<div class="live-kv"><span>Latest prompt</span><strong>${escapeHTML(summary.latestPrompt || '-')}</strong></div>
<div class="live-kv"><span>Run status</span><strong>${escapeHTML(summary.latestRunStatus || 'in_progress')}</strong></div>
<div class="live-kv"><span>Model</span><strong>${escapeHTML(summary.latestModel || '-')}</strong></div>
<div class="live-kv"><span>Last error</span><strong>${escapeHTML(summary.latestError || '-')}</strong></div>
</div>
<div class="agents-live-card">
<div class="agents-live-card-title">Usage</div>
<div class="live-kv"><span>Total tokens</span><strong>${escapeHTML(formatCount(summary.latestUsage && summary.latestUsage.total_tokens))}</strong></div>
<div class="live-kv"><span>Input tokens</span><strong>${escapeHTML(formatCount(summary.latestUsage && summary.latestUsage.input_tokens))}</strong></div>
<div class="live-kv"><span>Output tokens</span><strong>${escapeHTML(formatCount(summary.latestUsage && summary.latestUsage.output_tokens))}</strong></div>
<div class="live-kv"><span>Total cost</span><strong>${escapeHTML(formatCost(summary.latestUsage && summary.latestUsage.total_cost))}</strong></div>
</div>
<div class="agents-live-card">
<div class="agents-live-card-title">Session Context</div>
<div class="live-kv"><span>Session IDs</span><strong>${escapeHTML(summary.sessionIDs.join(', ') || '-')}</strong></div>
<div class="live-kv"><span>Window input</span><strong>${escapeHTML(formatCount(summary.latestContextWindow && summary.latestContextWindow.input_tokens))}</strong></div>
<div class="live-kv"><span>Window output</span><strong>${escapeHTML(formatCount(summary.latestContextWindow && summary.latestContextWindow.output_tokens))}</strong></div>
<div class="live-kv"><span>Remaining</span><strong>${escapeHTML(formatCount(summary.latestContextWindow && (summary.latestContextWindow.tokens_remaining ?? summary.latestContextWindow.used_tokens)))}</strong></div>
</div>
</div>
<div class="agents-live-timeline">
${runGroups.length > 0 ? runGroups.map(group => `
<section class="live-run-group">
<div class="live-run-group-header">
<div class="live-run-group-title">${escapeHTML(getRunGroupLabel(group.runID, group.events))}</div>
<div class="live-run-group-meta">
<span>${escapeHTML(group.runID ? `run ${group.runID.slice(0, 12)}...` : 'session-only')}</span>
<span>${escapeHTML(group.subagents.size > 0 ? `${group.subagents.size} subagents` : '0 subagents')}</span>
<span>${escapeHTML(group.tools.size > 0 ? `${group.tools.size} tools` : '0 tools')}</span>
</div>
</div>
<div class="live-run-events">
${group.events.map(evt => `
<div class="timeline-event live-event">
<div class="timeline-event-header">
${getEventIcon(getEnvelopeType(evt))}
<span class="timeline-event-type">${escapeHTML(getEventLabel(getEnvelopeType(evt)))}</span>
<span class="timeline-event-time">${escapeHTML(new Date(getEnvelopeTS(evt)).toLocaleTimeString())}</span>
</div>
${getEventBody(evt)}
<div class="live-detail-grid">${buildLiveEventContext(evt)}</div>
</div>`).join('')}
</div>
</section>
`).join('') : '<p class="empty-state">No recent activity</p>'}
</div>
</section>
</div>
`;
contentEl.querySelectorAll('[data-agent-key]').forEach(button => {
button.addEventListener('click', () => selectAgent(button.dataset.agentKey || '', 'live'));
});
}
function renderAgentVMStrip() {
// VM online/offline state is shown in each lane header via getVMStatus().
// Re-render lanes to pick up the updated openclawState.
renderAgentLanes();
renderAgentsContent();
}
function handleAgentsWS(msg) {
@@ -1251,12 +1825,10 @@
const eventType = getEnvelopeType(msg.data);
if (eventType === 'openclaw.snapshot') {
mergeOpenClawEvents([msg.data]);
renderAgentLanes();
renderAgentsContent();
return;
}
const framework = getEnvelopeSource(msg.data).framework || msg.data.source_framework;
if (framework !== 'openclaw') return;
if (!isAgentTimelineEvent(msg.data)) return;
if (eventType === 'run.start') agentsState.dbStats.messages++;
else if (eventType === 'span.end') {
@@ -1265,8 +1837,7 @@
} else if (eventType === 'error') agentsState.dbStats.errors++;
addAgentEvents([msg.data]);
renderAgentLanes();
renderAgentSummary();
renderAgentsContent();
}
function updateAgentTimers() {
@@ -1360,7 +1931,7 @@
}
function getVMName(evt) {
return getEnvelopeSource(evt).client_id || evt.client_id || 'unknown';
return getAgentIdentity(evt).name || 'unknown';
}
function getVMClassName(vmName) {
@@ -1379,11 +1950,13 @@
const duration = payload.duration_ms !== undefined && payload.duration_ms !== null
? ` <span class="timeline-duration">${escapeHTML(formatDuration(payload.duration_ms))}</span>`
: '';
return `<div class="timeline-event-body tool-name">${escapeHTML(name)}${duration}</div>`;
const detailClass = attrs.span_kind === 'agent' || attrs.type === 'subagent' ? ' subagent-name' : ' tool-name';
const prefix = attrs.span_kind === 'agent' || attrs.type === 'subagent' ? 'subagent ' : '';
return `<div class="timeline-event-body${detailClass}">${escapeHTML(prefix + name)}${duration}</div>`;
}
if (eventType === 'run.start') {
const preview = payload.message_preview || payload.message || '';
const preview = payload.prompt_preview || payload.message_preview || payload.message || '';
if (!preview) {
return '';
}