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