agentflow-dashboard 0.4.1 → 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-YDFLDRWO.js → chunk-N6IN5SHX.js} +564 -260
- package/dist/cli.cjs +561 -266
- package/dist/cli.js +1 -1
- package/dist/index.cjs +561 -266
- package/dist/index.js +1 -1
- package/dist/public/dashboard.js +1418 -470
- package/dist/public/debug.html +2 -2
- package/dist/public/index.html +12 -0
- package/dist/server.cjs +561 -266
- package/dist/server.js +1 -1
- package/package.json +3 -3
- package/public/dashboard.js +1418 -470
- package/public/debug.html +2 -2
- package/public/index.html +12 -0
package/dist/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,21 +300,30 @@ 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;
|
|
308
307
|
this.selectedTraceData = trace;
|
|
309
308
|
|
|
310
|
-
// Reset
|
|
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
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (this._agentTimelineAgent !== trace.agentId) {
|
|
318
|
+
this._agentTimelineAgent = null;
|
|
319
|
+
this._agentTimelineRendered = false;
|
|
314
320
|
}
|
|
315
321
|
|
|
316
322
|
// Update sidebar selection
|
|
317
|
-
document.querySelectorAll('.session-item').forEach(
|
|
318
|
-
|
|
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)}"]`);
|
|
319
327
|
if (activeEl) {
|
|
320
328
|
activeEl.classList.add('active');
|
|
321
329
|
// Scroll into view if needed
|
|
@@ -336,11 +344,13 @@ class AgentFlowDashboard {
|
|
|
336
344
|
if (!this.stats) return;
|
|
337
345
|
var s = this.stats;
|
|
338
346
|
document.getElementById('statAgents').textContent = s.totalAgents || 0;
|
|
339
|
-
document.getElementById('statExecutions').textContent = (
|
|
347
|
+
document.getElementById('statExecutions').textContent = (
|
|
348
|
+
s.totalExecutions || 0
|
|
349
|
+
).toLocaleString();
|
|
340
350
|
var rate = Math.round((s.globalSuccessRate || 0) * 10) / 10;
|
|
341
351
|
var rateEl = document.getElementById('statSuccessRate');
|
|
342
|
-
rateEl.textContent = rate
|
|
343
|
-
rateEl.className =
|
|
352
|
+
rateEl.textContent = `${rate}%`;
|
|
353
|
+
rateEl.className = `metric-value ${rate >= 90 ? 'success' : rate >= 70 ? 'warning' : 'error'}`;
|
|
344
354
|
document.getElementById('statActive').textContent = s.activeAgents || 0;
|
|
345
355
|
}
|
|
346
356
|
|
|
@@ -355,8 +365,13 @@ class AgentFlowDashboard {
|
|
|
355
365
|
}
|
|
356
366
|
|
|
357
367
|
var r = this.processHealth;
|
|
358
|
-
var hasContent =
|
|
359
|
-
|
|
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);
|
|
360
375
|
if (!hasContent) {
|
|
361
376
|
section.style.display = 'none';
|
|
362
377
|
return;
|
|
@@ -371,8 +386,8 @@ class AgentFlowDashboard {
|
|
|
371
386
|
var cls = pf.alive && pf.matchesProcess ? 'ok' : pf.stale ? 'bad' : 'warn';
|
|
372
387
|
html += '<div class="ph-row">';
|
|
373
388
|
html += '<span class="ph-label">PID File</span>';
|
|
374
|
-
html +=
|
|
375
|
-
html += pf.pid ?
|
|
389
|
+
html += `<span class="ph-value ${cls}">`;
|
|
390
|
+
html += pf.pid ? `PID ${pf.pid}${pf.alive ? ' (alive)' : ' (dead)'}` : 'No PID';
|
|
376
391
|
html += '</span></div>';
|
|
377
392
|
}
|
|
378
393
|
|
|
@@ -382,25 +397,31 @@ class AgentFlowDashboard {
|
|
|
382
397
|
var sdCls = sd.activeState === 'active' ? 'ok' : sd.failed ? 'bad' : 'warn';
|
|
383
398
|
html += '<div class="ph-row">';
|
|
384
399
|
html += '<span class="ph-label">Systemd</span>';
|
|
385
|
-
html +=
|
|
386
|
-
html +=
|
|
387
|
-
|
|
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]`;
|
|
388
409
|
html += '</span></div>';
|
|
389
410
|
}
|
|
390
411
|
|
|
391
412
|
// Workers detailed view
|
|
392
|
-
if (r.workers
|
|
413
|
+
if (r.workers?.workers) {
|
|
393
414
|
html += '<div class="ph-section">';
|
|
394
415
|
html += '<span class="ph-label">Workers</span>';
|
|
395
416
|
html += '<div class="process-grid">';
|
|
396
417
|
for (var i = 0; i < r.workers.workers.length; i++) {
|
|
397
418
|
var worker = r.workers.workers[i];
|
|
398
419
|
var statusCls = worker.alive ? 'ok' : worker.stale ? 'bad' : 'warn';
|
|
399
|
-
html +=
|
|
400
|
-
html +=
|
|
420
|
+
html += `<div class="worker-card ${statusCls}">`;
|
|
421
|
+
html += `<div class="worker-name">${escapeHtml(worker.name)}</div>`;
|
|
401
422
|
html += '<div class="worker-details">';
|
|
402
|
-
html +=
|
|
403
|
-
html +=
|
|
423
|
+
html += `<span>PID: ${worker.pid || '-'}</span>`;
|
|
424
|
+
html += `<span>${escapeHtml(worker.declaredStatus)}</span>`;
|
|
404
425
|
html += '</div></div>';
|
|
405
426
|
}
|
|
406
427
|
html += '</div></div>';
|
|
@@ -411,7 +432,7 @@ class AgentFlowDashboard {
|
|
|
411
432
|
|
|
412
433
|
if (categorized.agents.length > 0) {
|
|
413
434
|
html += '<div class="ph-section">';
|
|
414
|
-
html +=
|
|
435
|
+
html += `<span class="ph-label">Agent Services (${categorized.agents.length})</span>`;
|
|
415
436
|
|
|
416
437
|
// Build process tree for agents
|
|
417
438
|
var agentTree = this.buildProcessTree(categorized.agents);
|
|
@@ -422,7 +443,7 @@ class AgentFlowDashboard {
|
|
|
422
443
|
// Infrastructure processes
|
|
423
444
|
if (categorized.infrastructure.length > 0) {
|
|
424
445
|
html += '<div class="ph-section">';
|
|
425
|
-
html +=
|
|
446
|
+
html += `<span class="ph-label">Infrastructure (${categorized.infrastructure.length})</span>`;
|
|
426
447
|
|
|
427
448
|
// Build process tree for infrastructure
|
|
428
449
|
var infraTree = this.buildProcessTree(categorized.infrastructure);
|
|
@@ -434,15 +455,25 @@ class AgentFlowDashboard {
|
|
|
434
455
|
var uncategorized = this.getUncategorizedOrphans(r.orphans || [], categorized);
|
|
435
456
|
if (uncategorized.length > 0) {
|
|
436
457
|
html += '<div class="ph-section">';
|
|
437
|
-
html +=
|
|
458
|
+
html += `<span class="ph-label">Orphans (${uncategorized.length})</span>`;
|
|
438
459
|
html += '<div class="orphan-list">';
|
|
439
460
|
for (var j = 0; j < uncategorized.length; j++) {
|
|
440
461
|
var o = uncategorized[j];
|
|
441
462
|
html += '<div class="orphan-row">';
|
|
442
|
-
html +=
|
|
443
|
-
html +=
|
|
444
|
-
|
|
445
|
-
|
|
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>';
|
|
446
477
|
html += '</div>';
|
|
447
478
|
}
|
|
448
479
|
html += '</div></div>';
|
|
@@ -454,7 +485,7 @@ class AgentFlowDashboard {
|
|
|
454
485
|
html += '<span class="ph-label problems">Issues</span>';
|
|
455
486
|
html += '<div class="problems-list">';
|
|
456
487
|
for (var k = 0; k < r.problems.length; k++) {
|
|
457
|
-
html +=
|
|
488
|
+
html += `<div class="problem-item">⚠️ ${escapeHtml(r.problems[k])}</div>`;
|
|
458
489
|
}
|
|
459
490
|
html += '</div></div>';
|
|
460
491
|
}
|
|
@@ -486,11 +517,10 @@ class AgentFlowDashboard {
|
|
|
486
517
|
elapsed: proc.elapsed,
|
|
487
518
|
ppid: proc.ppid,
|
|
488
519
|
cmdline: proc.cmdline || proc.command,
|
|
489
|
-
tag: activityTag
|
|
520
|
+
tag: activityTag,
|
|
490
521
|
});
|
|
491
522
|
console.log('Detected infrastructure:', proc.pid, component, 'tag:', activityTag);
|
|
492
|
-
}
|
|
493
|
-
else if (service) {
|
|
523
|
+
} else if (service) {
|
|
494
524
|
agents.push({
|
|
495
525
|
service: service,
|
|
496
526
|
pid: proc.pid,
|
|
@@ -499,13 +529,16 @@ class AgentFlowDashboard {
|
|
|
499
529
|
elapsed: proc.elapsed,
|
|
500
530
|
ppid: proc.ppid,
|
|
501
531
|
cmdline: proc.cmdline || proc.command,
|
|
502
|
-
tag: activityTag
|
|
532
|
+
tag: activityTag,
|
|
503
533
|
});
|
|
504
534
|
console.log('Detected agent:', proc.pid, service, 'tag:', activityTag);
|
|
505
535
|
}
|
|
506
536
|
}
|
|
507
537
|
|
|
508
|
-
console.log('Categorization result:', {
|
|
538
|
+
console.log('Categorization result:', {
|
|
539
|
+
agents: agents.length,
|
|
540
|
+
infrastructure: infrastructure.length,
|
|
541
|
+
});
|
|
509
542
|
return { agents: agents, infrastructure: infrastructure };
|
|
510
543
|
}
|
|
511
544
|
|
|
@@ -527,7 +560,8 @@ class AgentFlowDashboard {
|
|
|
527
560
|
if (cmdline.includes('alfred') && cmdline.includes('janitor')) return 'Alfred Janitor';
|
|
528
561
|
if (cmdline.includes('alfred') && cmdline.includes('distiller')) return 'Alfred Distiller';
|
|
529
562
|
if (cmdline.includes('alfred') && cmdline.includes('surveyor')) return 'Alfred Surveyor';
|
|
530
|
-
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';
|
|
531
565
|
if (cmdline.includes('.alfred')) return 'Alfred Process';
|
|
532
566
|
|
|
533
567
|
// AI/ML agent frameworks
|
|
@@ -537,8 +571,10 @@ class AgentFlowDashboard {
|
|
|
537
571
|
if (cmdline.includes('mastra')) return 'Mastra Agent';
|
|
538
572
|
|
|
539
573
|
// Node.js/Python AI processes
|
|
540
|
-
if (
|
|
541
|
-
|
|
574
|
+
if (
|
|
575
|
+
(cmd.includes('node') || cmd.includes('python')) &&
|
|
576
|
+
(cmdline.includes('agent') || cmdline.includes('ai') || cmdline.includes('llm'))
|
|
577
|
+
) {
|
|
542
578
|
return 'AI Agent Process';
|
|
543
579
|
}
|
|
544
580
|
|
|
@@ -548,8 +584,10 @@ class AgentFlowDashboard {
|
|
|
548
584
|
}
|
|
549
585
|
|
|
550
586
|
// Generic agent indicators
|
|
551
|
-
if (
|
|
552
|
-
|
|
587
|
+
if (
|
|
588
|
+
cmdline.includes('agent') &&
|
|
589
|
+
(cmdline.includes('server') || cmdline.includes('worker') || cmdline.includes('daemon'))
|
|
590
|
+
) {
|
|
553
591
|
return 'Agent Service';
|
|
554
592
|
}
|
|
555
593
|
|
|
@@ -593,8 +631,10 @@ class AgentFlowDashboard {
|
|
|
593
631
|
|
|
594
632
|
// Get orphans that weren't categorized
|
|
595
633
|
getUncategorizedOrphans(orphans, categorized) {
|
|
596
|
-
var allCategorizedPids = categorized.agents
|
|
597
|
-
|
|
634
|
+
var allCategorizedPids = categorized.agents
|
|
635
|
+
.concat(categorized.infrastructure)
|
|
636
|
+
.map((p) => p.pid);
|
|
637
|
+
return orphans.filter((o) => allCategorizedPids.indexOf(o.pid) === -1);
|
|
598
638
|
}
|
|
599
639
|
|
|
600
640
|
// Build hierarchical process tree from flat process list
|
|
@@ -607,7 +647,7 @@ class AgentFlowDashboard {
|
|
|
607
647
|
var proc = processes[i];
|
|
608
648
|
processMap[proc.pid] = {
|
|
609
649
|
process: proc,
|
|
610
|
-
children: []
|
|
650
|
+
children: [],
|
|
611
651
|
};
|
|
612
652
|
}
|
|
613
653
|
|
|
@@ -641,33 +681,47 @@ class AgentFlowDashboard {
|
|
|
641
681
|
// Render individual process node recursively
|
|
642
682
|
renderProcessNode(node, type, depth) {
|
|
643
683
|
var proc = node.process;
|
|
644
|
-
var indent =
|
|
684
|
+
var indent = `padding-left: ${depth * 20}px;`;
|
|
645
685
|
var cpuNum = parseFloat(proc.cpu) || 0;
|
|
646
|
-
var cpuCls =
|
|
647
|
-
|
|
648
|
-
|
|
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';
|
|
649
698
|
|
|
650
699
|
var serviceName = type === 'agent' ? proc.service : proc.component;
|
|
651
700
|
|
|
652
|
-
var html =
|
|
701
|
+
var html = `<div class="process-node ${type}-node ${cpuCls}" style="${indent}">`;
|
|
653
702
|
|
|
654
703
|
// Process icon and name
|
|
655
704
|
if (depth > 0) {
|
|
656
705
|
html += '<span class="tree-connector">└─ </span>';
|
|
657
706
|
}
|
|
658
707
|
html += '<div class="process-main">';
|
|
659
|
-
html +=
|
|
708
|
+
html +=
|
|
709
|
+
'<div class="process-name" title="' +
|
|
710
|
+
escapeHtml(proc.cmdline) +
|
|
711
|
+
'">' +
|
|
712
|
+
escapeHtml(serviceName) +
|
|
713
|
+
'</div>';
|
|
660
714
|
|
|
661
715
|
// Add activity tag
|
|
662
716
|
if (proc.tag && proc.tag !== 'other') {
|
|
663
|
-
html +=
|
|
717
|
+
html += `<span class="activity-tag tag-${proc.tag}">${proc.tag}</span>`;
|
|
664
718
|
}
|
|
665
719
|
|
|
666
720
|
html += '<div class="process-metrics">';
|
|
667
|
-
html +=
|
|
668
|
-
html +=
|
|
669
|
-
html +=
|
|
670
|
-
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>`;
|
|
671
725
|
html += '</div>';
|
|
672
726
|
html += '</div>';
|
|
673
727
|
html += '</div>';
|
|
@@ -686,18 +740,18 @@ class AgentFlowDashboard {
|
|
|
686
740
|
renderTraceList() {
|
|
687
741
|
var container = document.getElementById('traceList');
|
|
688
742
|
var countEl = document.getElementById('traceCount');
|
|
689
|
-
var self = this;
|
|
690
743
|
|
|
691
744
|
var filtered = this.traces;
|
|
692
745
|
|
|
693
746
|
// Search filter
|
|
694
747
|
if (this.searchFilter) {
|
|
695
748
|
var sf = this.searchFilter;
|
|
696
|
-
filtered = filtered.filter(
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
+
);
|
|
701
755
|
}
|
|
702
756
|
|
|
703
757
|
// Time range filter
|
|
@@ -705,13 +759,20 @@ class AgentFlowDashboard {
|
|
|
705
759
|
var now = Date.now();
|
|
706
760
|
var cutoff;
|
|
707
761
|
switch (this.timeRangeFilter) {
|
|
708
|
-
case '1h':
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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;
|
|
712
773
|
}
|
|
713
|
-
filtered = filtered.filter(
|
|
714
|
-
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;
|
|
715
776
|
return ts >= cutoff;
|
|
716
777
|
});
|
|
717
778
|
}
|
|
@@ -719,26 +780,23 @@ class AgentFlowDashboard {
|
|
|
719
780
|
// Status filter
|
|
720
781
|
if (this.statusFilter !== 'all') {
|
|
721
782
|
var statusTarget = this.statusFilter;
|
|
722
|
-
filtered = filtered.filter(
|
|
723
|
-
return self.getTraceStatus(t) === statusTarget;
|
|
724
|
-
});
|
|
783
|
+
filtered = filtered.filter((t) => this.getTraceStatus(t) === statusTarget);
|
|
725
784
|
}
|
|
726
785
|
|
|
727
786
|
// Activity filter
|
|
728
787
|
if (this.activityFilter && this.activityFilter !== 'all') {
|
|
729
788
|
var activityTarget = this.activityFilter;
|
|
730
|
-
filtered = filtered.filter(
|
|
731
|
-
return self.getTraceActivity(t) === activityTarget;
|
|
732
|
-
});
|
|
789
|
+
filtered = filtered.filter((t) => this.getTraceActivity(t) === activityTarget);
|
|
733
790
|
}
|
|
734
791
|
|
|
735
|
-
countEl.textContent = filtered.length
|
|
792
|
+
countEl.textContent = `${filtered.length} of ${this.traces.length} traces`;
|
|
736
793
|
|
|
737
794
|
// Render max 100 items for performance
|
|
738
795
|
var visible = filtered.slice(0, 100);
|
|
739
796
|
|
|
740
797
|
if (visible.length === 0) {
|
|
741
|
-
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>';
|
|
742
800
|
return;
|
|
743
801
|
}
|
|
744
802
|
|
|
@@ -749,13 +807,30 @@ class AgentFlowDashboard {
|
|
|
749
807
|
var isActive = this.selectedTrace && this.selectedTrace.filename === trace.filename;
|
|
750
808
|
var name = trace.name || trace.agentId || trace.filename;
|
|
751
809
|
var ts = this.formatTimestamp(trace.timestamp || trace.startTime || trace.lastModified);
|
|
752
|
-
var badgeClass =
|
|
753
|
-
|
|
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
|
+
: '?';
|
|
754
826
|
|
|
755
827
|
// Compute node stats for this trace
|
|
756
828
|
var traceNodes = this.getNodesArray(trace);
|
|
757
829
|
var nodeCount = traceNodes.length;
|
|
758
|
-
var agentCount = 0,
|
|
830
|
+
var agentCount = 0,
|
|
831
|
+
toolCount = 0,
|
|
832
|
+
subagentCount = 0,
|
|
833
|
+
otherCount = 0;
|
|
759
834
|
for (var j = 0; j < traceNodes.length; j++) {
|
|
760
835
|
var nt = traceNodes[j].type;
|
|
761
836
|
if (nt === 'agent') agentCount++;
|
|
@@ -763,28 +838,67 @@ class AgentFlowDashboard {
|
|
|
763
838
|
else if (nt === 'subagent') subagentCount++;
|
|
764
839
|
else otherCount++;
|
|
765
840
|
}
|
|
766
|
-
var traceDuration = this.computeDuration(
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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>';
|
|
771
866
|
html += '<div class="session-meta">';
|
|
772
|
-
html +=
|
|
773
|
-
html +=
|
|
774
|
-
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>`;
|
|
775
870
|
html += '</div>';
|
|
776
871
|
// Node type breakdown + duration
|
|
777
872
|
html += '<div class="session-meta" style="margin-top:3px;">';
|
|
778
|
-
html +=
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
if (
|
|
783
|
-
|
|
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>';
|
|
784
890
|
if (trace.tokenUsage && trace.tokenUsage.total > 0) {
|
|
785
|
-
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>';
|
|
786
897
|
if (trace.tokenUsage.cost > 0) {
|
|
787
|
-
html +=
|
|
898
|
+
html +=
|
|
899
|
+
'<span style="font-size:0.7rem;color:#f0883e;">$' +
|
|
900
|
+
trace.tokenUsage.cost.toFixed(4) +
|
|
901
|
+
'</span>';
|
|
788
902
|
}
|
|
789
903
|
}
|
|
790
904
|
html += '</div>';
|
|
@@ -798,11 +912,11 @@ class AgentFlowDashboard {
|
|
|
798
912
|
if (!trace.nodes) return 'unknown';
|
|
799
913
|
var nodes = this.getNodesArray(trace);
|
|
800
914
|
if (nodes.length === 0) return 'unknown';
|
|
801
|
-
var hasFailed = nodes.some(
|
|
915
|
+
var hasFailed = nodes.some((n) => n.status === 'failed' || n.metadata?.error);
|
|
802
916
|
if (hasFailed) return 'failure';
|
|
803
|
-
var hasRunning = nodes.some(
|
|
917
|
+
var hasRunning = nodes.some((n) => n.status === 'running');
|
|
804
918
|
if (hasRunning) return 'running';
|
|
805
|
-
var hasCompleted = nodes.some(
|
|
919
|
+
var hasCompleted = nodes.some((n) => n.status === 'completed' || n.endTime);
|
|
806
920
|
if (hasCompleted) return 'success';
|
|
807
921
|
return 'unknown';
|
|
808
922
|
}
|
|
@@ -810,7 +924,7 @@ class AgentFlowDashboard {
|
|
|
810
924
|
getNodesArray(trace) {
|
|
811
925
|
if (!trace.nodes) return [];
|
|
812
926
|
if (Array.isArray(trace.nodes)) {
|
|
813
|
-
return trace.nodes.map(
|
|
927
|
+
return trace.nodes.map((entry) => (Array.isArray(entry) ? entry[1] : entry));
|
|
814
928
|
}
|
|
815
929
|
if (trace.nodes instanceof Map) return Array.from(trace.nodes.values());
|
|
816
930
|
return Object.values(trace.nodes);
|
|
@@ -819,29 +933,33 @@ class AgentFlowDashboard {
|
|
|
819
933
|
formatTimestamp(ts) {
|
|
820
934
|
if (!ts) return '';
|
|
821
935
|
var d = new Date(ts);
|
|
822
|
-
if (isNaN(d.getTime())) return String(ts);
|
|
936
|
+
if (Number.isNaN(d.getTime())) return String(ts);
|
|
823
937
|
var now = new Date();
|
|
824
938
|
var diffMs = now - d;
|
|
825
939
|
if (diffMs < 60000) return 'just now';
|
|
826
|
-
if (diffMs < 3600000) return Math.floor(diffMs / 60000)
|
|
827
|
-
if (diffMs < 86400000) return Math.floor(diffMs / 3600000)
|
|
828
|
-
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
|
+
);
|
|
829
947
|
}
|
|
830
948
|
|
|
831
949
|
computeDuration(startTime, endTime) {
|
|
832
950
|
if (!startTime || !endTime) return '--';
|
|
833
951
|
var ms = new Date(endTime).getTime() - new Date(startTime).getTime();
|
|
834
|
-
if (isNaN(ms) || ms < 0) return '--';
|
|
835
|
-
if (ms < 1000) return ms
|
|
836
|
-
if (ms < 60000) return (ms / 1000).toFixed(1)
|
|
837
|
-
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`;
|
|
838
956
|
}
|
|
839
957
|
|
|
840
958
|
formatDuration(ms) {
|
|
841
959
|
if (!ms || ms <= 0) return '--';
|
|
842
|
-
if (ms < 1000) return Math.round(ms)
|
|
843
|
-
if (ms < 60000) return (ms / 1000).toFixed(1)
|
|
844
|
-
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`;
|
|
845
963
|
}
|
|
846
964
|
|
|
847
965
|
// ---------------------------------------------------------------------------
|
|
@@ -849,14 +967,33 @@ class AgentFlowDashboard {
|
|
|
849
967
|
// ---------------------------------------------------------------------------
|
|
850
968
|
renderActiveTab() {
|
|
851
969
|
switch (this.activeTab) {
|
|
852
|
-
case 'timeline':
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
case '
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
case '
|
|
859
|
-
|
|
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;
|
|
860
997
|
}
|
|
861
998
|
this.updateToolbarInfo();
|
|
862
999
|
}
|
|
@@ -869,7 +1006,7 @@ class AgentFlowDashboard {
|
|
|
869
1006
|
return;
|
|
870
1007
|
}
|
|
871
1008
|
var nodes = this.getNodesArray(trace);
|
|
872
|
-
info.textContent = nodes.length
|
|
1009
|
+
info.textContent = `${nodes.length} nodes${trace.agentId ? ` | ${trace.agentId}` : ''}`;
|
|
873
1010
|
}
|
|
874
1011
|
|
|
875
1012
|
// ---------------------------------------------------------------------------
|
|
@@ -879,7 +1016,8 @@ class AgentFlowDashboard {
|
|
|
879
1016
|
var container = document.getElementById('timelineContent');
|
|
880
1017
|
var trace = this.selectedTraceData || this.selectedTrace;
|
|
881
1018
|
if (!trace || !trace.nodes) {
|
|
882
|
-
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>';
|
|
883
1021
|
return;
|
|
884
1022
|
}
|
|
885
1023
|
|
|
@@ -891,7 +1029,8 @@ class AgentFlowDashboard {
|
|
|
891
1029
|
|
|
892
1030
|
var nodes = this.getNodesArray(trace);
|
|
893
1031
|
if (nodes.length === 0) {
|
|
894
|
-
container.innerHTML =
|
|
1032
|
+
container.innerHTML =
|
|
1033
|
+
'<div class="empty-state"><div class="empty-state-text">No nodes in this trace.</div></div>';
|
|
895
1034
|
return;
|
|
896
1035
|
}
|
|
897
1036
|
|
|
@@ -901,11 +1040,14 @@ class AgentFlowDashboard {
|
|
|
901
1040
|
if (nodes[j].id) nodeMap[nodes[j].id] = nodes[j];
|
|
902
1041
|
}
|
|
903
1042
|
var depthCache = {};
|
|
904
|
-
var getDepth =
|
|
905
|
-
if (!nid ||
|
|
1043
|
+
var getDepth = (nid, visited) => {
|
|
1044
|
+
if (!nid || visited?.has(nid)) return 0;
|
|
906
1045
|
if (depthCache[nid] !== undefined) return depthCache[nid];
|
|
907
1046
|
var nd = nodeMap[nid];
|
|
908
|
-
if (!nd || !nd.parentId) {
|
|
1047
|
+
if (!nd || !nd.parentId) {
|
|
1048
|
+
depthCache[nid] = 0;
|
|
1049
|
+
return 0;
|
|
1050
|
+
}
|
|
909
1051
|
var vis = visited || new Set();
|
|
910
1052
|
vis.add(nid);
|
|
911
1053
|
depthCache[nid] = 1 + getDepth(nd.parentId, vis);
|
|
@@ -914,14 +1056,18 @@ class AgentFlowDashboard {
|
|
|
914
1056
|
for (var k = 0; k < nodes.length; k++) getDepth(nodes[k].id);
|
|
915
1057
|
|
|
916
1058
|
// Compute timeline range for duration bars
|
|
917
|
-
var allStarts = nodes
|
|
918
|
-
|
|
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);
|
|
919
1065
|
var timelineStart = allStarts.length > 0 ? Math.min.apply(null, allStarts) : 0;
|
|
920
1066
|
var timelineEnd = allEnds.length > 0 ? Math.max.apply(null, allEnds) : 0;
|
|
921
1067
|
var timelineSpan = timelineEnd - timelineStart || 1;
|
|
922
1068
|
|
|
923
1069
|
// Sort by startTime then depth
|
|
924
|
-
var sorted = nodes.slice().sort(
|
|
1070
|
+
var sorted = nodes.slice().sort((a, b) => {
|
|
925
1071
|
var sa = a.startTime ? new Date(a.startTime).getTime() : Infinity;
|
|
926
1072
|
var sb = b.startTime ? new Date(b.startTime).getTime() : Infinity;
|
|
927
1073
|
if (sa !== sb) return sa - sb;
|
|
@@ -929,45 +1075,90 @@ class AgentFlowDashboard {
|
|
|
929
1075
|
});
|
|
930
1076
|
|
|
931
1077
|
// Type icons
|
|
932
|
-
var typeIcons = {
|
|
933
|
-
|
|
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
|
+
};
|
|
934
1094
|
|
|
935
1095
|
var html = '';
|
|
936
1096
|
// Summary header
|
|
937
1097
|
html += '<div style="display:flex;gap:12px;margin-bottom:12px;flex-wrap:wrap;">';
|
|
938
1098
|
var typeCounts = {};
|
|
939
|
-
for (var m = 0; m < sorted.length; m++) {
|
|
940
|
-
|
|
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>';
|
|
941
1107
|
var typeEntries = Object.entries(typeCounts);
|
|
942
1108
|
for (var p = 0; p < typeEntries.length; p++) {
|
|
943
1109
|
var tIcon = typeIcons[typeEntries[p][0]] || '\u25cf';
|
|
944
|
-
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>';
|
|
945
1120
|
}
|
|
946
1121
|
if (timelineSpan > 1) {
|
|
947
|
-
html +=
|
|
1122
|
+
html +=
|
|
1123
|
+
'<span style="font-size:0.85rem;color:var(--text-secondary);">Total: ' +
|
|
1124
|
+
this.formatDuration(timelineSpan) +
|
|
1125
|
+
'</span>';
|
|
948
1126
|
}
|
|
949
1127
|
html += '</div>';
|
|
950
1128
|
|
|
951
1129
|
for (var i = 0; i < sorted.length; i++) {
|
|
952
1130
|
var n = sorted[i];
|
|
953
1131
|
var depth = depthCache[n.id] || 0;
|
|
954
|
-
var markerClass =
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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';
|
|
961
1148
|
|
|
962
1149
|
var typeIcon = typeIcons[n.type] || '\u25cf';
|
|
963
1150
|
var statusIcon = statusIcons[n.status] || '';
|
|
964
1151
|
var eventName = escapeHtml(n.name || n.id || 'unnamed');
|
|
965
1152
|
var eventTs = n.startTime ? new Date(n.startTime).toLocaleTimeString() : '--';
|
|
966
1153
|
var dur = this.computeDuration(n.startTime, n.endTime);
|
|
967
|
-
var durMs =
|
|
1154
|
+
var durMs =
|
|
1155
|
+
n.startTime && n.endTime
|
|
1156
|
+
? new Date(n.endTime).getTime() - new Date(n.startTime).getTime()
|
|
1157
|
+
: 0;
|
|
968
1158
|
|
|
969
1159
|
// Duration bar width proportional to timeline
|
|
970
|
-
var barLeft = 0,
|
|
1160
|
+
var barLeft = 0,
|
|
1161
|
+
barWidth = 0;
|
|
971
1162
|
if (n.startTime && timelineSpan > 1) {
|
|
972
1163
|
barLeft = ((new Date(n.startTime).getTime() - timelineStart) / timelineSpan) * 100;
|
|
973
1164
|
barWidth = Math.max(1, (durMs / timelineSpan) * 100);
|
|
@@ -975,37 +1166,67 @@ class AgentFlowDashboard {
|
|
|
975
1166
|
|
|
976
1167
|
var details = '';
|
|
977
1168
|
if (n.metadata) {
|
|
978
|
-
var showKeys = Object.keys(n.metadata).filter(
|
|
979
|
-
|
|
980
|
-
|
|
1169
|
+
var showKeys = Object.keys(n.metadata).filter(
|
|
1170
|
+
(k) => k !== 'error' && typeof n.metadata[k] !== 'object',
|
|
1171
|
+
);
|
|
981
1172
|
if (showKeys.length > 0) {
|
|
982
|
-
details = showKeys
|
|
983
|
-
|
|
984
|
-
|
|
1173
|
+
details = showKeys
|
|
1174
|
+
.slice(0, 4)
|
|
1175
|
+
.map((k) => `${escapeHtml(k)}: ${escapeHtml(String(n.metadata[k]).substring(0, 50))}`)
|
|
1176
|
+
.join(' \u00b7 ');
|
|
985
1177
|
}
|
|
986
1178
|
}
|
|
987
1179
|
|
|
988
1180
|
var indent = depth * 24;
|
|
989
|
-
html +=
|
|
990
|
-
html +=
|
|
1181
|
+
html += `<div class="timeline-item" style="margin-left:${indent}px;">`;
|
|
1182
|
+
html += `<div class="timeline-marker ${markerClass}"></div>`;
|
|
991
1183
|
html += '<div class="timeline-content">';
|
|
992
1184
|
html += '<div class="timeline-header">';
|
|
993
|
-
html +=
|
|
994
|
-
|
|
995
|
-
|
|
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>`;
|
|
996
1199
|
html += '</span></div>';
|
|
997
1200
|
// Duration bar
|
|
998
1201
|
if (barWidth > 0) {
|
|
999
|
-
var barColor =
|
|
1000
|
-
|
|
1001
|
-
|
|
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>';
|
|
1002
1220
|
html += '</div>';
|
|
1003
1221
|
}
|
|
1004
1222
|
if (details) {
|
|
1005
|
-
html +=
|
|
1223
|
+
html += `<div class="event-details">${details}</div>`;
|
|
1006
1224
|
}
|
|
1007
|
-
if (n.metadata
|
|
1008
|
-
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>';
|
|
1009
1230
|
}
|
|
1010
1231
|
html += '</div></div>';
|
|
1011
1232
|
}
|
|
@@ -1020,25 +1241,28 @@ class AgentFlowDashboard {
|
|
|
1020
1241
|
var container = document.getElementById('metricsContent');
|
|
1021
1242
|
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1022
1243
|
if (!trace || !trace.nodes) {
|
|
1023
|
-
container.innerHTML =
|
|
1244
|
+
container.innerHTML =
|
|
1245
|
+
'<div class="empty-state"><div class="empty-state-text">Select a trace to view metrics.</div></div>';
|
|
1024
1246
|
return;
|
|
1025
1247
|
}
|
|
1026
1248
|
|
|
1027
1249
|
var nodes = this.getNodesArray(trace);
|
|
1028
1250
|
var totalNodes = nodes.length;
|
|
1029
|
-
var completedNodes = nodes.filter(
|
|
1030
|
-
var failedNodes = nodes.filter(
|
|
1031
|
-
var runningNodes = nodes.filter(
|
|
1032
|
-
var
|
|
1033
|
-
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;
|
|
1034
1256
|
|
|
1035
1257
|
// Compute average and max duration
|
|
1036
|
-
var totalDur = 0,
|
|
1258
|
+
var totalDur = 0,
|
|
1259
|
+
durCount = 0,
|
|
1260
|
+
maxDur = 0;
|
|
1037
1261
|
for (var i = 0; i < nodes.length; i++) {
|
|
1038
1262
|
var n = nodes[i];
|
|
1039
1263
|
if (n.startTime && n.endTime) {
|
|
1040
1264
|
var ms = new Date(n.endTime).getTime() - new Date(n.startTime).getTime();
|
|
1041
|
-
if (!isNaN(ms) && ms >= 0) {
|
|
1265
|
+
if (!Number.isNaN(ms) && ms >= 0) {
|
|
1042
1266
|
totalDur += ms;
|
|
1043
1267
|
durCount++;
|
|
1044
1268
|
if (ms > maxDur) maxDur = ms;
|
|
@@ -1053,7 +1277,7 @@ class AgentFlowDashboard {
|
|
|
1053
1277
|
if (nodes[j].id) nodeMap[nodes[j].id] = nodes[j];
|
|
1054
1278
|
}
|
|
1055
1279
|
var maxDepth = 0;
|
|
1056
|
-
var depthOf =
|
|
1280
|
+
var depthOf = (nid, visited) => {
|
|
1057
1281
|
if (!nid || visited.has(nid)) return 0;
|
|
1058
1282
|
visited.add(nid);
|
|
1059
1283
|
var nd = nodeMap[nid];
|
|
@@ -1073,33 +1297,86 @@ class AgentFlowDashboard {
|
|
|
1073
1297
|
|
|
1074
1298
|
var html = '<div class="metrics-grid">';
|
|
1075
1299
|
html += this.metricCard('Total Nodes', totalNodes, 'primary');
|
|
1076
|
-
html += this.metricCard(
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
+
);
|
|
1079
1317
|
html += this.metricCard('Max Depth', maxDepth, 'primary');
|
|
1080
1318
|
html += this.metricCard('Failures', failedNodes, failedNodes > 0 ? 'error' : 'success');
|
|
1081
|
-
html += this.metricCard(
|
|
1319
|
+
html += this.metricCard(
|
|
1320
|
+
'Running/Active',
|
|
1321
|
+
runningNodes,
|
|
1322
|
+
runningNodes > 0 ? 'warning' : 'primary',
|
|
1323
|
+
);
|
|
1082
1324
|
html += this.metricCard('Completed', completedNodes, 'success');
|
|
1083
1325
|
html += '</div>';
|
|
1084
1326
|
|
|
1085
1327
|
// Token/cost metrics for session traces
|
|
1086
1328
|
if (trace.tokenUsage && trace.tokenUsage.total > 0) {
|
|
1087
|
-
html +=
|
|
1329
|
+
html +=
|
|
1330
|
+
'<h4 style="margin:1.5rem 0 0.75rem;font-size:0.85rem;color:var(--text-secondary);">Token Usage</h4>';
|
|
1088
1331
|
html += '<div class="metrics-grid">';
|
|
1089
|
-
html += this.metricCard(
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
+
);
|
|
1096
1372
|
html += '</div>';
|
|
1097
1373
|
}
|
|
1098
1374
|
|
|
1099
1375
|
// Type breakdown
|
|
1100
|
-
html +=
|
|
1376
|
+
html +=
|
|
1377
|
+
'<h4 style="margin:1.5rem 0 0.75rem;font-size:0.85rem;color:var(--text-secondary);">Node Type Breakdown</h4>';
|
|
1101
1378
|
html += '<div class="metrics-grid">';
|
|
1102
|
-
var typeEntries = Object.entries(typeCounts).sort(
|
|
1379
|
+
var typeEntries = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]);
|
|
1103
1380
|
for (var p = 0; p < typeEntries.length; p++) {
|
|
1104
1381
|
html += this.metricCard(typeEntries[p][0], typeEntries[p][1], 'primary');
|
|
1105
1382
|
}
|
|
@@ -1109,8 +1386,15 @@ class AgentFlowDashboard {
|
|
|
1109
1386
|
}
|
|
1110
1387
|
|
|
1111
1388
|
metricCard(label, value, colorClass, sub) {
|
|
1112
|
-
var html =
|
|
1113
|
-
|
|
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>`;
|
|
1114
1398
|
html += '</div>';
|
|
1115
1399
|
return html;
|
|
1116
1400
|
}
|
|
@@ -1122,7 +1406,10 @@ class AgentFlowDashboard {
|
|
|
1122
1406
|
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1123
1407
|
if (!trace || !trace.nodes) {
|
|
1124
1408
|
document.getElementById('graphEmpty').style.display = '';
|
|
1125
|
-
if (this.cy) {
|
|
1409
|
+
if (this.cy) {
|
|
1410
|
+
this.cy.destroy();
|
|
1411
|
+
this.cy = null;
|
|
1412
|
+
}
|
|
1126
1413
|
return;
|
|
1127
1414
|
}
|
|
1128
1415
|
|
|
@@ -1140,14 +1427,18 @@ class AgentFlowDashboard {
|
|
|
1140
1427
|
|
|
1141
1428
|
// Collect valid IDs
|
|
1142
1429
|
if (typeof trace.nodes === 'object' && !Array.isArray(trace.nodes)) {
|
|
1143
|
-
Object.keys(trace.nodes).forEach(
|
|
1430
|
+
Object.keys(trace.nodes).forEach((key) => {
|
|
1431
|
+
nodeIds.add(key);
|
|
1432
|
+
});
|
|
1144
1433
|
}
|
|
1145
|
-
nodes.forEach(
|
|
1434
|
+
nodes.forEach((n) => {
|
|
1435
|
+
if (n.id) nodeIds.add(n.id);
|
|
1436
|
+
});
|
|
1146
1437
|
|
|
1147
1438
|
// Add nodes
|
|
1148
1439
|
for (var i = 0; i < nodes.length; i++) {
|
|
1149
1440
|
var node = nodes[i];
|
|
1150
|
-
var id = node.id ||
|
|
1441
|
+
var id = node.id || `n-${i}`;
|
|
1151
1442
|
elements.push({
|
|
1152
1443
|
group: 'nodes',
|
|
1153
1444
|
data: {
|
|
@@ -1155,8 +1446,8 @@ class AgentFlowDashboard {
|
|
|
1155
1446
|
label: node.name || node.type || id,
|
|
1156
1447
|
status: node.status || 'unknown',
|
|
1157
1448
|
nodeType: node.type || 'custom',
|
|
1158
|
-
fullData: node
|
|
1159
|
-
}
|
|
1449
|
+
fullData: node,
|
|
1450
|
+
},
|
|
1160
1451
|
});
|
|
1161
1452
|
}
|
|
1162
1453
|
|
|
@@ -1169,8 +1460,8 @@ class AgentFlowDashboard {
|
|
|
1169
1460
|
data: {
|
|
1170
1461
|
source: n.parentId,
|
|
1171
1462
|
target: n.id,
|
|
1172
|
-
id:
|
|
1173
|
-
}
|
|
1463
|
+
id: `e-${n.parentId}-${n.id}`,
|
|
1464
|
+
},
|
|
1174
1465
|
});
|
|
1175
1466
|
}
|
|
1176
1467
|
}
|
|
@@ -1182,11 +1473,11 @@ class AgentFlowDashboard {
|
|
|
1182
1473
|
var src = edge.source || edge.from;
|
|
1183
1474
|
var tgt = edge.target || edge.to;
|
|
1184
1475
|
if (src && tgt && nodeIds.has(src) && nodeIds.has(tgt)) {
|
|
1185
|
-
var eid =
|
|
1186
|
-
if (!elements.some(
|
|
1476
|
+
var eid = `e-${src}-${tgt}`;
|
|
1477
|
+
if (!elements.some((el) => el.data && el.data.id === eid)) {
|
|
1187
1478
|
elements.push({
|
|
1188
1479
|
group: 'edges',
|
|
1189
|
-
data: { source: src, target: tgt, id: eid, edgeType: edge.type || '' }
|
|
1480
|
+
data: { source: src, target: tgt, id: eid, edgeType: edge.type || '' },
|
|
1190
1481
|
});
|
|
1191
1482
|
}
|
|
1192
1483
|
}
|
|
@@ -1194,7 +1485,10 @@ class AgentFlowDashboard {
|
|
|
1194
1485
|
}
|
|
1195
1486
|
|
|
1196
1487
|
// Destroy previous instance
|
|
1197
|
-
if (this.cy) {
|
|
1488
|
+
if (this.cy) {
|
|
1489
|
+
this.cy.destroy();
|
|
1490
|
+
this.cy = null;
|
|
1491
|
+
}
|
|
1198
1492
|
|
|
1199
1493
|
var cyContainer = document.getElementById('cy');
|
|
1200
1494
|
|
|
@@ -1205,46 +1499,76 @@ class AgentFlowDashboard {
|
|
|
1205
1499
|
{
|
|
1206
1500
|
selector: 'node',
|
|
1207
1501
|
style: {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1502
|
+
label: 'data(label)',
|
|
1503
|
+
width: 45,
|
|
1504
|
+
height: 45,
|
|
1211
1505
|
'font-size': '10px',
|
|
1212
1506
|
'text-valign': 'bottom',
|
|
1213
1507
|
'text-halign': 'center',
|
|
1214
1508
|
'text-margin-y': 6,
|
|
1215
|
-
|
|
1509
|
+
color: '#c9d1d9',
|
|
1216
1510
|
'text-outline-color': '#0d1117',
|
|
1217
1511
|
'text-outline-width': 2,
|
|
1218
1512
|
'border-width': 2,
|
|
1219
1513
|
'border-color': '#30363d',
|
|
1220
|
-
'background-color': '#3b82f6'
|
|
1221
|
-
}
|
|
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' },
|
|
1222
1536
|
},
|
|
1223
|
-
{ selector: 'node[status="completed"]', style: { 'background-color': '#10b981', 'border-color': '#2ea043' } },
|
|
1224
|
-
{ selector: 'node[status="failed"]', style: { 'background-color': '#ef4444', 'border-color': '#f85149', 'shape': 'diamond' } },
|
|
1225
|
-
{ selector: 'node[status="running"]', style: { 'background-color': '#3b82f6', 'border-color': '#79b8ff' } },
|
|
1226
|
-
{ selector: 'node[status="hung"]', style: { 'background-color': '#f0883e', 'border-color': '#f5a623' } },
|
|
1227
|
-
{ selector: 'node[status="timeout"]', style: { 'background-color': '#f0883e', 'border-color': '#f5a623' } },
|
|
1228
1537
|
// Shape by type
|
|
1229
|
-
{ selector: 'node[nodeType="agent"]', style: {
|
|
1230
|
-
{
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
{
|
|
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 } },
|
|
1235
1556
|
// Selected node — gold border
|
|
1236
|
-
{
|
|
1557
|
+
{
|
|
1558
|
+
selector: ':selected',
|
|
1559
|
+
style: { 'border-width': 4, 'border-color': '#f59e0b', 'overlay-opacity': 0.08 },
|
|
1560
|
+
},
|
|
1237
1561
|
// Edges
|
|
1238
1562
|
{
|
|
1239
1563
|
selector: 'edge',
|
|
1240
1564
|
style: {
|
|
1241
|
-
|
|
1565
|
+
width: 2,
|
|
1242
1566
|
'line-color': '#6b7280',
|
|
1243
1567
|
'target-arrow-color': '#6b7280',
|
|
1244
1568
|
'target-arrow-shape': 'triangle',
|
|
1245
1569
|
'curve-style': 'bezier',
|
|
1246
|
-
'arrow-scale': 0.8
|
|
1247
|
-
}
|
|
1570
|
+
'arrow-scale': 0.8,
|
|
1571
|
+
},
|
|
1248
1572
|
},
|
|
1249
1573
|
// Dashed edges for specific types
|
|
1250
1574
|
{
|
|
@@ -1252,27 +1576,32 @@ class AgentFlowDashboard {
|
|
|
1252
1576
|
style: {
|
|
1253
1577
|
'line-style': 'dashed',
|
|
1254
1578
|
'line-color': '#f0883e',
|
|
1255
|
-
'target-arrow-color': '#f0883e'
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1579
|
+
'target-arrow-color': '#f0883e',
|
|
1580
|
+
},
|
|
1581
|
+
},
|
|
1258
1582
|
],
|
|
1259
|
-
layout: {
|
|
1583
|
+
layout: {
|
|
1584
|
+
name: 'breadthfirst',
|
|
1585
|
+
directed: true,
|
|
1586
|
+
padding: 40,
|
|
1587
|
+
spacingFactor: 1.4,
|
|
1588
|
+
animate: true,
|
|
1589
|
+
animationDuration: 300,
|
|
1590
|
+
},
|
|
1260
1591
|
minZoom: 0.2,
|
|
1261
1592
|
maxZoom: 4,
|
|
1262
|
-
wheelSensitivity: 0.3
|
|
1593
|
+
wheelSensitivity: 0.3,
|
|
1263
1594
|
});
|
|
1264
1595
|
|
|
1265
|
-
var self = this;
|
|
1266
|
-
|
|
1267
1596
|
// Node tap -> detail panel
|
|
1268
|
-
this.cy.on('tap', 'node',
|
|
1597
|
+
this.cy.on('tap', 'node', (e) => {
|
|
1269
1598
|
var data = e.target.data();
|
|
1270
|
-
|
|
1599
|
+
this.showNodeDetail(data.fullData);
|
|
1271
1600
|
});
|
|
1272
1601
|
|
|
1273
1602
|
// Background tap -> close panel
|
|
1274
|
-
this.cy.on('tap',
|
|
1275
|
-
if (e.target ===
|
|
1603
|
+
this.cy.on('tap', (e) => {
|
|
1604
|
+
if (e.target === this.cy) {
|
|
1276
1605
|
document.getElementById('nodeDetailPanel').classList.remove('active');
|
|
1277
1606
|
}
|
|
1278
1607
|
});
|
|
@@ -1280,14 +1609,16 @@ class AgentFlowDashboard {
|
|
|
1280
1609
|
|
|
1281
1610
|
runCytoscapeLayout() {
|
|
1282
1611
|
if (!this.cy) return;
|
|
1283
|
-
this.cy
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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();
|
|
1291
1622
|
}
|
|
1292
1623
|
|
|
1293
1624
|
showNodeDetail(node) {
|
|
@@ -1302,16 +1633,25 @@ class AgentFlowDashboard {
|
|
|
1302
1633
|
var html = '';
|
|
1303
1634
|
html += this.detailRow('ID', node.id);
|
|
1304
1635
|
html += this.detailRow('Type', node.type);
|
|
1305
|
-
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>';
|
|
1306
1642
|
html += this.detailRow('Duration', duration);
|
|
1307
1643
|
if (node.startTime) html += this.detailRow('Start', new Date(node.startTime).toLocaleString());
|
|
1308
1644
|
if (node.endTime) html += this.detailRow('End', new Date(node.endTime).toLocaleString());
|
|
1309
1645
|
if (node.parentId) html += this.detailRow('Parent', node.parentId);
|
|
1310
|
-
if (node.children
|
|
1646
|
+
if (node.children?.length) html += this.detailRow('Children', node.children.length);
|
|
1311
1647
|
|
|
1312
1648
|
if (node.metadata && Object.keys(node.metadata).length > 0) {
|
|
1313
|
-
html +=
|
|
1314
|
-
|
|
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>';
|
|
1315
1655
|
}
|
|
1316
1656
|
|
|
1317
1657
|
body.innerHTML = html;
|
|
@@ -1320,15 +1660,23 @@ class AgentFlowDashboard {
|
|
|
1320
1660
|
|
|
1321
1661
|
detailRow(label, value) {
|
|
1322
1662
|
if (value === undefined || value === null || value === '') return '';
|
|
1323
|
-
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
|
+
);
|
|
1324
1670
|
}
|
|
1325
1671
|
|
|
1326
1672
|
exportGraphPNG() {
|
|
1327
1673
|
if (!this.cy) return;
|
|
1328
1674
|
var png = this.cy.png({ bg: '#0d1117', full: true, maxWidth: 4000, maxHeight: 4000 });
|
|
1329
1675
|
var link = document.createElement('a');
|
|
1330
|
-
var traceName = this.selectedTrace
|
|
1331
|
-
|
|
1676
|
+
var traceName = this.selectedTrace
|
|
1677
|
+
? this.selectedTrace.filename.replace(/\.json$/, '')
|
|
1678
|
+
: 'graph';
|
|
1679
|
+
link.download = `agentflow-${traceName}.png`;
|
|
1332
1680
|
link.href = png;
|
|
1333
1681
|
link.click();
|
|
1334
1682
|
}
|
|
@@ -1338,12 +1686,13 @@ class AgentFlowDashboard {
|
|
|
1338
1686
|
// ---------------------------------------------------------------------------
|
|
1339
1687
|
renderHeatmap() {
|
|
1340
1688
|
var container = document.getElementById('heatmapContent');
|
|
1341
|
-
var
|
|
1689
|
+
var _trace = this.selectedTraceData || this.selectedTrace;
|
|
1342
1690
|
|
|
1343
1691
|
// Build heatmap from recent traces (not just selected trace)
|
|
1344
1692
|
var tracesToUse = this.traces.slice(0, 100);
|
|
1345
1693
|
if (tracesToUse.length === 0) {
|
|
1346
|
-
container.innerHTML =
|
|
1694
|
+
container.innerHTML =
|
|
1695
|
+
'<div class="empty-state"><div class="empty-state-text">No traces available for heatmap.</div></div>';
|
|
1347
1696
|
return;
|
|
1348
1697
|
}
|
|
1349
1698
|
|
|
@@ -1368,22 +1717,35 @@ class AgentFlowDashboard {
|
|
|
1368
1717
|
|
|
1369
1718
|
var cellLabel = failCount > 0 ? failCount : '';
|
|
1370
1719
|
var agentName = escapeHtml(tr.agentId || tr.name || 'unknown');
|
|
1371
|
-
var tooltipText =
|
|
1372
|
-
|
|
1373
|
-
|
|
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}">`;
|
|
1374
1731
|
html += cellLabel;
|
|
1375
|
-
html +=
|
|
1732
|
+
html += `<div class="heatmap-tooltip">${tooltipText}</div>`;
|
|
1376
1733
|
html += '</div>';
|
|
1377
1734
|
}
|
|
1378
1735
|
|
|
1379
1736
|
html += '</div>';
|
|
1380
1737
|
|
|
1381
1738
|
// Legend
|
|
1382
|
-
html +=
|
|
1383
|
-
|
|
1384
|
-
html +=
|
|
1385
|
-
|
|
1386
|
-
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>';
|
|
1387
1749
|
html += '</div>';
|
|
1388
1750
|
|
|
1389
1751
|
container.innerHTML = html;
|
|
@@ -1396,12 +1758,16 @@ class AgentFlowDashboard {
|
|
|
1396
1758
|
var container = document.getElementById('stateContent');
|
|
1397
1759
|
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1398
1760
|
if (!trace || !trace.nodes) {
|
|
1399
|
-
container.innerHTML =
|
|
1761
|
+
container.innerHTML =
|
|
1762
|
+
'<div class="empty-state"><div class="empty-state-text">Select a trace to view state machine.</div></div>';
|
|
1400
1763
|
return;
|
|
1401
1764
|
}
|
|
1402
1765
|
|
|
1403
1766
|
var nodes = this.getNodesArray(trace);
|
|
1404
|
-
var pendingCount = 0,
|
|
1767
|
+
var pendingCount = 0,
|
|
1768
|
+
runningCount = 0,
|
|
1769
|
+
completedCount = 0,
|
|
1770
|
+
failedCount = 0;
|
|
1405
1771
|
|
|
1406
1772
|
for (var i = 0; i < nodes.length; i++) {
|
|
1407
1773
|
var s = nodes[i].status;
|
|
@@ -1420,28 +1786,48 @@ class AgentFlowDashboard {
|
|
|
1420
1786
|
var html = '<div class="state-machine">';
|
|
1421
1787
|
|
|
1422
1788
|
html += '<div class="state">';
|
|
1423
|
-
html +=
|
|
1789
|
+
html +=
|
|
1790
|
+
'<div class="state-circle' +
|
|
1791
|
+
pendingActive +
|
|
1792
|
+
'"><span class="state-count">' +
|
|
1793
|
+
pendingCount +
|
|
1794
|
+
'</span>PENDING</div>';
|
|
1424
1795
|
html += '<span class="state-label">Queued</span>';
|
|
1425
1796
|
html += '</div>';
|
|
1426
1797
|
|
|
1427
1798
|
html += '<div class="state-arrow">→</div>';
|
|
1428
1799
|
|
|
1429
1800
|
html += '<div class="state">';
|
|
1430
|
-
html +=
|
|
1801
|
+
html +=
|
|
1802
|
+
'<div class="state-circle' +
|
|
1803
|
+
runningActive +
|
|
1804
|
+
'"><span class="state-count">' +
|
|
1805
|
+
runningCount +
|
|
1806
|
+
'</span>RUNNING</div>';
|
|
1431
1807
|
html += '<span class="state-label">Active</span>';
|
|
1432
1808
|
html += '</div>';
|
|
1433
1809
|
|
|
1434
1810
|
html += '<div class="state-arrow">→</div>';
|
|
1435
1811
|
|
|
1436
1812
|
html += '<div class="state">';
|
|
1437
|
-
html +=
|
|
1813
|
+
html +=
|
|
1814
|
+
'<div class="state-circle' +
|
|
1815
|
+
completedActive +
|
|
1816
|
+
'"><span class="state-count">' +
|
|
1817
|
+
completedCount +
|
|
1818
|
+
'</span>COMPLETED</div>';
|
|
1438
1819
|
html += '<span class="state-label">Success</span>';
|
|
1439
1820
|
html += '</div>';
|
|
1440
1821
|
|
|
1441
1822
|
html += '<div class="state-arrow">↔</div>';
|
|
1442
1823
|
|
|
1443
1824
|
html += '<div class="state">';
|
|
1444
|
-
html +=
|
|
1825
|
+
html +=
|
|
1826
|
+
'<div class="state-circle' +
|
|
1827
|
+
failedActive +
|
|
1828
|
+
'"><span class="state-count">' +
|
|
1829
|
+
failedCount +
|
|
1830
|
+
'</span>FAILED</div>';
|
|
1445
1831
|
html += '<span class="state-label">Error</span>';
|
|
1446
1832
|
html += '</div>';
|
|
1447
1833
|
|
|
@@ -1466,24 +1852,30 @@ class AgentFlowDashboard {
|
|
|
1466
1852
|
var container = document.getElementById('summaryContent');
|
|
1467
1853
|
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1468
1854
|
if (!trace || !trace.nodes) {
|
|
1469
|
-
container.innerHTML =
|
|
1855
|
+
container.innerHTML =
|
|
1856
|
+
'<div class="empty-state"><div class="empty-state-text">Select a trace to view summary.</div></div>';
|
|
1470
1857
|
return;
|
|
1471
1858
|
}
|
|
1472
1859
|
|
|
1473
1860
|
// Show spinner briefly then generate
|
|
1474
|
-
container.innerHTML =
|
|
1861
|
+
container.innerHTML =
|
|
1862
|
+
'<div class="empty-state"><div class="spinner"></div><div class="empty-state-text">Generating summary...</div></div>';
|
|
1475
1863
|
|
|
1476
|
-
var self = this;
|
|
1477
1864
|
// Use setTimeout to avoid blocking render
|
|
1478
|
-
setTimeout(
|
|
1865
|
+
setTimeout(() => {
|
|
1866
|
+
this.generateSummary(trace, container);
|
|
1867
|
+
}, 50);
|
|
1479
1868
|
}
|
|
1480
1869
|
|
|
1481
1870
|
generateSummary(trace, container) {
|
|
1482
1871
|
var nodes = this.getNodesArray(trace);
|
|
1483
1872
|
var totalNodes = nodes.length;
|
|
1484
|
-
var completedCount = 0,
|
|
1873
|
+
var completedCount = 0,
|
|
1874
|
+
failedCount = 0,
|
|
1875
|
+
runningCount = 0;
|
|
1485
1876
|
var agentNames = new Set();
|
|
1486
|
-
var totalDur = 0,
|
|
1877
|
+
var totalDur = 0,
|
|
1878
|
+
durCount = 0;
|
|
1487
1879
|
|
|
1488
1880
|
for (var i = 0; i < nodes.length; i++) {
|
|
1489
1881
|
var n = nodes[i];
|
|
@@ -1497,64 +1889,79 @@ class AgentFlowDashboard {
|
|
|
1497
1889
|
|
|
1498
1890
|
if (n.startTime && n.endTime) {
|
|
1499
1891
|
var ms = new Date(n.endTime).getTime() - new Date(n.startTime).getTime();
|
|
1500
|
-
if (!isNaN(ms) && ms >= 0) {
|
|
1892
|
+
if (!Number.isNaN(ms) && ms >= 0) {
|
|
1893
|
+
totalDur += ms;
|
|
1894
|
+
durCount++;
|
|
1895
|
+
}
|
|
1501
1896
|
}
|
|
1502
1897
|
}
|
|
1503
1898
|
|
|
1504
|
-
var successRate = totalNodes > 0 ? Math.round(completedCount / totalNodes * 100) : 0;
|
|
1899
|
+
var successRate = totalNodes > 0 ? Math.round((completedCount / totalNodes) * 100) : 0;
|
|
1505
1900
|
var agentList = Array.from(agentNames);
|
|
1506
1901
|
|
|
1507
1902
|
// Build summary title
|
|
1508
|
-
var titleText =
|
|
1903
|
+
var titleText = `Trace: ${escapeHtml(trace.name || trace.agentId || trace.filename || 'Unknown')}`;
|
|
1509
1904
|
|
|
1510
1905
|
// Build summary text
|
|
1511
|
-
var summaryText =
|
|
1512
|
-
summaryText += completedCount
|
|
1513
|
-
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`;
|
|
1514
1909
|
summaryText += '. ';
|
|
1515
1910
|
if (durCount > 0) {
|
|
1516
|
-
summaryText +=
|
|
1517
|
-
summaryText +=
|
|
1911
|
+
summaryText += `Average node duration was ${this.formatDuration(totalDur / durCount)}. `;
|
|
1912
|
+
summaryText += `Total execution time: ${this.formatDuration(totalDur)}.`;
|
|
1518
1913
|
}
|
|
1519
1914
|
|
|
1520
1915
|
// Build details list
|
|
1521
1916
|
var details = [];
|
|
1522
|
-
details.push(
|
|
1523
|
-
details.push(
|
|
1524
|
-
details.push(
|
|
1525
|
-
if (runningCount > 0) details.push(
|
|
1526
|
-
if (agentList.length > 0) details.push(
|
|
1527
|
-
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}`);
|
|
1528
1923
|
|
|
1529
1924
|
// Recommendations
|
|
1530
1925
|
var recommendations = '';
|
|
1531
1926
|
if (failedCount === 0 && runningCount === 0) {
|
|
1532
|
-
recommendations =
|
|
1927
|
+
recommendations =
|
|
1928
|
+
'<strong>Status:</strong> All tasks completed successfully. No issues detected.';
|
|
1533
1929
|
} else if (failedCount > 0) {
|
|
1534
|
-
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.';
|
|
1535
1936
|
}
|
|
1536
1937
|
if (runningCount > 0) {
|
|
1537
|
-
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.';
|
|
1538
1945
|
}
|
|
1539
1946
|
|
|
1540
1947
|
var html = '<div class="summary-card">';
|
|
1541
|
-
html +=
|
|
1542
|
-
html +=
|
|
1948
|
+
html += `<h3 class="summary-title">${titleText}</h3>`;
|
|
1949
|
+
html += `<p class="summary-text">${escapeHtml(summaryText)}</p>`;
|
|
1543
1950
|
html += '<ul class="summary-details">';
|
|
1544
1951
|
for (var j = 0; j < details.length; j++) {
|
|
1545
|
-
html +=
|
|
1952
|
+
html += `<li>${escapeHtml(details[j])}</li>`;
|
|
1546
1953
|
}
|
|
1547
1954
|
html += '</ul>';
|
|
1548
1955
|
|
|
1549
1956
|
if (recommendations) {
|
|
1550
|
-
html +=
|
|
1957
|
+
html += `<div class="summary-recommendations">${recommendations}</div>`;
|
|
1551
1958
|
}
|
|
1552
1959
|
|
|
1553
1960
|
// Confidence bar based on success rate
|
|
1554
1961
|
html += '<div class="confidence-bar">';
|
|
1555
1962
|
html += '<span>Confidence:</span>';
|
|
1556
|
-
html +=
|
|
1557
|
-
html +=
|
|
1963
|
+
html += `<div class="bar"><div class="bar-fill" style="width:${successRate}%;"></div></div>`;
|
|
1964
|
+
html += `<span>${successRate}%</span>`;
|
|
1558
1965
|
html += '</div>';
|
|
1559
1966
|
|
|
1560
1967
|
html += '</div>';
|
|
@@ -1575,51 +1982,105 @@ class AgentFlowDashboard {
|
|
|
1575
1982
|
|
|
1576
1983
|
if (events.length === 0 && filename) {
|
|
1577
1984
|
try {
|
|
1578
|
-
var res = await fetch(
|
|
1985
|
+
var res = await fetch(`/api/traces/${encodeURIComponent(filename)}/events`);
|
|
1579
1986
|
if (res.ok) {
|
|
1580
1987
|
var data = await res.json();
|
|
1581
1988
|
events = data.events || [];
|
|
1582
1989
|
tokenUsage = data.tokenUsage || null;
|
|
1583
1990
|
}
|
|
1584
|
-
} catch (
|
|
1991
|
+
} catch (_e) {
|
|
1585
1992
|
// fall through to node-based rendering
|
|
1586
1993
|
}
|
|
1587
1994
|
}
|
|
1588
1995
|
|
|
1589
1996
|
if (events.length === 0) {
|
|
1590
1997
|
// Fallback: render nodes like a normal trace
|
|
1591
|
-
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>';
|
|
1592
2000
|
return;
|
|
1593
2001
|
}
|
|
1594
2002
|
|
|
1595
2003
|
// Token usage summary at top
|
|
1596
2004
|
if (tokenUsage && tokenUsage.total > 0) {
|
|
1597
|
-
html +=
|
|
1598
|
-
|
|
1599
|
-
html +=
|
|
1600
|
-
|
|
1601
|
-
|
|
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>';
|
|
1602
2026
|
html += '</div>';
|
|
1603
2027
|
}
|
|
1604
2028
|
|
|
1605
2029
|
// Summary badges
|
|
1606
|
-
var userCount = 0,
|
|
2030
|
+
var userCount = 0,
|
|
2031
|
+
assistantCount = 0,
|
|
2032
|
+
toolCount = 0,
|
|
2033
|
+
thinkCount = 0,
|
|
2034
|
+
spawnCount = 0;
|
|
1607
2035
|
for (var i = 0; i < events.length; i++) {
|
|
1608
2036
|
switch (events[i].type) {
|
|
1609
|
-
case 'user':
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
case '
|
|
1613
|
-
|
|
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;
|
|
1614
2052
|
}
|
|
1615
2053
|
}
|
|
1616
2054
|
html += '<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;">';
|
|
1617
|
-
html +=
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
if (
|
|
1622
|
-
|
|
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>';
|
|
1623
2084
|
html += '</div>';
|
|
1624
2085
|
|
|
1625
2086
|
// Render events
|
|
@@ -1647,29 +2108,48 @@ class AgentFlowDashboard {
|
|
|
1647
2108
|
}
|
|
1648
2109
|
|
|
1649
2110
|
html += '<div class="timeline-item">';
|
|
1650
|
-
html +=
|
|
2111
|
+
html += `<div class="timeline-marker" style="background:${marker.color};"></div>`;
|
|
1651
2112
|
html += '<div class="timeline-content">';
|
|
1652
2113
|
html += '<div class="timeline-header">';
|
|
1653
|
-
html +=
|
|
1654
|
-
|
|
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>`;
|
|
1655
2122
|
html += '</span>';
|
|
1656
|
-
html +=
|
|
1657
|
-
if (evt.duration) html +=
|
|
1658
|
-
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>';
|
|
1659
2130
|
html += '</span></div>';
|
|
1660
2131
|
|
|
1661
2132
|
if (contentPreview) {
|
|
1662
|
-
html +=
|
|
2133
|
+
html += `<div class="event-details" style="margin-top:4px;">${contentPreview}</div>`;
|
|
1663
2134
|
}
|
|
1664
2135
|
|
|
1665
2136
|
if (evt.type === 'tool_call' && evt.toolArgs) {
|
|
1666
|
-
var argsStr =
|
|
1667
|
-
|
|
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>';
|
|
1668
2143
|
}
|
|
1669
2144
|
|
|
1670
2145
|
if (evt.type === 'tool_result' && evt.toolResult) {
|
|
1671
2146
|
var resultColor = evt.toolError ? 'var(--accent-error)' : 'var(--text-secondary)';
|
|
1672
|
-
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>';
|
|
1673
2153
|
}
|
|
1674
2154
|
|
|
1675
2155
|
html += '</div></div>';
|
|
@@ -1686,28 +2166,33 @@ class AgentFlowDashboard {
|
|
|
1686
2166
|
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1687
2167
|
|
|
1688
2168
|
if (!trace) {
|
|
1689
|
-
container.innerHTML =
|
|
2169
|
+
container.innerHTML =
|
|
2170
|
+
'<div class="empty-state"><div class="empty-state-text">Select a trace to view transcript.</div></div>';
|
|
1690
2171
|
return;
|
|
1691
2172
|
}
|
|
1692
2173
|
|
|
1693
2174
|
if (trace.sourceType !== 'session') {
|
|
1694
|
-
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>';
|
|
1695
2177
|
return;
|
|
1696
2178
|
}
|
|
1697
2179
|
|
|
1698
2180
|
var events = trace.sessionEvents || [];
|
|
1699
2181
|
if (events.length === 0 && trace.filename) {
|
|
1700
2182
|
try {
|
|
1701
|
-
var res = await fetch(
|
|
2183
|
+
var res = await fetch(`/api/traces/${encodeURIComponent(trace.filename)}/events`);
|
|
1702
2184
|
if (res.ok) {
|
|
1703
2185
|
var data = await res.json();
|
|
1704
2186
|
events = data.events || [];
|
|
1705
2187
|
}
|
|
1706
|
-
} catch (
|
|
2188
|
+
} catch (_e) {
|
|
2189
|
+
/* ignore */
|
|
2190
|
+
}
|
|
1707
2191
|
}
|
|
1708
2192
|
|
|
1709
2193
|
if (events.length === 0) {
|
|
1710
|
-
container.innerHTML =
|
|
2194
|
+
container.innerHTML =
|
|
2195
|
+
'<div class="empty-state"><div class="empty-state-text">No session events found.</div></div>';
|
|
1711
2196
|
return;
|
|
1712
2197
|
}
|
|
1713
2198
|
|
|
@@ -1721,60 +2206,89 @@ class AgentFlowDashboard {
|
|
|
1721
2206
|
if (evt.type === 'user') {
|
|
1722
2207
|
html += '<div class="chat-bubble chat-user">';
|
|
1723
2208
|
html += escapeHtml(evt.content || '');
|
|
1724
|
-
html +=
|
|
2209
|
+
html += `<div class="chat-meta">${evtTime}</div>`;
|
|
1725
2210
|
html += '</div>';
|
|
1726
2211
|
}
|
|
1727
2212
|
|
|
1728
2213
|
if (evt.type === 'assistant') {
|
|
1729
2214
|
html += '<div class="chat-bubble chat-assistant">';
|
|
1730
2215
|
html += escapeHtml(evt.content || '');
|
|
1731
|
-
html +=
|
|
1732
|
-
if (evt.tokens
|
|
1733
|
-
html +=
|
|
1734
|
-
|
|
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)})`;
|
|
1735
2225
|
html += '</span>';
|
|
1736
2226
|
}
|
|
1737
|
-
if (evt.model) html +=
|
|
2227
|
+
if (evt.model) html += ` · ${escapeHtml(evt.model)}`;
|
|
1738
2228
|
html += '</div></div>';
|
|
1739
2229
|
}
|
|
1740
2230
|
|
|
1741
2231
|
if (evt.type === 'thinking') {
|
|
1742
2232
|
thinkingIdx++;
|
|
1743
|
-
var tId =
|
|
2233
|
+
var tId = `thinking-toggle-${thinkingIdx}`;
|
|
1744
2234
|
html += '<div class="chat-bubble chat-thinking">';
|
|
1745
|
-
html +=
|
|
1746
|
-
|
|
1747
|
-
|
|
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>`;
|
|
1748
2246
|
html += '</div>';
|
|
1749
2247
|
}
|
|
1750
2248
|
|
|
1751
2249
|
if (evt.type === 'tool_call') {
|
|
1752
2250
|
html += '<div class="chat-bubble chat-tool">';
|
|
1753
|
-
html +=
|
|
2251
|
+
html +=
|
|
2252
|
+
'<strong>\ud83d\udee0\ufe0f ' +
|
|
2253
|
+
escapeHtml(evt.toolName || evt.name || 'Tool') +
|
|
2254
|
+
'</strong>';
|
|
1754
2255
|
if (evt.toolArgs) {
|
|
1755
|
-
var argsStr =
|
|
1756
|
-
|
|
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>';
|
|
1757
2262
|
}
|
|
1758
|
-
html +=
|
|
1759
|
-
if (evt.duration) html +=
|
|
2263
|
+
html += `<div class="chat-meta">${evtTime}`;
|
|
2264
|
+
if (evt.duration) html += ` · ${this.formatDuration(evt.duration)}`;
|
|
1760
2265
|
html += '</div></div>';
|
|
1761
2266
|
}
|
|
1762
2267
|
|
|
1763
2268
|
if (evt.type === 'tool_result') {
|
|
1764
2269
|
var isError = !!evt.toolError;
|
|
1765
|
-
html +=
|
|
1766
|
-
|
|
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>`;
|
|
1767
2275
|
var resultText = evt.toolError || evt.toolResult || '';
|
|
1768
|
-
html +=
|
|
1769
|
-
|
|
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>`;
|
|
1770
2283
|
html += '</div>';
|
|
1771
2284
|
}
|
|
1772
2285
|
|
|
1773
2286
|
if (evt.type === 'spawn') {
|
|
1774
|
-
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;">';
|
|
1775
2289
|
html += '\ud83d\udc64 Subagent spawned';
|
|
1776
|
-
if (evt.content) html +=
|
|
1777
|
-
html +=
|
|
2290
|
+
if (evt.content) html += `: <code>${escapeHtml(evt.content.substring(0, 40))}</code>`;
|
|
2291
|
+
html += `<div class="chat-meta">${evtTime}</div>`;
|
|
1778
2292
|
html += '</div>';
|
|
1779
2293
|
}
|
|
1780
2294
|
}
|
|
@@ -1793,15 +2307,19 @@ class AgentFlowDashboard {
|
|
|
1793
2307
|
panel.classList.remove('show');
|
|
1794
2308
|
return;
|
|
1795
2309
|
}
|
|
1796
|
-
list.innerHTML = messages.map(
|
|
2310
|
+
list.innerHTML = messages.map((m) => `<li>${escapeHtml(m)}</li>`).join('');
|
|
1797
2311
|
panel.classList.add('show');
|
|
1798
2312
|
}
|
|
1799
2313
|
|
|
1800
2314
|
// ---------------------------------------------------------------------------
|
|
1801
2315
|
// Public / debug
|
|
1802
2316
|
// ---------------------------------------------------------------------------
|
|
1803
|
-
getStats() {
|
|
1804
|
-
|
|
2317
|
+
getStats() {
|
|
2318
|
+
return this.stats;
|
|
2319
|
+
}
|
|
2320
|
+
getTraces() {
|
|
2321
|
+
return this.traces;
|
|
2322
|
+
}
|
|
1805
2323
|
reconnect() {
|
|
1806
2324
|
if (this.ws) this.ws.close();
|
|
1807
2325
|
this.reconnectAttempts = 0;
|
|
@@ -1829,13 +2347,13 @@ class AgentFlowDashboard {
|
|
|
1829
2347
|
var nodeTypes = [];
|
|
1830
2348
|
|
|
1831
2349
|
if (nodes instanceof Map) {
|
|
1832
|
-
nodes.forEach(
|
|
2350
|
+
nodes.forEach((node) => {
|
|
1833
2351
|
if (node.type) nodeTypes.push(node.type);
|
|
1834
2352
|
});
|
|
1835
2353
|
} else if (typeof nodes === 'object') {
|
|
1836
2354
|
for (var nodeId in nodes) {
|
|
1837
2355
|
var node = nodes[nodeId];
|
|
1838
|
-
if (node
|
|
2356
|
+
if (node?.type) nodeTypes.push(node.type);
|
|
1839
2357
|
}
|
|
1840
2358
|
}
|
|
1841
2359
|
|
|
@@ -1851,10 +2369,14 @@ class AgentFlowDashboard {
|
|
|
1851
2369
|
}
|
|
1852
2370
|
|
|
1853
2371
|
// Tag processes by activity type
|
|
1854
|
-
getProcessActivityTag(
|
|
2372
|
+
getProcessActivityTag(_cmd, cmdline, _pid) {
|
|
1855
2373
|
// Main processes (primary orchestrators)
|
|
1856
|
-
if (
|
|
1857
|
-
|
|
2374
|
+
if (
|
|
2375
|
+
cmdline.includes('main') ||
|
|
2376
|
+
cmdline.includes('orchestrator') ||
|
|
2377
|
+
cmdline.includes('coordinator') ||
|
|
2378
|
+
cmdline.includes('master')
|
|
2379
|
+
) {
|
|
1858
2380
|
return 'main';
|
|
1859
2381
|
}
|
|
1860
2382
|
|
|
@@ -1864,50 +2386,82 @@ class AgentFlowDashboard {
|
|
|
1864
2386
|
}
|
|
1865
2387
|
|
|
1866
2388
|
// Browser/UI processes
|
|
1867
|
-
if (
|
|
1868
|
-
|
|
2389
|
+
if (
|
|
2390
|
+
cmdline.includes('browser') ||
|
|
2391
|
+
cmdline.includes('chrome') ||
|
|
2392
|
+
cmdline.includes('firefox') ||
|
|
2393
|
+
cmdline.includes('dashboard')
|
|
2394
|
+
) {
|
|
1869
2395
|
return 'browser';
|
|
1870
2396
|
}
|
|
1871
2397
|
|
|
1872
2398
|
// Context/memory processes
|
|
1873
|
-
if (
|
|
1874
|
-
|
|
2399
|
+
if (
|
|
2400
|
+
cmdline.includes('context') ||
|
|
2401
|
+
cmdline.includes('memory') ||
|
|
2402
|
+
cmdline.includes('cache') ||
|
|
2403
|
+
cmdline.includes('embedding')
|
|
2404
|
+
) {
|
|
1875
2405
|
return 'context';
|
|
1876
2406
|
}
|
|
1877
2407
|
|
|
1878
2408
|
// Execution processes
|
|
1879
|
-
if (
|
|
1880
|
-
|
|
2409
|
+
if (
|
|
2410
|
+
cmdline.includes('exec') ||
|
|
2411
|
+
cmdline.includes('runner') ||
|
|
2412
|
+
cmdline.includes('executor') ||
|
|
2413
|
+
cmdline.includes('worker')
|
|
2414
|
+
) {
|
|
1881
2415
|
return 'exec';
|
|
1882
2416
|
}
|
|
1883
2417
|
|
|
1884
2418
|
// Read operations
|
|
1885
|
-
if (
|
|
1886
|
-
|
|
2419
|
+
if (
|
|
2420
|
+
cmdline.includes('read') ||
|
|
2421
|
+
cmdline.includes('scanner') ||
|
|
2422
|
+
cmdline.includes('parser') ||
|
|
2423
|
+
cmdline.includes('loader')
|
|
2424
|
+
) {
|
|
1887
2425
|
return 'read';
|
|
1888
2426
|
}
|
|
1889
2427
|
|
|
1890
2428
|
// Tool processes
|
|
1891
|
-
if (
|
|
1892
|
-
|
|
2429
|
+
if (
|
|
2430
|
+
cmdline.includes('tool') ||
|
|
2431
|
+
cmdline.includes('utility') ||
|
|
2432
|
+
cmdline.includes('helper') ||
|
|
2433
|
+
cmdline.includes('script')
|
|
2434
|
+
) {
|
|
1893
2435
|
return 'tool';
|
|
1894
2436
|
}
|
|
1895
2437
|
|
|
1896
2438
|
// Thinking/AI processes
|
|
1897
|
-
if (
|
|
1898
|
-
|
|
2439
|
+
if (
|
|
2440
|
+
cmdline.includes('think') ||
|
|
2441
|
+
cmdline.includes('reason') ||
|
|
2442
|
+
cmdline.includes('llm') ||
|
|
2443
|
+
cmdline.includes('model')
|
|
2444
|
+
) {
|
|
1899
2445
|
return 'think';
|
|
1900
2446
|
}
|
|
1901
2447
|
|
|
1902
2448
|
// User interface processes
|
|
1903
|
-
if (
|
|
1904
|
-
|
|
2449
|
+
if (
|
|
2450
|
+
cmdline.includes('ui') ||
|
|
2451
|
+
cmdline.includes('frontend') ||
|
|
2452
|
+
cmdline.includes('interface') ||
|
|
2453
|
+
cmdline.includes('client')
|
|
2454
|
+
) {
|
|
1905
2455
|
return 'user';
|
|
1906
2456
|
}
|
|
1907
2457
|
|
|
1908
2458
|
// Write/output processes
|
|
1909
|
-
if (
|
|
1910
|
-
|
|
2459
|
+
if (
|
|
2460
|
+
cmdline.includes('write') ||
|
|
2461
|
+
cmdline.includes('output') ||
|
|
2462
|
+
cmdline.includes('export') ||
|
|
2463
|
+
cmdline.includes('save')
|
|
2464
|
+
) {
|
|
1911
2465
|
return 'write';
|
|
1912
2466
|
}
|
|
1913
2467
|
|
|
@@ -1915,7 +2469,271 @@ class AgentFlowDashboard {
|
|
|
1915
2469
|
}
|
|
1916
2470
|
|
|
1917
2471
|
// ---------------------------------------------------------------------------
|
|
1918
|
-
// Tab 8:
|
|
2472
|
+
// Tab 8: Agent Timeline (Gantt Chart)
|
|
2473
|
+
// ---------------------------------------------------------------------------
|
|
2474
|
+
renderAgentTimeline() {
|
|
2475
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
2476
|
+
if (!trace || !trace.agentId) {
|
|
2477
|
+
document.getElementById('agentTimelineEmpty').style.display = '';
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
var agentId = trace.agentId;
|
|
2482
|
+
|
|
2483
|
+
if (this._agentTimelineAgent === agentId && this._agentTimelineRendered) return;
|
|
2484
|
+
this._agentTimelineAgent = agentId;
|
|
2485
|
+
|
|
2486
|
+
var container = document.getElementById('agentTimelineContent');
|
|
2487
|
+
container.innerHTML =
|
|
2488
|
+
'<div class="empty-state"><div class="empty-state-icon" style="animation:spin 1s linear infinite">⚙</div>' +
|
|
2489
|
+
'<div class="empty-state-text">Loading timeline for ' +
|
|
2490
|
+
escapeHtml(agentId) +
|
|
2491
|
+
'...</div></div>';
|
|
2492
|
+
|
|
2493
|
+
fetch(`/api/agents/${encodeURIComponent(agentId)}/timeline?limit=50`)
|
|
2494
|
+
.then((r) => r.json())
|
|
2495
|
+
.then((data) => {
|
|
2496
|
+
if (data.error || !data.executions || data.executions.length === 0) {
|
|
2497
|
+
container.innerHTML =
|
|
2498
|
+
'<div class="empty-state"><div class="empty-state-text">No timeline data for ' +
|
|
2499
|
+
escapeHtml(agentId) +
|
|
2500
|
+
'</div></div>';
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
this._agentTimelineRendered = true;
|
|
2504
|
+
this._renderGantt(container, data);
|
|
2505
|
+
})
|
|
2506
|
+
.catch(() => {
|
|
2507
|
+
container.innerHTML =
|
|
2508
|
+
'<div class="empty-state"><div class="empty-state-text">Failed to load agent timeline.</div></div>';
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
_renderGantt(container, data) {
|
|
2513
|
+
var execs = data.executions;
|
|
2514
|
+
var minTime = data.minTime;
|
|
2515
|
+
var maxTime = data.maxTime;
|
|
2516
|
+
var timeSpan = maxTime - minTime || 1;
|
|
2517
|
+
|
|
2518
|
+
// Layout constants
|
|
2519
|
+
var labelW = 220;
|
|
2520
|
+
var chartW = 900;
|
|
2521
|
+
var rowH = 28;
|
|
2522
|
+
var subRowH = 20;
|
|
2523
|
+
var headerH = 36;
|
|
2524
|
+
var totalW = labelW + chartW + 20;
|
|
2525
|
+
|
|
2526
|
+
// Build HTML
|
|
2527
|
+
var html =
|
|
2528
|
+
'<div class="gantt-wrapper" style="font-size:11px;color:#c9d1d9;min-width:' +
|
|
2529
|
+
totalW +
|
|
2530
|
+
'px;">';
|
|
2531
|
+
|
|
2532
|
+
// Header with time axis
|
|
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>';
|
|
2543
|
+
html += '<div style="flex:1;position:relative;">';
|
|
2544
|
+
// Time ticks
|
|
2545
|
+
var tickCount = 6;
|
|
2546
|
+
for (var t = 0; t <= tickCount; t++) {
|
|
2547
|
+
var pct = (t / tickCount) * 100;
|
|
2548
|
+
var tickTime = minTime + (t / tickCount) * timeSpan;
|
|
2549
|
+
var d = new Date(tickTime);
|
|
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>';
|
|
2565
|
+
}
|
|
2566
|
+
html += '</div></div>';
|
|
2567
|
+
|
|
2568
|
+
// Rows
|
|
2569
|
+
html += '<div class="gantt-body">';
|
|
2570
|
+
for (var i = 0; i < execs.length; i++) {
|
|
2571
|
+
var exec = execs[i];
|
|
2572
|
+
var execStart = ((exec.startTime - minTime) / timeSpan) * 100;
|
|
2573
|
+
var execWidth = Math.max(0.3, ((exec.endTime - exec.startTime) / timeSpan) * 100);
|
|
2574
|
+
var statusColor =
|
|
2575
|
+
exec.status === 'failed' ? '#ef4444' : exec.status === 'running' ? '#3b82f6' : '#10b981';
|
|
2576
|
+
var hasActivities = exec.activities && exec.activities.length > 0;
|
|
2577
|
+
var execId = `gantt-exec-${i}`;
|
|
2578
|
+
|
|
2579
|
+
// Main execution row
|
|
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
|
+
'">';
|
|
2590
|
+
|
|
2591
|
+
// Label
|
|
2592
|
+
var execName = exec.name || exec.filename || exec.id;
|
|
2593
|
+
if (execName.length > 28) execName = `${execName.slice(0, 28)}...`;
|
|
2594
|
+
var dur = this.computeDuration(exec.startTime, exec.endTime);
|
|
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>';
|
|
2616
|
+
|
|
2617
|
+
// Bar
|
|
2618
|
+
html += '<div style="flex:1;position:relative;padding:4px 0;">';
|
|
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>';
|
|
2637
|
+
html += '</div></div>';
|
|
2638
|
+
|
|
2639
|
+
// Sub-activities (collapsed by default)
|
|
2640
|
+
if (hasActivities) {
|
|
2641
|
+
html += `<div id="${execId}" style="display:none;background:#0a0e14;">`;
|
|
2642
|
+
// Filter to top-level activities (no parentId or parentId is root)
|
|
2643
|
+
var rootIds = new Set();
|
|
2644
|
+
if (exec.activities.length > 0) {
|
|
2645
|
+
var firstAct = exec.activities[0];
|
|
2646
|
+
rootIds.add(firstAct.id);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
for (var j = 0; j < exec.activities.length; j++) {
|
|
2650
|
+
var act = exec.activities[j];
|
|
2651
|
+
var actStart = ((Math.max(act.startTime, exec.startTime) - minTime) / timeSpan) * 100;
|
|
2652
|
+
var actEnd = act.endTime || act.startTime;
|
|
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';
|
|
2673
|
+
var actName = act.name || act.type;
|
|
2674
|
+
if (actName.length > 30) actName = `${actName.slice(0, 30)}...`;
|
|
2675
|
+
var isChild = act.parentId && !rootIds.has(act.id);
|
|
2676
|
+
|
|
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>';
|
|
2693
|
+
html += '<div style="flex:1;position:relative;">';
|
|
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>';
|
|
2710
|
+
html += '</div></div>';
|
|
2711
|
+
}
|
|
2712
|
+
html += '</div>';
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
html += '</div>';
|
|
2717
|
+
|
|
2718
|
+
// Summary bar
|
|
2719
|
+
html += '<div style="padding:10px;border-top:1px solid #30363d;color:#8b949e;font-size:10px;">';
|
|
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}`;
|
|
2729
|
+
html += '</div>';
|
|
2730
|
+
|
|
2731
|
+
html += '</div>';
|
|
2732
|
+
container.innerHTML = html;
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
// ---------------------------------------------------------------------------
|
|
2736
|
+
// Tab 9: Process Map (Process Mining Graph)
|
|
1919
2737
|
// ---------------------------------------------------------------------------
|
|
1920
2738
|
renderProcessMap() {
|
|
1921
2739
|
var trace = this.selectedTraceData || this.selectedTrace;
|
|
@@ -1925,7 +2743,6 @@ class AgentFlowDashboard {
|
|
|
1925
2743
|
}
|
|
1926
2744
|
|
|
1927
2745
|
var agentId = trace.agentId;
|
|
1928
|
-
var self = this;
|
|
1929
2746
|
|
|
1930
2747
|
// Avoid re-fetching for same agent
|
|
1931
2748
|
if (this._processMapAgent === agentId && this._cyProcessMap) return;
|
|
@@ -1933,22 +2750,28 @@ class AgentFlowDashboard {
|
|
|
1933
2750
|
|
|
1934
2751
|
document.getElementById('processMapEmpty').innerHTML =
|
|
1935
2752
|
'<div class="empty-state-icon" style="animation:spin 1s linear infinite">⚙</div>' +
|
|
1936
|
-
'<div class="empty-state-text">Building process map for ' +
|
|
2753
|
+
'<div class="empty-state-text">Building process map for ' +
|
|
2754
|
+
escapeHtml(agentId) +
|
|
2755
|
+
'...</div>';
|
|
1937
2756
|
document.getElementById('processMapEmpty').style.display = '';
|
|
1938
2757
|
|
|
1939
|
-
fetch(
|
|
1940
|
-
.then(
|
|
1941
|
-
.then(
|
|
2758
|
+
fetch(`/api/agents/${encodeURIComponent(agentId)}/process-graph`)
|
|
2759
|
+
.then((r) => r.json())
|
|
2760
|
+
.then((data) => {
|
|
1942
2761
|
if (data.error || !data.nodes || data.nodes.length === 0) {
|
|
1943
2762
|
document.getElementById('processMapEmpty').innerHTML =
|
|
1944
2763
|
'<div class="empty-state-icon">⚙</div>' +
|
|
1945
|
-
'<div class="empty-state-text">No process data for ' +
|
|
2764
|
+
'<div class="empty-state-text">No process data for ' +
|
|
2765
|
+
escapeHtml(agentId) +
|
|
2766
|
+
'</div>';
|
|
1946
2767
|
return;
|
|
1947
2768
|
}
|
|
1948
2769
|
document.getElementById('processMapEmpty').style.display = 'none';
|
|
1949
|
-
|
|
2770
|
+
this._buildProcessMapGraph(data);
|
|
2771
|
+
this._loadVariantPanel(agentId);
|
|
2772
|
+
this._loadProfileCard(agentId);
|
|
1950
2773
|
})
|
|
1951
|
-
.catch(
|
|
2774
|
+
.catch(() => {
|
|
1952
2775
|
document.getElementById('processMapEmpty').innerHTML =
|
|
1953
2776
|
'<div class="empty-state-icon">⚙</div>' +
|
|
1954
2777
|
'<div class="empty-state-text">Failed to load process map.</div>';
|
|
@@ -1956,7 +2779,10 @@ class AgentFlowDashboard {
|
|
|
1956
2779
|
}
|
|
1957
2780
|
|
|
1958
2781
|
_buildProcessMapGraph(data) {
|
|
1959
|
-
if (this._cyProcessMap) {
|
|
2782
|
+
if (this._cyProcessMap) {
|
|
2783
|
+
this._cyProcessMap.destroy();
|
|
2784
|
+
this._cyProcessMap = null;
|
|
2785
|
+
}
|
|
1960
2786
|
|
|
1961
2787
|
var elements = [];
|
|
1962
2788
|
var maxNode = data.maxNodeCount || 1;
|
|
@@ -1970,7 +2796,7 @@ class AgentFlowDashboard {
|
|
|
1970
2796
|
|
|
1971
2797
|
var size = node.isVirtual ? 30 : Math.max(25, Math.min(70, 25 + 45 * (node.count / maxNode)));
|
|
1972
2798
|
var label = node.label;
|
|
1973
|
-
if (!node.isVirtual && node.count > 1) label +=
|
|
2799
|
+
if (!node.isVirtual && node.count > 1) label += ` (${node.count})`;
|
|
1974
2800
|
|
|
1975
2801
|
elements.push({
|
|
1976
2802
|
group: 'nodes',
|
|
@@ -1981,15 +2807,16 @@ class AgentFlowDashboard {
|
|
|
1981
2807
|
frequency: node.frequency,
|
|
1982
2808
|
avgDuration: node.avgDuration,
|
|
1983
2809
|
failRate: node.failRate,
|
|
2810
|
+
p95Duration: node.p95Duration || 0,
|
|
1984
2811
|
isVirtual: node.isVirtual,
|
|
1985
2812
|
size: size,
|
|
1986
|
-
fullData: node
|
|
1987
|
-
}
|
|
2813
|
+
fullData: node,
|
|
2814
|
+
},
|
|
1988
2815
|
});
|
|
1989
2816
|
}
|
|
1990
2817
|
|
|
1991
2818
|
// Collect valid node IDs
|
|
1992
|
-
var validIds = new Set(elements.map(
|
|
2819
|
+
var validIds = new Set(elements.map((e) => e.data.id));
|
|
1993
2820
|
|
|
1994
2821
|
// Add edges (only between valid nodes)
|
|
1995
2822
|
for (var j = 0; j < data.edges.length; j++) {
|
|
@@ -2004,20 +2831,19 @@ class AgentFlowDashboard {
|
|
|
2004
2831
|
elements.push({
|
|
2005
2832
|
group: 'edges',
|
|
2006
2833
|
data: {
|
|
2007
|
-
id:
|
|
2834
|
+
id: `pe-${edge.source}-${edge.target}`,
|
|
2008
2835
|
source: edge.source,
|
|
2009
2836
|
target: edge.target,
|
|
2010
2837
|
count: edge.count,
|
|
2011
2838
|
frequency: edge.frequency,
|
|
2012
2839
|
width: width,
|
|
2013
2840
|
opacity: opacity,
|
|
2014
|
-
label: edge.count > 1 ? String(edge.count) : ''
|
|
2015
|
-
}
|
|
2841
|
+
label: edge.count > 1 ? String(edge.count) : '',
|
|
2842
|
+
},
|
|
2016
2843
|
});
|
|
2017
2844
|
}
|
|
2018
2845
|
|
|
2019
2846
|
var container = document.getElementById('cyProcessMap');
|
|
2020
|
-
var self = this;
|
|
2021
2847
|
|
|
2022
2848
|
this._cyProcessMap = cytoscape({
|
|
2023
2849
|
container: container,
|
|
@@ -2026,14 +2852,14 @@ class AgentFlowDashboard {
|
|
|
2026
2852
|
{
|
|
2027
2853
|
selector: 'node',
|
|
2028
2854
|
style: {
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2855
|
+
label: 'data(label)',
|
|
2856
|
+
width: 'data(size)',
|
|
2857
|
+
height: 'data(size)',
|
|
2032
2858
|
'font-size': '9px',
|
|
2033
2859
|
'text-valign': 'bottom',
|
|
2034
2860
|
'text-halign': 'center',
|
|
2035
2861
|
'text-margin-y': 6,
|
|
2036
|
-
|
|
2862
|
+
color: '#c9d1d9',
|
|
2037
2863
|
'text-outline-color': '#0d1117',
|
|
2038
2864
|
'text-outline-width': 2,
|
|
2039
2865
|
'text-wrap': 'ellipsis',
|
|
@@ -2041,40 +2867,80 @@ class AgentFlowDashboard {
|
|
|
2041
2867
|
'border-width': 2,
|
|
2042
2868
|
'border-color': '#30363d',
|
|
2043
2869
|
'background-color': '#3b82f6',
|
|
2044
|
-
|
|
2045
|
-
}
|
|
2870
|
+
shape: 'round-rectangle',
|
|
2871
|
+
},
|
|
2046
2872
|
},
|
|
2047
2873
|
// Virtual START/END nodes
|
|
2048
|
-
{
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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
|
+
},
|
|
2052
2886
|
// Color by fail rate: green → yellow → red
|
|
2053
|
-
{
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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
|
+
},
|
|
2057
2920
|
// Selected
|
|
2058
|
-
{
|
|
2921
|
+
{
|
|
2922
|
+
selector: ':selected',
|
|
2923
|
+
style: { 'border-width': 4, 'border-color': '#f59e0b', 'overlay-opacity': 0.08 },
|
|
2924
|
+
},
|
|
2059
2925
|
// Edges
|
|
2060
2926
|
{
|
|
2061
2927
|
selector: 'edge',
|
|
2062
2928
|
style: {
|
|
2063
|
-
|
|
2064
|
-
|
|
2929
|
+
width: 'data(width)',
|
|
2930
|
+
opacity: 'data(opacity)',
|
|
2065
2931
|
'line-color': '#6b7280',
|
|
2066
2932
|
'target-arrow-color': '#6b7280',
|
|
2067
2933
|
'target-arrow-shape': 'triangle',
|
|
2068
2934
|
'curve-style': 'bezier',
|
|
2069
2935
|
'arrow-scale': 0.7,
|
|
2070
|
-
|
|
2936
|
+
label: 'data(label)',
|
|
2071
2937
|
'font-size': '8px',
|
|
2072
|
-
|
|
2938
|
+
color: '#8b949e',
|
|
2073
2939
|
'text-outline-color': '#0d1117',
|
|
2074
2940
|
'text-outline-width': 1.5,
|
|
2075
|
-
'text-rotation': 'autorotate'
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2941
|
+
'text-rotation': 'autorotate',
|
|
2942
|
+
},
|
|
2943
|
+
},
|
|
2078
2944
|
],
|
|
2079
2945
|
layout: {
|
|
2080
2946
|
name: 'breadthfirst',
|
|
@@ -2083,16 +2949,18 @@ class AgentFlowDashboard {
|
|
|
2083
2949
|
spacingFactor: 1.6,
|
|
2084
2950
|
animate: true,
|
|
2085
2951
|
animationDuration: 400,
|
|
2086
|
-
roots:
|
|
2087
|
-
|
|
2952
|
+
roots:
|
|
2953
|
+
elements.filter((e) => e.data && e.data.id === '[START]').length > 0
|
|
2954
|
+
? ['[START]']
|
|
2955
|
+
: undefined,
|
|
2088
2956
|
},
|
|
2089
2957
|
minZoom: 0.15,
|
|
2090
2958
|
maxZoom: 4,
|
|
2091
|
-
wheelSensitivity: 0.3
|
|
2959
|
+
wheelSensitivity: 0.3,
|
|
2092
2960
|
});
|
|
2093
2961
|
|
|
2094
2962
|
// Click node → show detail
|
|
2095
|
-
this._cyProcessMap.on('tap', 'node',
|
|
2963
|
+
this._cyProcessMap.on('tap', 'node', (e) => {
|
|
2096
2964
|
var d = e.target.data().fullData;
|
|
2097
2965
|
if (!d || d.isVirtual) return;
|
|
2098
2966
|
var panel = document.getElementById('processMapDetailPanel');
|
|
@@ -2101,16 +2969,19 @@ class AgentFlowDashboard {
|
|
|
2101
2969
|
|
|
2102
2970
|
title.textContent = d.label;
|
|
2103
2971
|
var html = '';
|
|
2104
|
-
html +=
|
|
2105
|
-
html +=
|
|
2106
|
-
if (d.avgDuration > 0)
|
|
2107
|
-
|
|
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)}%`);
|
|
2108
2979
|
body.innerHTML = html;
|
|
2109
2980
|
panel.classList.add('active');
|
|
2110
2981
|
});
|
|
2111
2982
|
|
|
2112
|
-
this._cyProcessMap.on('tap',
|
|
2113
|
-
if (e.target ===
|
|
2983
|
+
this._cyProcessMap.on('tap', (e) => {
|
|
2984
|
+
if (e.target === this._cyProcessMap) {
|
|
2114
2985
|
document.getElementById('processMapDetailPanel').classList.remove('active');
|
|
2115
2986
|
}
|
|
2116
2987
|
});
|
|
@@ -2118,15 +2989,92 @@ class AgentFlowDashboard {
|
|
|
2118
2989
|
// Close button
|
|
2119
2990
|
var closeBtn = document.getElementById('processMapDetailClose');
|
|
2120
2991
|
if (closeBtn) {
|
|
2121
|
-
closeBtn.onclick =
|
|
2992
|
+
closeBtn.onclick = () => {
|
|
2122
2993
|
document.getElementById('processMapDetailPanel').classList.remove('active');
|
|
2123
2994
|
};
|
|
2124
2995
|
}
|
|
2125
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
|
+
}
|
|
2126
3074
|
}
|
|
2127
3075
|
|
|
2128
3076
|
// Initialize
|
|
2129
|
-
document.addEventListener('DOMContentLoaded',
|
|
3077
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2130
3078
|
window.dashboard = new AgentFlowDashboard();
|
|
2131
3079
|
});
|
|
2132
3080
|
|