claude-code-memory-explorer 0.4.1 → 0.5.0
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/package.json +1 -1
- package/public/app.js +116 -53
- package/public/index.html +2 -2
- package/public/style.css +42 -21
- package/server.js +2 -2
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -258,6 +258,7 @@ const LOAD_ICONS = {
|
|
|
258
258
|
conditional: '\u25CB',
|
|
259
259
|
ondemand: '\u25CC',
|
|
260
260
|
import: '@',
|
|
261
|
+
link: '\u2197',
|
|
261
262
|
};
|
|
262
263
|
const LOAD_TITLES = {
|
|
263
264
|
always: 'Always loaded',
|
|
@@ -265,6 +266,7 @@ const LOAD_TITLES = {
|
|
|
265
266
|
conditional: 'Conditional (path-scoped)',
|
|
266
267
|
ondemand: 'On-demand',
|
|
267
268
|
import: 'Imported by parent file',
|
|
269
|
+
link: 'Referenced via markdown link (not auto-loaded)',
|
|
268
270
|
};
|
|
269
271
|
|
|
270
272
|
let treeIndex = null;
|
|
@@ -282,17 +284,12 @@ function getTreeIndex() {
|
|
|
282
284
|
groups[s.scope].push(s);
|
|
283
285
|
}
|
|
284
286
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (children) for (const c of children) collect(c);
|
|
290
|
-
}
|
|
291
|
-
for (const scope of SCOPE_ORDER) {
|
|
292
|
-
const items = groups[scope];
|
|
293
|
-
if (items) for (const item of items) collect(item);
|
|
287
|
+
// Default-expand parents whose children are real discovered files (e.g. auto-memory
|
|
288
|
+
// dirs indexed by MEMORY.md). Pure @import/link references (importedBy set) stay collapsed.
|
|
289
|
+
for (const parentId in childrenOf) {
|
|
290
|
+
if (childrenOf[parentId].some((c) => !c.importedBy)) expandedItems.add(parentId);
|
|
294
291
|
}
|
|
295
|
-
treeIndex = { groups, childrenOf
|
|
292
|
+
treeIndex = { groups, childrenOf };
|
|
296
293
|
return treeIndex;
|
|
297
294
|
}
|
|
298
295
|
|
|
@@ -315,16 +312,21 @@ function renderTree() {
|
|
|
315
312
|
const loadIcon = LOAD_ICONS[item.load] || '';
|
|
316
313
|
const loadTitle = LOAD_TITLES[item.load] || item.load;
|
|
317
314
|
const meta = `${item.lines}L`;
|
|
318
|
-
const isConditional = item.load === 'conditional' || item.load === 'ondemand';
|
|
315
|
+
const isConditional = item.load === 'conditional' || item.load === 'ondemand' || item.load === 'link';
|
|
319
316
|
const pad = indent ? ` style="padding-left:${12 + indent * 16}px"` : '';
|
|
320
317
|
const muted = item.scope === 'agent-memory' ? ' tree-agent-item' : '';
|
|
318
|
+
const children = childrenOf[item.id];
|
|
319
|
+
const expanded = children && isItemExpanded(item.id);
|
|
320
|
+
const chevron = children
|
|
321
|
+
? `<span class="tree-chevron" onclick="_toggleItem(event,'${escJs(item.id)}')" title="${children.length} referenced">${expanded ? '▾' : '▸'}</span>`
|
|
322
|
+
: '<span class="tree-chevron-spacer"></span>';
|
|
321
323
|
let h = `<div class="tree-item${sel}${indent ? ' tree-child' : ''}${isConditional ? ' tree-conditional' : ''}${muted}" data-id="${esc(item.id)}" title="${esc(item.path)}" onclick="selectFile('${escJs(item.id)}')"${pad}>`;
|
|
324
|
+
h += chevron;
|
|
322
325
|
h += `<span class="load-icon" title="${loadTitle}" style="color:var(--scope-${item.scope})">${loadIcon}</span>`;
|
|
323
326
|
h += `<span class="file-name">${esc(item.name)}</span>`;
|
|
324
327
|
h += `<span class="file-meta">${meta}</span>`;
|
|
325
328
|
h += '</div>';
|
|
326
|
-
|
|
327
|
-
if (children) {
|
|
329
|
+
if (expanded) {
|
|
328
330
|
for (const child of children) h += renderItem(child, (indent || 0) + 1);
|
|
329
331
|
}
|
|
330
332
|
return h;
|
|
@@ -396,6 +398,26 @@ function expandGroupForItem(item) {
|
|
|
396
398
|
if (scope && isGroupCollapsed(scope)) setGroupCollapsed(scope, false);
|
|
397
399
|
}
|
|
398
400
|
|
|
401
|
+
// Referenced/child files are collapsed by default; expand on click. Only top-level
|
|
402
|
+
// "root" files (those not pulled in via an import/link) are shown until expanded.
|
|
403
|
+
const expandedItems = new Set();
|
|
404
|
+
function isItemExpanded(id) {
|
|
405
|
+
return expandedItems.has(id);
|
|
406
|
+
}
|
|
407
|
+
function _toggleItem(event, id) {
|
|
408
|
+
event.stopPropagation();
|
|
409
|
+
if (expandedItems.has(id)) expandedItems.delete(id);
|
|
410
|
+
else expandedItems.add(id);
|
|
411
|
+
renderTree();
|
|
412
|
+
}
|
|
413
|
+
function expandItemAncestors(item) {
|
|
414
|
+
let cur = item;
|
|
415
|
+
while (cur?.parentId) {
|
|
416
|
+
expandedItems.add(cur.parentId);
|
|
417
|
+
cur = stackData.find((s) => s.id === cur.parentId);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
399
421
|
function pushFileState(id) {
|
|
400
422
|
const url = id ? `#${encodeURIComponent(id)}` : location.pathname;
|
|
401
423
|
history.pushState({ fileId: id }, '', url);
|
|
@@ -403,6 +425,13 @@ function pushFileState(id) {
|
|
|
403
425
|
|
|
404
426
|
function selectFile(id, pushState = true) {
|
|
405
427
|
selectedFileId = selectedFileId === id ? null : id;
|
|
428
|
+
if (selectedFileId) {
|
|
429
|
+
const item = stackData.find((s) => s.id === selectedFileId);
|
|
430
|
+
if (item) {
|
|
431
|
+
expandGroupForItem(item);
|
|
432
|
+
expandItemAncestors(item);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
406
435
|
if (pushState) pushFileState(selectedFileId);
|
|
407
436
|
renderTree();
|
|
408
437
|
renderPreview();
|
|
@@ -413,47 +442,76 @@ function scrollToSelected() {
|
|
|
413
442
|
if (el) el.scrollIntoView({ block: 'nearest' });
|
|
414
443
|
}
|
|
415
444
|
|
|
445
|
+
function currentItem() {
|
|
446
|
+
return stackData.find((s) => s.id === selectedFileId) || null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function selectNav(id) {
|
|
450
|
+
selectedFileId = id;
|
|
451
|
+
pushFileState(id);
|
|
452
|
+
renderTree();
|
|
453
|
+
renderPreview();
|
|
454
|
+
scrollToSelected();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Visible traversal order: collapsed subtrees and collapsed groups are skipped.
|
|
458
|
+
function getVisibleOrder() {
|
|
459
|
+
const { groups, childrenOf } = getTreeIndex();
|
|
460
|
+
const order = [];
|
|
461
|
+
const walk = (item) => {
|
|
462
|
+
order.push(item);
|
|
463
|
+
if (isItemExpanded(item.id)) {
|
|
464
|
+
const children = childrenOf[item.id];
|
|
465
|
+
if (children) for (const c of children) walk(c);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
for (const scope of SCOPE_ORDER) {
|
|
469
|
+
if (isGroupCollapsed(scope)) continue;
|
|
470
|
+
const items = groups[scope];
|
|
471
|
+
if (items) for (const item of items) walk(item);
|
|
472
|
+
}
|
|
473
|
+
return order;
|
|
474
|
+
}
|
|
475
|
+
|
|
416
476
|
function navigateTree(direction) {
|
|
417
|
-
const
|
|
418
|
-
if (!
|
|
419
|
-
let idx =
|
|
477
|
+
const order = getVisibleOrder();
|
|
478
|
+
if (!order.length) return;
|
|
479
|
+
let idx = order.findIndex((s) => s.id === selectedFileId);
|
|
420
480
|
if (idx === -1) {
|
|
421
|
-
idx = direction > 0 ? 0 :
|
|
481
|
+
idx = direction > 0 ? 0 : order.length - 1;
|
|
422
482
|
} else {
|
|
423
483
|
idx += direction;
|
|
424
|
-
if (idx < 0) idx =
|
|
425
|
-
if (idx >=
|
|
484
|
+
if (idx < 0) idx = order.length - 1;
|
|
485
|
+
if (idx >= order.length) idx = 0;
|
|
426
486
|
}
|
|
427
|
-
|
|
428
|
-
selectedFileId = target.id;
|
|
429
|
-
expandGroupForItem(target);
|
|
430
|
-
pushFileState(selectedFileId);
|
|
431
|
-
renderTree();
|
|
432
|
-
renderPreview();
|
|
433
|
-
scrollToSelected();
|
|
487
|
+
selectNav(order[idx].id);
|
|
434
488
|
}
|
|
435
489
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
if (!
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
490
|
+
// l / Right: expand a collapsed node, descend into an expanded one, leaf = no-op.
|
|
491
|
+
function expandOrDescend() {
|
|
492
|
+
const item = currentItem();
|
|
493
|
+
if (!item) return navigateTree(1);
|
|
494
|
+
const children = getTreeIndex().childrenOf[item.id];
|
|
495
|
+
if (!children?.length) return;
|
|
496
|
+
if (!isItemExpanded(item.id)) {
|
|
497
|
+
expandedItems.add(item.id);
|
|
498
|
+
renderTree();
|
|
445
499
|
} else {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
500
|
+
selectNav(children[0].id);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// h / Left: collapse an expanded node, otherwise ascend to its parent.
|
|
505
|
+
function collapseOrAscend() {
|
|
506
|
+
const item = currentItem();
|
|
507
|
+
if (!item) return navigateTree(-1);
|
|
508
|
+
const children = getTreeIndex().childrenOf[item.id];
|
|
509
|
+
if (children?.length && isItemExpanded(item.id)) {
|
|
510
|
+
expandedItems.delete(item.id);
|
|
511
|
+
renderTree();
|
|
512
|
+
} else if (item.parentId) {
|
|
513
|
+
selectNav(item.parentId);
|
|
514
|
+
}
|
|
457
515
|
}
|
|
458
516
|
|
|
459
517
|
// #endregion RENDER_TREE
|
|
@@ -821,21 +879,23 @@ document.addEventListener('keydown', (e) => {
|
|
|
821
879
|
e.preventDefault();
|
|
822
880
|
changeProject();
|
|
823
881
|
}
|
|
824
|
-
|
|
882
|
+
// Skip when modifiers are held (e.g. hub's Ctrl+Alt+Arrow app switching).
|
|
883
|
+
const plain = !e.ctrlKey && !e.altKey && !e.metaKey;
|
|
884
|
+
if (plain && (e.key === 'j' || e.key === 'ArrowDown')) {
|
|
825
885
|
e.preventDefault();
|
|
826
886
|
navigateTree(1);
|
|
827
887
|
}
|
|
828
|
-
if (e.key === 'k' || e.key === 'ArrowUp') {
|
|
888
|
+
if (plain && (e.key === 'k' || e.key === 'ArrowUp')) {
|
|
829
889
|
e.preventDefault();
|
|
830
890
|
navigateTree(-1);
|
|
831
891
|
}
|
|
832
|
-
if (e.key === 'h' || e.key === 'ArrowLeft') {
|
|
892
|
+
if (plain && (e.key === 'h' || e.key === 'ArrowLeft')) {
|
|
833
893
|
e.preventDefault();
|
|
834
|
-
|
|
894
|
+
collapseOrAscend();
|
|
835
895
|
}
|
|
836
|
-
if (e.key === 'l' || e.key === 'ArrowRight') {
|
|
896
|
+
if (plain && (e.key === 'l' || e.key === 'ArrowRight')) {
|
|
837
897
|
e.preventDefault();
|
|
838
|
-
|
|
898
|
+
expandOrDescend();
|
|
839
899
|
}
|
|
840
900
|
if (e.key === 'Enter' && selectedFileId) renderPreview();
|
|
841
901
|
if (e.key === 'e' && selectedFileId) {
|
|
@@ -979,8 +1039,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
979
1039
|
await loadData();
|
|
980
1040
|
// Restore file selection from hash
|
|
981
1041
|
const hash = decodeURIComponent(location.hash.slice(1));
|
|
982
|
-
|
|
1042
|
+
const hashItem = hash && stackData.find((s) => s.id === hash);
|
|
1043
|
+
if (hashItem) {
|
|
983
1044
|
selectedFileId = hash;
|
|
1045
|
+
expandGroupForItem(hashItem);
|
|
1046
|
+
expandItemAncestors(hashItem);
|
|
984
1047
|
renderTree();
|
|
985
1048
|
renderPreview();
|
|
986
1049
|
}
|
package/public/index.html
CHANGED
|
@@ -157,8 +157,8 @@
|
|
|
157
157
|
<table class="shortcut-table">
|
|
158
158
|
<tr><td><kbd>j</kbd> / <kbd>↓</kbd></td><td>Next item</td></tr>
|
|
159
159
|
<tr><td><kbd>k</kbd> / <kbd>↑</kbd></td><td>Previous item</td></tr>
|
|
160
|
-
<tr><td><kbd>
|
|
161
|
-
<tr><td><kbd>
|
|
160
|
+
<tr><td><kbd>l</kbd> / <kbd>→</kbd></td><td>Expand / enter child</td></tr>
|
|
161
|
+
<tr><td><kbd>h</kbd> / <kbd>←</kbd></td><td>Collapse / parent</td></tr>
|
|
162
162
|
<tr><td><kbd>Enter</kbd></td><td>Select item</td></tr>
|
|
163
163
|
<tr><td><kbd>Esc</kbd></td><td>Close modal</td></tr>
|
|
164
164
|
</table>
|
package/public/style.css
CHANGED
|
@@ -42,13 +42,12 @@ body {
|
|
|
42
42
|
background: var(--bg-deep);
|
|
43
43
|
color: var(--text-primary);
|
|
44
44
|
font-family: var(--font-sans);
|
|
45
|
-
font-size:
|
|
45
|
+
font-size: 14px;
|
|
46
46
|
line-height: 1.5;
|
|
47
|
-
height:
|
|
47
|
+
height: 100vh;
|
|
48
48
|
display: flex;
|
|
49
49
|
flex-direction: column;
|
|
50
50
|
overflow: hidden;
|
|
51
|
-
zoom: 1.25;
|
|
52
51
|
}
|
|
53
52
|
.mono {
|
|
54
53
|
font-family: var(--font-mono);
|
|
@@ -299,10 +298,10 @@ body {
|
|
|
299
298
|
color: var(--text-muted);
|
|
300
299
|
}
|
|
301
300
|
.tree-agent-item .file-name {
|
|
302
|
-
opacity: 0.
|
|
301
|
+
opacity: 0.78;
|
|
303
302
|
}
|
|
304
303
|
body.light .tree-agent-item .file-name {
|
|
305
|
-
opacity: 0.
|
|
304
|
+
opacity: 0.86;
|
|
306
305
|
}
|
|
307
306
|
.tree-item {
|
|
308
307
|
padding: 5px 12px 5px 24px;
|
|
@@ -321,6 +320,25 @@ body.light .tree-agent-item .file-name {
|
|
|
321
320
|
background: var(--accent-dim);
|
|
322
321
|
color: var(--accent);
|
|
323
322
|
}
|
|
323
|
+
.tree-item .tree-chevron,
|
|
324
|
+
.tree-item .tree-chevron-spacer {
|
|
325
|
+
width: 12px;
|
|
326
|
+
flex-shrink: 0;
|
|
327
|
+
text-align: center;
|
|
328
|
+
}
|
|
329
|
+
.tree-item .tree-chevron {
|
|
330
|
+
font-size: 9px;
|
|
331
|
+
color: var(--text-muted);
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
border-radius: 3px;
|
|
334
|
+
transition:
|
|
335
|
+
color 0.1s,
|
|
336
|
+
background 0.1s;
|
|
337
|
+
}
|
|
338
|
+
.tree-item .tree-chevron:hover {
|
|
339
|
+
color: var(--accent-text);
|
|
340
|
+
background: var(--accent-dim);
|
|
341
|
+
}
|
|
324
342
|
.tree-item .load-icon {
|
|
325
343
|
font-size: 10px;
|
|
326
344
|
width: 12px;
|
|
@@ -329,7 +347,7 @@ body.light .tree-agent-item .file-name {
|
|
|
329
347
|
}
|
|
330
348
|
.tree-item .file-name {
|
|
331
349
|
font-family: var(--font-mono);
|
|
332
|
-
font-size:
|
|
350
|
+
font-size: 13px;
|
|
333
351
|
flex: 1;
|
|
334
352
|
overflow: hidden;
|
|
335
353
|
text-overflow: ellipsis;
|
|
@@ -342,13 +360,13 @@ body.light .tree-agent-item .file-name {
|
|
|
342
360
|
flex-shrink: 0;
|
|
343
361
|
}
|
|
344
362
|
.tree-child .file-name {
|
|
345
|
-
opacity: 0.
|
|
363
|
+
opacity: 0.82;
|
|
346
364
|
}
|
|
347
365
|
.tree-conditional .file-name {
|
|
348
|
-
opacity: 0.
|
|
366
|
+
opacity: 0.68;
|
|
349
367
|
}
|
|
350
368
|
body.light .tree-conditional .file-name {
|
|
351
|
-
opacity: 0.
|
|
369
|
+
opacity: 0.8;
|
|
352
370
|
}
|
|
353
371
|
/* #endregion TREE */
|
|
354
372
|
|
|
@@ -366,8 +384,8 @@ body.light .tree-conditional .file-name {
|
|
|
366
384
|
}
|
|
367
385
|
.preview-title .file-path {
|
|
368
386
|
font-family: var(--font-mono);
|
|
369
|
-
font-size:
|
|
370
|
-
color: var(--text-
|
|
387
|
+
font-size: 12px;
|
|
388
|
+
color: var(--text-tertiary);
|
|
371
389
|
overflow: hidden;
|
|
372
390
|
text-overflow: ellipsis;
|
|
373
391
|
}
|
|
@@ -383,10 +401,10 @@ body.light .tree-conditional .file-name {
|
|
|
383
401
|
margin-top: 6px;
|
|
384
402
|
}
|
|
385
403
|
.preview-filepath {
|
|
386
|
-
padding:
|
|
404
|
+
padding: 5px 16px;
|
|
387
405
|
font-family: var(--font-mono);
|
|
388
|
-
font-size:
|
|
389
|
-
color: var(--text-
|
|
406
|
+
font-size: 12px;
|
|
407
|
+
color: var(--text-tertiary);
|
|
390
408
|
background: var(--bg-deep);
|
|
391
409
|
border-bottom: 1px solid var(--border);
|
|
392
410
|
overflow: hidden;
|
|
@@ -395,9 +413,9 @@ body.light .tree-conditional .file-name {
|
|
|
395
413
|
}
|
|
396
414
|
.preview-code {
|
|
397
415
|
font-family: var(--font-mono);
|
|
398
|
-
font-size:
|
|
416
|
+
font-size: 13px;
|
|
399
417
|
line-height: 1.6;
|
|
400
|
-
color: var(--text-
|
|
418
|
+
color: var(--text-primary);
|
|
401
419
|
white-space: pre-wrap;
|
|
402
420
|
word-break: break-word;
|
|
403
421
|
padding: 12px 16px;
|
|
@@ -860,8 +878,8 @@ kbd {
|
|
|
860
878
|
color: var(--error);
|
|
861
879
|
}
|
|
862
880
|
.toast.success {
|
|
863
|
-
border-color:
|
|
864
|
-
color:
|
|
881
|
+
border-color: var(--success);
|
|
882
|
+
color: var(--success);
|
|
865
883
|
}
|
|
866
884
|
@keyframes slideIn {
|
|
867
885
|
from {
|
|
@@ -878,15 +896,18 @@ kbd {
|
|
|
878
896
|
/* #region LIGHT_THEME */
|
|
879
897
|
body.light {
|
|
880
898
|
--bg-deep: #e8e6e3;
|
|
881
|
-
--bg-surface: #
|
|
882
|
-
--bg-elevated: #
|
|
899
|
+
--bg-surface: #efede9;
|
|
900
|
+
--bg-elevated: #fbfaf9;
|
|
883
901
|
--bg-hover: #d2d0cc;
|
|
884
|
-
--border: #
|
|
902
|
+
--border: #cfcbc4;
|
|
885
903
|
--text-primary: #0a0a0a;
|
|
886
904
|
--text-secondary: #444444;
|
|
887
905
|
--text-tertiary: #666666;
|
|
888
906
|
--text-muted: #888888;
|
|
889
907
|
--accent-text: #b85a20;
|
|
908
|
+
--accent-dim: rgba(232, 111, 51, 0.18);
|
|
909
|
+
--accent-glow: rgba(232, 111, 51, 0.5);
|
|
910
|
+
--success: #1a8a5a;
|
|
890
911
|
}
|
|
891
912
|
/* #endregion LIGHT_THEME */
|
|
892
913
|
|
package/server.js
CHANGED
|
@@ -374,10 +374,10 @@ function discoverMemorySources(projectPath) {
|
|
|
374
374
|
if (!info) continue;
|
|
375
375
|
const { resolved: imports, resolvedSoft: softImports, unresolved: unresolvedImports } = resolveAllImports(imp, info.content);
|
|
376
376
|
const source = {
|
|
377
|
-
id: `import-${
|
|
377
|
+
id: `import-${imp.replace(/[^a-zA-Z0-9]/g, '-')}`,
|
|
378
378
|
name: path.basename(imp),
|
|
379
379
|
scope: parent.scope,
|
|
380
|
-
load: 'import',
|
|
380
|
+
load: hard ? 'import' : 'link',
|
|
381
381
|
...info,
|
|
382
382
|
importedBy: parent.path,
|
|
383
383
|
parentId: parent.id,
|