@baanish/hydra-cli 0.1.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 (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -0
  3. package/dist/config.d.ts +29 -0
  4. package/dist/config.d.ts.map +1 -0
  5. package/dist/config.js +338 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/db/client.d.ts +10 -0
  8. package/dist/db/client.d.ts.map +1 -0
  9. package/dist/db/client.js +93 -0
  10. package/dist/db/client.js.map +1 -0
  11. package/dist/db/queries.d.ts +67 -0
  12. package/dist/db/queries.d.ts.map +1 -0
  13. package/dist/db/queries.js +336 -0
  14. package/dist/db/queries.js.map +1 -0
  15. package/dist/engine/concurrency.d.ts +3 -0
  16. package/dist/engine/concurrency.d.ts.map +1 -0
  17. package/dist/engine/concurrency.js +42 -0
  18. package/dist/engine/concurrency.js.map +1 -0
  19. package/dist/engine/eta.d.ts +16 -0
  20. package/dist/engine/eta.d.ts.map +1 -0
  21. package/dist/engine/eta.js +54 -0
  22. package/dist/engine/eta.js.map +1 -0
  23. package/dist/engine/model.d.ts +57 -0
  24. package/dist/engine/model.d.ts.map +1 -0
  25. package/dist/engine/model.js +445 -0
  26. package/dist/engine/model.js.map +1 -0
  27. package/dist/engine/personas.d.ts +30 -0
  28. package/dist/engine/personas.d.ts.map +1 -0
  29. package/dist/engine/personas.js +336 -0
  30. package/dist/engine/personas.js.map +1 -0
  31. package/dist/engine/pipeline.d.ts +61 -0
  32. package/dist/engine/pipeline.d.ts.map +1 -0
  33. package/dist/engine/pipeline.js +638 -0
  34. package/dist/engine/pipeline.js.map +1 -0
  35. package/dist/engine/prompts.d.ts +10 -0
  36. package/dist/engine/prompts.d.ts.map +1 -0
  37. package/dist/engine/prompts.js +49 -0
  38. package/dist/engine/prompts.js.map +1 -0
  39. package/dist/engine/search.d.ts +46 -0
  40. package/dist/engine/search.d.ts.map +1 -0
  41. package/dist/engine/search.js +159 -0
  42. package/dist/engine/search.js.map +1 -0
  43. package/dist/index.d.ts +5 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +648 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/security.d.ts +18 -0
  48. package/dist/security.d.ts.map +1 -0
  49. package/dist/security.js +168 -0
  50. package/dist/security.js.map +1 -0
  51. package/dist/types.d.ts +143 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +2 -0
  54. package/dist/types.js.map +1 -0
  55. package/dist/ui/agent-mode.d.ts +8 -0
  56. package/dist/ui/agent-mode.d.ts.map +1 -0
  57. package/dist/ui/agent-mode.js +138 -0
  58. package/dist/ui/agent-mode.js.map +1 -0
  59. package/dist/ui/animations.d.ts +8 -0
  60. package/dist/ui/animations.d.ts.map +1 -0
  61. package/dist/ui/animations.js +19 -0
  62. package/dist/ui/animations.js.map +1 -0
  63. package/dist/ui/components/agent-list.d.ts +2 -0
  64. package/dist/ui/components/agent-list.d.ts.map +1 -0
  65. package/dist/ui/components/agent-list.js +2 -0
  66. package/dist/ui/components/agent-list.js.map +1 -0
  67. package/dist/ui/components/header.d.ts +2 -0
  68. package/dist/ui/components/header.d.ts.map +1 -0
  69. package/dist/ui/components/header.js +2 -0
  70. package/dist/ui/components/header.js.map +1 -0
  71. package/dist/ui/components/phase-bar.d.ts +2 -0
  72. package/dist/ui/components/phase-bar.d.ts.map +1 -0
  73. package/dist/ui/components/phase-bar.js +2 -0
  74. package/dist/ui/components/phase-bar.js.map +1 -0
  75. package/dist/ui/components/stats-bar.d.ts +2 -0
  76. package/dist/ui/components/stats-bar.d.ts.map +1 -0
  77. package/dist/ui/components/stats-bar.js +2 -0
  78. package/dist/ui/components/stats-bar.js.map +1 -0
  79. package/dist/ui/tui.d.ts +18 -0
  80. package/dist/ui/tui.d.ts.map +1 -0
  81. package/dist/ui/tui.js +464 -0
  82. package/dist/ui/tui.js.map +1 -0
  83. package/dist/web/app.html +1352 -0
  84. package/dist/web/index.d.ts +2 -0
  85. package/dist/web/index.d.ts.map +1 -0
  86. package/dist/web/index.js +2 -0
  87. package/dist/web/index.js.map +1 -0
  88. package/dist/web/server.d.ts +2 -0
  89. package/dist/web/server.d.ts.map +1 -0
  90. package/dist/web/server.js +864 -0
  91. package/dist/web/server.js.map +1 -0
  92. package/package.json +59 -0
@@ -0,0 +1,1352 @@
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
+ <meta name="hydra-web-token" content="__HYDRA_WEB_TOKEN__" />
7
+ <title>hydra web</title>
8
+ <style>
9
+ :root {
10
+ --bg: #f6efd9;
11
+ --paper: #fff8e7;
12
+ --fg: #131313;
13
+ --muted: #3f3b36;
14
+ --surface: #fff6df;
15
+ --surface-soft: #eee4cb;
16
+ --surface-stroke: #3a3830;
17
+ --shadow: #121212;
18
+ --ink: #0f0f0f;
19
+ --ink-fg: #fff8e7;
20
+ --accent: #f24a2b;
21
+ --accent-muted: #d03e23;
22
+ --accent-fg: #fff9f0;
23
+ --signal: #ffdd35;
24
+ --cobalt: #1046e6;
25
+ --ring: #1046e6;
26
+ }
27
+
28
+ .dark {
29
+ --bg: #131313;
30
+ --paper: #1a1a1a;
31
+ --fg: #f6efd9;
32
+ --muted: #a8a090;
33
+ --surface: #1e1e1e;
34
+ --surface-soft: #2a2a2a;
35
+ --surface-stroke: #3a3830;
36
+ --shadow: #3a3830;
37
+ --ink: #f6efd9;
38
+ --ink-fg: #131313;
39
+ --accent: #f24a2b;
40
+ --accent-muted: #ff6347;
41
+ --accent-fg: #131313;
42
+ --signal: #b8960a;
43
+ --cobalt: #3a6aff;
44
+ --ring: #4a7aff;
45
+ }
46
+
47
+ * {
48
+ box-sizing: border-box;
49
+ border-radius: 0 !important;
50
+ }
51
+
52
+ html,
53
+ body {
54
+ min-height: 100%;
55
+ }
56
+
57
+ body {
58
+ margin: 0;
59
+ font-family: Arial, system-ui, sans-serif;
60
+ background: var(--bg);
61
+ color: var(--fg);
62
+ background-image: repeating-linear-gradient(0deg, transparent 0px, transparent 7px, rgba(0, 0, 0, 0.07) 8px);
63
+ }
64
+
65
+ #app {
66
+ max-width: 1100px;
67
+ margin: 0 auto;
68
+ padding: 32px 20px 24px;
69
+ }
70
+
71
+ header {
72
+ background: var(--ink);
73
+ color: var(--ink-fg);
74
+ border: 4px solid var(--surface-stroke);
75
+ border-bottom: 0;
76
+ padding: 16px 20px;
77
+ display: flex;
78
+ justify-content: space-between;
79
+ align-items: center;
80
+ gap: 16px;
81
+ }
82
+
83
+ .logo {
84
+ margin: 0;
85
+ background: var(--accent);
86
+ color: var(--accent-fg);
87
+ border: 4px solid var(--surface-stroke);
88
+ padding: 4px 14px;
89
+ font-size: 13px;
90
+ font-weight: 900;
91
+ letter-spacing: 0.1em;
92
+ text-transform: uppercase;
93
+ box-shadow: 4px 4px 0 var(--shadow);
94
+ }
95
+
96
+ .header-actions {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 10px;
100
+ }
101
+
102
+ .ticker {
103
+ border-top: 4px solid var(--surface-stroke);
104
+ border-bottom: 4px solid var(--surface-stroke);
105
+ background: var(--signal);
106
+ color: var(--fg);
107
+ overflow: hidden;
108
+ white-space: nowrap;
109
+ padding: 6px 0;
110
+ }
111
+
112
+ .ticker-track {
113
+ display: inline-flex;
114
+ min-width: max-content;
115
+ gap: 2.5rem;
116
+ padding-left: 20px;
117
+ padding-right: 2.5rem;
118
+ animation: ticker 34s linear infinite;
119
+ }
120
+
121
+ .ticker span {
122
+ font-size: 11px;
123
+ font-weight: 700;
124
+ letter-spacing: 0.16em;
125
+ text-transform: uppercase;
126
+ }
127
+
128
+ h2 {
129
+ margin: 0 0 12px;
130
+ text-transform: uppercase;
131
+ font-size: 14px;
132
+ letter-spacing: 0.08em;
133
+ }
134
+
135
+ .btn,
136
+ .btn:link,
137
+ .btn:visited {
138
+ background: var(--ink);
139
+ color: var(--ink-fg);
140
+ border: 3px solid var(--surface-stroke);
141
+ padding: 8px 18px;
142
+ font-size: 12px;
143
+ font-weight: 700;
144
+ letter-spacing: 0.1em;
145
+ text-transform: uppercase;
146
+ cursor: pointer;
147
+ text-decoration: none;
148
+ display: inline-block;
149
+ box-shadow: 3px 3px 0 var(--shadow);
150
+ transition: transform 0.1s, box-shadow 0.1s;
151
+ }
152
+
153
+ .btn:hover {
154
+ transform: translate(-1px, -1px);
155
+ box-shadow: 4px 4px 0 var(--shadow);
156
+ }
157
+
158
+ .btn:active {
159
+ transform: translate(1px, 1px);
160
+ }
161
+
162
+ .btn-accent {
163
+ background: var(--accent);
164
+ color: var(--accent-fg);
165
+ }
166
+
167
+ .btn-cobalt {
168
+ background: var(--cobalt);
169
+ color: white;
170
+ }
171
+
172
+ .panel,
173
+ .card,
174
+ .run-card,
175
+ .modal-card {
176
+ background: var(--paper);
177
+ border: 2px solid var(--surface-stroke);
178
+ padding: 16px;
179
+ margin-bottom: 12px;
180
+ box-shadow: 4px 4px 0 var(--shadow);
181
+ }
182
+
183
+ #run-modal {
184
+ position: fixed;
185
+ inset: 0;
186
+ display: none;
187
+ background: rgba(0, 0, 0, 0.35);
188
+ align-items: center;
189
+ justify-content: center;
190
+ padding: 16px;
191
+ z-index: 10;
192
+ }
193
+
194
+ #run-modal.open {
195
+ display: flex;
196
+ }
197
+
198
+ .modal-card {
199
+ width: min(700px, 92vw);
200
+ }
201
+
202
+ textarea,
203
+ input,
204
+ select {
205
+ width: 100%;
206
+ margin-top: 6px;
207
+ background: var(--surface);
208
+ color: var(--fg);
209
+ border: 2px solid var(--surface-stroke);
210
+ padding: 10px 12px;
211
+ font-size: 14px;
212
+ outline: none;
213
+ }
214
+
215
+ textarea {
216
+ min-height: 120px;
217
+ }
218
+
219
+ input:focus,
220
+ textarea:focus,
221
+ select:focus {
222
+ box-shadow: 3px 3px 0 var(--ring);
223
+ border-color: var(--ring);
224
+ }
225
+
226
+ input[type="range"] {
227
+ accent-color: var(--cobalt);
228
+ padding: 0;
229
+ }
230
+
231
+ label {
232
+ display: block;
233
+ margin-top: 12px;
234
+ font-size: 11px;
235
+ font-weight: 700;
236
+ letter-spacing: 0.14em;
237
+ text-transform: uppercase;
238
+ color: var(--muted);
239
+ }
240
+
241
+ .small,
242
+ .meta {
243
+ font-size: 12px;
244
+ color: var(--muted);
245
+ }
246
+
247
+ .hidden {
248
+ display: none;
249
+ }
250
+
251
+ .status,
252
+ .badge {
253
+ display: inline-block;
254
+ border: 2px solid var(--surface-stroke);
255
+ padding: 2px 8px;
256
+ margin-right: 8px;
257
+ font-size: 11px;
258
+ font-weight: 700;
259
+ letter-spacing: 0.1em;
260
+ text-transform: uppercase;
261
+ }
262
+
263
+ .badge-complete {
264
+ background: var(--signal);
265
+ color: var(--fg);
266
+ }
267
+
268
+ .badge-error {
269
+ background: var(--accent);
270
+ color: var(--accent-fg);
271
+ }
272
+
273
+ .badge-running,
274
+ .badge-researching,
275
+ .badge-decomposing,
276
+ .badge-debate,
277
+ .badge-debating,
278
+ .badge-synthesizing {
279
+ background: var(--surface-soft);
280
+ color: var(--fg);
281
+ }
282
+
283
+ .progress-bar {
284
+ margin: 8px 0;
285
+ width: 100%;
286
+ background: var(--surface-soft);
287
+ border: 2px solid var(--surface-stroke);
288
+ overflow: hidden;
289
+ height: 20px;
290
+ }
291
+
292
+ .progress-bar span {
293
+ display: block;
294
+ height: 100%;
295
+ background: var(--accent);
296
+ transition: width 0.3s;
297
+ }
298
+
299
+ .progress-bar span.indeterminate {
300
+ animation: progress-indeterminate 1.2s ease-in-out infinite;
301
+ background: linear-gradient(90deg, var(--accent), var(--accent-muted), var(--accent));
302
+ }
303
+
304
+ @keyframes progress-indeterminate {
305
+ 0% {
306
+ transform: translateX(-100%);
307
+ }
308
+ 50% {
309
+ transform: translateX(-10%);
310
+ }
311
+ 100% {
312
+ transform: translateX(100%);
313
+ }
314
+ }
315
+
316
+ details {
317
+ border: 2px solid var(--surface-stroke);
318
+ padding: 8px;
319
+ margin-bottom: 6px;
320
+ }
321
+
322
+ summary {
323
+ cursor: pointer;
324
+ font-size: 13px;
325
+ font-weight: 700;
326
+ letter-spacing: 0.08em;
327
+ text-transform: uppercase;
328
+ }
329
+
330
+ a {
331
+ color: var(--fg);
332
+ text-decoration: none;
333
+ }
334
+
335
+ pre,
336
+ .transcript-output,
337
+ #brief {
338
+ background: var(--surface);
339
+ border: 2px solid var(--surface-stroke);
340
+ padding: 10px;
341
+ overflow: auto;
342
+ margin-top: 8px;
343
+ }
344
+
345
+ .transcript-output {
346
+ white-space: normal;
347
+ }
348
+
349
+ #brief {
350
+ white-space: pre-wrap;
351
+ }
352
+
353
+ #run-status {
354
+ margin-top: 10px;
355
+ }
356
+
357
+ .row {
358
+ margin-top: 10px;
359
+ display: flex;
360
+ gap: 10px;
361
+ justify-content: flex-end;
362
+ }
363
+
364
+ .run-card {
365
+ padding: 12px;
366
+ }
367
+
368
+ .run-card h2 {
369
+ margin: 0;
370
+ font-size: 14px;
371
+ }
372
+
373
+ a .run-card {
374
+ display: block;
375
+ }
376
+
377
+ .ticker {
378
+ border-top: 4px solid var(--surface-stroke);
379
+ border-bottom: 4px solid var(--surface-stroke);
380
+ background: var(--signal);
381
+ color: var(--fg);
382
+ overflow: hidden;
383
+ white-space: nowrap;
384
+ padding: 6px 0;
385
+ }
386
+
387
+ .ticker-track {
388
+ display: inline-flex;
389
+ min-width: max-content;
390
+ gap: 2.5rem;
391
+ padding-left: 20px;
392
+ padding-right: 2.5rem;
393
+ animation: ticker-scroll 34s linear infinite;
394
+ }
395
+
396
+ .ticker span {
397
+ font-size: 11px;
398
+ font-weight: 700;
399
+ letter-spacing: 0.16em;
400
+ text-transform: uppercase;
401
+ }
402
+
403
+ @keyframes ticker-scroll {
404
+ from {
405
+ transform: translateX(0);
406
+ }
407
+ to {
408
+ transform: translateX(-50%);
409
+ }
410
+ }
411
+ </style>
412
+ </head>
413
+ <body>
414
+ <div id="app"></div>
415
+
416
+ <div id="run-modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="run-modal-title">
417
+ <div class="modal-card">
418
+ <h1 id="run-modal-title" class="logo">new run</h1>
419
+ <form id="run-form">
420
+ <label>
421
+ query
422
+ <textarea id="run-query" placeholder="ask a question"></textarea>
423
+ </label>
424
+ <label>
425
+ agents <span id="agent-count">5</span>
426
+ <input id="run-agents" type="range" min="1" max="20" value="5" />
427
+ </label>
428
+ <label>
429
+ <input id="run-search" type="checkbox" checked /> enable search
430
+ </label>
431
+ <div id="run-status" class="small"></div>
432
+ <div class="row">
433
+ <button class="btn" type="button" id="run-close">cancel</button>
434
+ <button class="btn" type="submit">start run</button>
435
+ </div>
436
+ </form>
437
+ </div>
438
+ </div>
439
+
440
+ <script>
441
+ const state = {
442
+ runs: [],
443
+ run: null,
444
+ runId: null,
445
+ currentPhase: "decompose",
446
+ progress: {
447
+ completed: 0,
448
+ total: 0,
449
+ },
450
+ agentCount: 0,
451
+ phases: {
452
+ decompose: { completed: 0, total: 0 },
453
+ research: { completed: 0, total: 0 },
454
+ debate: { completed: 0, total: 0 },
455
+ synthesis: { completed: 0, total: 0 },
456
+ },
457
+ transcripts: [],
458
+ source: null,
459
+ };
460
+ let routeEpoch = 0;
461
+
462
+ const app = document.getElementById("app");
463
+ const modal = document.getElementById("run-modal");
464
+ const runForm = document.getElementById("run-form");
465
+ const runQuery = document.getElementById("run-query");
466
+ const runAgents = document.getElementById("run-agents");
467
+ const runSearch = document.getElementById("run-search");
468
+ const agentCountLabel = document.getElementById("agent-count");
469
+ const runStatus = document.getElementById("run-status");
470
+ const runClose = document.getElementById("run-close");
471
+ let defaultAgentCount = Number(runAgents.value);
472
+ let defaultSearchEnabled = runSearch.checked;
473
+ let agentCountTouched = false;
474
+ let searchTouched = false;
475
+ const sessionToken =
476
+ document.querySelector('meta[name="hydra-web-token"]')?.getAttribute("content") || "";
477
+ const THEME_STORAGE_KEY = "hydra-web-theme";
478
+ const DEFAULT_PHASE = "decompose";
479
+ let runModalOpener = null;
480
+ let runModalKeydownHandler = null;
481
+
482
+ function apiFetch(input, init = {}) {
483
+ const headers = new Headers(init.headers || {});
484
+ if (sessionToken) {
485
+ headers.set("X-Hydra-Session", sessionToken);
486
+ }
487
+ return fetch(input, {
488
+ ...init,
489
+ headers,
490
+ });
491
+ }
492
+
493
+ function eventStreamPath(pathname) {
494
+ const url = new URL(pathname, window.location.origin);
495
+ if (sessionToken) {
496
+ url.searchParams.set("session", sessionToken);
497
+ }
498
+ return `${url.pathname}${url.search}`;
499
+ }
500
+
501
+ function setRunModalOpen(isOpen, opener = null) {
502
+ runModalOpener = isOpen ? opener : runModalOpener;
503
+ modal.setAttribute("aria-hidden", isOpen ? "false" : "true");
504
+ modal.classList.toggle("open", isOpen);
505
+
506
+ if (isOpen) {
507
+ runQuery.focus();
508
+ if (runModalKeydownHandler) {
509
+ return;
510
+ }
511
+ runModalKeydownHandler = (event) => {
512
+ if (event.key !== "Tab") {
513
+ return;
514
+ }
515
+ const focusables = [
516
+ runQuery,
517
+ runAgents,
518
+ runSearch,
519
+ runClose,
520
+ runForm.querySelector('button[type="submit"]'),
521
+ ].filter(
522
+ (element) =>
523
+ element instanceof HTMLElement &&
524
+ !element.disabled &&
525
+ element.tabIndex >= 0,
526
+ );
527
+ if (focusables.length === 0) {
528
+ return;
529
+ }
530
+ const first = focusables[0];
531
+ const last = focusables[focusables.length - 1];
532
+ const active = document.activeElement;
533
+ if (event.shiftKey) {
534
+ if (active === first) {
535
+ event.preventDefault();
536
+ last.focus();
537
+ }
538
+ return;
539
+ }
540
+ if (active === last) {
541
+ event.preventDefault();
542
+ first.focus();
543
+ }
544
+ };
545
+ modal.addEventListener("keydown", runModalKeydownHandler);
546
+ return;
547
+ }
548
+
549
+ if (runModalKeydownHandler) {
550
+ modal.removeEventListener("keydown", runModalKeydownHandler);
551
+ runModalKeydownHandler = null;
552
+ }
553
+ if (runModalOpener instanceof HTMLElement) {
554
+ runModalOpener.focus();
555
+ }
556
+ runModalOpener = null;
557
+ }
558
+
559
+ function getSavedTheme() {
560
+ try {
561
+ return localStorage.getItem(THEME_STORAGE_KEY);
562
+ } catch {
563
+ return null;
564
+ }
565
+ }
566
+
567
+ function updateThemeToggleLabels() {
568
+ const isDark = document.documentElement.classList.contains("dark");
569
+ const label = isDark ? "LIGHT MODE" : "DARK MODE";
570
+ document.querySelectorAll(".theme-toggle").forEach((button) => {
571
+ button.textContent = label;
572
+ });
573
+ }
574
+
575
+ function applyTheme(theme) {
576
+ const isDark = theme === "dark";
577
+ document.documentElement.classList.toggle("dark", isDark);
578
+ try {
579
+ localStorage.setItem(THEME_STORAGE_KEY, isDark ? "dark" : "light");
580
+ } catch {
581
+ // ignore localStorage failures in restricted environments.
582
+ }
583
+ updateThemeToggleLabels();
584
+ }
585
+
586
+ function initializeTheme() {
587
+ const savedTheme = getSavedTheme();
588
+ const preferred = savedTheme === "dark" || savedTheme === "light" ? savedTheme : "light";
589
+ applyTheme(preferred);
590
+ }
591
+
592
+ function statusToPhase(status, fallbackPhase = null) {
593
+ switch (String(status ?? "")) {
594
+ case "decomposing":
595
+ return "decompose";
596
+ case "researching":
597
+ return "research";
598
+ case "debating":
599
+ return "debate";
600
+ case "synthesizing":
601
+ return "synthesis";
602
+ case "complete":
603
+ case "error":
604
+ return fallbackPhase || "synthesis";
605
+ default:
606
+ return state.currentPhase || DEFAULT_PHASE;
607
+ }
608
+ }
609
+
610
+ function clampAgentCount(value) {
611
+ const parsed = Number(value);
612
+ if (!Number.isFinite(parsed)) {
613
+ return null;
614
+ }
615
+ return Math.min(20, Math.max(1, parsed));
616
+ }
617
+
618
+ async function loadRunDefaults() {
619
+ try {
620
+ const response = await apiFetch("/api/config");
621
+ if (!response.ok) {
622
+ return;
623
+ }
624
+ const config = await response.json();
625
+ const parsedAgentCount = clampAgentCount(config?.defaultAgentCount);
626
+ if (parsedAgentCount !== null) {
627
+ defaultAgentCount = parsedAgentCount;
628
+ }
629
+ if (typeof config?.searchEnabled === "boolean") {
630
+ defaultSearchEnabled = config.searchEnabled;
631
+ }
632
+ } catch {
633
+ return;
634
+ }
635
+
636
+ runAgents.value = String(defaultAgentCount);
637
+ agentCountLabel.textContent = runAgents.value;
638
+ runSearch.checked = defaultSearchEnabled;
639
+ agentCountTouched = false;
640
+ searchTouched = false;
641
+ }
642
+
643
+ function parseRunApiErrorText(response, fallback = "failed to start run") {
644
+ return (async () => {
645
+ const contentType = response.headers.get("content-type") ?? "";
646
+ if (contentType.includes("application/json")) {
647
+ try {
648
+ const payload = await response.json();
649
+ if (payload && typeof payload.error === "string" && payload.error) {
650
+ return payload.error;
651
+ }
652
+ } catch {
653
+ // ignore JSON parse failures and fallback below.
654
+ }
655
+ }
656
+
657
+ try {
658
+ const raw = await response.text();
659
+ const trimmed = raw.trim();
660
+ if (trimmed) {
661
+ return trimmed;
662
+ }
663
+ } catch {
664
+ // ignore fallback parsing failures.
665
+ }
666
+
667
+ return fallback;
668
+ })();
669
+ }
670
+
671
+ function toggleTheme() {
672
+ const isDark = document.documentElement.classList.contains("dark");
673
+ applyTheme(isDark ? "light" : "dark");
674
+ }
675
+
676
+ function normalizeStatusClass(status) {
677
+ return String(status ?? "unknown")
678
+ .toLowerCase()
679
+ .replace(/[^a-z0-9]+/g, "-");
680
+ }
681
+
682
+ function renderTicker() {
683
+ const track = [
684
+ "HYDRA SWARM INTELLIGENCE ENGINE",
685
+ "MULTI-MODEL DEBATE",
686
+ "SYNTHETIC SEARCH",
687
+ "LOCAL + PRIVATE",
688
+ "HYDRA SWARM INTELLIGENCE ENGINE",
689
+ "MULTI-MODEL DEBATE",
690
+ "SYNTHETIC SEARCH",
691
+ "LOCAL + PRIVATE",
692
+ ]
693
+ .map((item) => `<span>${item}</span>`)
694
+ .join("");
695
+ return `
696
+ <div class="ticker">
697
+ <div class="ticker-track">${track}${track}</div>
698
+ </div>
699
+ `;
700
+ }
701
+
702
+ function safeJson(raw) {
703
+ try {
704
+ return JSON.parse(raw);
705
+ } catch {
706
+ return null;
707
+ }
708
+ }
709
+
710
+ function escapeHtml(value) {
711
+ return String(value ?? "").replace(/[&<>"']/g, (char) => {
712
+ return {
713
+ "&": "&amp;",
714
+ "<": "&lt;",
715
+ ">": "&gt;",
716
+ '"': "&quot;",
717
+ "'": "&#39;",
718
+ }[char];
719
+ });
720
+ }
721
+
722
+ function inlineMarkup(text) {
723
+ const escaped = escapeHtml(text);
724
+ const bolded = escaped.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
725
+ return bolded.replace(
726
+ /(https?:\/\/[\w./?#%&=+:;,_-]+)/g,
727
+ '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
728
+ );
729
+ }
730
+
731
+ function truncateText(value, maxLength) {
732
+ const normalized = String(value ?? "");
733
+ if (normalized.length <= maxLength) {
734
+ return escapeHtml(normalized);
735
+ }
736
+ return `${escapeHtml(normalized.slice(0, maxLength - 1))}…`;
737
+ }
738
+
739
+ function renderMarkdown(text) {
740
+ const normalized = String(text ?? "").replace(/\r\n/g, "\n").split("\n");
741
+ let html = "";
742
+ let inCode = false;
743
+ let listOpen = false;
744
+ let para = [];
745
+
746
+ const flushParagraph = () => {
747
+ if (para.length === 0) {
748
+ return;
749
+ }
750
+ html += `<p>${para.join("<br>")}</p>`;
751
+ para = [];
752
+ };
753
+
754
+ const flushList = () => {
755
+ if (listOpen) {
756
+ html += "</ul>";
757
+ listOpen = false;
758
+ }
759
+ };
760
+
761
+ for (const line of normalized) {
762
+ if (line.startsWith("```")) {
763
+ flushParagraph();
764
+ flushList();
765
+ if (!inCode) {
766
+ html += "<pre><code>";
767
+ } else {
768
+ html += "</code></pre>";
769
+ }
770
+ inCode = !inCode;
771
+ continue;
772
+ }
773
+
774
+ if (inCode) {
775
+ html += `${escapeHtml(line)}\n`;
776
+ continue;
777
+ }
778
+
779
+ const heading = line.match(/^##\s+(.*)$/);
780
+ if (heading) {
781
+ flushParagraph();
782
+ flushList();
783
+ html += `<h2>${escapeHtml(heading[1])}</h2>`;
784
+ continue;
785
+ }
786
+
787
+ if (line.startsWith("- ")) {
788
+ if (!listOpen) {
789
+ flushParagraph();
790
+ html += "<ul>";
791
+ listOpen = true;
792
+ }
793
+ html += `<li>${inlineMarkup(line.slice(2))}</li>`;
794
+ continue;
795
+ }
796
+
797
+ if (!line.trim()) {
798
+ flushParagraph();
799
+ flushList();
800
+ continue;
801
+ }
802
+
803
+ para.push(inlineMarkup(line));
804
+ }
805
+
806
+ flushParagraph();
807
+ flushList();
808
+ if (inCode) {
809
+ html += "</code></pre>";
810
+ }
811
+
812
+ return html || "<p>no content</p>";
813
+ }
814
+
815
+ function elapsedSince(ts) {
816
+ const delta = Math.max(0, Date.now() - (ts || Date.now()));
817
+ const mins = Math.floor(delta / 60000);
818
+ const secs = Math.floor((delta % 60000) / 1000);
819
+ return mins > 0 ? `${mins}m ${String(secs).padStart(2, "0")}s` : `${secs}s`;
820
+ }
821
+
822
+ function statusBadge(status) {
823
+ const normalized = normalizeStatusClass(status);
824
+ return `<span class="status badge-${normalized}">${escapeHtml(status)}</span>`;
825
+ }
826
+
827
+ function progressBar(completed, total, indeterminate = false) {
828
+ const safeTotal = Math.max(1, total || 0);
829
+ const safeCompleted = indeterminate ? safeTotal : Math.max(0, Math.min(completed || 0, safeTotal));
830
+ const pct = Math.min(100, Math.round((safeCompleted / safeTotal) * 100));
831
+ const barClass = indeterminate ? " indeterminate" : "";
832
+ return `
833
+ <div class="progress-bar">
834
+ <span class="${barClass}" style="width:${pct}%"></span>
835
+ </div>
836
+ <div class="small">${indeterminate ? "synthesizing..." : `${safeCompleted}/${safeTotal} agents`}</div>
837
+ `;
838
+ }
839
+
840
+ function renderPhaseProgress() {
841
+ const phase = state.currentPhase || DEFAULT_PHASE;
842
+ if (phase === "synthesis") {
843
+ const total = state.phases.synthesis.total || state.progress.total || state.agentCount || 0;
844
+ const terminalRun = state.run?.run?.status === "complete" || state.run?.run?.status === "error";
845
+ return progressBar(total, total, !terminalRun);
846
+ }
847
+ const details = state.phases[phase] || null;
848
+ if (details) {
849
+ return progressBar(details.completed, details.total);
850
+ }
851
+ return progressBar(state.progress.completed, state.progress.total);
852
+ }
853
+
854
+ function renderDashboard() {
855
+ const cards = state.runs
856
+ .map((run) => {
857
+ const snippet = truncateText(run.query, 80);
858
+ const elapsed = run.elapsedMs != null ? elapsedSince(Date.now() - run.elapsedMs) : "running";
859
+ return `
860
+ <a href="#/runs/${encodeURIComponent(run.id)}">
861
+ <div class="run-card">
862
+ <h2>${snippet}</h2>
863
+ <div class="small">
864
+ ${escapeHtml(run.id)} ${statusBadge(run.status)} · elapsed: ${elapsed}
865
+ </div>
866
+ </div>
867
+ </a>
868
+ `;
869
+ })
870
+ .join("");
871
+
872
+ app.innerHTML = `
873
+ <header>
874
+ <h1 class="logo">hydra web</h1>
875
+ <div class="header-actions">
876
+ <button class="btn btn-cobalt theme-toggle" type="button">theme</button>
877
+ <button class="btn" id="open-run">new run</button>
878
+ </div>
879
+ </header>
880
+ ${renderTicker()}
881
+ <div class="panel">
882
+ ${cards || '<div class="small">no runs yet</div>'}
883
+ </div>
884
+ `;
885
+
886
+ const openRunButton = document.getElementById("open-run");
887
+ openRunButton.addEventListener("click", () => {
888
+ runQuery.value = "";
889
+ runAgents.value = String(defaultAgentCount);
890
+ agentCountLabel.textContent = runAgents.value;
891
+ runSearch.checked = defaultSearchEnabled;
892
+ agentCountTouched = false;
893
+ searchTouched = false;
894
+ setRunModalOpen(true, openRunButton);
895
+ });
896
+ updateThemeToggleLabels();
897
+ }
898
+
899
+ function renderRunPage(runRecord, transcripts) {
900
+ const run = runRecord.run;
901
+ const transcriptRows = transcripts
902
+ .map((item) => {
903
+ const escapedPhase = escapeHtml(item.phase);
904
+ const escapedPersona = escapeHtml(item.persona);
905
+ const summary = `${escapedPhase}: ${escapedPersona}`;
906
+ return `
907
+ <details>
908
+ <summary>${summary}</summary>
909
+ <div class="small">status: ${escapeHtml(item.status)}</div>
910
+ <div class="transcript-output">${renderMarkdown(item.output)}</div>
911
+ <div class="small">tokens in/out: ${item.promptTokens || 0}/${item.completionTokens || 0}</div>
912
+ </details>
913
+ `;
914
+ })
915
+ .join("");
916
+
917
+ app.innerHTML = `
918
+ <header>
919
+ <h1 class="logo">${escapeHtml(run.query)}</h1>
920
+ <div class="header-actions">
921
+ <button class="btn btn-cobalt theme-toggle" type="button">theme</button>
922
+ <a class="btn" href="#/">back</a>
923
+ </div>
924
+ </header>
925
+ ${renderTicker()}
926
+ <div class="panel">
927
+ <h2>status</h2>
928
+ ${statusBadge(run.status)}
929
+ <div class="small">id: ${escapeHtml(run.id)} · agents: ${state.agentCount || run.agentCount || 0}</div>
930
+ <div class="small">created: ${new Date(run.createdAt).toLocaleString()}</div>
931
+ ${renderPhaseProgress()}
932
+ </div>
933
+ <div class="panel">
934
+ <h2>brief</h2>
935
+ <div id="brief">${renderMarkdown(run.brief || "not ready")}</div>
936
+ </div>
937
+ <div class="panel">
938
+ <h2>agent transcripts</h2>
939
+ ${transcriptRows || '<div class="small">no transcripts yet</div>'}
940
+ </div>
941
+ `;
942
+ updateThemeToggleLabels();
943
+ }
944
+
945
+ async function refreshRuns() {
946
+ try {
947
+ const response = await apiFetch("/api/runs");
948
+ if (!response.ok) {
949
+ throw new Error("failed to fetch runs");
950
+ }
951
+ const runs = await response.json();
952
+ state.runs = runs;
953
+ } catch {
954
+ state.runs = [];
955
+ }
956
+ }
957
+
958
+ async function fetchRun(runId) {
959
+ const response = await apiFetch(`/api/runs/${runId}`);
960
+ if (!response.ok) {
961
+ throw new Error(`run not found (${response.status})`);
962
+ }
963
+ return response.json();
964
+ }
965
+
966
+ function handleEvent(raw) {
967
+ const event = typeof raw === "string" ? safeJson(raw) : null;
968
+ if (!event || typeof event !== "object") {
969
+ return;
970
+ }
971
+
972
+ if (event.type === "run-created") {
973
+ if (event.query && state.run?.run) {
974
+ state.run.run.query = event.query;
975
+ }
976
+ if (state.run?.run && Number.isFinite(event.agentCount)) {
977
+ state.run.run.agentCount = event.agentCount;
978
+ }
979
+ if (Number.isFinite(event.agentCount)) {
980
+ state.agentCount = event.agentCount;
981
+ }
982
+ if (Number.isFinite(event.totalAgents)) {
983
+ state.progress.total = event.totalAgents;
984
+ } else if (Number.isFinite(event.agentCount)) {
985
+ state.progress.total = event.agentCount;
986
+ }
987
+ state.currentPhase = "decompose";
988
+ state.progress.completed = 0;
989
+ return;
990
+ }
991
+
992
+ if (event.type === "agent-progress") {
993
+ if (state.phases[event.phase]) {
994
+ state.phases[event.phase].completed = event.completedAgents;
995
+ state.phases[event.phase].total = event.totalAgents;
996
+ }
997
+ state.currentPhase = event.phase;
998
+ state.progress.completed = event.completedAgents;
999
+ state.progress.total = event.totalAgents;
1000
+ return;
1001
+ }
1002
+
1003
+ if (event.type === "run-status-changed") {
1004
+ if (state.run?.run) {
1005
+ state.run.run.status = event.status;
1006
+ }
1007
+ if (event.status === "error") {
1008
+ state.currentPhase = statusToPhase(event.status, state.currentPhase);
1009
+ if (state.runId) {
1010
+ void refreshRun(state.runId);
1011
+ }
1012
+ if (state.source) {
1013
+ state.source.close();
1014
+ state.source = null;
1015
+ }
1016
+ return;
1017
+ }
1018
+ state.currentPhase = statusToPhase(event.status);
1019
+ return;
1020
+ }
1021
+
1022
+ if (event.type === "agent-complete") {
1023
+ const agentRunId = typeof event.agentRunId === "string" ? event.agentRunId : "";
1024
+ const index = state.transcripts.findIndex(
1025
+ (item) => item.agentRunId === agentRunId,
1026
+ );
1027
+ const payload = {
1028
+ agentRunId,
1029
+ persona: event.persona,
1030
+ phase: event.phase,
1031
+ status: event.state.status,
1032
+ promptTokens: event.state.promptTokens,
1033
+ completionTokens: event.state.completionTokens,
1034
+ output: event.state.output,
1035
+ };
1036
+
1037
+ if (index === -1) {
1038
+ state.transcripts.push(payload);
1039
+ } else {
1040
+ state.transcripts[index] = payload;
1041
+ }
1042
+ return;
1043
+ }
1044
+
1045
+ if (event.type === "run-complete") {
1046
+ if (state.run?.run) {
1047
+ state.run.run.status = "complete";
1048
+ }
1049
+ if (state.progress.total) {
1050
+ state.progress.completed = state.progress.total;
1051
+ }
1052
+ if (state.runId) {
1053
+ void refreshRun(state.runId);
1054
+ }
1055
+ if (state.source) {
1056
+ state.source.close();
1057
+ state.source = null;
1058
+ }
1059
+ return;
1060
+ }
1061
+
1062
+ if (event.type === "done") {
1063
+ if (state.source) {
1064
+ state.source.close();
1065
+ state.source = null;
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ async function connectRunEvents(runId) {
1071
+ if (state.source) {
1072
+ state.source.close();
1073
+ state.source = null;
1074
+ }
1075
+
1076
+ state.phases = {
1077
+ decompose: { completed: 0, total: 0 },
1078
+ research: { completed: 0, total: 0 },
1079
+ debate: { completed: 0, total: 0 },
1080
+ synthesis: { completed: 0, total: 0 },
1081
+ };
1082
+ state.currentPhase = statusToPhase(state.run?.run?.status) || "decompose";
1083
+ state.agentCount = state.run?.run?.agentCount ?? state.agentCount ?? 0;
1084
+ state.progress = {
1085
+ completed: 0,
1086
+ total: state.agentCount,
1087
+ };
1088
+ state.runId = runId;
1089
+
1090
+ const source = new EventSource(eventStreamPath(`/api/runs/${runId}/events`));
1091
+ state.source = source;
1092
+
1093
+ source.onmessage = (msg) => {
1094
+ handleEvent(msg.data);
1095
+ if (state.run) {
1096
+ renderRunPage(state.run, state.transcripts);
1097
+ }
1098
+ };
1099
+
1100
+ source.addEventListener("error", () => {
1101
+ // let browser EventSource retry semantics handle transient disconnects.
1102
+ });
1103
+ }
1104
+
1105
+ async function refreshRun(runId) {
1106
+ const epoch = routeEpoch;
1107
+ const payload = await fetchRun(runId);
1108
+ if (routeEpoch !== epoch) {
1109
+ return;
1110
+ }
1111
+ state.run = payload;
1112
+ state.runId = payload.run.id;
1113
+ state.agentCount = payload.run.agentCount;
1114
+ state.currentPhase = statusToPhase(payload.run.status);
1115
+ state.progress.total = payload.run.agentCount;
1116
+ state.transcripts = Array.isArray(payload.agentRuns)
1117
+ ? payload.agentRuns.map((item) => ({
1118
+ agentRunId: item.id,
1119
+ persona: item.persona,
1120
+ phase: item.phase,
1121
+ status: item.status,
1122
+ promptTokens: item.promptTokens,
1123
+ completionTokens: item.completionTokens,
1124
+ output: item.output,
1125
+ }))
1126
+ : [];
1127
+ state.progress.completed = payload.run.status === "complete" ? payload.run.agentCount : Math.min(
1128
+ state.progress.completed,
1129
+ payload.run.agentCount,
1130
+ );
1131
+ renderRunPage(payload, state.transcripts);
1132
+ }
1133
+
1134
+ async function route() {
1135
+ const epoch = ++routeEpoch;
1136
+ const hash = window.location.hash;
1137
+ if (!hash || hash === "#/") {
1138
+ await refreshRuns();
1139
+ if (epoch !== routeEpoch) {
1140
+ return;
1141
+ }
1142
+ renderDashboard();
1143
+ if (epoch !== routeEpoch) {
1144
+ return;
1145
+ }
1146
+ if (state.source) {
1147
+ state.source.close();
1148
+ state.source = null;
1149
+ }
1150
+ return;
1151
+ }
1152
+
1153
+ if (hash.startsWith("#/runs/")) {
1154
+ const runId = hash.replace("#/runs/", "");
1155
+ try {
1156
+ if (state.source) {
1157
+ state.source.close();
1158
+ state.source = null;
1159
+ }
1160
+ state.transcripts = [];
1161
+ state.run = null;
1162
+ await refreshRun(runId);
1163
+ if (epoch !== routeEpoch) {
1164
+ return;
1165
+ }
1166
+ await connectRunEvents(runId);
1167
+ if (epoch !== routeEpoch) {
1168
+ return;
1169
+ }
1170
+ } catch {
1171
+ if (epoch !== routeEpoch) {
1172
+ return;
1173
+ }
1174
+ window.location.hash = "#/";
1175
+ await refreshRuns();
1176
+ if (epoch !== routeEpoch) {
1177
+ return;
1178
+ }
1179
+ renderDashboard();
1180
+ }
1181
+ return;
1182
+ }
1183
+
1184
+ await refreshRuns();
1185
+ if (epoch !== routeEpoch) {
1186
+ return;
1187
+ }
1188
+ if (state.source) {
1189
+ state.source.close();
1190
+ state.source = null;
1191
+ }
1192
+ renderDashboard();
1193
+ }
1194
+
1195
+ runAgents.addEventListener("input", () => {
1196
+ agentCountTouched = true;
1197
+ agentCountLabel.textContent = runAgents.value;
1198
+ });
1199
+ runSearch.addEventListener("change", () => {
1200
+ searchTouched = true;
1201
+ });
1202
+
1203
+ function markRunStartFailure(message) {
1204
+ runStatus.textContent = message || "failed to start run";
1205
+ }
1206
+
1207
+ runForm.addEventListener("submit", async (event) => {
1208
+ event.preventDefault();
1209
+ const query = runQuery.value.trim();
1210
+ const agentCount = Number(runAgents.value);
1211
+ const searchEnabled = runSearch.checked;
1212
+ const body = { query };
1213
+ let reader = null;
1214
+
1215
+ if (!query) {
1216
+ runStatus.textContent = "query is required";
1217
+ return;
1218
+ }
1219
+
1220
+ if (agentCountTouched || agentCount !== defaultAgentCount) {
1221
+ body.agentCount = agentCount;
1222
+ }
1223
+ if (searchTouched || searchEnabled !== defaultSearchEnabled) {
1224
+ body.searchEnabled = searchEnabled;
1225
+ }
1226
+
1227
+ runStatus.textContent = "starting run...";
1228
+ try {
1229
+ const abortController = new AbortController();
1230
+ const response = await apiFetch("/api/run", {
1231
+ method: "POST",
1232
+ headers: {
1233
+ "Content-Type": "application/json",
1234
+ },
1235
+ body: JSON.stringify(body),
1236
+ signal: abortController.signal,
1237
+ });
1238
+
1239
+ if (!response.ok) {
1240
+ const errorMessage = await parseRunApiErrorText(response, "");
1241
+ const normalized = typeof errorMessage === "string" ? errorMessage.trim() : "";
1242
+ markRunStartFailure(
1243
+ normalized ? `failed to start run: ${normalized}` : "failed to start run",
1244
+ );
1245
+ return;
1246
+ }
1247
+
1248
+ const responseContentType = response.headers.get("content-type") ?? "";
1249
+ if (!response.body || !responseContentType.includes("text/event-stream")) {
1250
+ const errorMessage = await parseRunApiErrorText(response, "");
1251
+ const normalized = typeof errorMessage === "string" ? errorMessage.trim() : "";
1252
+ markRunStartFailure(
1253
+ normalized ? `failed to start run: ${normalized}` : "failed to start run: invalid response stream",
1254
+ );
1255
+ return;
1256
+ }
1257
+
1258
+ let sawExpectedEvent = false;
1259
+
1260
+ reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
1261
+ let buffered = "";
1262
+ while (true) {
1263
+ const chunk = await reader.read();
1264
+ if (chunk.done) {
1265
+ break;
1266
+ }
1267
+ buffered += chunk.value;
1268
+ const entries = buffered.split("\n\n");
1269
+ buffered = entries.pop() || "";
1270
+ for (const item of entries) {
1271
+ if (!item.startsWith("data:")) {
1272
+ continue;
1273
+ }
1274
+ const payload = safeJson(item.replace("data:", "").trim());
1275
+ if (!payload) {
1276
+ continue;
1277
+ }
1278
+ if (payload.type === "run-created" && payload.runId) {
1279
+ sawExpectedEvent = true;
1280
+ await reader.cancel();
1281
+ abortController.abort();
1282
+ setRunModalOpen(false, null);
1283
+ runStatus.textContent = "";
1284
+ runQuery.value = "";
1285
+ window.location.hash = `#/runs/${payload.runId}`;
1286
+ return;
1287
+ }
1288
+
1289
+ if (payload.type !== "done") {
1290
+ continue;
1291
+ }
1292
+
1293
+ sawExpectedEvent = true;
1294
+ const startedRunId =
1295
+ typeof payload.runId === "string" ? payload.runId.trim() : "";
1296
+
1297
+ if (!startedRunId) {
1298
+ markRunStartFailure("failed to start run");
1299
+ return;
1300
+ }
1301
+
1302
+ setRunModalOpen(false, null);
1303
+ runStatus.textContent = "";
1304
+ runQuery.value = "";
1305
+ window.location.hash = `#/runs/${startedRunId}`;
1306
+ return;
1307
+ }
1308
+ }
1309
+
1310
+ if (!sawExpectedEvent) {
1311
+ markRunStartFailure("failed to start run: no progress event received");
1312
+ return;
1313
+ }
1314
+ } catch {
1315
+ markRunStartFailure("failed to start run");
1316
+ } finally {
1317
+ if (reader) {
1318
+ try {
1319
+ await reader.cancel();
1320
+ } catch {
1321
+ // ignore reader cleanup failures.
1322
+ }
1323
+ }
1324
+ }
1325
+ });
1326
+
1327
+ runClose.addEventListener("click", () => {
1328
+ setRunModalOpen(false);
1329
+ runStatus.textContent = "";
1330
+ });
1331
+
1332
+ document.addEventListener("click", (event) => {
1333
+ const target = event.target;
1334
+ if (!(target instanceof HTMLElement)) {
1335
+ return;
1336
+ }
1337
+ if (target.classList.contains("theme-toggle")) {
1338
+ event.preventDefault();
1339
+ toggleTheme();
1340
+ }
1341
+ });
1342
+
1343
+ initializeTheme();
1344
+
1345
+ window.addEventListener("hashchange", route);
1346
+ window.addEventListener("load", async () => {
1347
+ await loadRunDefaults();
1348
+ await route();
1349
+ });
1350
+ </script>
1351
+ </body>
1352
+ </html>