agentflow-dashboard 0.4.1 → 0.6.0

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