@echomem/mcp 1.4.8 → 1.4.9

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 (36) hide show
  1. package/README.md +23 -3
  2. package/assets/canonical-scorer/README.md +18 -0
  3. package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
  4. package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
  5. package/assets/canonical-scorer/golden_anchors.mjs +83 -0
  6. package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
  7. package/dist/city/chaos-to-clarity-pencil.html +582 -0
  8. package/dist/city/echo-ai-city-only.html +1104 -105
  9. package/dist/city/echo-ai-city-only.template.html +1104 -105
  10. package/dist/city/pencil-pie-generator.html +883 -0
  11. package/dist/city/pencil-webgl-landscape.html +1239 -0
  12. package/dist/city/spatial-fan-story.html +479 -0
  13. package/dist/codex-session-files.js +283 -0
  14. package/dist/codex-sync.js +7 -2
  15. package/dist/context-analysis/canonical-golden.js +47 -0
  16. package/dist/context-analysis/claude-native-canonical.js +1193 -0
  17. package/dist/context-analysis/vendored-canonical.js +793 -0
  18. package/dist/context-analysis/workspace-report.js +1838 -0
  19. package/dist/context-metrics/calculate.js +56 -0
  20. package/dist/context-metrics/model-limits.js +26 -0
  21. package/dist/context-metrics/types.js +1 -0
  22. package/dist/forensics-10-problems.js +7 -6
  23. package/dist/forensics.js +863 -132
  24. package/dist/hud/adapters.js +8 -4
  25. package/dist/hud/metric.js +13 -4
  26. package/dist/hud/monitor.js +135 -16
  27. package/dist/hud/web.js +344 -298
  28. package/dist/index.js +7 -3
  29. package/dist/local-data-paths.js +87 -0
  30. package/dist/migrate.js +37 -29
  31. package/dist/report.js +101 -40
  32. package/dist/setup-page.js +3290 -196
  33. package/dist/setup-preview.js +245 -0
  34. package/dist/setup.js +432 -34
  35. package/package.json +5 -4
  36. package/templates/echomem-recall.md +2 -2
package/dist/hud/web.js CHANGED
@@ -1,4 +1,3 @@
1
- import { MCP_PACKAGE_VERSION } from "../package-metadata.js";
2
1
  export const HUD_HTML = String.raw `<!doctype html>
3
2
  <html lang="en">
4
3
  <head>
@@ -39,6 +38,11 @@ export const HUD_HTML = String.raw `<!doctype html>
39
38
  -webkit-font-smoothing: antialiased;
40
39
  cursor: grab;
41
40
  }
41
+ #hud.capture-preview {
42
+ position: fixed;
43
+ top: 0;
44
+ left: 0;
45
+ }
42
46
  #hud.dragging { cursor: grabbing; }
43
47
  .glance {
44
48
  position: relative;
@@ -129,24 +133,31 @@ export const HUD_HTML = String.raw `<!doctype html>
129
133
  min-width: 0;
130
134
  display: grid;
131
135
  grid-template-columns: minmax(0, 1fr);
132
- gap: 4px;
133
- padding: 20px 38px 18px 30px;
136
+ gap: 2px;
137
+ padding: 15px 38px 11px 30px;
134
138
  }
135
139
  .dot {
136
140
  position: absolute;
137
- left: 14px;
138
- top: 23px;
141
+ left: 13px;
142
+ top: 22px;
139
143
  z-index: 3;
140
- width: 10px;
141
- height: 10px;
144
+ width: 12px;
145
+ height: 12px;
142
146
  border: 2px solid #fdfaf2;
143
147
  border-radius: 999px;
144
148
  background: var(--green);
145
149
  box-shadow: 0 0 0 1px rgba(26, 58, 143, 0.14);
146
150
  }
147
151
  .dot.amber { background: var(--amber); }
148
- .dot.red { background: #ff5f57; }
149
- .dot.idle { background: #c9c5b8; }
152
+ .dot.red, .state-dot.red, #miniPill .mdot.red {
153
+ background: #ff5f57;
154
+ animation: red-pulse 1.15s ease-in-out infinite;
155
+ }
156
+ .dot.idle { background: var(--green); }
157
+ @keyframes red-pulse {
158
+ 0%, 100% { box-shadow: 0 0 0 1px rgba(199, 55, 47, 0.18), 0 0 0 0 rgba(255, 95, 87, 0.34); transform: scale(1); }
159
+ 50% { box-shadow: 0 0 0 1px rgba(199, 55, 47, 0.18), 0 0 0 7px rgba(255, 95, 87, 0); transform: scale(1.18); }
160
+ }
150
161
  .main {
151
162
  min-width: 0;
152
163
  overflow: hidden;
@@ -163,14 +174,22 @@ export const HUD_HTML = String.raw `<!doctype html>
163
174
  overflow: hidden;
164
175
  text-overflow: ellipsis;
165
176
  white-space: nowrap;
166
- font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
167
- font-size: 10px;
168
- letter-spacing: 0.12em;
177
+ font-size: 10.5px;
178
+ font-weight: 720;
179
+ letter-spacing: 0;
180
+ line-height: 1;
181
+ color: var(--faint);
182
+ }
183
+ .client-repo {
184
+ min-width: 0;
185
+ overflow: hidden;
186
+ text-overflow: ellipsis;
187
+ white-space: nowrap;
188
+ font-size: 9.5px;
189
+ font-weight: 620;
169
190
  line-height: 1;
170
- text-transform: uppercase;
171
191
  color: var(--faint);
172
192
  }
173
- /* When drifted, the bubble's second line becomes Echo speaking, not a log line. */
174
193
  .client.voice {
175
194
  font-family: inherit;
176
195
  font-size: 11.5px;
@@ -182,10 +201,10 @@ export const HUD_HTML = String.raw `<!doctype html>
182
201
  .details {
183
202
  display: none;
184
203
  position: relative;
185
- margin: 2px 8px 0 24px;
204
+ margin: 2px 8px 0 8px;
186
205
  padding: 16px 17px 15px;
187
206
  border: 1px solid var(--line);
188
- border-radius: 16px;
207
+ border-radius: 12px;
189
208
  background: var(--card);
190
209
  color: var(--ink);
191
210
  filter: var(--shadow);
@@ -206,18 +225,76 @@ export const HUD_HTML = String.raw `<!doctype html>
206
225
  font-size: 11px;
207
226
  color: var(--faint);
208
227
  }
209
- .tabs { display: flex; gap: 6px; overflow-x: auto; margin: 2px 0 12px; padding-bottom: 2px; }
210
- .tabs::-webkit-scrollbar { height: 0; }
228
+ .tabs {
229
+ display: grid;
230
+ gap: 6px;
231
+ max-height: 259px;
232
+ margin: 2px 0 12px;
233
+ padding-right: 3px;
234
+ overflow-y: auto;
235
+ overscroll-behavior: contain;
236
+ }
237
+ .tabs:empty { display: none; }
238
+ .tabs::-webkit-scrollbar { width: 4px; }
239
+ .tabs::-webkit-scrollbar-track { background: transparent; }
240
+ .tabs::-webkit-scrollbar-thumb { border-radius: 999px; background: rgba(26, 58, 143, .2); }
211
241
  .tab {
212
- flex: 0 0 auto; font-size: 10.5px; line-height: 1; padding: 5px 10px; border-radius: 999px;
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;
213
244
  border: 1px solid rgba(0, 0, 0, .12); background: rgba(0, 0, 0, .04);
214
- color: var(--faint); cursor: pointer; white-space: nowrap; font-family: inherit;
245
+ color: var(--faint); cursor: pointer; text-align: left; font-family: inherit;
215
246
  }
216
247
  .tab:hover { background: rgba(0, 0, 0, .08); }
217
- .tab.active { border-color: rgba(0, 0, 0, .38); background: rgba(0, 0, 0, .10); color: #111; font-weight: 700; }
218
- .tab.auto-pick { border-color: rgba(26, 58, 143, .2); background: rgba(26, 58, 143, .045); color: var(--muted); font-weight: 650; }
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; }
259
+ .tab-status {
260
+ width: 10px;
261
+ height: 10px;
262
+ justify-self: end;
263
+ border: 2px solid #fff;
264
+ border-radius: 50%;
265
+ background: var(--green);
266
+ box-shadow: 0 0 0 1px rgba(26, 58, 143, .18);
267
+ }
268
+ .tab-status.amber { background: var(--amber); }
269
+ .tab-status.red { background: #ff5f57; box-shadow: 0 0 0 1px rgba(199, 55, 47, .28); }
270
+ .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;
287
+ 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;
294
+ }
295
+ .auto-tab.mode-active .auto-check { display: inline-flex; }
219
296
  .spin {
220
- display: inline-block; width: 9px; height: 9px; margin-right: 5px; vertical-align: -1px;
297
+ display: inline-block; width: 8px; height: 8px; margin-left: 4px; vertical-align: -1px;
221
298
  border: 1.5px solid rgba(0, 0, 0, .2); border-top-color: #1a3a8f; border-radius: 50%;
222
299
  animation: echospin .8s linear infinite;
223
300
  }
@@ -225,18 +302,26 @@ export const HUD_HTML = String.raw `<!doctype html>
225
302
  /* Mini mode: a small always-on pill instead of the full mascot+bubble. Opening the panel
226
303
  temporarily shows the full layout; closing returns to the pill. */
227
304
  #miniPill {
228
- display: none; align-items: center; justify-content: center; gap: 7px; width: calc(100% - 16px); height: 34px;
229
- margin: 7px 0 0 8px; padding: 0 13px; cursor: pointer; user-select: none;
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;
230
307
  border: 1.6px solid rgba(26, 58, 143, 0.75); border-radius: 999px; background: #fdfaf2;
231
- filter: var(--shadow); font-family: "JetBrains Mono", ui-monospace, monospace;
232
- font-size: 11.5px; font-weight: 700; color: var(--ink); letter-spacing: 0.02em;
308
+ filter: var(--shadow); color: var(--ink); letter-spacing: 0;
233
309
  }
234
310
  #miniPill .mdot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); flex: 0 0 auto; }
235
311
  #miniPill .mdot.amber { background: var(--amber); }
236
- #miniPill .mdot.red { background: #ff5f57; }
237
- #miniPill .mdot.idle { background: #c9c5b8; }
238
- #mtext { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
239
- #hud.mini:not(.open) .glance { height: 48px; }
312
+ #miniPill .mdot.idle { background: var(--green); }
313
+ .micon { width: 16px; height: 16px; object-fit: contain; flex: 0 0 auto; }
314
+ #mscore {
315
+ flex: 0 0 auto;
316
+ font-family: "JetBrains Mono", ui-monospace, monospace;
317
+ font-size: 12px;
318
+ font-weight: 850;
319
+ }
320
+ .mcopy { min-width: 0; display: grid; gap: 1px; line-height: 1.1; }
321
+ #msession, #mrepo { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
322
+ #msession { font-size: 11.5px; font-weight: 760; color: var(--ink); }
323
+ #mrepo { font-size: 10px; font-weight: 650; color: var(--faint); }
324
+ #hud.mini:not(.open) .glance { height: 52px; }
240
325
  #hud.mini:not(.open) .mascot-stage, #hud.mini:not(.open) .status-bubble, #hud.mini:not(.open) .drag { display: none; }
241
326
  #hud.mini:not(.open) #miniPill { display: inline-flex; }
242
327
  .cornerbtn {
@@ -255,12 +340,11 @@ export const HUD_HTML = String.raw `<!doctype html>
255
340
  }
256
341
  .switchbtn:hover { background: rgba(0, 0, 0, .1); color: #111; }
257
342
  .agent-switch {
258
- width: auto;
259
- min-width: 116px;
260
- max-width: 154px;
343
+ width: 28px;
344
+ min-width: 28px;
345
+ max-width: 28px;
261
346
  height: 28px;
262
- gap: 6px;
263
- padding: 0 10px 0 7px;
347
+ padding: 0;
264
348
  border-radius: 999px;
265
349
  background: rgba(255, 255, 255, .78);
266
350
  color: #1f1f1f;
@@ -273,7 +357,7 @@ export const HUD_HTML = String.raw `<!doctype html>
273
357
  .agent-switch[data-target-family="claude"] { border-color: rgba(217, 119, 87, .42); }
274
358
  .agent-switch[data-target-family="codex"] { border-color: rgba(57, 65, 255, .34); }
275
359
  .switch-icon { width: 16px; height: 16px; flex: 0 0 auto; object-fit: contain; }
276
- .switch-copy { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
360
+ .switch-copy { display: none; }
277
361
  .reveal {
278
362
  margin-top: 8px; font-size: 10.5px; padding: 4px 11px; border-radius: 999px;
279
363
  border: 1px solid rgba(0, 0, 0, .12); background: rgba(0, 0, 0, .04); color: var(--faint);
@@ -284,87 +368,75 @@ export const HUD_HTML = String.raw `<!doctype html>
284
368
  display: flex;
285
369
  align-items: center;
286
370
  gap: 9px;
287
- margin-bottom: 7px;
371
+ margin-bottom: 9px;
288
372
  }
289
373
  .state-dot {
290
- width: 11px;
291
- height: 11px;
374
+ width: 12px;
375
+ height: 12px;
292
376
  flex: none;
293
377
  border-radius: 999px;
294
378
  background: var(--green);
295
379
  box-shadow: 0 0 0 1px rgba(26, 58, 143, 0.12);
296
380
  }
297
381
  .state-dot.amber { background: var(--amber); }
298
- .state-dot.red { background: #ff5f57; }
299
382
  .state-word {
300
383
  min-width: 0;
301
384
  overflow: hidden;
302
385
  text-overflow: ellipsis;
303
386
  white-space: nowrap;
304
- font-size: 16px;
305
- font-weight: 800;
306
- }
307
- .full {
308
- min-height: 17px;
309
- margin-bottom: 14px;
310
- font-size: 12px;
311
- font-weight: 650;
312
- color: #4a7a3e;
387
+ font-family: "JetBrains Mono", ui-monospace, monospace;
388
+ font-size: 18px;
389
+ font-weight: 850;
313
390
  }
314
- .lead {
315
- margin-bottom: 5px;
316
- font-size: 13.5px;
391
+ .why-btn {
392
+ margin-left: auto;
393
+ border: 1px solid rgba(26, 58, 143, 0.16);
394
+ border-radius: 999px;
395
+ padding: 4px 10px;
396
+ background: rgba(26, 58, 143, 0.04);
397
+ color: var(--muted);
398
+ font-family: inherit;
399
+ font-size: 11px;
317
400
  font-weight: 750;
318
- color: #2f7d4f;
401
+ cursor: pointer;
319
402
  }
320
- .evidence {
321
- min-height: 38px;
322
- margin-bottom: 12px;
323
- color: var(--muted);
403
+ .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;
324
410
  font-size: 12.5px;
325
- line-height: 1.5;
411
+ font-weight: 800;
412
+ color: var(--ink);
326
413
  }
327
- .oldbar {
328
- display: flex;
329
- height: 9px;
330
- margin: 0;
414
+ .lead {
415
+ margin-bottom: 0;
331
416
  overflow: hidden;
332
- border-radius: 999px;
333
- background: var(--track);
334
- }
335
- .oldbar span {
336
- display: block;
337
- height: 100%;
338
- min-width: 0;
339
- transition: width 0.35s ease;
417
+ text-overflow: ellipsis;
418
+ white-space: nowrap;
419
+ font-size: 11px;
420
+ font-weight: 650;
421
+ color: var(--faint);
340
422
  }
341
- .oldbar .useful { width: 100%; background: var(--blue); }
342
- .oldbar .old { width: 0; background: var(--brick); }
343
- .alert {
423
+ .evidence {
344
424
  display: none;
345
- margin-top: 13px;
346
- padding: 9px 0 9px 12px;
347
- border-left: 3px solid rgba(199, 55, 47, 0.42);
348
- }
349
- .alert-title {
350
- margin-bottom: 4px;
351
- color: var(--brick);
352
- font-size: 12.5px;
353
- font-weight: 750;
354
- }
355
- .alert-detail {
425
+ margin: 11px 0 12px;
426
+ padding: 9px 10px;
427
+ border: 1px solid rgba(26, 58, 143, 0.12);
428
+ border-radius: 8px;
429
+ background: rgba(26, 58, 143, 0.035);
356
430
  color: var(--muted);
357
431
  font-size: 12px;
358
- line-height: 1.45;
359
- }
360
- .nudge {
361
- display: none;
362
- margin-top: 13px;
363
- padding: 10px 12px;
364
- border-radius: 12px;
365
- font-size: 12.5px;
366
- line-height: 1.45;
432
+ line-height: 1.5;
367
433
  }
434
+ #hud.show-why .evidence { display: block; }
435
+ .why-line { display: flex; justify-content: space-between; gap: 12px; }
436
+ .why-line + .why-line { margin-top: 3px; }
437
+ .why-k { color: var(--faint); }
438
+ .why-v { color: var(--ink); font-weight: 750; text-align: right; }
439
+ .nudge { display: none; }
368
440
  .nudge.calm {
369
441
  border: 1px solid rgba(26, 58, 143, 0.14);
370
442
  background: rgba(26, 58, 143, 0.05);
@@ -375,30 +447,28 @@ export const HUD_HTML = String.raw `<!doctype html>
375
447
  background: rgba(199, 55, 47, 0.07);
376
448
  color: #7a241e;
377
449
  }
378
- /* Breathing CTA: quiet ghost pill while the session is healthy... */
379
450
  button.renew-btn {
380
451
  display: none;
381
- width: auto;
452
+ width: 100%;
382
453
  margin-top: 12px;
383
- border: 1px solid rgba(199, 55, 47, 0.28);
454
+ border: 1px solid rgba(26, 58, 143, 0.26);
384
455
  border-radius: 999px;
385
- padding: 6px 16px;
386
- background: transparent;
387
- color: var(--brick);
456
+ padding: 9px 14px;
457
+ background: rgba(26, 58, 143, 0.07);
458
+ color: var(--blue);
388
459
  font-weight: 750;
389
- font-size: 11.5px;
460
+ font-size: 12px;
390
461
  cursor: pointer;
391
462
  transition: all 0.25s ease;
392
463
  }
393
- /* ...and a full-size primary the moment the session drifts. */
394
464
  button.renew-btn.urgent {
395
- width: 100%;
396
- padding: 11px 12px;
397
- border-color: var(--brick);
398
- background: var(--brick);
399
- color: #fff7ef;
400
- font-size: 14px;
401
- box-shadow: 0 10px 22px -14px rgba(199, 55, 47, 0.7);
465
+ border-color: var(--blue);
466
+ padding: 11px 14px;
467
+ background: var(--blue);
468
+ color: #fff;
469
+ font-size: 13px;
470
+ font-weight: 800;
471
+ box-shadow: 0 4px 12px rgba(26, 58, 143, 0.22);
402
472
  }
403
473
  button.renew-btn b { font-weight: 800; }
404
474
  button.renew-btn small { display: none; }
@@ -410,19 +480,19 @@ export const HUD_HTML = String.raw `<!doctype html>
410
480
  opacity: 0.85;
411
481
  white-space: normal;
412
482
  }
413
- button.renew-btn:hover { background: rgba(199, 55, 47, 0.12); }
414
- button.renew-btn.urgent:hover { background: #b32e27; }
483
+ button.renew-btn:hover { background: rgba(26, 58, 143, 0.12); }
484
+ button.renew-btn.urgent:hover { background: #102d78; }
415
485
  button.renew-btn.loading { width: 100%; opacity: 0.75; cursor: wait; }
416
486
  .renew-result {
417
487
  display: none;
418
488
  margin-top: 10px;
419
489
  border: 1px solid rgba(26, 58, 143, 0.18);
420
- border-radius: 12px;
490
+ border-radius: 8px;
421
491
  background: rgba(26, 58, 143, 0.04);
422
492
  padding: 10px 12px;
423
493
  }
424
494
  .renew-result .renew-msg {
425
- margin-bottom: 6px;
495
+ margin-bottom: 0;
426
496
  color: var(--blue);
427
497
  font-size: 11.5px;
428
498
  font-weight: 750;
@@ -459,7 +529,7 @@ export const HUD_HTML = String.raw `<!doctype html>
459
529
  color: var(--faint);
460
530
  font-size: 11px;
461
531
  }
462
- .meta { margin-top: 13px; }
532
+ .meta { display: none; }
463
533
  .path { margin-top: 8px; }
464
534
  .missing { margin-top: 6px; }
465
535
  #hud.s-green .mascot-stage { filter: drop-shadow(0 2px 6px rgba(67, 209, 122, 0.3)); }
@@ -502,35 +572,34 @@ export const HUD_HTML = String.raw `<!doctype html>
502
572
  <span class="copy">
503
573
  <span class="main" id="main">Checking context...</span>
504
574
  <span class="client" id="client">EchoMem HUD</span>
575
+ <span class="client-repo" id="clientrepo"></span>
505
576
  </span>
506
577
  <button class="cornerbtn" id="minibtn" title="Shrink to mini pill">&ndash;</button>
507
578
  </div>
508
- <div id="miniPill" title="EchoMem — click to expand"><span class="mdot" id="mdot"></span><span id="mtext">EchoMem</span></div>
579
+ <div id="miniPill" title="EchoMem — click to expand">
580
+ <span class="mdot" id="mdot"></span>
581
+ <img class="micon" id="micon" src="/assets/hud/codex.svg" alt="" />
582
+ <span id="mscore">on</span>
583
+ <span class="mcopy"><span id="msession">EchoMem</span><span id="mrepo"></span></span>
584
+ </div>
509
585
  </div>
510
586
  <div class="details">
511
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>
512
588
  <div class="tabs" id="tabs"></div>
513
589
  <div class="state-row">
514
590
  <span class="state-dot" id="stateDot"></span>
515
- <span class="state-word" id="stateWord">Fresh</span>
591
+ <span class="state-word" id="stateWord">--%</span>
592
+ <button class="why-btn" id="whybtn" type="button" style="display:none">why</button>
516
593
  </div>
517
594
  <div class="full" id="fullness"></div>
518
- <div class="lead" id="lead">Clean and roomy</div>
519
- <div class="evidence" id="evidence">Collecting enough local context to judge the window.</div>
520
- <div class="oldbar" aria-hidden="true">
521
- <span class="useful" id="usefulbar"></span>
522
- <span class="old" id="oldbar"></span>
523
- </div>
524
- <div class="alert" id="alert">
525
- <div class="alert-title" id="alertTitle"></div>
526
- <div class="alert-detail" id="alertDetail"></div>
527
- </div>
595
+ <div class="lead" id="lead"></div>
596
+ <div class="evidence" id="evidence"></div>
528
597
  <div class="nudge" id="nudge"></div>
529
- <button class="renew-btn" id="renewbtn">Renew session</button>
598
+ <button class="renew-btn" id="renewbtn">Start new with context</button>
530
599
  <div class="renew-result" id="renewresult">
531
600
  <div class="renew-msg" id="renewmsg"></div>
532
601
  <pre id="renewpre"></pre>
533
- <button class="copy-btn" id="copybtn">Copy to clipboard</button>
602
+ <button class="copy-btn" id="copybtn">Copy</button>
534
603
  </div>
535
604
  <div class="meta" id="meta"></div>
536
605
  <div class="path" id="path"></div>
@@ -543,17 +612,14 @@ export const HUD_HTML = String.raw `<!doctype html>
543
612
  const dot = document.getElementById("dot");
544
613
  const main = document.getElementById("main");
545
614
  const client = document.getElementById("client");
615
+ const clientRepo = document.getElementById("clientrepo");
546
616
  const turnEl = document.getElementById("turn");
547
617
  const stateDot = document.getElementById("stateDot");
548
618
  const stateWordEl = document.getElementById("stateWord");
619
+ const whyBtn = document.getElementById("whybtn");
549
620
  const fullness = document.getElementById("fullness");
550
621
  const lead = document.getElementById("lead");
551
622
  const evidence = document.getElementById("evidence");
552
- const usefulbar = document.getElementById("usefulbar");
553
- const oldbar = document.getElementById("oldbar");
554
- const alertEl = document.getElementById("alert");
555
- const alertTitle = document.getElementById("alertTitle");
556
- const alertDetail = document.getElementById("alertDetail");
557
623
  const nudge = document.getElementById("nudge");
558
624
  const renewBtn = document.getElementById("renewbtn");
559
625
  const renewResult = document.getElementById("renewresult");
@@ -567,7 +633,6 @@ export const HUD_HTML = String.raw `<!doctype html>
567
633
  const switchBtn = document.getElementById("switchbtn");
568
634
  const revealEl = document.getElementById("reveal");
569
635
  const missing = document.getElementById("missing");
570
- const hudVersion = "v${MCP_PACKAGE_VERSION}";
571
636
  let prevTurnState = null;
572
637
  let lastCapsule = "";
573
638
  let renewResultSessionKey = "";
@@ -594,8 +659,12 @@ export const HUD_HTML = String.raw `<!doctype html>
594
659
  // returns to the pill. Preference persists (localStorage here, window size in main's bounds file).
595
660
  const miniBtn = document.getElementById("minibtn");
596
661
  const mdot = document.getElementById("mdot");
597
- const mtext = document.getElementById("mtext");
662
+ const micon = document.getElementById("micon");
663
+ const mscore = document.getElementById("mscore");
664
+ const msession = document.getElementById("msession");
665
+ const mrepo = document.getElementById("mrepo");
598
666
  let miniOn = false;
667
+ let whyOpen = false;
599
668
  try { miniOn = localStorage.getItem("echomemHudMini") === "1"; } catch {}
600
669
  if (miniOn) hud.classList.add("mini");
601
670
  function applyMini(next) {
@@ -606,6 +675,13 @@ export const HUD_HTML = String.raw `<!doctype html>
606
675
  if (window.echomemHud && typeof window.echomemHud.setMini === "function") window.echomemHud.setMini(next);
607
676
  }
608
677
  if (miniBtn) miniBtn.addEventListener("click", (e) => { e.stopPropagation(); applyMini(true); });
678
+ if (whyBtn) whyBtn.addEventListener("click", (e) => {
679
+ e.stopPropagation();
680
+ whyOpen = !whyOpen;
681
+ hud.classList.toggle("show-why", whyOpen);
682
+ whyBtn.textContent = whyOpen ? "hide" : "why";
683
+ syncHeight();
684
+ });
609
685
  let hudDrag = null;
610
686
  let suppressNextToggle = false;
611
687
  const dragBlockSelector = "button, input, textarea, select, a, pre, .tab, .switchbtn, .reveal, .renew-btn, .copy-btn";
@@ -694,8 +770,6 @@ export const HUD_HTML = String.raw `<!doctype html>
694
770
  syncHeight();
695
771
  });
696
772
 
697
- // Breathing CTA state: quiet while fresh, primary while drifted. Label carries the payoff so
698
- // the user knows renewing does not lose their trail.
699
773
  let renewUrgent = false;
700
774
  function refreshRenewLabel() {
701
775
  if (!renewBtn) return;
@@ -704,9 +778,7 @@ export const HUD_HTML = String.raw `<!doctype html>
704
778
  renderRenewLoadingLabel();
705
779
  return;
706
780
  }
707
- renewBtn.innerHTML = renewUrgent
708
- ? '<b>Renew session</b><small>I pack a checkpoint and carry it into your fresh start</small>'
709
- : "Renew session";
781
+ renewBtn.textContent = "Start new with context";
710
782
  }
711
783
 
712
784
  if (renewBtn) renewBtn.addEventListener("click", async () => {
@@ -754,20 +826,14 @@ export const HUD_HTML = String.raw `<!doctype html>
754
826
  showRenewResult({
755
827
  sessionKey: renewSessionKey,
756
828
  prompt,
757
- message: promptKind === "local"
758
- ? "Basic local capsule ready. Cloud decision extraction was unavailable; copy it below and paste it into a fresh session."
759
- : "Decision log ready, copied to your clipboard. Open a fresh session and paste it as the first message.",
829
+ message: promptKind === "local" ? "Local capsule ready" : "Decision log ready",
760
830
  canCopy: true,
761
831
  });
762
832
  try {
763
833
  await navigator.clipboard.writeText(prompt);
764
- if (renewMsg) renewMsg.textContent = promptKind === "local"
765
- ? "Basic local capsule copied. Cloud decision extraction was unavailable; this includes goal, recent instruction, and file anchors, but may miss decisions."
766
- : "Decision log ready, copied to your clipboard. Open a fresh session and paste it as the first message.";
834
+ if (renewMsg) renewMsg.textContent = promptKind === "local" ? "Local capsule copied" : "Decision log copied";
767
835
  } catch {
768
- if (renewMsg) renewMsg.textContent = promptKind === "local"
769
- ? "Basic local capsule ready. Cloud decision extraction was unavailable; copy it below and paste it into a fresh session."
770
- : "Decision log ready. Copy it below, then paste it into a fresh session.";
836
+ if (renewMsg) renewMsg.textContent = promptKind === "local" ? "Local capsule ready" : "Decision log ready";
771
837
  }
772
838
  } catch {
773
839
  } finally {
@@ -783,7 +849,7 @@ export const HUD_HTML = String.raw `<!doctype html>
783
849
  try {
784
850
  await navigator.clipboard.writeText(lastCapsule);
785
851
  copyBtn.textContent = "Copied";
786
- setTimeout(() => { copyBtn.textContent = "Copy to clipboard"; }, 1500);
852
+ setTimeout(() => { copyBtn.textContent = "Copy"; }, 1500);
787
853
  } catch {}
788
854
  });
789
855
 
@@ -806,11 +872,11 @@ export const HUD_HTML = String.raw `<!doctype html>
806
872
  renewResultSessionKey = "";
807
873
  if (renewPre) {
808
874
  renewPre.textContent = "";
809
- renewPre.style.display = "";
875
+ renewPre.style.display = "none";
810
876
  }
811
877
  if (renewMsg) renewMsg.textContent = "";
812
878
  if (copyBtn) {
813
- copyBtn.textContent = "Copy to clipboard";
879
+ copyBtn.textContent = "Copy";
814
880
  copyBtn.style.display = "";
815
881
  }
816
882
  if (renewResult) renewResult.style.display = "none";
@@ -824,10 +890,10 @@ export const HUD_HTML = String.raw `<!doctype html>
824
890
  if (renewMsg) renewMsg.textContent = String((opts && opts.message) || "");
825
891
  if (renewPre) {
826
892
  renewPre.textContent = body;
827
- renewPre.style.display = body ? "block" : "none";
893
+ renewPre.style.display = "none";
828
894
  }
829
895
  if (copyBtn) {
830
- copyBtn.textContent = "Copy to clipboard";
896
+ copyBtn.textContent = "Copy";
831
897
  copyBtn.style.display = opts && opts.canCopy ? "inline-block" : "none";
832
898
  }
833
899
  if (renewResult) renewResult.style.display = "block";
@@ -895,13 +961,11 @@ export const HUD_HTML = String.raw `<!doctype html>
895
961
  const stale = savedTurn > 0 && currentTurn > savedTurn;
896
962
  const summary = checkpointSummary(cached);
897
963
  if (cached && cached.kind === "local") {
898
- return stale
899
- ? "Basic local capsule from turn " + savedTurn + ". Renew again for the latest cloud decision log."
900
- : "Basic local capsule already ready for this session. Cloud decision extraction was unavailable when it was made.";
964
+ return savedTurn > 0 ? "Local capsule from turn " + savedTurn + checkpointSuffix(summary) : "Local capsule ready" + checkpointSuffix(summary);
901
965
  }
902
- return stale
903
- ? "Decision log from turn " + savedTurn + ". " + summary + " Copy it below, or Renew again for the latest."
904
- : "Decision log already ready for this session. " + summary + " Copy it below, or Renew again to replace it.";
966
+ return savedTurn > 0 && stale
967
+ ? "Decision log from turn " + savedTurn + checkpointSuffix(summary)
968
+ : "Decision log ready" + checkpointSuffix(summary);
905
969
  }
906
970
 
907
971
  function statusFromRenewData(data, meta) {
@@ -921,7 +985,11 @@ export const HUD_HTML = String.raw `<!doctype html>
921
985
  if (renewedAt) parts.push("renewed " + relTime(Date.now() - renewedAt));
922
986
  const count = Number(item && (item.memoryCount ?? item.sessionMemoryCount));
923
987
  if (Number.isFinite(count) && count > 0) parts.push(count + " " + (count === 1 ? "memory" : "memories") + " saved");
924
- return parts.length ? parts.join(" · ") + "." : "";
988
+ return parts.join(" · ");
989
+ }
990
+
991
+ function checkpointSuffix(summary) {
992
+ return summary ? " · " + summary : "";
925
993
  }
926
994
 
927
995
  function timestampMs(value) {
@@ -981,7 +1049,7 @@ export const HUD_HTML = String.raw `<!doctype html>
981
1049
  showRenewResult({
982
1050
  sessionKey: key,
983
1051
  prompt: body,
984
- message: "Decision log already saved. " + summary,
1052
+ message: "Decision log ready" + checkpointSuffix(summary),
985
1053
  canCopy: true,
986
1054
  });
987
1055
  }
@@ -1071,7 +1139,7 @@ export const HUD_HTML = String.raw `<!doctype html>
1071
1139
  const elapsed = Date.now() - renewStartMs;
1072
1140
  const remaining = Math.max(0, Number(renewEta && renewEta.ms) || 20000) - elapsed;
1073
1141
  const detail = remaining > 0 ? "ETA " + fmtDuration(remaining) + " left" : "ETA any moment";
1074
- renewBtn.innerHTML = "<b>Packing your checkpoint&hellip;</b><small>" + esc(detail) + "</small>";
1142
+ renewBtn.innerHTML = "<b>Preparing context&hellip;</b><small>" + esc(detail) + "</small>";
1075
1143
  }
1076
1144
 
1077
1145
  function fallbackEtaForScore(score) {
@@ -1094,13 +1162,17 @@ export const HUD_HTML = String.raw `<!doctype html>
1094
1162
 
1095
1163
  // Dev/design preview: ?preview=green|amber|red renders that health state with sample data and
1096
1164
  // skips the live feed — same pattern as the extraction page's ?preview hook.
1097
- const previewColor = new URLSearchParams(location.search).get("preview");
1165
+ const previewParams = new URLSearchParams(location.search);
1166
+ const previewColor = previewParams.get("preview");
1167
+ const previewPinned = previewParams.get("pinned") === "1";
1168
+ if (previewParams.get("capture") === "1") hud.classList.add("capture-preview");
1098
1169
  if (previewColor === "green" || previewColor === "amber" || previewColor === "red") {
1099
1170
  const sample = {
1100
1171
  client: "codex",
1101
1172
  label: "EchoMem-Chrome",
1102
- sourcePath: "/preview/session.jsonl",
1173
+ sourcePath: previewPinned ? "/preview/b.jsonl" : "/preview/session.jsonl",
1103
1174
  usefulPct: previewColor === "red" ? 55 : previewColor === "amber" ? 75 : 95,
1175
+ healthScorePct: previewColor === "red" ? 48 : previewColor === "amber" ? 72 : 96,
1104
1176
  color: previewColor,
1105
1177
  ctTokens: previewColor === "red" ? 165000 : previewColor === "amber" ? 110000 : 24000,
1106
1178
  modelContextWindow: 258000,
@@ -1114,11 +1186,16 @@ export const HUD_HTML = String.raw `<!doctype html>
1114
1186
  };
1115
1187
  render({
1116
1188
  active: sample,
1189
+ pinnedId: previewPinned ? "p2" : null,
1117
1190
  recentSessions: [
1118
- { id: "p1", client: "codex", title: "EchoMem-Chrome", sourcePath: "/preview/session.jsonl", live: true },
1119
- { id: "p2", client: "codex", title: "修复 HUD 切换和图标", sourcePath: "/preview/b.jsonl", live: false },
1120
- { id: "p3", client: "codex", title: "Fix extraction preview", sourcePath: "/preview/c.jsonl", live: false },
1121
- { id: "p4", client: "claude-code", title: "DG-Web refactor", sourcePath: "/preview/d.jsonl", live: false },
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 },
1122
1199
  ],
1123
1200
  });
1124
1201
  hud.classList.add("open");
@@ -1134,8 +1211,9 @@ export const HUD_HTML = String.raw `<!doctype html>
1134
1211
  lastState = state;
1135
1212
  const activeClient = score && score.client ? score.client : state && state.active && state.active.client;
1136
1213
  const currentSessionKey = sessionKeyFor(state, score);
1214
+ const viewColor = score && typeof score.usefulPct === "number" ? (score.color || "green") : "idle";
1137
1215
  if (renewResultSessionKey && renewResultSessionKey !== currentSessionKey) clearRenewResult();
1138
- renderTabs(state, score && score.sourcePath ? activeIdFor(state, score.sourcePath, activeClient) : null, activeClient);
1216
+ const hasSessionSwitcher = renderTabs(state, score && score.sourcePath ? activeIdFor(state, score.sourcePath, activeClient) : null, activeClient, viewColor);
1139
1217
  renderSwitchButton(state, activeClient);
1140
1218
  if (!score || typeof score.usefulPct !== "number") {
1141
1219
  renderIdle(state);
@@ -1143,54 +1221,43 @@ export const HUD_HTML = String.raw `<!doctype html>
1143
1221
  }
1144
1222
 
1145
1223
  const color = score.color || "green";
1146
- const stateName = stateWord(color);
1147
1224
  const sat = fullnessPct(score);
1148
1225
  const oldPct = clamp(score.pollutionPct || 0, 0, 95);
1226
+ const health = healthScore(score);
1149
1227
  const repeatCount = score.buckets && score.buckets.range_redundant ? score.buckets.range_redundant.count || 0 : 0;
1150
- const edits = score.stats && score.stats.patchEdits ? Number(score.stats.patchEdits) : 0;
1228
+ const info = activeSessionInfo(score, state);
1151
1229
 
1152
- // A session idle for >10 min shouldn't wear the cheerful "Fresh/clean" look — its quality
1153
- // numbers are stale. Show it muted as Idle so a 47h-old pinned session never reads as current.
1154
- const stale = typeof score.lastActiveMs === "number" && score.lastActiveMs > 600000;
1155
- const shownColor = stale ? "idle" : color;
1156
- dot.className = "dot " + shownColor;
1157
- stateDot.className = stale ? "state-dot" : "state-dot " + color;
1230
+ dot.className = "dot " + color;
1231
+ stateDot.className = "state-dot " + color;
1158
1232
  hud.classList.remove("s-green", "s-amber", "s-red");
1159
- if (!stale) hud.classList.add("s-" + color);
1160
- main.textContent = (stale ? "Idle" : stateName) + " - " + fmt(score.ctTokens) + " ctx";
1161
- if (mdot) mdot.className = "mdot " + shownColor;
1162
- if (mtext) mtext.textContent = (stale ? "Idle" : stateName) + " · " + fmt(score.ctTokens);
1163
- // Collapsed bubble: normally identifies the session (trust the auto-follow); when drifted
1164
- // it becomes Echo's nudge — the bubble itself sells the renew.
1165
- const urgent = !stale && color === "red";
1166
- client.textContent = urgent ? "let me carry this forward →" : sessionIdentity(score, state);
1167
- client.classList.toggle("voice", urgent);
1233
+ hud.classList.add("s-" + color);
1234
+ main.textContent = health + "%";
1235
+ renderMini(color, health + "%", info);
1236
+ client.textContent = info.title || info.repo;
1237
+ if (clientRepo) clientRepo.textContent = info.repo && info.repo !== info.title ? info.repo : "";
1238
+ client.classList.remove("voice");
1168
1239
  activityEpoch = typeof score.lastActiveMs === "number" ? Date.now() - score.lastActiveMs : null;
1169
1240
  renderUpdated();
1170
1241
  turnEl.textContent = typeof score.turn === "number" && score.turn > 0 ? "turn " + score.turn : "";
1171
- stateWordEl.textContent = stale ? "Idle" : stateName;
1172
- fullness.textContent = fullnessLine(sat, score);
1173
- fullness.style.color = stale ? "#6b675d" : tone(color);
1174
- lead.textContent = stale ? "This session is idle - showing its last state" : leadLine(color);
1175
- lead.style.color = stale ? "#6b675d" : tone(color);
1176
- evidence.textContent = evidenceLine(score, oldPct, repeatCount);
1177
- usefulbar.style.width = Math.max(0, 100 - oldPct) + "%";
1178
- oldbar.style.width = oldPct + "%";
1179
- metaEl.textContent = ctSourceLabel(score.ctSource) + " · " + edits + " edits";
1242
+ 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 = "";
1249
+ renderWhy(score, oldPct, repeatCount, sat, health, color);
1250
+ metaEl.textContent = "";
1180
1251
  pathEl.textContent = "";
1181
1252
  currentSrc = score.sourcePath || null;
1182
1253
  if (revealEl) revealEl.style.display = !DEMO_CLEAN && currentSrc ? "inline-block" : "none";
1183
1254
  missing.textContent = formatMissing(state.missing);
1184
1255
 
1185
- renderAlert(score, oldPct, repeatCount);
1186
- // The old advice box repeated the verdict a third time; its job now lives in leadLine + the
1187
- // renew button's own subtitle.
1188
1256
  nudge.style.display = "none";
1189
1257
 
1190
- // Breathing CTA: quiet ghost while things are fine, full-size primary when drifted.
1191
- renewUrgent = urgent;
1258
+ renewUrgent = color === "red";
1192
1259
  if (renewBtn) {
1193
- renewBtn.style.display = "block"; // always available — warm-start any session, even fresh
1260
+ renewBtn.style.display = "block";
1194
1261
  refreshRenewLabel();
1195
1262
  }
1196
1263
  const restoredPrompt = restoreCachedCheckpointFor(score, currentSessionKey);
@@ -1214,23 +1281,26 @@ export const HUD_HTML = String.raw `<!doctype html>
1214
1281
  stateDot.className = "state-dot";
1215
1282
  hud.classList.remove("s-green", "s-amber", "s-red");
1216
1283
  prevTurnState = null;
1217
- main.textContent = "No active session yet";
1218
- if (mdot) mdot.className = "mdot idle";
1219
- if (mtext) mtext.textContent = "EchoMem";
1220
- client.textContent = "EchoMem HUD - " + hudVersion;
1284
+ main.textContent = "MCP on";
1285
+ renderMini("idle", "on", { client: "EchoMem", title: "MCP", repo: "" });
1286
+ client.textContent = "Waiting for Codex or Claude";
1287
+ if (clientRepo) clientRepo.textContent = "";
1288
+ client.classList.remove("voice");
1221
1289
  activityEpoch = null;
1222
1290
  if (updatedEl) updatedEl.textContent = "";
1223
1291
  currentSrc = null;
1224
1292
  if (revealEl) revealEl.style.display = "none";
1225
1293
  turnEl.textContent = "";
1226
- stateWordEl.textContent = "Fresh";
1227
- fullness.textContent = "";
1228
- lead.textContent = "Waiting for a live Codex or Claude session";
1294
+ 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";
1229
1299
  lead.style.color = "#6b675d";
1230
- evidence.textContent = "Start working and EchoMem will show when the context is still clean, drifting, or ready to renew.";
1231
- usefulbar.style.width = "100%";
1232
- oldbar.style.width = "0";
1233
- alertEl.style.display = "none";
1300
+ evidence.textContent = "";
1301
+ if (whyBtn) whyBtn.style.display = "none";
1302
+ whyOpen = false;
1303
+ hud.classList.remove("show-why");
1234
1304
  nudge.style.display = "none";
1235
1305
  if (renewBtn) renewBtn.style.display = "none";
1236
1306
  clearRenewResult();
@@ -1240,66 +1310,33 @@ export const HUD_HTML = String.raw `<!doctype html>
1240
1310
  syncHeight();
1241
1311
  }
1242
1312
 
1243
- function renderAlert(score, oldPct, repeatCount) {
1244
- if (!alertEl) return;
1245
- const turnChanged = prevTurnState && prevTurnState.src === score.sourcePath && score.turn !== prevTurnState.turn;
1246
- const dCt = turnChanged ? (score.ctTokens || 0) - prevTurnState.ct : 0;
1247
- const dReads = turnChanged ? (score.reads || 0) - prevTurnState.reads : 0;
1248
- const dRep = turnChanged ? repeatCount - prevTurnState.repeat : 0;
1249
- const dOld = turnChanged ? (score.pollutionTok || 0) - prevTurnState.oldTok : 0;
1250
- if (dRep > 0 || dOld > 2000) {
1251
- alertTitle.textContent = "this turn re-read what it already had";
1252
- alertDetail.textContent = joinParts([
1253
- dOld > 0 ? "+" + fmt(dOld) + " old weight" : "",
1254
- dReads > 0 ? "+" + dReads + " read" + (dReads === 1 ? "" : "s") : "0 new files",
1255
- dCt !== 0 ? signedFmt(dCt) + " ctx" : "",
1256
- ]);
1257
- alertEl.style.display = "block";
1258
- return;
1313
+ function renderMini(color, scoreText, info) {
1314
+ if (mdot) mdot.className = "mdot " + color;
1315
+ if (micon) micon.src = iconForFamily(agentFamily(info && info.clientRaw) || "codex");
1316
+ if (mscore) mscore.textContent = scoreText;
1317
+ if (msession) msession.textContent = miniTitle(info);
1318
+ if (mrepo) mrepo.textContent = info && info.repo ? info.repo : "";
1319
+ }
1320
+
1321
+ function renderWhy(score, oldPct, repeatCount, sat, health, color) {
1322
+ 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)]);
1327
+ rows.push(["source", ctSourceLabel(score.ctSource)]);
1328
+ 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
+ const useful = color !== "green" || oldPct >= 10 || repeatCount > 0 || sat >= 80;
1330
+ if (whyBtn) {
1331
+ whyBtn.style.display = useful ? "inline-flex" : "none";
1332
+ whyBtn.textContent = whyOpen ? "hide" : "why";
1259
1333
  }
1260
- // No static "context has drifted" verdict here: the state word, lead line, and bar already
1261
- // say it. The alert is reserved for genuinely NEW information (a turn that re-read old files).
1262
- alertEl.style.display = "none";
1263
- }
1264
-
1265
- function renderNudge(color, sat) {
1266
- let text = "";
1267
- let cls = "nudge";
1268
- if (color === "red") {
1269
- text = "A fresh start now beats pushing past this - keep the goal, drop the clutter.";
1270
- cls += " red";
1271
- } else if (color === "amber" || sat >= 75) {
1272
- text = "Still fine - a fresh start soon will keep this snappy.";
1273
- cls += " calm";
1334
+ if (!useful) {
1335
+ whyOpen = false;
1336
+ hud.classList.remove("show-why");
1337
+ } else {
1338
+ hud.classList.toggle("show-why", whyOpen);
1274
1339
  }
1275
- nudge.textContent = text;
1276
- nudge.className = cls;
1277
- nudge.style.display = text ? "block" : "none";
1278
- }
1279
-
1280
- function stateWord(color) {
1281
- if (color === "red") return "Drifted";
1282
- if (color === "amber") return "Drifting";
1283
- return "Fresh";
1284
- }
1285
-
1286
- // Echo's voice: one line that says how it is AND what to do — replaces the old pile of
1287
- // metaphor headline + callout + advice box that all repeated the same verdict.
1288
- function leadLine(color) {
1289
- if (color === "red") return "Old context is crowding the window. Renew and I carry your work over.";
1290
- if (color === "amber") return "Getting full. Wrap this thought; a fresh start soon keeps it snappy.";
1291
- return "All clear. I am taking notes as you go.";
1292
- }
1293
-
1294
- function evidenceLine(score, oldPct, repeatCount) {
1295
- const repeat = repeatCount > 0 ? repeatCount + " repeated read" + (repeatCount === 1 ? "" : "s") : "no re-reads yet";
1296
- return fmt(score.pollutionTok) + " old context · " + repeat;
1297
- }
1298
-
1299
- function fullnessLine(sat, score) {
1300
- if (!score.modelContextWindow) return fmt(score.ctTokens) + " context tokens";
1301
- const room = sat >= 85 ? " - compaction near" : sat <= 30 ? " - plenty of room" : "";
1302
- return sat + "% full - " + fmt(score.ctTokens) + " / " + fmt(score.modelContextWindow) + room;
1303
1340
  }
1304
1341
 
1305
1342
  function fullnessPct(score) {
@@ -1308,10 +1345,10 @@ export const HUD_HTML = String.raw `<!doctype html>
1308
1345
  return 0;
1309
1346
  }
1310
1347
 
1311
- function tone(color) {
1312
- if (color === "red") return "#c7372f";
1313
- if (color === "amber") return "#8a6a12";
1314
- return "#2f7d4f";
1348
+ function healthScore(score) {
1349
+ if (typeof score.healthScorePct === "number" && isFinite(score.healthScorePct)) return clamp(score.healthScorePct, 0, 100);
1350
+ if (typeof score.usefulPct === "number" && isFinite(score.usefulPct)) return clamp(score.usefulPct, 0, 100);
1351
+ return 0;
1315
1352
  }
1316
1353
 
1317
1354
  function ctSourceLabel(src) {
@@ -1324,13 +1361,23 @@ export const HUD_HTML = String.raw `<!doctype html>
1324
1361
  return "Not seeing " + list.map(label).join(", ") + " yet";
1325
1362
  }
1326
1363
 
1327
- // Subtitle: which session this is (client + project) + version. The "other clients working"
1328
- // signal now lives in the live-spinner tabs, so it's dropped here to keep the line short.
1329
- function sessionIdentity(score) {
1330
- const parts = [label(score.client)];
1331
- if (score.label) parts.push(score.label);
1332
- parts.push(hudVersion);
1333
- return parts.join(" · ");
1364
+ function activeSessionInfo(score, state) {
1365
+ const list = (state && state.recentSessions) || [];
1366
+ const match = list.filter((s) => s.client === score.client && s.sourcePath === score.sourcePath)[0];
1367
+ const repo = String((match && match.label) || score.label || "");
1368
+ const title = String((match && match.title) || repo || label(score.client));
1369
+ return {
1370
+ client: label(score.client),
1371
+ clientRaw: score.client,
1372
+ title,
1373
+ repo,
1374
+ };
1375
+ }
1376
+
1377
+ function miniTitle(info) {
1378
+ if (!info) return "EchoMem";
1379
+ const title = info.title && info.title !== info.client ? info.title : info.repo;
1380
+ return title || info.client;
1334
1381
  }
1335
1382
 
1336
1383
  function renderSwitchButton(state, activeClient) {
@@ -1361,21 +1408,28 @@ export const HUD_HTML = String.raw `<!doctype html>
1361
1408
  return list.filter((s) => agentFamily(s.client) === targetFamily)[0] || null;
1362
1409
  }
1363
1410
 
1364
- // Recent sessions as switchable tabs. "Auto" clears the pin. Hidden when there's only one
1411
+ // Recent sessions as switchable rows. "Auto" clears the pin. Hidden when there's only one
1365
1412
  // session for the active agent family, so Codex never recommends Claude sessions or vice versa.
1366
- function renderTabs(state, activeId, activeClient) {
1367
- if (!tabsEl) return;
1413
+ function renderTabs(state, activeId, activeClient, viewColor) {
1414
+ if (!tabsEl) return false;
1368
1415
  const list = visibleRecentSessions(state, activeClient);
1369
- if (list.length <= 1) { tabsEl.innerHTML = ""; return; }
1416
+ if (list.length <= 1) { tabsEl.innerHTML = ""; return false; }
1370
1417
  const pinnedId = state && state.pinnedId ? state.pinnedId : null;
1371
1418
  const chips = list.map((s) => {
1372
- const cls = s.id === pinnedId ? "tab active" : !pinnedId && s.id === activeId ? "tab auto-pick" : "tab";
1373
- const dot = s.live ? '<span class="spin"></span>' : "";
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>' : "";
1374
1423
  const title = s.title || label(s.client);
1375
- return '<button class="' + cls + '" data-id="' + esc(s.id) + '" title="' + esc(label(s.client) + " · " + title) + '">' + dot + esc(shortTitle(title)) + "</button>";
1424
+ const repo = s.label && s.label !== title ? s.label : "";
1425
+ 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 : "")) + '">'
1427
+ + '<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>";
1376
1429
  });
1377
- chips.unshift('<button class="tab' + (pinnedId ? "" : " active") + '" data-id="auto" title="Auto-select the current foreground agent session">Auto</button>');
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>');
1378
1431
  tabsEl.innerHTML = chips.join("");
1432
+ return true;
1379
1433
  }
1380
1434
  function visibleRecentSessions(state, activeClient) {
1381
1435
  const list = (state && state.recentSessions) || [];
@@ -1432,14 +1486,6 @@ export const HUD_HTML = String.raw `<!doctype html>
1432
1486
  return String(Math.round(n));
1433
1487
  }
1434
1488
 
1435
- function signedFmt(value) {
1436
- return (value >= 0 ? "+" : "-") + fmt(Math.abs(value));
1437
- }
1438
-
1439
- function joinParts(parts) {
1440
- return parts.filter(Boolean).join(" - ");
1441
- }
1442
-
1443
1489
  function clamp(value, min, max) {
1444
1490
  return Math.max(min, Math.min(max, Number(value) || 0));
1445
1491
  }