clementine-agent 1.4.2 → 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.
- package/dist/cli/dashboard.js +304 -0
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -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
|
});
|
|
@@ -10610,6 +10744,26 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
10610
10744
|
/* Hide chat profile selector when default — the row gets cleaner */
|
|
10611
10745
|
.home-chat-input-row .chat-profile-spacer { display: none; }
|
|
10612
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
|
+
}
|
|
10766
|
+
|
|
10613
10767
|
/* ── Task Cards ─────────────────────────── */
|
|
10614
10768
|
.task-grid {
|
|
10615
10769
|
display: grid;
|
|
@@ -11765,6 +11919,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
11765
11919
|
<div class="tab-bar" id="intelligence-tabs" style="margin:0 0 0 18px">
|
|
11766
11920
|
<button class="active" data-icon="database" onclick="switchTab('intelligence','search')"><span class="icon-slot"></span> Memory</button>
|
|
11767
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>
|
|
11768
11923
|
<button data-icon="folder" onclick="switchTab('intelligence','sources')"><span class="icon-slot"></span> Ingestion</button>
|
|
11769
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>
|
|
11770
11925
|
<button data-icon="users" onclick="switchTab('intelligence','user-model')"><span class="icon-slot"></span> User Model</button>
|
|
@@ -11973,6 +12128,26 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
11973
12128
|
<div class="tab-pane" id="tab-intelligence-runs">
|
|
11974
12129
|
<div id="brain-runs-list"></div>
|
|
11975
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>
|
|
11976
12151
|
<div class="tab-pane" id="tab-intelligence-health">
|
|
11977
12152
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap">
|
|
11978
12153
|
<button class="btn-sm" onclick="memoryHealthAction('janitor')" title="Run the janitor cleanup pass now">Run cleanup</button>
|
|
@@ -14565,6 +14740,7 @@ function switchTab(group, tab) {
|
|
|
14565
14740
|
if (group === 'intelligence') {
|
|
14566
14741
|
if (tab === 'graph') refreshGraph();
|
|
14567
14742
|
if (tab === 'memory') refreshMemory();
|
|
14743
|
+
if (tab === 'files' && typeof refreshVaultFiles === 'function') refreshVaultFiles();
|
|
14568
14744
|
if (tab === 'health') {
|
|
14569
14745
|
if (typeof refreshMemoryHealth === 'function') refreshMemoryHealth();
|
|
14570
14746
|
if (typeof refreshClaims === 'function') refreshClaims();
|
|
@@ -19816,6 +19992,134 @@ async function memoryHealthAction(action) {
|
|
|
19816
19992
|
}
|
|
19817
19993
|
}
|
|
19818
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
|
+
|
|
19819
20123
|
// ── Goals: inline create form ────────────────────────────────────
|
|
19820
20124
|
function openNewGoalForm() {
|
|
19821
20125
|
var el = document.getElementById('new-goal-form');
|