@echomem/mcp 1.4.8 → 1.4.10
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 +23 -3
- 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/github.svg +1 -0
- package/dist/city/README.md +9 -0
- package/dist/city/echo-ai-city-only.html +1067 -107
- 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-canonical-adapter.js +315 -0
- package/dist/context-analysis/claude-native-canonical.js +1216 -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 +43 -0
- package/dist/context-metrics/estimator.js +45 -0
- package/dist/context-metrics/ledger.js +507 -0
- package/dist/context-metrics/model-limits.js +26 -0
- package/dist/context-metrics/parse-claude.js +227 -0
- package/dist/context-metrics/parse-codex.js +276 -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 +77 -202
- package/dist/hud/cli.js +0 -0
- package/dist/hud/efficiency.js +447 -0
- package/dist/hud/electron-main.js +3 -2
- package/dist/hud/fs.js +14 -0
- package/dist/hud/metric.js +17 -102
- package/dist/hud/monitor.js +136 -28
- package/dist/hud/render.js +4 -3
- package/dist/hud/server.js +30 -0
- package/dist/hud/web.js +622 -353
- package/dist/index.js +7 -3
- package/dist/local-data-paths.js +87 -0
- package/dist/migrate.js +37 -29
- package/dist/report.js +101 -40
- package/dist/setup-page/client-core.js +475 -0
- package/dist/setup-page/client-extraction.js +550 -0
- package/dist/setup-page/client-lifecycle.js +116 -0
- package/dist/setup-page/client-report-audit.js +818 -0
- package/dist/setup-page/client-report-city.js +204 -0
- package/dist/setup-page/client-report.js +6 -0
- package/dist/setup-page/client.js +15 -0
- package/dist/setup-page/document.js +37 -0
- package/dist/setup-page/styles-city-report.js +880 -0
- package/dist/setup-page/styles-context-audit.js +470 -0
- package/dist/setup-page/styles-extraction.js +821 -0
- package/dist/setup-page/styles-foundation.js +231 -0
- package/dist/setup-page/styles.js +11 -0
- package/dist/setup-page.js +15 -2538
- package/dist/setup-preview.js +245 -0
- package/dist/setup.js +456 -44
- package/package.json +8 -7
- package/templates/echomem-recall.md +2 -2
- package/dist/city/10-problems-report.html +0 -649
- package/dist/city/_live.html +0 -37
- package/dist/city/_serve.mjs +0 -45
- package/dist/city/card-data.json +0 -15
- package/dist/city/city-data.json +0 -248
- package/dist/city/echo-ai-city-only.template.html +0 -1272
- package/dist/city/generate-echo-city-only.mjs +0 -112
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;
|
|
@@ -110,6 +114,9 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
110
114
|
filter: var(--shadow);
|
|
111
115
|
transition: border-color 0.18s ease, background 0.18s ease, filter 0.18s ease;
|
|
112
116
|
}
|
|
117
|
+
/* Open = the card is the voice. Echo doesn't say the same thing twice, so the speech
|
|
118
|
+
bubble steps aside while the panel is up and comes back when it closes. */
|
|
119
|
+
#hud.open .status-bubble { display: none; }
|
|
113
120
|
.status-bubble::after {
|
|
114
121
|
content: "";
|
|
115
122
|
position: absolute;
|
|
@@ -129,24 +136,31 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
129
136
|
min-width: 0;
|
|
130
137
|
display: grid;
|
|
131
138
|
grid-template-columns: minmax(0, 1fr);
|
|
132
|
-
gap:
|
|
133
|
-
padding:
|
|
139
|
+
gap: 2px;
|
|
140
|
+
padding: 15px 38px 11px 30px;
|
|
134
141
|
}
|
|
135
142
|
.dot {
|
|
136
143
|
position: absolute;
|
|
137
|
-
left:
|
|
138
|
-
top:
|
|
144
|
+
left: 13px;
|
|
145
|
+
top: 22px;
|
|
139
146
|
z-index: 3;
|
|
140
|
-
width:
|
|
141
|
-
height:
|
|
147
|
+
width: 12px;
|
|
148
|
+
height: 12px;
|
|
142
149
|
border: 2px solid #fdfaf2;
|
|
143
150
|
border-radius: 999px;
|
|
144
151
|
background: var(--green);
|
|
145
152
|
box-shadow: 0 0 0 1px rgba(26, 58, 143, 0.14);
|
|
146
153
|
}
|
|
147
154
|
.dot.amber { background: var(--amber); }
|
|
148
|
-
.dot.red
|
|
149
|
-
|
|
155
|
+
.dot.red, .state-dot.red, #miniPill .mdot.red {
|
|
156
|
+
background: #ff5f57;
|
|
157
|
+
animation: red-pulse 1.15s ease-in-out infinite;
|
|
158
|
+
}
|
|
159
|
+
.dot.idle { background: var(--green); }
|
|
160
|
+
@keyframes red-pulse {
|
|
161
|
+
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); }
|
|
162
|
+
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); }
|
|
163
|
+
}
|
|
150
164
|
.main {
|
|
151
165
|
min-width: 0;
|
|
152
166
|
overflow: hidden;
|
|
@@ -158,19 +172,30 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
158
172
|
line-height: 1.1;
|
|
159
173
|
color: var(--ink);
|
|
160
174
|
}
|
|
175
|
+
.main-rest { font-size: 10.5px; font-weight: 680; color: var(--muted); }
|
|
176
|
+
.client-line { display: flex; align-items: center; gap: 5px; min-width: 0; }
|
|
177
|
+
.line-icon { width: 11px; height: 11px; flex: 0 0 auto; object-fit: contain; opacity: .85; }
|
|
161
178
|
.client {
|
|
162
179
|
min-width: 0;
|
|
163
180
|
overflow: hidden;
|
|
164
181
|
text-overflow: ellipsis;
|
|
165
182
|
white-space: nowrap;
|
|
166
|
-
font-
|
|
167
|
-
font-
|
|
168
|
-
letter-spacing: 0
|
|
183
|
+
font-size: 10.5px;
|
|
184
|
+
font-weight: 720;
|
|
185
|
+
letter-spacing: 0;
|
|
186
|
+
line-height: 1;
|
|
187
|
+
color: var(--faint);
|
|
188
|
+
}
|
|
189
|
+
.client-repo {
|
|
190
|
+
min-width: 0;
|
|
191
|
+
overflow: hidden;
|
|
192
|
+
text-overflow: ellipsis;
|
|
193
|
+
white-space: nowrap;
|
|
194
|
+
font-size: 9.5px;
|
|
195
|
+
font-weight: 620;
|
|
169
196
|
line-height: 1;
|
|
170
|
-
text-transform: uppercase;
|
|
171
197
|
color: var(--faint);
|
|
172
198
|
}
|
|
173
|
-
/* When drifted, the bubble's second line becomes Echo speaking, not a log line. */
|
|
174
199
|
.client.voice {
|
|
175
200
|
font-family: inherit;
|
|
176
201
|
font-size: 11.5px;
|
|
@@ -182,61 +207,191 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
182
207
|
.details {
|
|
183
208
|
display: none;
|
|
184
209
|
position: relative;
|
|
185
|
-
margin: 2px 8px 0
|
|
210
|
+
margin: 2px 8px 0 8px;
|
|
186
211
|
padding: 16px 17px 15px;
|
|
187
212
|
border: 1px solid var(--line);
|
|
188
|
-
border-radius:
|
|
213
|
+
border-radius: 12px;
|
|
189
214
|
background: var(--card);
|
|
190
215
|
color: var(--ink);
|
|
191
216
|
filter: var(--shadow);
|
|
192
217
|
cursor: grab;
|
|
193
218
|
}
|
|
194
219
|
#hud.open .details { display: block; }
|
|
195
|
-
.
|
|
220
|
+
/* The current session is the one object every number and action belongs to — one card. */
|
|
221
|
+
.now-card {
|
|
222
|
+
border: 1px solid rgba(26, 58, 143, 0.24);
|
|
223
|
+
border-radius: 12px;
|
|
224
|
+
padding: 12px 13px 13px;
|
|
225
|
+
background: #fff;
|
|
226
|
+
box-shadow: 0 1px 5px rgba(26, 58, 143, 0.07);
|
|
227
|
+
}
|
|
228
|
+
#hud.s-amber .now-card { border-color: rgba(233, 185, 73, 0.55); }
|
|
229
|
+
#hud.s-red .now-card { border-color: rgba(199, 55, 47, 0.45); }
|
|
230
|
+
.now-sess {
|
|
231
|
+
display: flex;
|
|
232
|
+
align-items: center;
|
|
233
|
+
gap: 8px;
|
|
234
|
+
margin-bottom: 9px;
|
|
235
|
+
}
|
|
236
|
+
.now-icon { width: 18px; height: 18px; flex: 0 0 auto; object-fit: contain; }
|
|
237
|
+
.now-copy { min-width: 0; flex: 1; display: grid; gap: 1px; line-height: 1.15; }
|
|
238
|
+
.now-title, .now-repo { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
239
|
+
.now-title { font-size: 12px; font-weight: 800; color: var(--ink); }
|
|
240
|
+
.now-repo { font-size: 10px; font-weight: 600; color: var(--faint); }
|
|
241
|
+
.verdict {
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
text-overflow: ellipsis;
|
|
244
|
+
white-space: nowrap;
|
|
245
|
+
font-size: 11.5px;
|
|
246
|
+
font-weight: 650;
|
|
247
|
+
color: var(--muted);
|
|
248
|
+
}
|
|
249
|
+
.verdict:empty { display: none; }
|
|
250
|
+
.meta-row {
|
|
196
251
|
display: flex;
|
|
252
|
+
align-items: center;
|
|
197
253
|
justify-content: space-between;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
margin-bottom: 10px;
|
|
254
|
+
gap: 8px;
|
|
255
|
+
margin-top: 10px;
|
|
201
256
|
}
|
|
202
|
-
.
|
|
257
|
+
.meta-line {
|
|
258
|
+
min-width: 0;
|
|
203
259
|
overflow: hidden;
|
|
204
260
|
text-overflow: ellipsis;
|
|
205
261
|
white-space: nowrap;
|
|
206
|
-
font-
|
|
262
|
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
263
|
+
font-size: 9.5px;
|
|
264
|
+
font-weight: 600;
|
|
265
|
+
letter-spacing: 0.02em;
|
|
266
|
+
color: var(--faint);
|
|
267
|
+
}
|
|
268
|
+
.session-actions {
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
gap: 6px;
|
|
272
|
+
margin: 0 0 8px 26px;
|
|
273
|
+
}
|
|
274
|
+
.pin-chip, .jump-chip {
|
|
275
|
+
display: none;
|
|
276
|
+
align-items: center;
|
|
277
|
+
justify-content: center;
|
|
278
|
+
min-height: 22px;
|
|
279
|
+
padding: 3px 9px;
|
|
280
|
+
border: 1px solid rgba(26, 58, 143, .18);
|
|
281
|
+
border-radius: 999px;
|
|
282
|
+
background: rgba(255, 255, 255, .78);
|
|
207
283
|
color: var(--faint);
|
|
284
|
+
cursor: pointer;
|
|
285
|
+
font-family: inherit;
|
|
286
|
+
font-size: 10px;
|
|
287
|
+
font-weight: 760;
|
|
288
|
+
letter-spacing: 0;
|
|
289
|
+
line-height: 1;
|
|
208
290
|
}
|
|
209
|
-
.
|
|
210
|
-
|
|
291
|
+
.pin-chip:hover, .jump-chip:hover {
|
|
292
|
+
border-color: rgba(26, 58, 143, .34);
|
|
293
|
+
background: #fff;
|
|
294
|
+
color: var(--blue);
|
|
295
|
+
}
|
|
296
|
+
.jump-chip {
|
|
297
|
+
width: 22px;
|
|
298
|
+
min-width: 22px;
|
|
299
|
+
padding: 0;
|
|
300
|
+
border-radius: 50%;
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
}
|
|
303
|
+
.tabs {
|
|
304
|
+
display: grid;
|
|
305
|
+
gap: 6px;
|
|
306
|
+
max-height: 259px;
|
|
307
|
+
margin: 10px 0 0;
|
|
308
|
+
padding-right: 3px;
|
|
309
|
+
overflow-y: auto;
|
|
310
|
+
overscroll-behavior: contain;
|
|
311
|
+
}
|
|
312
|
+
.tabs:empty { display: none; }
|
|
313
|
+
.tabs::-webkit-scrollbar { width: 4px; }
|
|
314
|
+
.tabs::-webkit-scrollbar-track { background: transparent; }
|
|
315
|
+
.tabs::-webkit-scrollbar-thumb { border-radius: 999px; background: rgba(26, 58, 143, .2); }
|
|
316
|
+
/* History rows: everything that is NOT the current session. Quiet on purpose. */
|
|
211
317
|
.tab {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
318
|
+
display: grid; grid-template-columns: 16px minmax(0, 1fr) auto auto auto; align-items: center; column-gap: 8px;
|
|
319
|
+
width: 100%; min-height: 30px; font-size: 10.5px; line-height: 1.15; padding: 5px 9px; border-radius: 8px;
|
|
320
|
+
border: 1px solid transparent; background: transparent;
|
|
321
|
+
color: var(--faint); cursor: pointer; text-align: left; font-family: inherit;
|
|
322
|
+
}
|
|
323
|
+
.tab:hover { border-color: rgba(0, 0, 0, .1); background: rgba(0, 0, 0, .05); }
|
|
324
|
+
.tab-icon { width: 14px; height: 14px; object-fit: contain; opacity: .75; }
|
|
325
|
+
.tab-copy { min-width: 0; display: grid; gap: 1px; }
|
|
326
|
+
.tab-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-weight: 680; }
|
|
327
|
+
.tab-repo { display: flex; align-items: center; gap: 4px; min-width: 0; color: var(--faint); font-size: 9.5px; font-weight: 600; }
|
|
328
|
+
.tab-reponame { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
329
|
+
.tab-gh { width: 10px; height: 10px; flex: 0 0 auto; object-fit: contain; opacity: .7; }
|
|
330
|
+
/* When you last worked here — sits just left of the jump arrow. */
|
|
331
|
+
.tab-time { justify-self: end; font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 9px; font-weight: 600; color: var(--faint); white-space: nowrap; }
|
|
332
|
+
.tab-status {
|
|
333
|
+
width: 10px;
|
|
334
|
+
height: 10px;
|
|
335
|
+
justify-self: end;
|
|
336
|
+
border: 2px solid #fff;
|
|
337
|
+
border-radius: 50%;
|
|
338
|
+
background: var(--green);
|
|
339
|
+
box-shadow: 0 0 0 1px rgba(26, 58, 143, .18);
|
|
340
|
+
}
|
|
341
|
+
.tab-status.amber { background: var(--amber); }
|
|
342
|
+
.tab-status.red { background: #ff5f57; box-shadow: 0 0 0 1px rgba(199, 55, 47, .28); }
|
|
343
|
+
.tab-status.idle { background: var(--faint); }
|
|
344
|
+
.tab-health {
|
|
345
|
+
display: inline-flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
gap: 5px;
|
|
348
|
+
justify-self: end;
|
|
349
|
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
350
|
+
font-size: 9.5px;
|
|
351
|
+
font-weight: 750;
|
|
352
|
+
color: var(--muted);
|
|
215
353
|
}
|
|
216
|
-
.tab
|
|
217
|
-
|
|
218
|
-
.tab
|
|
354
|
+
.tab-health .tab-status { width: 8px; height: 8px; border-width: 1.5px; }
|
|
355
|
+
/* Jump straight to this conversation in its own app. */
|
|
356
|
+
.tab-jump {
|
|
357
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
358
|
+
width: 18px; height: 18px; justify-self: end; border-radius: 50%;
|
|
359
|
+
color: var(--faint); font-size: 11px; font-weight: 800;
|
|
360
|
+
}
|
|
361
|
+
.tab-jump:hover { background: rgba(26, 58, 143, 0.1); color: var(--blue); }
|
|
219
362
|
.spin {
|
|
220
|
-
display: inline-block; width:
|
|
363
|
+
display: inline-block; width: 8px; height: 8px; margin-left: 4px; vertical-align: -1px;
|
|
221
364
|
border: 1.5px solid rgba(0, 0, 0, .2); border-top-color: #1a3a8f; border-radius: 50%;
|
|
222
365
|
animation: echospin .8s linear infinite;
|
|
223
366
|
}
|
|
224
367
|
@keyframes echospin { to { transform: rotate(360deg); } }
|
|
225
368
|
/* Mini mode: a small always-on pill instead of the full mascot+bubble. Opening the panel
|
|
226
369
|
temporarily shows the full layout; closing returns to the pill. */
|
|
370
|
+
/* One line, one reading order: dot · score · agent icon · session · repo icon · repo.
|
|
371
|
+
The session name gets the space; the repo shrinks first. */
|
|
227
372
|
#miniPill {
|
|
228
|
-
display: none; align-items: center;
|
|
229
|
-
margin: 7px 0 0 8px; padding:
|
|
373
|
+
display: none; align-items: center; gap: 7px; width: calc(100% - 16px); min-height: 38px;
|
|
374
|
+
margin: 7px 0 0 8px; padding: 5px 13px; cursor: pointer; user-select: none;
|
|
230
375
|
border: 1.6px solid rgba(26, 58, 143, 0.75); border-radius: 999px; background: #fdfaf2;
|
|
231
|
-
filter: var(--shadow);
|
|
232
|
-
font-size: 11.5px; font-weight: 700; color: var(--ink); letter-spacing: 0.02em;
|
|
376
|
+
filter: var(--shadow); color: var(--ink); letter-spacing: 0;
|
|
233
377
|
}
|
|
234
378
|
#miniPill .mdot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); flex: 0 0 auto; }
|
|
235
379
|
#miniPill .mdot.amber { background: var(--amber); }
|
|
236
|
-
#miniPill .mdot.
|
|
237
|
-
|
|
238
|
-
#
|
|
239
|
-
|
|
380
|
+
#miniPill .mdot.idle { background: var(--green); }
|
|
381
|
+
.micon { width: 13px; height: 13px; object-fit: contain; flex: 0 0 auto; opacity: .85; }
|
|
382
|
+
#mscore {
|
|
383
|
+
flex: 0 0 auto;
|
|
384
|
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
385
|
+
font-size: 12px;
|
|
386
|
+
font-weight: 850;
|
|
387
|
+
}
|
|
388
|
+
.mcopy { min-width: 0; flex: 1; display: grid; gap: 2px; line-height: 1.1; }
|
|
389
|
+
.mrow { display: flex; align-items: center; gap: 5px; min-width: 0; }
|
|
390
|
+
#msession, #mrepo { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
391
|
+
#msession { font-size: 11.5px; font-weight: 760; color: var(--ink); }
|
|
392
|
+
#mrepo { font-size: 10px; font-weight: 650; color: var(--faint); }
|
|
393
|
+
.mrow .micon { width: 11px; height: 11px; }
|
|
394
|
+
#hud.mini:not(.open) .glance { height: 52px; }
|
|
240
395
|
#hud.mini:not(.open) .mascot-stage, #hud.mini:not(.open) .status-bubble, #hud.mini:not(.open) .drag { display: none; }
|
|
241
396
|
#hud.mini:not(.open) #miniPill { display: inline-flex; }
|
|
242
397
|
.cornerbtn {
|
|
@@ -246,7 +401,6 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
246
401
|
cursor: pointer; font-family: inherit;
|
|
247
402
|
}
|
|
248
403
|
.cornerbtn:hover { background: #fff; color: #111; }
|
|
249
|
-
.headctl { display: flex; align-items: center; gap: 8px; }
|
|
250
404
|
.switchbtn {
|
|
251
405
|
display: inline-flex; align-items: center; justify-content: center;
|
|
252
406
|
width: 22px; height: 22px; border-radius: 50%; padding: 0; line-height: 1; font-size: 12px;
|
|
@@ -254,13 +408,12 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
254
408
|
cursor: pointer; font-family: inherit;
|
|
255
409
|
}
|
|
256
410
|
.switchbtn:hover { background: rgba(0, 0, 0, .1); color: #111; }
|
|
411
|
+
/* Explicit family switch: a labeled pill, not a mystery icon. */
|
|
257
412
|
.agent-switch {
|
|
258
413
|
width: auto;
|
|
259
|
-
|
|
260
|
-
max-width: 154px;
|
|
261
|
-
height: 28px;
|
|
414
|
+
height: 26px;
|
|
262
415
|
gap: 6px;
|
|
263
|
-
padding: 0
|
|
416
|
+
padding: 0 11px 0 8px;
|
|
264
417
|
border-radius: 999px;
|
|
265
418
|
background: rgba(255, 255, 255, .78);
|
|
266
419
|
color: #1f1f1f;
|
|
@@ -268,12 +421,13 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
268
421
|
font-weight: 760;
|
|
269
422
|
letter-spacing: 0;
|
|
270
423
|
white-space: nowrap;
|
|
424
|
+
flex: 0 0 auto;
|
|
271
425
|
}
|
|
272
426
|
.agent-switch:hover { background: #fff; border-color: rgba(26, 58, 143, .34); }
|
|
273
427
|
.agent-switch[data-target-family="claude"] { border-color: rgba(217, 119, 87, .42); }
|
|
274
428
|
.agent-switch[data-target-family="codex"] { border-color: rgba(57, 65, 255, .34); }
|
|
275
|
-
.switch-icon { width:
|
|
276
|
-
.switch-copy {
|
|
429
|
+
.switch-icon { width: 14px; height: 14px; flex: 0 0 auto; object-fit: contain; }
|
|
430
|
+
.switch-copy { display: inline; }
|
|
277
431
|
.reveal {
|
|
278
432
|
margin-top: 8px; font-size: 10.5px; padding: 4px 11px; border-radius: 999px;
|
|
279
433
|
border: 1px solid rgba(0, 0, 0, .12); background: rgba(0, 0, 0, .04); color: var(--faint);
|
|
@@ -284,87 +438,62 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
284
438
|
display: flex;
|
|
285
439
|
align-items: center;
|
|
286
440
|
gap: 9px;
|
|
287
|
-
margin-bottom:
|
|
441
|
+
margin-bottom: 4px;
|
|
288
442
|
}
|
|
289
443
|
.state-dot {
|
|
290
|
-
width:
|
|
291
|
-
height:
|
|
444
|
+
width: 12px;
|
|
445
|
+
height: 12px;
|
|
292
446
|
flex: none;
|
|
293
447
|
border-radius: 999px;
|
|
294
448
|
background: var(--green);
|
|
295
449
|
box-shadow: 0 0 0 1px rgba(26, 58, 143, 0.12);
|
|
296
450
|
}
|
|
297
451
|
.state-dot.amber { background: var(--amber); }
|
|
298
|
-
.state-dot.red { background: #ff5f57; }
|
|
299
452
|
.state-word {
|
|
300
453
|
min-width: 0;
|
|
301
454
|
overflow: hidden;
|
|
302
455
|
text-overflow: ellipsis;
|
|
303
456
|
white-space: nowrap;
|
|
304
|
-
font-
|
|
305
|
-
font-
|
|
306
|
-
|
|
307
|
-
.full {
|
|
308
|
-
min-height: 17px;
|
|
309
|
-
margin-bottom: 14px;
|
|
310
|
-
font-size: 12px;
|
|
311
|
-
font-weight: 650;
|
|
312
|
-
color: #4a7a3e;
|
|
313
|
-
}
|
|
314
|
-
.lead {
|
|
315
|
-
margin-bottom: 5px;
|
|
316
|
-
font-size: 13.5px;
|
|
317
|
-
font-weight: 750;
|
|
318
|
-
color: #2f7d4f;
|
|
457
|
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
458
|
+
font-size: 20px;
|
|
459
|
+
font-weight: 850;
|
|
319
460
|
}
|
|
320
|
-
.
|
|
321
|
-
|
|
322
|
-
|
|
461
|
+
.why-btn {
|
|
462
|
+
margin-left: auto;
|
|
463
|
+
display: inline-flex;
|
|
464
|
+
align-items: center;
|
|
465
|
+
justify-content: center;
|
|
466
|
+
width: 20px;
|
|
467
|
+
height: 20px;
|
|
468
|
+
padding: 0;
|
|
469
|
+
line-height: 1;
|
|
470
|
+
border: 1px solid rgba(26, 58, 143, 0.16);
|
|
471
|
+
border-radius: 50%;
|
|
472
|
+
background: rgba(26, 58, 143, 0.04);
|
|
323
473
|
color: var(--muted);
|
|
324
|
-
font-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
display: flex;
|
|
329
|
-
height: 9px;
|
|
330
|
-
margin: 0;
|
|
331
|
-
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;
|
|
474
|
+
font-family: inherit;
|
|
475
|
+
font-size: 15px;
|
|
476
|
+
font-weight: 700;
|
|
477
|
+
cursor: pointer;
|
|
340
478
|
}
|
|
341
|
-
.
|
|
342
|
-
.
|
|
343
|
-
.alert {
|
|
479
|
+
.why-btn:hover { background: rgba(26, 58, 143, 0.09); color: var(--ink); }
|
|
480
|
+
.evidence {
|
|
344
481
|
display: none;
|
|
345
|
-
margin
|
|
346
|
-
padding: 9px
|
|
347
|
-
border
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
margin-bottom: 4px;
|
|
351
|
-
color: var(--brick);
|
|
352
|
-
font-size: 12.5px;
|
|
353
|
-
font-weight: 750;
|
|
354
|
-
}
|
|
355
|
-
.alert-detail {
|
|
482
|
+
margin: 11px 0 12px;
|
|
483
|
+
padding: 9px 10px;
|
|
484
|
+
border: 1px solid rgba(26, 58, 143, 0.12);
|
|
485
|
+
border-radius: 8px;
|
|
486
|
+
background: rgba(26, 58, 143, 0.035);
|
|
356
487
|
color: var(--muted);
|
|
357
488
|
font-size: 12px;
|
|
358
|
-
line-height: 1.
|
|
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;
|
|
489
|
+
line-height: 1.5;
|
|
367
490
|
}
|
|
491
|
+
#hud.show-why .evidence { display: block; }
|
|
492
|
+
.why-line { display: flex; justify-content: space-between; gap: 12px; }
|
|
493
|
+
.why-line + .why-line { margin-top: 3px; }
|
|
494
|
+
.why-k { color: var(--faint); }
|
|
495
|
+
.why-v { color: var(--ink); font-weight: 750; text-align: right; }
|
|
496
|
+
.nudge { display: none; }
|
|
368
497
|
.nudge.calm {
|
|
369
498
|
border: 1px solid rgba(26, 58, 143, 0.14);
|
|
370
499
|
background: rgba(26, 58, 143, 0.05);
|
|
@@ -375,34 +504,31 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
375
504
|
background: rgba(199, 55, 47, 0.07);
|
|
376
505
|
color: #7a241e;
|
|
377
506
|
}
|
|
378
|
-
/* Breathing CTA: quiet ghost pill while the session is healthy... */
|
|
379
507
|
button.renew-btn {
|
|
380
508
|
display: none;
|
|
381
|
-
width:
|
|
509
|
+
width: 100%;
|
|
382
510
|
margin-top: 12px;
|
|
383
|
-
border: 1px solid rgba(
|
|
511
|
+
border: 1px solid rgba(26, 58, 143, 0.26);
|
|
384
512
|
border-radius: 999px;
|
|
385
|
-
padding:
|
|
386
|
-
background:
|
|
387
|
-
color: var(--
|
|
513
|
+
padding: 9px 14px;
|
|
514
|
+
background: rgba(26, 58, 143, 0.07);
|
|
515
|
+
color: var(--blue);
|
|
388
516
|
font-weight: 750;
|
|
389
|
-
font-size:
|
|
517
|
+
font-size: 12px;
|
|
390
518
|
cursor: pointer;
|
|
391
519
|
transition: all 0.25s ease;
|
|
392
520
|
}
|
|
393
|
-
/* ...and a full-size primary the moment the session drifts. */
|
|
394
521
|
button.renew-btn.urgent {
|
|
395
|
-
|
|
396
|
-
padding: 11px
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
font-
|
|
401
|
-
box-shadow: 0
|
|
522
|
+
border-color: var(--blue);
|
|
523
|
+
padding: 11px 14px;
|
|
524
|
+
background: var(--blue);
|
|
525
|
+
color: #fff;
|
|
526
|
+
font-size: 13px;
|
|
527
|
+
font-weight: 800;
|
|
528
|
+
box-shadow: 0 4px 12px rgba(26, 58, 143, 0.22);
|
|
402
529
|
}
|
|
403
530
|
button.renew-btn b { font-weight: 800; }
|
|
404
|
-
button.renew-btn small {
|
|
405
|
-
button.renew-btn.urgent small, button.renew-btn.loading small {
|
|
531
|
+
button.renew-btn small {
|
|
406
532
|
display: block;
|
|
407
533
|
margin-top: 3px;
|
|
408
534
|
font-size: 10.5px;
|
|
@@ -410,37 +536,46 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
410
536
|
opacity: 0.85;
|
|
411
537
|
white-space: normal;
|
|
412
538
|
}
|
|
413
|
-
button.renew-btn:hover { background: rgba(
|
|
414
|
-
button.renew-btn.urgent:hover { background: #
|
|
539
|
+
button.renew-btn:hover { background: rgba(26, 58, 143, 0.12); }
|
|
540
|
+
button.renew-btn.urgent:hover { background: #102d78; }
|
|
415
541
|
button.renew-btn.loading { width: 100%; opacity: 0.75; cursor: wait; }
|
|
416
542
|
.renew-result {
|
|
417
543
|
display: none;
|
|
418
544
|
margin-top: 10px;
|
|
419
545
|
border: 1px solid rgba(26, 58, 143, 0.18);
|
|
420
|
-
border-radius:
|
|
546
|
+
border-radius: 8px;
|
|
421
547
|
background: rgba(26, 58, 143, 0.04);
|
|
422
548
|
padding: 10px 12px;
|
|
423
549
|
}
|
|
424
550
|
.renew-result .renew-msg {
|
|
425
|
-
margin-bottom:
|
|
551
|
+
margin-bottom: 0;
|
|
426
552
|
color: var(--blue);
|
|
427
553
|
font-size: 11.5px;
|
|
428
554
|
font-weight: 750;
|
|
429
555
|
}
|
|
556
|
+
/* Preview of the generated context: a few lines by default, "Read all" opens the whole
|
|
557
|
+
thing right here. */
|
|
430
558
|
.renew-result pre {
|
|
431
|
-
max-height:
|
|
432
|
-
overflow:
|
|
433
|
-
margin: 0;
|
|
559
|
+
max-height: 92px;
|
|
560
|
+
overflow: hidden;
|
|
561
|
+
margin: 7px 0 0;
|
|
434
562
|
white-space: pre-wrap;
|
|
435
563
|
word-break: break-word;
|
|
436
564
|
color: var(--ink);
|
|
437
565
|
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
438
566
|
font-size: 9.5px;
|
|
567
|
+
line-height: 1.45;
|
|
568
|
+
-webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
|
|
569
|
+
}
|
|
570
|
+
.renew-result.expanded pre {
|
|
571
|
+
max-height: 300px;
|
|
572
|
+
overflow: auto;
|
|
573
|
+
-webkit-mask-image: none;
|
|
439
574
|
}
|
|
575
|
+
.renew-actions { display: flex; align-items: center; gap: 6px; margin-top: 8px; }
|
|
440
576
|
.renew-result .copy-btn {
|
|
441
577
|
display: inline-block;
|
|
442
578
|
width: auto;
|
|
443
|
-
margin-top: 6px;
|
|
444
579
|
border: 1px solid rgba(26, 58, 143, 0.22);
|
|
445
580
|
border-radius: 999px;
|
|
446
581
|
padding: 5px 14px;
|
|
@@ -450,7 +585,19 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
450
585
|
font-size: 11px;
|
|
451
586
|
cursor: pointer;
|
|
452
587
|
}
|
|
453
|
-
.
|
|
588
|
+
.ghost-btn {
|
|
589
|
+
border: 1px solid rgba(26, 58, 143, 0.14);
|
|
590
|
+
border-radius: 999px;
|
|
591
|
+
padding: 5px 12px;
|
|
592
|
+
background: transparent;
|
|
593
|
+
color: var(--muted);
|
|
594
|
+
font-family: inherit;
|
|
595
|
+
font-size: 11px;
|
|
596
|
+
font-weight: 750;
|
|
597
|
+
cursor: pointer;
|
|
598
|
+
}
|
|
599
|
+
.ghost-btn:hover { background: rgba(26, 58, 143, 0.06); color: var(--ink); }
|
|
600
|
+
#renewagainbtn { margin-left: auto; }
|
|
454
601
|
.path,
|
|
455
602
|
.missing {
|
|
456
603
|
overflow: hidden;
|
|
@@ -459,7 +606,6 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
459
606
|
color: var(--faint);
|
|
460
607
|
font-size: 11px;
|
|
461
608
|
}
|
|
462
|
-
.meta { margin-top: 13px; }
|
|
463
609
|
.path { margin-top: 8px; }
|
|
464
610
|
.missing { margin-top: 6px; }
|
|
465
611
|
#hud.s-green .mascot-stage { filter: drop-shadow(0 2px 6px rgba(67, 209, 122, 0.3)); }
|
|
@@ -501,38 +647,51 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
501
647
|
<span class="dot" id="dot"></span>
|
|
502
648
|
<span class="copy">
|
|
503
649
|
<span class="main" id="main">Checking context...</span>
|
|
504
|
-
<span class="client" id="client">EchoMem HUD</span>
|
|
650
|
+
<span class="client-line"><img class="line-icon" id="bubbleagent" src="/assets/hud/codex.svg" alt="" style="display:none" /><span class="client" id="client">EchoMem HUD</span></span>
|
|
651
|
+
<span class="client-line" id="repoline" style="display:none"><img class="line-icon" src="/assets/hud/github.svg" alt="" /><span class="client-repo" id="clientrepo"></span></span>
|
|
505
652
|
</span>
|
|
506
653
|
<button class="cornerbtn" id="minibtn" title="Shrink to mini pill">–</button>
|
|
507
654
|
</div>
|
|
508
|
-
<div id="miniPill" title="EchoMem — click to expand"
|
|
655
|
+
<div id="miniPill" title="EchoMem — click to expand">
|
|
656
|
+
<span class="mdot" id="mdot"></span>
|
|
657
|
+
<span id="mscore">on</span>
|
|
658
|
+
<span class="mcopy">
|
|
659
|
+
<span class="mrow"><img class="micon" id="micon" src="/assets/hud/codex.svg" alt="" style="display:none" /><span id="msession">EchoMem</span></span>
|
|
660
|
+
<span class="mrow" id="mrepoline" style="display:none"><img class="micon" id="mghicon" src="/assets/hud/github.svg" alt="" /><span id="mrepo"></span></span>
|
|
661
|
+
</span>
|
|
662
|
+
</div>
|
|
509
663
|
</div>
|
|
510
664
|
<div class="details">
|
|
511
|
-
<div class="
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
665
|
+
<div class="now-card" id="nowcard">
|
|
666
|
+
<div class="now-sess">
|
|
667
|
+
<img class="now-icon" id="nowicon" src="/assets/hud/codex.svg" alt="" />
|
|
668
|
+
<span class="now-copy"><span class="now-title" id="nowtitle">EchoMem</span><span class="client-line" id="nowrepoline" style="display:none"><img class="line-icon" src="/assets/hud/github.svg" alt="" /><span class="now-repo" id="nowrepo"></span></span></span>
|
|
669
|
+
</div>
|
|
670
|
+
<div class="session-actions">
|
|
671
|
+
<button class="pin-chip" id="pinchip" type="button" title="Return to auto-follow">pinned x</button>
|
|
672
|
+
<button class="jump-chip" id="jumpchip" type="button" title="Open this conversation">↗</button>
|
|
673
|
+
</div>
|
|
674
|
+
<div class="state-row">
|
|
675
|
+
<span class="state-dot" id="stateDot"></span>
|
|
676
|
+
<span class="state-word" id="stateWord">--%</span>
|
|
677
|
+
<button class="why-btn" id="whybtn" type="button" title="Details" aria-label="Details" style="display:none">+</button>
|
|
678
|
+
</div>
|
|
679
|
+
<div class="verdict" id="verdict"></div>
|
|
680
|
+
<div class="evidence" id="evidence"></div>
|
|
681
|
+
<div class="nudge" id="nudge"></div>
|
|
682
|
+
<button class="renew-btn" id="renewbtn"><b>Start new session with context</b><small>carry 100% signal, drop the junk</small></button>
|
|
683
|
+
<div class="renew-result" id="renewresult">
|
|
684
|
+
<div class="renew-msg" id="renewmsg"></div>
|
|
685
|
+
<pre id="renewpre"></pre>
|
|
686
|
+
<div class="renew-actions">
|
|
687
|
+
<button class="copy-btn" id="copybtn">Copy</button>
|
|
688
|
+
<button class="ghost-btn" id="expandbtn">Read all</button>
|
|
689
|
+
<button class="ghost-btn" id="renewagainbtn" title="Generate a fresh context from the latest turns">Renew</button>
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
523
692
|
</div>
|
|
524
|
-
<div class="
|
|
525
|
-
|
|
526
|
-
<div class="alert-detail" id="alertDetail"></div>
|
|
527
|
-
</div>
|
|
528
|
-
<div class="nudge" id="nudge"></div>
|
|
529
|
-
<button class="renew-btn" id="renewbtn">Renew session</button>
|
|
530
|
-
<div class="renew-result" id="renewresult">
|
|
531
|
-
<div class="renew-msg" id="renewmsg"></div>
|
|
532
|
-
<pre id="renewpre"></pre>
|
|
533
|
-
<button class="copy-btn" id="copybtn">Copy to clipboard</button>
|
|
534
|
-
</div>
|
|
535
|
-
<div class="meta" id="meta"></div>
|
|
693
|
+
<div class="tabs" id="tabs"></div>
|
|
694
|
+
<div class="meta-row"><span class="meta-line" id="metaline"></span><button class="switchbtn agent-switch" id="switchbtn" title="Switch agent" style="display:none" type="button"></button></div>
|
|
536
695
|
<div class="path" id="path"></div>
|
|
537
696
|
<button class="reveal" id="reveal" style="display:none">↗ Open session in viewer</button>
|
|
538
697
|
<div class="missing" id="missing"></div>
|
|
@@ -543,35 +702,39 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
543
702
|
const dot = document.getElementById("dot");
|
|
544
703
|
const main = document.getElementById("main");
|
|
545
704
|
const client = document.getElementById("client");
|
|
546
|
-
const
|
|
705
|
+
const clientRepo = document.getElementById("clientrepo");
|
|
547
706
|
const stateDot = document.getElementById("stateDot");
|
|
548
707
|
const stateWordEl = document.getElementById("stateWord");
|
|
549
|
-
const
|
|
550
|
-
const
|
|
708
|
+
const verdictEl = document.getElementById("verdict");
|
|
709
|
+
const nowIcon = document.getElementById("nowicon");
|
|
710
|
+
const nowTitle = document.getElementById("nowtitle");
|
|
711
|
+
const nowRepo = document.getElementById("nowrepo");
|
|
712
|
+
const nowRepoLine = document.getElementById("nowrepoline");
|
|
713
|
+
const pinChip = document.getElementById("pinchip");
|
|
714
|
+
const jumpChip = document.getElementById("jumpchip");
|
|
715
|
+
const whyBtn = document.getElementById("whybtn");
|
|
551
716
|
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
717
|
const nudge = document.getElementById("nudge");
|
|
558
718
|
const renewBtn = document.getElementById("renewbtn");
|
|
559
719
|
const renewResult = document.getElementById("renewresult");
|
|
560
720
|
const renewMsg = document.getElementById("renewmsg");
|
|
561
721
|
const renewPre = document.getElementById("renewpre");
|
|
562
722
|
const copyBtn = document.getElementById("copybtn");
|
|
563
|
-
const
|
|
723
|
+
const expandBtn = document.getElementById("expandbtn");
|
|
724
|
+
const renewAgainBtn = document.getElementById("renewagainbtn");
|
|
725
|
+
const metaLineEl = document.getElementById("metaline");
|
|
726
|
+
const bubbleAgent = document.getElementById("bubbleagent");
|
|
727
|
+
const repoLine = document.getElementById("repoline");
|
|
564
728
|
const pathEl = document.getElementById("path");
|
|
565
|
-
const updatedEl = document.getElementById("updated");
|
|
566
729
|
const tabsEl = document.getElementById("tabs");
|
|
567
730
|
const switchBtn = document.getElementById("switchbtn");
|
|
568
731
|
const revealEl = document.getElementById("reveal");
|
|
569
732
|
const missing = document.getElementById("missing");
|
|
570
|
-
const hudVersion = "v${MCP_PACKAGE_VERSION}";
|
|
571
733
|
let prevTurnState = null;
|
|
572
734
|
let lastCapsule = "";
|
|
573
735
|
let renewResultSessionKey = "";
|
|
574
736
|
let activityEpoch = null;
|
|
737
|
+
let metaTail = ""; // "turn 2 · 178k tokens" — composed in render(), joined with "updated …" each second
|
|
575
738
|
let lastState = null;
|
|
576
739
|
let currentSrc = null;
|
|
577
740
|
let renewTimer = null;
|
|
@@ -594,8 +757,13 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
594
757
|
// returns to the pill. Preference persists (localStorage here, window size in main's bounds file).
|
|
595
758
|
const miniBtn = document.getElementById("minibtn");
|
|
596
759
|
const mdot = document.getElementById("mdot");
|
|
597
|
-
const
|
|
760
|
+
const micon = document.getElementById("micon");
|
|
761
|
+
const mscore = document.getElementById("mscore");
|
|
762
|
+
const msession = document.getElementById("msession");
|
|
763
|
+
const mrepo = document.getElementById("mrepo");
|
|
764
|
+
const mrepoLine = document.getElementById("mrepoline");
|
|
598
765
|
let miniOn = false;
|
|
766
|
+
let whyOpen = false;
|
|
599
767
|
try { miniOn = localStorage.getItem("echomemHudMini") === "1"; } catch {}
|
|
600
768
|
if (miniOn) hud.classList.add("mini");
|
|
601
769
|
function applyMini(next) {
|
|
@@ -606,6 +774,13 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
606
774
|
if (window.echomemHud && typeof window.echomemHud.setMini === "function") window.echomemHud.setMini(next);
|
|
607
775
|
}
|
|
608
776
|
if (miniBtn) miniBtn.addEventListener("click", (e) => { e.stopPropagation(); applyMini(true); });
|
|
777
|
+
if (whyBtn) whyBtn.addEventListener("click", (e) => {
|
|
778
|
+
e.stopPropagation();
|
|
779
|
+
whyOpen = !whyOpen;
|
|
780
|
+
hud.classList.toggle("show-why", whyOpen);
|
|
781
|
+
whyBtn.textContent = whyOpen ? "−" : "+";
|
|
782
|
+
syncHeight();
|
|
783
|
+
});
|
|
609
784
|
let hudDrag = null;
|
|
610
785
|
let suppressNextToggle = false;
|
|
611
786
|
const dragBlockSelector = "button, input, textarea, select, a, pre, .tab, .switchbtn, .reveal, .renew-btn, .copy-btn";
|
|
@@ -670,17 +845,40 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
670
845
|
.catch(() => {});
|
|
671
846
|
});
|
|
672
847
|
|
|
673
|
-
// Click a
|
|
848
|
+
// Click a history row → pin that session as the current one. Auto-follow is the default;
|
|
849
|
+
// the card's "pinned ×" chip releases the pin. The ↗ corner jumps to the conversation in
|
|
850
|
+
// its own app instead of switching the HUD view.
|
|
674
851
|
if (tabsEl) tabsEl.addEventListener("click", (e) => {
|
|
852
|
+
const jump = e.target && e.target.closest ? e.target.closest("[data-jump]") : null;
|
|
853
|
+
if (jump) {
|
|
854
|
+
e.stopPropagation();
|
|
855
|
+
jumpToSession(jump.getAttribute("data-jump"));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
675
858
|
const btn = e.target && e.target.closest ? e.target.closest(".tab") : null;
|
|
676
859
|
if (!btn) return;
|
|
677
|
-
|
|
860
|
+
selectSession(btn.getAttribute("data-id"));
|
|
861
|
+
});
|
|
862
|
+
if (jumpChip) jumpChip.addEventListener("click", (e) => {
|
|
863
|
+
e.stopPropagation();
|
|
864
|
+
jumpToSession(jumpChip.getAttribute("data-jump"));
|
|
865
|
+
});
|
|
866
|
+
function jumpToSession(id) {
|
|
867
|
+
if (!id) return;
|
|
868
|
+
fetch("/jump?id=" + encodeURIComponent(id)).catch(() => {});
|
|
869
|
+
}
|
|
870
|
+
if (pinChip) pinChip.addEventListener("click", (e) => {
|
|
871
|
+
e.stopPropagation();
|
|
872
|
+
selectSession("auto");
|
|
873
|
+
});
|
|
874
|
+
function selectSession(id) {
|
|
875
|
+
if (!id) return;
|
|
678
876
|
fetch("/select?id=" + encodeURIComponent(id))
|
|
679
877
|
.then(() => fetch("/state"))
|
|
680
878
|
.then((r) => r.json())
|
|
681
879
|
.then(render)
|
|
682
880
|
.catch(() => {});
|
|
683
|
-
}
|
|
881
|
+
}
|
|
684
882
|
|
|
685
883
|
document.getElementById("toggle").addEventListener("click", (event) => {
|
|
686
884
|
if (suppressNextToggle) {
|
|
@@ -694,8 +892,6 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
694
892
|
syncHeight();
|
|
695
893
|
});
|
|
696
894
|
|
|
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
895
|
let renewUrgent = false;
|
|
700
896
|
function refreshRenewLabel() {
|
|
701
897
|
if (!renewBtn) return;
|
|
@@ -704,9 +900,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
704
900
|
renderRenewLoadingLabel();
|
|
705
901
|
return;
|
|
706
902
|
}
|
|
707
|
-
renewBtn.innerHTML =
|
|
708
|
-
? '<b>Renew session</b><small>I pack a checkpoint and carry it into your fresh start</small>'
|
|
709
|
-
: "Renew session";
|
|
903
|
+
renewBtn.innerHTML = "<b>Start new session with context</b><small>carry 100% signal, drop the junk</small>";
|
|
710
904
|
}
|
|
711
905
|
|
|
712
906
|
if (renewBtn) renewBtn.addEventListener("click", async () => {
|
|
@@ -751,23 +945,18 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
751
945
|
}
|
|
752
946
|
saveCachedCheckpoint(renewSessionKey, prompt, promptKind, meta);
|
|
753
947
|
if (promptKind === "cloud") saveCheckpointStatus(renewSessionKey, statusFromRenewData(renewData, meta));
|
|
948
|
+
const doneSummary = checkpointSuffix(checkpointSummary({ renewedAt: Date.now(), memoryCount: meta.sessionMemoryCount }));
|
|
754
949
|
showRenewResult({
|
|
755
950
|
sessionKey: renewSessionKey,
|
|
756
951
|
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.",
|
|
952
|
+
message: (promptKind === "local" ? "Local capsule ready" : "Decision log ready") + doneSummary,
|
|
760
953
|
canCopy: true,
|
|
761
954
|
});
|
|
762
955
|
try {
|
|
763
956
|
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.";
|
|
957
|
+
if (renewMsg) renewMsg.textContent = (promptKind === "local" ? "Local capsule copied" : "Decision log copied") + doneSummary;
|
|
767
958
|
} 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.";
|
|
959
|
+
if (renewMsg) renewMsg.textContent = (promptKind === "local" ? "Local capsule ready" : "Decision log ready") + doneSummary;
|
|
771
960
|
}
|
|
772
961
|
} catch {
|
|
773
962
|
} finally {
|
|
@@ -783,10 +972,29 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
783
972
|
try {
|
|
784
973
|
await navigator.clipboard.writeText(lastCapsule);
|
|
785
974
|
copyBtn.textContent = "Copied";
|
|
786
|
-
setTimeout(() => { copyBtn.textContent = "Copy
|
|
975
|
+
setTimeout(() => { copyBtn.textContent = "Copy"; }, 1500);
|
|
787
976
|
} catch {}
|
|
788
977
|
});
|
|
789
978
|
|
|
979
|
+
if (expandBtn) expandBtn.addEventListener("click", (e) => {
|
|
980
|
+
e.stopPropagation();
|
|
981
|
+
if (!renewResult) return;
|
|
982
|
+
const expanded = renewResult.classList.toggle("expanded");
|
|
983
|
+
expandBtn.textContent = expanded ? "Collapse" : "Read all";
|
|
984
|
+
syncHeight();
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// Escape hatch: the session moved on and the saved context is stale — clear it and
|
|
988
|
+
// run the renew flow again.
|
|
989
|
+
if (renewAgainBtn) renewAgainBtn.addEventListener("click", (e) => {
|
|
990
|
+
e.stopPropagation();
|
|
991
|
+
clearRenewResult();
|
|
992
|
+
if (renewBtn) {
|
|
993
|
+
renewBtn.style.display = "block";
|
|
994
|
+
renewBtn.click();
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
|
|
790
998
|
function activeRenewScore() {
|
|
791
999
|
const st = lastState || {};
|
|
792
1000
|
return st.active || st;
|
|
@@ -806,16 +1014,22 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
806
1014
|
renewResultSessionKey = "";
|
|
807
1015
|
if (renewPre) {
|
|
808
1016
|
renewPre.textContent = "";
|
|
809
|
-
renewPre.style.display = "";
|
|
1017
|
+
renewPre.style.display = "none";
|
|
810
1018
|
}
|
|
811
1019
|
if (renewMsg) renewMsg.textContent = "";
|
|
812
1020
|
if (copyBtn) {
|
|
813
|
-
copyBtn.textContent = "Copy
|
|
1021
|
+
copyBtn.textContent = "Copy";
|
|
814
1022
|
copyBtn.style.display = "";
|
|
815
1023
|
}
|
|
816
|
-
if (renewResult)
|
|
1024
|
+
if (renewResult) {
|
|
1025
|
+
renewResult.style.display = "none";
|
|
1026
|
+
renewResult.classList.remove("expanded");
|
|
1027
|
+
}
|
|
1028
|
+
if (expandBtn) expandBtn.textContent = "Read all";
|
|
817
1029
|
}
|
|
818
1030
|
|
|
1031
|
+
// The result replaces the action: once a context exists, the big renew button steps aside
|
|
1032
|
+
// and this card carries the preview + timestamp + copy.
|
|
819
1033
|
function showRenewResult(opts) {
|
|
820
1034
|
const prompt = String((opts && opts.prompt) || "");
|
|
821
1035
|
const body = String((opts && opts.body) || prompt || "");
|
|
@@ -826,11 +1040,19 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
826
1040
|
renewPre.textContent = body;
|
|
827
1041
|
renewPre.style.display = body ? "block" : "none";
|
|
828
1042
|
}
|
|
1043
|
+
if (expandBtn) {
|
|
1044
|
+
expandBtn.style.display = body ? "inline-block" : "none";
|
|
1045
|
+
expandBtn.textContent = "Read all";
|
|
1046
|
+
}
|
|
829
1047
|
if (copyBtn) {
|
|
830
|
-
copyBtn.textContent = "Copy
|
|
1048
|
+
copyBtn.textContent = "Copy";
|
|
831
1049
|
copyBtn.style.display = opts && opts.canCopy ? "inline-block" : "none";
|
|
832
1050
|
}
|
|
833
|
-
if (renewResult)
|
|
1051
|
+
if (renewResult) {
|
|
1052
|
+
renewResult.classList.remove("expanded");
|
|
1053
|
+
renewResult.style.display = "block";
|
|
1054
|
+
}
|
|
1055
|
+
if (renewBtn && !renewStartMs) renewBtn.style.display = "none";
|
|
834
1056
|
syncHeight();
|
|
835
1057
|
}
|
|
836
1058
|
|
|
@@ -895,13 +1117,11 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
895
1117
|
const stale = savedTurn > 0 && currentTurn > savedTurn;
|
|
896
1118
|
const summary = checkpointSummary(cached);
|
|
897
1119
|
if (cached && cached.kind === "local") {
|
|
898
|
-
return
|
|
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.";
|
|
1120
|
+
return savedTurn > 0 ? "Local capsule from turn " + savedTurn + checkpointSuffix(summary) : "Local capsule ready" + checkpointSuffix(summary);
|
|
901
1121
|
}
|
|
902
|
-
return stale
|
|
903
|
-
? "Decision log from turn " + savedTurn +
|
|
904
|
-
: "Decision log
|
|
1122
|
+
return savedTurn > 0 && stale
|
|
1123
|
+
? "Decision log from turn " + savedTurn + checkpointSuffix(summary)
|
|
1124
|
+
: "Decision log ready" + checkpointSuffix(summary);
|
|
905
1125
|
}
|
|
906
1126
|
|
|
907
1127
|
function statusFromRenewData(data, meta) {
|
|
@@ -918,10 +1138,14 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
918
1138
|
function checkpointSummary(item) {
|
|
919
1139
|
const parts = [];
|
|
920
1140
|
const renewedAt = timestampMs(item && item.renewedAt);
|
|
921
|
-
if (renewedAt) parts.push("
|
|
1141
|
+
if (renewedAt) parts.push("generated " + relTime(Date.now() - renewedAt));
|
|
922
1142
|
const count = Number(item && (item.memoryCount ?? item.sessionMemoryCount));
|
|
923
1143
|
if (Number.isFinite(count) && count > 0) parts.push(count + " " + (count === 1 ? "memory" : "memories") + " saved");
|
|
924
|
-
return parts.
|
|
1144
|
+
return parts.join(" · ");
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function checkpointSuffix(summary) {
|
|
1148
|
+
return summary ? " · " + summary : "";
|
|
925
1149
|
}
|
|
926
1150
|
|
|
927
1151
|
function timestampMs(value) {
|
|
@@ -981,7 +1205,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
981
1205
|
showRenewResult({
|
|
982
1206
|
sessionKey: key,
|
|
983
1207
|
prompt: body,
|
|
984
|
-
message: "Decision log
|
|
1208
|
+
message: "Decision log ready" + checkpointSuffix(summary),
|
|
985
1209
|
canCopy: true,
|
|
986
1210
|
});
|
|
987
1211
|
}
|
|
@@ -1071,7 +1295,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1071
1295
|
const elapsed = Date.now() - renewStartMs;
|
|
1072
1296
|
const remaining = Math.max(0, Number(renewEta && renewEta.ms) || 20000) - elapsed;
|
|
1073
1297
|
const detail = remaining > 0 ? "ETA " + fmtDuration(remaining) + " left" : "ETA any moment";
|
|
1074
|
-
renewBtn.innerHTML = "<b>
|
|
1298
|
+
renewBtn.innerHTML = "<b>Preparing context…</b><small>" + esc(detail) + "</small>";
|
|
1075
1299
|
}
|
|
1076
1300
|
|
|
1077
1301
|
function fallbackEtaForScore(score) {
|
|
@@ -1094,13 +1318,17 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1094
1318
|
|
|
1095
1319
|
// Dev/design preview: ?preview=green|amber|red renders that health state with sample data and
|
|
1096
1320
|
// skips the live feed — same pattern as the extraction page's ?preview hook.
|
|
1097
|
-
const
|
|
1321
|
+
const previewParams = new URLSearchParams(location.search);
|
|
1322
|
+
const previewColor = previewParams.get("preview");
|
|
1323
|
+
const previewPinned = previewParams.get("pinned") === "1";
|
|
1324
|
+
if (previewParams.get("capture") === "1") hud.classList.add("capture-preview");
|
|
1098
1325
|
if (previewColor === "green" || previewColor === "amber" || previewColor === "red") {
|
|
1099
1326
|
const sample = {
|
|
1100
1327
|
client: "codex",
|
|
1101
1328
|
label: "EchoMem-Chrome",
|
|
1102
|
-
sourcePath: "/preview/session.jsonl",
|
|
1329
|
+
sourcePath: previewPinned ? "/preview/b.jsonl" : "/preview/session.jsonl",
|
|
1103
1330
|
usefulPct: previewColor === "red" ? 55 : previewColor === "amber" ? 75 : 95,
|
|
1331
|
+
healthScorePct: previewColor === "red" ? 48 : previewColor === "amber" ? 72 : 96,
|
|
1104
1332
|
color: previewColor,
|
|
1105
1333
|
ctTokens: previewColor === "red" ? 165000 : previewColor === "amber" ? 110000 : 24000,
|
|
1106
1334
|
modelContextWindow: 258000,
|
|
@@ -1114,11 +1342,21 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1114
1342
|
};
|
|
1115
1343
|
render({
|
|
1116
1344
|
active: sample,
|
|
1345
|
+
pinnedId: previewPinned ? "p2" : null,
|
|
1346
|
+
sessions: [
|
|
1347
|
+
sample,
|
|
1348
|
+
{ client: "codex", sourcePath: "/preview/b.jsonl", usefulPct: 71, healthScorePct: 71, color: "amber" },
|
|
1349
|
+
{ client: "codex", sourcePath: "/preview/c.jsonl", usefulPct: 94, healthScorePct: 94, color: "green" },
|
|
1350
|
+
],
|
|
1117
1351
|
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: "
|
|
1352
|
+
{ id: "p1", client: "codex", title: "Simplify HUD session display", label: "EchoMem-Chrome", sourcePath: "/preview/session.jsonl", live: true, lastActiveMs: 4000 },
|
|
1353
|
+
{ id: "p2", client: "codex", title: "修复 HUD 切换和图标", label: "EchoMem-Chrome", sourcePath: "/preview/b.jsonl", live: false, lastActiveMs: 480000 },
|
|
1354
|
+
{ id: "p3", client: "codex", title: "Fix extraction preview", label: "EchoMem-Chrome", sourcePath: "/preview/c.jsonl", live: false, lastActiveMs: 2400000 },
|
|
1355
|
+
{ id: "p4", client: "codex", title: "Port Claude work", label: "EchoForPeople", sourcePath: "/preview/d.jsonl", live: false, lastActiveMs: 9000000 },
|
|
1356
|
+
{ id: "p5", client: "codex", title: "Stamp fixes with Echo mascot", label: "EchoMem-Chrome", sourcePath: "/preview/e.jsonl", live: false, lastActiveMs: 16200000 },
|
|
1357
|
+
{ id: "p6", client: "codex", title: "Review migration checks", label: "EchoForPeople", sourcePath: "/preview/f.jsonl", live: false, lastActiveMs: 21600000 },
|
|
1358
|
+
{ id: "p7", client: "codex", title: "Polish setup story", label: "EchoMem-Chrome", sourcePath: "/preview/g.jsonl", live: false, lastActiveMs: 34200000 },
|
|
1359
|
+
{ id: "p8", client: "claude-code", title: "DG-Web refactor", label: "DG-Web", sourcePath: "/preview/h.jsonl", live: false, lastActiveMs: 39600000 },
|
|
1122
1360
|
],
|
|
1123
1361
|
});
|
|
1124
1362
|
hud.classList.add("open");
|
|
@@ -1134,8 +1372,10 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1134
1372
|
lastState = state;
|
|
1135
1373
|
const activeClient = score && score.client ? score.client : state && state.active && state.active.client;
|
|
1136
1374
|
const currentSessionKey = sessionKeyFor(state, score);
|
|
1375
|
+
const viewColor = score && typeof score.usefulPct === "number" ? (score.color || "green") : "idle";
|
|
1137
1376
|
if (renewResultSessionKey && renewResultSessionKey !== currentSessionKey) clearRenewResult();
|
|
1138
|
-
|
|
1377
|
+
const activeId = score && score.sourcePath ? activeIdFor(state, score.sourcePath, activeClient) : null;
|
|
1378
|
+
renderTabs(state, activeId, activeClient);
|
|
1139
1379
|
renderSwitchButton(state, activeClient);
|
|
1140
1380
|
if (!score || typeof score.usefulPct !== "number") {
|
|
1141
1381
|
renderIdle(state);
|
|
@@ -1143,61 +1383,65 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1143
1383
|
}
|
|
1144
1384
|
|
|
1145
1385
|
const color = score.color || "green";
|
|
1146
|
-
const stateName = stateWord(color);
|
|
1147
1386
|
const sat = fullnessPct(score);
|
|
1148
1387
|
const oldPct = clamp(score.pollutionPct || 0, 0, 95);
|
|
1388
|
+
const health = healthScore(score);
|
|
1149
1389
|
const repeatCount = score.buckets && score.buckets.range_redundant ? score.buckets.range_redundant.count || 0 : 0;
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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;
|
|
1390
|
+
const info = activeSessionInfo(score, state);
|
|
1391
|
+
|
|
1392
|
+
dot.className = "dot " + color;
|
|
1393
|
+
stateDot.className = "state-dot " + color;
|
|
1158
1394
|
hud.classList.remove("s-green", "s-amber", "s-red");
|
|
1159
|
-
|
|
1160
|
-
main.
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1395
|
+
hud.classList.add("s-" + color);
|
|
1396
|
+
main.innerHTML = health + '%<span class="main-rest"> of tokens are useful</span>';
|
|
1397
|
+
renderMini(color, health + "%", info);
|
|
1398
|
+
client.textContent = info.title || info.repo;
|
|
1399
|
+
if (bubbleAgent) {
|
|
1400
|
+
bubbleAgent.src = iconForFamily(agentFamily(score.client) || "codex");
|
|
1401
|
+
bubbleAgent.style.display = "inline-block";
|
|
1402
|
+
}
|
|
1403
|
+
const bubbleRepo = info.repo && info.repo !== info.title ? info.repo : "";
|
|
1404
|
+
if (clientRepo) clientRepo.textContent = bubbleRepo;
|
|
1405
|
+
if (repoLine) repoLine.style.display = bubbleRepo ? "flex" : "none";
|
|
1406
|
+
client.classList.remove("voice");
|
|
1168
1407
|
activityEpoch = typeof score.lastActiveMs === "number" ? Date.now() - score.lastActiveMs : null;
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1408
|
+
const metaParts = [];
|
|
1409
|
+
if (typeof score.turn === "number" && score.turn > 0) metaParts.push("turn " + score.turn);
|
|
1410
|
+
if (score.ctTokens > 0) metaParts.push(fmt(score.ctTokens) + " tok");
|
|
1411
|
+
metaTail = metaParts.join(" · ");
|
|
1412
|
+
renderMetaLine();
|
|
1413
|
+
stateWordEl.textContent = health + "%";
|
|
1414
|
+
if (verdictEl) verdictEl.textContent = verdictFor(color, oldPct);
|
|
1415
|
+
if (nowIcon) nowIcon.src = iconForFamily(agentFamily(score.client) || "codex");
|
|
1416
|
+
if (nowTitle) nowTitle.textContent = info.title;
|
|
1417
|
+
if (nowRepo) nowRepo.textContent = bubbleRepo;
|
|
1418
|
+
if (nowRepoLine) nowRepoLine.style.display = bubbleRepo ? "flex" : "none";
|
|
1419
|
+
if (pinChip) pinChip.style.display = state && state.pinnedId ? "inline-block" : "none";
|
|
1420
|
+
if (jumpChip) {
|
|
1421
|
+
if (activeId) jumpChip.setAttribute("data-jump", activeId);
|
|
1422
|
+
jumpChip.style.display = activeId ? "inline-block" : "none";
|
|
1423
|
+
}
|
|
1424
|
+
renderWhy(score, oldPct, repeatCount, sat, health, color);
|
|
1180
1425
|
pathEl.textContent = "";
|
|
1181
1426
|
currentSrc = score.sourcePath || null;
|
|
1182
1427
|
if (revealEl) revealEl.style.display = !DEMO_CLEAN && currentSrc ? "inline-block" : "none";
|
|
1183
1428
|
missing.textContent = formatMissing(state.missing);
|
|
1184
1429
|
|
|
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
1430
|
nudge.style.display = "none";
|
|
1189
1431
|
|
|
1190
|
-
|
|
1191
|
-
renewUrgent = urgent;
|
|
1192
|
-
if (renewBtn) {
|
|
1193
|
-
renewBtn.style.display = "block"; // always available — warm-start any session, even fresh
|
|
1194
|
-
refreshRenewLabel();
|
|
1195
|
-
}
|
|
1432
|
+
renewUrgent = color === "red";
|
|
1196
1433
|
const restoredPrompt = restoreCachedCheckpointFor(score, currentSessionKey);
|
|
1197
1434
|
if (!restoredPrompt) {
|
|
1198
1435
|
const restoredStatus = restoreCheckpointStatusFor(score, currentSessionKey);
|
|
1199
1436
|
if (!restoredStatus) maybeFetchCheckpointStatus(score, currentSessionKey);
|
|
1200
1437
|
}
|
|
1438
|
+
// A generated context replaces the call to generate one: while the result card is up
|
|
1439
|
+
// for this session, the big button stays hidden (Renew in the card re-runs the flow).
|
|
1440
|
+
const resultShowing = renewResult && renewResult.style.display === "block" && renewResultSessionKey === currentSessionKey;
|
|
1441
|
+
if (renewBtn) {
|
|
1442
|
+
renewBtn.style.display = resultShowing && !renewStartMs ? "none" : "block";
|
|
1443
|
+
refreshRenewLabel();
|
|
1444
|
+
}
|
|
1201
1445
|
prevTurnState = {
|
|
1202
1446
|
src: score.sourcePath,
|
|
1203
1447
|
turn: score.turn,
|
|
@@ -1214,92 +1458,84 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1214
1458
|
stateDot.className = "state-dot";
|
|
1215
1459
|
hud.classList.remove("s-green", "s-amber", "s-red");
|
|
1216
1460
|
prevTurnState = null;
|
|
1217
|
-
main.textContent = "
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1461
|
+
main.textContent = "MCP on";
|
|
1462
|
+
renderMini("idle", "on", { client: "EchoMem", title: "MCP", repo: "" });
|
|
1463
|
+
client.textContent = "Waiting for Codex or Claude";
|
|
1464
|
+
if (bubbleAgent) bubbleAgent.style.display = "none";
|
|
1465
|
+
if (clientRepo) clientRepo.textContent = "";
|
|
1466
|
+
if (repoLine) repoLine.style.display = "none";
|
|
1467
|
+
client.classList.remove("voice");
|
|
1221
1468
|
activityEpoch = null;
|
|
1222
|
-
|
|
1469
|
+
metaTail = "";
|
|
1470
|
+
renderMetaLine();
|
|
1223
1471
|
currentSrc = null;
|
|
1224
1472
|
if (revealEl) revealEl.style.display = "none";
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1473
|
+
stateWordEl.textContent = "on";
|
|
1474
|
+
if (verdictEl) verdictEl.textContent = "Waiting for Codex or Claude.";
|
|
1475
|
+
if (nowTitle) nowTitle.textContent = "EchoMem MCP";
|
|
1476
|
+
if (nowRepo) nowRepo.textContent = "";
|
|
1477
|
+
if (nowRepoLine) nowRepoLine.style.display = "none";
|
|
1478
|
+
if (pinChip) pinChip.style.display = "none";
|
|
1479
|
+
if (jumpChip) jumpChip.style.display = "none";
|
|
1480
|
+
evidence.textContent = "";
|
|
1481
|
+
if (whyBtn) whyBtn.style.display = "none";
|
|
1482
|
+
whyOpen = false;
|
|
1483
|
+
hud.classList.remove("show-why");
|
|
1234
1484
|
nudge.style.display = "none";
|
|
1235
1485
|
if (renewBtn) renewBtn.style.display = "none";
|
|
1236
1486
|
clearRenewResult();
|
|
1237
|
-
metaEl.textContent = "";
|
|
1238
1487
|
pathEl.textContent = "";
|
|
1239
1488
|
missing.textContent = formatMissing(state && state.missing);
|
|
1240
1489
|
syncHeight();
|
|
1241
1490
|
}
|
|
1242
1491
|
|
|
1243
|
-
function
|
|
1244
|
-
if (
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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;
|
|
1492
|
+
function renderMini(color, scoreText, info) {
|
|
1493
|
+
if (mdot) mdot.className = "mdot " + color;
|
|
1494
|
+
const fam = agentFamily(info && info.clientRaw);
|
|
1495
|
+
if (micon) {
|
|
1496
|
+
micon.src = iconForFamily(fam || "codex");
|
|
1497
|
+
micon.style.display = fam ? "inline-block" : "none";
|
|
1259
1498
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1499
|
+
if (mscore) mscore.textContent = scoreText;
|
|
1500
|
+
if (msession) msession.textContent = miniTitle(info);
|
|
1501
|
+
const repo = info && info.repo ? info.repo : "";
|
|
1502
|
+
if (mrepo) mrepo.textContent = repo;
|
|
1503
|
+
if (mrepoLine) mrepoLine.style.display = repo ? "flex" : "none";
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function renderWhy(score, oldPct, repeatCount, sat, health, color) {
|
|
1507
|
+
const rows = [];
|
|
1508
|
+
if (sat > 0) rows.push(["context", score.modelContextWindow ? sat + "% full · " + fmt(score.ctTokens) : fmt(score.ctTokens) + " tokens"]);
|
|
1509
|
+
if (score.pollutionTok > 0) rows.push(["junk", fmt(score.pollutionTok) + " (" + Math.round(oldPct) + "% of window)"]);
|
|
1510
|
+
for (const row of junkBreakdown(score)) rows.push(row);
|
|
1511
|
+
rows.push(["source", ctSourceLabel(score.ctSource)]);
|
|
1512
|
+
evidence.innerHTML = rows.map((row) => '<div class="why-line"><span class="why-k">' + esc(row[0]) + '</span><span class="why-v">' + esc(row[1]) + "</span></div>").join("");
|
|
1513
|
+
const useful = color !== "green" || oldPct >= 10 || repeatCount > 0 || sat >= 80;
|
|
1514
|
+
if (whyBtn) {
|
|
1515
|
+
whyBtn.style.display = useful ? "inline-flex" : "none";
|
|
1516
|
+
whyBtn.textContent = whyOpen ? "−" : "+";
|
|
1517
|
+
}
|
|
1518
|
+
if (!useful) {
|
|
1519
|
+
whyOpen = false;
|
|
1520
|
+
hud.classList.remove("show-why");
|
|
1521
|
+
} else {
|
|
1522
|
+
hud.classList.toggle("show-why", whyOpen);
|
|
1274
1523
|
}
|
|
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
1524
|
}
|
|
1298
1525
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1526
|
+
// Top junk categories from the latest local-live waste partition, biggest first.
|
|
1527
|
+
function junkBreakdown(score) {
|
|
1528
|
+
const buckets = score.buckets || {};
|
|
1529
|
+
const tok = (key) => (buckets[key] && buckets[key].tokens) || 0;
|
|
1530
|
+
const copies = buckets.range_redundant || {};
|
|
1531
|
+
const entries = [
|
|
1532
|
+
["stale screenshots", tok("superseded_image"), ""],
|
|
1533
|
+
["superseded copies", tok("range_redundant"), copies.count > 0 ? " · " + copies.count + " re-read" : ""],
|
|
1534
|
+
["carried tool output", tok("stale_tool_output"), ""],
|
|
1535
|
+
["outdated reads", tok("stale_read"), ""],
|
|
1536
|
+
].filter((entry) => entry[1] > 0);
|
|
1537
|
+
entries.sort((a, b) => b[1] - a[1]);
|
|
1538
|
+
return entries.map((entry) => ["· " + entry[0], fmt(entry[1]) + entry[2]]);
|
|
1303
1539
|
}
|
|
1304
1540
|
|
|
1305
1541
|
function fullnessPct(score) {
|
|
@@ -1308,10 +1544,10 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1308
1544
|
return 0;
|
|
1309
1545
|
}
|
|
1310
1546
|
|
|
1311
|
-
function
|
|
1312
|
-
if (
|
|
1313
|
-
if (
|
|
1314
|
-
return
|
|
1547
|
+
function healthScore(score) {
|
|
1548
|
+
if (typeof score.healthScorePct === "number" && isFinite(score.healthScorePct)) return clamp(score.healthScorePct, 0, 100);
|
|
1549
|
+
if (typeof score.usefulPct === "number" && isFinite(score.usefulPct)) return clamp(score.usefulPct, 0, 100);
|
|
1550
|
+
return 0;
|
|
1315
1551
|
}
|
|
1316
1552
|
|
|
1317
1553
|
function ctSourceLabel(src) {
|
|
@@ -1324,13 +1560,23 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1324
1560
|
return "Not seeing " + list.map(label).join(", ") + " yet";
|
|
1325
1561
|
}
|
|
1326
1562
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1563
|
+
function activeSessionInfo(score, state) {
|
|
1564
|
+
const list = (state && state.recentSessions) || [];
|
|
1565
|
+
const match = list.filter((s) => s.client === score.client && s.sourcePath === score.sourcePath)[0];
|
|
1566
|
+
const repo = String((match && match.label) || score.label || "");
|
|
1567
|
+
const title = String((match && match.title) || repo || label(score.client));
|
|
1568
|
+
return {
|
|
1569
|
+
client: label(score.client),
|
|
1570
|
+
clientRaw: score.client,
|
|
1571
|
+
title,
|
|
1572
|
+
repo,
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function miniTitle(info) {
|
|
1577
|
+
if (!info) return "EchoMem";
|
|
1578
|
+
const title = info.title && info.title !== info.client ? info.title : info.repo;
|
|
1579
|
+
return title || info.client;
|
|
1334
1580
|
}
|
|
1335
1581
|
|
|
1336
1582
|
function renderSwitchButton(state, activeClient) {
|
|
@@ -1361,22 +1607,43 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1361
1607
|
return list.filter((s) => agentFamily(s.client) === targetFamily)[0] || null;
|
|
1362
1608
|
}
|
|
1363
1609
|
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1610
|
+
// History rows: recent sessions that are NOT the current one (the current session lives in the
|
|
1611
|
+
// now-card above — every number and action up there belongs to it). Clicking a row pins it as
|
|
1612
|
+
// the new current session; the card's "pinned ×" chip releases back to auto-follow. Scored
|
|
1613
|
+
// sessions carry their own health dot + %, so "Echo is following you across agents" reads off
|
|
1614
|
+
// the list itself. Family-scoped, so Codex never recommends Claude sessions or vice versa.
|
|
1366
1615
|
function renderTabs(state, activeId, activeClient) {
|
|
1367
1616
|
if (!tabsEl) return;
|
|
1368
|
-
const list = visibleRecentSessions(state, activeClient);
|
|
1369
|
-
if (list.length
|
|
1370
|
-
const pinnedId = state && state.pinnedId ? state.pinnedId : null;
|
|
1617
|
+
const list = visibleRecentSessions(state, activeClient).filter((s) => s.id !== activeId);
|
|
1618
|
+
if (!list.length) { tabsEl.innerHTML = ""; return; }
|
|
1371
1619
|
const chips = list.map((s) => {
|
|
1372
|
-
const
|
|
1373
|
-
const
|
|
1620
|
+
const activity = s.live ? '<span class="spin"></span>' : "";
|
|
1621
|
+
const hv = sessionHealthFor(state, s);
|
|
1622
|
+
const status = hv
|
|
1623
|
+
? '<span class="tab-health"><span class="tab-status ' + esc(hv.color) + '"></span>' + hv.health + "%</span>"
|
|
1624
|
+
: "";
|
|
1374
1625
|
const title = s.title || label(s.client);
|
|
1375
|
-
|
|
1626
|
+
const repo = s.label && s.label !== title ? s.label : "";
|
|
1627
|
+
const family = agentFamily(s.client);
|
|
1628
|
+
const repoCell = repo
|
|
1629
|
+
? '<span class="tab-repo"><img class="tab-gh" src="/assets/hud/github.svg" alt="" /><span class="tab-reponame">' + esc(repo) + "</span></span>"
|
|
1630
|
+
: '<span class="tab-repo"><span class="tab-reponame">' + esc(label(s.client)) + "</span></span>";
|
|
1631
|
+
const time = typeof s.lastActiveMs === "number" ? '<span class="tab-time" title="Last active here">' + esc(relTime(s.lastActiveMs)) + "</span>" : "";
|
|
1632
|
+
return '<button class="tab" data-id="' + esc(s.id) + '" title="' + esc("Switch to · " + label(s.client) + " · " + title + (repo ? " · " + repo : "")) + '">'
|
|
1633
|
+
+ '<img class="tab-icon" src="' + iconForFamily(family) + '" alt="" />'
|
|
1634
|
+
+ '<span class="tab-copy"><span class="tab-title">' + esc(title) + activity + "</span>" + repoCell + "</span>" + status + time
|
|
1635
|
+
+ '<span class="tab-jump" data-jump="' + esc(s.id) + '" title="Open this conversation in ' + esc(label(s.client)) + '">↗</span></button>';
|
|
1376
1636
|
});
|
|
1377
|
-
chips.unshift('<button class="tab' + (pinnedId ? "" : " active") + '" data-id="auto" title="Auto-select the current foreground agent session">Auto</button>');
|
|
1378
1637
|
tabsEl.innerHTML = chips.join("");
|
|
1379
1638
|
}
|
|
1639
|
+
// Health for a listed session, joined from the scored views the monitor already computes.
|
|
1640
|
+
// Sessions Echo isn't actively scoring get no dot — honest absence, not a fake green.
|
|
1641
|
+
function sessionHealthFor(state, s) {
|
|
1642
|
+
const views = ((state && state.sessions) || []).concat((state && state.scores) || []);
|
|
1643
|
+
const m = views.filter((v) => v && v.sourcePath === s.sourcePath && v.client === s.client && typeof v.usefulPct === "number")[0];
|
|
1644
|
+
if (!m) return null;
|
|
1645
|
+
return { color: m.color || "green", health: healthScore(m) };
|
|
1646
|
+
}
|
|
1380
1647
|
function visibleRecentSessions(state, activeClient) {
|
|
1381
1648
|
const list = (state && state.recentSessions) || [];
|
|
1382
1649
|
const family = agentFamily(activeClient);
|
|
@@ -1404,9 +1671,19 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1404
1671
|
return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
1405
1672
|
}
|
|
1406
1673
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1674
|
+
// One quiet mono line for all technical metadata: "16s ago · turn 2 · 178k tok".
|
|
1675
|
+
function renderMetaLine() {
|
|
1676
|
+
if (!metaLineEl) return;
|
|
1677
|
+
const upd = activityEpoch === null ? "" : relTime(Date.now() - activityEpoch);
|
|
1678
|
+
metaLineEl.textContent = [upd, metaTail].filter(Boolean).join(" · ");
|
|
1679
|
+
}
|
|
1680
|
+
// One plain sentence saying what the score means for THIS session. Direct, no hedging,
|
|
1681
|
+
// and no second number — the score above is the only number on the card.
|
|
1682
|
+
function verdictFor(color, oldPct) {
|
|
1683
|
+
const usefulPct = 100 - clamp(oldPct || 0, 0, 100);
|
|
1684
|
+
if (color === "red") return usefulPct <= 20 ? "Mostly junk — carry over now." : "Too much junk – consider to carry over.";
|
|
1685
|
+
if (color === "amber") return "Junk is piling up.";
|
|
1686
|
+
return "Context is clean.";
|
|
1410
1687
|
}
|
|
1411
1688
|
function relTime(ms) {
|
|
1412
1689
|
const s = Math.max(0, Math.round(ms / 1000));
|
|
@@ -1416,7 +1693,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1416
1693
|
if (m < 60) return m + "m ago";
|
|
1417
1694
|
return Math.round(m / 60) + "h ago";
|
|
1418
1695
|
}
|
|
1419
|
-
setInterval(
|
|
1696
|
+
setInterval(renderMetaLine, 1000);
|
|
1420
1697
|
|
|
1421
1698
|
function label(clientName) {
|
|
1422
1699
|
if (clientName === "codex") return "Codex";
|
|
@@ -1432,14 +1709,6 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1432
1709
|
return String(Math.round(n));
|
|
1433
1710
|
}
|
|
1434
1711
|
|
|
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
1712
|
function clamp(value, min, max) {
|
|
1444
1713
|
return Math.max(min, Math.min(max, Number(value) || 0));
|
|
1445
1714
|
}
|