@echomem/mcp 1.4.47 → 1.4.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +22 -26
  2. package/dist/context-analysis/claude-native-canonical.js +2 -2
  3. package/dist/context-analysis/vendored-canonical.js +2 -2
  4. package/dist/context-analysis/workspace-report.js +3 -3
  5. package/dist/index.js +5 -8
  6. package/dist/local-data-paths.js +68 -29
  7. package/dist/local-jsonl.js +87 -0
  8. package/dist/migrate.js +8 -9
  9. package/dist/onboarding-stats.js +16 -0
  10. package/dist/save-checkpoint-hook.js +1 -1
  11. package/dist/setup-page/client-core.js +18 -372
  12. package/dist/setup-page/client-extraction.js +14 -129
  13. package/dist/setup-page/client-lifecycle.js +28 -101
  14. package/dist/setup-page/client.js +0 -2
  15. package/dist/setup-page/styles-extraction.js +1 -31
  16. package/dist/setup-page/styles-foundation.js +0 -89
  17. package/dist/setup-page/styles-mvp.js +10 -155
  18. package/dist/setup-page/styles-website-alignment.js +0 -204
  19. package/dist/setup-page/styles.js +0 -4
  20. package/dist/setup-page.js +4 -4
  21. package/dist/setup-preview.js +4 -212
  22. package/dist/setup.js +119 -583
  23. package/dist/v1-contract.js +0 -8
  24. package/package.json +5 -7
  25. package/dist/city/README.md +0 -9
  26. package/dist/city/echo-ai-city-only.html +0 -2232
  27. package/dist/city/echo-extraction-plate.html +0 -330
  28. package/dist/city/echo-face-cutout.png +0 -0
  29. package/dist/city/personality_stickers/bossy.png +0 -0
  30. package/dist/city/personality_stickers/ghosty.png +0 -0
  31. package/dist/city/personality_stickers/loopy.png +0 -0
  32. package/dist/city/personality_stickers/lusty.png +0 -0
  33. package/dist/city/personality_stickers/maxxy.png +0 -0
  34. package/dist/city/personality_stickers/tabby.png +0 -0
  35. package/dist/city/vendor/OrbitControls.js +0 -1417
  36. package/dist/city/vendor/RoundedBoxGeometry.js +0 -155
  37. package/dist/city/vendor/echo_general-file-21.riv +0 -0
  38. package/dist/city/vendor/rive.js +0 -8139
  39. package/dist/city/vendor/rive.wasm +0 -0
  40. package/dist/city/vendor/three.module.min.js +0 -6
  41. package/dist/forensics.js +0 -1531
  42. package/dist/report.js +0 -721
  43. package/dist/setup-page/client-report-audit.js +0 -819
  44. package/dist/setup-page/client-report-city.js +0 -356
  45. package/dist/setup-page/client-report.js +0 -6
  46. package/dist/setup-page/styles-city-report.js +0 -880
  47. package/dist/setup-page/styles-context-audit.js +0 -470
@@ -1,330 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <title>Echo extraction plate</title>
6
- <style>
7
- *{margin:0;padding:0;box-sizing:border-box;}
8
- html,body{width:100%;height:100%;background:transparent;overflow:hidden;}
9
- #plate{width:100%;height:100%;display:block;}
10
- </style>
11
- </head>
12
- <body>
13
- <div id="plate"></div>
14
- <script type="importmap">
15
- { "imports": { "three": "./vendor/three.module.min.js" } }
16
- </script>
17
- <script type="module">
18
- // The real Echo clay city (ported from echo-ai-city-only / EchoCityScene), used as the extraction
19
- // plate: it renders the user's full repo-city from /report (token signpost = total tokens / input%).
20
- // The city stays standing (extraction is additive). While /progress is running, a continuous gentle
21
- // stream of memory motes flows from the city into the signpost; each completed conversation fires a
22
- // brighter BURST aimed at that conversation's repo building (p.latestRepo). The signpost cross-fades to
23
- // the real memory count (extracted). On any WebGL/data failure it stays transparent (SVG fallback shows).
24
- import * as THREE from 'three';
25
- import { RoundedBoxGeometry } from './vendor/RoundedBoxGeometry.js';
26
-
27
- const params = new URLSearchParams(location.search);
28
- const nonce = params.get('nonce') || '';
29
-
30
- const TOOL = { codex: 0x10a37f, claude: 0xd97757 };
31
- const TRAY_COL = 0xece5d3;
32
- const TPU = 80e6, BASE_EDGE = 36, TOWER = 1.4, MIN_TOKENS = 4e6, TRAY_TOP = 25;
33
- const LABEL_MIN_BASE = 24, MARGIN = 56, GAP = 16, MARGIN_FACTOR = 1.3;
34
- const fmtTok = (n) => n >= 1e9 ? (n / 1e9).toFixed(2) + 'B' : n >= 1e6 ? Math.round(n / 1e6) + 'M' : Math.round(n / 1e3) + 'K';
35
-
36
- const mount = document.getElementById('plate');
37
-
38
- (async function () {
39
- let report = null;
40
- try { report = await loadReport(); } catch (_) { return; }
41
- const repos = buildRepos(report);
42
- if (!repos.length) return; // nothing to draw → leave transparent, SVG fallback shows
43
- try { run(repos, (report && report.scale) || {}); } catch (e) { console.error('extraction-plate render failed', e); }
44
- })();
45
-
46
- async function loadReport() {
47
- if (!nonce) return null;
48
- for (let i = 0; i < 80; i++) {
49
- try {
50
- const res = await fetch('/report?nonce=' + encodeURIComponent(nonce), { credentials: 'omit', cache: 'no-store' });
51
- if (res.status === 200) return await res.json();
52
- if (res.status !== 202) return null;
53
- } catch (_) { return null; }
54
- await new Promise((r) => setTimeout(r, 500));
55
- }
56
- return null;
57
- }
58
-
59
- function buildRepos(report) {
60
- const source = Array.isArray(report && report.repos) ? report.repos : [];
61
- const usable = source.filter((r) => Number(r && r.tokens) > 0).sort((a, b) => (Number(b.tokens) || 0) - (Number(a.tokens) || 0)).slice(0, 16);
62
- return usable.map((repo, i) => {
63
- let x, z;
64
- if (i === 0) { x = -36; z = -58; }
65
- else if (i === 1) { x = 128; z = -12; }
66
- else { const a = i * 2.399963229728653, rad = 72 + Math.sqrt(i) * 42; x = Math.cos(a) * rad; z = Math.sin(a) * rad + 28; }
67
- const tool = (repo.dominantProvider === 'codex' || repo.dominantProvider === 'claude') ? repo.dominantProvider : 'claude';
68
- return { name: String(repo.name || ('Repo ' + (i + 1))), x, z, tool, tokens: Number(repo.tokens) || 0, pin: i < 2 };
69
- });
70
- }
71
-
72
- function run(repos, scale) {
73
- for (const o of repos) { const u = o.tokens / TPU; const b = Math.cbrt((u * BASE_EDGE * BASE_EDGE * BASE_EDGE) / TOWER); o.w = b; o.d = b; o.h = b * TOWER; }
74
- const all = repos.filter((o) => o.tokens >= MIN_TOKENS);
75
- separate(all);
76
- let minX = 1e9, maxX = -1e9, minZ = 1e9, maxZ = -1e9;
77
- for (const o of all) { minX = Math.min(minX, o.x - o.w / 2); maxX = Math.max(maxX, o.x + o.w / 2); minZ = Math.min(minZ, o.z - o.d / 2); maxZ = Math.max(maxZ, o.z + o.d / 2); }
78
- const CX = (minX + maxX) / 2, CZ = (minZ + maxZ) / 2;
79
- const traySize = Math.max(maxX - minX, maxZ - minZ) + 2 * MARGIN, trayW = traySize, trayD = traySize;
80
-
81
- const canvas = document.createElement('canvas');
82
- canvas.style.cssText = 'display:block;width:100%;height:100%;';
83
- mount.appendChild(canvas);
84
- const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
85
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
86
- renderer.setClearColor(0xffffff, 0);
87
- renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap;
88
- renderer.outputColorSpace = THREE.SRGBColorSpace;
89
- renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.05;
90
- const scene = new THREE.Scene();
91
-
92
- scene.environment = (() => {
93
- const c = document.createElement('canvas'); c.width = 512; c.height = 256;
94
- const g = c.getContext('2d');
95
- const gr = g.createLinearGradient(0, 0, 0, 256); gr.addColorStop(0, '#ffffff'); gr.addColorStop(0.5, '#eef1f6'); gr.addColorStop(1, '#d2d7e0');
96
- g.fillStyle = gr; g.fillRect(0, 0, 512, 256);
97
- const sp = g.createRadialGradient(150, 62, 8, 150, 62, 150); sp.addColorStop(0, 'rgba(255,255,255,0.95)'); sp.addColorStop(1, 'rgba(255,255,255,0)');
98
- g.fillStyle = sp; g.fillRect(0, 0, 512, 256);
99
- const t = new THREE.CanvasTexture(c); t.mapping = THREE.EquirectangularReflectionMapping; t.colorSpace = THREE.SRGBColorSpace;
100
- const pm = new THREE.PMREMGenerator(renderer); pm.compileEquirectangularShader();
101
- const e = pm.fromEquirectangular(t).texture; t.dispose(); pm.dispose(); return e;
102
- })();
103
-
104
- const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 1, 6000);
105
- const TARGET = new THREE.Vector3(CX, 78, CZ);
106
- camera.position.copy(TARGET).add(new THREE.Vector3(760, 660, 760)); camera.lookAt(TARGET);
107
-
108
- scene.add(new THREE.HemisphereLight(0xffffff, 0xdadce2, 0.7));
109
- scene.add(new THREE.AmbientLight(0xffffff, 0.2));
110
- const key = new THREE.DirectionalLight(0xffffff, 2.3);
111
- key.position.set(CX - 360, 560, CZ + 360); key.target.position.copy(TARGET);
112
- key.castShadow = true; key.shadow.mapSize.set(2048, 2048); key.shadow.radius = 16; key.shadow.bias = -0.0004;
113
- const sh = Math.max(trayW, trayD) * 0.8 + 140, scam = key.shadow.camera;
114
- scam.left = -sh; scam.right = sh; scam.top = sh; scam.bottom = -sh; scam.near = 100; scam.far = 1800;
115
- scene.add(key); scene.add(key.target);
116
- const floor = new THREE.Mesh(new THREE.PlaneGeometry(6000, 6000), new THREE.ShadowMaterial({ opacity: 0.22 }));
117
- floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor);
118
-
119
- const tray = new THREE.Mesh(new RoundedBoxGeometry(trayW, TRAY_TOP, trayD, 8, 12), new THREE.MeshPhysicalMaterial({ color: TRAY_COL, roughness: 0.6, clearcoat: 0.3, clearcoatRoughness: 0.5, envMapIntensity: 0.6 }));
120
- tray.position.set(CX, TRAY_TOP / 2, CZ); tray.castShadow = true; tray.receiveShadow = true; scene.add(tray);
121
- (() => {
122
- const scl = 3, W = Math.round(trayW * scl), H = Math.round(trayD * scl), cv = document.createElement('canvas'); cv.width = W; cv.height = H;
123
- const g = cv.getContext('2d'); const m = 14 * scl, rad = 26 * scl, rw = W - 2 * m, rh = H - 2 * m;
124
- g.save(); g.beginPath(); g.roundRect(m, m, rw, rh, rad); g.clip();
125
- const cell = 21 * scl; g.strokeStyle = 'rgba(74,80,96,0.42)'; g.lineWidth = 1.8; g.beginPath();
126
- for (let x = m; x <= m + rw + 0.5; x += cell) { g.moveTo(x, m); g.lineTo(x, m + rh); }
127
- for (let y = m; y <= m + rh + 0.5; y += cell) { g.moveTo(m, y); g.lineTo(m + rw, y); }
128
- g.stroke(); g.restore();
129
- g.strokeStyle = 'rgba(120,126,140,0.26)'; g.lineWidth = 0.7 * scl; g.beginPath(); g.roundRect(m, m, rw, rh, rad); g.stroke();
130
- const tex = new THREE.CanvasTexture(cv); tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 8;
131
- const pl = new THREE.Mesh(new THREE.PlaneGeometry(trayW, trayD), new THREE.MeshBasicMaterial({ map: tex, transparent: true, depthWrite: false, toneMapped: false }));
132
- pl.rotation.x = -Math.PI / 2; pl.position.set(CX, TRAY_TOP + 0.6, CZ); scene.add(pl);
133
- })();
134
-
135
- const clay = (hex) => { const c = new THREE.Color(hex); return new THREE.MeshPhysicalMaterial({ color: c, roughness: 0.42, transmission: 0.85, ior: 1.34, thickness: 70, attenuationColor: c.clone(), attenuationDistance: 95, clearcoat: 0.28, clearcoatRoughness: 0.45, envMapIntensity: 1.05, transparent: true }); };
136
- const mkLabel = (o) => {
137
- const PW = 320, PH = 280, cv = document.createElement('canvas'); cv.width = PW; cv.height = PH;
138
- const g = cv.getContext('2d'); g.textAlign = 'center'; g.shadowColor = 'rgba(0,0,0,0.34)'; g.shadowBlur = 9; g.shadowOffsetY = 1;
139
- g.fillStyle = '#ffffff'; g.font = '800 ' + Math.round(PW * 0.19) + 'px "IBM Plex Mono", monospace'; g.fillText(fmtTok(o.tokens), PW / 2, PH * 0.2);
140
- g.fillStyle = 'rgba(255,255,255,0.95)'; g.font = '600 ' + Math.round(PW * 0.078) + 'px "IBM Plex Mono", monospace';
141
- const fw = g.measureText(o.name).width, mw = PW * 0.94; g.save(); g.translate(PW / 2, PH * 0.86); if (fw > mw) g.scale(mw / fw, mw / fw); g.fillText(o.name, 0, 0); g.restore();
142
- const tex = new THREE.CanvasTexture(cv); tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 8;
143
- const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex, transparent: true, toneMapped: false, depthWrite: false, opacity: 0 }));
144
- const ww = Math.min(Math.max(o.w * 0.95, 42), 128), hw = (ww * PH) / PW; sp.scale.set(ww, hw, 1);
145
- return { sp, hw };
146
- };
147
-
148
- const items = []; let maxTop = 0;
149
- for (const o of all) {
150
- const r = Math.min(Math.min(o.w, o.h, o.d) * 0.12, 12);
151
- const mesh = new THREE.Mesh(new RoundedBoxGeometry(o.w, o.h, o.d, 8, r), clay(TOOL[o.tool]));
152
- mesh.castShadow = true; mesh.receiveShadow = true;
153
- mesh.position.set(o.x, TRAY_TOP + o.h / 2 - 3, o.z); // full height, fixed — extraction is additive, the city never shrinks
154
- scene.add(mesh);
155
- const L = (o.w >= LABEL_MIN_BASE) ? mkLabel(o) : null;
156
- if (L) { L.sp.position.set(o.x, TRAY_TOP + o.h - 3 + 6 + 0.36 * L.hw, o.z); L.sp.material.opacity = 1; scene.add(L.sp); }
157
- items.push({ o, mesh, L, base: new THREE.Color(TOOL[o.tool]), pop: 0, lit: 0 }); maxTop = Math.max(maxTop, TRAY_TOP + o.h);
158
- }
159
- const byName = {}; for (const it of items) byName[it.o.name] = it; // building lookup for aiming a burst at a repo
160
-
161
- // Signpost on a post: a "before" face (total tokens / input%) cross-fades into the "after" face
162
- // (memories created) as the city distills.
163
- const sgx = CX + trayW * 0.3, sgz = CZ + trayD * 0.3, SIGN_Y = TRAY_TOP + 70;
164
- const post = new THREE.Mesh(new THREE.CylinderGeometry(2.2, 2.2, 52, 12), new THREE.MeshStandardMaterial({ color: 0xddd9cc, roughness: 0.7 }));
165
- post.position.set(sgx, TRAY_TOP + 26, sgz); post.castShadow = true; scene.add(post);
166
- const mkSign = () => {
167
- const cv = document.createElement('canvas'); cv.width = 640; cv.height = 300;
168
- const tex = new THREE.CanvasTexture(cv); tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 8;
169
- const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex, transparent: true, toneMapped: false, depthTest: false }));
170
- sp.position.set(sgx, SIGN_Y, sgz); sp.scale.set(150, 70, 1); scene.add(sp);
171
- return { cv, tex, sp };
172
- };
173
- const cardBg = (g) => {
174
- g.clearRect(0, 0, 640, 300); g.textAlign = 'center';
175
- g.fillStyle = 'rgba(244,241,233,0.97)'; g.beginPath(); g.roundRect(8, 8, 624, 284, 30); g.fill();
176
- g.strokeStyle = 'rgba(120,126,140,0.30)'; g.lineWidth = 3; g.beginPath(); g.roundRect(8, 8, 624, 284, 30); g.stroke();
177
- };
178
- const tokSign = mkSign();
179
- (() => {
180
- const g = tokSign.cv.getContext('2d'); cardBg(g);
181
- g.fillStyle = '#1a1a1a'; g.font = '800 116px "IBM Plex Mono", monospace'; g.fillText(fmtTok(scale.totalTokens || 0), 320, 142);
182
- g.fillStyle = '#6b6b6b'; g.font = '600 34px "IBM Plex Mono", monospace'; g.fillText('TOTAL TOKENS', 320, 196);
183
- g.fillStyle = '#c7372f'; g.font = '800 52px "IBM Plex Mono", monospace'; g.fillText((scale.inputPct || 0) + '% INPUT', 320, 268);
184
- tokSign.tex.needsUpdate = true;
185
- })();
186
- const memSign = mkSign(); memSign.sp.material.opacity = 0;
187
- const drawMem = (mem) => {
188
- const g = memSign.cv.getContext('2d'); cardBg(g);
189
- g.fillStyle = '#1a3a8f'; g.font = '800 150px "IBM Plex Mono", monospace'; g.fillText(String(Math.round(mem)), 320, 168);
190
- g.fillStyle = '#6b6b6b'; g.font = '600 38px "IBM Plex Mono", monospace'; g.fillText('MEMORIES CREATED', 320, 232);
191
- memSign.tex.needsUpdate = true;
192
- };
193
- drawMem(0);
194
-
195
- // Memory motes: a continuous gentle stream rises off the city into the memory bank while a conversation
196
- // is being processed (so it is never frozen between completions), plus a brighter BURST aimed at a
197
- // repo's building each time a conversation completes. The signpost pulses as bursts land.
198
- const SG = new THREE.Vector3(sgx, SIGN_Y - 4, sgz), ARC = 52;
199
- const motes = [];
200
- for (let i = 0; i < 72; i++) {
201
- const m = new THREE.Mesh(new THREE.SphereGeometry(1, 10, 10), new THREE.MeshBasicMaterial({ color: 0x9fc4ff, transparent: true, opacity: 0, blending: THREE.AdditiveBlending, depthWrite: false, toneMapped: false }));
202
- m.visible = false; scene.add(m);
203
- motes.push({ m, life: 0, t: 0, dur: 1, sz: 3, p0: new THREE.Vector3(), tgt: new THREE.Vector3() });
204
- }
205
- let signPulse = 0;
206
- const launch = (it, big) => {
207
- if (!it) return;
208
- const f = motes.find((p) => p.life <= 0); if (!f) return;
209
- const o = it.o, h = o.h;
210
- f.p0.set(o.x + (Math.random() * 18 - 9), TRAY_TOP + h - 3 + 5, o.z + (Math.random() * 18 - 9));
211
- f.tgt.copy(SG).add(new THREE.Vector3(Math.random() * 46 - 23, Math.random() * 22 - 6, Math.random() * 46 - 23));
212
- f.life = 1; f.t = 0;
213
- f.dur = big ? (0.7 + Math.random() * 0.4) : (1.0 + Math.random() * 0.6);
214
- f.sz = big ? (3.4 + Math.random() * 2.8) : (1.7 + Math.random() * 1.9);
215
- f.m.material.color.setHex(big ? (Math.random() < 0.7 ? 0xbfd6ff : 0xffe6c2) : 0x9fc4ff);
216
- f.m.visible = true;
217
- };
218
- // pick a building weighted by token size, so the ambient flow + any fallback bursts favour bigger repos
219
- const pickWeighted = () => {
220
- let total = 0; for (const it of items) total += Math.sqrt(it.o.tokens || 1);
221
- let r = Math.random() * total;
222
- for (const it of items) { r -= Math.sqrt(it.o.tokens || 1); if (r <= 0) return it; }
223
- return items[items.length - 1];
224
- };
225
- const fireBurst = (it, n) => {
226
- if (it) { it.pop = 1; it.lit = Math.min(1, it.lit + 0.5); } // the building reacts (pop + flash) and keeps a "processed" glow
227
- for (let k = 0; k < n; k++) launch(it, true);
228
- signPulse = Math.max(signPulse, 1.9);
229
- };
230
-
231
- const corners = [];
232
- // frame to maxTop + headroom so the floating labels and the signpost (which fly above the cube tops)
233
- // are never clipped at the top edge.
234
- const frameTop = maxTop + 70;
235
- for (const sx of [CX - trayW / 2, CX + trayW / 2]) for (const sy of [0, frameTop]) for (const sz of [CZ - trayD / 2, CZ + trayD / 2]) corners.push(new THREE.Vector3(sx, sy, sz));
236
- const autoFrame = () => {
237
- const w = mount.clientWidth || 1, h = mount.clientHeight || 1; renderer.setSize(w, h, false);
238
- const aspect = w / h; camera.updateMatrixWorld();
239
- let a = 1e9, b = -1e9, c = 1e9, d = -1e9;
240
- for (const cc of corners) { const q = cc.clone().applyMatrix4(camera.matrixWorldInverse); a = Math.min(a, q.x); b = Math.max(b, q.x); c = Math.min(c, q.y); d = Math.max(d, q.y); }
241
- const mx = (a + b) / 2, my = (c + d) / 2, hw = ((b - a) / 2) * MARGIN_FACTOR, hh = ((d - c) / 2) * MARGIN_FACTOR;
242
- const Vh = Math.max(hh, hw / aspect), Vw = Vh * aspect;
243
- camera.left = mx - Vw; camera.right = mx + Vw; camera.top = my + Vh; camera.bottom = my - Vh; camera.updateProjectionMatrix();
244
- };
245
- autoFrame(); new ResizeObserver(autoFrame).observe(mount);
246
-
247
- let actAmt = 0, actTarget = 0, running = false, mem = 0, shownMem = 0, ambientAcc = 0, clock = 0, prevCompleted = 0;
248
- let last = performance.now();
249
- const loop = (now) => {
250
- requestAnimationFrame(loop);
251
- const dt = Math.min(0.05, (now - last) / 1000); last = now; clock += dt;
252
- if (Math.abs(actTarget - actAmt) > 0.001) actAmt += (actTarget - actAmt) * 0.06;
253
- // signpost cross-fade: total-tokens "before" → memories "after" once extraction is engaged
254
- tokSign.sp.material.opacity = Math.max(0, 1 - 1.6 * actAmt);
255
- memSign.sp.material.opacity = Math.max(0, (actAmt - 0.12) / 0.88);
256
- // per-building: a faint breathing shimmer while running, a POP (vertical stretch + bright flash) on
257
- // the building whose conversation just completed, and a lasting "processed" glow that accumulates.
258
- for (const it of items) {
259
- it.pop *= 0.88;
260
- const k = 1 + 0.2 * it.pop;
261
- it.mesh.scale.set(1, k, 1);
262
- it.mesh.position.y = TRAY_TOP + (it.o.h * k) / 2 - 3;
263
- if (it.L) it.L.sp.position.y = TRAY_TOP + it.o.h * k - 3 + 6 + 0.36 * it.L.hw;
264
- const breathe = running ? 0.05 * (0.5 + 0.5 * Math.sin(clock * 1.6 + it.o.x * 0.02)) : 0;
265
- it.mesh.material.emissiveIntensity = breathe + 0.8 * it.pop + 0.2 * it.lit;
266
- }
267
- if (Math.abs(mem - shownMem) > 0.4) { shownMem += (mem - shownMem) * 0.1; drawMem(shownMem); }
268
- // continuous ambient flow while a conversation is being processed
269
- if (running) { ambientAcc += dt; while (ambientAcc > 0.11) { ambientAcc -= 0.11; launch(pickWeighted(), false); } }
270
- for (const p of motes) {
271
- if (p.life <= 0) continue;
272
- p.t += dt / p.dur; const tt = p.t;
273
- if (tt >= 1) { p.life = 0; p.m.visible = false; signPulse = Math.max(signPulse, 0.5); continue; }
274
- const e = tt * tt * (3 - 2 * tt);
275
- p.m.position.lerpVectors(p.p0, p.tgt, e); p.m.position.y += Math.sin(Math.PI * tt) * ARC;
276
- const op = tt < 0.14 ? tt / 0.14 : (tt > 0.8 ? (1 - tt) / 0.2 : 1); p.m.material.opacity = op * 0.95;
277
- const s = p.sz * (0.55 + 0.6 * Math.sin(Math.PI * Math.min(1, tt * 1.05))); p.m.scale.set(s, s, s);
278
- }
279
- if (signPulse > 0.002) { const k = 1 + 0.06 * signPulse; memSign.sp.scale.set(150 * k, 70 * k, 1); signPulse *= 0.9; }
280
- else memSign.sp.scale.set(150, 70, 1);
281
- renderer.render(scene, camera);
282
- };
283
- requestAnimationFrame((t) => { last = t; loop(t); });
284
-
285
- // Drive the animation from real extraction progress: one burst per completed conversation, aimed at that
286
- // conversation's repo building (p.latestRepo); the count is the real memory total (extracted). The city
287
- // stays standing. Idle/pre-extract → the full city + the total-tokens signpost.
288
- (async function pollProgress() {
289
- if (!nonce) return;
290
- for (;;) {
291
- try {
292
- const res = await fetch('/progress?nonce=' + encodeURIComponent(nonce), { credentials: 'omit' });
293
- if (res.ok) {
294
- const p = await res.json(); const status = p.status || 'idle';
295
- const completed = p.completed || 0;
296
- actTarget = (status === 'starting' || status === 'preparing' || status === 'running' || status === 'completed' || status === 'failed') ? 1 : 0;
297
- running = status === 'running';
298
- mem = p.extracted || 0;
299
- if (status === 'idle') prevCompleted = 0;
300
- if (completed > prevCompleted) {
301
- const delta = completed - prevCompleted; prevCompleted = completed;
302
- const src = (p.latestRepo && byName[p.latestRepo]) ? byName[p.latestRepo] : null;
303
- for (let k = 0; k < delta; k++) fireBurst(src || pickWeighted(), 18);
304
- }
305
- }
306
- } catch (_) { /* keep last */ }
307
- await new Promise((r) => setTimeout(r, 1000));
308
- }
309
- })();
310
- }
311
-
312
- function separate(items, iters = 400) {
313
- for (let k = 0; k < iters; k++) {
314
- let moved = false;
315
- for (let i = 0; i < items.length; i++) for (let j = i + 1; j < items.length; j++) {
316
- const a = items[i], b = items[j];
317
- const hx = (a.w + b.w) / 2 + GAP, hz = (a.d + b.d) / 2 + GAP, dx = b.x - a.x, dz = b.z - a.z;
318
- const ox = hx - Math.abs(dx), oz = hz - Math.abs(dz);
319
- if (ox > 0 && oz > 0) {
320
- moved = true;
321
- if (ox < oz) { const s = dx < 0 ? -1 : 1; if (a.pin && b.pin) {} else if (a.pin) b.x += ox * s; else if (b.pin) a.x -= ox * s; else { a.x -= ox * 0.5 * s; b.x += ox * 0.5 * s; } }
322
- else { const s = dz < 0 ? -1 : 1; if (a.pin && b.pin) {} else if (a.pin) b.z += oz * s; else if (b.pin) a.z -= oz * s; else { a.z -= oz * 0.5 * s; b.z += oz * 0.5 * s; } }
323
- }
324
- }
325
- if (!moved) break;
326
- }
327
- }
328
- </script>
329
- </body>
330
- </html>
Binary file