@echomem/mcp 1.4.9 → 1.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/assets/hud/github.svg +1 -0
  2. package/dist/city/README.md +9 -0
  3. package/dist/city/echo-ai-city-only.html +54 -93
  4. package/dist/context-analysis/claude-native-canonical.js +35 -12
  5. package/dist/context-metrics/calculate.js +2 -15
  6. package/dist/context-metrics/estimator.js +45 -0
  7. package/dist/context-metrics/ledger.js +507 -0
  8. package/dist/context-metrics/parse-claude.js +227 -0
  9. package/dist/context-metrics/parse-codex.js +276 -0
  10. package/dist/hud/adapters.js +69 -198
  11. package/dist/hud/electron-main.js +3 -2
  12. package/dist/hud/fs.js +14 -0
  13. package/dist/hud/metric.js +4 -98
  14. package/dist/hud/monitor.js +1 -12
  15. package/dist/hud/render.js +4 -3
  16. package/dist/hud/server.js +30 -0
  17. package/dist/hud/web.js +409 -186
  18. package/dist/index.js +1 -0
  19. package/dist/setup-page/client-core.js +475 -0
  20. package/dist/setup-page/client-extraction.js +550 -0
  21. package/dist/setup-page/client-lifecycle.js +116 -0
  22. package/dist/setup-page/client-report-audit.js +818 -0
  23. package/dist/setup-page/client-report-city.js +204 -0
  24. package/dist/setup-page/client-report.js +6 -0
  25. package/dist/setup-page/client.js +15 -0
  26. package/dist/setup-page/document.js +37 -0
  27. package/dist/setup-page/styles-city-report.js +880 -0
  28. package/dist/setup-page/styles-context-audit.js +470 -0
  29. package/dist/setup-page/styles-extraction.js +821 -0
  30. package/dist/setup-page/styles-foundation.js +231 -0
  31. package/dist/setup-page/styles.js +11 -0
  32. package/dist/setup-page.js +6 -5623
  33. package/dist/setup.js +24 -10
  34. package/package.json +4 -4
  35. package/dist/city/10-problems-report.html +0 -649
  36. package/dist/city/_live.html +0 -37
  37. package/dist/city/_serve.mjs +0 -45
  38. package/dist/city/card-data.json +0 -15
  39. package/dist/city/chaos-to-clarity-pencil.html +0 -582
  40. package/dist/city/city-data.json +0 -248
  41. package/dist/city/echo-ai-city-only.template.html +0 -2271
  42. package/dist/city/generate-echo-city-only.mjs +0 -112
  43. package/dist/city/pencil-pie-generator.html +0 -883
  44. package/dist/city/pencil-webgl-landscape.html +0 -1239
  45. package/dist/city/spatial-fan-story.html +0 -479
package/dist/hud/web.js CHANGED
@@ -114,6 +114,9 @@ export const HUD_HTML = String.raw `<!doctype html>
114
114
  filter: var(--shadow);
115
115
  transition: border-color 0.18s ease, background 0.18s ease, filter 0.18s ease;
116
116
  }
117
+ /* Open = the card is the voice. Echo doesn't say the same thing twice, so the speech
118
+ bubble steps aside while the panel is up and comes back when it closes. */
119
+ #hud.open .status-bubble { display: none; }
117
120
  .status-bubble::after {
118
121
  content: "";
119
122
  position: absolute;
@@ -169,6 +172,9 @@ export const HUD_HTML = String.raw `<!doctype html>
169
172
  line-height: 1.1;
170
173
  color: var(--ink);
171
174
  }
175
+ .main-rest { font-size: 10.5px; font-weight: 680; color: var(--muted); }
176
+ .client-line { display: flex; align-items: center; gap: 5px; min-width: 0; }
177
+ .line-icon { width: 11px; height: 11px; flex: 0 0 auto; object-fit: contain; opacity: .85; }
172
178
  .client {
173
179
  min-width: 0;
174
180
  overflow: hidden;
@@ -211,25 +217,94 @@ export const HUD_HTML = String.raw `<!doctype html>
211
217
  cursor: grab;
212
218
  }
213
219
  #hud.open .details { display: block; }
214
- .head {
220
+ /* The current session is the one object every number and action belongs to — one card. */
221
+ .now-card {
222
+ border: 1px solid rgba(26, 58, 143, 0.24);
223
+ border-radius: 12px;
224
+ padding: 12px 13px 13px;
225
+ background: #fff;
226
+ box-shadow: 0 1px 5px rgba(26, 58, 143, 0.07);
227
+ }
228
+ #hud.s-amber .now-card { border-color: rgba(233, 185, 73, 0.55); }
229
+ #hud.s-red .now-card { border-color: rgba(199, 55, 47, 0.45); }
230
+ .now-sess {
215
231
  display: flex;
232
+ align-items: center;
233
+ gap: 8px;
234
+ margin-bottom: 9px;
235
+ }
236
+ .now-icon { width: 18px; height: 18px; flex: 0 0 auto; object-fit: contain; }
237
+ .now-copy { min-width: 0; flex: 1; display: grid; gap: 1px; line-height: 1.15; }
238
+ .now-title, .now-repo { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
239
+ .now-title { font-size: 12px; font-weight: 800; color: var(--ink); }
240
+ .now-repo { font-size: 10px; font-weight: 600; color: var(--faint); }
241
+ .verdict {
242
+ overflow: hidden;
243
+ text-overflow: ellipsis;
244
+ white-space: nowrap;
245
+ font-size: 11.5px;
246
+ font-weight: 650;
247
+ color: var(--muted);
248
+ }
249
+ .verdict:empty { display: none; }
250
+ .meta-row {
251
+ display: flex;
252
+ align-items: center;
216
253
  justify-content: space-between;
217
- align-items: baseline;
218
- min-height: 15px;
219
- margin-bottom: 10px;
254
+ gap: 8px;
255
+ margin-top: 10px;
220
256
  }
221
- .turn, .upd {
257
+ .meta-line {
258
+ min-width: 0;
222
259
  overflow: hidden;
223
260
  text-overflow: ellipsis;
224
261
  white-space: nowrap;
225
- font-size: 11px;
262
+ font-family: "JetBrains Mono", ui-monospace, monospace;
263
+ font-size: 9.5px;
264
+ font-weight: 600;
265
+ letter-spacing: 0.02em;
266
+ color: var(--faint);
267
+ }
268
+ .session-actions {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 6px;
272
+ margin: 0 0 8px 26px;
273
+ }
274
+ .pin-chip, .jump-chip {
275
+ display: none;
276
+ align-items: center;
277
+ justify-content: center;
278
+ min-height: 22px;
279
+ padding: 3px 9px;
280
+ border: 1px solid rgba(26, 58, 143, .18);
281
+ border-radius: 999px;
282
+ background: rgba(255, 255, 255, .78);
226
283
  color: var(--faint);
284
+ cursor: pointer;
285
+ font-family: inherit;
286
+ font-size: 10px;
287
+ font-weight: 760;
288
+ letter-spacing: 0;
289
+ line-height: 1;
290
+ }
291
+ .pin-chip:hover, .jump-chip:hover {
292
+ border-color: rgba(26, 58, 143, .34);
293
+ background: #fff;
294
+ color: var(--blue);
295
+ }
296
+ .jump-chip {
297
+ width: 22px;
298
+ min-width: 22px;
299
+ padding: 0;
300
+ border-radius: 50%;
301
+ font-size: 12px;
227
302
  }
228
303
  .tabs {
229
304
  display: grid;
230
305
  gap: 6px;
231
306
  max-height: 259px;
232
- margin: 2px 0 12px;
307
+ margin: 10px 0 0;
233
308
  padding-right: 3px;
234
309
  overflow-y: auto;
235
310
  overscroll-behavior: contain;
@@ -238,24 +313,22 @@ export const HUD_HTML = String.raw `<!doctype html>
238
313
  .tabs::-webkit-scrollbar { width: 4px; }
239
314
  .tabs::-webkit-scrollbar-track { background: transparent; }
240
315
  .tabs::-webkit-scrollbar-thumb { border-radius: 999px; background: rgba(26, 58, 143, .2); }
316
+ /* History rows: everything that is NOT the current session. Quiet on purpose. */
241
317
  .tab {
242
- display: grid; grid-template-columns: 18px minmax(0, 1fr) 12px; align-items: center; column-gap: 8px;
243
- width: 100%; min-height: 36px; font-size: 11px; line-height: 1.15; padding: 6px 9px; border-radius: 8px;
244
- border: 1px solid rgba(0, 0, 0, .12); background: rgba(0, 0, 0, .04);
318
+ display: grid; grid-template-columns: 16px minmax(0, 1fr) auto auto auto; align-items: center; column-gap: 8px;
319
+ width: 100%; min-height: 30px; font-size: 10.5px; line-height: 1.15; padding: 5px 9px; border-radius: 8px;
320
+ border: 1px solid transparent; background: transparent;
245
321
  color: var(--faint); cursor: pointer; text-align: left; font-family: inherit;
246
322
  }
247
- .tab:hover { background: rgba(0, 0, 0, .08); }
248
- .tab.viewing {
249
- border-color: rgba(26, 58, 143, .72);
250
- background: rgba(26, 58, 143, .11);
251
- box-shadow: inset 4px 0 0 var(--blue), 0 1px 4px rgba(26, 58, 143, .12);
252
- color: #111;
253
- }
254
- .tab-icon { width: 16px; height: 16px; object-fit: contain; }
255
- .tab-copy { min-width: 0; display: grid; gap: 2px; }
256
- .tab-title, .tab-repo { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
257
- .tab-title { color: var(--ink); font-weight: 760; }
258
- .tab-repo { color: var(--faint); font-size: 10px; font-weight: 600; }
323
+ .tab:hover { border-color: rgba(0, 0, 0, .1); background: rgba(0, 0, 0, .05); }
324
+ .tab-icon { width: 14px; height: 14px; object-fit: contain; opacity: .75; }
325
+ .tab-copy { min-width: 0; display: grid; gap: 1px; }
326
+ .tab-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-weight: 680; }
327
+ .tab-repo { display: flex; align-items: center; gap: 4px; min-width: 0; color: var(--faint); font-size: 9.5px; font-weight: 600; }
328
+ .tab-reponame { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
329
+ .tab-gh { width: 10px; height: 10px; flex: 0 0 auto; object-fit: contain; opacity: .7; }
330
+ /* When you last worked here sits just left of the jump arrow. */
331
+ .tab-time { justify-self: end; font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 9px; font-weight: 600; color: var(--faint); white-space: nowrap; }
259
332
  .tab-status {
260
333
  width: 10px;
261
334
  height: 10px;
@@ -268,31 +341,24 @@ export const HUD_HTML = String.raw `<!doctype html>
268
341
  .tab-status.amber { background: var(--amber); }
269
342
  .tab-status.red { background: #ff5f57; box-shadow: 0 0 0 1px rgba(199, 55, 47, .28); }
270
343
  .tab-status.idle { background: var(--faint); }
271
- .tab.auto-tab {
272
- position: sticky;
273
- top: 0;
274
- z-index: 1;
275
- grid-template-columns: minmax(0, 1fr) auto;
276
- min-height: 28px;
277
- background: #f4f2ed;
278
- }
279
- .tab.auto-tab.mode-active {
280
- border-color: rgba(26, 58, 143, .28);
281
- background: #f8f8f5;
282
- }
283
- .auto-check {
284
- display: none;
285
- width: 17px;
286
- height: 17px;
344
+ .tab-health {
345
+ display: inline-flex;
287
346
  align-items: center;
288
- justify-content: center;
289
- border-radius: 50%;
290
- background: var(--blue);
291
- color: #fff;
292
- font-size: 10px;
293
- font-weight: 850;
347
+ gap: 5px;
348
+ justify-self: end;
349
+ font-family: "JetBrains Mono", ui-monospace, monospace;
350
+ font-size: 9.5px;
351
+ font-weight: 750;
352
+ color: var(--muted);
294
353
  }
295
- .auto-tab.mode-active .auto-check { display: inline-flex; }
354
+ .tab-health .tab-status { width: 8px; height: 8px; border-width: 1.5px; }
355
+ /* Jump straight to this conversation in its own app. */
356
+ .tab-jump {
357
+ display: inline-flex; align-items: center; justify-content: center;
358
+ width: 18px; height: 18px; justify-self: end; border-radius: 50%;
359
+ color: var(--faint); font-size: 11px; font-weight: 800;
360
+ }
361
+ .tab-jump:hover { background: rgba(26, 58, 143, 0.1); color: var(--blue); }
296
362
  .spin {
297
363
  display: inline-block; width: 8px; height: 8px; margin-left: 4px; vertical-align: -1px;
298
364
  border: 1.5px solid rgba(0, 0, 0, .2); border-top-color: #1a3a8f; border-radius: 50%;
@@ -301,26 +367,30 @@ export const HUD_HTML = String.raw `<!doctype html>
301
367
  @keyframes echospin { to { transform: rotate(360deg); } }
302
368
  /* Mini mode: a small always-on pill instead of the full mascot+bubble. Opening the panel
303
369
  temporarily shows the full layout; closing returns to the pill. */
370
+ /* One line, one reading order: dot · score · agent icon · session · repo icon · repo.
371
+ The session name gets the space; the repo shrinks first. */
304
372
  #miniPill {
305
- display: none; align-items: center; gap: 8px; width: calc(100% - 16px); min-height: 38px;
306
- margin: 7px 0 0 8px; padding: 5px 11px; cursor: pointer; user-select: none;
373
+ display: none; align-items: center; gap: 7px; width: calc(100% - 16px); min-height: 38px;
374
+ margin: 7px 0 0 8px; padding: 5px 13px; cursor: pointer; user-select: none;
307
375
  border: 1.6px solid rgba(26, 58, 143, 0.75); border-radius: 999px; background: #fdfaf2;
308
376
  filter: var(--shadow); color: var(--ink); letter-spacing: 0;
309
377
  }
310
378
  #miniPill .mdot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); flex: 0 0 auto; }
311
379
  #miniPill .mdot.amber { background: var(--amber); }
312
380
  #miniPill .mdot.idle { background: var(--green); }
313
- .micon { width: 16px; height: 16px; object-fit: contain; flex: 0 0 auto; }
381
+ .micon { width: 13px; height: 13px; object-fit: contain; flex: 0 0 auto; opacity: .85; }
314
382
  #mscore {
315
383
  flex: 0 0 auto;
316
384
  font-family: "JetBrains Mono", ui-monospace, monospace;
317
385
  font-size: 12px;
318
386
  font-weight: 850;
319
387
  }
320
- .mcopy { min-width: 0; display: grid; gap: 1px; line-height: 1.1; }
388
+ .mcopy { min-width: 0; flex: 1; display: grid; gap: 2px; line-height: 1.1; }
389
+ .mrow { display: flex; align-items: center; gap: 5px; min-width: 0; }
321
390
  #msession, #mrepo { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
322
391
  #msession { font-size: 11.5px; font-weight: 760; color: var(--ink); }
323
392
  #mrepo { font-size: 10px; font-weight: 650; color: var(--faint); }
393
+ .mrow .micon { width: 11px; height: 11px; }
324
394
  #hud.mini:not(.open) .glance { height: 52px; }
325
395
  #hud.mini:not(.open) .mascot-stage, #hud.mini:not(.open) .status-bubble, #hud.mini:not(.open) .drag { display: none; }
326
396
  #hud.mini:not(.open) #miniPill { display: inline-flex; }
@@ -331,7 +401,6 @@ export const HUD_HTML = String.raw `<!doctype html>
331
401
  cursor: pointer; font-family: inherit;
332
402
  }
333
403
  .cornerbtn:hover { background: #fff; color: #111; }
334
- .headctl { display: flex; align-items: center; gap: 8px; }
335
404
  .switchbtn {
336
405
  display: inline-flex; align-items: center; justify-content: center;
337
406
  width: 22px; height: 22px; border-radius: 50%; padding: 0; line-height: 1; font-size: 12px;
@@ -339,12 +408,12 @@ export const HUD_HTML = String.raw `<!doctype html>
339
408
  cursor: pointer; font-family: inherit;
340
409
  }
341
410
  .switchbtn:hover { background: rgba(0, 0, 0, .1); color: #111; }
411
+ /* Explicit family switch: a labeled pill, not a mystery icon. */
342
412
  .agent-switch {
343
- width: 28px;
344
- min-width: 28px;
345
- max-width: 28px;
346
- height: 28px;
347
- padding: 0;
413
+ width: auto;
414
+ height: 26px;
415
+ gap: 6px;
416
+ padding: 0 11px 0 8px;
348
417
  border-radius: 999px;
349
418
  background: rgba(255, 255, 255, .78);
350
419
  color: #1f1f1f;
@@ -352,12 +421,13 @@ export const HUD_HTML = String.raw `<!doctype html>
352
421
  font-weight: 760;
353
422
  letter-spacing: 0;
354
423
  white-space: nowrap;
424
+ flex: 0 0 auto;
355
425
  }
356
426
  .agent-switch:hover { background: #fff; border-color: rgba(26, 58, 143, .34); }
357
427
  .agent-switch[data-target-family="claude"] { border-color: rgba(217, 119, 87, .42); }
358
428
  .agent-switch[data-target-family="codex"] { border-color: rgba(57, 65, 255, .34); }
359
- .switch-icon { width: 16px; height: 16px; flex: 0 0 auto; object-fit: contain; }
360
- .switch-copy { display: none; }
429
+ .switch-icon { width: 14px; height: 14px; flex: 0 0 auto; object-fit: contain; }
430
+ .switch-copy { display: inline; }
361
431
  .reveal {
362
432
  margin-top: 8px; font-size: 10.5px; padding: 4px 11px; border-radius: 999px;
363
433
  border: 1px solid rgba(0, 0, 0, .12); background: rgba(0, 0, 0, .04); color: var(--faint);
@@ -368,7 +438,7 @@ export const HUD_HTML = String.raw `<!doctype html>
368
438
  display: flex;
369
439
  align-items: center;
370
440
  gap: 9px;
371
- margin-bottom: 9px;
441
+ margin-bottom: 4px;
372
442
  }
373
443
  .state-dot {
374
444
  width: 12px;
@@ -385,41 +455,28 @@ export const HUD_HTML = String.raw `<!doctype html>
385
455
  text-overflow: ellipsis;
386
456
  white-space: nowrap;
387
457
  font-family: "JetBrains Mono", ui-monospace, monospace;
388
- font-size: 18px;
458
+ font-size: 20px;
389
459
  font-weight: 850;
390
460
  }
391
461
  .why-btn {
392
462
  margin-left: auto;
463
+ display: inline-flex;
464
+ align-items: center;
465
+ justify-content: center;
466
+ width: 20px;
467
+ height: 20px;
468
+ padding: 0;
469
+ line-height: 1;
393
470
  border: 1px solid rgba(26, 58, 143, 0.16);
394
- border-radius: 999px;
395
- padding: 4px 10px;
471
+ border-radius: 50%;
396
472
  background: rgba(26, 58, 143, 0.04);
397
473
  color: var(--muted);
398
474
  font-family: inherit;
399
- font-size: 11px;
400
- font-weight: 750;
475
+ font-size: 15px;
476
+ font-weight: 700;
401
477
  cursor: pointer;
402
478
  }
403
479
  .why-btn:hover { background: rgba(26, 58, 143, 0.09); color: var(--ink); }
404
- .full {
405
- min-height: 16px;
406
- margin-bottom: 3px;
407
- overflow: hidden;
408
- text-overflow: ellipsis;
409
- white-space: nowrap;
410
- font-size: 12.5px;
411
- font-weight: 800;
412
- color: var(--ink);
413
- }
414
- .lead {
415
- margin-bottom: 0;
416
- overflow: hidden;
417
- text-overflow: ellipsis;
418
- white-space: nowrap;
419
- font-size: 11px;
420
- font-weight: 650;
421
- color: var(--faint);
422
- }
423
480
  .evidence {
424
481
  display: none;
425
482
  margin: 11px 0 12px;
@@ -471,8 +528,7 @@ export const HUD_HTML = String.raw `<!doctype html>
471
528
  box-shadow: 0 4px 12px rgba(26, 58, 143, 0.22);
472
529
  }
473
530
  button.renew-btn b { font-weight: 800; }
474
- button.renew-btn small { display: none; }
475
- button.renew-btn.urgent small, button.renew-btn.loading small {
531
+ button.renew-btn small {
476
532
  display: block;
477
533
  margin-top: 3px;
478
534
  font-size: 10.5px;
@@ -497,20 +553,29 @@ export const HUD_HTML = String.raw `<!doctype html>
497
553
  font-size: 11.5px;
498
554
  font-weight: 750;
499
555
  }
556
+ /* Preview of the generated context: a few lines by default, "Read all" opens the whole
557
+ thing right here. */
500
558
  .renew-result pre {
501
- max-height: 120px;
502
- overflow: auto;
503
- margin: 0;
559
+ max-height: 92px;
560
+ overflow: hidden;
561
+ margin: 7px 0 0;
504
562
  white-space: pre-wrap;
505
563
  word-break: break-word;
506
564
  color: var(--ink);
507
565
  font-family: "JetBrains Mono", ui-monospace, monospace;
508
566
  font-size: 9.5px;
567
+ line-height: 1.45;
568
+ -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
509
569
  }
570
+ .renew-result.expanded pre {
571
+ max-height: 300px;
572
+ overflow: auto;
573
+ -webkit-mask-image: none;
574
+ }
575
+ .renew-actions { display: flex; align-items: center; gap: 6px; margin-top: 8px; }
510
576
  .renew-result .copy-btn {
511
577
  display: inline-block;
512
578
  width: auto;
513
- margin-top: 6px;
514
579
  border: 1px solid rgba(26, 58, 143, 0.22);
515
580
  border-radius: 999px;
516
581
  padding: 5px 14px;
@@ -520,7 +585,19 @@ export const HUD_HTML = String.raw `<!doctype html>
520
585
  font-size: 11px;
521
586
  cursor: pointer;
522
587
  }
523
- .meta,
588
+ .ghost-btn {
589
+ border: 1px solid rgba(26, 58, 143, 0.14);
590
+ border-radius: 999px;
591
+ padding: 5px 12px;
592
+ background: transparent;
593
+ color: var(--muted);
594
+ font-family: inherit;
595
+ font-size: 11px;
596
+ font-weight: 750;
597
+ cursor: pointer;
598
+ }
599
+ .ghost-btn:hover { background: rgba(26, 58, 143, 0.06); color: var(--ink); }
600
+ #renewagainbtn { margin-left: auto; }
524
601
  .path,
525
602
  .missing {
526
603
  overflow: hidden;
@@ -529,7 +606,6 @@ export const HUD_HTML = String.raw `<!doctype html>
529
606
  color: var(--faint);
530
607
  font-size: 11px;
531
608
  }
532
- .meta { display: none; }
533
609
  .path { margin-top: 8px; }
534
610
  .missing { margin-top: 6px; }
535
611
  #hud.s-green .mascot-stage { filter: drop-shadow(0 2px 6px rgba(67, 209, 122, 0.3)); }
@@ -571,37 +647,51 @@ export const HUD_HTML = String.raw `<!doctype html>
571
647
  <span class="dot" id="dot"></span>
572
648
  <span class="copy">
573
649
  <span class="main" id="main">Checking context...</span>
574
- <span class="client" id="client">EchoMem HUD</span>
575
- <span class="client-repo" id="clientrepo"></span>
650
+ <span class="client-line"><img class="line-icon" id="bubbleagent" src="/assets/hud/codex.svg" alt="" style="display:none" /><span class="client" id="client">EchoMem HUD</span></span>
651
+ <span class="client-line" id="repoline" style="display:none"><img class="line-icon" src="/assets/hud/github.svg" alt="" /><span class="client-repo" id="clientrepo"></span></span>
576
652
  </span>
577
653
  <button class="cornerbtn" id="minibtn" title="Shrink to mini pill">&ndash;</button>
578
654
  </div>
579
655
  <div id="miniPill" title="EchoMem — click to expand">
580
656
  <span class="mdot" id="mdot"></span>
581
- <img class="micon" id="micon" src="/assets/hud/codex.svg" alt="" />
582
657
  <span id="mscore">on</span>
583
- <span class="mcopy"><span id="msession">EchoMem</span><span id="mrepo"></span></span>
658
+ <span class="mcopy">
659
+ <span class="mrow"><img class="micon" id="micon" src="/assets/hud/codex.svg" alt="" style="display:none" /><span id="msession">EchoMem</span></span>
660
+ <span class="mrow" id="mrepoline" style="display:none"><img class="micon" id="mghicon" src="/assets/hud/github.svg" alt="" /><span id="mrepo"></span></span>
661
+ </span>
584
662
  </div>
585
663
  </div>
586
664
  <div class="details">
587
- <div class="head"><span class="upd" id="updated"></span><span class="headctl"><span class="turn" id="turn"></span><button class="switchbtn agent-switch" id="switchbtn" title="Switch agent" style="display:none" type="button"></button></span></div>
588
- <div class="tabs" id="tabs"></div>
589
- <div class="state-row">
590
- <span class="state-dot" id="stateDot"></span>
591
- <span class="state-word" id="stateWord">--%</span>
592
- <button class="why-btn" id="whybtn" type="button" style="display:none">why</button>
665
+ <div class="now-card" id="nowcard">
666
+ <div class="now-sess">
667
+ <img class="now-icon" id="nowicon" src="/assets/hud/codex.svg" alt="" />
668
+ <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>
669
+ </div>
670
+ <div class="session-actions">
671
+ <button class="pin-chip" id="pinchip" type="button" title="Return to auto-follow">pinned x</button>
672
+ <button class="jump-chip" id="jumpchip" type="button" title="Open this conversation">&#8599;</button>
673
+ </div>
674
+ <div class="state-row">
675
+ <span class="state-dot" id="stateDot"></span>
676
+ <span class="state-word" id="stateWord">--%</span>
677
+ <button class="why-btn" id="whybtn" type="button" title="Details" aria-label="Details" style="display:none">+</button>
678
+ </div>
679
+ <div class="verdict" id="verdict"></div>
680
+ <div class="evidence" id="evidence"></div>
681
+ <div class="nudge" id="nudge"></div>
682
+ <button class="renew-btn" id="renewbtn"><b>Start new session with context</b><small>carry 100% signal, drop the junk</small></button>
683
+ <div class="renew-result" id="renewresult">
684
+ <div class="renew-msg" id="renewmsg"></div>
685
+ <pre id="renewpre"></pre>
686
+ <div class="renew-actions">
687
+ <button class="copy-btn" id="copybtn">Copy</button>
688
+ <button class="ghost-btn" id="expandbtn">Read all</button>
689
+ <button class="ghost-btn" id="renewagainbtn" title="Generate a fresh context from the latest turns">Renew</button>
690
+ </div>
691
+ </div>
593
692
  </div>
594
- <div class="full" id="fullness"></div>
595
- <div class="lead" id="lead"></div>
596
- <div class="evidence" id="evidence"></div>
597
- <div class="nudge" id="nudge"></div>
598
- <button class="renew-btn" id="renewbtn">Start new with context</button>
599
- <div class="renew-result" id="renewresult">
600
- <div class="renew-msg" id="renewmsg"></div>
601
- <pre id="renewpre"></pre>
602
- <button class="copy-btn" id="copybtn">Copy</button>
603
- </div>
604
- <div class="meta" id="meta"></div>
693
+ <div class="tabs" id="tabs"></div>
694
+ <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>
605
695
  <div class="path" id="path"></div>
606
696
  <button class="reveal" id="reveal" style="display:none">↗ Open session in viewer</button>
607
697
  <div class="missing" id="missing"></div>
@@ -613,12 +703,16 @@ export const HUD_HTML = String.raw `<!doctype html>
613
703
  const main = document.getElementById("main");
614
704
  const client = document.getElementById("client");
615
705
  const clientRepo = document.getElementById("clientrepo");
616
- const turnEl = document.getElementById("turn");
617
706
  const stateDot = document.getElementById("stateDot");
618
707
  const stateWordEl = document.getElementById("stateWord");
708
+ const verdictEl = document.getElementById("verdict");
709
+ const nowIcon = document.getElementById("nowicon");
710
+ const nowTitle = document.getElementById("nowtitle");
711
+ const nowRepo = document.getElementById("nowrepo");
712
+ const nowRepoLine = document.getElementById("nowrepoline");
713
+ const pinChip = document.getElementById("pinchip");
714
+ const jumpChip = document.getElementById("jumpchip");
619
715
  const whyBtn = document.getElementById("whybtn");
620
- const fullness = document.getElementById("fullness");
621
- const lead = document.getElementById("lead");
622
716
  const evidence = document.getElementById("evidence");
623
717
  const nudge = document.getElementById("nudge");
624
718
  const renewBtn = document.getElementById("renewbtn");
@@ -626,9 +720,12 @@ export const HUD_HTML = String.raw `<!doctype html>
626
720
  const renewMsg = document.getElementById("renewmsg");
627
721
  const renewPre = document.getElementById("renewpre");
628
722
  const copyBtn = document.getElementById("copybtn");
629
- const metaEl = document.getElementById("meta");
723
+ const expandBtn = document.getElementById("expandbtn");
724
+ const renewAgainBtn = document.getElementById("renewagainbtn");
725
+ const metaLineEl = document.getElementById("metaline");
726
+ const bubbleAgent = document.getElementById("bubbleagent");
727
+ const repoLine = document.getElementById("repoline");
630
728
  const pathEl = document.getElementById("path");
631
- const updatedEl = document.getElementById("updated");
632
729
  const tabsEl = document.getElementById("tabs");
633
730
  const switchBtn = document.getElementById("switchbtn");
634
731
  const revealEl = document.getElementById("reveal");
@@ -637,6 +734,7 @@ export const HUD_HTML = String.raw `<!doctype html>
637
734
  let lastCapsule = "";
638
735
  let renewResultSessionKey = "";
639
736
  let activityEpoch = null;
737
+ let metaTail = ""; // "turn 2 · 178k tokens" — composed in render(), joined with "updated …" each second
640
738
  let lastState = null;
641
739
  let currentSrc = null;
642
740
  let renewTimer = null;
@@ -663,6 +761,7 @@ export const HUD_HTML = String.raw `<!doctype html>
663
761
  const mscore = document.getElementById("mscore");
664
762
  const msession = document.getElementById("msession");
665
763
  const mrepo = document.getElementById("mrepo");
764
+ const mrepoLine = document.getElementById("mrepoline");
666
765
  let miniOn = false;
667
766
  let whyOpen = false;
668
767
  try { miniOn = localStorage.getItem("echomemHudMini") === "1"; } catch {}
@@ -679,7 +778,7 @@ export const HUD_HTML = String.raw `<!doctype html>
679
778
  e.stopPropagation();
680
779
  whyOpen = !whyOpen;
681
780
  hud.classList.toggle("show-why", whyOpen);
682
- whyBtn.textContent = whyOpen ? "hide" : "why";
781
+ whyBtn.textContent = whyOpen ? "" : "+";
683
782
  syncHeight();
684
783
  });
685
784
  let hudDrag = null;
@@ -746,17 +845,40 @@ export const HUD_HTML = String.raw `<!doctype html>
746
845
  .catch(() => {});
747
846
  });
748
847
 
749
- // Click a tab → pin that session as the primary view (or "Auto" to return to auto-selection).
848
+ // Click a history row → pin that session as the current one. Auto-follow is the default;
849
+ // the card's "pinned ×" chip releases the pin. The ↗ corner jumps to the conversation in
850
+ // its own app instead of switching the HUD view.
750
851
  if (tabsEl) tabsEl.addEventListener("click", (e) => {
852
+ const jump = e.target && e.target.closest ? e.target.closest("[data-jump]") : null;
853
+ if (jump) {
854
+ e.stopPropagation();
855
+ jumpToSession(jump.getAttribute("data-jump"));
856
+ return;
857
+ }
751
858
  const btn = e.target && e.target.closest ? e.target.closest(".tab") : null;
752
859
  if (!btn) return;
753
- const id = btn.getAttribute("data-id");
860
+ selectSession(btn.getAttribute("data-id"));
861
+ });
862
+ if (jumpChip) jumpChip.addEventListener("click", (e) => {
863
+ e.stopPropagation();
864
+ jumpToSession(jumpChip.getAttribute("data-jump"));
865
+ });
866
+ function jumpToSession(id) {
867
+ if (!id) return;
868
+ fetch("/jump?id=" + encodeURIComponent(id)).catch(() => {});
869
+ }
870
+ if (pinChip) pinChip.addEventListener("click", (e) => {
871
+ e.stopPropagation();
872
+ selectSession("auto");
873
+ });
874
+ function selectSession(id) {
875
+ if (!id) return;
754
876
  fetch("/select?id=" + encodeURIComponent(id))
755
877
  .then(() => fetch("/state"))
756
878
  .then((r) => r.json())
757
879
  .then(render)
758
880
  .catch(() => {});
759
- });
881
+ }
760
882
 
761
883
  document.getElementById("toggle").addEventListener("click", (event) => {
762
884
  if (suppressNextToggle) {
@@ -778,7 +900,7 @@ export const HUD_HTML = String.raw `<!doctype html>
778
900
  renderRenewLoadingLabel();
779
901
  return;
780
902
  }
781
- renewBtn.textContent = "Start new with context";
903
+ renewBtn.innerHTML = "<b>Start new session with context</b><small>carry 100% signal, drop the junk</small>";
782
904
  }
783
905
 
784
906
  if (renewBtn) renewBtn.addEventListener("click", async () => {
@@ -823,17 +945,18 @@ export const HUD_HTML = String.raw `<!doctype html>
823
945
  }
824
946
  saveCachedCheckpoint(renewSessionKey, prompt, promptKind, meta);
825
947
  if (promptKind === "cloud") saveCheckpointStatus(renewSessionKey, statusFromRenewData(renewData, meta));
948
+ const doneSummary = checkpointSuffix(checkpointSummary({ renewedAt: Date.now(), memoryCount: meta.sessionMemoryCount }));
826
949
  showRenewResult({
827
950
  sessionKey: renewSessionKey,
828
951
  prompt,
829
- message: promptKind === "local" ? "Local capsule ready" : "Decision log ready",
952
+ message: (promptKind === "local" ? "Local capsule ready" : "Decision log ready") + doneSummary,
830
953
  canCopy: true,
831
954
  });
832
955
  try {
833
956
  await navigator.clipboard.writeText(prompt);
834
- if (renewMsg) renewMsg.textContent = promptKind === "local" ? "Local capsule copied" : "Decision log copied";
957
+ if (renewMsg) renewMsg.textContent = (promptKind === "local" ? "Local capsule copied" : "Decision log copied") + doneSummary;
835
958
  } catch {
836
- if (renewMsg) renewMsg.textContent = promptKind === "local" ? "Local capsule ready" : "Decision log ready";
959
+ if (renewMsg) renewMsg.textContent = (promptKind === "local" ? "Local capsule ready" : "Decision log ready") + doneSummary;
837
960
  }
838
961
  } catch {
839
962
  } finally {
@@ -853,6 +976,25 @@ export const HUD_HTML = String.raw `<!doctype html>
853
976
  } catch {}
854
977
  });
855
978
 
979
+ if (expandBtn) expandBtn.addEventListener("click", (e) => {
980
+ e.stopPropagation();
981
+ if (!renewResult) return;
982
+ const expanded = renewResult.classList.toggle("expanded");
983
+ expandBtn.textContent = expanded ? "Collapse" : "Read all";
984
+ syncHeight();
985
+ });
986
+
987
+ // Escape hatch: the session moved on and the saved context is stale — clear it and
988
+ // run the renew flow again.
989
+ if (renewAgainBtn) renewAgainBtn.addEventListener("click", (e) => {
990
+ e.stopPropagation();
991
+ clearRenewResult();
992
+ if (renewBtn) {
993
+ renewBtn.style.display = "block";
994
+ renewBtn.click();
995
+ }
996
+ });
997
+
856
998
  function activeRenewScore() {
857
999
  const st = lastState || {};
858
1000
  return st.active || st;
@@ -879,9 +1021,15 @@ export const HUD_HTML = String.raw `<!doctype html>
879
1021
  copyBtn.textContent = "Copy";
880
1022
  copyBtn.style.display = "";
881
1023
  }
882
- if (renewResult) renewResult.style.display = "none";
1024
+ if (renewResult) {
1025
+ renewResult.style.display = "none";
1026
+ renewResult.classList.remove("expanded");
1027
+ }
1028
+ if (expandBtn) expandBtn.textContent = "Read all";
883
1029
  }
884
1030
 
1031
+ // The result replaces the action: once a context exists, the big renew button steps aside
1032
+ // and this card carries the preview + timestamp + copy.
885
1033
  function showRenewResult(opts) {
886
1034
  const prompt = String((opts && opts.prompt) || "");
887
1035
  const body = String((opts && opts.body) || prompt || "");
@@ -890,13 +1038,21 @@ export const HUD_HTML = String.raw `<!doctype html>
890
1038
  if (renewMsg) renewMsg.textContent = String((opts && opts.message) || "");
891
1039
  if (renewPre) {
892
1040
  renewPre.textContent = body;
893
- renewPre.style.display = "none";
1041
+ renewPre.style.display = body ? "block" : "none";
1042
+ }
1043
+ if (expandBtn) {
1044
+ expandBtn.style.display = body ? "inline-block" : "none";
1045
+ expandBtn.textContent = "Read all";
894
1046
  }
895
1047
  if (copyBtn) {
896
1048
  copyBtn.textContent = "Copy";
897
1049
  copyBtn.style.display = opts && opts.canCopy ? "inline-block" : "none";
898
1050
  }
899
- if (renewResult) renewResult.style.display = "block";
1051
+ if (renewResult) {
1052
+ renewResult.classList.remove("expanded");
1053
+ renewResult.style.display = "block";
1054
+ }
1055
+ if (renewBtn && !renewStartMs) renewBtn.style.display = "none";
900
1056
  syncHeight();
901
1057
  }
902
1058
 
@@ -982,7 +1138,7 @@ export const HUD_HTML = String.raw `<!doctype html>
982
1138
  function checkpointSummary(item) {
983
1139
  const parts = [];
984
1140
  const renewedAt = timestampMs(item && item.renewedAt);
985
- if (renewedAt) parts.push("renewed " + relTime(Date.now() - renewedAt));
1141
+ if (renewedAt) parts.push("generated " + relTime(Date.now() - renewedAt));
986
1142
  const count = Number(item && (item.memoryCount ?? item.sessionMemoryCount));
987
1143
  if (Number.isFinite(count) && count > 0) parts.push(count + " " + (count === 1 ? "memory" : "memories") + " saved");
988
1144
  return parts.join(" · ");
@@ -1187,15 +1343,20 @@ export const HUD_HTML = String.raw `<!doctype html>
1187
1343
  render({
1188
1344
  active: sample,
1189
1345
  pinnedId: previewPinned ? "p2" : null,
1346
+ sessions: [
1347
+ sample,
1348
+ { client: "codex", sourcePath: "/preview/b.jsonl", usefulPct: 71, healthScorePct: 71, color: "amber" },
1349
+ { client: "codex", sourcePath: "/preview/c.jsonl", usefulPct: 94, healthScorePct: 94, color: "green" },
1350
+ ],
1190
1351
  recentSessions: [
1191
- { id: "p1", client: "codex", title: "Simplify HUD session display", label: "EchoMem-Chrome", sourcePath: "/preview/session.jsonl", live: true },
1192
- { id: "p2", client: "codex", title: "修复 HUD 切换和图标", label: "EchoMem-Chrome", sourcePath: "/preview/b.jsonl", live: false },
1193
- { id: "p3", client: "codex", title: "Fix extraction preview", label: "EchoMem-Chrome", sourcePath: "/preview/c.jsonl", live: false },
1194
- { id: "p4", client: "codex", title: "Port Claude work", label: "EchoMem-Chrome", sourcePath: "/preview/d.jsonl", live: false },
1195
- { id: "p5", client: "codex", title: "Stamp fixes with Echo mascot", label: "EchoMem-Chrome", sourcePath: "/preview/e.jsonl", live: false },
1196
- { id: "p6", client: "codex", title: "Review migration checks", label: "EchoMem-Chrome", sourcePath: "/preview/f.jsonl", live: false },
1197
- { id: "p7", client: "codex", title: "Polish setup story", label: "EchoMem-Chrome", sourcePath: "/preview/g.jsonl", live: false },
1198
- { id: "p8", client: "claude-code", title: "DG-Web refactor", label: "DG-Web", sourcePath: "/preview/h.jsonl", live: false },
1352
+ { id: "p1", client: "codex", title: "Simplify HUD session display", label: "EchoMem-Chrome", sourcePath: "/preview/session.jsonl", live: true, lastActiveMs: 4000 },
1353
+ { id: "p2", client: "codex", title: "修复 HUD 切换和图标", label: "EchoMem-Chrome", sourcePath: "/preview/b.jsonl", live: false, lastActiveMs: 480000 },
1354
+ { id: "p3", client: "codex", title: "Fix extraction preview", label: "EchoMem-Chrome", sourcePath: "/preview/c.jsonl", live: false, lastActiveMs: 2400000 },
1355
+ { id: "p4", client: "codex", title: "Port Claude work", label: "EchoForPeople", sourcePath: "/preview/d.jsonl", live: false, lastActiveMs: 9000000 },
1356
+ { id: "p5", client: "codex", title: "Stamp fixes with Echo mascot", label: "EchoMem-Chrome", sourcePath: "/preview/e.jsonl", live: false, lastActiveMs: 16200000 },
1357
+ { id: "p6", client: "codex", title: "Review migration checks", label: "EchoForPeople", sourcePath: "/preview/f.jsonl", live: false, lastActiveMs: 21600000 },
1358
+ { id: "p7", client: "codex", title: "Polish setup story", label: "EchoMem-Chrome", sourcePath: "/preview/g.jsonl", live: false, lastActiveMs: 34200000 },
1359
+ { id: "p8", client: "claude-code", title: "DG-Web refactor", label: "DG-Web", sourcePath: "/preview/h.jsonl", live: false, lastActiveMs: 39600000 },
1199
1360
  ],
1200
1361
  });
1201
1362
  hud.classList.add("open");
@@ -1213,7 +1374,8 @@ export const HUD_HTML = String.raw `<!doctype html>
1213
1374
  const currentSessionKey = sessionKeyFor(state, score);
1214
1375
  const viewColor = score && typeof score.usefulPct === "number" ? (score.color || "green") : "idle";
1215
1376
  if (renewResultSessionKey && renewResultSessionKey !== currentSessionKey) clearRenewResult();
1216
- const hasSessionSwitcher = renderTabs(state, score && score.sourcePath ? activeIdFor(state, score.sourcePath, activeClient) : null, activeClient, viewColor);
1377
+ const activeId = score && score.sourcePath ? activeIdFor(state, score.sourcePath, activeClient) : null;
1378
+ renderTabs(state, activeId, activeClient);
1217
1379
  renderSwitchButton(state, activeClient);
1218
1380
  if (!score || typeof score.usefulPct !== "number") {
1219
1381
  renderIdle(state);
@@ -1231,23 +1393,35 @@ export const HUD_HTML = String.raw `<!doctype html>
1231
1393
  stateDot.className = "state-dot " + color;
1232
1394
  hud.classList.remove("s-green", "s-amber", "s-red");
1233
1395
  hud.classList.add("s-" + color);
1234
- main.textContent = health + "%";
1396
+ main.innerHTML = health + '%<span class="main-rest"> of tokens are useful</span>';
1235
1397
  renderMini(color, health + "%", info);
1236
1398
  client.textContent = info.title || info.repo;
1237
- if (clientRepo) clientRepo.textContent = info.repo && info.repo !== info.title ? info.repo : "";
1399
+ if (bubbleAgent) {
1400
+ bubbleAgent.src = iconForFamily(agentFamily(score.client) || "codex");
1401
+ bubbleAgent.style.display = "inline-block";
1402
+ }
1403
+ const bubbleRepo = info.repo && info.repo !== info.title ? info.repo : "";
1404
+ if (clientRepo) clientRepo.textContent = bubbleRepo;
1405
+ if (repoLine) repoLine.style.display = bubbleRepo ? "flex" : "none";
1238
1406
  client.classList.remove("voice");
1239
1407
  activityEpoch = typeof score.lastActiveMs === "number" ? Date.now() - score.lastActiveMs : null;
1240
- renderUpdated();
1241
- turnEl.textContent = typeof score.turn === "number" && score.turn > 0 ? "turn " + score.turn : "";
1408
+ const metaParts = [];
1409
+ if (typeof score.turn === "number" && score.turn > 0) metaParts.push("turn " + score.turn);
1410
+ if (score.ctTokens > 0) metaParts.push(fmt(score.ctTokens) + " tok");
1411
+ metaTail = metaParts.join(" · ");
1412
+ renderMetaLine();
1242
1413
  stateWordEl.textContent = health + "%";
1243
- fullness.textContent = hasSessionSwitcher ? "" : info.title;
1244
- fullness.style.display = hasSessionSwitcher ? "none" : "block";
1245
- fullness.style.color = "";
1246
- lead.textContent = hasSessionSwitcher ? "" : (info.repo && info.repo !== info.title ? info.repo : "");
1247
- lead.style.display = hasSessionSwitcher || !lead.textContent ? "none" : "block";
1248
- lead.style.color = "";
1414
+ if (verdictEl) verdictEl.textContent = verdictFor(color, oldPct);
1415
+ if (nowIcon) nowIcon.src = iconForFamily(agentFamily(score.client) || "codex");
1416
+ if (nowTitle) nowTitle.textContent = info.title;
1417
+ if (nowRepo) nowRepo.textContent = bubbleRepo;
1418
+ if (nowRepoLine) nowRepoLine.style.display = bubbleRepo ? "flex" : "none";
1419
+ if (pinChip) pinChip.style.display = state && state.pinnedId ? "inline-block" : "none";
1420
+ if (jumpChip) {
1421
+ if (activeId) jumpChip.setAttribute("data-jump", activeId);
1422
+ jumpChip.style.display = activeId ? "inline-block" : "none";
1423
+ }
1249
1424
  renderWhy(score, oldPct, repeatCount, sat, health, color);
1250
- metaEl.textContent = "";
1251
1425
  pathEl.textContent = "";
1252
1426
  currentSrc = score.sourcePath || null;
1253
1427
  if (revealEl) revealEl.style.display = !DEMO_CLEAN && currentSrc ? "inline-block" : "none";
@@ -1256,15 +1430,18 @@ export const HUD_HTML = String.raw `<!doctype html>
1256
1430
  nudge.style.display = "none";
1257
1431
 
1258
1432
  renewUrgent = color === "red";
1259
- if (renewBtn) {
1260
- renewBtn.style.display = "block";
1261
- refreshRenewLabel();
1262
- }
1263
1433
  const restoredPrompt = restoreCachedCheckpointFor(score, currentSessionKey);
1264
1434
  if (!restoredPrompt) {
1265
1435
  const restoredStatus = restoreCheckpointStatusFor(score, currentSessionKey);
1266
1436
  if (!restoredStatus) maybeFetchCheckpointStatus(score, currentSessionKey);
1267
1437
  }
1438
+ // A generated context replaces the call to generate one: while the result card is up
1439
+ // for this session, the big button stays hidden (Renew in the card re-runs the flow).
1440
+ const resultShowing = renewResult && renewResult.style.display === "block" && renewResultSessionKey === currentSessionKey;
1441
+ if (renewBtn) {
1442
+ renewBtn.style.display = resultShowing && !renewStartMs ? "none" : "block";
1443
+ refreshRenewLabel();
1444
+ }
1268
1445
  prevTurnState = {
1269
1446
  src: score.sourcePath,
1270
1447
  turn: score.turn,
@@ -1284,19 +1461,22 @@ export const HUD_HTML = String.raw `<!doctype html>
1284
1461
  main.textContent = "MCP on";
1285
1462
  renderMini("idle", "on", { client: "EchoMem", title: "MCP", repo: "" });
1286
1463
  client.textContent = "Waiting for Codex or Claude";
1464
+ if (bubbleAgent) bubbleAgent.style.display = "none";
1287
1465
  if (clientRepo) clientRepo.textContent = "";
1466
+ if (repoLine) repoLine.style.display = "none";
1288
1467
  client.classList.remove("voice");
1289
1468
  activityEpoch = null;
1290
- if (updatedEl) updatedEl.textContent = "";
1469
+ metaTail = "";
1470
+ renderMetaLine();
1291
1471
  currentSrc = null;
1292
1472
  if (revealEl) revealEl.style.display = "none";
1293
- turnEl.textContent = "";
1294
1473
  stateWordEl.textContent = "on";
1295
- fullness.textContent = "EchoMem MCP";
1296
- fullness.style.display = "block";
1297
- lead.textContent = "Waiting for Codex or Claude";
1298
- lead.style.display = "block";
1299
- lead.style.color = "#6b675d";
1474
+ if (verdictEl) verdictEl.textContent = "Waiting for Codex or Claude.";
1475
+ if (nowTitle) nowTitle.textContent = "EchoMem MCP";
1476
+ if (nowRepo) nowRepo.textContent = "";
1477
+ if (nowRepoLine) nowRepoLine.style.display = "none";
1478
+ if (pinChip) pinChip.style.display = "none";
1479
+ if (jumpChip) jumpChip.style.display = "none";
1300
1480
  evidence.textContent = "";
1301
1481
  if (whyBtn) whyBtn.style.display = "none";
1302
1482
  whyOpen = false;
@@ -1304,7 +1484,6 @@ export const HUD_HTML = String.raw `<!doctype html>
1304
1484
  nudge.style.display = "none";
1305
1485
  if (renewBtn) renewBtn.style.display = "none";
1306
1486
  clearRenewResult();
1307
- metaEl.textContent = "";
1308
1487
  pathEl.textContent = "";
1309
1488
  missing.textContent = formatMissing(state && state.missing);
1310
1489
  syncHeight();
@@ -1312,24 +1491,29 @@ export const HUD_HTML = String.raw `<!doctype html>
1312
1491
 
1313
1492
  function renderMini(color, scoreText, info) {
1314
1493
  if (mdot) mdot.className = "mdot " + color;
1315
- if (micon) micon.src = iconForFamily(agentFamily(info && info.clientRaw) || "codex");
1494
+ const fam = agentFamily(info && info.clientRaw);
1495
+ if (micon) {
1496
+ micon.src = iconForFamily(fam || "codex");
1497
+ micon.style.display = fam ? "inline-block" : "none";
1498
+ }
1316
1499
  if (mscore) mscore.textContent = scoreText;
1317
1500
  if (msession) msession.textContent = miniTitle(info);
1318
- if (mrepo) mrepo.textContent = info && info.repo ? info.repo : "";
1501
+ const repo = info && info.repo ? info.repo : "";
1502
+ if (mrepo) mrepo.textContent = repo;
1503
+ if (mrepoLine) mrepoLine.style.display = repo ? "flex" : "none";
1319
1504
  }
1320
1505
 
1321
1506
  function renderWhy(score, oldPct, repeatCount, sat, health, color) {
1322
1507
  const rows = [];
1323
- rows.push(["score", health + "%"]);
1324
- if (sat > 0) rows.push(["context", score.modelContextWindow ? sat + "% full" : fmt(score.ctTokens) + " tokens"]);
1325
- if (oldPct > 0 || score.pollutionTok > 0) rows.push(["old context", fmt(score.pollutionTok || 0) + " (" + Math.round(oldPct) + "%)"]);
1326
- if (repeatCount > 0) rows.push(["repeated reads", String(repeatCount)]);
1508
+ if (sat > 0) rows.push(["context", score.modelContextWindow ? sat + "% full · " + fmt(score.ctTokens) : fmt(score.ctTokens) + " tokens"]);
1509
+ if (score.pollutionTok > 0) rows.push(["junk", fmt(score.pollutionTok) + " (" + Math.round(oldPct) + "% of window)"]);
1510
+ for (const row of junkBreakdown(score)) rows.push(row);
1327
1511
  rows.push(["source", ctSourceLabel(score.ctSource)]);
1328
1512
  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("");
1329
1513
  const useful = color !== "green" || oldPct >= 10 || repeatCount > 0 || sat >= 80;
1330
1514
  if (whyBtn) {
1331
1515
  whyBtn.style.display = useful ? "inline-flex" : "none";
1332
- whyBtn.textContent = whyOpen ? "hide" : "why";
1516
+ whyBtn.textContent = whyOpen ? "" : "+";
1333
1517
  }
1334
1518
  if (!useful) {
1335
1519
  whyOpen = false;
@@ -1339,6 +1523,21 @@ export const HUD_HTML = String.raw `<!doctype html>
1339
1523
  }
1340
1524
  }
1341
1525
 
1526
+ // Top junk categories from the latest local-live waste partition, biggest first.
1527
+ function junkBreakdown(score) {
1528
+ const buckets = score.buckets || {};
1529
+ const tok = (key) => (buckets[key] && buckets[key].tokens) || 0;
1530
+ const copies = buckets.range_redundant || {};
1531
+ const entries = [
1532
+ ["stale screenshots", tok("superseded_image"), ""],
1533
+ ["superseded copies", tok("range_redundant"), copies.count > 0 ? " · " + copies.count + " re-read" : ""],
1534
+ ["carried tool output", tok("stale_tool_output"), ""],
1535
+ ["outdated reads", tok("stale_read"), ""],
1536
+ ].filter((entry) => entry[1] > 0);
1537
+ entries.sort((a, b) => b[1] - a[1]);
1538
+ return entries.map((entry) => ["· " + entry[0], fmt(entry[1]) + entry[2]]);
1539
+ }
1540
+
1342
1541
  function fullnessPct(score) {
1343
1542
  if (typeof score.saturationPct === "number") return score.saturationPct;
1344
1543
  if (score.modelContextWindow) return Math.round(100 * (score.ctTokens || 0) / score.modelContextWindow);
@@ -1408,28 +1607,42 @@ export const HUD_HTML = String.raw `<!doctype html>
1408
1607
  return list.filter((s) => agentFamily(s.client) === targetFamily)[0] || null;
1409
1608
  }
1410
1609
 
1411
- // Recent sessions as switchable rows. "Auto" clears the pin. Hidden when there's only one
1412
- // session for the active agent family, so Codex never recommends Claude sessions or vice versa.
1413
- function renderTabs(state, activeId, activeClient, viewColor) {
1414
- if (!tabsEl) return false;
1415
- const list = visibleRecentSessions(state, activeClient);
1416
- if (list.length <= 1) { tabsEl.innerHTML = ""; return false; }
1417
- const pinnedId = state && state.pinnedId ? state.pinnedId : null;
1610
+ // History rows: recent sessions that are NOT the current one (the current session lives in the
1611
+ // now-card above every number and action up there belongs to it). Clicking a row pins it as
1612
+ // the new current session; the card's "pinned ×" chip releases back to auto-follow. Scored
1613
+ // sessions carry their own health dot + %, so "Echo is following you across agents" reads off
1614
+ // the list itself. Family-scoped, so Codex never recommends Claude sessions or vice versa.
1615
+ function renderTabs(state, activeId, activeClient) {
1616
+ if (!tabsEl) return;
1617
+ const list = visibleRecentSessions(state, activeClient).filter((s) => s.id !== activeId);
1618
+ if (!list.length) { tabsEl.innerHTML = ""; return; }
1418
1619
  const chips = list.map((s) => {
1419
- const viewing = s.id === activeId;
1420
- const cls = "tab" + (viewing ? " viewing" : "") + (s.id === pinnedId ? " pinned" : "");
1421
- const activity = s.live && !viewing ? '<span class="spin"></span>' : "";
1422
- const status = viewing ? '<span class="tab-status ' + esc(viewColor || "idle") + '" aria-label="Current session health"></span>' : "";
1620
+ const activity = s.live ? '<span class="spin"></span>' : "";
1621
+ const hv = sessionHealthFor(state, s);
1622
+ const status = hv
1623
+ ? '<span class="tab-health"><span class="tab-status ' + esc(hv.color) + '"></span>' + hv.health + "%</span>"
1624
+ : "";
1423
1625
  const title = s.title || label(s.client);
1424
1626
  const repo = s.label && s.label !== title ? s.label : "";
1425
1627
  const family = agentFamily(s.client);
1426
- return '<button class="' + cls + '" data-id="' + esc(s.id) + '"' + (viewing ? ' aria-current="true"' : "") + ' title="' + esc((viewing ? "Currently viewing · " : "") + label(s.client) + " · " + title + (repo ? " · " + repo : "")) + '">'
1628
+ const repoCell = repo
1629
+ ? '<span class="tab-repo"><img class="tab-gh" src="/assets/hud/github.svg" alt="" /><span class="tab-reponame">' + esc(repo) + "</span></span>"
1630
+ : '<span class="tab-repo"><span class="tab-reponame">' + esc(label(s.client)) + "</span></span>";
1631
+ const time = typeof s.lastActiveMs === "number" ? '<span class="tab-time" title="Last active here">' + esc(relTime(s.lastActiveMs)) + "</span>" : "";
1632
+ return '<button class="tab" data-id="' + esc(s.id) + '" title="' + esc("Switch to · " + label(s.client) + " · " + title + (repo ? " · " + repo : "")) + '">'
1427
1633
  + '<img class="tab-icon" src="' + iconForFamily(family) + '" alt="" />'
1428
- + '<span class="tab-copy"><span class="tab-title">' + esc(title) + activity + '</span><span class="tab-repo">' + esc(repo || label(s.client)) + "</span></span>" + status + "</button>";
1634
+ + '<span class="tab-copy"><span class="tab-title">' + esc(title) + activity + "</span>" + repoCell + "</span>" + status + time
1635
+ + '<span class="tab-jump" data-jump="' + esc(s.id) + '" title="Open this conversation in ' + esc(label(s.client)) + '">&#8599;</span></button>';
1429
1636
  });
1430
- chips.unshift('<button class="tab auto-tab' + (pinnedId ? "" : " mode-active") + '" data-id="auto" title="' + (pinnedId ? "Use Auto selection" : "Auto selection is on") + '"><span class="tab-title">Auto</span><span class="auto-check" aria-hidden="true">&#10003;</span></button>');
1431
1637
  tabsEl.innerHTML = chips.join("");
1432
- return true;
1638
+ }
1639
+ // Health for a listed session, joined from the scored views the monitor already computes.
1640
+ // Sessions Echo isn't actively scoring get no dot — honest absence, not a fake green.
1641
+ function sessionHealthFor(state, s) {
1642
+ const views = ((state && state.sessions) || []).concat((state && state.scores) || []);
1643
+ const m = views.filter((v) => v && v.sourcePath === s.sourcePath && v.client === s.client && typeof v.usefulPct === "number")[0];
1644
+ if (!m) return null;
1645
+ return { color: m.color || "green", health: healthScore(m) };
1433
1646
  }
1434
1647
  function visibleRecentSessions(state, activeClient) {
1435
1648
  const list = (state && state.recentSessions) || [];
@@ -1458,9 +1671,19 @@ export const HUD_HTML = String.raw `<!doctype html>
1458
1671
  return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
1459
1672
  }
1460
1673
 
1461
- function renderUpdated() {
1462
- if (!updatedEl) return;
1463
- updatedEl.textContent = activityEpoch === null ? "" : "updated " + relTime(Date.now() - activityEpoch);
1674
+ // One quiet mono line for all technical metadata: "16s ago · turn 2 · 178k tok".
1675
+ function renderMetaLine() {
1676
+ if (!metaLineEl) return;
1677
+ const upd = activityEpoch === null ? "" : relTime(Date.now() - activityEpoch);
1678
+ metaLineEl.textContent = [upd, metaTail].filter(Boolean).join(" · ");
1679
+ }
1680
+ // One plain sentence saying what the score means for THIS session. Direct, no hedging,
1681
+ // and no second number — the score above is the only number on the card.
1682
+ function verdictFor(color, oldPct) {
1683
+ const usefulPct = 100 - clamp(oldPct || 0, 0, 100);
1684
+ if (color === "red") return usefulPct <= 20 ? "Mostly junk — carry over now." : "Too much junk – consider to carry over.";
1685
+ if (color === "amber") return "Junk is piling up.";
1686
+ return "Context is clean.";
1464
1687
  }
1465
1688
  function relTime(ms) {
1466
1689
  const s = Math.max(0, Math.round(ms / 1000));
@@ -1470,7 +1693,7 @@ export const HUD_HTML = String.raw `<!doctype html>
1470
1693
  if (m < 60) return m + "m ago";
1471
1694
  return Math.round(m / 60) + "h ago";
1472
1695
  }
1473
- setInterval(renderUpdated, 1000);
1696
+ setInterval(renderMetaLine, 1000);
1474
1697
 
1475
1698
  function label(clientName) {
1476
1699
  if (clientName === "codex") return "Codex";