@echomem/mcp 1.4.3 → 1.4.5
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 +12 -7
- package/dist/hud/capsule.js +68 -25
- package/dist/hud/electron-main.js +11 -3
- package/dist/hud/metric.js +16 -3
- package/dist/hud/monitor.js +353 -15
- package/dist/hud/server.js +25 -0
- package/dist/hud/web.js +737 -428
- package/dist/index.js +14 -1
- package/dist/package-metadata.js +4 -2
- package/dist/setup.js +28 -33
- package/dist/update-check.js +154 -0
- package/dist/v1-contract.js +18 -1
- package/package.json +2 -2
package/dist/hud/web.js
CHANGED
|
@@ -8,93 +8,383 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
8
8
|
<style>
|
|
9
9
|
:root {
|
|
10
10
|
color-scheme: light;
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
11
|
+
--paper: #f5f3ee;
|
|
12
|
+
--card: rgba(255, 253, 248, 0.98);
|
|
13
|
+
--ink: #141414;
|
|
14
|
+
--blue: #1a3a8f;
|
|
15
|
+
--muted: #6b675d;
|
|
16
|
+
--faint: #8a8678;
|
|
17
|
+
--brick: #c7372f;
|
|
18
|
+
--green: #43d17a;
|
|
19
|
+
--amber: #e9b949;
|
|
20
|
+
--line: rgba(26, 58, 143, 0.22);
|
|
21
|
+
--track: rgba(26, 58, 143, 0.1);
|
|
22
|
+
--shadow: drop-shadow(0 2px 7px rgba(26, 58, 143, 0.12));
|
|
19
23
|
font-family: "Avenir Next", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
20
24
|
background: transparent;
|
|
21
25
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
* { box-sizing: border-box; }
|
|
27
|
+
body {
|
|
28
|
+
margin: 0;
|
|
29
|
+
min-height: 100vh;
|
|
30
|
+
display: grid;
|
|
31
|
+
place-items: start center;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
background: transparent;
|
|
34
|
+
color: var(--ink);
|
|
35
|
+
}
|
|
36
|
+
#hud {
|
|
37
|
+
width: min(360px, calc(100vw - 16px));
|
|
38
|
+
background: transparent;
|
|
39
|
+
-webkit-font-smoothing: antialiased;
|
|
40
|
+
}
|
|
41
|
+
.glance {
|
|
42
|
+
position: relative;
|
|
43
|
+
height: 112px;
|
|
44
|
+
user-select: none;
|
|
45
|
+
cursor: default;
|
|
46
|
+
}
|
|
47
|
+
.drag {
|
|
48
|
+
position: absolute;
|
|
49
|
+
left: 4px;
|
|
50
|
+
top: 40px;
|
|
51
|
+
width: 20px;
|
|
52
|
+
height: 32px;
|
|
53
|
+
border-radius: 8px;
|
|
54
|
+
-webkit-app-region: drag;
|
|
55
|
+
opacity: 0.78;
|
|
56
|
+
z-index: 5;
|
|
57
|
+
}
|
|
58
|
+
.drag::before {
|
|
59
|
+
content: "";
|
|
60
|
+
position: absolute;
|
|
61
|
+
left: 4px;
|
|
62
|
+
top: 7px;
|
|
63
|
+
width: 4px;
|
|
64
|
+
height: 4px;
|
|
65
|
+
border-radius: 50%;
|
|
66
|
+
background: rgba(26, 58, 143, 0.42);
|
|
67
|
+
box-shadow: 0 10px 0 rgba(26, 58, 143, 0.42), 10px 0 0 rgba(26, 58, 143, 0.42), 10px 10px 0 rgba(26, 58, 143, 0.42);
|
|
68
|
+
}
|
|
69
|
+
.mascot-stage {
|
|
70
|
+
position: absolute;
|
|
71
|
+
left: 24px;
|
|
72
|
+
top: 26px;
|
|
73
|
+
z-index: 4;
|
|
74
|
+
width: 70px;
|
|
75
|
+
height: 70px;
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
border-radius: 18px;
|
|
78
|
+
transition: filter 0.45s ease;
|
|
79
|
+
}
|
|
80
|
+
@keyframes echo-bob {
|
|
81
|
+
0%, 100% { transform: translateY(0); }
|
|
82
|
+
50% { transform: translateY(-2.5px); }
|
|
83
|
+
}
|
|
84
|
+
.mascot {
|
|
85
|
+
position: absolute;
|
|
86
|
+
left: -4px;
|
|
87
|
+
top: -2px;
|
|
88
|
+
width: 80px;
|
|
89
|
+
height: 78px;
|
|
90
|
+
object-fit: cover;
|
|
91
|
+
object-position: center top;
|
|
92
|
+
pointer-events: none;
|
|
93
|
+
animation: echo-bob 4.5s ease-in-out infinite;
|
|
94
|
+
}
|
|
95
|
+
@media (prefers-reduced-motion: reduce) {
|
|
96
|
+
.mascot { animation: none; }
|
|
97
|
+
}
|
|
98
|
+
.status-bubble {
|
|
99
|
+
position: absolute;
|
|
100
|
+
left: 84px;
|
|
101
|
+
right: 6px;
|
|
102
|
+
top: 15px;
|
|
103
|
+
min-height: 72px;
|
|
104
|
+
z-index: 2;
|
|
105
|
+
border: 1.7px solid rgba(26, 58, 143, 0.78);
|
|
106
|
+
border-radius: 34px;
|
|
107
|
+
background: #fdfaf2;
|
|
108
|
+
filter: var(--shadow);
|
|
109
|
+
}
|
|
110
|
+
.status-bubble::after {
|
|
111
|
+
content: "";
|
|
112
|
+
position: absolute;
|
|
113
|
+
left: 58px;
|
|
114
|
+
bottom: -14px;
|
|
115
|
+
width: 18px;
|
|
116
|
+
height: 18px;
|
|
117
|
+
border-left: 1.7px solid rgba(26, 58, 143, 0.78);
|
|
118
|
+
border-bottom: 1.7px solid rgba(26, 58, 143, 0.78);
|
|
119
|
+
border-bottom-left-radius: 10px;
|
|
120
|
+
background: #fdfaf2;
|
|
121
|
+
transform: rotate(-18deg) skewY(-18deg);
|
|
122
|
+
}
|
|
123
|
+
.copy {
|
|
124
|
+
position: relative;
|
|
125
|
+
z-index: 2;
|
|
126
|
+
min-width: 0;
|
|
127
|
+
display: grid;
|
|
128
|
+
grid-template-columns: minmax(0, 1fr);
|
|
129
|
+
gap: 4px;
|
|
130
|
+
padding: 20px 38px 18px 30px;
|
|
131
|
+
}
|
|
132
|
+
.dot {
|
|
133
|
+
position: absolute;
|
|
134
|
+
left: 14px;
|
|
135
|
+
top: 23px;
|
|
136
|
+
z-index: 3;
|
|
137
|
+
width: 10px;
|
|
138
|
+
height: 10px;
|
|
139
|
+
border: 2px solid #fdfaf2;
|
|
140
|
+
border-radius: 999px;
|
|
141
|
+
background: var(--green);
|
|
142
|
+
box-shadow: 0 0 0 1px rgba(26, 58, 143, 0.14);
|
|
143
|
+
}
|
|
144
|
+
.dot.amber { background: var(--amber); }
|
|
41
145
|
.dot.red { background: #ff5f57; }
|
|
42
|
-
.details { display: none; position: relative; margin: 2px 8px 0 24px; border: 1px solid var(--echo-line-brand); border-radius: 18px; background: rgba(253, 250, 242, .98); padding: 14px 16px 15px; font-size: 12px; color: var(--echo-ink-text); filter: var(--echo-shadow-blue); }
|
|
43
|
-
#hud.open .details { display: block; }
|
|
44
|
-
.metric { display: flex; justify-content: space-between; gap: 16px; padding: 6px 0; border-bottom: 1px solid rgba(26, 58, 143, .10); }
|
|
45
|
-
.metric:last-child { border-bottom: 0; }
|
|
46
|
-
.metric span { color: var(--echo-ink-mute); }
|
|
47
|
-
.metric strong { color: var(--echo-ink-primary); font-weight: 700; }
|
|
48
|
-
button.renew-btn { display: none; width: 100%; margin-top: 12px; border: 1px solid rgba(199, 55, 47, .30); border-radius: 999px; padding: 9px 10px; background: rgba(199, 55, 47, .08); color: #c7372f; font-weight: 700; font-size: 13px; cursor: pointer; transition: background .2s ease; }
|
|
49
|
-
button.renew-btn:hover { background: rgba(199, 55, 47, .14); }
|
|
50
|
-
button.renew-btn:active { background: rgba(199, 55, 47, .22); }
|
|
51
|
-
button.renew-btn.loading { opacity: .6; cursor: wait; }
|
|
52
|
-
.renew-result { display: none; margin-top: 10px; border: 1px solid rgba(26, 58, 143, .18); border-radius: 12px; background: rgba(26, 58, 143, .04); padding: 10px 12px; }
|
|
53
|
-
.renew-result .renew-msg { font-size: 11.5px; font-weight: 700; color: var(--echo-ink-primary); margin-bottom: 6px; }
|
|
54
|
-
.renew-result pre { max-height: 120px; overflow: auto; font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 9.5px; color: var(--echo-ink-text); white-space: pre-wrap; word-break: break-word; margin: 0; }
|
|
55
|
-
.renew-result .copy-btn { display: inline-block; margin-top: 6px; border: 1px solid rgba(26, 58, 143, .22); border-radius: 999px; padding: 5px 14px; background: rgba(26, 58, 143, .08); color: var(--echo-ink-primary); font-weight: 700; font-size: 11px; cursor: pointer; }
|
|
56
|
-
.renew-result .copy-btn:hover { background: rgba(26, 58, 143, .14); }
|
|
57
|
-
.path { margin-top: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--echo-ink-mute); }
|
|
58
|
-
.missing { margin-top: 8px; color: var(--echo-ink-mute); }
|
|
59
|
-
.summary { margin: 0 0 8px; font-size: 12.5px; font-weight: 700; color: var(--echo-ink-primary); line-height: 1.25; }
|
|
60
|
-
.note { margin: 10px 0 0; font-size: 10.5px; color: var(--echo-ink-mute); line-height: 1.3; }
|
|
61
146
|
.dot.idle { background: #c9c5b8; }
|
|
62
|
-
.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
#hud.
|
|
97
|
-
|
|
147
|
+
.main {
|
|
148
|
+
min-width: 0;
|
|
149
|
+
overflow: hidden;
|
|
150
|
+
text-overflow: ellipsis;
|
|
151
|
+
white-space: nowrap;
|
|
152
|
+
font-size: 15px;
|
|
153
|
+
font-weight: 800;
|
|
154
|
+
letter-spacing: 0;
|
|
155
|
+
line-height: 1.1;
|
|
156
|
+
color: var(--ink);
|
|
157
|
+
}
|
|
158
|
+
.client {
|
|
159
|
+
min-width: 0;
|
|
160
|
+
overflow: hidden;
|
|
161
|
+
text-overflow: ellipsis;
|
|
162
|
+
white-space: nowrap;
|
|
163
|
+
font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
|
164
|
+
font-size: 10px;
|
|
165
|
+
letter-spacing: 0.12em;
|
|
166
|
+
line-height: 1;
|
|
167
|
+
text-transform: uppercase;
|
|
168
|
+
color: var(--faint);
|
|
169
|
+
}
|
|
170
|
+
.details {
|
|
171
|
+
display: none;
|
|
172
|
+
position: relative;
|
|
173
|
+
margin: 2px 8px 0 24px;
|
|
174
|
+
padding: 16px 17px 15px;
|
|
175
|
+
border: 1px solid var(--line);
|
|
176
|
+
border-radius: 16px;
|
|
177
|
+
background: var(--card);
|
|
178
|
+
color: var(--ink);
|
|
179
|
+
filter: var(--shadow);
|
|
180
|
+
}
|
|
181
|
+
#hud.open .details { display: block; }
|
|
182
|
+
.head {
|
|
183
|
+
display: flex;
|
|
184
|
+
justify-content: space-between;
|
|
185
|
+
align-items: baseline;
|
|
186
|
+
min-height: 15px;
|
|
187
|
+
margin-bottom: 10px;
|
|
188
|
+
}
|
|
189
|
+
.turn, .upd {
|
|
190
|
+
overflow: hidden;
|
|
191
|
+
text-overflow: ellipsis;
|
|
192
|
+
white-space: nowrap;
|
|
193
|
+
font-size: 11px;
|
|
194
|
+
color: var(--faint);
|
|
195
|
+
}
|
|
196
|
+
.tabs { display: flex; gap: 6px; overflow-x: auto; margin: 2px 0 12px; padding-bottom: 2px; }
|
|
197
|
+
.tabs::-webkit-scrollbar { height: 0; }
|
|
198
|
+
.tab {
|
|
199
|
+
flex: 0 0 auto; font-size: 10.5px; line-height: 1; padding: 5px 10px; border-radius: 999px;
|
|
200
|
+
border: 1px solid rgba(0, 0, 0, .12); background: rgba(0, 0, 0, .04);
|
|
201
|
+
color: var(--faint); cursor: pointer; white-space: nowrap; font-family: inherit;
|
|
202
|
+
}
|
|
203
|
+
.tab:hover { background: rgba(0, 0, 0, .08); }
|
|
204
|
+
.tab.active { border-color: rgba(0, 0, 0, .38); background: rgba(0, 0, 0, .10); color: #111; font-weight: 700; }
|
|
205
|
+
.spin {
|
|
206
|
+
display: inline-block; width: 9px; height: 9px; margin-right: 5px; vertical-align: -1px;
|
|
207
|
+
border: 1.5px solid rgba(0, 0, 0, .2); border-top-color: #1a3a8f; border-radius: 50%;
|
|
208
|
+
animation: echospin .8s linear infinite;
|
|
209
|
+
}
|
|
210
|
+
@keyframes echospin { to { transform: rotate(360deg); } }
|
|
211
|
+
.headctl { display: flex; align-items: center; gap: 8px; }
|
|
212
|
+
.switchbtn {
|
|
213
|
+
width: 22px; height: 22px; border-radius: 50%; padding: 0; line-height: 1; font-size: 12px;
|
|
214
|
+
border: 1px solid rgba(0, 0, 0, .2); background: rgba(0, 0, 0, .05); color: var(--faint);
|
|
215
|
+
cursor: pointer; font-family: inherit;
|
|
216
|
+
}
|
|
217
|
+
.switchbtn:hover { background: rgba(0, 0, 0, .1); color: #111; }
|
|
218
|
+
.reveal {
|
|
219
|
+
margin-top: 8px; font-size: 10.5px; padding: 4px 11px; border-radius: 999px;
|
|
220
|
+
border: 1px solid rgba(0, 0, 0, .12); background: rgba(0, 0, 0, .04); color: var(--faint);
|
|
221
|
+
cursor: pointer; font-family: inherit;
|
|
222
|
+
}
|
|
223
|
+
.reveal:hover { background: rgba(0, 0, 0, .08); color: #111; }
|
|
224
|
+
.state-row {
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
gap: 9px;
|
|
228
|
+
margin-bottom: 7px;
|
|
229
|
+
}
|
|
230
|
+
.state-dot {
|
|
231
|
+
width: 11px;
|
|
232
|
+
height: 11px;
|
|
233
|
+
flex: none;
|
|
234
|
+
border-radius: 999px;
|
|
235
|
+
background: var(--green);
|
|
236
|
+
box-shadow: 0 0 0 1px rgba(26, 58, 143, 0.12);
|
|
237
|
+
}
|
|
238
|
+
.state-dot.amber { background: var(--amber); }
|
|
239
|
+
.state-dot.red { background: #ff5f57; }
|
|
240
|
+
.state-word {
|
|
241
|
+
min-width: 0;
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
text-overflow: ellipsis;
|
|
244
|
+
white-space: nowrap;
|
|
245
|
+
font-size: 16px;
|
|
246
|
+
font-weight: 800;
|
|
247
|
+
}
|
|
248
|
+
.full {
|
|
249
|
+
min-height: 17px;
|
|
250
|
+
margin-bottom: 14px;
|
|
251
|
+
font-size: 12px;
|
|
252
|
+
font-weight: 650;
|
|
253
|
+
color: #4a7a3e;
|
|
254
|
+
}
|
|
255
|
+
.lead {
|
|
256
|
+
margin-bottom: 5px;
|
|
257
|
+
font-size: 13.5px;
|
|
258
|
+
font-weight: 750;
|
|
259
|
+
color: #2f7d4f;
|
|
260
|
+
}
|
|
261
|
+
.evidence {
|
|
262
|
+
min-height: 38px;
|
|
263
|
+
margin-bottom: 12px;
|
|
264
|
+
color: var(--muted);
|
|
265
|
+
font-size: 12.5px;
|
|
266
|
+
line-height: 1.5;
|
|
267
|
+
}
|
|
268
|
+
.oldbar {
|
|
269
|
+
display: flex;
|
|
270
|
+
height: 9px;
|
|
271
|
+
margin: 0;
|
|
272
|
+
overflow: hidden;
|
|
273
|
+
border-radius: 999px;
|
|
274
|
+
background: var(--track);
|
|
275
|
+
}
|
|
276
|
+
.oldbar span {
|
|
277
|
+
display: block;
|
|
278
|
+
height: 100%;
|
|
279
|
+
min-width: 0;
|
|
280
|
+
transition: width 0.35s ease;
|
|
281
|
+
}
|
|
282
|
+
.oldbar .useful { width: 100%; background: var(--blue); }
|
|
283
|
+
.oldbar .old { width: 0; background: var(--brick); }
|
|
284
|
+
.alert {
|
|
285
|
+
display: none;
|
|
286
|
+
margin-top: 13px;
|
|
287
|
+
padding: 9px 0 9px 12px;
|
|
288
|
+
border-left: 3px solid rgba(199, 55, 47, 0.42);
|
|
289
|
+
}
|
|
290
|
+
.alert-title {
|
|
291
|
+
margin-bottom: 4px;
|
|
292
|
+
color: var(--brick);
|
|
293
|
+
font-size: 12.5px;
|
|
294
|
+
font-weight: 750;
|
|
295
|
+
}
|
|
296
|
+
.alert-detail {
|
|
297
|
+
color: var(--muted);
|
|
298
|
+
font-size: 12px;
|
|
299
|
+
line-height: 1.45;
|
|
300
|
+
}
|
|
301
|
+
.nudge {
|
|
302
|
+
display: none;
|
|
303
|
+
margin-top: 13px;
|
|
304
|
+
padding: 10px 12px;
|
|
305
|
+
border-radius: 12px;
|
|
306
|
+
font-size: 12.5px;
|
|
307
|
+
line-height: 1.45;
|
|
308
|
+
}
|
|
309
|
+
.nudge.calm {
|
|
310
|
+
border: 1px solid rgba(26, 58, 143, 0.14);
|
|
311
|
+
background: rgba(26, 58, 143, 0.05);
|
|
312
|
+
color: var(--muted);
|
|
313
|
+
}
|
|
314
|
+
.nudge.red {
|
|
315
|
+
border: 1px solid rgba(199, 55, 47, 0.22);
|
|
316
|
+
background: rgba(199, 55, 47, 0.07);
|
|
317
|
+
color: #7a241e;
|
|
318
|
+
}
|
|
319
|
+
button.renew-btn {
|
|
320
|
+
display: none;
|
|
321
|
+
width: 100%;
|
|
322
|
+
margin-top: 12px;
|
|
323
|
+
border: 1px solid rgba(199, 55, 47, 0.3);
|
|
324
|
+
border-radius: 999px;
|
|
325
|
+
padding: 9px 10px;
|
|
326
|
+
background: rgba(199, 55, 47, 0.08);
|
|
327
|
+
color: var(--brick);
|
|
328
|
+
font-weight: 750;
|
|
329
|
+
font-size: 13px;
|
|
330
|
+
cursor: pointer;
|
|
331
|
+
transition: background 0.2s ease, opacity 0.2s ease;
|
|
332
|
+
}
|
|
333
|
+
button.renew-btn:hover { background: rgba(199, 55, 47, 0.14); }
|
|
334
|
+
button.renew-btn:active { background: rgba(199, 55, 47, 0.22); }
|
|
335
|
+
button.renew-btn.loading { opacity: 0.6; cursor: wait; }
|
|
336
|
+
.renew-result {
|
|
337
|
+
display: none;
|
|
338
|
+
margin-top: 10px;
|
|
339
|
+
border: 1px solid rgba(26, 58, 143, 0.18);
|
|
340
|
+
border-radius: 12px;
|
|
341
|
+
background: rgba(26, 58, 143, 0.04);
|
|
342
|
+
padding: 10px 12px;
|
|
343
|
+
}
|
|
344
|
+
.renew-result .renew-msg {
|
|
345
|
+
margin-bottom: 6px;
|
|
346
|
+
color: var(--blue);
|
|
347
|
+
font-size: 11.5px;
|
|
348
|
+
font-weight: 750;
|
|
349
|
+
}
|
|
350
|
+
.renew-result pre {
|
|
351
|
+
max-height: 120px;
|
|
352
|
+
overflow: auto;
|
|
353
|
+
margin: 0;
|
|
354
|
+
white-space: pre-wrap;
|
|
355
|
+
word-break: break-word;
|
|
356
|
+
color: var(--ink);
|
|
357
|
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
358
|
+
font-size: 9.5px;
|
|
359
|
+
}
|
|
360
|
+
.renew-result .copy-btn {
|
|
361
|
+
display: inline-block;
|
|
362
|
+
width: auto;
|
|
363
|
+
margin-top: 6px;
|
|
364
|
+
border: 1px solid rgba(26, 58, 143, 0.22);
|
|
365
|
+
border-radius: 999px;
|
|
366
|
+
padding: 5px 14px;
|
|
367
|
+
background: rgba(26, 58, 143, 0.08);
|
|
368
|
+
color: var(--blue);
|
|
369
|
+
font-weight: 750;
|
|
370
|
+
font-size: 11px;
|
|
371
|
+
cursor: pointer;
|
|
372
|
+
}
|
|
373
|
+
.meta,
|
|
374
|
+
.path,
|
|
375
|
+
.missing {
|
|
376
|
+
overflow: hidden;
|
|
377
|
+
text-overflow: ellipsis;
|
|
378
|
+
white-space: nowrap;
|
|
379
|
+
color: var(--faint);
|
|
380
|
+
font-size: 11px;
|
|
381
|
+
}
|
|
382
|
+
.meta { margin-top: 13px; }
|
|
383
|
+
.path { margin-top: 8px; }
|
|
384
|
+
.missing { margin-top: 6px; }
|
|
385
|
+
#hud.s-green .mascot-stage { filter: drop-shadow(0 2px 6px rgba(67, 209, 122, 0.3)); }
|
|
386
|
+
#hud.s-amber .mascot-stage { filter: drop-shadow(0 2px 7px rgba(233, 185, 73, 0.42)); }
|
|
387
|
+
#hud.s-red .mascot-stage { filter: drop-shadow(0 3px 8px rgba(199, 55, 47, 0.48)); }
|
|
98
388
|
#hud.s-amber .mascot { animation-duration: 5.6s; }
|
|
99
389
|
#hud.s-red .mascot { animation-duration: 6.8s; }
|
|
100
390
|
</style>
|
|
@@ -104,405 +394,424 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
104
394
|
<div class="glance" id="toggle">
|
|
105
395
|
<span class="drag" title="Drag"></span>
|
|
106
396
|
<span class="mascot-stage"><img class="mascot" src="/assets/hud/echo-face-cutout.png" alt="" /></span>
|
|
107
|
-
<div class="
|
|
108
|
-
<div class="paper-bubble-skin" aria-hidden="true">
|
|
109
|
-
<svg class="paper-bubble-outline" preserveAspectRatio="none" focusable="false">
|
|
110
|
-
<path />
|
|
111
|
-
</svg>
|
|
112
|
-
</div>
|
|
397
|
+
<div class="status-bubble">
|
|
113
398
|
<span class="dot" id="dot"></span>
|
|
114
399
|
<span class="copy">
|
|
115
|
-
<span class="main" id="main">Checking context
|
|
400
|
+
<span class="main" id="main">Checking context...</span>
|
|
116
401
|
<span class="client" id="client">EchoMem HUD</span>
|
|
117
402
|
</span>
|
|
118
403
|
</div>
|
|
119
404
|
</div>
|
|
120
405
|
<div class="details">
|
|
121
|
-
<div class="
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<p class="delta" id="delta" style="display:none"></p>
|
|
127
|
-
<div class="trend">
|
|
128
|
-
<div class="trend-label"><span>Noise trend</span><strong id="trendnow"></strong></div>
|
|
129
|
-
<svg class="spark" id="spark" viewBox="0 0 100 28" preserveAspectRatio="none" aria-hidden="true" style="display:none">
|
|
130
|
-
<polygon id="sparkarea" fill="rgba(26, 58, 143, .08)" points=""></polygon>
|
|
131
|
-
<polyline id="sparkline" fill="none" stroke="var(--echo-ink-primary)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" points=""></polyline>
|
|
132
|
-
</svg>
|
|
133
|
-
<div class="trend-empty" id="trendempty">Collecting a few turns…</div>
|
|
134
|
-
</div>
|
|
135
|
-
<div class="tiles">
|
|
136
|
-
<div class="tile"><b id="t-reclaim">—</b><span>reclaimable ≥</span></div>
|
|
137
|
-
<div class="tile"><b id="t-repeat">—</b><span>repeated reads</span></div>
|
|
138
|
-
<div class="tile"><b id="t-reads">—</b><span>files read</span></div>
|
|
139
|
-
<div class="tile"><b id="t-edits">—</b><span>edits</span></div>
|
|
406
|
+
<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">⇄</button></span></div>
|
|
407
|
+
<div class="tabs" id="tabs"></div>
|
|
408
|
+
<div class="state-row">
|
|
409
|
+
<span class="state-dot" id="stateDot"></span>
|
|
410
|
+
<span class="state-word" id="stateWord">Fresh</span>
|
|
140
411
|
</div>
|
|
141
|
-
<div class="
|
|
142
|
-
<div class="
|
|
143
|
-
|
|
144
|
-
|
|
412
|
+
<div class="full" id="fullness"></div>
|
|
413
|
+
<div class="lead" id="lead">Clean and roomy</div>
|
|
414
|
+
<div class="evidence" id="evidence">Collecting enough local context to judge the window.</div>
|
|
415
|
+
<div class="oldbar" aria-hidden="true">
|
|
416
|
+
<span class="useful" id="usefulbar"></span>
|
|
417
|
+
<span class="old" id="oldbar"></span>
|
|
145
418
|
</div>
|
|
146
|
-
<div class="
|
|
147
|
-
<div class="
|
|
148
|
-
<div class="
|
|
419
|
+
<div class="alert" id="alert">
|
|
420
|
+
<div class="alert-title" id="alertTitle"></div>
|
|
421
|
+
<div class="alert-detail" id="alertDetail"></div>
|
|
149
422
|
</div>
|
|
150
|
-
<
|
|
151
|
-
<p class="nudge" id="nudge" style="display:none"></p>
|
|
423
|
+
<div class="nudge" id="nudge"></div>
|
|
152
424
|
<button class="renew-btn" id="renewbtn">Renew session</button>
|
|
153
425
|
<div class="renew-result" id="renewresult">
|
|
154
426
|
<div class="renew-msg" id="renewmsg"></div>
|
|
155
427
|
<pre id="renewpre"></pre>
|
|
156
428
|
<button class="copy-btn" id="copybtn">Copy to clipboard</button>
|
|
157
429
|
</div>
|
|
430
|
+
<div class="meta" id="meta"></div>
|
|
158
431
|
<div class="path" id="path"></div>
|
|
432
|
+
<button class="reveal" id="reveal" style="display:none">↗ Reveal session log in Finder</button>
|
|
159
433
|
<div class="missing" id="missing"></div>
|
|
160
434
|
</div>
|
|
161
435
|
</section>
|
|
162
436
|
<script>
|
|
163
|
-
const hud = document.getElementById(
|
|
164
|
-
const dot = document.getElementById(
|
|
165
|
-
const main = document.getElementById(
|
|
166
|
-
const client = document.getElementById(
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const renewPre = document.getElementById('renewpre');
|
|
193
|
-
const copyBtn = document.getElementById('copybtn');
|
|
194
|
-
const history = [];
|
|
195
|
-
let lastSource = '';
|
|
437
|
+
const hud = document.getElementById("hud");
|
|
438
|
+
const dot = document.getElementById("dot");
|
|
439
|
+
const main = document.getElementById("main");
|
|
440
|
+
const client = document.getElementById("client");
|
|
441
|
+
const turnEl = document.getElementById("turn");
|
|
442
|
+
const stateDot = document.getElementById("stateDot");
|
|
443
|
+
const stateWordEl = document.getElementById("stateWord");
|
|
444
|
+
const fullness = document.getElementById("fullness");
|
|
445
|
+
const lead = document.getElementById("lead");
|
|
446
|
+
const evidence = document.getElementById("evidence");
|
|
447
|
+
const usefulbar = document.getElementById("usefulbar");
|
|
448
|
+
const oldbar = document.getElementById("oldbar");
|
|
449
|
+
const alertEl = document.getElementById("alert");
|
|
450
|
+
const alertTitle = document.getElementById("alertTitle");
|
|
451
|
+
const alertDetail = document.getElementById("alertDetail");
|
|
452
|
+
const nudge = document.getElementById("nudge");
|
|
453
|
+
const renewBtn = document.getElementById("renewbtn");
|
|
454
|
+
const renewResult = document.getElementById("renewresult");
|
|
455
|
+
const renewMsg = document.getElementById("renewmsg");
|
|
456
|
+
const renewPre = document.getElementById("renewpre");
|
|
457
|
+
const copyBtn = document.getElementById("copybtn");
|
|
458
|
+
const metaEl = document.getElementById("meta");
|
|
459
|
+
const pathEl = document.getElementById("path");
|
|
460
|
+
const updatedEl = document.getElementById("updated");
|
|
461
|
+
const tabsEl = document.getElementById("tabs");
|
|
462
|
+
const switchBtn = document.getElementById("switchbtn");
|
|
463
|
+
const revealEl = document.getElementById("reveal");
|
|
464
|
+
const missing = document.getElementById("missing");
|
|
465
|
+
const hudVersion = "v${MCP_PACKAGE_VERSION}";
|
|
196
466
|
let prevTurnState = null;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
hud.classList.toggle('open');
|
|
207
|
-
const open = hud.classList.contains('open');
|
|
208
|
-
requestAnimationFrame(() => {
|
|
209
|
-
updateBubbleSkins();
|
|
210
|
-
const height = open ? Math.ceil(hud.getBoundingClientRect().height) + 14 : undefined;
|
|
211
|
-
window.echomemHud && window.echomemHud.setOpen(open, height);
|
|
212
|
-
});
|
|
467
|
+
let lastCapsule = "";
|
|
468
|
+
let activityEpoch = null;
|
|
469
|
+
let lastState = null;
|
|
470
|
+
let currentSrc = null;
|
|
471
|
+
|
|
472
|
+
// Reveal the active session's log file in Finder (Electron only; no-op in browser mode).
|
|
473
|
+
if (revealEl) revealEl.addEventListener("click", () => {
|
|
474
|
+
if (!currentSrc) return;
|
|
475
|
+
fetch("/reveal?path=" + encodeURIComponent(currentSrc)).catch(() => {});
|
|
213
476
|
});
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
477
|
+
|
|
478
|
+
// Circular top-right toggle: jump to the next agent's newest session (Claude ⇄ Codex).
|
|
479
|
+
if (switchBtn) switchBtn.addEventListener("click", (e) => {
|
|
480
|
+
e.stopPropagation();
|
|
481
|
+
const st = lastState;
|
|
482
|
+
if (!st || !st.recentSessions) return;
|
|
483
|
+
const clients = [];
|
|
484
|
+
for (const s of st.recentSessions) if (clients.indexOf(s.client) < 0) clients.push(s.client);
|
|
485
|
+
if (clients.length < 2) return;
|
|
486
|
+
const cur = st.active ? st.active.client : clients[0];
|
|
487
|
+
const next = clients[(clients.indexOf(cur) + 1) % clients.length];
|
|
488
|
+
const target = st.recentSessions.filter((s) => s.client === next)[0]; // newest of that agent
|
|
489
|
+
if (!target) return;
|
|
490
|
+
fetch("/select?id=" + encodeURIComponent(target.id))
|
|
491
|
+
.then(() => fetch("/state"))
|
|
492
|
+
.then((r) => r.json())
|
|
493
|
+
.then(render)
|
|
494
|
+
.catch(() => {});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Click a tab → pin that session as the primary view (or "Auto" to return to auto-selection).
|
|
498
|
+
if (tabsEl) tabsEl.addEventListener("click", (e) => {
|
|
499
|
+
const btn = e.target && e.target.closest ? e.target.closest(".tab") : null;
|
|
500
|
+
if (!btn) return;
|
|
501
|
+
const id = btn.getAttribute("data-id");
|
|
502
|
+
fetch("/select?id=" + encodeURIComponent(id))
|
|
503
|
+
.then(() => fetch("/state"))
|
|
504
|
+
.then((r) => r.json())
|
|
505
|
+
.then(render)
|
|
506
|
+
.catch(() => {});
|
|
226
507
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
508
|
+
|
|
509
|
+
document.getElementById("toggle").addEventListener("click", (event) => {
|
|
510
|
+
if (event.target instanceof HTMLElement && (event.target.closest(".drag") || event.target.closest(".switchbtn"))) return;
|
|
511
|
+
hud.classList.toggle("open");
|
|
512
|
+
syncHeight();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
if (renewBtn) renewBtn.addEventListener("click", async () => {
|
|
516
|
+
renewBtn.classList.add("loading");
|
|
517
|
+
renewBtn.textContent = "Capturing...";
|
|
231
518
|
try {
|
|
232
|
-
const res = await fetch(
|
|
519
|
+
const res = await fetch("/capsule");
|
|
233
520
|
const data = await res.json();
|
|
234
|
-
if (!data.ok) {
|
|
235
|
-
|
|
521
|
+
if (!data.ok) {
|
|
522
|
+
renewBtn.textContent = "Renew session";
|
|
523
|
+
renewBtn.classList.remove("loading");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const prompt = "Continue my previous session. Verify anything that references file state before trusting it.\n\n" + data.capsule;
|
|
236
527
|
lastCapsule = prompt;
|
|
237
528
|
if (renewPre) renewPre.textContent = prompt;
|
|
238
|
-
if (renewMsg) renewMsg.textContent =
|
|
239
|
-
if (renewResult)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
renewBtn.textContent = 'Renew session';
|
|
249
|
-
renewBtn.classList.remove('loading');
|
|
529
|
+
if (renewMsg) renewMsg.textContent = "Open a new session and paste this:";
|
|
530
|
+
if (renewResult) renewResult.style.display = "block";
|
|
531
|
+
try {
|
|
532
|
+
await navigator.clipboard.writeText(prompt);
|
|
533
|
+
if (renewMsg) renewMsg.textContent = "Copied. Open a new session and paste:";
|
|
534
|
+
} catch {}
|
|
535
|
+
} catch {}
|
|
536
|
+
renewBtn.textContent = "Renew session";
|
|
537
|
+
renewBtn.classList.remove("loading");
|
|
538
|
+
syncHeight();
|
|
250
539
|
});
|
|
251
|
-
|
|
540
|
+
|
|
541
|
+
if (copyBtn) copyBtn.addEventListener("click", async () => {
|
|
252
542
|
if (!lastCapsule) return;
|
|
253
|
-
try {
|
|
543
|
+
try {
|
|
544
|
+
await navigator.clipboard.writeText(lastCapsule);
|
|
545
|
+
copyBtn.textContent = "Copied";
|
|
546
|
+
setTimeout(() => { copyBtn.textContent = "Copy to clipboard"; }, 1500);
|
|
547
|
+
} catch {}
|
|
254
548
|
});
|
|
255
|
-
|
|
549
|
+
|
|
550
|
+
const events = new EventSource("/events");
|
|
256
551
|
events.onmessage = (event) => render(JSON.parse(event.data));
|
|
257
|
-
fetch(
|
|
552
|
+
fetch("/state").then((r) => r.json()).then(render).catch(() => {});
|
|
553
|
+
|
|
258
554
|
function render(state) {
|
|
259
555
|
const score = state.active || state;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
client.textContent = 'EchoMem HUD · ' + hudVersion;
|
|
267
|
-
if (summary) summary.textContent = 'Start working in Codex or Claude and I’ll check the context here.';
|
|
268
|
-
if (fullness) fullness.textContent = '';
|
|
269
|
-
if (meterfill) meterfill.style.width = '0';
|
|
270
|
-
history.length = 0;
|
|
271
|
-
if (spark) spark.style.display = 'none';
|
|
272
|
-
if (trendempty) trendempty.style.display = 'block';
|
|
273
|
-
if (trendnow) trendnow.textContent = '';
|
|
274
|
-
if (nudge) nudge.style.display = 'none';
|
|
275
|
-
if (toolbars) toolbars.innerHTML = '';
|
|
276
|
-
if (bigsec) bigsec.style.display = 'none';
|
|
277
|
-
if (toolstotal) toolstotal.textContent = '';
|
|
278
|
-
if (metaEl) metaEl.textContent = '';
|
|
279
|
-
if (tReclaim) tReclaim.textContent = '—';
|
|
280
|
-
if (tRepeat) tRepeat.textContent = '—';
|
|
281
|
-
if (tReads) tReads.textContent = '—';
|
|
282
|
-
if (tEdits) tEdits.textContent = '—';
|
|
283
|
-
missing.textContent = formatMissing(state.missing);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
dot.className = 'dot ' + (score.color || 'green');
|
|
287
|
-
hud.classList.remove('s-green', 's-amber', 's-red');
|
|
288
|
-
hud.classList.add('s-' + (score.color || 'green'));
|
|
289
|
-
main.textContent = stateWord(score.color) + ' · ' + fmt(score.ctTokens) + ' ctx';
|
|
290
|
-
client.textContent = label(score.client) + ' · ' + hudVersion;
|
|
291
|
-
if (summary) summary.textContent = summaryLine(score.color);
|
|
292
|
-
const sat = typeof score.saturationPct === 'number' ? score.saturationPct
|
|
293
|
-
: (score.modelContextWindow ? Math.round(100 * (score.ctTokens || 0) / score.modelContextWindow) : 0);
|
|
294
|
-
if (fullness) fullness.textContent = sat + '% full · ' + fmt(score.ctTokens) + (score.modelContextWindow ? ' / ' + fmt(score.modelContextWindow) : '');
|
|
295
|
-
if (meterfill) {
|
|
296
|
-
meterfill.style.width = Math.max(2, Math.min(100, sat)) + '%';
|
|
297
|
-
meterfill.style.background = sat >= 90 ? '#c7372f' : sat >= 75 ? '#e9b949' : 'var(--echo-ink-primary)';
|
|
556
|
+
lastState = state;
|
|
557
|
+
renderTabs(state, score && score.sourcePath ? activeIdFor(state, score.sourcePath) : null);
|
|
558
|
+
if (switchBtn) {
|
|
559
|
+
const clients = [];
|
|
560
|
+
for (const s of (state.recentSessions || [])) if (clients.indexOf(s.client) < 0) clients.push(s.client);
|
|
561
|
+
switchBtn.style.display = clients.length >= 2 ? "inline-block" : "none";
|
|
298
562
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (score.color === 'red') msg = 'Old context is piling up — renew to start a clean session with everything you need.';
|
|
303
|
-
else if (sat >= 85) msg = 'The window is getting full — renewing now would keep you fast.';
|
|
304
|
-
nudge.textContent = msg;
|
|
305
|
-
nudge.style.display = msg ? 'block' : 'none';
|
|
306
|
-
}
|
|
307
|
-
if (renewBtn) {
|
|
308
|
-
renewBtn.style.display = (score.color === 'red' || score.color === 'amber') ? 'block' : 'none';
|
|
563
|
+
if (!score || typeof score.usefulPct !== "number") {
|
|
564
|
+
renderIdle(state);
|
|
565
|
+
return;
|
|
309
566
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
567
|
+
|
|
568
|
+
const color = score.color || "green";
|
|
569
|
+
const stateName = stateWord(color);
|
|
570
|
+
const sat = fullnessPct(score);
|
|
571
|
+
const oldPct = clamp(score.pollutionPct || 0, 0, 95);
|
|
572
|
+
const repeatCount = score.buckets && score.buckets.range_redundant ? score.buckets.range_redundant.count || 0 : 0;
|
|
573
|
+
const edits = score.stats && score.stats.patchEdits ? Number(score.stats.patchEdits) : 0;
|
|
574
|
+
|
|
575
|
+
// A session idle for >10 min shouldn't wear the cheerful "Fresh/clean" look — its quality
|
|
576
|
+
// numbers are stale. Show it muted as Idle so a 47h-old pinned session never reads as current.
|
|
577
|
+
const stale = typeof score.lastActiveMs === "number" && score.lastActiveMs > 600000;
|
|
578
|
+
const shownColor = stale ? "idle" : color;
|
|
579
|
+
dot.className = "dot " + shownColor;
|
|
580
|
+
stateDot.className = stale ? "state-dot" : "state-dot " + color;
|
|
581
|
+
hud.classList.remove("s-green", "s-amber", "s-red");
|
|
582
|
+
if (!stale) hud.classList.add("s-" + color);
|
|
583
|
+
main.textContent = (stale ? "Idle" : stateName) + " - " + fmt(score.ctTokens) + " ctx";
|
|
584
|
+
client.textContent = sessionIdentity(score, state);
|
|
585
|
+
activityEpoch = typeof score.lastActiveMs === "number" ? Date.now() - score.lastActiveMs : null;
|
|
586
|
+
renderUpdated();
|
|
587
|
+
turnEl.textContent = typeof score.turn === "number" && score.turn > 0 ? "turn " + score.turn : "";
|
|
588
|
+
stateWordEl.textContent = stale ? "Idle" : stateName;
|
|
589
|
+
fullness.textContent = fullnessLine(sat, score);
|
|
590
|
+
fullness.style.color = stale ? "#6b675d" : tone(color);
|
|
591
|
+
lead.textContent = stale ? "This session is idle - showing its last state" : leadLine(color);
|
|
592
|
+
lead.style.color = stale ? "#6b675d" : tone(color);
|
|
593
|
+
evidence.textContent = evidenceLine(score, oldPct, repeatCount);
|
|
594
|
+
usefulbar.style.width = Math.max(0, 100 - oldPct) + "%";
|
|
595
|
+
oldbar.style.width = oldPct + "%";
|
|
596
|
+
metaEl.textContent = ctSourceLabel(score.ctSource) + " - lower-bound signal - " + edits + " edits";
|
|
597
|
+
pathEl.textContent = "";
|
|
598
|
+
currentSrc = score.sourcePath || null;
|
|
599
|
+
if (revealEl) revealEl.style.display = currentSrc ? "inline-block" : "none";
|
|
600
|
+
missing.textContent = formatMissing(state.missing);
|
|
601
|
+
|
|
602
|
+
renderAlert(score, oldPct, repeatCount);
|
|
603
|
+
renderNudge(color, sat);
|
|
604
|
+
|
|
605
|
+
if (renewBtn) renewBtn.style.display = color === "red" || color === "amber" ? "block" : "none";
|
|
606
|
+
prevTurnState = {
|
|
607
|
+
src: score.sourcePath,
|
|
608
|
+
turn: score.turn,
|
|
609
|
+
ct: score.ctTokens || 0,
|
|
610
|
+
reads: score.reads || 0,
|
|
611
|
+
repeat: repeatCount,
|
|
612
|
+
oldTok: score.pollutionTok || 0,
|
|
613
|
+
};
|
|
614
|
+
syncHeight();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function renderIdle(state) {
|
|
618
|
+
dot.className = "dot idle";
|
|
619
|
+
stateDot.className = "state-dot";
|
|
620
|
+
hud.classList.remove("s-green", "s-amber", "s-red");
|
|
621
|
+
prevTurnState = null;
|
|
622
|
+
main.textContent = "No active session yet";
|
|
623
|
+
client.textContent = "EchoMem HUD - " + hudVersion;
|
|
624
|
+
activityEpoch = null;
|
|
625
|
+
if (updatedEl) updatedEl.textContent = "";
|
|
626
|
+
currentSrc = null;
|
|
627
|
+
if (revealEl) revealEl.style.display = "none";
|
|
628
|
+
turnEl.textContent = "";
|
|
629
|
+
stateWordEl.textContent = "Fresh";
|
|
630
|
+
fullness.textContent = "";
|
|
631
|
+
lead.textContent = "Waiting for a live Codex or Claude session";
|
|
632
|
+
lead.style.color = "#6b675d";
|
|
633
|
+
evidence.textContent = "Start working and EchoMem will show when the context is still clean, drifting, or ready to renew.";
|
|
634
|
+
usefulbar.style.width = "100%";
|
|
635
|
+
oldbar.style.width = "0";
|
|
636
|
+
alertEl.style.display = "none";
|
|
637
|
+
nudge.style.display = "none";
|
|
638
|
+
if (renewBtn) renewBtn.style.display = "none";
|
|
639
|
+
if (renewResult) renewResult.style.display = "none";
|
|
640
|
+
metaEl.textContent = "";
|
|
641
|
+
pathEl.textContent = "";
|
|
642
|
+
missing.textContent = formatMissing(state && state.missing);
|
|
643
|
+
syncHeight();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function renderAlert(score, oldPct, repeatCount) {
|
|
647
|
+
if (!alertEl) return;
|
|
648
|
+
const turnChanged = prevTurnState && prevTurnState.src === score.sourcePath && score.turn !== prevTurnState.turn;
|
|
649
|
+
const dCt = turnChanged ? (score.ctTokens || 0) - prevTurnState.ct : 0;
|
|
650
|
+
const dReads = turnChanged ? (score.reads || 0) - prevTurnState.reads : 0;
|
|
651
|
+
const dRep = turnChanged ? repeatCount - prevTurnState.repeat : 0;
|
|
652
|
+
const dOld = turnChanged ? (score.pollutionTok || 0) - prevTurnState.oldTok : 0;
|
|
653
|
+
if (dRep > 0 || dOld > 2000) {
|
|
654
|
+
alertTitle.textContent = "this turn re-read what it already had";
|
|
655
|
+
alertDetail.textContent = joinParts([
|
|
656
|
+
dOld > 0 ? "+" + fmt(dOld) + " old weight" : "",
|
|
657
|
+
dReads > 0 ? "+" + dReads + " read" + (dReads === 1 ? "" : "s") : "0 new files",
|
|
658
|
+
dCt !== 0 ? signedFmt(dCt) + " ctx" : "",
|
|
659
|
+
]);
|
|
660
|
+
alertEl.style.display = "block";
|
|
661
|
+
return;
|
|
314
662
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (deltaEl) {
|
|
321
|
-
const repeatNow = bucket ? bucket.count : 0;
|
|
322
|
-
if (prevTurnState && prevTurnState.src === score.sourcePath && score.turn !== prevTurnState.turn) {
|
|
323
|
-
const dCt = (score.ctTokens || 0) - prevTurnState.ct;
|
|
324
|
-
const dReads = (score.reads || 0) - prevTurnState.reads;
|
|
325
|
-
const dRep = repeatNow - prevTurnState.repeat;
|
|
326
|
-
const parts = [(dCt >= 0 ? '+' : '−') + fmt(Math.abs(dCt)) + ' ctx' + (dCt < -20000 ? ' (compacted)' : '')];
|
|
327
|
-
if (dReads > 0) parts.push('+' + dReads + ' read' + (dReads > 1 ? 's' : ''));
|
|
328
|
-
if (dRep > 0) parts.push('+' + dRep + ' repeat' + (dRep > 1 ? 's' : ''));
|
|
329
|
-
deltaEl.textContent = 'last turn ' + parts.join(' · ');
|
|
330
|
-
deltaEl.style.display = 'block';
|
|
331
|
-
} else if (!prevTurnState || prevTurnState.src !== score.sourcePath) {
|
|
332
|
-
deltaEl.style.display = 'none';
|
|
333
|
-
}
|
|
334
|
-
if (!prevTurnState || prevTurnState.src !== score.sourcePath || score.turn !== prevTurnState.turn) {
|
|
335
|
-
prevTurnState = { src: score.sourcePath, turn: score.turn, ct: score.ctTokens || 0, reads: score.reads || 0, repeat: repeatNow };
|
|
336
|
-
}
|
|
663
|
+
if (score.color === "red") {
|
|
664
|
+
alertTitle.textContent = "context has drifted";
|
|
665
|
+
alertDetail.textContent = "at least " + oldPct + "% old weight is still riding in the window";
|
|
666
|
+
alertEl.style.display = "block";
|
|
667
|
+
return;
|
|
337
668
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
669
|
+
alertEl.style.display = "none";
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function renderNudge(color, sat) {
|
|
673
|
+
let text = "";
|
|
674
|
+
let cls = "nudge";
|
|
675
|
+
if (color === "red") {
|
|
676
|
+
text = "A fresh start now beats pushing past this - keep the goal, drop the clutter.";
|
|
677
|
+
cls += " red";
|
|
678
|
+
} else if (color === "amber" || sat >= 75) {
|
|
679
|
+
text = "Still fine - a fresh start soon will keep this snappy.";
|
|
680
|
+
cls += " calm";
|
|
346
681
|
}
|
|
347
|
-
|
|
348
|
-
|
|
682
|
+
nudge.textContent = text;
|
|
683
|
+
nudge.className = cls;
|
|
684
|
+
nudge.style.display = text ? "block" : "none";
|
|
349
685
|
}
|
|
686
|
+
|
|
350
687
|
function stateWord(color) {
|
|
351
|
-
if (color ===
|
|
352
|
-
if (color ===
|
|
353
|
-
return
|
|
688
|
+
if (color === "red") return "Drifted";
|
|
689
|
+
if (color === "amber") return "Drifting";
|
|
690
|
+
return "Fresh";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function leadLine(color) {
|
|
694
|
+
if (color === "red") return "Working from a cluttered desk";
|
|
695
|
+
if (color === "amber") return "Old context is building up";
|
|
696
|
+
return "Clean and roomy";
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function evidenceLine(score, oldPct, repeatCount) {
|
|
700
|
+
const repeat = repeatCount > 0 ? repeatCount + " repeated read" + (repeatCount === 1 ? "" : "s") : "nothing has been re-read yet";
|
|
701
|
+
return "at least " + oldPct + "% old weight (" + fmt(score.pollutionTok) + ") - " + repeat;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function fullnessLine(sat, score) {
|
|
705
|
+
if (!score.modelContextWindow) return fmt(score.ctTokens) + " context tokens";
|
|
706
|
+
const room = sat >= 85 ? " - compaction near" : sat <= 30 ? " - plenty of room" : "";
|
|
707
|
+
return sat + "% full - " + fmt(score.ctTokens) + " / " + fmt(score.modelContextWindow) + room;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function fullnessPct(score) {
|
|
711
|
+
if (typeof score.saturationPct === "number") return score.saturationPct;
|
|
712
|
+
if (score.modelContextWindow) return Math.round(100 * (score.ctTokens || 0) / score.modelContextWindow);
|
|
713
|
+
return 0;
|
|
354
714
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (color ===
|
|
358
|
-
|
|
715
|
+
|
|
716
|
+
function tone(color) {
|
|
717
|
+
if (color === "red") return "#c7372f";
|
|
718
|
+
if (color === "amber") return "#8a6a12";
|
|
719
|
+
return "#2f7d4f";
|
|
359
720
|
}
|
|
721
|
+
|
|
360
722
|
function ctSourceLabel(src) {
|
|
361
|
-
if (src ===
|
|
362
|
-
return
|
|
723
|
+
if (src === "token_count" || src === "statusline") return "measured";
|
|
724
|
+
return "estimated";
|
|
363
725
|
}
|
|
726
|
+
|
|
364
727
|
function formatMissing(list) {
|
|
365
|
-
if (!list || !list.length) return
|
|
366
|
-
return
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
function
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
const hasFiles = files && files.length;
|
|
389
|
-
const nameTxt = (hasFiles ? '▸ ' : '') + e[0];
|
|
390
|
-
const filesHTML = hasFiles ? '<div class="files">' + files.map(function (f) {
|
|
391
|
-
return '<div class="file" title="' + escapeHTML(f) + '">' + escapeHTML(shortFile(f)) + '</div>';
|
|
392
|
-
}).join('') + '</div>' : '';
|
|
393
|
-
return '<div class="barwrap' + (hasFiles ? ' has-files' : '') + '" data-tool="' + escapeHTML(e[0]) + '">'
|
|
394
|
-
+ '<div class="bar' + (big ? ' big' : '') + '">'
|
|
395
|
-
+ '<span class="bar-name" title="' + escapeHTML(e[0]) + '">' + escapeHTML(nameTxt) + '</span>'
|
|
396
|
-
+ '<span class="bar-track"><span class="bar-fill" style="width:' + w + '%"></span></span>'
|
|
397
|
-
+ '<span class="bar-val">' + fmtVal(e[1]) + '</span></div>'
|
|
398
|
-
+ filesHTML + '</div>';
|
|
399
|
-
}).join('');
|
|
400
|
-
}
|
|
401
|
-
function pushTrend(score) {
|
|
402
|
-
const src = score.sourcePath || score.client || '';
|
|
403
|
-
if (src !== lastSource) { history.length = 0; lastSource = src; }
|
|
404
|
-
const t = typeof score.turn === 'number' ? score.turn : history.length;
|
|
405
|
-
const p = Math.max(0, Math.min(100, score.pollutionPct || 0));
|
|
406
|
-
if (history.length && history[history.length - 1].t === t) history[history.length - 1].p = p;
|
|
407
|
-
else { history.push({ t: t, p: p }); if (history.length > 40) history.shift(); }
|
|
408
|
-
drawSpark();
|
|
409
|
-
}
|
|
410
|
-
function drawSpark() {
|
|
411
|
-
if (!spark) return;
|
|
412
|
-
const n = history.length;
|
|
413
|
-
if (n < 2) {
|
|
414
|
-
spark.style.display = 'none';
|
|
415
|
-
if (trendempty) trendempty.style.display = 'block';
|
|
416
|
-
if (trendnow) trendnow.textContent = n ? history[0].p + '%' : '';
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
if (trendempty) trendempty.style.display = 'none';
|
|
420
|
-
spark.style.display = 'block';
|
|
421
|
-
const pts = history.map(function (h, i) {
|
|
422
|
-
const x = (i / (n - 1)) * 100;
|
|
423
|
-
const y = 27 - (h.p / 100) * 25;
|
|
424
|
-
return x.toFixed(1) + ',' + y.toFixed(1);
|
|
728
|
+
if (!list || !list.length) return "";
|
|
729
|
+
return "Not seeing " + list.map(label).join(", ") + " yet";
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Subtitle: which session this is (client + project) + version. The "other clients working"
|
|
733
|
+
// signal now lives in the live-spinner tabs, so it's dropped here to keep the line short.
|
|
734
|
+
function sessionIdentity(score) {
|
|
735
|
+
const parts = [label(score.client)];
|
|
736
|
+
if (score.label) parts.push(score.label);
|
|
737
|
+
parts.push(hudVersion);
|
|
738
|
+
return parts.join(" · ");
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Recent sessions as switchable tabs. "Auto" clears the pin. Hidden when there's only one session.
|
|
742
|
+
function renderTabs(state, activeId) {
|
|
743
|
+
if (!tabsEl) return;
|
|
744
|
+
const list = (state && state.recentSessions) || [];
|
|
745
|
+
if (list.length <= 1) { tabsEl.innerHTML = ""; return; }
|
|
746
|
+
const chips = list.map((s) => {
|
|
747
|
+
const cls = s.id === activeId ? "tab active" : "tab";
|
|
748
|
+
const dot = s.live ? '<span class="spin"></span>' : "";
|
|
749
|
+
const title = s.title || label(s.client);
|
|
750
|
+
return '<button class="' + cls + '" data-id="' + esc(s.id) + '" title="' + esc(label(s.client) + " · " + title) + '">' + dot + esc(shortTitle(title)) + "</button>";
|
|
425
751
|
});
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return
|
|
752
|
+
chips.unshift('<button class="tab' + (activeId ? "" : " active") + '" data-id="auto">Auto</button>');
|
|
753
|
+
tabsEl.innerHTML = chips.join("");
|
|
754
|
+
}
|
|
755
|
+
function activeIdFor(state, src) {
|
|
756
|
+
const list = (state && state.recentSessions) || [];
|
|
757
|
+
const match = list.filter((s) => s.sourcePath === src)[0];
|
|
758
|
+
return match ? match.id : null;
|
|
759
|
+
}
|
|
760
|
+
function shortTitle(t) {
|
|
761
|
+
t = String(t || "");
|
|
762
|
+
return t.length > 22 ? t.slice(0, 21) + "…" : t;
|
|
437
763
|
}
|
|
764
|
+
function esc(s) {
|
|
765
|
+
return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function renderUpdated() {
|
|
769
|
+
if (!updatedEl) return;
|
|
770
|
+
updatedEl.textContent = activityEpoch === null ? "" : "updated " + relTime(Date.now() - activityEpoch);
|
|
771
|
+
}
|
|
772
|
+
function relTime(ms) {
|
|
773
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
774
|
+
if (s < 5) return "now";
|
|
775
|
+
if (s < 60) return s + "s ago";
|
|
776
|
+
const m = Math.round(s / 60);
|
|
777
|
+
if (m < 60) return m + "m ago";
|
|
778
|
+
return Math.round(m / 60) + "h ago";
|
|
779
|
+
}
|
|
780
|
+
setInterval(renderUpdated, 1000);
|
|
781
|
+
|
|
782
|
+
function label(clientName) {
|
|
783
|
+
if (clientName === "codex") return "Codex";
|
|
784
|
+
if (clientName === "claude-code") return "Claude Code";
|
|
785
|
+
if (clientName === "claude-desktop") return "Claude Desktop";
|
|
786
|
+
return "EchoMem HUD";
|
|
787
|
+
}
|
|
788
|
+
|
|
438
789
|
function fmt(tokens) {
|
|
439
790
|
const n = Number(tokens || 0);
|
|
440
|
-
if (n >= 1000000) return (n / 1000000).toFixed(1) +
|
|
441
|
-
if (n >= 1000) return Math.round(n / 1000) +
|
|
791
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + "m";
|
|
792
|
+
if (n >= 1000) return Math.round(n / 1000) + "k";
|
|
442
793
|
return String(Math.round(n));
|
|
443
794
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const path = bubble.querySelector('path');
|
|
448
|
-
if (!svg || !path) return;
|
|
449
|
-
const rect = bubble.getBoundingClientRect();
|
|
450
|
-
const width = Math.max(96, Math.round(rect.width));
|
|
451
|
-
const height = Math.max(72, Math.round(rect.height));
|
|
452
|
-
svg.setAttribute('viewBox', '0 0 ' + width + ' ' + (height + 16));
|
|
453
|
-
path.setAttribute('d', buildEchoBubblePath(width, height, 0.08));
|
|
454
|
-
});
|
|
795
|
+
|
|
796
|
+
function signedFmt(value) {
|
|
797
|
+
return (value >= 0 ? "+" : "-") + fmt(Math.abs(value));
|
|
455
798
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
let y = top + radius;
|
|
473
|
-
let toggle = 0;
|
|
474
|
-
while (y < bottom - radius - 10) {
|
|
475
|
-
const nextY = Math.min(bottom - radius, y + 72);
|
|
476
|
-
const x1 = right + (toggle % 2 === 0 ? 1.4 : -0.8);
|
|
477
|
-
const x2 = right + (toggle % 2 === 0 ? -1.6 : 1.0);
|
|
478
|
-
rightSegments.push('C ' + x1 + ' ' + (y + 21) + ', ' + x2 + ' ' + (nextY - 21) + ', ' + right + ' ' + nextY);
|
|
479
|
-
y = nextY;
|
|
480
|
-
toggle += 1;
|
|
481
|
-
}
|
|
482
|
-
const leftSegments = [];
|
|
483
|
-
y = bottom - radius;
|
|
484
|
-
toggle = 0;
|
|
485
|
-
while (y > top + radius + 10) {
|
|
486
|
-
const nextY = Math.max(top + radius, y - 72);
|
|
487
|
-
const x1 = left + (toggle % 2 === 0 ? -1.2 : 1.4);
|
|
488
|
-
const x2 = left + (toggle % 2 === 0 ? 1.6 : -1.0);
|
|
489
|
-
leftSegments.push('C ' + x1 + ' ' + (y - 21) + ', ' + x2 + ' ' + (nextY + 21) + ', ' + left + ' ' + nextY);
|
|
490
|
-
y = nextY;
|
|
491
|
-
toggle += 1;
|
|
492
|
-
}
|
|
493
|
-
return [
|
|
494
|
-
'M ' + (left + radius) + ' ' + (top + 0.6),
|
|
495
|
-
'C ' + (w * 0.32) + ' ' + (top - 1.1) + ', ' + (w * 0.58) + ' ' + (top + 1.6) + ', ' + (right - radius) + ' ' + (top + 0.8),
|
|
496
|
-
'C ' + (right - 8) + ' ' + (top + 1.8) + ', ' + (right + 0.5) + ' ' + (top + 9.5) + ', ' + (right - 0.3) + ' ' + (top + radius),
|
|
497
|
-
...rightSegments,
|
|
498
|
-
'C ' + (right + 0.6) + ' ' + (bottom - 14) + ', ' + (right - 8) + ' ' + (bottom - 1.2) + ', ' + (right - radius) + ' ' + bottom,
|
|
499
|
-
'C ' + (w * 0.72) + ' ' + (bottom + 1.2) + ', ' + (w * 0.5) + ' ' + (bottom - 0.6) + ', ' + tailBaseRight + ' ' + bottom,
|
|
500
|
-
'C ' + (tailBaseRight - 3) + ' ' + (bottom + 5) + ', ' + (tailTipX + 4) + ' ' + (tailTipY - 1.5) + ', ' + tailTipX + ' ' + tailTipY,
|
|
501
|
-
'C ' + (tailTipX - 4) + ' ' + (tailTipY - 1.5) + ', ' + (tailBaseLeft - 1) + ' ' + (bottom + 5) + ', ' + tailBaseLeft + ' ' + bottom,
|
|
502
|
-
'C ' + (left + 18) + ' ' + (bottom + 1.0) + ', ' + (left + 5) + ' ' + (bottom - 8) + ', ' + (left + 0.6) + ' ' + (bottom - radius),
|
|
503
|
-
...leftSegments,
|
|
504
|
-
'C ' + (left - 0.8) + ' ' + (top + 14) + ', ' + (left + 7) + ' ' + (top + 2.2) + ', ' + (left + radius) + ' ' + (top + 0.6),
|
|
505
|
-
].join(' ');
|
|
799
|
+
|
|
800
|
+
function joinParts(parts) {
|
|
801
|
+
return parts.filter(Boolean).join(" - ");
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function clamp(value, min, max) {
|
|
805
|
+
return Math.max(min, Math.min(max, Number(value) || 0));
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function syncHeight() {
|
|
809
|
+
requestAnimationFrame(() => {
|
|
810
|
+
if (!window.echomemHud) return;
|
|
811
|
+
const open = hud.classList.contains("open");
|
|
812
|
+
const height = open ? Math.ceil(hud.getBoundingClientRect().height) + 14 : undefined;
|
|
813
|
+
window.echomemHud.setOpen(open, height);
|
|
814
|
+
});
|
|
506
815
|
}
|
|
507
816
|
</script>
|
|
508
817
|
</body>
|