@echomem/mcp 1.4.0 → 1.4.2
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 +42 -7
- package/assets/hud/echo-face-cutout.png +0 -0
- package/dist/city/echo-ai-city-only.html +643 -35
- package/dist/city/personality_stickers/bossy.png +0 -0
- package/dist/city/personality_stickers/ghosty.png +0 -0
- package/dist/city/personality_stickers/loopy.png +0 -0
- package/dist/city/personality_stickers/lusty.png +0 -0
- package/dist/city/personality_stickers/maxxy.png +0 -0
- package/dist/city/personality_stickers/tabby.png +0 -0
- package/dist/forensics.js +9 -4
- package/dist/hud/adapters.js +288 -0
- package/dist/hud/api.js +29 -0
- package/dist/hud/capsule.js +125 -0
- package/dist/hud/cli.js +142 -0
- package/dist/hud/electron-main.js +224 -0
- package/dist/hud/fs.js +63 -0
- package/dist/hud/hooks.js +50 -0
- package/dist/hud/metric.js +158 -0
- package/dist/hud/monitor.js +106 -0
- package/dist/hud/preload.cjs +10 -0
- package/dist/hud/render.js +39 -0
- package/dist/hud/report.js +125 -0
- package/dist/hud/server.js +95 -0
- package/dist/hud/web.js +507 -0
- package/dist/index.js +119 -6
- package/dist/package-metadata.js +32 -0
- package/dist/report.js +1 -1
- package/dist/setup-page.js +1 -2
- package/dist/setup.js +80 -1
- package/dist/v1-contract.js +61 -2
- package/package.json +12 -8
package/dist/hud/web.js
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
export const HUD_HTML = String.raw `<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>EchoMem Context HUD</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light;
|
|
10
|
+
--echo-paper-canvas: #fdfaf2;
|
|
11
|
+
--echo-paper-cream: #fbf8f1;
|
|
12
|
+
--echo-paper-white: #ffffff;
|
|
13
|
+
--echo-ink-text: #141414;
|
|
14
|
+
--echo-ink-primary: #1a3a8f;
|
|
15
|
+
--echo-ink-mute: #8a8678;
|
|
16
|
+
--echo-line-brand: rgba(26, 58, 143, 0.34);
|
|
17
|
+
--echo-shadow-blue: drop-shadow(0 2px 3px rgba(26, 58, 143, 0.09));
|
|
18
|
+
font-family: "Avenir Next", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
19
|
+
background: transparent;
|
|
20
|
+
}
|
|
21
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: start center; background: transparent; color: var(--echo-ink-text); overflow: hidden; }
|
|
22
|
+
#hud { width: min(360px, calc(100vw - 16px)); background: transparent; }
|
|
23
|
+
.glance { position: relative; height: 112px; user-select: none; cursor: default; }
|
|
24
|
+
.drag { position: absolute; left: 4px; top: 40px; width: 20px; height: 32px; border-radius: 8px; -webkit-app-region: drag; opacity: .78; z-index: 5; }
|
|
25
|
+
.drag:before { content: ""; position: absolute; left: 4px; top: 7px; width: 4px; height: 4px; border-radius: 50%; background: rgba(26, 58, 143, .42); box-shadow: 0 10px 0 rgba(26, 58, 143, .42), 10px 0 0 rgba(26, 58, 143, .42), 10px 10px 0 rgba(26, 58, 143, .42); }
|
|
26
|
+
.mascot-stage { position: absolute; left: 24px; top: 26px; width: 70px; height: 70px; border-radius: 18px; overflow: hidden; z-index: 4; }
|
|
27
|
+
@keyframes echo-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2.5px); } }
|
|
28
|
+
.mascot { position: absolute; left: -4px; top: -2px; width: 80px; height: 78px; object-fit: cover; object-position: center top; pointer-events: none; animation: echo-bob 4.5s ease-in-out infinite; }
|
|
29
|
+
@media (prefers-reduced-motion: reduce) { .mascot { animation: none; } }
|
|
30
|
+
.paper-bubble { position: relative; color: var(--echo-ink-text); }
|
|
31
|
+
.paper-bubble-skin { position: absolute; inset: 0; overflow: visible; pointer-events: none; filter: var(--echo-shadow-blue); }
|
|
32
|
+
.paper-bubble-outline { position: absolute; left: 0; top: 0; width: 100%; height: calc(100% + 16px); overflow: visible; }
|
|
33
|
+
.paper-bubble-outline path { fill: var(--echo-paper-canvas); stroke: var(--echo-ink-primary); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; opacity: .78; vector-effect: non-scaling-stroke; }
|
|
34
|
+
.status-bubble { position: absolute; left: 84px; right: 6px; top: 15px; min-height: 72px; z-index: 2; }
|
|
35
|
+
.copy { position: relative; z-index: 2; min-width: 0; display: grid; gap: 4px; padding: 20px 22px 19px 28px; }
|
|
36
|
+
.main { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; font-weight: 800; letter-spacing: 0; color: var(--echo-ink-text); line-height: 1.1; }
|
|
37
|
+
.client { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace; font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--echo-ink-mute); line-height: 1; }
|
|
38
|
+
.dot { position: absolute; left: 14px; top: 23px; width: 10px; height: 10px; border: 2px solid var(--echo-paper-canvas); border-radius: 999px; background: #43d17a; z-index: 3; box-shadow: 0 0 0 1px rgba(26, 58, 143, .14); }
|
|
39
|
+
.dot.amber { background: #e9b949; }
|
|
40
|
+
.dot.red { background: #ff5f57; }
|
|
41
|
+
.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); }
|
|
42
|
+
#hud.open .details { display: block; }
|
|
43
|
+
.metric { display: flex; justify-content: space-between; gap: 16px; padding: 6px 0; border-bottom: 1px solid rgba(26, 58, 143, .10); }
|
|
44
|
+
.metric:last-child { border-bottom: 0; }
|
|
45
|
+
.metric span { color: var(--echo-ink-mute); }
|
|
46
|
+
.metric strong { color: var(--echo-ink-primary); font-weight: 700; }
|
|
47
|
+
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; }
|
|
48
|
+
button.renew-btn:hover { background: rgba(199, 55, 47, .14); }
|
|
49
|
+
button.renew-btn:active { background: rgba(199, 55, 47, .22); }
|
|
50
|
+
button.renew-btn.loading { opacity: .6; cursor: wait; }
|
|
51
|
+
.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; }
|
|
52
|
+
.renew-result .renew-msg { font-size: 11.5px; font-weight: 700; color: var(--echo-ink-primary); margin-bottom: 6px; }
|
|
53
|
+
.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; }
|
|
54
|
+
.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; }
|
|
55
|
+
.renew-result .copy-btn:hover { background: rgba(26, 58, 143, .14); }
|
|
56
|
+
.path { margin-top: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--echo-ink-mute); }
|
|
57
|
+
.missing { margin-top: 8px; color: var(--echo-ink-mute); }
|
|
58
|
+
.summary { margin: 0 0 8px; font-size: 12.5px; font-weight: 700; color: var(--echo-ink-primary); line-height: 1.25; }
|
|
59
|
+
.note { margin: 10px 0 0; font-size: 10.5px; color: var(--echo-ink-mute); line-height: 1.3; }
|
|
60
|
+
.dot.idle { background: #c9c5b8; }
|
|
61
|
+
.meter { margin: 0 0 11px; }
|
|
62
|
+
.meter-label { display: flex; justify-content: space-between; gap: 12px; font-size: 11px; color: var(--echo-ink-mute); margin-bottom: 5px; }
|
|
63
|
+
.meter-label strong { color: var(--echo-ink-primary); font-weight: 700; }
|
|
64
|
+
.meter-track { height: 6px; border-radius: 999px; background: rgba(26, 58, 143, .10); overflow: hidden; }
|
|
65
|
+
.meter-fill { height: 100%; width: 0; border-radius: 999px; background: var(--echo-ink-primary); transition: width .35s ease, background-color .35s ease; }
|
|
66
|
+
.trend { margin: 0 0 11px; }
|
|
67
|
+
.trend-label { display: flex; justify-content: space-between; gap: 12px; font-size: 11px; color: var(--echo-ink-mute); margin-bottom: 4px; }
|
|
68
|
+
.trend-label strong { color: var(--echo-ink-primary); font-weight: 700; }
|
|
69
|
+
.spark { width: 100%; height: 28px; display: block; }
|
|
70
|
+
.trend-empty { font-size: 10.5px; color: var(--echo-ink-mute); }
|
|
71
|
+
.nudge { margin: 11px 0 0; font-size: 11.5px; line-height: 1.35; color: var(--echo-ink-text); background: rgba(199, 55, 47, .06); border: 1px solid rgba(199, 55, 47, .20); border-radius: 12px; padding: 8px 11px; }
|
|
72
|
+
.tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin: 2px 0 12px; }
|
|
73
|
+
.tile { background: rgba(26, 58, 143, .05); border: 1px solid rgba(26, 58, 143, .10); border-radius: 12px; padding: 8px 10px; min-width: 0; }
|
|
74
|
+
.tile b { display: block; font-size: 16px; font-weight: 800; color: var(--echo-ink-primary); line-height: 1.12; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
75
|
+
.tile span { font-size: 9.5px; letter-spacing: .04em; text-transform: uppercase; color: var(--echo-ink-mute); }
|
|
76
|
+
.meta { font-size: 10.5px; color: var(--echo-ink-mute); margin: -4px 0 12px; }
|
|
77
|
+
.section { margin: 0 0 12px; }
|
|
78
|
+
.section-h { display: flex; justify-content: space-between; gap: 10px; font-size: 11px; color: var(--echo-ink-mute); margin-bottom: 7px; }
|
|
79
|
+
.section-h strong { color: var(--echo-ink-primary); font-weight: 700; }
|
|
80
|
+
.bars { display: grid; gap: 6px; }
|
|
81
|
+
.bar { display: grid; grid-template-columns: 92px 1fr 30px; align-items: center; gap: 8px; }
|
|
82
|
+
.bar-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--echo-ink-text); font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace; font-size: 10px; }
|
|
83
|
+
.bar-track { height: 8px; border-radius: 999px; background: rgba(26, 58, 143, .08); overflow: hidden; }
|
|
84
|
+
.bar-fill { display: block; height: 100%; border-radius: 999px; background: var(--echo-ink-primary); transition: width .35s ease; }
|
|
85
|
+
.bar.big .bar-fill { background: #c7372f; }
|
|
86
|
+
.bar-val { text-align: right; color: var(--echo-ink-primary); font-weight: 700; font-size: 11px; }
|
|
87
|
+
.barwrap.has-files .bar { cursor: pointer; }
|
|
88
|
+
.barwrap.has-files .bar-name { color: var(--echo-ink-primary); }
|
|
89
|
+
.files { display: none; margin: 2px 0 6px 8px; }
|
|
90
|
+
.barwrap.open .files { display: block; }
|
|
91
|
+
.file { font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace; font-size: 9.5px; color: var(--echo-ink-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 1px 0; }
|
|
92
|
+
.delta { margin: -4px 0 10px; font-size: 10.5px; color: var(--echo-ink-mute); }
|
|
93
|
+
.mascot-stage { transition: filter .45s ease; }
|
|
94
|
+
#hud.s-green .mascot-stage { filter: drop-shadow(0 2px 6px rgba(67, 209, 122, .30)); }
|
|
95
|
+
#hud.s-amber .mascot-stage { filter: drop-shadow(0 2px 7px rgba(233, 185, 73, .42)); }
|
|
96
|
+
#hud.s-red .mascot-stage { filter: drop-shadow(0 3px 8px rgba(199, 55, 47, .48)); }
|
|
97
|
+
#hud.s-amber .mascot { animation-duration: 5.6s; }
|
|
98
|
+
#hud.s-red .mascot { animation-duration: 6.8s; }
|
|
99
|
+
</style>
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<section id="hud" aria-live="polite">
|
|
103
|
+
<div class="glance" id="toggle">
|
|
104
|
+
<span class="drag" title="Drag"></span>
|
|
105
|
+
<span class="mascot-stage"><img class="mascot" src="/assets/hud/echo-face-cutout.png" alt="" /></span>
|
|
106
|
+
<div class="paper-bubble status-bubble">
|
|
107
|
+
<div class="paper-bubble-skin" aria-hidden="true">
|
|
108
|
+
<svg class="paper-bubble-outline" preserveAspectRatio="none" focusable="false">
|
|
109
|
+
<path />
|
|
110
|
+
</svg>
|
|
111
|
+
</div>
|
|
112
|
+
<span class="dot" id="dot"></span>
|
|
113
|
+
<span class="copy">
|
|
114
|
+
<span class="main" id="main">Checking context…</span>
|
|
115
|
+
<span class="client" id="client">EchoMem HUD</span>
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="details">
|
|
120
|
+
<div class="meter">
|
|
121
|
+
<div class="meter-label"><span>Window</span><strong id="fullness">—</strong></div>
|
|
122
|
+
<div class="meter-track"><div class="meter-fill" id="meterfill"></div></div>
|
|
123
|
+
</div>
|
|
124
|
+
<p class="summary" id="summary">Checking how noisy this session is…</p>
|
|
125
|
+
<p class="delta" id="delta" style="display:none"></p>
|
|
126
|
+
<div class="trend">
|
|
127
|
+
<div class="trend-label"><span>Noise trend</span><strong id="trendnow"></strong></div>
|
|
128
|
+
<svg class="spark" id="spark" viewBox="0 0 100 28" preserveAspectRatio="none" aria-hidden="true" style="display:none">
|
|
129
|
+
<polygon id="sparkarea" fill="rgba(26, 58, 143, .08)" points=""></polygon>
|
|
130
|
+
<polyline id="sparkline" fill="none" stroke="var(--echo-ink-primary)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" points=""></polyline>
|
|
131
|
+
</svg>
|
|
132
|
+
<div class="trend-empty" id="trendempty">Collecting a few turns…</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="tiles">
|
|
135
|
+
<div class="tile"><b id="t-reclaim">—</b><span>reclaimable ≥</span></div>
|
|
136
|
+
<div class="tile"><b id="t-repeat">—</b><span>repeated reads</span></div>
|
|
137
|
+
<div class="tile"><b id="t-reads">—</b><span>files read</span></div>
|
|
138
|
+
<div class="tile"><b id="t-edits">—</b><span>edits</span></div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="meta" id="meta"></div>
|
|
141
|
+
<div class="section">
|
|
142
|
+
<div class="section-h"><span>Tools used</span><strong id="toolstotal"></strong></div>
|
|
143
|
+
<div class="bars" id="toolbars"></div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="section" id="bigsec" style="display:none">
|
|
146
|
+
<div class="section-h"><span>Output volume · by tool</span><strong id="bigtotal"></strong></div>
|
|
147
|
+
<div class="bars" id="bigtools"></div>
|
|
148
|
+
</div>
|
|
149
|
+
<p class="note">Tracked, lower-bound estimate — there may be more we can't see.</p>
|
|
150
|
+
<p class="nudge" id="nudge" style="display:none"></p>
|
|
151
|
+
<button class="renew-btn" id="renewbtn">Renew session</button>
|
|
152
|
+
<div class="renew-result" id="renewresult">
|
|
153
|
+
<div class="renew-msg" id="renewmsg"></div>
|
|
154
|
+
<pre id="renewpre"></pre>
|
|
155
|
+
<button class="copy-btn" id="copybtn">Copy to clipboard</button>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="path" id="path"></div>
|
|
158
|
+
<div class="missing" id="missing"></div>
|
|
159
|
+
</div>
|
|
160
|
+
</section>
|
|
161
|
+
<script>
|
|
162
|
+
const hud = document.getElementById('hud');
|
|
163
|
+
const dot = document.getElementById('dot');
|
|
164
|
+
const main = document.getElementById('main');
|
|
165
|
+
const client = document.getElementById('client');
|
|
166
|
+
const tReclaim = document.getElementById('t-reclaim');
|
|
167
|
+
const tRepeat = document.getElementById('t-repeat');
|
|
168
|
+
const tReads = document.getElementById('t-reads');
|
|
169
|
+
const tEdits = document.getElementById('t-edits');
|
|
170
|
+
const metaEl = document.getElementById('meta');
|
|
171
|
+
const toolbars = document.getElementById('toolbars');
|
|
172
|
+
const toolstotal = document.getElementById('toolstotal');
|
|
173
|
+
const bigsec = document.getElementById('bigsec');
|
|
174
|
+
const bigtools = document.getElementById('bigtools');
|
|
175
|
+
const bigtotal = document.getElementById('bigtotal');
|
|
176
|
+
const pathEl = document.getElementById('path');
|
|
177
|
+
const missing = document.getElementById('missing');
|
|
178
|
+
const summary = document.getElementById('summary');
|
|
179
|
+
const fullness = document.getElementById('fullness');
|
|
180
|
+
const meterfill = document.getElementById('meterfill');
|
|
181
|
+
const spark = document.getElementById('spark');
|
|
182
|
+
const sparkline = document.getElementById('sparkline');
|
|
183
|
+
const sparkarea = document.getElementById('sparkarea');
|
|
184
|
+
const trendnow = document.getElementById('trendnow');
|
|
185
|
+
const trendempty = document.getElementById('trendempty');
|
|
186
|
+
const nudge = document.getElementById('nudge');
|
|
187
|
+
const deltaEl = document.getElementById('delta');
|
|
188
|
+
const renewBtn = document.getElementById('renewbtn');
|
|
189
|
+
const renewResult = document.getElementById('renewresult');
|
|
190
|
+
const renewMsg = document.getElementById('renewmsg');
|
|
191
|
+
const renewPre = document.getElementById('renewpre');
|
|
192
|
+
const copyBtn = document.getElementById('copybtn');
|
|
193
|
+
const history = [];
|
|
194
|
+
let lastSource = '';
|
|
195
|
+
let prevTurnState = null;
|
|
196
|
+
updateBubbleSkins();
|
|
197
|
+
if ('ResizeObserver' in window) {
|
|
198
|
+
const observer = new ResizeObserver(updateBubbleSkins);
|
|
199
|
+
document.querySelectorAll('.paper-bubble').forEach((bubble) => observer.observe(bubble));
|
|
200
|
+
}
|
|
201
|
+
window.addEventListener('load', updateBubbleSkins);
|
|
202
|
+
document.getElementById('toggle').addEventListener('click', (event) => {
|
|
203
|
+
if (event.target instanceof HTMLElement && event.target.closest('.drag')) return;
|
|
204
|
+
hud.classList.toggle('open');
|
|
205
|
+
const open = hud.classList.contains('open');
|
|
206
|
+
requestAnimationFrame(() => {
|
|
207
|
+
updateBubbleSkins();
|
|
208
|
+
const height = open ? Math.ceil(hud.getBoundingClientRect().height) + 14 : undefined;
|
|
209
|
+
window.echomemHud && window.echomemHud.setOpen(open, height);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
if (toolbars) toolbars.addEventListener('click', (event) => {
|
|
213
|
+
const wrap = event.target && event.target.closest ? event.target.closest('.barwrap') : null;
|
|
214
|
+
if (!wrap || !wrap.classList.contains('has-files')) return;
|
|
215
|
+
const open = wrap.classList.toggle('open');
|
|
216
|
+
const nameEl = wrap.querySelector('.bar-name');
|
|
217
|
+
if (nameEl) nameEl.textContent = (open ? '▾ ' : '▸ ') + (wrap.getAttribute('data-tool') || '');
|
|
218
|
+
requestAnimationFrame(() => {
|
|
219
|
+
if (hud.classList.contains('open')) {
|
|
220
|
+
const height = Math.ceil(hud.getBoundingClientRect().height) + 14;
|
|
221
|
+
window.echomemHud && window.echomemHud.setOpen(true, height);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
let lastCapsule = '';
|
|
226
|
+
if (renewBtn) renewBtn.addEventListener('click', async () => {
|
|
227
|
+
renewBtn.classList.add('loading');
|
|
228
|
+
renewBtn.textContent = 'Capturing…';
|
|
229
|
+
try {
|
|
230
|
+
const res = await fetch('/capsule');
|
|
231
|
+
const data = await res.json();
|
|
232
|
+
if (!data.ok) { renewBtn.textContent = 'Renew session'; renewBtn.classList.remove('loading'); return; }
|
|
233
|
+
const prompt = 'Continue my previous session. Verify anything that references file state before trusting it.\n\n' + data.capsule;
|
|
234
|
+
lastCapsule = prompt;
|
|
235
|
+
if (renewPre) renewPre.textContent = prompt;
|
|
236
|
+
if (renewMsg) renewMsg.textContent = 'Open a new session and paste this:';
|
|
237
|
+
if (renewResult) {
|
|
238
|
+
renewResult.style.display = 'block';
|
|
239
|
+
requestAnimationFrame(() => {
|
|
240
|
+
const height = Math.ceil(hud.getBoundingClientRect().height) + 14;
|
|
241
|
+
window.echomemHud && window.echomemHud.setOpen(true, height);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
try { await navigator.clipboard.writeText(prompt); if (renewMsg) renewMsg.textContent = 'Copied! Open a new session and paste:'; } catch {}
|
|
245
|
+
} catch { /* best-effort */ }
|
|
246
|
+
renewBtn.textContent = 'Renew session';
|
|
247
|
+
renewBtn.classList.remove('loading');
|
|
248
|
+
});
|
|
249
|
+
if (copyBtn) copyBtn.addEventListener('click', async () => {
|
|
250
|
+
if (!lastCapsule) return;
|
|
251
|
+
try { await navigator.clipboard.writeText(lastCapsule); copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.textContent = 'Copy to clipboard'; }, 1500); } catch {}
|
|
252
|
+
});
|
|
253
|
+
const events = new EventSource('/events');
|
|
254
|
+
events.onmessage = (event) => render(JSON.parse(event.data));
|
|
255
|
+
fetch('/state').then((r) => r.json()).then(render).catch(() => {});
|
|
256
|
+
function render(state) {
|
|
257
|
+
const score = state.active || state;
|
|
258
|
+
if (!score || typeof score.usefulPct !== 'number') {
|
|
259
|
+
dot.className = 'dot idle';
|
|
260
|
+
hud.classList.remove('s-green', 's-amber', 's-red');
|
|
261
|
+
if (deltaEl) deltaEl.style.display = 'none';
|
|
262
|
+
prevTurnState = null;
|
|
263
|
+
main.textContent = 'No active session yet';
|
|
264
|
+
client.textContent = '';
|
|
265
|
+
if (summary) summary.textContent = 'Start working in Codex or Claude and I’ll check the context here.';
|
|
266
|
+
if (fullness) fullness.textContent = '';
|
|
267
|
+
if (meterfill) meterfill.style.width = '0';
|
|
268
|
+
history.length = 0;
|
|
269
|
+
if (spark) spark.style.display = 'none';
|
|
270
|
+
if (trendempty) trendempty.style.display = 'block';
|
|
271
|
+
if (trendnow) trendnow.textContent = '';
|
|
272
|
+
if (nudge) nudge.style.display = 'none';
|
|
273
|
+
if (toolbars) toolbars.innerHTML = '';
|
|
274
|
+
if (bigsec) bigsec.style.display = 'none';
|
|
275
|
+
if (toolstotal) toolstotal.textContent = '';
|
|
276
|
+
if (metaEl) metaEl.textContent = '';
|
|
277
|
+
if (tReclaim) tReclaim.textContent = '—';
|
|
278
|
+
if (tRepeat) tRepeat.textContent = '—';
|
|
279
|
+
if (tReads) tReads.textContent = '—';
|
|
280
|
+
if (tEdits) tEdits.textContent = '—';
|
|
281
|
+
missing.textContent = formatMissing(state.missing);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
dot.className = 'dot ' + (score.color || 'green');
|
|
285
|
+
hud.classList.remove('s-green', 's-amber', 's-red');
|
|
286
|
+
hud.classList.add('s-' + (score.color || 'green'));
|
|
287
|
+
main.textContent = stateWord(score.color) + ' · ' + fmt(score.ctTokens) + ' ctx';
|
|
288
|
+
client.textContent = label(score.client);
|
|
289
|
+
if (summary) summary.textContent = summaryLine(score.color);
|
|
290
|
+
const sat = typeof score.saturationPct === 'number' ? score.saturationPct
|
|
291
|
+
: (score.modelContextWindow ? Math.round(100 * (score.ctTokens || 0) / score.modelContextWindow) : 0);
|
|
292
|
+
if (fullness) fullness.textContent = sat + '% full · ' + fmt(score.ctTokens) + (score.modelContextWindow ? ' / ' + fmt(score.modelContextWindow) : '');
|
|
293
|
+
if (meterfill) {
|
|
294
|
+
meterfill.style.width = Math.max(2, Math.min(100, sat)) + '%';
|
|
295
|
+
meterfill.style.background = sat >= 90 ? '#c7372f' : sat >= 75 ? '#e9b949' : 'var(--echo-ink-primary)';
|
|
296
|
+
}
|
|
297
|
+
pushTrend(score);
|
|
298
|
+
if (nudge) {
|
|
299
|
+
let msg = '';
|
|
300
|
+
if (score.color === 'red') msg = 'Old context is piling up — renew to start a clean session with everything you need.';
|
|
301
|
+
else if (sat >= 85) msg = 'The window is getting full — renewing now would keep you fast.';
|
|
302
|
+
nudge.textContent = msg;
|
|
303
|
+
nudge.style.display = msg ? 'block' : 'none';
|
|
304
|
+
}
|
|
305
|
+
if (renewBtn) {
|
|
306
|
+
renewBtn.style.display = (score.color === 'red' || score.color === 'amber') ? 'block' : 'none';
|
|
307
|
+
}
|
|
308
|
+
if (history.length >= 2) {
|
|
309
|
+
const c = history[history.length - 1].p, pr = history[history.length - 2].p;
|
|
310
|
+
const a = c > pr + 1 ? ' ↑' : c < pr - 1 ? ' ↓' : '';
|
|
311
|
+
main.textContent = stateWord(score.color) + a + ' · ' + fmt(score.ctTokens) + ' ctx';
|
|
312
|
+
}
|
|
313
|
+
const bucket = score.buckets && score.buckets.range_redundant;
|
|
314
|
+
if (tReclaim) tReclaim.textContent = fmt(score.pollutionTok);
|
|
315
|
+
if (tRepeat) tRepeat.textContent = (bucket ? bucket.count : 0) + '×';
|
|
316
|
+
if (tReads) tReads.textContent = String(score.reads || 0);
|
|
317
|
+
if (tEdits) tEdits.textContent = String((score.stats && score.stats.patchEdits) || 0);
|
|
318
|
+
if (deltaEl) {
|
|
319
|
+
const repeatNow = bucket ? bucket.count : 0;
|
|
320
|
+
if (prevTurnState && prevTurnState.src === score.sourcePath && score.turn !== prevTurnState.turn) {
|
|
321
|
+
const dCt = (score.ctTokens || 0) - prevTurnState.ct;
|
|
322
|
+
const dReads = (score.reads || 0) - prevTurnState.reads;
|
|
323
|
+
const dRep = repeatNow - prevTurnState.repeat;
|
|
324
|
+
const parts = [(dCt >= 0 ? '+' : '−') + fmt(Math.abs(dCt)) + ' ctx' + (dCt < -20000 ? ' (compacted)' : '')];
|
|
325
|
+
if (dReads > 0) parts.push('+' + dReads + ' read' + (dReads > 1 ? 's' : ''));
|
|
326
|
+
if (dRep > 0) parts.push('+' + dRep + ' repeat' + (dRep > 1 ? 's' : ''));
|
|
327
|
+
deltaEl.textContent = 'last turn ' + parts.join(' · ');
|
|
328
|
+
deltaEl.style.display = 'block';
|
|
329
|
+
} else if (!prevTurnState || prevTurnState.src !== score.sourcePath) {
|
|
330
|
+
deltaEl.style.display = 'none';
|
|
331
|
+
}
|
|
332
|
+
if (!prevTurnState || prevTurnState.src !== score.sourcePath || score.turn !== prevTurnState.turn) {
|
|
333
|
+
prevTurnState = { src: score.sourcePath, turn: score.turn, ct: score.ctTokens || 0, reads: score.reads || 0, repeat: repeatNow };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (metaEl) metaEl.textContent = ctSourceLabel(score.ctSource) + ' · ' + ((score.stats && score.stats.compactMarkers) || 0) + ' compactions';
|
|
337
|
+
renderBars(toolbars, score.tools, false, undefined, score.filesByTool);
|
|
338
|
+
if (toolstotal) toolstotal.textContent = sumCounts(score.tools) + ' calls';
|
|
339
|
+
const bigMap = score.outputTokensByTool || {};
|
|
340
|
+
if (bigsec) {
|
|
341
|
+
const hasBig = Object.keys(bigMap).length > 0;
|
|
342
|
+
bigsec.style.display = hasBig ? 'block' : 'none';
|
|
343
|
+
if (hasBig) { renderBars(bigtools, bigMap, true, fmt); if (bigtotal) bigtotal.textContent = '≈ ' + fmt(sumCounts(bigMap)) + ' seen'; }
|
|
344
|
+
}
|
|
345
|
+
pathEl.textContent = score.sourcePath ? 'Reading ' + score.sourcePath : '';
|
|
346
|
+
missing.textContent = formatMissing(state.missing);
|
|
347
|
+
}
|
|
348
|
+
function stateWord(color) {
|
|
349
|
+
if (color === 'red') return 'Drifted';
|
|
350
|
+
if (color === 'amber') return 'Drifting';
|
|
351
|
+
return 'Fresh';
|
|
352
|
+
}
|
|
353
|
+
function summaryLine(color) {
|
|
354
|
+
if (color === 'red') return 'Old context is weighing this session down.';
|
|
355
|
+
if (color === 'amber') return 'Some context is getting stale.';
|
|
356
|
+
return 'Looks fresh — barely any repeated context.';
|
|
357
|
+
}
|
|
358
|
+
function ctSourceLabel(src) {
|
|
359
|
+
if (src === 'token_count' || src === 'statusline') return 'measured';
|
|
360
|
+
return 'estimated';
|
|
361
|
+
}
|
|
362
|
+
function formatMissing(list) {
|
|
363
|
+
if (!list || !list.length) return '';
|
|
364
|
+
return 'Not seeing ' + list.map(label).join(', ') + ' yet';
|
|
365
|
+
}
|
|
366
|
+
function sumCounts(map) {
|
|
367
|
+
let s = 0; for (const k in (map || {})) s += map[k] || 0; return s;
|
|
368
|
+
}
|
|
369
|
+
function escapeHTML(s) {
|
|
370
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
371
|
+
}
|
|
372
|
+
function shortFile(f) {
|
|
373
|
+
const parts = String(f).split('/').filter(Boolean);
|
|
374
|
+
return parts.length <= 2 ? String(f) : '…/' + parts.slice(-2).join('/');
|
|
375
|
+
}
|
|
376
|
+
function renderBars(container, map, big, fmtFn, filesByTool) {
|
|
377
|
+
if (!container) return;
|
|
378
|
+
const fmtVal = fmtFn || String;
|
|
379
|
+
const entries = Object.keys(map || {}).map(function (k) { return [k, map[k] || 0]; })
|
|
380
|
+
.filter(function (e) { return e[1] > 0; }).sort(function (a, b) { return b[1] - a[1]; }).slice(0, 6);
|
|
381
|
+
if (!entries.length) { container.innerHTML = '<div class="trend-empty">none yet</div>'; return; }
|
|
382
|
+
const max = entries[0][1];
|
|
383
|
+
container.innerHTML = entries.map(function (e) {
|
|
384
|
+
const w = Math.max(6, Math.round(e[1] / max * 100));
|
|
385
|
+
const files = filesByTool && filesByTool[e[0]] ? filesByTool[e[0]] : null;
|
|
386
|
+
const hasFiles = files && files.length;
|
|
387
|
+
const nameTxt = (hasFiles ? '▸ ' : '') + e[0];
|
|
388
|
+
const filesHTML = hasFiles ? '<div class="files">' + files.map(function (f) {
|
|
389
|
+
return '<div class="file" title="' + escapeHTML(f) + '">' + escapeHTML(shortFile(f)) + '</div>';
|
|
390
|
+
}).join('') + '</div>' : '';
|
|
391
|
+
return '<div class="barwrap' + (hasFiles ? ' has-files' : '') + '" data-tool="' + escapeHTML(e[0]) + '">'
|
|
392
|
+
+ '<div class="bar' + (big ? ' big' : '') + '">'
|
|
393
|
+
+ '<span class="bar-name" title="' + escapeHTML(e[0]) + '">' + escapeHTML(nameTxt) + '</span>'
|
|
394
|
+
+ '<span class="bar-track"><span class="bar-fill" style="width:' + w + '%"></span></span>'
|
|
395
|
+
+ '<span class="bar-val">' + fmtVal(e[1]) + '</span></div>'
|
|
396
|
+
+ filesHTML + '</div>';
|
|
397
|
+
}).join('');
|
|
398
|
+
}
|
|
399
|
+
function pushTrend(score) {
|
|
400
|
+
const src = score.sourcePath || score.client || '';
|
|
401
|
+
if (src !== lastSource) { history.length = 0; lastSource = src; }
|
|
402
|
+
const t = typeof score.turn === 'number' ? score.turn : history.length;
|
|
403
|
+
const p = Math.max(0, Math.min(100, score.pollutionPct || 0));
|
|
404
|
+
if (history.length && history[history.length - 1].t === t) history[history.length - 1].p = p;
|
|
405
|
+
else { history.push({ t: t, p: p }); if (history.length > 40) history.shift(); }
|
|
406
|
+
drawSpark();
|
|
407
|
+
}
|
|
408
|
+
function drawSpark() {
|
|
409
|
+
if (!spark) return;
|
|
410
|
+
const n = history.length;
|
|
411
|
+
if (n < 2) {
|
|
412
|
+
spark.style.display = 'none';
|
|
413
|
+
if (trendempty) trendempty.style.display = 'block';
|
|
414
|
+
if (trendnow) trendnow.textContent = n ? history[0].p + '%' : '';
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (trendempty) trendempty.style.display = 'none';
|
|
418
|
+
spark.style.display = 'block';
|
|
419
|
+
const pts = history.map(function (h, i) {
|
|
420
|
+
const x = (i / (n - 1)) * 100;
|
|
421
|
+
const y = 27 - (h.p / 100) * 25;
|
|
422
|
+
return x.toFixed(1) + ',' + y.toFixed(1);
|
|
423
|
+
});
|
|
424
|
+
sparkline.setAttribute('points', pts.join(' '));
|
|
425
|
+
sparkarea.setAttribute('points', pts.join(' ') + ' 100,28 0,28');
|
|
426
|
+
const cur = history[n - 1].p, prev = history[n - 2].p;
|
|
427
|
+
const arrow = cur > prev + 1 ? '↑' : cur < prev - 1 ? '↓' : '→';
|
|
428
|
+
if (trendnow) trendnow.textContent = cur + '% ' + arrow;
|
|
429
|
+
}
|
|
430
|
+
function label(client) {
|
|
431
|
+
if (client === 'codex') return 'Codex';
|
|
432
|
+
if (client === 'claude-code') return 'Claude Code';
|
|
433
|
+
if (client === 'claude-desktop') return 'Claude Desktop';
|
|
434
|
+
return '';
|
|
435
|
+
}
|
|
436
|
+
function fmt(tokens) {
|
|
437
|
+
const n = Number(tokens || 0);
|
|
438
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'm';
|
|
439
|
+
if (n >= 1000) return Math.round(n / 1000) + 'k';
|
|
440
|
+
return String(Math.round(n));
|
|
441
|
+
}
|
|
442
|
+
function updateBubbleSkins() {
|
|
443
|
+
document.querySelectorAll('.paper-bubble').forEach((bubble) => {
|
|
444
|
+
const svg = bubble.querySelector('svg');
|
|
445
|
+
const path = bubble.querySelector('path');
|
|
446
|
+
if (!svg || !path) return;
|
|
447
|
+
const rect = bubble.getBoundingClientRect();
|
|
448
|
+
const width = Math.max(96, Math.round(rect.width));
|
|
449
|
+
const height = Math.max(72, Math.round(rect.height));
|
|
450
|
+
svg.setAttribute('viewBox', '0 0 ' + width + ' ' + (height + 16));
|
|
451
|
+
path.setAttribute('d', buildEchoBubblePath(width, height, 0.08));
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
function buildEchoBubblePath(width, height, tailRatio) {
|
|
455
|
+
const w = Math.max(96, Math.round(width));
|
|
456
|
+
const h = Math.max(72, Math.round(height));
|
|
457
|
+
const left = 2.5;
|
|
458
|
+
const right = w - 2.5;
|
|
459
|
+
const top = 2.5;
|
|
460
|
+
const bottom = h - 3;
|
|
461
|
+
const radius = Math.min(34, Math.max(24, Math.min(w, h) * 0.16));
|
|
462
|
+
const clampedTailRatio = Math.min(1, Math.max(0, tailRatio || 0));
|
|
463
|
+
const safeTailLeft = left + radius + 6;
|
|
464
|
+
const safeTailRight = right - radius - 6;
|
|
465
|
+
const tailTipX = safeTailLeft + (safeTailRight - safeTailLeft) * clampedTailRatio;
|
|
466
|
+
const tailBaseLeft = tailTipX - 3;
|
|
467
|
+
const tailBaseRight = tailTipX + 13;
|
|
468
|
+
const tailTipY = bottom + 11;
|
|
469
|
+
const rightSegments = [];
|
|
470
|
+
let y = top + radius;
|
|
471
|
+
let toggle = 0;
|
|
472
|
+
while (y < bottom - radius - 10) {
|
|
473
|
+
const nextY = Math.min(bottom - radius, y + 72);
|
|
474
|
+
const x1 = right + (toggle % 2 === 0 ? 1.4 : -0.8);
|
|
475
|
+
const x2 = right + (toggle % 2 === 0 ? -1.6 : 1.0);
|
|
476
|
+
rightSegments.push('C ' + x1 + ' ' + (y + 21) + ', ' + x2 + ' ' + (nextY - 21) + ', ' + right + ' ' + nextY);
|
|
477
|
+
y = nextY;
|
|
478
|
+
toggle += 1;
|
|
479
|
+
}
|
|
480
|
+
const leftSegments = [];
|
|
481
|
+
y = bottom - radius;
|
|
482
|
+
toggle = 0;
|
|
483
|
+
while (y > top + radius + 10) {
|
|
484
|
+
const nextY = Math.max(top + radius, y - 72);
|
|
485
|
+
const x1 = left + (toggle % 2 === 0 ? -1.2 : 1.4);
|
|
486
|
+
const x2 = left + (toggle % 2 === 0 ? 1.6 : -1.0);
|
|
487
|
+
leftSegments.push('C ' + x1 + ' ' + (y - 21) + ', ' + x2 + ' ' + (nextY + 21) + ', ' + left + ' ' + nextY);
|
|
488
|
+
y = nextY;
|
|
489
|
+
toggle += 1;
|
|
490
|
+
}
|
|
491
|
+
return [
|
|
492
|
+
'M ' + (left + radius) + ' ' + (top + 0.6),
|
|
493
|
+
'C ' + (w * 0.32) + ' ' + (top - 1.1) + ', ' + (w * 0.58) + ' ' + (top + 1.6) + ', ' + (right - radius) + ' ' + (top + 0.8),
|
|
494
|
+
'C ' + (right - 8) + ' ' + (top + 1.8) + ', ' + (right + 0.5) + ' ' + (top + 9.5) + ', ' + (right - 0.3) + ' ' + (top + radius),
|
|
495
|
+
...rightSegments,
|
|
496
|
+
'C ' + (right + 0.6) + ' ' + (bottom - 14) + ', ' + (right - 8) + ' ' + (bottom - 1.2) + ', ' + (right - radius) + ' ' + bottom,
|
|
497
|
+
'C ' + (w * 0.72) + ' ' + (bottom + 1.2) + ', ' + (w * 0.5) + ' ' + (bottom - 0.6) + ', ' + tailBaseRight + ' ' + bottom,
|
|
498
|
+
'C ' + (tailBaseRight - 3) + ' ' + (bottom + 5) + ', ' + (tailTipX + 4) + ' ' + (tailTipY - 1.5) + ', ' + tailTipX + ' ' + tailTipY,
|
|
499
|
+
'C ' + (tailTipX - 4) + ' ' + (tailTipY - 1.5) + ', ' + (tailBaseLeft - 1) + ' ' + (bottom + 5) + ', ' + tailBaseLeft + ' ' + bottom,
|
|
500
|
+
'C ' + (left + 18) + ' ' + (bottom + 1.0) + ', ' + (left + 5) + ' ' + (bottom - 8) + ', ' + (left + 0.6) + ' ' + (bottom - radius),
|
|
501
|
+
...leftSegments,
|
|
502
|
+
'C ' + (left - 0.8) + ' ' + (top + 14) + ', ' + (left + 7) + ' ' + (top + 2.2) + ', ' + (left + radius) + ' ' + (top + 0.6),
|
|
503
|
+
].join(' ');
|
|
504
|
+
}
|
|
505
|
+
</script>
|
|
506
|
+
</body>
|
|
507
|
+
</html>`;
|