clementine-agent 1.4.1 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli/dashboard.js +417 -64
  2. package/package.json +1 -1
@@ -2240,6 +2240,140 @@ export async function cmdDashboard(opts) {
2240
2240
  writeFileSync(queueFile, JSON.stringify(queue, null, 2));
2241
2241
  res.json({ ok: true, id });
2242
2242
  });
2243
+ app.get('/api/vault-files', async (req, res) => {
2244
+ try {
2245
+ const limit = Math.min(parseInt(String(req.query.limit ?? '120'), 10) || 120, 500);
2246
+ const sinceDays = Math.max(parseInt(String(req.query.sinceDays ?? '30'), 10) || 30, 1);
2247
+ const agentFilter = typeof req.query.agent === 'string' ? req.query.agent : '';
2248
+ const folderFilter = typeof req.query.folder === 'string' ? req.query.folder : '';
2249
+ const search = typeof req.query.q === 'string' ? req.query.q.toLowerCase() : '';
2250
+ const includeAuto = req.query.includeAuto === '1';
2251
+ const cutoffMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
2252
+ const vaultRoot = path.join(BASE_DIR, 'vault');
2253
+ const matter = (await import('gray-matter')).default;
2254
+ const files = [];
2255
+ function walk(dir) {
2256
+ let entries = [];
2257
+ try {
2258
+ entries = readdirSync(dir);
2259
+ }
2260
+ catch {
2261
+ return;
2262
+ }
2263
+ for (const e of entries) {
2264
+ if (e.startsWith('.'))
2265
+ continue;
2266
+ const full = path.join(dir, e);
2267
+ let stat;
2268
+ try {
2269
+ stat = statSync(full);
2270
+ }
2271
+ catch {
2272
+ continue;
2273
+ }
2274
+ if (stat.isDirectory()) {
2275
+ walk(full);
2276
+ continue;
2277
+ }
2278
+ if (!e.endsWith('.md'))
2279
+ continue;
2280
+ if (e.endsWith('.md.bak'))
2281
+ continue;
2282
+ if (stat.mtimeMs < cutoffMs)
2283
+ continue;
2284
+ const rel = path.relative(vaultRoot, full);
2285
+ // Skip auto-generated MCP/skill wrappers unless explicitly requested.
2286
+ // Path patterns: 00-System/skills/auto/* (generated tool wrappers).
2287
+ if (!includeAuto && rel.startsWith('00-System/skills/auto/'))
2288
+ continue;
2289
+ if (!includeAuto && rel.startsWith('00-System/agents/') && /\/(MEMORY|HEARTBEAT|TASKS|CRON)\.md$/.test(rel))
2290
+ continue;
2291
+ const folder = path.dirname(rel).split(path.sep)[0] || '';
2292
+ let agentSlug = null;
2293
+ if (rel.startsWith('00-System/agents/')) {
2294
+ const m = rel.match(/^00-System\/agents\/([^/]+)\//);
2295
+ if (m)
2296
+ agentSlug = m[1];
2297
+ }
2298
+ // Skip system housekeeping files (their author will surface via mtime in agent's own dir)
2299
+ let title = path.basename(rel, '.md');
2300
+ let typeTag = null;
2301
+ try {
2302
+ const head = readFileSync(full, 'utf-8').slice(0, 4000);
2303
+ const parsed = matter(head);
2304
+ const data = parsed.data;
2305
+ if (typeof data.title === 'string')
2306
+ title = data.title;
2307
+ else if (typeof data.name === 'string')
2308
+ title = data.name;
2309
+ else {
2310
+ const h1 = (parsed.content || '').match(/^#\s+(.+)$/m);
2311
+ if (h1)
2312
+ title = h1[1].trim();
2313
+ }
2314
+ if (typeof data.type === 'string')
2315
+ typeTag = data.type;
2316
+ }
2317
+ catch { /* */ }
2318
+ files.push({
2319
+ path: full,
2320
+ relPath: rel,
2321
+ title,
2322
+ folder,
2323
+ agentSlug,
2324
+ mtime: new Date(stat.mtimeMs).toISOString(),
2325
+ sizeBytes: stat.size,
2326
+ type: typeTag,
2327
+ });
2328
+ }
2329
+ }
2330
+ walk(vaultRoot);
2331
+ let filtered = files
2332
+ .sort((a, b) => b.mtime.localeCompare(a.mtime))
2333
+ .filter(f => {
2334
+ if (agentFilter === '__shared__' && f.agentSlug != null)
2335
+ return false;
2336
+ if (agentFilter && agentFilter !== '__shared__' && f.agentSlug !== agentFilter)
2337
+ return false;
2338
+ if (folderFilter && f.folder !== folderFilter)
2339
+ return false;
2340
+ if (search) {
2341
+ const hay = (f.title + ' ' + f.relPath).toLowerCase();
2342
+ if (!hay.includes(search))
2343
+ return false;
2344
+ }
2345
+ return true;
2346
+ })
2347
+ .slice(0, limit);
2348
+ // Compute folder counts for filter chips
2349
+ const folderCounts = {};
2350
+ for (const f of files)
2351
+ folderCounts[f.folder] = (folderCounts[f.folder] || 0) + 1;
2352
+ res.json({ files: filtered, total: files.length, folderCounts });
2353
+ }
2354
+ catch (err) {
2355
+ res.status(500).json({ error: String(err) });
2356
+ }
2357
+ });
2358
+ app.get('/api/vault-file', async (req, res) => {
2359
+ try {
2360
+ const relPath = typeof req.query.path === 'string' ? req.query.path : '';
2361
+ if (!relPath || relPath.includes('..')) {
2362
+ res.status(400).json({ error: 'Bad path' });
2363
+ return;
2364
+ }
2365
+ const full = path.join(BASE_DIR, 'vault', relPath);
2366
+ if (!existsSync(full)) {
2367
+ res.status(404).json({ error: 'Not found' });
2368
+ return;
2369
+ }
2370
+ const content = readFileSync(full, 'utf-8');
2371
+ res.json({ path: relPath, content });
2372
+ }
2373
+ catch (err) {
2374
+ res.status(500).json({ error: String(err) });
2375
+ }
2376
+ });
2243
2377
  app.get('/api/memory', async (_req, res) => {
2244
2378
  res.json(await getMemory());
2245
2379
  });
@@ -8718,13 +8852,15 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
8718
8852
  .home-rail {
8719
8853
  display: flex;
8720
8854
  flex-direction: column;
8721
- gap: 12px;
8855
+ gap: 8px;
8722
8856
  overflow-y: auto;
8723
8857
  position: relative;
8724
8858
  }
8725
8859
  .home-rail.collapsed {
8726
8860
  display: none;
8727
8861
  }
8862
+ /* Auto-hide cards that have no actionable content (set via JS toggling .rail-card.empty) */
8863
+ .rail-card.empty { display: none; }
8728
8864
  .rail-collapse-btn {
8729
8865
  position: absolute;
8730
8866
  top: -4px;
@@ -8745,23 +8881,24 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
8745
8881
  .rail-card {
8746
8882
  background: var(--bg-card);
8747
8883
  border: 1px solid var(--border);
8748
- border-radius: 10px;
8884
+ border-radius: var(--radius-md);
8749
8885
  overflow: hidden;
8750
- box-shadow: 0 1px 4px rgba(0,0,0,0.04);
8886
+ box-shadow: var(--shadow-xs);
8751
8887
  }
8752
8888
  .rail-header {
8753
8889
  display: flex;
8754
8890
  align-items: center;
8755
8891
  justify-content: space-between;
8756
- padding: 10px 14px;
8757
- font-size: 12px;
8892
+ padding: 8px 12px;
8893
+ font-size: var(--text-xs);
8758
8894
  font-weight: 600;
8759
- color: var(--text-secondary);
8760
- border-bottom: 1px solid var(--border);
8761
- background: var(--bg-secondary);
8895
+ text-transform: uppercase;
8896
+ letter-spacing: 0.04em;
8897
+ color: var(--text-muted);
8898
+ background: transparent;
8762
8899
  }
8763
- .rail-body { padding: 12px 14px; font-size: 12.5px; line-height: 1.5; }
8764
- .rail-body .empty-state, .rail-body .skel-row { font-size: 11px; }
8900
+ .rail-body { padding: 8px 12px 10px; font-size: var(--text-sm); line-height: 1.45; }
8901
+ .rail-body .empty-state, .rail-body .skel-row { font-size: var(--text-xs); }
8765
8902
  .rail-badge {
8766
8903
  display: inline-flex;
8767
8904
  align-items: center;
@@ -10540,35 +10677,39 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10540
10677
  margin-top: 4px;
10541
10678
  }
10542
10679
 
10543
- /* ── Timeline ───────────────────────────── */
10680
+ /* ── Timeline (compact) ───────────────────── */
10544
10681
  .timeline {
10545
10682
  position: relative;
10546
- padding-left: 24px;
10683
+ padding-left: 18px;
10547
10684
  }
10548
10685
  .timeline::before {
10549
10686
  content: '';
10550
10687
  position: absolute;
10551
- left: 7px;
10688
+ left: 5px;
10552
10689
  top: 4px;
10553
10690
  bottom: 4px;
10554
- width: 2px;
10691
+ width: 1px;
10555
10692
  background: var(--border);
10556
10693
  }
10557
10694
  .timeline-item {
10558
10695
  position: relative;
10559
- padding: 8px 0;
10560
- font-size: 12px;
10696
+ padding: 4px 0;
10697
+ font-size: var(--text-sm);
10561
10698
  display: flex;
10562
- align-items: flex-start;
10563
- gap: 10px;
10699
+ align-items: center;
10700
+ gap: 8px;
10701
+ border-radius: var(--radius-xs);
10702
+ transition: background var(--motion);
10564
10703
  }
10704
+ .timeline-item:hover { background: var(--bg-hover); }
10565
10705
  .timeline-item::before {
10566
10706
  content: '';
10567
10707
  position: absolute;
10568
- left: -20px;
10569
- top: 14px;
10570
- width: 8px;
10571
- height: 8px;
10708
+ left: -16px;
10709
+ top: 50%;
10710
+ transform: translateY(-50%);
10711
+ width: 7px;
10712
+ height: 7px;
10572
10713
  border-radius: 50%;
10573
10714
  background: var(--text-muted);
10574
10715
  border: 2px solid var(--bg-primary);
@@ -10576,8 +10717,52 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10576
10717
  }
10577
10718
  .timeline-item.ok::before { background: var(--green); }
10578
10719
  .timeline-item.error::before { background: var(--red); }
10579
- .timeline-msg { flex: 1; color: var(--text-primary); line-height: 1.4; }
10580
- .timeline-time { flex-shrink: 0; color: var(--text-muted); font-size: 11px; }
10720
+ .timeline-msg {
10721
+ flex: 1;
10722
+ color: var(--text-primary);
10723
+ line-height: 1.35;
10724
+ white-space: nowrap;
10725
+ overflow: hidden;
10726
+ text-overflow: ellipsis;
10727
+ min-width: 0;
10728
+ }
10729
+ .timeline-title { font-weight: 500; }
10730
+ .timeline-agent {
10731
+ color: var(--clementine);
10732
+ font-size: var(--text-xs);
10733
+ margin-left: 6px;
10734
+ font-weight: 500;
10735
+ }
10736
+ .timeline-body {
10737
+ color: var(--text-muted);
10738
+ font-size: var(--text-xs);
10739
+ margin-left: 4px;
10740
+ }
10741
+ .timeline-time { flex-shrink: 0; color: var(--text-muted); font-size: var(--text-xs); }
10742
+ /* Cap activity card height so chat dominates the page */
10743
+ .home-activity .card-body { max-height: 320px; overflow-y: auto; padding: 10px 16px; }
10744
+ /* Hide chat profile selector when default — the row gets cleaner */
10745
+ .home-chat-input-row .chat-profile-spacer { display: none; }
10746
+
10747
+ /* Vault file folder chips */
10748
+ .vault-folder-chip {
10749
+ padding: 4px 12px;
10750
+ border: 1px solid var(--border);
10751
+ border-radius: 16px;
10752
+ font-size: var(--text-sm);
10753
+ color: var(--text-secondary);
10754
+ cursor: pointer;
10755
+ transition: all var(--motion);
10756
+ background: var(--bg-secondary);
10757
+ user-select: none;
10758
+ }
10759
+ .vault-folder-chip:hover { background: var(--bg-hover); color: var(--text-primary); }
10760
+ .vault-folder-chip.active {
10761
+ background: var(--clementine-bg);
10762
+ color: var(--clementine);
10763
+ border-color: var(--clementine);
10764
+ font-weight: 500;
10765
+ }
10581
10766
 
10582
10767
  /* ── Task Cards ─────────────────────────── */
10583
10768
  .task-grid {
@@ -11734,6 +11919,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11734
11919
  <div class="tab-bar" id="intelligence-tabs" style="margin:0 0 0 18px">
11735
11920
  <button class="active" data-icon="database" onclick="switchTab('intelligence','search')"><span class="icon-slot"></span> Memory</button>
11736
11921
  <button data-icon="sparkles" onclick="switchTab('intelligence','graph')"><span class="icon-slot"></span> Knowledge</button>
11922
+ <button data-icon="fileText" onclick="switchTab('intelligence','files')"><span class="icon-slot"></span> Files</button>
11737
11923
  <button data-icon="folder" onclick="switchTab('intelligence','sources')"><span class="icon-slot"></span> Ingestion</button>
11738
11924
  <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>
11739
11925
  <button data-icon="users" onclick="switchTab('intelligence','user-model')"><span class="icon-slot"></span> User Model</button>
@@ -11942,6 +12128,26 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11942
12128
  <div class="tab-pane" id="tab-intelligence-runs">
11943
12129
  <div id="brain-runs-list"></div>
11944
12130
  </div>
12131
+ <div class="tab-pane" id="tab-intelligence-files">
12132
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap">
12133
+ <input type="text" id="vault-files-search" placeholder="Search title or path..." style="flex:1;min-width:200px;padding:7px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-input);color:var(--text-primary);font-size:13px" oninput="refreshVaultFiles()">
12134
+ <select id="vault-files-agent-filter" onchange="refreshVaultFiles()" style="padding:7px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-primary);font-size:12px">
12135
+ <option value="">All authors</option>
12136
+ <option value="__shared__">Shared (vault root)</option>
12137
+ </select>
12138
+ <select id="vault-files-since" onchange="refreshVaultFiles()" style="padding:7px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-primary);font-size:12px">
12139
+ <option value="7">Past 7 days</option>
12140
+ <option value="30" selected>Past 30 days</option>
12141
+ <option value="90">Past 90 days</option>
12142
+ <option value="365">Past year</option>
12143
+ </select>
12144
+ <button class="btn-sm" onclick="refreshVaultFiles()">Refresh</button>
12145
+ </div>
12146
+ <div id="vault-files-folder-chips" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px"></div>
12147
+ <div id="vault-files-list">
12148
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
12149
+ </div>
12150
+ </div>
11945
12151
  <div class="tab-pane" id="tab-intelligence-health">
11946
12152
  <div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap">
11947
12153
  <button class="btn-sm" onclick="memoryHealthAction('janitor')" title="Run the janitor cleanup pass now">Run cleanup</button>
@@ -14534,6 +14740,7 @@ function switchTab(group, tab) {
14534
14740
  if (group === 'intelligence') {
14535
14741
  if (tab === 'graph') refreshGraph();
14536
14742
  if (tab === 'memory') refreshMemory();
14743
+ if (tab === 'files' && typeof refreshVaultFiles === 'function') refreshVaultFiles();
14537
14744
  if (tab === 'health') {
14538
14745
  if (typeof refreshMemoryHealth === 'function') refreshMemoryHealth();
14539
14746
  if (typeof refreshClaims === 'function') refreshClaims();
@@ -17260,15 +17467,15 @@ var sourceIcons = {
17260
17467
  };
17261
17468
 
17262
17469
  function activityEventHtml(e) {
17263
- var icon = sourceIcons[e.source] || '&#9679;';
17264
17470
  var statusCls = e.status === 'ok' || e.status === 'approved' ? 'ok'
17265
17471
  : (e.status === 'error' || e.eventType === 'cron_error') ? 'error'
17266
- : e.status === 'pending' ? '' : '';
17267
- var agentLabel = e.agentSlug ? '<span style="color:var(--accent);font-size:11px;margin-left:4px">[' + esc(e.agentSlug) + ']</span>' : '';
17268
- return '<div class="timeline-item ' + statusCls + '">'
17269
- + '<span style="margin-right:6px">' + icon + '</span>'
17270
- + '<span class="timeline-msg">' + esc(e.title) + agentLabel
17271
- + (e.body ? '<span onclick="this.style.whiteSpace=this.style.whiteSpace===\\x27normal\\x27?\\x27nowrap\\x27:\\x27normal\\x27;this.style.overflow=this.style.whiteSpace===\\x27normal\\x27?\\x27visible\\x27:\\x27hidden\\x27;this.style.maxWidth=this.style.whiteSpace===\\x27normal\\x27?\\x27none\\x27:\\x27400px\\x27" style="display:block;font-size:11px;color:var(--text-muted);margin-top:2px;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer" title="Click to expand">' + esc(e.body) + '</span>' : '')
17472
+ : e.status === 'pending' ? 'pending' : '';
17473
+ var agentLabel = e.agentSlug ? '<span class="timeline-agent">[' + esc(e.agentSlug) + ']</span>' : '';
17474
+ return '<div class="timeline-item ' + statusCls + '" title="' + esc(e.body || e.title) + '">'
17475
+ + '<span class="timeline-msg">'
17476
+ + '<span class="timeline-title">' + esc(e.title) + '</span>'
17477
+ + agentLabel
17478
+ + (e.body ? ' <span class="timeline-body">&middot; ' + esc(e.body) + '</span>' : '')
17272
17479
  + '</span>'
17273
17480
  + '<span class="timeline-time">' + timeAgo(e.timestamp) + '</span>'
17274
17481
  + '</div>';
@@ -18396,13 +18603,17 @@ async function loadProfiles() {
18396
18603
  var d = await r.json();
18397
18604
  var sel = document.getElementById('chat-profile-select');
18398
18605
  sel.innerHTML = '<option value="">Default</option>';
18606
+ var customCount = 0;
18399
18607
  for (var p of (d.profiles || [])) {
18400
18608
  var opt = document.createElement('option');
18401
18609
  opt.value = p.slug;
18402
18610
  opt.textContent = p.name + (p.description ? ' — ' + p.description : '');
18403
18611
  if (p.slug === d.active) opt.selected = true;
18404
18612
  sel.appendChild(opt);
18613
+ customCount++;
18405
18614
  }
18615
+ // Hide the picker entirely if there are no custom profiles — declutters the chat input row.
18616
+ sel.style.display = customCount === 0 ? 'none' : '';
18406
18617
  } catch(e) { /* profiles are optional */ }
18407
18618
  }
18408
18619
 
@@ -19781,6 +19992,134 @@ async function memoryHealthAction(action) {
19781
19992
  }
19782
19993
  }
19783
19994
 
19995
+ // ── Vault Files (Brain → Files tab) ──────────────────────────────
19996
+ var _vaultFilesCache = null;
19997
+ var _vaultFilesFolder = ''; // current folder filter
19998
+
19999
+ async function refreshVaultFiles() {
20000
+ var listEl = document.getElementById('vault-files-list');
20001
+ if (!listEl) return;
20002
+ var q = document.getElementById('vault-files-search')?.value || '';
20003
+ var agent = document.getElementById('vault-files-agent-filter')?.value || '';
20004
+ var since = document.getElementById('vault-files-since')?.value || '30';
20005
+ // Show skeleton while loading
20006
+ listEl.innerHTML = '<div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>';
20007
+ try {
20008
+ var url = '/api/vault-files?sinceDays=' + encodeURIComponent(since)
20009
+ + (q ? '&q=' + encodeURIComponent(q) : '')
20010
+ + (agent ? '&agent=' + encodeURIComponent(agent) : '')
20011
+ + (_vaultFilesFolder ? '&folder=' + encodeURIComponent(_vaultFilesFolder) : '');
20012
+ var r = await apiFetch(url);
20013
+ var d = await r.json();
20014
+ var files = d.files || [];
20015
+ _vaultFilesCache = files;
20016
+ // Populate agent filter from ALL files response (use server's full set, not filtered)
20017
+ var agentSel = document.getElementById('vault-files-agent-filter');
20018
+ if (agentSel && agentSel.options.length <= 2) {
20019
+ var slugs = [...new Set(files.map(function(f) { return f.agentSlug; }).filter(Boolean))].sort();
20020
+ slugs.forEach(function(slug) {
20021
+ var opt = document.createElement('option');
20022
+ opt.value = slug;
20023
+ opt.textContent = slug;
20024
+ agentSel.appendChild(opt);
20025
+ });
20026
+ }
20027
+ // Render folder filter chips (using folderCounts from server)
20028
+ var chipsEl = document.getElementById('vault-files-folder-chips');
20029
+ if (chipsEl && d.folderCounts) {
20030
+ var folders = Object.entries(d.folderCounts).sort(function(a, b) { return b[1] - a[1]; });
20031
+ var totalCount = folders.reduce(function(s, p) { return s + p[1]; }, 0);
20032
+ var chipHtml = '<div class="vault-folder-chip' + (_vaultFilesFolder === '' ? ' active' : '') + '" data-folder="" onclick="setVaultFolderFilter(\\x27\\x27)">All <span style="opacity:0.6">' + totalCount + '</span></div>';
20033
+ folders.forEach(function(p) {
20034
+ var folder = p[0]; var count = p[1];
20035
+ if (!folder) return;
20036
+ chipHtml += '<div class="vault-folder-chip' + (_vaultFilesFolder === folder ? ' active' : '') + '" data-folder="' + esc(folder) + '" onclick="setVaultFolderFilter(\\x27' + esc(folder) + '\\x27)">' + esc(folder) + ' <span style="opacity:0.6">' + count + '</span></div>';
20037
+ });
20038
+ chipsEl.innerHTML = chipHtml;
20039
+ }
20040
+ if (files.length === 0) {
20041
+ listEl.innerHTML = '<div class="empty-cta"><div class="label">No recent files</div><div class="hint">Try a wider time window or different filter.</div></div>';
20042
+ return;
20043
+ }
20044
+ var html = '<div style="font-size:11px;color:var(--text-muted);margin-bottom:10px">Showing ' + files.length + ' of ' + d.total + ' files modified in the last ' + since + ' days.</div>';
20045
+ html += '<div style="display:flex;flex-direction:column;gap:1px;border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden;background:var(--bg-card)">';
20046
+ for (var i = 0; i < files.length; i++) {
20047
+ var f = files[i];
20048
+ var agentBadge = f.agentSlug
20049
+ ? '<span style="font-size:10px;background:var(--clementine-bg);color:var(--clementine);padding:2px 7px;border-radius:var(--radius-xs);font-weight:500">' + esc(f.agentSlug) + '</span>'
20050
+ : '<span style="font-size:10px;background:var(--bg-tertiary);color:var(--text-muted);padding:2px 7px;border-radius:var(--radius-xs)">shared</span>';
20051
+ var typeBadge = f.type ? '<span style="font-size:10px;color:var(--text-muted);margin-right:6px">' + esc(f.type) + '</span>' : '';
20052
+ html += '<div class="vault-file-row clickable-row" data-path="' + esc(f.relPath) + '" style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg-secondary);border-bottom:1px solid var(--border-light);font-size:13px">'
20053
+ + '<div style="flex:1;min-width:0">'
20054
+ + '<div style="font-weight:500;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(f.title) + '</div>'
20055
+ + '<div style="font-size:11px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px">' + esc(f.relPath) + '</div>'
20056
+ + '</div>'
20057
+ + '<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">'
20058
+ + typeBadge + agentBadge
20059
+ + '<span style="font-size:11px;color:var(--text-muted);min-width:60px;text-align:right">' + esc(timeAgo(f.mtime)) + '</span>'
20060
+ + '</div>'
20061
+ + '</div>';
20062
+ }
20063
+ html += '</div>';
20064
+ listEl.innerHTML = html;
20065
+ // Wire row clicks
20066
+ listEl.querySelectorAll('.vault-file-row').forEach(function(row) {
20067
+ row.onclick = function() { openVaultFile(row.getAttribute('data-path')); };
20068
+ });
20069
+ } catch (err) {
20070
+ listEl.innerHTML = '<div style="padding:24px;color:var(--red);font-size:13px">Failed to load: ' + esc(String(err)) + '</div>';
20071
+ }
20072
+ }
20073
+
20074
+ async function openVaultFile(relPath) {
20075
+ if (!relPath) return;
20076
+ // Build/reuse a slide-out drawer for content preview
20077
+ var drawer = document.getElementById('vault-file-drawer');
20078
+ if (!drawer) {
20079
+ drawer = document.createElement('div');
20080
+ drawer.id = 'vault-file-drawer';
20081
+ drawer.style.cssText = 'position:fixed;right:0;top:0;bottom:0;width:560px;max-width:92vw;background:var(--bg-secondary);border-left:1px solid var(--border);box-shadow:-8px 0 32px rgba(0,0,0,0.18);z-index:200;display:flex;flex-direction:column;transform:translateX(100%);transition:transform 200ms ease';
20082
+ drawer.innerHTML =
20083
+ '<div style="display:flex;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid var(--border);flex-shrink:0">'
20084
+ + '<div style="flex:1;min-width:0">'
20085
+ + '<div id="vault-file-drawer-title" style="font-weight:600;font-size:15px;letter-spacing:-0.01em"></div>'
20086
+ + '<div id="vault-file-drawer-path" style="font-size:11px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></div>'
20087
+ + '</div>'
20088
+ + '<button class="btn-icon btn-sm" onclick="closeVaultFileDrawer()" title="Close">' + lucide('x', 'icn-sm') + '</button>'
20089
+ + '</div>'
20090
+ + '<div id="vault-file-drawer-body" style="flex:1;overflow-y:auto;padding:18px 22px;font-size:13px;line-height:1.55"></div>';
20091
+ document.body.appendChild(drawer);
20092
+ }
20093
+ var titleEl = document.getElementById('vault-file-drawer-title');
20094
+ var pathEl = document.getElementById('vault-file-drawer-path');
20095
+ var body = document.getElementById('vault-file-drawer-body');
20096
+ if (titleEl) titleEl.textContent = relPath.split('/').pop().replace(/\\.md$/, '');
20097
+ if (pathEl) pathEl.textContent = relPath;
20098
+ if (body) body.innerHTML = '<div class="skel-block"><div class="skel-row"></div><div class="skel-row med"></div><div class="skel-row short"></div></div>';
20099
+ drawer.style.transform = 'translateX(0)';
20100
+ try {
20101
+ var r = await apiFetch('/api/vault-file?path=' + encodeURIComponent(relPath));
20102
+ var d = await r.json();
20103
+ if (d.error) {
20104
+ body.innerHTML = '<div style="color:var(--red)">' + esc(d.error) + '</div>';
20105
+ return;
20106
+ }
20107
+ body.innerHTML = renderMd(d.content);
20108
+ } catch (err) {
20109
+ body.innerHTML = '<div style="color:var(--red)">Failed: ' + esc(String(err)) + '</div>';
20110
+ }
20111
+ }
20112
+
20113
+ function closeVaultFileDrawer() {
20114
+ var drawer = document.getElementById('vault-file-drawer');
20115
+ if (drawer) drawer.style.transform = 'translateX(100%)';
20116
+ }
20117
+
20118
+ function setVaultFolderFilter(folder) {
20119
+ _vaultFilesFolder = folder || '';
20120
+ refreshVaultFiles();
20121
+ }
20122
+
19784
20123
  // ── Goals: inline create form ────────────────────────────────────
19785
20124
  function openNewGoalForm() {
19786
20125
  var el = document.getElementById('new-goal-form');
@@ -20561,44 +20900,53 @@ function toggleHomeRail() {
20561
20900
  }
20562
20901
  }
20563
20902
 
20903
+ function _railCard(bodyId) {
20904
+ var body = document.getElementById(bodyId);
20905
+ return body ? body.closest('.rail-card') : null;
20906
+ }
20907
+ function _setRailEmpty(bodyId, isEmpty) {
20908
+ var card = _railCard(bodyId);
20909
+ if (card) card.classList.toggle('empty', !!isEmpty);
20910
+ }
20911
+
20564
20912
  async function refreshHomeRail() {
20565
- // Daemon status
20913
+ // Daemon status — only surface when explicitly stopped. Treat null/undefined
20914
+ // (running-state unknown) as "fine, hide" since the dashboard wouldn't be
20915
+ // serving requests if the daemon were truly down.
20566
20916
  try {
20567
20917
  var rs = await apiFetch('/api/status');
20568
20918
  var ds = await rs.json();
20919
+ var stopped = ds.running === false;
20569
20920
  var pip = document.querySelector('#rail-daemon-body .agent-activity-dot');
20570
20921
  var label = document.querySelector('#rail-daemon-body .agent-activity span:last-child');
20571
- if (label) label.textContent = ds.running ? 'Daemon running' : 'Daemon stopped';
20572
- if (pip) pip.style.background = ds.running ? '#22c55e' : '#ef4444';
20922
+ if (label) label.textContent = stopped ? 'Daemon stopped' : 'Running';
20923
+ if (pip) pip.style.background = stopped ? '#ef4444' : '#22c55e';
20573
20924
  var up = document.getElementById('rail-daemon-uptime');
20574
20925
  if (up && ds.uptimeMs) up.textContent = Math.round(ds.uptimeMs / 60000) + 'm';
20575
- } catch { /* */ }
20926
+ _setRailEmpty('rail-daemon-body', !stopped);
20927
+ } catch { _setRailEmpty('rail-daemon-body', true); }
20576
20928
 
20577
- // Today's plan (compact)
20929
+ // Today's plan (compact). Hide card if no plan or zero items.
20578
20930
  try {
20579
20931
  var rp = await apiFetch('/api/daily-plan');
20580
20932
  var dp = await rp.json();
20581
20933
  var planEl = document.getElementById('home-plan-content');
20934
+ var items = dp && dp.plan ? (dp.plan.items || []) : [];
20582
20935
  if (planEl) {
20583
- if (!dp || !dp.plan) {
20584
- planEl.innerHTML = '<div style="font-size:12px;color:var(--text-muted)">No plan yet today.</div>';
20936
+ if (items.length === 0) {
20937
+ planEl.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No plan yet today.</div>';
20585
20938
  } else {
20586
- var items = (dp.plan.items || []).slice(0, 4);
20587
- if (items.length === 0) {
20588
- planEl.innerHTML = '<div style="font-size:12px;color:var(--text-muted)">No items in today\\x27s plan.</div>';
20589
- } else {
20590
- planEl.innerHTML = items.map(function(it) {
20591
- return '<div class="rail-row"><span class="label">' + esc(it.title || it.text || '') + '</span><span class="meta">' + esc(it.time || '') + '</span></div>';
20592
- }).join('');
20593
- }
20939
+ planEl.innerHTML = items.slice(0, 4).map(function(it) {
20940
+ return '<div class="rail-row"><span class="label">' + esc(it.title || it.text || '') + '</span><span class="meta">' + esc(it.time || '') + '</span></div>';
20941
+ }).join('');
20594
20942
  }
20595
20943
  }
20944
+ _setRailEmpty('home-plan-content', items.length === 0);
20596
20945
  } catch {
20597
- var pe = document.getElementById('home-plan-content');
20598
- if (pe) pe.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No plan available.</div>';
20946
+ _setRailEmpty('home-plan-content', true);
20599
20947
  }
20600
20948
 
20601
- // Upcoming cron fires (next 3)
20949
+ // Upcoming cron fires (next 3) — hide card if nothing scheduled
20602
20950
  try {
20603
20951
  var rc = await apiFetch('/api/cron');
20604
20952
  var dc = await rc.json();
@@ -20609,13 +20957,14 @@ async function refreshHomeRail() {
20609
20957
  var uc = document.getElementById('rail-upcoming-count');
20610
20958
  if (uc) uc.textContent = String(jobs.length);
20611
20959
  if (ue) {
20612
- ue.innerHTML = top.length ? top.map(function(j) {
20960
+ ue.innerHTML = top.map(function(j) {
20613
20961
  return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27build\\x27,{tab:\\x27crons\\x27})"><span class="label">' + esc(j.name) + '</span><span class="meta">' + esc(timeUntil(j.nextRun)) + '</span></div>';
20614
- }).join('') : '<div style="font-size:11px;color:var(--text-muted)">Nothing scheduled soon.</div>';
20962
+ }).join('');
20615
20963
  }
20616
- } catch { /* */ }
20964
+ _setRailEmpty('rail-upcoming', top.length === 0);
20965
+ } catch { _setRailEmpty('rail-upcoming', true); }
20617
20966
 
20618
- // Active unleashed runs
20967
+ // Active unleashed runs — hide card unless something running
20619
20968
  try {
20620
20969
  var ru = await apiFetch('/api/unleashed');
20621
20970
  var du = await ru.json();
@@ -20627,25 +20976,27 @@ async function refreshHomeRail() {
20627
20976
  else ac.style.display = 'none';
20628
20977
  }
20629
20978
  if (ae) {
20630
- ae.innerHTML = active.length ? active.map(function(t) {
20979
+ ae.innerHTML = active.map(function(t) {
20631
20980
  return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27build\\x27,{tab:\\x27workflows\\x27})"><span class="label">' + esc(t.name) + '</span><span class="meta">' + esc(t.phase || '') + '</span></div>';
20632
- }).join('') : '<div style="font-size:11px;color:var(--text-muted)">Nothing running.</div>';
20981
+ }).join('');
20633
20982
  }
20634
- } catch { /* */ }
20983
+ _setRailEmpty('rail-active', active.length === 0);
20984
+ } catch { _setRailEmpty('rail-active', true); }
20635
20985
 
20636
- // Time saved (rough: cron runs * 5min + activity exchanges * 2min, this week)
20986
+ // Time saved (compact). Hide if zero.
20637
20987
  try {
20638
20988
  var rm = await apiFetch('/api/metrics?period=week');
20639
20989
  var dm = await rm.json();
20640
20990
  var minutes = ((dm.cronRuns || 0) * 5) + ((dm.exchanges || 0) * 2);
20641
20991
  var ts = document.getElementById('rail-time-saved');
20642
20992
  if (ts) {
20643
- if (minutes >= 60) ts.innerHTML = '<div style="font-size:18px;font-weight:600">' + (minutes / 60).toFixed(1) + 'h</div><div style="font-size:11px;color:var(--text-muted)">across ' + (dm.cronRuns || 0) + ' cron runs + ' + (dm.exchanges || 0) + ' chats</div>';
20644
- else ts.innerHTML = '<div style="font-size:18px;font-weight:600">' + minutes + 'm</div><div style="font-size:11px;color:var(--text-muted)">across ' + (dm.cronRuns || 0) + ' cron runs</div>';
20993
+ if (minutes >= 60) ts.innerHTML = '<div style="font-size:var(--text-md);font-weight:600">' + (minutes / 60).toFixed(1) + 'h</div><div style="font-size:11px;color:var(--text-muted)">' + (dm.cronRuns || 0) + ' runs · ' + (dm.exchanges || 0) + ' chats</div>';
20994
+ else ts.innerHTML = '<div style="font-size:var(--text-md);font-weight:600">' + minutes + 'm</div><div style="font-size:11px;color:var(--text-muted)">' + (dm.cronRuns || 0) + ' runs</div>';
20645
20995
  }
20646
- } catch { /* */ }
20996
+ _setRailEmpty('rail-time-saved', minutes === 0);
20997
+ } catch { _setRailEmpty('rail-time-saved', true); }
20647
20998
 
20648
- // Approvals (self-improve proposals + pending skills)
20999
+ // Approvals hide card unless something pending
20649
21000
  try {
20650
21001
  var rsi = await apiFetch('/api/self-improve');
20651
21002
  var dsi = await rsi.json();
@@ -20657,12 +21008,12 @@ async function refreshHomeRail() {
20657
21008
  else ac2.style.display = 'none';
20658
21009
  }
20659
21010
  if (ae2) {
20660
- if (pending.length === 0) ae2.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">Nothing pending.</div>';
20661
- else ae2.innerHTML = pending.slice(0, 3).map(function(p) {
21011
+ ae2.innerHTML = pending.slice(0, 3).map(function(p) {
20662
21012
  return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27brain\\x27,{tab:\\x27learning\\x27})"><span class="label">' + esc(p.area || 'proposal') + ': ' + esc((p.target || '').slice(0, 40)) + '</span><span class="meta">' + esc(((p.score || 0) * 100).toFixed(0)) + '%</span></div>';
20663
21013
  }).join('');
20664
21014
  }
20665
- } catch { /* */ }
21015
+ _setRailEmpty('rail-approvals', pending.length === 0);
21016
+ } catch { _setRailEmpty('rail-approvals', true); }
20666
21017
  }
20667
21018
 
20668
21019
  function timeUntil(iso) {
@@ -23992,6 +24343,8 @@ async function refreshSalesforce() {
23992
24343
  if (d.status) { try { refreshStatus(d.status); } catch(e) { console.warn('init: status', e); } }
23993
24344
  if (d.activity) { try { refreshActivity(false, d.activity); } catch(e) { console.warn('init: activity', e); } }
23994
24345
  else { try { refreshActivity(); } catch(e) { console.warn('init: activity fallback', e); } }
24346
+ // Populate the home right rail (daemon, plan, runs, time-saved, approvals)
24347
+ if (typeof refreshHomeRail === 'function') { try { refreshHomeRail(); } catch(e) { console.warn('init: rail', e); } }
23995
24348
  if (d.office) { try { refreshTeamNav(d.office); refreshTeamPulse(d.office); } catch(e) { console.warn('init: office', e); } }
23996
24349
  if (d.plan) { try { refreshHomePlan(d.plan); } catch(e) { console.warn('init: plan', e); } }
23997
24350
  if (d.version) { try { _loadedHash = d.version.started; } catch(e) { /* ignore */ } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",