@echomem/mcp 1.4.6 → 1.4.7

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.
@@ -0,0 +1,1699 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Codex JSONL Viewer</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ --bg: #f6f8fb;
11
+ --panel: #ffffff;
12
+ --ink: #172033;
13
+ --muted: #667085;
14
+ --line: #d9e0ea;
15
+ --line-strong: #b8c4d6;
16
+ --teal: #0f766e;
17
+ --blue: #2563eb;
18
+ --amber: #b45309;
19
+ --rose: #be123c;
20
+ --violet: #6d28d9;
21
+ --green: #15803d;
22
+ --chip: #eef2f7;
23
+ --shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
24
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
25
+ --sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26
+ }
27
+
28
+ * {
29
+ box-sizing: border-box;
30
+ }
31
+
32
+ html,
33
+ body {
34
+ height: 100%;
35
+ margin: 0;
36
+ overflow: hidden;
37
+ background: var(--bg);
38
+ color: var(--ink);
39
+ font-family: var(--sans);
40
+ font-size: 14px;
41
+ letter-spacing: 0;
42
+ }
43
+
44
+ button,
45
+ input,
46
+ select {
47
+ font: inherit;
48
+ }
49
+
50
+ button {
51
+ cursor: pointer;
52
+ }
53
+
54
+ .app {
55
+ display: grid;
56
+ grid-template-rows: auto 1fr;
57
+ height: 100%;
58
+ min-width: 760px;
59
+ }
60
+
61
+ .topbar {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 12px;
65
+ min-height: 64px;
66
+ padding: 10px 16px;
67
+ border-bottom: 1px solid var(--line);
68
+ background: rgba(255, 255, 255, 0.94);
69
+ box-shadow: 0 1px 0 rgba(15, 23, 42, 0.03);
70
+ backdrop-filter: blur(10px);
71
+ }
72
+
73
+ .title {
74
+ display: grid;
75
+ gap: 2px;
76
+ min-width: 190px;
77
+ }
78
+
79
+ .title strong {
80
+ font-size: 15px;
81
+ font-weight: 760;
82
+ }
83
+
84
+ .title span {
85
+ color: var(--muted);
86
+ font-size: 12px;
87
+ }
88
+
89
+ .file-control {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 8px;
93
+ min-width: 260px;
94
+ padding: 7px 10px;
95
+ border: 1px solid var(--line);
96
+ border-radius: 8px;
97
+ background: #fff;
98
+ }
99
+
100
+ .file-control input {
101
+ min-width: 0;
102
+ width: 100%;
103
+ color: var(--muted);
104
+ font-size: 12px;
105
+ }
106
+
107
+ .field {
108
+ display: flex;
109
+ align-items: center;
110
+ gap: 8px;
111
+ min-width: 220px;
112
+ padding: 7px 10px;
113
+ border: 1px solid var(--line);
114
+ border-radius: 8px;
115
+ background: #fff;
116
+ }
117
+
118
+ .field label {
119
+ color: var(--muted);
120
+ font-size: 12px;
121
+ white-space: nowrap;
122
+ }
123
+
124
+ .field input {
125
+ min-width: 0;
126
+ width: 100%;
127
+ border: 0;
128
+ outline: 0;
129
+ color: var(--ink);
130
+ background: transparent;
131
+ }
132
+
133
+ .field.compact {
134
+ min-width: 132px;
135
+ max-width: 148px;
136
+ }
137
+
138
+ .stats {
139
+ display: flex;
140
+ flex-wrap: wrap;
141
+ align-items: center;
142
+ gap: 6px;
143
+ margin-left: auto;
144
+ justify-content: flex-end;
145
+ }
146
+
147
+ .chip {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ gap: 5px;
151
+ min-height: 28px;
152
+ padding: 4px 8px;
153
+ border: 1px solid var(--line);
154
+ border-radius: 999px;
155
+ background: var(--chip);
156
+ color: var(--muted);
157
+ font-size: 12px;
158
+ white-space: nowrap;
159
+ }
160
+
161
+ .chip strong {
162
+ color: var(--ink);
163
+ font-weight: 720;
164
+ }
165
+
166
+ .main {
167
+ display: grid;
168
+ grid-template-columns: minmax(330px, 34vw) minmax(0, 1fr);
169
+ min-height: 0;
170
+ }
171
+
172
+ .timeline {
173
+ display: grid;
174
+ grid-template-rows: auto 1fr;
175
+ min-width: 0;
176
+ min-height: 0;
177
+ border-right: 1px solid var(--line);
178
+ background: #fbfcfe;
179
+ }
180
+
181
+ .timeline-head {
182
+ display: grid;
183
+ gap: 10px;
184
+ padding: 12px;
185
+ border-bottom: 1px solid var(--line);
186
+ background: #fff;
187
+ }
188
+
189
+ .timeline-meta {
190
+ display: flex;
191
+ justify-content: space-between;
192
+ gap: 8px;
193
+ color: var(--muted);
194
+ font-size: 12px;
195
+ }
196
+
197
+ .virtual-list {
198
+ position: relative;
199
+ min-height: 0;
200
+ overflow: auto;
201
+ outline: none;
202
+ }
203
+
204
+ .virtual-spacer {
205
+ position: relative;
206
+ width: 1px;
207
+ }
208
+
209
+ .virtual-rows {
210
+ position: absolute;
211
+ top: 0;
212
+ right: 0;
213
+ left: 0;
214
+ }
215
+
216
+ .record {
217
+ display: grid;
218
+ grid-template-columns: 70px minmax(0, 1fr);
219
+ gap: 10px;
220
+ width: 100%;
221
+ height: 66px;
222
+ padding: 7px 10px;
223
+ border: 0;
224
+ border-bottom: 1px solid #edf1f7;
225
+ border-left: 4px solid var(--stripe, var(--line));
226
+ background: transparent;
227
+ color: inherit;
228
+ text-align: left;
229
+ transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
230
+ }
231
+
232
+ .record:hover {
233
+ background: #f2f6fb;
234
+ }
235
+
236
+ .record.selected {
237
+ background: #eaf3ff;
238
+ border-left-color: var(--blue);
239
+ box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.18);
240
+ }
241
+
242
+ .line-no {
243
+ display: grid;
244
+ align-content: start;
245
+ gap: 3px;
246
+ min-width: 0;
247
+ color: var(--muted);
248
+ font-family: var(--mono);
249
+ font-size: 11px;
250
+ }
251
+
252
+ .line-no strong {
253
+ color: var(--ink);
254
+ font-size: 12px;
255
+ font-weight: 760;
256
+ }
257
+
258
+ .record-body {
259
+ display: grid;
260
+ gap: 4px;
261
+ min-width: 0;
262
+ }
263
+
264
+ .record-main {
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 6px;
268
+ min-width: 0;
269
+ }
270
+
271
+ .kind {
272
+ display: inline-flex;
273
+ align-items: center;
274
+ max-width: 128px;
275
+ min-height: 20px;
276
+ padding: 2px 6px;
277
+ border-radius: 6px;
278
+ color: #fff;
279
+ font-size: 11px;
280
+ font-weight: 720;
281
+ white-space: nowrap;
282
+ }
283
+
284
+ .kind.session { background: var(--teal); }
285
+ .kind.user { background: var(--blue); }
286
+ .kind.assistant { background: var(--green); }
287
+ .kind.call { background: var(--violet); }
288
+ .kind.output { background: var(--amber); }
289
+ .kind.tokens { background: #475467; }
290
+ .kind.compact { background: #7c3aed; }
291
+ .kind.developer { background: #475467; }
292
+ .kind.reasoning { background: #be123c; }
293
+ .kind.context { background: #0891b2; }
294
+ .kind.error { background: var(--rose); }
295
+ .kind.event { background: #0f766e; }
296
+ .kind.other { background: #344054; }
297
+
298
+ .record-type {
299
+ min-width: 0;
300
+ overflow: hidden;
301
+ color: #24324a;
302
+ font-family: var(--mono);
303
+ font-size: 12px;
304
+ text-overflow: ellipsis;
305
+ white-space: nowrap;
306
+ }
307
+
308
+ .preview {
309
+ min-width: 0;
310
+ overflow: hidden;
311
+ color: var(--muted);
312
+ font-size: 12px;
313
+ text-overflow: ellipsis;
314
+ white-space: nowrap;
315
+ }
316
+
317
+ .detail {
318
+ display: grid;
319
+ grid-template-rows: auto 1fr;
320
+ min-width: 0;
321
+ min-height: 0;
322
+ background: var(--panel);
323
+ }
324
+
325
+ .detail-head {
326
+ display: grid;
327
+ gap: 10px;
328
+ padding: 14px 16px 12px;
329
+ border-bottom: 1px solid var(--line);
330
+ background: #fff;
331
+ }
332
+
333
+ .detail-title {
334
+ display: flex;
335
+ align-items: center;
336
+ gap: 8px;
337
+ min-width: 0;
338
+ }
339
+
340
+ .detail-title h1 {
341
+ min-width: 0;
342
+ margin: 0;
343
+ overflow: hidden;
344
+ color: var(--ink);
345
+ font-size: 16px;
346
+ font-weight: 780;
347
+ text-overflow: ellipsis;
348
+ white-space: nowrap;
349
+ }
350
+
351
+ .detail-actions {
352
+ display: flex;
353
+ align-items: center;
354
+ flex-wrap: wrap;
355
+ gap: 8px;
356
+ }
357
+
358
+ .action {
359
+ min-height: 30px;
360
+ padding: 5px 9px;
361
+ border: 1px solid var(--line-strong);
362
+ border-radius: 8px;
363
+ background: #fff;
364
+ color: #26354f;
365
+ font-size: 12px;
366
+ font-weight: 650;
367
+ }
368
+
369
+ .action:hover {
370
+ border-color: var(--blue);
371
+ color: var(--blue);
372
+ }
373
+
374
+ .detail-scroll {
375
+ min-height: 0;
376
+ overflow: auto;
377
+ background:
378
+ linear-gradient(90deg, rgba(37, 99, 235, 0.05), transparent 260px),
379
+ #fff;
380
+ scroll-behavior: smooth;
381
+ }
382
+
383
+ .empty {
384
+ display: grid;
385
+ place-items: center;
386
+ height: 100%;
387
+ min-height: 360px;
388
+ padding: 24px;
389
+ color: var(--muted);
390
+ text-align: center;
391
+ }
392
+
393
+ .empty-panel {
394
+ display: grid;
395
+ gap: 8px;
396
+ max-width: 520px;
397
+ padding: 24px;
398
+ border: 1px dashed var(--line-strong);
399
+ border-radius: 8px;
400
+ background: #fbfcff;
401
+ }
402
+
403
+ .empty-panel strong {
404
+ color: var(--ink);
405
+ font-size: 16px;
406
+ }
407
+
408
+ .tree {
409
+ padding: 14px 18px 42px;
410
+ font-family: var(--mono);
411
+ font-size: 12px;
412
+ line-height: 1.5;
413
+ }
414
+
415
+ .node {
416
+ position: relative;
417
+ margin-top: 2px;
418
+ margin-left: calc(var(--depth) * 18px);
419
+ padding: 3px 0 3px 10px;
420
+ border-left: 3px solid var(--stripe);
421
+ border-radius: 0 6px 6px 0;
422
+ background: rgba(255, 255, 255, 0.68);
423
+ animation: node-in 140ms ease both;
424
+ transition: background 140ms ease, border-color 140ms ease;
425
+ }
426
+
427
+ .node:hover {
428
+ background: #f8fbff;
429
+ }
430
+
431
+ .node-row {
432
+ display: flex;
433
+ align-items: flex-start;
434
+ gap: 6px;
435
+ min-width: 0;
436
+ }
437
+
438
+ .node-row.text-row {
439
+ flex-wrap: wrap;
440
+ padding-bottom: 2px;
441
+ }
442
+
443
+ .node-row.text-row .key {
444
+ white-space: nowrap;
445
+ }
446
+
447
+ .node-row.text-row .text-block {
448
+ flex: 1 0 calc(100% - 32px);
449
+ margin-left: 25px;
450
+ }
451
+
452
+ .toggle {
453
+ display: inline-grid;
454
+ place-items: center;
455
+ width: 19px;
456
+ height: 19px;
457
+ flex: 0 0 auto;
458
+ border: 1px solid var(--line);
459
+ border-radius: 5px;
460
+ background: #fff;
461
+ color: var(--muted);
462
+ font-family: var(--mono);
463
+ font-size: 11px;
464
+ line-height: 1;
465
+ }
466
+
467
+ .toggle:hover {
468
+ border-color: var(--blue);
469
+ color: var(--blue);
470
+ }
471
+
472
+ .leaf-pad {
473
+ width: 19px;
474
+ flex: 0 0 auto;
475
+ }
476
+
477
+ .key {
478
+ color: #1d4ed8;
479
+ font-weight: 740;
480
+ overflow-wrap: anywhere;
481
+ }
482
+
483
+ .punct {
484
+ color: #98a2b3;
485
+ }
486
+
487
+ .type-label {
488
+ color: #667085;
489
+ font-size: 11px;
490
+ }
491
+
492
+ .value {
493
+ min-width: 0;
494
+ flex: 1 1 auto;
495
+ overflow-wrap: anywhere;
496
+ white-space: pre-wrap;
497
+ }
498
+
499
+ .value.string {
500
+ color: #9a3412;
501
+ line-height: 1.55;
502
+ }
503
+
504
+ .string-inline {
505
+ display: inline;
506
+ color: #9a3412;
507
+ }
508
+
509
+ .text-block {
510
+ display: grid;
511
+ gap: 6px;
512
+ width: min(100%, 980px);
513
+ min-width: 0;
514
+ margin-top: -2px;
515
+ padding: 8px 10px 9px;
516
+ border: 1px solid #ead7cb;
517
+ border-left: 4px solid #c2410c;
518
+ border-radius: 8px;
519
+ background: #fff8f3;
520
+ box-shadow: 0 6px 16px rgba(154, 52, 18, 0.06);
521
+ }
522
+
523
+ .text-block.expanded {
524
+ background: #fffdfa;
525
+ }
526
+
527
+ .text-block-header {
528
+ display: flex;
529
+ align-items: center;
530
+ justify-content: space-between;
531
+ gap: 10px;
532
+ color: #9a3412;
533
+ font-family: var(--sans);
534
+ font-size: 11px;
535
+ font-weight: 720;
536
+ }
537
+
538
+ .text-meta {
539
+ display: flex;
540
+ flex-wrap: wrap;
541
+ align-items: center;
542
+ gap: 6px;
543
+ min-width: 0;
544
+ }
545
+
546
+ .text-meta span {
547
+ padding: 1px 6px;
548
+ border: 1px solid #f1d6c7;
549
+ border-radius: 999px;
550
+ background: #fff;
551
+ color: #9a3412;
552
+ white-space: nowrap;
553
+ }
554
+
555
+ .text-content {
556
+ max-width: 100%;
557
+ margin: 0;
558
+ overflow-x: auto;
559
+ color: #7c2d12;
560
+ font-family: var(--mono);
561
+ font-size: 12px;
562
+ line-height: 1.62;
563
+ white-space: pre-wrap;
564
+ overflow-wrap: anywhere;
565
+ }
566
+
567
+ .text-block.collapsed .text-content {
568
+ max-height: 260px;
569
+ overflow: hidden;
570
+ mask-image: linear-gradient(180deg, #000 82%, transparent);
571
+ }
572
+
573
+ .value.number { color: #047857; }
574
+ .value.boolean { color: #7c3aed; }
575
+ .value.null { color: #667085; font-style: italic; }
576
+ .value.error { color: var(--rose); }
577
+
578
+ .inline-button {
579
+ margin-left: 6px;
580
+ padding: 1px 6px;
581
+ border: 1px solid var(--line);
582
+ border-radius: 6px;
583
+ background: #fff;
584
+ color: var(--blue);
585
+ font-size: 11px;
586
+ font-family: var(--sans);
587
+ font-weight: 680;
588
+ vertical-align: baseline;
589
+ transition: border-color 140ms ease, color 140ms ease, background 140ms ease;
590
+ }
591
+
592
+ .text-block .inline-button {
593
+ flex: 0 0 auto;
594
+ margin-left: 0;
595
+ border-color: #f1c5ae;
596
+ color: #9a3412;
597
+ }
598
+
599
+ .text-block .inline-button:hover {
600
+ background: #fff;
601
+ border-color: #c2410c;
602
+ }
603
+
604
+ .more-row {
605
+ margin-top: 4px;
606
+ margin-left: calc((var(--depth) + 1) * 18px);
607
+ padding: 4px 8px;
608
+ border-left: 3px solid var(--line-strong);
609
+ color: var(--muted);
610
+ font-family: var(--sans);
611
+ font-size: 12px;
612
+ }
613
+
614
+ .raw {
615
+ padding: 16px;
616
+ margin: 0;
617
+ font-family: var(--mono);
618
+ font-size: 12px;
619
+ line-height: 1.5;
620
+ white-space: pre-wrap;
621
+ overflow-wrap: anywhere;
622
+ }
623
+
624
+ .progress {
625
+ height: 3px;
626
+ overflow: hidden;
627
+ border-radius: 999px;
628
+ background: #e6edf6;
629
+ }
630
+
631
+ .progress div {
632
+ width: var(--progress, 0%);
633
+ height: 100%;
634
+ background: linear-gradient(90deg, var(--teal), var(--blue), var(--amber));
635
+ transition: width 120ms ease;
636
+ }
637
+
638
+ @keyframes node-in {
639
+ from {
640
+ opacity: 0;
641
+ transform: translateY(3px);
642
+ }
643
+ to {
644
+ opacity: 1;
645
+ transform: translateY(0);
646
+ }
647
+ }
648
+
649
+ @media (prefers-reduced-motion: reduce) {
650
+ *,
651
+ *::before,
652
+ *::after {
653
+ animation-duration: 1ms !important;
654
+ scroll-behavior: auto !important;
655
+ transition-duration: 1ms !important;
656
+ }
657
+ }
658
+
659
+ @media (max-width: 900px) {
660
+ body {
661
+ overflow: auto;
662
+ }
663
+
664
+ .app {
665
+ min-width: 0;
666
+ height: auto;
667
+ min-height: 100%;
668
+ }
669
+
670
+ .topbar,
671
+ .stats,
672
+ .detail-actions {
673
+ align-items: stretch;
674
+ }
675
+
676
+ .topbar {
677
+ flex-wrap: wrap;
678
+ }
679
+
680
+ .file-control,
681
+ .field {
682
+ min-width: 100%;
683
+ }
684
+
685
+ .main {
686
+ grid-template-columns: 1fr;
687
+ grid-template-rows: 42vh minmax(540px, 1fr);
688
+ }
689
+
690
+ .timeline {
691
+ border-right: 0;
692
+ border-bottom: 1px solid var(--line);
693
+ }
694
+ }
695
+ </style>
696
+ </head>
697
+ <body>
698
+ <div class="app">
699
+ <header class="topbar">
700
+ <div class="title">
701
+ <strong>Codex JSONL Viewer</strong>
702
+ <span>Timeline plus expandable JSON tree</span>
703
+ </div>
704
+ <label class="file-control">
705
+ <span class="chip"><strong>Open</strong></span>
706
+ <input id="fileInput" type="file" accept=".jsonl,.json,.txt,application/json">
707
+ </label>
708
+ <label class="field">
709
+ <span>Filter</span>
710
+ <input id="filterInput" type="search" placeholder="type, payload, turn, preview">
711
+ </label>
712
+ <label class="field compact">
713
+ <span>Trim</span>
714
+ <input id="trimInput" type="number" min="40" max="5000" step="20" value="320">
715
+ </label>
716
+ <div class="stats" id="stats">
717
+ <span class="chip"><strong>0</strong> records</span>
718
+ <span class="chip"><strong>0</strong> turns</span>
719
+ <span class="chip"><strong>0 B</strong></span>
720
+ </div>
721
+ </header>
722
+
723
+ <main class="main">
724
+ <section class="timeline">
725
+ <div class="timeline-head">
726
+ <div class="timeline-meta">
727
+ <span id="fileName">No file selected</span>
728
+ <span id="readState">Idle</span>
729
+ </div>
730
+ <div class="progress" aria-hidden="true"><div id="progressBar"></div></div>
731
+ </div>
732
+ <div class="virtual-list" id="list" tabindex="0">
733
+ <div class="virtual-spacer" id="spacer"></div>
734
+ <div class="virtual-rows" id="rows"></div>
735
+ </div>
736
+ </section>
737
+
738
+ <section class="detail">
739
+ <div class="detail-head">
740
+ <div class="detail-title">
741
+ <span class="kind other" id="detailKind">ready</span>
742
+ <h1 id="detailTitle">Select a JSONL file</h1>
743
+ </div>
744
+ <div class="detail-actions">
745
+ <button class="action" id="expandTop" type="button">Expand top</button>
746
+ <button class="action" id="collapseDeep" type="button">Collapse deep</button>
747
+ <button class="action" id="rawToggle" type="button">Raw JSON</button>
748
+ </div>
749
+ </div>
750
+ <div class="detail-scroll" id="detailScroll">
751
+ <div class="empty">
752
+ <div class="empty-panel">
753
+ <strong>Open a Codex session JSONL file.</strong>
754
+ <span>The left rail shows records by line. Pick one to inspect the nested payload.</span>
755
+ </div>
756
+ </div>
757
+ </div>
758
+ </section>
759
+ </main>
760
+ </div>
761
+
762
+ <script>
763
+ const ROW_HEIGHT = 66;
764
+ const WINDOW_PAD = 8;
765
+ const DEFAULT_CHILD_LIMIT = 80;
766
+ const DEFAULT_OPEN_DEPTH = 2;
767
+ const INLINE_STRING_LIMIT = 140;
768
+ const COLLAPSED_TEXT_LINES = 14;
769
+ const DEPTH_COLORS = [
770
+ "#2563eb",
771
+ "#0f766e",
772
+ "#b45309",
773
+ "#7c3aed",
774
+ "#be123c",
775
+ "#15803d",
776
+ "#475467"
777
+ ];
778
+
779
+ const state = {
780
+ file: null,
781
+ records: [],
782
+ filtered: [],
783
+ selected: -1,
784
+ selectedObject: null,
785
+ selectedRaw: "",
786
+ filter: "",
787
+ trim: 320,
788
+ turns: 0,
789
+ parseErrors: 0,
790
+ bytesRead: 0,
791
+ sourceLineNo: 0,
792
+ fullPaths: new Set(),
793
+ expandedPaths: new Set(),
794
+ collapsedPaths: new Set(),
795
+ expandedTextPaths: new Set(),
796
+ rawMode: false,
797
+ loadToken: 0
798
+ };
799
+
800
+ const els = {
801
+ fileInput: document.getElementById("fileInput"),
802
+ filterInput: document.getElementById("filterInput"),
803
+ trimInput: document.getElementById("trimInput"),
804
+ stats: document.getElementById("stats"),
805
+ fileName: document.getElementById("fileName"),
806
+ readState: document.getElementById("readState"),
807
+ progressBar: document.getElementById("progressBar"),
808
+ list: document.getElementById("list"),
809
+ spacer: document.getElementById("spacer"),
810
+ rows: document.getElementById("rows"),
811
+ detailKind: document.getElementById("detailKind"),
812
+ detailTitle: document.getElementById("detailTitle"),
813
+ detailScroll: document.getElementById("detailScroll"),
814
+ expandTop: document.getElementById("expandTop"),
815
+ collapseDeep: document.getElementById("collapseDeep"),
816
+ rawToggle: document.getElementById("rawToggle")
817
+ };
818
+
819
+ els.fileInput.addEventListener("change", () => {
820
+ const file = els.fileInput.files && els.fileInput.files[0];
821
+ if (file) {
822
+ loadFile(file);
823
+ }
824
+ });
825
+
826
+ els.filterInput.addEventListener("input", () => {
827
+ state.filter = els.filterInput.value.trim().toLowerCase();
828
+ applyFilter();
829
+ renderVirtualList();
830
+ });
831
+
832
+ els.trimInput.addEventListener("input", () => {
833
+ const next = Number(els.trimInput.value || 320);
834
+ state.trim = Math.max(40, Math.min(5000, next));
835
+ renderDetail();
836
+ });
837
+
838
+ els.list.addEventListener("scroll", renderVirtualList);
839
+ window.addEventListener("resize", renderVirtualList);
840
+
841
+ els.rows.addEventListener("click", (event) => {
842
+ const row = event.target.closest("[data-record-index]");
843
+ if (!row) return;
844
+ selectRecord(Number(row.dataset.recordIndex));
845
+ });
846
+
847
+ els.expandTop.addEventListener("click", () => {
848
+ if (!state.selectedObject) return;
849
+ state.collapsedPaths.clear();
850
+ state.expandedPaths.add("$");
851
+ state.expandedPaths.add("$.payload");
852
+ state.expandedPaths.add("$.payload.info");
853
+ state.expandedPaths.add("$.payload.info.last_token_usage");
854
+ renderDetail();
855
+ });
856
+
857
+ els.collapseDeep.addEventListener("click", () => {
858
+ state.expandedPaths.clear();
859
+ state.fullPaths.clear();
860
+ state.expandedTextPaths.clear();
861
+ state.collapsedPaths.clear();
862
+ renderDetail();
863
+ });
864
+
865
+ els.rawToggle.addEventListener("click", () => {
866
+ state.rawMode = !state.rawMode;
867
+ els.rawToggle.textContent = state.rawMode ? "Tree view" : "Raw JSON";
868
+ renderDetail();
869
+ });
870
+
871
+ initFromQuery();
872
+
873
+ async function loadFile(file) {
874
+ const token = state.loadToken + 1;
875
+ resetState();
876
+ state.loadToken = token;
877
+ state.file = file;
878
+ els.fileName.textContent = file.name;
879
+ els.readState.textContent = "Reading";
880
+ setProgress(0);
881
+ renderStats();
882
+ renderVirtualList();
883
+ showEmpty("Reading JSONL", "Records will appear as the file is scanned.");
884
+
885
+ try {
886
+ await scanJsonlFile(file, token);
887
+ if (state.loadToken !== token) return;
888
+ els.readState.textContent = state.parseErrors ? "Done with errors" : "Done";
889
+ setProgress(100);
890
+ applyFilter();
891
+ renderStats();
892
+ renderVirtualList();
893
+ if (state.filtered.length) {
894
+ await selectRecord(state.filtered[0]);
895
+ } else {
896
+ showEmpty("No records", "The selected file did not contain parseable JSONL lines.");
897
+ }
898
+ } catch (error) {
899
+ if (state.loadToken !== token) return;
900
+ els.readState.textContent = "Failed";
901
+ showError(error);
902
+ }
903
+ }
904
+
905
+ function resetState() {
906
+ state.file = null;
907
+ state.records = [];
908
+ state.filtered = [];
909
+ state.selected = -1;
910
+ state.selectedObject = null;
911
+ state.selectedRaw = "";
912
+ state.turns = 0;
913
+ state.parseErrors = 0;
914
+ state.bytesRead = 0;
915
+ state.sourceLineNo = 0;
916
+ state.fullPaths = new Set();
917
+ state.expandedPaths = new Set();
918
+ state.collapsedPaths = new Set();
919
+ state.expandedTextPaths = new Set();
920
+ state.rawMode = false;
921
+ els.rawToggle.textContent = "Raw JSON";
922
+ els.spacer.style.height = "0px";
923
+ els.rows.replaceChildren();
924
+ }
925
+
926
+ async function scanJsonlFile(file, token) {
927
+ const reader = file.stream().getReader();
928
+ let pending = new Uint8Array(0);
929
+ let chunkStart = 0;
930
+ let lastYieldAt = performance.now();
931
+
932
+ while (true) {
933
+ const result = await reader.read();
934
+ if (result.done) break;
935
+ if (state.loadToken !== token) return;
936
+ const chunk = result.value;
937
+ const combined = concatBytes(pending, chunk);
938
+ const combinedStart = chunkStart - pending.length;
939
+ let lineStart = 0;
940
+
941
+ for (let i = 0; i < combined.length; i += 1) {
942
+ if (combined[i] !== 10) continue;
943
+ const rawEnd = i > lineStart && combined[i - 1] === 13 ? i - 1 : i;
944
+ ingestLineBytes(combined.subarray(lineStart, rawEnd), combinedStart + lineStart, combinedStart + rawEnd);
945
+ lineStart = i + 1;
946
+ }
947
+
948
+ pending = combined.subarray(lineStart);
949
+ chunkStart += chunk.length;
950
+ state.bytesRead = chunkStart;
951
+
952
+ if (performance.now() - lastYieldAt > 80) {
953
+ applyFilter();
954
+ renderStats();
955
+ renderVirtualList();
956
+ setProgress(file.size ? Math.min(99, (chunkStart / file.size) * 100) : 0);
957
+ els.readState.textContent = "Reading " + formatInt(state.records.length);
958
+ await nextFrame();
959
+ lastYieldAt = performance.now();
960
+ }
961
+ }
962
+
963
+ if (pending.length) {
964
+ const end = chunkStart;
965
+ ingestLineBytes(pending, end - pending.length, end);
966
+ }
967
+ state.bytesRead = file.size;
968
+ }
969
+
970
+ function ingestLineBytes(bytes, start, end) {
971
+ state.sourceLineNo += 1;
972
+ const lineNo = state.sourceLineNo;
973
+ if (!bytes.length) return;
974
+ const raw = new TextDecoder().decode(bytes);
975
+ if (!raw.trim()) return;
976
+
977
+ let parsed = null;
978
+ let parseError = null;
979
+ try {
980
+ parsed = JSON.parse(raw);
981
+ } catch (error) {
982
+ parseError = error;
983
+ state.parseErrors += 1;
984
+ }
985
+
986
+ const summary = parsed
987
+ ? summarizeRecord(parsed, lineNo, end - start)
988
+ : summarizeErrorRecord(raw, lineNo, end - start, parseError);
989
+
990
+ state.records.push({
991
+ lineNo,
992
+ start,
993
+ end,
994
+ bytes: end - start,
995
+ ...summary
996
+ });
997
+ }
998
+
999
+ function summarizeRecord(record, lineNo, bytes) {
1000
+ const payload = isObject(record.payload) ? record.payload : {};
1001
+ const type = String(record.type || "(no type)");
1002
+ const payloadType = payload.type ? String(payload.type) : "";
1003
+ let turn = state.turns || null;
1004
+ if (type === "event_msg" && payloadType === "user_message") {
1005
+ state.turns += 1;
1006
+ turn = state.turns;
1007
+ }
1008
+
1009
+ const kind = classifyRecord(type, payloadType, payload);
1010
+ const timestamp = record.timestamp || payload.timestamp || "";
1011
+ const preview = previewForRecord(record, payload, type, payloadType);
1012
+ const label = labelForRecord(record, payload, type, payloadType);
1013
+ const searchText = [
1014
+ lineNo,
1015
+ "turn:" + (turn || ""),
1016
+ type,
1017
+ payloadType,
1018
+ kind,
1019
+ timestamp,
1020
+ preview
1021
+ ].join(" ").toLowerCase();
1022
+
1023
+ return {
1024
+ kind,
1025
+ type,
1026
+ payloadType,
1027
+ label,
1028
+ timestamp,
1029
+ turn,
1030
+ preview,
1031
+ searchText,
1032
+ error: "",
1033
+ keyCount: isObject(record) ? Object.keys(record).length : 0,
1034
+ bytes
1035
+ };
1036
+ }
1037
+
1038
+ function summarizeErrorRecord(raw, lineNo, bytes, error) {
1039
+ return {
1040
+ kind: "error",
1041
+ type: "parse_error",
1042
+ payloadType: "",
1043
+ label: "parse_error",
1044
+ timestamp: "",
1045
+ turn: state.turns || null,
1046
+ preview: trimOneLine(error.message + " | " + raw, 180),
1047
+ searchText: ("parse_error " + lineNo + " " + error.message + " " + raw.slice(0, 500)).toLowerCase(),
1048
+ error: error.message,
1049
+ keyCount: 0,
1050
+ bytes
1051
+ };
1052
+ }
1053
+
1054
+ function classifyRecord(type, payloadType, payload) {
1055
+ if (type === "session_meta" || payloadType === "session_meta") return "session";
1056
+ if (type === "turn_context") return "context";
1057
+ if (type === "compacted" || payloadType === "context_compacted" || payloadType === "compacted") return "compact";
1058
+ if (payloadType === "user_message") return "user";
1059
+ if (payloadType === "agent_message") return "assistant";
1060
+ if (payloadType === "token_count") return "tokens";
1061
+ if (type === "response_item") {
1062
+ if (payload.type === "message") {
1063
+ if (payload.role === "user") return "user";
1064
+ if (payload.role === "assistant") return "assistant";
1065
+ if (payload.role === "developer" || payload.role === "system") return "developer";
1066
+ return "other";
1067
+ }
1068
+ if (payload.type === "reasoning") return "reasoning";
1069
+ if (payload.type === "function_call" || payload.type === "custom_tool_call") return "call";
1070
+ if (payload.type === "function_call_output" || payload.type === "custom_tool_call_output") return "output";
1071
+ }
1072
+ if (type === "event_msg") return "event";
1073
+ return "other";
1074
+ }
1075
+
1076
+ function labelForRecord(record, payload, type, payloadType) {
1077
+ if (type === "response_item" && payloadType === "message" && payload.role) {
1078
+ return type + " / " + payloadType + " / " + payload.role;
1079
+ }
1080
+ if (type === "response_item" && payloadType === "reasoning") {
1081
+ return type + " / reasoning";
1082
+ }
1083
+ if (type === "turn_context") {
1084
+ return "turn_context";
1085
+ }
1086
+ return payloadType ? type + " / " + payloadType : type;
1087
+ }
1088
+
1089
+ function previewForRecord(record, payload, type, payloadType) {
1090
+ if (payloadType === "user_message") return trimOneLine(payload.message || "", 180);
1091
+ if (payloadType === "agent_message") return trimOneLine(payload.message || "", 180);
1092
+ if (payloadType === "token_count") {
1093
+ const usage = payload.info && payload.info.last_token_usage ? payload.info.last_token_usage : {};
1094
+ return "input " + formatInt(usage.input_tokens || 0) + ", total " + formatInt(usage.total_tokens || 0);
1095
+ }
1096
+ if (type === "session_meta") {
1097
+ return trimOneLine([payload.id, payload.cwd].filter(Boolean).join(" | "), 180);
1098
+ }
1099
+ if (type === "turn_context") {
1100
+ return trimOneLine([
1101
+ payload.model ? "model " + payload.model : "",
1102
+ payload.effort ? "effort " + payload.effort : "",
1103
+ payload.cwd || "",
1104
+ Array.isArray(payload.workspace_roots) ? payload.workspace_roots.join(", ") : ""
1105
+ ].filter(Boolean).join(" | "), 180);
1106
+ }
1107
+ if (type === "response_item" && payload.type === "message") {
1108
+ const text = extractContentText(payload);
1109
+ return trimOneLine(text || payload.role || "message", 220);
1110
+ }
1111
+ if (type === "response_item" && payload.type === "reasoning") {
1112
+ const summaryText = Array.isArray(payload.summary)
1113
+ ? payload.summary.map((part) => typeof part === "string" ? part : part.text || part.summary || "").filter(Boolean).join(" ")
1114
+ : "";
1115
+ return trimOneLine(summaryText || payload.id || "reasoning", 180);
1116
+ }
1117
+ if (type === "response_item" && payload.type === "function_call") {
1118
+ return trimOneLine([payload.name, extractArgumentsPreview(payload.arguments)].filter(Boolean).join(" | "), 180);
1119
+ }
1120
+ if (type === "response_item" && payload.type === "function_call_output") {
1121
+ return trimOneLine([payload.call_id, payload.output || ""].filter(Boolean).join(" | "), 180);
1122
+ }
1123
+ if (type === "response_item" && payload.type === "custom_tool_call") {
1124
+ return trimOneLine([payload.name, payload.input || ""].filter(Boolean).join(" | "), 180);
1125
+ }
1126
+ if (type === "response_item" && payload.type === "custom_tool_call_output") {
1127
+ return trimOneLine([payload.call_id, payload.output || ""].filter(Boolean).join(" | "), 180);
1128
+ }
1129
+ return trimOneLine(JSON.stringify(record), 180);
1130
+ }
1131
+
1132
+ function extractContentText(payload) {
1133
+ if (!Array.isArray(payload.content)) return "";
1134
+ const parts = [];
1135
+ for (const block of payload.content) {
1136
+ if (typeof block === "string") {
1137
+ parts.push(block);
1138
+ } else if (block && typeof block.text === "string") {
1139
+ parts.push(block.text);
1140
+ } else if (block && typeof block.input_text === "string") {
1141
+ parts.push(block.input_text);
1142
+ } else if (block && typeof block.output_text === "string") {
1143
+ parts.push(block.output_text);
1144
+ }
1145
+ }
1146
+ return parts.join("\n\n");
1147
+ }
1148
+
1149
+ function extractArgumentsPreview(rawArguments) {
1150
+ if (typeof rawArguments !== "string") return "";
1151
+ try {
1152
+ const args = JSON.parse(rawArguments);
1153
+ if (args && typeof args.cmd === "string") return args.cmd;
1154
+ return JSON.stringify(args);
1155
+ } catch {
1156
+ return rawArguments;
1157
+ }
1158
+ }
1159
+
1160
+ function applyFilter() {
1161
+ if (!state.filter) {
1162
+ state.filtered = state.records.map((_, index) => index);
1163
+ return;
1164
+ }
1165
+ const terms = state.filter.split(/\s+/).filter(Boolean);
1166
+ state.filtered = [];
1167
+ for (let i = 0; i < state.records.length; i += 1) {
1168
+ const text = state.records[i].searchText;
1169
+ if (terms.every((term) => text.includes(term))) {
1170
+ state.filtered.push(i);
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ function renderStats() {
1176
+ els.stats.replaceChildren(
1177
+ chip(formatInt(state.records.length), "records"),
1178
+ chip(formatInt(state.turns), "turns"),
1179
+ chip(formatBytes(state.bytesRead || (state.file && state.file.size) || 0), ""),
1180
+ chip(formatInt(state.filtered.length || state.records.length), "visible"),
1181
+ chip(formatInt(state.parseErrors), "errors")
1182
+ );
1183
+ }
1184
+
1185
+ function chip(value, label) {
1186
+ const node = document.createElement("span");
1187
+ node.className = "chip";
1188
+ const strong = document.createElement("strong");
1189
+ strong.textContent = value;
1190
+ node.append(strong);
1191
+ if (label) node.append(document.createTextNode(" " + label));
1192
+ return node;
1193
+ }
1194
+
1195
+ function renderVirtualList() {
1196
+ const total = state.filtered.length;
1197
+ els.spacer.style.height = total * ROW_HEIGHT + "px";
1198
+
1199
+ const scrollTop = els.list.scrollTop;
1200
+ const viewHeight = Math.max(0, els.list.clientHeight);
1201
+ const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - WINDOW_PAD);
1202
+ const end = Math.min(total, Math.ceil((scrollTop + viewHeight) / ROW_HEIGHT) + WINDOW_PAD);
1203
+
1204
+ els.rows.style.transform = "translateY(" + start * ROW_HEIGHT + "px)";
1205
+ const fragment = document.createDocumentFragment();
1206
+
1207
+ for (let pos = start; pos < end; pos += 1) {
1208
+ const recordIndex = state.filtered[pos];
1209
+ const record = state.records[recordIndex];
1210
+ fragment.append(renderRecordRow(record, recordIndex));
1211
+ }
1212
+
1213
+ els.rows.replaceChildren(fragment);
1214
+ }
1215
+
1216
+ function renderRecordRow(record, recordIndex) {
1217
+ const row = document.createElement("button");
1218
+ row.type = "button";
1219
+ row.className = "record" + (recordIndex === state.selected ? " selected" : "");
1220
+ row.dataset.recordIndex = String(recordIndex);
1221
+ row.style.setProperty("--stripe", colorForKind(record.kind));
1222
+
1223
+ const line = document.createElement("div");
1224
+ line.className = "line-no";
1225
+ const strong = document.createElement("strong");
1226
+ strong.textContent = "#" + record.lineNo;
1227
+ const turn = document.createElement("span");
1228
+ turn.textContent = record.turn ? "T" + record.turn : "no turn";
1229
+ const size = document.createElement("span");
1230
+ size.textContent = formatBytes(record.bytes);
1231
+ line.append(strong);
1232
+ line.append(turn);
1233
+ line.append(size);
1234
+
1235
+ const body = document.createElement("div");
1236
+ body.className = "record-body";
1237
+ const main = document.createElement("div");
1238
+ main.className = "record-main";
1239
+ const kind = document.createElement("span");
1240
+ kind.className = "kind " + record.kind;
1241
+ kind.textContent = record.kind;
1242
+ const type = document.createElement("span");
1243
+ type.className = "record-type";
1244
+ type.textContent = record.label;
1245
+ main.append(kind, type);
1246
+ const preview = document.createElement("div");
1247
+ preview.className = "preview";
1248
+ preview.textContent = record.preview || record.timestamp || "(empty)";
1249
+ body.append(main, preview);
1250
+
1251
+ row.append(line, body);
1252
+ return row;
1253
+ }
1254
+
1255
+ async function selectRecord(recordIndex) {
1256
+ const record = state.records[recordIndex];
1257
+ if (!record || !state.file) return;
1258
+ state.selected = recordIndex;
1259
+ state.selectedObject = null;
1260
+ state.selectedRaw = "";
1261
+ state.fullPaths.clear();
1262
+ state.expandedPaths.clear();
1263
+ state.collapsedPaths.clear();
1264
+ state.expandedTextPaths.clear();
1265
+ renderVirtualList();
1266
+ showEmpty("Loading record", "Reading line #" + record.lineNo + " from the file.");
1267
+
1268
+ const raw = await state.file.slice(record.start, record.end).text();
1269
+ if (state.selected !== recordIndex) return;
1270
+ state.selectedRaw = raw;
1271
+
1272
+ try {
1273
+ state.selectedObject = JSON.parse(raw);
1274
+ } catch (error) {
1275
+ state.selectedObject = { parse_error: error.message, raw };
1276
+ }
1277
+
1278
+ renderDetailHeader(record);
1279
+ renderDetail();
1280
+ }
1281
+
1282
+ function renderDetailHeader(record) {
1283
+ els.detailKind.className = "kind " + record.kind;
1284
+ els.detailKind.textContent = record.kind;
1285
+ els.detailTitle.textContent = "Line #" + record.lineNo + " / " + record.label;
1286
+ }
1287
+
1288
+ function renderDetail() {
1289
+ if (state.selected < 0 || !state.selectedObject) return;
1290
+
1291
+ if (state.rawMode) {
1292
+ const pre = document.createElement("pre");
1293
+ pre.className = "raw";
1294
+ pre.textContent = prettyRaw(state.selectedRaw);
1295
+ els.detailScroll.replaceChildren(pre);
1296
+ return;
1297
+ }
1298
+
1299
+ const tree = document.createElement("div");
1300
+ tree.className = "tree";
1301
+ renderNode(tree, "record", state.selectedObject, "$", 0);
1302
+ els.detailScroll.replaceChildren(tree);
1303
+ }
1304
+
1305
+ function renderNode(parent, key, value, path, depth) {
1306
+ const type = valueType(value);
1307
+ const node = document.createElement("div");
1308
+ node.className = "node";
1309
+ node.style.setProperty("--depth", String(depth));
1310
+ node.style.setProperty("--stripe", DEPTH_COLORS[depth % DEPTH_COLORS.length]);
1311
+
1312
+ const row = document.createElement("div");
1313
+ row.className = "node-row";
1314
+
1315
+ if (type === "object" || type === "array") {
1316
+ const expanded = isExpanded(path, depth);
1317
+ const toggle = document.createElement("button");
1318
+ toggle.type = "button";
1319
+ toggle.className = "toggle";
1320
+ toggle.textContent = expanded ? "v" : ">";
1321
+ toggle.title = expanded ? "Collapse" : "Expand";
1322
+ toggle.addEventListener("click", () => {
1323
+ if (isExpanded(path, depth)) {
1324
+ state.collapsedPaths.add(path);
1325
+ state.expandedPaths.delete(path);
1326
+ } else {
1327
+ state.expandedPaths.add(path);
1328
+ state.collapsedPaths.delete(path);
1329
+ }
1330
+ renderDetail();
1331
+ });
1332
+ row.append(toggle);
1333
+ row.append(keyNode(key), punct(":"), typeNode(value));
1334
+ node.append(row);
1335
+ parent.append(node);
1336
+
1337
+ if (expanded) {
1338
+ const entries = entriesFor(value);
1339
+ const limit = state.fullPaths.has(path) ? entries.length : Math.min(entries.length, DEFAULT_CHILD_LIMIT);
1340
+ for (let i = 0; i < limit; i += 1) {
1341
+ const entry = entries[i];
1342
+ renderNode(parent, entry.key, entry.value, childPath(path, entry.key, type === "array"), depth + 1);
1343
+ }
1344
+ if (entries.length > limit) {
1345
+ renderMoreRow(parent, path, depth, entries.length - limit);
1346
+ }
1347
+ }
1348
+ return;
1349
+ }
1350
+
1351
+ if (type === "string" && shouldRenderTextBlock(value)) {
1352
+ row.classList.add("text-row");
1353
+ }
1354
+ row.append(leafPad(), keyNode(key), punct(":"), primitiveNode(value, type, path));
1355
+ node.append(row);
1356
+ parent.append(node);
1357
+ }
1358
+
1359
+ function renderMoreRow(parent, path, depth, remaining) {
1360
+ const row = document.createElement("div");
1361
+ row.className = "more-row";
1362
+ row.style.setProperty("--depth", String(depth));
1363
+ row.textContent = formatInt(remaining) + " more child nodes hidden ";
1364
+ const button = document.createElement("button");
1365
+ button.type = "button";
1366
+ button.className = "inline-button";
1367
+ button.textContent = "show all";
1368
+ button.addEventListener("click", () => {
1369
+ state.fullPaths.add(path);
1370
+ renderDetail();
1371
+ });
1372
+ row.append(button);
1373
+ parent.append(row);
1374
+ }
1375
+
1376
+ function primitiveNode(value, type, path) {
1377
+ if (type === "string") {
1378
+ const full = String(value);
1379
+ if (shouldRenderTextBlock(full)) {
1380
+ return textBlockNode(full, path);
1381
+ }
1382
+
1383
+ const span = document.createElement("span");
1384
+ span.className = "value string string-inline";
1385
+ const expanded = state.expandedTextPaths.has(path);
1386
+ const needsTrim = full.length > state.trim;
1387
+ span.textContent = quoteText(needsTrim && !expanded ? full.slice(0, state.trim) + "..." : full);
1388
+ if (needsTrim) {
1389
+ const button = document.createElement("button");
1390
+ button.type = "button";
1391
+ button.className = "inline-button";
1392
+ button.textContent = expanded ? "less" : "more";
1393
+ button.addEventListener("click", () => {
1394
+ if (state.expandedTextPaths.has(path)) {
1395
+ state.expandedTextPaths.delete(path);
1396
+ } else {
1397
+ state.expandedTextPaths.add(path);
1398
+ }
1399
+ renderDetail();
1400
+ });
1401
+ span.append(button);
1402
+ }
1403
+ return span;
1404
+ }
1405
+ const span = document.createElement("span");
1406
+ span.className = "value " + type;
1407
+ if (type === "null") {
1408
+ span.textContent = "null";
1409
+ return span;
1410
+ }
1411
+ span.textContent = String(value);
1412
+ return span;
1413
+ }
1414
+
1415
+ function textBlockNode(full, path) {
1416
+ const expanded = state.expandedTextPaths.has(path);
1417
+ const truncated = truncateReadableText(full, state.trim, COLLAPSED_TEXT_LINES);
1418
+ const visible = expanded ? full : truncated.text;
1419
+ const needsTrim = truncated.didTrim;
1420
+ const lineCount = countLines(full);
1421
+
1422
+ const block = document.createElement("div");
1423
+ block.className = "value string text-block " + (expanded ? "expanded" : "collapsed");
1424
+
1425
+ const header = document.createElement("div");
1426
+ header.className = "text-block-header";
1427
+
1428
+ const meta = document.createElement("div");
1429
+ meta.className = "text-meta";
1430
+ meta.append(metaPill("text"));
1431
+ meta.append(metaPill(formatInt(full.length) + " chars"));
1432
+ meta.append(metaPill(formatInt(lineCount) + " lines"));
1433
+ header.append(meta);
1434
+
1435
+ if (needsTrim) {
1436
+ const button = document.createElement("button");
1437
+ button.type = "button";
1438
+ button.className = "inline-button";
1439
+ button.textContent = expanded ? "less" : "more";
1440
+ button.addEventListener("click", () => {
1441
+ if (state.expandedTextPaths.has(path)) {
1442
+ state.expandedTextPaths.delete(path);
1443
+ } else {
1444
+ state.expandedTextPaths.add(path);
1445
+ }
1446
+ renderDetail();
1447
+ });
1448
+ header.append(button);
1449
+ }
1450
+
1451
+ const content = document.createElement("pre");
1452
+ content.className = "text-content";
1453
+ content.textContent = visible;
1454
+ block.append(header, content);
1455
+ return block;
1456
+ }
1457
+
1458
+ function metaPill(text) {
1459
+ const span = document.createElement("span");
1460
+ span.textContent = text;
1461
+ return span;
1462
+ }
1463
+
1464
+ function shouldRenderTextBlock(value) {
1465
+ const text = String(value);
1466
+ return text.length > INLINE_STRING_LIMIT || text.includes("\n") || /(^|\n)\s*(#{1,6}\s|[-*]\s|\d+\.\s|```|<[^>]+>)/.test(text);
1467
+ }
1468
+
1469
+ function truncateReadableText(text, maxChars, maxLines) {
1470
+ const lines = String(text).split(/\r?\n/);
1471
+ const lineTrimmed = lines.length > maxLines;
1472
+ let visible = lineTrimmed ? lines.slice(0, maxLines).join("\n") : String(text);
1473
+ let charTrimmed = visible.length > maxChars;
1474
+ if (charTrimmed) {
1475
+ visible = visible.slice(0, maxChars).trimEnd();
1476
+ }
1477
+ const didTrim = lineTrimmed || charTrimmed || String(text).length > visible.length;
1478
+ if (didTrim) {
1479
+ visible = visible.trimEnd() + "\n...";
1480
+ }
1481
+ return { text: visible, didTrim };
1482
+ }
1483
+
1484
+ function countLines(text) {
1485
+ if (!String(text).length) return 0;
1486
+ return String(text).split(/\r?\n/).length;
1487
+ }
1488
+
1489
+ function quoteText(text) {
1490
+ return "\"" + String(text).replaceAll("\\", "\\\\").replaceAll("\"", "\\\"") + "\"";
1491
+ }
1492
+
1493
+ function keyNode(key) {
1494
+ const span = document.createElement("span");
1495
+ span.className = "key";
1496
+ span.textContent = key;
1497
+ return span;
1498
+ }
1499
+
1500
+ function typeNode(value) {
1501
+ const span = document.createElement("span");
1502
+ span.className = "type-label";
1503
+ const type = valueType(value);
1504
+ const count = type === "array" ? value.length : Object.keys(value).length;
1505
+ span.textContent = type + "(" + formatInt(count) + ")";
1506
+ return span;
1507
+ }
1508
+
1509
+ function punct(text) {
1510
+ const span = document.createElement("span");
1511
+ span.className = "punct";
1512
+ span.textContent = text;
1513
+ return span;
1514
+ }
1515
+
1516
+ function leafPad() {
1517
+ const span = document.createElement("span");
1518
+ span.className = "leaf-pad";
1519
+ return span;
1520
+ }
1521
+
1522
+ function isExpanded(path, depth) {
1523
+ if (state.collapsedPaths.has(path)) return false;
1524
+ if (state.expandedPaths.has(path)) return true;
1525
+ if (isNaturallyExpandedPath(path)) return true;
1526
+ return depth < DEFAULT_OPEN_DEPTH;
1527
+ }
1528
+
1529
+ function isNaturallyExpandedPath(path) {
1530
+ return path === "$.payload.content"
1531
+ || /^\$\.payload\.content\[\d+\]$/.test(path)
1532
+ || path === "$.payload.info"
1533
+ || path === "$.payload.info.last_token_usage";
1534
+ }
1535
+
1536
+ function entriesFor(value) {
1537
+ if (Array.isArray(value)) {
1538
+ return value.map((entry, index) => ({ key: String(index), value: entry }));
1539
+ }
1540
+ return Object.keys(value).map((key) => ({ key, value: value[key] }));
1541
+ }
1542
+
1543
+ function childPath(path, key, parentIsArray) {
1544
+ if (parentIsArray) return path + "[" + key + "]";
1545
+ return path + "." + key.replace(/[^\w$]/g, "_");
1546
+ }
1547
+
1548
+ function valueType(value) {
1549
+ if (value === null) return "null";
1550
+ if (Array.isArray(value)) return "array";
1551
+ return typeof value === "object" ? "object" : typeof value;
1552
+ }
1553
+
1554
+ function prettyRaw(raw) {
1555
+ try {
1556
+ return JSON.stringify(JSON.parse(raw), null, 2);
1557
+ } catch {
1558
+ return raw;
1559
+ }
1560
+ }
1561
+
1562
+ function showEmpty(title, text) {
1563
+ const outer = document.createElement("div");
1564
+ outer.className = "empty";
1565
+ const panel = document.createElement("div");
1566
+ panel.className = "empty-panel";
1567
+ const strong = document.createElement("strong");
1568
+ strong.textContent = title;
1569
+ const span = document.createElement("span");
1570
+ span.textContent = text;
1571
+ panel.append(strong, span);
1572
+ outer.append(panel);
1573
+ els.detailScroll.replaceChildren(outer);
1574
+ }
1575
+
1576
+ function showError(error) {
1577
+ const outer = document.createElement("div");
1578
+ outer.className = "empty";
1579
+ const panel = document.createElement("div");
1580
+ panel.className = "empty-panel";
1581
+ const strong = document.createElement("strong");
1582
+ strong.textContent = "Could not read file";
1583
+ const span = document.createElement("span");
1584
+ span.className = "value error";
1585
+ span.textContent = error && error.message ? error.message : String(error);
1586
+ panel.append(strong, span);
1587
+ outer.append(panel);
1588
+ els.detailScroll.replaceChildren(outer);
1589
+ }
1590
+
1591
+ function colorForKind(kind) {
1592
+ return {
1593
+ session: "#0f766e",
1594
+ user: "#2563eb",
1595
+ assistant: "#15803d",
1596
+ call: "#7c3aed",
1597
+ output: "#b45309",
1598
+ tokens: "#475467",
1599
+ compact: "#7c3aed",
1600
+ developer: "#475467",
1601
+ reasoning: "#be123c",
1602
+ context: "#0891b2",
1603
+ event: "#0f766e",
1604
+ error: "#be123c",
1605
+ other: "#344054"
1606
+ }[kind] || "#344054";
1607
+ }
1608
+
1609
+ function trimOneLine(value, max) {
1610
+ const text = String(value || "").replace(/\s+/g, " ").trim();
1611
+ return text.length > max ? text.slice(0, max) + "..." : text;
1612
+ }
1613
+
1614
+ function isObject(value) {
1615
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1616
+ }
1617
+
1618
+ function concatBytes(a, b) {
1619
+ if (!a.length) return b;
1620
+ const out = new Uint8Array(a.length + b.length);
1621
+ out.set(a, 0);
1622
+ out.set(b, a.length);
1623
+ return out;
1624
+ }
1625
+
1626
+ function nextFrame() {
1627
+ return new Promise((resolve) => requestAnimationFrame(resolve));
1628
+ }
1629
+
1630
+ function setProgress(value) {
1631
+ els.progressBar.style.setProperty("--progress", Math.max(0, Math.min(100, value)) + "%");
1632
+ }
1633
+
1634
+ function formatInt(value) {
1635
+ return Number(value || 0).toLocaleString();
1636
+ }
1637
+
1638
+ function formatBytes(bytes) {
1639
+ const value = Number(bytes || 0);
1640
+ if (value < 1024) return value + " B";
1641
+ if (value < 1024 * 1024) return (value / 1024).toFixed(1) + " KB";
1642
+ if (value < 1024 * 1024 * 1024) return (value / (1024 * 1024)).toFixed(1) + " MB";
1643
+ return (value / (1024 * 1024 * 1024)).toFixed(2) + " GB";
1644
+ }
1645
+
1646
+ // EchoMem addition: a sticky toggle to switch the SAME session between its two data sources —
1647
+ // the rollout event log (/session-raw) and the real context-window payload (/session-payload).
1648
+ function injectModeToggle(sessionPath, mode) {
1649
+ if (document.getElementById("echoModeBar")) return;
1650
+ const bar = document.createElement("div");
1651
+ bar.id = "echoModeBar";
1652
+ bar.style.cssText = "position:sticky;top:0;z-index:60;display:flex;gap:8px;align-items:center;padding:8px 14px;background:#0d1117;border-bottom:1px solid #30363d;font:12px system-ui,-apple-system,sans-serif";
1653
+ const mk = (label, m, desc) => {
1654
+ const b = document.createElement("button");
1655
+ b.textContent = label; b.title = desc;
1656
+ b.style.cssText = "padding:6px 14px;border-radius:999px;border:1px solid #30363d;cursor:pointer;color:#c9d1d9;background:" + (m === mode ? "#1f6feb" : "#161b22");
1657
+ b.onclick = () => {
1658
+ const u = new URL(window.location.href);
1659
+ u.searchParams.set("path", sessionPath); u.searchParams.set("mode", m);
1660
+ window.location.href = u.toString();
1661
+ };
1662
+ return b;
1663
+ };
1664
+ const tag = document.createElement("span");
1665
+ tag.textContent = "context source:"; tag.style.cssText = "color:#8b949e;margin-right:2px";
1666
+ bar.append(tag, mk("Event log", "raw", "The rollout — what the agent logged (reconstruction)"),
1667
+ mk("Real payload", "payload", "The actual context window sent to the model, from logs_2.sqlite"));
1668
+ document.body.prepend(bar);
1669
+ }
1670
+
1671
+ async function initFromQuery() {
1672
+ const params = new URLSearchParams(window.location.search);
1673
+ const sessionPath = params.get("path");
1674
+ const mode = params.get("mode") || "raw";
1675
+ let src = params.get("src");
1676
+ if (sessionPath) {
1677
+ injectModeToggle(sessionPath, mode);
1678
+ src = (mode === "payload" ? "/session-payload" : "/session-raw") + "?path=" + encodeURIComponent(sessionPath);
1679
+ }
1680
+ if (!src) return;
1681
+
1682
+ els.fileName.textContent = (sessionPath ? (mode === "payload" ? "Real payload · " : "Event log · ") : "") + src;
1683
+ els.readState.textContent = "Fetching";
1684
+ try {
1685
+ const response = await fetch(src);
1686
+ if (!response.ok) {
1687
+ throw new Error("HTTP " + response.status + " while fetching " + src);
1688
+ }
1689
+ const blob = await response.blob();
1690
+ const name = (sessionPath || src).split("/").filter(Boolean).pop() || "session.jsonl";
1691
+ await loadFile(new File([blob], name, { type: "application/jsonl" }));
1692
+ } catch (error) {
1693
+ els.readState.textContent = "Failed";
1694
+ showError(error);
1695
+ }
1696
+ }
1697
+ </script>
1698
+ </body>
1699
+ </html>