aura-code 0.3.7 → 0.6.3

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 (160) hide show
  1. package/README.md +56 -4
  2. package/dist/agent/compactor.d.ts +26 -0
  3. package/dist/agent/compactor.js +156 -0
  4. package/dist/agent/compactor.js.map +1 -0
  5. package/dist/agent/context.d.ts +4 -0
  6. package/dist/agent/context.js +38 -0
  7. package/dist/agent/context.js.map +1 -1
  8. package/dist/agent/design.d.ts +17 -0
  9. package/dist/agent/design.js +175 -0
  10. package/dist/agent/design.js.map +1 -0
  11. package/dist/agent/loop.js +24 -1
  12. package/dist/agent/loop.js.map +1 -1
  13. package/dist/agent/system-prompt.d.ts +1 -1
  14. package/dist/agent/system-prompt.js +14 -3
  15. package/dist/agent/system-prompt.js.map +1 -1
  16. package/dist/cli/display.js +46 -5
  17. package/dist/cli/display.js.map +1 -1
  18. package/dist/cli/index.js +323 -46
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/config/defaults.js +8 -1
  21. package/dist/config/defaults.js.map +1 -1
  22. package/dist/config/project-config.d.ts +6 -0
  23. package/dist/config/project-config.js +2 -0
  24. package/dist/config/project-config.js.map +1 -1
  25. package/dist/dream/dream.d.ts +71 -0
  26. package/dist/dream/dream.js +217 -0
  27. package/dist/dream/dream.js.map +1 -0
  28. package/dist/dream/okf.d.ts +10 -0
  29. package/dist/dream/okf.js +280 -0
  30. package/dist/dream/okf.js.map +1 -0
  31. package/dist/dream/parser.d.ts +60 -0
  32. package/dist/dream/parser.js +176 -0
  33. package/dist/dream/parser.js.map +1 -0
  34. package/dist/dream/reconcile.d.ts +69 -0
  35. package/dist/dream/reconcile.js +303 -0
  36. package/dist/dream/reconcile.js.map +1 -0
  37. package/dist/kanban/index.d.ts +5 -0
  38. package/dist/kanban/index.js +11 -0
  39. package/dist/kanban/index.js.map +1 -0
  40. package/dist/kanban/pipeline.d.ts +16 -0
  41. package/dist/kanban/pipeline.js +910 -0
  42. package/dist/kanban/pipeline.js.map +1 -0
  43. package/dist/kanban/server.d.ts +5 -0
  44. package/dist/kanban/server.js +820 -0
  45. package/dist/kanban/server.js.map +1 -0
  46. package/dist/kanban/types.d.ts +75 -0
  47. package/dist/kanban/types.js +6 -0
  48. package/dist/kanban/types.js.map +1 -0
  49. package/dist/learnlight/driven.d.ts +2 -0
  50. package/dist/learnlight/driven.js +109 -0
  51. package/dist/learnlight/driven.js.map +1 -0
  52. package/dist/learnlight/index.d.ts +4 -0
  53. package/dist/learnlight/index.js +32 -0
  54. package/dist/learnlight/index.js.map +1 -0
  55. package/dist/learnlight/lesson-prep.d.ts +36 -0
  56. package/dist/learnlight/lesson-prep.js +224 -0
  57. package/dist/learnlight/lesson-prep.js.map +1 -0
  58. package/dist/learnlight/report.d.ts +2 -0
  59. package/dist/learnlight/report.js +37 -0
  60. package/dist/learnlight/report.js.map +1 -0
  61. package/dist/learnlight/types.d.ts +72 -0
  62. package/dist/learnlight/types.js +6 -0
  63. package/dist/learnlight/types.js.map +1 -0
  64. package/dist/machina/index.d.ts +16 -0
  65. package/dist/machina/index.js +61 -0
  66. package/dist/machina/index.js.map +1 -0
  67. package/dist/machina/render-html.d.ts +2 -0
  68. package/dist/machina/render-html.js +189 -0
  69. package/dist/machina/render-html.js.map +1 -0
  70. package/dist/machina/render-terminal.d.ts +2 -0
  71. package/dist/machina/render-terminal.js +92 -0
  72. package/dist/machina/render-terminal.js.map +1 -0
  73. package/dist/machina/spec.d.ts +61 -0
  74. package/dist/machina/spec.js +124 -0
  75. package/dist/machina/spec.js.map +1 -0
  76. package/dist/machina/verify.d.ts +27 -0
  77. package/dist/machina/verify.js +85 -0
  78. package/dist/machina/verify.js.map +1 -0
  79. package/dist/perception/graph-store.d.ts +11 -0
  80. package/dist/perception/graph-store.js +33 -0
  81. package/dist/perception/graph-store.js.map +1 -1
  82. package/dist/perception/index.d.ts +1 -1
  83. package/dist/perception/index.js +2 -1
  84. package/dist/perception/index.js.map +1 -1
  85. package/dist/providers/anthropic.js +1 -1
  86. package/dist/providers/anthropic.js.map +1 -1
  87. package/dist/providers/factory.d.ts +49 -0
  88. package/dist/providers/factory.js +99 -0
  89. package/dist/providers/factory.js.map +1 -1
  90. package/dist/providers/google.js +1 -1
  91. package/dist/providers/google.js.map +1 -1
  92. package/dist/providers/openai-compatible.js +81 -50
  93. package/dist/providers/openai-compatible.js.map +1 -1
  94. package/dist/rem/graph.d.ts +72 -0
  95. package/dist/rem/graph.js +139 -0
  96. package/dist/rem/graph.js.map +1 -0
  97. package/dist/rem/index.d.ts +16 -0
  98. package/dist/rem/index.js +60 -0
  99. package/dist/rem/index.js.map +1 -0
  100. package/dist/rem/render-html.d.ts +2 -0
  101. package/dist/rem/render-html.js +189 -0
  102. package/dist/rem/render-html.js.map +1 -0
  103. package/dist/rem/render-terminal.d.ts +4 -0
  104. package/dist/rem/render-terminal.js +94 -0
  105. package/dist/rem/render-terminal.js.map +1 -0
  106. package/dist/research/council.d.ts +27 -0
  107. package/dist/research/council.js +263 -0
  108. package/dist/research/council.js.map +1 -0
  109. package/dist/research/research.d.ts +24 -0
  110. package/dist/research/research.js +116 -0
  111. package/dist/research/research.js.map +1 -0
  112. package/dist/ruby/alternator.d.ts +36 -1
  113. package/dist/ruby/alternator.js +40 -31
  114. package/dist/ruby/alternator.js.map +1 -1
  115. package/dist/ruby/stats.js +10 -11
  116. package/dist/ruby/stats.js.map +1 -1
  117. package/dist/ruby/types.js +1 -1
  118. package/dist/ruby/types.js.map +1 -1
  119. package/dist/safety/permissions.d.ts +8 -0
  120. package/dist/safety/permissions.js +12 -0
  121. package/dist/safety/permissions.js.map +1 -1
  122. package/dist/setup/provider-registry.js +14 -0
  123. package/dist/setup/provider-registry.js.map +1 -1
  124. package/dist/setup/provider-test.js +24 -2
  125. package/dist/setup/provider-test.js.map +1 -1
  126. package/dist/setup/telegram-wizard.d.ts +57 -0
  127. package/dist/setup/telegram-wizard.js +343 -0
  128. package/dist/setup/telegram-wizard.js.map +1 -0
  129. package/dist/tools/audio-transcribe.d.ts +20 -0
  130. package/dist/tools/audio-transcribe.js +39 -28
  131. package/dist/tools/audio-transcribe.js.map +1 -1
  132. package/dist/tools/browser.js +8 -10
  133. package/dist/tools/browser.js.map +1 -1
  134. package/dist/tools/gmail-tool.d.ts +15 -0
  135. package/dist/tools/gmail-tool.js +361 -0
  136. package/dist/tools/gmail-tool.js.map +1 -0
  137. package/dist/tools/read-file.js +13 -0
  138. package/dist/tools/read-file.js.map +1 -1
  139. package/dist/tools/telegram-bot.js +103 -20
  140. package/dist/tools/telegram-bot.js.map +1 -1
  141. package/dist/tools/telegram-voice.d.ts +22 -0
  142. package/dist/tools/telegram-voice.js +203 -0
  143. package/dist/tools/telegram-voice.js.map +1 -0
  144. package/dist/tools/telegram.d.ts +6 -1
  145. package/dist/tools/telegram.js +115 -5
  146. package/dist/tools/telegram.js.map +1 -1
  147. package/dist/tools/video-render.d.ts +11 -0
  148. package/dist/tools/video-render.js +189 -0
  149. package/dist/tools/video-render.js.map +1 -0
  150. package/dist/tools/web-search.d.ts +20 -0
  151. package/dist/tools/web-search.js +134 -38
  152. package/dist/tools/web-search.js.map +1 -1
  153. package/dist/util/errors.js +6 -0
  154. package/dist/util/errors.js.map +1 -1
  155. package/dist/viz/index.js +816 -30
  156. package/dist/viz/index.js.map +1 -1
  157. package/dist/viz/reader.d.ts +33 -0
  158. package/dist/viz/reader.js +343 -0
  159. package/dist/viz/reader.js.map +1 -0
  160. package/package.json +3 -1
package/dist/viz/index.js CHANGED
@@ -151,23 +151,144 @@ function stripPlan(p) {
151
151
  })),
152
152
  };
153
153
  }
154
- function loadMemory(projectRoot) {
155
- const base = path.join(process.env.HOME ?? '/tmp', '.aura', 'memory');
156
- const safe = projectRoot.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80);
157
- const dir = path.join(base, safe);
154
+ const episode_capture_js_1 = require("../ruby/episode-capture.js");
155
+ const competence_js_1 = require("../ruby/competence.js");
156
+ /**
157
+ * Reads the REAL memory store: `~/.aura/memory/{namespace}.json`, one flat
158
+ * file per namespace, each `{ [key]: { value, updated, created? } }`.
159
+ *
160
+ * This memory store is global, not project-scoped — the `memory` tool never
161
+ * keyed entries by project root. (The previous version of this function read
162
+ * from a per-project-hash subdirectory that nothing in the codebase ever
163
+ * wrote to, so this panel has always returned an empty list.)
164
+ */
165
+ function loadMemory() {
166
+ const dir = path.join(process.env.HOME ?? '/tmp', '.aura', 'memory');
158
167
  if (!fs.existsSync(dir))
159
168
  return [];
160
- return fs.readdirSync(dir)
161
- .filter(f => f.endsWith('.json') && !f.endsWith('.tmp'))
162
- .map(f => {
169
+ const facts = [];
170
+ for (const file of fs.readdirSync(dir)) {
171
+ if (!file.endsWith('.json') || file.endsWith('.tmp'))
172
+ continue;
173
+ const namespace = file.replace(/\.json$/, '');
163
174
  try {
164
- return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
175
+ const store = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
176
+ for (const [key, entry] of Object.entries(store)) {
177
+ if (!entry || typeof entry.value !== 'string')
178
+ continue;
179
+ facts.push({
180
+ key,
181
+ namespace,
182
+ value: entry.value,
183
+ updated: entry.updated,
184
+ // Entries written before `created` existed fall back to `updated`
185
+ // — the earliest timestamp we actually have for them.
186
+ created: entry.created ?? entry.updated,
187
+ });
188
+ }
165
189
  }
166
- catch {
167
- return null;
190
+ catch { /* skip an unreadable/corrupt namespace file */ }
191
+ }
192
+ return facts.sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
193
+ }
194
+ /**
195
+ * Synchronous episode reader. Mirrors `episodeStore.loadEpisodes()` exactly
196
+ * (same directory via the store's own `projectDir()`, same "never throws,
197
+ * skip bad files" behaviour) but stays sync — `generateDashboard` must
198
+ * remain sync, since existing tests call it without `await`.
199
+ */
200
+ function loadEpisodesSync(projectRoot) {
201
+ try {
202
+ const dir = episode_capture_js_1.episodeStore.projectDir(projectRoot);
203
+ if (!fs.existsSync(dir))
204
+ return [];
205
+ const episodes = [];
206
+ for (const file of fs.readdirSync(dir)) {
207
+ if (!file.endsWith('.json'))
208
+ continue;
209
+ try {
210
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
211
+ if (parsed && typeof parsed.timestamp === 'number')
212
+ episodes.push(parsed);
213
+ }
214
+ catch { /* skip a corrupt episode file */ }
168
215
  }
169
- })
170
- .filter((m) => m !== null);
216
+ return episodes.sort((a, b) => a.timestamp - b.timestamp);
217
+ }
218
+ catch {
219
+ return [];
220
+ }
221
+ }
222
+ /** Day-bucketed cumulative growth of remembered facts, by first-created date. */
223
+ function buildMemoryGrowth(facts) {
224
+ const byDay = new Map();
225
+ for (const f of facts) {
226
+ const day = (f.created || '').slice(0, 10);
227
+ if (!day)
228
+ continue;
229
+ byDay.set(day, (byDay.get(day) ?? 0) + 1);
230
+ }
231
+ const days = Array.from(byDay.keys()).sort();
232
+ let running = 0;
233
+ return days.map(date => {
234
+ running += byDay.get(date);
235
+ return { date, cumulative: running };
236
+ });
237
+ }
238
+ /**
239
+ * Day-bucketed learning curve: cumulative episode count, plus a rolling
240
+ * Ruby (small-model) success rate over the last `windowSize` attempted
241
+ * episodes up to and including each day. This is the actual "is it getting
242
+ * better at routing to the small model" signal — competence.ts itself
243
+ * doesn't persist a timeline, it recomputes fresh from episodes each call,
244
+ * so this reconstructs the trend from each episode's own timestamp.
245
+ */
246
+ function buildLearningSeries(episodes, windowSize = 10) {
247
+ const byDay = new Map();
248
+ for (const ep of episodes) {
249
+ const day = new Date(ep.timestamp).toISOString().slice(0, 10);
250
+ const arr = byDay.get(day) ?? [];
251
+ arr.push(ep);
252
+ byDay.set(day, arr);
253
+ }
254
+ const days = Array.from(byDay.keys()).sort();
255
+ let cumulative = 0;
256
+ const seenSoFar = [];
257
+ return days.map(date => {
258
+ const todays = byDay.get(date);
259
+ cumulative += todays.length;
260
+ seenSoFar.push(...todays);
261
+ const attempted = seenSoFar.filter(e => e.rubyAttempted);
262
+ const recent = attempted.slice(-windowSize);
263
+ const rollingSuccessRate = recent.length === 0
264
+ ? 0
265
+ : recent.filter(e => e.rubySucceeded).length / recent.length;
266
+ return { date, cumulativeEpisodes: cumulative, rollingSuccessRate };
267
+ });
268
+ }
269
+ /** Plain-data summary mirroring formatStats()'s definitions, for stat cards. */
270
+ function summariseEpisodes(episodes) {
271
+ const total = episodes.length;
272
+ const completed = episodes.filter(e => e.reviewerApproved).length;
273
+ const avgDurationMs = total === 0
274
+ ? 0
275
+ : episodes.reduce((s, e) => s + (e.durationMs ?? 0), 0) / total;
276
+ const totalTokens = episodes.reduce((s, e) => s + (e.tokensUsed?.ruby ?? 0) + (e.tokensUsed?.largeModel ?? 0), 0);
277
+ const rubyAttempted = episodes.filter(e => e.rubyAttempted);
278
+ const rubySuccessRate = rubyAttempted.length === 0
279
+ ? 0
280
+ : rubyAttempted.filter(e => e.rubySucceeded).length / rubyAttempted.length;
281
+ const modelCounts = new Map();
282
+ for (const e of episodes) {
283
+ const model = e.largeModelUsed ?? (e.rubySucceeded ? 'ruby (small model)' : undefined);
284
+ if (model)
285
+ modelCounts.set(model, (modelCounts.get(model) ?? 0) + 1);
286
+ }
287
+ const topModels = Array.from(modelCounts.entries())
288
+ .sort((a, b) => b[1] - a[1])
289
+ .slice(0, 6)
290
+ .map(([model, count]) => ({ model, count }));
291
+ return { total, completed, avgDurationMs, totalTokens, rubySuccessRate, topModels };
171
292
  }
172
293
  // ─────────────────────────────────────────────────────────────────────────────
173
294
  // HTML template
@@ -182,6 +303,8 @@ function buildHtml(data) {
182
303
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
183
304
  <title>Aura — Memory Dashboard · ${data.projectName}</title>
184
305
  <script src="https://d3js.org/d3.v7.min.js"></script>
306
+ <script src="https://unpkg.com/three@0.147.0/build/three.min.js"></script>
307
+ <script src="https://unpkg.com/three@0.147.0/examples/js/controls/OrbitControls.js"></script>
185
308
  <style>
186
309
  :root {
187
310
  --bg: #0d1117;
@@ -221,7 +344,11 @@ function buildHtml(data) {
221
344
  /* Graph panel */
222
345
  #graph-svg { background: var(--canvas); border: 1px solid var(--border); border-radius: 8px; flex: 1; min-height: 0; cursor: grab; }
223
346
  #graph-svg:active { cursor: grabbing; }
347
+ #graph-3d-wrap { background: var(--canvas); border: 1px solid var(--border); border-radius: 8px; flex: 1; min-height: 0; position: relative; overflow: hidden; display: none; }
348
+ #graph-3d-wrap canvas { display: block; width: 100%; height: 100%; }
349
+ .graph-3d-hint { position: absolute; bottom: 10px; right: 14px; color: var(--dim); font-size: 10px; pointer-events: none; z-index: 2; }
224
350
  .graph-controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
351
+ .layout-toggle { display: flex; gap: 4px; }
225
352
  .graph-controls input {
226
353
  background: var(--card); border: 1px solid var(--border2); border-radius: 6px;
227
354
  color: var(--text); font: inherit; font-size: 12px; padding: 6px 12px; width: 220px; outline: none;
@@ -288,6 +415,29 @@ function buildHtml(data) {
288
415
  .memory-table td:first-child { color: var(--primary); white-space: nowrap; font-weight: 600; }
289
416
  .memory-table tr:hover td { background: var(--card); }
290
417
  .memory-val { max-width: 560px; white-space: pre-wrap; word-break: break-word; color: var(--text); }
418
+ .ns-badge { background: var(--canvas); border: 1px solid var(--border2); border-radius: 10px; color: var(--blue); font-size: 10px; padding: 1px 8px; white-space: nowrap; }
419
+
420
+ /* Line charts (memory growth, learning curve) */
421
+ .chart-card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px 18px; }
422
+ .chart-card .lbl { color: var(--muted); font-size: 10px; margin-bottom: 10px; text-transform: uppercase; letter-spacing: .08em; }
423
+ .chart-svg { width: 100%; height: 220px; overflow: visible; }
424
+ .chart-line { fill: none; stroke-width: 2; }
425
+ .chart-area { opacity: .12; }
426
+ .chart-dot { stroke: var(--bg); stroke-width: 1.5; }
427
+ .chart-axis text { fill: var(--dim); font-size: 9px; font-family: inherit; }
428
+ .chart-axis path, .chart-axis line { stroke: var(--border); }
429
+ .chart-empty { align-items: center; color: var(--dim); display: flex; font-size: 11px; height: 220px; justify-content: center; }
430
+
431
+ /* 3D bar charts (true.js cylinders) + their HTML legends */
432
+ .chart-3d-container { height: 260px; position: relative; width: 100%; }
433
+ .chart-3d-container canvas { display: block; height: 100%; width: 100%; }
434
+ .legend-3d { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-top: 10px; }
435
+ .legend-3d-row { align-items: center; display: flex; gap: 7px; }
436
+ .legend-3d-dot { border-radius: 50%; flex-shrink: 0; height: 10px; width: 10px; }
437
+ .legend-3d-name { color: var(--text); font-size: 11px; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
438
+ .legend-3d-val { color: var(--muted); font-size: 10px; }
439
+ .two-col { display: grid; gap: 14px; grid-template-columns: 1fr 1fr; }
440
+ @media (max-width: 900px) { .two-col { grid-template-columns: 1fr; } }
291
441
 
292
442
  .empty { color: var(--dim); font-size: 12px; padding: 32px; text-align: center; }
293
443
  ::-webkit-scrollbar { width: 5px; height: 5px; }
@@ -305,7 +455,8 @@ function buildHtml(data) {
305
455
  <button onclick="showPanel('graph',this)">Codebase Graph</button>
306
456
  <button onclick="showPanel('sessions',this)">Sessions</button>
307
457
  <button onclick="showPanel('plans',this)">Execution Plans</button>
308
- <button onclick="showPanel('memory',this)">Agent Memory</button>
458
+ <button onclick="showPanel('memory',this)">Memory Growth</button>
459
+ <button onclick="showPanel('learning',this)">Learning</button>
309
460
  </nav>
310
461
 
311
462
  <div id="overview" class="panel active"></div>
@@ -313,6 +464,7 @@ function buildHtml(data) {
313
464
  <div id="sessions" class="panel"></div>
314
465
  <div id="plans" class="panel"></div>
315
466
  <div id="memory" class="panel"></div>
467
+ <div id="learning" class="panel"></div>
316
468
 
317
469
  <div class="tooltip" id="tooltip" style="display:none"></div>
318
470
 
@@ -326,6 +478,8 @@ function showPanel(id, btn) {
326
478
  if (btn) btn.classList.add('active');
327
479
  if (id === 'graph' && !graphInit) initGraph();
328
480
  if (id === 'plans' && !plansInit) initPlans();
481
+ if (id === 'memory' && !memoryInit) initMemoryCharts();
482
+ if (id === 'learning' && !learningInit) initLearningCharts();
329
483
  }
330
484
 
331
485
  // ── Overview ─────────────────────────────────────────────────────────────────
@@ -334,6 +488,7 @@ function showPanel(id, btn) {
334
488
  const nc = g ? g.nodes.length : 0, ec = g ? g.edges.length : 0;
335
489
  const sc = DATA.sessions.length, pc = DATA.plans.length;
336
490
  const mc = DATA.memory.length, dc = DATA.plans.filter(p=>p.status==='done').length;
491
+ const epc = DATA.episodeSummary.total;
337
492
  const last = sc ? new Date(DATA.sessions[0].updatedAt).toLocaleString()
338
493
  : pc ? new Date(DATA.plans[0].created).toLocaleString() : '—';
339
494
 
@@ -354,6 +509,7 @@ function showPanel(id, btn) {
354
509
  <div class="stat-card"><div class="num">\${pc}</div><div class="lbl">Exec Plans</div></div>
355
510
  <div class="stat-card"><div class="num">\${dc}</div><div class="lbl">Plans Done</div></div>
356
511
  <div class="stat-card"><div class="num">\${mc}</div><div class="lbl">Memory Entries</div></div>
512
+ <div class="stat-card"><div class="num">\${epc}</div><div class="lbl">Episodes Recorded</div></div>
357
513
  </div>
358
514
  <div class="stat-card" style="max-width:640px">
359
515
  <div class="lbl" style="margin-bottom:10px">Codebase Breakdown</div>
@@ -384,9 +540,12 @@ function initGraph() {
384
540
  const: '#ffa657',
385
541
  type: '#79c0ff',
386
542
  enum: '#f85149',
543
+ concept: '#e3b341',
544
+ decision: '#bc8cff',
545
+ constraint: '#56d4dd',
387
546
  node: '#8b949e',
388
547
  };
389
- const NODE_R = { file: 13, class: 11, interface: 10, function: 8, const: 7, type: 7, enum: 8, node: 7 };
548
+ const NODE_R = { file: 13, class: 11, interface: 10, function: 8, const: 7, type: 7, enum: 8, concept: 9, decision: 9, constraint: 9, node: 7 };
390
549
 
391
550
  const allTypes = [...new Set(DATA.graph.nodes.map(n => n.type || 'node'))];
392
551
  const activeTypes = new Set(allTypes);
@@ -394,10 +553,17 @@ function initGraph() {
394
553
  panel.innerHTML = \`
395
554
  <div class="graph-controls">
396
555
  <input id="graph-search" placeholder="🔍 Search nodes, files…" oninput="filterGraph()">
556
+ <div class="layout-toggle" id="layout-toggle">
557
+ <span class="legend-item on" id="layout-force">Force</span>
558
+ <span class="legend-item" id="layout-radial">Radial</span>
559
+ <span class="legend-item" id="layout-3d-spread">3D Spread</span>
560
+ <span class="legend-item" id="layout-3d-explore">3D Explore</span>
561
+ </div>
397
562
  <div class="legend" id="legend"></div>
398
- <span class="hint">scroll to zoom · drag to pan · drag nodes</span>
563
+ <span class="hint" id="graph-hint">scroll to zoom · drag to pan · drag nodes</span>
399
564
  </div>
400
565
  <svg id="graph-svg"></svg>
566
+ <div id="graph-3d-wrap"><div class="graph-3d-hint" id="graph-3d-hint"></div></div>
401
567
  \`;
402
568
 
403
569
  const legendEl = document.getElementById('legend');
@@ -461,6 +627,24 @@ function initGraph() {
461
627
  .force('center', d3.forceCenter(W/2, H/2))
462
628
  .force('collision', d3.forceCollide(d => (NODE_R[d.type||'node']||7) + 6));
463
629
 
630
+ // Ring distance per node type — files innermost (the structural anchors),
631
+ // then class/interface, then everything else outward. Same key set as
632
+ // NODE_COLORS/NODE_R above, so every type already has a sensible color
633
+ // and radius gets a sensible ring too.
634
+ const RADIAL_BY_TYPE = { file: 70, class: 170, interface: 170, concept: 220, decision: 220, constraint: 220, const: 260, type: 260, enum: 260, function: 320, node: 320 };
635
+ const radiusFor = d => RADIAL_BY_TYPE[d.type || 'node'] ?? 320;
636
+
637
+ function applyLayout(mode) {
638
+ if (mode === 'radial') {
639
+ sim.force('center', null);
640
+ sim.force('radial', d3.forceRadial(radiusFor, W / 2, H / 2).strength(0.9));
641
+ } else {
642
+ sim.force('radial', null);
643
+ sim.force('center', d3.forceCenter(W / 2, H / 2));
644
+ }
645
+ sim.alpha(1).restart();
646
+ }
647
+
464
648
  const gLinks = g.append('g').selectAll('line').data(edges).enter().append('line')
465
649
  .attr('stroke','#484f58').attr('stroke-width', d => {
466
650
  const r = d.relation || '';
@@ -506,6 +690,321 @@ function initGraph() {
506
690
  gNodes.attr('cx',d=>d.x).attr('cy',d=>d.y);
507
691
  gLabels.attr('x',d=>d.x).attr('y',d=>d.y);
508
692
  });
693
+
694
+ // ── 3D mode state ──────────────────────────────────────────────────────────
695
+ let three3dCleanup = null; // function to stop the 3D render loop
696
+ let current3dMode = null; // 'spread' | 'explore' | null
697
+
698
+ function destroy3d() {
699
+ if (three3dCleanup) { three3dCleanup(); three3dCleanup = null; }
700
+ current3dMode = null;
701
+ const wrap = document.getElementById('graph-3d-wrap');
702
+ // Remove old canvas but keep the hint div
703
+ const oldCanvas = wrap.querySelector('canvas');
704
+ if (oldCanvas) oldCanvas.remove();
705
+ }
706
+
707
+ function show2d() {
708
+ destroy3d();
709
+ document.getElementById('graph-svg').style.display = '';
710
+ document.getElementById('graph-3d-wrap').style.display = 'none';
711
+ document.getElementById('graph-hint').textContent = 'scroll to zoom · drag to pan · drag nodes';
712
+ }
713
+
714
+ function show3d(mode) {
715
+ destroy3d();
716
+ current3dMode = mode;
717
+ document.getElementById('graph-svg').style.display = 'none';
718
+ const wrap = document.getElementById('graph-3d-wrap');
719
+ wrap.style.display = 'block';
720
+ const hintEl = document.getElementById('graph-3d-hint');
721
+ if (mode === 'spread') {
722
+ document.getElementById('graph-hint').textContent = 'auto-rotating 3D layout · scroll to zoom';
723
+ hintEl.textContent = '✦ auto-rotate';
724
+ } else {
725
+ document.getElementById('graph-hint').textContent = 'drag to rotate · scroll to zoom · right-drag to pan';
726
+ hintEl.textContent = '✦ orbit controls';
727
+ }
728
+ init3dGraph(wrap, mode);
729
+ }
730
+
731
+ function init3dGraph(container, mode) {
732
+ const W = container.clientWidth || 900;
733
+ const H = container.clientHeight || 580;
734
+
735
+ // ── Scene setup ──────────────────────────────────────────────────
736
+ const scene = new THREE.Scene();
737
+ scene.background = new THREE.Color(0x1c2128);
738
+ scene.fog = new THREE.FogExp2(0x1c2128, 0.0008);
739
+
740
+ const camera = new THREE.PerspectiveCamera(55, W / H, 1, 8000);
741
+ camera.position.set(0, 0, 600);
742
+
743
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
744
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
745
+ renderer.setSize(W, H);
746
+ container.appendChild(renderer.domElement);
747
+
748
+ // ── OrbitControls (explore mode) or auto-rotate (spread mode) ─
749
+ let controls = null;
750
+ if (mode === 'explore' && typeof THREE.OrbitControls !== 'undefined') {
751
+ controls = new THREE.OrbitControls(camera, renderer.domElement);
752
+ controls.enableDamping = true;
753
+ controls.dampingFactor = 0.08;
754
+ controls.rotateSpeed = 0.6;
755
+ controls.zoomSpeed = 1.2;
756
+ controls.minDistance = 80;
757
+ controls.maxDistance = 3000;
758
+ }
759
+
760
+ // ── Zoom via scroll (spread mode — no orbit controls) ────────
761
+ let spreadRadius = 600;
762
+ if (mode === 'spread') {
763
+ renderer.domElement.addEventListener('wheel', e => {
764
+ e.preventDefault();
765
+ spreadRadius = Math.max(120, Math.min(3000, spreadRadius + e.deltaY * 0.5));
766
+ }, { passive: false });
767
+ }
768
+
769
+ // ── Node data with 3D positions ──────────────────────────────
770
+ const n3 = DATA.graph.nodes.map((n, i) => {
771
+ // Initial sphere around origin, randomised
772
+ const phi = Math.acos(2 * Math.random() - 1);
773
+ const theta = Math.random() * Math.PI * 2;
774
+ const r = 100 + Math.random() * 250;
775
+ return {
776
+ ...n,
777
+ x: r * Math.sin(phi) * Math.cos(theta),
778
+ y: r * Math.sin(phi) * Math.sin(theta),
779
+ z: r * Math.cos(phi),
780
+ vx: 0, vy: 0, vz: 0,
781
+ };
782
+ });
783
+ const idMap = new Map(n3.map((n, i) => [n.id, i]));
784
+ const e3 = DATA.graph.edges.map(e => ({
785
+ si: idMap.get(typeof e.source === 'object' ? e.source.id : e.source) ?? -1,
786
+ ti: idMap.get(typeof e.target === 'object' ? e.target.id : e.target) ?? -1,
787
+ relation: e.relation,
788
+ })).filter(e => e.si >= 0 && e.ti >= 0);
789
+
790
+ // ── 3D force simulation (simple Euler integration) ──────────
791
+ const SIM_ITERS = 200;
792
+ const REPULSION = 800;
793
+ const SPRING_K = 0.04;
794
+ const SPRING_REST = 60;
795
+ const DAMPING = 0.85;
796
+ const CENTER_PULL = 0.001;
797
+
798
+ for (let iter = 0; iter < SIM_ITERS; iter++) {
799
+ // Repulsion (O(n²) — fine for < 2000 nodes)
800
+ for (let i = 0; i < n3.length; i++) {
801
+ for (let j = i + 1; j < n3.length; j++) {
802
+ let dx = n3[i].x - n3[j].x, dy = n3[i].y - n3[j].y, dz = n3[i].z - n3[j].z;
803
+ let dist = Math.sqrt(dx*dx + dy*dy + dz*dz) || 1;
804
+ let force = REPULSION / (dist * dist);
805
+ let fx = dx / dist * force, fy = dy / dist * force, fz = dz / dist * force;
806
+ n3[i].vx += fx; n3[i].vy += fy; n3[i].vz += fz;
807
+ n3[j].vx -= fx; n3[j].vy -= fy; n3[j].vz -= fz;
808
+ }
809
+ }
810
+ // Springs (edges)
811
+ for (const e of e3) {
812
+ const s = n3[e.si], t = n3[e.ti];
813
+ let dx = t.x - s.x, dy = t.y - s.y, dz = t.z - s.z;
814
+ let dist = Math.sqrt(dx*dx + dy*dy + dz*dz) || 1;
815
+ let force = SPRING_K * (dist - SPRING_REST);
816
+ let fx = dx / dist * force, fy = dy / dist * force, fz = dz / dist * force;
817
+ s.vx += fx; s.vy += fy; s.vz += fz;
818
+ t.vx -= fx; t.vy -= fy; t.vz -= fz;
819
+ }
820
+ // Center pull + damping + integrate
821
+ for (const n of n3) {
822
+ n.vx -= n.x * CENTER_PULL;
823
+ n.vy -= n.y * CENTER_PULL;
824
+ n.vz -= n.z * CENTER_PULL;
825
+ n.vx *= DAMPING; n.vy *= DAMPING; n.vz *= DAMPING;
826
+ n.x += n.vx; n.y += n.vy; n.z += n.vz;
827
+ }
828
+ }
829
+
830
+ // ── Build Three.js objects ───────────────────────────────────
831
+ const NC = {
832
+ file:'#58a6ff', function:'#ff7b72', class:'#d2a8ff', interface:'#3fb950',
833
+ const:'#ffa657', type:'#79c0ff', enum:'#f85149', concept:'#e3b341',
834
+ decision:'#bc8cff', constraint:'#56d4dd', node:'#8b949e', module:'#58a6ff',
835
+ };
836
+ const NR3 = { file:5, class:4.5, interface:4, function:3.5, const:3, type:3, enum:3.5, concept:3.5, decision:3.5, constraint:3.5, node:3, module:4 };
837
+
838
+ // Nodes — glowing spheres
839
+ const spheres = [];
840
+ for (const n of n3) {
841
+ const color = new THREE.Color(NC[n.type||'node'] || '#8b949e');
842
+ const r = NR3[n.type||'node'] || 3;
843
+ const geo = new THREE.SphereGeometry(r, 16, 12);
844
+ const mat = new THREE.MeshBasicMaterial({ color });
845
+ const mesh = new THREE.Mesh(geo, mat);
846
+ mesh.position.set(n.x, n.y, n.z);
847
+ mesh.userData = n;
848
+ scene.add(mesh);
849
+ spheres.push(mesh);
850
+
851
+ // Glow ring
852
+ const glowGeo = new THREE.SphereGeometry(r * 1.6, 12, 8);
853
+ const glowMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.08 });
854
+ const glow = new THREE.Mesh(glowGeo, glowMat);
855
+ glow.position.copy(mesh.position);
856
+ scene.add(glow);
857
+ }
858
+
859
+ // Edges — lines
860
+ const edgeLineMat = new THREE.LineBasicMaterial({ color: 0x484f58, transparent: true, opacity: 0.35 });
861
+ for (const e of e3) {
862
+ const s = n3[e.si], t = n3[e.ti];
863
+ const geo = new THREE.BufferGeometry().setFromPoints([
864
+ new THREE.Vector3(s.x, s.y, s.z),
865
+ new THREE.Vector3(t.x, t.y, t.z),
866
+ ]);
867
+ scene.add(new THREE.Line(geo, edgeLineMat));
868
+ }
869
+
870
+ // Labels (sprite text) — only for file / class / interface
871
+ const LABEL_TYPES = new Set(['file','class','interface']);
872
+ for (const n of n3) {
873
+ if (!LABEL_TYPES.has(n.type || '')) continue;
874
+ const label = n.label.length > 18 ? n.label.slice(0, 16) + '…' : n.label;
875
+ const canvas2 = document.createElement('canvas');
876
+ const ctx2 = canvas2.getContext('2d');
877
+ canvas2.width = 256; canvas2.height = 48;
878
+ ctx2.fillStyle = 'transparent';
879
+ ctx2.fillRect(0, 0, 256, 48);
880
+ ctx2.font = 'bold 22px monospace';
881
+ ctx2.fillStyle = NC[n.type||'node'] || '#8b949e';
882
+ ctx2.fillText(label, 4, 32);
883
+ const tex = new THREE.CanvasTexture(canvas2);
884
+ tex.minFilter = THREE.LinearFilter;
885
+ const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.85, depthWrite: false });
886
+ const sprite = new THREE.Sprite(spriteMat);
887
+ sprite.position.set(n.x + (NR3[n.type||'node']||3) + 6, n.y + 2, n.z);
888
+ sprite.scale.set(48, 9, 1);
889
+ scene.add(sprite);
890
+ }
891
+
892
+ // ── Raycaster for tooltips ──────────────────────────────────
893
+ const raycaster = new THREE.Raycaster();
894
+ raycaster.params.Points = { threshold: 5 };
895
+ const mouse = new THREE.Vector2();
896
+ const tooltip3d = document.getElementById('tooltip');
897
+ let hoveredMesh = null;
898
+
899
+ renderer.domElement.addEventListener('mousemove', e => {
900
+ const rect = renderer.domElement.getBoundingClientRect();
901
+ mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
902
+ mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
903
+ raycaster.setFromCamera(mouse, camera);
904
+ const hits = raycaster.intersectObjects(spheres);
905
+ if (hits.length > 0) {
906
+ const d = hits[0].object.userData;
907
+ tooltip3d.style.display = 'block';
908
+ tooltip3d.innerHTML = \`<strong>\${d.label}</strong><br>
909
+ <span class="t-type">\${d.type||'node'}</span>
910
+ \${d.file ? \`<br><span class="t-file">\${d.file}</span>\` : ''}\`;
911
+ tooltip3d.style.left = (e.clientX + 15) + 'px';
912
+ tooltip3d.style.top = (e.clientY - 8) + 'px';
913
+ if (hoveredMesh !== hits[0].object) {
914
+ if (hoveredMesh) hoveredMesh.scale.setScalar(1);
915
+ hoveredMesh = hits[0].object;
916
+ hoveredMesh.scale.setScalar(1.5);
917
+ }
918
+ } else {
919
+ tooltip3d.style.display = 'none';
920
+ if (hoveredMesh) { hoveredMesh.scale.setScalar(1); hoveredMesh = null; }
921
+ }
922
+ });
923
+
924
+ // ── Ambient particles for depth perception ──────────────────
925
+ const particleCount = 200;
926
+ const pGeo = new THREE.BufferGeometry();
927
+ const pPos = new Float32Array(particleCount * 3);
928
+ for (let i = 0; i < particleCount; i++) {
929
+ pPos[i*3] = (Math.random() - 0.5) * 1200;
930
+ pPos[i*3+1] = (Math.random() - 0.5) * 1200;
931
+ pPos[i*3+2] = (Math.random() - 0.5) * 1200;
932
+ }
933
+ pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
934
+ const pMat = new THREE.PointsMaterial({ color: 0x30363d, size: 1.5, transparent: true, opacity: 0.5 });
935
+ scene.add(new THREE.Points(pGeo, pMat));
936
+
937
+ // ── Animate ─────────────────────────────────────────────────
938
+ let animId = null;
939
+ let autoAngle = 0;
940
+ function animate() {
941
+ animId = requestAnimationFrame(animate);
942
+
943
+ if (mode === 'spread') {
944
+ autoAngle += 0.003;
945
+ // Orbit on a fixed radius. Previously x/y were derived from the live,
946
+ // mutating camera.position.z (read and overwritten in the same frame),
947
+ // which made the camera teleport between frames — the flicker. Driving
948
+ // the orbit from a stable radius keeps it smooth; the wheel adjusts it.
949
+ camera.position.x = Math.sin(autoAngle) * spreadRadius;
950
+ camera.position.z = Math.cos(autoAngle) * spreadRadius;
951
+ camera.position.y = Math.sin(autoAngle * 0.4) * spreadRadius * 0.3;
952
+ camera.lookAt(0, 0, 0);
953
+ }
954
+
955
+ if (controls) controls.update();
956
+ renderer.render(scene, camera);
957
+ }
958
+ animate();
959
+
960
+ // ── Resize handler ──────────────────────────────────────────
961
+ function onResize() {
962
+ const w = container.clientWidth || 900;
963
+ const h = container.clientHeight || 580;
964
+ camera.aspect = w / h;
965
+ camera.updateProjectionMatrix();
966
+ renderer.setSize(w, h);
967
+ }
968
+ window.addEventListener('resize', onResize);
969
+
970
+ // ── Cleanup function ────────────────────────────────────────
971
+ three3dCleanup = () => {
972
+ if (animId) cancelAnimationFrame(animId);
973
+ window.removeEventListener('resize', onResize);
974
+ if (controls) controls.dispose();
975
+ renderer.dispose();
976
+ };
977
+ }
978
+
979
+ const forcePill = document.getElementById('layout-force');
980
+ const radialPill = document.getElementById('layout-radial');
981
+ const spread3dPill = document.getElementById('layout-3d-spread');
982
+ const explore3dPill = document.getElementById('layout-3d-explore');
983
+ const allPills = [forcePill, radialPill, spread3dPill, explore3dPill];
984
+
985
+ function activatePill(pill) {
986
+ allPills.forEach(p => p.classList.remove('on'));
987
+ pill.classList.add('on');
988
+ }
989
+
990
+ forcePill.onclick = () => {
991
+ activatePill(forcePill);
992
+ show2d();
993
+ applyLayout('force');
994
+ };
995
+ radialPill.onclick = () => {
996
+ activatePill(radialPill);
997
+ show2d();
998
+ applyLayout('radial');
999
+ };
1000
+ spread3dPill.onclick = () => {
1001
+ activatePill(spread3dPill);
1002
+ show3d('spread');
1003
+ };
1004
+ explore3dPill.onclick = () => {
1005
+ activatePill(explore3dPill);
1006
+ show3d('explore');
1007
+ };
509
1008
  }
510
1009
 
511
1010
  // ── Sessions ──────────────────────────────────────────────────────────────────
@@ -665,30 +1164,308 @@ function renderDag(plan) {
665
1164
  });
666
1165
  }
667
1166
 
668
- // ── Agent Memory ──────────────────────────────────────────────────────────────
1167
+ // ── Shared helpers ───────────────────────────────────────────────────────────
1168
+ function fmtDuration(ms) {
1169
+ if (!ms || ms < 0) return '—';
1170
+ const s = Math.round(ms / 1000);
1171
+ if (s < 60) return s + 's';
1172
+ return Math.floor(s / 60) + 'm ' + (s % 60) + 's';
1173
+ }
1174
+ function fmtTokens(n) {
1175
+ if (!n) return '0';
1176
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
1177
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
1178
+ return String(n);
1179
+ }
1180
+ function fmtPct(r) { return Math.round(r * 100) + '%'; }
1181
+
1182
+ /** Minimal D3 line+area chart. data is an array of day-bucketed points. */
1183
+ function drawLineChart(svg, data, xKey, yKey, color, fmt) {
1184
+ fmt = fmt || (v => v);
1185
+ const W = svg.clientWidth || 600, H = 220;
1186
+ const margin = { top: 10, right: 16, bottom: 24, left: 40 };
1187
+ const x = d3.scaleTime()
1188
+ .domain(d3.extent(data, d => new Date(d[xKey])))
1189
+ .range([margin.left, W - margin.right]);
1190
+ const yMax = Math.max(d3.max(data, d => d[yKey]) || 1, 0.0001);
1191
+ const y = d3.scaleLinear().domain([0, yMax * 1.15]).range([H - margin.bottom, margin.top]);
1192
+
1193
+ const s = d3.select(svg).attr('viewBox', \`0 0 \${W} \${H}\`);
1194
+
1195
+ s.append('g').attr('class', 'chart-axis')
1196
+ .attr('transform', \`translate(0,\${H - margin.bottom})\`)
1197
+ .call(d3.axisBottom(x).ticks(Math.min(6, data.length)).tickFormat(d3.timeFormat('%b %d')));
1198
+ s.append('g').attr('class', 'chart-axis')
1199
+ .attr('transform', \`translate(\${margin.left},0)\`)
1200
+ .call(d3.axisLeft(y).ticks(4).tickFormat(fmt));
1201
+
1202
+ const line = d3.line().x(d => x(new Date(d[xKey]))).y(d => y(d[yKey])).curve(d3.curveMonotoneX);
1203
+ const area = d3.area().x(d => x(new Date(d[xKey]))).y0(H - margin.bottom).y1(d => y(d[yKey])).curve(d3.curveMonotoneX);
1204
+
1205
+ s.append('path').datum(data).attr('class', 'chart-area').attr('fill', color).attr('d', area);
1206
+ s.append('path').datum(data).attr('class', 'chart-line').attr('stroke', color).attr('d', line);
1207
+
1208
+ s.selectAll('.chart-dot').data(data).join('circle')
1209
+ .attr('class', 'chart-dot')
1210
+ .attr('cx', d => x(new Date(d[xKey]))).attr('cy', d => y(d[yKey])).attr('r', 3.5).attr('fill', color)
1211
+ .on('mouseover', (e, d) => {
1212
+ tooltip.style.display = 'block';
1213
+ tooltip.innerHTML = \`<strong>\${new Date(d[xKey]).toLocaleDateString()}</strong><br>\${fmt(d[yKey])}\`;
1214
+ })
1215
+ .on('mousemove', e => { tooltip.style.left = (e.clientX + 15) + 'px'; tooltip.style.top = (e.clientY - 8) + 'px'; })
1216
+ .on('mouseout', () => tooltip.style.display = 'none');
1217
+ }
1218
+
1219
+ // ── 3D bar chart colors (shared between THREE materials and HTML legend swatches —
1220
+ // same string feeds both, so the legend always matches what's on screen) ────
1221
+ function successRateColor(rate) {
1222
+ const hue = Math.round(rate * 120); // 0=red (bad) → 120=green (good)
1223
+ return 'hsl(' + hue + ',65%,50%)';
1224
+ }
1225
+ const MODEL_PALETTE = ['#f0883e', '#58a6ff', '#bc8cff', '#3fb950', '#d29922', '#f85149'];
1226
+
1227
+ function legendRow(label, value, swatch) {
1228
+ return \`<div class="legend-3d-row">
1229
+ <span class="legend-3d-dot" style="background:\${swatch}"></span>
1230
+ <span class="legend-3d-name">\${String(label).replace(/</g,'&lt;')}</span>
1231
+ <span class="legend-3d-val">\${value}</span>
1232
+ </div>\`;
1233
+ }
1234
+
1235
+ /**
1236
+ * True rotatable 3D bar chart (cylinders) via three.js. Drag horizontally to
1237
+ * orbit; auto-rotates slowly when idle. Hover a bar for its value via
1238
+ * raycasting into the existing global tooltip div.
1239
+ *
1240
+ * 'items': [{ label, value, color }] — color is any CSS color string, used
1241
+ * directly as both the THREE.js material color and (by the caller) the
1242
+ * legend swatch, so the two can never drift out of sync.
1243
+ *
1244
+ * Must only be called once its canvas is actually visible — a hidden
1245
+ * (display:none) container has zero width, which would size the renderer
1246
+ * wrong. That's why this is invoked from the lazy init*Charts() functions
1247
+ * below, the same way the codebase graph defers initGraph() until shown.
1248
+ */
1249
+ function init3DBarChart(canvas, items) {
1250
+ if (!canvas || !items.length) return;
1251
+ const container = canvas.parentElement;
1252
+ const W = container.clientWidth || 480, H = 260;
1253
+ canvas.width = W; canvas.height = H;
1254
+
1255
+ const scene = new THREE.Scene();
1256
+ const camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 100);
1257
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
1258
+ renderer.setSize(W, H, false);
1259
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
1260
+
1261
+ scene.add(new THREE.AmbientLight(0xffffff, 0.55));
1262
+ const key = new THREE.DirectionalLight(0xffffff, 0.95);
1263
+ key.position.set(4, 8, 6);
1264
+ scene.add(key);
1265
+ const rim = new THREE.DirectionalLight(0x88aaff, 0.3);
1266
+ rim.position.set(-4, 2, -4);
1267
+ scene.add(rim);
1268
+
1269
+ const group = new THREE.Group();
1270
+ scene.add(group);
1271
+
1272
+ const maxVal = Math.max(1, ...items.map(it => it.value));
1273
+ const n = items.length;
1274
+ const spacing = 1.7;
1275
+ const totalWidth = (n - 1) * spacing;
1276
+ const meshes = [];
1277
+
1278
+ items.forEach((it, i) => {
1279
+ const h = Math.max(0.2, (it.value / maxVal) * 3.2);
1280
+ const geo = new THREE.CylinderGeometry(0.55, 0.62, h, 28);
1281
+ const mat = new THREE.MeshStandardMaterial({ color: new THREE.Color(it.color), roughness: 0.45, metalness: 0.15 });
1282
+ const mesh = new THREE.Mesh(geo, mat);
1283
+ mesh.position.set(i * spacing - totalWidth / 2, h / 2, 0);
1284
+ mesh.userData = { label: it.label, value: it.value };
1285
+ group.add(mesh);
1286
+ meshes.push(mesh);
1287
+ });
1288
+
1289
+ const ground = new THREE.Mesh(
1290
+ new THREE.CircleGeometry(totalWidth / 2 + 1.5, 48),
1291
+ new THREE.MeshStandardMaterial({ color: 0x161b22, roughness: 0.95 }),
1292
+ );
1293
+ ground.rotation.x = -Math.PI / 2;
1294
+ group.add(ground);
1295
+
1296
+ camera.position.set(0, 3.6, totalWidth + 5.5);
1297
+ camera.lookAt(0, 1, 0);
1298
+
1299
+ let rotY = 0, dragging = false, lastX = 0, autoRotate = true;
1300
+ canvas.style.cursor = 'grab';
1301
+
1302
+ canvas.addEventListener('pointerdown', e => {
1303
+ dragging = true; autoRotate = false; lastX = e.clientX; canvas.style.cursor = 'grabbing';
1304
+ });
1305
+ window.addEventListener('pointerup', () => { dragging = false; canvas.style.cursor = 'grab'; });
1306
+ window.addEventListener('pointermove', e => {
1307
+ if (dragging) { rotY += (e.clientX - lastX) * 0.008; lastX = e.clientX; }
1308
+ });
1309
+
1310
+ const raycaster = new THREE.Raycaster();
1311
+ const ndc = new THREE.Vector2();
1312
+ canvas.addEventListener('pointermove', e => {
1313
+ const rect = canvas.getBoundingClientRect();
1314
+ ndc.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
1315
+ ndc.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
1316
+ raycaster.setFromCamera(ndc, camera);
1317
+ const hit = raycaster.intersectObjects(meshes)[0];
1318
+ if (hit) {
1319
+ tooltip.style.display = 'block';
1320
+ tooltip.innerHTML = \`<strong>\${hit.object.userData.label}</strong><br>\${hit.object.userData.value}\`;
1321
+ tooltip.style.left = (e.clientX + 15) + 'px';
1322
+ tooltip.style.top = (e.clientY - 8) + 'px';
1323
+ } else {
1324
+ tooltip.style.display = 'none';
1325
+ }
1326
+ });
1327
+ canvas.addEventListener('pointerleave', () => { tooltip.style.display = 'none'; });
1328
+
1329
+ (function tick() {
1330
+ if (autoRotate) rotY += 0.0025;
1331
+ group.rotation.y = rotY;
1332
+ renderer.render(scene, camera);
1333
+ requestAnimationFrame(tick);
1334
+ })();
1335
+ }
1336
+
1337
+ // ── Memory Growth ────────────────────────────────────────────────────────────
1338
+ let memoryInit = false;
669
1339
  (function() {
670
1340
  const panel = document.getElementById('memory');
671
- if (!DATA.memory.length) {
672
- panel.innerHTML = '<div class="empty">No orchestration memory entries found.</div>';
673
- return;
674
- }
675
- const rows = DATA.memory.map(m => {
676
- const ts = new Date(m.timestamp).toLocaleString();
677
- const val = typeof m.value === 'string' ? m.value : JSON.stringify(m.value, null, 2);
1341
+ const facts = DATA.memory;
1342
+ const growth = DATA.memoryGrowth;
1343
+ const namespaces = new Set(facts.map(f => f.namespace)).size;
1344
+ const oldest = facts.length ? new Date(facts[0].created).toLocaleDateString() : '—';
1345
+ const newest = facts.length ? new Date(facts[facts.length-1].created).toLocaleDateString() : '—';
1346
+
1347
+ const rows = [...facts].reverse().map(f => {
1348
+ const val = f.value || '';
678
1349
  return \`<tr>
679
- <td>\${(m.key||'').replace(/</g,'&lt;')}</td>
680
- <td style="color:var(--muted)">\${(m.stepId||'').replace(/</g,'&lt;')}</td>
681
- <td style="color:var(--dim);white-space:nowrap">\${ts}</td>
1350
+ <td>\${f.key.replace(/</g,'&lt;')}</td>
1351
+ <td><span class="ns-badge">\${f.namespace.replace(/</g,'&lt;')}</span></td>
1352
+ <td style="color:var(--dim);white-space:nowrap">\${new Date(f.created).toLocaleString()}</td>
1353
+ <td style="color:var(--dim);white-space:nowrap">\${new Date(f.updated).toLocaleString()}</td>
682
1354
  <td class="memory-val">\${val.slice(0,400).replace(/</g,'&lt;')}\${val.length>400?'…':''}</td>
683
1355
  </tr>\`;
684
1356
  }).join('');
1357
+
685
1358
  panel.innerHTML = \`
686
- <table class="memory-table">
687
- <thead><tr><th>Key</th><th>Step</th><th>Written</th><th>Value</th></tr></thead>
1359
+ <div class="stats-grid">
1360
+ <div class="stat-card"><div class="num">\${facts.length}</div><div class="lbl">Facts Remembered</div></div>
1361
+ <div class="stat-card"><div class="num">\${namespaces}</div><div class="lbl">Namespaces</div></div>
1362
+ <div class="stat-card"><div class="num" style="font-size:18px">\${oldest}</div><div class="lbl">First Remembered</div></div>
1363
+ <div class="stat-card"><div class="num" style="font-size:18px">\${newest}</div><div class="lbl">Most Recent</div></div>
1364
+ </div>
1365
+ <div class="chart-card">
1366
+ <div class="lbl">Cumulative facts remembered, over time</div>
1367
+ \${growth.length ? '<svg class="chart-svg" id="mem-growth-svg"></svg>' : '<div class="chart-empty">No memory yet — facts appear here as Aura remembers things.</div>'}
1368
+ </div>
1369
+ \${facts.length ? \`<table class="memory-table">
1370
+ <thead><tr><th>Key</th><th>Namespace</th><th>Created</th><th>Updated</th><th>Value</th></tr></thead>
688
1371
  <tbody>\${rows}</tbody>
689
- </table>
1372
+ </table>\` : '<div class="empty">No memory entries found.</div>'}
690
1373
  \`;
691
1374
  })();
1375
+
1376
+ /** Draws the memory-growth line chart. Called once, the first time the
1377
+ * Memory Growth tab is actually shown (see showPanel above) — doing it at
1378
+ * page-load time would measure a hidden, zero-width panel. */
1379
+ function initMemoryCharts() {
1380
+ memoryInit = true;
1381
+ const svg = document.getElementById('mem-growth-svg');
1382
+ if (svg && DATA.memoryGrowth.length) {
1383
+ drawLineChart(svg, DATA.memoryGrowth, 'date', 'cumulative', 'var(--blue)', v => Math.round(v));
1384
+ }
1385
+ }
1386
+
1387
+ // ── Learning ─────────────────────────────────────────────────────────────────
1388
+ let learningInit = false;
1389
+ (function() {
1390
+ const panel = document.getElementById('learning');
1391
+ const sum = DATA.episodeSummary;
1392
+ const series = DATA.learningSeries;
1393
+ const report = DATA.competenceReport;
1394
+
1395
+ if (sum.total === 0) {
1396
+ panel.innerHTML = '<div class="empty">No episodes recorded yet — this fills in as Aura completes tasks with self-improvement enabled.</div>';
1397
+ return;
1398
+ }
1399
+
1400
+ const completePct = sum.total === 0 ? 0 : sum.completed / sum.total;
1401
+
1402
+ const catLegend = report.length
1403
+ ? report.map(r => legendRow(r.category, \`\${r.count} · \${fmtPct(r.successRate)}\`, successRateColor(r.successRate))).join('')
1404
+ : '<div class="empty" style="padding:14px">No Ruby-attempted episodes yet.</div>';
1405
+
1406
+ const modelLegend = sum.topModels.length
1407
+ ? sum.topModels.map((m, i) => legendRow(m.model, String(m.count), MODEL_PALETTE[i % MODEL_PALETTE.length])).join('')
1408
+ : '<div class="empty" style="padding:14px">No model usage recorded yet.</div>';
1409
+
1410
+ panel.innerHTML = \`
1411
+ <div class="stats-grid">
1412
+ <div class="stat-card"><div class="num">\${sum.total}</div><div class="lbl">Episodes Recorded</div></div>
1413
+ <div class="stat-card"><div class="num">\${fmtPct(completePct)}</div><div class="lbl">Tasks Completed</div></div>
1414
+ <div class="stat-card"><div class="num">\${fmtPct(sum.rubySuccessRate)}</div><div class="lbl">Ruby Success Rate</div></div>
1415
+ <div class="stat-card"><div class="num" style="font-size:20px">\${fmtDuration(sum.avgDurationMs)}</div><div class="lbl">Avg Duration</div></div>
1416
+ <div class="stat-card"><div class="num" style="font-size:20px">\${fmtTokens(sum.totalTokens)}</div><div class="lbl">Total Tokens</div></div>
1417
+ </div>
1418
+ <div class="two-col">
1419
+ <div class="chart-card">
1420
+ <div class="lbl">Cumulative episodes, over time</div>
1421
+ \${series.length ? '<svg class="chart-svg" id="ep-growth-svg"></svg>' : '<div class="chart-empty">Not enough data yet.</div>'}
1422
+ </div>
1423
+ <div class="chart-card">
1424
+ <div class="lbl">Ruby (small-model) success rate — rolling, last 10 attempts</div>
1425
+ \${series.length ? '<svg class="chart-svg" id="ep-success-svg"></svg>' : '<div class="chart-empty">Not enough data yet.</div>'}
1426
+ </div>
1427
+ </div>
1428
+ <div class="two-col">
1429
+ <div class="chart-card">
1430
+ <div class="lbl">By task category — height = volume, color = Ruby success rate. Drag to rotate.</div>
1431
+ <div class="chart-3d-container"><canvas id="cat-3d-canvas"></canvas></div>
1432
+ <div class="legend-3d">\${catLegend}</div>
1433
+ </div>
1434
+ <div class="chart-card">
1435
+ <div class="lbl">By model used — height = task count. Drag to rotate.</div>
1436
+ <div class="chart-3d-container"><canvas id="model-3d-canvas"></canvas></div>
1437
+ <div class="legend-3d">\${modelLegend}</div>
1438
+ </div>
1439
+ </div>
1440
+ \`;
1441
+ })();
1442
+
1443
+ /** Draws the two D3 line charts and the two three.js 3D bar charts for the
1444
+ * Learning tab. Lazy — see initMemoryCharts() for why. */
1445
+ function initLearningCharts() {
1446
+ learningInit = true;
1447
+ const series = DATA.learningSeries;
1448
+ const report = DATA.competenceReport;
1449
+ const sum = DATA.episodeSummary;
1450
+
1451
+ const growthSvg = document.getElementById('ep-growth-svg');
1452
+ if (growthSvg && series.length) {
1453
+ drawLineChart(growthSvg, series, 'date', 'cumulativeEpisodes', 'var(--primary)', v => Math.round(v));
1454
+ }
1455
+ const successSvg = document.getElementById('ep-success-svg');
1456
+ if (successSvg && series.length) {
1457
+ drawLineChart(successSvg, series, 'date', 'rollingSuccessRate', 'var(--success)', v => fmtPct(v));
1458
+ }
1459
+
1460
+ init3DBarChart(
1461
+ document.getElementById('cat-3d-canvas'),
1462
+ report.map(r => ({ label: r.category, value: r.count, color: successRateColor(r.successRate) })),
1463
+ );
1464
+ init3DBarChart(
1465
+ document.getElementById('model-3d-canvas'),
1466
+ sum.topModels.map((m, i) => ({ label: m.model, value: m.count, color: MODEL_PALETTE[i % MODEL_PALETTE.length] })),
1467
+ );
1468
+ }
692
1469
  </script>
693
1470
  </body>
694
1471
  </html>`;
@@ -700,7 +1477,12 @@ function generateDashboard(projectRoot) {
700
1477
  const graph = loadGraph(projectRoot);
701
1478
  const plans = loadPlans(projectRoot).map(stripPlan);
702
1479
  const sessions = loadSessions(projectRoot).map(stripSession);
703
- const memory = loadMemory(projectRoot);
1480
+ const memory = loadMemory();
1481
+ const memoryGrowth = buildMemoryGrowth(memory);
1482
+ const episodes = loadEpisodesSync(projectRoot);
1483
+ const learningSeries = buildLearningSeries(episodes);
1484
+ const episodeSummary = summariseEpisodes(episodes);
1485
+ const competenceReport = (0, competence_js_1.getCompetenceReport)(episodes);
704
1486
  const pkgPath = path.join(projectRoot, 'package.json');
705
1487
  let projectName = path.basename(projectRoot);
706
1488
  try {
@@ -713,6 +1495,10 @@ function generateDashboard(projectRoot) {
713
1495
  plans,
714
1496
  sessions,
715
1497
  memory,
1498
+ memoryGrowth,
1499
+ episodeSummary,
1500
+ learningSeries,
1501
+ competenceReport,
716
1502
  projectName,
717
1503
  generatedAt: new Date().toLocaleString(),
718
1504
  });