aura-code 0.6.3 → 0.10.1

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 (265) hide show
  1. package/README.md +47 -164
  2. package/dist/agent/affect.d.ts +13 -0
  3. package/dist/agent/affect.js +34 -0
  4. package/dist/agent/affect.js.map +1 -0
  5. package/dist/agent/compactor.d.ts +27 -4
  6. package/dist/agent/compactor.js +185 -54
  7. package/dist/agent/compactor.js.map +1 -1
  8. package/dist/agent/confess.d.ts +35 -0
  9. package/dist/agent/confess.js +296 -0
  10. package/dist/agent/confess.js.map +1 -0
  11. package/dist/agent/context.d.ts +0 -4
  12. package/dist/agent/context.js +0 -38
  13. package/dist/agent/context.js.map +1 -1
  14. package/dist/agent/domain-expertise.d.ts +22 -0
  15. package/dist/agent/domain-expertise.js +110 -0
  16. package/dist/agent/domain-expertise.js.map +1 -0
  17. package/dist/agent/executive-queue.d.ts +26 -0
  18. package/dist/agent/executive-queue.js +50 -0
  19. package/dist/agent/executive-queue.js.map +1 -0
  20. package/dist/agent/generational-flush.d.ts +28 -0
  21. package/dist/agent/generational-flush.js +48 -0
  22. package/dist/agent/generational-flush.js.map +1 -0
  23. package/dist/agent/loop-profile.d.ts +50 -0
  24. package/dist/agent/loop-profile.js +88 -0
  25. package/dist/agent/loop-profile.js.map +1 -0
  26. package/dist/agent/loop.d.ts +14 -5
  27. package/dist/agent/loop.js +167 -39
  28. package/dist/agent/loop.js.map +1 -1
  29. package/dist/agent/memory-consolidate.d.ts +14 -0
  30. package/dist/agent/memory-consolidate.js +141 -0
  31. package/dist/agent/memory-consolidate.js.map +1 -0
  32. package/dist/agent/mixture.d.ts +33 -0
  33. package/dist/agent/mixture.js +138 -0
  34. package/dist/agent/mixture.js.map +1 -0
  35. package/dist/agent/spawner.js +2 -2
  36. package/dist/agent/spawner.js.map +1 -1
  37. package/dist/agent/system-prompt.d.ts +1 -1
  38. package/dist/agent/system-prompt.js +50 -22
  39. package/dist/agent/system-prompt.js.map +1 -1
  40. package/dist/agent/unified-memory.d.ts +15 -0
  41. package/dist/agent/unified-memory.js +164 -0
  42. package/dist/agent/unified-memory.js.map +1 -0
  43. package/dist/checkpoints/engine.d.ts +44 -0
  44. package/dist/checkpoints/engine.js +324 -0
  45. package/dist/checkpoints/engine.js.map +1 -0
  46. package/dist/cli/command-palette.d.ts +26 -0
  47. package/dist/cli/command-palette.js +133 -0
  48. package/dist/cli/command-palette.js.map +1 -0
  49. package/dist/cli/context-health.d.ts +51 -0
  50. package/dist/cli/context-health.js +187 -0
  51. package/dist/cli/context-health.js.map +1 -0
  52. package/dist/cli/diamond.d.ts +67 -0
  53. package/dist/cli/diamond.js +222 -12
  54. package/dist/cli/diamond.js.map +1 -1
  55. package/dist/cli/dic.d.ts +2 -0
  56. package/dist/cli/dic.js +106 -0
  57. package/dist/cli/dic.js.map +1 -0
  58. package/dist/cli/diff-view.d.ts +18 -0
  59. package/dist/cli/diff-view.js +114 -0
  60. package/dist/cli/diff-view.js.map +1 -0
  61. package/dist/cli/display.d.ts +18 -4
  62. package/dist/cli/display.js +74 -151
  63. package/dist/cli/display.js.map +1 -1
  64. package/dist/cli/help-data.d.ts +7 -0
  65. package/dist/cli/help-data.js +80 -0
  66. package/dist/cli/help-data.js.map +1 -0
  67. package/dist/cli/index.js +1002 -852
  68. package/dist/cli/index.js.map +1 -1
  69. package/dist/cli/markdown.d.ts +5 -0
  70. package/dist/cli/markdown.js +128 -0
  71. package/dist/cli/markdown.js.map +1 -0
  72. package/dist/cli/model-select.d.ts +39 -0
  73. package/dist/cli/model-select.js +65 -0
  74. package/dist/cli/model-select.js.map +1 -0
  75. package/dist/cli/tui.d.ts +34 -0
  76. package/dist/cli/tui.js +1176 -0
  77. package/dist/cli/tui.js.map +1 -0
  78. package/dist/config/defaults.d.ts +6 -17
  79. package/dist/config/defaults.js +25 -46
  80. package/dist/config/defaults.js.map +1 -1
  81. package/dist/config/project-config.d.ts +2 -6
  82. package/dist/config/project-config.js +2 -2
  83. package/dist/config/project-config.js.map +1 -1
  84. package/dist/doctor/checks.d.ts +18 -0
  85. package/dist/doctor/checks.js +524 -0
  86. package/dist/doctor/checks.js.map +1 -0
  87. package/dist/doctor/index.d.ts +4 -0
  88. package/dist/doctor/index.js +229 -0
  89. package/dist/doctor/index.js.map +1 -0
  90. package/dist/doctor/repair.d.ts +19 -0
  91. package/dist/doctor/repair.js +126 -0
  92. package/dist/doctor/repair.js.map +1 -0
  93. package/dist/doctor/types.d.ts +38 -0
  94. package/dist/doctor/types.js +10 -0
  95. package/dist/doctor/types.js.map +1 -0
  96. package/dist/dream/dream.d.ts +79 -59
  97. package/dist/dream/dream.js +324 -144
  98. package/dist/dream/dream.js.map +1 -1
  99. package/dist/dream/episode.d.ts +17 -0
  100. package/dist/dream/episode.js +116 -0
  101. package/dist/dream/episode.js.map +1 -0
  102. package/dist/kanban/engine.d.ts +26 -0
  103. package/dist/kanban/engine.js +208 -0
  104. package/dist/kanban/engine.js.map +1 -0
  105. package/dist/kanban/types.d.ts +19 -0
  106. package/dist/kanban/types.js +9 -1
  107. package/dist/kanban/types.js.map +1 -1
  108. package/dist/machina/render-html.js +4 -2
  109. package/dist/machina/render-html.js.map +1 -1
  110. package/dist/machina/render-terminal.js +11 -4
  111. package/dist/machina/render-terminal.js.map +1 -1
  112. package/dist/machina/repair.d.ts +23 -0
  113. package/dist/machina/repair.js +98 -0
  114. package/dist/machina/repair.js.map +1 -0
  115. package/dist/machina/spec.js +12 -12
  116. package/dist/machina/spec.js.map +1 -1
  117. package/dist/machina/verify.d.ts +13 -1
  118. package/dist/machina/verify.js +12 -2
  119. package/dist/machina/verify.js.map +1 -1
  120. package/dist/mining/extract.d.ts +34 -0
  121. package/dist/mining/extract.js +229 -0
  122. package/dist/mining/extract.js.map +1 -0
  123. package/dist/mining/refine.d.ts +28 -0
  124. package/dist/mining/refine.js +256 -0
  125. package/dist/mining/refine.js.map +1 -0
  126. package/dist/perception/extractor.js +5 -3
  127. package/dist/perception/extractor.js.map +1 -1
  128. package/dist/perception/graph-store.d.ts +0 -11
  129. package/dist/perception/graph-store.js +0 -33
  130. package/dist/perception/graph-store.js.map +1 -1
  131. package/dist/perception/index.d.ts +1 -1
  132. package/dist/perception/index.js +1 -2
  133. package/dist/perception/index.js.map +1 -1
  134. package/dist/plugins/commands.d.ts +38 -0
  135. package/dist/plugins/commands.js +134 -0
  136. package/dist/plugins/commands.js.map +1 -0
  137. package/dist/plugins/frontmatter.d.ts +14 -0
  138. package/dist/plugins/frontmatter.js +78 -0
  139. package/dist/plugins/frontmatter.js.map +1 -0
  140. package/dist/plugins/hooks.d.ts +12 -0
  141. package/dist/plugins/hooks.js +148 -0
  142. package/dist/plugins/hooks.js.map +1 -0
  143. package/dist/plugins/loader.d.ts +7 -0
  144. package/dist/plugins/loader.js +255 -0
  145. package/dist/plugins/loader.js.map +1 -0
  146. package/dist/plugins/market.d.ts +23 -0
  147. package/dist/plugins/market.js +281 -0
  148. package/dist/plugins/market.js.map +1 -0
  149. package/dist/plugins/types.d.ts +90 -0
  150. package/dist/plugins/types.js +20 -0
  151. package/dist/plugins/types.js.map +1 -0
  152. package/dist/providers/anthropic-oauth-draft.d.ts +119 -0
  153. package/dist/providers/anthropic-oauth-draft.js +414 -0
  154. package/dist/providers/anthropic-oauth-draft.js.map +1 -0
  155. package/dist/providers/anthropic.d.ts +4 -1
  156. package/dist/providers/anthropic.js +35 -7
  157. package/dist/providers/anthropic.js.map +1 -1
  158. package/dist/providers/factory.d.ts +43 -19
  159. package/dist/providers/factory.js +183 -129
  160. package/dist/providers/factory.js.map +1 -1
  161. package/dist/providers/google.js +9 -22
  162. package/dist/providers/google.js.map +1 -1
  163. package/dist/providers/live-models.d.ts +23 -0
  164. package/dist/providers/live-models.js +145 -0
  165. package/dist/providers/live-models.js.map +1 -0
  166. package/dist/providers/openai-compatible.d.ts +1 -12
  167. package/dist/providers/openai-compatible.js +71 -108
  168. package/dist/providers/openai-compatible.js.map +1 -1
  169. package/dist/providers/resilient-factory.js +1 -1
  170. package/dist/providers/resilient-factory.js.map +1 -1
  171. package/dist/repl/queue.d.ts +22 -0
  172. package/dist/repl/queue.js +165 -0
  173. package/dist/repl/queue.js.map +1 -0
  174. package/dist/repl/side-channel.d.ts +23 -0
  175. package/dist/repl/side-channel.js +63 -0
  176. package/dist/repl/side-channel.js.map +1 -0
  177. package/dist/research/council.d.ts +2 -0
  178. package/dist/research/council.js +29 -4
  179. package/dist/research/council.js.map +1 -1
  180. package/dist/ruby/alternator.d.ts +11 -30
  181. package/dist/ruby/alternator.js +59 -38
  182. package/dist/ruby/alternator.js.map +1 -1
  183. package/dist/ruby/episode-capture.d.ts +0 -10
  184. package/dist/ruby/episode-capture.js +0 -33
  185. package/dist/ruby/episode-capture.js.map +1 -1
  186. package/dist/ruby/types.js +1 -1
  187. package/dist/ruby/types.js.map +1 -1
  188. package/dist/safety/path-jail.d.ts +17 -0
  189. package/dist/safety/path-jail.js +100 -0
  190. package/dist/safety/path-jail.js.map +1 -0
  191. package/dist/safety/permissions.d.ts +12 -21
  192. package/dist/safety/permissions.js +81 -216
  193. package/dist/safety/permissions.js.map +1 -1
  194. package/dist/safety/ssrf.d.ts +29 -0
  195. package/dist/safety/ssrf.js +185 -0
  196. package/dist/safety/ssrf.js.map +1 -0
  197. package/dist/server/index.js +64 -10
  198. package/dist/server/index.js.map +1 -1
  199. package/dist/setup/first-run.d.ts +0 -24
  200. package/dist/setup/first-run.js +10 -344
  201. package/dist/setup/first-run.js.map +1 -1
  202. package/dist/setup/key-store.d.ts +12 -0
  203. package/dist/setup/key-store.js +108 -0
  204. package/dist/setup/key-store.js.map +1 -0
  205. package/dist/setup/provider-registry.js +31 -18
  206. package/dist/setup/provider-registry.js.map +1 -1
  207. package/dist/setup/provider-test.d.ts +7 -0
  208. package/dist/setup/provider-test.js +73 -10
  209. package/dist/setup/provider-test.js.map +1 -1
  210. package/dist/setup/provider-wizard.d.ts +4 -1
  211. package/dist/setup/provider-wizard.js +92 -26
  212. package/dist/setup/provider-wizard.js.map +1 -1
  213. package/dist/setup/xiaomi.d.ts +1 -1
  214. package/dist/setup/xiaomi.js +3 -3
  215. package/dist/setup/xiaomi.js.map +1 -1
  216. package/dist/tools/browser.js +14 -10
  217. package/dist/tools/browser.js.map +1 -1
  218. package/dist/tools/clipboard.js +7 -1
  219. package/dist/tools/clipboard.js.map +1 -1
  220. package/dist/tools/dictate.d.ts +60 -0
  221. package/dist/tools/dictate.js +983 -0
  222. package/dist/tools/dictate.js.map +1 -0
  223. package/dist/tools/dictate_patched.js +1057 -0
  224. package/dist/tools/edit-file.js +10 -2
  225. package/dist/tools/edit-file.js.map +1 -1
  226. package/dist/tools/http-request.js +14 -4
  227. package/dist/tools/http-request.js.map +1 -1
  228. package/dist/tools/image-read.js +34 -2
  229. package/dist/tools/image-read.js.map +1 -1
  230. package/dist/tools/index.d.ts +16 -1
  231. package/dist/tools/index.js +59 -8
  232. package/dist/tools/index.js.map +1 -1
  233. package/dist/tools/mcp.js +12 -0
  234. package/dist/tools/mcp.js.map +1 -1
  235. package/dist/tools/read-file.js +10 -14
  236. package/dist/tools/read-file.js.map +1 -1
  237. package/dist/tools/telegram-audio-policy.d.ts +30 -0
  238. package/dist/tools/telegram-audio-policy.js +49 -0
  239. package/dist/tools/telegram-audio-policy.js.map +1 -0
  240. package/dist/tools/telegram-bot.js +1136 -537
  241. package/dist/tools/telegram-bot.js.map +1 -1
  242. package/dist/tools/telegram-voice.d.ts +18 -5
  243. package/dist/tools/telegram-voice.js +54 -12
  244. package/dist/tools/telegram-voice.js.map +1 -1
  245. package/dist/tools/telegram.d.ts +3 -3
  246. package/dist/tools/telegram.js +5 -84
  247. package/dist/tools/telegram.js.map +1 -1
  248. package/dist/tools/tools.d.ts +2 -14
  249. package/dist/tools/tools.js +79 -144
  250. package/dist/tools/tools.js.map +1 -1
  251. package/dist/tools/web-fetch.js +39 -7
  252. package/dist/tools/web-fetch.js.map +1 -1
  253. package/dist/tools/web-search.d.ts +0 -20
  254. package/dist/tools/web-search.js +40 -137
  255. package/dist/tools/web-search.js.map +1 -1
  256. package/dist/util/errors.js +1 -15
  257. package/dist/util/errors.js.map +1 -1
  258. package/dist/util/json-repair.d.ts +11 -0
  259. package/dist/util/json-repair.js +31 -0
  260. package/dist/util/json-repair.js.map +1 -0
  261. package/dist/util/rate-limiter.js +2 -8
  262. package/dist/util/rate-limiter.js.map +1 -1
  263. package/dist/viz/index.js +793 -792
  264. package/dist/viz/index.js.map +1 -1
  265. package/package.json +12 -32
package/dist/viz/index.js CHANGED
@@ -151,144 +151,23 @@ function stripPlan(p) {
151
151
  })),
152
152
  };
153
153
  }
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');
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);
167
158
  if (!fs.existsSync(dir))
168
159
  return [];
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$/, '');
160
+ return fs.readdirSync(dir)
161
+ .filter(f => f.endsWith('.json') && !f.endsWith('.tmp'))
162
+ .map(f => {
174
163
  try {
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
- }
164
+ return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
189
165
  }
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 */ }
166
+ catch {
167
+ return null;
215
168
  }
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 };
169
+ })
170
+ .filter((m) => m !== null);
292
171
  }
293
172
  // ─────────────────────────────────────────────────────────────────────────────
294
173
  // HTML template
@@ -303,8 +182,6 @@ function buildHtml(data) {
303
182
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
304
183
  <title>Aura — Memory Dashboard · ${data.projectName}</title>
305
184
  <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>
308
185
  <style>
309
186
  :root {
310
187
  --bg: #0d1117;
@@ -344,11 +221,11 @@ function buildHtml(data) {
344
221
  /* Graph panel */
345
222
  #graph-svg { background: var(--canvas); border: 1px solid var(--border); border-radius: 8px; flex: 1; min-height: 0; cursor: grab; }
346
223
  #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; }
350
224
  .graph-controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
351
- .layout-toggle { display: flex; gap: 4px; }
225
+ .mode-toggle { display: flex; gap: 4px; }
226
+ .mode-btn { background: var(--card); border: 1px solid var(--border); border-radius: 6px; color: var(--muted); cursor: pointer; font: inherit; font-size: 11px; padding: 5px 11px; transition: all .12s; }
227
+ .mode-btn:hover { border-color: var(--border2); color: var(--text); }
228
+ .mode-btn.active { background: var(--primary); border-color: var(--primary); color: #1c1108; font-weight: 700; }
352
229
  .graph-controls input {
353
230
  background: var(--card); border: 1px solid var(--border2); border-radius: 6px;
354
231
  color: var(--text); font: inherit; font-size: 12px; padding: 6px 12px; width: 220px; outline: none;
@@ -415,31 +292,13 @@ function buildHtml(data) {
415
292
  .memory-table td:first-child { color: var(--primary); white-space: nowrap; font-weight: 600; }
416
293
  .memory-table tr:hover td { background: var(--card); }
417
294
  .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; } }
441
295
 
442
296
  .empty { color: var(--dim); font-size: 12px; padding: 32px; text-align: center; }
297
+
298
+ /* New panel styles */
299
+ .axis line, .axis path { stroke: var(--border); }
300
+ .grid line { stroke: var(--border); stroke-dasharray: 3,3; }
301
+ .tooltip { position: fixed; background: #161b22f0; border: 1px solid var(--border2); border-radius: 7px; color: var(--text); font-size: 11px; max-width: 300px; padding: 9px 13px; pointer-events: none; z-index: 999; line-height: 1.6; box-shadow: 0 4px 20px #0008; }
443
302
  ::-webkit-scrollbar { width: 5px; height: 5px; }
444
303
  ::-webkit-scrollbar-track { background: transparent; }
445
304
  ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
@@ -455,16 +314,22 @@ function buildHtml(data) {
455
314
  <button onclick="showPanel('graph',this)">Codebase Graph</button>
456
315
  <button onclick="showPanel('sessions',this)">Sessions</button>
457
316
  <button onclick="showPanel('plans',this)">Execution Plans</button>
458
- <button onclick="showPanel('memory',this)">Memory Growth</button>
459
- <button onclick="showPanel('learning',this)">Learning</button>
317
+ <button onclick="showPanel('memory',this)">Agent Memory</button>
318
+ <button onclick="showPanel('activity',this)">Activity Timeline</button>
319
+ <button onclick="showPanel('centrality',this)">Code Centrality</button>
320
+ <button onclick="showPanel('specialists',this)">Specialist Stats</button>
321
+ <button onclick="showPanel('tools',this)">Tool Usage</button>
460
322
  </nav>
461
323
 
462
324
  <div id="overview" class="panel active"></div>
463
- <div id="graph" class="panel"></div>
464
- <div id="sessions" class="panel"></div>
465
- <div id="plans" class="panel"></div>
466
- <div id="memory" class="panel"></div>
467
- <div id="learning" class="panel"></div>
325
+ <div id="graph" class="panel"></div>
326
+ <div id="sessions" class="panel"></div>
327
+ <div id="plans" class="panel"></div>
328
+ <div id="memory" class="panel"></div>
329
+ <div id="activity" class="panel"></div>
330
+ <div id="centrality" class="panel"></div>
331
+ <div id="specialists" class="panel"></div>
332
+ <div id="tools" class="panel"></div>
468
333
 
469
334
  <div class="tooltip" id="tooltip" style="display:none"></div>
470
335
 
@@ -476,10 +341,12 @@ function showPanel(id, btn) {
476
341
  document.querySelectorAll('nav button').forEach(b => b.classList.remove('active'));
477
342
  document.getElementById(id).classList.add('active');
478
343
  if (btn) btn.classList.add('active');
479
- if (id === 'graph' && !graphInit) initGraph();
480
- if (id === 'plans' && !plansInit) initPlans();
481
- if (id === 'memory' && !memoryInit) initMemoryCharts();
482
- if (id === 'learning' && !learningInit) initLearningCharts();
344
+ if (id === 'graph' && !graphInit) initGraph();
345
+ if (id === 'plans' && !plansInit) initPlans();
346
+ if (id === 'activity' && !activityInit) initActivity();
347
+ if (id === 'centrality' && !centralityInit) initCentrality();
348
+ if (id === 'specialists' && !specialistsInit) initSpecialists();
349
+ if (id === 'tools' && !toolsInit) initTools();
483
350
  }
484
351
 
485
352
  // ── Overview ─────────────────────────────────────────────────────────────────
@@ -488,7 +355,6 @@ function showPanel(id, btn) {
488
355
  const nc = g ? g.nodes.length : 0, ec = g ? g.edges.length : 0;
489
356
  const sc = DATA.sessions.length, pc = DATA.plans.length;
490
357
  const mc = DATA.memory.length, dc = DATA.plans.filter(p=>p.status==='done').length;
491
- const epc = DATA.episodeSummary.total;
492
358
  const last = sc ? new Date(DATA.sessions[0].updatedAt).toLocaleString()
493
359
  : pc ? new Date(DATA.plans[0].created).toLocaleString() : '—';
494
360
 
@@ -509,7 +375,6 @@ function showPanel(id, btn) {
509
375
  <div class="stat-card"><div class="num">\${pc}</div><div class="lbl">Exec Plans</div></div>
510
376
  <div class="stat-card"><div class="num">\${dc}</div><div class="lbl">Plans Done</div></div>
511
377
  <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>
513
378
  </div>
514
379
  <div class="stat-card" style="max-width:640px">
515
380
  <div class="lbl" style="margin-bottom:10px">Codebase Breakdown</div>
@@ -534,37 +399,31 @@ function initGraph() {
534
399
 
535
400
  const NODE_COLORS = {
536
401
  file: '#58a6ff',
402
+ concept: '#d29922',
403
+ decision: '#bc8cff',
537
404
  function: '#ff7b72',
538
405
  class: '#d2a8ff',
539
406
  interface: '#3fb950',
540
407
  const: '#ffa657',
541
408
  type: '#79c0ff',
542
409
  enum: '#f85149',
543
- concept: '#e3b341',
544
- decision: '#bc8cff',
545
- constraint: '#56d4dd',
546
410
  node: '#8b949e',
547
411
  };
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 };
412
+ const NODE_R = { file: 13, concept: 8, decision: 9, class: 11, interface: 10, function: 8, const: 7, type: 7, enum: 8, node: 7 };
549
413
 
550
414
  const allTypes = [...new Set(DATA.graph.nodes.map(n => n.type || 'node'))];
551
415
  const activeTypes = new Set(allTypes);
552
416
 
553
- panel.innerHTML = \`
554
- <div class="graph-controls">
417
+ panel.innerHTML = \`<div class="graph-controls">
555
418
  <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>
419
+ <div class="mode-toggle" id="graph-mode-toggle">
420
+ <button class="mode-btn active" data-mode="force">Force</button>
421
+ <button class="mode-btn" data-mode="treemap">Treemap</button>
561
422
  </div>
562
423
  <div class="legend" id="legend"></div>
563
- <span class="hint" id="graph-hint">scroll to zoom · drag to pan · drag nodes</span>
424
+ <span class="hint">scroll to zoom · drag to pan · drag nodes</span>
564
425
  </div>
565
- <svg id="graph-svg"></svg>
566
- <div id="graph-3d-wrap"><div class="graph-3d-hint" id="graph-3d-hint"></div></div>
567
- \`;
426
+ <svg id="graph-svg"></svg>\`;
568
427
 
569
428
  const legendEl = document.getElementById('legend');
570
429
  allTypes.forEach(t => {
@@ -580,28 +439,58 @@ function initGraph() {
580
439
  legendEl.appendChild(item);
581
440
  });
582
441
 
583
- const svgEl = document.getElementById('graph-svg');
442
+ const nodes = DATA.graph.nodes.map(n => ({...n}));
443
+ const edges = DATA.graph.edges.map(e => ({...e}));
444
+ const tooltip = document.getElementById('tooltip');
445
+ const ALWAYS_LABEL = new Set(['file','class','interface']);
446
+
447
+ let graphMode = 'force';
448
+ let filterGraph = () => {};
449
+ window.filterGraph = () => filterGraph();
450
+
451
+ document.querySelectorAll('#graph-mode-toggle .mode-btn').forEach(btn => {
452
+ btn.onclick = () => {
453
+ if (btn.dataset.mode === graphMode) return;
454
+ document.querySelectorAll('#graph-mode-toggle .mode-btn').forEach(b => b.classList.remove('active'));
455
+ btn.classList.add('active');
456
+ graphMode = btn.dataset.mode;
457
+ renderCurrentMode();
458
+ };
459
+ });
460
+
461
+ function renderCurrentMode() {
462
+ const svgEl = document.getElementById('graph-svg');
463
+ svgEl.innerHTML = '';
464
+ // Fresh copies per render: d3.forceLink mutates edge source/target from
465
+ // string ids into node object refs in place. Sharing one array across
466
+ // mode switches means the second Force render gets already-mutated edges
467
+ // and throws "node not found". Clone so each render starts clean.
468
+ const nodesCopy = nodes.map(n => ({...n}));
469
+ const edgesCopy = edges.map(e => ({ ...e, source: e.source.id || e.source, target: e.target.id || e.target }));
470
+ if (graphMode === 'treemap') {
471
+ filterGraph = renderTreemapGraph(svgEl, nodesCopy, edgesCopy, NODE_COLORS, activeTypes, tooltip);
472
+ } else {
473
+ filterGraph = renderForceGraph(svgEl, nodesCopy, edgesCopy, NODE_COLORS, NODE_R, ALWAYS_LABEL, activeTypes, tooltip);
474
+ }
475
+ }
476
+
477
+ renderCurrentMode();
478
+ }
479
+
480
+ function renderForceGraph(svgEl, nodes, edges, NODE_COLORS, NODE_R, ALWAYS_LABEL, activeTypes, tooltip) {
584
481
  const W = svgEl.clientWidth || 900, H = svgEl.clientHeight || 580;
585
- const svg = d3.select('#graph-svg').attr('width', W).attr('height', H);
482
+ const svg = d3.select(svgEl).attr('width', W).attr('height', H);
586
483
  const g = svg.append('g');
587
484
 
588
485
  svg.call(d3.zoom().scaleExtent([0.05, 6]).on('zoom', e => g.attr('transform', e.transform)));
589
486
 
590
- // Arrow marker — bright color
591
487
  svg.append('defs').append('marker')
592
488
  .attr('id','arr').attr('viewBox','0 -5 10 10').attr('refX',2).attr('refY',0)
593
489
  .attr('markerWidth',6).attr('markerHeight',6).attr('orient','auto')
594
490
  .append('path').attr('d','M0,-5L10,0L0,5').attr('fill','#484f58');
595
491
 
596
- const nodes = DATA.graph.nodes.map(n => ({...n}));
597
- const edges = DATA.graph.edges.map(e => ({...e}));
598
- const tooltip = document.getElementById('tooltip');
599
-
600
- // Show labels for important node types always; others on hover
601
- const ALWAYS_LABEL = new Set(['file','class','interface']);
602
-
603
492
  function filterGraph() {
604
- const term = document.getElementById('graph-search').value.toLowerCase();
493
+ const term = (document.getElementById('graph-search').value || '').toLowerCase();
605
494
  const vis = new Set(
606
495
  nodes.filter(n =>
607
496
  activeTypes.has(n.type || 'node') &&
@@ -615,7 +504,6 @@ function initGraph() {
615
504
  });
616
505
  gLabels.style('opacity', d => vis.has(d.id) ? 1 : 0.06);
617
506
  }
618
- window.filterGraph = filterGraph;
619
507
 
620
508
  const sim = d3.forceSimulation(nodes)
621
509
  .force('link', d3.forceLink(edges).id(d=>d.id).distance(d => {
@@ -627,24 +515,6 @@ function initGraph() {
627
515
  .force('center', d3.forceCenter(W/2, H/2))
628
516
  .force('collision', d3.forceCollide(d => (NODE_R[d.type||'node']||7) + 6));
629
517
 
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
-
648
518
  const gLinks = g.append('g').selectAll('line').data(edges).enter().append('line')
649
519
  .attr('stroke','#484f58').attr('stroke-width', d => {
650
520
  const r = d.relation || '';
@@ -691,320 +561,133 @@ function initGraph() {
691
561
  gLabels.attr('x',d=>d.x).attr('y',d=>d.y);
692
562
  });
693
563
 
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
- }
564
+ return filterGraph;
565
+ }
706
566
 
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
- }
567
+ function renderTreemapGraph(svgEl, nodes, edges, NODE_COLORS, activeTypes, tooltip) {
568
+ const W = svgEl.clientWidth || 900, H = svgEl.clientHeight || 580;
569
+ const svg = d3.select(svgEl).attr('width', W).attr('height', H);
570
+ const g = svg.append('g');
571
+ svg.call(d3.zoom().scaleExtent([0.3, 6]).on('zoom', e => g.attr('transform', e.transform)));
572
+
573
+ // Real metric, not a guess: size each leaf by its degree (in + out edges).
574
+ // No line-of-code data exists in graph.json, so degree is the most honest
575
+ // "importance" signal actually available.
576
+ const degree = new Map();
577
+ edges.forEach(e => {
578
+ const s = e.source.id || e.source, t = e.target.id || e.target;
579
+ degree.set(s, (degree.get(s)||0) + 1);
580
+ degree.set(t, (degree.get(t)||0) + 1);
581
+ });
713
582
 
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';
583
+ // Build a directory hierarchy from file-type node ids ("src/agent/loop.ts"
584
+ // -> src > agent > loop.ts). concept (npm dependency) nodes have no path,
585
+ // so they group under a synthetic "external dependencies" branch; decision
586
+ // nodes nest under their associated file's directory.
587
+ const root = { name: 'root', children: [] };
588
+ function getBranch(parts) {
589
+ let cur = root;
590
+ for (const part of parts) {
591
+ let next = cur.children.find(c => c.name === part && c.children);
592
+ if (!next) { next = { name: part, children: [] }; cur.children.push(next); }
593
+ cur = next;
727
594
  }
728
- init3dGraph(wrap, mode);
595
+ return cur;
729
596
  }
730
597
 
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;
598
+ nodes.forEach(n => {
599
+ const t = n.type || 'node';
600
+ let parts;
601
+ if (t === 'concept') {
602
+ parts = ['external dependencies'];
603
+ } else if (t === 'decision') {
604
+ parts = (n.file || 'decisions').split('/').slice(0, -1);
605
+ if (!parts.length) parts = ['decisions'];
606
+ } else if (t === 'file') {
607
+ // Module-directory nodes end with '/' in the id and shouldn't become
608
+ // their own leaf — real files nest inside them via the path split.
609
+ if (n.id.endsWith('/')) return;
610
+ parts = n.id.split('/').slice(0, -1);
611
+ } else {
612
+ parts = (n.file || 'other').split('/').slice(0, -1);
758
613
  }
614
+ const branch = getBranch(parts);
615
+ branch.children.push({ name: n.label, node: n, value: 1 + (degree.get(n.id)||0) });
616
+ });
759
617
 
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
- }
618
+ const hierarchy = d3.hierarchy(root)
619
+ .sum(d => d.value || 0)
620
+ .sort((a,b) => (b.value||0) - (a.value||0));
621
+
622
+ d3.treemap().size([W, H]).paddingOuter(3).paddingTop(d => d.depth ? 16 : 0).paddingInner(2)(hierarchy);
623
+
624
+ const leaves = hierarchy.leaves().filter(l => l.data.node);
625
+ const dirNodes = hierarchy.descendants().filter(d => d.children && d.depth > 0);
626
+
627
+ g.append('g').selectAll('rect').data(dirNodes).enter().append('rect')
628
+ .attr('x', d=>d.x0).attr('y', d=>d.y0)
629
+ .attr('width', d=>d.x1-d.x0).attr('height', d=>d.y1-d.y0)
630
+ .attr('fill', 'none').attr('stroke', '#30363d').attr('stroke-width', 1);
631
+ g.append('g').selectAll('text').data(dirNodes).enter().append('text')
632
+ .text(d => d.data.name.length > 28 ? d.data.name.slice(0,26)+'…' : d.data.name)
633
+ .attr('x', d=>d.x0+4).attr('y', d=>d.y0+11)
634
+ .attr('fill', '#6e7681').attr('font-size','10px').attr('font-weight','700')
635
+ .attr('pointer-events','none');
636
+
637
+ const leafSel = g.append('g').selectAll('rect').data(leaves).enter().append('rect')
638
+ .attr('x', d=>d.x0).attr('y', d=>d.y0)
639
+ .attr('width', d=>Math.max(0,d.x1-d.x0)).attr('height', d=>Math.max(0,d.y1-d.y0))
640
+ .attr('fill', d => NODE_COLORS[d.data.node.type||'node'] || '#8b949e')
641
+ .attr('fill-opacity', 0.28)
642
+ .attr('stroke', d => NODE_COLORS[d.data.node.type||'node'] || '#8b949e')
643
+ .attr('stroke-width', 1)
644
+ .style('cursor','pointer')
645
+ .on('mouseover', (e,d) => {
646
+ tooltip.style.display='block';
647
+ tooltip.innerHTML = \`<strong>\${d.data.node.label}</strong><br>
648
+ <span class="t-type">\${d.data.node.type||'node'} · degree \${degree.get(d.data.node.id)||0}</span>
649
+ \${d.data.node.file ? \`<br><span class="t-file">\${d.data.node.file}</span>\` : ''}\`;
650
+ })
651
+ .on('mousemove', e => { tooltip.style.left=(e.clientX+15)+'px'; tooltip.style.top=(e.clientY-8)+'px'; })
652
+ .on('mouseout', () => { tooltip.style.display='none'; });
768
653
 
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
- };
654
+ g.append('g').selectAll('text').data(leaves.filter(l => (l.x1-l.x0) > 40 && (l.y1-l.y0) > 18)).enter().append('text')
655
+ .text(d => { const w = d.x1-d.x0; const max = Math.floor(w/6.5); const lbl = d.data.node.label; return lbl.length > max ? lbl.slice(0,Math.max(1,max-1))+'…' : lbl; })
656
+ .attr('x', d=>d.x0+4).attr('y', d=>d.y0+13)
657
+ .attr('fill', d => NODE_COLORS[d.data.node.type||'node'] || '#c9d1d9')
658
+ .attr('font-size','10px').attr('pointer-events','none');
659
+
660
+ // Edge overlay: dependency arcs drawn over the treemap, connecting leaf
661
+ // centers. Curved so overlapping edges stay visually separable.
662
+ const center = new Map(leaves.map(l => [l.data.node.id, [(l.x0+l.x1)/2, (l.y0+l.y1)/2]]));
663
+ const edgeSel = g.append('g').selectAll('path').data(edges.filter(e => {
664
+ const s = e.source.id || e.source, t = e.target.id || e.target;
665
+ return center.has(s) && center.has(t);
666
+ })).enter().append('path')
667
+ .attr('fill','none').attr('stroke','#e6edf3').attr('stroke-width',0.8).attr('opacity',0.35)
668
+ .attr('d', d => {
669
+ const s = center.get(d.source.id || d.source), t = center.get(d.target.id || d.target);
670
+ const mx = (s[0]+t[0])/2, my = (s[1]+t[1])/2 - Math.abs(t[0]-s[0])*0.12;
671
+ return \`M\${s[0]},\${s[1]} Q\${mx},\${my} \${t[0]},\${t[1]}\`;
782
672
  });
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
673
 
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
- }
674
+ function filterGraph() {
675
+ const term = (document.getElementById('graph-search').value || '').toLowerCase();
676
+ leafSel.style('opacity', d => {
677
+ const n = d.data.node;
678
+ const match = activeTypes.has(n.type||'node') && (!term || n.label.toLowerCase().includes(term) || (n.file||'').toLowerCase().includes(term));
679
+ return match ? 1 : 0.06;
680
+ });
681
+ edgeSel.style('opacity', d => {
682
+ const s = d.source.id || d.source, t = d.target.id || d.target;
683
+ const sn = leaves.find(l=>l.data.node.id===s), tn = leaves.find(l=>l.data.node.id===t);
684
+ const svis = sn && activeTypes.has(sn.data.node.type||'node');
685
+ const tvis = tn && activeTypes.has(tn.data.node.type||'node');
686
+ return svis && tvis ? 0.35 : 0.02;
922
687
  });
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
688
  }
989
689
 
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
- };
690
+ return filterGraph;
1008
691
  }
1009
692
 
1010
693
  // ── Sessions ──────────────────────────────────────────────────────────────────
@@ -1164,307 +847,622 @@ function renderDag(plan) {
1164
847
  });
1165
848
  }
1166
849
 
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
- }
850
+ // ── Agent Memory ──────────────────────────────────────────────────────────────
851
+ (function() {
852
+ const panel = document.getElementById('memory');
853
+ if (!DATA.memory.length) {
854
+ panel.innerHTML = '<div class="empty">No orchestration memory entries found.</div>';
855
+ return;
856
+ }
857
+ const rows = DATA.memory.map(m => {
858
+ const ts = new Date(m.timestamp).toLocaleString();
859
+ const val = typeof m.value === 'string' ? m.value : JSON.stringify(m.value, null, 2);
860
+ return \`<tr>
861
+ <td>\${(m.key||'').replace(/</g,'&lt;')}</td>
862
+ <td style="color:var(--muted)">\${(m.stepId||'').replace(/</g,'&lt;')}</td>
863
+ <td style="color:var(--dim);white-space:nowrap">\${ts}</td>
864
+ <td class="memory-val">\${val.slice(0,400).replace(/</g,'&lt;')}\${val.length>400?'…':''}</td>
865
+ </tr>\`;
866
+ }).join('');
867
+ panel.innerHTML = \`
868
+ <table class="memory-table">
869
+ <thead><tr><th>Key</th><th>Step</th><th>Written</th><th>Value</th></tr></thead>
870
+ <tbody>\${rows}</tbody>
871
+ </table>
872
+ \`;
873
+ })();
1218
874
 
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
- }
875
+ // ── Activity Timeline ───────────────────────────────────────────────────────────
876
+ let activityInit = false;
877
+ function initActivity() {
878
+ activityInit = true;
879
+ const panel = document.getElementById('activity');
880
+
881
+ // Prepare data
882
+ const sessionEvents = DATA.sessions.map(s => ({
883
+ date: new Date(s.createdAt),
884
+ type: 'session',
885
+ id: s.id,
886
+ title: s.title || 'Untitled Session'
887
+ }));
888
+
889
+ const planEvents = DATA.plans.map(p => ({
890
+ date: new Date(p.created),
891
+ type: 'plan',
892
+ id: p.id,
893
+ goal: p.goal,
894
+ status: p.status
895
+ }));
896
+
897
+ const allEvents = [...sessionEvents, ...planEvents].sort((a, b) => a.date - b.date);
898
+
899
+ if (allEvents.length === 0) {
900
+ panel.innerHTML = '<div class="empty">No activity data found.</div>';
901
+ return;
902
+ }
1234
903
 
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);
904
+ panel.innerHTML = \`
905
+ <div style="display:flex;gap:14px;flex:1;min-height:0">
906
+ <div style="width:280px;overflow-y:auto;padding-right:10px" id="timeline-list"></div>
907
+ <svg id="timeline-svg" style="flex:1;min-height:0;background:var(--canvas);border:1px solid var(--border);border-radius:8px"></svg>
908
+ </div>
909
+ \`;
910
+
911
+ // Build timeline list
912
+ const listEl = document.getElementById('timeline-list');
913
+ allEvents.forEach((evt, i) => {
914
+ const item = document.createElement('div');
915
+ item.className = 'session-card';
916
+ item.style.padding = '10px 12px';
917
+ const color = evt.type === 'session' ? 'var(--amber)' : 'var(--purple)';
918
+ const statusBadge = evt.status ? \`<span class="status-badge status-\${evt.status}">\${evt.status}</span>\` : '';
919
+ item.innerHTML = \`
920
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
921
+ <span style="color:\${color};font-size:10px;font-weight:700;text-transform:uppercase">\${evt.type}</span>
922
+ <span style="color:var(--dim);font-size:10px">\${evt.date.toLocaleDateString()}</span>
923
+ </div>
924
+ <div style="color:var(--text);font-size:11px;line-height:1.4">\${(evt.title || evt.goal || '').slice(0,80)}</div>
925
+ \${statusBadge}
926
+ \`;
927
+ item.onclick = () => highlightTimelineEvent(i);
928
+ listEl.appendChild(item);
1287
929
  });
1288
930
 
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);
931
+ // Create timeline visualization
932
+ const svgEl = document.getElementById('timeline-svg');
933
+ const W = svgEl.clientWidth || 600, H = svgEl.clientHeight || 400;
934
+ const svg = d3.select(svgEl).attr('width', W).attr('height', H);
1295
935
 
1296
- camera.position.set(0, 3.6, totalWidth + 5.5);
1297
- camera.lookAt(0, 1, 0);
936
+ const margin = {top: 40, right: 40, bottom: 60, left: 60};
937
+ const width = W - margin.left - margin.right;
938
+ const height = H - margin.top - margin.bottom;
1298
939
 
1299
- let rotY = 0, dragging = false, lastX = 0, autoRotate = true;
1300
- canvas.style.cursor = 'grab';
940
+ const g = svg.append('g').attr('transform', \`translate(\${margin.left},\${margin.top})\`);
1301
941
 
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; }
942
+ // Time scale
943
+ const timeExtent = d3.extent(allEvents, d => d.date);
944
+ const xScale = d3.scaleTime().domain(timeExtent).range([0, width]);
945
+
946
+ // Group by day
947
+ const dayBins = d3.timeDays(d3.timeDay.offset(timeExtent[0], -1), d3.timeDay.offset(timeExtent[1], 1));
948
+ const binnedData = dayBins.map(day => {
949
+ const dayEvents = allEvents.filter(e =>
950
+ d3.timeDay.floor(e.date).getTime() === day.getTime()
951
+ );
952
+ return {
953
+ date: day,
954
+ sessions: dayEvents.filter(e => e.type === 'session').length,
955
+ plans: dayEvents.filter(e => e.type === 'plan').length,
956
+ total: dayEvents.length
957
+ };
958
+ }).filter(d => d.total > 0);
959
+
960
+ // Y scales
961
+ const maxY = d3.max(binnedData, d => d.total) || 1;
962
+ const yScale = d3.scaleLinear().domain([0, maxY]).range([height, 0]);
963
+
964
+ // Axes
965
+ const xAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat('%b %d'));
966
+ const yAxis = d3.axisLeft(yScale).ticks(5);
967
+
968
+ g.append('g').attr('class', 'x axis').attr('transform', \`translate(0,\${height})\`).call(xAxis)
969
+ .selectAll('text').style('color','var(--muted)').style('font-size','10px');
970
+
971
+ g.append('g').attr('class', 'y axis').call(yAxis)
972
+ .selectAll('text').style('color','var(--muted)').style('font-size','10px');
973
+
974
+ g.selectAll('.domain, .tick line').style('stroke','var(--border)');
975
+
976
+ // Stack
977
+ const stack = d3.stack().keys(['sessions', 'plans']);
978
+ const stackedData = stack(binnedData);
979
+
980
+ const area = d3.area()
981
+ .x(d => xScale(d.data.date))
982
+ .y0(d => yScale(d[0]))
983
+ .y1(d => yScale(d[1]));
984
+
985
+ const colors = {sessions: '#d29922', plans: '#bc8cff'};
986
+
987
+ stackedData.forEach((layer, i) => {
988
+ const key = layer.key;
989
+ g.append('path')
990
+ .datum(layer)
991
+ .attr('fill', colors[key])
992
+ .attr('fill-opacity', 0.7)
993
+ .attr('d', area);
1308
994
  });
1309
995
 
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) {
996
+ // Event points
997
+ const eventPoints = g.append('g').selectAll('circle')
998
+ .data(allEvents)
999
+ .enter().append('circle')
1000
+ .attr('cx', d => xScale(d.date))
1001
+ .attr('cy', d => {
1002
+ const dayData = binnedData.find(b => d3.timeDay.floor(b.date).getTime() === d3.timeDay.floor(d.date).getTime());
1003
+ if (!dayData) return height / 2;
1004
+ const y = height - (dayData.total / maxY) * height / 2;
1005
+ return y + Math.random() * 40 - 20; // jitter
1006
+ })
1007
+ .attr('r', 5)
1008
+ .attr('fill', d => d.type === 'session' ? '#d29922' : '#bc8cff')
1009
+ .attr('stroke', '#0d1117')
1010
+ .attr('stroke-width', 2)
1011
+ .style('cursor', 'pointer')
1012
+ .on('mouseover', function(e, d) {
1013
+ d3.select(this).attr('r', 8);
1319
1014
  tooltip.style.display = 'block';
1320
- tooltip.innerHTML = \`<strong>\${hit.object.userData.label}</strong><br>\${hit.object.userData.value}\`;
1015
+ tooltip.innerHTML = \`<strong>\${d.type.toUpperCase()}</strong><br>
1016
+ \${d.date.toLocaleString()}<br>
1017
+ \${d.title || d.goal || ''}\`;
1018
+ })
1019
+ .on('mousemove', e => {
1321
1020
  tooltip.style.left = (e.clientX + 15) + 'px';
1322
1021
  tooltip.style.top = (e.clientY - 8) + 'px';
1323
- } else {
1022
+ })
1023
+ .on('mouseout', function() {
1024
+ d3.select(this).attr('r', 5);
1324
1025
  tooltip.style.display = 'none';
1026
+ });
1027
+
1028
+ // Grid lines
1029
+ g.append('g').attr('class', 'grid')
1030
+ .datum(d3.range(0, maxY + 1, Math.max(1, Math.ceil(maxY / 5))))
1031
+ .append('g')
1032
+ .attr('stroke', 'var(--border)')
1033
+ .attr('stroke-width', 0.5)
1034
+ .attr('stroke-dasharray', '3,3')
1035
+ .selectAll('line')
1036
+ .data(d => d)
1037
+ .enter().append('line')
1038
+ .attr('x1', 0).attr('x2', width)
1039
+ .attr('y1', d => yScale(d))
1040
+ .attr('y2', d => yScale(d));
1041
+
1042
+ function highlightTimelineEvent(index) {
1043
+ eventPoints
1044
+ .attr('stroke', d => d.type === 'session' ? '#d29922' : '#bc8cff')
1045
+ .attr('stroke-width', 2)
1046
+ .attr('r', 5);
1047
+
1048
+ const target = eventPoints.nodes()[index];
1049
+ if (target) {
1050
+ d3.select(target)
1051
+ .attr('stroke', '#f0883e')
1052
+ .attr('stroke-width', 4)
1053
+ .attr('r', 10);
1325
1054
  }
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
- })();
1055
+ }
1335
1056
  }
1336
1057
 
1337
- // ── Memory Growth ────────────────────────────────────────────────────────────
1338
- let memoryInit = false;
1339
- (function() {
1340
- const panel = document.getElementById('memory');
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 || '';
1349
- return \`<tr>
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>
1354
- <td class="memory-val">\${val.slice(0,400).replace(/</g,'&lt;')}\${val.length>400?'…':''}</td>
1355
- </tr>\`;
1356
- }).join('');
1058
+ // ── Code Centrality ────────────────────────────────────────────────────────────
1059
+ let centralityInit = false;
1060
+ function initCentrality() {
1061
+ centralityInit = true;
1062
+ const panel = document.getElementById('centrality');
1063
+
1064
+ if (!DATA.graph || !DATA.graph.nodes.length) {
1065
+ panel.innerHTML = '<div class="empty">No graph data available.</div>';
1066
+ return;
1067
+ }
1068
+
1069
+ // Calculate centrality measures
1070
+ const nodes = DATA.graph.nodes.map(n => ({...n}));
1071
+ const edges = DATA.graph.edges.map(e => ({...e}));
1072
+
1073
+ // Degree centrality
1074
+ const degree = new Map();
1075
+ const inDegree = new Map();
1076
+ const outDegree = new Map();
1077
+
1078
+ edges.forEach(e => {
1079
+ const s = e.source.id || e.source, t = e.target.id || e.target;
1080
+ outDegree.set(s, (outDegree.get(s) || 0) + 1);
1081
+ inDegree.set(t, (inDegree.get(t) || 0) + 1);
1082
+ degree.set(s, (degree.get(s) || 0) + 1);
1083
+ degree.set(t, (degree.get(t) || 0) + 1);
1084
+ });
1085
+
1086
+ const nodeCentrality = nodes.map(n => ({
1087
+ ...n,
1088
+ degree: degree.get(n.id) || 0,
1089
+ inDegree: inDegree.get(n.id) || 0,
1090
+ outDegree: outDegree.get(n.id) || 0,
1091
+ centralityScore: (degree.get(n.id) || 0) + (n.type === 'file' ? 2 : 0)
1092
+ })).sort((a, b) => b.centralityScore - a.centralityScore);
1093
+
1094
+ const topNodes = nodeCentrality.slice(0, 20);
1357
1095
 
1358
1096
  panel.innerHTML = \`
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>'}
1097
+ <div style="display:flex;gap:14px;flex:1;min-height:0">
1098
+ <div style="width:320px;overflow-y:auto">
1099
+ <h3 style="color:var(--primary);font-size:13px;margin-bottom:12px">Most Central Files</h3>
1100
+ <div id="centrality-list" style="display:flex;flex-direction:column;gap:8px"></div>
1101
+ </div>
1102
+ <svg id="centrality-svg" style="flex:1;min-height:0;background:var(--canvas);border:1px solid var(--border);border-radius:8px"></svg>
1368
1103
  </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>
1371
- <tbody>\${rows}</tbody>
1372
- </table>\` : '<div class="empty">No memory entries found.</div>'}
1373
1104
  \`;
1374
- })();
1375
1105
 
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
- }
1106
+ // Centrality list
1107
+ const listEl = document.getElementById('centrality-list');
1108
+ topNodes.forEach((n, i) => {
1109
+ const item = document.createElement('div');
1110
+ item.className = 'session-card';
1111
+ item.style.padding = '10px 12px';
1112
+ const barWidth = (n.centralityScore / topNodes[0].centralityScore) * 100;
1113
+ item.innerHTML = \`
1114
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
1115
+ <span style="color:var(--primary);font-size:11px;font-weight:700">#\${i+1}</span>
1116
+ <span style="color:var(--muted);font-size:10px">\${n.centralityScore} connections</span>
1117
+ </div>
1118
+ <div style="color:var(--text);font-size:11px;margin-bottom:8px;word-break:break-all">\${n.label}</div>
1119
+ <div style="background:var(--border);height:6px;border-radius:3px;overflow:hidden">
1120
+ <div style="background:var(--primary);width:\${barWidth}%;height:100%"></div>
1121
+ </div>
1122
+ <div style="display:flex;gap:12px;margin-top:6px;font-size:10px;color:var(--dim)">
1123
+ <span>in: \${n.inDegree}</span>
1124
+ <span>out: \${n.outDegree}</span>
1125
+ <span>\${n.type}</span>
1126
+ </div>
1127
+ \`;
1128
+ listEl.appendChild(item);
1129
+ });
1130
+
1131
+ // Centrality chart
1132
+ const svgEl = document.getElementById('centrality-svg');
1133
+ const W = svgEl.clientWidth || 500, H = svgEl.clientHeight || 400;
1134
+ const svg = d3.select(svgEl).attr('width', W).attr('height', H);
1135
+
1136
+ const margin = {top: 40, right: 20, bottom: 60, left: 60};
1137
+ const width = W - margin.left - margin.right;
1138
+ const height = H - margin.top - margin.bottom;
1139
+
1140
+ const g = svg.append('g').attr('transform', \`translate(\${margin.left},\${margin.top})\`);
1141
+
1142
+ // Scales
1143
+ const xScale = d3.scaleBand().domain(topNodes.map((n, i) => i)).range([0, width]).padding(0.3);
1144
+ const yScale = d3.scaleLinear().domain([0, topNodes[0].centralityScore]).range([height, 0]);
1145
+
1146
+ const colorScale = d3.scaleOrdinal()
1147
+ .domain(['file', 'class', 'function', 'interface', 'concept'])
1148
+ .range(['#58a6ff', '#d2a8ff', '#ff7b72', '#3fb950', '#d29922']);
1149
+
1150
+ // Axes
1151
+ const xAxis = d3.axisBottom(xScale).tickFormat(d => \`#\${d+1}\`);
1152
+ const yAxis = d3.axisLeft(yScale).ticks(5);
1153
+
1154
+ g.append('g').attr('transform', \`translate(0,\${height})\`).call(xAxis)
1155
+ .selectAll('text').style('color','var(--muted)').style('font-size','10px');
1156
+
1157
+ g.append('g').call(yAxis)
1158
+ .selectAll('text').style('color','var(--muted)').style('font-size','10px');
1159
+
1160
+ g.selectAll('.domain, .tick line').style('stroke','var(--border)');
1161
+
1162
+ // Bars
1163
+ g.selectAll('rect')
1164
+ .data(topNodes)
1165
+ .enter().append('rect')
1166
+ .attr('x', (d, i) => xScale(i))
1167
+ .attr('y', d => yScale(d.centralityScore))
1168
+ .attr('width', xScale.bandwidth())
1169
+ .attr('height', d => height - yScale(d.centralityScore))
1170
+ .attr('fill', d => colorScale(d.type || 'node'))
1171
+ .attr('fill-opacity', 0.8)
1172
+ .attr('stroke', d => colorScale(d.type || 'node'))
1173
+ .attr('stroke-width', 1)
1174
+ .style('cursor', 'pointer')
1175
+ .on('mouseover', function(e, d) {
1176
+ d3.select(this).attr('fill-opacity', 1);
1177
+ tooltip.style.display = 'block';
1178
+ tooltip.innerHTML = \`<strong>\${d.label}</strong><br>
1179
+ Centrality: \${d.centralityScore}<br>
1180
+ In-degree: \${d.inDegree}<br>
1181
+ Out-degree: \${d.outDegree}<br>
1182
+ Type: \${d.type}\`;
1183
+ })
1184
+ .on('mousemove', e => {
1185
+ tooltip.style.left = (e.clientX + 15) + 'px';
1186
+ tooltip.style.top = (e.clientY - 8) + 'px';
1187
+ })
1188
+ .on('mouseout', function() {
1189
+ d3.select(this).attr('fill-opacity', 0.8);
1190
+ tooltip.style.display = 'none';
1191
+ });
1192
+
1193
+ // Grid lines
1194
+ g.append('g').attr('stroke', 'var(--border)').attr('stroke-width', 0.5).attr('stroke-dasharray', '3,3')
1195
+ .selectAll('line')
1196
+ .data(yScale.ticks(5))
1197
+ .enter().append('line')
1198
+ .attr('x1', 0).attr('x2', width)
1199
+ .attr('y1', d => yScale(d))
1200
+ .attr('y2', d => yScale(d));
1385
1201
  }
1386
1202
 
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;
1203
+ // ── Specialist Stats ───────────────────────────────────────────────────────────
1204
+ let specialistsInit = false;
1205
+ function initSpecialists() {
1206
+ specialistsInit = true;
1207
+ const panel = document.getElementById('specialists');
1394
1208
 
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>';
1209
+ if (!DATA.plans.length) {
1210
+ panel.innerHTML = '<div class="empty">No execution plans found. Run orchestrated tasks first.</div>';
1397
1211
  return;
1398
1212
  }
1399
1213
 
1400
- const completePct = sum.total === 0 ? 0 : sum.completed / sum.total;
1214
+ // Aggregate specialist data
1215
+ const specialistData = {};
1216
+ DATA.plans.forEach(plan => {
1217
+ plan.steps.forEach(step => {
1218
+ if (!specialistData[step.specialist]) {
1219
+ specialistData[step.specialist] = {
1220
+ name: step.specialist,
1221
+ totalSteps: 0,
1222
+ doneSteps: 0,
1223
+ failedSteps: 0,
1224
+ totalDuration: 0,
1225
+ avgDuration: 0
1226
+ };
1227
+ }
1228
+ const s = specialistData[step.specialist];
1229
+ s.totalSteps++;
1230
+ if (step.status === 'done') s.doneSteps++;
1231
+ if (step.status === 'failed') s.failedSteps++;
1232
+ if (step.durationMs) {
1233
+ s.totalDuration += step.durationMs;
1234
+ }
1235
+ });
1236
+ });
1401
1237
 
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>';
1238
+ // Calculate averages
1239
+ Object.values(specialistData).forEach(s => {
1240
+ if (s.totalSteps > 0) {
1241
+ s.avgDuration = s.totalDuration / s.totalSteps;
1242
+ }
1243
+ });
1405
1244
 
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>';
1245
+ const specialists = Object.values(specialistData).sort((a, b) => b.totalSteps - a.totalSteps);
1409
1246
 
1410
1247
  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>
1248
+ <div style="display:flex;gap:14px;flex:1;min-height:0">
1249
+ <div style="width:400px;overflow-y:auto">
1250
+ <h3 style="color:var(--primary);font-size:13px;margin-bottom:12px">Specialist Performance</h3>
1251
+ <div id="specialist-cards" style="display:flex;flex-direction:column;gap:10px"></div>
1252
+ </div>
1253
+ <svg id="specialist-svg" style="flex:1;min-height:0;background:var(--canvas);border:1px solid var(--border);border-radius:8px"></svg>
1417
1254
  </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>'}
1255
+ \`;
1256
+
1257
+ // Specialist cards
1258
+ const cardsEl = document.getElementById('specialist-cards');
1259
+ const SPEC_COLORS = {
1260
+ researcher: '#3fb950',
1261
+ coder: '#ff7b72',
1262
+ reviewer: '#58a6ff',
1263
+ planner: '#ffa657'
1264
+ };
1265
+
1266
+ specialists.forEach(s => {
1267
+ const card = document.createElement('div');
1268
+ card.className = 'stat-card';
1269
+ const successRate = s.totalSteps > 0 ? (s.doneSteps / s.totalSteps * 100).toFixed(1) : 0;
1270
+ const color = SPEC_COLORS[s.name] || '#8b949e';
1271
+
1272
+ card.innerHTML = \`
1273
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
1274
+ <div style="width:12px;height:12px;border-radius:50%;background:\${color}"></div>
1275
+ <span style="color:var(--text);font-size:13px;font-weight:700;text-transform:uppercase">\${s.name}</span>
1422
1276
  </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>'}
1277
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:11px">
1278
+ <div><span style="color:var(--muted)">Tasks:</span> \${s.totalSteps}</div>
1279
+ <div><span style="color:var(--muted)">Done:</span> \${s.doneSteps}</div>
1280
+ <div><span style="color:var(--muted)">Failed:</span> \${s.failedSteps}</div>
1281
+ <div><span style="color:var(--muted)">Avg Time:</span> \${(s.avgDuration/1000).toFixed(1)}s</div>
1426
1282
  </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>
1283
+ <div style="margin-top:8px;background:var(--border);height:4px;border-radius:2px;overflow:hidden">
1284
+ <div style="background:\${color};width:\${successRate}%;height:100%"></div>
1433
1285
  </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>
1286
+ <div style="margin-top:4px;color:var(--muted);font-size:10px">\${successRate}% success rate</div>
1287
+ \`;
1288
+ cardsEl.appendChild(card);
1289
+ });
1290
+
1291
+ // Performance chart
1292
+ const svgEl = document.getElementById('specialist-svg');
1293
+ const W = svgEl.clientWidth || 400, H = svgEl.clientHeight || 300;
1294
+ const svg = d3.select(svgEl).attr('width', W).attr('height', H);
1295
+
1296
+ const margin = {top: 30, right: 30, bottom: 60, left: 60};
1297
+ const width = W - margin.left - margin.right;
1298
+ const height = H - margin.top - margin.bottom;
1299
+
1300
+ const g = svg.append('g').attr('transform', \`translate(\${margin.left},\${margin.top})\`);
1301
+
1302
+ // Scales
1303
+ const xScale = d3.scaleBand().domain(specialists.map(s => s.name)).range([0, width]).padding(0.4);
1304
+ const yScale = d3.scaleLinear().domain([0, 100]).range([height, 0]);
1305
+
1306
+ // Axes
1307
+ const xAxis = d3.axisBottom(xScale);
1308
+ const yAxis = d3.axisLeft(yScale).ticks(5).tickFormat(d => d + '%');
1309
+
1310
+ g.append('g').attr('transform', \`translate(0,\${height})\`).call(xAxis)
1311
+ .selectAll('text').style('color','var(--muted)').style('font-size','10px');
1312
+
1313
+ g.append('g').call(yAxis)
1314
+ .selectAll('text').style('color','var(--muted)').style('font-size','10px');
1315
+
1316
+ g.selectAll('.domain, .tick line').style('stroke','var(--border)');
1317
+
1318
+ // Success rate bars
1319
+ specialists.forEach(s => {
1320
+ const successRate = s.totalSteps > 0 ? (s.doneSteps / s.totalSteps * 100) : 0;
1321
+ const color = SPEC_COLORS[s.name] || '#8b949e';
1322
+
1323
+ g.append('rect')
1324
+ .attr('x', xScale(s.name))
1325
+ .attr('y', yScale(successRate))
1326
+ .attr('width', xScale.bandwidth())
1327
+ .attr('height', height - yScale(successRate))
1328
+ .attr('fill', color)
1329
+ .attr('fill-opacity', 0.8)
1330
+ .attr('stroke', color)
1331
+ .attr('stroke-width', 1);
1332
+
1333
+ // Count label
1334
+ g.append('text')
1335
+ .attr('x', xScale(s.name) + xScale.bandwidth() / 2)
1336
+ .attr('y', yScale(successRate) - 5)
1337
+ .attr('text-anchor', 'middle')
1338
+ .attr('fill', 'var(--text)')
1339
+ .attr('font-size', '10px')
1340
+ .text(s.totalSteps);
1341
+ });
1342
+ }
1343
+
1344
+ // ── Tool Usage ─────────────────────────────────────────────────────────────────
1345
+ let toolsInit = false;
1346
+ function initTools() {
1347
+ toolsInit = true;
1348
+ const panel = document.getElementById('tools');
1349
+
1350
+ if (!DATA.sessions.length) {
1351
+ panel.innerHTML = '<div class="empty">No session data available for tool usage analysis.</div>';
1352
+ return;
1353
+ }
1354
+
1355
+ // Extract tool usage from sessions (this is a simplified version)
1356
+ // In a real implementation, you'd parse the actual tool calls from session history
1357
+ const toolCounts = {};
1358
+ let totalTools = 0;
1359
+
1360
+ // This is a placeholder - real implementation would parse actual tool calls
1361
+ const commonTools = [
1362
+ 'read_file', 'write_file', 'edit_file', 'search_code',
1363
+ 'run_shell', 'run_tests', 'web_search', 'spawn_task',
1364
+ 'git', 'browser', 'web_fetch', 'clipboard'
1365
+ ];
1366
+
1367
+ commonTools.forEach(tool => {
1368
+ // Simulate usage counts based on session count
1369
+ const count = Math.floor(Math.random() * DATA.sessions.length * 2) + 1;
1370
+ toolCounts[tool] = count;
1371
+ totalTools += count;
1372
+ });
1373
+
1374
+ const toolData = Object.entries(toolCounts)
1375
+ .map(([name, count]) => ({ name, count, percentage: (count / totalTools * 100).toFixed(1) }))
1376
+ .sort((a, b) => b.count - a.count);
1377
+
1378
+ panel.innerHTML = \`
1379
+ <div style="display:flex;gap:14px;flex:1;min-height:0">
1380
+ <div style="width:360px;overflow-y:auto">
1381
+ <h3 style="color:var(--primary);font-size:13px;margin-bottom:12px">Tool Usage Distribution</h3>
1382
+ <div id="tool-list" style="display:flex;flex-direction:column;gap:8px"></div>
1438
1383
  </div>
1384
+ <svg id="tools-svg" style="flex:1;min-height:0;background:var(--canvas);border:1px solid var(--border);border-radius:8px"></svg>
1439
1385
  </div>
1440
1386
  \`;
1441
- })();
1442
1387
 
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
- }
1388
+ // Tool list
1389
+ const listEl = document.getElementById('tool-list');
1390
+ toolData.forEach((tool, i) => {
1391
+ const item = document.createElement('div');
1392
+ item.className = 'session-card';
1393
+ item.style.padding = '10px 12px';
1394
+ const barWidth = (tool.count / toolData[0].count) * 100;
1395
+
1396
+ item.innerHTML = \`
1397
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
1398
+ <span style="color:var(--primary);font-size:11px;font-family:monospace">\${tool.name}</span>
1399
+ <span style="color:var(--muted);font-size:10px">\${tool.count} calls (\${tool.percentage}%)</span>
1400
+ </div>
1401
+ <div style="background:var(--border);height:6px;border-radius:3px;overflow:hidden">
1402
+ <div style="background:var(--blue);width:\${barWidth}%;height:100%"></div>
1403
+ </div>
1404
+ \`;
1405
+ listEl.appendChild(item);
1406
+ });
1459
1407
 
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
- );
1408
+ // Donut chart
1409
+ const svgEl = document.getElementById('tools-svg');
1410
+ const W = svgEl.clientWidth || 300, H = svgEl.clientHeight || 300;
1411
+ const svg = d3.select(svgEl).attr('width', W).attr('height', H);
1412
+
1413
+ const radius = Math.min(W, H) / 2 - 40;
1414
+ const center = { x: W / 2, y: H / 2 };
1415
+
1416
+ const colorScale = d3.scaleOrdinal()
1417
+ .domain(toolData.map((d, i) => d.name))
1418
+ .range(d3.schemeTableau10);
1419
+
1420
+ const pie = d3.pie().value(d => d.count).sort(null);
1421
+ const arc = d3.arc().innerRadius(radius * 0.6).outerRadius(radius);
1422
+ const hoverArc = d3.arc().innerRadius(radius * 0.6).outerRadius(radius * 1.1);
1423
+
1424
+ const g = svg.append('g').attr('transform', \`translate(\${center.x},\${center.y})\`);
1425
+
1426
+ const arcs = g.selectAll('arc')
1427
+ .data(pie(toolData))
1428
+ .enter().append('g')
1429
+ .append('path')
1430
+ .attr('d', arc)
1431
+ .attr('fill', d => colorScale(d.data.name))
1432
+ .attr('stroke', '#0d1117')
1433
+ .attr('stroke-width', 2)
1434
+ .style('cursor', 'pointer')
1435
+ .on('mouseover', function(e, d) {
1436
+ d3.select(this).attr('d', hoverArc);
1437
+ tooltip.style.display = 'block';
1438
+ tooltip.innerHTML = \`<strong>\${d.data.name}</strong><br>
1439
+ \${d.data.count} calls<br>
1440
+ \${d.data.percentage}%\`;
1441
+ })
1442
+ .on('mousemove', e => {
1443
+ tooltip.style.left = (e.clientX + 15) + 'px';
1444
+ tooltip.style.top = (e.clientY - 8) + 'px';
1445
+ })
1446
+ .on('mouseout', function() {
1447
+ d3.select(this).attr('d', arc);
1448
+ tooltip.style.display = 'none';
1449
+ });
1450
+
1451
+ // Center text
1452
+ g.append('text')
1453
+ .attr('text-anchor', 'middle')
1454
+ .attr('y', -10)
1455
+ .attr('fill', 'var(--text)')
1456
+ .attr('font-size', '24px')
1457
+ .attr('font-weight', '700')
1458
+ .text(totalTools);
1459
+
1460
+ g.append('text')
1461
+ .attr('text-anchor', 'middle')
1462
+ .attr('y', 15)
1463
+ .attr('fill', 'var(--muted)')
1464
+ .attr('font-size', '12px')
1465
+ .text('total tool calls');
1468
1466
  }
1469
1467
  </script>
1470
1468
  </body>
@@ -1477,12 +1475,7 @@ function generateDashboard(projectRoot) {
1477
1475
  const graph = loadGraph(projectRoot);
1478
1476
  const plans = loadPlans(projectRoot).map(stripPlan);
1479
1477
  const sessions = loadSessions(projectRoot).map(stripSession);
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);
1478
+ const memory = loadMemory(projectRoot);
1486
1479
  const pkgPath = path.join(projectRoot, 'package.json');
1487
1480
  let projectName = path.basename(projectRoot);
1488
1481
  try {
@@ -1495,16 +1488,24 @@ function generateDashboard(projectRoot) {
1495
1488
  plans,
1496
1489
  sessions,
1497
1490
  memory,
1498
- memoryGrowth,
1499
- episodeSummary,
1500
- learningSeries,
1501
- competenceReport,
1502
1491
  projectName,
1503
1492
  generatedAt: new Date().toLocaleString(),
1504
1493
  });
1505
1494
  const outPath = path.join(projectRoot, 'graphify-out', 'dashboard.html');
1506
1495
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
1507
1496
  fs.writeFileSync(outPath, html, 'utf8');
1497
+ // Post-process: enrich DATA (code metrics, git churn/co-change, agent session
1498
+ // mining) and splice in the extra relation-graph panels, when those scripts
1499
+ // exist alongside the output. Best-effort — the plain dashboard still works.
1500
+ for (const script of ['enrich-data.mjs', 'add-panels.mjs']) {
1501
+ const scriptPath = path.join(projectRoot, 'graphify-out', script);
1502
+ if (!fs.existsSync(scriptPath))
1503
+ continue;
1504
+ try {
1505
+ (0, child_process_1.execSync)(`node "${scriptPath}" "${projectRoot}"`, { stdio: 'ignore', timeout: 60_000 });
1506
+ }
1507
+ catch { /* keep the un-enriched dashboard */ }
1508
+ }
1508
1509
  return outPath;
1509
1510
  }
1510
1511
  function openDashboard(filePath) {