@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/electron-main.js +7 -2
- package/dist/hud/monitor.js +164 -50
- package/dist/hud/server.js +79 -3
- package/dist/hud/session-launcher.js +395 -0
- package/dist/hud/web.js +336 -164
- package/dist/index.js +4 -1
- package/dist/migrate.js +19 -7
- package/dist/setup-page/client-extraction.js +16 -4
- package/dist/setup.js +83 -1
- package/dist/v1-contract.js +3 -2
- 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); }
|
|
@@ -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">↗</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="
|
|
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
|
|
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
|
|
872
|
-
//
|
|
873
|
-
//
|
|
945
|
+
// Demo-clean mode: hide only the research-y session VIEWER (raw log / payload). Keep the unified
|
|
946
|
+
// session feed — it 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, .
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
|
|
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
|
|
1150
|
+
function toggleSessionDetails(id) {
|
|
1088
1151
|
if (!id) return;
|
|
1089
|
-
|
|
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 &&
|
|
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: (
|
|
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
|
|
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:
|
|
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
|
-
|
|
1653
|
+
expanded: previewExpanded ? expandedSample : null,
|
|
1654
|
+
expandedId: previewExpanded ? "p2" : null,
|
|
1560
1655
|
sessions: [
|
|
1561
1656
|
sample,
|
|
1562
|
-
|
|
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: "
|
|
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
|
|
1605
|
-
renderTabs(state, activeId
|
|
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
|
|
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 =
|
|
1624
|
-
renderMini(color,
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 −</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
|
|
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
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
-
|
|
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)) + '">↗</span
|
|
1964
|
+
+ '<span class="tab-jump" data-jump="' + esc(s.id) + '" title="Open this conversation in ' + esc(label(s.client)) + '">↗</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…</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:
|
|
2074
|
+
return { color: m.color || "green", health: usefulSignalPct(m) };
|
|
1896
2075
|
}
|
|
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);
|
|
2076
|
+
function allRecentSessions(state) {
|
|
2077
|
+
return (state && state.recentSessions) || [];
|
|
1902
2078
|
}
|
|
1903
|
-
function activeIdFor(state, src
|
|
1904
|
-
const list =
|
|
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
|
}
|