@echomem/mcp 1.4.16 → 1.4.18

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/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 15px;
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
- .verdict {
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
- .tabs::-webkit-scrollbar { width: 4px; }
344
- .tabs::-webkit-scrollbar-track { background: transparent; }
345
- .tabs::-webkit-scrollbar-thumb { border-radius: 999px; background: rgba(26, 58, 143, .2); }
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); }
@@ -707,6 +782,7 @@ export const HUD_HTML = String.raw `<!doctype html>
707
782
  }
708
783
  .path { margin-top: 8px; }
709
784
  .missing { margin-top: 6px; }
785
+ .path:empty, .missing:empty { display: none; }
710
786
  #hud.s-green .mascot-stage { filter: drop-shadow(0 2px 6px rgba(67, 209, 122, 0.3)); }
711
787
  #hud.s-amber .mascot-stage { filter: drop-shadow(0 2px 7px rgba(233, 185, 73, 0.42)); }
712
788
  #hud.s-red .mascot-stage { filter: drop-shadow(0 3px 8px rgba(199, 55, 47, 0.48)); }
@@ -761,13 +837,13 @@ export const HUD_HTML = String.raw `<!doctype html>
761
837
  </div>
762
838
  </div>
763
839
  <div class="details">
840
+ <div class="timeline-scrollbar hidden" id="timelineScrollbar" aria-hidden="true"><span class="timeline-thumb" id="timelineThumb"></span></div>
764
841
  <div class="now-card" id="nowcard">
765
842
  <div class="now-sess">
766
843
  <img class="now-icon" id="nowicon" src="/assets/hud/codex.svg" alt="" />
767
844
  <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
845
  </div>
769
846
  <div class="session-actions">
770
- <button class="pin-chip" id="pinchip" type="button" title="Return to auto-follow">pinned x</button>
771
847
  <button class="jump-chip" id="jumpchip" type="button" title="Open this conversation">&#8599;</button>
772
848
  </div>
773
849
  <div class="billing-banner" id="billingbanner">
@@ -783,7 +859,7 @@ export const HUD_HTML = String.raw `<!doctype html>
783
859
  <span class="state-word" id="stateWord">--%</span>
784
860
  <button class="why-btn" id="whybtn" type="button" title="Details" aria-label="Details" style="display:none">+</button>
785
861
  </div>
786
- <div class="verdict" id="verdict"></div>
862
+ <div class="signal-meta"><span class="meta-line" id="metaline"></span></div>
787
863
  <div class="evidence" id="evidence"></div>
788
864
  <div class="nudge" id="nudge"></div>
789
865
  <button class="renew-btn" id="renewbtn"><b>Start new session with context</b><small>carry 100% signal, drop the junk</small></button>
@@ -802,7 +878,6 @@ export const HUD_HTML = String.raw `<!doctype html>
802
878
  <div class="diagnostics-body" id="diagnosticsbody"><span class="diagnostics-v">Open this panel to check account status.</span></div>
803
879
  </section>
804
880
  <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
881
  <div class="path" id="path"></div>
807
882
  <button class="reveal" id="reveal" style="display:none">↗ Open session in viewer</button>
808
883
  <div class="missing" id="missing"></div>
@@ -816,12 +891,10 @@ export const HUD_HTML = String.raw `<!doctype html>
816
891
  const clientRepo = document.getElementById("clientrepo");
817
892
  const stateDot = document.getElementById("stateDot");
818
893
  const stateWordEl = document.getElementById("stateWord");
819
- const verdictEl = document.getElementById("verdict");
820
894
  const nowIcon = document.getElementById("nowicon");
821
895
  const nowTitle = document.getElementById("nowtitle");
822
896
  const nowRepo = document.getElementById("nowrepo");
823
897
  const nowRepoLine = document.getElementById("nowrepoline");
824
- const pinChip = document.getElementById("pinchip");
825
898
  const jumpChip = document.getElementById("jumpchip");
826
899
  const whyBtn = document.getElementById("whybtn");
827
900
  const evidence = document.getElementById("evidence");
@@ -842,7 +915,8 @@ export const HUD_HTML = String.raw `<!doctype html>
842
915
  const repoLine = document.getElementById("repoline");
843
916
  const pathEl = document.getElementById("path");
844
917
  const tabsEl = document.getElementById("tabs");
845
- const switchBtn = document.getElementById("switchbtn");
918
+ const timelineScrollbar = document.getElementById("timelineScrollbar");
919
+ const timelineThumb = document.getElementById("timelineThumb");
846
920
  const revealEl = document.getElementById("reveal");
847
921
  const missing = document.getElementById("missing");
848
922
  const diagnosticsBody = document.getElementById("diagnosticsbody");
@@ -868,9 +942,9 @@ export const HUD_HTML = String.raw `<!doctype html>
868
942
  const CHECKPOINT_STATUS_CACHE_LIMIT = 48;
869
943
  const CHECKPOINT_STATUS_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
870
944
  const statusFetchInFlight = {};
871
- // Demo-clean mode: hide only the research-y session VIEWER (raw log / payload). Keep the session
872
- // tabs + agent-switch they're the manual override for "we can't detect which session you're
873
- // viewing", so the user can always point the HUD at the right session. Flip false to show the viewer.
945
+ // Demo-clean mode: hide only the research-y session VIEWER (raw log / payload). Keep the unified
946
+ // session feedit is the manual override for "we can't detect which session you're viewing",
947
+ // so the user can always point the HUD at the right session. Flip false to show the viewer.
874
948
  const DEMO_CLEAN = true;
875
949
 
876
950
  function diagnosticsLine(label, value, bad, wrap) {
@@ -964,6 +1038,7 @@ export const HUD_HTML = String.raw `<!doctype html>
964
1038
  const mrepoLine = document.getElementById("mrepoline");
965
1039
  let miniOn = false;
966
1040
  let whyOpen = false;
1041
+ let timelineScrollDrag = null;
967
1042
  try { miniOn = localStorage.getItem("echomemHudMini") === "1"; } catch {}
968
1043
  if (miniOn) hud.classList.add("mini");
969
1044
  function applyMini(next) {
@@ -983,7 +1058,7 @@ export const HUD_HTML = String.raw `<!doctype html>
983
1058
  });
984
1059
  let hudDrag = null;
985
1060
  let suppressNextToggle = false;
986
- const dragBlockSelector = "button, input, textarea, select, a, pre, .tab, .switchbtn, .reveal, .renew-btn, .copy-btn";
1061
+ const dragBlockSelector = "button, input, textarea, select, a, pre, .tab, .timeline-scrollbar, .reveal, .renew-btn, .copy-btn";
987
1062
 
988
1063
  hud.addEventListener("pointerdown", beginHudDrag);
989
1064
  window.addEventListener("pointermove", moveHudDrag);
@@ -1032,22 +1107,8 @@ export const HUD_HTML = String.raw `<!doctype html>
1032
1107
  fetch("/open-viewer?path=" + encodeURIComponent(currentSrc)).catch(() => {});
1033
1108
  });
1034
1109
 
1035
- // Jump to the other agent family while the tabs stay scoped to the family you're viewing.
1036
- if (switchBtn) switchBtn.addEventListener("click", (e) => {
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.
1110
+ // Click a history row expand it where it already sits. The newest conversation remains in
1111
+ // the top card; the ↗ corner opens the historical conversation in its own app.
1051
1112
  if (tabsEl) tabsEl.addEventListener("click", (e) => {
1052
1113
  const jump = e.target && e.target.closest ? e.target.closest("[data-jump]") : null;
1053
1114
  if (jump) {
@@ -1057,8 +1118,14 @@ export const HUD_HTML = String.raw `<!doctype html>
1057
1118
  }
1058
1119
  const btn = e.target && e.target.closest ? e.target.closest(".tab") : null;
1059
1120
  if (!btn) return;
1060
- selectSession(btn.getAttribute("data-id"));
1121
+ toggleSessionDetails(btn.getAttribute("data-id"));
1061
1122
  });
1123
+ if (tabsEl) tabsEl.addEventListener("scroll", syncTabsOverflowCue, { passive: true });
1124
+ if (timelineScrollbar) timelineScrollbar.addEventListener("pointerdown", beginTimelineScroll);
1125
+ window.addEventListener("pointermove", moveTimelineScroll);
1126
+ window.addEventListener("pointerup", endTimelineScroll);
1127
+ window.addEventListener("pointercancel", endTimelineScroll);
1128
+ window.addEventListener("resize", syncTabsOverflowCue);
1062
1129
  if (jumpChip) jumpChip.addEventListener("click", (e) => {
1063
1130
  e.stopPropagation();
1064
1131
  jumpToSession(jumpChip.getAttribute("data-jump"));
@@ -1067,10 +1134,6 @@ export const HUD_HTML = String.raw `<!doctype html>
1067
1134
  if (!id) return;
1068
1135
  fetch("/jump?id=" + encodeURIComponent(id)).catch(() => {});
1069
1136
  }
1070
- if (pinChip) pinChip.addEventListener("click", (e) => {
1071
- e.stopPropagation();
1072
- selectSession("auto");
1073
- });
1074
1137
  if (billingLink) billingLink.addEventListener("click", (e) => {
1075
1138
  e.stopPropagation();
1076
1139
  const url = billingStatus && billingStatus.pricingUrl;
@@ -1084,9 +1147,10 @@ export const HUD_HTML = String.raw `<!doctype html>
1084
1147
  try { window.open(url, "_blank", "noopener"); } catch {}
1085
1148
  });
1086
1149
  });
1087
- function selectSession(id) {
1150
+ function toggleSessionDetails(id) {
1088
1151
  if (!id) return;
1089
- fetch("/select?id=" + encodeURIComponent(id))
1152
+ const nextId = lastState && lastState.expandedId === id ? "auto" : id;
1153
+ fetch("/expand?id=" + encodeURIComponent(nextId))
1090
1154
  .then(() => fetch("/state"))
1091
1155
  .then((r) => r.json())
1092
1156
  .then(render)
@@ -1100,7 +1164,7 @@ export const HUD_HTML = String.raw `<!doctype html>
1100
1164
  event.stopPropagation();
1101
1165
  return;
1102
1166
  }
1103
- if (event.target instanceof HTMLElement && (event.target.closest(".switchbtn") || event.target.closest(".cornerbtn"))) return;
1167
+ if (event.target instanceof HTMLElement && event.target.closest(".cornerbtn")) return;
1104
1168
  hud.classList.toggle("open");
1105
1169
  if (hud.classList.contains("open")) maybeLoadDiagnostics();
1106
1170
  syncHeight();
@@ -1160,18 +1224,31 @@ export const HUD_HTML = String.raw `<!doctype html>
1160
1224
  saveCachedCheckpoint(renewSessionKey, prompt, promptKind, meta);
1161
1225
  if (promptKind === "cloud") saveCheckpointStatus(renewSessionKey, statusFromRenewData(renewData, meta));
1162
1226
  const doneSummary = checkpointSuffix(checkpointSummary({ renewedAt: Date.now(), memoryCount: meta.sessionMemoryCount }));
1227
+ let launchData = null;
1228
+ try {
1229
+ const launchRes = await fetch("/launch", {
1230
+ method: "POST",
1231
+ headers: { "Content-Type": "application/json" },
1232
+ body: JSON.stringify({ prompt, sourcePath: String((scoreAtRenew && scoreAtRenew.sourcePath) || "") }),
1233
+ });
1234
+ launchData = await launchRes.json();
1235
+ } catch {}
1236
+ const opened = Boolean(launchData && launchData.ok);
1237
+ let fallbackCopied = false;
1238
+ if (!opened) {
1239
+ try {
1240
+ await navigator.clipboard.writeText(prompt);
1241
+ fallbackCopied = true;
1242
+ } catch {}
1243
+ }
1244
+ const readyLabel = promptKind === "local" ? "Local capsule" : "Decision log";
1245
+ const openedLabel = launchData && launchData.mode === "claude-code" ? "Carryover sent in a fresh Claude Code session" : "Carryover sent in a fresh Codex session";
1163
1246
  showRenewResult({
1164
1247
  sessionKey: renewSessionKey,
1165
1248
  prompt,
1166
- message: (promptKind === "local" ? "Local capsule ready" : "Decision log ready") + doneSummary,
1249
+ message: (opened ? openedLabel : readyLabel + (fallbackCopied ? " copied" : " ready")) + doneSummary,
1167
1250
  canCopy: true,
1168
1251
  });
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
1252
  } catch {
1176
1253
  } finally {
1177
1254
  stopRenewTimer();
@@ -1534,13 +1611,13 @@ export const HUD_HTML = String.raw `<!doctype html>
1534
1611
  // skips the live feed — same pattern as the extraction page's ?preview hook.
1535
1612
  const previewParams = new URLSearchParams(location.search);
1536
1613
  const previewColor = previewParams.get("preview");
1537
- const previewPinned = previewParams.get("pinned") === "1";
1614
+ const previewExpanded = previewParams.get("expanded") === "1" || previewParams.get("pinned") === "1";
1538
1615
  if (previewParams.get("capture") === "1") hud.classList.add("capture-preview");
1539
1616
  if (previewColor === "green" || previewColor === "amber" || previewColor === "red") {
1540
1617
  const sample = {
1541
1618
  client: "codex",
1542
1619
  label: "EchoMem-Chrome",
1543
- sourcePath: previewPinned ? "/preview/b.jsonl" : "/preview/session.jsonl",
1620
+ sourcePath: "/preview/session.jsonl",
1544
1621
  usefulPct: previewColor === "red" ? 55 : previewColor === "amber" ? 75 : 95,
1545
1622
  healthScorePct: previewColor === "red" ? 48 : previewColor === "amber" ? 72 : 96,
1546
1623
  color: previewColor,
@@ -1554,19 +1631,37 @@ export const HUD_HTML = String.raw `<!doctype html>
1554
1631
  lastActiveMs: 6000,
1555
1632
  ctSource: "measured",
1556
1633
  };
1634
+ const expandedSample = {
1635
+ client: "codex",
1636
+ label: "EchoMem-Chrome",
1637
+ sourcePath: "/preview/b.jsonl",
1638
+ usefulPct: 71,
1639
+ healthScorePct: 71,
1640
+ color: "amber",
1641
+ ctTokens: 193000,
1642
+ modelContextWindow: 258000,
1643
+ pollutionPct: 29,
1644
+ pollutionTok: 55970,
1645
+ buckets: { range_redundant: { count: 12 } },
1646
+ stats: { patchEdits: 22 },
1647
+ turn: 3,
1648
+ lastActiveMs: 480000,
1649
+ ctSource: "measured",
1650
+ };
1557
1651
  render({
1558
1652
  active: sample,
1559
- pinnedId: previewPinned ? "p2" : null,
1653
+ expanded: previewExpanded ? expandedSample : null,
1654
+ expandedId: previewExpanded ? "p2" : null,
1560
1655
  sessions: [
1561
1656
  sample,
1562
- { client: "codex", sourcePath: "/preview/b.jsonl", usefulPct: 71, healthScorePct: 71, color: "amber" },
1657
+ expandedSample,
1563
1658
  { client: "codex", sourcePath: "/preview/c.jsonl", usefulPct: 94, healthScorePct: 94, color: "green" },
1564
1659
  ],
1565
1660
  recentSessions: [
1566
1661
  { id: "p1", client: "codex", title: "Simplify HUD session display", label: "EchoMem-Chrome", sourcePath: "/preview/session.jsonl", live: true, lastActiveMs: 4000 },
1567
1662
  { id: "p2", client: "codex", title: "修复 HUD 切换和图标", label: "EchoMem-Chrome", sourcePath: "/preview/b.jsonl", live: false, lastActiveMs: 480000 },
1568
1663
  { id: "p3", client: "codex", title: "Fix extraction preview", label: "EchoMem-Chrome", sourcePath: "/preview/c.jsonl", live: false, lastActiveMs: 2400000 },
1569
- { id: "p4", client: "codex", title: "Port Claude work", label: "EchoForPeople", sourcePath: "/preview/d.jsonl", live: false, lastActiveMs: 9000000 },
1664
+ { id: "p4", client: "claude-code", title: "Port Claude work", label: "EchoForPeople", sourcePath: "/preview/d.jsonl", live: false, lastActiveMs: 9000000 },
1570
1665
  { id: "p5", client: "codex", title: "Stamp fixes with Echo mascot", label: "EchoMem-Chrome", sourcePath: "/preview/e.jsonl", live: false, lastActiveMs: 16200000 },
1571
1666
  { id: "p6", client: "codex", title: "Review migration checks", label: "EchoForPeople", sourcePath: "/preview/f.jsonl", live: false, lastActiveMs: 21600000 },
1572
1667
  { id: "p7", client: "codex", title: "Polish setup story", label: "EchoMem-Chrome", sourcePath: "/preview/g.jsonl", live: false, lastActiveMs: 34200000 },
@@ -1601,9 +1696,8 @@ export const HUD_HTML = String.raw `<!doctype html>
1601
1696
  const currentSessionKey = sessionKeyFor(state, score);
1602
1697
  const viewColor = score && typeof score.usefulPct === "number" ? (score.color || "green") : "idle";
1603
1698
  if (renewResultSessionKey && renewResultSessionKey !== currentSessionKey) clearRenewResult();
1604
- const activeId = score && score.sourcePath ? activeIdFor(state, score.sourcePath, activeClient) : null;
1605
- renderTabs(state, activeId, activeClient);
1606
- renderSwitchButton(state, activeClient);
1699
+ const activeId = score && score.sourcePath ? activeIdFor(state, score.sourcePath) : null;
1700
+ renderTabs(state, activeId);
1607
1701
  if (!score || typeof score.usefulPct !== "number") {
1608
1702
  renderIdle(state);
1609
1703
  return;
@@ -1612,7 +1706,7 @@ export const HUD_HTML = String.raw `<!doctype html>
1612
1706
  const color = score.color || "green";
1613
1707
  const sat = fullnessPct(score);
1614
1708
  const oldPct = clamp(score.pollutionPct || 0, 0, 95);
1615
- const health = healthScore(score);
1709
+ const signal = usefulSignalPct(score);
1616
1710
  const repeatCount = score.buckets && score.buckets.range_redundant ? score.buckets.range_redundant.count || 0 : 0;
1617
1711
  const info = activeSessionInfo(score, state);
1618
1712
 
@@ -1620,8 +1714,8 @@ export const HUD_HTML = String.raw `<!doctype html>
1620
1714
  stateDot.className = "state-dot " + color;
1621
1715
  hud.classList.remove("s-green", "s-amber", "s-red");
1622
1716
  hud.classList.add("s-" + color);
1623
- main.innerHTML = health + '%<span class="main-rest"> of tokens are useful</span>';
1624
- renderMini(color, health + "%", info);
1717
+ main.innerHTML = signal + '%<span class="main-rest"> useful</span>';
1718
+ renderMini(color, signal + "%", info);
1625
1719
  client.textContent = info.title || info.repo;
1626
1720
  if (bubbleAgent) {
1627
1721
  bubbleAgent.src = iconForFamily(agentFamily(score.client) || "codex");
@@ -1637,18 +1731,16 @@ export const HUD_HTML = String.raw `<!doctype html>
1637
1731
  if (score.ctTokens > 0) metaParts.push(fmt(score.ctTokens) + " tok");
1638
1732
  metaTail = metaParts.join(" · ");
1639
1733
  renderMetaLine();
1640
- stateWordEl.textContent = health + "%";
1641
- if (verdictEl) verdictEl.textContent = verdictFor(color, oldPct);
1734
+ stateWordEl.innerHTML = '<span class="snr-prefix">SNR</span>' + signal + '%<span class="signal-label">useful</span>';
1642
1735
  if (nowIcon) nowIcon.src = iconForFamily(agentFamily(score.client) || "codex");
1643
1736
  if (nowTitle) nowTitle.textContent = info.title;
1644
1737
  if (nowRepo) nowRepo.textContent = bubbleRepo;
1645
1738
  if (nowRepoLine) nowRepoLine.style.display = bubbleRepo ? "flex" : "none";
1646
- if (pinChip) pinChip.style.display = state && state.pinnedId ? "inline-block" : "none";
1647
1739
  if (jumpChip) {
1648
1740
  if (activeId) jumpChip.setAttribute("data-jump", activeId);
1649
1741
  jumpChip.style.display = activeId ? "inline-block" : "none";
1650
1742
  }
1651
- renderWhy(score, oldPct, repeatCount, sat, health, color);
1743
+ renderWhy(score, oldPct, repeatCount, sat, signal, color);
1652
1744
  renderBillingStatus();
1653
1745
  pathEl.textContent = "";
1654
1746
  currentSrc = score.sourcePath || null;
@@ -1699,11 +1791,9 @@ export const HUD_HTML = String.raw `<!doctype html>
1699
1791
  currentSrc = null;
1700
1792
  if (revealEl) revealEl.style.display = "none";
1701
1793
  stateWordEl.textContent = "on";
1702
- if (verdictEl) verdictEl.textContent = "Waiting for Codex or Claude.";
1703
1794
  if (nowTitle) nowTitle.textContent = "EchoMem MCP";
1704
1795
  if (nowRepo) nowRepo.textContent = "";
1705
1796
  if (nowRepoLine) nowRepoLine.style.display = "none";
1706
- if (pinChip) pinChip.style.display = "none";
1707
1797
  if (jumpChip) jumpChip.style.display = "none";
1708
1798
  evidence.textContent = "";
1709
1799
  if (whyBtn) whyBtn.style.display = "none";
@@ -1753,13 +1843,20 @@ export const HUD_HTML = String.raw `<!doctype html>
1753
1843
  if (billingCopy) billingCopy.textContent = detail;
1754
1844
  }
1755
1845
 
1756
- function renderWhy(score, oldPct, repeatCount, sat, health, color) {
1846
+ function renderWhy(score, oldPct, repeatCount, sat, signal, color) {
1757
1847
  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
1848
  for (const row of junkBreakdown(score)) rows.push(row);
1761
1849
  rows.push(["source", ctSourceLabel(score.ctSource)]);
1762
- evidence.innerHTML = 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("");
1850
+ const contextTokens = Math.max(0, Number(score.ctTokens) || 0);
1851
+ const junkTokens = Math.min(contextTokens, Math.max(0, Number(score.pollutionTok) || 0));
1852
+ const equation = contextTokens > 0
1853
+ ? '<div class="snr-equation" aria-label="One minus ' + fmtExact(junkTokens) + ' junk divided by ' + fmtExact(contextTokens) + ' in context equals ' + signal + ' percent SNR">' +
1854
+ '<span>1 &minus;</span>' +
1855
+ '<span class="snr-fraction"><span class="snr-numerator">' + fmtExact(junkTokens) + ' junk</span><span class="snr-denominator">' + fmtExact(contextTokens) + ' in context</span></span>' +
1856
+ '<span class="snr-result">= ' + signal + '% SNR</span>' +
1857
+ '</div>'
1858
+ : '';
1859
+ 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
1860
  const useful = color !== "green" || oldPct >= 10 || repeatCount > 0 || sat >= 80;
1764
1861
  if (whyBtn) {
1765
1862
  whyBtn.style.display = useful ? "inline-flex" : "none";
@@ -1794,9 +1891,10 @@ export const HUD_HTML = String.raw `<!doctype html>
1794
1891
  return 0;
1795
1892
  }
1796
1893
 
1797
- function healthScore(score) {
1798
- if (typeof score.healthScorePct === "number" && isFinite(score.healthScorePct)) return clamp(score.healthScorePct, 0, 100);
1894
+ function usefulSignalPct(score) {
1799
1895
  if (typeof score.usefulPct === "number" && isFinite(score.usefulPct)) return clamp(score.usefulPct, 0, 100);
1896
+ if (typeof score.pollutionPct === "number" && isFinite(score.pollutionPct)) return clamp(100 - score.pollutionPct, 0, 100);
1897
+ if (typeof score.healthScorePct === "number" && isFinite(score.healthScorePct)) return clamp(score.healthScorePct, 0, 100);
1800
1898
  return 0;
1801
1899
  }
1802
1900
 
@@ -1829,44 +1927,23 @@ export const HUD_HTML = String.raw `<!doctype html>
1829
1927
  return title || info.client;
1830
1928
  }
1831
1929
 
1832
- function renderSwitchButton(state, activeClient) {
1833
- if (!switchBtn) return;
1834
- const target = switchTarget(state, activeClient);
1835
- if (!target) {
1836
- switchBtn.style.display = "none";
1837
- switchBtn.removeAttribute("data-target-family");
1838
- switchBtn.removeAttribute("data-target-id");
1839
- switchBtn.innerHTML = "";
1930
+ // History rows: recent sessions that are NOT the newest one in the top card. Clicking a row
1931
+ // expands it in place without changing this newest-first sequence. Codex and Claude share one
1932
+ // timeline; their icons preserve source identity.
1933
+ function renderTabs(state, activeId) {
1934
+ if (!tabsEl) return;
1935
+ const anchor = captureTimelineAnchor();
1936
+ const list = allRecentSessions(state).filter((s) => s.id !== activeId);
1937
+ if (!list.length) {
1938
+ tabsEl.innerHTML = "";
1939
+ tabsEl.scrollTop = 0;
1940
+ tabsEl.classList.remove("has-more-above", "has-more-below");
1941
+ if (timelineScrollbar) timelineScrollbar.classList.add("hidden");
1840
1942
  return;
1841
1943
  }
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
1944
  const chips = list.map((s) => {
1945
+ const expanded = Boolean(state && state.expandedId === s.id);
1946
+ const expandedView = expanded ? expandedSessionFor(state, s) : null;
1870
1947
  const activity = s.live ? '<span class="spin"></span>' : "";
1871
1948
  const hv = sessionHealthFor(state, s);
1872
1949
  const status = hv
@@ -1879,12 +1956,114 @@ export const HUD_HTML = String.raw `<!doctype html>
1879
1956
  ? '<span class="tab-repo"><img class="tab-gh" src="/assets/hud/github.svg" alt="" /><span class="tab-reponame">' + esc(repo) + "</span></span>"
1880
1957
  : '<span class="tab-repo"><span class="tab-reponame">' + esc(label(s.client)) + "</span></span>";
1881
1958
  const time = typeof s.lastActiveMs === "number" ? '<span class="tab-time" title="Last active here">' + esc(relTime(s.lastActiveMs)) + "</span>" : "";
1882
- return '<button class="tab" data-id="' + esc(s.id) + '" title="' + esc("Switch to · " + label(s.client) + " · " + title + (repo ? " · " + repo : "")) + '">'
1959
+ const detail = expanded ? expandedSessionDetail(expandedView, s) : "";
1960
+ const action = expanded ? "Hide details" : "Show details";
1961
+ 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
1962
  + '<img class="tab-icon" src="' + iconForFamily(family) + '" alt="" />'
1884
1963
  + '<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)) + '">&#8599;</span></button>';
1964
+ + '<span class="tab-jump" data-jump="' + esc(s.id) + '" title="Open this conversation in ' + esc(label(s.client)) + '">&#8599;</span>'
1965
+ + detail + "</button>";
1886
1966
  });
1887
1967
  tabsEl.innerHTML = chips.join("");
1968
+ restoreTimelineAnchor(anchor);
1969
+ }
1970
+ function expandedSessionFor(state, session) {
1971
+ const view = state && state.expanded;
1972
+ return view && view.sourcePath === session.sourcePath && view.client === session.client ? view : null;
1973
+ }
1974
+ function expandedSessionDetail(view, session) {
1975
+ if (!view) return '<span class="tab-detail"><span class="tab-detail-score">Loading SNR&hellip;</span></span>';
1976
+ const meta = [];
1977
+ if (typeof session.lastActiveMs === "number") meta.push(relTime(session.lastActiveMs));
1978
+ if (typeof view.turn === "number" && view.turn > 0) meta.push("turn " + view.turn);
1979
+ if (Number(view.ctTokens) > 0) meta.push(fmt(view.ctTokens) + " tok");
1980
+ return '<span class="tab-detail"><span class="tab-detail-score">SNR ' + usefulSignalPct(view) + '% useful</span>'
1981
+ + '<span class="tab-detail-meta">' + esc(meta.join(" · ")) + "</span></span>";
1982
+ }
1983
+ function captureTimelineAnchor() {
1984
+ if (!tabsEl || !tabsEl.children.length) return null;
1985
+ const viewportTop = tabsEl.getBoundingClientRect().top;
1986
+ const rows = Array.from(tabsEl.querySelectorAll(".tab"));
1987
+ const row = rows.find((item) => item.getBoundingClientRect().bottom > viewportTop + 1);
1988
+ if (!row) return { id: "", offset: 0, scrollTop: tabsEl.scrollTop };
1989
+ return {
1990
+ id: row.getAttribute("data-id") || "",
1991
+ offset: row.getBoundingClientRect().top - viewportTop,
1992
+ scrollTop: tabsEl.scrollTop,
1993
+ };
1994
+ }
1995
+ function restoreTimelineAnchor(anchor) {
1996
+ if (!tabsEl) return;
1997
+ const maxScroll = Math.max(0, tabsEl.scrollHeight - tabsEl.clientHeight);
1998
+ if (!anchor) {
1999
+ tabsEl.scrollTop = Math.min(tabsEl.scrollTop, maxScroll);
2000
+ requestAnimationFrame(syncTabsOverflowCue);
2001
+ return;
2002
+ }
2003
+ const row = Array.from(tabsEl.querySelectorAll(".tab")).find((item) => item.getAttribute("data-id") === anchor.id);
2004
+ if (row) {
2005
+ const nextOffset = row.getBoundingClientRect().top - tabsEl.getBoundingClientRect().top;
2006
+ tabsEl.scrollTop = clamp(tabsEl.scrollTop + nextOffset - anchor.offset, 0, maxScroll);
2007
+ } else {
2008
+ tabsEl.scrollTop = clamp(anchor.scrollTop, 0, maxScroll);
2009
+ }
2010
+ requestAnimationFrame(syncTabsOverflowCue);
2011
+ }
2012
+ function syncTabsOverflowCue() {
2013
+ if (!tabsEl) return;
2014
+ const remaining = tabsEl.scrollHeight - tabsEl.scrollTop - tabsEl.clientHeight;
2015
+ tabsEl.classList.toggle("has-more-above", tabsEl.scrollTop > 3);
2016
+ tabsEl.classList.toggle("has-more-below", remaining > 3);
2017
+ syncTimelineScrollbar();
2018
+ }
2019
+ function syncTimelineScrollbar() {
2020
+ if (!tabsEl || !timelineScrollbar || !timelineThumb) return;
2021
+ const maxScroll = Math.max(0, tabsEl.scrollHeight - tabsEl.clientHeight);
2022
+ const trackHeight = timelineScrollbar.clientHeight;
2023
+ const scrollable = maxScroll > 1 && trackHeight > 0;
2024
+ timelineScrollbar.classList.toggle("hidden", !scrollable);
2025
+ if (!scrollable) {
2026
+ timelineThumb.style.height = "100%";
2027
+ timelineThumb.style.transform = "translateY(0)";
2028
+ return;
2029
+ }
2030
+ const thumbHeight = Math.max(30, Math.round(trackHeight * tabsEl.clientHeight / tabsEl.scrollHeight));
2031
+ const travel = Math.max(0, trackHeight - thumbHeight);
2032
+ const offset = travel * tabsEl.scrollTop / maxScroll;
2033
+ timelineThumb.style.height = thumbHeight + "px";
2034
+ timelineThumb.style.transform = "translateY(" + Math.round(offset) + "px)";
2035
+ }
2036
+ function beginTimelineScroll(event) {
2037
+ if (!tabsEl || !timelineScrollbar || !timelineThumb || event.button !== 0) return;
2038
+ const thumbRect = timelineThumb.getBoundingClientRect();
2039
+ const onThumb = event.target === timelineThumb;
2040
+ timelineScrollDrag = {
2041
+ pointerId: event.pointerId,
2042
+ grabOffset: onThumb ? event.clientY - thumbRect.top : thumbRect.height / 2,
2043
+ };
2044
+ try { timelineScrollbar.setPointerCapture(event.pointerId); } catch {}
2045
+ if (!onThumb) setTimelineScrollFromPointer(event.clientY);
2046
+ event.preventDefault();
2047
+ event.stopPropagation();
2048
+ }
2049
+ function moveTimelineScroll(event) {
2050
+ if (!timelineScrollDrag || event.pointerId !== timelineScrollDrag.pointerId) return;
2051
+ setTimelineScrollFromPointer(event.clientY);
2052
+ event.preventDefault();
2053
+ }
2054
+ function endTimelineScroll(event) {
2055
+ if (!timelineScrollDrag || event.pointerId !== timelineScrollDrag.pointerId) return;
2056
+ timelineScrollDrag = null;
2057
+ try { timelineScrollbar.releasePointerCapture(event.pointerId); } catch {}
2058
+ }
2059
+ function setTimelineScrollFromPointer(clientY) {
2060
+ if (!tabsEl || !timelineScrollbar || !timelineThumb || !timelineScrollDrag) return;
2061
+ const trackRect = timelineScrollbar.getBoundingClientRect();
2062
+ const thumbHeight = timelineThumb.getBoundingClientRect().height;
2063
+ const travel = Math.max(0, trackRect.height - thumbHeight);
2064
+ const maxScroll = Math.max(0, tabsEl.scrollHeight - tabsEl.clientHeight);
2065
+ const offset = clamp(clientY - trackRect.top - timelineScrollDrag.grabOffset, 0, travel);
2066
+ tabsEl.scrollTop = travel > 0 ? maxScroll * offset / travel : 0;
1888
2067
  }
1889
2068
  // Health for a listed session, joined from the scored views the monitor already computes.
1890
2069
  // Sessions Echo isn't actively scoring get no dot — honest absence, not a fake green.
@@ -1892,16 +2071,13 @@ export const HUD_HTML = String.raw `<!doctype html>
1892
2071
  const views = ((state && state.sessions) || []).concat((state && state.scores) || []);
1893
2072
  const m = views.filter((v) => v && v.sourcePath === s.sourcePath && v.client === s.client && typeof v.usefulPct === "number")[0];
1894
2073
  if (!m) return null;
1895
- return { color: m.color || "green", health: healthScore(m) };
2074
+ return { color: m.color || "green", health: usefulSignalPct(m) };
1896
2075
  }
1897
- function visibleRecentSessions(state, activeClient) {
1898
- const list = (state && state.recentSessions) || [];
1899
- const family = agentFamily(activeClient);
1900
- if (family !== "codex" && family !== "claude") return list;
1901
- return list.filter((s) => agentFamily(s.client) === family);
2076
+ function allRecentSessions(state) {
2077
+ return (state && state.recentSessions) || [];
1902
2078
  }
1903
- function activeIdFor(state, src, activeClient) {
1904
- const list = visibleRecentSessions(state, activeClient);
2079
+ function activeIdFor(state, src) {
2080
+ const list = allRecentSessions(state);
1905
2081
  const match = list.filter((s) => s.sourcePath === src)[0];
1906
2082
  return match ? match.id : null;
1907
2083
  }
@@ -1927,14 +2103,6 @@ export const HUD_HTML = String.raw `<!doctype html>
1927
2103
  const upd = activityEpoch === null ? "" : relTime(Date.now() - activityEpoch);
1928
2104
  metaLineEl.textContent = [upd, metaTail].filter(Boolean).join(" · ");
1929
2105
  }
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
2106
  function relTime(ms) {
1939
2107
  const s = Math.max(0, Math.round(ms / 1000));
1940
2108
  if (s < 5) return "now";
@@ -1959,6 +2127,10 @@ export const HUD_HTML = String.raw `<!doctype html>
1959
2127
  return String(Math.round(n));
1960
2128
  }
1961
2129
 
2130
+ function fmtExact(tokens) {
2131
+ return Math.max(0, Math.round(Number(tokens) || 0)).toLocaleString("en-US");
2132
+ }
2133
+
1962
2134
  function clamp(value, min, max) {
1963
2135
  return Math.max(min, Math.min(max, Number(value) || 0));
1964
2136
  }