claude-memory-admin 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -6
- package/package.json +1 -1
- package/public/app.mjs +387 -90
- package/public/styles.css +37 -0
- package/server.mjs +61 -20
- package/src/instructions.mjs +423 -0
- package/src/model.mjs +44 -11
- package/src/mutate.mjs +17 -23
- package/src/pathcache.mjs +93 -0
- package/src/projects.mjs +120 -34
- package/src/search.mjs +13 -11
- package/src/settings.mjs +151 -0
- package/src/stats.mjs +105 -12
- package/src/stores.mjs +146 -0
package/public/app.mjs
CHANGED
|
@@ -5,9 +5,9 @@ import { renderGraph } from '/graph.mjs';
|
|
|
5
5
|
marked.setOptions({ gfm: true, breaks: false });
|
|
6
6
|
|
|
7
7
|
const state = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
stores: [],
|
|
9
|
+
storeId: null,
|
|
10
|
+
store: null,
|
|
11
11
|
tab: 'memories',
|
|
12
12
|
selected: null,
|
|
13
13
|
showAll: false,
|
|
@@ -114,33 +114,59 @@ function renderBody(container, memory, project) {
|
|
|
114
114
|
|
|
115
115
|
/* --------------------------------------------------------------- sidebar */
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
/** The second line under a store's name: where it lives, in its own terms. */
|
|
118
|
+
function storeSubtitle(store) {
|
|
119
|
+
if (store.kind === 'auto') return store.path;
|
|
120
|
+
const scope = store.kind === 'agent-user' ? 'user'
|
|
121
|
+
: store.kind === 'agent-project' ? 'project' : 'local';
|
|
122
|
+
return `${scope} · ${store.sublabel}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function storeButton(store) {
|
|
126
|
+
const health = store.memoryCount === 0 ? 'none' : 'ok';
|
|
127
|
+
const off = store.autoMemory && store.autoMemory.known && !store.autoMemory.enabled;
|
|
128
|
+
return node('button', {
|
|
129
|
+
class: `project${store.id === state.storeId ? ' active' : ''}${store.hasMemoryDir ? '' : ' dim'}`,
|
|
130
|
+
onclick: () => openStore(store.id),
|
|
131
|
+
title: store.kind === 'auto' && store.resolvedBy === 'unresolved'
|
|
132
|
+
? 'Real path could not be resolved - showing the raw folder name'
|
|
133
|
+
: store.dir,
|
|
134
|
+
}, [
|
|
135
|
+
node('span', { class: 'project-row' }, [
|
|
136
|
+
node('span', { class: `dot ${health}` }),
|
|
137
|
+
node('span', { class: 'project-name', text: store.label }),
|
|
138
|
+
off ? node('span', { class: 'off-marker', text: 'off', title: 'Auto memory is disabled for this project' }) : null,
|
|
139
|
+
node('span', { class: 'project-count', text: store.hasMemoryDir ? String(store.memoryCount) : '-' }),
|
|
140
|
+
]),
|
|
141
|
+
node('span', { class: 'project-path', text: storeSubtitle(store) }),
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderStores() {
|
|
118
146
|
const list = el('project-list');
|
|
119
147
|
list.textContent = '';
|
|
120
148
|
|
|
121
|
-
const visible = state.
|
|
149
|
+
const visible = state.stores.filter((s) => state.showAll || s.hasMemoryDir);
|
|
122
150
|
if (!visible.length) {
|
|
123
|
-
list.append(node('p', { class: 'muted', text: 'No
|
|
151
|
+
list.append(node('p', { class: 'muted', text: 'No memory stores found.', style: 'padding:12px' }));
|
|
124
152
|
return;
|
|
125
153
|
}
|
|
126
154
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
]);
|
|
143
|
-
list.append(button);
|
|
155
|
+
// Subagent memory is a different thing from a project's auto memory - a
|
|
156
|
+
// different scope, written by a different agent - so it gets its own group
|
|
157
|
+
// rather than being mixed into the project list.
|
|
158
|
+
const groups = [
|
|
159
|
+
['Projects', visible.filter((s) => s.kind === 'auto')],
|
|
160
|
+
['Subagents', visible.filter((s) => s.kind !== 'auto')],
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
for (const [title, stores] of groups) {
|
|
164
|
+
if (!stores.length) continue;
|
|
165
|
+
// With nothing to tell apart, a single heading is just noise.
|
|
166
|
+
if (groups.every(([, group]) => group.length) ) {
|
|
167
|
+
list.append(node('div', { class: 'sidebar-group', text: title }));
|
|
168
|
+
}
|
|
169
|
+
for (const store of stores) list.append(storeButton(store));
|
|
144
170
|
}
|
|
145
171
|
}
|
|
146
172
|
|
|
@@ -151,6 +177,7 @@ const TABS = [
|
|
|
151
177
|
{ id: 'index', label: 'MEMORY.md' },
|
|
152
178
|
{ id: 'graph', label: 'Graph' },
|
|
153
179
|
{ id: 'prune', label: 'Prune' },
|
|
180
|
+
{ id: 'context', label: 'Context' },
|
|
154
181
|
{ id: 'health', label: 'Health' },
|
|
155
182
|
{ id: 'trash', label: 'Trash' },
|
|
156
183
|
];
|
|
@@ -158,12 +185,17 @@ const TABS = [
|
|
|
158
185
|
function renderTabs() {
|
|
159
186
|
const container = el('tabs');
|
|
160
187
|
container.textContent = '';
|
|
188
|
+
const hasProjectDir = state.store.kind === 'auto'
|
|
189
|
+
? state.store.resolvedBy !== 'unresolved'
|
|
190
|
+
: Boolean(state.store.projectPath);
|
|
191
|
+
|
|
161
192
|
for (const tab of TABS) {
|
|
193
|
+
if (tab.id === 'context' && !hasProjectDir) continue;
|
|
162
194
|
let badge = null;
|
|
163
|
-
if (tab.id === 'memories') badge = String(state.
|
|
164
|
-
if (tab.id === 'health' && state.
|
|
165
|
-
if (tab.id === 'trash' && state.
|
|
166
|
-
if (tab.id === 'prune' && state.
|
|
195
|
+
if (tab.id === 'memories') badge = String(state.store.memories.length);
|
|
196
|
+
if (tab.id === 'health' && state.store.health.issueCount) badge = String(state.store.health.issueCount);
|
|
197
|
+
if (tab.id === 'trash' && state.store.trash.length) badge = String(state.store.trash.length);
|
|
198
|
+
if (tab.id === 'prune' && state.store.stats.index.overTarget) badge = '!';
|
|
167
199
|
|
|
168
200
|
container.append(node('button', {
|
|
169
201
|
class: `tab${state.tab === tab.id ? ' active' : ''}`,
|
|
@@ -194,7 +226,7 @@ function memoryButton(memory) {
|
|
|
194
226
|
}
|
|
195
227
|
|
|
196
228
|
function renderMemories(container) {
|
|
197
|
-
const project = state.
|
|
229
|
+
const project = state.store;
|
|
198
230
|
if (!project.memories.length) {
|
|
199
231
|
container.append(node('p', { class: 'muted', text: 'This project has no memory files yet.' }));
|
|
200
232
|
return;
|
|
@@ -224,7 +256,7 @@ function renderDetail() {
|
|
|
224
256
|
if (!host) return;
|
|
225
257
|
host.textContent = '';
|
|
226
258
|
|
|
227
|
-
const memory = state.
|
|
259
|
+
const memory = state.store.memories.find((m) => m.file === state.selected);
|
|
228
260
|
if (!memory) {
|
|
229
261
|
host.append(node('p', { class: 'muted', text: 'Select a memory to read it.' }));
|
|
230
262
|
return;
|
|
@@ -247,7 +279,11 @@ function renderDetail() {
|
|
|
247
279
|
const rows = [
|
|
248
280
|
['file', memory.file],
|
|
249
281
|
['status', memory.status],
|
|
250
|
-
['modified', memory.modified
|
|
282
|
+
['modified', memory.modified
|
|
283
|
+
// mtime is reset by any copy or restore, so a date that is only a file
|
|
284
|
+
// timestamp is weaker evidence than one Claude Code actually stamped.
|
|
285
|
+
? `${memory.modified.replace('T', ' ').replace(/\..*$/, '')}${memory.modifiedFrom === 'mtime' ? ' (file mtime)' : ''}`
|
|
286
|
+
: '-'],
|
|
251
287
|
['size', `${(memory.bytes / 1024).toFixed(1)} KB`],
|
|
252
288
|
];
|
|
253
289
|
if (!memory.nameMatchesFile) rows.push(['name', `${memory.name} (differs from filename)`]);
|
|
@@ -261,7 +297,7 @@ function renderDetail() {
|
|
|
261
297
|
|
|
262
298
|
const body = node('div', { class: 'body' });
|
|
263
299
|
card.append(body);
|
|
264
|
-
renderBody(body, memory, state.
|
|
300
|
+
renderBody(body, memory, state.store);
|
|
265
301
|
|
|
266
302
|
const outbound = memory.outboundResolved || [];
|
|
267
303
|
if (outbound.length || memory.inbound.length) {
|
|
@@ -282,7 +318,7 @@ function renderDetail() {
|
|
|
282
318
|
links.append(node('div', { class: 'section-label', text: 'Linked from' }));
|
|
283
319
|
const chips = node('div', { class: 'links' });
|
|
284
320
|
for (const link of memory.inbound) {
|
|
285
|
-
const source = state.
|
|
321
|
+
const source = state.store.memories.find((m) => m.file === link.from);
|
|
286
322
|
chips.append(node('button', {
|
|
287
323
|
class: 'chip',
|
|
288
324
|
text: source ? source.name : link.from,
|
|
@@ -300,19 +336,27 @@ function renderDetail() {
|
|
|
300
336
|
/* ----------------------------------------------------------- MEMORY.md */
|
|
301
337
|
|
|
302
338
|
function renderIndex(container) {
|
|
303
|
-
const project = state.
|
|
339
|
+
const project = state.store;
|
|
304
340
|
if (!project.hasIndex) {
|
|
305
341
|
container.append(node('p', { class: 'muted', text: 'This project has no MEMORY.md.' }));
|
|
306
342
|
return;
|
|
307
343
|
}
|
|
308
344
|
|
|
309
345
|
const missing = new Set(project.health.danglingIndex.map((d) => d.index));
|
|
346
|
+
const { cutoff } = project.stats.index;
|
|
310
347
|
const card = node('div', { class: 'card' });
|
|
311
348
|
card.append(node('p', { class: 'muted', style: 'margin-top:0', text: `${project.index.entries.length} index entries · ${project.index.lines.length} lines` }));
|
|
312
349
|
|
|
313
350
|
for (const line of project.index.lines) {
|
|
351
|
+
// The cutoff is a raw line number, so it lands in the right place even
|
|
352
|
+
// though frontmatter and comments are stripped before the limit is measured.
|
|
353
|
+
if (cutoff && line.index === cutoff.rawLine) {
|
|
354
|
+
card.append(node('div', { class: 'cut-line' }, [
|
|
355
|
+
node('span', { class: 'cut-label', text: `not loaded past here — ${cutoff.droppedLines} lines dropped on ${cutoff.by}` }),
|
|
356
|
+
]));
|
|
357
|
+
}
|
|
314
358
|
const row = node('div', {
|
|
315
|
-
class: `index-line kind-${line.kind}${missing.has(line.index) ? ' dangling' : ''}`,
|
|
359
|
+
class: `index-line kind-${line.kind}${missing.has(line.index) ? ' dangling' : ''}${cutoff && line.index >= cutoff.rawLine ? ' beyond-cut' : ''}`,
|
|
316
360
|
}, [
|
|
317
361
|
node('span', { class: 'ln', text: String(line.index + 1) }),
|
|
318
362
|
node('span', { class: 'content', text: line.text || ' ' }),
|
|
@@ -325,6 +369,100 @@ function renderIndex(container) {
|
|
|
325
369
|
container.append(card);
|
|
326
370
|
}
|
|
327
371
|
|
|
372
|
+
|
|
373
|
+
/* ------------------------------------------------------------- context */
|
|
374
|
+
|
|
375
|
+
const SCOPE_ORDER = ['managed', 'user', 'project', 'local'];
|
|
376
|
+
|
|
377
|
+
const CONTEXT_PROBLEMS = {
|
|
378
|
+
missing: (p) => ['Import does not resolve', `${p.spec} from ${p.from} - nothing at ${p.file}`],
|
|
379
|
+
'too-deep': (p) => ['Import chain is too deep', `${p.file} is a fifth hop; Claude Code follows four, so this never loads`],
|
|
380
|
+
cycle: (p) => ['Circular import', `${p.from} imports ${p.file}, which was already loaded`],
|
|
381
|
+
external: (p) => ['Import resolves outside the project', `${p.file} - Claude Code asks you to approve these once, and a declined one stays disabled silently`],
|
|
382
|
+
'invalid-glob': (p) => [`Rule matches nothing: ${p.pattern}`, `${p.file} - ${p.reason}`],
|
|
383
|
+
'glob-budget': (p) => ['Rule has too many brace expansions', `${p.file} expands to ${p.expansions} patterns, past the 1,000 budget, so it is used unexpanded and its literal braces match no file`],
|
|
384
|
+
'long-claude-md': (p) => [`${p.lines} lines, over the 200-line guidance`, `${p.file} - long files cost context every session and reduce adherence`],
|
|
385
|
+
'agents-md-not-imported': (p) => ['AGENTS.md is not loaded', `${p.file} exists, but Claude Code reads CLAUDE.md. Import it with @AGENTS.md, or symlink it.`],
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
async function renderContext(container) {
|
|
389
|
+
container.append(node('p', { class: 'muted', text: 'Reading instruction files…' }));
|
|
390
|
+
let data;
|
|
391
|
+
try {
|
|
392
|
+
data = await api(`/api/stores/${encodeURIComponent(state.storeId)}/instructions`);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
container.textContent = '';
|
|
395
|
+
return container.append(node('p', { class: 'muted', text: err.message }));
|
|
396
|
+
}
|
|
397
|
+
// The tab may have been switched while the request was in flight.
|
|
398
|
+
if (state.tab !== 'context') return;
|
|
399
|
+
container.textContent = '';
|
|
400
|
+
|
|
401
|
+
if (!data.projectDir) {
|
|
402
|
+
return container.append(node('p', { class: 'muted', text: 'This store is not tied to a project directory, so there are no instruction files to resolve.' }));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const { totals } = data;
|
|
406
|
+
container.append(node('div', { class: 'meter' }, [
|
|
407
|
+
node('div', { class: 'meter-top' }, [
|
|
408
|
+
node('span', { class: 'meter-value', text: `~${totals.alwaysTokens.toLocaleString()}` }),
|
|
409
|
+
node('span', { class: 'meter-unit', text: 'estimated tokens of instructions, every session' }),
|
|
410
|
+
]),
|
|
411
|
+
node('div', { class: 'meter-facts' }, [
|
|
412
|
+
node('span', {}, [node('b', { text: String(totals.files) }), document.createTextNode(' files resolved')]),
|
|
413
|
+
node('span', {}, [node('b', { text: String(totals.alwaysLines) }), document.createTextNode(' lines always loaded')]),
|
|
414
|
+
node('span', {}, [node('b', { text: `${(totals.alwaysBytes / 1024).toFixed(1)} KB` }), document.createTextNode(' always loaded')]),
|
|
415
|
+
node('span', {}, [node('b', { text: String(totals.conditionalFiles) }), document.createTextNode(' path-scoped rules, loaded only on a match')]),
|
|
416
|
+
]),
|
|
417
|
+
node('p', { class: 'meter-note', text: 'Unlike MEMORY.md, none of this is truncated: CLAUDE.md files load in full however long they are. The 200-line figure is Claude Code’s guidance, not a cutoff, because long files cost context every session and are followed less reliably.' }),
|
|
418
|
+
]));
|
|
419
|
+
|
|
420
|
+
if (data.problems.length) {
|
|
421
|
+
container.append(node('div', { class: 'section-label', text: `Problems · ${data.problems.length}` }));
|
|
422
|
+
for (const problem of data.problems) {
|
|
423
|
+
const describe = CONTEXT_PROBLEMS[problem.kind];
|
|
424
|
+
const [title, detail] = describe ? describe(problem) : [problem.kind, problem.file || ''];
|
|
425
|
+
const bad = problem.kind === 'missing' || problem.kind === 'invalid-glob' || problem.kind === 'glob-budget';
|
|
426
|
+
container.append(node('div', { class: `issue${bad ? ' bad' : ''}` }, [
|
|
427
|
+
node('div', { class: 'issue-body' }, [
|
|
428
|
+
node('div', { class: 'issue-title', text: title }),
|
|
429
|
+
node('div', { class: 'issue-detail', text: detail }),
|
|
430
|
+
]),
|
|
431
|
+
]));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
container.append(node('div', { class: 'section-label', text: `Loaded in this order · ${data.files.length}` }));
|
|
436
|
+
const card = node('div', { class: 'card' });
|
|
437
|
+
for (const file of data.files) {
|
|
438
|
+
const tags = [node('span', { class: `badge scope-${file.scope}`, text: file.scope })];
|
|
439
|
+
if (file.kind === 'import') tags.push(node('span', { class: 'badge', text: `import · depth ${file.depth}` }));
|
|
440
|
+
if (file.kind === 'rule') tags.push(node('span', { class: 'badge', text: 'rule' }));
|
|
441
|
+
if (file.kind === 'managed-settings') tags.push(node('span', { class: 'badge', text: 'claudeMd setting' }));
|
|
442
|
+
if (file.conditional) tags.push(node('span', { class: 'badge status-referenced', text: 'only on a path match' }));
|
|
443
|
+
|
|
444
|
+
card.append(node('div', { class: 'ctx-row' }, [
|
|
445
|
+
node('div', { class: 'ctx-main' }, [
|
|
446
|
+
node('div', { class: 'ctx-tags' }, tags),
|
|
447
|
+
node('div', { class: 'ctx-file', text: file.file }),
|
|
448
|
+
]),
|
|
449
|
+
node('div', { class: 'ctx-size', text: `${file.lines} L · ~${file.tokens.toLocaleString()} tok` }),
|
|
450
|
+
]));
|
|
451
|
+
}
|
|
452
|
+
container.append(card);
|
|
453
|
+
|
|
454
|
+
if (data.excluded.length) {
|
|
455
|
+
container.append(node('div', { class: 'section-label', text: `Excluded by claudeMdExcludes · ${data.excluded.length}` }));
|
|
456
|
+
for (const file of data.excluded) {
|
|
457
|
+
container.append(node('div', { class: 'issue' }, [
|
|
458
|
+
node('div', { class: 'issue-body' }, [node('div', { class: 'issue-detail', text: file })]),
|
|
459
|
+
]));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
container.append(node('p', { class: 'meter-note', text: 'This is re-derived from the documented resolution rules, not a report from Claude Code. Run /context in a session to see what it actually loaded.' }));
|
|
464
|
+
}
|
|
465
|
+
|
|
328
466
|
/* -------------------------------------------------------------- health */
|
|
329
467
|
|
|
330
468
|
function issue(title, detail, { bad = false, action, secondary } = {}) {
|
|
@@ -339,7 +477,7 @@ function issue(title, detail, { bad = false, action, secondary } = {}) {
|
|
|
339
477
|
}
|
|
340
478
|
|
|
341
479
|
function renderHealth(container) {
|
|
342
|
-
const { health, memories } = state.
|
|
480
|
+
const { health, memories } = state.store;
|
|
343
481
|
if (!health.issues.length) {
|
|
344
482
|
container.append(node('div', { class: 'card' }, [
|
|
345
483
|
node('p', { style: 'margin:0', text: '\u2713 No consistency problems found.' }),
|
|
@@ -369,12 +507,12 @@ function renderIssue(item, memories) {
|
|
|
369
507
|
run: async () => {
|
|
370
508
|
if (!confirm(`Remove line ${entry.index + 1} from MEMORY.md?\n\n${entry.text}`)) return;
|
|
371
509
|
try {
|
|
372
|
-
await api(`/api/
|
|
510
|
+
await api(`/api/stores/${encodeURIComponent(state.storeId)}/index-line/delete`, {
|
|
373
511
|
method: 'POST',
|
|
374
512
|
body: JSON.stringify({ lineIndex: entry.index, expectedText: entry.text }),
|
|
375
513
|
});
|
|
376
514
|
toast('Pointer removed');
|
|
377
|
-
await
|
|
515
|
+
await openStore(state.storeId, { keepTab: true });
|
|
378
516
|
} catch (err) {
|
|
379
517
|
toast(err.message, { error: true });
|
|
380
518
|
}
|
|
@@ -397,11 +535,11 @@ function renderIssue(item, memories) {
|
|
|
397
535
|
run: async () => {
|
|
398
536
|
if (!confirm(`Remove [[${link.target}]] from ${link.from}?\n\nThe link markup goes, the words stay:\n "see [[${link.target}]]" -> "see ${link.target}"\n\nA copy of the file is kept in the trash, so this can be undone.`)) return;
|
|
399
537
|
try {
|
|
400
|
-
const result = await api(`/api/
|
|
538
|
+
const result = await api(`/api/stores/${encodeURIComponent(state.storeId)}/wikilink/remove`, {
|
|
401
539
|
method: 'POST',
|
|
402
540
|
body: JSON.stringify({ file: link.from, target: link.target }),
|
|
403
541
|
});
|
|
404
|
-
await
|
|
542
|
+
await openStore(state.storeId, { keepTab: true });
|
|
405
543
|
toast(`Unlinked ${result.occurrences} reference(s)`, {
|
|
406
544
|
action: { label: 'Undo', run: () => restoreFromTrash(result.record.id) },
|
|
407
545
|
});
|
|
@@ -467,11 +605,11 @@ function renderIssue(item, memories) {
|
|
|
467
605
|
|
|
468
606
|
async function restoreFromTrash(id) {
|
|
469
607
|
try {
|
|
470
|
-
const result = await api(`/api/
|
|
608
|
+
const result = await api(`/api/stores/${encodeURIComponent(state.storeId)}/restore`, {
|
|
471
609
|
method: 'POST',
|
|
472
610
|
body: JSON.stringify({ id }),
|
|
473
611
|
});
|
|
474
|
-
await
|
|
612
|
+
await openStore(state.storeId, { keepTab: true });
|
|
475
613
|
toast(result.indexRestored === 'appended'
|
|
476
614
|
? 'Restored, but MEMORY.md had changed - the index line was appended at the end'
|
|
477
615
|
: 'Restored');
|
|
@@ -483,7 +621,7 @@ async function restoreFromTrash(id) {
|
|
|
483
621
|
/* --------------------------------------------------------------- trash */
|
|
484
622
|
|
|
485
623
|
function renderTrash(container) {
|
|
486
|
-
const trash = state.
|
|
624
|
+
const trash = state.store.trash;
|
|
487
625
|
if (!trash.length) {
|
|
488
626
|
container.append(node('p', { class: 'muted', text: 'Nothing in the trash. Deleted memories land here and can be restored.' }));
|
|
489
627
|
return;
|
|
@@ -514,7 +652,7 @@ function closeModal() {
|
|
|
514
652
|
async function openDeleteDialog(file) {
|
|
515
653
|
let preview;
|
|
516
654
|
try {
|
|
517
|
-
preview = await api(`/api/
|
|
655
|
+
preview = await api(`/api/stores/${encodeURIComponent(state.storeId)}/delete-preview`, {
|
|
518
656
|
method: 'POST',
|
|
519
657
|
body: JSON.stringify({ file }),
|
|
520
658
|
});
|
|
@@ -606,12 +744,12 @@ async function openDeleteDialog(file) {
|
|
|
606
744
|
onclick: async () => {
|
|
607
745
|
closeModal();
|
|
608
746
|
try {
|
|
609
|
-
const result = await api(`/api/
|
|
747
|
+
const result = await api(`/api/stores/${encodeURIComponent(state.storeId)}/delete`, {
|
|
610
748
|
method: 'POST',
|
|
611
749
|
body: JSON.stringify({ file, alsoDelete: [...cascade] }),
|
|
612
750
|
});
|
|
613
751
|
if (state.selected === file || cascade.has(state.selected)) state.selected = null;
|
|
614
|
-
await
|
|
752
|
+
await openStore(state.storeId, { keepTab: true });
|
|
615
753
|
const count = result.record.files.length;
|
|
616
754
|
toast(count > 1 ? `Deleted ${count} memories` : `Deleted "${result.record.label || file}"`, {
|
|
617
755
|
action: { label: 'Undo', run: () => restoreFromTrash(result.record.id) },
|
|
@@ -634,16 +772,85 @@ async function openDeleteDialog(file) {
|
|
|
634
772
|
}
|
|
635
773
|
}
|
|
636
774
|
|
|
637
|
-
|
|
775
|
+
/**
|
|
776
|
+
* Name a store whose transcripts have aged out.
|
|
777
|
+
*
|
|
778
|
+
* Claude Code sweeps session transcripts on a retention period but keeps memory
|
|
779
|
+
* forever, so the evidence that names a store expires while the store does not.
|
|
780
|
+
* This is the only thing in the app that writes outside a memory directory, so
|
|
781
|
+
* it happens only when asked for, and says so.
|
|
782
|
+
*/
|
|
783
|
+
function openRememberPathDialog() {
|
|
784
|
+
const store = state.store;
|
|
785
|
+
const input = node('input', {
|
|
786
|
+
type: 'text',
|
|
787
|
+
class: 'path-input',
|
|
788
|
+
value: store.guess || '',
|
|
789
|
+
placeholder: '/Users/you/repos/the-project',
|
|
790
|
+
spellcheck: 'false',
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const body = node('div', { class: 'modal-body' }, [
|
|
794
|
+
node('p', { class: 'muted', style: 'margin-top:0', text: `The folder on disk is "${store.slug}", and its name is a lossy encoding of a path. No session transcript remains to recover the real one from, so tell it once and it will be used from now on.` }),
|
|
795
|
+
input,
|
|
796
|
+
node('p', { class: 'muted', style: 'margin-bottom:0;font-size:12.5px', text: 'Saved to ~/.claude-memory-admin/paths.json. That file records slugs and folder paths only, never memory content, and is the one thing this app writes outside a memory/ directory.' }),
|
|
797
|
+
]);
|
|
798
|
+
|
|
799
|
+
const modal = node('div', { class: 'modal' }, [
|
|
800
|
+
node('header', {}, [node('h3', { text: 'Remember this project\u2019s path' })]),
|
|
801
|
+
body,
|
|
802
|
+
node('footer', {}, [
|
|
803
|
+
node('button', { class: 'btn', text: 'Cancel', onclick: closeModal }),
|
|
804
|
+
node('button', {
|
|
805
|
+
class: 'btn primary',
|
|
806
|
+
text: 'Remember',
|
|
807
|
+
onclick: async () => {
|
|
808
|
+
const value = input.value.trim();
|
|
809
|
+
if (!value) return;
|
|
810
|
+
try {
|
|
811
|
+
await api(`/api/stores/${encodeURIComponent(state.storeId)}/path/remember`, {
|
|
812
|
+
method: 'POST',
|
|
813
|
+
body: JSON.stringify({ path: value }),
|
|
814
|
+
});
|
|
815
|
+
} catch (err) {
|
|
816
|
+
return toast(err.message, { error: true });
|
|
817
|
+
}
|
|
818
|
+
closeModal();
|
|
819
|
+
await reloadStores();
|
|
820
|
+
await openStore(state.storeId, { keepTab: true });
|
|
821
|
+
toast(`Remembered ${value}`);
|
|
822
|
+
},
|
|
823
|
+
}),
|
|
824
|
+
]),
|
|
825
|
+
]);
|
|
826
|
+
|
|
827
|
+
const backdrop = node('div', { class: 'modal-backdrop', onclick: (event) => { if (event.target === backdrop) closeModal(); } }, [modal]);
|
|
828
|
+
el('modal-root').append(backdrop);
|
|
829
|
+
input.focus();
|
|
830
|
+
input.select();
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async function forgetProjectPath() {
|
|
834
|
+
try {
|
|
835
|
+
await api(`/api/stores/${encodeURIComponent(state.storeId)}/path/forget`, { method: 'POST' });
|
|
836
|
+
} catch (err) {
|
|
837
|
+
return toast(err.message, { error: true });
|
|
838
|
+
}
|
|
839
|
+
await reloadStores();
|
|
840
|
+
await openStore(state.storeId, { keepTab: true });
|
|
841
|
+
toast('Forgot the remembered path');
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function openStoreDeleteDialog() {
|
|
638
845
|
let preview;
|
|
639
846
|
try {
|
|
640
|
-
preview = await api(`/api/
|
|
847
|
+
preview = await api(`/api/stores/${encodeURIComponent(state.storeId)}/project/delete-preview`, { method: 'POST' });
|
|
641
848
|
} catch (err) {
|
|
642
849
|
return toast(err.message, { error: true });
|
|
643
850
|
}
|
|
644
851
|
|
|
645
852
|
if (!preview.files.length && !preview.hasIndex) {
|
|
646
|
-
return toast('This
|
|
853
|
+
return toast('This store has no memory to delete');
|
|
647
854
|
}
|
|
648
855
|
|
|
649
856
|
const body = node('div', { class: 'modal-body' });
|
|
@@ -658,14 +865,21 @@ async function openProjectDeleteDialog() {
|
|
|
658
865
|
|
|
659
866
|
body.append(node('div', { class: 'will' }, [
|
|
660
867
|
node('div', { class: 'will-title', text: 'Will be kept' }),
|
|
661
|
-
node('div', { class: 'will-item keep', text:
|
|
868
|
+
node('div', { class: 'will-item keep', text: state.store.kind === 'auto'
|
|
869
|
+
? 'Session transcripts (*.jsonl) and the project folder itself'
|
|
870
|
+
: 'The agent-memory folder itself, and every other agent\u2019s memory' }),
|
|
662
871
|
]));
|
|
663
872
|
|
|
664
|
-
body.append(node('p', { class: 'muted', style: 'margin:0;font-size:12.5px', text: 'Everything moves to
|
|
873
|
+
body.append(node('p', { class: 'muted', style: 'margin:0;font-size:12.5px', text: 'Everything moves to .trash/ inside the store as one restore point, so it can be put back in a single step from the Trash tab.' }));
|
|
874
|
+
|
|
875
|
+
// A project-scoped agent store is committed, so this is a working-tree change.
|
|
876
|
+
if (state.store.kind === 'agent-project') {
|
|
877
|
+
body.append(node('p', { class: 'sub-warn', style: 'margin:8px 0 0', text: 'This store is checked into the repository. Deleting from it changes tracked files, and will show up in git status.' }));
|
|
878
|
+
}
|
|
665
879
|
|
|
666
880
|
const modal = node('div', { class: 'modal' }, [
|
|
667
881
|
node('header', {}, [
|
|
668
|
-
node('h3', { text: `Delete all memory for ${state.
|
|
882
|
+
node('h3', { text: `Delete all memory for ${state.store.label}?` }),
|
|
669
883
|
node('p', { class: 'muted', style: 'margin:0;font-size:13px', text: `${preview.files.length} memories${preview.hasIndex ? ' + MEMORY.md' : ''}` }),
|
|
670
884
|
]),
|
|
671
885
|
body,
|
|
@@ -677,10 +891,10 @@ async function openProjectDeleteDialog() {
|
|
|
677
891
|
onclick: async () => {
|
|
678
892
|
closeModal();
|
|
679
893
|
try {
|
|
680
|
-
const result = await api(`/api/
|
|
894
|
+
const result = await api(`/api/stores/${encodeURIComponent(state.storeId)}/project/delete`, { method: 'POST' });
|
|
681
895
|
state.selected = null;
|
|
682
|
-
await
|
|
683
|
-
toast(`Cleared ${state.
|
|
896
|
+
await openStore(state.storeId, { keepTab: true });
|
|
897
|
+
toast(`Cleared ${state.store.label}`, {
|
|
684
898
|
action: { label: 'Undo', run: () => restoreFromTrash(result.record.id) },
|
|
685
899
|
});
|
|
686
900
|
} catch (err) {
|
|
@@ -728,7 +942,7 @@ const PRUNE_SORTS = {
|
|
|
728
942
|
};
|
|
729
943
|
|
|
730
944
|
function renderBudget(container) {
|
|
731
|
-
const { index } = state.
|
|
945
|
+
const { index } = state.store.stats;
|
|
732
946
|
const percent = Math.min(100, index.worstPercent);
|
|
733
947
|
const level = index.level === 'over' ? 'way-over' : index.level === 'near' ? 'over' : '';
|
|
734
948
|
|
|
@@ -738,6 +952,28 @@ function renderBudget(container) {
|
|
|
738
952
|
? 'Approaching the limit. Once MEMORY.md passes it, everything after the cutoff stops being loaded at all.'
|
|
739
953
|
: 'Comfortably inside the limit. MEMORY.md is loaded at the start of every session.';
|
|
740
954
|
|
|
955
|
+
if (index.cutoff) {
|
|
956
|
+
const dropped = index.cutoff.droppedEntries;
|
|
957
|
+
const box = node('div', { class: 'cut-summary' });
|
|
958
|
+
box.append(node('div', { class: 'cut-summary-head', text: dropped.length
|
|
959
|
+
? `${dropped.length} ${dropped.length === 1 ? 'memory is' : 'memories are'} past the cutoff and not loaded`
|
|
960
|
+
: `${index.cutoff.droppedLines} lines are past the cutoff and not loaded` }));
|
|
961
|
+
box.append(node('p', { class: 'muted', text: `MEMORY.md stops loading at line ${index.cutoff.rawLine + 1}, bounded by ${index.cutoff.by}. Claude cannot see anything below it.` }));
|
|
962
|
+
for (const entry of dropped.slice(0, 20)) {
|
|
963
|
+
box.append(node('div', { class: 'issue' }, [
|
|
964
|
+
node('div', { class: 'issue-body' }, [
|
|
965
|
+
node('div', { class: 'issue-title', text: entry.title || entry.file }),
|
|
966
|
+
node('div', { class: 'issue-detail', text: `MEMORY.md line ${entry.index + 1} · ${entry.file}` }),
|
|
967
|
+
]),
|
|
968
|
+
node('button', { class: 'btn small', text: 'Open', onclick: () => selectMemory(entry.file) }),
|
|
969
|
+
]));
|
|
970
|
+
}
|
|
971
|
+
if (dropped.length > 20) {
|
|
972
|
+
box.append(node('p', { class: 'muted', text: `${dropped.length - 20} more not listed.` }));
|
|
973
|
+
}
|
|
974
|
+
container.append(box);
|
|
975
|
+
}
|
|
976
|
+
|
|
741
977
|
container.append(node('div', { class: 'meter' }, [
|
|
742
978
|
node('div', { class: 'meter-top' }, [
|
|
743
979
|
node('span', { class: 'meter-value', text: `${Math.round(index.worstPercent)}%` }),
|
|
@@ -758,7 +994,7 @@ function renderBudget(container) {
|
|
|
758
994
|
}
|
|
759
995
|
|
|
760
996
|
function renderPrune(container) {
|
|
761
|
-
const project = state.
|
|
997
|
+
const project = state.store;
|
|
762
998
|
renderBudget(container);
|
|
763
999
|
|
|
764
1000
|
if (!project.memories.length) {
|
|
@@ -867,7 +1103,7 @@ function renderPrune(container) {
|
|
|
867
1103
|
|
|
868
1104
|
async function openBulkDeleteDialog(files) {
|
|
869
1105
|
if (!files.length) return;
|
|
870
|
-
const memories = files.map((file) => state.
|
|
1106
|
+
const memories = files.map((file) => state.store.memories.find((m) => m.file === file)).filter(Boolean);
|
|
871
1107
|
|
|
872
1108
|
const body = node('div', { class: 'modal-body' });
|
|
873
1109
|
const removals = node('div', { class: 'will' }, [node('div', { class: 'will-title', text: `${memories.length} memories will be trashed` })]);
|
|
@@ -903,13 +1139,13 @@ async function openBulkDeleteDialog(files) {
|
|
|
903
1139
|
onclick: async () => {
|
|
904
1140
|
closeModal();
|
|
905
1141
|
try {
|
|
906
|
-
const result = await api(`/api/
|
|
1142
|
+
const result = await api(`/api/stores/${encodeURIComponent(state.storeId)}/delete-many`, {
|
|
907
1143
|
method: 'POST',
|
|
908
1144
|
body: JSON.stringify({ files, label: `${memories.length} pruned memories` }),
|
|
909
1145
|
});
|
|
910
1146
|
state.pruneSelection.clear();
|
|
911
1147
|
if (doomed.has(state.selected)) state.selected = null;
|
|
912
|
-
await
|
|
1148
|
+
await openStore(state.storeId, { keepTab: true });
|
|
913
1149
|
toast(`Deleted ${memories.length} memories`, {
|
|
914
1150
|
action: { label: 'Undo', run: () => restoreFromTrash(result.record.id) },
|
|
915
1151
|
});
|
|
@@ -958,19 +1194,21 @@ function renderSearch() {
|
|
|
958
1194
|
? `${data.total} result${data.total === 1 ? '' : 's'} for "${state.query.trim()}"`
|
|
959
1195
|
: `No results for "${state.query.trim()}"`;
|
|
960
1196
|
|
|
961
|
-
if (!data.total && !data.
|
|
1197
|
+
if (!data.total && !data.stores.length) {
|
|
962
1198
|
host.append(node('p', { class: 'muted', text: 'Nothing matched. Every word has to appear somewhere in a memory, so try fewer words.' }));
|
|
963
1199
|
return;
|
|
964
1200
|
}
|
|
965
1201
|
|
|
966
|
-
for (const group of data.
|
|
1202
|
+
for (const group of data.stores) {
|
|
967
1203
|
const block = node('div', { class: 'result-group' });
|
|
968
|
-
block.append(node('div', { class: 'section-label', text:
|
|
1204
|
+
block.append(node('div', { class: 'section-label', text: group.sublabel
|
|
1205
|
+
? `${group.label} · ${group.sublabel} · ${group.results.length}`
|
|
1206
|
+
: `${group.label} · ${group.results.length}` }));
|
|
969
1207
|
|
|
970
1208
|
for (const result of group.results) {
|
|
971
1209
|
const button = node('button', {
|
|
972
1210
|
class: 'result',
|
|
973
|
-
onclick: () => openFromSearch(group.
|
|
1211
|
+
onclick: () => openFromSearch(group.id, result.file),
|
|
974
1212
|
});
|
|
975
1213
|
const top = node('div', { class: 'result-top' });
|
|
976
1214
|
const name = node('span', { class: 'result-name' });
|
|
@@ -991,7 +1229,7 @@ function renderSearch() {
|
|
|
991
1229
|
for (const hit of group.indexHits) {
|
|
992
1230
|
const button = node('button', {
|
|
993
1231
|
class: 'result',
|
|
994
|
-
onclick: () => openFromSearch(group.
|
|
1232
|
+
onclick: () => openFromSearch(group.id, hit.file, 'index'),
|
|
995
1233
|
});
|
|
996
1234
|
button.append(node('div', { class: 'result-where', text: `MEMORY.md line ${hit.index + 1}` }));
|
|
997
1235
|
const line = node('div', { class: 'result-snippet' });
|
|
@@ -1003,9 +1241,9 @@ function renderSearch() {
|
|
|
1003
1241
|
}
|
|
1004
1242
|
}
|
|
1005
1243
|
|
|
1006
|
-
async function openFromSearch(
|
|
1244
|
+
async function openFromSearch(id, file, tab = 'memories') {
|
|
1007
1245
|
clearSearch();
|
|
1008
|
-
if (
|
|
1246
|
+
if (id !== state.storeId) await openStore(id);
|
|
1009
1247
|
state.tab = tab;
|
|
1010
1248
|
if (file) state.selected = file;
|
|
1011
1249
|
renderTabs();
|
|
@@ -1022,8 +1260,8 @@ function clearSearch() {
|
|
|
1022
1260
|
function renderView() {
|
|
1023
1261
|
const searching = state.search !== null;
|
|
1024
1262
|
el('search-view').hidden = !searching;
|
|
1025
|
-
el('project-view').hidden = searching || !state.
|
|
1026
|
-
el('empty-state').hidden = searching || Boolean(state.
|
|
1263
|
+
el('project-view').hidden = searching || !state.store;
|
|
1264
|
+
el('empty-state').hidden = searching || Boolean(state.store);
|
|
1027
1265
|
if (searching) renderSearch();
|
|
1028
1266
|
}
|
|
1029
1267
|
|
|
@@ -1034,13 +1272,14 @@ function renderTab() {
|
|
|
1034
1272
|
container.textContent = '';
|
|
1035
1273
|
if (state.tab === 'memories') renderMemories(container);
|
|
1036
1274
|
else if (state.tab === 'prune') renderPrune(container);
|
|
1275
|
+
else if (state.tab === 'context') renderContext(container);
|
|
1037
1276
|
else if (state.tab === 'index') renderIndex(container);
|
|
1038
1277
|
else if (state.tab === 'health') renderHealth(container);
|
|
1039
1278
|
else if (state.tab === 'trash') renderTrash(container);
|
|
1040
1279
|
else if (state.tab === 'graph') {
|
|
1041
1280
|
const wrap = node('div', { id: 'graph-wrap' });
|
|
1042
1281
|
container.append(wrap);
|
|
1043
|
-
renderGraph(wrap, state.
|
|
1282
|
+
renderGraph(wrap, state.store.graph, {
|
|
1044
1283
|
selected: state.selected,
|
|
1045
1284
|
spread: state.spread,
|
|
1046
1285
|
onSelect: (file) => selectMemory(file),
|
|
@@ -1065,34 +1304,87 @@ function selectMemory(file) {
|
|
|
1065
1304
|
document.querySelector('.mem.active')?.scrollIntoView({ block: 'nearest' });
|
|
1066
1305
|
}
|
|
1067
1306
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1307
|
+
/** Re-read the store list, e.g. after a path is remembered or forgotten. */
|
|
1308
|
+
async function reloadStores() {
|
|
1309
|
+
const data = await api('/api/stores');
|
|
1310
|
+
state.stores = data.stores;
|
|
1311
|
+
renderStores();
|
|
1312
|
+
return data;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/** The name and provenance line above the tabs, which differs by store kind. */
|
|
1316
|
+
function renderStoreHeader() {
|
|
1317
|
+
const store = state.store;
|
|
1318
|
+
el('project-title').textContent = store.label;
|
|
1319
|
+
|
|
1320
|
+
const sub = el('project-sub');
|
|
1321
|
+
sub.textContent = '';
|
|
1322
|
+
|
|
1323
|
+
if (store.kind !== 'auto') {
|
|
1324
|
+
const scope = store.kind === 'agent-user' ? 'user'
|
|
1325
|
+
: store.kind === 'agent-project' ? 'project' : 'local';
|
|
1326
|
+
sub.append(node('span', { text: store.dir }));
|
|
1327
|
+
sub.append(node('span', { class: 'sub-note', text: `subagent memory, ${scope} scope` }));
|
|
1328
|
+
// A project-scoped store is committed, so deleting from it edits the repo
|
|
1329
|
+
// rather than a private cache. Worth saying before, not after.
|
|
1330
|
+
if (store.kind === 'agent-project') {
|
|
1331
|
+
sub.append(node('span', { class: 'sub-warn', text: 'checked into the repository - changes here show up in git status' }));
|
|
1332
|
+
}
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
sub.append(node('span', { text: store.resolvedBy === 'unresolved'
|
|
1337
|
+
? `${store.path} (real path unresolved)`
|
|
1338
|
+
: store.path }));
|
|
1339
|
+
|
|
1340
|
+
// One store serves every worktree and subdirectory of a repository, which is
|
|
1341
|
+
// not obvious from a single path and changes what a memory here applies to.
|
|
1342
|
+
const others = (store.workingDirs || []).filter((d) => d !== store.path);
|
|
1343
|
+
if (others.length) {
|
|
1344
|
+
sub.append(node('span', { class: 'sub-note', text: `also used from ${others.join(', ')}` }));
|
|
1345
|
+
}
|
|
1346
|
+
if (store.resolvedBy === 'unresolved') {
|
|
1347
|
+
sub.append(node('button', { class: 'btn small link-btn', text: 'Remember path\u2026', onclick: openRememberPathDialog }));
|
|
1348
|
+
} else if (store.resolvedBy === 'remembered') {
|
|
1349
|
+
sub.append(node('span', { class: 'sub-note' }, [
|
|
1350
|
+
document.createTextNode('path remembered, not recovered from a transcript '),
|
|
1351
|
+
node('button', { class: 'btn small link-btn', text: 'Forget', onclick: forgetProjectPath }),
|
|
1352
|
+
]));
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
const auto = store.autoMemory;
|
|
1356
|
+
if (auto && auto.known && !auto.enabled) {
|
|
1357
|
+
sub.append(node('span', { class: 'sub-warn', text: auto.scope === 'env'
|
|
1358
|
+
? 'auto memory is off (CLAUDE_CODE_DISABLE_AUTO_MEMORY) - this store will not grow'
|
|
1359
|
+
: `auto memory is off (${auto.setBy}) - this store will not grow` }));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
async function openStore(id, { keepTab = false } = {}) {
|
|
1364
|
+
if (id !== state.storeId) state.pruneSelection.clear();
|
|
1365
|
+
state.storeId = id;
|
|
1071
1366
|
if (!keepTab) {
|
|
1072
1367
|
state.tab = 'memories';
|
|
1073
1368
|
state.selected = null;
|
|
1074
1369
|
}
|
|
1075
|
-
|
|
1370
|
+
renderStores();
|
|
1076
1371
|
|
|
1077
1372
|
try {
|
|
1078
|
-
state.
|
|
1373
|
+
state.store = await api(`/api/stores/${encodeURIComponent(id)}`);
|
|
1079
1374
|
} catch (err) {
|
|
1080
1375
|
return toast(err.message, { error: true });
|
|
1081
1376
|
}
|
|
1082
1377
|
|
|
1083
|
-
if (state.selected && !state.
|
|
1378
|
+
if (state.selected && !state.store.memories.some((m) => m.file === state.selected)) {
|
|
1084
1379
|
state.selected = null;
|
|
1085
1380
|
}
|
|
1086
1381
|
|
|
1087
|
-
|
|
1088
|
-
el('project-sub').textContent = state.project.resolvedBy === 'unresolved'
|
|
1089
|
-
? `${state.project.path} (real path unresolved)`
|
|
1090
|
-
: state.project.path;
|
|
1382
|
+
renderStoreHeader();
|
|
1091
1383
|
|
|
1092
1384
|
// Refresh the sidebar count in case a delete changed it.
|
|
1093
|
-
const listed = state.
|
|
1094
|
-
if (listed) listed.memoryCount = state.
|
|
1095
|
-
|
|
1385
|
+
const listed = state.stores.find((store) => store.id === id);
|
|
1386
|
+
if (listed) listed.memoryCount = state.store.memories.length;
|
|
1387
|
+
renderStores();
|
|
1096
1388
|
|
|
1097
1389
|
renderView();
|
|
1098
1390
|
renderTabs();
|
|
@@ -1110,20 +1402,20 @@ function setCollapsed(value) {
|
|
|
1110
1402
|
applyCollapsed();
|
|
1111
1403
|
// The graph sizes itself to the container, so it has to be redrawn once the
|
|
1112
1404
|
// pane has actually changed width.
|
|
1113
|
-
if (state.tab === 'graph' && state.
|
|
1405
|
+
if (state.tab === 'graph' && state.store) requestAnimationFrame(renderTab);
|
|
1114
1406
|
}
|
|
1115
1407
|
|
|
1116
1408
|
async function init() {
|
|
1117
1409
|
el('show-all').addEventListener('change', (event) => {
|
|
1118
1410
|
state.showAll = event.target.checked;
|
|
1119
|
-
|
|
1411
|
+
renderStores();
|
|
1120
1412
|
});
|
|
1121
1413
|
|
|
1122
1414
|
el('search').addEventListener('input', (event) => scheduleSearch(event.target.value));
|
|
1123
1415
|
el('search-clear').addEventListener('click', clearSearch);
|
|
1124
1416
|
el('collapse').addEventListener('click', () => setCollapsed(true));
|
|
1125
1417
|
el('expand').addEventListener('click', () => setCollapsed(false));
|
|
1126
|
-
el('delete-project').addEventListener('click',
|
|
1418
|
+
el('delete-project').addEventListener('click', openStoreDeleteDialog);
|
|
1127
1419
|
document.addEventListener('keydown', (event) => {
|
|
1128
1420
|
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(event.target.tagName);
|
|
1129
1421
|
if (event.key === '/' && !typing) {
|
|
@@ -1142,12 +1434,17 @@ async function init() {
|
|
|
1142
1434
|
applyCollapsed();
|
|
1143
1435
|
|
|
1144
1436
|
try {
|
|
1145
|
-
const data = await
|
|
1146
|
-
state.projects = data.projects;
|
|
1437
|
+
const data = await reloadStores();
|
|
1147
1438
|
el('root-path').textContent = data.root;
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1439
|
+
if (data.rootSource && data.rootSource !== 'default') {
|
|
1440
|
+
el('root-path').title = data.rootFile
|
|
1441
|
+
? `autoMemoryDirectory set in ${data.rootFile}`
|
|
1442
|
+
: 'store chosen with --root';
|
|
1443
|
+
el('root-path').append(node('span', { class: 'root-source', text: data.rootSource }));
|
|
1444
|
+
}
|
|
1445
|
+
if (data.rootWarning) toast(data.rootWarning, { error: true });
|
|
1446
|
+
const first = state.stores.find((store) => store.memoryCount > 0);
|
|
1447
|
+
if (first) openStore(first.id);
|
|
1151
1448
|
} catch (err) {
|
|
1152
1449
|
toast(err.message, { error: true });
|
|
1153
1450
|
}
|