@probelabs/visor 0.1.97 → 0.1.100

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 (110) hide show
  1. package/README.md +16 -15
  2. package/action.yml +7 -2
  3. package/defaults/.visor.yaml +7 -6
  4. package/dist/action-cli-bridge.d.ts +1 -0
  5. package/dist/action-cli-bridge.d.ts.map +1 -1
  6. package/dist/ai-review-service.d.ts.map +1 -1
  7. package/dist/check-execution-engine.d.ts +8 -2
  8. package/dist/check-execution-engine.d.ts.map +1 -1
  9. package/dist/cli-main.d.ts.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/config.d.ts +5 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/debug-visualizer/debug-span-exporter.d.ts +47 -0
  14. package/dist/debug-visualizer/debug-span-exporter.d.ts.map +1 -0
  15. package/dist/debug-visualizer/trace-reader.d.ts +117 -0
  16. package/dist/debug-visualizer/trace-reader.d.ts.map +1 -0
  17. package/dist/debug-visualizer/ui/index.html +2568 -0
  18. package/dist/debug-visualizer/ws-server.d.ts +99 -0
  19. package/dist/debug-visualizer/ws-server.d.ts.map +1 -0
  20. package/dist/defaults/.visor.yaml +7 -6
  21. package/dist/failure-condition-evaluator.d.ts.map +1 -1
  22. package/dist/generated/config-schema.d.ts +7 -3
  23. package/dist/generated/config-schema.d.ts.map +1 -1
  24. package/dist/generated/config-schema.json +7 -3
  25. package/dist/git-repository-analyzer.d.ts +1 -7
  26. package/dist/git-repository-analyzer.d.ts.map +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +17668 -1760
  29. package/dist/liquid-extensions.d.ts +1 -1
  30. package/dist/liquid-extensions.d.ts.map +1 -1
  31. package/dist/output/code-review/schema.json +2 -2
  32. package/dist/output/traces/run-2025-10-22T10-40-34-055Z.ndjson +218 -0
  33. package/dist/pr-analyzer.d.ts +2 -1
  34. package/dist/pr-analyzer.d.ts.map +1 -1
  35. package/dist/providers/ai-check-provider.d.ts.map +1 -1
  36. package/dist/providers/check-provider-registry.d.ts.map +1 -1
  37. package/dist/providers/check-provider.interface.d.ts +17 -6
  38. package/dist/providers/check-provider.interface.d.ts.map +1 -1
  39. package/dist/providers/command-check-provider.d.ts.map +1 -1
  40. package/dist/providers/github-ops-provider.d.ts.map +1 -1
  41. package/dist/providers/http-check-provider.d.ts.map +1 -1
  42. package/dist/providers/human-input-check-provider.d.ts +78 -0
  43. package/dist/providers/human-input-check-provider.d.ts.map +1 -0
  44. package/dist/providers/index.d.ts +2 -1
  45. package/dist/providers/index.d.ts.map +1 -1
  46. package/dist/providers/mcp-check-provider.d.ts.map +1 -1
  47. package/dist/providers/memory-check-provider.d.ts.map +1 -1
  48. package/dist/sdk/check-execution-engine-F3662LY7.mjs +11 -0
  49. package/dist/sdk/{chunk-I3GQJIR7.mjs → chunk-B5QBV2QJ.mjs} +2 -2
  50. package/dist/sdk/chunk-B5QBV2QJ.mjs.map +1 -0
  51. package/dist/sdk/{chunk-IG3BFIIN.mjs → chunk-FVS5CJ5S.mjs} +30 -1
  52. package/dist/sdk/chunk-FVS5CJ5S.mjs.map +1 -0
  53. package/dist/sdk/{chunk-YXOWIDEF.mjs → chunk-TUTOLSFV.mjs} +15 -3
  54. package/dist/sdk/chunk-TUTOLSFV.mjs.map +1 -0
  55. package/dist/sdk/{chunk-4VK6WTYU.mjs → chunk-X2JKUOE5.mjs} +1375 -570
  56. package/dist/sdk/chunk-X2JKUOE5.mjs.map +1 -0
  57. package/dist/sdk/{liquid-extensions-GMEGEGC3.mjs → liquid-extensions-KVL4MKRH.mjs} +2 -2
  58. package/dist/sdk/{mermaid-telemetry-4DUEYCLE.mjs → mermaid-telemetry-FBF6D35S.mjs} +2 -2
  59. package/dist/sdk/sdk.d.mts +62 -4
  60. package/dist/sdk/sdk.d.ts +62 -4
  61. package/dist/sdk/sdk.js +1658 -723
  62. package/dist/sdk/sdk.js.map +1 -1
  63. package/dist/sdk/sdk.mjs +60 -15
  64. package/dist/sdk/sdk.mjs.map +1 -1
  65. package/dist/sdk/{tracer-init-RJGAIOBP.mjs → tracer-init-WC75N5NW.mjs} +2 -2
  66. package/dist/sdk.d.ts +5 -2
  67. package/dist/sdk.d.ts.map +1 -1
  68. package/dist/telemetry/file-span-exporter.d.ts.map +1 -1
  69. package/dist/telemetry/opentelemetry.d.ts +2 -0
  70. package/dist/telemetry/opentelemetry.d.ts.map +1 -1
  71. package/dist/telemetry/state-capture.d.ts +53 -0
  72. package/dist/telemetry/state-capture.d.ts.map +1 -0
  73. package/dist/telemetry/trace-helpers.d.ts.map +1 -1
  74. package/dist/traces/run-2025-10-22T10-40-34-055Z.ndjson +218 -0
  75. package/dist/types/cli.d.ts +6 -0
  76. package/dist/types/cli.d.ts.map +1 -1
  77. package/dist/types/config.d.ts +44 -3
  78. package/dist/types/config.d.ts.map +1 -1
  79. package/dist/utils/config-loader.d.ts +5 -0
  80. package/dist/utils/config-loader.d.ts.map +1 -1
  81. package/dist/utils/file-exclusion.d.ts +50 -0
  82. package/dist/utils/file-exclusion.d.ts.map +1 -0
  83. package/dist/utils/interactive-prompt.d.ts +26 -0
  84. package/dist/utils/interactive-prompt.d.ts.map +1 -0
  85. package/dist/utils/sandbox.d.ts +26 -0
  86. package/dist/utils/sandbox.d.ts.map +1 -0
  87. package/dist/utils/stdin-reader.d.ts +22 -0
  88. package/dist/utils/stdin-reader.d.ts.map +1 -0
  89. package/dist/utils/tracer-init.d.ts +0 -5
  90. package/dist/utils/tracer-init.d.ts.map +1 -1
  91. package/package.json +8 -4
  92. package/dist/output/traces/run-2025-10-19T14-24-36-341Z.ndjson +0 -40
  93. package/dist/output/traces/run-2025-10-19T14-24-48-674Z.ndjson +0 -40
  94. package/dist/output/traces/run-2025-10-19T14-24-49-238Z.ndjson +0 -40
  95. package/dist/output/traces/run-2025-10-19T14-24-49-761Z.ndjson +0 -40
  96. package/dist/output/traces/run-2025-10-19T14-24-50-279Z.ndjson +0 -12
  97. package/dist/sdk/check-execution-engine-S7BFPVWA.mjs +0 -11
  98. package/dist/sdk/chunk-4VK6WTYU.mjs.map +0 -1
  99. package/dist/sdk/chunk-I3GQJIR7.mjs.map +0 -1
  100. package/dist/sdk/chunk-IG3BFIIN.mjs.map +0 -1
  101. package/dist/sdk/chunk-YXOWIDEF.mjs.map +0 -1
  102. package/dist/traces/run-2025-10-19T14-24-36-341Z.ndjson +0 -40
  103. package/dist/traces/run-2025-10-19T14-24-48-674Z.ndjson +0 -40
  104. package/dist/traces/run-2025-10-19T14-24-49-238Z.ndjson +0 -40
  105. package/dist/traces/run-2025-10-19T14-24-49-761Z.ndjson +0 -40
  106. package/dist/traces/run-2025-10-19T14-24-50-279Z.ndjson +0 -12
  107. /package/dist/sdk/{check-execution-engine-S7BFPVWA.mjs.map → check-execution-engine-F3662LY7.mjs.map} +0 -0
  108. /package/dist/sdk/{liquid-extensions-GMEGEGC3.mjs.map → liquid-extensions-KVL4MKRH.mjs.map} +0 -0
  109. /package/dist/sdk/{mermaid-telemetry-4DUEYCLE.mjs.map → mermaid-telemetry-FBF6D35S.mjs.map} +0 -0
  110. /package/dist/sdk/{tracer-init-RJGAIOBP.mjs.map → tracer-init-WC75N5NW.mjs.map} +0 -0
@@ -0,0 +1,2568 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Visor Debug Visualizer</title>
7
+ <script src="https://d3js.org/d3.v7.min.js"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
9
+ <style>
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ body {
17
+ font-family:
18
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
19
+ sans-serif;
20
+ background: #1e1e1e;
21
+ color: #d4d4d4;
22
+ overflow: hidden;
23
+ }
24
+
25
+ #app {
26
+ display: flex;
27
+ flex-direction: column;
28
+ height: 100vh;
29
+ }
30
+
31
+ /* Header */
32
+ header {
33
+ background: #252526;
34
+ padding: 16px 24px;
35
+ border-bottom: 1px solid #3e3e42;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ }
40
+
41
+ header h1 {
42
+ font-size: 18px;
43
+ font-weight: 600;
44
+ color: #cccccc;
45
+ }
46
+
47
+ .hidden {
48
+ display: none !important;
49
+ }
50
+
51
+ .header-controls {
52
+ display: flex;
53
+ gap: 12px;
54
+ align-items: center;
55
+ }
56
+
57
+ .btn {
58
+ background: #0e639c;
59
+ color: white;
60
+ border: none;
61
+ padding: 6px 14px;
62
+ border-radius: 4px;
63
+ cursor: pointer;
64
+ font-size: 13px;
65
+ transition: background 0.2s;
66
+ }
67
+
68
+ .btn:hover {
69
+ background: #1177bb;
70
+ }
71
+
72
+ .btn-secondary {
73
+ background: #3e3e42;
74
+ }
75
+
76
+ .btn-secondary:hover {
77
+ background: #4e4e52;
78
+ }
79
+
80
+ input[type='file'] {
81
+ display: none;
82
+ }
83
+
84
+ .file-info {
85
+ font-size: 12px;
86
+ color: #858585;
87
+ }
88
+
89
+ /* Main Content */
90
+ #main {
91
+ flex: 1;
92
+ display: flex;
93
+ overflow: hidden;
94
+ }
95
+
96
+ /* Config Sidebar */
97
+ #config-sidebar {
98
+ width: 800px;
99
+ background: #1e1e1e;
100
+ border-right: 1px solid #3e3e42;
101
+ overflow: hidden;
102
+ flex-shrink: 0;
103
+ display: flex;
104
+ flex-direction: column;
105
+ }
106
+
107
+ #config-sidebar.hidden {
108
+ display: none;
109
+ }
110
+
111
+ /* Graph Container */
112
+ #graph-container {
113
+ flex: 1;
114
+ position: relative;
115
+ background: #1e1e1e;
116
+ overflow: hidden;
117
+ }
118
+
119
+ #graph-svg {
120
+ width: 100%;
121
+ height: 100%;
122
+ }
123
+
124
+ /* Inspector Panel */
125
+ #inspector {
126
+ width: 400px;
127
+ background: #252526;
128
+ border-left: 1px solid #3e3e42;
129
+ display: flex;
130
+ flex-direction: column;
131
+ overflow: hidden;
132
+ }
133
+
134
+ #inspector.hidden {
135
+ display: none;
136
+ }
137
+
138
+ .inspector-header {
139
+ padding: 16px;
140
+ border-bottom: 1px solid #3e3e42;
141
+ display: flex;
142
+ justify-content: space-between;
143
+ align-items: center;
144
+ }
145
+
146
+ .inspector-header h2 {
147
+ font-size: 14px;
148
+ font-weight: 600;
149
+ color: #cccccc;
150
+ }
151
+
152
+ .close-btn {
153
+ background: none;
154
+ border: none;
155
+ color: #858585;
156
+ cursor: pointer;
157
+ font-size: 18px;
158
+ padding: 0;
159
+ width: 24px;
160
+ height: 24px;
161
+ }
162
+
163
+ .close-btn:hover {
164
+ color: #cccccc;
165
+ }
166
+
167
+ .inspector-tabs {
168
+ display: flex;
169
+ border-bottom: 1px solid #3e3e42;
170
+ background: #2d2d30;
171
+ }
172
+
173
+ .tab {
174
+ padding: 8px 16px;
175
+ background: none;
176
+ border: none;
177
+ color: #858585;
178
+ cursor: pointer;
179
+ font-size: 12px;
180
+ border-bottom: 2px solid transparent;
181
+ }
182
+
183
+ .tab:hover {
184
+ color: #cccccc;
185
+ }
186
+
187
+ .tab.active {
188
+ color: #ffffff;
189
+ border-bottom-color: #0e639c;
190
+ }
191
+
192
+ .inspector-content {
193
+ flex: 1;
194
+ overflow-y: auto;
195
+ padding: 16px;
196
+ }
197
+
198
+ .tab-panel {
199
+ display: none;
200
+ }
201
+
202
+ .tab-panel.active {
203
+ display: block;
204
+ }
205
+
206
+ .json-viewer {
207
+ font-family: 'Consolas', 'Monaco', monospace;
208
+ font-size: 12px;
209
+ line-height: 1.6;
210
+ }
211
+
212
+ .json-key {
213
+ color: #9cdcfe;
214
+ }
215
+
216
+ .json-string {
217
+ color: #ce9178;
218
+ }
219
+
220
+ .json-number {
221
+ color: #b5cea8;
222
+ }
223
+
224
+ .json-boolean {
225
+ color: #569cd6;
226
+ }
227
+
228
+ .json-null {
229
+ color: #569cd6;
230
+ }
231
+
232
+ .info-row {
233
+ display: flex;
234
+ margin-bottom: 8px;
235
+ font-size: 13px;
236
+ }
237
+
238
+ .info-label {
239
+ color: #858585;
240
+ min-width: 100px;
241
+ }
242
+
243
+ .info-value {
244
+ color: #cccccc;
245
+ }
246
+
247
+ /* Legend */
248
+ .legend {
249
+ position: absolute;
250
+ bottom: 20px;
251
+ left: 20px;
252
+ background: rgba(37, 37, 38, 0.95);
253
+ border: 1px solid #3e3e42;
254
+ border-radius: 4px;
255
+ padding: 12px 16px;
256
+ font-size: 12px;
257
+ }
258
+
259
+ .legend-title {
260
+ font-weight: 600;
261
+ margin-bottom: 8px;
262
+ color: #cccccc;
263
+ }
264
+
265
+ .legend-item {
266
+ display: flex;
267
+ align-items: center;
268
+ margin-bottom: 4px;
269
+ }
270
+
271
+ .legend-color {
272
+ width: 12px;
273
+ height: 12px;
274
+ border-radius: 2px;
275
+ margin-right: 8px;
276
+ }
277
+
278
+ /* Graph Styles */
279
+ .node {
280
+ cursor: pointer;
281
+ stroke: #3e3e42;
282
+ stroke-width: 2px;
283
+ }
284
+
285
+ .node:hover {
286
+ stroke: #ffffff;
287
+ stroke-width: 3px;
288
+ }
289
+
290
+ .node.selected {
291
+ stroke: #0e639c;
292
+ stroke-width: 3px;
293
+ }
294
+
295
+ .node-label {
296
+ font-size: 11px;
297
+ fill: #cccccc;
298
+ text-anchor: middle;
299
+ pointer-events: none;
300
+ user-select: none;
301
+ }
302
+
303
+ .link {
304
+ stroke: #3e3e42;
305
+ stroke-width: 1.5px;
306
+ stroke-opacity: 0.6;
307
+ fill: none;
308
+ }
309
+
310
+ .link-data-flow {
311
+ stroke-dasharray: 5, 5;
312
+ stroke: #569cd6;
313
+ }
314
+
315
+ /* Status Colors */
316
+ .status-pending {
317
+ fill: #6e6e6e;
318
+ }
319
+ .status-running {
320
+ fill: #0e639c;
321
+ }
322
+ .status-completed {
323
+ fill: #4ec9b0;
324
+ }
325
+ .status-error {
326
+ fill: #f48771;
327
+ }
328
+ .status-skipped {
329
+ fill: #dcdcaa;
330
+ }
331
+
332
+ /* Loading State */
333
+ #loading {
334
+ position: absolute;
335
+ top: 50%;
336
+ left: 50%;
337
+ transform: translate(-50%, -50%);
338
+ text-align: center;
339
+ }
340
+
341
+ #loading.hidden {
342
+ display: none;
343
+ }
344
+
345
+ .spinner {
346
+ border: 3px solid #3e3e42;
347
+ border-top: 3px solid #0e639c;
348
+ border-radius: 50%;
349
+ width: 40px;
350
+ height: 40px;
351
+ animation: spin 1s linear infinite;
352
+ margin: 0 auto 16px;
353
+ }
354
+
355
+ @keyframes spin {
356
+ 0% {
357
+ transform: rotate(0deg);
358
+ }
359
+ 100% {
360
+ transform: rotate(360deg);
361
+ }
362
+ }
363
+
364
+ /* Empty State */
365
+ #empty-state {
366
+ position: absolute;
367
+ top: 50%;
368
+ left: 50%;
369
+ transform: translate(-50%, -50%);
370
+ text-align: center;
371
+ color: #858585;
372
+ }
373
+
374
+ #empty-state.hidden {
375
+ display: none;
376
+ }
377
+
378
+ #empty-state h2 {
379
+ font-size: 18px;
380
+ margin-bottom: 8px;
381
+ color: #cccccc;
382
+ }
383
+
384
+ #empty-state p {
385
+ font-size: 13px;
386
+ margin-bottom: 20px;
387
+ }
388
+
389
+ /* Timeline Controls */
390
+ #timeline-container {
391
+ height: 120px;
392
+ background: #252526;
393
+ border-top: 1px solid #3e3e42;
394
+ display: flex;
395
+ flex-direction: column;
396
+ padding: 12px 24px;
397
+ }
398
+
399
+ #timeline-container.hidden {
400
+ display: none;
401
+ }
402
+
403
+ .timeline-controls {
404
+ display: flex;
405
+ align-items: center;
406
+ gap: 12px;
407
+ margin-bottom: 12px;
408
+ }
409
+
410
+ .playback-btn {
411
+ background: #3e3e42;
412
+ color: #cccccc;
413
+ border: none;
414
+ padding: 8px 12px;
415
+ border-radius: 4px;
416
+ cursor: pointer;
417
+ font-size: 14px;
418
+ transition: background 0.2s;
419
+ min-width: 36px;
420
+ height: 36px;
421
+ }
422
+
423
+ .playback-btn:hover {
424
+ background: #4e4e52;
425
+ }
426
+
427
+ .playback-btn.active {
428
+ background: #0e639c;
429
+ color: white;
430
+ }
431
+
432
+ .playback-btn:disabled {
433
+ opacity: 0.5;
434
+ cursor: not-allowed;
435
+ }
436
+
437
+ .timeline-info {
438
+ flex: 1;
439
+ display: flex;
440
+ align-items: center;
441
+ gap: 16px;
442
+ font-size: 12px;
443
+ color: #858585;
444
+ }
445
+
446
+ .timeline-time {
447
+ color: #cccccc;
448
+ font-family: 'Consolas', 'Monaco', monospace;
449
+ }
450
+
451
+ .speed-control {
452
+ display: flex;
453
+ align-items: center;
454
+ gap: 8px;
455
+ }
456
+
457
+ .speed-btn {
458
+ background: #3e3e42;
459
+ color: #858585;
460
+ border: none;
461
+ padding: 4px 8px;
462
+ border-radius: 3px;
463
+ cursor: pointer;
464
+ font-size: 11px;
465
+ }
466
+
467
+ .speed-btn.active {
468
+ background: #0e639c;
469
+ color: white;
470
+ }
471
+
472
+ /* Timeline Scrubber */
473
+ .timeline-scrubber {
474
+ position: relative;
475
+ height: 40px;
476
+ background: #1e1e1e;
477
+ border-radius: 4px;
478
+ cursor: pointer;
479
+ }
480
+
481
+ .timeline-track {
482
+ position: absolute;
483
+ top: 18px;
484
+ left: 0;
485
+ right: 0;
486
+ height: 4px;
487
+ background: #3e3e42;
488
+ border-radius: 2px;
489
+ }
490
+
491
+ .timeline-progress {
492
+ position: absolute;
493
+ top: 0;
494
+ left: 0;
495
+ height: 100%;
496
+ background: #0e639c;
497
+ border-radius: 2px;
498
+ transition: width 0.1s linear;
499
+ }
500
+
501
+ .timeline-handle {
502
+ position: absolute;
503
+ top: 50%;
504
+ transform: translate(-50%, -50%);
505
+ width: 16px;
506
+ height: 16px;
507
+ background: #ffffff;
508
+ border: 2px solid #0e639c;
509
+ border-radius: 50%;
510
+ cursor: grab;
511
+ transition: left 0.1s linear;
512
+ }
513
+
514
+ .timeline-handle:active {
515
+ cursor: grabbing;
516
+ }
517
+
518
+ .timeline-events {
519
+ position: absolute;
520
+ top: 0;
521
+ left: 0;
522
+ right: 0;
523
+ height: 100%;
524
+ pointer-events: none;
525
+ }
526
+
527
+ .timeline-event-marker {
528
+ position: absolute;
529
+ top: 50%;
530
+ transform: translate(-50%, -50%);
531
+ width: 8px;
532
+ height: 8px;
533
+ border-radius: 50%;
534
+ pointer-events: all;
535
+ cursor: pointer;
536
+ }
537
+
538
+ .timeline-event-marker.check-started {
539
+ background: #569cd6;
540
+ }
541
+
542
+ .timeline-event-marker.check-completed {
543
+ background: #4ec9b0;
544
+ }
545
+
546
+ .timeline-event-marker.check-failed {
547
+ background: #f48771;
548
+ }
549
+
550
+ .timeline-event-marker.state-snapshot {
551
+ background: #dcdcaa;
552
+ border: 2px solid #858585;
553
+ }
554
+
555
+ /* Snapshot Panel */
556
+ #snapshot-panel {
557
+ width: 350px;
558
+ background: #252526;
559
+ border-right: 1px solid #3e3e42;
560
+ display: flex;
561
+ flex-direction: column;
562
+ overflow: hidden;
563
+ }
564
+
565
+ #snapshot-panel.hidden {
566
+ display: none;
567
+ }
568
+
569
+ .snapshot-header {
570
+ padding: 16px;
571
+ border-bottom: 1px solid #3e3e42;
572
+ display: flex;
573
+ justify-content: space-between;
574
+ align-items: center;
575
+ }
576
+
577
+ .snapshot-header h2 {
578
+ font-size: 14px;
579
+ font-weight: 600;
580
+ color: #cccccc;
581
+ }
582
+
583
+ .snapshot-list {
584
+ flex: 1;
585
+ overflow-y: auto;
586
+ }
587
+
588
+ .snapshot-item {
589
+ padding: 12px 16px;
590
+ border-bottom: 1px solid #3e3e42;
591
+ cursor: pointer;
592
+ transition: background 0.2s;
593
+ }
594
+
595
+ .snapshot-item:hover {
596
+ background: #2d2d30;
597
+ }
598
+
599
+ .snapshot-item.active {
600
+ background: #094771;
601
+ border-left: 3px solid #0e639c;
602
+ }
603
+
604
+ .snapshot-item-header {
605
+ display: flex;
606
+ justify-content: space-between;
607
+ align-items: center;
608
+ margin-bottom: 4px;
609
+ }
610
+
611
+ .snapshot-check-id {
612
+ font-size: 12px;
613
+ font-weight: 600;
614
+ color: #cccccc;
615
+ }
616
+
617
+ .snapshot-time {
618
+ font-size: 11px;
619
+ color: #858585;
620
+ font-family: 'Consolas', 'Monaco', monospace;
621
+ }
622
+
623
+ .snapshot-summary {
624
+ font-size: 11px;
625
+ color: #858585;
626
+ }
627
+
628
+ /* Diff Viewer */
629
+ .diff-viewer {
630
+ font-family: 'Consolas', 'Monaco', monospace;
631
+ font-size: 12px;
632
+ line-height: 1.6;
633
+ }
634
+
635
+ .diff-added {
636
+ background: rgba(78, 201, 176, 0.2);
637
+ color: #4ec9b0;
638
+ }
639
+
640
+ .diff-removed {
641
+ background: rgba(244, 135, 113, 0.2);
642
+ color: #f48771;
643
+ }
644
+
645
+ .diff-modified {
646
+ background: rgba(220, 220, 170, 0.2);
647
+ color: #dcdcaa;
648
+ }
649
+
650
+ /* Scrollbar */
651
+ ::-webkit-scrollbar {
652
+ width: 10px;
653
+ }
654
+
655
+ ::-webkit-scrollbar-track {
656
+ background: #1e1e1e;
657
+ }
658
+
659
+ ::-webkit-scrollbar-thumb {
660
+ background: #3e3e42;
661
+ border-radius: 5px;
662
+ }
663
+
664
+ ::-webkit-scrollbar-thumb:hover {
665
+ background: #4e4e52;
666
+ }
667
+ </style>
668
+ </head>
669
+ <body>
670
+ <div id="app">
671
+ <header>
672
+ <h1>🔍 Visor Debug Visualizer</h1>
673
+ <div class="header-controls">
674
+ <!-- Live Mode Controls -->
675
+ <div
676
+ id="live-controls"
677
+ class="hidden"
678
+ style="display: flex; gap: 8px; align-items: center"
679
+ >
680
+ <button class="btn" id="btn-start-execution" onclick="liveMode.start()">
681
+ ▶ Start Execution
682
+ </button>
683
+ <button
684
+ class="btn btn-secondary hidden"
685
+ id="btn-pause-execution"
686
+ onclick="liveMode.pause()"
687
+ >
688
+ ⏸ Pause
689
+ </button>
690
+ <button
691
+ class="btn btn-secondary hidden"
692
+ id="btn-resume-execution"
693
+ onclick="liveMode.resume()"
694
+ >
695
+ ▶ Resume
696
+ </button>
697
+ <button
698
+ class="btn btn-secondary hidden"
699
+ id="btn-stop-execution"
700
+ onclick="liveMode.stop()"
701
+ >
702
+ ⏹ Stop
703
+ </button>
704
+ <button
705
+ class="btn btn-secondary hidden"
706
+ id="btn-reset-execution"
707
+ onclick="liveMode.reset()"
708
+ >
709
+ 🔄 Reset
710
+ </button>
711
+ <span id="live-status" style="font-size: 12px; color: #858585; margin-left: 8px"
712
+ >Waiting to start...</span
713
+ >
714
+ </div>
715
+
716
+ <span class="file-info" id="file-info">No trace loaded</span>
717
+ <label for="file-input" class="btn btn-secondary"> 📂 Load Trace </label>
718
+ <input type="file" id="file-input" accept=".ndjson,.json" />
719
+ </div>
720
+ </header>
721
+
722
+ <div id="main">
723
+ <!-- Config Sidebar -->
724
+ <div id="config-sidebar" class="hidden">
725
+ <div
726
+ style="
727
+ padding: 12px 16px;
728
+ background: #252526;
729
+ border-bottom: 1px solid #3e3e42;
730
+ display: flex;
731
+ justify-content: space-between;
732
+ align-items: center;
733
+ "
734
+ >
735
+ <h2 style="color: #dcdcaa; font-size: 14px; font-weight: 600">
736
+ 📋 Configuration Editor
737
+ </h2>
738
+ <button id="apply-config-btn" class="btn" style="padding: 4px 12px; font-size: 12px">
739
+ Apply Changes
740
+ </button>
741
+ </div>
742
+ <div id="config-editor-container" style="flex: 1; overflow: hidden"></div>
743
+ <div
744
+ style="
745
+ padding: 8px 16px;
746
+ background: #252526;
747
+ border-top: 1px solid #3e3e42;
748
+ font-size: 11px;
749
+ color: #858585;
750
+ "
751
+ >
752
+ Edit the configuration and click "Apply Changes" to update dynamically.
753
+ </div>
754
+ </div>
755
+
756
+ <!-- Snapshot Panel -->
757
+ <div id="snapshot-panel" class="hidden">
758
+ <div class="snapshot-header">
759
+ <h2>Snapshots</h2>
760
+ <button class="close-btn" onclick="toggleSnapshotPanel()">×</button>
761
+ </div>
762
+ <div class="snapshot-list" id="snapshot-list">
763
+ <!-- Dynamically populated -->
764
+ </div>
765
+ </div>
766
+
767
+ <div id="graph-container">
768
+ <svg id="graph-svg"></svg>
769
+
770
+ <div id="loading" class="hidden">
771
+ <div class="spinner"></div>
772
+ <p>Loading trace...</p>
773
+ </div>
774
+
775
+ <div id="empty-state">
776
+ <h2>No Trace Loaded</h2>
777
+ <p>Load a trace file to visualize execution</p>
778
+ <label for="file-input" class="btn"> 📂 Load Trace File </label>
779
+ </div>
780
+
781
+ <div
782
+ id="config-display"
783
+ class="hidden"
784
+ style="padding: 40px; max-width: 1200px; margin: 0 auto; color: #cccccc"
785
+ >
786
+ <h2 style="color: #dcdcaa; margin-bottom: 24px">📋 Loaded Configuration</h2>
787
+ <pre
788
+ id="config-content"
789
+ style="
790
+ background: #1e1e1e;
791
+ padding: 24px;
792
+ border-radius: 8px;
793
+ overflow-x: auto;
794
+ font-family: 'Consolas', 'Monaco', monospace;
795
+ font-size: 13px;
796
+ line-height: 1.6;
797
+ border: 1px solid #3e3e42;
798
+ color: #d4d4d4;
799
+ white-space: pre-wrap;
800
+ word-wrap: break-word;
801
+ "
802
+ ></pre>
803
+ <p style="color: #858585; margin-top: 16px; font-size: 13px">
804
+ Click "Start Execution" to begin analyzing your repository with this configuration.
805
+ </p>
806
+ </div>
807
+
808
+ <div class="legend">
809
+ <div class="legend-title">Status</div>
810
+ <div class="legend-item">
811
+ <div class="legend-color status-pending"></div>
812
+ <span>Pending</span>
813
+ </div>
814
+ <div class="legend-item">
815
+ <div class="legend-color status-running"></div>
816
+ <span>Running</span>
817
+ </div>
818
+ <div class="legend-item">
819
+ <div class="legend-color status-completed"></div>
820
+ <span>Completed</span>
821
+ </div>
822
+ <div class="legend-item">
823
+ <div class="legend-color status-error"></div>
824
+ <span>Error</span>
825
+ </div>
826
+ <div class="legend-item">
827
+ <div class="legend-color status-skipped"></div>
828
+ <span>Skipped</span>
829
+ </div>
830
+ </div>
831
+ </div>
832
+
833
+ <div id="inspector" class="hidden">
834
+ <div class="inspector-header">
835
+ <h2 id="inspector-title">Check Details</h2>
836
+ <button class="close-btn" onclick="closeInspector()">×</button>
837
+ </div>
838
+
839
+ <div class="inspector-tabs">
840
+ <button class="tab active" onclick="switchTab('overview', event)">Overview</button>
841
+ <button class="tab" onclick="switchTab('input', event)">Input</button>
842
+ <button class="tab" onclick="switchTab('output', event)">Output</button>
843
+ <button class="tab" onclick="switchTab('events', event)">Events</button>
844
+ <button class="tab" onclick="switchTab('diff', event)">Diff</button>
845
+ <button class="tab" onclick="switchTab('config', event)">Config</button>
846
+ <button class="tab" onclick="switchTab('results', event)">Results</button>
847
+ </div>
848
+
849
+ <div class="inspector-content">
850
+ <div id="tab-overview" class="tab-panel active"></div>
851
+ <div id="tab-input" class="tab-panel"></div>
852
+ <div id="tab-output" class="tab-panel"></div>
853
+ <div id="tab-events" class="tab-panel"></div>
854
+ <div id="tab-diff" class="tab-panel"></div>
855
+ <div id="tab-config" class="tab-panel"></div>
856
+ <div id="tab-results" class="tab-panel"></div>
857
+ </div>
858
+ </div>
859
+ </div>
860
+
861
+ <!-- Timeline Container -->
862
+ <div id="timeline-container" class="hidden">
863
+ <div class="timeline-controls">
864
+ <button
865
+ class="playback-btn"
866
+ id="btn-first"
867
+ onclick="timeTravel.seekToStart()"
868
+ title="First"
869
+ >
870
+
871
+ </button>
872
+ <button
873
+ class="playback-btn"
874
+ id="btn-prev"
875
+ onclick="timeTravel.stepBackward()"
876
+ title="Previous"
877
+ >
878
+
879
+ </button>
880
+ <button
881
+ class="playback-btn"
882
+ id="btn-play"
883
+ onclick="timeTravel.togglePlay()"
884
+ title="Play/Pause"
885
+ >
886
+
887
+ </button>
888
+ <button
889
+ class="playback-btn"
890
+ id="btn-next"
891
+ onclick="timeTravel.stepForward()"
892
+ title="Next"
893
+ >
894
+
895
+ </button>
896
+ <button class="playback-btn" id="btn-last" onclick="timeTravel.seekToEnd()" title="Last">
897
+
898
+ </button>
899
+
900
+ <div class="timeline-info">
901
+ <span
902
+ >Event <span class="timeline-time" id="current-event">0</span> /
903
+ <span class="timeline-time" id="total-events">0</span></span
904
+ >
905
+ <span class="timeline-time" id="current-time">00:00.000</span>
906
+ </div>
907
+
908
+ <div class="speed-control">
909
+ <span>Speed:</span>
910
+ <button class="speed-btn" onclick="timeTravel.setSpeed(0.5)">0.5×</button>
911
+ <button class="speed-btn active" onclick="timeTravel.setSpeed(1)">1×</button>
912
+ <button class="speed-btn" onclick="timeTravel.setSpeed(2)">2×</button>
913
+ <button class="speed-btn" onclick="timeTravel.setSpeed(5)">5×</button>
914
+ </div>
915
+
916
+ <button
917
+ class="playback-btn"
918
+ id="btn-snapshots"
919
+ onclick="toggleSnapshotPanel()"
920
+ title="Toggle Snapshots"
921
+ >
922
+ 📸
923
+ </button>
924
+ </div>
925
+
926
+ <div class="timeline-scrubber" id="timeline-scrubber">
927
+ <div class="timeline-track">
928
+ <div class="timeline-progress" id="timeline-progress"></div>
929
+ </div>
930
+ <div class="timeline-events" id="timeline-events">
931
+ <!-- Event markers dynamically populated -->
932
+ </div>
933
+ <div class="timeline-handle" id="timeline-handle"></div>
934
+ </div>
935
+ </div>
936
+ </div>
937
+
938
+ <script>
939
+ // ========================================================================
940
+ // Global State
941
+ // ========================================================================
942
+ let currentTrace = null;
943
+ let selectedNode = null;
944
+ let simulation = null;
945
+ let previousSnapshot = null; // For diff view
946
+
947
+ // ========================================================================
948
+ // Trace Parser (Inline version of trace-reader.ts)
949
+ // ========================================================================
950
+ async function parseTraceFile(file) {
951
+ const text = await file.text();
952
+ const lines = text.trim().split('\n');
953
+ const spans = [];
954
+
955
+ for (const line of lines) {
956
+ if (!line.trim()) continue;
957
+
958
+ try {
959
+ const rawSpan = JSON.parse(line);
960
+ const span = processRawSpan(rawSpan);
961
+ spans.push(span);
962
+ } catch (e) {
963
+ console.warn('Failed to parse line:', e);
964
+ }
965
+ }
966
+
967
+ if (spans.length === 0) {
968
+ throw new Error('No valid spans found in trace file');
969
+ }
970
+
971
+ // Build tree
972
+ const tree = buildExecutionTree(spans);
973
+
974
+ // Extract snapshots
975
+ const snapshots = extractStateSnapshots(spans);
976
+
977
+ // Compute timeline
978
+ const timeline = computeTimeline(spans);
979
+
980
+ // Calculate metadata
981
+ const sortedSpans = [...spans].sort((a, b) => compareTimeValues(a.startTime, b.startTime));
982
+ const firstSpan = sortedSpans[0];
983
+ const lastSpan = sortedSpans[sortedSpans.length - 1];
984
+
985
+ return {
986
+ runId: tree.checkId,
987
+ traceId: firstSpan.traceId,
988
+ spans,
989
+ tree,
990
+ timeline,
991
+ snapshots,
992
+ metadata: {
993
+ startTime: timeValueToISO(firstSpan.startTime),
994
+ endTime: timeValueToISO(lastSpan.endTime),
995
+ duration: timeValueToMillis(lastSpan.endTime) - timeValueToMillis(firstSpan.startTime),
996
+ totalSpans: spans.length,
997
+ totalSnapshots: snapshots.length,
998
+ },
999
+ };
1000
+ }
1001
+
1002
+ function processRawSpan(raw) {
1003
+ return {
1004
+ traceId: raw.traceId || '',
1005
+ spanId: raw.spanId || '',
1006
+ parentSpanId: raw.parentSpanId,
1007
+ name: raw.name || 'unknown',
1008
+ startTime: raw.startTime || [0, 0],
1009
+ endTime: raw.endTime || raw.startTime || [0, 0],
1010
+ duration:
1011
+ timeValueToMillis(raw.endTime || raw.startTime) - timeValueToMillis(raw.startTime),
1012
+ attributes: raw.attributes || {},
1013
+ events: (raw.events || []).map(evt => ({
1014
+ name: evt.name || 'unknown',
1015
+ time: evt.time || [0, 0],
1016
+ timestamp: evt.timestamp || timeValueToISO(evt.time || [0, 0]),
1017
+ attributes: evt.attributes || {},
1018
+ })),
1019
+ status: raw.status?.code === 2 ? 'error' : 'ok',
1020
+ };
1021
+ }
1022
+
1023
+ function buildExecutionTree(spans) {
1024
+ const nodeMap = new Map();
1025
+
1026
+ // Create nodes
1027
+ for (const span of spans) {
1028
+ const node = createExecutionNode(span);
1029
+ nodeMap.set(span.spanId, node);
1030
+ }
1031
+
1032
+ // Build relationships
1033
+ let rootNode;
1034
+ for (const span of spans) {
1035
+ const node = nodeMap.get(span.spanId);
1036
+ if (!span.parentSpanId) {
1037
+ rootNode = node;
1038
+ } else {
1039
+ const parent = nodeMap.get(span.parentSpanId);
1040
+ if (parent) {
1041
+ parent.children.push(node);
1042
+ }
1043
+ }
1044
+ }
1045
+
1046
+ return (
1047
+ rootNode || {
1048
+ checkId: 'root',
1049
+ type: 'run',
1050
+ status: 'completed',
1051
+ children: Array.from(nodeMap.values()),
1052
+ span: spans[0],
1053
+ state: {},
1054
+ }
1055
+ );
1056
+ }
1057
+
1058
+ function createExecutionNode(span) {
1059
+ const attrs = span.attributes;
1060
+ // Extract check ID from span name (e.g., "visor.check.hello" -> "hello")
1061
+ let checkId = attrs['visor.check.id'] || attrs['visor.run.id'];
1062
+ if (!checkId && span.name.startsWith('visor.check.')) {
1063
+ checkId = span.name.replace('visor.check.', '');
1064
+ }
1065
+ if (!checkId) {
1066
+ checkId = span.name === 'visor.run' ? 'visor.run' : span.spanId;
1067
+ }
1068
+
1069
+ let type = 'unknown';
1070
+ if (span.name === 'visor.run') type = 'run';
1071
+ else if (span.name.startsWith('visor.check.')) type = 'check';
1072
+ else if (span.name === 'visor.check') type = 'check';
1073
+ else if (span.name.startsWith('visor.provider.')) type = 'provider';
1074
+
1075
+ let status = 'completed';
1076
+ if (span.status === 'error') status = 'error';
1077
+ else if (attrs['visor.check.skipped']) status = 'skipped';
1078
+
1079
+ const state = {
1080
+ inputContext: parseJSON(attrs['visor.check.input.context']),
1081
+ output: parseJSON(attrs['visor.check.output']),
1082
+ metadata: {
1083
+ type: attrs['visor.check.type'],
1084
+ duration: span.duration,
1085
+ provider: attrs['visor.provider.type'],
1086
+ },
1087
+ };
1088
+
1089
+ if (span.status === 'error') {
1090
+ state.errors = [attrs['visor.check.error'] || 'Unknown error'];
1091
+ }
1092
+
1093
+ return { checkId, type, status, children: [], span, state };
1094
+ }
1095
+
1096
+ function extractStateSnapshots(spans) {
1097
+ const snapshots = [];
1098
+ for (const span of spans) {
1099
+ for (const event of span.events) {
1100
+ if (event.name === 'state.snapshot') {
1101
+ const attrs = event.attributes;
1102
+ snapshots.push({
1103
+ checkId: attrs['visor.snapshot.check_id'] || 'unknown',
1104
+ timestamp: attrs['visor.snapshot.timestamp'] || event.timestamp,
1105
+ timestampNanos: event.time,
1106
+ outputs: parseJSON(attrs['visor.snapshot.outputs']) || {},
1107
+ memory: parseJSON(attrs['visor.snapshot.memory']) || {},
1108
+ });
1109
+ }
1110
+ }
1111
+ }
1112
+ snapshots.sort((a, b) => compareTimeValues(a.timestampNanos, b.timestampNanos));
1113
+ return snapshots;
1114
+ }
1115
+
1116
+ function computeTimeline(spans) {
1117
+ const events = [];
1118
+ for (const span of spans) {
1119
+ const checkId = span.attributes['visor.check.id'] || span.spanId;
1120
+
1121
+ events.push({
1122
+ type: 'check.started',
1123
+ checkId,
1124
+ timestamp: timeValueToISO(span.startTime),
1125
+ timestampNanos: span.startTime,
1126
+ });
1127
+
1128
+ events.push({
1129
+ type: span.status === 'error' ? 'check.failed' : 'check.completed',
1130
+ checkId,
1131
+ timestamp: timeValueToISO(span.endTime),
1132
+ timestampNanos: span.endTime,
1133
+ duration: span.duration,
1134
+ status: span.status,
1135
+ });
1136
+
1137
+ for (const evt of span.events) {
1138
+ events.push({
1139
+ type: evt.name === 'state.snapshot' ? 'state.snapshot' : 'event',
1140
+ checkId,
1141
+ timestamp: evt.timestamp,
1142
+ timestampNanos: evt.time,
1143
+ metadata: { eventName: evt.name, attributes: evt.attributes },
1144
+ });
1145
+ }
1146
+ }
1147
+ events.sort((a, b) => compareTimeValues(a.timestampNanos, b.timestampNanos));
1148
+ return events;
1149
+ }
1150
+
1151
+ // Utility functions
1152
+ function timeValueToMillis(tv) {
1153
+ return tv[0] * 1000 + tv[1] / 1_000_000;
1154
+ }
1155
+
1156
+ function timeValueToISO(tv) {
1157
+ return new Date(timeValueToMillis(tv)).toISOString();
1158
+ }
1159
+
1160
+ function compareTimeValues(a, b) {
1161
+ if (a[0] !== b[0]) return a[0] - b[0];
1162
+ return a[1] - b[1];
1163
+ }
1164
+
1165
+ function parseJSON(str) {
1166
+ if (typeof str !== 'string') return null;
1167
+ try {
1168
+ return JSON.parse(str);
1169
+ } catch (e) {
1170
+ return null;
1171
+ }
1172
+ }
1173
+
1174
+ // ========================================================================
1175
+ // File Handling
1176
+ // ========================================================================
1177
+ document.getElementById('file-input').addEventListener('change', async e => {
1178
+ const file = e.target.files[0];
1179
+ if (!file) return;
1180
+
1181
+ showLoading();
1182
+ hideEmptyState();
1183
+
1184
+ try {
1185
+ currentTrace = await parseTraceFile(file);
1186
+ document.getElementById('file-info').textContent =
1187
+ `${file.name} (${currentTrace.metadata.totalSpans} spans, ${currentTrace.metadata.duration.toFixed(0)}ms)`;
1188
+
1189
+ visualizeTrace(currentTrace);
1190
+ timeTravel.init(currentTrace);
1191
+ hideLoading();
1192
+ } catch (error) {
1193
+ alert('Failed to parse trace file: ' + error.message);
1194
+ console.error(error);
1195
+ showEmptyState();
1196
+ hideLoading();
1197
+ }
1198
+ });
1199
+
1200
+ // Check for URL parameter
1201
+ const urlParams = new URLSearchParams(window.location.search);
1202
+ const traceUrl = urlParams.get('trace');
1203
+ if (traceUrl) {
1204
+ loadTraceFromUrl(traceUrl);
1205
+ }
1206
+
1207
+ async function loadTraceFromUrl(url) {
1208
+ showLoading();
1209
+ hideEmptyState();
1210
+
1211
+ try {
1212
+ const response = await fetch(url);
1213
+ const blob = await response.blob();
1214
+ const file = new File([blob], url.split('/').pop());
1215
+ currentTrace = await parseTraceFile(file);
1216
+
1217
+ document.getElementById('file-info').textContent =
1218
+ `${url.split('/').pop()} (${currentTrace.metadata.totalSpans} spans)`;
1219
+
1220
+ visualizeTrace(currentTrace);
1221
+ timeTravel.init(currentTrace);
1222
+ hideLoading();
1223
+ } catch (error) {
1224
+ alert('Failed to load trace from URL: ' + error.message);
1225
+ console.error(error);
1226
+ showEmptyState();
1227
+ hideLoading();
1228
+ }
1229
+ }
1230
+
1231
+ // ========================================================================
1232
+ // Visualization
1233
+ // ========================================================================
1234
+ // Keep track of graph container
1235
+ let graphContainer = null;
1236
+
1237
+ function visualizeTrace(trace) {
1238
+ const svg = d3.select('#graph-svg');
1239
+ const width = document.getElementById('graph-container').clientWidth;
1240
+ const height = document.getElementById('graph-container').clientHeight;
1241
+
1242
+ // Convert tree to graph nodes and links
1243
+ const { nodes, links } = treeToGraph(trace.tree);
1244
+
1245
+ // Add hierarchy levels for vertical positioning
1246
+ assignHierarchyLevels(trace.tree);
1247
+
1248
+ // Initialize graph container and zoom on first render
1249
+ if (!graphContainer) {
1250
+ svg.selectAll('*').remove(); // Clear only on first render
1251
+ graphContainer = svg.append('g');
1252
+
1253
+ // Add zoom behavior once
1254
+ svg.call(
1255
+ d3
1256
+ .zoom()
1257
+ .scaleExtent([0.1, 4])
1258
+ .on('zoom', event => {
1259
+ graphContainer.attr('transform', event.transform);
1260
+ })
1261
+ );
1262
+ }
1263
+
1264
+ // Preserve positions of existing nodes and set initial positions for new ones
1265
+ if (simulation && simulation.nodes().length > 0) {
1266
+ const existingNodes = new Map(simulation.nodes().map(n => [n.id, n]));
1267
+ nodes.forEach(node => {
1268
+ const existing = existingNodes.get(node.id);
1269
+ if (existing) {
1270
+ // Preserve existing node position
1271
+ node.x = existing.x;
1272
+ node.y = existing.y;
1273
+ node.vx = existing.vx;
1274
+ node.vy = existing.vy;
1275
+ } else {
1276
+ // New node - set initial position based on hierarchy to avoid overlap
1277
+ node.x = width / 2 + (Math.random() - 0.5) * 100;
1278
+ node.y = 100 + (node.level || 0) * 200;
1279
+ node.vx = 0;
1280
+ node.vy = 0;
1281
+ }
1282
+ });
1283
+ } else {
1284
+ // First render - initialize all nodes
1285
+ nodes.forEach(node => {
1286
+ node.x = width / 2 + (Math.random() - 0.5) * 100;
1287
+ node.y = 100 + (node.level || 0) * 200;
1288
+ node.vx = 0;
1289
+ node.vy = 0;
1290
+ });
1291
+ }
1292
+
1293
+ // Create or update force simulation
1294
+ if (!simulation || simulation.nodes().length === 0) {
1295
+ // Initial creation
1296
+ simulation = d3
1297
+ .forceSimulation(nodes)
1298
+ .force(
1299
+ 'link',
1300
+ d3
1301
+ .forceLink(links)
1302
+ .id(d => d.id)
1303
+ .distance(200)
1304
+ .strength(1)
1305
+ )
1306
+ .force('charge', d3.forceManyBody().strength(-1000))
1307
+ .force('center', d3.forceCenter(width / 2, height / 2))
1308
+ .force('collision', d3.forceCollide().radius(70).strength(1.5))
1309
+ .force('y', d3.forceY(d => 100 + (d.level || 0) * 200).strength(0.8))
1310
+ .force('x', d3.forceX(width / 2).strength(0.05))
1311
+ .alphaDecay(0.01);
1312
+ } else {
1313
+ // Update existing simulation with new nodes/links
1314
+ simulation.nodes(nodes);
1315
+ simulation.force('link').links(links);
1316
+ simulation.alpha(0.5).restart(); // Reheat simulation to settle new nodes
1317
+ }
1318
+
1319
+ const g = graphContainer;
1320
+
1321
+ // Update links using data join
1322
+ const link = g
1323
+ .selectAll('.link')
1324
+ .data(links, d => `${d.source.id || d.source}-${d.target.id || d.target}`)
1325
+ .join(
1326
+ enter =>
1327
+ enter
1328
+ .append('path')
1329
+ .attr('class', 'link')
1330
+ .attr('stroke', '#3e3e42')
1331
+ .attr('stroke-width', 2),
1332
+ update => update,
1333
+ exit => exit.remove()
1334
+ );
1335
+
1336
+ // Update nodes using data join
1337
+ const node = g
1338
+ .selectAll('.node')
1339
+ .data(nodes, d => d.id)
1340
+ .join(
1341
+ enter =>
1342
+ enter
1343
+ .append('circle')
1344
+ .attr('class', d => `node status-${d.status}`)
1345
+ .attr('r', 25)
1346
+ .attr('stroke', '#252526')
1347
+ .attr('stroke-width', 3)
1348
+ .call(d3.drag().on('start', dragStarted).on('drag', dragged).on('end', dragEnded))
1349
+ .on('click', (event, d) => {
1350
+ event.stopPropagation();
1351
+ selectNode(d);
1352
+ }),
1353
+ update => update.attr('class', d => `node status-${d.status}`),
1354
+ exit => exit.remove()
1355
+ );
1356
+
1357
+ // Update labels using data join
1358
+ const label = g
1359
+ .selectAll('.node-label')
1360
+ .data(nodes, d => d.id)
1361
+ .join(
1362
+ enter =>
1363
+ enter
1364
+ .append('text')
1365
+ .attr('class', 'node-label')
1366
+ .attr('dy', 35)
1367
+ .attr('text-anchor', 'middle')
1368
+ .style('pointer-events', 'none')
1369
+ .style('fill', '#d4d4d4')
1370
+ .style('font-size', '12px')
1371
+ .text(d =>
1372
+ d.checkId.length > 15 ? d.checkId.substring(0, 12) + '...' : d.checkId
1373
+ ),
1374
+ update =>
1375
+ update.text(d =>
1376
+ d.checkId.length > 15 ? d.checkId.substring(0, 12) + '...' : d.checkId
1377
+ ),
1378
+ exit => exit.remove()
1379
+ );
1380
+
1381
+ // Update positions on tick
1382
+ simulation.on('tick', () => {
1383
+ link.attr('d', d => {
1384
+ const dx = d.target.x - d.source.x;
1385
+ const dy = d.target.y - d.source.y;
1386
+ const dr = Math.sqrt(dx * dx + dy * dy);
1387
+ return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;
1388
+ });
1389
+
1390
+ node.attr('cx', d => d.x).attr('cy', d => d.y);
1391
+
1392
+ label.attr('x', d => d.x).attr('y', d => d.y);
1393
+ });
1394
+ }
1395
+
1396
+ function treeToGraph(tree, nodes = [], links = [], parent = null) {
1397
+ const node = {
1398
+ id: tree.checkId,
1399
+ checkId: tree.checkId,
1400
+ type: tree.type,
1401
+ status: tree.status,
1402
+ level: tree.level || 0,
1403
+ data: tree,
1404
+ };
1405
+ nodes.push(node);
1406
+
1407
+ if (parent) {
1408
+ links.push({ source: parent.id, target: node.id });
1409
+ }
1410
+
1411
+ for (const child of tree.children) {
1412
+ treeToGraph(child, nodes, links, node);
1413
+ }
1414
+
1415
+ return { nodes, links };
1416
+ }
1417
+
1418
+ // Assign hierarchy levels for vertical layout
1419
+ function assignHierarchyLevels(tree, level = 0) {
1420
+ if (!tree) return;
1421
+ tree.level = level;
1422
+ for (const child of tree.children || []) {
1423
+ assignHierarchyLevels(child, level + 1);
1424
+ }
1425
+ }
1426
+
1427
+ // Drag handlers
1428
+ function dragStarted(event, d) {
1429
+ if (!event.active) simulation.alphaTarget(0.3).restart();
1430
+ d.fx = d.x;
1431
+ d.fy = d.y;
1432
+ }
1433
+
1434
+ function dragged(event, d) {
1435
+ d.fx = event.x;
1436
+ d.fy = event.y;
1437
+ }
1438
+
1439
+ function dragEnded(event, d) {
1440
+ if (!event.active) simulation.alphaTarget(0);
1441
+ d.fx = null;
1442
+ d.fy = null;
1443
+ }
1444
+
1445
+ // ========================================================================
1446
+ // Inspector
1447
+ // ========================================================================
1448
+ function selectNode(node) {
1449
+ selectedNode = node;
1450
+
1451
+ // Update visual selection
1452
+ d3.selectAll('.node').classed('selected', false);
1453
+ d3.selectAll('.node')
1454
+ .filter(d => d.id === node.id)
1455
+ .classed('selected', true);
1456
+
1457
+ // Show inspector
1458
+ document.getElementById('inspector').classList.remove('hidden');
1459
+ document.getElementById('inspector-title').textContent = node.checkId;
1460
+
1461
+ // Populate tabs
1462
+ populateOverview(node);
1463
+ populateInput(node);
1464
+ populateOutput(node);
1465
+ populateEvents(node);
1466
+ }
1467
+
1468
+ function closeInspector() {
1469
+ document.getElementById('inspector').classList.add('hidden');
1470
+ d3.selectAll('.node').classed('selected', false);
1471
+ selectedNode = null;
1472
+ }
1473
+
1474
+ function switchTab(tabName, event) {
1475
+ // Update tab buttons
1476
+ document.querySelectorAll('.tab').forEach(tab => {
1477
+ tab.classList.remove('active');
1478
+ });
1479
+
1480
+ // If called from onclick, highlight the clicked tab
1481
+ if (event && event.target) {
1482
+ event.target.classList.add('active');
1483
+ } else {
1484
+ // If called programmatically, find and highlight the tab by name
1485
+ document.querySelectorAll('.tab').forEach(tab => {
1486
+ if (tab.textContent.toLowerCase() === tabName.toLowerCase()) {
1487
+ tab.classList.add('active');
1488
+ }
1489
+ });
1490
+ }
1491
+
1492
+ // Update tab panels
1493
+ document.querySelectorAll('.tab-panel').forEach(panel => {
1494
+ panel.classList.remove('active');
1495
+ });
1496
+ document.getElementById(`tab-${tabName}`).classList.add('active');
1497
+ }
1498
+
1499
+ function populateOverview(node) {
1500
+ const data = node.data;
1501
+ const html = `
1502
+ <div class="info-row">
1503
+ <span class="info-label">Check ID:</span>
1504
+ <span class="info-value">${data.checkId}</span>
1505
+ </div>
1506
+ <div class="info-row">
1507
+ <span class="info-label">Type:</span>
1508
+ <span class="info-value">${data.type}</span>
1509
+ </div>
1510
+ <div class="info-row">
1511
+ <span class="info-label">Status:</span>
1512
+ <span class="info-value">${data.status}</span>
1513
+ </div>
1514
+ <div class="info-row">
1515
+ <span class="info-label">Duration:</span>
1516
+ <span class="info-value">${data.span.duration.toFixed(2)}ms</span>
1517
+ </div>
1518
+ <div class="info-row">
1519
+ <span class="info-label">Start Time:</span>
1520
+ <span class="info-value">${timeValueToISO(data.span.startTime)}</span>
1521
+ </div>
1522
+ <div class="info-row">
1523
+ <span class="info-label">End Time:</span>
1524
+ <span class="info-value">${timeValueToISO(data.span.endTime)}</span>
1525
+ </div>
1526
+ ${
1527
+ data.state.metadata?.type
1528
+ ? `
1529
+ <div class="info-row">
1530
+ <span class="info-label">Check Type:</span>
1531
+ <span class="info-value">${data.state.metadata.type}</span>
1532
+ </div>
1533
+ `
1534
+ : ''
1535
+ }
1536
+ ${
1537
+ data.state.errors
1538
+ ? `
1539
+ <div class="info-row">
1540
+ <span class="info-label">Error:</span>
1541
+ <span class="info-value" style="color: #f48771;">${data.state.errors.join(', ')}</span>
1542
+ </div>
1543
+ `
1544
+ : ''
1545
+ }
1546
+ `;
1547
+ document.getElementById('tab-overview').innerHTML = html;
1548
+ }
1549
+
1550
+ function populateInput(node) {
1551
+ const input = node.data.state.inputContext;
1552
+ const html = input
1553
+ ? `<div class="json-viewer">${syntaxHighlightJSON(input)}</div>`
1554
+ : '<p style="color: #858585;">No input context available</p>';
1555
+ document.getElementById('tab-input').innerHTML = html;
1556
+ }
1557
+
1558
+ function populateOutput(node) {
1559
+ const output = node.data.state.output;
1560
+ const html = output
1561
+ ? `<div class="json-viewer">${syntaxHighlightJSON(output)}</div>`
1562
+ : '<p style="color: #858585;">No output available</p>';
1563
+ document.getElementById('tab-output').innerHTML = html;
1564
+ }
1565
+
1566
+ function populateEvents(node) {
1567
+ const events = node.data.span.events;
1568
+ if (events.length === 0) {
1569
+ document.getElementById('tab-events').innerHTML =
1570
+ '<p style="color: #858585;">No events</p>';
1571
+ return;
1572
+ }
1573
+
1574
+ const html = events
1575
+ .map(
1576
+ evt => `
1577
+ <div style="margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid #3e3e42;">
1578
+ <div class="info-row">
1579
+ <span class="info-label">Event:</span>
1580
+ <span class="info-value">${evt.name}</span>
1581
+ </div>
1582
+ <div class="info-row">
1583
+ <span class="info-label">Time:</span>
1584
+ <span class="info-value">${evt.timestamp}</span>
1585
+ </div>
1586
+ ${
1587
+ Object.keys(evt.attributes).length > 0
1588
+ ? `
1589
+ <div style="margin-top: 8px;">
1590
+ <div class="info-label" style="margin-bottom: 4px;">Attributes:</div>
1591
+ <div class="json-viewer">${syntaxHighlightJSON(evt.attributes)}</div>
1592
+ </div>
1593
+ `
1594
+ : ''
1595
+ }
1596
+ </div>
1597
+ `
1598
+ )
1599
+ .join('');
1600
+
1601
+ document.getElementById('tab-events').innerHTML = html;
1602
+ }
1603
+
1604
+ function syntaxHighlightJSON(obj) {
1605
+ const json = JSON.stringify(obj, null, 2);
1606
+ return json
1607
+ .replace(/&/g, '&amp;')
1608
+ .replace(/</g, '&lt;')
1609
+ .replace(/>/g, '&gt;')
1610
+ .replace(
1611
+ /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
1612
+ match => {
1613
+ let cls = 'json-number';
1614
+ if (/^"/.test(match)) {
1615
+ if (/:$/.test(match)) {
1616
+ cls = 'json-key';
1617
+ } else {
1618
+ cls = 'json-string';
1619
+ }
1620
+ } else if (/true|false/.test(match)) {
1621
+ cls = 'json-boolean';
1622
+ } else if (/null/.test(match)) {
1623
+ cls = 'json-null';
1624
+ }
1625
+ return '<span class="' + cls + '">' + match + '</span>';
1626
+ }
1627
+ );
1628
+ }
1629
+
1630
+ // ========================================================================
1631
+ // UI Helpers
1632
+ // ========================================================================
1633
+ function showLoading() {
1634
+ document.getElementById('loading').classList.remove('hidden');
1635
+ }
1636
+
1637
+ function hideLoading() {
1638
+ document.getElementById('loading').classList.add('hidden');
1639
+ }
1640
+
1641
+ function showEmptyState() {
1642
+ document.getElementById('empty-state').classList.remove('hidden');
1643
+ }
1644
+
1645
+ function hideEmptyState() {
1646
+ document.getElementById('empty-state').classList.add('hidden');
1647
+ }
1648
+
1649
+ // ========================================================================
1650
+ // Time-Travel Debugging
1651
+ // ========================================================================
1652
+ const timeTravel = {
1653
+ currentIndex: 0,
1654
+ isPlaying: false,
1655
+ playbackSpeed: 1,
1656
+ playbackInterval: null,
1657
+
1658
+ init(trace) {
1659
+ if (!trace || !trace.timeline || trace.timeline.length === 0) {
1660
+ return;
1661
+ }
1662
+
1663
+ // Show timeline
1664
+ document.getElementById('timeline-container').classList.remove('hidden');
1665
+ document.getElementById('total-events').textContent = trace.timeline.length;
1666
+
1667
+ // Build timeline scrubber
1668
+ this.buildTimelineScrubber(trace.timeline);
1669
+
1670
+ // Populate snapshot panel
1671
+ this.populateSnapshotPanel(trace.snapshots);
1672
+
1673
+ // Seek to start
1674
+ this.seekToIndex(0);
1675
+ },
1676
+
1677
+ buildTimelineScrubber(timeline) {
1678
+ const eventsContainer = document.getElementById('timeline-events');
1679
+ eventsContainer.innerHTML = '';
1680
+
1681
+ if (timeline.length === 0) return;
1682
+
1683
+ const startTime = timeValueToMillis(timeline[0].timestampNanos);
1684
+ const endTime = timeValueToMillis(timeline[timeline.length - 1].timestampNanos);
1685
+ const duration = endTime - startTime;
1686
+
1687
+ timeline.forEach((event, index) => {
1688
+ const eventTime = timeValueToMillis(event.timestampNanos);
1689
+ const position = duration > 0 ? ((eventTime - startTime) / duration) * 100 : 0;
1690
+
1691
+ const marker = document.createElement('div');
1692
+ marker.className = `timeline-event-marker ${event.type.replace('.', '-')}`;
1693
+ marker.style.left = `${position}%`;
1694
+ marker.title = `${event.type} - ${event.checkId}`;
1695
+ marker.onclick = () => this.seekToIndex(index);
1696
+ eventsContainer.appendChild(marker);
1697
+ });
1698
+
1699
+ // Add scrubber click handler
1700
+ const scrubber = document.getElementById('timeline-scrubber');
1701
+ scrubber.onclick = e => {
1702
+ const rect = scrubber.getBoundingClientRect();
1703
+ const x = e.clientX - rect.left;
1704
+ const percent = x / rect.width;
1705
+ const index = Math.floor(percent * timeline.length);
1706
+ this.seekToIndex(Math.max(0, Math.min(timeline.length - 1, index)));
1707
+ };
1708
+
1709
+ // Add drag handler for handle
1710
+ const handle = document.getElementById('timeline-handle');
1711
+ let isDragging = false;
1712
+
1713
+ handle.onmousedown = e => {
1714
+ isDragging = true;
1715
+ e.preventDefault();
1716
+ };
1717
+
1718
+ document.onmousemove = e => {
1719
+ if (!isDragging) return;
1720
+
1721
+ const rect = scrubber.getBoundingClientRect();
1722
+ const x = e.clientX - rect.left;
1723
+ const percent = Math.max(0, Math.min(1, x / rect.width));
1724
+ const index = Math.floor(percent * timeline.length);
1725
+ this.seekToIndex(Math.max(0, Math.min(timeline.length - 1, index)));
1726
+ };
1727
+
1728
+ document.onmouseup = () => {
1729
+ isDragging = false;
1730
+ };
1731
+ },
1732
+
1733
+ populateSnapshotPanel(snapshots) {
1734
+ const list = document.getElementById('snapshot-list');
1735
+ list.innerHTML = '';
1736
+
1737
+ if (!snapshots || snapshots.length === 0) {
1738
+ list.innerHTML = '<p style="padding: 16px; color: #858585;">No snapshots available</p>';
1739
+ return;
1740
+ }
1741
+
1742
+ snapshots.forEach((snapshot, index) => {
1743
+ const item = document.createElement('div');
1744
+ item.className = 'snapshot-item';
1745
+ item.onclick = () => this.jumpToSnapshot(snapshot);
1746
+
1747
+ const outputCount = Object.keys(snapshot.outputs || {}).length;
1748
+ const memoryCount = Object.keys(snapshot.memory || {}).length;
1749
+
1750
+ item.innerHTML = `
1751
+ <div class="snapshot-item-header">
1752
+ <span class="snapshot-check-id">${snapshot.checkId}</span>
1753
+ <span class="snapshot-time">${new Date(snapshot.timestamp).toLocaleTimeString()}</span>
1754
+ </div>
1755
+ <div class="snapshot-summary">
1756
+ ${outputCount} outputs, ${memoryCount} memory keys
1757
+ </div>
1758
+ `;
1759
+
1760
+ list.appendChild(item);
1761
+ });
1762
+ },
1763
+
1764
+ seekToIndex(index) {
1765
+ if (!currentTrace || !currentTrace.timeline) return;
1766
+
1767
+ this.currentIndex = Math.max(0, Math.min(currentTrace.timeline.length - 1, index));
1768
+ const event = currentTrace.timeline[this.currentIndex];
1769
+
1770
+ // Update UI
1771
+ document.getElementById('current-event').textContent = this.currentIndex + 1;
1772
+
1773
+ const startTime = timeValueToMillis(currentTrace.timeline[0].timestampNanos);
1774
+ const eventTime = timeValueToMillis(event.timestampNanos);
1775
+ const elapsed = eventTime - startTime;
1776
+ const seconds = Math.floor(elapsed / 1000);
1777
+ const millis = Math.floor(elapsed % 1000);
1778
+ document.getElementById('current-time').textContent =
1779
+ `${String(Math.floor(seconds / 60)).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}.${String(millis).padStart(3, '0')}`;
1780
+
1781
+ // Update scrubber position
1782
+ const percent =
1783
+ currentTrace.timeline.length > 1
1784
+ ? (this.currentIndex / (currentTrace.timeline.length - 1)) * 100
1785
+ : 0;
1786
+ document.getElementById('timeline-handle').style.left = `${percent}%`;
1787
+ document.getElementById('timeline-progress').style.width = `${percent}%`;
1788
+
1789
+ // Apply state at this point in time
1790
+ this.applyStateAtIndex(this.currentIndex);
1791
+ },
1792
+
1793
+ applyStateAtIndex(index) {
1794
+ if (!currentTrace) return;
1795
+
1796
+ // Build execution state up to this point
1797
+ const eventsUpToNow = currentTrace.timeline.slice(0, index + 1);
1798
+ const activeChecks = new Set();
1799
+ const completedChecks = new Set();
1800
+ const failedChecks = new Set();
1801
+
1802
+ for (const event of eventsUpToNow) {
1803
+ if (event.type === 'check.started') {
1804
+ activeChecks.add(event.checkId);
1805
+ } else if (event.type === 'check.completed') {
1806
+ activeChecks.delete(event.checkId);
1807
+ completedChecks.add(event.checkId);
1808
+ } else if (event.type === 'check.failed') {
1809
+ activeChecks.delete(event.checkId);
1810
+ failedChecks.add(event.checkId);
1811
+ }
1812
+ }
1813
+
1814
+ // Update graph node colors to reflect state at this point
1815
+ d3.selectAll('.node').attr('class', d => {
1816
+ const checkId = d.checkId;
1817
+ let status = 'pending';
1818
+
1819
+ if (failedChecks.has(checkId)) {
1820
+ status = 'error';
1821
+ } else if (completedChecks.has(checkId)) {
1822
+ status = 'completed';
1823
+ } else if (activeChecks.has(checkId)) {
1824
+ status = 'running';
1825
+ }
1826
+
1827
+ return `node status-${status}`;
1828
+ });
1829
+
1830
+ // Highlight current event's check
1831
+ const currentEvent = currentTrace.timeline[index];
1832
+ if (currentEvent && currentEvent.checkId) {
1833
+ d3.selectAll('.node')
1834
+ .filter(d => d.checkId === currentEvent.checkId)
1835
+ .classed('selected', true);
1836
+ }
1837
+ },
1838
+
1839
+ togglePlay() {
1840
+ if (this.isPlaying) {
1841
+ this.pause();
1842
+ } else {
1843
+ this.play();
1844
+ }
1845
+ },
1846
+
1847
+ play() {
1848
+ if (!currentTrace || !currentTrace.timeline) return;
1849
+
1850
+ this.isPlaying = true;
1851
+ document.getElementById('btn-play').textContent = '⏸';
1852
+ document.getElementById('btn-play').classList.add('active');
1853
+
1854
+ const baseInterval = 100; // 100ms per event at 1x speed
1855
+ const interval = baseInterval / this.playbackSpeed;
1856
+
1857
+ this.playbackInterval = setInterval(() => {
1858
+ if (this.currentIndex >= currentTrace.timeline.length - 1) {
1859
+ this.pause();
1860
+ return;
1861
+ }
1862
+ this.seekToIndex(this.currentIndex + 1);
1863
+ }, interval);
1864
+ },
1865
+
1866
+ pause() {
1867
+ this.isPlaying = false;
1868
+ document.getElementById('btn-play').textContent = '▶';
1869
+ document.getElementById('btn-play').classList.remove('active');
1870
+
1871
+ if (this.playbackInterval) {
1872
+ clearInterval(this.playbackInterval);
1873
+ this.playbackInterval = null;
1874
+ }
1875
+ },
1876
+
1877
+ stepForward() {
1878
+ if (!currentTrace) return;
1879
+ this.pause();
1880
+ this.seekToIndex(this.currentIndex + 1);
1881
+ },
1882
+
1883
+ stepBackward() {
1884
+ this.pause();
1885
+ this.seekToIndex(this.currentIndex - 1);
1886
+ },
1887
+
1888
+ seekToStart() {
1889
+ this.pause();
1890
+ this.seekToIndex(0);
1891
+ },
1892
+
1893
+ seekToEnd() {
1894
+ if (!currentTrace) return;
1895
+ this.pause();
1896
+ this.seekToIndex(currentTrace.timeline.length - 1);
1897
+ },
1898
+
1899
+ setSpeed(speed) {
1900
+ this.playbackSpeed = speed;
1901
+
1902
+ // Update active speed button
1903
+ document.querySelectorAll('.speed-btn').forEach(btn => {
1904
+ btn.classList.remove('active');
1905
+ if (btn.textContent === `${speed}×`) {
1906
+ btn.classList.add('active');
1907
+ }
1908
+ });
1909
+
1910
+ // Restart playback if playing
1911
+ if (this.isPlaying) {
1912
+ this.pause();
1913
+ this.play();
1914
+ }
1915
+ },
1916
+
1917
+ jumpToSnapshot(snapshot) {
1918
+ if (!currentTrace) return;
1919
+
1920
+ // Find the timeline event for this snapshot
1921
+ const index = currentTrace.timeline.findIndex(
1922
+ evt =>
1923
+ evt.type === 'state.snapshot' &&
1924
+ evt.checkId === snapshot.checkId &&
1925
+ evt.timestamp === snapshot.timestamp
1926
+ );
1927
+
1928
+ if (index >= 0) {
1929
+ this.seekToIndex(index);
1930
+ }
1931
+
1932
+ // Update snapshot panel active state
1933
+ const items = document.querySelectorAll('.snapshot-item');
1934
+ items.forEach((item, i) => {
1935
+ item.classList.toggle('active', currentTrace.snapshots[i] === snapshot);
1936
+ });
1937
+
1938
+ // Show diff if there's a previous snapshot
1939
+ if (previousSnapshot) {
1940
+ this.showDiff(previousSnapshot, snapshot);
1941
+ }
1942
+ previousSnapshot = snapshot;
1943
+ },
1944
+
1945
+ showDiff(prev, current) {
1946
+ const diffHtml = this.computeDiff(prev.outputs, current.outputs);
1947
+ document.getElementById('tab-diff').innerHTML = diffHtml;
1948
+ },
1949
+
1950
+ computeDiff(prevOutputs, currentOutputs) {
1951
+ const allKeys = new Set([
1952
+ ...Object.keys(prevOutputs || {}),
1953
+ ...Object.keys(currentOutputs || {}),
1954
+ ]);
1955
+
1956
+ if (allKeys.size === 0) {
1957
+ return '<p style="color: #858585;">No outputs to compare</p>';
1958
+ }
1959
+
1960
+ const changes = [];
1961
+
1962
+ for (const key of allKeys) {
1963
+ const prevValue = prevOutputs?.[key];
1964
+ const currentValue = currentOutputs?.[key];
1965
+
1966
+ if (prevValue === undefined && currentValue !== undefined) {
1967
+ changes.push({
1968
+ type: 'added',
1969
+ key,
1970
+ value: currentValue,
1971
+ });
1972
+ } else if (prevValue !== undefined && currentValue === undefined) {
1973
+ changes.push({
1974
+ type: 'removed',
1975
+ key,
1976
+ value: prevValue,
1977
+ });
1978
+ } else if (JSON.stringify(prevValue) !== JSON.stringify(currentValue)) {
1979
+ changes.push({
1980
+ type: 'modified',
1981
+ key,
1982
+ prevValue,
1983
+ currentValue,
1984
+ });
1985
+ }
1986
+ }
1987
+
1988
+ if (changes.length === 0) {
1989
+ return '<p style="color: #858585;">No changes detected</p>';
1990
+ }
1991
+
1992
+ let html = '<div class="diff-viewer">';
1993
+ for (const change of changes) {
1994
+ if (change.type === 'added') {
1995
+ html += `<div class="diff-added">+ ${change.key}: ${JSON.stringify(change.value, null, 2)}</div>`;
1996
+ } else if (change.type === 'removed') {
1997
+ html += `<div class="diff-removed">- ${change.key}: ${JSON.stringify(change.value, null, 2)}</div>`;
1998
+ } else if (change.type === 'modified') {
1999
+ html += `<div class="diff-modified">~ ${change.key}:</div>`;
2000
+ html += `<div class="diff-removed"> - ${JSON.stringify(change.prevValue, null, 2)}</div>`;
2001
+ html += `<div class="diff-added"> + ${JSON.stringify(change.currentValue, null, 2)}</div>`;
2002
+ }
2003
+ }
2004
+ html += '</div>';
2005
+ return html;
2006
+ },
2007
+ };
2008
+
2009
+ function toggleSnapshotPanel() {
2010
+ const panel = document.getElementById('snapshot-panel');
2011
+ panel.classList.toggle('hidden');
2012
+ document.getElementById('btn-snapshots').classList.toggle('active');
2013
+ }
2014
+
2015
+ // Keyboard shortcuts
2016
+ document.addEventListener('keydown', e => {
2017
+ if (!currentTrace || !currentTrace.timeline) return;
2018
+
2019
+ // Ignore if user is typing in an input
2020
+ if (e.target.tagName === 'INPUT') return;
2021
+
2022
+ switch (e.key) {
2023
+ case ' ': // Space - play/pause
2024
+ e.preventDefault();
2025
+ timeTravel.togglePlay();
2026
+ break;
2027
+ case 'ArrowLeft': // Left arrow - step backward
2028
+ e.preventDefault();
2029
+ timeTravel.stepBackward();
2030
+ break;
2031
+ case 'ArrowRight': // Right arrow - step forward
2032
+ e.preventDefault();
2033
+ timeTravel.stepForward();
2034
+ break;
2035
+ case 'Home': // Home - seek to start
2036
+ e.preventDefault();
2037
+ timeTravel.seekToStart();
2038
+ break;
2039
+ case 'End': // End - seek to end
2040
+ e.preventDefault();
2041
+ timeTravel.seekToEnd();
2042
+ break;
2043
+ case 's': // S - toggle snapshot panel
2044
+ if (!e.ctrlKey && !e.metaKey) {
2045
+ toggleSnapshotPanel();
2046
+ }
2047
+ break;
2048
+ }
2049
+ });
2050
+
2051
+ // ========================================================================
2052
+ // HTTP Polling Live Mode with Execution Control
2053
+ // ========================================================================
2054
+ const liveMode = {
2055
+ isLive: false,
2056
+ isRunning: false,
2057
+ isPaused: false,
2058
+ pollingInterval: null,
2059
+ lastSpanCount: 0,
2060
+ baseUrl: '',
2061
+ configEditor: null,
2062
+ currentConfig: null,
2063
+
2064
+ init(baseUrl) {
2065
+ console.log('[live] Initializing live mode with base URL:', baseUrl);
2066
+ this.isLive = true;
2067
+ this.baseUrl = baseUrl;
2068
+
2069
+ // Show live controls
2070
+ document.getElementById('live-controls').classList.remove('hidden');
2071
+ document.getElementById('file-info').textContent = 'Live Mode - Ready';
2072
+
2073
+ // Load initial config (will hide empty state once loaded)
2074
+ this.loadConfig();
2075
+ },
2076
+
2077
+ async loadConfig() {
2078
+ try {
2079
+ console.log('[live] Fetching config from:', `${this.baseUrl}/api/config`);
2080
+ const response = await fetch(`${this.baseUrl}/api/config`);
2081
+ const data = await response.json();
2082
+ console.log('[live] Loaded config:', data);
2083
+ console.log('[live] Config data exists?', !!data.config);
2084
+ console.log('[live] Config keys:', data.config ? Object.keys(data.config) : 'none');
2085
+
2086
+ if (data.config) {
2087
+ // Store config
2088
+ this.currentConfig = data.config;
2089
+
2090
+ // Initialize Monaco editor
2091
+ this.initializeEditor(data.config);
2092
+
2093
+ // Show config sidebar
2094
+ document.getElementById('config-sidebar').classList.remove('hidden');
2095
+
2096
+ console.log('[live] Config editor initialized successfully');
2097
+ } else {
2098
+ console.warn('[live] No config data received');
2099
+ }
2100
+ } catch (error) {
2101
+ console.error('[live] Failed to load config:', error);
2102
+ }
2103
+ },
2104
+
2105
+ initializeEditor(config) {
2106
+ // Convert config to YAML
2107
+ const yamlText = this.objectToYAML(config, 0);
2108
+
2109
+ // Configure Monaco
2110
+ require.config({
2111
+ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' },
2112
+ });
2113
+
2114
+ require(['vs/editor/editor.main'], () => {
2115
+ // Create editor
2116
+ this.configEditor = monaco.editor.create(
2117
+ document.getElementById('config-editor-container'),
2118
+ {
2119
+ value: yamlText,
2120
+ language: 'yaml',
2121
+ theme: 'vs-dark',
2122
+ automaticLayout: true,
2123
+ fontSize: 13,
2124
+ lineNumbers: 'on',
2125
+ minimap: { enabled: false },
2126
+ scrollBeyondLastLine: false,
2127
+ wordWrap: 'off',
2128
+ wrappingStrategy: 'advanced',
2129
+ tabSize: 2,
2130
+ }
2131
+ );
2132
+
2133
+ console.log('[live] Monaco editor created');
2134
+
2135
+ // Handle apply button
2136
+ document.getElementById('apply-config-btn').onclick = () => this.applyConfigChanges();
2137
+ });
2138
+ },
2139
+
2140
+ async applyConfigChanges() {
2141
+ try {
2142
+ const yamlText = this.configEditor.getValue();
2143
+ console.log('[live] Applying config changes...');
2144
+ console.log('[live] New YAML:', yamlText.substring(0, 200));
2145
+
2146
+ // Parse YAML to JSON (simple conversion - you might want a proper YAML parser)
2147
+ // For now, we'll send the YAML text and let the server handle it
2148
+ const response = await fetch(`${this.baseUrl}/api/config`, {
2149
+ method: 'POST',
2150
+ headers: { 'Content-Type': 'application/json' },
2151
+ body: JSON.stringify({ yaml: yamlText }),
2152
+ });
2153
+
2154
+ if (response.ok) {
2155
+ console.log('[live] Config updated successfully');
2156
+ alert('Configuration updated! Changes will apply to the next execution.');
2157
+ } else {
2158
+ console.error('[live] Failed to update config');
2159
+ alert('Failed to update configuration. Check console for details.');
2160
+ }
2161
+ } catch (error) {
2162
+ console.error('[live] Error applying config:', error);
2163
+ alert('Error: ' + error.message);
2164
+ }
2165
+ },
2166
+
2167
+ async start() {
2168
+ if (this.isRunning) return;
2169
+
2170
+ console.log('[live] Sending start signal to server...');
2171
+
2172
+ // Send start signal to server
2173
+ try {
2174
+ const response = await fetch(`${this.baseUrl}/api/start`, {
2175
+ method: 'POST',
2176
+ headers: { 'Content-Type': 'application/json' },
2177
+ });
2178
+
2179
+ if (!response.ok) {
2180
+ console.error('[live] Failed to send start signal:', response.statusText);
2181
+ return;
2182
+ }
2183
+
2184
+ console.log('[live] Start signal sent successfully');
2185
+ } catch (error) {
2186
+ console.error('[live] Error sending start signal:', error);
2187
+ return;
2188
+ }
2189
+
2190
+ this.isRunning = true;
2191
+ this.isPaused = false;
2192
+ console.log('[live] Starting execution - beginning HTTP polling');
2193
+
2194
+ // Hide empty state if still visible
2195
+ document.getElementById('empty-state').classList.add('hidden');
2196
+ // Keep config sidebar visible
2197
+
2198
+ // Update UI
2199
+ this.updateStatus('Running...');
2200
+ document.getElementById('btn-start-execution').classList.add('hidden');
2201
+ document.getElementById('btn-pause-execution').classList.remove('hidden');
2202
+ document.getElementById('btn-stop-execution').classList.remove('hidden');
2203
+
2204
+ // Start HTTP polling (every second)
2205
+ this.startPolling();
2206
+ },
2207
+
2208
+ startPolling() {
2209
+ console.log('[live] Starting HTTP polling (1 second interval)');
2210
+
2211
+ // Initial fetch
2212
+ this.pollSpans();
2213
+
2214
+ // Poll every second
2215
+ this.pollingInterval = setInterval(() => {
2216
+ if (!this.isPaused && this.isRunning) {
2217
+ this.pollSpans();
2218
+ }
2219
+ }, 1000);
2220
+ },
2221
+
2222
+ async pollSpans() {
2223
+ try {
2224
+ const response = await fetch(`${this.baseUrl}/api/spans`);
2225
+ const data = await response.json();
2226
+
2227
+ console.log(
2228
+ `[live] Polled ${data.total} total spans (last count: ${this.lastSpanCount})`
2229
+ );
2230
+
2231
+ // Only process if we have new spans
2232
+ if (data.total > this.lastSpanCount) {
2233
+ const newSpans = data.spans.slice(this.lastSpanCount);
2234
+ console.log(`[live] Processing ${newSpans.length} new spans`);
2235
+
2236
+ for (const span of newSpans) {
2237
+ this.handleLiveSpan(span);
2238
+ }
2239
+
2240
+ this.lastSpanCount = data.total;
2241
+ }
2242
+
2243
+ // Also poll for results
2244
+ await this.pollResults();
2245
+ } catch (error) {
2246
+ console.error('[live] Failed to poll spans:', error);
2247
+ }
2248
+ },
2249
+
2250
+ async pollResults() {
2251
+ try {
2252
+ const response = await fetch(`${this.baseUrl}/api/results`);
2253
+ const data = await response.json();
2254
+
2255
+ if (data.results) {
2256
+ console.log('[live] Results received:', data.results);
2257
+ this.displayResults(data.results);
2258
+ }
2259
+ } catch (error) {
2260
+ console.error('[live] Failed to poll results:', error);
2261
+ }
2262
+ },
2263
+
2264
+ displayResults(results) {
2265
+ const resultsTab = document.getElementById('tab-results');
2266
+ if (!resultsTab) return;
2267
+
2268
+ // Build HTML for results
2269
+ let html = '<div style="padding: 16px;">';
2270
+ html += '<h3 style="color: #dcdcaa; margin-bottom: 16px;">Execution Results</h3>';
2271
+
2272
+ // Display each check group
2273
+ for (const [checkName, checkResults] of Object.entries(results)) {
2274
+ html += `<div style="margin-bottom: 24px; border: 1px solid #3e3e42; border-radius: 4px; padding: 16px; background: #1e1e1e;">`;
2275
+ html += `<h4 style="color: #569cd6; margin-bottom: 12px;">📝 ${checkName}</h4>`;
2276
+
2277
+ if (Array.isArray(checkResults)) {
2278
+ for (const result of checkResults) {
2279
+ const statusColor =
2280
+ result.status === 'success'
2281
+ ? '#4ec9b0'
2282
+ : result.status === 'error'
2283
+ ? '#f48771'
2284
+ : '#dcdcaa';
2285
+ html += `<div style="margin-bottom: 12px; padding: 12px; background: #252526; border-left: 3px solid ${statusColor};">`;
2286
+ html += `<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">`;
2287
+ html += `<strong style="color: ${statusColor};">${result.status?.toUpperCase() || 'UNKNOWN'}</strong>`;
2288
+ if (result.duration) {
2289
+ html += `<span style="color: #858585;">${result.duration}ms</span>`;
2290
+ }
2291
+ html += `</div>`;
2292
+
2293
+ if (result.message) {
2294
+ html += `<div style="color: #cccccc; margin-bottom: 8px;">${this.escapeHtml(result.message)}</div>`;
2295
+ }
2296
+
2297
+ if (result.issues && result.issues.length > 0) {
2298
+ html += `<div style="margin-top: 12px;">`;
2299
+ html += `<strong style="color: #dcdcaa;">Issues (${result.issues.length}):</strong>`;
2300
+ html += `<ul style="margin: 8px 0; padding-left: 20px;">`;
2301
+ for (const issue of result.issues) {
2302
+ const severityColor =
2303
+ issue.severity === 'critical'
2304
+ ? '#f48771'
2305
+ : issue.severity === 'error'
2306
+ ? '#f48771'
2307
+ : issue.severity === 'warning'
2308
+ ? '#dcdcaa'
2309
+ : '#858585';
2310
+ html += `<li style="color: ${severityColor}; margin-bottom: 4px;">`;
2311
+ html += `[${issue.severity?.toUpperCase() || 'INFO'}] ${this.escapeHtml(issue.title || issue.message || 'No message')}`;
2312
+ html += `</li>`;
2313
+ }
2314
+ html += `</ul></div>`;
2315
+ }
2316
+
2317
+ html += `</div>`;
2318
+ }
2319
+ }
2320
+
2321
+ html += `</div>`;
2322
+ }
2323
+
2324
+ html += '</div>';
2325
+ resultsTab.innerHTML = html;
2326
+
2327
+ // Auto-switch to results tab after execution completes
2328
+ console.log('[live] Results displayed, total checks:', Object.keys(results).length);
2329
+ },
2330
+
2331
+ escapeHtml(text) {
2332
+ const div = document.createElement('div');
2333
+ div.textContent = text;
2334
+ return div.innerHTML;
2335
+ },
2336
+
2337
+ stopPolling() {
2338
+ if (this.pollingInterval) {
2339
+ console.log('[live] Stopping HTTP polling');
2340
+ clearInterval(this.pollingInterval);
2341
+ this.pollingInterval = null;
2342
+ }
2343
+ },
2344
+
2345
+ async pause() {
2346
+ if (!this.isRunning || this.isPaused) return;
2347
+
2348
+ this.isPaused = true;
2349
+ console.log('[live] Pausing execution');
2350
+
2351
+ try {
2352
+ await fetch(`${this.baseUrl}/api/pause`, { method: 'POST' });
2353
+ } catch (e) {
2354
+ console.warn('[live] pause endpoint failed', e);
2355
+ }
2356
+
2357
+ // Update UI
2358
+ this.updateStatus('Paused');
2359
+ document.getElementById('btn-pause-execution').classList.add('hidden');
2360
+ document.getElementById('btn-resume-execution').classList.remove('hidden');
2361
+ },
2362
+
2363
+ async resume() {
2364
+ if (!this.isPaused) return;
2365
+
2366
+ this.isPaused = false;
2367
+ console.log('[live] Resuming execution');
2368
+
2369
+ try {
2370
+ await fetch(`${this.baseUrl}/api/resume`, { method: 'POST' });
2371
+ } catch (e) {
2372
+ console.warn('[live] resume endpoint failed', e);
2373
+ }
2374
+
2375
+ // Update UI
2376
+ this.updateStatus('Running...');
2377
+ document.getElementById('btn-resume-execution').classList.add('hidden');
2378
+ document.getElementById('btn-pause-execution').classList.remove('hidden');
2379
+ },
2380
+
2381
+ async stop() {
2382
+ if (!this.isRunning) return;
2383
+
2384
+ this.isRunning = false;
2385
+ this.isPaused = false;
2386
+ console.log('[live] Stopping execution');
2387
+
2388
+ // Stop polling
2389
+ this.stopPolling();
2390
+
2391
+ try {
2392
+ await fetch(`${this.baseUrl}/api/stop`, { method: 'POST' });
2393
+ } catch (e) {
2394
+ console.warn('[live] stop endpoint failed', e);
2395
+ }
2396
+
2397
+ // Update UI
2398
+ this.updateStatus('Stopped');
2399
+ this.showResetButton();
2400
+ },
2401
+
2402
+ async reset() {
2403
+ console.log('[live] Resetting execution');
2404
+
2405
+ // Stop polling
2406
+ this.stopPolling();
2407
+
2408
+ // Clear trace
2409
+ currentTrace = null;
2410
+ this.isRunning = false;
2411
+ this.isPaused = false;
2412
+ this.lastSpanCount = 0;
2413
+
2414
+ try {
2415
+ await fetch(`${this.baseUrl}/api/reset`, { method: 'POST' });
2416
+ } catch (e) {
2417
+ console.warn('[live] reset endpoint failed', e);
2418
+ }
2419
+
2420
+ // Clear visualization
2421
+ const svg = d3.select('#graph-svg');
2422
+ svg.selectAll('*').remove();
2423
+ graphContainer = null; // Reset graph container for fresh start
2424
+ if (simulation) {
2425
+ simulation.stop();
2426
+ }
2427
+
2428
+ // Hide timeline
2429
+ document.getElementById('timeline-container').classList.add('hidden');
2430
+
2431
+ // Reset UI
2432
+ this.updateStatus('Ready - Click Start to begin execution');
2433
+ document.getElementById('btn-reset-execution').classList.add('hidden');
2434
+ document.getElementById('btn-stop-execution').classList.add('hidden');
2435
+ document.getElementById('btn-pause-execution').classList.add('hidden');
2436
+ document.getElementById('btn-resume-execution').classList.add('hidden');
2437
+ document.getElementById('btn-start-execution').classList.remove('hidden');
2438
+ },
2439
+
2440
+ displayConfig(config) {
2441
+ const configPanel = document.getElementById('tab-config');
2442
+ if (!config) {
2443
+ configPanel.innerHTML = '<p style="color: #858585;">No configuration available</p>';
2444
+ return;
2445
+ }
2446
+
2447
+ // Convert config object to readable YAML-like format
2448
+ let configText = '';
2449
+
2450
+ if (typeof config === 'string') {
2451
+ configText = config;
2452
+ } else {
2453
+ // Format as YAML-like text
2454
+ configText = this.objectToYAML(config, 0);
2455
+ }
2456
+
2457
+ configPanel.innerHTML = `
2458
+ <div class="json-viewer" style="padding: 16px;">
2459
+ <h3 style="margin-top: 0; color: #dcdcaa;">Loaded Configuration</h3>
2460
+ <pre style="background: #1e1e1e; padding: 12px; border-radius: 4px; overflow-x: auto;">${configText}</pre>
2461
+ </div>
2462
+ `;
2463
+ },
2464
+
2465
+ objectToYAML(obj, indent = 0) {
2466
+ const spaces = ' '.repeat(indent);
2467
+ let yaml = '';
2468
+
2469
+ for (const [key, value] of Object.entries(obj)) {
2470
+ if (value === null || value === undefined) {
2471
+ yaml += `${spaces}${key}: null\n`;
2472
+ } else if (Array.isArray(value)) {
2473
+ yaml += `${spaces}${key}:\n`;
2474
+ value.forEach(item => {
2475
+ if (typeof item === 'object' && item !== null) {
2476
+ yaml += `${spaces} -\n`;
2477
+ yaml += this.objectToYAML(item, indent + 2)
2478
+ .split('\n')
2479
+ .map(line => (line ? `${spaces} ${line}` : ''))
2480
+ .join('\n');
2481
+ } else {
2482
+ yaml += `${spaces} - ${item}\n`;
2483
+ }
2484
+ });
2485
+ } else if (typeof value === 'object') {
2486
+ yaml += `${spaces}${key}:\n`;
2487
+ yaml += this.objectToYAML(value, indent + 1);
2488
+ } else if (typeof value === 'string') {
2489
+ // Quote strings if they contain special characters
2490
+ const needsQuotes =
2491
+ value.includes(':') || value.includes('#') || value.includes('\n');
2492
+ yaml += `${spaces}${key}: ${needsQuotes ? `"${value}"` : value}\n`;
2493
+ } else {
2494
+ yaml += `${spaces}${key}: ${value}\n`;
2495
+ }
2496
+ }
2497
+
2498
+ return yaml;
2499
+ },
2500
+
2501
+ handleLiveSpan(span) {
2502
+ // Add span to current trace
2503
+ if (!currentTrace) {
2504
+ currentTrace = {
2505
+ runId: 'live',
2506
+ traceId: span.traceId,
2507
+ spans: [],
2508
+ tree: null,
2509
+ timeline: [],
2510
+ snapshots: [],
2511
+ metadata: {
2512
+ startTime: new Date().toISOString(),
2513
+ endTime: new Date().toISOString(),
2514
+ duration: 0,
2515
+ totalSpans: 0,
2516
+ totalSnapshots: 0,
2517
+ },
2518
+ };
2519
+ }
2520
+
2521
+ currentTrace.spans.push(span);
2522
+
2523
+ // Rebuild tree incrementally
2524
+ currentTrace.tree = buildExecutionTree(currentTrace.spans);
2525
+
2526
+ // Re-visualize with updated data
2527
+ visualizeTrace(currentTrace);
2528
+
2529
+ // Update metadata
2530
+ currentTrace.metadata.totalSpans = currentTrace.spans.length;
2531
+ currentTrace.metadata.endTime = new Date().toISOString();
2532
+
2533
+ this.updateStatus(`Running... (${currentTrace.spans.length} spans)`);
2534
+ },
2535
+
2536
+ handleStateUpdate(data) {
2537
+ // Update node state in real-time
2538
+ console.log('[live] State update for', data.checkId, data.state);
2539
+ },
2540
+
2541
+ updateStatus(text) {
2542
+ document.getElementById('live-status').textContent = text;
2543
+ document.getElementById('file-info').textContent = `Live Mode - ${text}`;
2544
+ },
2545
+
2546
+ showResetButton() {
2547
+ document.getElementById('btn-pause-execution').classList.add('hidden');
2548
+ document.getElementById('btn-resume-execution').classList.add('hidden');
2549
+ document.getElementById('btn-stop-execution').classList.add('hidden');
2550
+ document.getElementById('btn-reset-execution').classList.remove('hidden');
2551
+ },
2552
+ };
2553
+
2554
+ // Check if debug server URL is injected (live mode)
2555
+ console.log(
2556
+ '[init] Checking for DEBUG_SERVER_URL:',
2557
+ typeof window.DEBUG_SERVER_URL,
2558
+ window.DEBUG_SERVER_URL
2559
+ );
2560
+ if (typeof window.DEBUG_SERVER_URL !== 'undefined') {
2561
+ console.log('[init] DEBUG_SERVER_URL found, initializing live mode');
2562
+ liveMode.init(window.DEBUG_SERVER_URL);
2563
+ } else {
2564
+ console.log('[init] No DEBUG_SERVER_URL found, not in live mode');
2565
+ }
2566
+ </script>
2567
+ </body>
2568
+ </html>