172 lines
5.4 KiB
JavaScript
172 lines
5.4 KiB
JavaScript
/**
|
|
* Event Log — real-time SSE events with filtering
|
|
*/
|
|
|
|
const EVENT_CATEGORIES = {
|
|
'session.created': 'lifecycle',
|
|
'execution.started': 'lifecycle',
|
|
'execution.completed': 'lifecycle',
|
|
'agent.delta': 'content',
|
|
'tool.started': 'tool',
|
|
'tool.completed': 'tool',
|
|
'subagent.assigned': 'orchestration',
|
|
'error': 'error',
|
|
'keepalive': 'keepalive',
|
|
};
|
|
|
|
const CATEGORY_LABELS = ['lifecycle', 'content', 'tool', 'orchestration', 'error'];
|
|
|
|
let entriesEl;
|
|
let activeFilters = new Set(CATEGORY_LABELS);
|
|
let stickToBottom = true;
|
|
let events = [];
|
|
|
|
export function initEventLog() {
|
|
const panel = document.getElementById('event-log-panel');
|
|
|
|
panel.innerHTML = `
|
|
<div class="event-log-header" id="event-log-toggle">
|
|
<span class="chevron">▼</span>
|
|
<span>Event Log</span>
|
|
<span class="toolbar-spacer"></span>
|
|
<span class="text-muted text-sm" id="event-count">0 events</span>
|
|
<button class="btn btn-sm" id="btn-clear-events" style="margin-left:8px">Clear</button>
|
|
<button class="btn btn-sm" id="btn-export-events">Export</button>
|
|
</div>
|
|
<div class="event-log-filters" id="event-filters"></div>
|
|
<div class="event-log-entries" id="event-entries"></div>
|
|
`;
|
|
|
|
entriesEl = document.getElementById('event-entries');
|
|
|
|
// Toggle panel
|
|
document.getElementById('event-log-toggle').addEventListener('click', () => {
|
|
panel.classList.toggle('open');
|
|
});
|
|
|
|
// Filters
|
|
const filtersEl = document.getElementById('event-filters');
|
|
for (const cat of CATEGORY_LABELS) {
|
|
const btn = document.createElement('button');
|
|
btn.className = `event-filter-btn active`;
|
|
btn.textContent = cat;
|
|
btn.dataset.category = cat;
|
|
btn.addEventListener('click', () => {
|
|
if (activeFilters.has(cat)) {
|
|
activeFilters.delete(cat);
|
|
btn.classList.remove('active');
|
|
} else {
|
|
activeFilters.add(cat);
|
|
btn.classList.add('active');
|
|
}
|
|
renderEvents();
|
|
});
|
|
filtersEl.appendChild(btn);
|
|
}
|
|
|
|
// Auto-scroll detection
|
|
entriesEl.addEventListener('scroll', () => {
|
|
const { scrollTop, scrollHeight, clientHeight } = entriesEl;
|
|
stickToBottom = scrollHeight - scrollTop - clientHeight < 20;
|
|
});
|
|
|
|
// Clear
|
|
document.getElementById('btn-clear-events').addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
clearEvents();
|
|
});
|
|
|
|
// Export
|
|
document.getElementById('btn-export-events').addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const blob = new Blob([JSON.stringify(events, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `events-${Date.now()}.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
}
|
|
|
|
export function addEvent(event) {
|
|
events.push(event);
|
|
document.getElementById('event-count').textContent = `${events.length} events`;
|
|
|
|
const type = event.type || '';
|
|
const category = EVENT_CATEGORIES[type] || 'lifecycle';
|
|
|
|
if (!activeFilters.has(category)) return;
|
|
if (category === 'keepalive') return; // Hide keepalives by default
|
|
|
|
const entry = createEventEntry(event, type, category);
|
|
entriesEl.appendChild(entry);
|
|
|
|
if (stickToBottom) {
|
|
entriesEl.scrollTop = entriesEl.scrollHeight;
|
|
}
|
|
}
|
|
|
|
function createEventEntry(event, type, category) {
|
|
const el = document.createElement('div');
|
|
el.className = 'event-entry';
|
|
|
|
const time = event.timestamp
|
|
? new Date(event.timestamp).toLocaleTimeString('en-US', { hour12: false, fractionalSecondDigits: 3 })
|
|
: '--:--:--';
|
|
|
|
const data = event.data || {};
|
|
const summary = summarizeEventData(type, data);
|
|
|
|
el.innerHTML = `
|
|
<span class="event-time">${time}</span>
|
|
<span class="event-type-badge ${category}">${type.split('.').pop()}</span>
|
|
<span class="event-data" title="Click to expand">${summary}</span>
|
|
`;
|
|
|
|
// Toggle expand
|
|
const dataEl = el.querySelector('.event-data');
|
|
dataEl.addEventListener('click', () => {
|
|
if (dataEl.classList.contains('expanded')) {
|
|
dataEl.classList.remove('expanded');
|
|
dataEl.textContent = summary;
|
|
} else {
|
|
dataEl.classList.add('expanded');
|
|
dataEl.textContent = JSON.stringify(data, null, 2);
|
|
}
|
|
});
|
|
|
|
return el;
|
|
}
|
|
|
|
function summarizeEventData(type, data) {
|
|
switch (type) {
|
|
case 'agent.delta': return `[${data.agent || '?'}] "${(data.delta || '').substring(0, 60)}..."`;
|
|
case 'tool.started': return `Tool: ${data.tool}`;
|
|
case 'tool.completed': return `Tool: ${data.tool} → ${data.status}`;
|
|
case 'subagent.assigned': return `Step ${data.step}/${data.total_steps}: ${data.agent} — ${(data.description || '').substring(0, 50)}`;
|
|
case 'execution.started': return `Session: ${(data.session_id || '').substring(0, 12)}`;
|
|
case 'execution.completed': return `Steps: ${data.steps_completed} — ${data.status}`;
|
|
case 'error': return JSON.stringify(data);
|
|
default: return JSON.stringify(data).substring(0, 80);
|
|
}
|
|
}
|
|
|
|
function renderEvents() {
|
|
entriesEl.innerHTML = '';
|
|
for (const event of events) {
|
|
const type = event.type || '';
|
|
const category = EVENT_CATEGORIES[type] || 'lifecycle';
|
|
if (!activeFilters.has(category)) continue;
|
|
if (category === 'keepalive') continue;
|
|
entriesEl.appendChild(createEventEntry(event, type, category));
|
|
}
|
|
}
|
|
|
|
export function clearEvents() {
|
|
events = [];
|
|
if (entriesEl) entriesEl.innerHTML = '';
|
|
const countEl = document.getElementById('event-count');
|
|
if (countEl) countEl.textContent = '0 events';
|
|
}
|