claude-code-memory-explorer 0.14.0 → 0.14.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/package.json +1 -1
- package/public/app.js +68 -13
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/index.html +5 -0
- package/server.js +1 -1
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -4,7 +4,7 @@ let projectData = null;
|
|
|
4
4
|
let stackData = [];
|
|
5
5
|
let summaryData = null;
|
|
6
6
|
let selectedFileId = null;
|
|
7
|
-
let healthViewOpen =
|
|
7
|
+
let healthViewOpen = false; // opt-in view; the Analyze button toggles it, selecting a file exits it
|
|
8
8
|
|
|
9
9
|
// #endregion STATE
|
|
10
10
|
|
|
@@ -209,6 +209,8 @@ async function loadProject() {
|
|
|
209
209
|
// Shared by the project picker, the boot restore, and the hub project shim. Throws with the
|
|
210
210
|
// server's error message so callers can surface it directly.
|
|
211
211
|
async function putProject(dirPath) {
|
|
212
|
+
// Recents can only be written under the right key once the config dir is known.
|
|
213
|
+
await projectInfoPromise;
|
|
212
214
|
const res = await fetch('/api/project', {
|
|
213
215
|
method: 'PUT',
|
|
214
216
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -248,10 +250,40 @@ async function submitProjectPicker() {
|
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
const RECENT_PROJECTS_MAX = 20;
|
|
253
|
+
const LEGACY_RECENTS_KEY = 'recentProjects';
|
|
254
|
+
|
|
255
|
+
// Under the hub every config dir's memory app is proxied on one public port, so localStorage is
|
|
256
|
+
// shared between them. Recents are keyed by config dir to keep each one's project separate.
|
|
257
|
+
let claudeConfigDir = '';
|
|
258
|
+
// Retries like the initial load below: in a hub iframe the server may not answer yet.
|
|
259
|
+
const projectInfoPromise = (async () => {
|
|
260
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
261
|
+
try {
|
|
262
|
+
const info = await fetchJSON('/api/project');
|
|
263
|
+
claudeConfigDir = info.configDir || '';
|
|
264
|
+
migrateLegacyRecents();
|
|
265
|
+
return info;
|
|
266
|
+
} catch {
|
|
267
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return {};
|
|
271
|
+
})();
|
|
272
|
+
|
|
273
|
+
function recentsKey() {
|
|
274
|
+
return claudeConfigDir ? `${LEGACY_RECENTS_KEY}::${claudeConfigDir}` : LEGACY_RECENTS_KEY;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function migrateLegacyRecents() {
|
|
278
|
+
const legacy = localStorage.getItem(LEGACY_RECENTS_KEY);
|
|
279
|
+
if (!legacy || !claudeConfigDir) return;
|
|
280
|
+
if (!localStorage.getItem(recentsKey())) localStorage.setItem(recentsKey(), legacy);
|
|
281
|
+
localStorage.removeItem(LEGACY_RECENTS_KEY);
|
|
282
|
+
}
|
|
251
283
|
|
|
252
284
|
function getRecentProjects() {
|
|
253
285
|
try {
|
|
254
|
-
return JSON.parse(localStorage.getItem(
|
|
286
|
+
return JSON.parse(localStorage.getItem(recentsKey()) || '[]');
|
|
255
287
|
} catch {
|
|
256
288
|
return [];
|
|
257
289
|
}
|
|
@@ -260,7 +292,7 @@ function getRecentProjects() {
|
|
|
260
292
|
function addRecentProject(p) {
|
|
261
293
|
const recent = getRecentProjects().filter((r) => r !== p);
|
|
262
294
|
recent.unshift(p);
|
|
263
|
-
localStorage.setItem(
|
|
295
|
+
localStorage.setItem(recentsKey(), JSON.stringify(recent.slice(0, RECENT_PROJECTS_MAX)));
|
|
264
296
|
}
|
|
265
297
|
|
|
266
298
|
// The recents list is the switcher; typing a path is the secondary act behind "+ Add path".
|
|
@@ -330,7 +362,7 @@ function _removeRecentProject(idx, e) {
|
|
|
330
362
|
e.stopPropagation();
|
|
331
363
|
const target = pickerRows[idx];
|
|
332
364
|
if (!target) return;
|
|
333
|
-
localStorage.setItem(
|
|
365
|
+
localStorage.setItem(recentsKey(), JSON.stringify(getRecentProjects().filter((p) => p !== target)));
|
|
334
366
|
renderProjectPicker();
|
|
335
367
|
}
|
|
336
368
|
|
|
@@ -356,6 +388,9 @@ function initProjectPicker() {
|
|
|
356
388
|
e.preventDefault();
|
|
357
389
|
e.stopPropagation();
|
|
358
390
|
});
|
|
391
|
+
const modal = document.getElementById('projectPickerModal');
|
|
392
|
+
onEscapeBlur(filter, modal, () => closeModal('projectPickerModal'));
|
|
393
|
+
onEscapeBlur(document.getElementById('projectPathInput'), modal, () => setPickerMode(false));
|
|
359
394
|
}
|
|
360
395
|
|
|
361
396
|
// #endregion PROJECT
|
|
@@ -366,6 +401,22 @@ function closeModal(id) {
|
|
|
366
401
|
document.getElementById(id).classList.remove('open');
|
|
367
402
|
}
|
|
368
403
|
|
|
404
|
+
// Vimium eats Escape inside a text input and only blurs it, so the page never sees the key.
|
|
405
|
+
// A blur that no click in the modal caused, while the window keeps focus and the input is
|
|
406
|
+
// still shown, is that Escape.
|
|
407
|
+
function onEscapeBlur(input, modal, handler) {
|
|
408
|
+
let pointerDown = false;
|
|
409
|
+
modal.addEventListener('mousedown', () => {
|
|
410
|
+
pointerDown = true;
|
|
411
|
+
});
|
|
412
|
+
document.addEventListener('mouseup', () => {
|
|
413
|
+
pointerDown = false;
|
|
414
|
+
});
|
|
415
|
+
input.addEventListener('blur', () => {
|
|
416
|
+
if (!pointerDown && document.hasFocus() && input.checkVisibility()) handler();
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
369
420
|
// Each entry pairs a left and a right group onto the same grid rows, so their
|
|
370
421
|
// headings sit level and the shorter group just leaves empty rows.
|
|
371
422
|
// `combo` joins the keys with a plus (a chord) instead of listing them as
|
|
@@ -2092,21 +2143,24 @@ document.addEventListener('keydown', (e) => {
|
|
|
2092
2143
|
.catch(() => ({}));
|
|
2093
2144
|
if (!cfg.enabled) return;
|
|
2094
2145
|
window.__HUB__ = cfg;
|
|
2095
|
-
|
|
2146
|
+
// e.code travels with e.key because macOS composes Option+<key> into a character (Option+P is
|
|
2147
|
+
// 'π'), so the key alone cannot identify the binding. The hub owns the keymap and normalizes;
|
|
2148
|
+
// these tests only decide whether a press is the hub's to handle.
|
|
2096
2149
|
document.addEventListener('keydown', (e) => {
|
|
2097
|
-
|
|
2150
|
+
const fwd = () => {
|
|
2098
2151
|
e.preventDefault();
|
|
2099
|
-
|
|
2152
|
+
hubPost({ type: 'hub:keydown', key: e.key, code: e.code, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey });
|
|
2153
|
+
};
|
|
2154
|
+
if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
|
|
2155
|
+
fwd();
|
|
2100
2156
|
}
|
|
2101
2157
|
// Own branch: the Alt+digit case below requires !ctrlKey. The hub owns the Ctrl+Alt+letter
|
|
2102
2158
|
// keymap and ignores unbound letters.
|
|
2103
|
-
if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && /^[a-z]$/i.test(e.key)) {
|
|
2104
|
-
|
|
2105
|
-
fwd(e);
|
|
2159
|
+
if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && (/^[a-z]$/i.test(e.key) || /^Key[A-Z]$/.test(e.code))) {
|
|
2160
|
+
fwd();
|
|
2106
2161
|
}
|
|
2107
|
-
if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && /^[1-9]$/.test(e.key)) {
|
|
2108
|
-
|
|
2109
|
-
fwd(e);
|
|
2162
|
+
if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && (/^[1-9]$/.test(e.key) || /^Digit[1-9]$/.test(e.code))) {
|
|
2163
|
+
fwd();
|
|
2110
2164
|
}
|
|
2111
2165
|
});
|
|
2112
2166
|
})();
|
|
@@ -2248,6 +2302,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
2248
2302
|
let desiredProject = params.get('project');
|
|
2249
2303
|
// A hub-pushed project outranks the localStorage recent: initHub() is fire-and-forget at script
|
|
2250
2304
|
// eval, so hub:project can land before or during this block.
|
|
2305
|
+
await projectInfoPromise;
|
|
2251
2306
|
if (!desiredProject) desiredProject = hubProjectPath || getRecentProjects()[0] || null;
|
|
2252
2307
|
if (desiredProject) {
|
|
2253
2308
|
try {
|
|
Binary file
|
|
Binary file
|
package/public/index.html
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
|
15
15
|
<link rel="manifest" href="/manifest.json">
|
|
16
16
|
<link rel="icon" type="image/svg+xml" href="/icons/icon-svg.svg">
|
|
17
|
+
<!-- Chrome uses the SVG only in the tab strip; app windows, shortcuts and the
|
|
18
|
+
history list need a raster icon or they fall back to /favicon.ico and then to
|
|
19
|
+
whatever icon localhost was last seen with, which is a sibling app's. -->
|
|
20
|
+
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
|
|
21
|
+
<link rel="icon" type="image/png" sizes="512x512" href="/icons/icon-512.png">
|
|
17
22
|
<meta name="theme-color" content="#101114" media="(prefers-color-scheme: dark)">
|
|
18
23
|
<meta name="theme-color" content="#e8e6e3" media="(prefers-color-scheme: light)">
|
|
19
24
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
package/server.js
CHANGED
|
@@ -622,7 +622,7 @@ app.get('/hub-config', (_req, res) => {
|
|
|
622
622
|
});
|
|
623
623
|
|
|
624
624
|
app.get('/api/project', (_req, res) => {
|
|
625
|
-
res.json({ path: currentProjectPath, name: path.basename(currentProjectPath) });
|
|
625
|
+
res.json({ path: currentProjectPath, name: path.basename(currentProjectPath), configDir: CLAUDE_DIR });
|
|
626
626
|
});
|
|
627
627
|
|
|
628
628
|
app.put('/api/project', (req, res) => {
|