swarm-engine 1.43.0 → 1.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +145 -42
  2. package/agents/implementer.md +1 -14
  3. package/agents/orchestrator.md +127 -246
  4. package/agents/reviewer.md +3 -18
  5. package/dist/cli/commands/agents.js +13 -1
  6. package/dist/cli/commands/agents.js.map +1 -1
  7. package/dist/cli/commands/dashboard.d.ts +3 -0
  8. package/dist/cli/commands/dashboard.d.ts.map +1 -0
  9. package/dist/cli/commands/dashboard.js +43 -0
  10. package/dist/cli/commands/dashboard.js.map +1 -0
  11. package/dist/cli/commands/orchestrate.d.ts.map +1 -1
  12. package/dist/cli/commands/orchestrate.js +17 -0
  13. package/dist/cli/commands/orchestrate.js.map +1 -1
  14. package/dist/cli/commands/run.d.ts.map +1 -1
  15. package/dist/cli/commands/run.js +20 -1
  16. package/dist/cli/commands/run.js.map +1 -1
  17. package/dist/cli/commands/status.d.ts +1 -0
  18. package/dist/cli/commands/status.d.ts.map +1 -1
  19. package/dist/cli/commands/status.js +5 -2
  20. package/dist/cli/commands/status.js.map +1 -1
  21. package/dist/core/event-bus.d.ts.map +1 -1
  22. package/dist/core/event-bus.js +4 -1
  23. package/dist/core/event-bus.js.map +1 -1
  24. package/dist/core/types.d.ts +24 -1
  25. package/dist/core/types.d.ts.map +1 -1
  26. package/dist/core/types.js +4 -0
  27. package/dist/core/types.js.map +1 -1
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/memory/index.js +2 -2
  33. package/dist/memory/index.js.map +1 -1
  34. package/dist/runtime/agent-runner.d.ts +8 -0
  35. package/dist/runtime/agent-runner.d.ts.map +1 -1
  36. package/dist/runtime/agent-runner.js +93 -4
  37. package/dist/runtime/agent-runner.js.map +1 -1
  38. package/dist/runtime/backends/claude.d.ts +1 -0
  39. package/dist/runtime/backends/claude.d.ts.map +1 -1
  40. package/dist/runtime/backends/claude.js +50 -2
  41. package/dist/runtime/backends/claude.js.map +1 -1
  42. package/dist/runtime/backends/codex.d.ts.map +1 -1
  43. package/dist/runtime/backends/codex.js +4 -0
  44. package/dist/runtime/backends/codex.js.map +1 -1
  45. package/dist/runtime/backends/gemini.d.ts.map +1 -1
  46. package/dist/runtime/backends/gemini.js +4 -0
  47. package/dist/runtime/backends/gemini.js.map +1 -1
  48. package/dist/runtime/benefits.d.ts +81 -1
  49. package/dist/runtime/benefits.d.ts.map +1 -1
  50. package/dist/runtime/benefits.js +199 -12
  51. package/dist/runtime/benefits.js.map +1 -1
  52. package/dist/runtime/cache-optimizer.d.ts +7 -3
  53. package/dist/runtime/cache-optimizer.d.ts.map +1 -1
  54. package/dist/runtime/cache-optimizer.js +11 -7
  55. package/dist/runtime/cache-optimizer.js.map +1 -1
  56. package/dist/runtime/compaction.d.ts +6 -1
  57. package/dist/runtime/compaction.d.ts.map +1 -1
  58. package/dist/runtime/compaction.js +39 -2
  59. package/dist/runtime/compaction.js.map +1 -1
  60. package/dist/runtime/cost-model.d.ts.map +1 -1
  61. package/dist/runtime/cost-model.js +20 -17
  62. package/dist/runtime/cost-model.js.map +1 -1
  63. package/dist/runtime/engine.d.ts +1 -0
  64. package/dist/runtime/engine.d.ts.map +1 -1
  65. package/dist/runtime/engine.js +62 -2
  66. package/dist/runtime/engine.js.map +1 -1
  67. package/dist/runtime/graph-discovery.js +2 -2
  68. package/dist/runtime/graph-discovery.js.map +1 -1
  69. package/dist/runtime/graph-trajectory.js +3 -3
  70. package/dist/runtime/graph-trajectory.js.map +1 -1
  71. package/dist/runtime/lsp.d.ts.map +1 -1
  72. package/dist/runtime/lsp.js +4 -0
  73. package/dist/runtime/lsp.js.map +1 -1
  74. package/dist/runtime/mcp.d.ts +1 -0
  75. package/dist/runtime/mcp.d.ts.map +1 -1
  76. package/dist/runtime/mcp.js +38 -0
  77. package/dist/runtime/mcp.js.map +1 -1
  78. package/dist/runtime/output-summarizer.d.ts +45 -0
  79. package/dist/runtime/output-summarizer.d.ts.map +1 -0
  80. package/dist/runtime/output-summarizer.js +171 -0
  81. package/dist/runtime/output-summarizer.js.map +1 -0
  82. package/dist/runtime/plugins.d.ts +5 -1
  83. package/dist/runtime/plugins.d.ts.map +1 -1
  84. package/dist/runtime/plugins.js +14 -2
  85. package/dist/runtime/plugins.js.map +1 -1
  86. package/dist/runtime/prompt-tier.d.ts +33 -0
  87. package/dist/runtime/prompt-tier.d.ts.map +1 -0
  88. package/dist/runtime/prompt-tier.js +105 -0
  89. package/dist/runtime/prompt-tier.js.map +1 -0
  90. package/dist/runtime/sharing.js +2 -1
  91. package/dist/runtime/sharing.js.map +1 -1
  92. package/dist/runtime/stats.d.ts +2 -0
  93. package/dist/runtime/stats.d.ts.map +1 -1
  94. package/dist/runtime/stats.js +17 -3
  95. package/dist/runtime/stats.js.map +1 -1
  96. package/dist/utils/project-config.d.ts +20 -0
  97. package/dist/utils/project-config.d.ts.map +1 -1
  98. package/dist/utils/project-config.js +46 -1
  99. package/dist/utils/project-config.js.map +1 -1
  100. package/dist/utils/redact.d.ts.map +1 -1
  101. package/dist/utils/redact.js +5 -1
  102. package/dist/utils/redact.js.map +1 -1
  103. package/dist/web/bridge.d.ts +47 -0
  104. package/dist/web/bridge.d.ts.map +1 -0
  105. package/dist/web/bridge.js +267 -0
  106. package/dist/web/bridge.js.map +1 -0
  107. package/dist/web/graph-api.d.ts +19 -0
  108. package/dist/web/graph-api.d.ts.map +1 -0
  109. package/dist/web/graph-api.js +157 -0
  110. package/dist/web/graph-api.js.map +1 -0
  111. package/dist/web/index.d.ts +21 -0
  112. package/dist/web/index.d.ts.map +1 -0
  113. package/dist/web/index.js +38 -0
  114. package/dist/web/index.js.map +1 -0
  115. package/dist/web/public/index.html +1304 -0
  116. package/dist/web/public/public/index.html +1307 -0
  117. package/dist/web/server.d.ts +24 -0
  118. package/dist/web/server.d.ts.map +1 -0
  119. package/dist/web/server.js +113 -0
  120. package/dist/web/server.js.map +1 -0
  121. package/dist/web/tray.d.ts +23 -0
  122. package/dist/web/tray.d.ts.map +1 -0
  123. package/dist/web/tray.js +205 -0
  124. package/dist/web/tray.js.map +1 -0
  125. package/package.json +1 -1
@@ -0,0 +1,1307 @@
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">
6
+ <title>Swarm Engine — Live Dashboard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@300;400;500;600;700&family=Source+Serif+4:ital,opsz,wght@0,8..60,300;0,8..60,400;0,8..60,600;0,8..60,700;1,8..60,400&display=swap" rel="stylesheet">
10
+ <style>
11
+ /* ─── Reset & Base ──────────────────────────────────────────────── */
12
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13
+
14
+ :root {
15
+ /* OKLCH surfaces — warm tinted, never pure black */
16
+ --surface-0: oklch(0.13 0.008 55);
17
+ --surface-1: oklch(0.16 0.008 55);
18
+ --surface-2: oklch(0.19 0.007 55);
19
+ --surface-3: oklch(0.23 0.006 55);
20
+ --surface-hover: oklch(0.25 0.008 55);
21
+
22
+ /* Text */
23
+ --text-primary: oklch(0.92 0.01 55);
24
+ --text-secondary: oklch(0.65 0.008 55);
25
+ --text-muted: oklch(0.48 0.006 55);
26
+
27
+ /* Brand: warm amber — NOT cyan, NOT purple, NOT neon */
28
+ --accent: oklch(0.75 0.14 65);
29
+ --accent-dim: oklch(0.55 0.10 65);
30
+ --accent-bg: oklch(0.20 0.03 65);
31
+
32
+ /* Status palette */
33
+ --status-running: oklch(0.78 0.15 85);
34
+ --status-done: oklch(0.72 0.16 155);
35
+ --status-error: oklch(0.65 0.20 25);
36
+ --status-waiting: oklch(0.42 0.005 55);
37
+ --status-spawning: oklch(0.70 0.12 85);
38
+ --status-timeout: oklch(0.60 0.15 25);
39
+
40
+ /* Graph node colors */
41
+ --node-orchestration: oklch(0.75 0.14 65);
42
+ --node-phase: oklch(0.68 0.12 200);
43
+ --node-agent: oklch(0.72 0.14 140);
44
+ --node-file: oklch(0.55 0.01 55);
45
+ --node-pattern: oklch(0.70 0.12 50);
46
+ --node-error: oklch(0.65 0.20 25);
47
+ --node-heuristic: oklch(0.55 0.06 280);
48
+
49
+ /* Border */
50
+ --border: oklch(0.25 0.005 55);
51
+ --border-subtle: oklch(0.20 0.005 55);
52
+
53
+ /* Spacing scale: 4pt */
54
+ --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
55
+ --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;
56
+
57
+ /* Type */
58
+ --font-body: 'Inconsolata', monospace;
59
+ --font-display: 'Source Serif 4', serif;
60
+ --font-size-xs: 0.6875rem;
61
+ --font-size-sm: 0.8125rem;
62
+ --font-size-base: 0.9375rem;
63
+ --font-size-lg: 1.125rem;
64
+ --font-size-md: 1rem;
65
+ --font-size-xl: 1.5rem;
66
+ --font-size-2xl: 2rem;
67
+ }
68
+
69
+ @media (prefers-reduced-motion: reduce) {
70
+ *, *::before, *::after {
71
+ animation-duration: 0.01ms !important;
72
+ transition-duration: 0.01ms !important;
73
+ }
74
+ }
75
+
76
+ html { font-size: 16px; }
77
+ body {
78
+ font-family: var(--font-body);
79
+ font-size: var(--font-size-base);
80
+ font-weight: 400;
81
+ color: var(--text-primary);
82
+ background: var(--surface-0);
83
+ line-height: 1.5;
84
+ min-height: 100vh;
85
+ -webkit-font-smoothing: antialiased;
86
+ }
87
+
88
+ /* ─── Layout ────────────────────────────────────────────────────── */
89
+ .app {
90
+ display: grid;
91
+ grid-template-rows: auto 1fr;
92
+ min-height: 100vh;
93
+ }
94
+
95
+ .header {
96
+ display: grid;
97
+ grid-template-columns: 1fr auto;
98
+ align-items: start;
99
+ padding: var(--sp-5) var(--sp-6) var(--sp-4);
100
+ border-bottom: 1px solid var(--border);
101
+ gap: var(--sp-5);
102
+ }
103
+
104
+ .header-left { display: flex; flex-direction: column; gap: var(--sp-2); }
105
+
106
+ .orch-name {
107
+ font-family: var(--font-display);
108
+ font-size: var(--font-size-xl);
109
+ font-weight: 600;
110
+ color: var(--text-primary);
111
+ letter-spacing: -0.02em;
112
+ line-height: 1.2;
113
+ overflow: hidden;
114
+ text-overflow: ellipsis;
115
+ white-space: nowrap;
116
+ max-width: 100%;
117
+ }
118
+
119
+ .header-meta {
120
+ display: flex;
121
+ gap: var(--sp-5);
122
+ align-items: center;
123
+ flex-wrap: wrap;
124
+ }
125
+
126
+ .meta-item {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: var(--sp-2);
130
+ font-size: var(--font-size-sm);
131
+ color: var(--text-secondary);
132
+ }
133
+
134
+ .meta-label { color: var(--text-muted); }
135
+ .meta-value { color: var(--text-secondary); font-weight: 500; }
136
+ .meta-value.accent { color: var(--accent); }
137
+
138
+ .header-right {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: var(--sp-5);
142
+ }
143
+
144
+ .conn-status {
145
+ display: flex;
146
+ align-items: center;
147
+ gap: var(--sp-2);
148
+ font-size: var(--font-size-xs);
149
+ color: var(--text-muted);
150
+ }
151
+
152
+ .conn-dot {
153
+ width: 7px; height: 7px;
154
+ border-radius: 50%;
155
+ background: var(--status-done);
156
+ flex-shrink: 0;
157
+ }
158
+ .conn-dot.connected { background: var(--status-done); }
159
+ .conn-dot.disconnected { background: var(--status-error); box-shadow: 0 0 6px var(--status-error); }
160
+ .conn-status.disconnected { color: var(--status-error); font-weight: 600; }
161
+ .conn-dot.reconnecting {
162
+ background: var(--status-running);
163
+ animation: pulse 1.5s ease-in-out infinite;
164
+ }
165
+
166
+ @keyframes pulse {
167
+ 0%, 100% { opacity: 1; }
168
+ 50% { opacity: 0.3; }
169
+ }
170
+
171
+ /* ─── Tabs ──────────────────────────────────────────────────────── */
172
+ .tab-bar {
173
+ display: flex;
174
+ gap: 0;
175
+ padding: 0 var(--sp-6);
176
+ border-bottom: 1px solid var(--border);
177
+ background: var(--surface-0);
178
+ }
179
+
180
+ .tab-btn {
181
+ background: none;
182
+ border: none;
183
+ border-bottom: 2px solid transparent;
184
+ padding: var(--sp-3) var(--sp-5);
185
+ font-family: var(--font-body);
186
+ font-size: var(--font-size-sm);
187
+ font-weight: 500;
188
+ color: var(--text-muted);
189
+ cursor: pointer;
190
+ transition: color 0.15s ease-out, border-color 0.15s ease-out;
191
+ letter-spacing: 0.03em;
192
+ text-transform: uppercase;
193
+ }
194
+ .tab-btn:hover { color: var(--text-secondary); }
195
+ .tab-btn.active {
196
+ color: var(--accent);
197
+ border-bottom-color: var(--accent);
198
+ }
199
+
200
+ /* ─── Tab Panels ────────────────────────────────────────────────── */
201
+ .tab-panels { overflow: hidden; position: relative; }
202
+ .tab-panel {
203
+ display: none;
204
+ height: 100%;
205
+ }
206
+ .tab-panel.active { display: grid; }
207
+
208
+ /* ─── Activity Tab ──────────────────────────────────────────────── */
209
+ .activity-panel {
210
+ grid-template-rows: 1fr auto;
211
+ overflow: hidden;
212
+ }
213
+
214
+ .phases-scroll {
215
+ overflow-y: auto;
216
+ padding: var(--sp-5) var(--sp-6);
217
+ scrollbar-width: thin;
218
+ scrollbar-color: var(--surface-3) transparent;
219
+ }
220
+
221
+ .phases-empty {
222
+ color: var(--text-muted);
223
+ font-size: var(--font-size-sm);
224
+ padding: var(--sp-7) 0;
225
+ }
226
+
227
+ /* Phase */
228
+ .phase {
229
+ margin-bottom: var(--sp-5);
230
+ }
231
+ .phase:last-child { margin-bottom: 0; }
232
+
233
+ .phase-header {
234
+ display: grid;
235
+ grid-template-columns: auto 1fr auto auto;
236
+ align-items: center;
237
+ gap: var(--sp-3);
238
+ padding-bottom: var(--sp-2);
239
+ }
240
+
241
+ .phase-indicator {
242
+ width: 6px; height: 6px;
243
+ border-radius: 50%;
244
+ background: var(--status-waiting);
245
+ flex-shrink: 0;
246
+ }
247
+ .phase-indicator.running { background: var(--status-running); animation: pulse 1.5s ease-in-out infinite; }
248
+ .phase-indicator.completed { background: var(--status-done); }
249
+ .phase-indicator.failed { background: var(--status-error); }
250
+ .phase-indicator.skipped { background: var(--text-muted); }
251
+
252
+ .phase-name {
253
+ font-weight: 600;
254
+ font-size: var(--font-size-base);
255
+ color: var(--text-primary);
256
+ white-space: nowrap;
257
+ overflow: hidden;
258
+ text-overflow: ellipsis;
259
+ }
260
+
261
+ .phase-agents-count {
262
+ font-size: var(--font-size-xs);
263
+ color: var(--text-muted);
264
+ }
265
+
266
+ .phase-elapsed {
267
+ font-size: var(--font-size-xs);
268
+ color: var(--text-muted);
269
+ font-variant-numeric: tabular-nums;
270
+ }
271
+
272
+ /* Agent rows */
273
+ .agents-list {
274
+ display: grid;
275
+ gap: var(--sp-1);
276
+ padding-left: var(--sp-4);
277
+ }
278
+
279
+ .agent-row {
280
+ display: grid;
281
+ grid-template-columns: var(--sp-4) minmax(100px, 180px) minmax(60px, 90px) minmax(60px, 80px) minmax(50px, 70px) 1fr;
282
+ align-items: center;
283
+ gap: var(--sp-3);
284
+ padding: var(--sp-1) var(--sp-2);
285
+ font-size: var(--font-size-sm);
286
+ border-radius: 3px;
287
+ transition: background 0.1s ease-out;
288
+ }
289
+ .agent-row:hover { background: var(--surface-hover); }
290
+
291
+ .agent-dot {
292
+ width: 5px; height: 5px;
293
+ border-radius: 50%;
294
+ justify-self: center;
295
+ }
296
+ .agent-dot.waiting { background: var(--status-waiting); }
297
+ .agent-dot.spawning { background: var(--status-spawning); animation: pulse 1s ease-in-out infinite; }
298
+ .agent-dot.running { background: var(--status-running); animation: pulse 1.2s ease-in-out infinite; }
299
+ .agent-dot.done { background: var(--status-done); }
300
+ .agent-dot.error { background: var(--status-error); }
301
+ .agent-dot.timeout { background: var(--status-timeout); }
302
+
303
+ .agent-name {
304
+ color: var(--text-primary);
305
+ font-weight: 500;
306
+ white-space: nowrap;
307
+ overflow: hidden;
308
+ text-overflow: ellipsis;
309
+ }
310
+
311
+ .agent-type {
312
+ color: var(--text-muted);
313
+ font-size: var(--font-size-xs);
314
+ white-space: nowrap;
315
+ overflow: hidden;
316
+ text-overflow: ellipsis;
317
+ }
318
+
319
+ .agent-model {
320
+ color: var(--text-muted);
321
+ font-size: var(--font-size-xs);
322
+ white-space: nowrap;
323
+ overflow: hidden;
324
+ text-overflow: ellipsis;
325
+ }
326
+
327
+ .agent-tokens {
328
+ color: var(--text-muted);
329
+ font-size: var(--font-size-xs);
330
+ font-variant-numeric: tabular-nums;
331
+ text-align: right;
332
+ }
333
+
334
+ .agent-detail {
335
+ color: var(--text-muted);
336
+ font-size: var(--font-size-xs);
337
+ white-space: nowrap;
338
+ overflow: hidden;
339
+ text-overflow: ellipsis;
340
+ max-width: 75ch;
341
+ }
342
+
343
+ /* ─── Event Feed ────────────────────────────────────────────────── */
344
+ .event-feed {
345
+ border-top: 1px solid var(--border);
346
+ background: var(--surface-1);
347
+ max-height: 200px;
348
+ overflow-y: auto;
349
+ scrollbar-width: thin;
350
+ scrollbar-color: var(--surface-3) transparent;
351
+ }
352
+
353
+ .event-feed-header {
354
+ position: sticky;
355
+ top: 0;
356
+ z-index: 1;
357
+ background: var(--surface-1);
358
+ padding: var(--sp-2) var(--sp-6);
359
+ font-size: var(--font-size-xs);
360
+ color: var(--text-muted);
361
+ text-transform: uppercase;
362
+ letter-spacing: 0.05em;
363
+ font-weight: 600;
364
+ border-bottom: 1px solid var(--border-subtle);
365
+ }
366
+
367
+ .event-row {
368
+ display: grid;
369
+ grid-template-columns: 70px 150px 80px 1fr;
370
+ gap: var(--sp-3);
371
+ padding: 2px var(--sp-6);
372
+ font-size: var(--font-size-xs);
373
+ line-height: 1.6;
374
+ }
375
+
376
+ .event-time { color: var(--text-muted); font-variant-numeric: tabular-nums; }
377
+ .event-type { color: var(--accent-dim); }
378
+ .event-source { color: var(--text-secondary); }
379
+ .event-detail { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
380
+
381
+ /* ─── Graph Tab ─────────────────────────────────────────────────── */
382
+ .graph-panel {
383
+ grid-template-rows: auto 1fr;
384
+ overflow: hidden;
385
+ }
386
+
387
+ .graph-toolbar {
388
+ display: flex;
389
+ align-items: center;
390
+ gap: var(--sp-4);
391
+ padding: var(--sp-3) var(--sp-6);
392
+ border-bottom: 1px solid var(--border-subtle);
393
+ }
394
+
395
+ .graph-btn {
396
+ background: var(--surface-2);
397
+ border: 1px solid var(--border);
398
+ color: var(--text-secondary);
399
+ font-family: var(--font-body);
400
+ font-size: var(--font-size-xs);
401
+ padding: var(--sp-1) var(--sp-3);
402
+ border-radius: 3px;
403
+ cursor: pointer;
404
+ transition: background 0.1s ease-out, color 0.1s ease-out;
405
+ text-transform: uppercase;
406
+ letter-spacing: 0.04em;
407
+ font-weight: 500;
408
+ }
409
+ .graph-btn:hover { background: var(--surface-3); color: var(--text-primary); }
410
+
411
+ .graph-stats {
412
+ font-size: var(--font-size-xs);
413
+ color: var(--text-muted);
414
+ margin-left: auto;
415
+ }
416
+
417
+
418
+ .metrics-container {
419
+ padding: var(--sp-4);
420
+ overflow-y: auto;
421
+ }
422
+
423
+ .metrics-grid {
424
+ display: grid;
425
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
426
+ gap: var(--sp-3);
427
+ margin-bottom: var(--sp-6);
428
+ }
429
+
430
+ .metric-card {
431
+ background: var(--surface-1);
432
+ padding: var(--sp-4) var(--sp-3);
433
+ border: 1px solid var(--border);
434
+ }
435
+
436
+ .metric-value {
437
+ font-family: 'Inconsolata', monospace;
438
+ font-size: 2rem;
439
+ font-weight: 600;
440
+ color: var(--accent);
441
+ line-height: 1;
442
+ }
443
+
444
+ .metric-label {
445
+ font-size: var(--font-size-xs);
446
+ color: var(--text-muted);
447
+ margin-top: var(--sp-1);
448
+ text-transform: uppercase;
449
+ letter-spacing: 0.05em;
450
+ }
451
+
452
+ .metrics-section {
453
+ margin-bottom: var(--sp-6);
454
+ }
455
+
456
+ .metrics-heading {
457
+ font-family: 'Source Serif 4', serif;
458
+ font-size: var(--font-size-md);
459
+ font-weight: 600;
460
+ color: var(--text-primary);
461
+ margin-bottom: var(--sp-3);
462
+ }
463
+
464
+ .bar-chart {
465
+ display: grid;
466
+ gap: var(--sp-2);
467
+ }
468
+
469
+ .bar-row {
470
+ display: grid;
471
+ grid-template-columns: 120px 1fr 60px;
472
+ align-items: center;
473
+ gap: var(--sp-3);
474
+ }
475
+
476
+ .bar-label {
477
+ font-family: 'Inconsolata', monospace;
478
+ font-size: var(--font-size-sm);
479
+ color: var(--text-secondary);
480
+ text-align: right;
481
+ }
482
+
483
+ .bar-track {
484
+ height: 20px;
485
+ background: var(--surface-1);
486
+ border: 1px solid var(--border);
487
+ overflow: hidden;
488
+ }
489
+
490
+ .bar-fill {
491
+ height: 100%;
492
+ transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
493
+ }
494
+
495
+ .bar-count {
496
+ font-family: 'Inconsolata', monospace;
497
+ font-size: var(--font-size-sm);
498
+ color: var(--text-muted);
499
+ text-align: right;
500
+ }
501
+
502
+ .orchestration-list {
503
+ display: grid;
504
+ gap: var(--sp-2);
505
+ }
506
+
507
+ .orch-row {
508
+ display: grid;
509
+ grid-template-columns: 1fr auto auto;
510
+ gap: var(--sp-3);
511
+ padding: var(--sp-2) var(--sp-3);
512
+ background: var(--surface-1);
513
+ border: 1px solid var(--border);
514
+ align-items: center;
515
+ }
516
+
517
+ .orch-name {
518
+ font-family: 'Inconsolata', monospace;
519
+ font-size: var(--font-size-sm);
520
+ color: var(--text-primary);
521
+ overflow: hidden;
522
+ text-overflow: ellipsis;
523
+ white-space: nowrap;
524
+ }
525
+
526
+ .orch-date {
527
+ font-size: var(--font-size-xs);
528
+ color: var(--text-muted);
529
+ }
530
+
531
+ .orch-phases {
532
+ font-family: 'Inconsolata', monospace;
533
+ font-size: var(--font-size-xs);
534
+ color: var(--accent-dim);
535
+ }
536
+
537
+ /* ─── Entrance animation ────────────────────────────────────────── */
538
+ @keyframes fadeSlideIn {
539
+ from { opacity: 0; transform: translateY(6px); }
540
+ to { opacity: 1; transform: translateY(0); }
541
+ }
542
+
543
+ .phase { animation: fadeSlideIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) both; }
544
+ .phase:nth-child(1) { animation-delay: 0.04s; }
545
+ .phase:nth-child(2) { animation-delay: 0.08s; }
546
+ .phase:nth-child(3) { animation-delay: 0.12s; }
547
+ .phase:nth-child(4) { animation-delay: 0.16s; }
548
+ .phase:nth-child(5) { animation-delay: 0.20s; }
549
+ .phase:nth-child(6) { animation-delay: 0.24s; }
550
+
551
+ .agent-row { animation: fadeSlideIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) both; }
552
+
553
+ /* ─── Orch status badge ─────────────────────────────────────────── */
554
+ .orch-status {
555
+ display: inline-flex;
556
+ align-items: center;
557
+ gap: var(--sp-2);
558
+ padding: 2px var(--sp-3);
559
+ font-size: var(--font-size-xs);
560
+ font-weight: 600;
561
+ text-transform: uppercase;
562
+ letter-spacing: 0.05em;
563
+ border-radius: 2px;
564
+ }
565
+ .orch-status.pending { color: var(--text-muted); background: var(--surface-2); }
566
+ .orch-status.running { color: var(--status-running); background: oklch(0.20 0.02 85); }
567
+ .orch-status.completed { color: var(--status-done); background: oklch(0.20 0.03 155); }
568
+ .orch-status.failed { color: var(--status-error); background: oklch(0.20 0.03 25); }
569
+
570
+ /* ─── Responsive ───────────────────────────────────────────────── */
571
+ @media (max-width: 768px) {
572
+ .dashboard-header {
573
+ flex-direction: column;
574
+ gap: var(--sp-3);
575
+ }
576
+ .header-meta {
577
+ flex-wrap: wrap;
578
+ gap: var(--sp-3);
579
+ }
580
+ .agent-row {
581
+ grid-template-columns: 8px 1fr 1fr;
582
+ gap: var(--sp-2);
583
+ }
584
+ .agent-row > :nth-child(n+4) { display: none; }
585
+ .event-row {
586
+ grid-template-columns: 55px 120px 1fr;
587
+ }
588
+ .event-row .event-source { display: none; }
589
+ .metrics-grid {
590
+ grid-template-columns: repeat(2, 1fr);
591
+ }
592
+ .bar-row {
593
+ grid-template-columns: 80px 1fr 50px;
594
+ }
595
+ }
596
+
597
+ @media (max-width: 480px) {
598
+ .orch-name { font-size: var(--font-size-lg); }
599
+ .metrics-grid { grid-template-columns: 1fr; }
600
+ .tab-btn { font-size: var(--font-size-xs); padding: var(--sp-2) var(--sp-3); }
601
+ }
602
+ </style>
603
+ </head>
604
+ <body>
605
+ <div class="app">
606
+
607
+ <!-- ─── Header ─────────────────────────────────────────────── -->
608
+ <header class="header" role="banner">
609
+ <div class="header-left">
610
+ <h1 class="orch-name" id="orchName">Waiting for orchestration...</h1>
611
+ <div class="header-meta">
612
+ <div class="meta-item">
613
+ <span class="meta-label">Pattern</span>
614
+ <span class="meta-value" id="orchPattern">—</span>
615
+ </div>
616
+ <span class="orch-status pending" id="orchStatus" aria-live="polite">pending</span>
617
+ <div class="meta-item">
618
+ <span class="meta-label">Elapsed</span>
619
+ <span class="meta-value accent" id="orchElapsed">0s</span>
620
+ </div>
621
+ <div class="meta-item">
622
+ <span class="meta-label">Tokens</span>
623
+ <span class="meta-value" id="orchTokens">0</span>
624
+ </div>
625
+ <div class="meta-item">
626
+ <span class="meta-label">Cost</span>
627
+ <span class="meta-value" id="orchCost">$0.00</span>
628
+ </div>
629
+ </div>
630
+ </div>
631
+ <div class="header-right">
632
+ <div class="conn-status" id="connStatus" aria-live="polite">
633
+ <span class="conn-dot" id="connDot"></span>
634
+ <span id="connLabel">connecting</span>
635
+ </div>
636
+ </div>
637
+ </header>
638
+
639
+ <!-- ─── Tab Bar ────────────────────────────────────────────── -->
640
+ <main>
641
+ <div class="tab-bar" role="tablist">
642
+ <button class="tab-btn active" id="tab-activity" data-tab="activity" role="tab" aria-selected="true" aria-controls="panel-activity">Activity</button>
643
+ <button class="tab-btn" id="tab-graph" data-tab="graph" role="tab" aria-selected="false" aria-controls="panel-graph">Knowledge Graph</button>
644
+ </div>
645
+
646
+ <!-- ─── Tab Panels ─────────────────────────────────────────── -->
647
+ <div class="tab-panels">
648
+
649
+ <!-- Activity -->
650
+ <div class="tab-panel activity-panel active" id="panel-activity" role="tabpanel" aria-labelledby="tab-activity">
651
+ <div class="phases-scroll" id="phasesContainer">
652
+ <div class="phases-empty">Waiting for orchestration events...</div>
653
+ </div>
654
+ <div class="event-feed" id="eventFeed">
655
+ <div class="event-feed-header">Event Log</div>
656
+ </div>
657
+ </div>
658
+
659
+ <!-- Knowledge Graph Metrics -->
660
+ <div class="tab-panel graph-panel" id="panel-graph" role="tabpanel" aria-labelledby="tab-graph">
661
+ <div class="graph-toolbar">
662
+ <button class="graph-btn" id="graphRefresh">Refresh</button>
663
+ <div class="graph-stats" id="graphStats">—</div>
664
+ </div>
665
+ <div class="metrics-container" id="metricsContainer">
666
+ <div class="metrics-grid">
667
+ <div class="metric-card metric-total">
668
+ <div class="metric-value" id="metricNodes">—</div>
669
+ <div class="metric-label">Total Nodes</div>
670
+ </div>
671
+ <div class="metric-card metric-total">
672
+ <div class="metric-value" id="metricEdges">—</div>
673
+ <div class="metric-label">Total Edges</div>
674
+ </div>
675
+ <div class="metric-card metric-total">
676
+ <div class="metric-value" id="metricOrchestrations">—</div>
677
+ <div class="metric-label">Orchestrations</div>
678
+ </div>
679
+ <div class="metric-card metric-total">
680
+ <div class="metric-value" id="metricPatterns">—</div>
681
+ <div class="metric-label">Patterns</div>
682
+ </div>
683
+ </div>
684
+
685
+ <div class="metrics-section">
686
+ <h3 class="metrics-heading">Nodes by Type</h3>
687
+ <div class="bar-chart" id="nodesByType"></div>
688
+ </div>
689
+
690
+ <div class="metrics-section">
691
+ <h3 class="metrics-heading">Edges by Type</h3>
692
+ <div class="bar-chart" id="edgesByType"></div>
693
+ </div>
694
+
695
+ <div class="metrics-section">
696
+ <h3 class="metrics-heading">Recent Orchestrations</h3>
697
+ <div class="orchestration-list" id="recentOrchestrations"></div>
698
+ </div>
699
+ </div>
700
+ </div>
701
+
702
+ </div>
703
+ </main>
704
+ </div>
705
+
706
+ <script>
707
+ /* ═══════════════════════════════════════════════════════════════════
708
+ Swarm Engine — Live Web Dashboard
709
+ ═══════════════════════════════════════════════════════════════════ */
710
+
711
+ (function() {
712
+ 'use strict';
713
+
714
+ // ─── State ─────────────────────────────────────────────────────────
715
+
716
+ const state = {
717
+ orchestrationName: 'Waiting for orchestration...',
718
+ pattern: '',
719
+ status: 'pending',
720
+ startedAt: null,
721
+ phases: [],
722
+ usage: { tokens: 0, cost: 0, turns: 0, toolCalls: 0 },
723
+ };
724
+
725
+ const events = []; // last 50 events
726
+ const MAX_EVENTS = 50;
727
+
728
+ // ─── DOM refs ──────────────────────────────────────────────────────
729
+
730
+ const $orchName = document.getElementById('orchName');
731
+ const $orchPattern = document.getElementById('orchPattern');
732
+ const $orchStatus = document.getElementById('orchStatus');
733
+ const $orchElapsed = document.getElementById('orchElapsed');
734
+ const $orchTokens = document.getElementById('orchTokens');
735
+ const $orchCost = document.getElementById('orchCost');
736
+ const $connDot = document.getElementById('connDot');
737
+ const $connLabel = document.getElementById('connLabel');
738
+ const $phasesContainer = document.getElementById('phasesContainer');
739
+ const $eventFeed = document.getElementById('eventFeed');
740
+ const $graphStats = document.getElementById('graphStats');
741
+ const $metricsContainer = document.getElementById('metricsContainer');
742
+
743
+ // ─── Utilities ─────────────────────────────────────────────────────
744
+
745
+ function formatNum(n) {
746
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
747
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'k';
748
+ return String(n);
749
+ }
750
+
751
+ function formatElapsed(ms) {
752
+ if (ms < 0) ms = 0;
753
+ const s = Math.floor(ms / 1000);
754
+ if (s < 60) return s + 's';
755
+ const m = Math.floor(s / 60);
756
+ const rem = s % 60;
757
+ if (m < 60) return m + 'm ' + rem + 's';
758
+ const h = Math.floor(m / 60);
759
+ return h + 'h ' + (m % 60) + 'm';
760
+ }
761
+
762
+ function formatTime(iso) {
763
+ try {
764
+ const d = new Date(iso);
765
+ return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
766
+ } catch { return '—'; }
767
+ }
768
+
769
+ function escHtml(s) {
770
+ const d = document.createElement('div');
771
+ d.textContent = s;
772
+ return d.innerHTML;
773
+ }
774
+
775
+ // ─── Tab Switching ─────────────────────────────────────────────────
776
+
777
+ let activeTab = 'activity';
778
+
779
+ const tabBtns = [...document.querySelectorAll('.tab-btn')];
780
+
781
+ function activateTab(btn) {
782
+ if (btn.dataset.tab === activeTab) return;
783
+ const prevBtn = document.querySelector('.tab-btn.active');
784
+ prevBtn.classList.remove('active');
785
+ prevBtn.setAttribute('aria-selected', 'false');
786
+ prevBtn.tabIndex = -1;
787
+ btn.classList.add('active');
788
+ btn.setAttribute('aria-selected', 'true');
789
+ btn.tabIndex = 0;
790
+ btn.focus();
791
+ document.querySelector('.tab-panel.active').classList.remove('active');
792
+ document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
793
+ activeTab = btn.dataset.tab;
794
+ if (activeTab === 'graph') fetchMetrics();
795
+ }
796
+
797
+ tabBtns.forEach((btn, i) => {
798
+ btn.addEventListener('click', () => activateTab(btn));
799
+ btn.tabIndex = i === 0 ? 0 : -1;
800
+ btn.addEventListener('keydown', (e) => {
801
+ let target = null;
802
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') target = tabBtns[(i + 1) % tabBtns.length];
803
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') target = tabBtns[(i - 1 + tabBtns.length) % tabBtns.length];
804
+ if (e.key === 'Home') target = tabBtns[0];
805
+ if (e.key === 'End') target = tabBtns[tabBtns.length - 1];
806
+ if (target) { e.preventDefault(); activateTab(target); }
807
+ });
808
+ });
809
+
810
+ // ─── Elapsed Timer ─────────────────────────────────────────────────
811
+
812
+ setInterval(() => {
813
+ if (state.startedAt && state.status === 'running') {
814
+ $orchElapsed.textContent = formatElapsed(Date.now() - new Date(state.startedAt).getTime());
815
+ }
816
+ }, 1000);
817
+
818
+ // ═══════════════════════════════════════════════════════════════════
819
+ // WebSocket Connection
820
+ // ═══════════════════════════════════════════════════════════════════
821
+
822
+ let ws = null;
823
+ let reconnectDelay = 1000;
824
+ let reconnectTimer = null;
825
+
826
+ function setConnStatus(status) {
827
+ const $connStatus = document.getElementById('connStatus');
828
+ $connDot.className = 'conn-dot';
829
+ $connStatus.className = 'conn-status';
830
+ if (status === 'connected') {
831
+ $connDot.classList.add('connected');
832
+ $connLabel.textContent = 'connected';
833
+ } else if (status === 'disconnected') {
834
+ $connDot.classList.add('disconnected');
835
+ $connStatus.classList.add('disconnected');
836
+ $connLabel.textContent = 'disconnected';
837
+ } else {
838
+ $connDot.classList.add('reconnecting');
839
+ $connLabel.textContent = 'reconnecting...';
840
+ }
841
+ }
842
+
843
+ function connect() {
844
+ setConnStatus('reconnecting');
845
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
846
+ ws = new WebSocket(proto + '//' + location.host);
847
+
848
+ ws.onopen = () => {
849
+ reconnectDelay = 1000;
850
+ setConnStatus('connected');
851
+ };
852
+
853
+ ws.onmessage = (evt) => {
854
+ try {
855
+ const msg = JSON.parse(evt.data);
856
+ if (msg.type === 'snapshot') {
857
+ applySnapshot(msg.data);
858
+ } else if (msg.type === 'event') {
859
+ handleEvent(msg.data);
860
+ }
861
+ } catch (e) {
862
+ console.warn('WS parse error:', e);
863
+ }
864
+ };
865
+
866
+ ws.onclose = () => {
867
+ setConnStatus('disconnected');
868
+ scheduleReconnect();
869
+ };
870
+
871
+ ws.onerror = () => {
872
+ ws.close();
873
+ };
874
+ }
875
+
876
+ function scheduleReconnect() {
877
+ if (reconnectTimer) return;
878
+ setConnStatus('reconnecting');
879
+ reconnectTimer = setTimeout(() => {
880
+ reconnectTimer = null;
881
+ reconnectDelay = Math.min(reconnectDelay * 2, 30000);
882
+ connect();
883
+ }, reconnectDelay);
884
+ }
885
+
886
+ // ─── Snapshot ──────────────────────────────────────────────────────
887
+
888
+ function applySnapshot(data) {
889
+ state.orchestrationName = data.orchestrationName || 'Orchestration';
890
+ state.pattern = data.pattern || '';
891
+ state.status = data.status || 'pending';
892
+ state.startedAt = data.startedAt || null;
893
+ state.phases = (data.phases || []).map(p => ({
894
+ name: p.name,
895
+ status: p.status || 'pending',
896
+ agents: (p.agents || []).map(a => ({ ...a })),
897
+ startedAt: p.startedAt || null,
898
+ completedAt: p.completedAt || null,
899
+ }));
900
+ state.usage = { ...state.usage, ...(data.usage || {}) };
901
+ renderAll();
902
+ }
903
+
904
+ // ─── Event Handling ────────────────────────────────────────────────
905
+
906
+ function handleEvent(evt) {
907
+ // Push to event log
908
+ events.push(evt);
909
+ if (events.length > MAX_EVENTS) events.shift();
910
+
911
+ const data = evt.data || {};
912
+ const type = evt.type;
913
+
914
+ switch (type) {
915
+ case 'orchestration:start':
916
+ state.orchestrationName = data.name || 'Orchestration';
917
+ state.pattern = data.pattern || '';
918
+ state.status = 'running';
919
+ state.startedAt = evt.timestamp || new Date().toISOString();
920
+ if (state.phases.length === 0 && data.phaseCount > 0) {
921
+ for (let i = 0; i < data.phaseCount; i++) {
922
+ state.phases.push(makePhase('Phase ' + (i + 1)));
923
+ }
924
+ }
925
+ break;
926
+
927
+ case 'orchestration:phase-start': {
928
+ const phase = findOrCreatePhase(data.phase || '');
929
+ phase.status = 'running';
930
+ phase.startedAt = evt.timestamp || new Date().toISOString();
931
+ if (phase.agents.length === 0 && data.agentCount > 0) {
932
+ for (let i = 0; i < data.agentCount; i++) {
933
+ phase.agents.push(makeAgent('agent-' + (i + 1)));
934
+ }
935
+ }
936
+ break;
937
+ }
938
+
939
+ case 'orchestration:phase-complete': {
940
+ const phase = findPhase(data.phase || '');
941
+ if (phase) {
942
+ phase.status = data.status === 'failed' ? 'failed' : 'completed';
943
+ phase.completedAt = evt.timestamp || new Date().toISOString();
944
+ }
945
+ if (data.usage) {
946
+ state.usage.tokens += data.usage.totalTokens || 0;
947
+ state.usage.cost += data.usage.costUsd || 0;
948
+ state.usage.turns += data.usage.turns || 0;
949
+ state.usage.toolCalls += data.usage.toolCalls || 0;
950
+ }
951
+ break;
952
+ }
953
+
954
+ case 'orchestration:complete':
955
+ state.status = 'completed';
956
+ break;
957
+
958
+ case 'orchestration:failed':
959
+ state.status = 'failed';
960
+ break;
961
+
962
+ case 'agent:spawning': {
963
+ const agent = findOrCreateAgent(data);
964
+ agent.status = 'spawning';
965
+ break;
966
+ }
967
+
968
+ case 'agent:running': {
969
+ const agent = findOrCreateAgent(data);
970
+ agent.status = 'running';
971
+ if (data.backend) agent.backend = data.backend;
972
+ if (data.task) agent.detail = String(data.task).slice(0, 120);
973
+ break;
974
+ }
975
+
976
+ case 'agent:idle': {
977
+ const agent = findOrCreateAgent(data);
978
+ agent.status = 'done';
979
+ if (data.usage) agent.tokens = data.usage.totalTokens || 0;
980
+ if (data.result && data.result.filesTouched) {
981
+ const files = data.result.filesTouched;
982
+ agent.detail = files.length > 0 ? files.length + ' files' : '';
983
+ }
984
+ break;
985
+ }
986
+
987
+ case 'agent:error': {
988
+ const agent = findOrCreateAgent(data);
989
+ agent.status = 'error';
990
+ agent.detail = String(data.error || 'unknown error').slice(0, 80);
991
+ break;
992
+ }
993
+
994
+ case 'agent:timeout': {
995
+ const agent = findOrCreateAgent(data);
996
+ agent.status = 'timeout';
997
+ agent.detail = 'timed out';
998
+ break;
999
+ }
1000
+ }
1001
+
1002
+ renderAll();
1003
+ }
1004
+
1005
+ // ─── State Helpers ─────────────────────────────────────────────────
1006
+
1007
+ function makePhase(name) {
1008
+ return { name, status: 'pending', agents: [], startedAt: null, completedAt: null };
1009
+ }
1010
+
1011
+ function makeAgent(name) {
1012
+ return { id: '', name, type: '', model: '', backend: '', status: 'waiting', tokens: 0, detail: '' };
1013
+ }
1014
+
1015
+ function findPhase(name) {
1016
+ return state.phases.find(p => p.name === name);
1017
+ }
1018
+
1019
+ function findOrCreatePhase(name) {
1020
+ let phase = state.phases.find(p => p.name === name);
1021
+ if (!phase) {
1022
+ // Try to fill an unnamed placeholder
1023
+ phase = state.phases.find(p => p.name.startsWith('Phase ') && p.status === 'pending');
1024
+ if (phase) {
1025
+ phase.name = name;
1026
+ } else {
1027
+ phase = makePhase(name);
1028
+ state.phases.push(phase);
1029
+ }
1030
+ }
1031
+ return phase;
1032
+ }
1033
+
1034
+ function findOrCreateAgent(data) {
1035
+ const agentId = data.agentId || '';
1036
+ const agentType = data.agentType || '';
1037
+
1038
+ // Search across all phases
1039
+ for (const phase of state.phases) {
1040
+ const existing = phase.agents.find(a => a.id === agentId && agentId !== '');
1041
+ if (existing) return existing;
1042
+ }
1043
+
1044
+ // Find the running phase
1045
+ const runningPhase = state.phases.find(p => p.status === 'running');
1046
+ if (runningPhase) {
1047
+ // Fill a waiting slot
1048
+ const slot = runningPhase.agents.find(a => a.status === 'waiting' && a.id === '');
1049
+ if (slot) {
1050
+ slot.id = agentId;
1051
+ slot.name = agentType || slot.name;
1052
+ slot.type = agentType;
1053
+ return slot;
1054
+ }
1055
+ const agent = { id: agentId, name: agentType, type: agentType, model: data.model || '', backend: data.backend || '', status: 'waiting', tokens: 0, detail: '' };
1056
+ runningPhase.agents.push(agent);
1057
+ return agent;
1058
+ }
1059
+
1060
+ // Last phase fallback
1061
+ const last = state.phases[state.phases.length - 1];
1062
+ if (last) {
1063
+ const agent = { id: agentId, name: agentType, type: agentType, model: data.model || '', backend: data.backend || '', status: 'waiting', tokens: 0, detail: '' };
1064
+ last.agents.push(agent);
1065
+ return agent;
1066
+ }
1067
+
1068
+ // No phases — create one
1069
+ const phase = makePhase('Phase 1');
1070
+ phase.status = 'running';
1071
+ phase.startedAt = new Date().toISOString();
1072
+ state.phases.push(phase);
1073
+ const agent = { id: agentId, name: agentType, type: agentType, model: data.model || '', backend: data.backend || '', status: 'waiting', tokens: 0, detail: '' };
1074
+ phase.agents.push(agent);
1075
+ return agent;
1076
+ }
1077
+
1078
+ // ═══════════════════════════════════════════════════════════════════
1079
+ // Rendering — Activity Tab
1080
+ // ═══════════════════════════════════════════════════════════════════
1081
+
1082
+ let renderScheduled = false;
1083
+
1084
+ function renderAll() {
1085
+ if (renderScheduled) return;
1086
+ renderScheduled = true;
1087
+ requestAnimationFrame(() => {
1088
+ renderScheduled = false;
1089
+ renderHeader();
1090
+ renderPhases();
1091
+ renderEvents();
1092
+ });
1093
+ }
1094
+
1095
+ function renderHeader() {
1096
+ $orchName.textContent = state.orchestrationName;
1097
+ $orchPattern.textContent = state.pattern || '—';
1098
+
1099
+ $orchStatus.textContent = state.status;
1100
+ $orchStatus.className = 'orch-status ' + state.status;
1101
+
1102
+ if (state.startedAt) {
1103
+ $orchElapsed.textContent = formatElapsed(Date.now() - new Date(state.startedAt).getTime());
1104
+ }
1105
+
1106
+ $orchTokens.textContent = formatNum(state.usage.tokens);
1107
+ $orchCost.textContent = '$' + state.usage.cost.toFixed(2);
1108
+ }
1109
+
1110
+ function renderPhases() {
1111
+ if (state.phases.length === 0) {
1112
+ $phasesContainer.innerHTML = '<div class="phases-empty">Waiting for orchestration events...</div>';
1113
+ return;
1114
+ }
1115
+
1116
+ const now = Date.now();
1117
+ let html = '';
1118
+
1119
+ for (let i = 0; i < state.phases.length; i++) {
1120
+ const p = state.phases[i];
1121
+ const elapsed = p.startedAt
1122
+ ? formatElapsed((p.completedAt ? new Date(p.completedAt).getTime() : now) - new Date(p.startedAt).getTime())
1123
+ : '';
1124
+
1125
+ const doneCount = p.agents.filter(a => a.status === 'done' || a.status === 'error' || a.status === 'timeout').length;
1126
+ const total = p.agents.length;
1127
+
1128
+ html += '<div class="phase">';
1129
+ html += '<div class="phase-header">';
1130
+ html += '<span class="phase-indicator ' + escHtml(p.status) + '"></span>';
1131
+ html += '<span class="phase-name">' + escHtml(p.name) + '</span>';
1132
+ html += '<span class="phase-agents-count">' + (total > 0 ? doneCount + '/' + total + ' agents' : '') + '</span>';
1133
+ html += '<span class="phase-elapsed">' + escHtml(elapsed) + '</span>';
1134
+ html += '</div>';
1135
+
1136
+ if (total > 0) {
1137
+ html += '<div class="agents-list">';
1138
+ for (const a of p.agents) {
1139
+ html += '<div class="agent-row">';
1140
+ html += '<span class="agent-dot ' + escHtml(a.status) + '"></span>';
1141
+ html += '<span class="agent-name">' + escHtml(a.name || a.type || 'agent') + '</span>';
1142
+ html += '<span class="agent-type">' + escHtml(a.type || '') + '</span>';
1143
+ html += '<span class="agent-model">' + escHtml(a.model || a.backend || '') + '</span>';
1144
+ html += '<span class="agent-tokens">' + (a.tokens > 0 ? formatNum(a.tokens) + ' tok' : '') + '</span>';
1145
+ html += '<span class="agent-detail">' + escHtml(a.detail || '') + '</span>';
1146
+ html += '</div>';
1147
+ }
1148
+ html += '</div>';
1149
+ }
1150
+
1151
+ html += '</div>';
1152
+ }
1153
+
1154
+ $phasesContainer.innerHTML = html;
1155
+ }
1156
+
1157
+ function eventDetail(evt) {
1158
+ const d = evt.data || {};
1159
+ switch (evt.type) {
1160
+ case 'orchestration:start': return d.name || '';
1161
+ case 'orchestration:phase-start': return d.phase || '';
1162
+ case 'orchestration:phase-complete': return (d.phase || '') + ' ' + (d.status || '');
1163
+ case 'orchestration:complete': return 'completed';
1164
+ case 'orchestration:failed': return 'failed';
1165
+ case 'agent:spawning': return d.agentType || '';
1166
+ case 'agent:running': return (d.task || '').slice(0, 60);
1167
+ case 'agent:idle': return (d.result?.filesTouched?.length || 0) + ' files';
1168
+ case 'agent:error': return d.error || 'unknown error';
1169
+ case 'agent:timeout': return 'timed out';
1170
+ case 'agent:finding': return '[' + (d.severity || 'info') + '] ' + (d.finding || '').slice(0, 50);
1171
+ case 'agent:doom-loop': return (d.tool || '') + ' x' + (d.callCount || '?');
1172
+ case 'system:warning': return d.type || d.reason || d.message || '';
1173
+ default: return '';
1174
+ }
1175
+ }
1176
+
1177
+ function renderEvents() {
1178
+ // Build event rows only — header stays as static DOM
1179
+ const header = $eventFeed.querySelector('.event-feed-header');
1180
+ let html = '';
1181
+ for (const evt of events) {
1182
+ const time = formatTime(evt.timestamp);
1183
+ const type = evt.type || '—';
1184
+ const source = evt.source || '—';
1185
+ const detail = eventDetail(evt);
1186
+ html += '<div class="event-row">';
1187
+ html += '<span class="event-time">' + escHtml(time) + '</span>';
1188
+ html += '<span class="event-type">' + escHtml(type) + '</span>';
1189
+ html += '<span class="event-source">' + escHtml(source) + '</span>';
1190
+ html += '<span class="event-detail">' + escHtml(detail) + '</span>';
1191
+ html += '</div>';
1192
+ }
1193
+ // Preserve the static header, only replace event rows
1194
+ const temp = document.createElement('div');
1195
+ temp.innerHTML = html;
1196
+ const rows = $eventFeed.querySelectorAll('.event-row');
1197
+ rows.forEach(r => r.remove());
1198
+ while (temp.firstChild) $eventFeed.appendChild(temp.firstChild);
1199
+ $eventFeed.scrollTop = $eventFeed.scrollHeight;
1200
+ }
1201
+
1202
+ // ═══════════════════════════════════════════════════════════════════
1203
+ // Knowledge Graph Metrics Tab
1204
+ // ═══════════════════════════════════════════════════════════════════
1205
+
1206
+ const NODE_COLORS = {
1207
+ orchestration: 'var(--node-orchestration)',
1208
+ phase: 'var(--node-phase)',
1209
+ agent: 'var(--node-agent)',
1210
+ file: 'var(--node-file)',
1211
+ pattern: 'var(--node-pattern)',
1212
+ error: 'var(--node-error)',
1213
+ heuristic: 'var(--node-heuristic)',
1214
+ };
1215
+
1216
+ async function fetchMetrics() {
1217
+ $graphStats.textContent = 'loading...';
1218
+ document.querySelectorAll('.metric-value').forEach(el => el.textContent = '...');
1219
+ try {
1220
+ const [stats, orchestrations] = await Promise.all([
1221
+ fetch('/api/graph/stats').then(r => r.ok ? r.json() : null),
1222
+ fetch('/api/graph/orchestrations').then(r => r.ok ? r.json() : []),
1223
+ ]);
1224
+
1225
+ if (!stats || stats.nodeCount === 0) {
1226
+ $graphStats.textContent = 'no data';
1227
+ document.querySelectorAll('.metric-value').forEach(el => el.textContent = '0');
1228
+ document.getElementById('nodesByType').innerHTML = '<div style="color:var(--text-muted);font-size:var(--font-size-sm)">No graph data yet. Run an orchestration to populate.</div>';
1229
+ document.getElementById('edgesByType').innerHTML = '';
1230
+ document.getElementById('recentOrchestrations').innerHTML = '<div style="color:var(--text-muted);font-size:var(--font-size-sm)">No orchestrations recorded yet</div>';
1231
+ return;
1232
+ }
1233
+
1234
+ $graphStats.textContent = formatNum(stats.nodeCount) + ' nodes, ' + formatNum(stats.edgeCount) + ' edges';
1235
+
1236
+ document.getElementById('metricNodes').textContent = formatNum(stats.nodeCount);
1237
+ document.getElementById('metricEdges').textContent = formatNum(stats.edgeCount);
1238
+ document.getElementById('metricOrchestrations').textContent = formatNum(stats.nodesByType?.orchestration ?? 0);
1239
+ document.getElementById('metricPatterns').textContent = formatNum(stats.nodesByType?.pattern ?? 0);
1240
+
1241
+ renderBarChart('nodesByType', stats.nodesByType || {}, NODE_COLORS);
1242
+ renderBarChart('edgesByType', stats.edgesByType || {}, {});
1243
+ renderRecentOrchestrations(orchestrations.slice(0, 20));
1244
+
1245
+ } catch (e) {
1246
+ console.warn('Metrics fetch error:', e);
1247
+ $graphStats.textContent = 'fetch error';
1248
+ }
1249
+ }
1250
+
1251
+ function renderBarChart(containerId, data, colors) {
1252
+ const container = document.getElementById(containerId);
1253
+ if (!container) return;
1254
+
1255
+ const entries = Object.entries(data).sort((a, b) => b[1] - a[1]);
1256
+ const max = entries.length > 0 ? entries[0][1] : 1;
1257
+
1258
+ let html = '';
1259
+ for (const [label, count] of entries) {
1260
+ const pct = max > 0 ? (count / max) * 100 : 0;
1261
+ const color = colors[label] || 'var(--accent-dim)';
1262
+ html += '<div class="bar-row">';
1263
+ html += '<div class="bar-label">' + escHtml(label) + '</div>';
1264
+ html += '<div class="bar-track"><div class="bar-fill" style="width:' + pct.toFixed(1) + '%;background:' + color + '"></div></div>';
1265
+ html += '<div class="bar-count">' + formatNum(count) + '</div>';
1266
+ html += '</div>';
1267
+ }
1268
+ container.innerHTML = html;
1269
+ }
1270
+
1271
+ function renderRecentOrchestrations(orchestrations) {
1272
+ const container = document.getElementById('recentOrchestrations');
1273
+ if (!container) return;
1274
+
1275
+ if (orchestrations.length === 0) {
1276
+ container.innerHTML = '<div style="color:var(--text-muted);font-size:var(--font-size-sm)">No orchestrations recorded yet</div>';
1277
+ return;
1278
+ }
1279
+
1280
+ let html = '';
1281
+ for (const orch of orchestrations) {
1282
+ const props = typeof orch.properties === 'string' ? JSON.parse(orch.properties) : (orch.properties || {});
1283
+ const date = orch.created_at ? new Date(orch.created_at).toLocaleString() : '';
1284
+ const status = props.status || '';
1285
+ const statusColor = status === 'completed' ? 'var(--status-done)' : status === 'failed' ? 'var(--status-error)' : 'var(--text-muted)';
1286
+
1287
+ html += '<div class="orch-row">';
1288
+ html += '<div class="orch-name">' + escHtml(orch.label || orch.id) + '</div>';
1289
+ html += '<div class="orch-phases" style="color:' + statusColor + '">' + escHtml(status) + '</div>';
1290
+ html += '<div class="orch-date">' + escHtml(date) + '</div>';
1291
+ html += '</div>';
1292
+ }
1293
+ container.innerHTML = html;
1294
+ }
1295
+
1296
+ document.getElementById('graphRefresh').addEventListener('click', fetchMetrics);
1297
+
1298
+ // ═══════════════════════════════════════════════════════════════════
1299
+ // Boot
1300
+ // ═══════════════════════════════════════════════════════════════════
1301
+
1302
+ connect();
1303
+
1304
+ })();
1305
+ </script>
1306
+ </body>
1307
+ </html>