clementine-agent 1.18.21 → 1.18.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli/dashboard.js +1015 -64
  2. package/package.json +1 -1
@@ -288,6 +288,255 @@ async function searchMemory(query, limit = 20, filters = {}) {
288
288
  db.close();
289
289
  }
290
290
  }
291
+ function quotedFtsQuery(query) {
292
+ return query
293
+ .split(/\s+/)
294
+ .map((w) => w.replace(/"/g, '').trim())
295
+ .filter((w) => w.length > 0)
296
+ .map((w) => `"${w}"`)
297
+ .join(' OR ');
298
+ }
299
+ function textPreview(raw, query = '', max = 520) {
300
+ const compact = String(raw ?? '').replace(/\s+/g, ' ').trim();
301
+ if (!compact)
302
+ return '';
303
+ const q = query.trim().toLowerCase();
304
+ if (!q)
305
+ return compact.slice(0, max);
306
+ const idx = compact.toLowerCase().indexOf(q);
307
+ if (idx < 0)
308
+ return compact.slice(0, max);
309
+ const start = Math.max(0, idx - 120);
310
+ const end = Math.min(compact.length, start + max);
311
+ return (start > 0 ? '…' : '') + compact.slice(start, end) + (end < compact.length ? '…' : '');
312
+ }
313
+ function classifyVaultOrigin(relPath) {
314
+ if (relPath.startsWith('04-Ingest/'))
315
+ return 'Seeded';
316
+ if (relPath.startsWith('00-System/skills/') || relPath.startsWith('00-System/workflows/') || relPath.startsWith('00-System/agents/')) {
317
+ return 'Agent-created';
318
+ }
319
+ if (relPath.startsWith('01-Daily/'))
320
+ return 'Conversation';
321
+ return 'Vault';
322
+ }
323
+ async function searchBrainLibrary(query, limit = 30, scope = 'all') {
324
+ const trimmed = query.trim();
325
+ const lowered = trimmed.toLowerCase();
326
+ const qWords = lowered.split(/\s+/).filter(Boolean);
327
+ const perTypeLimit = Math.max(8, Math.ceil(limit / 2));
328
+ const results = [];
329
+ const totalByType = { memory: 0, files: 0, artifacts: 0 };
330
+ const include = (kind) => scope === 'all' || scope === kind;
331
+ const dbExists = existsSync(MEMORY_DB_PATH);
332
+ if (include('memory') && dbExists) {
333
+ const Database = (await import('better-sqlite3')).default;
334
+ const db = new Database(MEMORY_DB_PATH, { readonly: true });
335
+ try {
336
+ let rows = [];
337
+ if (trimmed) {
338
+ const ftsQuery = quotedFtsQuery(trimmed);
339
+ if (ftsQuery) {
340
+ rows = db.prepare(`SELECT c.id, c.source_file, c.section, c.content, c.chunk_type,
341
+ c.updated_at, c.source_slug, c.source_type, c.agent_slug, c.pinned,
342
+ bm25(chunks_fts) AS score
343
+ FROM chunks_fts f
344
+ JOIN chunks c ON c.id = f.rowid
345
+ LEFT JOIN chunk_soft_deletes sd ON sd.chunk_id = c.id
346
+ WHERE chunks_fts MATCH ? AND sd.chunk_id IS NULL
347
+ ORDER BY bm25(chunks_fts)
348
+ LIMIT ?`).all(ftsQuery, perTypeLimit);
349
+ }
350
+ }
351
+ else {
352
+ rows = db.prepare(`SELECT c.id, c.source_file, c.section, c.content, c.chunk_type,
353
+ c.updated_at, c.source_slug, c.source_type, c.agent_slug, c.pinned,
354
+ 0 AS score
355
+ FROM chunks c
356
+ LEFT JOIN chunk_soft_deletes sd ON sd.chunk_id = c.id
357
+ WHERE sd.chunk_id IS NULL
358
+ ORDER BY c.updated_at DESC, c.id DESC
359
+ LIMIT ?`).all(perTypeLimit);
360
+ }
361
+ totalByType.memory = rows.length;
362
+ for (const row of rows) {
363
+ const sourceFile = String(row.source_file ?? '');
364
+ const sourceSlug = row.source_slug ? String(row.source_slug) : '';
365
+ const section = String(row.section ?? '');
366
+ const score = Number(row.score ?? 0);
367
+ const badges = [
368
+ row.chunk_type ? String(row.chunk_type) : 'chunk',
369
+ sourceSlug ? `source:${sourceSlug}` : classifyVaultOrigin(sourceFile),
370
+ row.pinned ? 'pinned' : '',
371
+ ].filter(Boolean);
372
+ results.push({
373
+ id: `memory:${row.id}`,
374
+ kind: 'memory',
375
+ title: section || path.basename(sourceFile) || `Chunk #${row.id}`,
376
+ subtitle: sourceFile,
377
+ preview: textPreview(String(row.content ?? ''), trimmed),
378
+ timestamp: row.updated_at ? String(row.updated_at) : null,
379
+ score: trimmed ? 100 - Math.abs(score) : 0,
380
+ badges,
381
+ chunkId: Number(row.id),
382
+ source: sourceSlug || sourceFile,
383
+ });
384
+ }
385
+ }
386
+ catch {
387
+ // Missing legacy tables should not break dashboard search.
388
+ }
389
+ finally {
390
+ db.close();
391
+ }
392
+ }
393
+ if (include('artifacts') && dbExists) {
394
+ const Database = (await import('better-sqlite3')).default;
395
+ const db = new Database(MEMORY_DB_PATH, { readonly: true });
396
+ try {
397
+ let rows = [];
398
+ if (trimmed) {
399
+ const ftsQuery = quotedFtsQuery(trimmed);
400
+ if (ftsQuery) {
401
+ rows = db.prepare(`SELECT a.id, a.tool_name, a.summary, substr(a.content, 1, 900) AS preview,
402
+ a.tags, a.stored_at, a.session_key, a.agent_slug, bm25(tool_artifacts_fts) AS score
403
+ FROM tool_artifacts_fts f
404
+ JOIN tool_artifacts a ON a.id = f.rowid
405
+ WHERE tool_artifacts_fts MATCH ?
406
+ ORDER BY bm25(tool_artifacts_fts)
407
+ LIMIT ?`).all(ftsQuery, perTypeLimit);
408
+ }
409
+ }
410
+ else {
411
+ rows = db.prepare(`SELECT id, tool_name, summary, substr(content, 1, 900) AS preview,
412
+ tags, stored_at, session_key, agent_slug, 0 AS score
413
+ FROM tool_artifacts
414
+ ORDER BY stored_at DESC, id DESC
415
+ LIMIT ?`).all(perTypeLimit);
416
+ }
417
+ totalByType.artifacts = rows.length;
418
+ for (const row of rows) {
419
+ const tags = String(row.tags ?? '').split(',').map((t) => t.trim()).filter(Boolean).slice(0, 4);
420
+ results.push({
421
+ id: `artifact:${row.id}`,
422
+ kind: 'artifact',
423
+ title: String(row.summary ?? '').trim() || String(row.tool_name ?? 'Tool artifact'),
424
+ subtitle: `Tool output · ${String(row.tool_name ?? 'unknown')}`,
425
+ preview: textPreview(`${row.summary ?? ''}\n${row.preview ?? ''}`, trimmed),
426
+ timestamp: row.stored_at ? String(row.stored_at) : null,
427
+ score: trimmed ? 100 - Math.abs(Number(row.score ?? 0)) : 0,
428
+ badges: ['artifact', ...tags],
429
+ artifactId: Number(row.id),
430
+ source: row.session_key ? String(row.session_key) : undefined,
431
+ });
432
+ }
433
+ }
434
+ catch {
435
+ // Artifact memory may not exist on older DBs.
436
+ }
437
+ finally {
438
+ db.close();
439
+ }
440
+ }
441
+ if (include('files')) {
442
+ const vaultRoot = VAULT_DIR;
443
+ const files = [];
444
+ const maxScan = trimmed ? 5000 : 500;
445
+ let scanned = 0;
446
+ function walk(dir) {
447
+ if (scanned >= maxScan)
448
+ return;
449
+ let entries = [];
450
+ try {
451
+ entries = readdirSync(dir);
452
+ }
453
+ catch {
454
+ return;
455
+ }
456
+ for (const entry of entries) {
457
+ if (scanned >= maxScan)
458
+ break;
459
+ if (entry.startsWith('.'))
460
+ continue;
461
+ const full = path.join(dir, entry);
462
+ let stat;
463
+ try {
464
+ stat = statSync(full);
465
+ }
466
+ catch {
467
+ continue;
468
+ }
469
+ if (stat.isDirectory()) {
470
+ if (entry === 'node_modules' || entry === '.git')
471
+ continue;
472
+ walk(full);
473
+ continue;
474
+ }
475
+ if (!entry.endsWith('.md') || entry.endsWith('.md.bak'))
476
+ continue;
477
+ scanned += 1;
478
+ const relPath = path.relative(vaultRoot, full);
479
+ let raw = '';
480
+ try {
481
+ raw = readFileSync(full, 'utf-8');
482
+ }
483
+ catch {
484
+ continue;
485
+ }
486
+ let title = path.basename(entry, '.md');
487
+ let typeTag = '';
488
+ let content = raw;
489
+ try {
490
+ const parsed = matter(raw.slice(0, 80_000));
491
+ content = parsed.content || raw;
492
+ const data = parsed.data;
493
+ if (typeof data.title === 'string')
494
+ title = data.title;
495
+ else if (typeof data.name === 'string')
496
+ title = data.name;
497
+ else {
498
+ const h1 = content.match(/^#\s+(.+)$/m);
499
+ if (h1)
500
+ title = h1[1].trim();
501
+ }
502
+ if (typeof data.type === 'string')
503
+ typeTag = data.type;
504
+ }
505
+ catch { /* keep filename */ }
506
+ const hay = `${title}\n${relPath}\n${content.slice(0, 80_000)}`.toLowerCase();
507
+ const match = !trimmed || qWords.every((word) => hay.includes(word)) || hay.includes(lowered);
508
+ if (!match)
509
+ continue;
510
+ const titleHit = lowered && title.toLowerCase().includes(lowered) ? 30 : 0;
511
+ const pathHit = lowered && relPath.toLowerCase().includes(lowered) ? 18 : 0;
512
+ const contentHit = lowered && content.toLowerCase().includes(lowered) ? 10 : 0;
513
+ files.push({
514
+ id: `file:${relPath}`,
515
+ kind: 'file',
516
+ title,
517
+ subtitle: relPath,
518
+ preview: textPreview(content, trimmed),
519
+ timestamp: stat.mtime.toISOString(),
520
+ score: trimmed ? titleHit + pathHit + contentHit : stat.mtimeMs / 1_000_000_000,
521
+ badges: [typeTag || 'note', classifyVaultOrigin(relPath)].filter(Boolean),
522
+ relPath,
523
+ source: relPath,
524
+ });
525
+ }
526
+ }
527
+ if (existsSync(vaultRoot))
528
+ walk(vaultRoot);
529
+ files.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
530
+ totalByType.files = files.length;
531
+ results.push(...files.slice(0, perTypeLimit));
532
+ }
533
+ results.sort((a, b) => {
534
+ if (trimmed)
535
+ return (b.score ?? 0) - (a.score ?? 0);
536
+ return String(b.timestamp ?? '').localeCompare(String(a.timestamp ?? ''));
537
+ });
538
+ return { results: results.slice(0, limit), totalByType, dbExists };
539
+ }
291
540
  // ── Remote access config ────────────────────────────────────────────
292
541
  const REMOTE_CONFIG_PATH = path.join(BASE_DIR, 'remote-access.json');
293
542
  function generateAccessToken() {
@@ -4335,7 +4584,8 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
4335
4584
  const { getStore } = await import('../tools/shared.js');
4336
4585
  const store = await getStore();
4337
4586
  const slug = typeof req.query.slug === 'string' ? req.query.slug : undefined;
4338
- const runs = store.listIngestionRuns(slug, 50);
4587
+ const limit = Math.min(Math.max(Number(req.query.limit) || 50, 1), 200);
4588
+ const runs = store.listIngestionRuns(slug, limit);
4339
4589
  res.json({ runs });
4340
4590
  }
4341
4591
  catch (err) {
@@ -6754,6 +7004,30 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
6754
7004
  res.status(500).json({ error: String(err) });
6755
7005
  }
6756
7006
  });
7007
+ app.get('/api/brain/artifacts/:id', async (req, res) => {
7008
+ try {
7009
+ const id = Number(req.params.id);
7010
+ if (!Number.isFinite(id) || id <= 0) {
7011
+ res.status(400).json({ error: 'invalid artifact id' });
7012
+ return;
7013
+ }
7014
+ const { getStore } = await import('../tools/shared.js');
7015
+ const store = await getStore();
7016
+ if (!store?.getArtifact) {
7017
+ res.status(503).json({ error: 'Artifact memory not available' });
7018
+ return;
7019
+ }
7020
+ const artifact = store.getArtifact(id);
7021
+ if (!artifact) {
7022
+ res.status(404).json({ error: 'artifact not found' });
7023
+ return;
7024
+ }
7025
+ res.json({ ok: true, artifact });
7026
+ }
7027
+ catch (err) {
7028
+ res.status(500).json({ error: String(err) });
7029
+ }
7030
+ });
6757
7031
  // ── Cron training chat endpoint ─────────────────────────────────────
6758
7032
  app.post('/api/cron/train', async (req, res) => {
6759
7033
  const { message, jobName, prompt, context, agentSlug } = req.body;
@@ -7120,6 +7394,19 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
7120
7394
  res.status(500).json({ results: [], error: String(err) });
7121
7395
  }
7122
7396
  });
7397
+ app.get('/api/brain/library/search', async (req, res) => {
7398
+ try {
7399
+ const q = String(req.query.q ?? '');
7400
+ const rawScope = String(req.query.scope ?? 'all');
7401
+ const scope = ['all', 'memory', 'files', 'artifacts'].includes(rawScope) ? rawScope : 'all';
7402
+ const limit = Math.min(Math.max(Number(req.query.limit) || 30, 1), 80);
7403
+ const data = await searchBrainLibrary(q, limit, scope);
7404
+ res.json({ ok: true, query: q, scope, ...data });
7405
+ }
7406
+ catch (err) {
7407
+ res.status(500).json({ ok: false, error: String(err), results: [] });
7408
+ }
7409
+ });
7123
7410
  // ── Metrics route ─────────────────────────────────────────────────
7124
7411
  app.get('/api/metrics', (_req, res) => {
7125
7412
  res.json(computeMetrics());
@@ -11312,6 +11599,201 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11312
11599
  overflow-y: auto;
11313
11600
  }
11314
11601
 
11602
+ /* ── Brain command center ──────────────── */
11603
+ .brain-command-shell {
11604
+ display: flex;
11605
+ flex-direction: column;
11606
+ gap: 16px;
11607
+ }
11608
+ .brain-hero-panel {
11609
+ border: 1px solid var(--border);
11610
+ border-radius: var(--radius);
11611
+ background: var(--bg-card);
11612
+ padding: 18px;
11613
+ }
11614
+ .brain-pillar-grid {
11615
+ display: grid;
11616
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
11617
+ gap: 12px;
11618
+ }
11619
+ .brain-pillar-card {
11620
+ border: 1px solid var(--border);
11621
+ border-radius: var(--radius);
11622
+ background: var(--bg-card);
11623
+ padding: 16px;
11624
+ display: flex;
11625
+ flex-direction: column;
11626
+ gap: 10px;
11627
+ min-height: 178px;
11628
+ }
11629
+ .brain-pillar-card strong {
11630
+ font-size: 14px;
11631
+ color: var(--text-primary);
11632
+ }
11633
+ .brain-pillar-card p {
11634
+ margin: 0;
11635
+ color: var(--text-secondary);
11636
+ font-size: 12px;
11637
+ line-height: 1.5;
11638
+ }
11639
+ .brain-pillar-actions {
11640
+ display: flex;
11641
+ gap: 8px;
11642
+ flex-wrap: wrap;
11643
+ margin-top: auto;
11644
+ }
11645
+ .brain-kpi-row {
11646
+ display: grid;
11647
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
11648
+ gap: 10px;
11649
+ }
11650
+ .brain-kpi {
11651
+ border: 1px solid var(--border);
11652
+ border-radius: var(--radius-sm);
11653
+ background: var(--bg-secondary);
11654
+ padding: 12px;
11655
+ }
11656
+ .brain-kpi .value {
11657
+ font-size: 22px;
11658
+ font-weight: 700;
11659
+ color: var(--text-primary);
11660
+ line-height: 1.1;
11661
+ }
11662
+ .brain-kpi .label {
11663
+ font-size: 11px;
11664
+ color: var(--text-muted);
11665
+ text-transform: uppercase;
11666
+ letter-spacing: 0.04em;
11667
+ margin-top: 4px;
11668
+ }
11669
+ .brain-library-toolbar {
11670
+ display: flex;
11671
+ align-items: center;
11672
+ gap: 8px;
11673
+ flex-wrap: wrap;
11674
+ }
11675
+ .brain-library-toolbar input {
11676
+ flex: 1;
11677
+ min-width: 220px;
11678
+ }
11679
+ .brain-result-row {
11680
+ border: 1px solid var(--border);
11681
+ border-radius: var(--radius);
11682
+ background: var(--bg-card);
11683
+ padding: 13px 15px;
11684
+ margin-bottom: 10px;
11685
+ }
11686
+ .brain-result-row:hover { border-color: var(--accent); }
11687
+ .brain-result-top {
11688
+ display: flex;
11689
+ justify-content: space-between;
11690
+ align-items: flex-start;
11691
+ gap: 12px;
11692
+ }
11693
+ .brain-result-title {
11694
+ font-weight: 600;
11695
+ font-size: 13px;
11696
+ color: var(--text-primary);
11697
+ overflow-wrap: anywhere;
11698
+ }
11699
+ .brain-result-subtitle {
11700
+ font-family: 'JetBrains Mono', monospace;
11701
+ font-size: 11px;
11702
+ color: var(--text-muted);
11703
+ margin-top: 3px;
11704
+ overflow-wrap: anywhere;
11705
+ }
11706
+ .brain-result-preview {
11707
+ font-size: 12px;
11708
+ color: var(--text-secondary);
11709
+ line-height: 1.55;
11710
+ margin-top: 8px;
11711
+ white-space: pre-wrap;
11712
+ max-height: 112px;
11713
+ overflow: hidden;
11714
+ }
11715
+ .brain-badge {
11716
+ display: inline-flex;
11717
+ align-items: center;
11718
+ border: 1px solid var(--border);
11719
+ border-radius: 999px;
11720
+ padding: 2px 7px;
11721
+ font-size: 10px;
11722
+ color: var(--text-secondary);
11723
+ background: var(--bg-secondary);
11724
+ white-space: nowrap;
11725
+ }
11726
+ .brain-flow-list {
11727
+ border: 1px solid var(--border);
11728
+ border-radius: var(--radius);
11729
+ background: var(--bg-card);
11730
+ overflow: hidden;
11731
+ }
11732
+ .brain-flow-row {
11733
+ display: grid;
11734
+ grid-template-columns: minmax(140px, 1.2fr) 100px repeat(4, 72px) minmax(120px, 1fr);
11735
+ gap: 8px;
11736
+ align-items: center;
11737
+ padding: 10px 12px;
11738
+ border-bottom: 1px solid var(--border-light);
11739
+ font-size: 12px;
11740
+ }
11741
+ .brain-flow-row:last-child { border-bottom: none; }
11742
+ .brain-source-card {
11743
+ border: 1px solid var(--border);
11744
+ border-radius: var(--radius);
11745
+ background: var(--bg-card);
11746
+ padding: 12px;
11747
+ display: flex;
11748
+ align-items: flex-start;
11749
+ justify-content: space-between;
11750
+ gap: 12px;
11751
+ margin-bottom: 10px;
11752
+ }
11753
+ .brain-drop-zone {
11754
+ border: 1px dashed var(--border-light);
11755
+ border-radius: var(--radius);
11756
+ background: var(--bg-secondary);
11757
+ padding: 18px;
11758
+ display: flex;
11759
+ align-items: center;
11760
+ justify-content: space-between;
11761
+ gap: 14px;
11762
+ flex-wrap: wrap;
11763
+ }
11764
+ .brain-drop-zone.dragover {
11765
+ border-color: var(--accent);
11766
+ background: var(--accent-glow);
11767
+ }
11768
+ .brain-kv-builder {
11769
+ display: flex;
11770
+ flex-direction: column;
11771
+ gap: 6px;
11772
+ min-width: 0;
11773
+ }
11774
+ .brain-kv-row {
11775
+ display: grid;
11776
+ grid-template-columns: minmax(120px, 1fr) minmax(160px, 1.5fr) 30px;
11777
+ gap: 6px;
11778
+ align-items: center;
11779
+ }
11780
+ .brain-kv-row input {
11781
+ padding: 7px 9px;
11782
+ font-size: 12px;
11783
+ }
11784
+ @media (max-width: 760px) {
11785
+ .brain-flow-row {
11786
+ grid-template-columns: 1fr;
11787
+ gap: 3px;
11788
+ }
11789
+ .brain-result-top {
11790
+ flex-direction: column;
11791
+ }
11792
+ .brain-kv-row {
11793
+ grid-template-columns: 1fr;
11794
+ }
11795
+ }
11796
+
11315
11797
  /* ── Toast ──────────────────────────────── */
11316
11798
  .toast-container {
11317
11799
  position: fixed;
@@ -13744,9 +14226,10 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
13744
14226
  <h1>Brain</h1>
13745
14227
  <p class="desc">Query what you know, feed new knowledge in, and watch the system learn.</p>
13746
14228
  </div>
13747
- <div class="actions" style="flex:1;max-width:560px;display:flex;gap:8px">
13748
- <input type="text" id="memory-search-input" placeholder="Search vault, notes, memory..." style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px" onkeydown="if(event.key==='Enter')runMemorySearch()">
13749
- <button class="btn-primary btn-sm" onclick="runMemorySearch()">Search</button>
14229
+ <div class="actions" style="flex:1;max-width:640px;display:flex;gap:8px">
14230
+ <input type="text" id="memory-search-input" placeholder="Find seeded files, memories, notes, and artifacts..." style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px" onkeydown="if(event.key==='Enter')brainUnifiedSearchFromHeader()">
14231
+ <button class="btn-primary btn-sm" onclick="brainUnifiedSearchFromHeader()" title="Search the whole brain"><span class="icon-slot" data-icon="search"></span> Find</button>
14232
+ <button class="btn-sm" onclick="switchTab('intelligence','seed')" title="Upload a local file or folder"><span class="icon-slot" data-icon="upload"></span> Seed</button>
13750
14233
  <button class="btn-sm" onclick="openQuickAddMemory()" title="Append a quick note to today's daily log">+ Add memory</button>
13751
14234
  </div>
13752
14235
  </div>
@@ -13779,18 +14262,109 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
13779
14262
  </div>
13780
14263
  </div>
13781
14264
  <div class="tab-bar" id="intelligence-tabs" style="margin:0 0 0 18px">
13782
- <button class="active" data-icon="database" onclick="switchTab('intelligence','search')"><span class="icon-slot"></span> Memory</button>
14265
+ <button class="active" data-icon="layoutDashboard" onclick="switchTab('intelligence','overview')"><span class="icon-slot"></span> Overview</button>
14266
+ <button data-icon="database" onclick="switchTab('intelligence','search')"><span class="icon-slot"></span> Memory</button>
14267
+ <button data-icon="upload" onclick="switchTab('intelligence','seed')"><span class="icon-slot"></span> Seed</button>
14268
+ <button data-icon="repeat" onclick="switchTab('intelligence','sources')"><span class="icon-slot"></span> Automate</button>
14269
+ <button data-icon="listChecks" onclick="switchTab('intelligence','runs')"><span class="icon-slot"></span> Runs</button>
13783
14270
  <button data-icon="sparkles" onclick="switchTab('intelligence','graph')"><span class="icon-slot"></span> Knowledge</button>
13784
14271
  <button data-icon="fileText" onclick="switchTab('intelligence','files')"><span class="icon-slot"></span> Files</button>
13785
- <button data-icon="folder" onclick="switchTab('intelligence','sources')"><span class="icon-slot"></span> Ingestion</button>
13786
14272
  <button data-icon="zap" onclick="switchTab('intelligence','health')"><span class="icon-slot"></span> Health <span class="tab-badge" id="brain-health-badge" style="display:none;background:#ef4444;color:#fff">0</span></button>
13787
14273
  <button data-icon="users" onclick="switchTab('intelligence','user-model')"><span class="icon-slot"></span> User Model</button>
13788
14274
  <button data-icon="brain" onclick="switchTab('intelligence','learning')"><span class="icon-slot"></span> Learning <span class="tab-badge" id="brain-learning-badge" style="display:none;background:#f59e0b;color:#000">0</span></button>
13789
- <button onclick="switchTab('intelligence','seed')">Seed</button>
13790
- <button onclick="switchTab('intelligence','runs')">Runs</button>
13791
14275
  </div>
13792
14276
  <div id="intelligence-tab-content">
13793
- <div class="tab-pane active" id="tab-intelligence-search">
14277
+ <div class="tab-pane active" id="tab-intelligence-overview">
14278
+ <div class="brain-command-shell">
14279
+ <div class="brain-hero-panel">
14280
+ <div style="display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;margin-bottom:14px">
14281
+ <div style="max-width:720px">
14282
+ <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:5px">Brain command center</div>
14283
+ <div style="font-size:18px;font-weight:700;margin-bottom:5px">Find anything Clementine knows, then prove how it got there.</div>
14284
+ <div style="font-size:13px;color:var(--text-secondary);line-height:1.5">Use this page to seed local data, keep connected sources refreshed on a schedule, search agent-created artifacts, and verify retrieval coverage.</div>
14285
+ </div>
14286
+ <div style="display:flex;gap:8px;flex-wrap:wrap">
14287
+ <button class="btn-primary btn-sm" onclick="switchTab('intelligence','seed')"><span class="icon-slot" data-icon="upload"></span> Seed local data</button>
14288
+ <button class="btn-sm" onclick="switchTab('intelligence','sources')"><span class="icon-slot" data-icon="repeat"></span> Add scheduled feed</button>
14289
+ <button class="btn-sm" onclick="switchTab('intelligence','health')"><span class="icon-slot" data-icon="activity"></span> Verify health</button>
14290
+ </div>
14291
+ </div>
14292
+ <div id="brain-command-kpis" class="brain-kpi-row">
14293
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
14294
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
14295
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
14296
+ </div>
14297
+ </div>
14298
+
14299
+ <div class="brain-pillar-grid">
14300
+ <div class="brain-pillar-card">
14301
+ <strong>Find</strong>
14302
+ <p>Search memories, seeded records, vault files, and saved tool artifacts in one place.</p>
14303
+ <div class="brain-pillar-actions">
14304
+ <button class="btn-sm btn-primary" onclick="focusBrainLibrarySearch()">Search library</button>
14305
+ <button class="btn-sm" onclick="switchTab('intelligence','files')">Browse files</button>
14306
+ </div>
14307
+ </div>
14308
+ <div class="brain-pillar-card">
14309
+ <strong>Seed</strong>
14310
+ <p>Choose files or folders from the local machine, preview what will be written, then commit to memory.</p>
14311
+ <div class="brain-pillar-actions">
14312
+ <button class="btn-sm btn-primary" onclick="switchTab('intelligence','seed')">Upload data</button>
14313
+ <button class="btn-sm" onclick="document.getElementById('brain-file-input')?.click()">Choose files</button>
14314
+ </div>
14315
+ </div>
14316
+ <div class="brain-pillar-card">
14317
+ <strong>Automate</strong>
14318
+ <p>Create scheduled feeds for REST endpoints or connected apps so the brain keeps learning without manual uploads.</p>
14319
+ <div class="brain-pillar-actions">
14320
+ <button class="btn-sm btn-primary" onclick="switchTab('intelligence','sources');setTimeout(brainShowPollForm,80)">REST feed</button>
14321
+ <button class="btn-sm" onclick="switchTab('intelligence','sources');setTimeout(brainOpenFeedWizard,80)">Connected app</button>
14322
+ </div>
14323
+ </div>
14324
+ <div class="brain-pillar-card">
14325
+ <strong>Verify</strong>
14326
+ <p>Check dense model readiness, retrieval coverage, ingestion runs, and whether new data is actually searchable.</p>
14327
+ <div class="brain-pillar-actions">
14328
+ <button class="btn-sm btn-primary" onclick="switchTab('intelligence','health')">Open health</button>
14329
+ <button class="btn-sm" onclick="switchTab('intelligence','runs')">View runs</button>
14330
+ </div>
14331
+ </div>
14332
+ </div>
14333
+
14334
+ <div class="brain-hero-panel">
14335
+ <div class="brain-library-toolbar" style="margin-bottom:12px">
14336
+ <input id="brain-library-search-input" type="text" placeholder="Search the whole brain..." onkeydown="if(event.key==='Enter')runBrainLibrarySearch()">
14337
+ <select id="brain-library-scope" onchange="runBrainLibrarySearch()" style="width:150px">
14338
+ <option value="all">Everything</option>
14339
+ <option value="memory">Memory</option>
14340
+ <option value="files">Files</option>
14341
+ <option value="artifacts">Artifacts</option>
14342
+ </select>
14343
+ <button class="btn-primary btn-sm" onclick="runBrainLibrarySearch()">Search</button>
14344
+ <button class="btn-sm" onclick="runBrainLibrarySearch('')">Recent</button>
14345
+ </div>
14346
+ <div id="brain-library-summary" style="font-size:12px;color:var(--text-muted);margin-bottom:10px"></div>
14347
+ <div id="brain-library-results">
14348
+ <div class="empty-state" style="padding:20px">Search the whole brain, or click Recent to inspect the latest remembered items.</div>
14349
+ </div>
14350
+ </div>
14351
+
14352
+ <div>
14353
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
14354
+ <div style="font-weight:600">Recent knowledge flow</div>
14355
+ <button class="btn-sm" onclick="refreshBrainOverview()">Refresh</button>
14356
+ </div>
14357
+ <div id="brain-overview-flow"><div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div></div></div>
14358
+ </div>
14359
+ </div>
14360
+ </div>
14361
+ <div class="tab-pane" id="tab-intelligence-search">
14362
+ <div style="display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
14363
+ <input type="text" id="memory-detail-search-input" placeholder="Search editable memory chunks..." style="flex:1;min-width:220px" onkeydown="if(event.key==='Enter')runMemoryDetailSearch()">
14364
+ <button class="btn-primary btn-sm" onclick="runMemoryDetailSearch()">Search chunks</button>
14365
+ <button class="btn-sm" onclick="document.getElementById('memory-detail-search-input').value='pinned:true';runMemoryDetailSearch()">Pinned</button>
14366
+ <button class="btn-sm" onclick="document.getElementById('memory-detail-search-input').value='since:7d';runMemoryDetailSearch()">Last 7d</button>
14367
+ </div>
13794
14368
  <div id="memory-coverage-strip" style="margin-bottom:14px"></div>
13795
14369
  <div id="memory-search-results"></div>
13796
14370
  <div id="memory-overview" style="margin-top:18px">
@@ -13861,18 +14435,24 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
13861
14435
  <div class="card" style="padding:20px;margin-bottom:16px">
13862
14436
  <div style="font-weight:600;margin-bottom:8px">Drop a file or folder into the brain</div>
13863
14437
  <div style="color:var(--muted);margin-bottom:12px;font-size:13px">
13864
- Supports CSV, JSON, JSONL, Markdown, PDF, email (.eml / .mbox), DOCX. Preview runs the first 10 records through the full pipeline without writing anything; Commit ingests everything.
14438
+ Supports CSV, JSON, JSONL, Markdown, PDF, email (.eml / .mbox), DOCX. Preview runs the first 10 records through the full pipeline without writing anything; Save to brain writes the full source.
13865
14439
  </div>
13866
14440
 
13867
14441
  <!-- Primary: native file/folder pickers -->
13868
- <div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap">
13869
- <button class="btn-primary" onclick="document.getElementById('brain-file-input').click()">📄 Choose file(s)…</button>
13870
- <button class="btn-primary" onclick="document.getElementById('brain-folder-input').click()">📁 Choose folder…</button>
13871
- <input type="text" id="brain-seed-slug" placeholder="slug (auto if blank)" style="width:220px">
14442
+ <div id="brain-drop-zone" class="brain-drop-zone" ondragover="brainHandleDrag(event, true)" ondragleave="brainHandleDrag(event, false)" ondrop="brainHandleDrop(event)">
14443
+ <div style="min-width:220px;flex:1">
14444
+ <div style="font-weight:600;margin-bottom:4px">Upload local knowledge</div>
14445
+ <div style="font-size:12px;color:var(--text-muted);line-height:1.5">Drop files here, choose a folder, or use the advanced path option for data already on this machine. Clementine previews records before writing anything.</div>
14446
+ </div>
14447
+ <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
14448
+ <button class="btn-primary" onclick="document.getElementById('brain-file-input').click()" type="button"><span class="icon-slot" data-icon="fileText"></span> Choose files</button>
14449
+ <button class="btn-primary" onclick="document.getElementById('brain-folder-input').click()" type="button"><span class="icon-slot" data-icon="folder"></span> Choose folder</button>
14450
+ <input type="text" id="brain-seed-slug" placeholder="source name (optional)" style="width:220px">
14451
+ </div>
13872
14452
  </div>
13873
14453
  <input type="file" id="brain-file-input" multiple style="display:none" onchange="brainHandleFilesChosen(this.files, false)">
13874
14454
  <input type="file" id="brain-folder-input" webkitdirectory directory multiple style="display:none" onchange="brainHandleFilesChosen(this.files, true)">
13875
- <div id="brain-upload-status" style="margin-bottom:8px;color:var(--muted);font-size:13px"></div>
14455
+ <div id="brain-upload-status" style="margin:8px 0;color:var(--muted);font-size:13px"></div>
13876
14456
 
13877
14457
  <!-- Secondary: for power users who want to point at an existing on-disk path -->
13878
14458
  <details style="margin-bottom:8px">
@@ -13883,8 +14463,8 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
13883
14463
  </details>
13884
14464
 
13885
14465
  <div style="display:flex;gap:8px;margin-top:8px">
13886
- <button class="btn-primary" onclick="brainPreviewSeed()">Preview</button>
13887
- <button class="btn-primary" id="brain-commit-btn" onclick="brainCommitSeed()" style="display:none">Commit ingestion</button>
14466
+ <button class="btn-primary" onclick="brainPreviewSeed()">Preview records</button>
14467
+ <button class="btn-primary" id="brain-commit-btn" onclick="brainCommitSeed()" style="display:none">Save to brain</button>
13888
14468
  </div>
13889
14469
 
13890
14470
  <div id="brain-seed-manifest" style="margin-top:16px"></div>
@@ -13962,14 +14542,25 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
13962
14542
  <input type="text" id="brain-poll-url" placeholder="https://api.example.com/v1/items">
13963
14543
  <label>Method</label>
13964
14544
  <select id="brain-poll-method"><option>GET</option><option>POST</option></select>
13965
- <label>Headers (JSON)</label>
13966
- <input type="text" id="brain-poll-headers" placeholder='{"Authorization":"Bearer $\{stripe_api_key}"}'>
13967
- <label>Query params (JSON)</label>
13968
- <input type="text" id="brain-poll-params" placeholder='{"limit":"100"}'>
13969
- <label>Records JSON path</label>
13970
- <input type="text" id="brain-poll-recordspath" placeholder="data">
14545
+ <label>Headers</label>
14546
+ <div class="brain-kv-builder">
14547
+ <div id="brain-poll-headers-rows"></div>
14548
+ <button class="btn-sm" type="button" onclick="brainAddKvRow('headers')">Add header</button>
14549
+ <input type="hidden" id="brain-poll-headers">
14550
+ </div>
14551
+ <label>Query params</label>
14552
+ <div class="brain-kv-builder">
14553
+ <div id="brain-poll-params-rows"></div>
14554
+ <button class="btn-sm" type="button" onclick="brainAddKvRow('params')">Add param</button>
14555
+ <input type="hidden" id="brain-poll-params">
14556
+ </div>
14557
+ <label>Record list field</label>
14558
+ <input type="text" id="brain-poll-recordspath" placeholder="data, items, results">
13971
14559
  <label>Cron schedule</label>
13972
- <input type="text" id="brain-poll-cron" placeholder="0 * * * * (hourly)">
14560
+ <div>
14561
+ <input type="text" id="brain-poll-cron" placeholder="0 * * * * (hourly)">
14562
+ <div id="brain-poll-schedule-chips" style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px"></div>
14563
+ </div>
13973
14564
  <label>Target folder</label>
13974
14565
  <input type="text" id="brain-poll-folder" placeholder="04-Ingest/stripe">
13975
14566
  <label>Project (optional)</label>
@@ -14181,6 +14772,269 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14181
14772
  error: 'Error',
14182
14773
  };
14183
14774
 
14775
+ function brainTypeLabel(kind) {
14776
+ if (kind === 'memory') return 'Memory';
14777
+ if (kind === 'file') return 'File';
14778
+ if (kind === 'artifact') return 'Artifact';
14779
+ return kind || 'Item';
14780
+ }
14781
+
14782
+ function brainStatusBadge(status) {
14783
+ var s = String(status || 'unknown');
14784
+ var color = s === 'ok' ? 'var(--green)' : (s === 'partial' ? 'var(--yellow)' : (s === 'error' ? 'var(--red)' : 'var(--text-muted)'));
14785
+ return '<span class="brain-badge" style="color:' + color + ';border-color:' + color + '33">' + escapeHtml(s) + '</span>';
14786
+ }
14787
+
14788
+ function brainUnifiedSearchFromHeader() {
14789
+ var header = document.getElementById('memory-search-input');
14790
+ var q = header ? header.value.trim() : '';
14791
+ switchTab('intelligence', 'overview');
14792
+ setTimeout(function() {
14793
+ var input = document.getElementById('brain-library-search-input');
14794
+ if (input) input.value = q;
14795
+ runBrainLibrarySearch(q);
14796
+ }, 60);
14797
+ }
14798
+
14799
+ function focusBrainLibrarySearch() {
14800
+ switchTab('intelligence', 'overview');
14801
+ setTimeout(function() {
14802
+ var input = document.getElementById('brain-library-search-input');
14803
+ if (input) input.focus();
14804
+ }, 80);
14805
+ }
14806
+
14807
+ async function runBrainLibrarySearch(forceQuery) {
14808
+ var input = document.getElementById('brain-library-search-input');
14809
+ var header = document.getElementById('memory-search-input');
14810
+ var scopeEl = document.getElementById('brain-library-scope');
14811
+ var q = forceQuery !== undefined ? String(forceQuery || '') : (input ? input.value.trim() : '');
14812
+ if (input && forceQuery !== undefined) input.value = q;
14813
+ if (header && q) header.value = q;
14814
+ var scope = scopeEl ? scopeEl.value : 'all';
14815
+ var resultsEl = document.getElementById('brain-library-results');
14816
+ var summaryEl = document.getElementById('brain-library-summary');
14817
+ if (!resultsEl) return;
14818
+ resultsEl.innerHTML = '<div class="empty-state" style="padding:20px">Searching…</div>';
14819
+ if (summaryEl) summaryEl.textContent = '';
14820
+ try {
14821
+ var r = await apiFetch('/api/brain/library/search?q=' + encodeURIComponent(q) + '&scope=' + encodeURIComponent(scope) + '&limit=36');
14822
+ var d = await r.json();
14823
+ if (!r.ok || !d.ok) {
14824
+ resultsEl.innerHTML = '<div class="empty-state" style="color:var(--red)">Search failed: ' + escapeHtml(d.error || r.status) + '</div>';
14825
+ return;
14826
+ }
14827
+ var counts = d.totalByType || {};
14828
+ if (summaryEl) {
14829
+ var label = q ? ('Results for "' + q + '"') : 'Recent brain items';
14830
+ summaryEl.innerHTML = escapeHtml(label) + ' · '
14831
+ + (counts.memory || 0) + ' memory · '
14832
+ + (counts.files || 0) + ' files · '
14833
+ + (counts.artifacts || 0) + ' artifacts';
14834
+ }
14835
+ if (!d.results || d.results.length === 0) {
14836
+ resultsEl.innerHTML = '<div class="empty-state" style="padding:20px">No matches. Try fewer words or switch the scope to Everything.</div>';
14837
+ return;
14838
+ }
14839
+ var html = '';
14840
+ for (var i = 0; i < d.results.length; i++) {
14841
+ var item = d.results[i];
14842
+ var badges = ['<span class="brain-badge">' + escapeHtml(brainTypeLabel(item.kind)) + '</span>'];
14843
+ (item.badges || []).slice(0, 4).forEach(function(b) {
14844
+ badges.push('<span class="brain-badge">' + escapeHtml(b) + '</span>');
14845
+ });
14846
+ var actions = '';
14847
+ if (item.kind === 'file' && item.relPath) {
14848
+ actions = '<button class="btn-sm" onclick="openVaultFile(\\'' + escapeHtml(item.relPath) + '\\')">Open</button>';
14849
+ } else if (item.kind === 'memory' && item.chunkId) {
14850
+ actions = '<button class="btn-sm" onclick="editChunk(' + item.chunkId + ')">Edit</button>'
14851
+ + '<button class="btn-sm" onclick="openBrainChunkTrace(' + item.chunkId + ')">Trace</button>';
14852
+ } else if (item.kind === 'artifact' && item.artifactId) {
14853
+ actions = '<button class="btn-sm" onclick="openBrainArtifact(' + item.artifactId + ')">Open</button>';
14854
+ }
14855
+ html += '<div class="brain-result-row">'
14856
+ + '<div class="brain-result-top">'
14857
+ + '<div style="min-width:0;flex:1">'
14858
+ + '<div class="brain-result-title">' + escapeHtml(item.title || '(untitled)') + '</div>'
14859
+ + '<div class="brain-result-subtitle">' + escapeHtml(item.subtitle || item.source || '') + '</div>'
14860
+ + '</div>'
14861
+ + '<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;align-items:center">' + badges.join('') + actions + '</div>'
14862
+ + '</div>'
14863
+ + (item.preview ? '<div class="brain-result-preview">' + escapeHtml(item.preview) + '</div>' : '')
14864
+ + (item.timestamp ? '<div style="font-size:11px;color:var(--text-muted);margin-top:8px">' + escapeHtml(timeAgo(item.timestamp)) + '</div>' : '')
14865
+ + '</div>';
14866
+ }
14867
+ resultsEl.innerHTML = html;
14868
+ } catch (err) {
14869
+ resultsEl.innerHTML = '<div class="empty-state" style="color:var(--red)">Search error: ' + escapeHtml(String(err)) + '</div>';
14870
+ }
14871
+ }
14872
+
14873
+ async function openBrainChunkTrace(id) {
14874
+ var drawer = document.getElementById('brain-chunk-trace-drawer');
14875
+ if (!drawer) {
14876
+ drawer = document.createElement('div');
14877
+ drawer.id = 'brain-chunk-trace-drawer';
14878
+ drawer.style.cssText = 'position:fixed;right:0;top:0;bottom:0;width:620px;max-width:94vw;background:var(--bg-secondary);border-left:1px solid var(--border);box-shadow:-8px 0 32px rgba(0,0,0,0.18);z-index:221;display:flex;flex-direction:column;transform:translateX(100%);transition:transform 200ms ease';
14879
+ drawer.innerHTML =
14880
+ '<div style="display:flex;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid var(--border);flex-shrink:0">'
14881
+ + '<div style="flex:1;min-width:0">'
14882
+ + '<div id="brain-chunk-title" style="font-weight:600;font-size:15px"></div>'
14883
+ + '<div id="brain-chunk-subtitle" style="font-size:11px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;margin-top:2px"></div>'
14884
+ + '</div>'
14885
+ + '<button class="btn-icon btn-sm" onclick="closeBrainChunkTrace()" title="Close">' + lucide('x', 'icn-sm') + '</button>'
14886
+ + '</div>'
14887
+ + '<div id="brain-chunk-body" style="flex:1;overflow:auto;padding:18px 22px;font-size:12px;line-height:1.55"></div>';
14888
+ document.body.appendChild(drawer);
14889
+ }
14890
+ drawer.style.transform = 'translateX(0)';
14891
+ document.getElementById('brain-chunk-title').textContent = 'Memory chunk #' + id;
14892
+ document.getElementById('brain-chunk-subtitle').textContent = 'Loading…';
14893
+ document.getElementById('brain-chunk-body').innerHTML = '<div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div></div>';
14894
+ try {
14895
+ var chunkResp = await apiFetch('/api/memory/chunks/' + id);
14896
+ var chunkData = await chunkResp.json();
14897
+ var historyResp = await apiFetch('/api/memory/chunks/' + id + '/history');
14898
+ var historyData = await historyResp.json();
14899
+ if (!chunkResp.ok || !chunkData.ok || !chunkData.chunk) throw new Error(chunkData.error || 'chunk not found');
14900
+ var c = chunkData.chunk;
14901
+ document.getElementById('brain-chunk-title').textContent = c.section || ('Memory chunk #' + id);
14902
+ document.getElementById('brain-chunk-subtitle').textContent = c.sourceFile || c.source_file || '';
14903
+ var metaRows = [
14904
+ ['ID', c.id || id],
14905
+ ['Type', c.chunkType || c.chunk_type || '—'],
14906
+ ['Source', c.sourceFile || c.source_file || '—'],
14907
+ ['Agent', c.agentSlug || c.agent_slug || 'global'],
14908
+ ['Salience', c.salience != null ? Number(c.salience).toFixed(2) : '—'],
14909
+ ['Confidence', c.confidence != null ? Number(c.confidence).toFixed(2) : '—'],
14910
+ ['Updated', c.lastUpdated || c.updated_at || '—'],
14911
+ ];
14912
+ var html = '<div style="display:grid;grid-template-columns:110px 1fr;gap:6px 12px;margin-bottom:14px">';
14913
+ metaRows.forEach(function(row) {
14914
+ html += '<div style="color:var(--text-muted)">' + escapeHtml(row[0]) + '</div><div style="overflow-wrap:anywhere">' + escapeHtml(row[1]) + '</div>';
14915
+ });
14916
+ html += '</div>';
14917
+ html += '<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:6px">Stored content</div>';
14918
+ html += '<div style="white-space:pre-wrap;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;margin-bottom:14px">' + escapeHtml(c.content || '') + '</div>';
14919
+ var history = historyData.ok && Array.isArray(historyData.history) ? historyData.history : [];
14920
+ html += '<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:6px">Change history</div>';
14921
+ if (!history.length) {
14922
+ html += '<div class="empty-state" style="padding:16px;text-align:left">No edits or supersedes recorded.</div>';
14923
+ } else {
14924
+ html += '<div style="border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden">';
14925
+ history.forEach(function(h) {
14926
+ html += '<div style="padding:8px 10px;border-bottom:1px solid var(--border-light);font-size:12px">'
14927
+ + '<div style="color:var(--text-muted);font-size:11px">' + escapeHtml(h.timestamp || h.at || '') + '</div>'
14928
+ + '<div>' + escapeHtml(h.kind || h.action || 'edit') + (h.reason ? ' · ' + escapeHtml(h.reason) : '') + '</div>'
14929
+ + '</div>';
14930
+ });
14931
+ html += '</div>';
14932
+ }
14933
+ document.getElementById('brain-chunk-body').innerHTML = html;
14934
+ } catch (err) {
14935
+ document.getElementById('brain-chunk-subtitle').textContent = 'Failed';
14936
+ document.getElementById('brain-chunk-body').innerHTML = '<div style="color:var(--red)">' + escapeHtml(String(err)) + '</div>';
14937
+ }
14938
+ }
14939
+
14940
+ function closeBrainChunkTrace() {
14941
+ var drawer = document.getElementById('brain-chunk-trace-drawer');
14942
+ if (drawer) drawer.style.transform = 'translateX(100%)';
14943
+ }
14944
+
14945
+ async function openBrainArtifact(id) {
14946
+ var drawer = document.getElementById('brain-artifact-drawer');
14947
+ if (!drawer) {
14948
+ drawer = document.createElement('div');
14949
+ drawer.id = 'brain-artifact-drawer';
14950
+ drawer.style.cssText = 'position:fixed;right:0;top:0;bottom:0;width:620px;max-width:94vw;background:var(--bg-secondary);border-left:1px solid var(--border);box-shadow:-8px 0 32px rgba(0,0,0,0.18);z-index:220;display:flex;flex-direction:column;transform:translateX(100%);transition:transform 200ms ease';
14951
+ drawer.innerHTML =
14952
+ '<div style="display:flex;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid var(--border);flex-shrink:0">'
14953
+ + '<div style="flex:1;min-width:0">'
14954
+ + '<div id="brain-artifact-title" style="font-weight:600;font-size:15px"></div>'
14955
+ + '<div id="brain-artifact-subtitle" style="font-size:11px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;margin-top:2px"></div>'
14956
+ + '</div>'
14957
+ + '<button class="btn-icon btn-sm" onclick="closeBrainArtifact()" title="Close">' + lucide('x', 'icn-sm') + '</button>'
14958
+ + '</div>'
14959
+ + '<div id="brain-artifact-body" style="flex:1;overflow:auto;padding:18px 22px;font-size:12px;line-height:1.55;white-space:pre-wrap;font-family:\\x27JetBrains Mono\\x27,monospace"></div>';
14960
+ document.body.appendChild(drawer);
14961
+ }
14962
+ drawer.style.transform = 'translateX(0)';
14963
+ document.getElementById('brain-artifact-title').textContent = 'Artifact #' + id;
14964
+ document.getElementById('brain-artifact-subtitle').textContent = 'Loading…';
14965
+ document.getElementById('brain-artifact-body').textContent = '';
14966
+ try {
14967
+ var r = await apiFetch('/api/brain/artifacts/' + encodeURIComponent(id));
14968
+ var d = await r.json();
14969
+ if (!r.ok || !d.ok || !d.artifact) throw new Error(d.error || 'not found');
14970
+ var a = d.artifact;
14971
+ document.getElementById('brain-artifact-title').textContent = a.summary || ('Artifact #' + id);
14972
+ document.getElementById('brain-artifact-subtitle').textContent = (a.toolName || 'tool') + ' · ' + (a.storedAt || '');
14973
+ document.getElementById('brain-artifact-body').textContent = a.content || a.summary || '';
14974
+ } catch (err) {
14975
+ document.getElementById('brain-artifact-subtitle').textContent = 'Failed';
14976
+ document.getElementById('brain-artifact-body').textContent = String(err);
14977
+ }
14978
+ }
14979
+
14980
+ function closeBrainArtifact() {
14981
+ var drawer = document.getElementById('brain-artifact-drawer');
14982
+ if (drawer) drawer.style.transform = 'translateX(100%)';
14983
+ }
14984
+
14985
+ async function refreshBrainOverview() {
14986
+ var kpis = document.getElementById('brain-command-kpis');
14987
+ var flow = document.getElementById('brain-overview-flow');
14988
+ try {
14989
+ var all = await Promise.all([
14990
+ apiFetch('/api/memory/health').then(function(r) { return r.json(); }).catch(function() { return {}; }),
14991
+ apiFetch('/api/brain/sources').then(function(r) { return r.json(); }).catch(function() { return {}; }),
14992
+ apiFetch('/api/brain/runs?limit=8').then(function(r) { return r.json(); }).catch(function() { return {}; }),
14993
+ ]);
14994
+ var h = (all[0] && all[0].health) || {};
14995
+ var sources = (all[1] && all[1].sources) || [];
14996
+ var runs = (all[2] && all[2].runs) || [];
14997
+ var dense = h.denseEmbeddings || {};
14998
+ var densePct = dense.total > 0 ? Math.round((dense.withDense / Math.max(1, dense.total)) * 100) + '%' : '0%';
14999
+ var activeSources = sources.filter(function(s) { return s.enabled; }).length;
15000
+ var scheduledSources = sources.filter(function(s) { return s.enabled && s.scheduleCron; }).length;
15001
+ var lastRun = runs.length ? runs[0] : null;
15002
+ if (kpis) {
15003
+ kpis.innerHTML =
15004
+ '<div class="brain-kpi"><div class="value">' + ((h.chunks && h.chunks.total) || 0).toLocaleString() + '</div><div class="label">Searchable chunks</div></div>'
15005
+ + '<div class="brain-kpi"><div class="value">' + densePct + '</div><div class="label">Semantic coverage</div></div>'
15006
+ + '<div class="brain-kpi"><div class="value">' + activeSources + '</div><div class="label">Active sources</div></div>'
15007
+ + '<div class="brain-kpi"><div class="value">' + scheduledSources + '</div><div class="label">Scheduled feeds</div></div>'
15008
+ + '<div class="brain-kpi"><div class="value">' + (lastRun ? escapeHtml(lastRun.status) : 'none') + '</div><div class="label">Latest ingestion</div></div>';
15009
+ }
15010
+ if (flow) {
15011
+ if (!runs.length) {
15012
+ flow.innerHTML = '<div class="empty-cta"><div class="label">No ingestion runs yet</div><div class="hint">Seed local data or add a scheduled feed to start the knowledge flow.</div></div>';
15013
+ } else {
15014
+ var html = '<div class="brain-flow-list">';
15015
+ html += '<div class="brain-flow-row" style="color:var(--text-muted);font-size:11px;text-transform:uppercase;letter-spacing:0.04em"><div>Source</div><div>Status</div><div>In</div><div>Written</div><div>Same</div><div>Failed</div><div>When</div></div>';
15016
+ for (var i = 0; i < runs.length; i++) {
15017
+ var run = runs[i];
15018
+ html += '<div class="brain-flow-row">'
15019
+ + '<div style="font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis">' + escapeHtml(run.sourceSlug || '—') + '</div>'
15020
+ + '<div>' + brainStatusBadge(run.status) + '</div>'
15021
+ + '<div>' + (run.recordsIn || 0) + '</div>'
15022
+ + '<div>' + (run.recordsWritten || 0) + '</div>'
15023
+ + '<div>' + (run.recordsUnchanged || 0) + '</div>'
15024
+ + '<div>' + (run.recordsFailed || 0) + '</div>'
15025
+ + '<div style="color:var(--text-muted)">' + escapeHtml(timeAgo(run.finishedAt || run.startedAt)) + '</div>'
15026
+ + '</div>';
15027
+ }
15028
+ html += '</div>';
15029
+ flow.innerHTML = html;
15030
+ }
15031
+ }
15032
+ if (document.getElementById('brain-library-results')) runBrainLibrarySearch('');
15033
+ } catch (err) {
15034
+ if (kpis) kpis.innerHTML = '<div class="empty-state" style="color:var(--red)">Failed to load overview: ' + escapeHtml(String(err)) + '</div>';
15035
+ }
15036
+ }
15037
+
14184
15038
  function brainRenderProgress(el, opts) {
14185
15039
  const label = BRAIN_STAGE_LABELS[opts.stage] || opts.stage || 'Working';
14186
15040
  const elapsed = Math.floor((Date.now() - opts.startedAt) / 1000);
@@ -14368,6 +15222,55 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14368
15222
  ];
14369
15223
  }
14370
15224
 
15225
+ function brainSetPollCron(expr) {
15226
+ var input = document.getElementById('brain-poll-cron');
15227
+ if (input) input.value = expr;
15228
+ }
15229
+
15230
+ function brainRenderPollScheduleChips() {
15231
+ var el = document.getElementById('brain-poll-schedule-chips');
15232
+ if (!el) return;
15233
+ el.innerHTML = brainScheduleChips().map(function(c) {
15234
+ return '<button class="btn-sm" type="button" onclick="brainSetPollCron(\\'' + escapeHtml(c.cron) + '\\')">' + escapeHtml(c.label) + '</button>';
15235
+ }).join('');
15236
+ }
15237
+
15238
+ function brainAddKvRow(kind, key, value) {
15239
+ var el = document.getElementById(kind === 'params' ? 'brain-poll-params-rows' : 'brain-poll-headers-rows');
15240
+ if (!el) return;
15241
+ var row = document.createElement('div');
15242
+ row.className = 'brain-kv-row';
15243
+ row.innerHTML =
15244
+ '<input type="text" class="brain-kv-key" placeholder="' + (kind === 'params' ? 'limit' : 'Authorization') + '">'
15245
+ + '<input type="text" class="brain-kv-value" placeholder="' + (kind === 'params' ? '100' : 'Bearer ${"${"}ref}') + '">'
15246
+ + '<button class="btn-icon btn-sm" type="button" onclick="this.closest(\\'.brain-kv-row\\').remove()" title="Remove">' + lucide('x', 'icn-sm') + '</button>';
15247
+ el.appendChild(row);
15248
+ row.querySelector('.brain-kv-key').value = key || '';
15249
+ row.querySelector('.brain-kv-value').value = value || '';
15250
+ }
15251
+
15252
+ function brainEnsureKvRows() {
15253
+ var h = document.getElementById('brain-poll-headers-rows');
15254
+ var p = document.getElementById('brain-poll-params-rows');
15255
+ if (h && h.children.length === 0) brainAddKvRow('headers', 'Authorization', 'Bearer ${"${"}api_key}');
15256
+ if (p && p.children.length === 0) brainAddKvRow('params', 'limit', '100');
15257
+ brainRenderPollScheduleChips();
15258
+ }
15259
+
15260
+ function brainCollectKv(kind) {
15261
+ var el = document.getElementById(kind === 'params' ? 'brain-poll-params-rows' : 'brain-poll-headers-rows');
15262
+ var out = {};
15263
+ if (!el) return out;
15264
+ el.querySelectorAll('.brain-kv-row').forEach(function(row) {
15265
+ var keyEl = row.querySelector('.brain-kv-key');
15266
+ var valEl = row.querySelector('.brain-kv-value');
15267
+ var key = keyEl ? keyEl.value.trim() : '';
15268
+ var val = valEl ? valEl.value.trim() : '';
15269
+ if (key) out[key] = val;
15270
+ });
15271
+ return out;
15272
+ }
15273
+
14371
15274
  async function brainLoadFeedConnectors() {
14372
15275
  try {
14373
15276
  const resp = await apiFetch('/api/brain/connectors');
@@ -14417,7 +15320,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14417
15320
  '<div style="font-size:12px;margin-top:4px">' + fieldsLine + '</div>' +
14418
15321
  '</div>' +
14419
15322
  '<button class="btn-primary" onclick="brainRunFeed(\\'' + f.name.replace(/"/g, '') + '\\')">Run now</button> ' +
14420
- '<button class="btn" onclick="brainDeleteFeed(\\'' + f.name.replace(/"/g, '') + '\\')">🗑</button>' +
15323
+ '<button class="btn" onclick="brainDeleteFeed(\\'' + f.name.replace(/"/g, '') + '\\')">Delete</button>' +
14421
15324
  '</div>';
14422
15325
  }).join('');
14423
15326
  } catch (err) {
@@ -14846,25 +15749,44 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14846
15749
  const data = await resp.json();
14847
15750
  const el = document.getElementById('brain-sources-list');
14848
15751
  if (!data.sources || !data.sources.length) {
14849
- el.innerHTML = '<div style="color:var(--muted);padding:20px">No sources yet. Seed a local file/folder in the <a href="#" onclick="switchTab(\\'intelligence\\',\\'seed\\');return false">Seed Upload</a> tab, or register a scheduled REST poll above.</div>';
15752
+ el.innerHTML = '<div class="empty-cta"><div class="label">No ingestion sources yet</div><div class="hint">Seed a local file/folder or create a scheduled feed. Ingested data remains searchable even if a source is later disabled.</div></div>';
14850
15753
  return;
14851
15754
  }
14852
- el.innerHTML = '<table class="data-table"><thead><tr><th>Slug</th><th>Kind</th><th>Adapter</th><th>Schedule</th><th>Target</th><th>Project</th><th>Enabled</th><th>Last run</th><th>Status</th><th>Actions</th></tr></thead><tbody>' +
14853
- data.sources.map((s) => {
14854
- const projTag = s.project
14855
- ? '<code style="font-size:11px">' + escapeHtml(s.project.split('/').filter(Boolean).pop() || s.project) + '</code>'
14856
- : '';
14857
- return '<tr><td>' + escapeHtml(s.slug) + '</td><td>' + escapeHtml(s.kind) + '</td><td>' + escapeHtml(s.adapter) + '</td>' +
14858
- '<td><code style="font-size:11px">' + escapeHtml(s.scheduleCron || '—') + '</code></td>' +
14859
- '<td>' + escapeHtml(s.targetFolder || '—') + '</td>' +
14860
- '<td>' + projTag + '</td>' +
14861
- '<td>' + (s.enabled ? 'yes' : 'no') + '</td>' +
14862
- '<td>' + escapeHtml(s.lastRunAt || '—') + '</td>' +
14863
- '<td>' + escapeHtml(s.lastStatus || '—') + '</td>' +
14864
- '<td><button class="btn" onclick="brainRunSource(\\'' + escapeHtml(s.slug) + '\\')">Run</button> ' +
14865
- '<button class="btn" onclick="brainDeleteSource(\\'' + escapeHtml(s.slug) + '\\')">🗑</button></td></tr>';
14866
- }).join('') +
14867
- '</tbody></table>';
15755
+ var html = '<div style="display:flex;flex-direction:column;gap:10px">';
15756
+ data.sources.forEach(function(s) {
15757
+ const projTag = s.project
15758
+ ? '<span class="brain-badge">' + escapeHtml(s.project.split('/').filter(Boolean).pop() || s.project) + '</span>'
15759
+ : '';
15760
+ const schedule = s.scheduleCron ? '<span class="brain-badge">' + escapeHtml(s.scheduleCron) + '</span>' : '<span class="brain-badge">manual</span>';
15761
+ const enabled = s.enabled ? '<span class="brain-badge" style="color:var(--green);border-color:var(--green)33">enabled</span>' : '<span class="brain-badge">disabled</span>';
15762
+ html += '<div class="brain-source-card">'
15763
+ + '<div style="min-width:0;flex:1">'
15764
+ + '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:5px">'
15765
+ + '<strong style="font-size:14px">' + escapeHtml(s.slug) + '</strong>'
15766
+ + '<span class="brain-badge">' + escapeHtml(s.kind) + '</span>'
15767
+ + '<span class="brain-badge">' + escapeHtml(s.adapter) + '</span>'
15768
+ + schedule + enabled + projTag
15769
+ + '</div>'
15770
+ + '<div style="font-size:12px;color:var(--text-secondary);line-height:1.5">'
15771
+ + 'Target: <code>' + escapeHtml(s.targetFolder || 'default') + '</code>'
15772
+ + ' · Last run: ' + escapeHtml(s.lastRunAt ? timeAgo(s.lastRunAt) : 'never')
15773
+ + ' · Status: ' + escapeHtml(s.lastStatus || 'none')
15774
+ + '</div>'
15775
+ + '</div>'
15776
+ + '<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">'
15777
+ + '<button class="btn-sm brain-run-source" data-slug="' + escapeHtml(s.slug) + '">Run now</button>'
15778
+ + '<button class="btn-sm brain-delete-source" data-slug="' + escapeHtml(s.slug) + '" title="Delete source">Delete</button>'
15779
+ + '</div>'
15780
+ + '</div>';
15781
+ });
15782
+ html += '</div>';
15783
+ el.innerHTML = html;
15784
+ el.querySelectorAll('.brain-run-source').forEach(function(btn) {
15785
+ btn.onclick = function() { brainRunSource(btn.getAttribute('data-slug') || ''); };
15786
+ });
15787
+ el.querySelectorAll('.brain-delete-source').forEach(function(btn) {
15788
+ btn.onclick = function() { brainDeleteSource(btn.getAttribute('data-slug') || ''); };
15789
+ });
14868
15790
  }
14869
15791
 
14870
15792
  async function brainRunSource(slug) {
@@ -14902,6 +15824,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14902
15824
  document.getElementById('brain-creds-form').style.display = 'none';
14903
15825
  const wf = document.getElementById('brain-webhook-form'); if (wf) wf.style.display = 'none';
14904
15826
  brainLoadProjects(['brain-poll-project']);
15827
+ brainEnsureKvRows();
14905
15828
  }
14906
15829
 
14907
15830
  function brainShowWebhookForm() {
@@ -14972,17 +15895,14 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14972
15895
  const slug = document.getElementById('brain-poll-slug').value.trim();
14973
15896
  const url = document.getElementById('brain-poll-url').value.trim();
14974
15897
  const method = document.getElementById('brain-poll-method').value;
14975
- const headersText = document.getElementById('brain-poll-headers').value.trim();
14976
- const paramsText = document.getElementById('brain-poll-params').value.trim();
14977
15898
  const recordsPath = document.getElementById('brain-poll-recordspath').value.trim();
14978
15899
  const cronExpr = document.getElementById('brain-poll-cron').value.trim();
14979
15900
  const folder = document.getElementById('brain-poll-folder').value.trim();
14980
15901
  const statusEl = document.getElementById('brain-poll-status');
14981
15902
  if (!slug || !url) { statusEl.innerHTML = '<span style="color:#e66">slug and URL required</span>'; return; }
14982
15903
 
14983
- let headers = {}, params = {};
14984
- try { if (headersText) headers = JSON.parse(headersText); } catch (e) { statusEl.innerHTML = '<span style="color:#e66">Invalid headers JSON</span>'; return; }
14985
- try { if (paramsText) params = JSON.parse(paramsText); } catch (e) { statusEl.innerHTML = '<span style="color:#e66">Invalid params JSON</span>'; return; }
15904
+ const headers = brainCollectKv('headers');
15905
+ const params = brainCollectKv('params');
14986
15906
 
14987
15907
  const cfg = { url, method, headers, params };
14988
15908
  if (recordsPath) cfg.recordsJsonPath = recordsPath;
@@ -15018,6 +15938,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
15018
15938
  async function brainShowCredsForm() {
15019
15939
  document.getElementById('brain-creds-form').style.display = '';
15020
15940
  document.getElementById('brain-poll-form').style.display = 'none';
15941
+ const wf = document.getElementById('brain-webhook-form'); if (wf) wf.style.display = 'none';
15021
15942
  const resp = await apiFetch('/api/brain/credentials');
15022
15943
  const data = await resp.json();
15023
15944
  const refs = data.refs || [];
@@ -15045,20 +15966,21 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
15045
15966
  }
15046
15967
 
15047
15968
  async function brainLoadRuns() {
15048
- const resp = await apiFetch('/api/brain/runs');
15969
+ const resp = await apiFetch('/api/brain/runs?limit=80');
15049
15970
  const data = await resp.json();
15050
15971
  const el = document.getElementById('brain-runs-list');
15051
15972
  if (!data.runs || !data.runs.length) {
15052
- el.innerHTML = '<div style="color:var(--muted);padding:20px">No ingestion runs yet.</div>';
15973
+ el.innerHTML = '<div class="empty-cta"><div class="label">No ingestion runs yet</div><div class="hint">Preview and commit a seed, or run a scheduled source.</div></div>';
15053
15974
  return;
15054
15975
  }
15055
- el.innerHTML = '<table class="data-table"><thead><tr><th>#</th><th>Source</th><th>Started</th><th>Status</th><th>In</th><th>Written</th><th>Skipped</th><th>Failed</th><th>Overview</th></tr></thead><tbody>' +
15976
+ el.innerHTML = '<table class="data-table"><thead><tr><th>#</th><th>Source</th><th>Started</th><th>Status</th><th>In</th><th>Written</th><th>Same</th><th>Skipped</th><th>Failed</th><th>Recall check</th><th>Overview</th></tr></thead><tbody>' +
15056
15977
  data.runs.map((r) =>
15057
15978
  '<tr><td>' + r.id + '</td><td>' + escapeHtml(r.sourceSlug) + '</td>' +
15058
15979
  '<td>' + escapeHtml(r.startedAt) + '</td>' +
15059
- '<td>' + escapeHtml(r.status) + '</td>' +
15980
+ '<td>' + brainStatusBadge(r.status) + '</td>' +
15060
15981
  '<td>' + r.recordsIn + '</td><td>' + r.recordsWritten + '</td>' +
15061
- '<td>' + r.recordsSkipped + '</td><td>' + r.recordsFailed + '</td>' +
15982
+ '<td>' + (r.recordsUnchanged || 0) + '</td><td>' + r.recordsSkipped + '</td><td>' + r.recordsFailed + '</td>' +
15983
+ '<td>' + escapeHtml(r.recallCheckStatus || '—') + '</td>' +
15062
15984
  '<td>' + (r.overviewNotePath ? '<code style="font-size:12px">' + escapeHtml(r.overviewNotePath) + '</code>' : '—') + '</td></tr>').join('') +
15063
15985
  '</tbody></table>';
15064
15986
  }
@@ -15078,6 +16000,23 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
15078
16000
  // the on-disk path, which we feed into the existing preview/
15079
16001
  // commit pipeline.
15080
16002
 
16003
+ function brainHandleDrag(event, over) {
16004
+ event.preventDefault();
16005
+ var zone = document.getElementById('brain-drop-zone');
16006
+ if (!zone) return;
16007
+ if (over) zone.classList.add('dragover');
16008
+ else zone.classList.remove('dragover');
16009
+ }
16010
+
16011
+ async function brainHandleDrop(event) {
16012
+ event.preventDefault();
16013
+ var zone = document.getElementById('brain-drop-zone');
16014
+ if (zone) zone.classList.remove('dragover');
16015
+ var files = event.dataTransfer && event.dataTransfer.files ? event.dataTransfer.files : null;
16016
+ if (!files || files.length === 0) return;
16017
+ await brainHandleFilesChosen(files, false);
16018
+ }
16019
+
15081
16020
  async function brainHandleFilesChosen(fileList, isFolder) {
15082
16021
  const statusEl = document.getElementById('brain-upload-status');
15083
16022
  if (!fileList || fileList.length === 0) return;
@@ -15126,15 +16065,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
15126
16065
  }
15127
16066
  }
15128
16067
 
15129
- // Wire tab refresh on switchTab brain tabs
15130
- (function() {
15131
- const origSwitch = window.switchTab;
15132
- window.switchTab = function(page, tab) {
15133
- origSwitch(page, tab);
15134
- if (page === 'intelligence' && tab === 'sources') { brainLoadSources(); brainLoadFeedConnectors(); brainLoadFeeds(); }
15135
- if (page === 'intelligence' && tab === 'runs') brainLoadRuns();
15136
- };
15137
- })();
16068
+ // Brain tab refresh is handled by the global switchTab dispatcher.
15138
16069
  </script>
15139
16070
  </div>
15140
16071
 
@@ -16343,6 +17274,11 @@ var LUCIDE = {
16343
17274
  send: '<path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/>',
16344
17275
  arrowRight: '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
16345
17276
  tool: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
17277
+ upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/>',
17278
+ repeat: '<path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/>',
17279
+ listChecks: '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/>',
17280
+ activity: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
17281
+ layoutDashboard:'<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>',
16346
17282
  database: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5"/><path d="M3 12a9 3 0 0 0 18 0"/>',
16347
17283
  };
16348
17284
  function lucide(name, cls) {
@@ -16366,7 +17302,7 @@ var ROUTE_REDIRECTS = {
16366
17302
  'heartbeats': { page: 'heartbeat' },
16367
17303
  'team-status': { page: 'team', tab: 'activity' },
16368
17304
  'agent-detail': { page: 'team', tab: 'roster' },
16369
- 'intelligence': { page: 'brain', tab: 'memory' },
17305
+ 'intelligence': { page: 'brain', tab: 'overview' },
16370
17306
  'memory-health': { page: 'brain', tab: 'health' },
16371
17307
  'claims': { page: 'brain', tab: 'health' },
16372
17308
  'metrics': { page: 'team', tab: 'activity' },
@@ -16439,9 +17375,10 @@ function navigateTo(page, opts) {
16439
17375
  }
16440
17376
  break;
16441
17377
  case 'brain':
16442
- var bt = opts.tab || 'memory';
17378
+ var bt = opts.tab || 'overview';
16443
17379
  // Spec tab names → internal intelligence-tab ids
16444
- var intelTab = bt === 'memory' ? 'search'
17380
+ var intelTab = bt === 'overview' ? 'overview'
17381
+ : bt === 'memory' ? 'search'
16445
17382
  : bt === 'knowledge' ? 'graph'
16446
17383
  : bt === 'ingestion' ? 'sources'
16447
17384
  : bt === 'health' ? 'health'
@@ -16846,6 +17783,7 @@ function switchTab(group, tab) {
16846
17783
  }
16847
17784
  // Tab-specific refresh
16848
17785
  if (group === 'intelligence') {
17786
+ if (tab === 'overview' && typeof refreshBrainOverview === 'function') refreshBrainOverview();
16849
17787
  if (tab === 'graph') refreshGraph();
16850
17788
  if (tab === 'search') {
16851
17789
  // Consolidated Memory tab: search results + stats + MEMORY.md + recent writes + supersedes + coverage strip.
@@ -16855,6 +17793,12 @@ function switchTab(group, tab) {
16855
17793
  if (typeof refreshCoverageStrip === 'function') refreshCoverageStrip();
16856
17794
  }
16857
17795
  if (tab === 'files' && typeof refreshVaultFiles === 'function') refreshVaultFiles();
17796
+ if (tab === 'sources') {
17797
+ if (typeof brainLoadSources === 'function') brainLoadSources();
17798
+ if (typeof brainLoadFeedConnectors === 'function') brainLoadFeedConnectors();
17799
+ if (typeof brainLoadFeeds === 'function') brainLoadFeeds();
17800
+ }
17801
+ if (tab === 'runs' && typeof brainLoadRuns === 'function') brainLoadRuns();
16858
17802
  if (tab === 'health') {
16859
17803
  if (typeof refreshMemoryHealth === 'function') refreshMemoryHealth();
16860
17804
  if (typeof refreshGraphStats === 'function') refreshGraphStats();
@@ -22513,6 +23457,13 @@ function parseSearchFilters(raw) {
22513
23457
  return { q: cleaned, filters: filters };
22514
23458
  }
22515
23459
 
23460
+ function runMemoryDetailSearch() {
23461
+ var detail = document.getElementById('memory-detail-search-input');
23462
+ var header = document.getElementById('memory-search-input');
23463
+ if (header && detail) header.value = detail.value;
23464
+ runMemorySearch();
23465
+ }
23466
+
22516
23467
  async function runMemorySearch() {
22517
23468
  const input = document.getElementById('memory-search-input');
22518
23469
  const raw = input.value.trim();