@echomem/mcp 1.4.7 → 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.
- package/README.md +35 -9
- package/assets/canonical-scorer/README.md +18 -0
- package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
- package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
- package/assets/canonical-scorer/golden_anchors.mjs +83 -0
- package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
- package/assets/hud/claude.svg +1 -0
- package/assets/hud/codex.svg +1 -0
- package/assets/hud/session-viewer.html +35 -0
- package/dist/city/chaos-to-clarity-pencil.html +582 -0
- package/dist/city/echo-ai-city-only.html +1126 -109
- package/dist/city/echo-ai-city-only.template.html +1126 -109
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/city/pencil-pie-generator.html +883 -0
- package/dist/city/pencil-webgl-landscape.html +1239 -0
- package/dist/city/spatial-fan-story.html +479 -0
- package/dist/codex-session-files.js +283 -0
- package/dist/codex-sync.js +7 -2
- package/dist/context-analysis/canonical-golden.js +47 -0
- package/dist/context-analysis/claude-native-canonical.js +1193 -0
- package/dist/context-analysis/vendored-canonical.js +793 -0
- package/dist/context-analysis/workspace-report.js +1838 -0
- package/dist/context-metrics/calculate.js +56 -0
- package/dist/context-metrics/model-limits.js +26 -0
- package/dist/context-metrics/types.js +1 -0
- package/dist/forensics-10-problems.js +7 -6
- package/dist/forensics.js +863 -132
- package/dist/hud/adapters.js +8 -4
- package/dist/hud/autostart.js +66 -0
- package/dist/hud/cli.js +31 -0
- package/dist/hud/electron-main.js +182 -19
- package/dist/hud/metric.js +13 -4
- package/dist/hud/monitor.js +171 -84
- package/dist/hud/preload.cjs +3 -0
- package/dist/hud/server.js +321 -4
- package/dist/hud/web.js +880 -270
- package/dist/index.js +122 -24
- package/dist/local-data-paths.js +87 -0
- package/dist/migrate.js +55 -29
- package/dist/report.js +101 -40
- package/dist/setup-page.js +4257 -245
- package/dist/setup-preview.js +245 -0
- package/dist/setup.js +786 -75
- package/dist/v1-contract.js +20 -2
- package/package.json +6 -4
- 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:
|
|
133
|
-
padding:
|
|
136
|
+
gap: 2px;
|
|
137
|
+
padding: 15px 38px 11px 30px;
|
|
134
138
|
}
|
|
135
139
|
.dot {
|
|
136
140
|
position: absolute;
|
|
137
|
-
left:
|
|
138
|
-
top:
|
|
141
|
+
left: 13px;
|
|
142
|
+
top: 22px;
|
|
139
143
|
z-index: 3;
|
|
140
|
-
width:
|
|
141
|
-
height:
|
|
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
|
|
149
|
-
|
|
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,20 +174,37 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
163
174
|
overflow: hidden;
|
|
164
175
|
text-overflow: ellipsis;
|
|
165
176
|
white-space: nowrap;
|
|
166
|
-
font-
|
|
167
|
-
font-
|
|
168
|
-
letter-spacing: 0
|
|
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
|
}
|
|
193
|
+
.client.voice {
|
|
194
|
+
font-family: inherit;
|
|
195
|
+
font-size: 11.5px;
|
|
196
|
+
font-weight: 750;
|
|
197
|
+
letter-spacing: 0.01em;
|
|
198
|
+
text-transform: none;
|
|
199
|
+
color: var(--brick);
|
|
200
|
+
}
|
|
173
201
|
.details {
|
|
174
202
|
display: none;
|
|
175
203
|
position: relative;
|
|
176
|
-
margin: 2px 8px 0
|
|
204
|
+
margin: 2px 8px 0 8px;
|
|
177
205
|
padding: 16px 17px 15px;
|
|
178
206
|
border: 1px solid var(--line);
|
|
179
|
-
border-radius:
|
|
207
|
+
border-radius: 12px;
|
|
180
208
|
background: var(--card);
|
|
181
209
|
color: var(--ink);
|
|
182
210
|
filter: var(--shadow);
|
|
@@ -197,28 +225,139 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
197
225
|
font-size: 11px;
|
|
198
226
|
color: var(--faint);
|
|
199
227
|
}
|
|
200
|
-
.tabs {
|
|
201
|
-
|
|
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); }
|
|
202
241
|
.tab {
|
|
203
|
-
|
|
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;
|
|
204
244
|
border: 1px solid rgba(0, 0, 0, .12); background: rgba(0, 0, 0, .04);
|
|
205
|
-
color: var(--faint); cursor: pointer;
|
|
245
|
+
color: var(--faint); cursor: pointer; text-align: left; font-family: inherit;
|
|
206
246
|
}
|
|
207
247
|
.tab:hover { background: rgba(0, 0, 0, .08); }
|
|
208
|
-
.tab.
|
|
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; }
|
|
209
296
|
.spin {
|
|
210
|
-
display: inline-block; width:
|
|
297
|
+
display: inline-block; width: 8px; height: 8px; margin-left: 4px; vertical-align: -1px;
|
|
211
298
|
border: 1.5px solid rgba(0, 0, 0, .2); border-top-color: #1a3a8f; border-radius: 50%;
|
|
212
299
|
animation: echospin .8s linear infinite;
|
|
213
300
|
}
|
|
214
301
|
@keyframes echospin { to { transform: rotate(360deg); } }
|
|
302
|
+
/* Mini mode: a small always-on pill instead of the full mascot+bubble. Opening the panel
|
|
303
|
+
temporarily shows the full layout; closing returns to the pill. */
|
|
304
|
+
#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;
|
|
307
|
+
border: 1.6px solid rgba(26, 58, 143, 0.75); border-radius: 999px; background: #fdfaf2;
|
|
308
|
+
filter: var(--shadow); color: var(--ink); letter-spacing: 0;
|
|
309
|
+
}
|
|
310
|
+
#miniPill .mdot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); flex: 0 0 auto; }
|
|
311
|
+
#miniPill .mdot.amber { background: var(--amber); }
|
|
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; }
|
|
325
|
+
#hud.mini:not(.open) .mascot-stage, #hud.mini:not(.open) .status-bubble, #hud.mini:not(.open) .drag { display: none; }
|
|
326
|
+
#hud.mini:not(.open) #miniPill { display: inline-flex; }
|
|
327
|
+
.cornerbtn {
|
|
328
|
+
position: absolute; top: 7px; right: 10px; z-index: 3;
|
|
329
|
+
width: 20px; height: 20px; border-radius: 50%; padding: 0; line-height: 1; font-size: 12px;
|
|
330
|
+
border: 1px solid rgba(0, 0, 0, .16); background: rgba(255, 255, 255, .7); color: var(--faint);
|
|
331
|
+
cursor: pointer; font-family: inherit;
|
|
332
|
+
}
|
|
333
|
+
.cornerbtn:hover { background: #fff; color: #111; }
|
|
215
334
|
.headctl { display: flex; align-items: center; gap: 8px; }
|
|
216
335
|
.switchbtn {
|
|
336
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
217
337
|
width: 22px; height: 22px; border-radius: 50%; padding: 0; line-height: 1; font-size: 12px;
|
|
218
338
|
border: 1px solid rgba(0, 0, 0, .2); background: rgba(0, 0, 0, .05); color: var(--faint);
|
|
219
339
|
cursor: pointer; font-family: inherit;
|
|
220
340
|
}
|
|
221
341
|
.switchbtn:hover { background: rgba(0, 0, 0, .1); color: #111; }
|
|
342
|
+
.agent-switch {
|
|
343
|
+
width: 28px;
|
|
344
|
+
min-width: 28px;
|
|
345
|
+
max-width: 28px;
|
|
346
|
+
height: 28px;
|
|
347
|
+
padding: 0;
|
|
348
|
+
border-radius: 999px;
|
|
349
|
+
background: rgba(255, 255, 255, .78);
|
|
350
|
+
color: #1f1f1f;
|
|
351
|
+
font-size: 10.5px;
|
|
352
|
+
font-weight: 760;
|
|
353
|
+
letter-spacing: 0;
|
|
354
|
+
white-space: nowrap;
|
|
355
|
+
}
|
|
356
|
+
.agent-switch:hover { background: #fff; border-color: rgba(26, 58, 143, .34); }
|
|
357
|
+
.agent-switch[data-target-family="claude"] { border-color: rgba(217, 119, 87, .42); }
|
|
358
|
+
.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; }
|
|
222
361
|
.reveal {
|
|
223
362
|
margin-top: 8px; font-size: 10.5px; padding: 4px 11px; border-radius: 999px;
|
|
224
363
|
border: 1px solid rgba(0, 0, 0, .12); background: rgba(0, 0, 0, .04); color: var(--faint);
|
|
@@ -229,87 +368,75 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
229
368
|
display: flex;
|
|
230
369
|
align-items: center;
|
|
231
370
|
gap: 9px;
|
|
232
|
-
margin-bottom:
|
|
371
|
+
margin-bottom: 9px;
|
|
233
372
|
}
|
|
234
373
|
.state-dot {
|
|
235
|
-
width:
|
|
236
|
-
height:
|
|
374
|
+
width: 12px;
|
|
375
|
+
height: 12px;
|
|
237
376
|
flex: none;
|
|
238
377
|
border-radius: 999px;
|
|
239
378
|
background: var(--green);
|
|
240
379
|
box-shadow: 0 0 0 1px rgba(26, 58, 143, 0.12);
|
|
241
380
|
}
|
|
242
381
|
.state-dot.amber { background: var(--amber); }
|
|
243
|
-
.state-dot.red { background: #ff5f57; }
|
|
244
382
|
.state-word {
|
|
245
383
|
min-width: 0;
|
|
246
384
|
overflow: hidden;
|
|
247
385
|
text-overflow: ellipsis;
|
|
248
386
|
white-space: nowrap;
|
|
249
|
-
font-
|
|
250
|
-
font-
|
|
251
|
-
|
|
252
|
-
.full {
|
|
253
|
-
min-height: 17px;
|
|
254
|
-
margin-bottom: 14px;
|
|
255
|
-
font-size: 12px;
|
|
256
|
-
font-weight: 650;
|
|
257
|
-
color: #4a7a3e;
|
|
387
|
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
388
|
+
font-size: 18px;
|
|
389
|
+
font-weight: 850;
|
|
258
390
|
}
|
|
259
|
-
.
|
|
260
|
-
margin-
|
|
261
|
-
|
|
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;
|
|
262
400
|
font-weight: 750;
|
|
263
|
-
|
|
401
|
+
cursor: pointer;
|
|
264
402
|
}
|
|
265
|
-
.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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;
|
|
269
410
|
font-size: 12.5px;
|
|
270
|
-
|
|
411
|
+
font-weight: 800;
|
|
412
|
+
color: var(--ink);
|
|
271
413
|
}
|
|
272
|
-
.
|
|
273
|
-
|
|
274
|
-
height: 9px;
|
|
275
|
-
margin: 0;
|
|
414
|
+
.lead {
|
|
415
|
+
margin-bottom: 0;
|
|
276
416
|
overflow: hidden;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
height: 100%;
|
|
283
|
-
min-width: 0;
|
|
284
|
-
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);
|
|
285
422
|
}
|
|
286
|
-
.
|
|
287
|
-
.oldbar .old { width: 0; background: var(--brick); }
|
|
288
|
-
.alert {
|
|
423
|
+
.evidence {
|
|
289
424
|
display: none;
|
|
290
|
-
margin
|
|
291
|
-
padding: 9px
|
|
292
|
-
border
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
margin-bottom: 4px;
|
|
296
|
-
color: var(--brick);
|
|
297
|
-
font-size: 12.5px;
|
|
298
|
-
font-weight: 750;
|
|
299
|
-
}
|
|
300
|
-
.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);
|
|
301
430
|
color: var(--muted);
|
|
302
431
|
font-size: 12px;
|
|
303
|
-
line-height: 1.
|
|
304
|
-
}
|
|
305
|
-
.nudge {
|
|
306
|
-
display: none;
|
|
307
|
-
margin-top: 13px;
|
|
308
|
-
padding: 10px 12px;
|
|
309
|
-
border-radius: 12px;
|
|
310
|
-
font-size: 12.5px;
|
|
311
|
-
line-height: 1.45;
|
|
432
|
+
line-height: 1.5;
|
|
312
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; }
|
|
313
440
|
.nudge.calm {
|
|
314
441
|
border: 1px solid rgba(26, 58, 143, 0.14);
|
|
315
442
|
background: rgba(26, 58, 143, 0.05);
|
|
@@ -324,29 +451,48 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
324
451
|
display: none;
|
|
325
452
|
width: 100%;
|
|
326
453
|
margin-top: 12px;
|
|
327
|
-
border: 1px solid rgba(
|
|
454
|
+
border: 1px solid rgba(26, 58, 143, 0.26);
|
|
328
455
|
border-radius: 999px;
|
|
329
|
-
padding: 9px
|
|
330
|
-
background: rgba(
|
|
331
|
-
color: var(--
|
|
456
|
+
padding: 9px 14px;
|
|
457
|
+
background: rgba(26, 58, 143, 0.07);
|
|
458
|
+
color: var(--blue);
|
|
332
459
|
font-weight: 750;
|
|
333
|
-
font-size:
|
|
460
|
+
font-size: 12px;
|
|
334
461
|
cursor: pointer;
|
|
335
|
-
transition:
|
|
462
|
+
transition: all 0.25s ease;
|
|
336
463
|
}
|
|
337
|
-
button.renew-btn
|
|
338
|
-
|
|
339
|
-
|
|
464
|
+
button.renew-btn.urgent {
|
|
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);
|
|
472
|
+
}
|
|
473
|
+
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 {
|
|
476
|
+
display: block;
|
|
477
|
+
margin-top: 3px;
|
|
478
|
+
font-size: 10.5px;
|
|
479
|
+
font-weight: 600;
|
|
480
|
+
opacity: 0.85;
|
|
481
|
+
white-space: normal;
|
|
482
|
+
}
|
|
483
|
+
button.renew-btn:hover { background: rgba(26, 58, 143, 0.12); }
|
|
484
|
+
button.renew-btn.urgent:hover { background: #102d78; }
|
|
485
|
+
button.renew-btn.loading { width: 100%; opacity: 0.75; cursor: wait; }
|
|
340
486
|
.renew-result {
|
|
341
487
|
display: none;
|
|
342
488
|
margin-top: 10px;
|
|
343
489
|
border: 1px solid rgba(26, 58, 143, 0.18);
|
|
344
|
-
border-radius:
|
|
490
|
+
border-radius: 8px;
|
|
345
491
|
background: rgba(26, 58, 143, 0.04);
|
|
346
492
|
padding: 10px 12px;
|
|
347
493
|
}
|
|
348
494
|
.renew-result .renew-msg {
|
|
349
|
-
margin-bottom:
|
|
495
|
+
margin-bottom: 0;
|
|
350
496
|
color: var(--blue);
|
|
351
497
|
font-size: 11.5px;
|
|
352
498
|
font-weight: 750;
|
|
@@ -383,7 +529,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
383
529
|
color: var(--faint);
|
|
384
530
|
font-size: 11px;
|
|
385
531
|
}
|
|
386
|
-
.meta {
|
|
532
|
+
.meta { display: none; }
|
|
387
533
|
.path { margin-top: 8px; }
|
|
388
534
|
.missing { margin-top: 6px; }
|
|
389
535
|
#hud.s-green .mascot-stage { filter: drop-shadow(0 2px 6px rgba(67, 209, 122, 0.3)); }
|
|
@@ -426,33 +572,34 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
426
572
|
<span class="copy">
|
|
427
573
|
<span class="main" id="main">Checking context...</span>
|
|
428
574
|
<span class="client" id="client">EchoMem HUD</span>
|
|
575
|
+
<span class="client-repo" id="clientrepo"></span>
|
|
429
576
|
</span>
|
|
577
|
+
<button class="cornerbtn" id="minibtn" title="Shrink to mini pill">–</button>
|
|
578
|
+
</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>
|
|
430
584
|
</div>
|
|
431
585
|
</div>
|
|
432
586
|
<div class="details">
|
|
433
|
-
<div class="head"><span class="upd" id="updated"></span><span class="headctl"><span class="turn" id="turn"></span><button class="switchbtn" id="switchbtn" title="Switch agent" style="display:none"
|
|
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>
|
|
434
588
|
<div class="tabs" id="tabs"></div>
|
|
435
589
|
<div class="state-row">
|
|
436
590
|
<span class="state-dot" id="stateDot"></span>
|
|
437
|
-
<span class="state-word" id="stateWord"
|
|
591
|
+
<span class="state-word" id="stateWord">--%</span>
|
|
592
|
+
<button class="why-btn" id="whybtn" type="button" style="display:none">why</button>
|
|
438
593
|
</div>
|
|
439
594
|
<div class="full" id="fullness"></div>
|
|
440
|
-
<div class="lead" id="lead"
|
|
441
|
-
<div class="evidence" id="evidence"
|
|
442
|
-
<div class="oldbar" aria-hidden="true">
|
|
443
|
-
<span class="useful" id="usefulbar"></span>
|
|
444
|
-
<span class="old" id="oldbar"></span>
|
|
445
|
-
</div>
|
|
446
|
-
<div class="alert" id="alert">
|
|
447
|
-
<div class="alert-title" id="alertTitle"></div>
|
|
448
|
-
<div class="alert-detail" id="alertDetail"></div>
|
|
449
|
-
</div>
|
|
595
|
+
<div class="lead" id="lead"></div>
|
|
596
|
+
<div class="evidence" id="evidence"></div>
|
|
450
597
|
<div class="nudge" id="nudge"></div>
|
|
451
|
-
<button class="renew-btn" id="renewbtn">
|
|
598
|
+
<button class="renew-btn" id="renewbtn">Start new with context</button>
|
|
452
599
|
<div class="renew-result" id="renewresult">
|
|
453
600
|
<div class="renew-msg" id="renewmsg"></div>
|
|
454
601
|
<pre id="renewpre"></pre>
|
|
455
|
-
<button class="copy-btn" id="copybtn">Copy
|
|
602
|
+
<button class="copy-btn" id="copybtn">Copy</button>
|
|
456
603
|
</div>
|
|
457
604
|
<div class="meta" id="meta"></div>
|
|
458
605
|
<div class="path" id="path"></div>
|
|
@@ -465,17 +612,14 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
465
612
|
const dot = document.getElementById("dot");
|
|
466
613
|
const main = document.getElementById("main");
|
|
467
614
|
const client = document.getElementById("client");
|
|
615
|
+
const clientRepo = document.getElementById("clientrepo");
|
|
468
616
|
const turnEl = document.getElementById("turn");
|
|
469
617
|
const stateDot = document.getElementById("stateDot");
|
|
470
618
|
const stateWordEl = document.getElementById("stateWord");
|
|
619
|
+
const whyBtn = document.getElementById("whybtn");
|
|
471
620
|
const fullness = document.getElementById("fullness");
|
|
472
621
|
const lead = document.getElementById("lead");
|
|
473
622
|
const evidence = document.getElementById("evidence");
|
|
474
|
-
const usefulbar = document.getElementById("usefulbar");
|
|
475
|
-
const oldbar = document.getElementById("oldbar");
|
|
476
|
-
const alertEl = document.getElementById("alert");
|
|
477
|
-
const alertTitle = document.getElementById("alertTitle");
|
|
478
|
-
const alertDetail = document.getElementById("alertDetail");
|
|
479
623
|
const nudge = document.getElementById("nudge");
|
|
480
624
|
const renewBtn = document.getElementById("renewbtn");
|
|
481
625
|
const renewResult = document.getElementById("renewresult");
|
|
@@ -489,12 +633,55 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
489
633
|
const switchBtn = document.getElementById("switchbtn");
|
|
490
634
|
const revealEl = document.getElementById("reveal");
|
|
491
635
|
const missing = document.getElementById("missing");
|
|
492
|
-
const hudVersion = "v${MCP_PACKAGE_VERSION}";
|
|
493
636
|
let prevTurnState = null;
|
|
494
637
|
let lastCapsule = "";
|
|
638
|
+
let renewResultSessionKey = "";
|
|
495
639
|
let activityEpoch = null;
|
|
496
640
|
let lastState = null;
|
|
497
641
|
let currentSrc = null;
|
|
642
|
+
let renewTimer = null;
|
|
643
|
+
let renewStartMs = 0;
|
|
644
|
+
let renewEta = null;
|
|
645
|
+
let renewEstimateSeq = 0;
|
|
646
|
+
const CHECKPOINT_CACHE_KEY = "echomemHudCheckpointCacheV2";
|
|
647
|
+
const CHECKPOINT_CACHE_LIMIT = 24;
|
|
648
|
+
const CHECKPOINT_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
649
|
+
const CHECKPOINT_STATUS_CACHE_KEY = "echomemHudCheckpointStatusV2";
|
|
650
|
+
const CHECKPOINT_STATUS_CACHE_LIMIT = 48;
|
|
651
|
+
const CHECKPOINT_STATUS_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
652
|
+
const statusFetchInFlight = {};
|
|
653
|
+
// Demo-clean mode: hide only the research-y session VIEWER (raw log / payload). Keep the session
|
|
654
|
+
// tabs + agent-switch — they're the manual override for "we can't detect which session you're
|
|
655
|
+
// viewing", so the user can always point the HUD at the right session. Flip false to show the viewer.
|
|
656
|
+
const DEMO_CLEAN = true;
|
|
657
|
+
|
|
658
|
+
// Mini mode: resting state is a small pill; opening the panel shows the full layout, closing
|
|
659
|
+
// returns to the pill. Preference persists (localStorage here, window size in main's bounds file).
|
|
660
|
+
const miniBtn = document.getElementById("minibtn");
|
|
661
|
+
const mdot = document.getElementById("mdot");
|
|
662
|
+
const micon = document.getElementById("micon");
|
|
663
|
+
const mscore = document.getElementById("mscore");
|
|
664
|
+
const msession = document.getElementById("msession");
|
|
665
|
+
const mrepo = document.getElementById("mrepo");
|
|
666
|
+
let miniOn = false;
|
|
667
|
+
let whyOpen = false;
|
|
668
|
+
try { miniOn = localStorage.getItem("echomemHudMini") === "1"; } catch {}
|
|
669
|
+
if (miniOn) hud.classList.add("mini");
|
|
670
|
+
function applyMini(next) {
|
|
671
|
+
miniOn = next;
|
|
672
|
+
try { localStorage.setItem("echomemHudMini", next ? "1" : "0"); } catch {}
|
|
673
|
+
hud.classList.toggle("mini", next);
|
|
674
|
+
if (next) hud.classList.remove("open");
|
|
675
|
+
if (window.echomemHud && typeof window.echomemHud.setMini === "function") window.echomemHud.setMini(next);
|
|
676
|
+
}
|
|
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
|
+
});
|
|
498
685
|
let hudDrag = null;
|
|
499
686
|
let suppressNextToggle = false;
|
|
500
687
|
const dragBlockSelector = "button, input, textarea, select, a, pre, .tab, .switchbtn, .reveal, .renew-btn, .copy-btn";
|
|
@@ -509,7 +696,8 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
509
696
|
if (event.target.closest(dragBlockSelector)) return;
|
|
510
697
|
if (!window.echomemHud || typeof window.echomemHud.startDrag !== "function") return;
|
|
511
698
|
hudDrag = { pointerId: event.pointerId, startX: event.screenX, startY: event.screenY, moved: false };
|
|
512
|
-
|
|
699
|
+
// NOTE: do NOT setPointerCapture here — capturing on pointerdown re-targets the click to #hud,
|
|
700
|
+
// so #toggle never gets it and the panel can't expand. Capture only once a real drag starts.
|
|
513
701
|
window.echomemHud.startDrag(event.screenX, event.screenY);
|
|
514
702
|
}
|
|
515
703
|
|
|
@@ -519,6 +707,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
519
707
|
const dx = event.screenX - hudDrag.startX;
|
|
520
708
|
const dy = event.screenY - hudDrag.startY;
|
|
521
709
|
if (!hudDrag.moved && Math.hypot(dx, dy) < 3) return;
|
|
710
|
+
if (!hudDrag.moved) { try { hud.setPointerCapture(hudDrag.pointerId); } catch {} }
|
|
522
711
|
hudDrag.moved = true;
|
|
523
712
|
hud.classList.add("dragging");
|
|
524
713
|
window.echomemHud.moveDrag(event.screenX, event.screenY);
|
|
@@ -544,17 +733,11 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
544
733
|
fetch("/open-viewer?path=" + encodeURIComponent(currentSrc)).catch(() => {});
|
|
545
734
|
});
|
|
546
735
|
|
|
547
|
-
//
|
|
736
|
+
// Jump to the other agent family while the tabs stay scoped to the family you're viewing.
|
|
548
737
|
if (switchBtn) switchBtn.addEventListener("click", (e) => {
|
|
549
738
|
e.stopPropagation();
|
|
550
739
|
const st = lastState;
|
|
551
|
-
|
|
552
|
-
const clients = [];
|
|
553
|
-
for (const s of st.recentSessions) if (clients.indexOf(s.client) < 0) clients.push(s.client);
|
|
554
|
-
if (clients.length < 2) return;
|
|
555
|
-
const cur = st.active ? st.active.client : clients[0];
|
|
556
|
-
const next = clients[(clients.indexOf(cur) + 1) % clients.length];
|
|
557
|
-
const target = st.recentSessions.filter((s) => s.client === next)[0]; // newest of that agent
|
|
740
|
+
const target = switchTarget(st, st && st.active && st.active.client);
|
|
558
741
|
if (!target) return;
|
|
559
742
|
fetch("/select?id=" + encodeURIComponent(target.id))
|
|
560
743
|
.then(() => fetch("/state"))
|
|
@@ -582,35 +765,83 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
582
765
|
event.stopPropagation();
|
|
583
766
|
return;
|
|
584
767
|
}
|
|
585
|
-
if (event.target instanceof HTMLElement && event.target.closest(".switchbtn")) return;
|
|
768
|
+
if (event.target instanceof HTMLElement && (event.target.closest(".switchbtn") || event.target.closest(".cornerbtn"))) return;
|
|
586
769
|
hud.classList.toggle("open");
|
|
587
770
|
syncHeight();
|
|
588
771
|
});
|
|
589
772
|
|
|
773
|
+
let renewUrgent = false;
|
|
774
|
+
function refreshRenewLabel() {
|
|
775
|
+
if (!renewBtn) return;
|
|
776
|
+
renewBtn.classList.toggle("urgent", renewUrgent);
|
|
777
|
+
if (renewStartMs) {
|
|
778
|
+
renderRenewLoadingLabel();
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
renewBtn.textContent = "Start new with context";
|
|
782
|
+
}
|
|
783
|
+
|
|
590
784
|
if (renewBtn) renewBtn.addEventListener("click", async () => {
|
|
785
|
+
if (renewStartMs) return;
|
|
786
|
+
const stateAtRenew = lastState;
|
|
787
|
+
const scoreAtRenew = activeRenewScore();
|
|
788
|
+
const renewSessionKey = sessionKeyFor(stateAtRenew, scoreAtRenew);
|
|
591
789
|
renewBtn.classList.add("loading");
|
|
592
|
-
|
|
790
|
+
clearRenewResult();
|
|
791
|
+
startRenewTimer(scoreAtRenew);
|
|
593
792
|
try {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
793
|
+
// Cloud extraction (coding checkpoint) produces the real decision-log carryover. If that is
|
|
794
|
+
// unavailable, fall back transparently to a basic local capsule instead of the same artifact.
|
|
795
|
+
let prompt = "";
|
|
796
|
+
let promptKind = "cloud";
|
|
797
|
+
let renewData = null;
|
|
798
|
+
try {
|
|
799
|
+
const res = await fetch("/renew");
|
|
800
|
+
const data = await res.json();
|
|
801
|
+
if (data.ok && data.carryover) {
|
|
802
|
+
prompt = data.carryover;
|
|
803
|
+
renewData = data;
|
|
804
|
+
}
|
|
805
|
+
} catch {}
|
|
806
|
+
if (!prompt) {
|
|
807
|
+
const cap = await (await fetch("/capsule")).json();
|
|
808
|
+
if (cap.ok && cap.capsule) {
|
|
809
|
+
promptKind = "local";
|
|
810
|
+
prompt = localCapsulePrompt(cap.capsule);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (!prompt) {
|
|
599
814
|
return;
|
|
600
815
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
816
|
+
if (renewSessionKey !== sessionKeyFor(lastState, activeRenewScore())) return;
|
|
817
|
+
const meta = checkpointMetaFor(scoreAtRenew);
|
|
818
|
+
if (renewData) {
|
|
819
|
+
meta.contextId = typeof renewData.contextId === "string" ? renewData.contextId : "";
|
|
820
|
+
meta.sessionMemoryCount = Number.isFinite(Number(renewData.sessionMemoryCount)) ? Number(renewData.sessionMemoryCount) : 0;
|
|
821
|
+
meta.saved = Number.isFinite(Number(renewData.saved)) ? Number(renewData.saved) : 0;
|
|
822
|
+
meta.renewedAt = Date.now();
|
|
823
|
+
}
|
|
824
|
+
saveCachedCheckpoint(renewSessionKey, prompt, promptKind, meta);
|
|
825
|
+
if (promptKind === "cloud") saveCheckpointStatus(renewSessionKey, statusFromRenewData(renewData, meta));
|
|
826
|
+
showRenewResult({
|
|
827
|
+
sessionKey: renewSessionKey,
|
|
828
|
+
prompt,
|
|
829
|
+
message: promptKind === "local" ? "Local capsule ready" : "Decision log ready",
|
|
830
|
+
canCopy: true,
|
|
831
|
+
});
|
|
606
832
|
try {
|
|
607
833
|
await navigator.clipboard.writeText(prompt);
|
|
608
|
-
if (renewMsg) renewMsg.textContent = "
|
|
609
|
-
} catch {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
834
|
+
if (renewMsg) renewMsg.textContent = promptKind === "local" ? "Local capsule copied" : "Decision log copied";
|
|
835
|
+
} catch {
|
|
836
|
+
if (renewMsg) renewMsg.textContent = promptKind === "local" ? "Local capsule ready" : "Decision log ready";
|
|
837
|
+
}
|
|
838
|
+
} catch {
|
|
839
|
+
} finally {
|
|
840
|
+
stopRenewTimer();
|
|
841
|
+
renewBtn.classList.remove("loading");
|
|
842
|
+
refreshRenewLabel();
|
|
843
|
+
syncHeight();
|
|
844
|
+
}
|
|
614
845
|
});
|
|
615
846
|
|
|
616
847
|
if (copyBtn) copyBtn.addEventListener("click", async () => {
|
|
@@ -618,66 +849,422 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
618
849
|
try {
|
|
619
850
|
await navigator.clipboard.writeText(lastCapsule);
|
|
620
851
|
copyBtn.textContent = "Copied";
|
|
621
|
-
setTimeout(() => { copyBtn.textContent = "Copy
|
|
852
|
+
setTimeout(() => { copyBtn.textContent = "Copy"; }, 1500);
|
|
622
853
|
} catch {}
|
|
623
854
|
});
|
|
624
855
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
856
|
+
function activeRenewScore() {
|
|
857
|
+
const st = lastState || {};
|
|
858
|
+
return st.active || st;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function sessionKeyFor(state, score) {
|
|
862
|
+
if (!score || !score.sourcePath) return "";
|
|
863
|
+
const clientName = String(score.client || "");
|
|
864
|
+
const src = String(score.sourcePath || "");
|
|
865
|
+
const list = (state && state.recentSessions) || [];
|
|
866
|
+
const match = list.filter((s) => s.client === clientName && s.sourcePath === src)[0];
|
|
867
|
+
return clientName + "::" + (match && match.id ? match.id : src);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function clearRenewResult() {
|
|
871
|
+
lastCapsule = "";
|
|
872
|
+
renewResultSessionKey = "";
|
|
873
|
+
if (renewPre) {
|
|
874
|
+
renewPre.textContent = "";
|
|
875
|
+
renewPre.style.display = "none";
|
|
876
|
+
}
|
|
877
|
+
if (renewMsg) renewMsg.textContent = "";
|
|
878
|
+
if (copyBtn) {
|
|
879
|
+
copyBtn.textContent = "Copy";
|
|
880
|
+
copyBtn.style.display = "";
|
|
881
|
+
}
|
|
882
|
+
if (renewResult) renewResult.style.display = "none";
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function showRenewResult(opts) {
|
|
886
|
+
const prompt = String((opts && opts.prompt) || "");
|
|
887
|
+
const body = String((opts && opts.body) || prompt || "");
|
|
888
|
+
lastCapsule = prompt;
|
|
889
|
+
renewResultSessionKey = String((opts && opts.sessionKey) || "");
|
|
890
|
+
if (renewMsg) renewMsg.textContent = String((opts && opts.message) || "");
|
|
891
|
+
if (renewPre) {
|
|
892
|
+
renewPre.textContent = body;
|
|
893
|
+
renewPre.style.display = "none";
|
|
894
|
+
}
|
|
895
|
+
if (copyBtn) {
|
|
896
|
+
copyBtn.textContent = "Copy";
|
|
897
|
+
copyBtn.style.display = opts && opts.canCopy ? "inline-block" : "none";
|
|
898
|
+
}
|
|
899
|
+
if (renewResult) renewResult.style.display = "block";
|
|
900
|
+
syncHeight();
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function checkpointMetaFor(score) {
|
|
904
|
+
return {
|
|
905
|
+
turn: Number(score && score.turn) || 0,
|
|
906
|
+
ctTokens: Number(score && score.ctTokens) || 0,
|
|
907
|
+
createdAt: Date.now(),
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function saveCachedCheckpoint(key, prompt, kind, meta) {
|
|
912
|
+
if (!key || !prompt) return;
|
|
913
|
+
const next = readCheckpointCache().filter((item) => item.key !== key);
|
|
914
|
+
next.unshift({
|
|
915
|
+
key,
|
|
916
|
+
prompt: String(prompt),
|
|
917
|
+
kind: kind === "local" ? "local" : "cloud",
|
|
918
|
+
turn: Number(meta && meta.turn) || 0,
|
|
919
|
+
ctTokens: Number(meta && meta.ctTokens) || 0,
|
|
920
|
+
createdAt: Number(meta && meta.createdAt) || Date.now(),
|
|
921
|
+
renewedAt: Number(meta && meta.renewedAt) || Number(meta && meta.createdAt) || Date.now(),
|
|
922
|
+
contextId: typeof (meta && meta.contextId) === "string" ? meta.contextId : "",
|
|
923
|
+
sessionMemoryCount: Number.isFinite(Number(meta && meta.sessionMemoryCount)) ? Number(meta.sessionMemoryCount) : null,
|
|
924
|
+
saved: Number.isFinite(Number(meta && meta.saved)) ? Number(meta.saved) : null,
|
|
925
|
+
});
|
|
926
|
+
try { localStorage.setItem(CHECKPOINT_CACHE_KEY, JSON.stringify(next.slice(0, CHECKPOINT_CACHE_LIMIT))); } catch {}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function readCheckpointCache() {
|
|
930
|
+
try {
|
|
931
|
+
const cutoff = Date.now() - CHECKPOINT_CACHE_TTL_MS;
|
|
932
|
+
const raw = JSON.parse(localStorage.getItem(CHECKPOINT_CACHE_KEY) || "[]");
|
|
933
|
+
return Array.isArray(raw)
|
|
934
|
+
? raw.filter((item) => item && typeof item.key === "string" && typeof item.prompt === "string" && Number(item.createdAt || 0) >= cutoff)
|
|
935
|
+
: [];
|
|
936
|
+
} catch {
|
|
937
|
+
return [];
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function cachedCheckpointFor(key) {
|
|
942
|
+
return readCheckpointCache().filter((item) => item.key === key)[0] || null;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function restoreCachedCheckpointFor(score, key) {
|
|
946
|
+
if (!key || renewStartMs || renewResultSessionKey === key) return false;
|
|
947
|
+
const cached = cachedCheckpointFor(key);
|
|
948
|
+
if (!cached) return false;
|
|
949
|
+
showRenewResult({
|
|
950
|
+
sessionKey: key,
|
|
951
|
+
prompt: cached.prompt,
|
|
952
|
+
message: cachedCheckpointMessage(score, cached),
|
|
953
|
+
canCopy: true,
|
|
954
|
+
});
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function cachedCheckpointMessage(score, cached) {
|
|
959
|
+
const savedTurn = Number(cached && cached.turn) || 0;
|
|
960
|
+
const currentTurn = Number(score && score.turn) || 0;
|
|
961
|
+
const stale = savedTurn > 0 && currentTurn > savedTurn;
|
|
962
|
+
const summary = checkpointSummary(cached);
|
|
963
|
+
if (cached && cached.kind === "local") {
|
|
964
|
+
return savedTurn > 0 ? "Local capsule from turn " + savedTurn + checkpointSuffix(summary) : "Local capsule ready" + checkpointSuffix(summary);
|
|
965
|
+
}
|
|
966
|
+
return savedTurn > 0 && stale
|
|
967
|
+
? "Decision log from turn " + savedTurn + checkpointSuffix(summary)
|
|
968
|
+
: "Decision log ready" + checkpointSuffix(summary);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function statusFromRenewData(data, meta) {
|
|
972
|
+
return {
|
|
973
|
+
fetched: true,
|
|
974
|
+
hasCheckpoint: Boolean(data && data.ok),
|
|
975
|
+
contextId: typeof (data && data.contextId) === "string" ? data.contextId : String((meta && meta.contextId) || ""),
|
|
976
|
+
renewedAt: Number(meta && meta.renewedAt) || Date.now(),
|
|
977
|
+
memoryCount: Number.isFinite(Number(data && data.sessionMemoryCount)) ? Number(data.sessionMemoryCount) : Number(meta && meta.sessionMemoryCount) || 0,
|
|
978
|
+
checkpointMemoryId: "",
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function checkpointSummary(item) {
|
|
983
|
+
const parts = [];
|
|
984
|
+
const renewedAt = timestampMs(item && item.renewedAt);
|
|
985
|
+
if (renewedAt) parts.push("renewed " + relTime(Date.now() - renewedAt));
|
|
986
|
+
const count = Number(item && (item.memoryCount ?? item.sessionMemoryCount));
|
|
987
|
+
if (Number.isFinite(count) && count > 0) parts.push(count + " " + (count === 1 ? "memory" : "memories") + " saved");
|
|
988
|
+
return parts.join(" · ");
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function checkpointSuffix(summary) {
|
|
992
|
+
return summary ? " · " + summary : "";
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function timestampMs(value) {
|
|
996
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
997
|
+
if (typeof value === "string" && value) {
|
|
998
|
+
const ms = Date.parse(value);
|
|
999
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
1000
|
+
}
|
|
1001
|
+
return 0;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function readCheckpointStatusCache() {
|
|
1005
|
+
try {
|
|
1006
|
+
const cutoff = Date.now() - CHECKPOINT_STATUS_CACHE_TTL_MS;
|
|
1007
|
+
const raw = JSON.parse(localStorage.getItem(CHECKPOINT_STATUS_CACHE_KEY) || "[]");
|
|
1008
|
+
return Array.isArray(raw)
|
|
1009
|
+
? raw.filter((item) => item && typeof item.key === "string" && item.fetched === true && Number(item.fetchedAt || 0) >= cutoff)
|
|
1010
|
+
: [];
|
|
1011
|
+
} catch {
|
|
1012
|
+
return [];
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function cachedCheckpointStatusFor(key) {
|
|
1017
|
+
return readCheckpointStatusCache().filter((item) => item.key === key)[0] || null;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function saveCheckpointStatus(key, status) {
|
|
1021
|
+
if (!key || !status) return;
|
|
1022
|
+
const next = readCheckpointStatusCache().filter((item) => item.key !== key);
|
|
1023
|
+
const fetchedAt = status.unavailable ? Date.now() - CHECKPOINT_STATUS_CACHE_TTL_MS + 60 * 1000 : Date.now();
|
|
1024
|
+
next.unshift({
|
|
1025
|
+
key,
|
|
1026
|
+
fetched: true,
|
|
1027
|
+
fetchedAt,
|
|
1028
|
+
unavailable: Boolean(status.unavailable),
|
|
1029
|
+
hasCheckpoint: Boolean(status.hasCheckpoint),
|
|
1030
|
+
contextId: typeof status.contextId === "string" ? status.contextId : "",
|
|
1031
|
+
checkpointMemoryId: typeof status.checkpointMemoryId === "string" ? status.checkpointMemoryId : "",
|
|
1032
|
+
renewedAt: status.renewedAt || null,
|
|
1033
|
+
memoryCount: Number.isFinite(Number(status.memoryCount)) ? Number(status.memoryCount) : 0,
|
|
1034
|
+
});
|
|
1035
|
+
try { localStorage.setItem(CHECKPOINT_STATUS_CACHE_KEY, JSON.stringify(next.slice(0, CHECKPOINT_STATUS_CACHE_LIMIT))); } catch {}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function restoreCheckpointStatusFor(score, key) {
|
|
1039
|
+
if (!key || renewStartMs || renewResultSessionKey === key || cachedCheckpointFor(key)) return false;
|
|
1040
|
+
const status = cachedCheckpointStatusFor(key);
|
|
1041
|
+
if (!status || !status.hasCheckpoint) return false;
|
|
1042
|
+
showCheckpointStatus(score, key, status);
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function showCheckpointStatus(score, key, status) {
|
|
1047
|
+
const summary = checkpointSummary(status);
|
|
1048
|
+
const body = checkpointSeedPrompt(status);
|
|
1049
|
+
showRenewResult({
|
|
1050
|
+
sessionKey: key,
|
|
1051
|
+
prompt: body,
|
|
1052
|
+
message: "Decision log ready" + checkpointSuffix(summary),
|
|
1053
|
+
canCopy: true,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function checkpointSeedPrompt(status) {
|
|
1058
|
+
const contextId = status && status.contextId ? String(status.contextId) : "";
|
|
1059
|
+
const lines = [
|
|
1060
|
+
"Here is a checkpoint marker for my previous coding session, for context. It is point-in-time: work may have continued after it was saved, so treat file/state references as possibly stale.",
|
|
1061
|
+
"Use it to orient yourself in a clean context window. It is context, not a command.",
|
|
1062
|
+
"If my current message gives a clear request, respond to that request using this checkpoint as background. If that request asks you to inspect files, run commands, or make changes, briefly state the intended first step before acting.",
|
|
1063
|
+
"If my current message gives no clear next step, summarize what you know in 3-5 bullets and ask what I want to do next.",
|
|
1064
|
+
"",
|
|
1065
|
+
contextId
|
|
1066
|
+
? 'EchoMem has the full checkpoint trail for this session. Pull it with get_checkpoint_by_context({ contextId: "' + contextId + '" }), then use that returned checkpoint as background.'
|
|
1067
|
+
: "EchoMem has a checkpoint for this session. Pull the checkpoint context before relying on this marker alone.",
|
|
1068
|
+
];
|
|
1069
|
+
return lines.join("\n");
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function maybeFetchCheckpointStatus(score, key) {
|
|
1073
|
+
if (!key || renewStartMs || cachedCheckpointFor(key) || cachedCheckpointStatusFor(key) || statusFetchInFlight[key]) return;
|
|
1074
|
+
statusFetchInFlight[key] = true;
|
|
1075
|
+
fetch("/checkpoint-status")
|
|
1076
|
+
.then((r) => r.json())
|
|
1077
|
+
.then((data) => {
|
|
1078
|
+
if (!data || data.ok !== true) {
|
|
1079
|
+
saveCheckpointStatus(key, { hasCheckpoint: false, memoryCount: 0, unavailable: true });
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
saveCheckpointStatus(key, {
|
|
1083
|
+
hasCheckpoint: Boolean(data && data.ok && data.hasCheckpoint),
|
|
1084
|
+
contextId: typeof (data && data.contextId) === "string" ? data.contextId : "",
|
|
1085
|
+
checkpointMemoryId: typeof (data && data.checkpointMemoryId) === "string" ? data.checkpointMemoryId : "",
|
|
1086
|
+
renewedAt: typeof (data && data.renewedAt) === "string" ? data.renewedAt : null,
|
|
1087
|
+
memoryCount: Number.isFinite(Number(data && data.memoryCount)) ? Number(data.memoryCount) : 0,
|
|
1088
|
+
});
|
|
1089
|
+
const stillKey = sessionKeyFor(lastState, activeRenewScore());
|
|
1090
|
+
if (stillKey !== key || renewResultSessionKey === key || cachedCheckpointFor(key)) return;
|
|
1091
|
+
const status = cachedCheckpointStatusFor(key);
|
|
1092
|
+
if (status && status.hasCheckpoint) showCheckpointStatus(score, key, status);
|
|
1093
|
+
})
|
|
1094
|
+
.catch(() => {
|
|
1095
|
+
saveCheckpointStatus(key, { hasCheckpoint: false, memoryCount: 0, unavailable: true });
|
|
1096
|
+
})
|
|
1097
|
+
.finally(() => { delete statusFetchInFlight[key]; });
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function localCapsulePrompt(capsule) {
|
|
1101
|
+
return [
|
|
1102
|
+
"Basic local capsule, not a full EchoMem decision log.",
|
|
1103
|
+
"EchoMem cloud decision extraction was unavailable, so this fallback was built from local session signals such as goal, latest instruction, file anchors, and context-health numbers. It may miss decisions or product nuance from the discussion.",
|
|
1104
|
+
"Use it to orient yourself in a clean context window. The user's current message decides whether to continue, redirect, or only preserve context. Do not inspect files, run commands, or start work until intent is clear.",
|
|
1105
|
+
"",
|
|
1106
|
+
String(capsule || ""),
|
|
1107
|
+
].join("\n");
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function startRenewTimer(score) {
|
|
1111
|
+
const seq = ++renewEstimateSeq;
|
|
1112
|
+
renewEta = { ms: fallbackEtaForScore(score || activeRenewScore()) };
|
|
1113
|
+
renewStartMs = Date.now();
|
|
1114
|
+
if (renewTimer) clearInterval(renewTimer);
|
|
1115
|
+
renderRenewLoadingLabel();
|
|
1116
|
+
renewTimer = setInterval(renderRenewLoadingLabel, 1000);
|
|
1117
|
+
fetch("/renew-estimate")
|
|
1118
|
+
.then((r) => r.json())
|
|
1119
|
+
.then((data) => {
|
|
1120
|
+
if (seq !== renewEstimateSeq || !renewStartMs) return;
|
|
1121
|
+
if (data && data.ok && Number.isFinite(Number(data.etaMs))) {
|
|
1122
|
+
renewEta = { ms: Number(data.etaMs) };
|
|
1123
|
+
renderRenewLoadingLabel();
|
|
1124
|
+
}
|
|
1125
|
+
})
|
|
1126
|
+
.catch(() => {});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function stopRenewTimer() {
|
|
1130
|
+
if (renewTimer) clearInterval(renewTimer);
|
|
1131
|
+
renewTimer = null;
|
|
1132
|
+
renewStartMs = 0;
|
|
1133
|
+
renewEta = null;
|
|
1134
|
+
renewEstimateSeq += 1;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function renderRenewLoadingLabel() {
|
|
1138
|
+
if (!renewBtn || !renewStartMs) return;
|
|
1139
|
+
const elapsed = Date.now() - renewStartMs;
|
|
1140
|
+
const remaining = Math.max(0, Number(renewEta && renewEta.ms) || 20000) - elapsed;
|
|
1141
|
+
const detail = remaining > 0 ? "ETA " + fmtDuration(remaining) + " left" : "ETA any moment";
|
|
1142
|
+
renewBtn.innerHTML = "<b>Preparing context…</b><small>" + esc(detail) + "</small>";
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function fallbackEtaForScore(score) {
|
|
1146
|
+
const n = Number(score && score.ctTokens) || 0;
|
|
1147
|
+
if (n <= 5000) return 10000;
|
|
1148
|
+
if (n <= 10000) return 12000;
|
|
1149
|
+
if (n <= 25000) return 15000;
|
|
1150
|
+
if (n <= 50000) return 18000;
|
|
1151
|
+
if (n <= 100000) return 20000;
|
|
1152
|
+
return 45000;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function fmtDuration(ms) {
|
|
1156
|
+
const s = Math.max(0, Math.ceil(Number(ms || 0) / 1000));
|
|
1157
|
+
if (s < 60) return s + "s";
|
|
1158
|
+
const m = Math.floor(s / 60);
|
|
1159
|
+
const r = s % 60;
|
|
1160
|
+
return r ? m + "m " + r + "s" : m + "m";
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Dev/design preview: ?preview=green|amber|red renders that health state with sample data and
|
|
1164
|
+
// skips the live feed — same pattern as the extraction page's ?preview hook.
|
|
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");
|
|
1169
|
+
if (previewColor === "green" || previewColor === "amber" || previewColor === "red") {
|
|
1170
|
+
const sample = {
|
|
1171
|
+
client: "codex",
|
|
1172
|
+
label: "EchoMem-Chrome",
|
|
1173
|
+
sourcePath: previewPinned ? "/preview/b.jsonl" : "/preview/session.jsonl",
|
|
1174
|
+
usefulPct: previewColor === "red" ? 55 : previewColor === "amber" ? 75 : 95,
|
|
1175
|
+
healthScorePct: previewColor === "red" ? 48 : previewColor === "amber" ? 72 : 96,
|
|
1176
|
+
color: previewColor,
|
|
1177
|
+
ctTokens: previewColor === "red" ? 165000 : previewColor === "amber" ? 110000 : 24000,
|
|
1178
|
+
modelContextWindow: 258000,
|
|
1179
|
+
pollutionPct: previewColor === "red" ? 45 : previewColor === "amber" ? 24 : 4,
|
|
1180
|
+
pollutionTok: previewColor === "red" ? 74000 : previewColor === "amber" ? 26000 : 900,
|
|
1181
|
+
buckets: { range_redundant: { count: previewColor === "red" ? 61 : previewColor === "amber" ? 18 : 0 } },
|
|
1182
|
+
stats: { patchEdits: 90 },
|
|
1183
|
+
turn: 28,
|
|
1184
|
+
lastActiveMs: 6000,
|
|
1185
|
+
ctSource: "measured",
|
|
1186
|
+
};
|
|
1187
|
+
render({
|
|
1188
|
+
active: sample,
|
|
1189
|
+
pinnedId: previewPinned ? "p2" : null,
|
|
1190
|
+
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 },
|
|
1199
|
+
],
|
|
1200
|
+
});
|
|
1201
|
+
hud.classList.add("open");
|
|
1202
|
+
syncHeight();
|
|
1203
|
+
} else {
|
|
1204
|
+
const events = new EventSource("/events");
|
|
1205
|
+
events.onmessage = (event) => render(JSON.parse(event.data));
|
|
1206
|
+
fetch("/state").then((r) => r.json()).then(render).catch(() => {});
|
|
1207
|
+
}
|
|
628
1208
|
|
|
629
1209
|
function render(state) {
|
|
630
1210
|
const score = state.active || state;
|
|
631
1211
|
lastState = state;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
1212
|
+
const activeClient = score && score.client ? score.client : state && state.active && state.active.client;
|
|
1213
|
+
const currentSessionKey = sessionKeyFor(state, score);
|
|
1214
|
+
const viewColor = score && typeof score.usefulPct === "number" ? (score.color || "green") : "idle";
|
|
1215
|
+
if (renewResultSessionKey && renewResultSessionKey !== currentSessionKey) clearRenewResult();
|
|
1216
|
+
const hasSessionSwitcher = renderTabs(state, score && score.sourcePath ? activeIdFor(state, score.sourcePath, activeClient) : null, activeClient, viewColor);
|
|
1217
|
+
renderSwitchButton(state, activeClient);
|
|
638
1218
|
if (!score || typeof score.usefulPct !== "number") {
|
|
639
1219
|
renderIdle(state);
|
|
640
1220
|
return;
|
|
641
1221
|
}
|
|
642
1222
|
|
|
643
1223
|
const color = score.color || "green";
|
|
644
|
-
const stateName = stateWord(color);
|
|
645
1224
|
const sat = fullnessPct(score);
|
|
646
1225
|
const oldPct = clamp(score.pollutionPct || 0, 0, 95);
|
|
1226
|
+
const health = healthScore(score);
|
|
647
1227
|
const repeatCount = score.buckets && score.buckets.range_redundant ? score.buckets.range_redundant.count || 0 : 0;
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const stale = typeof score.lastActiveMs === "number" && score.lastActiveMs > 600000;
|
|
653
|
-
const shownColor = stale ? "idle" : color;
|
|
654
|
-
dot.className = "dot " + shownColor;
|
|
655
|
-
stateDot.className = stale ? "state-dot" : "state-dot " + color;
|
|
1228
|
+
const info = activeSessionInfo(score, state);
|
|
1229
|
+
|
|
1230
|
+
dot.className = "dot " + color;
|
|
1231
|
+
stateDot.className = "state-dot " + color;
|
|
656
1232
|
hud.classList.remove("s-green", "s-amber", "s-red");
|
|
657
|
-
|
|
658
|
-
main.textContent =
|
|
659
|
-
|
|
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");
|
|
660
1239
|
activityEpoch = typeof score.lastActiveMs === "number" ? Date.now() - score.lastActiveMs : null;
|
|
661
1240
|
renderUpdated();
|
|
662
1241
|
turnEl.textContent = typeof score.turn === "number" && score.turn > 0 ? "turn " + score.turn : "";
|
|
663
|
-
stateWordEl.textContent =
|
|
664
|
-
fullness.textContent =
|
|
665
|
-
fullness.style.
|
|
666
|
-
|
|
667
|
-
lead.
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
metaEl.textContent =
|
|
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 = "";
|
|
672
1251
|
pathEl.textContent = "";
|
|
673
1252
|
currentSrc = score.sourcePath || null;
|
|
674
|
-
if (revealEl) revealEl.style.display = currentSrc ? "inline-block" : "none";
|
|
1253
|
+
if (revealEl) revealEl.style.display = !DEMO_CLEAN && currentSrc ? "inline-block" : "none";
|
|
675
1254
|
missing.textContent = formatMissing(state.missing);
|
|
676
1255
|
|
|
677
|
-
|
|
678
|
-
renderNudge(color, sat);
|
|
1256
|
+
nudge.style.display = "none";
|
|
679
1257
|
|
|
680
|
-
|
|
1258
|
+
renewUrgent = color === "red";
|
|
1259
|
+
if (renewBtn) {
|
|
1260
|
+
renewBtn.style.display = "block";
|
|
1261
|
+
refreshRenewLabel();
|
|
1262
|
+
}
|
|
1263
|
+
const restoredPrompt = restoreCachedCheckpointFor(score, currentSessionKey);
|
|
1264
|
+
if (!restoredPrompt) {
|
|
1265
|
+
const restoredStatus = restoreCheckpointStatusFor(score, currentSessionKey);
|
|
1266
|
+
if (!restoredStatus) maybeFetchCheckpointStatus(score, currentSessionKey);
|
|
1267
|
+
}
|
|
681
1268
|
prevTurnState = {
|
|
682
1269
|
src: score.sourcePath,
|
|
683
1270
|
turn: score.turn,
|
|
@@ -694,92 +1281,62 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
694
1281
|
stateDot.className = "state-dot";
|
|
695
1282
|
hud.classList.remove("s-green", "s-amber", "s-red");
|
|
696
1283
|
prevTurnState = null;
|
|
697
|
-
main.textContent = "
|
|
698
|
-
client
|
|
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");
|
|
699
1289
|
activityEpoch = null;
|
|
700
1290
|
if (updatedEl) updatedEl.textContent = "";
|
|
701
1291
|
currentSrc = null;
|
|
702
1292
|
if (revealEl) revealEl.style.display = "none";
|
|
703
1293
|
turnEl.textContent = "";
|
|
704
|
-
stateWordEl.textContent = "
|
|
705
|
-
fullness.textContent = "";
|
|
706
|
-
|
|
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";
|
|
707
1299
|
lead.style.color = "#6b675d";
|
|
708
|
-
evidence.textContent = "
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1300
|
+
evidence.textContent = "";
|
|
1301
|
+
if (whyBtn) whyBtn.style.display = "none";
|
|
1302
|
+
whyOpen = false;
|
|
1303
|
+
hud.classList.remove("show-why");
|
|
712
1304
|
nudge.style.display = "none";
|
|
713
1305
|
if (renewBtn) renewBtn.style.display = "none";
|
|
714
|
-
|
|
1306
|
+
clearRenewResult();
|
|
715
1307
|
metaEl.textContent = "";
|
|
716
1308
|
pathEl.textContent = "";
|
|
717
1309
|
missing.textContent = formatMissing(state && state.missing);
|
|
718
1310
|
syncHeight();
|
|
719
1311
|
}
|
|
720
1312
|
|
|
721
|
-
function
|
|
722
|
-
if (
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const dOld = turnChanged ? (score.pollutionTok || 0) - prevTurnState.oldTok : 0;
|
|
728
|
-
if (dRep > 0 || dOld > 2000) {
|
|
729
|
-
alertTitle.textContent = "this turn re-read what it already had";
|
|
730
|
-
alertDetail.textContent = joinParts([
|
|
731
|
-
dOld > 0 ? "+" + fmt(dOld) + " old weight" : "",
|
|
732
|
-
dReads > 0 ? "+" + dReads + " read" + (dReads === 1 ? "" : "s") : "0 new files",
|
|
733
|
-
dCt !== 0 ? signedFmt(dCt) + " ctx" : "",
|
|
734
|
-
]);
|
|
735
|
-
alertEl.style.display = "block";
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
if (score.color === "red") {
|
|
739
|
-
alertTitle.textContent = "context has drifted";
|
|
740
|
-
alertDetail.textContent = "at least " + oldPct + "% old weight is still riding in the window";
|
|
741
|
-
alertEl.style.display = "block";
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
alertEl.style.display = "none";
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function renderNudge(color, sat) {
|
|
748
|
-
let text = "";
|
|
749
|
-
let cls = "nudge";
|
|
750
|
-
if (color === "red") {
|
|
751
|
-
text = "A fresh start now beats pushing past this - keep the goal, drop the clutter.";
|
|
752
|
-
cls += " red";
|
|
753
|
-
} else if (color === "amber" || sat >= 75) {
|
|
754
|
-
text = "Still fine - a fresh start soon will keep this snappy.";
|
|
755
|
-
cls += " calm";
|
|
756
|
-
}
|
|
757
|
-
nudge.textContent = text;
|
|
758
|
-
nudge.className = cls;
|
|
759
|
-
nudge.style.display = text ? "block" : "none";
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function stateWord(color) {
|
|
763
|
-
if (color === "red") return "Drifted";
|
|
764
|
-
if (color === "amber") return "Drifting";
|
|
765
|
-
return "Fresh";
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function leadLine(color) {
|
|
769
|
-
if (color === "red") return "Working from a cluttered desk";
|
|
770
|
-
if (color === "amber") return "Old context is building up";
|
|
771
|
-
return "Clean and roomy";
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function evidenceLine(score, oldPct, repeatCount) {
|
|
775
|
-
const repeat = repeatCount > 0 ? repeatCount + " repeated read" + (repeatCount === 1 ? "" : "s") : "nothing has been re-read yet";
|
|
776
|
-
return "at least " + oldPct + "% old weight (" + fmt(score.pollutionTok) + ") - " + repeat;
|
|
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 : "";
|
|
777
1319
|
}
|
|
778
1320
|
|
|
779
|
-
function
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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";
|
|
1333
|
+
}
|
|
1334
|
+
if (!useful) {
|
|
1335
|
+
whyOpen = false;
|
|
1336
|
+
hud.classList.remove("show-why");
|
|
1337
|
+
} else {
|
|
1338
|
+
hud.classList.toggle("show-why", whyOpen);
|
|
1339
|
+
}
|
|
783
1340
|
}
|
|
784
1341
|
|
|
785
1342
|
function fullnessPct(score) {
|
|
@@ -788,10 +1345,10 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
788
1345
|
return 0;
|
|
789
1346
|
}
|
|
790
1347
|
|
|
791
|
-
function
|
|
792
|
-
if (
|
|
793
|
-
if (
|
|
794
|
-
return
|
|
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;
|
|
795
1352
|
}
|
|
796
1353
|
|
|
797
1354
|
function ctSourceLabel(src) {
|
|
@@ -804,34 +1361,95 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
804
1361
|
return "Not seeing " + list.map(label).join(", ") + " yet";
|
|
805
1362
|
}
|
|
806
1363
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
+
};
|
|
814
1375
|
}
|
|
815
1376
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function renderSwitchButton(state, activeClient) {
|
|
1384
|
+
if (!switchBtn) return;
|
|
1385
|
+
const target = switchTarget(state, activeClient);
|
|
1386
|
+
if (!target) {
|
|
1387
|
+
switchBtn.style.display = "none";
|
|
1388
|
+
switchBtn.removeAttribute("data-target-family");
|
|
1389
|
+
switchBtn.removeAttribute("data-target-id");
|
|
1390
|
+
switchBtn.innerHTML = "";
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
const family = agentFamily(target.client);
|
|
1394
|
+
const name = family === "claude" ? "Claude" : "Codex";
|
|
1395
|
+
switchBtn.dataset.targetFamily = family;
|
|
1396
|
+
switchBtn.dataset.targetId = target.id;
|
|
1397
|
+
switchBtn.title = "Switch to " + name;
|
|
1398
|
+
switchBtn.innerHTML = '<img class="switch-icon" src="' + iconForFamily(family) + '" alt="" /><span class="switch-copy">Switch to ' + name + "</span>";
|
|
1399
|
+
switchBtn.style.display = "inline-flex";
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function switchTarget(state, activeClient) {
|
|
819
1403
|
const list = (state && state.recentSessions) || [];
|
|
820
|
-
if (list.length
|
|
1404
|
+
if (!list.length) return null;
|
|
1405
|
+
const currentFamily = agentFamily(activeClient || (state.active && state.active.client));
|
|
1406
|
+
const targetFamily = currentFamily === "codex" ? "claude" : currentFamily === "claude" ? "codex" : "";
|
|
1407
|
+
if (!targetFamily) return null;
|
|
1408
|
+
return list.filter((s) => agentFamily(s.client) === targetFamily)[0] || null;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
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;
|
|
821
1418
|
const chips = list.map((s) => {
|
|
822
|
-
const
|
|
823
|
-
const
|
|
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>' : "";
|
|
824
1423
|
const title = s.title || label(s.client);
|
|
825
|
-
|
|
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>";
|
|
826
1429
|
});
|
|
827
|
-
chips.unshift('<button class="tab' + (
|
|
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">✓</span></button>');
|
|
828
1431
|
tabsEl.innerHTML = chips.join("");
|
|
1432
|
+
return true;
|
|
829
1433
|
}
|
|
830
|
-
function
|
|
1434
|
+
function visibleRecentSessions(state, activeClient) {
|
|
831
1435
|
const list = (state && state.recentSessions) || [];
|
|
1436
|
+
const family = agentFamily(activeClient);
|
|
1437
|
+
if (family !== "codex" && family !== "claude") return list;
|
|
1438
|
+
return list.filter((s) => agentFamily(s.client) === family);
|
|
1439
|
+
}
|
|
1440
|
+
function activeIdFor(state, src, activeClient) {
|
|
1441
|
+
const list = visibleRecentSessions(state, activeClient);
|
|
832
1442
|
const match = list.filter((s) => s.sourcePath === src)[0];
|
|
833
1443
|
return match ? match.id : null;
|
|
834
1444
|
}
|
|
1445
|
+
function agentFamily(clientName) {
|
|
1446
|
+
if (clientName === "codex") return "codex";
|
|
1447
|
+
if (String(clientName || "").indexOf("claude") === 0) return "claude";
|
|
1448
|
+
return "";
|
|
1449
|
+
}
|
|
1450
|
+
function iconForFamily(family) {
|
|
1451
|
+
return family === "claude" ? "/assets/hud/claude.svg" : "/assets/hud/codex.svg";
|
|
1452
|
+
}
|
|
835
1453
|
function shortTitle(t) {
|
|
836
1454
|
t = String(t || "");
|
|
837
1455
|
return t.length > 22 ? t.slice(0, 21) + "…" : t;
|
|
@@ -868,14 +1486,6 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
868
1486
|
return String(Math.round(n));
|
|
869
1487
|
}
|
|
870
1488
|
|
|
871
|
-
function signedFmt(value) {
|
|
872
|
-
return (value >= 0 ? "+" : "-") + fmt(Math.abs(value));
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function joinParts(parts) {
|
|
876
|
-
return parts.filter(Boolean).join(" - ");
|
|
877
|
-
}
|
|
878
|
-
|
|
879
1489
|
function clamp(value, min, max) {
|
|
880
1490
|
return Math.max(min, Math.min(max, Number(value) || 0));
|
|
881
1491
|
}
|