@ai-cortex/daemon 0.1.1 → 0.1.2

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.
@@ -1,1616 +1,1136 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Cortex Dashboard</title>
7
- <style>
8
- :root {
9
- --bg: #141414;
10
- --surface: #1e1e1e;
11
- --surface2: #252525;
12
- --border: #333;
13
- --text: #e0e0e0;
14
- --text-muted: #888;
15
- --green: #22c55e;
16
- --yellow: #eab308;
17
- --gray: #6b7280;
18
- --red: #ef4444;
19
- --blue: #3b82f6;
20
- --header-h: 60px;
21
- }
22
-
23
- * { margin: 0; padding: 0; box-sizing: border-box; }
24
-
25
- html, body {
26
- background: var(--bg);
27
- color: var(--text);
28
- font-family: system-ui, -apple-system, sans-serif;
29
- height: 100%;
30
- }
31
-
32
- /* Header */
33
- .header {
34
- position: fixed;
35
- top: 0;
36
- left: 0;
37
- right: 0;
38
- height: var(--header-h);
39
- background: var(--surface);
40
- border-bottom: 1px solid var(--border);
41
- display: flex;
42
- align-items: center;
43
- justify-content: space-between;
44
- padding: 0 24px;
45
- z-index: 100;
46
- }
47
-
48
- .header-title {
49
- font-size: 18px;
50
- font-weight: 700;
51
- letter-spacing: -0.3px;
52
- }
53
-
54
- .header-stats {
55
- display: flex;
56
- gap: 24px;
57
- font-size: 13px;
58
- font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
59
- }
60
-
61
- .stat-item {
62
- display: flex;
63
- align-items: center;
64
- gap: 6px;
65
- }
66
-
67
- .stat-label {
68
- color: var(--text-muted);
69
- }
70
-
71
- .stat-value {
72
- color: var(--text);
73
- font-weight: 600;
74
- }
75
-
76
- /* Main layout */
77
- .main {
78
- margin-top: var(--header-h);
79
- display: flex;
80
- height: calc(100vh - var(--header-h));
81
- }
82
-
83
- /* Stream panel */
84
- .stream-panel {
85
- flex: 7;
86
- overflow-y: auto;
87
- padding: 16px;
88
- }
89
-
90
- .stream-panel::-webkit-scrollbar { width: 6px; }
91
- .stream-panel::-webkit-scrollbar-track { background: transparent; }
92
- .stream-panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
93
-
94
- .empty-state {
95
- display: flex;
96
- align-items: center;
97
- justify-content: center;
98
- height: 100%;
99
- color: var(--text-muted);
100
- font-size: 14px;
101
- }
102
-
103
- /* Request card */
104
- .card {
105
- background: var(--surface);
106
- border: 1px solid var(--border);
107
- border-left: 4px solid var(--gray);
108
- border-radius: 8px;
109
- padding: 14px 16px;
110
- margin-bottom: 10px;
111
- animation: fadeIn 0.2s ease-out;
112
- }
113
-
114
- @keyframes fadeIn {
115
- from { opacity: 0; transform: translateY(-6px); }
116
- to { opacity: 1; transform: translateY(0); }
117
- }
118
-
119
- .card.status-injected { border-left-color: var(--green); }
120
- .card.status-pruned { border-left-color: var(--yellow); }
121
- .card.status-passthrough { border-left-color: var(--gray); }
122
- .card.status-error { border-left-color: var(--red); }
123
-
124
- .card-header {
125
- display: flex;
126
- justify-content: space-between;
127
- align-items: center;
128
- margin-bottom: 10px;
129
- font-size: 13px;
130
- }
131
-
132
- .card-id {
133
- font-weight: 700;
134
- font-size: 14px;
135
- }
136
-
137
- .card-meta {
138
- color: var(--text-muted);
139
- font-family: monospace;
140
- font-size: 12px;
141
- }
142
-
143
- .card-line {
144
- font-size: 12px;
145
- font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
146
- padding: 3px 0;
147
- color: var(--text-muted);
148
- }
149
-
150
- .card-line span.label {
151
- display: inline-block;
152
- width: 90px;
153
- color: var(--text-muted);
154
- text-transform: uppercase;
155
- font-size: 10px;
156
- font-weight: 600;
157
- letter-spacing: 0.5px;
158
- }
159
-
160
- .card-line .value { color: var(--text); }
161
- .card-line .green { color: var(--green); }
162
- .card-line .yellow { color: var(--yellow); }
163
- .card-line .red { color: var(--red); }
164
-
165
- /* Sidebar */
166
- .sidebar {
167
- flex: 3;
168
- border-left: 1px solid var(--border);
169
- overflow-y: auto;
170
- background: var(--surface);
171
- display: flex;
172
- flex-direction: column;
173
- }
174
-
175
- .sidebar::-webkit-scrollbar { width: 6px; }
176
- .sidebar::-webkit-scrollbar-track { background: transparent; }
177
- .sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
178
-
179
- .sidebar-section {
180
- padding: 16px;
181
- border-bottom: 1px solid var(--border);
182
- }
183
-
184
- .sidebar-section:last-child {
185
- border-bottom: none;
186
- }
187
-
188
- .section-title {
189
- font-size: 11px;
190
- font-weight: 700;
191
- margin-bottom: 12px;
192
- color: var(--text-muted);
193
- text-transform: uppercase;
194
- letter-spacing: 0.5px;
195
- }
196
-
197
- /* Brain section */
198
- .brain-entry {
199
- background: var(--surface2);
200
- border: 1px solid var(--border);
201
- border-radius: 6px;
202
- padding: 10px 12px;
203
- margin-bottom: 8px;
204
- }
205
-
206
- .brain-entry-title {
207
- font-size: 13px;
208
- font-weight: 600;
209
- margin-bottom: 6px;
210
- }
211
-
212
- .brain-summary-row {
213
- display: flex;
214
- justify-content: space-between;
215
- font-size: 12px;
216
- margin-bottom: 8px;
217
- }
218
-
219
- .brain-summary-label {
220
- color: var(--text-muted);
221
- }
222
-
223
- .brain-summary-value {
224
- font-weight: 600;
225
- font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
226
- }
227
-
228
- .badge {
229
- display: inline-block;
230
- font-size: 10px;
231
- font-weight: 600;
232
- padding: 2px 7px;
233
- border-radius: 4px;
234
- margin-right: 4px;
235
- margin-bottom: 3px;
236
- text-transform: uppercase;
237
- letter-spacing: 0.3px;
238
- }
239
-
240
- .badge-lang {
241
- background: rgba(59, 130, 246, 0.15);
242
- color: var(--blue);
243
- }
244
-
245
- .badge-tag {
246
- background: rgba(107, 114, 128, 0.2);
247
- color: var(--text-muted);
248
- }
249
-
250
- .sidebar-empty {
251
- color: var(--text-muted);
252
- font-size: 13px;
253
- text-align: center;
254
- padding: 16px 0;
255
- }
256
-
257
- /* Features section */
258
- .feature-row {
259
- display: flex;
260
- align-items: center;
261
- justify-content: space-between;
262
- padding: 8px 0;
263
- border-bottom: 1px solid var(--border);
264
- }
265
-
266
- .feature-row:last-child {
267
- border-bottom: none;
268
- }
269
-
270
- .feature-info {
271
- display: flex;
272
- flex-direction: column;
273
- gap: 2px;
274
- }
275
-
276
- .feature-name {
277
- font-size: 13px;
278
- font-weight: 600;
279
- text-transform: capitalize;
280
- }
281
-
282
- .feature-reason {
283
- font-size: 11px;
284
- color: var(--text-muted);
285
- }
286
-
287
- /* Toggle switch */
288
- .toggle {
289
- position: relative;
290
- width: 36px;
291
- height: 20px;
292
- flex-shrink: 0;
293
- }
294
-
295
- .toggle input {
296
- opacity: 0;
297
- width: 0;
298
- height: 0;
299
- position: absolute;
300
- }
301
-
302
- .toggle-track {
303
- position: absolute;
304
- inset: 0;
305
- background: var(--border);
306
- border-radius: 10px;
307
- cursor: pointer;
308
- transition: background 0.2s;
309
- }
310
-
311
- .toggle input:checked + .toggle-track {
312
- background: var(--green);
313
- }
314
-
315
- .toggle input:disabled + .toggle-track {
316
- opacity: 0.4;
317
- cursor: not-allowed;
318
- }
319
-
320
- .toggle-thumb {
321
- position: absolute;
322
- top: 3px;
323
- left: 3px;
324
- width: 14px;
325
- height: 14px;
326
- background: white;
327
- border-radius: 50%;
328
- transition: transform 0.2s;
329
- pointer-events: none;
330
- }
331
-
332
- .toggle input:checked ~ .toggle-thumb {
333
- transform: translateX(16px);
334
- }
335
-
336
- /* Agent type badges */
337
- .badge-main { background: var(--green); color: #000; }
338
- .badge-subagent { background: var(--blue); color: #fff; }
339
- .badge-teammate { background: #a855f7; color: #fff; }
340
- .badge-unknown { background: var(--gray); color: #fff; }
341
-
342
- .agent-badge {
343
- display: inline-block;
344
- font-size: 10px;
345
- font-weight: 600;
346
- padding: 1px 6px;
347
- border-radius: 3px;
348
- text-transform: uppercase;
349
- letter-spacing: 0.5px;
350
- }
351
-
352
- /* Agent sidebar section */
353
- .agent-section { margin-top: 16px; }
354
- .agent-section h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); margin-bottom: 8px; }
355
- .agent-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 4px; cursor: pointer; font-size: 13px; }
356
- .agent-row:hover { background: var(--surface2); }
357
- .agent-row.active { background: var(--surface2); border-left: 2px solid var(--blue); }
358
- .agent-model { color: var(--text-muted); font-size: 11px; }
359
- .agent-requests { color: var(--text-muted); font-size: 11px; margin-left: auto; }
360
-
361
- /* History section */
362
- .history-section { margin-top: 12px; }
363
- .history-toggle { cursor: pointer; font-size: 12px; color: var(--text-muted); display: flex; align-items: center; gap: 4px; }
364
- .history-toggle:hover { color: var(--text); }
365
- .history-list { display: none; }
366
- .history-list.open { display: block; }
367
- .history-row { padding: 4px 8px; font-size: 12px; color: var(--text-muted); }
368
-
369
- /* Hierarchical session tree */
370
- .session-group { margin-bottom: 4px; }
371
-
372
- .session-header {
373
- display: flex;
374
- align-items: center;
375
- gap: 6px;
376
- padding: 6px 8px;
377
- border-radius: 4px;
378
- cursor: pointer;
379
- font-size: 13px;
380
- }
381
- .session-header:hover { background: var(--surface2); }
382
- .session-header.active { background: var(--surface2); border-left: 2px solid var(--blue); padding-left: 6px; }
383
-
384
- .session-toggle { font-size: 10px; color: var(--text-muted); width: 12px; flex-shrink: 0; }
385
-
386
- .session-children { padding-left: 16px; display: block; }
387
- .session-children.collapsed { display: none; }
388
-
389
- .child-group-label {
390
- font-size: 10px;
391
- font-weight: 700;
392
- text-transform: uppercase;
393
- letter-spacing: 0.5px;
394
- color: var(--text-muted);
395
- padding: 6px 8px 3px;
396
- }
397
-
398
- .child-row {
399
- display: flex;
400
- align-items: center;
401
- gap: 6px;
402
- padding: 4px 8px;
403
- border-radius: 4px;
404
- cursor: pointer;
405
- font-size: 12px;
406
- }
407
- .child-row:hover { background: var(--surface2); }
408
- .child-row.active { background: var(--surface2); border-left: 2px solid var(--blue); padding-left: 6px; }
409
-
410
- .connector {
411
- font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
412
- font-size: 11px;
413
- color: var(--text-muted);
414
- flex-shrink: 0;
415
- }
416
- .connector.solid { color: var(--green); }
417
- .connector.dashed { color: var(--yellow); }
418
-
419
- .unlinked-section { margin-top: 8px; border-top: 1px solid var(--border); padding-top: 8px; }
420
-
421
- /* Responsive */
422
- @media (max-width: 768px) {
423
- .main { flex-direction: column; }
424
- .sidebar { border-left: none; border-top: 1px solid var(--border); max-height: 40vh; }
425
- .header-stats { gap: 12px; font-size: 11px; }
426
- }
427
-
428
- /* Slide-over panel */
429
- .context-overlay { position:fixed; top:var(--header-h); left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:50; display:none; }
430
- .context-overlay.open { display:block; }
431
- .context-panel { position:fixed; top:var(--header-h); right:0; bottom:0; width:60%; background:var(--surface); border-left:2px solid var(--blue); z-index:51; display:none; overflow-y:auto; flex-direction:column; }
432
- .context-panel.open { display:flex; }
433
- .context-header { padding:12px 16px; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; flex-shrink:0; }
434
- .context-header h3 { font-size:14px; font-weight:600; }
435
- .context-close { cursor:pointer; color:var(--text-muted); font-size:18px; background:none; border:none; }
436
- .context-close:hover { color:var(--text); }
437
- .context-tabs { display:flex; gap:0; border-bottom:1px solid var(--border); flex-shrink:0; }
438
- .context-tab { padding:8px 16px; font-size:12px; cursor:pointer; border-bottom:2px solid transparent; color:var(--text-muted); background:none; border-top:none; border-left:none; border-right:none; }
439
- .context-tab.active { color:var(--blue); border-bottom-color:var(--blue); }
440
- .context-body { display:flex; flex:1; overflow:hidden; }
441
- .context-timeline { width:100px; border-right:1px solid var(--border); overflow-y:auto; padding:8px; flex-shrink:0; }
442
- .context-timeline-item { padding:4px 8px; font-size:11px; cursor:pointer; border-radius:3px; margin-bottom:4px; color:var(--text-muted); }
443
- .context-timeline-item:hover { background:var(--surface2); }
444
- .context-timeline-item.active { background:var(--surface2); color:var(--text); border-left:2px solid var(--blue); padding-left:6px; }
445
- .context-content { flex:1; overflow-y:auto; padding:16px; }
446
- .context-stats { padding:8px 16px; border-top:1px solid var(--border); font-size:12px; color:var(--text-muted); display:flex; gap:16px; flex-shrink:0; flex-wrap:wrap; }
447
-
448
- /* Message rows (Overview) */
449
- .msg-row { display:flex; align-items:center; gap:8px; padding:6px 8px; border-radius:4px; cursor:pointer; font-size:13px; border-bottom:1px solid var(--border); }
450
- .msg-row:hover { background:var(--surface2); }
451
- .msg-role { font-weight:600; width:50px; font-size:11px; text-transform:uppercase; flex-shrink:0; }
452
- .msg-role.user { color:var(--blue); }
453
- .msg-role.assistant { color:var(--green); }
454
- .msg-type { font-size:10px; padding:1px 5px; border-radius:3px; background:var(--surface2); color:var(--text-muted); flex-shrink:0; }
455
- .msg-tokens { font-size:11px; color:var(--text-muted); margin-left:auto; flex-shrink:0; }
456
- .msg-preview { font-size:12px; color:var(--text-muted); flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:300px; }
457
- .msg-markers { display:flex; gap:3px; flex-shrink:0; }
458
- .msg-marker { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
459
- .msg-marker.pruned { background:var(--red); }
460
- .msg-marker.injected { background:var(--green); }
461
-
462
- /* Diff highlights (Expanded/Cortex Only) */
463
- .diff-pruned { background:rgba(239,68,68,0.1); border-left:3px solid var(--red); padding:8px; margin:4px 0; font-family:monospace; font-size:12px; white-space:pre-wrap; word-break:break-word; }
464
- .diff-pruned-header { color:var(--red); font-size:11px; font-weight:600; margin-bottom:4px; }
465
- .diff-pruned .strikethrough { text-decoration:line-through; opacity:0.6; }
466
- .diff-injected { background:rgba(34,197,94,0.1); border-left:3px solid var(--green); padding:8px; margin:4px 0; font-family:monospace; font-size:12px; white-space:pre-wrap; word-break:break-word; }
467
- .diff-injected-header { color:var(--green); font-size:11px; font-weight:600; margin-bottom:4px; }
468
- .diff-expand { cursor:pointer; color:var(--blue); font-size:11px; padding:4px 0; display:block; }
469
- .diff-expand:hover { text-decoration:underline; }
470
- .msg-expanded { padding:8px; font-family:monospace; font-size:12px; white-space:pre-wrap; word-break:break-word; border-bottom:1px solid var(--border); }
471
- .msg-expanded-header { font-size:11px; font-weight:600; color:var(--text-muted); margin-bottom:4px; padding:4px 8px; background:var(--surface2); border-radius:3px; }
472
- .context-section-title { font-size:12px; font-weight:700; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.5px; margin:12px 0 6px; }
473
-
474
- /* Agent row context button */
475
- .agent-context-btn { font-size:10px; color:var(--blue); cursor:pointer; padding:1px 5px; border-radius:3px; border:1px solid var(--blue); background:none; flex-shrink:0; }
476
- .agent-context-btn:hover { background:rgba(59,130,246,0.1); }
477
- </style>
478
- </head>
479
- <body>
480
-
481
- <div class="header">
482
- <div class="header-title">Cortex Dashboard</div>
483
- <div class="header-stats">
484
- <div class="stat-item">
485
- <span class="stat-label">Uptime</span>
486
- <span class="stat-value" id="stat-uptime">0s</span>
487
- </div>
488
- <div class="stat-item">
489
- <span class="stat-label">Requests</span>
490
- <span class="stat-value" id="stat-requests">0</span>
491
- </div>
492
- <div class="stat-item">
493
- <span class="stat-label">Tokens saved</span>
494
- <span class="stat-value" id="stat-tokens">0</span>
495
- </div>
496
- <div class="stat-item">
497
- <span class="stat-label">Injection rate</span>
498
- <span class="stat-value" id="stat-injection">0%</span>
499
- </div>
500
- <div class="stat-item" id="agentsStat" style="display:none">
501
- <span class="stat-label">Agents</span>
502
- <span class="stat-value" id="agentsCount">0</span>
503
- </div>
504
- </div>
505
- </div>
506
-
507
- <div class="main">
508
- <div class="stream-panel" id="stream">
509
- <div class="empty-state" id="empty-state">Waiting for requests...</div>
510
- </div>
511
- <div class="sidebar">
512
- <div class="sidebar-section">
513
- <div class="section-title">Brain Directory</div>
514
- <div id="brain-content"><div class="sidebar-empty">Loading...</div></div>
515
- <div class="agent-section" id="agentSection" style="display:none">
516
- <h3>Active Agents</h3>
517
- <div id="agentList"></div>
518
- <div class="history-section">
519
- <div class="history-toggle" onclick="toggleHistory()">
520
- <span id="historyArrow">&#9658;</span> History (<span id="historyCount">0</span>)
521
- </div>
522
- <div class="history-list" id="historyList"></div>
523
- </div>
524
- </div>
525
- </div>
526
- <div class="sidebar-section">
527
- <div class="section-title">Features</div>
528
- <div id="features-list"><div class="sidebar-empty">Loading...</div></div>
529
- </div>
530
- </div>
531
- </div>
532
-
533
- <div class="context-overlay" id="contextOverlay" onclick="closeContextPanel()"></div>
534
- <div class="context-panel" id="contextPanel">
535
- <div class="context-header">
536
- <h3 id="contextTitle">Context Window</h3>
537
- <button class="context-close" onclick="closeContextPanel()">&times;</button>
538
- </div>
539
- <div class="context-tabs">
540
- <button class="context-tab active" id="ctxTabOverview" onclick="setViewLevel('overview', this)">Overview</button>
541
- <button class="context-tab" id="ctxTabExpanded" onclick="setViewLevel('expanded', this)">Expanded</button>
542
- <button class="context-tab" id="ctxTabCortex" onclick="setViewLevel('cortex', this)">Cortex Only</button>
543
- </div>
544
- <div class="context-body">
545
- <div class="context-timeline" id="contextTimeline"></div>
546
- <div class="context-content" id="contextContent"></div>
547
- </div>
548
- <div class="context-stats" id="contextStats"></div>
549
- </div>
550
-
551
- <script>
552
- (function() {
553
- // Authenticated fetch helper for API calls
554
- function apiFetch(url, options) {
555
- options = options || {};
556
- var headers = Object.assign({}, options.headers || {});
557
- if (window.CORTEX_API_TOKEN) {
558
- headers['Authorization'] = 'Bearer ' + window.CORTEX_API_TOKEN;
559
- }
560
- return fetch(url, Object.assign({}, options, { headers: headers }));
561
- }
562
-
563
- // State
564
- const requests = {};
565
- const MAX_CARDS = 100;
566
- const startTime = Date.now();
567
-
568
- // Agent tracking
569
- const agents = new Map(); // agentId agent info
570
- const requestAgentMap = new Map(); // requestId → { agentId, agentType }
571
- let agentFilter = null; // null = show all, string = filter by agentId
572
- let historyAgents = [];
573
-
574
- // DOM refs
575
- const streamEl = document.getElementById('stream');
576
- const emptyEl = document.getElementById('empty-state');
577
- const brainContentEl = document.getElementById('brain-content');
578
- const featuresListEl = document.getElementById('features-list');
579
- const statUptime = document.getElementById('stat-uptime');
580
- const statRequests = document.getElementById('stat-requests');
581
- const statTokens = document.getElementById('stat-tokens');
582
- const statInjection = document.getElementById('stat-injection');
583
-
584
- // Uptime timer (client-side, tracks time since page load)
585
- setInterval(function() {
586
- var s = Math.floor((Date.now() - startTime) / 1000);
587
- var h = Math.floor(s / 3600);
588
- var m = Math.floor((s % 3600) / 60);
589
- var sec = s % 60;
590
- statUptime.textContent = h > 0 ? h + 'h ' + m + 'm' : m > 0 ? m + 'm ' + sec + 's' : sec + 's';
591
- }, 1000);
592
-
593
- // Load initial session stats from /api/session/current
594
- // Shape: { requestCount, tokensSaved, injectionsCount, matchRate, avgLatencyMs }
595
- function loadSessionStats() {
596
- apiFetch('/api/session/current')
597
- .then(function(r) { return r.json(); })
598
- .then(function(data) {
599
- statRequests.textContent = data.requestCount;
600
- statTokens.textContent = formatNum(data.tokensSaved);
601
- statInjection.textContent = Math.round(data.matchRate * 100) + '%';
602
- })
603
- .catch(function() {});
604
- }
605
- loadSessionStats();
606
-
607
- // Load brain summary from /api/brain/summary
608
- // Shape: { totalEntries, bySource: Record<string, number>, recentMatches }
609
- function loadBrainSummary() {
610
- apiFetch('/api/brain/summary')
611
- .then(function(r) { return r.json(); })
612
- .then(function(data) {
613
- var html = '';
614
- html += '<div class="brain-summary-row">';
615
- html += '<span class="brain-summary-label">Total entries</span>';
616
- html += '<span class="brain-summary-value">' + data.totalEntries + '</span>';
617
- html += '</div>';
618
-
619
- var sources = Object.keys(data.bySource || {});
620
- if (sources.length > 0) {
621
- sources.forEach(function(src) {
622
- html += '<div class="brain-entry">';
623
- html += '<div class="brain-entry-title">' + esc(src) + '</div>';
624
- html += '<span class="badge badge-tag">' + data.bySource[src] + ' entries</span>';
625
- html += '</div>';
626
- });
627
- } else if (data.totalEntries === 0) {
628
- html += '<div class="sidebar-empty">No entries loaded</div>';
629
- }
630
-
631
- brainContentEl.innerHTML = html;
632
- })
633
- .catch(function() {
634
- brainContentEl.innerHTML = '<div class="sidebar-empty">Failed to load</div>';
635
- });
636
- }
637
- loadBrainSummary();
638
-
639
- // Load features from /api/features
640
- // Shape: { features: Array<{ name, enabled, reason? }> }
641
- function loadFeatures() {
642
- apiFetch('/api/features')
643
- .then(function(r) { return r.json(); })
644
- .then(function(data) {
645
- renderFeatures(data.features || []);
646
- })
647
- .catch(function() {
648
- featuresListEl.innerHTML = '<div class="sidebar-empty">Failed to load</div>';
649
- });
650
- }
651
-
652
- function renderFeatures(features) {
653
- if (!features.length) {
654
- featuresListEl.innerHTML = '<div class="sidebar-empty">No features</div>';
655
- return;
656
- }
657
-
658
- featuresListEl.innerHTML = '';
659
- features.forEach(function(feature) {
660
- var row = document.createElement('div');
661
- row.className = 'feature-row';
662
- row.setAttribute('data-feature', feature.name);
663
-
664
- var info = document.createElement('div');
665
- info.className = 'feature-info';
666
-
667
- var nameEl = document.createElement('div');
668
- nameEl.className = 'feature-name';
669
- nameEl.textContent = feature.name;
670
- info.appendChild(nameEl);
671
-
672
- if (feature.reason) {
673
- var reasonEl = document.createElement('div');
674
- reasonEl.className = 'feature-reason';
675
- reasonEl.textContent = feature.reason;
676
- info.appendChild(reasonEl);
677
- }
678
-
679
- var label = document.createElement('label');
680
- label.className = 'toggle';
681
-
682
- var input = document.createElement('input');
683
- input.type = 'checkbox';
684
- input.checked = feature.enabled;
685
- input.disabled = !!feature.reason;
686
-
687
- // Capture current input/row in closure for the event listener
688
- (function(inp, rw) {
689
- inp.addEventListener('change', function() {
690
- toggleFeature(feature.name, inp, rw);
691
- });
692
- })(input, row);
693
-
694
- var track = document.createElement('div');
695
- track.className = 'toggle-track';
696
-
697
- var thumb = document.createElement('div');
698
- thumb.className = 'toggle-thumb';
699
-
700
- label.appendChild(input);
701
- label.appendChild(track);
702
- label.appendChild(thumb);
703
-
704
- row.appendChild(info);
705
- row.appendChild(label);
706
- featuresListEl.appendChild(row);
707
- });
708
- }
709
-
710
- // POST /api/features/:name/toggle
711
- // Response shape: { name, enabled, reason? }
712
- function toggleFeature(name, inputEl, rowEl) {
713
- inputEl.disabled = true;
714
- apiFetch('/api/features/' + encodeURIComponent(name) + '/toggle', { method: 'POST' })
715
- .then(function(r) { return r.json(); })
716
- .then(function(updated) {
717
- inputEl.checked = updated.enabled;
718
- var infoEl = rowEl.querySelector('.feature-info');
719
- var existingReason = rowEl.querySelector('.feature-reason');
720
- if (updated.reason) {
721
- if (existingReason) {
722
- existingReason.textContent = updated.reason;
723
- } else {
724
- var reasonEl = document.createElement('div');
725
- reasonEl.className = 'feature-reason';
726
- reasonEl.textContent = updated.reason;
727
- infoEl.appendChild(reasonEl);
728
- }
729
- inputEl.disabled = true;
730
- } else {
731
- if (existingReason) existingReason.remove();
732
- inputEl.disabled = false;
733
- }
734
- })
735
- .catch(function() {
736
- // Revert optimistic toggle on error
737
- inputEl.checked = !inputEl.checked;
738
- inputEl.disabled = false;
739
- });
740
- }
741
- loadFeatures();
742
-
743
- // Agent functions — topology-based session tree
744
- async function renderSessionTree() {
745
- try {
746
- var topoRes = await apiFetch('/api/agents/topology');
747
- if (!topoRes.ok) return; // graceful 404 — tracking may be disabled
748
- var topo = await topoRes.json();
749
- if (!topo.enabled) return;
750
-
751
- document.getElementById('agentSection').style.display = '';
752
- document.getElementById('agentsStat').style.display = '';
753
-
754
- // Sync agents map from topology so card tagging and context panel still work
755
- if (topo.version === 2 && Array.isArray(topo.sessions)) {
756
- topo.sessions.forEach(function(session) {
757
- agents.set(session.agent.conversationId, session.agent);
758
- (session.teams || []).forEach(function(team) {
759
- (team.members || []).forEach(function(m) { agents.set(m.agent.conversationId, m.agent); });
760
- });
761
- (session.subagents || []).forEach(function(s) { agents.set(s.agent.conversationId, s.agent); });
762
- });
763
- (topo.unlinked || []).forEach(function(a) { agents.set(a.conversationId, a); });
764
- }
765
-
766
- var list = document.getElementById('agentList');
767
- list.innerHTML = '';
768
-
769
- if (topo.version === 2 && Array.isArray(topo.sessions)) {
770
- // Hierarchical tree rendering
771
- topo.sessions.forEach(function(session) {
772
- list.appendChild(buildSessionGroup(session));
773
- });
774
-
775
- // Unlinked section
776
- if (topo.unlinked && topo.unlinked.length > 0) {
777
- var unlinkedSection = document.createElement('div');
778
- unlinkedSection.className = 'unlinked-section';
779
- var label = document.createElement('div');
780
- label.className = 'child-group-label';
781
- label.textContent = 'Unlinked';
782
- unlinkedSection.appendChild(label);
783
- topo.unlinked.forEach(function(agent) {
784
- unlinkedSection.appendChild(buildChildRow(agent, null));
785
- });
786
- list.appendChild(unlinkedSection);
787
- }
788
- } else {
789
- // Fallback: flat list from legacy /api/agents
790
- agents.forEach(function(agent, id) {
791
- list.appendChild(buildFlatAgentRow(agent, id));
792
- });
793
- }
794
-
795
- // "Show all" button when filtered
796
- if (agentFilter) {
797
- var allBtn = document.createElement('div');
798
- allBtn.className = 'agent-row';
799
- allBtn.style.cssText = 'color:var(--blue);margin-top:4px';
800
- allBtn.textContent = '\u2190 Show all';
801
- allBtn.onclick = function() { filterByAgent(null); };
802
- list.insertBefore(allBtn, list.firstChild);
803
- }
804
-
805
- var total = agents.size + historyAgents.length;
806
- document.getElementById('agentsCount').textContent = agents.size + '/' + total;
807
- } catch(_) {}
808
- }
809
-
810
- function buildSessionGroup(session) {
811
- var agent = session.agent;
812
- var id = agent.conversationId;
813
- var hasChildren = (session.teams && session.teams.length > 0) ||
814
- (session.subagents && session.subagents.length > 0);
815
-
816
- var group = document.createElement('div');
817
- group.className = 'session-group';
818
- group.dataset.sessionId = id;
819
-
820
- var header = document.createElement('div');
821
- header.className = 'session-header' + (agentFilter === id ? ' active' : '');
822
-
823
- var toggleSpan = document.createElement('span');
824
- toggleSpan.className = 'session-toggle';
825
- toggleSpan.textContent = hasChildren ? '\u25bc' : '';
826
- header.appendChild(toggleSpan);
827
-
828
- var badge = document.createElement('span');
829
- badge.className = 'agent-badge badge-main';
830
- badge.textContent = 'main';
831
- header.appendChild(badge);
832
-
833
- var modelShort = agent.model ? agent.model.split('-').pop() : '';
834
- if (modelShort) {
835
- var modelSpan = document.createElement('span');
836
- modelSpan.className = 'agent-model';
837
- modelSpan.textContent = modelShort;
838
- header.appendChild(modelSpan);
839
- }
840
-
841
- var reqSpan = document.createElement('span');
842
- reqSpan.className = 'agent-requests';
843
- reqSpan.textContent = (agent.requestCount || 0) + ' req';
844
- header.appendChild(reqSpan);
845
-
846
- var ctxBtn = document.createElement('button');
847
- ctxBtn.className = 'agent-context-btn';
848
- ctxBtn.textContent = 'view';
849
- ctxBtn.onclick = function(e) { e.stopPropagation(); openContextPanel(id); };
850
- header.appendChild(ctxBtn);
851
-
852
- header.onclick = function(e) {
853
- if (e.target === ctxBtn) return;
854
- if (hasChildren) {
855
- var childrenEl = group.querySelector('.session-children');
856
- var isCollapsed = childrenEl && childrenEl.classList.contains('collapsed');
857
- if (childrenEl) childrenEl.classList.toggle('collapsed');
858
- toggleSpan.textContent = isCollapsed ? '\u25bc' : '\u25b6';
859
- }
860
- filterBySession(id, session);
861
- };
862
-
863
- group.appendChild(header);
864
-
865
- if (hasChildren) {
866
- var children = document.createElement('div');
867
- children.className = 'session-children';
868
-
869
- // Team children
870
- (session.teams || []).forEach(function(team) {
871
- if (!team.members || team.members.length === 0) return;
872
- var teamLabel = document.createElement('div');
873
- teamLabel.className = 'child-group-label';
874
- teamLabel.textContent = 'Team: ' + team.name;
875
- children.appendChild(teamLabel);
876
- team.members.forEach(function(m) {
877
- children.appendChild(buildChildRow(m.agent, m.parentLink));
878
- });
879
- });
880
-
881
- // Subagent children
882
- if (session.subagents && session.subagents.length > 0) {
883
- var subLabel = document.createElement('div');
884
- subLabel.className = 'child-group-label';
885
- subLabel.textContent = 'Subagents';
886
- children.appendChild(subLabel);
887
- session.subagents.forEach(function(s) {
888
- children.appendChild(buildChildRow(s.agent, s.parentLink));
889
- });
890
- }
891
-
892
- group.appendChild(children);
893
- }
894
-
895
- return group;
896
- }
897
-
898
- function buildChildRow(agent, parentLink) {
899
- var id = agent.conversationId;
900
- var row = document.createElement('div');
901
- row.className = 'child-row' + (agentFilter === id ? ' active' : '');
902
- row.dataset.agentId = id;
903
-
904
- var connText = '\u2500\u2500'; // solid ──
905
- var connClass = 'solid';
906
- var connTitle = '';
907
- if (parentLink) {
908
- if (parentLink.confidence === 'inferred') {
909
- connText = '\u254c\u254c'; // dashed ╌╌
910
- connClass = 'dashed';
911
- }
912
- if (parentLink.method === 'team-config') connTitle = 'Linked via team config (confirmed)';
913
- else if (parentLink.method === 'temporal' && parentLink.confidence === 'high') connTitle = 'Linked via temporal correlation (high confidence)';
914
- else if (parentLink.method === 'content-match') connTitle = 'Linked via content matching (confirmed)';
915
- else if (parentLink.method === 'temporal' && parentLink.confidence === 'inferred') connTitle = 'Linked via temporal fallback (inferred)';
916
- else if (parentLink.method === 'direct') connTitle = 'Linked via direct assignment (confirmed)';
917
- }
918
-
919
- var connSpan = document.createElement('span');
920
- connSpan.className = 'connector ' + connClass;
921
- connSpan.textContent = connText;
922
- if (connTitle) connSpan.title = connTitle;
923
- row.appendChild(connSpan);
924
-
925
- var badge = document.createElement('span');
926
- badge.className = 'agent-badge badge-' + escClass(agent.type);
927
- badge.textContent = agent.teamRole || agent.type || 'unknown';
928
- row.appendChild(badge);
929
-
930
- var modelShort = agent.model ? agent.model.split('-').pop() : '';
931
- if (modelShort) {
932
- var modelSpan = document.createElement('span');
933
- modelSpan.className = 'agent-model';
934
- modelSpan.textContent = modelShort;
935
- row.appendChild(modelSpan);
936
- }
937
-
938
- var reqSpan = document.createElement('span');
939
- reqSpan.className = 'agent-requests';
940
- reqSpan.textContent = (agent.requestCount || 0) + ' req';
941
- row.appendChild(reqSpan);
942
-
943
- var ctxBtn = document.createElement('button');
944
- ctxBtn.className = 'agent-context-btn';
945
- ctxBtn.textContent = 'view';
946
- ctxBtn.onclick = function(e) { e.stopPropagation(); openContextPanel(id); };
947
- row.appendChild(ctxBtn);
948
-
949
- row.onclick = function(e) { if (e.target !== ctxBtn) filterByAgent(id); };
950
-
951
- return row;
952
- }
953
-
954
- function buildFlatAgentRow(agent, id) {
955
- var row = document.createElement('div');
956
- row.className = 'agent-row' + (agentFilter === id ? ' active' : '');
957
- var modelShort = agent.model ? agent.model.split('-').pop() : '';
958
-
959
- var badge = document.createElement('span');
960
- badge.className = 'agent-badge badge-' + escClass(agent.type);
961
- badge.textContent = agent.type || 'unknown';
962
- row.appendChild(badge);
963
-
964
- var modelSpan = document.createElement('span');
965
- modelSpan.className = 'agent-model';
966
- modelSpan.textContent = modelShort;
967
- row.appendChild(modelSpan);
968
-
969
- var reqSpan = document.createElement('span');
970
- reqSpan.className = 'agent-requests';
971
- reqSpan.textContent = (agent.requestCount || 0) + ' req';
972
- row.appendChild(reqSpan);
973
-
974
- var ctxBtn = document.createElement('button');
975
- ctxBtn.className = 'agent-context-btn';
976
- ctxBtn.textContent = 'view';
977
- ctxBtn.onclick = function(e) { e.stopPropagation(); openContextPanel(id); };
978
- row.appendChild(ctxBtn);
979
-
980
- row.onclick = function(e) { if (e.target === row) filterByAgent(id); };
981
- return row;
982
- }
983
-
984
- // Filter cards to a session (main + all its children) or a specific child
985
- function filterBySession(mainId, session) {
986
- var ids = new Set([mainId]);
987
- (session.teams || []).forEach(function(t) {
988
- (t.members || []).forEach(function(m) { ids.add(m.agent.conversationId); });
989
- });
990
- (session.subagents || []).forEach(function(s) { ids.add(s.agent.conversationId); });
991
-
992
- agentFilter = mainId;
993
- document.querySelectorAll('.card').forEach(function(card) {
994
- card.style.display = ids.has(card.dataset.agentId) ? '' : 'none';
995
- });
996
- renderSessionTree();
997
- }
998
-
999
- function renderHistory() {
1000
- var list = document.getElementById('historyList');
1001
- var count = document.getElementById('historyCount');
1002
- count.textContent = historyAgents.length;
1003
- list.innerHTML = '';
1004
- historyAgents.forEach(function(h) {
1005
- var row = document.createElement('div');
1006
- row.className = 'history-row';
1007
- var dur = Math.round((h.duration || 0) / 1000);
1008
- var type = (h.info && h.info.type) ? h.info.type : 'unknown';
1009
- row.innerHTML =
1010
- '<span class="agent-badge badge-' + escClass(type) + '">' + esc(type) + '</span> ' +
1011
- dur + 's \u00b7 ' + (h.totalRequests || 0) + ' req \u00b7 ' + esc(h.completionReason || '');
1012
- list.appendChild(row);
1013
- });
1014
- }
1015
-
1016
- function toggleHistory() {
1017
- var list = document.getElementById('historyList');
1018
- var arrow = document.getElementById('historyArrow');
1019
- list.classList.toggle('open');
1020
- arrow.textContent = list.classList.contains('open') ? '\u25bc' : '\u25ba';
1021
- }
1022
-
1023
- function filterByAgent(id) {
1024
- agentFilter = id;
1025
- renderSessionTree();
1026
- document.querySelectorAll('.card').forEach(function(card) {
1027
- if (!id) { card.style.display = ''; return; }
1028
- card.style.display = card.dataset.agentId === id ? '' : 'none';
1029
- });
1030
- }
1031
-
1032
- async function loadAgents() {
1033
- try {
1034
- var histRes = await apiFetch('/api/agents/history');
1035
- var histData = await histRes.json();
1036
- historyAgents = histData.history || [];
1037
- renderHistory();
1038
- } catch(_) {}
1039
- renderSessionTree();
1040
- }
1041
-
1042
- loadAgents();
1043
-
1044
- // SSE — connect to /api/pipeline/live with token auth via query param
1045
- var sseUrl = '/api/pipeline/live?token=' + encodeURIComponent(window.CORTEX_API_TOKEN || '');
1046
- var eventSource = new EventSource(sseUrl);
1047
-
1048
- eventSource.onmessage = function(e) {
1049
- var event;
1050
- try { event = JSON.parse(e.data); } catch(_) { return; }
1051
- if (event.type === 'ping') return;
1052
-
1053
- if (event.type === 'agent:detected') {
1054
- // Update local agents map for request-to-agent mapping
1055
- var existing = agents.get(event.agentId);
1056
- agents.set(event.agentId, {
1057
- conversationId: event.agentId,
1058
- type: event.agentType,
1059
- model: event.model,
1060
- toolCount: event.toolCount,
1061
- teamName: event.teamName,
1062
- teamRole: event.teamRole,
1063
- requestCount: existing ? (existing.requestCount || 0) + 1 : 1,
1064
- });
1065
- // Map this request to the agent for card tagging
1066
- if (event.requestId !== undefined) {
1067
- requestAgentMap.set(event.requestId, { agentId: event.agentId, agentType: event.agentType });
1068
- }
1069
- document.getElementById('agentSection').style.display = '';
1070
- document.getElementById('agentsStat').style.display = '';
1071
- renderSessionTree();
1072
- return;
1073
- }
1074
-
1075
- if (event.type === 'agent:completed') {
1076
- agents.delete(event.agentId);
1077
- historyAgents.unshift({
1078
- info: { type: event.agentType, conversationId: event.agentId },
1079
- totalRequests: event.totalRequests,
1080
- duration: event.duration,
1081
- completionReason: event.completionReason,
1082
- });
1083
- renderSessionTree();
1084
- renderHistory();
1085
- return;
1086
- }
1087
-
1088
- var rid = event.requestId;
1089
- if (rid === undefined) return;
1090
-
1091
- if (!requests[rid]) {
1092
- requests[rid] = { events: [] };
1093
- }
1094
- requests[rid].events.push(event);
1095
-
1096
- // Map request to agent from request:start if available
1097
- if (event.type === 'request:start' && event.agentId && !requestAgentMap.has(rid)) {
1098
- requestAgentMap.set(rid, { agentId: event.agentId, agentType: event.agentType || 'unknown' });
1099
- }
1100
-
1101
- if (event.type === 'request:end') {
1102
- // Refresh header stats from server after each completed request
1103
- loadSessionStats();
1104
- renderCard(rid);
1105
- }
1106
- if (event.type === 'pipeline:error') {
1107
- renderCard(rid);
1108
- }
1109
- };
1110
-
1111
- function renderCard(rid) {
1112
- var data = requests[rid];
1113
- if (!data) return;
1114
-
1115
- var evts = data.events;
1116
- var startEvt = find(evts, 'request:start');
1117
- var pruneEvt = find(evts, 'prune:complete');
1118
- var fpEvt = find(evts, 'fingerprint:complete');
1119
- var matchEvt = find(evts, 'match:complete');
1120
- var injectEvt = find(evts, 'inject:complete');
1121
- var endEvt = find(evts, 'request:end');
1122
- var errEvt = find(evts, 'pipeline:error');
1123
-
1124
- // Determine status
1125
- var status = 'passthrough';
1126
- if (errEvt) status = 'error';
1127
- else if (injectEvt && injectEvt.injected) status = 'injected';
1128
- else if (pruneEvt && pruneEvt.tokensSaved > 0) status = 'pruned';
1129
-
1130
- // Look up agent info for this request
1131
- var agentInfo = requestAgentMap.get(rid);
1132
- var agentId = agentInfo ? agentInfo.agentId : '';
1133
- var agentType = agentInfo ? agentInfo.agentType : '';
1134
-
1135
- // Build card HTML
1136
- var html = '<div class="card-header">';
1137
- html += '<span class="card-id">Request #' + rid + '</span>';
1138
- if (agentId) {
1139
- html += ' <span class="agent-badge badge-' + escClass(agentType) + '">' + esc(agentType) + '</span>';
1140
- }
1141
- html += '<span class="card-meta">';
1142
- if (startEvt) html += formatTime(startEvt.timestamp);
1143
- if (endEvt) html += ' &middot; ' + endEvt.totalTimeMs + 'ms';
1144
- html += '</span></div>';
1145
-
1146
- if (pruneEvt) {
1147
- html += line('Prune',
1148
- pruneEvt.tokensBefore + ' &rarr; ' + pruneEvt.tokensAfter +
1149
- ' <span class="' + (pruneEvt.tokensSaved > 0 ? 'green' : '') + '">(saved ' + pruneEvt.tokensSaved + ')</span>');
1150
- }
1151
-
1152
- if (fpEvt) {
1153
- html += line('Fingerprint',
1154
- '<span class="value">' + esc(fpEvt.languages.join(', ')) + '</span>' +
1155
- (fpEvt.errorTypes.length ? ' &middot; ' + esc(fpEvt.errorTypes.join(', ')) : '') +
1156
- ' &middot; ' + fpEvt.phase);
1157
- }
1158
-
1159
- if (matchEvt && matchEvt.topMatches.length) {
1160
- var matches = matchEvt.topMatches.slice(0, 3).map(function(m) {
1161
- return esc(m.title) + ' (' + m.score.toFixed(2) + ')';
1162
- }).join(', ');
1163
- html += line('Match', '<span class="value">' + matches + '</span>');
1164
- }
1165
-
1166
- if (injectEvt) {
1167
- if (injectEvt.injected) {
1168
- html += line('Inject',
1169
- '<span class="green">&#10003; ' + esc(injectEvt.entryTitle || '') +
1170
- ' (' + (injectEvt.confidence ? injectEvt.confidence.toFixed(2) : '?') + ')</span>');
1171
- } else {
1172
- html += line('Inject', '<span class="red">&#10007; no injection</span>');
1173
- }
1174
- }
1175
-
1176
- if (errEvt) {
1177
- html += line('Error', '<span class="red">[' + esc(errEvt.stage) + '] ' + esc(errEvt.error) + '</span>');
1178
- }
1179
-
1180
- if (endEvt && endEvt.reason) {
1181
- html += line('Reason', '<span class="value">' + esc(endEvt.reason) + '</span>');
1182
- }
1183
-
1184
- var card = document.createElement('div');
1185
- card.className = 'card status-' + status;
1186
- card.setAttribute('data-rid', rid);
1187
- card.dataset.agentId = agentId;
1188
- card.innerHTML = html;
1189
-
1190
- if (emptyEl) emptyEl.style.display = 'none';
1191
-
1192
- var existing = streamEl.querySelector('[data-rid="' + rid + '"]');
1193
- if (existing) existing.remove();
1194
-
1195
- streamEl.insertBefore(card, streamEl.firstChild);
1196
-
1197
- while (streamEl.children.length > MAX_CARDS + 1) {
1198
- streamEl.removeChild(streamEl.lastChild);
1199
- }
1200
- }
1201
-
1202
- // Helpers
1203
- function find(arr, type) {
1204
- for (var i = arr.length - 1; i >= 0; i--) {
1205
- if (arr[i].type === type) return arr[i];
1206
- }
1207
- return null;
1208
- }
1209
-
1210
- function line(label, content) {
1211
- return '<div class="card-line"><span class="label">' + label + '</span>' + content + '</div>';
1212
- }
1213
-
1214
- function esc(s) {
1215
- if (!s) return '';
1216
- var d = document.createElement('div');
1217
- d.textContent = s;
1218
- return d.innerHTML;
1219
- }
1220
-
1221
- function escClass(s) {
1222
- if (!s) return 'unknown';
1223
- return s.replace(/[^a-zA-Z0-9-_]/g, '');
1224
- }
1225
-
1226
- function formatTime(ts) {
1227
- var d = new Date(ts);
1228
- return d.toLocaleTimeString();
1229
- }
1230
-
1231
- function formatNum(n) {
1232
- if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
1233
- if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
1234
- return '' + n;
1235
- }
1236
-
1237
- // ── Context Window Panel ──────────────────────────────────────────────────
1238
-
1239
- var contextAgentId = null;
1240
- var contextSnapshot = null;
1241
- var contextViewLevel = 'overview';
1242
-
1243
- function openContextPanel(agentId) {
1244
- contextAgentId = agentId;
1245
- var agent = agents.get(agentId);
1246
- var label = agent
1247
- ? (agent.type || 'agent') + (agent.teamRole ? ' (' + agent.teamRole + ')' : '')
1248
- : agentId.slice(0, 8);
1249
- document.getElementById('contextTitle').textContent = 'Context: ' + label;
1250
- document.getElementById('contextOverlay').classList.add('open');
1251
- document.getElementById('contextPanel').classList.add('open');
1252
- document.getElementById('contextContent').innerHTML = '<p style="color:var(--text-muted);font-size:13px">Loading...</p>';
1253
- document.getElementById('contextStats').innerHTML = '';
1254
- document.getElementById('contextTimeline').innerHTML = '';
1255
- // Reset to overview tab
1256
- setViewLevel('overview', document.getElementById('ctxTabOverview'));
1257
- loadRequestTimeline(agentId);
1258
- document.addEventListener('keydown', contextEscHandler);
1259
- }
1260
-
1261
- function closeContextPanel() {
1262
- document.getElementById('contextOverlay').classList.remove('open');
1263
- document.getElementById('contextPanel').classList.remove('open');
1264
- document.removeEventListener('keydown', contextEscHandler);
1265
- contextAgentId = null;
1266
- contextSnapshot = null;
1267
- }
1268
-
1269
- function contextEscHandler(e) {
1270
- if (e.key === 'Escape') closeContextPanel();
1271
- }
1272
-
1273
- async function loadRequestTimeline(agentId) {
1274
- try {
1275
- var res = await apiFetch('/api/agents/' + encodeURIComponent(agentId) + '/requests');
1276
- if (!res.ok) throw new Error('status ' + res.status);
1277
- var data = await res.json();
1278
- renderTimeline(data.requests || [], data.capped);
1279
- if (data.requests && data.requests.length > 0) {
1280
- // Load the most recent request
1281
- loadSnapshot(agentId, data.requests[data.requests.length - 1].requestId);
1282
- } else {
1283
- document.getElementById('contextContent').innerHTML = '<p style="color:var(--text-muted);font-size:13px">No requests captured yet.</p>';
1284
- }
1285
- } catch(e) {
1286
- document.getElementById('contextContent').innerHTML = '<p style="color:var(--red);font-size:13px">Failed to load request history.</p>';
1287
- }
1288
- }
1289
-
1290
- async function loadSnapshot(agentId, requestId) {
1291
- document.getElementById('contextContent').innerHTML = '<p style="color:var(--text-muted);font-size:13px">Loading...</p>';
1292
- try {
1293
- var res = await apiFetch('/api/agents/' + encodeURIComponent(agentId) + '/requests/' + requestId);
1294
- if (!res.ok) throw new Error('status ' + res.status);
1295
- var data = await res.json();
1296
- contextSnapshot = data.snapshot;
1297
- renderViewLevel();
1298
- renderContextStats();
1299
- // Highlight active timeline item
1300
- document.querySelectorAll('.context-timeline-item').forEach(function(el) {
1301
- el.classList.toggle('active', parseInt(el.dataset.requestId, 10) === requestId);
1302
- });
1303
- } catch(e) {
1304
- document.getElementById('contextContent').innerHTML = '<p style="color:var(--red);font-size:13px">Failed to load snapshot.</p>';
1305
- }
1306
- }
1307
-
1308
- function renderTimeline(requests, capped) {
1309
- var el = document.getElementById('contextTimeline');
1310
- el.innerHTML = '';
1311
- // Newest first
1312
- requests.slice().reverse().forEach(function(r) {
1313
- var item = document.createElement('div');
1314
- item.className = 'context-timeline-item';
1315
- item.dataset.requestId = r.requestId;
1316
- var ago = Math.round((Date.now() - r.timestamp) / 1000);
1317
- var agoStr = ago < 60 ? ago + 's' : Math.round(ago / 60) + 'm';
1318
- item.textContent = '#' + r.requestId + ' ' + agoStr;
1319
- item.title = r.messageCount + ' msgs, ' + r.tokensBefore + ' tok';
1320
- (function(rid) {
1321
- item.onclick = function() { loadSnapshot(contextAgentId, rid); };
1322
- })(r.requestId);
1323
- el.appendChild(item);
1324
- });
1325
- if (capped) {
1326
- var btn = document.createElement('div');
1327
- btn.className = 'context-timeline-item';
1328
- btn.style.color = 'var(--blue)';
1329
- btn.style.marginTop = '8px';
1330
- btn.style.borderTop = '1px solid var(--border)';
1331
- btn.style.paddingTop = '8px';
1332
- btn.textContent = 'Keep all future';
1333
- btn.onclick = function() {
1334
- if (!contextAgentId) return;
1335
- apiFetch('/api/agents/' + encodeURIComponent(contextAgentId) + '/uncap-history', { method: 'POST' })
1336
- .then(function() {
1337
- btn.textContent = 'Uncapped';
1338
- btn.style.color = 'var(--green)';
1339
- btn.onclick = null;
1340
- })
1341
- .catch(function() {});
1342
- };
1343
- el.appendChild(btn);
1344
- }
1345
- }
1346
-
1347
- function setViewLevel(level, tabEl) {
1348
- contextViewLevel = level;
1349
- document.querySelectorAll('.context-tab').forEach(function(t) { t.classList.remove('active'); });
1350
- if (tabEl) tabEl.classList.add('active');
1351
- renderViewLevel();
1352
- }
1353
-
1354
- function renderViewLevel() {
1355
- if (!contextSnapshot) return;
1356
- var el = document.getElementById('contextContent');
1357
- if (contextViewLevel === 'overview') {
1358
- el.innerHTML = renderOverview(contextSnapshot);
1359
- } else if (contextViewLevel === 'expanded') {
1360
- el.innerHTML = renderExpanded(contextSnapshot);
1361
- } else {
1362
- el.innerHTML = renderCortexOnly(contextSnapshot);
1363
- }
1364
- }
1365
-
1366
- // Level A — compact message table
1367
- function renderOverview(snap) {
1368
- var ops = snap.operations || [];
1369
- var prunedIndexes = new Set();
1370
- var injectedIndexes = new Set();
1371
- ops.forEach(function(op) {
1372
- if (op.type === 'prune') prunedIndexes.add(op.messageIndex);
1373
- if (op.type === 'inject') injectedIndexes.add(op.insertedAtIndex);
1374
- });
1375
-
1376
- var html = '';
1377
- if (snap.systemPromptPreview) {
1378
- html += '<div class="msg-expanded-header">System: ' + esc(snap.systemPromptPreview.slice(0, 80)) + (snap.systemPromptPreview.length > 80 ? '...' : '') + '</div>';
1379
- }
1380
- (snap.messagesSummary || []).forEach(function(msg) {
1381
- var markers = '';
1382
- if (prunedIndexes.has(msg.index)) markers += '<span class="msg-marker pruned" title="Pruned"></span>';
1383
- if (injectedIndexes.has(msg.index)) markers += '<span class="msg-marker injected" title="Injected before this"></span>';
1384
-
1385
- html += '<div class="msg-row" onclick="setViewLevel(\'expanded\', document.getElementById(\'ctxTabExpanded\'))">';
1386
- html += '<span class="msg-role ' + escClass(msg.role) + '">' + esc(msg.role) + '</span>';
1387
- html += '<span class="msg-type">' + esc(msg.contentType) + '</span>';
1388
- html += '<span class="msg-preview">' + esc(msg.preview || '') + '</span>';
1389
- html += '<span class="msg-tokens">' + msg.tokenCount + ' tok</span>';
1390
- if (markers) html += '<span class="msg-markers">' + markers + '</span>';
1391
- html += '</div>';
1392
- });
1393
- if (!snap.messagesSummary || snap.messagesSummary.length === 0) {
1394
- html += '<p style="color:var(--text-muted);font-size:13px">No messages.</p>';
1395
- }
1396
- return html;
1397
- }
1398
-
1399
- // Level B — full expanded messages with diff highlights
1400
- function renderExpanded(snap) {
1401
- var ops = snap.operations || [];
1402
- var prunesByMsg = {};
1403
- var injectOp = null;
1404
- ops.forEach(function(op) {
1405
- if (op.type === 'prune') {
1406
- if (!prunesByMsg[op.messageIndex]) prunesByMsg[op.messageIndex] = [];
1407
- prunesByMsg[op.messageIndex].push(op);
1408
- }
1409
- if (op.type === 'inject') injectOp = op;
1410
- });
1411
-
1412
- var html = '';
1413
- if (snap.systemPromptPreview) {
1414
- html += '<div class="msg-expanded-header">System: ' + esc(snap.systemPromptPreview) + '</div>';
1415
- }
1416
-
1417
- var msgs = snap.originalMessages || [];
1418
- msgs.forEach(function(msg, i) {
1419
- html += '<div class="msg-expanded-header">[' + esc(msg.role) + '] message ' + i + '</div>';
1420
-
1421
- // Show injected content if it was inserted before this message
1422
- if (injectOp && injectOp.insertedAtIndex === i) {
1423
- html += '<div class="diff-injected">';
1424
- html += '<div class="diff-injected-header">+ INJECTED: ' + esc(injectOp.brainEntryTitle) + ' (confidence: ' + (injectOp.confidence || 0).toFixed(2) + ')</div>';
1425
- html += esc(injectOp.content.slice(0, 500)) + (injectOp.content.length > 500 ? '\n...' : '');
1426
- html += '</div>';
1427
- }
1428
-
1429
- var msgPruneOps = prunesByMsg[i] || [];
1430
- var content = msg.content;
1431
-
1432
- if (typeof content === 'string') {
1433
- var pruned = msgPruneOps[0];
1434
- if (pruned) {
1435
- html += renderPrunedBlock(pruned);
1436
- } else {
1437
- html += '<div class="msg-expanded">' + esc(content.slice(0, 2000)) + (content.length > 2000 ? '\n...[truncated]' : '') + '</div>';
1438
- }
1439
- } else if (Array.isArray(content)) {
1440
- content.forEach(function(block, bi) {
1441
- var blockPrune = msgPruneOps.find(function(p) { return p.blockIndex === bi; });
1442
- html += renderContentBlock(block, blockPrune);
1443
- });
1444
- }
1445
- });
1446
-
1447
- // Show injected content at end if insertedAtIndex >= msgs.length
1448
- if (injectOp && injectOp.insertedAtIndex >= msgs.length) {
1449
- html += '<div class="diff-injected">';
1450
- html += '<div class="diff-injected-header">+ INJECTED (appended): ' + esc(injectOp.brainEntryTitle) + ' (confidence: ' + (injectOp.confidence || 0).toFixed(2) + ')</div>';
1451
- html += esc(injectOp.content.slice(0, 500)) + (injectOp.content.length > 500 ? '\n...' : '');
1452
- html += '</div>';
1453
- }
1454
-
1455
- if (msgs.length === 0) html += '<p style="color:var(--text-muted);font-size:13px">No messages stored.</p>';
1456
- return html;
1457
- }
1458
-
1459
- function renderContentBlock(block, pruneOp) {
1460
- var html = '';
1461
- if (block.type === 'text') {
1462
- if (pruneOp) {
1463
- html += renderPrunedBlock(pruneOp);
1464
- } else {
1465
- var txt = block.text || '';
1466
- html += '<div class="msg-expanded">' + esc(txt.slice(0, 2000)) + (txt.length > 2000 ? '\n...[truncated]' : '') + '</div>';
1467
- }
1468
- } else if (block.type === 'tool_use') {
1469
- html += '<div class="msg-expanded"><span style="color:var(--yellow)">[tool_use] ' + esc(block.name || '') + '</span>';
1470
- if (block.input) {
1471
- var inp = JSON.stringify(block.input, null, 2);
1472
- html += '\n' + esc(inp.slice(0, 500)) + (inp.length > 500 ? '\n...' : '');
1473
- }
1474
- html += '</div>';
1475
- } else if (block.type === 'tool_result') {
1476
- html += '<div class="msg-expanded"><span style="color:var(--text-muted)">[tool_result]</span>\n';
1477
- if (typeof block.content === 'string') {
1478
- if (pruneOp) {
1479
- html += renderPrunedBlock(pruneOp);
1480
- } else {
1481
- html += esc(block.content.slice(0, 1000)) + (block.content.length > 1000 ? '\n...' : '');
1482
- }
1483
- } else if (Array.isArray(block.content)) {
1484
- (block.content || []).forEach(function(sub, si) {
1485
- var subPrune = pruneOp && pruneOp.subBlockIndex === si ? pruneOp : null;
1486
- if (sub.type === 'text') {
1487
- if (subPrune) {
1488
- html += renderPrunedBlock(subPrune);
1489
- } else {
1490
- html += esc((sub.text || '').slice(0, 500)) + ((sub.text || '').length > 500 ? '\n...' : '');
1491
- }
1492
- }
1493
- });
1494
- }
1495
- html += '</div>';
1496
- } else if (block.type === 'image') {
1497
- html += '<div class="msg-expanded" style="color:var(--text-muted)">[image: ' + esc((block.source && block.source.media_type) || 'unknown') + ']</div>';
1498
- } else if (block.type === 'thinking') {
1499
- html += '<div class="msg-expanded" style="color:var(--text-muted)">[thinking block — ' + ((block.thinking || '').length) + ' chars]</div>';
1500
- } else {
1501
- html += '<div class="msg-expanded" style="color:var(--text-muted)">[' + esc(block.type || 'unknown') + ']</div>';
1502
- }
1503
- return html;
1504
- }
1505
-
1506
- function renderPrunedBlock(op) {
1507
- var lines = (op.originalContent || '').split('\n');
1508
- var preview = lines.slice(0, 3).join('\n');
1509
- var remaining = lines.length - 3;
1510
- var id = 'prune-' + op.messageIndex + '-' + op.blockIndex + (op.subBlockIndex !== undefined ? '-' + op.subBlockIndex : '');
1511
- var html = '<div class="diff-pruned">';
1512
- html += '<div class="diff-pruned-header">- PRUNED: ' + op.tokensBefore + ' tok &rarr; ' + op.tokensAfter + ' tok (saved ' + (op.tokensBefore - op.tokensAfter) + ')</div>';
1513
- html += '<span class="strikethrough">' + esc(preview) + '</span>';
1514
- if (remaining > 0) {
1515
- html += '\n<span class="diff-expand" onclick="togglePruneExpand(\'' + id + '\')">Show ' + remaining + ' more pruned lines</span>';
1516
- html += '<span id="' + id + '" style="display:none"><span class="strikethrough">' + esc(lines.slice(3).join('\n')) + '</span></span>';
1517
- }
1518
- html += '\n<span style="color:var(--green);font-size:11px">+ ' + esc(op.prunedContent || '[removed]') + '</span>';
1519
- html += '</div>';
1520
- return html;
1521
- }
1522
-
1523
- // Level C — only Cortex operations + surrounding context
1524
- function renderCortexOnly(snap) {
1525
- var ops = snap.operations || [];
1526
- if (ops.length === 0) {
1527
- return '<p style="color:var(--text-muted);font-size:13px">No Cortex operations in this request.</p>';
1528
- }
1529
-
1530
- var msgs = snap.originalMessages || [];
1531
- var html = '';
1532
-
1533
- ops.forEach(function(op) {
1534
- if (op.type === 'prune') {
1535
- html += '<div class="context-section-title">Prune — message ' + op.messageIndex + ', block ' + op.blockIndex + (op.subBlockIndex !== undefined ? '.' + op.subBlockIndex : '') + '</div>';
1536
- html += renderPrunedBlock(op);
1537
- // Show 2 messages of surrounding context
1538
- var ctxStart = Math.max(0, op.messageIndex - 1);
1539
- var ctxEnd = Math.min(msgs.length - 1, op.messageIndex + 1);
1540
- if (ctxStart < op.messageIndex) {
1541
- var prev = msgs[ctxStart];
1542
- html += '<div style="opacity:0.5;font-size:11px;padding:4px 8px;color:var(--text-muted)">[context] [' + esc(prev.role) + '] ' + esc(getPreview(prev)) + '</div>';
1543
- }
1544
- if (ctxEnd > op.messageIndex && msgs[ctxEnd]) {
1545
- var next = msgs[ctxEnd];
1546
- html += '<div style="opacity:0.5;font-size:11px;padding:4px 8px;color:var(--text-muted)">[context] [' + esc(next.role) + '] ' + esc(getPreview(next)) + '</div>';
1547
- }
1548
- } else if (op.type === 'inject') {
1549
- html += '<div class="context-section-title">Inject — at position ' + op.insertedAtIndex + '</div>';
1550
- // 2 messages of context before injection point
1551
- var before = msgs.slice(Math.max(0, op.insertedAtIndex - 2), op.insertedAtIndex);
1552
- before.forEach(function(m) {
1553
- html += '<div style="opacity:0.5;font-size:11px;padding:4px 8px;color:var(--text-muted)">[context] [' + esc(m.role) + '] ' + esc(getPreview(m)) + '</div>';
1554
- });
1555
- html += '<div class="diff-injected">';
1556
- html += '<div class="diff-injected-header">+ INJECTED: ' + esc(op.brainEntryTitle) + ' (confidence: ' + (op.confidence || 0).toFixed(2) + ')</div>';
1557
- html += esc(op.content.slice(0, 1000)) + (op.content.length > 1000 ? '\n...' : '');
1558
- html += '</div>';
1559
- // 2 messages after
1560
- var after = msgs.slice(op.insertedAtIndex, op.insertedAtIndex + 2);
1561
- after.forEach(function(m) {
1562
- html += '<div style="opacity:0.5;font-size:11px;padding:4px 8px;color:var(--text-muted)">[context] [' + esc(m.role) + '] ' + esc(getPreview(m)) + '</div>';
1563
- });
1564
- }
1565
- });
1566
- return html;
1567
- }
1568
-
1569
- function getPreview(msg) {
1570
- if (typeof msg.content === 'string') return msg.content.slice(0, 80);
1571
- if (Array.isArray(msg.content)) {
1572
- var first = msg.content.find(function(b) { return b.type === 'text' && b.text; });
1573
- if (first) return first.text.slice(0, 80);
1574
- return '[' + (msg.content[0] ? msg.content[0].type : 'empty') + ']';
1575
- }
1576
- return '';
1577
- }
1578
-
1579
- function renderContextStats() {
1580
- if (!contextSnapshot) return;
1581
- var snap = contextSnapshot;
1582
- var pruneCount = (snap.operations || []).filter(function(o) { return o.type === 'prune'; }).length;
1583
- var injectOp = (snap.operations || []).find(function(o) { return o.type === 'inject'; });
1584
- var saved = snap.tokensBefore - snap.tokensAfter;
1585
- var el = document.getElementById('contextStats');
1586
- el.innerHTML =
1587
- '<span>Tokens: ' + snap.tokensBefore + ' &rarr; ' + snap.tokensAfter + ' <span style="color:var(--green)">(-' + saved + ')</span></span>' +
1588
- (pruneCount ? '<span>Pruned: ' + pruneCount + ' block' + (pruneCount > 1 ? 's' : '') + '</span>' : '') +
1589
- (injectOp ? '<span style="color:var(--green)">Injected: ' + esc(injectOp.brainEntryTitle) + ' (' + (injectOp.confidence || 0).toFixed(2) + ')</span>' : '<span>No injection</span>') +
1590
- '<span style="color:var(--text-muted)">' + (snap.messagesSummary || []).length + ' messages</span>';
1591
- }
1592
-
1593
- function togglePruneExpand(id) {
1594
- var el = document.getElementById(id);
1595
- if (!el) return;
1596
- var shown = el.style.display !== 'none';
1597
- el.style.display = shown ? 'none' : 'inline';
1598
- var btn = el.previousElementSibling;
1599
- if (btn && btn.classList.contains('diff-expand')) {
1600
- btn.textContent = shown ? btn.textContent.replace('Hide', 'Show') : btn.textContent.replace('Show', 'Hide');
1601
- }
1602
- }
1603
-
1604
- // Expose to global scope for onclick handlers
1605
- window.openContextPanel = openContextPanel;
1606
- window.closeContextPanel = closeContextPanel;
1607
- window.setViewLevel = setViewLevel;
1608
- window.togglePruneExpand = togglePruneExpand;
1609
- window.toggleHistory = toggleHistory;
1610
- window.filterByAgent = filterByAgent;
1611
-
1612
- })();
1613
- </script>
1614
-
1615
- </body>
1616
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Cortex Dashboard</title>
7
+ <style>
8
+ :root {
9
+ --bg: #141414;
10
+ --surface: #1e1e1e;
11
+ --surface2: #252525;
12
+ --border: #333;
13
+ --text: #e0e0e0;
14
+ --text-muted: #888;
15
+ --green: #22c55e;
16
+ --yellow: #eab308;
17
+ --gray: #6b7280;
18
+ --red: #ef4444;
19
+ --blue: #3b82f6;
20
+ --header-h: 60px;
21
+ }
22
+
23
+ * { margin: 0; padding: 0; box-sizing: border-box; }
24
+
25
+ html, body {
26
+ background: var(--bg);
27
+ color: var(--text);
28
+ font-family: system-ui, -apple-system, sans-serif;
29
+ height: 100%;
30
+ }
31
+
32
+ /* Header */
33
+ .header {
34
+ position: fixed;
35
+ top: 0;
36
+ left: 0;
37
+ right: 0;
38
+ height: var(--header-h);
39
+ background: var(--surface);
40
+ border-bottom: 1px solid var(--border);
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ padding: 0 24px;
45
+ z-index: 100;
46
+ }
47
+
48
+ .header-title {
49
+ font-size: 18px;
50
+ font-weight: 700;
51
+ letter-spacing: -0.3px;
52
+ }
53
+
54
+ .header-stats {
55
+ display: flex;
56
+ gap: 24px;
57
+ font-size: 13px;
58
+ font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
59
+ }
60
+
61
+ .stat-item {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 6px;
65
+ }
66
+
67
+ .stat-label {
68
+ color: var(--text-muted);
69
+ }
70
+
71
+ .stat-value {
72
+ color: var(--text);
73
+ font-weight: 600;
74
+ }
75
+
76
+ /* Main layout */
77
+ .main {
78
+ margin-top: var(--header-h);
79
+ display: flex;
80
+ height: calc(100vh - var(--header-h));
81
+ }
82
+
83
+ /* Stream panel */
84
+ .stream-panel {
85
+ flex: 7;
86
+ overflow-y: auto;
87
+ padding: 16px;
88
+ }
89
+
90
+ .stream-panel::-webkit-scrollbar { width: 6px; }
91
+ .stream-panel::-webkit-scrollbar-track { background: transparent; }
92
+ .stream-panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
93
+
94
+ .empty-state {
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ height: 100%;
99
+ color: var(--text-muted);
100
+ font-size: 14px;
101
+ }
102
+
103
+ /* Request card */
104
+ .card {
105
+ background: var(--surface);
106
+ border: 1px solid var(--border);
107
+ border-left: 4px solid var(--gray);
108
+ border-radius: 8px;
109
+ padding: 14px 16px;
110
+ margin-bottom: 10px;
111
+ animation: fadeIn 0.2s ease-out;
112
+ }
113
+
114
+ @keyframes fadeIn {
115
+ from { opacity: 0; transform: translateY(-6px); }
116
+ to { opacity: 1; transform: translateY(0); }
117
+ }
118
+
119
+ .card.status-injected { border-left-color: var(--green); }
120
+ .card.status-pruned { border-left-color: var(--yellow); }
121
+ .card.status-passthrough { border-left-color: var(--gray); }
122
+ .card.status-error { border-left-color: var(--red); }
123
+
124
+ .card-header {
125
+ display: flex;
126
+ justify-content: space-between;
127
+ align-items: center;
128
+ margin-bottom: 10px;
129
+ font-size: 13px;
130
+ }
131
+
132
+ .card-id {
133
+ font-weight: 700;
134
+ font-size: 14px;
135
+ }
136
+
137
+ .card-meta {
138
+ color: var(--text-muted);
139
+ font-family: monospace;
140
+ font-size: 12px;
141
+ }
142
+
143
+ .card-line {
144
+ font-size: 12px;
145
+ font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
146
+ padding: 3px 0;
147
+ color: var(--text-muted);
148
+ }
149
+
150
+ .card-line span.label {
151
+ display: inline-block;
152
+ width: 90px;
153
+ color: var(--text-muted);
154
+ text-transform: uppercase;
155
+ font-size: 10px;
156
+ font-weight: 600;
157
+ letter-spacing: 0.5px;
158
+ }
159
+
160
+ .card-line .value { color: var(--text); }
161
+ .card-line .green { color: var(--green); }
162
+ .card-line .yellow { color: var(--yellow); }
163
+ .card-line .red { color: var(--red); }
164
+
165
+ /* Sidebar */
166
+ .sidebar {
167
+ flex: 3;
168
+ border-left: 1px solid var(--border);
169
+ overflow-y: auto;
170
+ background: var(--surface);
171
+ display: flex;
172
+ flex-direction: column;
173
+ }
174
+
175
+ .sidebar::-webkit-scrollbar { width: 6px; }
176
+ .sidebar::-webkit-scrollbar-track { background: transparent; }
177
+ .sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
178
+
179
+ .sidebar-section {
180
+ padding: 16px;
181
+ border-bottom: 1px solid var(--border);
182
+ }
183
+
184
+ .sidebar-section:last-child {
185
+ border-bottom: none;
186
+ }
187
+
188
+ .section-title {
189
+ font-size: 11px;
190
+ font-weight: 700;
191
+ margin-bottom: 12px;
192
+ color: var(--text-muted);
193
+ text-transform: uppercase;
194
+ letter-spacing: 0.5px;
195
+ }
196
+
197
+ /* Brain section */
198
+ .brain-entry {
199
+ background: var(--surface2);
200
+ border: 1px solid var(--border);
201
+ border-radius: 6px;
202
+ padding: 10px 12px;
203
+ margin-bottom: 8px;
204
+ }
205
+
206
+ .brain-entry-title {
207
+ font-size: 13px;
208
+ font-weight: 600;
209
+ margin-bottom: 6px;
210
+ }
211
+
212
+ .brain-summary-row {
213
+ display: flex;
214
+ justify-content: space-between;
215
+ font-size: 12px;
216
+ margin-bottom: 8px;
217
+ }
218
+
219
+ .brain-summary-label {
220
+ color: var(--text-muted);
221
+ }
222
+
223
+ .brain-summary-value {
224
+ font-weight: 600;
225
+ font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
226
+ }
227
+
228
+ .badge {
229
+ display: inline-block;
230
+ font-size: 10px;
231
+ font-weight: 600;
232
+ padding: 2px 7px;
233
+ border-radius: 4px;
234
+ margin-right: 4px;
235
+ margin-bottom: 3px;
236
+ text-transform: uppercase;
237
+ letter-spacing: 0.3px;
238
+ }
239
+
240
+ .badge-lang {
241
+ background: rgba(59, 130, 246, 0.15);
242
+ color: var(--blue);
243
+ }
244
+
245
+ .badge-tag {
246
+ background: rgba(107, 114, 128, 0.2);
247
+ color: var(--text-muted);
248
+ }
249
+
250
+ .sidebar-empty {
251
+ color: var(--text-muted);
252
+ font-size: 13px;
253
+ text-align: center;
254
+ padding: 16px 0;
255
+ }
256
+
257
+ /* Features section */
258
+ .feature-row {
259
+ display: flex;
260
+ align-items: center;
261
+ justify-content: space-between;
262
+ padding: 8px 0;
263
+ border-bottom: 1px solid var(--border);
264
+ }
265
+
266
+ .feature-row:last-child {
267
+ border-bottom: none;
268
+ }
269
+
270
+ .feature-info {
271
+ display: flex;
272
+ flex-direction: column;
273
+ gap: 2px;
274
+ }
275
+
276
+ .feature-name {
277
+ font-size: 13px;
278
+ font-weight: 600;
279
+ text-transform: capitalize;
280
+ }
281
+
282
+ .feature-reason {
283
+ font-size: 11px;
284
+ color: var(--text-muted);
285
+ }
286
+
287
+ /* Toggle switch */
288
+ .toggle {
289
+ position: relative;
290
+ width: 36px;
291
+ height: 20px;
292
+ flex-shrink: 0;
293
+ }
294
+
295
+ .toggle input {
296
+ opacity: 0;
297
+ width: 0;
298
+ height: 0;
299
+ position: absolute;
300
+ }
301
+
302
+ .toggle-track {
303
+ position: absolute;
304
+ inset: 0;
305
+ background: var(--border);
306
+ border-radius: 10px;
307
+ cursor: pointer;
308
+ transition: background 0.2s;
309
+ }
310
+
311
+ .toggle input:checked + .toggle-track {
312
+ background: var(--green);
313
+ }
314
+
315
+ .toggle input:disabled + .toggle-track {
316
+ opacity: 0.4;
317
+ cursor: not-allowed;
318
+ }
319
+
320
+ .toggle-thumb {
321
+ position: absolute;
322
+ top: 3px;
323
+ left: 3px;
324
+ width: 14px;
325
+ height: 14px;
326
+ background: white;
327
+ border-radius: 50%;
328
+ transition: transform 0.2s;
329
+ pointer-events: none;
330
+ }
331
+
332
+ .toggle input:checked ~ .toggle-thumb {
333
+ transform: translateX(16px);
334
+ }
335
+
336
+ /* Agent type badges */
337
+ .badge-main { background: var(--green); color: #000; }
338
+ .badge-subagent { background: var(--blue); color: #fff; }
339
+ .badge-teammate { background: #a855f7; color: #fff; }
340
+ .badge-unknown { background: var(--gray); color: #fff; }
341
+
342
+ .agent-badge {
343
+ display: inline-block;
344
+ font-size: 10px;
345
+ font-weight: 600;
346
+ padding: 1px 6px;
347
+ border-radius: 3px;
348
+ text-transform: uppercase;
349
+ letter-spacing: 0.5px;
350
+ }
351
+
352
+ /* Agent sidebar section */
353
+ .agent-section { margin-top: 16px; }
354
+ .agent-section h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); margin-bottom: 8px; }
355
+ .agent-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 4px; cursor: pointer; font-size: 13px; }
356
+ .agent-row:hover { background: var(--surface2); }
357
+ .agent-row.active { background: var(--surface2); border-left: 2px solid var(--blue); }
358
+ .agent-model { color: var(--text-muted); font-size: 11px; }
359
+ .agent-requests { color: var(--text-muted); font-size: 11px; margin-left: auto; }
360
+
361
+ /* History section */
362
+ .history-section { margin-top: 12px; }
363
+ .history-toggle { cursor: pointer; font-size: 12px; color: var(--text-muted); display: flex; align-items: center; gap: 4px; }
364
+ .history-toggle:hover { color: var(--text); }
365
+ .history-list { display: none; }
366
+ .history-list.open { display: block; }
367
+ .history-row { padding: 4px 8px; font-size: 12px; color: var(--text-muted); }
368
+
369
+ /* Hierarchical session tree */
370
+ .session-group { margin-bottom: 4px; }
371
+
372
+ .session-header {
373
+ display: flex;
374
+ align-items: center;
375
+ gap: 6px;
376
+ padding: 6px 8px;
377
+ border-radius: 4px;
378
+ cursor: pointer;
379
+ font-size: 13px;
380
+ }
381
+ .session-header:hover { background: var(--surface2); }
382
+ .session-header.active { background: var(--surface2); border-left: 2px solid var(--blue); padding-left: 6px; }
383
+
384
+ .session-toggle { font-size: 10px; color: var(--text-muted); width: 12px; flex-shrink: 0; }
385
+
386
+ .session-children { padding-left: 16px; display: block; }
387
+ .session-children.collapsed { display: none; }
388
+
389
+ .child-group-label {
390
+ font-size: 10px;
391
+ font-weight: 700;
392
+ text-transform: uppercase;
393
+ letter-spacing: 0.5px;
394
+ color: var(--text-muted);
395
+ padding: 6px 8px 3px;
396
+ }
397
+
398
+ .child-row {
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 6px;
402
+ padding: 4px 8px;
403
+ border-radius: 4px;
404
+ cursor: pointer;
405
+ font-size: 12px;
406
+ }
407
+ .child-row:hover { background: var(--surface2); }
408
+ .child-row.active { background: var(--surface2); border-left: 2px solid var(--blue); padding-left: 6px; }
409
+
410
+ .connector {
411
+ font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
412
+ font-size: 11px;
413
+ color: var(--text-muted);
414
+ flex-shrink: 0;
415
+ }
416
+ .connector.solid { color: var(--green); }
417
+ .connector.dashed { color: var(--yellow); }
418
+
419
+ .unlinked-section { margin-top: 8px; border-top: 1px solid var(--border); padding-top: 8px; }
420
+
421
+ /* Responsive */
422
+ @media (max-width: 768px) {
423
+ .main { flex-direction: column; }
424
+ .sidebar { border-left: none; border-top: 1px solid var(--border); max-height: 40vh; }
425
+ .header-stats { gap: 12px; font-size: 11px; }
426
+ }
427
+ </style>
428
+ </head>
429
+ <body>
430
+
431
+ <div class="header">
432
+ <div class="header-title">Cortex Dashboard</div>
433
+ <div class="header-stats">
434
+ <div class="stat-item">
435
+ <span class="stat-label">Uptime</span>
436
+ <span class="stat-value" id="stat-uptime">0s</span>
437
+ </div>
438
+ <div class="stat-item">
439
+ <span class="stat-label">Requests</span>
440
+ <span class="stat-value" id="stat-requests">0</span>
441
+ </div>
442
+ <div class="stat-item">
443
+ <span class="stat-label">Tokens saved</span>
444
+ <span class="stat-value" id="stat-tokens">0</span>
445
+ </div>
446
+ <div class="stat-item">
447
+ <span class="stat-label">Injection rate</span>
448
+ <span class="stat-value" id="stat-injection">0%</span>
449
+ </div>
450
+ <div class="stat-item" id="agentsStat" style="display:none">
451
+ <span class="stat-label">Agents</span>
452
+ <span class="stat-value" id="agentsCount">0</span>
453
+ </div>
454
+ </div>
455
+ </div>
456
+
457
+ <div class="main">
458
+ <div class="stream-panel" id="stream">
459
+ <div class="empty-state" id="empty-state">Waiting for requests...</div>
460
+ </div>
461
+ <div class="sidebar">
462
+ <div class="sidebar-section">
463
+ <div class="section-title">Brain Directory</div>
464
+ <div id="brain-content"><div class="sidebar-empty">Loading...</div></div>
465
+ <div class="agent-section" id="agentSection" style="display:none">
466
+ <h3>Active Agents</h3>
467
+ <div id="agentList"></div>
468
+ <div class="history-section">
469
+ <div class="history-toggle" onclick="toggleHistory()">
470
+ <span id="historyArrow">&#9658;</span> History (<span id="historyCount">0</span>)
471
+ </div>
472
+ <div class="history-list" id="historyList"></div>
473
+ </div>
474
+ </div>
475
+ </div>
476
+ <div class="sidebar-section">
477
+ <div class="section-title">Features</div>
478
+ <div id="features-list"><div class="sidebar-empty">Loading...</div></div>
479
+ </div>
480
+ </div>
481
+ </div>
482
+
483
+ <script>
484
+ (function() {
485
+ // Authenticated fetch helper for API calls
486
+ function apiFetch(url, options) {
487
+ options = options || {};
488
+ var headers = Object.assign({}, options.headers || {});
489
+ if (window.CORTEX_API_TOKEN) {
490
+ headers['Authorization'] = 'Bearer ' + window.CORTEX_API_TOKEN;
491
+ }
492
+ return fetch(url, Object.assign({}, options, { headers: headers }));
493
+ }
494
+
495
+ // State
496
+ var requests = {};
497
+ var MAX_CARDS = 100;
498
+ var startTime = Date.now();
499
+
500
+ // Agent tracking
501
+ var agents = new Map();
502
+ var requestAgentMap = new Map();
503
+ var agentFilter = null;
504
+ var historyAgents = [];
505
+
506
+ // DOM refs
507
+ var streamEl = document.getElementById('stream');
508
+ var emptyEl = document.getElementById('empty-state');
509
+ var brainContentEl = document.getElementById('brain-content');
510
+ var featuresListEl = document.getElementById('features-list');
511
+ var statUptime = document.getElementById('stat-uptime');
512
+ var statRequests = document.getElementById('stat-requests');
513
+ var statTokens = document.getElementById('stat-tokens');
514
+ var statInjection = document.getElementById('stat-injection');
515
+
516
+ // Uptime timer
517
+ setInterval(function() {
518
+ var s = Math.floor((Date.now() - startTime) / 1000);
519
+ var h = Math.floor(s / 3600);
520
+ var m = Math.floor((s % 3600) / 60);
521
+ var sec = s % 60;
522
+ statUptime.textContent = h > 0 ? h + 'h ' + m + 'm' : m > 0 ? m + 'm ' + sec + 's' : sec + 's';
523
+ }, 1000);
524
+
525
+ // Load session stats
526
+ function loadSessionStats() {
527
+ apiFetch('/api/session/current')
528
+ .then(function(r) { return r.json(); })
529
+ .then(function(data) {
530
+ statRequests.textContent = data.requestCount;
531
+ statTokens.textContent = formatNum(data.tokensSaved);
532
+ statInjection.textContent = Math.round(data.matchRate * 100) + '%';
533
+ })
534
+ .catch(function() {});
535
+ }
536
+ loadSessionStats();
537
+
538
+ // Load brain summary
539
+ function loadBrainSummary() {
540
+ apiFetch('/api/brain/summary')
541
+ .then(function(r) { return r.json(); })
542
+ .then(function(data) {
543
+ var html = '';
544
+ html += '<div class="brain-summary-row">';
545
+ html += '<span class="brain-summary-label">Total entries</span>';
546
+ html += '<span class="brain-summary-value">' + data.totalEntries + '</span>';
547
+ html += '</div>';
548
+
549
+ var sources = Object.keys(data.bySource || {});
550
+ if (sources.length > 0) {
551
+ sources.forEach(function(src) {
552
+ html += '<div class="brain-entry">';
553
+ html += '<div class="brain-entry-title">' + esc(src) + '</div>';
554
+ html += '<span class="badge badge-tag">' + data.bySource[src] + ' entries</span>';
555
+ html += '</div>';
556
+ });
557
+ } else if (data.totalEntries === 0) {
558
+ html += '<div class="sidebar-empty">No entries loaded</div>';
559
+ }
560
+
561
+ brainContentEl.innerHTML = html;
562
+ })
563
+ .catch(function() {
564
+ brainContentEl.innerHTML = '<div class="sidebar-empty">Failed to load</div>';
565
+ });
566
+ }
567
+ loadBrainSummary();
568
+
569
+ // Load and render features
570
+ function loadFeatures() {
571
+ apiFetch('/api/features')
572
+ .then(function(r) { return r.json(); })
573
+ .then(function(data) {
574
+ renderFeatures(data.features || []);
575
+ })
576
+ .catch(function() {
577
+ featuresListEl.innerHTML = '<div class="sidebar-empty">Failed to load</div>';
578
+ });
579
+ }
580
+
581
+ function renderFeatures(features) {
582
+ if (!features.length) {
583
+ featuresListEl.innerHTML = '<div class="sidebar-empty">No features</div>';
584
+ return;
585
+ }
586
+
587
+ featuresListEl.innerHTML = '';
588
+ features.forEach(function(feature) {
589
+ var row = document.createElement('div');
590
+ row.className = 'feature-row';
591
+ row.setAttribute('data-feature', feature.name);
592
+
593
+ var info = document.createElement('div');
594
+ info.className = 'feature-info';
595
+
596
+ var nameEl = document.createElement('div');
597
+ nameEl.className = 'feature-name';
598
+ nameEl.textContent = feature.name;
599
+ info.appendChild(nameEl);
600
+
601
+ if (feature.reason) {
602
+ var reasonEl = document.createElement('div');
603
+ reasonEl.className = 'feature-reason';
604
+ reasonEl.textContent = feature.reason;
605
+ info.appendChild(reasonEl);
606
+ }
607
+
608
+ var label = document.createElement('label');
609
+ label.className = 'toggle';
610
+
611
+ var input = document.createElement('input');
612
+ input.type = 'checkbox';
613
+ input.checked = feature.enabled;
614
+ input.disabled = !!feature.reason;
615
+
616
+ (function(inp, rw) {
617
+ inp.addEventListener('change', function() {
618
+ toggleFeature(feature.name, inp, rw);
619
+ });
620
+ })(input, row);
621
+
622
+ var track = document.createElement('div');
623
+ track.className = 'toggle-track';
624
+
625
+ var thumb = document.createElement('div');
626
+ thumb.className = 'toggle-thumb';
627
+
628
+ label.appendChild(input);
629
+ label.appendChild(track);
630
+ label.appendChild(thumb);
631
+
632
+ row.appendChild(info);
633
+ row.appendChild(label);
634
+ featuresListEl.appendChild(row);
635
+ });
636
+ }
637
+
638
+ function toggleFeature(name, inputEl, rowEl) {
639
+ inputEl.disabled = true;
640
+ apiFetch('/api/features/' + encodeURIComponent(name) + '/toggle', { method: 'POST' })
641
+ .then(function(r) { return r.json(); })
642
+ .then(function(updated) {
643
+ inputEl.checked = updated.enabled;
644
+ var infoEl = rowEl.querySelector('.feature-info');
645
+ var existingReason = rowEl.querySelector('.feature-reason');
646
+ if (updated.reason) {
647
+ if (existingReason) {
648
+ existingReason.textContent = updated.reason;
649
+ } else {
650
+ var reasonEl = document.createElement('div');
651
+ reasonEl.className = 'feature-reason';
652
+ reasonEl.textContent = updated.reason;
653
+ infoEl.appendChild(reasonEl);
654
+ }
655
+ inputEl.disabled = true;
656
+ } else {
657
+ if (existingReason) existingReason.remove();
658
+ inputEl.disabled = false;
659
+ }
660
+ })
661
+ .catch(function() {
662
+ inputEl.checked = !inputEl.checked;
663
+ inputEl.disabled = false;
664
+ });
665
+ }
666
+ loadFeatures();
667
+
668
+ // Agent topology — session tree (no context panel, no "view" buttons)
669
+ async function renderSessionTree() {
670
+ try {
671
+ var topoRes = await apiFetch('/api/agents/topology');
672
+ if (!topoRes.ok) return;
673
+ var topo = await topoRes.json();
674
+ if (!topo.enabled) return;
675
+
676
+ document.getElementById('agentSection').style.display = '';
677
+ document.getElementById('agentsStat').style.display = '';
678
+
679
+ if (topo.version === 2 && Array.isArray(topo.sessions)) {
680
+ topo.sessions.forEach(function(session) {
681
+ agents.set(session.agent.conversationId, session.agent);
682
+ (session.teams || []).forEach(function(team) {
683
+ (team.members || []).forEach(function(m) { agents.set(m.agent.conversationId, m.agent); });
684
+ });
685
+ (session.subagents || []).forEach(function(s) { agents.set(s.agent.conversationId, s.agent); });
686
+ });
687
+ (topo.unlinked || []).forEach(function(a) { agents.set(a.conversationId, a); });
688
+ }
689
+
690
+ var list = document.getElementById('agentList');
691
+ list.innerHTML = '';
692
+
693
+ if (topo.version === 2 && Array.isArray(topo.sessions)) {
694
+ topo.sessions.forEach(function(session) {
695
+ list.appendChild(buildSessionGroup(session));
696
+ });
697
+
698
+ if (topo.unlinked && topo.unlinked.length > 0) {
699
+ var unlinkedSection = document.createElement('div');
700
+ unlinkedSection.className = 'unlinked-section';
701
+ var label = document.createElement('div');
702
+ label.className = 'child-group-label';
703
+ label.textContent = 'Unlinked';
704
+ unlinkedSection.appendChild(label);
705
+ topo.unlinked.forEach(function(agent) {
706
+ unlinkedSection.appendChild(buildChildRow(agent, null));
707
+ });
708
+ list.appendChild(unlinkedSection);
709
+ }
710
+ } else {
711
+ agents.forEach(function(agent, id) {
712
+ list.appendChild(buildFlatAgentRow(agent, id));
713
+ });
714
+ }
715
+
716
+ // "Show all" button when filtered
717
+ if (agentFilter) {
718
+ var allBtn = document.createElement('div');
719
+ allBtn.className = 'agent-row';
720
+ allBtn.style.cssText = 'color:var(--blue);margin-top:4px';
721
+ allBtn.textContent = '\u2190 Show all';
722
+ allBtn.onclick = function() { filterByAgent(null); };
723
+ list.insertBefore(allBtn, list.firstChild);
724
+ }
725
+
726
+ var total = agents.size + historyAgents.length;
727
+ document.getElementById('agentsCount').textContent = agents.size + '/' + total;
728
+ } catch(_) {}
729
+ }
730
+
731
+ function buildSessionGroup(session) {
732
+ var agent = session.agent;
733
+ var id = agent.conversationId;
734
+ var hasChildren = (session.teams && session.teams.length > 0) ||
735
+ (session.subagents && session.subagents.length > 0);
736
+
737
+ var group = document.createElement('div');
738
+ group.className = 'session-group';
739
+ group.dataset.sessionId = id;
740
+
741
+ var header = document.createElement('div');
742
+ header.className = 'session-header' + (agentFilter === id ? ' active' : '');
743
+
744
+ var toggleSpan = document.createElement('span');
745
+ toggleSpan.className = 'session-toggle';
746
+ toggleSpan.textContent = hasChildren ? '\u25bc' : '';
747
+ header.appendChild(toggleSpan);
748
+
749
+ var badge = document.createElement('span');
750
+ badge.className = 'agent-badge badge-main';
751
+ badge.textContent = 'main';
752
+ header.appendChild(badge);
753
+
754
+ var modelShort = agent.model ? agent.model.split('-').pop() : '';
755
+ if (modelShort) {
756
+ var modelSpan = document.createElement('span');
757
+ modelSpan.className = 'agent-model';
758
+ modelSpan.textContent = modelShort;
759
+ header.appendChild(modelSpan);
760
+ }
761
+
762
+ var reqSpan = document.createElement('span');
763
+ reqSpan.className = 'agent-requests';
764
+ reqSpan.textContent = (agent.requestCount || 0) + ' req';
765
+ header.appendChild(reqSpan);
766
+
767
+ header.onclick = function() {
768
+ if (hasChildren) {
769
+ var childrenEl = group.querySelector('.session-children');
770
+ var isCollapsed = childrenEl && childrenEl.classList.contains('collapsed');
771
+ if (childrenEl) childrenEl.classList.toggle('collapsed');
772
+ toggleSpan.textContent = isCollapsed ? '\u25bc' : '\u25b6';
773
+ }
774
+ filterBySession(id, session);
775
+ };
776
+
777
+ group.appendChild(header);
778
+
779
+ if (hasChildren) {
780
+ var children = document.createElement('div');
781
+ children.className = 'session-children';
782
+
783
+ (session.teams || []).forEach(function(team) {
784
+ if (!team.members || team.members.length === 0) return;
785
+ var teamLabel = document.createElement('div');
786
+ teamLabel.className = 'child-group-label';
787
+ teamLabel.textContent = 'Team: ' + team.name;
788
+ children.appendChild(teamLabel);
789
+ team.members.forEach(function(m) {
790
+ children.appendChild(buildChildRow(m.agent, m.parentLink));
791
+ });
792
+ });
793
+
794
+ if (session.subagents && session.subagents.length > 0) {
795
+ var subLabel = document.createElement('div');
796
+ subLabel.className = 'child-group-label';
797
+ subLabel.textContent = 'Subagents';
798
+ children.appendChild(subLabel);
799
+ session.subagents.forEach(function(s) {
800
+ children.appendChild(buildChildRow(s.agent, s.parentLink));
801
+ });
802
+ }
803
+
804
+ group.appendChild(children);
805
+ }
806
+
807
+ return group;
808
+ }
809
+
810
+ function buildChildRow(agent, parentLink) {
811
+ var id = agent.conversationId;
812
+ var row = document.createElement('div');
813
+ row.className = 'child-row' + (agentFilter === id ? ' active' : '');
814
+ row.dataset.agentId = id;
815
+
816
+ var connText = '\u2500\u2500';
817
+ var connClass = 'solid';
818
+ var connTitle = '';
819
+ if (parentLink) {
820
+ if (parentLink.confidence === 'inferred') {
821
+ connText = '\u254c\u254c';
822
+ connClass = 'dashed';
823
+ }
824
+ if (parentLink.method === 'team-config') connTitle = 'Linked via team config';
825
+ else if (parentLink.method === 'temporal') connTitle = 'Linked via temporal correlation';
826
+ else if (parentLink.method === 'content-match') connTitle = 'Linked via content matching';
827
+ else if (parentLink.method === 'direct') connTitle = 'Linked via direct assignment';
828
+ }
829
+
830
+ var connSpan = document.createElement('span');
831
+ connSpan.className = 'connector ' + connClass;
832
+ connSpan.textContent = connText;
833
+ if (connTitle) connSpan.title = connTitle;
834
+ row.appendChild(connSpan);
835
+
836
+ var badge = document.createElement('span');
837
+ badge.className = 'agent-badge badge-' + escClass(agent.type);
838
+ badge.textContent = agent.teamRole || agent.type || 'unknown';
839
+ row.appendChild(badge);
840
+
841
+ var modelShort = agent.model ? agent.model.split('-').pop() : '';
842
+ if (modelShort) {
843
+ var modelSpan = document.createElement('span');
844
+ modelSpan.className = 'agent-model';
845
+ modelSpan.textContent = modelShort;
846
+ row.appendChild(modelSpan);
847
+ }
848
+
849
+ var reqSpan = document.createElement('span');
850
+ reqSpan.className = 'agent-requests';
851
+ reqSpan.textContent = (agent.requestCount || 0) + ' req';
852
+ row.appendChild(reqSpan);
853
+
854
+ row.onclick = function() { filterByAgent(id); };
855
+
856
+ return row;
857
+ }
858
+
859
+ function buildFlatAgentRow(agent, id) {
860
+ var row = document.createElement('div');
861
+ row.className = 'agent-row' + (agentFilter === id ? ' active' : '');
862
+ var modelShort = agent.model ? agent.model.split('-').pop() : '';
863
+
864
+ var badge = document.createElement('span');
865
+ badge.className = 'agent-badge badge-' + escClass(agent.type);
866
+ badge.textContent = agent.type || 'unknown';
867
+ row.appendChild(badge);
868
+
869
+ var modelSpan = document.createElement('span');
870
+ modelSpan.className = 'agent-model';
871
+ modelSpan.textContent = modelShort;
872
+ row.appendChild(modelSpan);
873
+
874
+ var reqSpan = document.createElement('span');
875
+ reqSpan.className = 'agent-requests';
876
+ reqSpan.textContent = (agent.requestCount || 0) + ' req';
877
+ row.appendChild(reqSpan);
878
+
879
+ row.onclick = function() { filterByAgent(id); };
880
+ return row;
881
+ }
882
+
883
+ function filterBySession(mainId, session) {
884
+ var ids = new Set([mainId]);
885
+ (session.teams || []).forEach(function(t) {
886
+ (t.members || []).forEach(function(m) { ids.add(m.agent.conversationId); });
887
+ });
888
+ (session.subagents || []).forEach(function(s) { ids.add(s.agent.conversationId); });
889
+
890
+ agentFilter = mainId;
891
+ document.querySelectorAll('.card').forEach(function(card) {
892
+ card.style.display = ids.has(card.dataset.agentId) ? '' : 'none';
893
+ });
894
+ renderSessionTree();
895
+ }
896
+
897
+ function renderHistory() {
898
+ var list = document.getElementById('historyList');
899
+ var count = document.getElementById('historyCount');
900
+ count.textContent = historyAgents.length;
901
+ list.innerHTML = '';
902
+ historyAgents.forEach(function(h) {
903
+ var row = document.createElement('div');
904
+ row.className = 'history-row';
905
+ var dur = Math.round((h.duration || 0) / 1000);
906
+ var type = (h.info && h.info.type) ? h.info.type : 'unknown';
907
+ row.innerHTML =
908
+ '<span class="agent-badge badge-' + escClass(type) + '">' + esc(type) + '</span> ' +
909
+ dur + 's \u00b7 ' + (h.totalRequests || 0) + ' req';
910
+ list.appendChild(row);
911
+ });
912
+ }
913
+
914
+ function toggleHistory() {
915
+ var list = document.getElementById('historyList');
916
+ var arrow = document.getElementById('historyArrow');
917
+ list.classList.toggle('open');
918
+ arrow.textContent = list.classList.contains('open') ? '\u25bc' : '\u25ba';
919
+ }
920
+
921
+ function filterByAgent(id) {
922
+ agentFilter = id;
923
+ renderSessionTree();
924
+ document.querySelectorAll('.card').forEach(function(card) {
925
+ if (!id) { card.style.display = ''; return; }
926
+ card.style.display = card.dataset.agentId === id ? '' : 'none';
927
+ });
928
+ }
929
+
930
+ async function loadAgents() {
931
+ try {
932
+ var histRes = await apiFetch('/api/agents/history');
933
+ var histData = await histRes.json();
934
+ historyAgents = histData.history || [];
935
+ renderHistory();
936
+ } catch(_) {}
937
+ renderSessionTree();
938
+ }
939
+
940
+ loadAgents();
941
+
942
+ // SSE — live pipeline events
943
+ var sseUrl = '/api/pipeline/live?token=' + encodeURIComponent(window.CORTEX_API_TOKEN || '');
944
+ var eventSource = new EventSource(sseUrl);
945
+
946
+ eventSource.onmessage = function(e) {
947
+ var event;
948
+ try { event = JSON.parse(e.data); } catch(_) { return; }
949
+ if (event.type === 'ping') return;
950
+
951
+ if (event.type === 'agent:detected') {
952
+ var existing = agents.get(event.agentId);
953
+ agents.set(event.agentId, {
954
+ conversationId: event.agentId,
955
+ type: event.agentType,
956
+ model: event.model,
957
+ toolCount: event.toolCount,
958
+ teamName: event.teamName,
959
+ teamRole: event.teamRole,
960
+ requestCount: existing ? (existing.requestCount || 0) + 1 : 1,
961
+ });
962
+ if (event.requestId !== undefined) {
963
+ requestAgentMap.set(event.requestId, { agentId: event.agentId, agentType: event.agentType });
964
+ }
965
+ document.getElementById('agentSection').style.display = '';
966
+ document.getElementById('agentsStat').style.display = '';
967
+ renderSessionTree();
968
+ return;
969
+ }
970
+
971
+ if (event.type === 'agent:completed') {
972
+ agents.delete(event.agentId);
973
+ historyAgents.unshift({
974
+ info: { type: event.agentType, conversationId: event.agentId },
975
+ totalRequests: event.totalRequests,
976
+ duration: event.duration,
977
+ completionReason: event.completionReason,
978
+ });
979
+ renderSessionTree();
980
+ renderHistory();
981
+ return;
982
+ }
983
+
984
+ var rid = event.requestId;
985
+ if (rid === undefined) return;
986
+
987
+ if (!requests[rid]) {
988
+ requests[rid] = { events: [] };
989
+ }
990
+ requests[rid].events.push(event);
991
+
992
+ if (event.type === 'request:start' && event.agentId && !requestAgentMap.has(rid)) {
993
+ requestAgentMap.set(rid, { agentId: event.agentId, agentType: event.agentType || 'unknown' });
994
+ }
995
+
996
+ if (event.type === 'request:end') {
997
+ loadSessionStats();
998
+ renderCard(rid);
999
+ }
1000
+ if (event.type === 'pipeline:error') {
1001
+ renderCard(rid);
1002
+ }
1003
+ };
1004
+
1005
+ function renderCard(rid) {
1006
+ var data = requests[rid];
1007
+ if (!data) return;
1008
+
1009
+ var evts = data.events;
1010
+ var startEvt = find(evts, 'request:start');
1011
+ var pruneEvt = find(evts, 'prune:complete');
1012
+ var fpEvt = find(evts, 'fingerprint:complete');
1013
+ var matchEvt = find(evts, 'match:complete');
1014
+ var injectEvt = find(evts, 'inject:complete');
1015
+ var endEvt = find(evts, 'request:end');
1016
+ var errEvt = find(evts, 'pipeline:error');
1017
+
1018
+ var status = 'passthrough';
1019
+ if (errEvt) status = 'error';
1020
+ else if (injectEvt && injectEvt.injected) status = 'injected';
1021
+ else if (pruneEvt && pruneEvt.tokensSaved > 0) status = 'pruned';
1022
+
1023
+ var agentInfo = requestAgentMap.get(rid);
1024
+ var agentId = agentInfo ? agentInfo.agentId : '';
1025
+ var agentType = agentInfo ? agentInfo.agentType : '';
1026
+
1027
+ var html = '<div class="card-header">';
1028
+ html += '<span class="card-id">Request #' + rid + '</span>';
1029
+ if (agentId) {
1030
+ html += ' <span class="agent-badge badge-' + escClass(agentType) + '">' + esc(agentType) + '</span>';
1031
+ }
1032
+ html += '<span class="card-meta">';
1033
+ if (startEvt) html += formatTime(startEvt.timestamp);
1034
+ if (endEvt) html += ' &middot; ' + endEvt.totalTimeMs + 'ms';
1035
+ html += '</span></div>';
1036
+
1037
+ if (pruneEvt) {
1038
+ html += line('Prune',
1039
+ pruneEvt.tokensBefore + ' &rarr; ' + pruneEvt.tokensAfter +
1040
+ ' <span class="' + (pruneEvt.tokensSaved > 0 ? 'green' : '') + '">(saved ' + pruneEvt.tokensSaved + ')</span>');
1041
+ }
1042
+
1043
+ if (fpEvt) {
1044
+ html += line('Fingerprint',
1045
+ '<span class="value">' + esc(fpEvt.languages.join(', ')) + '</span>' +
1046
+ (fpEvt.errorTypes.length ? ' &middot; ' + esc(fpEvt.errorTypes.join(', ')) : '') +
1047
+ ' &middot; ' + fpEvt.phase);
1048
+ }
1049
+
1050
+ if (matchEvt && matchEvt.topMatches.length) {
1051
+ var matches = matchEvt.topMatches.slice(0, 3).map(function(m) {
1052
+ return esc(m.title) + ' (' + m.score.toFixed(2) + ')';
1053
+ }).join(', ');
1054
+ html += line('Match', '<span class="value">' + matches + '</span>');
1055
+ }
1056
+
1057
+ if (injectEvt) {
1058
+ if (injectEvt.injected) {
1059
+ html += line('Inject',
1060
+ '<span class="green">&#10003; ' + esc(injectEvt.entryTitle || '') +
1061
+ ' (' + (injectEvt.confidence ? injectEvt.confidence.toFixed(2) : '?') + ')</span>');
1062
+ } else {
1063
+ html += line('Inject', '<span class="red">&#10007; no injection</span>');
1064
+ }
1065
+ }
1066
+
1067
+ if (errEvt) {
1068
+ html += line('Error', '<span class="red">[' + esc(errEvt.stage) + '] ' + esc(errEvt.error) + '</span>');
1069
+ }
1070
+
1071
+ if (endEvt && endEvt.reason) {
1072
+ html += line('Reason', '<span class="value">' + esc(endEvt.reason) + '</span>');
1073
+ }
1074
+
1075
+ var card = document.createElement('div');
1076
+ card.className = 'card status-' + status;
1077
+ card.setAttribute('data-rid', rid);
1078
+ card.dataset.agentId = agentId;
1079
+ card.innerHTML = html;
1080
+
1081
+ if (emptyEl) emptyEl.style.display = 'none';
1082
+
1083
+ var existing = streamEl.querySelector('[data-rid="' + rid + '"]');
1084
+ if (existing) existing.remove();
1085
+
1086
+ streamEl.insertBefore(card, streamEl.firstChild);
1087
+
1088
+ while (streamEl.children.length > MAX_CARDS + 1) {
1089
+ streamEl.removeChild(streamEl.lastChild);
1090
+ }
1091
+ }
1092
+
1093
+ // Helpers
1094
+ function find(arr, type) {
1095
+ for (var i = arr.length - 1; i >= 0; i--) {
1096
+ if (arr[i].type === type) return arr[i];
1097
+ }
1098
+ return null;
1099
+ }
1100
+
1101
+ function line(label, content) {
1102
+ return '<div class="card-line"><span class="label">' + label + '</span>' + content + '</div>';
1103
+ }
1104
+
1105
+ function esc(s) {
1106
+ if (!s) return '';
1107
+ var d = document.createElement('div');
1108
+ d.textContent = s;
1109
+ return d.innerHTML;
1110
+ }
1111
+
1112
+ function escClass(s) {
1113
+ if (!s) return 'unknown';
1114
+ return s.replace(/[^a-zA-Z0-9-_]/g, '');
1115
+ }
1116
+
1117
+ function formatTime(ts) {
1118
+ var d = new Date(ts);
1119
+ return d.toLocaleTimeString();
1120
+ }
1121
+
1122
+ function formatNum(n) {
1123
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
1124
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
1125
+ return '' + n;
1126
+ }
1127
+
1128
+ // Expose to global scope for onclick handlers
1129
+ window.toggleHistory = toggleHistory;
1130
+ window.filterByAgent = filterByAgent;
1131
+
1132
+ })();
1133
+ </script>
1134
+
1135
+ </body>
1136
+ </html>