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