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.
- package/README.md +47 -164
- package/dist/agent/affect.d.ts +13 -0
- package/dist/agent/affect.js +34 -0
- package/dist/agent/affect.js.map +1 -0
- package/dist/agent/compactor.d.ts +27 -4
- package/dist/agent/compactor.js +185 -54
- package/dist/agent/compactor.js.map +1 -1
- package/dist/agent/confess.d.ts +35 -0
- package/dist/agent/confess.js +296 -0
- package/dist/agent/confess.js.map +1 -0
- package/dist/agent/context.d.ts +0 -4
- package/dist/agent/context.js +0 -38
- package/dist/agent/context.js.map +1 -1
- package/dist/agent/domain-expertise.d.ts +22 -0
- package/dist/agent/domain-expertise.js +110 -0
- package/dist/agent/domain-expertise.js.map +1 -0
- package/dist/agent/executive-queue.d.ts +26 -0
- package/dist/agent/executive-queue.js +50 -0
- package/dist/agent/executive-queue.js.map +1 -0
- package/dist/agent/generational-flush.d.ts +28 -0
- package/dist/agent/generational-flush.js +48 -0
- package/dist/agent/generational-flush.js.map +1 -0
- package/dist/agent/loop-profile.d.ts +50 -0
- package/dist/agent/loop-profile.js +88 -0
- package/dist/agent/loop-profile.js.map +1 -0
- package/dist/agent/loop.d.ts +14 -5
- package/dist/agent/loop.js +167 -39
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/memory-consolidate.d.ts +14 -0
- package/dist/agent/memory-consolidate.js +141 -0
- package/dist/agent/memory-consolidate.js.map +1 -0
- package/dist/agent/mixture.d.ts +33 -0
- package/dist/agent/mixture.js +138 -0
- package/dist/agent/mixture.js.map +1 -0
- package/dist/agent/spawner.js +2 -2
- package/dist/agent/spawner.js.map +1 -1
- package/dist/agent/system-prompt.d.ts +1 -1
- package/dist/agent/system-prompt.js +50 -22
- package/dist/agent/system-prompt.js.map +1 -1
- package/dist/agent/unified-memory.d.ts +15 -0
- package/dist/agent/unified-memory.js +164 -0
- package/dist/agent/unified-memory.js.map +1 -0
- package/dist/checkpoints/engine.d.ts +44 -0
- package/dist/checkpoints/engine.js +324 -0
- package/dist/checkpoints/engine.js.map +1 -0
- package/dist/cli/command-palette.d.ts +26 -0
- package/dist/cli/command-palette.js +133 -0
- package/dist/cli/command-palette.js.map +1 -0
- package/dist/cli/context-health.d.ts +51 -0
- package/dist/cli/context-health.js +187 -0
- package/dist/cli/context-health.js.map +1 -0
- package/dist/cli/diamond.d.ts +67 -0
- package/dist/cli/diamond.js +222 -12
- package/dist/cli/diamond.js.map +1 -1
- package/dist/cli/dic.d.ts +2 -0
- package/dist/cli/dic.js +106 -0
- package/dist/cli/dic.js.map +1 -0
- package/dist/cli/diff-view.d.ts +18 -0
- package/dist/cli/diff-view.js +114 -0
- package/dist/cli/diff-view.js.map +1 -0
- package/dist/cli/display.d.ts +18 -4
- package/dist/cli/display.js +74 -151
- package/dist/cli/display.js.map +1 -1
- package/dist/cli/help-data.d.ts +7 -0
- package/dist/cli/help-data.js +80 -0
- package/dist/cli/help-data.js.map +1 -0
- package/dist/cli/index.js +1002 -852
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/markdown.d.ts +5 -0
- package/dist/cli/markdown.js +128 -0
- package/dist/cli/markdown.js.map +1 -0
- package/dist/cli/model-select.d.ts +39 -0
- package/dist/cli/model-select.js +65 -0
- package/dist/cli/model-select.js.map +1 -0
- package/dist/cli/tui.d.ts +34 -0
- package/dist/cli/tui.js +1176 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/config/defaults.d.ts +6 -17
- package/dist/config/defaults.js +25 -46
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/project-config.d.ts +2 -6
- package/dist/config/project-config.js +2 -2
- package/dist/config/project-config.js.map +1 -1
- package/dist/doctor/checks.d.ts +18 -0
- package/dist/doctor/checks.js +524 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/doctor/index.d.ts +4 -0
- package/dist/doctor/index.js +229 -0
- package/dist/doctor/index.js.map +1 -0
- package/dist/doctor/repair.d.ts +19 -0
- package/dist/doctor/repair.js +126 -0
- package/dist/doctor/repair.js.map +1 -0
- package/dist/doctor/types.d.ts +38 -0
- package/dist/doctor/types.js +10 -0
- package/dist/doctor/types.js.map +1 -0
- package/dist/dream/dream.d.ts +79 -59
- package/dist/dream/dream.js +324 -144
- package/dist/dream/dream.js.map +1 -1
- package/dist/dream/episode.d.ts +17 -0
- package/dist/dream/episode.js +116 -0
- package/dist/dream/episode.js.map +1 -0
- package/dist/kanban/engine.d.ts +26 -0
- package/dist/kanban/engine.js +208 -0
- package/dist/kanban/engine.js.map +1 -0
- package/dist/kanban/types.d.ts +19 -0
- package/dist/kanban/types.js +9 -1
- package/dist/kanban/types.js.map +1 -1
- package/dist/machina/render-html.js +4 -2
- package/dist/machina/render-html.js.map +1 -1
- package/dist/machina/render-terminal.js +11 -4
- package/dist/machina/render-terminal.js.map +1 -1
- package/dist/machina/repair.d.ts +23 -0
- package/dist/machina/repair.js +98 -0
- package/dist/machina/repair.js.map +1 -0
- package/dist/machina/spec.js +12 -12
- package/dist/machina/spec.js.map +1 -1
- package/dist/machina/verify.d.ts +13 -1
- package/dist/machina/verify.js +12 -2
- package/dist/machina/verify.js.map +1 -1
- package/dist/mining/extract.d.ts +34 -0
- package/dist/mining/extract.js +229 -0
- package/dist/mining/extract.js.map +1 -0
- package/dist/mining/refine.d.ts +28 -0
- package/dist/mining/refine.js +256 -0
- package/dist/mining/refine.js.map +1 -0
- package/dist/perception/extractor.js +5 -3
- package/dist/perception/extractor.js.map +1 -1
- package/dist/perception/graph-store.d.ts +0 -11
- package/dist/perception/graph-store.js +0 -33
- package/dist/perception/graph-store.js.map +1 -1
- package/dist/perception/index.d.ts +1 -1
- package/dist/perception/index.js +1 -2
- package/dist/perception/index.js.map +1 -1
- package/dist/plugins/commands.d.ts +38 -0
- package/dist/plugins/commands.js +134 -0
- package/dist/plugins/commands.js.map +1 -0
- package/dist/plugins/frontmatter.d.ts +14 -0
- package/dist/plugins/frontmatter.js +78 -0
- package/dist/plugins/frontmatter.js.map +1 -0
- package/dist/plugins/hooks.d.ts +12 -0
- package/dist/plugins/hooks.js +148 -0
- package/dist/plugins/hooks.js.map +1 -0
- package/dist/plugins/loader.d.ts +7 -0
- package/dist/plugins/loader.js +255 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/market.d.ts +23 -0
- package/dist/plugins/market.js +281 -0
- package/dist/plugins/market.js.map +1 -0
- package/dist/plugins/types.d.ts +90 -0
- package/dist/plugins/types.js +20 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/providers/anthropic-oauth-draft.d.ts +119 -0
- package/dist/providers/anthropic-oauth-draft.js +414 -0
- package/dist/providers/anthropic-oauth-draft.js.map +1 -0
- package/dist/providers/anthropic.d.ts +4 -1
- package/dist/providers/anthropic.js +35 -7
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +43 -19
- package/dist/providers/factory.js +183 -129
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/google.js +9 -22
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/live-models.d.ts +23 -0
- package/dist/providers/live-models.js +145 -0
- package/dist/providers/live-models.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +1 -12
- package/dist/providers/openai-compatible.js +71 -108
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/resilient-factory.js +1 -1
- package/dist/providers/resilient-factory.js.map +1 -1
- package/dist/repl/queue.d.ts +22 -0
- package/dist/repl/queue.js +165 -0
- package/dist/repl/queue.js.map +1 -0
- package/dist/repl/side-channel.d.ts +23 -0
- package/dist/repl/side-channel.js +63 -0
- package/dist/repl/side-channel.js.map +1 -0
- package/dist/research/council.d.ts +2 -0
- package/dist/research/council.js +29 -4
- package/dist/research/council.js.map +1 -1
- package/dist/ruby/alternator.d.ts +11 -30
- package/dist/ruby/alternator.js +59 -38
- package/dist/ruby/alternator.js.map +1 -1
- package/dist/ruby/episode-capture.d.ts +0 -10
- package/dist/ruby/episode-capture.js +0 -33
- package/dist/ruby/episode-capture.js.map +1 -1
- package/dist/ruby/types.js +1 -1
- package/dist/ruby/types.js.map +1 -1
- package/dist/safety/path-jail.d.ts +17 -0
- package/dist/safety/path-jail.js +100 -0
- package/dist/safety/path-jail.js.map +1 -0
- package/dist/safety/permissions.d.ts +12 -21
- package/dist/safety/permissions.js +81 -216
- package/dist/safety/permissions.js.map +1 -1
- package/dist/safety/ssrf.d.ts +29 -0
- package/dist/safety/ssrf.js +185 -0
- package/dist/safety/ssrf.js.map +1 -0
- package/dist/server/index.js +64 -10
- package/dist/server/index.js.map +1 -1
- package/dist/setup/first-run.d.ts +0 -24
- package/dist/setup/first-run.js +10 -344
- package/dist/setup/first-run.js.map +1 -1
- package/dist/setup/key-store.d.ts +12 -0
- package/dist/setup/key-store.js +108 -0
- package/dist/setup/key-store.js.map +1 -0
- package/dist/setup/provider-registry.js +31 -18
- package/dist/setup/provider-registry.js.map +1 -1
- package/dist/setup/provider-test.d.ts +7 -0
- package/dist/setup/provider-test.js +73 -10
- package/dist/setup/provider-test.js.map +1 -1
- package/dist/setup/provider-wizard.d.ts +4 -1
- package/dist/setup/provider-wizard.js +92 -26
- package/dist/setup/provider-wizard.js.map +1 -1
- package/dist/setup/xiaomi.d.ts +1 -1
- package/dist/setup/xiaomi.js +3 -3
- package/dist/setup/xiaomi.js.map +1 -1
- package/dist/tools/browser.js +14 -10
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/clipboard.js +7 -1
- package/dist/tools/clipboard.js.map +1 -1
- package/dist/tools/dictate.d.ts +60 -0
- package/dist/tools/dictate.js +983 -0
- package/dist/tools/dictate.js.map +1 -0
- package/dist/tools/dictate_patched.js +1057 -0
- package/dist/tools/edit-file.js +10 -2
- package/dist/tools/edit-file.js.map +1 -1
- package/dist/tools/http-request.js +14 -4
- package/dist/tools/http-request.js.map +1 -1
- package/dist/tools/image-read.js +34 -2
- package/dist/tools/image-read.js.map +1 -1
- package/dist/tools/index.d.ts +16 -1
- package/dist/tools/index.js +59 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp.js +12 -0
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/read-file.js +10 -14
- package/dist/tools/read-file.js.map +1 -1
- package/dist/tools/telegram-audio-policy.d.ts +30 -0
- package/dist/tools/telegram-audio-policy.js +49 -0
- package/dist/tools/telegram-audio-policy.js.map +1 -0
- package/dist/tools/telegram-bot.js +1136 -537
- package/dist/tools/telegram-bot.js.map +1 -1
- package/dist/tools/telegram-voice.d.ts +18 -5
- package/dist/tools/telegram-voice.js +54 -12
- package/dist/tools/telegram-voice.js.map +1 -1
- package/dist/tools/telegram.d.ts +3 -3
- package/dist/tools/telegram.js +5 -84
- package/dist/tools/telegram.js.map +1 -1
- package/dist/tools/tools.d.ts +2 -14
- package/dist/tools/tools.js +79 -144
- package/dist/tools/tools.js.map +1 -1
- package/dist/tools/web-fetch.js +39 -7
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/web-search.d.ts +0 -20
- package/dist/tools/web-search.js +40 -137
- package/dist/tools/web-search.js.map +1 -1
- package/dist/util/errors.js +1 -15
- package/dist/util/errors.js.map +1 -1
- package/dist/util/json-repair.d.ts +11 -0
- package/dist/util/json-repair.js +31 -0
- package/dist/util/json-repair.js.map +1 -0
- package/dist/util/rate-limiter.js +2 -8
- package/dist/util/rate-limiter.js.map +1 -1
- package/dist/viz/index.js +793 -792
- package/dist/viz/index.js.map +1 -1
- 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
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
|
459
|
-
<button onclick="showPanel('
|
|
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"
|
|
464
|
-
<div id="sessions"
|
|
465
|
-
<div id="plans"
|
|
466
|
-
<div id="memory"
|
|
467
|
-
<div id="
|
|
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'
|
|
480
|
-
if (id === 'plans'
|
|
481
|
-
if (id === '
|
|
482
|
-
if (id === '
|
|
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,
|
|
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="
|
|
557
|
-
<
|
|
558
|
-
<
|
|
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"
|
|
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
|
|
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(
|
|
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
|
-
|
|
695
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
595
|
+
return cur;
|
|
729
596
|
}
|
|
730
597
|
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
1168
|
-
function
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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,'<')}</td>
|
|
862
|
+
<td style="color:var(--muted)">\${(m.stepId||'').replace(/</g,'<')}</td>
|
|
863
|
+
<td style="color:var(--dim);white-space:nowrap">\${ts}</td>
|
|
864
|
+
<td class="memory-val">\${val.slice(0,400).replace(/</g,'<')}\${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
|
-
// ──
|
|
1220
|
-
|
|
1221
|
-
function
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
-
|
|
1297
|
-
|
|
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
|
-
|
|
1300
|
-
canvas.style.cursor = 'grab';
|
|
940
|
+
const g = svg.append('g').attr('transform', \`translate(\${margin.left},\${margin.top})\`);
|
|
1301
941
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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>\${
|
|
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
|
-
}
|
|
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
|
-
// ──
|
|
1338
|
-
let
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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
|
|
1360
|
-
<div
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
// ──
|
|
1388
|
-
let
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
const
|
|
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 (
|
|
1396
|
-
panel.innerHTML = '<div class="empty">No
|
|
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
|
-
|
|
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
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
|
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
|
|
1412
|
-
<div
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
<
|
|
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
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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
|
|
1424
|
-
<div
|
|
1425
|
-
|
|
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
|
-
|
|
1428
|
-
|
|
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
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
);
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
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) {
|