agentflow-dashboard 0.5.0 → 0.7.0

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