@echomem/mcp 1.4.17 → 1.4.19
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.
- package/dist/context-analysis/claude-canonical-adapter.js +315 -0
- package/dist/hud/cli.js +0 -0
- package/dist/hud/efficiency.js +447 -0
- package/dist/hud/electron-main.js +7 -2
- package/dist/hud/monitor.js +164 -50
- package/dist/hud/server.js +96 -16
- package/dist/hud/session-launcher.js +395 -0
- package/dist/hud/web.js +540 -177
- package/dist/index.js +54 -22
- package/dist/migrate.js +47 -11
- package/dist/setup-page/client-core.js +31 -1
- package/dist/setup-page/client-extraction.js +533 -149
- package/dist/setup-page/client-lifecycle.js +64 -43
- package/dist/setup-page/styles-extraction.js +1107 -148
- package/dist/setup-page/styles-foundation.js +20 -15
- package/dist/setup-page/styles-website-alignment.js +635 -0
- package/dist/setup-page/styles.js +2 -0
- package/dist/setup-preview.js +49 -0
- package/dist/setup.js +240 -21
- package/dist/v1-contract.js +8 -8
- package/package.json +1 -1
package/dist/hud/web.js
CHANGED
|
@@ -212,7 +212,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
212
212
|
display: none;
|
|
213
213
|
position: relative;
|
|
214
214
|
margin: 2px 8px 0 8px;
|
|
215
|
-
padding: 16px 17px
|
|
215
|
+
padding: 16px 17px 10px;
|
|
216
216
|
border: 1px solid var(--line);
|
|
217
217
|
border-radius: 12px;
|
|
218
218
|
background: var(--card);
|
|
@@ -268,22 +268,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
268
268
|
.now-title, .now-repo { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
269
269
|
.now-title { font-size: 12px; font-weight: 800; color: var(--ink); }
|
|
270
270
|
.now-repo { font-size: 10px; font-weight: 600; color: var(--faint); }
|
|
271
|
-
.
|
|
272
|
-
overflow: hidden;
|
|
273
|
-
text-overflow: ellipsis;
|
|
274
|
-
white-space: nowrap;
|
|
275
|
-
font-size: 11.5px;
|
|
276
|
-
font-weight: 650;
|
|
277
|
-
color: var(--muted);
|
|
278
|
-
}
|
|
279
|
-
.verdict:empty { display: none; }
|
|
280
|
-
.meta-row {
|
|
281
|
-
display: flex;
|
|
282
|
-
align-items: center;
|
|
283
|
-
justify-content: space-between;
|
|
284
|
-
gap: 8px;
|
|
285
|
-
margin-top: 10px;
|
|
286
|
-
}
|
|
271
|
+
.signal-meta { min-height: 12px; margin: -1px 0 5px 21px; }
|
|
287
272
|
.meta-line {
|
|
288
273
|
min-width: 0;
|
|
289
274
|
overflow: hidden;
|
|
@@ -333,16 +318,59 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
333
318
|
.tabs {
|
|
334
319
|
display: grid;
|
|
335
320
|
gap: 6px;
|
|
321
|
+
height: 259px;
|
|
336
322
|
max-height: 259px;
|
|
337
323
|
margin: 10px 0 0;
|
|
338
324
|
padding-right: 3px;
|
|
339
325
|
overflow-y: auto;
|
|
326
|
+
overflow-anchor: none;
|
|
340
327
|
overscroll-behavior: contain;
|
|
328
|
+
scrollbar-gutter: stable;
|
|
329
|
+
scrollbar-width: none;
|
|
341
330
|
}
|
|
342
331
|
.tabs:empty { display: none; }
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
.tabs
|
|
332
|
+
/* Let roughly two rows taper at each scroll edge while keeping the edge rows visible.
|
|
333
|
+
At mid-scroll both cues are active; reaching either end restores that edge to full opacity. */
|
|
334
|
+
.tabs.has-more-below:not(.has-more-above) {
|
|
335
|
+
-webkit-mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 82px), rgba(0, 0, 0, .82) calc(100% - 54px), rgba(0, 0, 0, .58) calc(100% - 24px), rgba(0, 0, 0, .42) 100%);
|
|
336
|
+
mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 82px), rgba(0, 0, 0, .82) calc(100% - 54px), rgba(0, 0, 0, .58) calc(100% - 24px), rgba(0, 0, 0, .42) 100%);
|
|
337
|
+
}
|
|
338
|
+
.tabs.has-more-above:not(.has-more-below) {
|
|
339
|
+
-webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, .42) 0, rgba(0, 0, 0, .58) 24px, rgba(0, 0, 0, .82) 54px, #000 82px, #000 100%);
|
|
340
|
+
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, .42) 0, rgba(0, 0, 0, .58) 24px, rgba(0, 0, 0, .82) 54px, #000 82px, #000 100%);
|
|
341
|
+
}
|
|
342
|
+
.tabs.has-more-above.has-more-below {
|
|
343
|
+
-webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, .42) 0, rgba(0, 0, 0, .58) 24px, rgba(0, 0, 0, .82) 54px, #000 82px, #000 calc(100% - 82px), rgba(0, 0, 0, .82) calc(100% - 54px), rgba(0, 0, 0, .58) calc(100% - 24px), rgba(0, 0, 0, .42) 100%);
|
|
344
|
+
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, .42) 0, rgba(0, 0, 0, .58) 24px, rgba(0, 0, 0, .82) 54px, #000 82px, #000 calc(100% - 82px), rgba(0, 0, 0, .82) calc(100% - 54px), rgba(0, 0, 0, .58) calc(100% - 24px), rgba(0, 0, 0, .42) 100%);
|
|
345
|
+
}
|
|
346
|
+
.tabs::-webkit-scrollbar { width: 0; height: 0; }
|
|
347
|
+
/* One scrollbar for the whole white window. It mirrors the stable timeline viewport below
|
|
348
|
+
instead of shrinking to the list's own 259px box. */
|
|
349
|
+
.timeline-scrollbar {
|
|
350
|
+
position: absolute;
|
|
351
|
+
top: 8px;
|
|
352
|
+
right: 4px;
|
|
353
|
+
bottom: 8px;
|
|
354
|
+
z-index: 5;
|
|
355
|
+
width: 4px;
|
|
356
|
+
border-radius: 999px;
|
|
357
|
+
background: rgba(26, 58, 143, .055);
|
|
358
|
+
cursor: pointer;
|
|
359
|
+
touch-action: none;
|
|
360
|
+
transition: opacity .16s ease;
|
|
361
|
+
}
|
|
362
|
+
.timeline-scrollbar.hidden { opacity: 0; pointer-events: none; }
|
|
363
|
+
.timeline-thumb {
|
|
364
|
+
position: absolute;
|
|
365
|
+
top: 0;
|
|
366
|
+
left: 0;
|
|
367
|
+
width: 100%;
|
|
368
|
+
min-height: 30px;
|
|
369
|
+
border-radius: 999px;
|
|
370
|
+
background: rgba(26, 58, 143, .24);
|
|
371
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, .5);
|
|
372
|
+
will-change: transform, height;
|
|
373
|
+
}
|
|
346
374
|
/* History rows: everything that is NOT the current session. Quiet on purpose. */
|
|
347
375
|
.tab {
|
|
348
376
|
display: grid; grid-template-columns: 16px minmax(0, 1fr) auto auto auto; align-items: center; column-gap: 8px;
|
|
@@ -351,6 +379,16 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
351
379
|
color: var(--faint); cursor: pointer; text-align: left; font-family: inherit;
|
|
352
380
|
}
|
|
353
381
|
.tab:hover { border-color: rgba(0, 0, 0, .1); background: rgba(0, 0, 0, .05); }
|
|
382
|
+
.tab.expanded {
|
|
383
|
+
grid-template-rows: auto auto;
|
|
384
|
+
min-height: 78px;
|
|
385
|
+
padding-top: 8px;
|
|
386
|
+
padding-bottom: 8px;
|
|
387
|
+
border-color: rgba(26, 58, 143, .24);
|
|
388
|
+
background: rgba(255, 255, 255, .82);
|
|
389
|
+
box-shadow: 0 1px 4px rgba(26, 58, 143, .06);
|
|
390
|
+
}
|
|
391
|
+
.tab.expanded .tab-title { color: var(--ink); font-weight: 760; }
|
|
354
392
|
.tab-icon { width: 14px; height: 14px; object-fit: contain; opacity: .75; }
|
|
355
393
|
.tab-copy { min-width: 0; display: grid; gap: 1px; }
|
|
356
394
|
.tab-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-weight: 680; }
|
|
@@ -382,6 +420,36 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
382
420
|
color: var(--muted);
|
|
383
421
|
}
|
|
384
422
|
.tab-health .tab-status { width: 8px; height: 8px; border-width: 1.5px; }
|
|
423
|
+
.tab-detail {
|
|
424
|
+
grid-column: 2 / -1;
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: baseline;
|
|
427
|
+
justify-content: space-between;
|
|
428
|
+
gap: 8px;
|
|
429
|
+
min-width: 0;
|
|
430
|
+
margin-top: 5px;
|
|
431
|
+
padding-top: 7px;
|
|
432
|
+
border-top: 1px solid rgba(26, 58, 143, .11);
|
|
433
|
+
text-align: left;
|
|
434
|
+
}
|
|
435
|
+
.tab-detail-score {
|
|
436
|
+
flex: 0 0 auto;
|
|
437
|
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
438
|
+
font-size: 10px;
|
|
439
|
+
font-weight: 800;
|
|
440
|
+
color: var(--muted);
|
|
441
|
+
white-space: nowrap;
|
|
442
|
+
}
|
|
443
|
+
.tab-detail-meta {
|
|
444
|
+
min-width: 0;
|
|
445
|
+
overflow: hidden;
|
|
446
|
+
text-overflow: ellipsis;
|
|
447
|
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
448
|
+
font-size: 9px;
|
|
449
|
+
font-weight: 600;
|
|
450
|
+
color: var(--faint);
|
|
451
|
+
white-space: nowrap;
|
|
452
|
+
}
|
|
385
453
|
/* Jump straight to this conversation in its own app. */
|
|
386
454
|
.tab-jump {
|
|
387
455
|
display: inline-flex; align-items: center; justify-content: center;
|
|
@@ -431,33 +499,6 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
431
499
|
cursor: pointer; font-family: inherit;
|
|
432
500
|
}
|
|
433
501
|
.cornerbtn:hover { background: #fff; color: #111; }
|
|
434
|
-
.switchbtn {
|
|
435
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
436
|
-
width: 22px; height: 22px; border-radius: 50%; padding: 0; line-height: 1; font-size: 12px;
|
|
437
|
-
border: 1px solid rgba(0, 0, 0, .2); background: rgba(0, 0, 0, .05); color: var(--faint);
|
|
438
|
-
cursor: pointer; font-family: inherit;
|
|
439
|
-
}
|
|
440
|
-
.switchbtn:hover { background: rgba(0, 0, 0, .1); color: #111; }
|
|
441
|
-
/* Explicit family switch: a labeled pill, not a mystery icon. */
|
|
442
|
-
.agent-switch {
|
|
443
|
-
width: auto;
|
|
444
|
-
height: 26px;
|
|
445
|
-
gap: 6px;
|
|
446
|
-
padding: 0 11px 0 8px;
|
|
447
|
-
border-radius: 999px;
|
|
448
|
-
background: rgba(255, 255, 255, .78);
|
|
449
|
-
color: #1f1f1f;
|
|
450
|
-
font-size: 10.5px;
|
|
451
|
-
font-weight: 760;
|
|
452
|
-
letter-spacing: 0;
|
|
453
|
-
white-space: nowrap;
|
|
454
|
-
flex: 0 0 auto;
|
|
455
|
-
}
|
|
456
|
-
.agent-switch:hover { background: #fff; border-color: rgba(26, 58, 143, .34); }
|
|
457
|
-
.agent-switch[data-target-family="claude"] { border-color: rgba(217, 119, 87, .42); }
|
|
458
|
-
.agent-switch[data-target-family="codex"] { border-color: rgba(57, 65, 255, .34); }
|
|
459
|
-
.switch-icon { width: 14px; height: 14px; flex: 0 0 auto; object-fit: contain; }
|
|
460
|
-
.switch-copy { display: inline; }
|
|
461
502
|
.reveal {
|
|
462
503
|
margin-top: 8px; font-size: 10.5px; padding: 4px 11px; border-radius: 999px;
|
|
463
504
|
border: 1px solid rgba(0, 0, 0, .12); background: rgba(0, 0, 0, .04); color: var(--faint);
|
|
@@ -488,6 +529,20 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
488
529
|
font-size: 20px;
|
|
489
530
|
font-weight: 850;
|
|
490
531
|
}
|
|
532
|
+
.state-word .signal-label {
|
|
533
|
+
margin-left: 5px;
|
|
534
|
+
font-family: "Avenir Next", Inter, ui-sans-serif, system-ui, sans-serif;
|
|
535
|
+
font-size: 11px;
|
|
536
|
+
font-weight: 700;
|
|
537
|
+
color: var(--muted);
|
|
538
|
+
}
|
|
539
|
+
.state-word .snr-prefix {
|
|
540
|
+
margin-right: 7px;
|
|
541
|
+
font-size: 10.5px;
|
|
542
|
+
font-weight: 800;
|
|
543
|
+
letter-spacing: 0.04em;
|
|
544
|
+
color: var(--faint);
|
|
545
|
+
}
|
|
491
546
|
.why-btn {
|
|
492
547
|
margin-left: auto;
|
|
493
548
|
display: inline-flex;
|
|
@@ -519,6 +574,26 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
519
574
|
line-height: 1.5;
|
|
520
575
|
}
|
|
521
576
|
#hud.show-why .evidence { display: block; }
|
|
577
|
+
.snr-equation {
|
|
578
|
+
display: grid;
|
|
579
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
580
|
+
align-items: center;
|
|
581
|
+
gap: 7px;
|
|
582
|
+
margin-bottom: 8px;
|
|
583
|
+
color: var(--ink);
|
|
584
|
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
585
|
+
font-size: 9.5px;
|
|
586
|
+
line-height: 1.2;
|
|
587
|
+
}
|
|
588
|
+
.snr-fraction { display: grid; min-width: 0; text-align: center; }
|
|
589
|
+
.snr-numerator {
|
|
590
|
+
padding: 0 4px 3px;
|
|
591
|
+
border-bottom: 1px solid rgba(26, 58, 143, 0.32);
|
|
592
|
+
font-weight: 750;
|
|
593
|
+
white-space: nowrap;
|
|
594
|
+
}
|
|
595
|
+
.snr-denominator { padding: 3px 4px 0; color: var(--muted); white-space: nowrap; }
|
|
596
|
+
.snr-result { color: var(--brick); font-weight: 850; white-space: nowrap; }
|
|
522
597
|
.why-line { display: flex; justify-content: space-between; gap: 12px; }
|
|
523
598
|
.why-line + .why-line { margin-top: 3px; }
|
|
524
599
|
.why-k { color: var(--faint); }
|
|
@@ -603,6 +678,85 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
603
678
|
box-shadow: 0 12px 20px -16px rgba(122, 36, 30, 0.95);
|
|
604
679
|
}
|
|
605
680
|
.billing-link:hover { filter: brightness(0.96); }
|
|
681
|
+
.quota-card {
|
|
682
|
+
display: none;
|
|
683
|
+
margin: 0 0 10px;
|
|
684
|
+
padding: 12px 13px 13px;
|
|
685
|
+
border: 1px solid rgba(26, 58, 143, 0.18);
|
|
686
|
+
border-radius: 12px;
|
|
687
|
+
background: rgba(255, 255, 255, 0.72);
|
|
688
|
+
box-shadow: 0 1px 5px rgba(26, 58, 143, 0.05);
|
|
689
|
+
}
|
|
690
|
+
.quota-head {
|
|
691
|
+
display: flex;
|
|
692
|
+
align-items: center;
|
|
693
|
+
gap: 7px;
|
|
694
|
+
margin-bottom: 9px;
|
|
695
|
+
}
|
|
696
|
+
.quota-title {
|
|
697
|
+
color: var(--ink);
|
|
698
|
+
font-size: 10.5px;
|
|
699
|
+
font-weight: 850;
|
|
700
|
+
letter-spacing: 0.015em;
|
|
701
|
+
}
|
|
702
|
+
.quota-plan {
|
|
703
|
+
border-radius: 999px;
|
|
704
|
+
background: rgba(26, 58, 143, 0.07);
|
|
705
|
+
padding: 2px 7px;
|
|
706
|
+
color: var(--blue);
|
|
707
|
+
font-size: 9px;
|
|
708
|
+
font-weight: 850;
|
|
709
|
+
text-transform: uppercase;
|
|
710
|
+
}
|
|
711
|
+
.quota-manage {
|
|
712
|
+
margin-left: auto;
|
|
713
|
+
border: 0;
|
|
714
|
+
padding: 0;
|
|
715
|
+
background: transparent;
|
|
716
|
+
color: var(--faint);
|
|
717
|
+
font-family: inherit;
|
|
718
|
+
font-size: 9.5px;
|
|
719
|
+
font-weight: 750;
|
|
720
|
+
cursor: pointer;
|
|
721
|
+
}
|
|
722
|
+
.quota-manage:hover { color: var(--blue); }
|
|
723
|
+
.quota-list { display: grid; gap: 9px; }
|
|
724
|
+
.quota-row { --quota-color: var(--blue); }
|
|
725
|
+
.quota-row.is-warn { --quota-color: #c98712; }
|
|
726
|
+
.quota-row.is-limit { --quota-color: #c7372f; }
|
|
727
|
+
.quota-line, .quota-subline {
|
|
728
|
+
display: flex;
|
|
729
|
+
align-items: baseline;
|
|
730
|
+
justify-content: space-between;
|
|
731
|
+
gap: 10px;
|
|
732
|
+
}
|
|
733
|
+
.quota-name { color: var(--muted); font-size: 10px; font-weight: 720; }
|
|
734
|
+
.quota-value {
|
|
735
|
+
color: var(--ink);
|
|
736
|
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
737
|
+
font-size: 9.5px;
|
|
738
|
+
font-weight: 760;
|
|
739
|
+
white-space: nowrap;
|
|
740
|
+
}
|
|
741
|
+
.quota-track {
|
|
742
|
+
height: 4px;
|
|
743
|
+
margin-top: 4px;
|
|
744
|
+
overflow: hidden;
|
|
745
|
+
border-radius: 999px;
|
|
746
|
+
background: rgba(26, 58, 143, 0.11);
|
|
747
|
+
}
|
|
748
|
+
.quota-fill {
|
|
749
|
+
width: 0;
|
|
750
|
+
height: 100%;
|
|
751
|
+
border-radius: inherit;
|
|
752
|
+
background: var(--quota-color);
|
|
753
|
+
transition: width .28s ease, background .2s ease;
|
|
754
|
+
}
|
|
755
|
+
.quota-subline { margin-top: 3px; }
|
|
756
|
+
.quota-reset, .quota-percent { color: var(--faint); font-size: 8.5px; font-weight: 650; }
|
|
757
|
+
.quota-row.is-limit .quota-value,
|
|
758
|
+
.quota-row.is-limit .quota-percent { color: #9d2b25; font-weight: 850; }
|
|
759
|
+
.quota-row.is-unavailable .quota-fill { background: rgba(26, 58, 143, 0.14); }
|
|
606
760
|
button.renew-btn {
|
|
607
761
|
display: none;
|
|
608
762
|
width: 100%;
|
|
@@ -707,6 +861,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
707
861
|
}
|
|
708
862
|
.path { margin-top: 8px; }
|
|
709
863
|
.missing { margin-top: 6px; }
|
|
864
|
+
.path:empty, .missing:empty { display: none; }
|
|
710
865
|
#hud.s-green .mascot-stage { filter: drop-shadow(0 2px 6px rgba(67, 209, 122, 0.3)); }
|
|
711
866
|
#hud.s-amber .mascot-stage { filter: drop-shadow(0 2px 7px rgba(233, 185, 73, 0.42)); }
|
|
712
867
|
#hud.s-red .mascot-stage { filter: drop-shadow(0 3px 8px rgba(199, 55, 47, 0.48)); }
|
|
@@ -761,13 +916,37 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
761
916
|
</div>
|
|
762
917
|
</div>
|
|
763
918
|
<div class="details">
|
|
919
|
+
<div class="timeline-scrollbar hidden" id="timelineScrollbar" aria-hidden="true"><span class="timeline-thumb" id="timelineThumb"></span></div>
|
|
920
|
+
<section class="quota-card" id="quotacard" aria-label="Plan usage">
|
|
921
|
+
<div class="quota-head">
|
|
922
|
+
<span class="quota-title">Plan usage</span>
|
|
923
|
+
<span class="quota-plan" id="quotaplan">Free</span>
|
|
924
|
+
<button class="quota-manage" id="quotamanage" type="button">Manage</button>
|
|
925
|
+
</div>
|
|
926
|
+
<div class="quota-list">
|
|
927
|
+
<div class="quota-row" data-quota="processing">
|
|
928
|
+
<div class="quota-line"><span class="quota-name">Processing this week</span><span class="quota-value"></span></div>
|
|
929
|
+
<div class="quota-track"><div class="quota-fill"></div></div>
|
|
930
|
+
<div class="quota-subline"><span class="quota-reset"></span><span class="quota-percent"></span></div>
|
|
931
|
+
</div>
|
|
932
|
+
<div class="quota-row" data-quota="searches">
|
|
933
|
+
<div class="quota-line"><span class="quota-name">Memory searches</span><span class="quota-value"></span></div>
|
|
934
|
+
<div class="quota-track"><div class="quota-fill"></div></div>
|
|
935
|
+
<div class="quota-subline"><span class="quota-reset"></span><span class="quota-percent"></span></div>
|
|
936
|
+
</div>
|
|
937
|
+
<div class="quota-row" data-quota="historical">
|
|
938
|
+
<div class="quota-line"><span class="quota-name">Bulk history imports</span><span class="quota-value"></span></div>
|
|
939
|
+
<div class="quota-track"><div class="quota-fill"></div></div>
|
|
940
|
+
<div class="quota-subline"><span class="quota-reset">Lifetime limit</span><span class="quota-percent"></span></div>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
</section>
|
|
764
944
|
<div class="now-card" id="nowcard">
|
|
765
945
|
<div class="now-sess">
|
|
766
946
|
<img class="now-icon" id="nowicon" src="/assets/hud/codex.svg" alt="" />
|
|
767
947
|
<span class="now-copy"><span class="now-title" id="nowtitle">EchoMem</span><span class="client-line" id="nowrepoline" style="display:none"><img class="line-icon" src="/assets/hud/github.svg" alt="" /><span class="now-repo" id="nowrepo"></span></span></span>
|
|
768
948
|
</div>
|
|
769
949
|
<div class="session-actions">
|
|
770
|
-
<button class="pin-chip" id="pinchip" type="button" title="Return to auto-follow">pinned x</button>
|
|
771
950
|
<button class="jump-chip" id="jumpchip" type="button" title="Open this conversation">↗</button>
|
|
772
951
|
</div>
|
|
773
952
|
<div class="billing-banner" id="billingbanner">
|
|
@@ -783,7 +962,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
783
962
|
<span class="state-word" id="stateWord">--%</span>
|
|
784
963
|
<button class="why-btn" id="whybtn" type="button" title="Details" aria-label="Details" style="display:none">+</button>
|
|
785
964
|
</div>
|
|
786
|
-
<div class="
|
|
965
|
+
<div class="signal-meta"><span class="meta-line" id="metaline"></span></div>
|
|
787
966
|
<div class="evidence" id="evidence"></div>
|
|
788
967
|
<div class="nudge" id="nudge"></div>
|
|
789
968
|
<button class="renew-btn" id="renewbtn"><b>Start new session with context</b><small>carry 100% signal, drop the junk</small></button>
|
|
@@ -802,7 +981,6 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
802
981
|
<div class="diagnostics-body" id="diagnosticsbody"><span class="diagnostics-v">Open this panel to check account status.</span></div>
|
|
803
982
|
</section>
|
|
804
983
|
<div class="tabs" id="tabs"></div>
|
|
805
|
-
<div class="meta-row"><span class="meta-line" id="metaline"></span><button class="switchbtn agent-switch" id="switchbtn" title="Switch agent" style="display:none" type="button"></button></div>
|
|
806
984
|
<div class="path" id="path"></div>
|
|
807
985
|
<button class="reveal" id="reveal" style="display:none">↗ Open session in viewer</button>
|
|
808
986
|
<div class="missing" id="missing"></div>
|
|
@@ -816,12 +994,10 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
816
994
|
const clientRepo = document.getElementById("clientrepo");
|
|
817
995
|
const stateDot = document.getElementById("stateDot");
|
|
818
996
|
const stateWordEl = document.getElementById("stateWord");
|
|
819
|
-
const verdictEl = document.getElementById("verdict");
|
|
820
997
|
const nowIcon = document.getElementById("nowicon");
|
|
821
998
|
const nowTitle = document.getElementById("nowtitle");
|
|
822
999
|
const nowRepo = document.getElementById("nowrepo");
|
|
823
1000
|
const nowRepoLine = document.getElementById("nowrepoline");
|
|
824
|
-
const pinChip = document.getElementById("pinchip");
|
|
825
1001
|
const jumpChip = document.getElementById("jumpchip");
|
|
826
1002
|
const whyBtn = document.getElementById("whybtn");
|
|
827
1003
|
const evidence = document.getElementById("evidence");
|
|
@@ -830,6 +1006,9 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
830
1006
|
const billingTitle = document.getElementById("billingtitle");
|
|
831
1007
|
const billingCopy = document.getElementById("billingcopy");
|
|
832
1008
|
const billingLink = document.getElementById("billinglink");
|
|
1009
|
+
const quotaCard = document.getElementById("quotacard");
|
|
1010
|
+
const quotaPlan = document.getElementById("quotaplan");
|
|
1011
|
+
const quotaManage = document.getElementById("quotamanage");
|
|
833
1012
|
const renewBtn = document.getElementById("renewbtn");
|
|
834
1013
|
const renewResult = document.getElementById("renewresult");
|
|
835
1014
|
const renewMsg = document.getElementById("renewmsg");
|
|
@@ -842,7 +1021,8 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
842
1021
|
const repoLine = document.getElementById("repoline");
|
|
843
1022
|
const pathEl = document.getElementById("path");
|
|
844
1023
|
const tabsEl = document.getElementById("tabs");
|
|
845
|
-
const
|
|
1024
|
+
const timelineScrollbar = document.getElementById("timelineScrollbar");
|
|
1025
|
+
const timelineThumb = document.getElementById("timelineThumb");
|
|
846
1026
|
const revealEl = document.getElementById("reveal");
|
|
847
1027
|
const missing = document.getElementById("missing");
|
|
848
1028
|
const diagnosticsBody = document.getElementById("diagnosticsbody");
|
|
@@ -868,9 +1048,9 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
868
1048
|
const CHECKPOINT_STATUS_CACHE_LIMIT = 48;
|
|
869
1049
|
const CHECKPOINT_STATUS_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
870
1050
|
const statusFetchInFlight = {};
|
|
871
|
-
// Demo-clean mode: hide only the research-y session VIEWER (raw log / payload). Keep the
|
|
872
|
-
//
|
|
873
|
-
//
|
|
1051
|
+
// Demo-clean mode: hide only the research-y session VIEWER (raw log / payload). Keep the unified
|
|
1052
|
+
// session feed — it is the manual override for "we can't detect which session you're viewing",
|
|
1053
|
+
// so the user can always point the HUD at the right session. Flip false to show the viewer.
|
|
874
1054
|
const DEMO_CLEAN = true;
|
|
875
1055
|
|
|
876
1056
|
function diagnosticsLine(label, value, bad, wrap) {
|
|
@@ -964,6 +1144,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
964
1144
|
const mrepoLine = document.getElementById("mrepoline");
|
|
965
1145
|
let miniOn = false;
|
|
966
1146
|
let whyOpen = false;
|
|
1147
|
+
let timelineScrollDrag = null;
|
|
967
1148
|
try { miniOn = localStorage.getItem("echomemHudMini") === "1"; } catch {}
|
|
968
1149
|
if (miniOn) hud.classList.add("mini");
|
|
969
1150
|
function applyMini(next) {
|
|
@@ -983,7 +1164,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
983
1164
|
});
|
|
984
1165
|
let hudDrag = null;
|
|
985
1166
|
let suppressNextToggle = false;
|
|
986
|
-
const dragBlockSelector = "button, input, textarea, select, a, pre, .tab, .
|
|
1167
|
+
const dragBlockSelector = "button, input, textarea, select, a, pre, .tab, .timeline-scrollbar, .reveal, .renew-btn, .copy-btn";
|
|
987
1168
|
|
|
988
1169
|
hud.addEventListener("pointerdown", beginHudDrag);
|
|
989
1170
|
window.addEventListener("pointermove", moveHudDrag);
|
|
@@ -1032,22 +1213,8 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1032
1213
|
fetch("/open-viewer?path=" + encodeURIComponent(currentSrc)).catch(() => {});
|
|
1033
1214
|
});
|
|
1034
1215
|
|
|
1035
|
-
//
|
|
1036
|
-
|
|
1037
|
-
e.stopPropagation();
|
|
1038
|
-
const st = lastState;
|
|
1039
|
-
const target = switchTarget(st, st && st.active && st.active.client);
|
|
1040
|
-
if (!target) return;
|
|
1041
|
-
fetch("/select?id=" + encodeURIComponent(target.id))
|
|
1042
|
-
.then(() => fetch("/state"))
|
|
1043
|
-
.then((r) => r.json())
|
|
1044
|
-
.then(render)
|
|
1045
|
-
.catch(() => {});
|
|
1046
|
-
});
|
|
1047
|
-
|
|
1048
|
-
// Click a history row → pin that session as the current one. Auto-follow is the default;
|
|
1049
|
-
// the card's "pinned ×" chip releases the pin. The ↗ corner jumps to the conversation in
|
|
1050
|
-
// its own app instead of switching the HUD view.
|
|
1216
|
+
// Click a history row → expand it where it already sits. The newest conversation remains in
|
|
1217
|
+
// the top card; the ↗ corner opens the historical conversation in its own app.
|
|
1051
1218
|
if (tabsEl) tabsEl.addEventListener("click", (e) => {
|
|
1052
1219
|
const jump = e.target && e.target.closest ? e.target.closest("[data-jump]") : null;
|
|
1053
1220
|
if (jump) {
|
|
@@ -1057,8 +1224,14 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1057
1224
|
}
|
|
1058
1225
|
const btn = e.target && e.target.closest ? e.target.closest(".tab") : null;
|
|
1059
1226
|
if (!btn) return;
|
|
1060
|
-
|
|
1227
|
+
toggleSessionDetails(btn.getAttribute("data-id"));
|
|
1061
1228
|
});
|
|
1229
|
+
if (tabsEl) tabsEl.addEventListener("scroll", syncTabsOverflowCue, { passive: true });
|
|
1230
|
+
if (timelineScrollbar) timelineScrollbar.addEventListener("pointerdown", beginTimelineScroll);
|
|
1231
|
+
window.addEventListener("pointermove", moveTimelineScroll);
|
|
1232
|
+
window.addEventListener("pointerup", endTimelineScroll);
|
|
1233
|
+
window.addEventListener("pointercancel", endTimelineScroll);
|
|
1234
|
+
window.addEventListener("resize", syncTabsOverflowCue);
|
|
1062
1235
|
if (jumpChip) jumpChip.addEventListener("click", (e) => {
|
|
1063
1236
|
e.stopPropagation();
|
|
1064
1237
|
jumpToSession(jumpChip.getAttribute("data-jump"));
|
|
@@ -1067,11 +1240,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1067
1240
|
if (!id) return;
|
|
1068
1241
|
fetch("/jump?id=" + encodeURIComponent(id)).catch(() => {});
|
|
1069
1242
|
}
|
|
1070
|
-
|
|
1071
|
-
e.stopPropagation();
|
|
1072
|
-
selectSession("auto");
|
|
1073
|
-
});
|
|
1074
|
-
if (billingLink) billingLink.addEventListener("click", (e) => {
|
|
1243
|
+
function openPricing(e) {
|
|
1075
1244
|
e.stopPropagation();
|
|
1076
1245
|
const url = billingStatus && billingStatus.pricingUrl;
|
|
1077
1246
|
if (!url) return;
|
|
@@ -1083,10 +1252,13 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1083
1252
|
.catch(() => {
|
|
1084
1253
|
try { window.open(url, "_blank", "noopener"); } catch {}
|
|
1085
1254
|
});
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1255
|
+
}
|
|
1256
|
+
if (billingLink) billingLink.addEventListener("click", openPricing);
|
|
1257
|
+
if (quotaManage) quotaManage.addEventListener("click", openPricing);
|
|
1258
|
+
function toggleSessionDetails(id) {
|
|
1088
1259
|
if (!id) return;
|
|
1089
|
-
|
|
1260
|
+
const nextId = lastState && lastState.expandedId === id ? "auto" : id;
|
|
1261
|
+
fetch("/expand?id=" + encodeURIComponent(nextId))
|
|
1090
1262
|
.then(() => fetch("/state"))
|
|
1091
1263
|
.then((r) => r.json())
|
|
1092
1264
|
.then(render)
|
|
@@ -1100,7 +1272,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1100
1272
|
event.stopPropagation();
|
|
1101
1273
|
return;
|
|
1102
1274
|
}
|
|
1103
|
-
if (event.target instanceof HTMLElement &&
|
|
1275
|
+
if (event.target instanceof HTMLElement && event.target.closest(".cornerbtn")) return;
|
|
1104
1276
|
hud.classList.toggle("open");
|
|
1105
1277
|
if (hud.classList.contains("open")) maybeLoadDiagnostics();
|
|
1106
1278
|
syncHeight();
|
|
@@ -1160,18 +1332,31 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1160
1332
|
saveCachedCheckpoint(renewSessionKey, prompt, promptKind, meta);
|
|
1161
1333
|
if (promptKind === "cloud") saveCheckpointStatus(renewSessionKey, statusFromRenewData(renewData, meta));
|
|
1162
1334
|
const doneSummary = checkpointSuffix(checkpointSummary({ renewedAt: Date.now(), memoryCount: meta.sessionMemoryCount }));
|
|
1335
|
+
let launchData = null;
|
|
1336
|
+
try {
|
|
1337
|
+
const launchRes = await fetch("/launch", {
|
|
1338
|
+
method: "POST",
|
|
1339
|
+
headers: { "Content-Type": "application/json" },
|
|
1340
|
+
body: JSON.stringify({ prompt, sourcePath: String((scoreAtRenew && scoreAtRenew.sourcePath) || "") }),
|
|
1341
|
+
});
|
|
1342
|
+
launchData = await launchRes.json();
|
|
1343
|
+
} catch {}
|
|
1344
|
+
const opened = Boolean(launchData && launchData.ok);
|
|
1345
|
+
let fallbackCopied = false;
|
|
1346
|
+
if (!opened) {
|
|
1347
|
+
try {
|
|
1348
|
+
await navigator.clipboard.writeText(prompt);
|
|
1349
|
+
fallbackCopied = true;
|
|
1350
|
+
} catch {}
|
|
1351
|
+
}
|
|
1352
|
+
const readyLabel = promptKind === "local" ? "Local capsule" : "Decision log";
|
|
1353
|
+
const openedLabel = launchData && launchData.mode === "claude-code" ? "Carryover sent in a fresh Claude Code session" : "Carryover sent in a fresh Codex session";
|
|
1163
1354
|
showRenewResult({
|
|
1164
1355
|
sessionKey: renewSessionKey,
|
|
1165
1356
|
prompt,
|
|
1166
|
-
message: (
|
|
1357
|
+
message: (opened ? openedLabel : readyLabel + (fallbackCopied ? " copied" : " ready")) + doneSummary,
|
|
1167
1358
|
canCopy: true,
|
|
1168
1359
|
});
|
|
1169
|
-
try {
|
|
1170
|
-
await navigator.clipboard.writeText(prompt);
|
|
1171
|
-
if (renewMsg) renewMsg.textContent = (promptKind === "local" ? "Local capsule copied" : "Decision log copied") + doneSummary;
|
|
1172
|
-
} catch {
|
|
1173
|
-
if (renewMsg) renewMsg.textContent = (promptKind === "local" ? "Local capsule ready" : "Decision log ready") + doneSummary;
|
|
1174
|
-
}
|
|
1175
1360
|
} catch {
|
|
1176
1361
|
} finally {
|
|
1177
1362
|
stopRenewTimer();
|
|
@@ -1534,13 +1719,25 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1534
1719
|
// skips the live feed — same pattern as the extraction page's ?preview hook.
|
|
1535
1720
|
const previewParams = new URLSearchParams(location.search);
|
|
1536
1721
|
const previewColor = previewParams.get("preview");
|
|
1537
|
-
const
|
|
1722
|
+
const previewExpanded = previewParams.get("expanded") === "1" || previewParams.get("pinned") === "1";
|
|
1538
1723
|
if (previewParams.get("capture") === "1") hud.classList.add("capture-preview");
|
|
1539
1724
|
if (previewColor === "green" || previewColor === "amber" || previewColor === "red") {
|
|
1725
|
+
const previewProcessingUsed = previewColor === "red" ? 200000 : previewColor === "amber" ? 163000 : 84000;
|
|
1726
|
+
const previewSearchUsed = previewColor === "red" ? 100 : previewColor === "amber" ? 82 : 32;
|
|
1727
|
+
billingStatus = {
|
|
1728
|
+
ok: true,
|
|
1729
|
+
state: "ok",
|
|
1730
|
+
plan: "pro",
|
|
1731
|
+
paid: true,
|
|
1732
|
+
pricingUrl: "https://yeahecho.com/pricing?source=hud_preview",
|
|
1733
|
+
memoryProcessingQuota: { used: previewProcessingUsed, limit: 200000, remaining: 200000 - previewProcessingUsed, resetAt: new Date(Date.now() + 31 * 60 * 60 * 1000).toISOString() },
|
|
1734
|
+
memorySearchQuota: { used: previewSearchUsed, limit: 100, remaining: 100 - previewSearchUsed, resetAt: new Date(Date.now() + 31 * 60 * 60 * 1000).toISOString() },
|
|
1735
|
+
historicalConversationQuota: { used: 191, limit: 250, remaining: 59 },
|
|
1736
|
+
};
|
|
1540
1737
|
const sample = {
|
|
1541
1738
|
client: "codex",
|
|
1542
1739
|
label: "EchoMem-Chrome",
|
|
1543
|
-
sourcePath:
|
|
1740
|
+
sourcePath: "/preview/session.jsonl",
|
|
1544
1741
|
usefulPct: previewColor === "red" ? 55 : previewColor === "amber" ? 75 : 95,
|
|
1545
1742
|
healthScorePct: previewColor === "red" ? 48 : previewColor === "amber" ? 72 : 96,
|
|
1546
1743
|
color: previewColor,
|
|
@@ -1554,19 +1751,37 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1554
1751
|
lastActiveMs: 6000,
|
|
1555
1752
|
ctSource: "measured",
|
|
1556
1753
|
};
|
|
1754
|
+
const expandedSample = {
|
|
1755
|
+
client: "codex",
|
|
1756
|
+
label: "EchoMem-Chrome",
|
|
1757
|
+
sourcePath: "/preview/b.jsonl",
|
|
1758
|
+
usefulPct: 71,
|
|
1759
|
+
healthScorePct: 71,
|
|
1760
|
+
color: "amber",
|
|
1761
|
+
ctTokens: 193000,
|
|
1762
|
+
modelContextWindow: 258000,
|
|
1763
|
+
pollutionPct: 29,
|
|
1764
|
+
pollutionTok: 55970,
|
|
1765
|
+
buckets: { range_redundant: { count: 12 } },
|
|
1766
|
+
stats: { patchEdits: 22 },
|
|
1767
|
+
turn: 3,
|
|
1768
|
+
lastActiveMs: 480000,
|
|
1769
|
+
ctSource: "measured",
|
|
1770
|
+
};
|
|
1557
1771
|
render({
|
|
1558
1772
|
active: sample,
|
|
1559
|
-
|
|
1773
|
+
expanded: previewExpanded ? expandedSample : null,
|
|
1774
|
+
expandedId: previewExpanded ? "p2" : null,
|
|
1560
1775
|
sessions: [
|
|
1561
1776
|
sample,
|
|
1562
|
-
|
|
1777
|
+
expandedSample,
|
|
1563
1778
|
{ client: "codex", sourcePath: "/preview/c.jsonl", usefulPct: 94, healthScorePct: 94, color: "green" },
|
|
1564
1779
|
],
|
|
1565
1780
|
recentSessions: [
|
|
1566
1781
|
{ id: "p1", client: "codex", title: "Simplify HUD session display", label: "EchoMem-Chrome", sourcePath: "/preview/session.jsonl", live: true, lastActiveMs: 4000 },
|
|
1567
1782
|
{ id: "p2", client: "codex", title: "修复 HUD 切换和图标", label: "EchoMem-Chrome", sourcePath: "/preview/b.jsonl", live: false, lastActiveMs: 480000 },
|
|
1568
1783
|
{ id: "p3", client: "codex", title: "Fix extraction preview", label: "EchoMem-Chrome", sourcePath: "/preview/c.jsonl", live: false, lastActiveMs: 2400000 },
|
|
1569
|
-
{ id: "p4", client: "
|
|
1784
|
+
{ id: "p4", client: "claude-code", title: "Port Claude work", label: "EchoForPeople", sourcePath: "/preview/d.jsonl", live: false, lastActiveMs: 9000000 },
|
|
1570
1785
|
{ id: "p5", client: "codex", title: "Stamp fixes with Echo mascot", label: "EchoMem-Chrome", sourcePath: "/preview/e.jsonl", live: false, lastActiveMs: 16200000 },
|
|
1571
1786
|
{ id: "p6", client: "codex", title: "Review migration checks", label: "EchoForPeople", sourcePath: "/preview/f.jsonl", live: false, lastActiveMs: 21600000 },
|
|
1572
1787
|
{ id: "p7", client: "codex", title: "Polish setup story", label: "EchoMem-Chrome", sourcePath: "/preview/g.jsonl", live: false, lastActiveMs: 34200000 },
|
|
@@ -1601,9 +1816,8 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1601
1816
|
const currentSessionKey = sessionKeyFor(state, score);
|
|
1602
1817
|
const viewColor = score && typeof score.usefulPct === "number" ? (score.color || "green") : "idle";
|
|
1603
1818
|
if (renewResultSessionKey && renewResultSessionKey !== currentSessionKey) clearRenewResult();
|
|
1604
|
-
const activeId = score && score.sourcePath ? activeIdFor(state, score.sourcePath
|
|
1605
|
-
renderTabs(state, activeId
|
|
1606
|
-
renderSwitchButton(state, activeClient);
|
|
1819
|
+
const activeId = score && score.sourcePath ? activeIdFor(state, score.sourcePath) : null;
|
|
1820
|
+
renderTabs(state, activeId);
|
|
1607
1821
|
if (!score || typeof score.usefulPct !== "number") {
|
|
1608
1822
|
renderIdle(state);
|
|
1609
1823
|
return;
|
|
@@ -1612,7 +1826,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1612
1826
|
const color = score.color || "green";
|
|
1613
1827
|
const sat = fullnessPct(score);
|
|
1614
1828
|
const oldPct = clamp(score.pollutionPct || 0, 0, 95);
|
|
1615
|
-
const
|
|
1829
|
+
const signal = usefulSignalPct(score);
|
|
1616
1830
|
const repeatCount = score.buckets && score.buckets.range_redundant ? score.buckets.range_redundant.count || 0 : 0;
|
|
1617
1831
|
const info = activeSessionInfo(score, state);
|
|
1618
1832
|
|
|
@@ -1620,8 +1834,8 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1620
1834
|
stateDot.className = "state-dot " + color;
|
|
1621
1835
|
hud.classList.remove("s-green", "s-amber", "s-red");
|
|
1622
1836
|
hud.classList.add("s-" + color);
|
|
1623
|
-
main.innerHTML =
|
|
1624
|
-
renderMini(color,
|
|
1837
|
+
main.innerHTML = signal + '%<span class="main-rest"> useful</span>';
|
|
1838
|
+
renderMini(color, signal + "%", info);
|
|
1625
1839
|
client.textContent = info.title || info.repo;
|
|
1626
1840
|
if (bubbleAgent) {
|
|
1627
1841
|
bubbleAgent.src = iconForFamily(agentFamily(score.client) || "codex");
|
|
@@ -1637,18 +1851,16 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1637
1851
|
if (score.ctTokens > 0) metaParts.push(fmt(score.ctTokens) + " tok");
|
|
1638
1852
|
metaTail = metaParts.join(" · ");
|
|
1639
1853
|
renderMetaLine();
|
|
1640
|
-
stateWordEl.
|
|
1641
|
-
if (verdictEl) verdictEl.textContent = verdictFor(color, oldPct);
|
|
1854
|
+
stateWordEl.innerHTML = '<span class="snr-prefix">SNR</span>' + signal + '%<span class="signal-label">useful</span>';
|
|
1642
1855
|
if (nowIcon) nowIcon.src = iconForFamily(agentFamily(score.client) || "codex");
|
|
1643
1856
|
if (nowTitle) nowTitle.textContent = info.title;
|
|
1644
1857
|
if (nowRepo) nowRepo.textContent = bubbleRepo;
|
|
1645
1858
|
if (nowRepoLine) nowRepoLine.style.display = bubbleRepo ? "flex" : "none";
|
|
1646
|
-
if (pinChip) pinChip.style.display = state && state.pinnedId ? "inline-block" : "none";
|
|
1647
1859
|
if (jumpChip) {
|
|
1648
1860
|
if (activeId) jumpChip.setAttribute("data-jump", activeId);
|
|
1649
1861
|
jumpChip.style.display = activeId ? "inline-block" : "none";
|
|
1650
1862
|
}
|
|
1651
|
-
renderWhy(score, oldPct, repeatCount, sat,
|
|
1863
|
+
renderWhy(score, oldPct, repeatCount, sat, signal, color);
|
|
1652
1864
|
renderBillingStatus();
|
|
1653
1865
|
pathEl.textContent = "";
|
|
1654
1866
|
currentSrc = score.sourcePath || null;
|
|
@@ -1699,11 +1911,9 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1699
1911
|
currentSrc = null;
|
|
1700
1912
|
if (revealEl) revealEl.style.display = "none";
|
|
1701
1913
|
stateWordEl.textContent = "on";
|
|
1702
|
-
if (verdictEl) verdictEl.textContent = "Waiting for Codex or Claude.";
|
|
1703
1914
|
if (nowTitle) nowTitle.textContent = "EchoMem MCP";
|
|
1704
1915
|
if (nowRepo) nowRepo.textContent = "";
|
|
1705
1916
|
if (nowRepoLine) nowRepoLine.style.display = "none";
|
|
1706
|
-
if (pinChip) pinChip.style.display = "none";
|
|
1707
1917
|
if (jumpChip) jumpChip.style.display = "none";
|
|
1708
1918
|
evidence.textContent = "";
|
|
1709
1919
|
if (whyBtn) whyBtn.style.display = "none";
|
|
@@ -1734,32 +1944,110 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1734
1944
|
|
|
1735
1945
|
function renderBillingStatus() {
|
|
1736
1946
|
if (!billingBanner) return;
|
|
1947
|
+
renderQuotaUsage();
|
|
1737
1948
|
const state = billingStatus && billingStatus.state;
|
|
1738
|
-
const blocked = state === "
|
|
1949
|
+
const blocked = state === "quota_exceeded" || state === "billing_attention";
|
|
1739
1950
|
billingBanner.style.display = blocked ? "block" : "none";
|
|
1740
1951
|
billingBanner.classList.toggle("limit", state === "quota_exceeded");
|
|
1741
1952
|
if (!blocked) return;
|
|
1742
|
-
const title = state === "quota_exceeded"
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
: billingStatus.message || "Billing needs attention";
|
|
1747
|
-
const detail = state === "quota_exceeded"
|
|
1748
|
-
? "You used all Pro searches this month. Upgrade or wait for reset."
|
|
1749
|
-
: state === "plan_required"
|
|
1750
|
-
? "Search memory requires Pro or Power. Saving still works."
|
|
1751
|
-
: billingStatus.detail || "Open pricing to restore memory search.";
|
|
1953
|
+
const title = billingStatus.message || (state === "quota_exceeded" ? "Plan limit reached" : "Billing needs attention");
|
|
1954
|
+
const detail = billingStatus.detail || (state === "quota_exceeded"
|
|
1955
|
+
? "A weekly limit has been reached. Wait for reset or change your plan."
|
|
1956
|
+
: "Open pricing to review your plan.");
|
|
1752
1957
|
if (billingTitle) billingTitle.textContent = title;
|
|
1753
1958
|
if (billingCopy) billingCopy.textContent = detail;
|
|
1754
1959
|
}
|
|
1755
1960
|
|
|
1756
|
-
function
|
|
1961
|
+
function renderQuotaUsage() {
|
|
1962
|
+
if (!quotaCard) return;
|
|
1963
|
+
const status = billingStatus || {};
|
|
1964
|
+
const quotaRows = [
|
|
1965
|
+
["processing", status.memoryProcessingQuota, false],
|
|
1966
|
+
["searches", status.memorySearchQuota, false],
|
|
1967
|
+
["historical", status.historicalConversationQuota, true],
|
|
1968
|
+
];
|
|
1969
|
+
const hasQuota = quotaRows.some((entry) => entry[1] && typeof entry[1] === "object");
|
|
1970
|
+
const visible = hasQuota && status.state !== "not_connected" && status.plan !== "unknown";
|
|
1971
|
+
quotaCard.style.display = visible ? "block" : "none";
|
|
1972
|
+
if (!visible) return;
|
|
1973
|
+
if (quotaPlan) quotaPlan.textContent = planLabel(status.plan || "free");
|
|
1974
|
+
for (const entry of quotaRows) renderQuotaRow(entry[0], entry[1], entry[2]);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function renderQuotaRow(key, snapshot, lifetime) {
|
|
1978
|
+
if (!quotaCard) return;
|
|
1979
|
+
const row = quotaCard.querySelector('[data-quota="' + key + '"]');
|
|
1980
|
+
if (!row) return;
|
|
1981
|
+
const valueEl = row.querySelector(".quota-value");
|
|
1982
|
+
const fillEl = row.querySelector(".quota-fill");
|
|
1983
|
+
const resetEl = row.querySelector(".quota-reset");
|
|
1984
|
+
const percentEl = row.querySelector(".quota-percent");
|
|
1985
|
+
const used = quotaNumber(snapshot && snapshot.used);
|
|
1986
|
+
const limit = quotaNumber(snapshot && snapshot.limit);
|
|
1987
|
+
const available = used !== null && limit !== null;
|
|
1988
|
+
const unlimited = available && limit < 0;
|
|
1989
|
+
const percent = available && limit > 0 ? clamp(Math.round(used / limit * 100), 0, 100) : 0;
|
|
1990
|
+
|
|
1991
|
+
row.classList.toggle("is-unavailable", !available);
|
|
1992
|
+
row.classList.toggle("is-warn", available && !unlimited && percent >= 80 && percent < 100);
|
|
1993
|
+
row.classList.toggle("is-limit", available && !unlimited && percent >= 100);
|
|
1994
|
+
if (valueEl) {
|
|
1995
|
+
valueEl.textContent = unlimited
|
|
1996
|
+
? formatQuotaNumber(used) + " / Unlimited"
|
|
1997
|
+
: (used === null ? "—" : formatQuotaNumber(used)) + " / " + (limit === null ? "—" : formatQuotaNumber(limit));
|
|
1998
|
+
}
|
|
1999
|
+
if (fillEl) fillEl.style.width = unlimited ? "0%" : percent + "%";
|
|
2000
|
+
if (percentEl) percentEl.textContent = !available
|
|
2001
|
+
? "Usage unavailable"
|
|
2002
|
+
: unlimited
|
|
2003
|
+
? "Unlimited"
|
|
2004
|
+
: percent >= 100
|
|
2005
|
+
? "Limit reached"
|
|
2006
|
+
: percent + "%";
|
|
2007
|
+
if (resetEl) resetEl.textContent = lifetime ? "Lifetime limit" : quotaResetLabel(snapshot && snapshot.resetAt);
|
|
2008
|
+
row.setAttribute("aria-label", key + " usage " + (percentEl ? percentEl.textContent : ""));
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function quotaNumber(value) {
|
|
2012
|
+
return typeof value === "number" && isFinite(value) ? value : null;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
function formatQuotaNumber(value) {
|
|
2016
|
+
const n = Math.max(0, Number(value) || 0);
|
|
2017
|
+
if (n >= 1000000) return (n / 1000000).toFixed(n < 10000000 ? 1 : 0).replace(/\.0$/, "") + "M";
|
|
2018
|
+
if (n >= 1000) return (n / 1000).toFixed(n < 100000 ? 1 : 0).replace(/\.0$/, "") + "K";
|
|
2019
|
+
return Math.round(n).toLocaleString("en-US");
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
function quotaResetLabel(value) {
|
|
2023
|
+
if (!value) return "Weekly reset";
|
|
2024
|
+
const resetAt = new Date(value);
|
|
2025
|
+
if (!isFinite(resetAt.getTime())) return "Weekly reset";
|
|
2026
|
+
if (resetAt.getTime() <= Date.now()) return "Resetting now";
|
|
2027
|
+
return "Resets " + resetAt.toLocaleString(undefined, {
|
|
2028
|
+
weekday: "short",
|
|
2029
|
+
month: "short",
|
|
2030
|
+
day: "numeric",
|
|
2031
|
+
hour: "numeric",
|
|
2032
|
+
minute: "2-digit",
|
|
2033
|
+
timeZoneName: "short",
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function renderWhy(score, oldPct, repeatCount, sat, signal, color) {
|
|
1757
2038
|
const rows = [];
|
|
1758
|
-
if (sat > 0) rows.push(["context", score.modelContextWindow ? sat + "% full · " + fmt(score.ctTokens) : fmt(score.ctTokens) + " tokens"]);
|
|
1759
|
-
if (score.pollutionTok > 0) rows.push(["junk", fmt(score.pollutionTok) + " (" + Math.round(oldPct) + "% of window)"]);
|
|
1760
2039
|
for (const row of junkBreakdown(score)) rows.push(row);
|
|
1761
2040
|
rows.push(["source", ctSourceLabel(score.ctSource)]);
|
|
1762
|
-
|
|
2041
|
+
const contextTokens = Math.max(0, Number(score.ctTokens) || 0);
|
|
2042
|
+
const junkTokens = Math.min(contextTokens, Math.max(0, Number(score.pollutionTok) || 0));
|
|
2043
|
+
const equation = contextTokens > 0
|
|
2044
|
+
? '<div class="snr-equation" aria-label="One minus ' + fmtExact(junkTokens) + ' junk divided by ' + fmtExact(contextTokens) + ' in context equals ' + signal + ' percent SNR">' +
|
|
2045
|
+
'<span>1 −</span>' +
|
|
2046
|
+
'<span class="snr-fraction"><span class="snr-numerator">' + fmtExact(junkTokens) + ' junk</span><span class="snr-denominator">' + fmtExact(contextTokens) + ' in context</span></span>' +
|
|
2047
|
+
'<span class="snr-result">= ' + signal + '% SNR</span>' +
|
|
2048
|
+
'</div>'
|
|
2049
|
+
: '';
|
|
2050
|
+
evidence.innerHTML = equation + rows.map((row) => '<div class="why-line"><span class="why-k">' + esc(row[0]) + '</span><span class="why-v">' + esc(row[1]) + "</span></div>").join("");
|
|
1763
2051
|
const useful = color !== "green" || oldPct >= 10 || repeatCount > 0 || sat >= 80;
|
|
1764
2052
|
if (whyBtn) {
|
|
1765
2053
|
whyBtn.style.display = useful ? "inline-flex" : "none";
|
|
@@ -1794,9 +2082,10 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1794
2082
|
return 0;
|
|
1795
2083
|
}
|
|
1796
2084
|
|
|
1797
|
-
function
|
|
1798
|
-
if (typeof score.healthScorePct === "number" && isFinite(score.healthScorePct)) return clamp(score.healthScorePct, 0, 100);
|
|
2085
|
+
function usefulSignalPct(score) {
|
|
1799
2086
|
if (typeof score.usefulPct === "number" && isFinite(score.usefulPct)) return clamp(score.usefulPct, 0, 100);
|
|
2087
|
+
if (typeof score.pollutionPct === "number" && isFinite(score.pollutionPct)) return clamp(100 - score.pollutionPct, 0, 100);
|
|
2088
|
+
if (typeof score.healthScorePct === "number" && isFinite(score.healthScorePct)) return clamp(score.healthScorePct, 0, 100);
|
|
1800
2089
|
return 0;
|
|
1801
2090
|
}
|
|
1802
2091
|
|
|
@@ -1829,44 +2118,23 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1829
2118
|
return title || info.client;
|
|
1830
2119
|
}
|
|
1831
2120
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
2121
|
+
// History rows: recent sessions that are NOT the newest one in the top card. Clicking a row
|
|
2122
|
+
// expands it in place without changing this newest-first sequence. Codex and Claude share one
|
|
2123
|
+
// timeline; their icons preserve source identity.
|
|
2124
|
+
function renderTabs(state, activeId) {
|
|
2125
|
+
if (!tabsEl) return;
|
|
2126
|
+
const anchor = captureTimelineAnchor();
|
|
2127
|
+
const list = allRecentSessions(state).filter((s) => s.id !== activeId);
|
|
2128
|
+
if (!list.length) {
|
|
2129
|
+
tabsEl.innerHTML = "";
|
|
2130
|
+
tabsEl.scrollTop = 0;
|
|
2131
|
+
tabsEl.classList.remove("has-more-above", "has-more-below");
|
|
2132
|
+
if (timelineScrollbar) timelineScrollbar.classList.add("hidden");
|
|
1840
2133
|
return;
|
|
1841
2134
|
}
|
|
1842
|
-
const family = agentFamily(target.client);
|
|
1843
|
-
const name = family === "claude" ? "Claude" : "Codex";
|
|
1844
|
-
switchBtn.dataset.targetFamily = family;
|
|
1845
|
-
switchBtn.dataset.targetId = target.id;
|
|
1846
|
-
switchBtn.title = "Switch to " + name;
|
|
1847
|
-
switchBtn.innerHTML = '<img class="switch-icon" src="' + iconForFamily(family) + '" alt="" /><span class="switch-copy">Switch to ' + name + "</span>";
|
|
1848
|
-
switchBtn.style.display = "inline-flex";
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
function switchTarget(state, activeClient) {
|
|
1852
|
-
const list = (state && state.recentSessions) || [];
|
|
1853
|
-
if (!list.length) return null;
|
|
1854
|
-
const currentFamily = agentFamily(activeClient || (state.active && state.active.client));
|
|
1855
|
-
const targetFamily = currentFamily === "codex" ? "claude" : currentFamily === "claude" ? "codex" : "";
|
|
1856
|
-
if (!targetFamily) return null;
|
|
1857
|
-
return list.filter((s) => agentFamily(s.client) === targetFamily)[0] || null;
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
// History rows: recent sessions that are NOT the current one (the current session lives in the
|
|
1861
|
-
// now-card above — every number and action up there belongs to it). Clicking a row pins it as
|
|
1862
|
-
// the new current session; the card's "pinned ×" chip releases back to auto-follow. Scored
|
|
1863
|
-
// sessions carry their own health dot + %, so "Echo is following you across agents" reads off
|
|
1864
|
-
// the list itself. Family-scoped, so Codex never recommends Claude sessions or vice versa.
|
|
1865
|
-
function renderTabs(state, activeId, activeClient) {
|
|
1866
|
-
if (!tabsEl) return;
|
|
1867
|
-
const list = visibleRecentSessions(state, activeClient).filter((s) => s.id !== activeId);
|
|
1868
|
-
if (!list.length) { tabsEl.innerHTML = ""; return; }
|
|
1869
2135
|
const chips = list.map((s) => {
|
|
2136
|
+
const expanded = Boolean(state && state.expandedId === s.id);
|
|
2137
|
+
const expandedView = expanded ? expandedSessionFor(state, s) : null;
|
|
1870
2138
|
const activity = s.live ? '<span class="spin"></span>' : "";
|
|
1871
2139
|
const hv = sessionHealthFor(state, s);
|
|
1872
2140
|
const status = hv
|
|
@@ -1879,12 +2147,114 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1879
2147
|
? '<span class="tab-repo"><img class="tab-gh" src="/assets/hud/github.svg" alt="" /><span class="tab-reponame">' + esc(repo) + "</span></span>"
|
|
1880
2148
|
: '<span class="tab-repo"><span class="tab-reponame">' + esc(label(s.client)) + "</span></span>";
|
|
1881
2149
|
const time = typeof s.lastActiveMs === "number" ? '<span class="tab-time" title="Last active here">' + esc(relTime(s.lastActiveMs)) + "</span>" : "";
|
|
1882
|
-
|
|
2150
|
+
const detail = expanded ? expandedSessionDetail(expandedView, s) : "";
|
|
2151
|
+
const action = expanded ? "Hide details" : "Show details";
|
|
2152
|
+
return '<button class="tab' + (expanded ? " expanded" : "") + '" data-id="' + esc(s.id) + '" aria-expanded="' + (expanded ? "true" : "false") + '" title="' + esc(action + " · " + label(s.client) + " · " + title + (repo ? " · " + repo : "")) + '">'
|
|
1883
2153
|
+ '<img class="tab-icon" src="' + iconForFamily(family) + '" alt="" />'
|
|
1884
2154
|
+ '<span class="tab-copy"><span class="tab-title">' + esc(title) + activity + "</span>" + repoCell + "</span>" + status + time
|
|
1885
|
-
+ '<span class="tab-jump" data-jump="' + esc(s.id) + '" title="Open this conversation in ' + esc(label(s.client)) + '">↗</span
|
|
2155
|
+
+ '<span class="tab-jump" data-jump="' + esc(s.id) + '" title="Open this conversation in ' + esc(label(s.client)) + '">↗</span>'
|
|
2156
|
+
+ detail + "</button>";
|
|
1886
2157
|
});
|
|
1887
2158
|
tabsEl.innerHTML = chips.join("");
|
|
2159
|
+
restoreTimelineAnchor(anchor);
|
|
2160
|
+
}
|
|
2161
|
+
function expandedSessionFor(state, session) {
|
|
2162
|
+
const view = state && state.expanded;
|
|
2163
|
+
return view && view.sourcePath === session.sourcePath && view.client === session.client ? view : null;
|
|
2164
|
+
}
|
|
2165
|
+
function expandedSessionDetail(view, session) {
|
|
2166
|
+
if (!view) return '<span class="tab-detail"><span class="tab-detail-score">Loading SNR…</span></span>';
|
|
2167
|
+
const meta = [];
|
|
2168
|
+
if (typeof session.lastActiveMs === "number") meta.push(relTime(session.lastActiveMs));
|
|
2169
|
+
if (typeof view.turn === "number" && view.turn > 0) meta.push("turn " + view.turn);
|
|
2170
|
+
if (Number(view.ctTokens) > 0) meta.push(fmt(view.ctTokens) + " tok");
|
|
2171
|
+
return '<span class="tab-detail"><span class="tab-detail-score">SNR ' + usefulSignalPct(view) + '% useful</span>'
|
|
2172
|
+
+ '<span class="tab-detail-meta">' + esc(meta.join(" · ")) + "</span></span>";
|
|
2173
|
+
}
|
|
2174
|
+
function captureTimelineAnchor() {
|
|
2175
|
+
if (!tabsEl || !tabsEl.children.length) return null;
|
|
2176
|
+
const viewportTop = tabsEl.getBoundingClientRect().top;
|
|
2177
|
+
const rows = Array.from(tabsEl.querySelectorAll(".tab"));
|
|
2178
|
+
const row = rows.find((item) => item.getBoundingClientRect().bottom > viewportTop + 1);
|
|
2179
|
+
if (!row) return { id: "", offset: 0, scrollTop: tabsEl.scrollTop };
|
|
2180
|
+
return {
|
|
2181
|
+
id: row.getAttribute("data-id") || "",
|
|
2182
|
+
offset: row.getBoundingClientRect().top - viewportTop,
|
|
2183
|
+
scrollTop: tabsEl.scrollTop,
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
function restoreTimelineAnchor(anchor) {
|
|
2187
|
+
if (!tabsEl) return;
|
|
2188
|
+
const maxScroll = Math.max(0, tabsEl.scrollHeight - tabsEl.clientHeight);
|
|
2189
|
+
if (!anchor) {
|
|
2190
|
+
tabsEl.scrollTop = Math.min(tabsEl.scrollTop, maxScroll);
|
|
2191
|
+
requestAnimationFrame(syncTabsOverflowCue);
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
const row = Array.from(tabsEl.querySelectorAll(".tab")).find((item) => item.getAttribute("data-id") === anchor.id);
|
|
2195
|
+
if (row) {
|
|
2196
|
+
const nextOffset = row.getBoundingClientRect().top - tabsEl.getBoundingClientRect().top;
|
|
2197
|
+
tabsEl.scrollTop = clamp(tabsEl.scrollTop + nextOffset - anchor.offset, 0, maxScroll);
|
|
2198
|
+
} else {
|
|
2199
|
+
tabsEl.scrollTop = clamp(anchor.scrollTop, 0, maxScroll);
|
|
2200
|
+
}
|
|
2201
|
+
requestAnimationFrame(syncTabsOverflowCue);
|
|
2202
|
+
}
|
|
2203
|
+
function syncTabsOverflowCue() {
|
|
2204
|
+
if (!tabsEl) return;
|
|
2205
|
+
const remaining = tabsEl.scrollHeight - tabsEl.scrollTop - tabsEl.clientHeight;
|
|
2206
|
+
tabsEl.classList.toggle("has-more-above", tabsEl.scrollTop > 3);
|
|
2207
|
+
tabsEl.classList.toggle("has-more-below", remaining > 3);
|
|
2208
|
+
syncTimelineScrollbar();
|
|
2209
|
+
}
|
|
2210
|
+
function syncTimelineScrollbar() {
|
|
2211
|
+
if (!tabsEl || !timelineScrollbar || !timelineThumb) return;
|
|
2212
|
+
const maxScroll = Math.max(0, tabsEl.scrollHeight - tabsEl.clientHeight);
|
|
2213
|
+
const trackHeight = timelineScrollbar.clientHeight;
|
|
2214
|
+
const scrollable = maxScroll > 1 && trackHeight > 0;
|
|
2215
|
+
timelineScrollbar.classList.toggle("hidden", !scrollable);
|
|
2216
|
+
if (!scrollable) {
|
|
2217
|
+
timelineThumb.style.height = "100%";
|
|
2218
|
+
timelineThumb.style.transform = "translateY(0)";
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
const thumbHeight = Math.max(30, Math.round(trackHeight * tabsEl.clientHeight / tabsEl.scrollHeight));
|
|
2222
|
+
const travel = Math.max(0, trackHeight - thumbHeight);
|
|
2223
|
+
const offset = travel * tabsEl.scrollTop / maxScroll;
|
|
2224
|
+
timelineThumb.style.height = thumbHeight + "px";
|
|
2225
|
+
timelineThumb.style.transform = "translateY(" + Math.round(offset) + "px)";
|
|
2226
|
+
}
|
|
2227
|
+
function beginTimelineScroll(event) {
|
|
2228
|
+
if (!tabsEl || !timelineScrollbar || !timelineThumb || event.button !== 0) return;
|
|
2229
|
+
const thumbRect = timelineThumb.getBoundingClientRect();
|
|
2230
|
+
const onThumb = event.target === timelineThumb;
|
|
2231
|
+
timelineScrollDrag = {
|
|
2232
|
+
pointerId: event.pointerId,
|
|
2233
|
+
grabOffset: onThumb ? event.clientY - thumbRect.top : thumbRect.height / 2,
|
|
2234
|
+
};
|
|
2235
|
+
try { timelineScrollbar.setPointerCapture(event.pointerId); } catch {}
|
|
2236
|
+
if (!onThumb) setTimelineScrollFromPointer(event.clientY);
|
|
2237
|
+
event.preventDefault();
|
|
2238
|
+
event.stopPropagation();
|
|
2239
|
+
}
|
|
2240
|
+
function moveTimelineScroll(event) {
|
|
2241
|
+
if (!timelineScrollDrag || event.pointerId !== timelineScrollDrag.pointerId) return;
|
|
2242
|
+
setTimelineScrollFromPointer(event.clientY);
|
|
2243
|
+
event.preventDefault();
|
|
2244
|
+
}
|
|
2245
|
+
function endTimelineScroll(event) {
|
|
2246
|
+
if (!timelineScrollDrag || event.pointerId !== timelineScrollDrag.pointerId) return;
|
|
2247
|
+
timelineScrollDrag = null;
|
|
2248
|
+
try { timelineScrollbar.releasePointerCapture(event.pointerId); } catch {}
|
|
2249
|
+
}
|
|
2250
|
+
function setTimelineScrollFromPointer(clientY) {
|
|
2251
|
+
if (!tabsEl || !timelineScrollbar || !timelineThumb || !timelineScrollDrag) return;
|
|
2252
|
+
const trackRect = timelineScrollbar.getBoundingClientRect();
|
|
2253
|
+
const thumbHeight = timelineThumb.getBoundingClientRect().height;
|
|
2254
|
+
const travel = Math.max(0, trackRect.height - thumbHeight);
|
|
2255
|
+
const maxScroll = Math.max(0, tabsEl.scrollHeight - tabsEl.clientHeight);
|
|
2256
|
+
const offset = clamp(clientY - trackRect.top - timelineScrollDrag.grabOffset, 0, travel);
|
|
2257
|
+
tabsEl.scrollTop = travel > 0 ? maxScroll * offset / travel : 0;
|
|
1888
2258
|
}
|
|
1889
2259
|
// Health for a listed session, joined from the scored views the monitor already computes.
|
|
1890
2260
|
// Sessions Echo isn't actively scoring get no dot — honest absence, not a fake green.
|
|
@@ -1892,16 +2262,13 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1892
2262
|
const views = ((state && state.sessions) || []).concat((state && state.scores) || []);
|
|
1893
2263
|
const m = views.filter((v) => v && v.sourcePath === s.sourcePath && v.client === s.client && typeof v.usefulPct === "number")[0];
|
|
1894
2264
|
if (!m) return null;
|
|
1895
|
-
return { color: m.color || "green", health:
|
|
2265
|
+
return { color: m.color || "green", health: usefulSignalPct(m) };
|
|
1896
2266
|
}
|
|
1897
|
-
function
|
|
1898
|
-
|
|
1899
|
-
const family = agentFamily(activeClient);
|
|
1900
|
-
if (family !== "codex" && family !== "claude") return list;
|
|
1901
|
-
return list.filter((s) => agentFamily(s.client) === family);
|
|
2267
|
+
function allRecentSessions(state) {
|
|
2268
|
+
return (state && state.recentSessions) || [];
|
|
1902
2269
|
}
|
|
1903
|
-
function activeIdFor(state, src
|
|
1904
|
-
const list =
|
|
2270
|
+
function activeIdFor(state, src) {
|
|
2271
|
+
const list = allRecentSessions(state);
|
|
1905
2272
|
const match = list.filter((s) => s.sourcePath === src)[0];
|
|
1906
2273
|
return match ? match.id : null;
|
|
1907
2274
|
}
|
|
@@ -1927,14 +2294,6 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1927
2294
|
const upd = activityEpoch === null ? "" : relTime(Date.now() - activityEpoch);
|
|
1928
2295
|
metaLineEl.textContent = [upd, metaTail].filter(Boolean).join(" · ");
|
|
1929
2296
|
}
|
|
1930
|
-
// One plain sentence saying what the score means for THIS session. Direct, no hedging,
|
|
1931
|
-
// and no second number — the score above is the only number on the card.
|
|
1932
|
-
function verdictFor(color, oldPct) {
|
|
1933
|
-
const usefulPct = 100 - clamp(oldPct || 0, 0, 100);
|
|
1934
|
-
if (color === "red") return usefulPct <= 20 ? "Mostly junk — carry over now." : "Too much junk – consider to carry over.";
|
|
1935
|
-
if (color === "amber") return "Junk is piling up.";
|
|
1936
|
-
return "Context is clean.";
|
|
1937
|
-
}
|
|
1938
2297
|
function relTime(ms) {
|
|
1939
2298
|
const s = Math.max(0, Math.round(ms / 1000));
|
|
1940
2299
|
if (s < 5) return "now";
|
|
@@ -1959,6 +2318,10 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1959
2318
|
return String(Math.round(n));
|
|
1960
2319
|
}
|
|
1961
2320
|
|
|
2321
|
+
function fmtExact(tokens) {
|
|
2322
|
+
return Math.max(0, Math.round(Number(tokens) || 0)).toLocaleString("en-US");
|
|
2323
|
+
}
|
|
2324
|
+
|
|
1962
2325
|
function clamp(value, min, max) {
|
|
1963
2326
|
return Math.max(min, Math.min(max, Number(value) || 0));
|
|
1964
2327
|
}
|