agentflow-dashboard 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/dist/{chunk-RWNVLZU7.js → chunk-GA2Y6E62.js} +1362 -335
- package/dist/cli.cjs +1352 -341
- package/dist/cli.js +1 -1
- package/dist/client/assets/index-BQUye2Lc.js +50 -0
- package/dist/client/assets/index-Ds_npIxI.css +1 -0
- package/dist/client/dashboard.js +3113 -0
- package/dist/client/index.html +13 -0
- package/dist/index.cjs +1352 -341
- package/dist/index.js +1 -1
- package/dist/public/dashboard.js +1363 -545
- package/dist/public/index.html +13 -0
- package/dist/server.cjs +1352 -341
- package/dist/server.js +1 -1
- package/package.json +12 -4
- package/public/dashboard.js +1363 -545
- package/public/index.html +13 -0
- package/dist/public/debug.html +0 -43
- package/public/debug.html +0 -43
|
@@ -0,0 +1,3113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentFlow Dashboard — Production monitoring UI for AI agent infrastructure.
|
|
3
|
+
* Connects to the Express backend via REST + WebSocket.
|
|
4
|
+
* Handles 860+ traces efficiently with DOM limiting and lazy loading.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function escapeHtml(str) {
|
|
8
|
+
if (typeof str !== 'string') return str == null ? '' : String(str);
|
|
9
|
+
return str
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, ''');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class AgentFlowDashboard {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.ws = null;
|
|
20
|
+
this.reconnectAttempts = 0;
|
|
21
|
+
this.maxReconnectAttempts = 20;
|
|
22
|
+
this.reconnectDelay = 1000;
|
|
23
|
+
|
|
24
|
+
this.traces = [];
|
|
25
|
+
this.stats = null;
|
|
26
|
+
this.processHealth = null;
|
|
27
|
+
this.selectedTrace = null;
|
|
28
|
+
this.selectedTraceData = null;
|
|
29
|
+
this.activeTab = 'timeline';
|
|
30
|
+
this.searchFilter = '';
|
|
31
|
+
this.statusFilter = 'all';
|
|
32
|
+
this.timeRangeFilter = 'all';
|
|
33
|
+
this.activityFilter = 'all';
|
|
34
|
+
this.isLive = true;
|
|
35
|
+
|
|
36
|
+
this.cy = null;
|
|
37
|
+
|
|
38
|
+
this.init();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Initialization
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
init() {
|
|
45
|
+
this.connectWebSocket();
|
|
46
|
+
this.loadInitialData();
|
|
47
|
+
this.loadProcessHealth();
|
|
48
|
+
this.setupEventListeners();
|
|
49
|
+
this._healthInterval = setInterval(() => this.loadProcessHealth(), 10000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// WebSocket with auto-reconnect + exponential backoff
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
connectWebSocket() {
|
|
56
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
57
|
+
const wsUrl = `${protocol}//${window.location.host}`;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
this.ws = new WebSocket(wsUrl);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error('WebSocket creation failed:', e);
|
|
63
|
+
this.updateConnectionStatus(false);
|
|
64
|
+
this.attemptReconnect();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.ws.onopen = () => {
|
|
69
|
+
this.reconnectAttempts = 0;
|
|
70
|
+
this.updateConnectionStatus(true);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
this.ws.onmessage = (event) => {
|
|
74
|
+
try {
|
|
75
|
+
var message = JSON.parse(event.data);
|
|
76
|
+
this.handleWebSocketMessage(message);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error('WS parse error:', e);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.ws.onclose = () => {
|
|
83
|
+
this.updateConnectionStatus(false);
|
|
84
|
+
this.attemptReconnect();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
this.ws.onerror = () => {
|
|
88
|
+
this.updateConnectionStatus(false);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
attemptReconnect() {
|
|
93
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
|
94
|
+
this.reconnectAttempts++;
|
|
95
|
+
var delay = this.reconnectDelay * Math.min(1.5 ** (this.reconnectAttempts - 1), 30);
|
|
96
|
+
setTimeout(() => this.connectWebSocket(), delay);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
handleWebSocketMessage(msg) {
|
|
100
|
+
switch (msg.type) {
|
|
101
|
+
case 'init':
|
|
102
|
+
if (msg.data?.traces) this.traces = msg.data.traces;
|
|
103
|
+
if (msg.data?.stats) this.stats = msg.data.stats;
|
|
104
|
+
this.renderTraceList();
|
|
105
|
+
this.renderStatsOverview();
|
|
106
|
+
if (this.traces.length > 0 && !this.selectedTrace) {
|
|
107
|
+
this.selectTrace(this.traces[0].filename);
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case 'trace-added':
|
|
111
|
+
if (this.isLive) {
|
|
112
|
+
this.traces.unshift(msg.data);
|
|
113
|
+
this.renderTraceList();
|
|
114
|
+
this.refreshStats();
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
case 'trace-updated': {
|
|
118
|
+
var idx = this.traces.findIndex((t) => t.filename === msg.data.filename);
|
|
119
|
+
if (idx >= 0) this.traces[idx] = msg.data;
|
|
120
|
+
this.renderTraceList();
|
|
121
|
+
if (this.selectedTrace && this.selectedTrace.filename === msg.data.filename) {
|
|
122
|
+
this.selectedTrace = msg.data;
|
|
123
|
+
this.selectedTraceData = msg.data;
|
|
124
|
+
this.renderActiveTab();
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'stats-updated':
|
|
129
|
+
this.stats = msg.data;
|
|
130
|
+
this.renderStatsOverview();
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
updateConnectionStatus(connected) {
|
|
136
|
+
var dot = document.getElementById('connectionDot');
|
|
137
|
+
var text = document.getElementById('connectionText');
|
|
138
|
+
var liveInd = document.getElementById('liveIndicator');
|
|
139
|
+
if (connected) {
|
|
140
|
+
dot.className = 'status-dot connected';
|
|
141
|
+
text.textContent = 'Connected';
|
|
142
|
+
if (liveInd) liveInd.className = 'live-indicator active';
|
|
143
|
+
} else {
|
|
144
|
+
dot.className = 'status-dot';
|
|
145
|
+
text.textContent = 'Disconnected';
|
|
146
|
+
if (liveInd) liveInd.className = 'live-indicator';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Data loading
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
async loadInitialData() {
|
|
154
|
+
try {
|
|
155
|
+
var results = await Promise.all([fetch('/api/traces'), fetch('/api/stats')]);
|
|
156
|
+
if (results[0].ok) this.traces = await results[0].json();
|
|
157
|
+
if (results[1].ok) this.stats = await results[1].json();
|
|
158
|
+
this.renderTraceList();
|
|
159
|
+
this.renderStatsOverview();
|
|
160
|
+
// Auto-select first trace
|
|
161
|
+
if (this.traces.length > 0 && !this.selectedTrace) {
|
|
162
|
+
this.selectTrace(this.traces[0].filename);
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.error('Initial data load failed:', e);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async refreshStats() {
|
|
170
|
+
try {
|
|
171
|
+
var res = await fetch('/api/stats');
|
|
172
|
+
if (res.ok) {
|
|
173
|
+
this.stats = await res.json();
|
|
174
|
+
this.renderStatsOverview();
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error('Stats refresh failed:', e);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async loadTraceDetail(filename) {
|
|
182
|
+
try {
|
|
183
|
+
var res = await fetch(`/api/traces/${encodeURIComponent(filename)}`);
|
|
184
|
+
if (res.ok) {
|
|
185
|
+
this.selectedTraceData = await res.json();
|
|
186
|
+
this.renderActiveTab();
|
|
187
|
+
}
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error('Trace detail load failed:', e);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async loadProcessHealth() {
|
|
194
|
+
try {
|
|
195
|
+
var res = await fetch('/api/process-health');
|
|
196
|
+
if (!res.ok) return;
|
|
197
|
+
this.processHealth = await res.json();
|
|
198
|
+
this.renderProcessHealth();
|
|
199
|
+
} catch (_e) {
|
|
200
|
+
// silent — endpoint may not always be available
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Event listeners
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
setupEventListeners() {
|
|
208
|
+
// Tab switching
|
|
209
|
+
document.querySelectorAll('.tab').forEach((tab) => {
|
|
210
|
+
tab.addEventListener('click', () => {
|
|
211
|
+
this.activeTab = tab.dataset.tab;
|
|
212
|
+
document.querySelectorAll('.tab').forEach((t) => {
|
|
213
|
+
t.classList.remove('active');
|
|
214
|
+
});
|
|
215
|
+
tab.classList.add('active');
|
|
216
|
+
document.querySelectorAll('.tab-panel').forEach((p) => {
|
|
217
|
+
p.classList.remove('active');
|
|
218
|
+
});
|
|
219
|
+
document.getElementById(`panel-${this.activeTab}`).classList.add('active');
|
|
220
|
+
this.renderActiveTab();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Search
|
|
225
|
+
document.getElementById('traceSearch').addEventListener('input', (e) => {
|
|
226
|
+
this.searchFilter = e.target.value.toLowerCase();
|
|
227
|
+
this.renderTraceList();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Status filter dropdown
|
|
231
|
+
document.getElementById('statusFilter').addEventListener('change', (e) => {
|
|
232
|
+
this.statusFilter = e.target.value;
|
|
233
|
+
this.renderTraceList();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Time range filter dropdown
|
|
237
|
+
document.getElementById('timeRangeFilter').addEventListener('change', (e) => {
|
|
238
|
+
this.timeRangeFilter = e.target.value;
|
|
239
|
+
this.renderTraceList();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Activity filter dropdown (if exists)
|
|
243
|
+
var activityFilter = document.getElementById('activityFilter');
|
|
244
|
+
if (activityFilter) {
|
|
245
|
+
activityFilter.addEventListener('change', (e) => {
|
|
246
|
+
this.activityFilter = e.target.value;
|
|
247
|
+
this.renderTraceList();
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Toolbar buttons
|
|
252
|
+
document.getElementById('btnFit').addEventListener('click', () => {
|
|
253
|
+
if (this.cy) this.cy.fit(50);
|
|
254
|
+
});
|
|
255
|
+
document.getElementById('btnLayout').addEventListener('click', () => {
|
|
256
|
+
this.runCytoscapeLayout();
|
|
257
|
+
});
|
|
258
|
+
document.getElementById('btnExportPng').addEventListener('click', () => {
|
|
259
|
+
this.exportGraphPNG();
|
|
260
|
+
});
|
|
261
|
+
document.getElementById('btnRefresh').addEventListener('click', () => {
|
|
262
|
+
this.loadInitialData();
|
|
263
|
+
this.loadProcessHealth();
|
|
264
|
+
});
|
|
265
|
+
document.getElementById('btnPlayPause').addEventListener('click', () => {
|
|
266
|
+
this.isLive = !this.isLive;
|
|
267
|
+
var btn = document.getElementById('btnPlayPause');
|
|
268
|
+
btn.innerHTML = this.isLive ? '⏸' : '▶';
|
|
269
|
+
btn.title = this.isLive ? 'Pause live tail' : 'Resume live tail';
|
|
270
|
+
var liveInd = document.getElementById('liveIndicator');
|
|
271
|
+
if (this.isLive && this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
272
|
+
liveInd.className = 'live-indicator active';
|
|
273
|
+
} else {
|
|
274
|
+
liveInd.className = 'live-indicator';
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Node detail close
|
|
279
|
+
document.getElementById('nodeDetailClose').addEventListener('click', () => {
|
|
280
|
+
document.getElementById('nodeDetailPanel').classList.remove('active');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Trace list click delegation
|
|
284
|
+
document.getElementById('traceList').addEventListener('click', (e) => {
|
|
285
|
+
var item = e.target.closest('.session-item');
|
|
286
|
+
if (!item) return;
|
|
287
|
+
var filename = item.dataset.filename;
|
|
288
|
+
this.selectTrace(filename);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Auto-refresh stats every 30s
|
|
292
|
+
setInterval(() => {
|
|
293
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
294
|
+
this.refreshStats();
|
|
295
|
+
}
|
|
296
|
+
}, 30000);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Trace selection
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
selectTrace(filename) {
|
|
303
|
+
var trace = this.traces.find((t) => t.filename === filename);
|
|
304
|
+
if (!trace) return;
|
|
305
|
+
|
|
306
|
+
this.selectedTrace = trace;
|
|
307
|
+
this.selectedTraceData = trace;
|
|
308
|
+
|
|
309
|
+
// Reset agent-level caches when agent changes
|
|
310
|
+
if (this._processMapAgent !== trace.agentId) {
|
|
311
|
+
this._processMapAgent = null;
|
|
312
|
+
if (this._cyProcessMap) {
|
|
313
|
+
this._cyProcessMap.destroy();
|
|
314
|
+
this._cyProcessMap = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (this._agentTimelineAgent !== trace.agentId) {
|
|
318
|
+
this._agentTimelineAgent = null;
|
|
319
|
+
this._agentTimelineRendered = false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Update sidebar selection
|
|
323
|
+
document.querySelectorAll('.session-item').forEach((el) => {
|
|
324
|
+
el.classList.remove('active');
|
|
325
|
+
});
|
|
326
|
+
var activeEl = document.querySelector(`.session-item[data-filename="${CSS.escape(filename)}"]`);
|
|
327
|
+
if (activeEl) {
|
|
328
|
+
activeEl.classList.add('active');
|
|
329
|
+
// Scroll into view if needed
|
|
330
|
+
activeEl.scrollIntoView({ block: 'nearest' });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Load full detail
|
|
334
|
+
this.loadTraceDetail(filename);
|
|
335
|
+
|
|
336
|
+
// Render current tab immediately with list data
|
|
337
|
+
this.renderActiveTab();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Rendering: Stats overview bar
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
renderStatsOverview() {
|
|
344
|
+
if (!this.stats) return;
|
|
345
|
+
var s = this.stats;
|
|
346
|
+
document.getElementById('statAgents').textContent = s.totalAgents || 0;
|
|
347
|
+
document.getElementById('statExecutions').textContent = (
|
|
348
|
+
s.totalExecutions || 0
|
|
349
|
+
).toLocaleString();
|
|
350
|
+
var rate = Math.round((s.globalSuccessRate || 0) * 10) / 10;
|
|
351
|
+
var rateEl = document.getElementById('statSuccessRate');
|
|
352
|
+
rateEl.textContent = `${rate}%`;
|
|
353
|
+
rateEl.className = `metric-value ${rate >= 90 ? 'success' : rate >= 70 ? 'warning' : 'error'}`;
|
|
354
|
+
document.getElementById('statActive').textContent = s.activeAgents || 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Rendering: Process Health (above metrics, not a tab)
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
renderProcessHealth() {
|
|
361
|
+
var section = document.getElementById('processHealthSection');
|
|
362
|
+
if (!this.processHealth) {
|
|
363
|
+
section.style.display = 'none';
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
var r = this.processHealth;
|
|
368
|
+
var hasContent =
|
|
369
|
+
r.pidFile ||
|
|
370
|
+
r.systemd ||
|
|
371
|
+
r.workers ||
|
|
372
|
+
(r.orphans && r.orphans.length > 0) ||
|
|
373
|
+
(r.osProcesses && r.osProcesses.length > 0) ||
|
|
374
|
+
(r.problems && r.problems.length > 0);
|
|
375
|
+
if (!hasContent) {
|
|
376
|
+
section.style.display = 'none';
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
section.style.display = '';
|
|
381
|
+
var html = '<h4>Process Health</h4>';
|
|
382
|
+
|
|
383
|
+
// Render all discovered services (new multi-service format)
|
|
384
|
+
var services = r.services || [];
|
|
385
|
+
if (services.length > 0) {
|
|
386
|
+
for (var si = 0; si < services.length; si++) {
|
|
387
|
+
var svc = services[si];
|
|
388
|
+
html += '<div class="ph-service">';
|
|
389
|
+
html += `<div class="ph-service-name">${escapeHtml(svc.name)}</div>`;
|
|
390
|
+
|
|
391
|
+
// PID File for this service
|
|
392
|
+
if (svc.pidFile) {
|
|
393
|
+
var pf = svc.pidFile;
|
|
394
|
+
var cls = pf.alive && pf.matchesProcess ? 'ok' : pf.stale ? 'bad' : 'warn';
|
|
395
|
+
html += '<div class="ph-row">';
|
|
396
|
+
html += '<span class="ph-label">PID File</span>';
|
|
397
|
+
html += `<span class="ph-value ${cls}">`;
|
|
398
|
+
html += pf.pid ? `PID ${pf.pid}${pf.alive ? ' (alive)' : ' (dead)'}` : 'No PID';
|
|
399
|
+
html += '</span></div>';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Systemd for this service
|
|
403
|
+
if (svc.systemd) {
|
|
404
|
+
var sd = svc.systemd;
|
|
405
|
+
var sdCls = sd.activeState === 'active' ? 'ok' : sd.failed ? 'bad' : 'warn';
|
|
406
|
+
html += '<div class="ph-row">';
|
|
407
|
+
html += '<span class="ph-label">Systemd</span>';
|
|
408
|
+
html += `<span class="ph-value ${sdCls}">`;
|
|
409
|
+
html +=
|
|
410
|
+
escapeHtml(sd.unit) +
|
|
411
|
+
' \u2014 ' +
|
|
412
|
+
escapeHtml(sd.activeState) +
|
|
413
|
+
' (' +
|
|
414
|
+
escapeHtml(sd.subState) +
|
|
415
|
+
')';
|
|
416
|
+
if (sd.restarts > 0) html += ` [${sd.restarts} restarts]`;
|
|
417
|
+
html += '</span></div>';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Workers for this service
|
|
421
|
+
if (svc.workers?.workers) {
|
|
422
|
+
html += '<div class="ph-section">';
|
|
423
|
+
html += '<span class="ph-label">Workers</span>';
|
|
424
|
+
html += '<div class="process-grid">';
|
|
425
|
+
for (var i = 0; i < svc.workers.workers.length; i++) {
|
|
426
|
+
var worker = svc.workers.workers[i];
|
|
427
|
+
var statusCls = worker.alive ? 'ok' : worker.stale ? 'bad' : 'warn';
|
|
428
|
+
html += `<div class="worker-card ${statusCls}">`;
|
|
429
|
+
html += `<div class="worker-name">${escapeHtml(worker.name)}</div>`;
|
|
430
|
+
html += '<div class="worker-details">';
|
|
431
|
+
html += `<span>PID: ${worker.pid || '-'}</span>`;
|
|
432
|
+
html += `<span>${escapeHtml(worker.declaredStatus)}</span>`;
|
|
433
|
+
html += '</div></div>';
|
|
434
|
+
}
|
|
435
|
+
html += '</div></div>';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
html += '</div>'; // .ph-service
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
// Fallback: legacy single-service format
|
|
442
|
+
if (r.pidFile) {
|
|
443
|
+
var pf2 = r.pidFile;
|
|
444
|
+
var cls2 = pf2.alive && pf2.matchesProcess ? 'ok' : pf2.stale ? 'bad' : 'warn';
|
|
445
|
+
html += '<div class="ph-row">';
|
|
446
|
+
html += '<span class="ph-label">PID File</span>';
|
|
447
|
+
html += `<span class="ph-value ${cls2}">`;
|
|
448
|
+
html += pf2.pid ? `PID ${pf2.pid}${pf2.alive ? ' (alive)' : ' (dead)'}` : 'No PID';
|
|
449
|
+
html += '</span></div>';
|
|
450
|
+
}
|
|
451
|
+
if (r.systemd) {
|
|
452
|
+
var sd2 = r.systemd;
|
|
453
|
+
var sdCls2 = sd2.activeState === 'active' ? 'ok' : sd2.failed ? 'bad' : 'warn';
|
|
454
|
+
html += '<div class="ph-row">';
|
|
455
|
+
html += '<span class="ph-label">Systemd</span>';
|
|
456
|
+
html += `<span class="ph-value ${sdCls2}">`;
|
|
457
|
+
html += escapeHtml(sd2.unit) + ' \u2014 ' + escapeHtml(sd2.activeState) + ' (' + escapeHtml(sd2.subState) + ')';
|
|
458
|
+
html += '</span></div>';
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Agent Services - categorize processes and build tree
|
|
463
|
+
var categorized = this.categorizeProcesses(r.osProcesses || []);
|
|
464
|
+
|
|
465
|
+
if (categorized.agents.length > 0) {
|
|
466
|
+
html += '<div class="ph-section">';
|
|
467
|
+
html += `<span class="ph-label">Agent Services (${categorized.agents.length})</span>`;
|
|
468
|
+
|
|
469
|
+
// Build process tree for agents
|
|
470
|
+
var agentTree = this.buildProcessTree(categorized.agents);
|
|
471
|
+
html += this.renderProcessTree(agentTree, 'agent');
|
|
472
|
+
html += '</div>';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Infrastructure processes
|
|
476
|
+
if (categorized.infrastructure.length > 0) {
|
|
477
|
+
html += '<div class="ph-section">';
|
|
478
|
+
html += `<span class="ph-label">Infrastructure (${categorized.infrastructure.length})</span>`;
|
|
479
|
+
|
|
480
|
+
// Build process tree for infrastructure
|
|
481
|
+
var infraTree = this.buildProcessTree(categorized.infrastructure);
|
|
482
|
+
html += this.renderProcessTree(infraTree, 'infrastructure');
|
|
483
|
+
html += '</div>';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Orphaned processes (uncategorized)
|
|
487
|
+
var uncategorized = this.getUncategorizedOrphans(r.orphans || [], categorized);
|
|
488
|
+
if (uncategorized.length > 0) {
|
|
489
|
+
html += '<div class="ph-section">';
|
|
490
|
+
html += `<span class="ph-label">Orphans (${uncategorized.length})</span>`;
|
|
491
|
+
html += '<div class="orphan-list">';
|
|
492
|
+
for (var j = 0; j < uncategorized.length; j++) {
|
|
493
|
+
var o = uncategorized[j];
|
|
494
|
+
html += '<div class="orphan-row">';
|
|
495
|
+
html += `<span class="orphan-pid">PID ${o.pid}</span>`;
|
|
496
|
+
html +=
|
|
497
|
+
'<span class="orphan-resources">CPU: ' +
|
|
498
|
+
escapeHtml(o.cpu) +
|
|
499
|
+
'% | MEM: ' +
|
|
500
|
+
escapeHtml(o.mem) +
|
|
501
|
+
'%</span>';
|
|
502
|
+
html +=
|
|
503
|
+
'<span class="orphan-cmd" title="' +
|
|
504
|
+
escapeHtml(o.cmdline || o.command) +
|
|
505
|
+
'">' +
|
|
506
|
+
escapeHtml((o.command || '').substring(0, 60)) +
|
|
507
|
+
(o.command && o.command.length > 60 ? '...' : '') +
|
|
508
|
+
'</span>';
|
|
509
|
+
html += '</div>';
|
|
510
|
+
}
|
|
511
|
+
html += '</div></div>';
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Problems section
|
|
515
|
+
if (r.problems && r.problems.length > 0) {
|
|
516
|
+
html += '<div class="ph-section problems-section">';
|
|
517
|
+
html += '<span class="ph-label problems">Issues</span>';
|
|
518
|
+
html += '<div class="problems-list">';
|
|
519
|
+
for (var k = 0; k < r.problems.length; k++) {
|
|
520
|
+
html += `<div class="problem-item">⚠️ ${escapeHtml(r.problems[k])}</div>`;
|
|
521
|
+
}
|
|
522
|
+
html += '</div></div>';
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
section.innerHTML = html;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Helper to categorize processes with enhanced detection and tagging
|
|
529
|
+
categorizeProcesses(processes) {
|
|
530
|
+
var agents = [];
|
|
531
|
+
var infrastructure = [];
|
|
532
|
+
|
|
533
|
+
console.log('Categorizing', processes.length, 'processes');
|
|
534
|
+
|
|
535
|
+
for (var i = 0; i < processes.length; i++) {
|
|
536
|
+
var proc = processes[i];
|
|
537
|
+
var cmd = proc.command.toLowerCase();
|
|
538
|
+
var cmdline = (proc.cmdline || '').toLowerCase();
|
|
539
|
+
var service = this.detectAgentService(cmd, cmdline);
|
|
540
|
+
var component = this.detectInfrastructureComponent(cmd, cmdline);
|
|
541
|
+
var activityTag = this.getProcessActivityTag(cmd, cmdline, proc.pid);
|
|
542
|
+
|
|
543
|
+
if (component) {
|
|
544
|
+
infrastructure.push({
|
|
545
|
+
component: component,
|
|
546
|
+
pid: proc.pid,
|
|
547
|
+
cpu: proc.cpu,
|
|
548
|
+
mem: proc.mem,
|
|
549
|
+
elapsed: proc.elapsed,
|
|
550
|
+
ppid: proc.ppid,
|
|
551
|
+
cmdline: proc.cmdline || proc.command,
|
|
552
|
+
tag: activityTag,
|
|
553
|
+
});
|
|
554
|
+
console.log('Detected infrastructure:', proc.pid, component, 'tag:', activityTag);
|
|
555
|
+
} else if (service) {
|
|
556
|
+
agents.push({
|
|
557
|
+
service: service,
|
|
558
|
+
pid: proc.pid,
|
|
559
|
+
cpu: proc.cpu,
|
|
560
|
+
mem: proc.mem,
|
|
561
|
+
elapsed: proc.elapsed,
|
|
562
|
+
ppid: proc.ppid,
|
|
563
|
+
cmdline: proc.cmdline || proc.command,
|
|
564
|
+
tag: activityTag,
|
|
565
|
+
});
|
|
566
|
+
console.log('Detected agent:', proc.pid, service, 'tag:', activityTag);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
console.log('Categorization result:', {
|
|
571
|
+
agents: agents.length,
|
|
572
|
+
infrastructure: infrastructure.length,
|
|
573
|
+
});
|
|
574
|
+
return { agents: agents, infrastructure: infrastructure };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Enhanced agent service detection
|
|
578
|
+
detectAgentService(cmd, cmdline) {
|
|
579
|
+
// AgentFlow processes
|
|
580
|
+
if (cmdline.includes('agentflow-dashboard')) return 'AgentFlow Dashboard';
|
|
581
|
+
if (cmdline.includes('agentflow live')) return 'AgentFlow Live';
|
|
582
|
+
if (cmdline.includes('agentflow') && cmdline.includes('server')) return 'AgentFlow Server';
|
|
583
|
+
|
|
584
|
+
// OpenClaw ecosystem
|
|
585
|
+
if (cmdline.includes('openclaw-gateway')) return 'OpenClaw Gateway';
|
|
586
|
+
if (cmdline.includes('openclaw-agent')) return 'OpenClaw Agent';
|
|
587
|
+
if (cmdline.includes('openclaw') && cmdline.includes('worker')) return 'OpenClaw Worker';
|
|
588
|
+
if (cmdline.includes('claw-gateway')) return 'Claw Gateway';
|
|
589
|
+
|
|
590
|
+
// Alfred workers and processes
|
|
591
|
+
if (cmdline.includes('alfred') && cmdline.includes('curator')) return 'Alfred Curator';
|
|
592
|
+
if (cmdline.includes('alfred') && cmdline.includes('janitor')) return 'Alfred Janitor';
|
|
593
|
+
if (cmdline.includes('alfred') && cmdline.includes('distiller')) return 'Alfred Distiller';
|
|
594
|
+
if (cmdline.includes('alfred') && cmdline.includes('surveyor')) return 'Alfred Surveyor';
|
|
595
|
+
if (cmdline.includes('alfred') && (cmdline.includes('worker') || cmdline.includes('daemon')))
|
|
596
|
+
return 'Alfred Worker';
|
|
597
|
+
if (cmdline.includes('.alfred')) return 'Alfred Process';
|
|
598
|
+
|
|
599
|
+
// AI/ML agent frameworks
|
|
600
|
+
if (cmdline.includes('langchain') && cmdline.includes('agent')) return 'LangChain Agent';
|
|
601
|
+
if (cmdline.includes('crewai')) return 'CrewAI Agent';
|
|
602
|
+
if (cmdline.includes('autogen')) return 'AutoGen Agent';
|
|
603
|
+
if (cmdline.includes('mastra')) return 'Mastra Agent';
|
|
604
|
+
|
|
605
|
+
// Node.js/Python AI processes
|
|
606
|
+
if (
|
|
607
|
+
(cmd.includes('node') || cmd.includes('python')) &&
|
|
608
|
+
(cmdline.includes('agent') || cmdline.includes('ai') || cmdline.includes('llm'))
|
|
609
|
+
) {
|
|
610
|
+
return 'AI Agent Process';
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Temporal workflow processes
|
|
614
|
+
if (cmdline.includes('temporal') && (cmdline.includes('worker') || cmdline.includes('agent'))) {
|
|
615
|
+
return 'Temporal Agent';
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Generic agent indicators
|
|
619
|
+
if (
|
|
620
|
+
cmdline.includes('agent') &&
|
|
621
|
+
(cmdline.includes('server') || cmdline.includes('worker') || cmdline.includes('daemon'))
|
|
622
|
+
) {
|
|
623
|
+
return 'Agent Service';
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Enhanced infrastructure component detection
|
|
630
|
+
detectInfrastructureComponent(cmd, cmdline) {
|
|
631
|
+
// Debug logging
|
|
632
|
+
if (cmdline.includes('milvus')) {
|
|
633
|
+
console.log('Found Milvus process:', cmdline.substring(0, 100));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Vector databases
|
|
637
|
+
if (cmd.includes('milvus') || cmdline.includes('milvus')) return 'Milvus Vector DB';
|
|
638
|
+
if (cmd.includes('weaviate') || cmdline.includes('weaviate')) return 'Weaviate Vector DB';
|
|
639
|
+
if (cmd.includes('pinecone') || cmdline.includes('pinecone')) return 'Pinecone Vector DB';
|
|
640
|
+
if (cmd.includes('qdrant') || cmdline.includes('qdrant')) return 'Qdrant Vector DB';
|
|
641
|
+
|
|
642
|
+
// Traditional databases
|
|
643
|
+
if (cmd.includes('redis') || cmdline.includes('redis')) return 'Redis Cache';
|
|
644
|
+
if (cmd.includes('postgres') || cmdline.includes('postgres')) return 'PostgreSQL';
|
|
645
|
+
if (cmd.includes('mongodb') || cmdline.includes('mongo')) return 'MongoDB';
|
|
646
|
+
|
|
647
|
+
// Message queues and workflows
|
|
648
|
+
if (cmdline.includes('temporal') && cmdline.includes('server')) return 'Temporal Server';
|
|
649
|
+
if (cmd.includes('rabbitmq') || cmdline.includes('rabbitmq')) return 'RabbitMQ';
|
|
650
|
+
if (cmd.includes('kafka') || cmdline.includes('kafka')) return 'Apache Kafka';
|
|
651
|
+
|
|
652
|
+
// Observability
|
|
653
|
+
if (cmdline.includes('prometheus')) return 'Prometheus';
|
|
654
|
+
if (cmdline.includes('grafana')) return 'Grafana';
|
|
655
|
+
if (cmdline.includes('jaeger')) return 'Jaeger Tracing';
|
|
656
|
+
|
|
657
|
+
// Container/orchestration
|
|
658
|
+
if (cmd.includes('docker') && !cmdline.includes('agent')) return 'Docker';
|
|
659
|
+
if (cmd.includes('k3s') || cmd.includes('kubectl')) return 'Kubernetes';
|
|
660
|
+
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Get orphans that weren't categorized
|
|
665
|
+
getUncategorizedOrphans(orphans, categorized) {
|
|
666
|
+
var allCategorizedPids = categorized.agents
|
|
667
|
+
.concat(categorized.infrastructure)
|
|
668
|
+
.map((p) => p.pid);
|
|
669
|
+
return orphans.filter((o) => allCategorizedPids.indexOf(o.pid) === -1);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Build hierarchical process tree from flat process list
|
|
673
|
+
buildProcessTree(processes) {
|
|
674
|
+
var tree = [];
|
|
675
|
+
var processMap = {};
|
|
676
|
+
|
|
677
|
+
// Create a map of all processes
|
|
678
|
+
for (var i = 0; i < processes.length; i++) {
|
|
679
|
+
var proc = processes[i];
|
|
680
|
+
processMap[proc.pid] = {
|
|
681
|
+
process: proc,
|
|
682
|
+
children: [],
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Build parent-child relationships
|
|
687
|
+
for (var j = 0; j < processes.length; j++) {
|
|
688
|
+
var proc = processes[j];
|
|
689
|
+
if (proc.ppid && processMap[proc.ppid]) {
|
|
690
|
+
// This process has a parent in our categorized list
|
|
691
|
+
processMap[proc.ppid].children.push(processMap[proc.pid]);
|
|
692
|
+
} else {
|
|
693
|
+
// This is a root process (no parent in our list)
|
|
694
|
+
tree.push(processMap[proc.pid]);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return tree;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Render process tree with indentation
|
|
702
|
+
renderProcessTree(tree, type) {
|
|
703
|
+
var html = '<div class="process-tree">';
|
|
704
|
+
|
|
705
|
+
for (var i = 0; i < tree.length; i++) {
|
|
706
|
+
html += this.renderProcessNode(tree[i], type, 0);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
html += '</div>';
|
|
710
|
+
return html;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Render individual process node recursively
|
|
714
|
+
renderProcessNode(node, type, depth) {
|
|
715
|
+
var proc = node.process;
|
|
716
|
+
var indent = `padding-left: ${depth * 20}px;`;
|
|
717
|
+
var cpuNum = parseFloat(proc.cpu) || 0;
|
|
718
|
+
var cpuCls =
|
|
719
|
+
type === 'agent'
|
|
720
|
+
? cpuNum > 50
|
|
721
|
+
? 'high'
|
|
722
|
+
: cpuNum > 10
|
|
723
|
+
? 'medium'
|
|
724
|
+
: 'low'
|
|
725
|
+
: cpuNum > 20
|
|
726
|
+
? 'high'
|
|
727
|
+
: cpuNum > 5
|
|
728
|
+
? 'medium'
|
|
729
|
+
: 'low';
|
|
730
|
+
|
|
731
|
+
var serviceName = type === 'agent' ? proc.service : proc.component;
|
|
732
|
+
|
|
733
|
+
var html = `<div class="process-node ${type}-node ${cpuCls}" style="${indent}">`;
|
|
734
|
+
|
|
735
|
+
// Process icon and name
|
|
736
|
+
if (depth > 0) {
|
|
737
|
+
html += '<span class="tree-connector">└─ </span>';
|
|
738
|
+
}
|
|
739
|
+
html += '<div class="process-main">';
|
|
740
|
+
html +=
|
|
741
|
+
'<div class="process-name" title="' +
|
|
742
|
+
escapeHtml(proc.cmdline) +
|
|
743
|
+
'">' +
|
|
744
|
+
escapeHtml(serviceName) +
|
|
745
|
+
'</div>';
|
|
746
|
+
|
|
747
|
+
// Add activity tag
|
|
748
|
+
if (proc.tag && proc.tag !== 'other') {
|
|
749
|
+
html += `<span class="activity-tag tag-${proc.tag}">${proc.tag}</span>`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
html += '<div class="process-metrics">';
|
|
753
|
+
html += `<span class="pid-badge">PID: ${proc.pid}</span>`;
|
|
754
|
+
html += `<span class="cpu-badge">CPU: ${escapeHtml(proc.cpu)}%</span>`;
|
|
755
|
+
html += `<span class="mem-badge">MEM: ${escapeHtml(proc.mem)}%</span>`;
|
|
756
|
+
html += `<span class="time-badge">Up: ${escapeHtml(proc.elapsed)}</span>`;
|
|
757
|
+
html += '</div>';
|
|
758
|
+
html += '</div>';
|
|
759
|
+
html += '</div>';
|
|
760
|
+
|
|
761
|
+
// Render children recursively
|
|
762
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
763
|
+
html += this.renderProcessNode(node.children[i], type, depth + 1);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return html;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ---------------------------------------------------------------------------
|
|
770
|
+
// Rendering: Trace list (limit to 100 most recent for perf)
|
|
771
|
+
// ---------------------------------------------------------------------------
|
|
772
|
+
renderTraceList() {
|
|
773
|
+
var container = document.getElementById('traceList');
|
|
774
|
+
var countEl = document.getElementById('traceCount');
|
|
775
|
+
|
|
776
|
+
var filtered = this.traces;
|
|
777
|
+
|
|
778
|
+
// Search filter
|
|
779
|
+
if (this.searchFilter) {
|
|
780
|
+
var sf = this.searchFilter;
|
|
781
|
+
filtered = filtered.filter(
|
|
782
|
+
(t) =>
|
|
783
|
+
(t.agentId || '').toLowerCase().indexOf(sf) >= 0 ||
|
|
784
|
+
(t.name || '').toLowerCase().indexOf(sf) >= 0 ||
|
|
785
|
+
(t.filename || '').toLowerCase().indexOf(sf) >= 0,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Time range filter
|
|
790
|
+
if (this.timeRangeFilter !== 'all') {
|
|
791
|
+
var now = Date.now();
|
|
792
|
+
var cutoff;
|
|
793
|
+
switch (this.timeRangeFilter) {
|
|
794
|
+
case '1h':
|
|
795
|
+
cutoff = now - 3600000;
|
|
796
|
+
break;
|
|
797
|
+
case '24h':
|
|
798
|
+
cutoff = now - 86400000;
|
|
799
|
+
break;
|
|
800
|
+
case '7d':
|
|
801
|
+
cutoff = now - 604800000;
|
|
802
|
+
break;
|
|
803
|
+
default:
|
|
804
|
+
cutoff = 0;
|
|
805
|
+
}
|
|
806
|
+
filtered = filtered.filter((t) => {
|
|
807
|
+
var ts = t.timestamp ? new Date(t.timestamp).getTime() : t.startTime || t.lastModified || 0;
|
|
808
|
+
return ts >= cutoff;
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Status filter
|
|
813
|
+
if (this.statusFilter !== 'all') {
|
|
814
|
+
var statusTarget = this.statusFilter;
|
|
815
|
+
filtered = filtered.filter((t) => this.getTraceStatus(t) === statusTarget);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Activity filter
|
|
819
|
+
if (this.activityFilter && this.activityFilter !== 'all') {
|
|
820
|
+
var activityTarget = this.activityFilter;
|
|
821
|
+
filtered = filtered.filter((t) => this.getTraceActivity(t) === activityTarget);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
countEl.textContent = `${filtered.length} of ${this.traces.length} traces`;
|
|
825
|
+
|
|
826
|
+
// Render max 100 items for performance
|
|
827
|
+
var visible = filtered.slice(0, 100);
|
|
828
|
+
|
|
829
|
+
if (visible.length === 0) {
|
|
830
|
+
container.innerHTML =
|
|
831
|
+
'<div class="empty-state" style="height:120px;"><div class="empty-state-text">No traces match the filter.</div></div>';
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
var html = '';
|
|
836
|
+
for (var i = 0; i < visible.length; i++) {
|
|
837
|
+
var trace = visible[i];
|
|
838
|
+
var status = this.getTraceStatus(trace);
|
|
839
|
+
var isActive = this.selectedTrace && this.selectedTrace.filename === trace.filename;
|
|
840
|
+
var name = trace.name || trace.agentId || trace.filename;
|
|
841
|
+
var ts = this.formatTimestamp(trace.timestamp || trace.startTime || trace.lastModified);
|
|
842
|
+
var badgeClass =
|
|
843
|
+
status === 'success'
|
|
844
|
+
? 'badge-success'
|
|
845
|
+
: status === 'failure'
|
|
846
|
+
? 'badge-error'
|
|
847
|
+
: status === 'running'
|
|
848
|
+
? 'badge-running'
|
|
849
|
+
: 'badge-unknown';
|
|
850
|
+
var badgeText =
|
|
851
|
+
status === 'success'
|
|
852
|
+
? 'OK'
|
|
853
|
+
: status === 'failure'
|
|
854
|
+
? 'FAIL'
|
|
855
|
+
: status === 'running'
|
|
856
|
+
? 'LIVE'
|
|
857
|
+
: '?';
|
|
858
|
+
|
|
859
|
+
// Compute node stats for this trace
|
|
860
|
+
var traceNodes = this.getNodesArray(trace);
|
|
861
|
+
var nodeCount = traceNodes.length;
|
|
862
|
+
var agentCount = 0,
|
|
863
|
+
toolCount = 0,
|
|
864
|
+
subagentCount = 0,
|
|
865
|
+
otherCount = 0;
|
|
866
|
+
for (var j = 0; j < traceNodes.length; j++) {
|
|
867
|
+
var nt = traceNodes[j].type;
|
|
868
|
+
if (nt === 'agent') agentCount++;
|
|
869
|
+
else if (nt === 'tool') toolCount++;
|
|
870
|
+
else if (nt === 'subagent') subagentCount++;
|
|
871
|
+
else otherCount++;
|
|
872
|
+
}
|
|
873
|
+
var traceDuration = this.computeDuration(
|
|
874
|
+
trace.startTime,
|
|
875
|
+
traceNodes.length > 0
|
|
876
|
+
? Math.max.apply(
|
|
877
|
+
null,
|
|
878
|
+
traceNodes
|
|
879
|
+
.map((n) => (n.endTime ? new Date(n.endTime).getTime() : 0))
|
|
880
|
+
.filter((v) => v > 0),
|
|
881
|
+
) || null
|
|
882
|
+
: null,
|
|
883
|
+
);
|
|
884
|
+
var _sourceLabel = trace.sourceType === 'session' ? 'session' : 'trace';
|
|
885
|
+
|
|
886
|
+
html +=
|
|
887
|
+
'<div class="session-item' +
|
|
888
|
+
(isActive ? ' active' : '') +
|
|
889
|
+
'" data-filename="' +
|
|
890
|
+
escapeHtml(trace.filename) +
|
|
891
|
+
'">';
|
|
892
|
+
html +=
|
|
893
|
+
'<div class="session-id" title="' +
|
|
894
|
+
escapeHtml(trace.filename) +
|
|
895
|
+
'">' +
|
|
896
|
+
escapeHtml(name.length > 45 ? `${name.substring(0, 42)}...` : name) +
|
|
897
|
+
'</div>';
|
|
898
|
+
html += '<div class="session-meta">';
|
|
899
|
+
html += `<span class="session-agent">${escapeHtml(trace.agentId || '')}</span>`;
|
|
900
|
+
html += `<span>${escapeHtml(ts)}</span>`;
|
|
901
|
+
html += `<span class="badge ${badgeClass}">${badgeText}</span>`;
|
|
902
|
+
html += '</div>';
|
|
903
|
+
// Node type breakdown + duration
|
|
904
|
+
html += '<div class="session-meta" style="margin-top:3px;">';
|
|
905
|
+
html +=
|
|
906
|
+
'<span style="font-size:0.7rem;color:var(--accent-primary);">' +
|
|
907
|
+
nodeCount +
|
|
908
|
+
' nodes</span>';
|
|
909
|
+
if (agentCount > 0)
|
|
910
|
+
html += `<span class="badge badge-type badge-agent">${agentCount} agent</span>`;
|
|
911
|
+
if (toolCount > 0)
|
|
912
|
+
html += `<span class="badge badge-type badge-tool">${toolCount} tool</span>`;
|
|
913
|
+
if (subagentCount > 0)
|
|
914
|
+
html += `<span class="badge badge-type badge-subagent">${subagentCount} sub</span>`;
|
|
915
|
+
if (otherCount > 0)
|
|
916
|
+
html += `<span class="badge badge-type badge-other">${otherCount} other</span>`;
|
|
917
|
+
if (traceDuration !== '--')
|
|
918
|
+
html +=
|
|
919
|
+
'<span style="font-size:0.7rem;color:var(--text-secondary);">' +
|
|
920
|
+
escapeHtml(traceDuration) +
|
|
921
|
+
'</span>';
|
|
922
|
+
if (trace.tokenUsage && trace.tokenUsage.total > 0) {
|
|
923
|
+
html +=
|
|
924
|
+
'<span style="font-size:0.7rem;color:#bc8cff;">' +
|
|
925
|
+
(trace.tokenUsage.total > 1000
|
|
926
|
+
? `${Math.round(trace.tokenUsage.total / 1000)}k`
|
|
927
|
+
: trace.tokenUsage.total) +
|
|
928
|
+
' tok</span>';
|
|
929
|
+
if (trace.tokenUsage.cost > 0) {
|
|
930
|
+
html +=
|
|
931
|
+
'<span style="font-size:0.7rem;color:#f0883e;">$' +
|
|
932
|
+
trace.tokenUsage.cost.toFixed(4) +
|
|
933
|
+
'</span>';
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
html += '</div>';
|
|
937
|
+
html += '</div>';
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
container.innerHTML = html;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
getTraceStatus(trace) {
|
|
944
|
+
if (!trace.nodes) return 'unknown';
|
|
945
|
+
var nodes = this.getNodesArray(trace);
|
|
946
|
+
if (nodes.length === 0) return 'unknown';
|
|
947
|
+
var hasFailed = nodes.some((n) => n.status === 'failed' || n.metadata?.error);
|
|
948
|
+
if (hasFailed) return 'failure';
|
|
949
|
+
var hasRunning = nodes.some((n) => n.status === 'running');
|
|
950
|
+
if (hasRunning) return 'running';
|
|
951
|
+
var hasCompleted = nodes.some((n) => n.status === 'completed' || n.endTime);
|
|
952
|
+
if (hasCompleted) return 'success';
|
|
953
|
+
return 'unknown';
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
getNodesArray(trace) {
|
|
957
|
+
if (!trace.nodes) return [];
|
|
958
|
+
if (Array.isArray(trace.nodes)) {
|
|
959
|
+
return trace.nodes.map((entry) => (Array.isArray(entry) ? entry[1] : entry));
|
|
960
|
+
}
|
|
961
|
+
if (trace.nodes instanceof Map) return Array.from(trace.nodes.values());
|
|
962
|
+
return Object.values(trace.nodes);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
formatTimestamp(ts) {
|
|
966
|
+
if (!ts) return '';
|
|
967
|
+
var d = new Date(ts);
|
|
968
|
+
if (Number.isNaN(d.getTime())) return String(ts);
|
|
969
|
+
var now = new Date();
|
|
970
|
+
var diffMs = now - d;
|
|
971
|
+
if (diffMs < 60000) return 'just now';
|
|
972
|
+
if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)}m ago`;
|
|
973
|
+
if (diffMs < 86400000) return `${Math.floor(diffMs / 3600000)}h ago`;
|
|
974
|
+
return (
|
|
975
|
+
d.toLocaleDateString() +
|
|
976
|
+
' ' +
|
|
977
|
+
d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
computeDuration(startTime, endTime) {
|
|
982
|
+
if (!startTime || !endTime) return '--';
|
|
983
|
+
var ms = new Date(endTime).getTime() - new Date(startTime).getTime();
|
|
984
|
+
if (Number.isNaN(ms) || ms < 0) return '--';
|
|
985
|
+
if (ms < 1000) return `${ms}ms`;
|
|
986
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
987
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
formatDuration(ms) {
|
|
991
|
+
if (!ms || ms <= 0) return '--';
|
|
992
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
993
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
994
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// ---------------------------------------------------------------------------
|
|
998
|
+
// Render active tab
|
|
999
|
+
// ---------------------------------------------------------------------------
|
|
1000
|
+
renderActiveTab() {
|
|
1001
|
+
switch (this.activeTab) {
|
|
1002
|
+
case 'timeline':
|
|
1003
|
+
this.renderTimeline();
|
|
1004
|
+
break;
|
|
1005
|
+
case 'metrics':
|
|
1006
|
+
this.renderMetrics();
|
|
1007
|
+
break;
|
|
1008
|
+
case 'graph':
|
|
1009
|
+
this.renderGraph();
|
|
1010
|
+
break;
|
|
1011
|
+
case 'heatmap':
|
|
1012
|
+
this.renderHeatmap();
|
|
1013
|
+
break;
|
|
1014
|
+
case 'state':
|
|
1015
|
+
this.renderStateMachine();
|
|
1016
|
+
break;
|
|
1017
|
+
case 'summary':
|
|
1018
|
+
this.renderSummary();
|
|
1019
|
+
break;
|
|
1020
|
+
case 'transcript':
|
|
1021
|
+
this.renderTranscript();
|
|
1022
|
+
break;
|
|
1023
|
+
case 'agenttimeline':
|
|
1024
|
+
this.renderAgentTimeline();
|
|
1025
|
+
break;
|
|
1026
|
+
case 'processmap':
|
|
1027
|
+
this.renderProcessMap();
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
this.updateToolbarInfo();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
updateToolbarInfo() {
|
|
1034
|
+
var info = document.getElementById('toolbarInfo');
|
|
1035
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1036
|
+
if (!trace) {
|
|
1037
|
+
info.textContent = '';
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
var nodes = this.getNodesArray(trace);
|
|
1041
|
+
info.textContent = `${nodes.length} nodes${trace.agentId ? ` | ${trace.agentId}` : ''}`;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// ---------------------------------------------------------------------------
|
|
1045
|
+
// Tab 1: Timeline
|
|
1046
|
+
// ---------------------------------------------------------------------------
|
|
1047
|
+
renderTimeline() {
|
|
1048
|
+
var container = document.getElementById('timelineContent');
|
|
1049
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1050
|
+
if (!trace || !trace.nodes) {
|
|
1051
|
+
container.innerHTML =
|
|
1052
|
+
'<div class="empty-state"><div class="empty-state-icon">☰</div><div class="empty-state-title">Select a trace</div><div class="empty-state-text">Choose a trace from the sidebar to view its execution timeline.</div></div>';
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// For session traces, render rich session timeline if available
|
|
1057
|
+
if (trace.sourceType === 'session') {
|
|
1058
|
+
this.renderSessionTimeline(trace, container);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
var nodes = this.getNodesArray(trace);
|
|
1063
|
+
if (nodes.length === 0) {
|
|
1064
|
+
container.innerHTML =
|
|
1065
|
+
'<div class="empty-state"><div class="empty-state-text">No nodes in this trace.</div></div>';
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Build depth map for tree indentation
|
|
1070
|
+
var nodeMap = {};
|
|
1071
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
1072
|
+
if (nodes[j].id) nodeMap[nodes[j].id] = nodes[j];
|
|
1073
|
+
}
|
|
1074
|
+
var depthCache = {};
|
|
1075
|
+
var getDepth = (nid, visited) => {
|
|
1076
|
+
if (!nid || visited?.has(nid)) return 0;
|
|
1077
|
+
if (depthCache[nid] !== undefined) return depthCache[nid];
|
|
1078
|
+
var nd = nodeMap[nid];
|
|
1079
|
+
if (!nd || !nd.parentId) {
|
|
1080
|
+
depthCache[nid] = 0;
|
|
1081
|
+
return 0;
|
|
1082
|
+
}
|
|
1083
|
+
var vis = visited || new Set();
|
|
1084
|
+
vis.add(nid);
|
|
1085
|
+
depthCache[nid] = 1 + getDepth(nd.parentId, vis);
|
|
1086
|
+
return depthCache[nid];
|
|
1087
|
+
};
|
|
1088
|
+
for (var k = 0; k < nodes.length; k++) getDepth(nodes[k].id);
|
|
1089
|
+
|
|
1090
|
+
// Compute timeline range for duration bars
|
|
1091
|
+
var allStarts = nodes
|
|
1092
|
+
.map((n) => (n.startTime ? new Date(n.startTime).getTime() : Infinity))
|
|
1093
|
+
.filter((v) => Number.isFinite(v));
|
|
1094
|
+
var allEnds = nodes
|
|
1095
|
+
.map((n) => (n.endTime ? new Date(n.endTime).getTime() : 0))
|
|
1096
|
+
.filter((v) => v > 0);
|
|
1097
|
+
var timelineStart = allStarts.length > 0 ? Math.min.apply(null, allStarts) : 0;
|
|
1098
|
+
var timelineEnd = allEnds.length > 0 ? Math.max.apply(null, allEnds) : 0;
|
|
1099
|
+
var timelineSpan = timelineEnd - timelineStart || 1;
|
|
1100
|
+
|
|
1101
|
+
// Sort by startTime then depth
|
|
1102
|
+
var sorted = nodes.slice().sort((a, b) => {
|
|
1103
|
+
var sa = a.startTime ? new Date(a.startTime).getTime() : Infinity;
|
|
1104
|
+
var sb = b.startTime ? new Date(b.startTime).getTime() : Infinity;
|
|
1105
|
+
if (sa !== sb) return sa - sb;
|
|
1106
|
+
return (depthCache[a.id] || 0) - (depthCache[b.id] || 0);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// Type icons
|
|
1110
|
+
var typeIcons = {
|
|
1111
|
+
agent: '\ud83e\udd16',
|
|
1112
|
+
tool: '\ud83d\udee0\ufe0f',
|
|
1113
|
+
subagent: '\ud83d\udc64',
|
|
1114
|
+
wait: '\u23f3',
|
|
1115
|
+
decision: '\ud83d\udd00',
|
|
1116
|
+
custom: '\u2b50',
|
|
1117
|
+
exec: '\u25b6\ufe0f',
|
|
1118
|
+
};
|
|
1119
|
+
var statusIcons = {
|
|
1120
|
+
completed: '\u2705',
|
|
1121
|
+
failed: '\u274c',
|
|
1122
|
+
running: '\ud83d\udfe2',
|
|
1123
|
+
hung: '\u26a0\ufe0f',
|
|
1124
|
+
timeout: '\u23f0',
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
var html = '';
|
|
1128
|
+
// Summary header
|
|
1129
|
+
html += '<div style="display:flex;gap:12px;margin-bottom:12px;flex-wrap:wrap;">';
|
|
1130
|
+
var typeCounts = {};
|
|
1131
|
+
for (var m = 0; m < sorted.length; m++) {
|
|
1132
|
+
var tt = sorted[m].type || 'unknown';
|
|
1133
|
+
typeCounts[tt] = (typeCounts[tt] || 0) + 1;
|
|
1134
|
+
}
|
|
1135
|
+
html +=
|
|
1136
|
+
'<span style="font-size:0.85rem;color:var(--text-secondary);">' +
|
|
1137
|
+
sorted.length +
|
|
1138
|
+
' nodes</span>';
|
|
1139
|
+
var typeEntries = Object.entries(typeCounts);
|
|
1140
|
+
for (var p = 0; p < typeEntries.length; p++) {
|
|
1141
|
+
var tIcon = typeIcons[typeEntries[p][0]] || '\u25cf';
|
|
1142
|
+
html +=
|
|
1143
|
+
'<span class="badge badge-type badge-' +
|
|
1144
|
+
escapeHtml(typeEntries[p][0]) +
|
|
1145
|
+
'">' +
|
|
1146
|
+
tIcon +
|
|
1147
|
+
' ' +
|
|
1148
|
+
typeEntries[p][1] +
|
|
1149
|
+
' ' +
|
|
1150
|
+
escapeHtml(typeEntries[p][0]) +
|
|
1151
|
+
'</span>';
|
|
1152
|
+
}
|
|
1153
|
+
if (timelineSpan > 1) {
|
|
1154
|
+
html +=
|
|
1155
|
+
'<span style="font-size:0.85rem;color:var(--text-secondary);">Total: ' +
|
|
1156
|
+
this.formatDuration(timelineSpan) +
|
|
1157
|
+
'</span>';
|
|
1158
|
+
}
|
|
1159
|
+
html += '</div>';
|
|
1160
|
+
|
|
1161
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
1162
|
+
var n = sorted[i];
|
|
1163
|
+
var depth = depthCache[n.id] || 0;
|
|
1164
|
+
var markerClass =
|
|
1165
|
+
n.status === 'failed'
|
|
1166
|
+
? 'failed'
|
|
1167
|
+
: n.status === 'completed'
|
|
1168
|
+
? 'completed'
|
|
1169
|
+
: n.status === 'running'
|
|
1170
|
+
? 'running'
|
|
1171
|
+
: n.status === 'hung' || n.status === 'timeout'
|
|
1172
|
+
? 'hung'
|
|
1173
|
+
: n.type === 'agent'
|
|
1174
|
+
? 'agent'
|
|
1175
|
+
: n.type === 'tool'
|
|
1176
|
+
? 'tool'
|
|
1177
|
+
: n.type === 'subagent'
|
|
1178
|
+
? 'subagent'
|
|
1179
|
+
: 'agent';
|
|
1180
|
+
|
|
1181
|
+
var typeIcon = typeIcons[n.type] || '\u25cf';
|
|
1182
|
+
var statusIcon = statusIcons[n.status] || '';
|
|
1183
|
+
var eventName = escapeHtml(n.name || n.id || 'unnamed');
|
|
1184
|
+
var eventTs = n.startTime ? new Date(n.startTime).toLocaleTimeString() : '--';
|
|
1185
|
+
var dur = this.computeDuration(n.startTime, n.endTime);
|
|
1186
|
+
var durMs =
|
|
1187
|
+
n.startTime && n.endTime
|
|
1188
|
+
? new Date(n.endTime).getTime() - new Date(n.startTime).getTime()
|
|
1189
|
+
: 0;
|
|
1190
|
+
|
|
1191
|
+
// Duration bar width proportional to timeline
|
|
1192
|
+
var barLeft = 0,
|
|
1193
|
+
barWidth = 0;
|
|
1194
|
+
if (n.startTime && timelineSpan > 1) {
|
|
1195
|
+
barLeft = ((new Date(n.startTime).getTime() - timelineStart) / timelineSpan) * 100;
|
|
1196
|
+
barWidth = Math.max(1, (durMs / timelineSpan) * 100);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
var details = '';
|
|
1200
|
+
if (n.metadata) {
|
|
1201
|
+
var showKeys = Object.keys(n.metadata).filter(
|
|
1202
|
+
(k) => k !== 'error' && typeof n.metadata[k] !== 'object',
|
|
1203
|
+
);
|
|
1204
|
+
if (showKeys.length > 0) {
|
|
1205
|
+
details = showKeys
|
|
1206
|
+
.slice(0, 4)
|
|
1207
|
+
.map((k) => `${escapeHtml(k)}: ${escapeHtml(String(n.metadata[k]).substring(0, 50))}`)
|
|
1208
|
+
.join(' \u00b7 ');
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
var indent = depth * 24;
|
|
1213
|
+
html += `<div class="timeline-item" style="margin-left:${indent}px;">`;
|
|
1214
|
+
html += `<div class="timeline-marker ${markerClass}"></div>`;
|
|
1215
|
+
html += '<div class="timeline-content">';
|
|
1216
|
+
html += '<div class="timeline-header">';
|
|
1217
|
+
html +=
|
|
1218
|
+
'<span class="event-type">' +
|
|
1219
|
+
typeIcon +
|
|
1220
|
+
' <span class="badge badge-type badge-' +
|
|
1221
|
+
escapeHtml(n.type || 'unknown') +
|
|
1222
|
+
'" style="font-size:0.7rem;">' +
|
|
1223
|
+
escapeHtml(n.type || 'node') +
|
|
1224
|
+
'</span> ' +
|
|
1225
|
+
eventName +
|
|
1226
|
+
' ' +
|
|
1227
|
+
statusIcon +
|
|
1228
|
+
'</span>';
|
|
1229
|
+
html += `<span class="event-time">${eventTs}`;
|
|
1230
|
+
if (dur !== '--') html += ` \u00b7 <strong>${escapeHtml(dur)}</strong>`;
|
|
1231
|
+
html += '</span></div>';
|
|
1232
|
+
// Duration bar
|
|
1233
|
+
if (barWidth > 0) {
|
|
1234
|
+
var barColor =
|
|
1235
|
+
n.status === 'failed'
|
|
1236
|
+
? 'var(--accent-error)'
|
|
1237
|
+
: n.status === 'completed'
|
|
1238
|
+
? 'var(--accent-success)'
|
|
1239
|
+
: n.status === 'running'
|
|
1240
|
+
? 'var(--accent-primary)'
|
|
1241
|
+
: 'var(--accent-warning)';
|
|
1242
|
+
html +=
|
|
1243
|
+
'<div style="position:relative;height:6px;background:var(--bg-tertiary);border-radius:3px;margin:4px 0;">';
|
|
1244
|
+
html +=
|
|
1245
|
+
'<div style="position:absolute;left:' +
|
|
1246
|
+
barLeft.toFixed(1) +
|
|
1247
|
+
'%;width:' +
|
|
1248
|
+
barWidth.toFixed(1) +
|
|
1249
|
+
'%;height:100%;background:' +
|
|
1250
|
+
barColor +
|
|
1251
|
+
';border-radius:3px;"></div>';
|
|
1252
|
+
html += '</div>';
|
|
1253
|
+
}
|
|
1254
|
+
if (details) {
|
|
1255
|
+
html += `<div class="event-details">${details}</div>`;
|
|
1256
|
+
}
|
|
1257
|
+
if (n.metadata?.error) {
|
|
1258
|
+
html +=
|
|
1259
|
+
'<div class="event-details" style="color:var(--accent-error);">\u274c ' +
|
|
1260
|
+
escapeHtml(String(n.metadata.error).substring(0, 120)) +
|
|
1261
|
+
'</div>';
|
|
1262
|
+
}
|
|
1263
|
+
html += '</div></div>';
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
container.innerHTML = html;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// ---------------------------------------------------------------------------
|
|
1270
|
+
// Tab 2: Metrics
|
|
1271
|
+
// ---------------------------------------------------------------------------
|
|
1272
|
+
renderMetrics() {
|
|
1273
|
+
var container = document.getElementById('metricsContent');
|
|
1274
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1275
|
+
if (!trace || !trace.nodes) {
|
|
1276
|
+
container.innerHTML =
|
|
1277
|
+
'<div class="empty-state"><div class="empty-state-text">Select a trace to view metrics.</div></div>';
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
var nodes = this.getNodesArray(trace);
|
|
1282
|
+
var totalNodes = nodes.length;
|
|
1283
|
+
var completedNodes = nodes.filter((n) => n.status === 'completed').length;
|
|
1284
|
+
var failedNodes = nodes.filter((n) => n.status === 'failed').length;
|
|
1285
|
+
var runningNodes = nodes.filter((n) => n.status === 'running').length;
|
|
1286
|
+
var _hungNodes = nodes.filter((n) => n.status === 'hung' || n.status === 'timeout').length;
|
|
1287
|
+
var successRate = totalNodes > 0 ? Math.round((completedNodes / totalNodes) * 1000) / 10 : 0;
|
|
1288
|
+
|
|
1289
|
+
// Compute average and max duration
|
|
1290
|
+
var totalDur = 0,
|
|
1291
|
+
durCount = 0,
|
|
1292
|
+
maxDur = 0;
|
|
1293
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1294
|
+
var n = nodes[i];
|
|
1295
|
+
if (n.startTime && n.endTime) {
|
|
1296
|
+
var ms = new Date(n.endTime).getTime() - new Date(n.startTime).getTime();
|
|
1297
|
+
if (!Number.isNaN(ms) && ms >= 0) {
|
|
1298
|
+
totalDur += ms;
|
|
1299
|
+
durCount++;
|
|
1300
|
+
if (ms > maxDur) maxDur = ms;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
var avgDur = durCount > 0 ? totalDur / durCount : 0;
|
|
1305
|
+
|
|
1306
|
+
// Compute max depth
|
|
1307
|
+
var nodeMap = {};
|
|
1308
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
1309
|
+
if (nodes[j].id) nodeMap[nodes[j].id] = nodes[j];
|
|
1310
|
+
}
|
|
1311
|
+
var maxDepth = 0;
|
|
1312
|
+
var depthOf = (nid, visited) => {
|
|
1313
|
+
if (!nid || visited.has(nid)) return 0;
|
|
1314
|
+
visited.add(nid);
|
|
1315
|
+
var nd = nodeMap[nid];
|
|
1316
|
+
if (!nd || !nd.parentId) return 0;
|
|
1317
|
+
return 1 + depthOf(nd.parentId, visited);
|
|
1318
|
+
};
|
|
1319
|
+
for (var k = 0; k < nodes.length; k++) {
|
|
1320
|
+
maxDepth = Math.max(maxDepth, depthOf(nodes[k].id, new Set()));
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Type breakdown
|
|
1324
|
+
var typeCounts = {};
|
|
1325
|
+
for (var m = 0; m < nodes.length; m++) {
|
|
1326
|
+
var t = nodes[m].type || 'unknown';
|
|
1327
|
+
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
var html = '<div class="metrics-grid">';
|
|
1331
|
+
html += this.metricCard('Total Nodes', totalNodes, 'primary');
|
|
1332
|
+
html += this.metricCard(
|
|
1333
|
+
'Success Rate',
|
|
1334
|
+
`${successRate}%`,
|
|
1335
|
+
successRate >= 90 ? 'success' : successRate >= 70 ? 'warning' : 'error',
|
|
1336
|
+
);
|
|
1337
|
+
html += this.metricCard(
|
|
1338
|
+
'Avg Duration',
|
|
1339
|
+
this.formatDuration(avgDur),
|
|
1340
|
+
'primary',
|
|
1341
|
+
durCount > 0 ? `across ${durCount} nodes` : 'no timing data',
|
|
1342
|
+
);
|
|
1343
|
+
html += this.metricCard(
|
|
1344
|
+
'Max Duration',
|
|
1345
|
+
this.formatDuration(maxDur),
|
|
1346
|
+
'primary',
|
|
1347
|
+
'tool execution time',
|
|
1348
|
+
);
|
|
1349
|
+
html += this.metricCard('Max Depth', maxDepth, 'primary');
|
|
1350
|
+
html += this.metricCard('Failures', failedNodes, failedNodes > 0 ? 'error' : 'success');
|
|
1351
|
+
html += this.metricCard(
|
|
1352
|
+
'Running/Active',
|
|
1353
|
+
runningNodes,
|
|
1354
|
+
runningNodes > 0 ? 'warning' : 'primary',
|
|
1355
|
+
);
|
|
1356
|
+
html += this.metricCard('Completed', completedNodes, 'success');
|
|
1357
|
+
html += '</div>';
|
|
1358
|
+
|
|
1359
|
+
// Token/cost metrics for session traces
|
|
1360
|
+
if (trace.tokenUsage && trace.tokenUsage.total > 0) {
|
|
1361
|
+
html +=
|
|
1362
|
+
'<h4 style="margin:1.5rem 0 0.75rem;font-size:0.85rem;color:var(--text-secondary);">Token Usage</h4>';
|
|
1363
|
+
html += '<div class="metrics-grid">';
|
|
1364
|
+
html += this.metricCard(
|
|
1365
|
+
'Total Tokens',
|
|
1366
|
+
trace.tokenUsage.total > 1000
|
|
1367
|
+
? `${Math.round(trace.tokenUsage.total / 1000)}k`
|
|
1368
|
+
: trace.tokenUsage.total,
|
|
1369
|
+
'primary',
|
|
1370
|
+
);
|
|
1371
|
+
html += this.metricCard(
|
|
1372
|
+
'Input Tokens',
|
|
1373
|
+
trace.tokenUsage.input > 1000
|
|
1374
|
+
? `${Math.round(trace.tokenUsage.input / 1000)}k`
|
|
1375
|
+
: trace.tokenUsage.input,
|
|
1376
|
+
'primary',
|
|
1377
|
+
);
|
|
1378
|
+
html += this.metricCard(
|
|
1379
|
+
'Output Tokens',
|
|
1380
|
+
trace.tokenUsage.output > 1000
|
|
1381
|
+
? `${Math.round(trace.tokenUsage.output / 1000)}k`
|
|
1382
|
+
: trace.tokenUsage.output,
|
|
1383
|
+
'primary',
|
|
1384
|
+
);
|
|
1385
|
+
html += this.metricCard(
|
|
1386
|
+
'Estimated Cost',
|
|
1387
|
+
trace.tokenUsage.cost > 0 ? `$${trace.tokenUsage.cost.toFixed(4)}` : '$0',
|
|
1388
|
+
trace.tokenUsage.cost > 0.1 ? 'warning' : 'success',
|
|
1389
|
+
);
|
|
1390
|
+
if (totalNodes > 0)
|
|
1391
|
+
html += this.metricCard(
|
|
1392
|
+
'Tokens/Node',
|
|
1393
|
+
Math.round(trace.tokenUsage.total / totalNodes),
|
|
1394
|
+
'primary',
|
|
1395
|
+
);
|
|
1396
|
+
var modelName = trace.metadata?.model || '';
|
|
1397
|
+
if (modelName)
|
|
1398
|
+
html += this.metricCard(
|
|
1399
|
+
'Model',
|
|
1400
|
+
modelName.length > 20 ? `${modelName.slice(0, 18)}..` : modelName,
|
|
1401
|
+
'primary',
|
|
1402
|
+
trace.metadata?.provider || '',
|
|
1403
|
+
);
|
|
1404
|
+
html += '</div>';
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Type breakdown
|
|
1408
|
+
html +=
|
|
1409
|
+
'<h4 style="margin:1.5rem 0 0.75rem;font-size:0.85rem;color:var(--text-secondary);">Node Type Breakdown</h4>';
|
|
1410
|
+
html += '<div class="metrics-grid">';
|
|
1411
|
+
var typeEntries = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]);
|
|
1412
|
+
for (var p = 0; p < typeEntries.length; p++) {
|
|
1413
|
+
html += this.metricCard(typeEntries[p][0], typeEntries[p][1], 'primary');
|
|
1414
|
+
}
|
|
1415
|
+
html += '</div>';
|
|
1416
|
+
|
|
1417
|
+
container.innerHTML = html;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
metricCard(label, value, colorClass, sub) {
|
|
1421
|
+
var html =
|
|
1422
|
+
'<div class="metric-card"><div class="metric-label">' +
|
|
1423
|
+
escapeHtml(label) +
|
|
1424
|
+
'</div><div class="metric-value ' +
|
|
1425
|
+
colorClass +
|
|
1426
|
+
'">' +
|
|
1427
|
+
escapeHtml(String(value)) +
|
|
1428
|
+
'</div>';
|
|
1429
|
+
if (sub) html += `<div class="metric-sub">${escapeHtml(sub)}</div>`;
|
|
1430
|
+
html += '</div>';
|
|
1431
|
+
return html;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// ---------------------------------------------------------------------------
|
|
1435
|
+
// Tab 3: Dependency Graph (Cytoscape.js)
|
|
1436
|
+
// ---------------------------------------------------------------------------
|
|
1437
|
+
renderGraph() {
|
|
1438
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1439
|
+
if (!trace || !trace.nodes) {
|
|
1440
|
+
document.getElementById('graphEmpty').style.display = '';
|
|
1441
|
+
if (this.cy) {
|
|
1442
|
+
this.cy.destroy();
|
|
1443
|
+
this.cy = null;
|
|
1444
|
+
}
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
document.getElementById('graphEmpty').style.display = 'none';
|
|
1449
|
+
|
|
1450
|
+
var nodes = this.getNodesArray(trace);
|
|
1451
|
+
if (nodes.length === 0) {
|
|
1452
|
+
document.getElementById('graphEmpty').style.display = '';
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Build cytoscape elements
|
|
1457
|
+
var elements = [];
|
|
1458
|
+
var nodeIds = new Set();
|
|
1459
|
+
|
|
1460
|
+
// Collect valid IDs
|
|
1461
|
+
if (typeof trace.nodes === 'object' && !Array.isArray(trace.nodes)) {
|
|
1462
|
+
Object.keys(trace.nodes).forEach((key) => {
|
|
1463
|
+
nodeIds.add(key);
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
nodes.forEach((n) => {
|
|
1467
|
+
if (n.id) nodeIds.add(n.id);
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
// Add nodes
|
|
1471
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1472
|
+
var node = nodes[i];
|
|
1473
|
+
var id = node.id || `n-${i}`;
|
|
1474
|
+
elements.push({
|
|
1475
|
+
group: 'nodes',
|
|
1476
|
+
data: {
|
|
1477
|
+
id: id,
|
|
1478
|
+
label: node.name || node.type || id,
|
|
1479
|
+
status: node.status || 'unknown',
|
|
1480
|
+
nodeType: node.type || 'custom',
|
|
1481
|
+
fullData: node,
|
|
1482
|
+
},
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Build edges from parentId relationships
|
|
1487
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
1488
|
+
var n = nodes[j];
|
|
1489
|
+
if (n.parentId && nodeIds.has(n.parentId) && n.id) {
|
|
1490
|
+
elements.push({
|
|
1491
|
+
group: 'edges',
|
|
1492
|
+
data: {
|
|
1493
|
+
source: n.parentId,
|
|
1494
|
+
target: n.id,
|
|
1495
|
+
id: `e-${n.parentId}-${n.id}`,
|
|
1496
|
+
},
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// Also add explicit trace edges if present
|
|
1502
|
+
if (trace.edges && Array.isArray(trace.edges)) {
|
|
1503
|
+
for (var k = 0; k < trace.edges.length; k++) {
|
|
1504
|
+
var edge = trace.edges[k];
|
|
1505
|
+
var src = edge.source || edge.from;
|
|
1506
|
+
var tgt = edge.target || edge.to;
|
|
1507
|
+
if (src && tgt && nodeIds.has(src) && nodeIds.has(tgt)) {
|
|
1508
|
+
var eid = `e-${src}-${tgt}`;
|
|
1509
|
+
if (!elements.some((el) => el.data && el.data.id === eid)) {
|
|
1510
|
+
elements.push({
|
|
1511
|
+
group: 'edges',
|
|
1512
|
+
data: { source: src, target: tgt, id: eid, edgeType: edge.type || '' },
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// Destroy previous instance
|
|
1520
|
+
if (this.cy) {
|
|
1521
|
+
this.cy.destroy();
|
|
1522
|
+
this.cy = null;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
var cyContainer = document.getElementById('cy');
|
|
1526
|
+
|
|
1527
|
+
this.cy = cytoscape({
|
|
1528
|
+
container: cyContainer,
|
|
1529
|
+
elements: elements,
|
|
1530
|
+
style: [
|
|
1531
|
+
{
|
|
1532
|
+
selector: 'node',
|
|
1533
|
+
style: {
|
|
1534
|
+
label: 'data(label)',
|
|
1535
|
+
width: 45,
|
|
1536
|
+
height: 45,
|
|
1537
|
+
'font-size': '10px',
|
|
1538
|
+
'text-valign': 'bottom',
|
|
1539
|
+
'text-halign': 'center',
|
|
1540
|
+
'text-margin-y': 6,
|
|
1541
|
+
color: '#c9d1d9',
|
|
1542
|
+
'text-outline-color': '#0d1117',
|
|
1543
|
+
'text-outline-width': 2,
|
|
1544
|
+
'border-width': 2,
|
|
1545
|
+
'border-color': '#30363d',
|
|
1546
|
+
'background-color': '#3b82f6',
|
|
1547
|
+
},
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
selector: 'node[status="completed"]',
|
|
1551
|
+
style: { 'background-color': '#10b981', 'border-color': '#2ea043' },
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
selector: 'node[status="failed"]',
|
|
1555
|
+
style: { 'background-color': '#ef4444', 'border-color': '#f85149', shape: 'diamond' },
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
selector: 'node[status="running"]',
|
|
1559
|
+
style: { 'background-color': '#3b82f6', 'border-color': '#79b8ff' },
|
|
1560
|
+
},
|
|
1561
|
+
{
|
|
1562
|
+
selector: 'node[status="hung"]',
|
|
1563
|
+
style: { 'background-color': '#f0883e', 'border-color': '#f5a623' },
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
selector: 'node[status="timeout"]',
|
|
1567
|
+
style: { 'background-color': '#f0883e', 'border-color': '#f5a623' },
|
|
1568
|
+
},
|
|
1569
|
+
// Shape by type
|
|
1570
|
+
{ selector: 'node[nodeType="agent"]', style: { shape: 'ellipse', width: 50, height: 50 } },
|
|
1571
|
+
{
|
|
1572
|
+
selector: 'node[nodeType="tool"]',
|
|
1573
|
+
style: { shape: 'round-rectangle', width: 50, height: 35 },
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
selector: 'node[nodeType="subagent"]',
|
|
1577
|
+
style: { shape: 'ellipse', width: 38, height: 38 },
|
|
1578
|
+
},
|
|
1579
|
+
{
|
|
1580
|
+
selector: 'node[nodeType="wait"]',
|
|
1581
|
+
style: { shape: 'round-rectangle', width: 40, height: 30 },
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
selector: 'node[nodeType="decision"]',
|
|
1585
|
+
style: { shape: 'diamond', width: 45, height: 45 },
|
|
1586
|
+
},
|
|
1587
|
+
{ selector: 'node[nodeType="custom"]', style: { shape: 'diamond', width: 40, height: 40 } },
|
|
1588
|
+
// Selected node — gold border
|
|
1589
|
+
{
|
|
1590
|
+
selector: ':selected',
|
|
1591
|
+
style: { 'border-width': 4, 'border-color': '#f59e0b', 'overlay-opacity': 0.08 },
|
|
1592
|
+
},
|
|
1593
|
+
// Edges
|
|
1594
|
+
{
|
|
1595
|
+
selector: 'edge',
|
|
1596
|
+
style: {
|
|
1597
|
+
width: 2,
|
|
1598
|
+
'line-color': '#6b7280',
|
|
1599
|
+
'target-arrow-color': '#6b7280',
|
|
1600
|
+
'target-arrow-shape': 'triangle',
|
|
1601
|
+
'curve-style': 'bezier',
|
|
1602
|
+
'arrow-scale': 0.8,
|
|
1603
|
+
},
|
|
1604
|
+
},
|
|
1605
|
+
// Dashed edges for specific types
|
|
1606
|
+
{
|
|
1607
|
+
selector: 'edge[edgeType]',
|
|
1608
|
+
style: {
|
|
1609
|
+
'line-style': 'dashed',
|
|
1610
|
+
'line-color': '#f0883e',
|
|
1611
|
+
'target-arrow-color': '#f0883e',
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1614
|
+
],
|
|
1615
|
+
layout: {
|
|
1616
|
+
name: 'breadthfirst',
|
|
1617
|
+
directed: true,
|
|
1618
|
+
padding: 40,
|
|
1619
|
+
spacingFactor: 1.4,
|
|
1620
|
+
animate: true,
|
|
1621
|
+
animationDuration: 300,
|
|
1622
|
+
},
|
|
1623
|
+
minZoom: 0.2,
|
|
1624
|
+
maxZoom: 4,
|
|
1625
|
+
wheelSensitivity: 0.3,
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
// Node tap -> detail panel
|
|
1629
|
+
this.cy.on('tap', 'node', (e) => {
|
|
1630
|
+
var data = e.target.data();
|
|
1631
|
+
this.showNodeDetail(data.fullData);
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
// Background tap -> close panel
|
|
1635
|
+
this.cy.on('tap', (e) => {
|
|
1636
|
+
if (e.target === this.cy) {
|
|
1637
|
+
document.getElementById('nodeDetailPanel').classList.remove('active');
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
runCytoscapeLayout() {
|
|
1643
|
+
if (!this.cy) return;
|
|
1644
|
+
this.cy
|
|
1645
|
+
.layout({
|
|
1646
|
+
name: 'breadthfirst',
|
|
1647
|
+
directed: true,
|
|
1648
|
+
padding: 40,
|
|
1649
|
+
spacingFactor: 1.4,
|
|
1650
|
+
animate: true,
|
|
1651
|
+
animationDuration: 400,
|
|
1652
|
+
})
|
|
1653
|
+
.run();
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
showNodeDetail(node) {
|
|
1657
|
+
var panel = document.getElementById('nodeDetailPanel');
|
|
1658
|
+
var body = document.getElementById('nodeDetailBody');
|
|
1659
|
+
var title = document.getElementById('nodeDetailTitle');
|
|
1660
|
+
|
|
1661
|
+
title.textContent = node.name || node.id || 'Node';
|
|
1662
|
+
|
|
1663
|
+
var duration = this.computeDuration(node.startTime, node.endTime);
|
|
1664
|
+
|
|
1665
|
+
var html = '';
|
|
1666
|
+
html += this.detailRow('ID', node.id);
|
|
1667
|
+
html += this.detailRow('Type', node.type);
|
|
1668
|
+
html +=
|
|
1669
|
+
'<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value status-' +
|
|
1670
|
+
escapeHtml(node.status || '') +
|
|
1671
|
+
'">' +
|
|
1672
|
+
escapeHtml(node.status || 'unknown') +
|
|
1673
|
+
'</span></div>';
|
|
1674
|
+
html += this.detailRow('Duration', duration);
|
|
1675
|
+
if (node.startTime) html += this.detailRow('Start', new Date(node.startTime).toLocaleString());
|
|
1676
|
+
if (node.endTime) html += this.detailRow('End', new Date(node.endTime).toLocaleString());
|
|
1677
|
+
if (node.parentId) html += this.detailRow('Parent', node.parentId);
|
|
1678
|
+
if (node.children?.length) html += this.detailRow('Children', node.children.length);
|
|
1679
|
+
|
|
1680
|
+
if (node.metadata && Object.keys(node.metadata).length > 0) {
|
|
1681
|
+
html +=
|
|
1682
|
+
'<div style="margin-top:0.5rem;font-size:0.7rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.3px;">Metadata</div>';
|
|
1683
|
+
html +=
|
|
1684
|
+
'<div class="detail-metadata">' +
|
|
1685
|
+
escapeHtml(JSON.stringify(node.metadata, null, 2)) +
|
|
1686
|
+
'</div>';
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
body.innerHTML = html;
|
|
1690
|
+
panel.classList.add('active');
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
detailRow(label, value) {
|
|
1694
|
+
if (value === undefined || value === null || value === '') return '';
|
|
1695
|
+
return (
|
|
1696
|
+
'<div class="detail-row"><span class="detail-label">' +
|
|
1697
|
+
escapeHtml(label) +
|
|
1698
|
+
'</span><span class="detail-value">' +
|
|
1699
|
+
escapeHtml(String(value)) +
|
|
1700
|
+
'</span></div>'
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
exportGraphPNG() {
|
|
1705
|
+
if (!this.cy) return;
|
|
1706
|
+
var png = this.cy.png({ bg: '#0d1117', full: true, maxWidth: 4000, maxHeight: 4000 });
|
|
1707
|
+
var link = document.createElement('a');
|
|
1708
|
+
var traceName = this.selectedTrace
|
|
1709
|
+
? this.selectedTrace.filename.replace(/\.json$/, '')
|
|
1710
|
+
: 'graph';
|
|
1711
|
+
link.download = `agentflow-${traceName}.png`;
|
|
1712
|
+
link.href = png;
|
|
1713
|
+
link.click();
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// ---------------------------------------------------------------------------
|
|
1717
|
+
// Tab 4: Error Heatmap
|
|
1718
|
+
// ---------------------------------------------------------------------------
|
|
1719
|
+
renderHeatmap() {
|
|
1720
|
+
var container = document.getElementById('heatmapContent');
|
|
1721
|
+
var _trace = this.selectedTraceData || this.selectedTrace;
|
|
1722
|
+
|
|
1723
|
+
// Build heatmap from recent traces (not just selected trace)
|
|
1724
|
+
var tracesToUse = this.traces.slice(0, 100);
|
|
1725
|
+
if (tracesToUse.length === 0) {
|
|
1726
|
+
container.innerHTML =
|
|
1727
|
+
'<div class="empty-state"><div class="empty-state-text">No traces available for heatmap.</div></div>';
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
var html = '<h3 class="heatmap-header">Error Distribution Across Recent Traces</h3>';
|
|
1732
|
+
html += '<div class="heatmap-grid">';
|
|
1733
|
+
|
|
1734
|
+
for (var i = 0; i < Math.min(tracesToUse.length, 100); i++) {
|
|
1735
|
+
var tr = tracesToUse[i];
|
|
1736
|
+
var nodes = this.getNodesArray(tr);
|
|
1737
|
+
var failCount = 0;
|
|
1738
|
+
var warnCount = 0;
|
|
1739
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
1740
|
+
if (nodes[j].status === 'failed') failCount++;
|
|
1741
|
+
if (nodes[j].status === 'hung' || nodes[j].status === 'timeout') warnCount++;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
var color;
|
|
1745
|
+
if (failCount > 2) color = 'rgba(218, 54, 51, 0.9)';
|
|
1746
|
+
else if (failCount > 0) color = 'rgba(218, 54, 51, 0.5)';
|
|
1747
|
+
else if (warnCount > 0) color = 'rgba(240, 136, 62, 0.5)';
|
|
1748
|
+
else color = 'rgba(35, 134, 54, 0.3)';
|
|
1749
|
+
|
|
1750
|
+
var cellLabel = failCount > 0 ? failCount : '';
|
|
1751
|
+
var agentName = escapeHtml(tr.agentId || tr.name || 'unknown');
|
|
1752
|
+
var tooltipText =
|
|
1753
|
+
escapeHtml((tr.name || tr.filename || '').substring(0, 30)) +
|
|
1754
|
+
' | ' +
|
|
1755
|
+
agentName +
|
|
1756
|
+
' | ' +
|
|
1757
|
+
failCount +
|
|
1758
|
+
' errors, ' +
|
|
1759
|
+
warnCount +
|
|
1760
|
+
' warnings';
|
|
1761
|
+
|
|
1762
|
+
html += `<div class="heatmap-cell" style="background:${color};" title="${tooltipText}">`;
|
|
1763
|
+
html += cellLabel;
|
|
1764
|
+
html += `<div class="heatmap-tooltip">${tooltipText}</div>`;
|
|
1765
|
+
html += '</div>';
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
html += '</div>';
|
|
1769
|
+
|
|
1770
|
+
// Legend
|
|
1771
|
+
html +=
|
|
1772
|
+
'<div style="display:flex;gap:1.5rem;font-size:0.75rem;color:var(--text-secondary);margin-top:0.5rem;">';
|
|
1773
|
+
html +=
|
|
1774
|
+
'<span><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:rgba(35,134,54,0.3);vertical-align:middle;margin-right:4px;"></span>No errors</span>';
|
|
1775
|
+
html +=
|
|
1776
|
+
'<span><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:rgba(240,136,62,0.5);vertical-align:middle;margin-right:4px;"></span>Warnings</span>';
|
|
1777
|
+
html +=
|
|
1778
|
+
'<span><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:rgba(218,54,51,0.5);vertical-align:middle;margin-right:4px;"></span>1-2 failures</span>';
|
|
1779
|
+
html +=
|
|
1780
|
+
'<span><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:rgba(218,54,51,0.9);vertical-align:middle;margin-right:4px;"></span>3+ failures</span>';
|
|
1781
|
+
html += '</div>';
|
|
1782
|
+
|
|
1783
|
+
container.innerHTML = html;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// ---------------------------------------------------------------------------
|
|
1787
|
+
// Tab 5: State Machine
|
|
1788
|
+
// ---------------------------------------------------------------------------
|
|
1789
|
+
renderStateMachine() {
|
|
1790
|
+
var container = document.getElementById('stateContent');
|
|
1791
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1792
|
+
if (!trace || !trace.nodes) {
|
|
1793
|
+
container.innerHTML =
|
|
1794
|
+
'<div class="empty-state"><div class="empty-state-text">Select a trace to view state machine.</div></div>';
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
var nodes = this.getNodesArray(trace);
|
|
1799
|
+
var pendingCount = 0,
|
|
1800
|
+
runningCount = 0,
|
|
1801
|
+
completedCount = 0,
|
|
1802
|
+
failedCount = 0;
|
|
1803
|
+
|
|
1804
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1805
|
+
var s = nodes[i].status;
|
|
1806
|
+
if (s === 'completed') completedCount++;
|
|
1807
|
+
else if (s === 'failed') failedCount++;
|
|
1808
|
+
else if (s === 'running') runningCount++;
|
|
1809
|
+
else pendingCount++;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// Determine which states are "active" (have nodes)
|
|
1813
|
+
var pendingActive = pendingCount > 0 ? ' pending' : '';
|
|
1814
|
+
var runningActive = runningCount > 0 ? ' running' : '';
|
|
1815
|
+
var completedActive = completedCount > 0 ? ' completed' : '';
|
|
1816
|
+
var failedActive = failedCount > 0 ? ' failed' : '';
|
|
1817
|
+
|
|
1818
|
+
var html = '<div class="state-machine">';
|
|
1819
|
+
|
|
1820
|
+
html += '<div class="state">';
|
|
1821
|
+
html +=
|
|
1822
|
+
'<div class="state-circle' +
|
|
1823
|
+
pendingActive +
|
|
1824
|
+
'"><span class="state-count">' +
|
|
1825
|
+
pendingCount +
|
|
1826
|
+
'</span>PENDING</div>';
|
|
1827
|
+
html += '<span class="state-label">Queued</span>';
|
|
1828
|
+
html += '</div>';
|
|
1829
|
+
|
|
1830
|
+
html += '<div class="state-arrow">→</div>';
|
|
1831
|
+
|
|
1832
|
+
html += '<div class="state">';
|
|
1833
|
+
html +=
|
|
1834
|
+
'<div class="state-circle' +
|
|
1835
|
+
runningActive +
|
|
1836
|
+
'"><span class="state-count">' +
|
|
1837
|
+
runningCount +
|
|
1838
|
+
'</span>RUNNING</div>';
|
|
1839
|
+
html += '<span class="state-label">Active</span>';
|
|
1840
|
+
html += '</div>';
|
|
1841
|
+
|
|
1842
|
+
html += '<div class="state-arrow">→</div>';
|
|
1843
|
+
|
|
1844
|
+
html += '<div class="state">';
|
|
1845
|
+
html +=
|
|
1846
|
+
'<div class="state-circle' +
|
|
1847
|
+
completedActive +
|
|
1848
|
+
'"><span class="state-count">' +
|
|
1849
|
+
completedCount +
|
|
1850
|
+
'</span>COMPLETED</div>';
|
|
1851
|
+
html += '<span class="state-label">Success</span>';
|
|
1852
|
+
html += '</div>';
|
|
1853
|
+
|
|
1854
|
+
html += '<div class="state-arrow">↔</div>';
|
|
1855
|
+
|
|
1856
|
+
html += '<div class="state">';
|
|
1857
|
+
html +=
|
|
1858
|
+
'<div class="state-circle' +
|
|
1859
|
+
failedActive +
|
|
1860
|
+
'"><span class="state-count">' +
|
|
1861
|
+
failedCount +
|
|
1862
|
+
'</span>FAILED</div>';
|
|
1863
|
+
html += '<span class="state-label">Error</span>';
|
|
1864
|
+
html += '</div>';
|
|
1865
|
+
|
|
1866
|
+
html += '</div>';
|
|
1867
|
+
|
|
1868
|
+
// State details
|
|
1869
|
+
html += '<div style="padding:1rem;">';
|
|
1870
|
+
html += '<div class="metrics-grid">';
|
|
1871
|
+
html += this.metricCard('Pending', pendingCount, 'primary');
|
|
1872
|
+
html += this.metricCard('Running', runningCount, runningCount > 0 ? 'warning' : 'primary');
|
|
1873
|
+
html += this.metricCard('Completed', completedCount, 'success');
|
|
1874
|
+
html += this.metricCard('Failed', failedCount, failedCount > 0 ? 'error' : 'success');
|
|
1875
|
+
html += '</div></div>';
|
|
1876
|
+
|
|
1877
|
+
container.innerHTML = html;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// ---------------------------------------------------------------------------
|
|
1881
|
+
// Tab 6: Summary
|
|
1882
|
+
// ---------------------------------------------------------------------------
|
|
1883
|
+
renderSummary() {
|
|
1884
|
+
var container = document.getElementById('summaryContent');
|
|
1885
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1886
|
+
if (!trace || !trace.nodes) {
|
|
1887
|
+
container.innerHTML =
|
|
1888
|
+
'<div class="empty-state"><div class="empty-state-text">Select a trace to view summary.</div></div>';
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// Show spinner briefly then generate
|
|
1893
|
+
container.innerHTML =
|
|
1894
|
+
'<div class="empty-state"><div class="spinner"></div><div class="empty-state-text">Generating summary...</div></div>';
|
|
1895
|
+
|
|
1896
|
+
// Use setTimeout to avoid blocking render
|
|
1897
|
+
setTimeout(() => {
|
|
1898
|
+
this.generateSummary(trace, container);
|
|
1899
|
+
}, 50);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
generateSummary(trace, container) {
|
|
1903
|
+
var nodes = this.getNodesArray(trace);
|
|
1904
|
+
var totalNodes = nodes.length;
|
|
1905
|
+
var completedCount = 0,
|
|
1906
|
+
failedCount = 0,
|
|
1907
|
+
runningCount = 0;
|
|
1908
|
+
var agentNames = new Set();
|
|
1909
|
+
var totalDur = 0,
|
|
1910
|
+
durCount = 0;
|
|
1911
|
+
|
|
1912
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1913
|
+
var n = nodes[i];
|
|
1914
|
+
if (n.status === 'completed') completedCount++;
|
|
1915
|
+
else if (n.status === 'failed') failedCount++;
|
|
1916
|
+
else if (n.status === 'running') runningCount++;
|
|
1917
|
+
|
|
1918
|
+
if (n.type === 'agent' || n.type === 'subagent') {
|
|
1919
|
+
agentNames.add(n.name || n.id || 'unnamed');
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
if (n.startTime && n.endTime) {
|
|
1923
|
+
var ms = new Date(n.endTime).getTime() - new Date(n.startTime).getTime();
|
|
1924
|
+
if (!Number.isNaN(ms) && ms >= 0) {
|
|
1925
|
+
totalDur += ms;
|
|
1926
|
+
durCount++;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
var successRate = totalNodes > 0 ? Math.round((completedCount / totalNodes) * 100) : 0;
|
|
1932
|
+
var agentList = Array.from(agentNames);
|
|
1933
|
+
|
|
1934
|
+
// Build summary title
|
|
1935
|
+
var titleText = `Trace: ${escapeHtml(trace.name || trace.agentId || trace.filename || 'Unknown')}`;
|
|
1936
|
+
|
|
1937
|
+
// Build summary text
|
|
1938
|
+
var summaryText = `This trace contains ${totalNodes} node${totalNodes !== 1 ? 's' : ''}. `;
|
|
1939
|
+
summaryText += `${completedCount} completed successfully, ${failedCount} failed`;
|
|
1940
|
+
if (runningCount > 0) summaryText += `, and ${runningCount} are still running`;
|
|
1941
|
+
summaryText += '. ';
|
|
1942
|
+
if (durCount > 0) {
|
|
1943
|
+
summaryText += `Average node duration was ${this.formatDuration(totalDur / durCount)}. `;
|
|
1944
|
+
summaryText += `Total execution time: ${this.formatDuration(totalDur)}.`;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// Build details list
|
|
1948
|
+
var details = [];
|
|
1949
|
+
details.push(`Total nodes: ${totalNodes}`);
|
|
1950
|
+
details.push(`Completed: ${completedCount}`);
|
|
1951
|
+
details.push(`Failed: ${failedCount}`);
|
|
1952
|
+
if (runningCount > 0) details.push(`Running: ${runningCount}`);
|
|
1953
|
+
if (agentList.length > 0) details.push(`Agents involved: ${agentList.join(', ')}`);
|
|
1954
|
+
if (trace.trigger) details.push(`Trigger: ${trace.trigger}`);
|
|
1955
|
+
|
|
1956
|
+
// Recommendations
|
|
1957
|
+
var recommendations = '';
|
|
1958
|
+
if (failedCount === 0 && runningCount === 0) {
|
|
1959
|
+
recommendations =
|
|
1960
|
+
'<strong>Status:</strong> All tasks completed successfully. No issues detected.';
|
|
1961
|
+
} else if (failedCount > 0) {
|
|
1962
|
+
recommendations =
|
|
1963
|
+
'<strong>Action needed:</strong> ' +
|
|
1964
|
+
failedCount +
|
|
1965
|
+
' node' +
|
|
1966
|
+
(failedCount !== 1 ? 's' : '') +
|
|
1967
|
+
' failed. Investigate the failed nodes in the Timeline or Dependency Graph tabs for error details.';
|
|
1968
|
+
}
|
|
1969
|
+
if (runningCount > 0) {
|
|
1970
|
+
recommendations +=
|
|
1971
|
+
(recommendations ? ' ' : '') +
|
|
1972
|
+
'<strong>Note:</strong> ' +
|
|
1973
|
+
runningCount +
|
|
1974
|
+
' node' +
|
|
1975
|
+
(runningCount !== 1 ? 's are' : ' is') +
|
|
1976
|
+
' still running. The trace may not be complete yet.';
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
var html = '<div class="summary-card">';
|
|
1980
|
+
html += `<h3 class="summary-title">${titleText}</h3>`;
|
|
1981
|
+
html += `<p class="summary-text">${escapeHtml(summaryText)}</p>`;
|
|
1982
|
+
html += '<ul class="summary-details">';
|
|
1983
|
+
for (var j = 0; j < details.length; j++) {
|
|
1984
|
+
html += `<li>${escapeHtml(details[j])}</li>`;
|
|
1985
|
+
}
|
|
1986
|
+
html += '</ul>';
|
|
1987
|
+
|
|
1988
|
+
if (recommendations) {
|
|
1989
|
+
html += `<div class="summary-recommendations">${recommendations}</div>`;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// Confidence bar based on success rate
|
|
1993
|
+
html += '<div class="confidence-bar">';
|
|
1994
|
+
html += '<span>Confidence:</span>';
|
|
1995
|
+
html += `<div class="bar"><div class="bar-fill" style="width:${successRate}%;"></div></div>`;
|
|
1996
|
+
html += `<span>${successRate}%</span>`;
|
|
1997
|
+
html += '</div>';
|
|
1998
|
+
|
|
1999
|
+
html += '</div>';
|
|
2000
|
+
|
|
2001
|
+
container.innerHTML = html;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// ---------------------------------------------------------------------------
|
|
2005
|
+
// Session Timeline (rich event-based timeline for JSONL sessions)
|
|
2006
|
+
// ---------------------------------------------------------------------------
|
|
2007
|
+
async renderSessionTimeline(trace, container) {
|
|
2008
|
+
var filename = trace.filename;
|
|
2009
|
+
var html = '';
|
|
2010
|
+
|
|
2011
|
+
// Try to fetch session events from the API
|
|
2012
|
+
var events = trace.sessionEvents || [];
|
|
2013
|
+
var tokenUsage = trace.tokenUsage || null;
|
|
2014
|
+
|
|
2015
|
+
if (events.length === 0 && filename) {
|
|
2016
|
+
try {
|
|
2017
|
+
var res = await fetch(`/api/traces/${encodeURIComponent(filename)}/events`);
|
|
2018
|
+
if (res.ok) {
|
|
2019
|
+
var data = await res.json();
|
|
2020
|
+
events = data.events || [];
|
|
2021
|
+
tokenUsage = data.tokenUsage || null;
|
|
2022
|
+
}
|
|
2023
|
+
} catch (_e) {
|
|
2024
|
+
// fall through to node-based rendering
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
if (events.length === 0) {
|
|
2029
|
+
// Fallback: render nodes like a normal trace
|
|
2030
|
+
container.innerHTML =
|
|
2031
|
+
'<div class="empty-state"><div class="empty-state-text">No session events found. Try the node-based timeline.</div></div>';
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// Token usage summary at top
|
|
2036
|
+
if (tokenUsage && tokenUsage.total > 0) {
|
|
2037
|
+
html +=
|
|
2038
|
+
'<div style="display:flex;gap:16px;margin-bottom:12px;flex-wrap:wrap;padding:8px 12px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;">';
|
|
2039
|
+
html +=
|
|
2040
|
+
'<span style="font-size:0.8rem;color:#bc8cff;">Tokens: ' +
|
|
2041
|
+
(tokenUsage.total > 1000 ? `${Math.round(tokenUsage.total / 1000)}k` : tokenUsage.total) +
|
|
2042
|
+
'</span>';
|
|
2043
|
+
html +=
|
|
2044
|
+
'<span style="font-size:0.8rem;color:var(--text-secondary);">In: ' +
|
|
2045
|
+
(tokenUsage.input > 1000 ? `${Math.round(tokenUsage.input / 1000)}k` : tokenUsage.input) +
|
|
2046
|
+
'</span>';
|
|
2047
|
+
html +=
|
|
2048
|
+
'<span style="font-size:0.8rem;color:var(--text-secondary);">Out: ' +
|
|
2049
|
+
(tokenUsage.output > 1000
|
|
2050
|
+
? `${Math.round(tokenUsage.output / 1000)}k`
|
|
2051
|
+
: tokenUsage.output) +
|
|
2052
|
+
'</span>';
|
|
2053
|
+
if (tokenUsage.cost > 0)
|
|
2054
|
+
html +=
|
|
2055
|
+
'<span style="font-size:0.8rem;color:#f0883e;">Cost: $' +
|
|
2056
|
+
tokenUsage.cost.toFixed(4) +
|
|
2057
|
+
'</span>';
|
|
2058
|
+
html += '</div>';
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// Summary badges
|
|
2062
|
+
var userCount = 0,
|
|
2063
|
+
assistantCount = 0,
|
|
2064
|
+
toolCount = 0,
|
|
2065
|
+
thinkCount = 0,
|
|
2066
|
+
spawnCount = 0;
|
|
2067
|
+
for (var i = 0; i < events.length; i++) {
|
|
2068
|
+
switch (events[i].type) {
|
|
2069
|
+
case 'user':
|
|
2070
|
+
userCount++;
|
|
2071
|
+
break;
|
|
2072
|
+
case 'assistant':
|
|
2073
|
+
assistantCount++;
|
|
2074
|
+
break;
|
|
2075
|
+
case 'tool_call':
|
|
2076
|
+
toolCount++;
|
|
2077
|
+
break;
|
|
2078
|
+
case 'thinking':
|
|
2079
|
+
thinkCount++;
|
|
2080
|
+
break;
|
|
2081
|
+
case 'spawn':
|
|
2082
|
+
spawnCount++;
|
|
2083
|
+
break;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
html += '<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;">';
|
|
2087
|
+
html +=
|
|
2088
|
+
'<span style="font-size:0.8rem;color:var(--text-secondary);">' +
|
|
2089
|
+
events.length +
|
|
2090
|
+
' events</span>';
|
|
2091
|
+
if (userCount)
|
|
2092
|
+
html +=
|
|
2093
|
+
'<span class="badge" style="background:rgba(88,166,255,0.15);color:#58a6ff;">' +
|
|
2094
|
+
userCount +
|
|
2095
|
+
' user</span>';
|
|
2096
|
+
if (assistantCount)
|
|
2097
|
+
html +=
|
|
2098
|
+
'<span class="badge" style="background:rgba(35,134,54,0.15);color:#3fb950;">' +
|
|
2099
|
+
assistantCount +
|
|
2100
|
+
' assistant</span>';
|
|
2101
|
+
if (toolCount)
|
|
2102
|
+
html +=
|
|
2103
|
+
'<span class="badge" style="background:rgba(240,136,62,0.15);color:#f0883e;">' +
|
|
2104
|
+
toolCount +
|
|
2105
|
+
' tools</span>';
|
|
2106
|
+
if (thinkCount)
|
|
2107
|
+
html +=
|
|
2108
|
+
'<span class="badge" style="background:rgba(188,140,255,0.15);color:#bc8cff;">' +
|
|
2109
|
+
thinkCount +
|
|
2110
|
+
' thinking</span>';
|
|
2111
|
+
if (spawnCount)
|
|
2112
|
+
html +=
|
|
2113
|
+
'<span class="badge" style="background:rgba(0,200,200,0.15);color:#00c8c8;">' +
|
|
2114
|
+
spawnCount +
|
|
2115
|
+
' spawns</span>';
|
|
2116
|
+
html += '</div>';
|
|
2117
|
+
|
|
2118
|
+
// Render events
|
|
2119
|
+
var typeMarkers = {
|
|
2120
|
+
user: { icon: '\ud83e\uddd1', color: '#58a6ff', label: 'User' },
|
|
2121
|
+
assistant: { icon: '\ud83e\udd16', color: '#3fb950', label: 'Assistant' },
|
|
2122
|
+
thinking: { icon: '\ud83d\udcad', color: '#bc8cff', label: 'Thinking' },
|
|
2123
|
+
tool_call: { icon: '\ud83d\udee0\ufe0f', color: '#f0883e', label: 'Tool Call' },
|
|
2124
|
+
tool_result: { icon: '\u2705', color: '#3fb950', label: 'Tool Result' },
|
|
2125
|
+
spawn: { icon: '\ud83d\udc64', color: '#00c8c8', label: 'Subagent' },
|
|
2126
|
+
model_change: { icon: '\u2699\ufe0f', color: '#8b949e', label: 'Model' },
|
|
2127
|
+
system: { icon: '\u2139\ufe0f', color: '#6e7681', label: 'System' },
|
|
2128
|
+
};
|
|
2129
|
+
|
|
2130
|
+
for (var j = 0; j < events.length; j++) {
|
|
2131
|
+
var evt = events[j];
|
|
2132
|
+
var marker = typeMarkers[evt.type] || typeMarkers.system;
|
|
2133
|
+
var evtTime = evt.timestamp ? new Date(evt.timestamp).toLocaleTimeString() : '';
|
|
2134
|
+
var contentPreview = escapeHtml((evt.content || '').substring(0, 300));
|
|
2135
|
+
if ((evt.content || '').length > 300) contentPreview += '...';
|
|
2136
|
+
|
|
2137
|
+
// Tool result with error gets red marker
|
|
2138
|
+
if (evt.type === 'tool_result' && evt.toolError) {
|
|
2139
|
+
marker = { icon: '\u274c', color: '#f85149', label: 'Tool Error' };
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
html += '<div class="timeline-item">';
|
|
2143
|
+
html += `<div class="timeline-marker" style="background:${marker.color};"></div>`;
|
|
2144
|
+
html += '<div class="timeline-content">';
|
|
2145
|
+
html += '<div class="timeline-header">';
|
|
2146
|
+
html +=
|
|
2147
|
+
'<span class="event-type">' +
|
|
2148
|
+
marker.icon +
|
|
2149
|
+
' <strong>' +
|
|
2150
|
+
escapeHtml(evt.name || marker.label) +
|
|
2151
|
+
'</strong>';
|
|
2152
|
+
if (evt.type === 'tool_call' && evt.toolName)
|
|
2153
|
+
html += ` <code style="font-size:0.75rem;color:#f0883e;">${escapeHtml(evt.toolName)}</code>`;
|
|
2154
|
+
html += '</span>';
|
|
2155
|
+
html += `<span class="event-time">${evtTime}`;
|
|
2156
|
+
if (evt.duration) html += ` · ${this.formatDuration(evt.duration)}`;
|
|
2157
|
+
if (evt.tokens?.total)
|
|
2158
|
+
html +=
|
|
2159
|
+
' · <span style="color:#bc8cff;">' +
|
|
2160
|
+
(evt.tokens.total > 1000 ? `${Math.round(evt.tokens.total / 1000)}k` : evt.tokens.total) +
|
|
2161
|
+
' tok</span>';
|
|
2162
|
+
html += '</span></div>';
|
|
2163
|
+
|
|
2164
|
+
if (contentPreview) {
|
|
2165
|
+
html += `<div class="event-details" style="margin-top:4px;">${contentPreview}</div>`;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
if (evt.type === 'tool_call' && evt.toolArgs) {
|
|
2169
|
+
var argsStr =
|
|
2170
|
+
typeof evt.toolArgs === 'string' ? evt.toolArgs : JSON.stringify(evt.toolArgs);
|
|
2171
|
+
html +=
|
|
2172
|
+
'<div class="event-details" style="margin-top:2px;font-family:monospace;font-size:0.7rem;color:var(--text-secondary);max-height:60px;overflow:hidden;">' +
|
|
2173
|
+
escapeHtml(argsStr.substring(0, 200)) +
|
|
2174
|
+
'</div>';
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
if (evt.type === 'tool_result' && evt.toolResult) {
|
|
2178
|
+
var resultColor = evt.toolError ? 'var(--accent-error)' : 'var(--text-secondary)';
|
|
2179
|
+
html +=
|
|
2180
|
+
'<div class="event-details" style="margin-top:2px;font-family:monospace;font-size:0.7rem;color:' +
|
|
2181
|
+
resultColor +
|
|
2182
|
+
';max-height:80px;overflow:hidden;">' +
|
|
2183
|
+
escapeHtml(evt.toolResult.substring(0, 300)) +
|
|
2184
|
+
'</div>';
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
html += '</div></div>';
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
container.innerHTML = html;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// ---------------------------------------------------------------------------
|
|
2194
|
+
// Tab 7: Transcript (chat bubble UI for session traces)
|
|
2195
|
+
// ---------------------------------------------------------------------------
|
|
2196
|
+
async renderTranscript() {
|
|
2197
|
+
var container = document.getElementById('transcriptContent');
|
|
2198
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
2199
|
+
|
|
2200
|
+
if (!trace) {
|
|
2201
|
+
container.innerHTML =
|
|
2202
|
+
'<div class="empty-state"><div class="empty-state-text">Select a trace to view transcript.</div></div>';
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
if (trace.sourceType !== 'session') {
|
|
2207
|
+
container.innerHTML =
|
|
2208
|
+
'<div class="empty-state"><div class="empty-state-text">Transcript view is only available for session traces (JSONL files).</div></div>';
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
var events = trace.sessionEvents || [];
|
|
2213
|
+
if (events.length === 0 && trace.filename) {
|
|
2214
|
+
try {
|
|
2215
|
+
var res = await fetch(`/api/traces/${encodeURIComponent(trace.filename)}/events`);
|
|
2216
|
+
if (res.ok) {
|
|
2217
|
+
var data = await res.json();
|
|
2218
|
+
events = data.events || [];
|
|
2219
|
+
}
|
|
2220
|
+
} catch (_e) {
|
|
2221
|
+
/* ignore */
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
if (events.length === 0) {
|
|
2226
|
+
container.innerHTML =
|
|
2227
|
+
'<div class="empty-state"><div class="empty-state-text">No session events found.</div></div>';
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
var html = '<div style="display:flex;flex-direction:column;gap:4px;padding:0.5rem;">';
|
|
2232
|
+
|
|
2233
|
+
var thinkingIdx = 0;
|
|
2234
|
+
for (var i = 0; i < events.length; i++) {
|
|
2235
|
+
var evt = events[i];
|
|
2236
|
+
var evtTime = evt.timestamp ? new Date(evt.timestamp).toLocaleTimeString() : '';
|
|
2237
|
+
|
|
2238
|
+
if (evt.type === 'user') {
|
|
2239
|
+
html += '<div class="chat-bubble chat-user">';
|
|
2240
|
+
html += escapeHtml(evt.content || '');
|
|
2241
|
+
html += `<div class="chat-meta">${evtTime}</div>`;
|
|
2242
|
+
html += '</div>';
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
if (evt.type === 'assistant') {
|
|
2246
|
+
html += '<div class="chat-bubble chat-assistant">';
|
|
2247
|
+
html += escapeHtml(evt.content || '');
|
|
2248
|
+
html += `<div class="chat-meta">${evtTime}`;
|
|
2249
|
+
if (evt.tokens?.total) {
|
|
2250
|
+
html +=
|
|
2251
|
+
' · <span class="chat-tokens">' +
|
|
2252
|
+
(evt.tokens.total > 1000
|
|
2253
|
+
? `${Math.round(evt.tokens.total / 1000)}k`
|
|
2254
|
+
: evt.tokens.total) +
|
|
2255
|
+
' tokens';
|
|
2256
|
+
if (evt.tokens.cost) html += ` ($${evt.tokens.cost.toFixed(4)})`;
|
|
2257
|
+
html += '</span>';
|
|
2258
|
+
}
|
|
2259
|
+
if (evt.model) html += ` · ${escapeHtml(evt.model)}`;
|
|
2260
|
+
html += '</div></div>';
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
if (evt.type === 'thinking') {
|
|
2264
|
+
thinkingIdx++;
|
|
2265
|
+
var tId = `thinking-toggle-${thinkingIdx}`;
|
|
2266
|
+
html += '<div class="chat-bubble chat-thinking">';
|
|
2267
|
+
html +=
|
|
2268
|
+
'<span class="chat-thinking-toggle" onclick="var b=document.getElementById(\'' +
|
|
2269
|
+
tId +
|
|
2270
|
+
"');b.classList.toggle('open');\">\ud83d\udcad Thinking (click to expand)</span>";
|
|
2271
|
+
html +=
|
|
2272
|
+
'<div class="chat-thinking-body" id="' +
|
|
2273
|
+
tId +
|
|
2274
|
+
'">' +
|
|
2275
|
+
escapeHtml(evt.content || '') +
|
|
2276
|
+
'</div>';
|
|
2277
|
+
html += `<div class="chat-meta">${evtTime}</div>`;
|
|
2278
|
+
html += '</div>';
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
if (evt.type === 'tool_call') {
|
|
2282
|
+
html += '<div class="chat-bubble chat-tool">';
|
|
2283
|
+
html +=
|
|
2284
|
+
'<strong>\ud83d\udee0\ufe0f ' +
|
|
2285
|
+
escapeHtml(evt.toolName || evt.name || 'Tool') +
|
|
2286
|
+
'</strong>';
|
|
2287
|
+
if (evt.toolArgs) {
|
|
2288
|
+
var argsStr =
|
|
2289
|
+
typeof evt.toolArgs === 'string' ? evt.toolArgs : JSON.stringify(evt.toolArgs, null, 2);
|
|
2290
|
+
html +=
|
|
2291
|
+
'<div style="margin-top:4px;max-height:100px;overflow:hidden;font-size:0.75rem;color:var(--text-secondary);">' +
|
|
2292
|
+
escapeHtml(argsStr.substring(0, 300)) +
|
|
2293
|
+
'</div>';
|
|
2294
|
+
}
|
|
2295
|
+
html += `<div class="chat-meta">${evtTime}`;
|
|
2296
|
+
if (evt.duration) html += ` · ${this.formatDuration(evt.duration)}`;
|
|
2297
|
+
html += '</div></div>';
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
if (evt.type === 'tool_result') {
|
|
2301
|
+
var isError = !!evt.toolError;
|
|
2302
|
+
html +=
|
|
2303
|
+
'<div class="chat-bubble chat-tool" style="' +
|
|
2304
|
+
(isError ? 'border-color:var(--accent-error);' : 'border-color:rgba(35,134,54,0.3);') +
|
|
2305
|
+
'">';
|
|
2306
|
+
html += `<strong>${isError ? '\u274c' : '\u2705'} Result</strong>`;
|
|
2307
|
+
var resultText = evt.toolError || evt.toolResult || '';
|
|
2308
|
+
html +=
|
|
2309
|
+
'<div style="margin-top:4px;max-height:120px;overflow:hidden;font-size:0.75rem;color:' +
|
|
2310
|
+
(isError ? 'var(--accent-error)' : 'var(--text-secondary)') +
|
|
2311
|
+
';">' +
|
|
2312
|
+
escapeHtml(resultText.substring(0, 400)) +
|
|
2313
|
+
'</div>';
|
|
2314
|
+
html += `<div class="chat-meta">${evtTime}</div>`;
|
|
2315
|
+
html += '</div>';
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
if (evt.type === 'spawn') {
|
|
2319
|
+
html +=
|
|
2320
|
+
'<div class="chat-bubble" style="margin:0 auto;max-width:70%;background:rgba(0,200,200,0.08);border:1px solid rgba(0,200,200,0.25);text-align:center;">';
|
|
2321
|
+
html += '\ud83d\udc64 Subagent spawned';
|
|
2322
|
+
if (evt.content) html += `: <code>${escapeHtml(evt.content.substring(0, 40))}</code>`;
|
|
2323
|
+
html += `<div class="chat-meta">${evtTime}</div>`;
|
|
2324
|
+
html += '</div>';
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
html += '</div>';
|
|
2329
|
+
container.innerHTML = html;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// ---------------------------------------------------------------------------
|
|
2333
|
+
// Alert panel
|
|
2334
|
+
// ---------------------------------------------------------------------------
|
|
2335
|
+
showAlert(messages) {
|
|
2336
|
+
var panel = document.getElementById('alertPanel');
|
|
2337
|
+
var list = document.getElementById('alertList');
|
|
2338
|
+
if (!messages || messages.length === 0) {
|
|
2339
|
+
panel.classList.remove('show');
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
list.innerHTML = messages.map((m) => `<li>${escapeHtml(m)}</li>`).join('');
|
|
2343
|
+
panel.classList.add('show');
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// ---------------------------------------------------------------------------
|
|
2347
|
+
// Public / debug
|
|
2348
|
+
// ---------------------------------------------------------------------------
|
|
2349
|
+
getStats() {
|
|
2350
|
+
return this.stats;
|
|
2351
|
+
}
|
|
2352
|
+
getTraces() {
|
|
2353
|
+
return this.traces;
|
|
2354
|
+
}
|
|
2355
|
+
reconnect() {
|
|
2356
|
+
if (this.ws) this.ws.close();
|
|
2357
|
+
this.reconnectAttempts = 0;
|
|
2358
|
+
this.connectWebSocket();
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// Categorize traces by activity type
|
|
2362
|
+
getTraceActivity(trace) {
|
|
2363
|
+
if (!trace) return 'unknown';
|
|
2364
|
+
|
|
2365
|
+
var agentId = (trace.agentId || '').toLowerCase();
|
|
2366
|
+
var name = (trace.name || '').toLowerCase();
|
|
2367
|
+
var filename = (trace.filename || '').toLowerCase();
|
|
2368
|
+
|
|
2369
|
+
// Check for specific agent types
|
|
2370
|
+
if (agentId.includes('main') || name.includes('main')) return 'main';
|
|
2371
|
+
if (agentId.includes('agent') || name.includes('agent')) return 'agents';
|
|
2372
|
+
|
|
2373
|
+
// Check filename patterns
|
|
2374
|
+
if (filename.includes('browser') || name.includes('browser')) return 'browser';
|
|
2375
|
+
if (filename.includes('context') || name.includes('context')) return 'context';
|
|
2376
|
+
|
|
2377
|
+
// Check for activity types in trace content
|
|
2378
|
+
var nodes = trace.nodes || {};
|
|
2379
|
+
var nodeTypes = [];
|
|
2380
|
+
|
|
2381
|
+
if (nodes instanceof Map) {
|
|
2382
|
+
nodes.forEach((node) => {
|
|
2383
|
+
if (node.type) nodeTypes.push(node.type);
|
|
2384
|
+
});
|
|
2385
|
+
} else if (typeof nodes === 'object') {
|
|
2386
|
+
for (var nodeId in nodes) {
|
|
2387
|
+
var node = nodes[nodeId];
|
|
2388
|
+
if (node?.type) nodeTypes.push(node.type);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// Categorize based on node types and content
|
|
2393
|
+
if (nodeTypes.includes('tool') || nodeTypes.includes('exec')) return 'exec';
|
|
2394
|
+
if (nodeTypes.includes('read') || name.includes('read')) return 'read';
|
|
2395
|
+
if (nodeTypes.includes('write') || name.includes('write')) return 'write';
|
|
2396
|
+
if (nodeTypes.includes('think') || name.includes('think')) return 'think';
|
|
2397
|
+
if (nodeTypes.includes('user') || name.includes('user')) return 'user';
|
|
2398
|
+
if (nodeTypes.includes('tool')) return 'tool';
|
|
2399
|
+
|
|
2400
|
+
return 'other';
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// Tag processes by activity type
|
|
2404
|
+
getProcessActivityTag(_cmd, cmdline, _pid) {
|
|
2405
|
+
// Main processes (primary orchestrators)
|
|
2406
|
+
if (
|
|
2407
|
+
cmdline.includes('main') ||
|
|
2408
|
+
cmdline.includes('orchestrator') ||
|
|
2409
|
+
cmdline.includes('coordinator') ||
|
|
2410
|
+
cmdline.includes('master')
|
|
2411
|
+
) {
|
|
2412
|
+
return 'main';
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
// Agent processes
|
|
2416
|
+
if (cmdline.includes('agent') && !cmdline.includes('browser')) {
|
|
2417
|
+
return 'agents';
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// Browser/UI processes
|
|
2421
|
+
if (
|
|
2422
|
+
cmdline.includes('browser') ||
|
|
2423
|
+
cmdline.includes('chrome') ||
|
|
2424
|
+
cmdline.includes('firefox') ||
|
|
2425
|
+
cmdline.includes('dashboard')
|
|
2426
|
+
) {
|
|
2427
|
+
return 'browser';
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
// Context/memory processes
|
|
2431
|
+
if (
|
|
2432
|
+
cmdline.includes('context') ||
|
|
2433
|
+
cmdline.includes('memory') ||
|
|
2434
|
+
cmdline.includes('cache') ||
|
|
2435
|
+
cmdline.includes('embedding')
|
|
2436
|
+
) {
|
|
2437
|
+
return 'context';
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// Execution processes
|
|
2441
|
+
if (
|
|
2442
|
+
cmdline.includes('exec') ||
|
|
2443
|
+
cmdline.includes('runner') ||
|
|
2444
|
+
cmdline.includes('executor') ||
|
|
2445
|
+
cmdline.includes('worker')
|
|
2446
|
+
) {
|
|
2447
|
+
return 'exec';
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// Read operations
|
|
2451
|
+
if (
|
|
2452
|
+
cmdline.includes('read') ||
|
|
2453
|
+
cmdline.includes('scanner') ||
|
|
2454
|
+
cmdline.includes('parser') ||
|
|
2455
|
+
cmdline.includes('loader')
|
|
2456
|
+
) {
|
|
2457
|
+
return 'read';
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// Tool processes
|
|
2461
|
+
if (
|
|
2462
|
+
cmdline.includes('tool') ||
|
|
2463
|
+
cmdline.includes('utility') ||
|
|
2464
|
+
cmdline.includes('helper') ||
|
|
2465
|
+
cmdline.includes('script')
|
|
2466
|
+
) {
|
|
2467
|
+
return 'tool';
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
// Thinking/AI processes
|
|
2471
|
+
if (
|
|
2472
|
+
cmdline.includes('think') ||
|
|
2473
|
+
cmdline.includes('reason') ||
|
|
2474
|
+
cmdline.includes('llm') ||
|
|
2475
|
+
cmdline.includes('model')
|
|
2476
|
+
) {
|
|
2477
|
+
return 'think';
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
// User interface processes
|
|
2481
|
+
if (
|
|
2482
|
+
cmdline.includes('ui') ||
|
|
2483
|
+
cmdline.includes('frontend') ||
|
|
2484
|
+
cmdline.includes('interface') ||
|
|
2485
|
+
cmdline.includes('client')
|
|
2486
|
+
) {
|
|
2487
|
+
return 'user';
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// Write/output processes
|
|
2491
|
+
if (
|
|
2492
|
+
cmdline.includes('write') ||
|
|
2493
|
+
cmdline.includes('output') ||
|
|
2494
|
+
cmdline.includes('export') ||
|
|
2495
|
+
cmdline.includes('save')
|
|
2496
|
+
) {
|
|
2497
|
+
return 'write';
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
return 'other';
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// ---------------------------------------------------------------------------
|
|
2504
|
+
// Tab 8: Agent Timeline (Gantt Chart)
|
|
2505
|
+
// ---------------------------------------------------------------------------
|
|
2506
|
+
renderAgentTimeline() {
|
|
2507
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
2508
|
+
if (!trace || !trace.agentId) {
|
|
2509
|
+
document.getElementById('agentTimelineEmpty').style.display = '';
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
var agentId = trace.agentId;
|
|
2514
|
+
|
|
2515
|
+
if (this._agentTimelineAgent === agentId && this._agentTimelineRendered) return;
|
|
2516
|
+
this._agentTimelineAgent = agentId;
|
|
2517
|
+
|
|
2518
|
+
var container = document.getElementById('agentTimelineContent');
|
|
2519
|
+
container.innerHTML =
|
|
2520
|
+
'<div class="empty-state"><div class="empty-state-icon" style="animation:spin 1s linear infinite">⚙</div>' +
|
|
2521
|
+
'<div class="empty-state-text">Loading timeline for ' +
|
|
2522
|
+
escapeHtml(agentId) +
|
|
2523
|
+
'...</div></div>';
|
|
2524
|
+
|
|
2525
|
+
fetch(`/api/agents/${encodeURIComponent(agentId)}/timeline?limit=50`)
|
|
2526
|
+
.then((r) => r.json())
|
|
2527
|
+
.then((data) => {
|
|
2528
|
+
if (data.error || !data.executions || data.executions.length === 0) {
|
|
2529
|
+
container.innerHTML =
|
|
2530
|
+
'<div class="empty-state"><div class="empty-state-text">No timeline data for ' +
|
|
2531
|
+
escapeHtml(agentId) +
|
|
2532
|
+
'</div></div>';
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
this._agentTimelineRendered = true;
|
|
2536
|
+
this._renderGantt(container, data);
|
|
2537
|
+
})
|
|
2538
|
+
.catch(() => {
|
|
2539
|
+
container.innerHTML =
|
|
2540
|
+
'<div class="empty-state"><div class="empty-state-text">Failed to load agent timeline.</div></div>';
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
_renderGantt(container, data) {
|
|
2545
|
+
var execs = data.executions;
|
|
2546
|
+
var minTime = data.minTime;
|
|
2547
|
+
var maxTime = data.maxTime;
|
|
2548
|
+
var timeSpan = maxTime - minTime || 1;
|
|
2549
|
+
|
|
2550
|
+
// Layout constants
|
|
2551
|
+
var labelW = 220;
|
|
2552
|
+
var chartW = 900;
|
|
2553
|
+
var rowH = 28;
|
|
2554
|
+
var subRowH = 20;
|
|
2555
|
+
var headerH = 36;
|
|
2556
|
+
var totalW = labelW + chartW + 20;
|
|
2557
|
+
|
|
2558
|
+
// Build HTML
|
|
2559
|
+
var html =
|
|
2560
|
+
'<div class="gantt-wrapper" style="font-size:11px;color:#c9d1d9;min-width:' +
|
|
2561
|
+
totalW +
|
|
2562
|
+
'px;">';
|
|
2563
|
+
|
|
2564
|
+
// Header with time axis
|
|
2565
|
+
html +=
|
|
2566
|
+
'<div class="gantt-header" style="display:flex;height:' +
|
|
2567
|
+
headerH +
|
|
2568
|
+
'px;border-bottom:1px solid #30363d;position:sticky;top:0;background:#0d1117;z-index:2;">';
|
|
2569
|
+
html +=
|
|
2570
|
+
'<div style="width:' +
|
|
2571
|
+
labelW +
|
|
2572
|
+
'px;min-width:' +
|
|
2573
|
+
labelW +
|
|
2574
|
+
'px;padding:8px 10px;font-weight:600;color:#8b949e;">Execution</div>';
|
|
2575
|
+
html += '<div style="flex:1;position:relative;">';
|
|
2576
|
+
// Time ticks
|
|
2577
|
+
var tickCount = 6;
|
|
2578
|
+
for (var t = 0; t <= tickCount; t++) {
|
|
2579
|
+
var pct = (t / tickCount) * 100;
|
|
2580
|
+
var tickTime = minTime + (t / tickCount) * timeSpan;
|
|
2581
|
+
var d = new Date(tickTime);
|
|
2582
|
+
var label =
|
|
2583
|
+
d.getMonth() +
|
|
2584
|
+
1 +
|
|
2585
|
+
'/' +
|
|
2586
|
+
d.getDate() +
|
|
2587
|
+
' ' +
|
|
2588
|
+
String(d.getHours()).padStart(2, '0') +
|
|
2589
|
+
':' +
|
|
2590
|
+
String(d.getMinutes()).padStart(2, '0');
|
|
2591
|
+
html +=
|
|
2592
|
+
'<div style="position:absolute;left:' +
|
|
2593
|
+
pct +
|
|
2594
|
+
'%;top:0;height:100%;border-left:1px solid #21262d;padding:8px 4px;font-size:9px;color:#6b7280;white-space:nowrap;">' +
|
|
2595
|
+
label +
|
|
2596
|
+
'</div>';
|
|
2597
|
+
}
|
|
2598
|
+
html += '</div></div>';
|
|
2599
|
+
|
|
2600
|
+
// Rows
|
|
2601
|
+
html += '<div class="gantt-body">';
|
|
2602
|
+
for (var i = 0; i < execs.length; i++) {
|
|
2603
|
+
var exec = execs[i];
|
|
2604
|
+
var execStart = ((exec.startTime - minTime) / timeSpan) * 100;
|
|
2605
|
+
var execWidth = Math.max(0.3, ((exec.endTime - exec.startTime) / timeSpan) * 100);
|
|
2606
|
+
var statusColor =
|
|
2607
|
+
exec.status === 'failed' ? '#ef4444' : exec.status === 'running' ? '#3b82f6' : '#10b981';
|
|
2608
|
+
var hasActivities = exec.activities && exec.activities.length > 0;
|
|
2609
|
+
var execId = `gantt-exec-${i}`;
|
|
2610
|
+
|
|
2611
|
+
// Main execution row
|
|
2612
|
+
html +=
|
|
2613
|
+
'<div class="gantt-row" style="display:flex;height:' +
|
|
2614
|
+
rowH +
|
|
2615
|
+
'px;border-bottom:1px solid #161b22;cursor:pointer;" ' +
|
|
2616
|
+
'onclick="(function(){var el=document.getElementById(\'' +
|
|
2617
|
+
execId +
|
|
2618
|
+
"');if(el)el.style.display=el.style.display==='none'?'block':'none';})()\" " +
|
|
2619
|
+
'title="Click to ' +
|
|
2620
|
+
(hasActivities ? 'expand' : 'view') +
|
|
2621
|
+
'">';
|
|
2622
|
+
|
|
2623
|
+
// Label
|
|
2624
|
+
var execName = exec.name || exec.filename || exec.id;
|
|
2625
|
+
if (execName.length > 28) execName = `${execName.slice(0, 28)}...`;
|
|
2626
|
+
var dur = this.computeDuration(exec.startTime, exec.endTime);
|
|
2627
|
+
var triggerBadge = exec.trigger
|
|
2628
|
+
? '<span style="background:#1f2937;padding:1px 4px;border-radius:3px;font-size:8px;margin-left:4px;">' +
|
|
2629
|
+
escapeHtml(exec.trigger) +
|
|
2630
|
+
'</span>'
|
|
2631
|
+
: '';
|
|
2632
|
+
var expandIcon = hasActivities
|
|
2633
|
+
? '<span style="color:#6b7280;margin-right:4px;">▶</span>'
|
|
2634
|
+
: '<span style="width:14px;display:inline-block;"></span>';
|
|
2635
|
+
|
|
2636
|
+
html +=
|
|
2637
|
+
'<div style="width:' +
|
|
2638
|
+
labelW +
|
|
2639
|
+
'px;min-width:' +
|
|
2640
|
+
labelW +
|
|
2641
|
+
'px;padding:4px 10px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;line-height:' +
|
|
2642
|
+
(rowH - 8) +
|
|
2643
|
+
'px;">' +
|
|
2644
|
+
expandIcon +
|
|
2645
|
+
escapeHtml(execName) +
|
|
2646
|
+
triggerBadge +
|
|
2647
|
+
'</div>';
|
|
2648
|
+
|
|
2649
|
+
// Bar
|
|
2650
|
+
html += '<div style="flex:1;position:relative;padding:4px 0;">';
|
|
2651
|
+
html +=
|
|
2652
|
+
'<div style="position:absolute;left:' +
|
|
2653
|
+
execStart +
|
|
2654
|
+
'%;width:' +
|
|
2655
|
+
execWidth +
|
|
2656
|
+
'%;top:4px;height:' +
|
|
2657
|
+
(rowH - 12) +
|
|
2658
|
+
'px;' +
|
|
2659
|
+
'background:' +
|
|
2660
|
+
statusColor +
|
|
2661
|
+
';border-radius:3px;opacity:0.85;min-width:3px;" ' +
|
|
2662
|
+
'title="' +
|
|
2663
|
+
escapeHtml(exec.name || '') +
|
|
2664
|
+
' | ' +
|
|
2665
|
+
dur +
|
|
2666
|
+
' | ' +
|
|
2667
|
+
escapeHtml(exec.status) +
|
|
2668
|
+
'"></div>';
|
|
2669
|
+
html += '</div></div>';
|
|
2670
|
+
|
|
2671
|
+
// Sub-activities (collapsed by default)
|
|
2672
|
+
if (hasActivities) {
|
|
2673
|
+
html += `<div id="${execId}" style="display:none;background:#0a0e14;">`;
|
|
2674
|
+
// Filter to top-level activities (no parentId or parentId is root)
|
|
2675
|
+
var rootIds = new Set();
|
|
2676
|
+
if (exec.activities.length > 0) {
|
|
2677
|
+
var firstAct = exec.activities[0];
|
|
2678
|
+
rootIds.add(firstAct.id);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
for (var j = 0; j < exec.activities.length; j++) {
|
|
2682
|
+
var act = exec.activities[j];
|
|
2683
|
+
var actStart = ((Math.max(act.startTime, exec.startTime) - minTime) / timeSpan) * 100;
|
|
2684
|
+
var actEnd = act.endTime || act.startTime;
|
|
2685
|
+
var actWidth = Math.max(
|
|
2686
|
+
0.2,
|
|
2687
|
+
((actEnd - Math.max(act.startTime, exec.startTime)) / timeSpan) * 100,
|
|
2688
|
+
);
|
|
2689
|
+
var actColor =
|
|
2690
|
+
act.status === 'failed'
|
|
2691
|
+
? '#f87171'
|
|
2692
|
+
: act.type === 'user'
|
|
2693
|
+
? '#60a5fa'
|
|
2694
|
+
: act.type === 'assistant'
|
|
2695
|
+
? '#34d399'
|
|
2696
|
+
: act.type === 'thinking'
|
|
2697
|
+
? '#a78bfa'
|
|
2698
|
+
: act.type === 'tool_call'
|
|
2699
|
+
? '#fb923c'
|
|
2700
|
+
: act.type === 'tool_result'
|
|
2701
|
+
? '#4ade80'
|
|
2702
|
+
: act.type === 'agent'
|
|
2703
|
+
? '#38bdf8'
|
|
2704
|
+
: '#6b7280';
|
|
2705
|
+
var actName = act.name || act.type;
|
|
2706
|
+
if (actName.length > 30) actName = `${actName.slice(0, 30)}...`;
|
|
2707
|
+
var isChild = act.parentId && !rootIds.has(act.id);
|
|
2708
|
+
|
|
2709
|
+
html += `<div style="display:flex;height:${subRowH}px;border-bottom:1px solid #0d1117;">`;
|
|
2710
|
+
html +=
|
|
2711
|
+
'<div style="width:' +
|
|
2712
|
+
labelW +
|
|
2713
|
+
'px;min-width:' +
|
|
2714
|
+
labelW +
|
|
2715
|
+
'px;padding:2px 10px 2px ' +
|
|
2716
|
+
(isChild ? '30' : '20') +
|
|
2717
|
+
'px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:10px;color:#8b949e;line-height:' +
|
|
2718
|
+
(subRowH - 4) +
|
|
2719
|
+
'px;">' +
|
|
2720
|
+
'<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' +
|
|
2721
|
+
actColor +
|
|
2722
|
+
';margin-right:6px;vertical-align:middle;"></span>' +
|
|
2723
|
+
escapeHtml(actName) +
|
|
2724
|
+
'</div>';
|
|
2725
|
+
html += '<div style="flex:1;position:relative;">';
|
|
2726
|
+
html +=
|
|
2727
|
+
'<div style="position:absolute;left:' +
|
|
2728
|
+
actStart +
|
|
2729
|
+
'%;width:' +
|
|
2730
|
+
actWidth +
|
|
2731
|
+
'%;top:3px;height:' +
|
|
2732
|
+
(subRowH - 8) +
|
|
2733
|
+
'px;' +
|
|
2734
|
+
'background:' +
|
|
2735
|
+
actColor +
|
|
2736
|
+
';border-radius:2px;opacity:0.7;min-width:2px;" ' +
|
|
2737
|
+
'title="' +
|
|
2738
|
+
escapeHtml(act.name || act.type) +
|
|
2739
|
+
' | ' +
|
|
2740
|
+
escapeHtml(act.status) +
|
|
2741
|
+
'"></div>';
|
|
2742
|
+
html += '</div></div>';
|
|
2743
|
+
}
|
|
2744
|
+
html += '</div>';
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
html += '</div>';
|
|
2749
|
+
|
|
2750
|
+
// Summary bar
|
|
2751
|
+
html += '<div style="padding:10px;border-top:1px solid #30363d;color:#8b949e;font-size:10px;">';
|
|
2752
|
+
html +=
|
|
2753
|
+
escapeHtml(data.agentId) +
|
|
2754
|
+
' — ' +
|
|
2755
|
+
data.executions.length +
|
|
2756
|
+
' of ' +
|
|
2757
|
+
data.totalExecutions +
|
|
2758
|
+
' executions shown';
|
|
2759
|
+
var timeRange = `${new Date(minTime).toLocaleDateString()} to ${new Date(maxTime).toLocaleDateString()}`;
|
|
2760
|
+
html += ` — ${timeRange}`;
|
|
2761
|
+
html += '</div>';
|
|
2762
|
+
|
|
2763
|
+
html += '</div>';
|
|
2764
|
+
container.innerHTML = html;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// ---------------------------------------------------------------------------
|
|
2768
|
+
// Tab 9: Process Map (Process Mining Graph)
|
|
2769
|
+
// ---------------------------------------------------------------------------
|
|
2770
|
+
renderProcessMap() {
|
|
2771
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
2772
|
+
if (!trace || !trace.agentId) {
|
|
2773
|
+
document.getElementById('processMapEmpty').style.display = '';
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
var agentId = trace.agentId;
|
|
2778
|
+
|
|
2779
|
+
// Avoid re-fetching for same agent
|
|
2780
|
+
if (this._processMapAgent === agentId && this._cyProcessMap) return;
|
|
2781
|
+
this._processMapAgent = agentId;
|
|
2782
|
+
|
|
2783
|
+
document.getElementById('processMapEmpty').innerHTML =
|
|
2784
|
+
'<div class="empty-state-icon" style="animation:spin 1s linear infinite">⚙</div>' +
|
|
2785
|
+
'<div class="empty-state-text">Building process map for ' +
|
|
2786
|
+
escapeHtml(agentId) +
|
|
2787
|
+
'...</div>';
|
|
2788
|
+
document.getElementById('processMapEmpty').style.display = '';
|
|
2789
|
+
|
|
2790
|
+
fetch(`/api/agents/${encodeURIComponent(agentId)}/process-graph`)
|
|
2791
|
+
.then((r) => r.json())
|
|
2792
|
+
.then((data) => {
|
|
2793
|
+
if (data.error || !data.nodes || data.nodes.length === 0) {
|
|
2794
|
+
document.getElementById('processMapEmpty').innerHTML =
|
|
2795
|
+
'<div class="empty-state-icon">⚙</div>' +
|
|
2796
|
+
'<div class="empty-state-text">No process data for ' +
|
|
2797
|
+
escapeHtml(agentId) +
|
|
2798
|
+
'</div>';
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
document.getElementById('processMapEmpty').style.display = 'none';
|
|
2802
|
+
this._buildProcessMapGraph(data);
|
|
2803
|
+
this._loadVariantPanel(agentId);
|
|
2804
|
+
this._loadProfileCard(agentId);
|
|
2805
|
+
})
|
|
2806
|
+
.catch(() => {
|
|
2807
|
+
document.getElementById('processMapEmpty').innerHTML =
|
|
2808
|
+
'<div class="empty-state-icon">⚙</div>' +
|
|
2809
|
+
'<div class="empty-state-text">Failed to load process map.</div>';
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
_buildProcessMapGraph(data) {
|
|
2814
|
+
if (this._cyProcessMap) {
|
|
2815
|
+
this._cyProcessMap.destroy();
|
|
2816
|
+
this._cyProcessMap = null;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
var elements = [];
|
|
2820
|
+
var maxNode = data.maxNodeCount || 1;
|
|
2821
|
+
var maxEdge = data.maxEdgeCount || 1;
|
|
2822
|
+
|
|
2823
|
+
// Add nodes
|
|
2824
|
+
for (var i = 0; i < data.nodes.length; i++) {
|
|
2825
|
+
var node = data.nodes[i];
|
|
2826
|
+
// Skip very rare activities (< 2% frequency) to reduce clutter, but keep virtual nodes
|
|
2827
|
+
if (!node.isVirtual && node.frequency < 0.02 && data.nodes.length > 15) continue;
|
|
2828
|
+
|
|
2829
|
+
var size = node.isVirtual ? 30 : Math.max(25, Math.min(70, 25 + 45 * (node.count / maxNode)));
|
|
2830
|
+
var label = node.label;
|
|
2831
|
+
if (!node.isVirtual && node.count > 1) label += ` (${node.count})`;
|
|
2832
|
+
|
|
2833
|
+
elements.push({
|
|
2834
|
+
group: 'nodes',
|
|
2835
|
+
data: {
|
|
2836
|
+
id: node.id,
|
|
2837
|
+
label: label,
|
|
2838
|
+
count: node.count,
|
|
2839
|
+
frequency: node.frequency,
|
|
2840
|
+
avgDuration: node.avgDuration,
|
|
2841
|
+
failRate: node.failRate,
|
|
2842
|
+
p95Duration: node.p95Duration || 0,
|
|
2843
|
+
isVirtual: node.isVirtual,
|
|
2844
|
+
size: size,
|
|
2845
|
+
fullData: node,
|
|
2846
|
+
},
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// Collect valid node IDs
|
|
2851
|
+
var validIds = new Set(elements.map((e) => e.data.id));
|
|
2852
|
+
|
|
2853
|
+
// Add edges (only between valid nodes)
|
|
2854
|
+
for (var j = 0; j < data.edges.length; j++) {
|
|
2855
|
+
var edge = data.edges[j];
|
|
2856
|
+
if (!validIds.has(edge.source) || !validIds.has(edge.target)) continue;
|
|
2857
|
+
// Skip very rare transitions
|
|
2858
|
+
if (edge.frequency < 0.02 && data.edges.length > 30) continue;
|
|
2859
|
+
|
|
2860
|
+
var width = Math.max(1, Math.min(8, 1 + 7 * (edge.count / maxEdge)));
|
|
2861
|
+
var opacity = Math.max(0.3, Math.min(1.0, 0.3 + 0.7 * (edge.count / maxEdge)));
|
|
2862
|
+
|
|
2863
|
+
elements.push({
|
|
2864
|
+
group: 'edges',
|
|
2865
|
+
data: {
|
|
2866
|
+
id: `pe-${edge.source}-${edge.target}`,
|
|
2867
|
+
source: edge.source,
|
|
2868
|
+
target: edge.target,
|
|
2869
|
+
count: edge.count,
|
|
2870
|
+
frequency: edge.frequency,
|
|
2871
|
+
width: width,
|
|
2872
|
+
opacity: opacity,
|
|
2873
|
+
label: edge.count > 1 ? String(edge.count) : '',
|
|
2874
|
+
},
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
var container = document.getElementById('cyProcessMap');
|
|
2879
|
+
|
|
2880
|
+
this._cyProcessMap = cytoscape({
|
|
2881
|
+
container: container,
|
|
2882
|
+
elements: elements,
|
|
2883
|
+
style: [
|
|
2884
|
+
{
|
|
2885
|
+
selector: 'node',
|
|
2886
|
+
style: {
|
|
2887
|
+
label: 'data(label)',
|
|
2888
|
+
width: 'data(size)',
|
|
2889
|
+
height: 'data(size)',
|
|
2890
|
+
'font-size': '9px',
|
|
2891
|
+
'text-valign': 'bottom',
|
|
2892
|
+
'text-halign': 'center',
|
|
2893
|
+
'text-margin-y': 6,
|
|
2894
|
+
color: '#c9d1d9',
|
|
2895
|
+
'text-outline-color': '#0d1117',
|
|
2896
|
+
'text-outline-width': 2,
|
|
2897
|
+
'text-wrap': 'ellipsis',
|
|
2898
|
+
'text-max-width': '100px',
|
|
2899
|
+
'border-width': 2,
|
|
2900
|
+
'border-color': '#30363d',
|
|
2901
|
+
'background-color': '#3b82f6',
|
|
2902
|
+
shape: 'round-rectangle',
|
|
2903
|
+
},
|
|
2904
|
+
},
|
|
2905
|
+
// Virtual START/END nodes
|
|
2906
|
+
{
|
|
2907
|
+
selector: 'node[?isVirtual]',
|
|
2908
|
+
style: {
|
|
2909
|
+
'background-color': '#6b7280',
|
|
2910
|
+
shape: 'ellipse',
|
|
2911
|
+
'border-color': '#4b5563',
|
|
2912
|
+
'font-size': '8px',
|
|
2913
|
+
'font-weight': 'bold',
|
|
2914
|
+
'text-valign': 'center',
|
|
2915
|
+
'text-margin-y': 0,
|
|
2916
|
+
},
|
|
2917
|
+
},
|
|
2918
|
+
// Color by fail rate: green → yellow → red
|
|
2919
|
+
{
|
|
2920
|
+
selector: 'node[failRate <= 0]',
|
|
2921
|
+
style: { 'background-color': '#10b981', 'border-color': '#2ea043' },
|
|
2922
|
+
},
|
|
2923
|
+
{
|
|
2924
|
+
selector: 'node[failRate > 0][failRate <= 0.1]',
|
|
2925
|
+
style: { 'background-color': '#22c55e', 'border-color': '#3fb950' },
|
|
2926
|
+
},
|
|
2927
|
+
{
|
|
2928
|
+
selector: 'node[failRate > 0.1][failRate <= 0.3]',
|
|
2929
|
+
style: { 'background-color': '#eab308', 'border-color': '#d29922' },
|
|
2930
|
+
},
|
|
2931
|
+
{
|
|
2932
|
+
selector: 'node[failRate > 0.3]',
|
|
2933
|
+
style: { 'background-color': '#ef4444', 'border-color': '#f85149' },
|
|
2934
|
+
},
|
|
2935
|
+
// Bottleneck heat: p95 duration highlighting (overrides failRate coloring when present)
|
|
2936
|
+
{
|
|
2937
|
+
selector: 'node[p95Duration > 0][p95Duration <= 1000]',
|
|
2938
|
+
style: { 'border-color': '#22c55e', 'border-width': 3 },
|
|
2939
|
+
},
|
|
2940
|
+
{
|
|
2941
|
+
selector: 'node[p95Duration > 1000][p95Duration <= 10000]',
|
|
2942
|
+
style: { 'border-color': '#eab308', 'border-width': 3 },
|
|
2943
|
+
},
|
|
2944
|
+
{
|
|
2945
|
+
selector: 'node[p95Duration > 10000][p95Duration <= 60000]',
|
|
2946
|
+
style: { 'border-color': '#f97316', 'border-width': 4 },
|
|
2947
|
+
},
|
|
2948
|
+
{
|
|
2949
|
+
selector: 'node[p95Duration > 60000]',
|
|
2950
|
+
style: { 'border-color': '#ef4444', 'border-width': 4 },
|
|
2951
|
+
},
|
|
2952
|
+
// Selected
|
|
2953
|
+
{
|
|
2954
|
+
selector: ':selected',
|
|
2955
|
+
style: { 'border-width': 4, 'border-color': '#f59e0b', 'overlay-opacity': 0.08 },
|
|
2956
|
+
},
|
|
2957
|
+
// Edges
|
|
2958
|
+
{
|
|
2959
|
+
selector: 'edge',
|
|
2960
|
+
style: {
|
|
2961
|
+
width: 'data(width)',
|
|
2962
|
+
opacity: 'data(opacity)',
|
|
2963
|
+
'line-color': '#6b7280',
|
|
2964
|
+
'target-arrow-color': '#6b7280',
|
|
2965
|
+
'target-arrow-shape': 'triangle',
|
|
2966
|
+
'curve-style': 'bezier',
|
|
2967
|
+
'arrow-scale': 0.7,
|
|
2968
|
+
label: 'data(label)',
|
|
2969
|
+
'font-size': '8px',
|
|
2970
|
+
color: '#8b949e',
|
|
2971
|
+
'text-outline-color': '#0d1117',
|
|
2972
|
+
'text-outline-width': 1.5,
|
|
2973
|
+
'text-rotation': 'autorotate',
|
|
2974
|
+
},
|
|
2975
|
+
},
|
|
2976
|
+
],
|
|
2977
|
+
layout: {
|
|
2978
|
+
name: 'breadthfirst',
|
|
2979
|
+
directed: true,
|
|
2980
|
+
padding: 50,
|
|
2981
|
+
spacingFactor: 1.6,
|
|
2982
|
+
animate: true,
|
|
2983
|
+
animationDuration: 400,
|
|
2984
|
+
roots:
|
|
2985
|
+
elements.filter((e) => e.data && e.data.id === '[START]').length > 0
|
|
2986
|
+
? ['[START]']
|
|
2987
|
+
: undefined,
|
|
2988
|
+
},
|
|
2989
|
+
minZoom: 0.15,
|
|
2990
|
+
maxZoom: 4,
|
|
2991
|
+
wheelSensitivity: 0.3,
|
|
2992
|
+
});
|
|
2993
|
+
|
|
2994
|
+
// Click node → show detail
|
|
2995
|
+
this._cyProcessMap.on('tap', 'node', (e) => {
|
|
2996
|
+
var d = e.target.data().fullData;
|
|
2997
|
+
if (!d || d.isVirtual) return;
|
|
2998
|
+
var panel = document.getElementById('processMapDetailPanel');
|
|
2999
|
+
var title = document.getElementById('processMapDetailTitle');
|
|
3000
|
+
var body = document.getElementById('processMapDetailBody');
|
|
3001
|
+
|
|
3002
|
+
title.textContent = d.label;
|
|
3003
|
+
var html = '';
|
|
3004
|
+
html += this.detailRow('Occurrences', d.count);
|
|
3005
|
+
html += this.detailRow('Frequency', `${(d.frequency * 100).toFixed(1)}% of traces`);
|
|
3006
|
+
if (d.avgDuration > 0)
|
|
3007
|
+
html += this.detailRow('Avg Duration', this.computeDuration(0, d.avgDuration));
|
|
3008
|
+
if (d.p95Duration > 0)
|
|
3009
|
+
html += this.detailRow('p95 Duration', this.computeDuration(0, d.p95Duration));
|
|
3010
|
+
html += this.detailRow('Failure Rate', `${(d.failRate * 100).toFixed(1)}%`);
|
|
3011
|
+
body.innerHTML = html;
|
|
3012
|
+
panel.classList.add('active');
|
|
3013
|
+
});
|
|
3014
|
+
|
|
3015
|
+
this._cyProcessMap.on('tap', (e) => {
|
|
3016
|
+
if (e.target === this._cyProcessMap) {
|
|
3017
|
+
document.getElementById('processMapDetailPanel').classList.remove('active');
|
|
3018
|
+
}
|
|
3019
|
+
});
|
|
3020
|
+
|
|
3021
|
+
// Close button
|
|
3022
|
+
var closeBtn = document.getElementById('processMapDetailClose');
|
|
3023
|
+
if (closeBtn) {
|
|
3024
|
+
closeBtn.onclick = () => {
|
|
3025
|
+
document.getElementById('processMapDetailPanel').classList.remove('active');
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
_loadVariantPanel(agentId) {
|
|
3031
|
+
var panel = document.getElementById('variantPanel');
|
|
3032
|
+
if (!panel) {
|
|
3033
|
+
// Create variant panel below the process map container
|
|
3034
|
+
var container = document.getElementById('cyProcessMap');
|
|
3035
|
+
if (!container) return;
|
|
3036
|
+
panel = document.createElement('div');
|
|
3037
|
+
panel.id = 'variantPanel';
|
|
3038
|
+
panel.style.cssText = 'margin-top:12px;padding:12px;background:#161b22;border:1px solid #30363d;border-radius:6px;max-height:200px;overflow-y:auto;display:none;';
|
|
3039
|
+
container.parentNode.insertBefore(panel, container.nextSibling);
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
fetch(`/api/agents/${encodeURIComponent(agentId)}/variants`)
|
|
3043
|
+
.then(function(r) { return r.json(); })
|
|
3044
|
+
.then(function(data) {
|
|
3045
|
+
if (!data.variants || data.variants.length === 0) {
|
|
3046
|
+
panel.innerHTML = '<div style="color:#8b949e;font-size:12px;">No variant data available</div>';
|
|
3047
|
+
panel.style.display = 'block';
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
var html = '<div style="font-size:11px;font-weight:600;color:#c9d1d9;margin-bottom:8px;">Top Variants (' + data.totalTraces + ' traces)</div>';
|
|
3051
|
+
var variants = data.variants.slice(0, 5);
|
|
3052
|
+
for (var i = 0; i < variants.length; i++) {
|
|
3053
|
+
var v = variants[i];
|
|
3054
|
+
var sig = v.pathSignature.length > 60 ? v.pathSignature.slice(0, 57) + '...' : v.pathSignature;
|
|
3055
|
+
html += '<div style="margin-bottom:4px;font-size:11px;">' +
|
|
3056
|
+
'<span style="color:#58a6ff;font-weight:600;">' + v.percentage.toFixed(1) + '%</span>' +
|
|
3057
|
+
' <span style="color:#8b949e;">(n=' + v.count + ')</span> ' +
|
|
3058
|
+
'<code style="color:#c9d1d9;font-size:10px;">' + escapeHtml(sig) + '</code></div>';
|
|
3059
|
+
}
|
|
3060
|
+
panel.innerHTML = html;
|
|
3061
|
+
panel.style.display = 'block';
|
|
3062
|
+
})
|
|
3063
|
+
.catch(function() {
|
|
3064
|
+
panel.style.display = 'none';
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
_loadProfileCard(agentId) {
|
|
3069
|
+
var card = document.getElementById('agentProfileCard');
|
|
3070
|
+
if (!card) {
|
|
3071
|
+
var container = document.getElementById('cyProcessMap');
|
|
3072
|
+
if (!container) return;
|
|
3073
|
+
card = document.createElement('div');
|
|
3074
|
+
card.id = 'agentProfileCard';
|
|
3075
|
+
card.style.cssText = 'margin-bottom:12px;padding:10px 14px;background:#161b22;border:1px solid #30363d;border-radius:6px;display:none;font-size:12px;';
|
|
3076
|
+
container.parentNode.insertBefore(card, container);
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
fetch(`/api/agents/${encodeURIComponent(agentId)}/profile`)
|
|
3080
|
+
.then(function(r) {
|
|
3081
|
+
if (r.status === 404) return null;
|
|
3082
|
+
return r.json();
|
|
3083
|
+
})
|
|
3084
|
+
.then(function(profile) {
|
|
3085
|
+
if (!profile) {
|
|
3086
|
+
card.style.display = 'none';
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
var html = '<div style="display:flex;gap:20px;flex-wrap:wrap;align-items:center;">';
|
|
3090
|
+
html += '<span style="font-weight:600;color:#c9d1d9;">' + escapeHtml(profile.agentId) + '</span>';
|
|
3091
|
+
html += '<span style="color:#8b949e;">Runs: <span style="color:#c9d1d9;">' + profile.totalRuns + '</span></span>';
|
|
3092
|
+
html += '<span style="color:#8b949e;">Success: <span style="color:#3fb950;">' + profile.successCount + '</span></span>';
|
|
3093
|
+
html += '<span style="color:#8b949e;">Failed: <span style="color:' + (profile.failureCount > 0 ? '#f85149' : '#8b949e') + ';">' + profile.failureCount + '</span></span>';
|
|
3094
|
+
html += '<span style="color:#8b949e;">Failure Rate: <span style="color:' + (profile.failureRate > 0.3 ? '#f85149' : '#c9d1d9') + ';">' + (profile.failureRate * 100).toFixed(1) + '%</span></span>';
|
|
3095
|
+
if (profile.knownBottlenecks && profile.knownBottlenecks.length > 0) {
|
|
3096
|
+
html += '<span style="color:#8b949e;">Bottlenecks: <span style="color:#f97316;">' + profile.knownBottlenecks.slice(0, 3).join(', ') + '</span></span>';
|
|
3097
|
+
}
|
|
3098
|
+
html += '</div>';
|
|
3099
|
+
card.innerHTML = html;
|
|
3100
|
+
card.style.display = 'block';
|
|
3101
|
+
})
|
|
3102
|
+
.catch(function() {
|
|
3103
|
+
card.style.display = 'none';
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
// Initialize
|
|
3109
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
3110
|
+
window.dashboard = new AgentFlowDashboard();
|
|
3111
|
+
});
|
|
3112
|
+
|
|
3113
|
+
window.AgentFlowDashboard = AgentFlowDashboard;
|