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