@brianmichel/pi-noodle 0.1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/index.ts +1 -0
  4. package/package.json +70 -0
  5. package/src/AGENTS.md +33 -0
  6. package/src/commands/index.ts +51 -0
  7. package/src/commands/memory-crud.ts +136 -0
  8. package/src/commands/review.ts +291 -0
  9. package/src/commands/setup.ts +189 -0
  10. package/src/commands/status.ts +32 -0
  11. package/src/commands/ui.ts +14 -0
  12. package/src/commands/web.ts +40 -0
  13. package/src/commands.ts +1 -0
  14. package/src/config/schema.ts +234 -0
  15. package/src/config-screen.ts +439 -0
  16. package/src/config.ts +159 -0
  17. package/src/constants.ts +1 -0
  18. package/src/debug-overlay.ts +230 -0
  19. package/src/extension.ts +166 -0
  20. package/src/index.ts +1 -0
  21. package/src/memory/backend.ts +22 -0
  22. package/src/memory/embedder.ts +7 -0
  23. package/src/memory/embedders/lm-studio.ts +25 -0
  24. package/src/memory/embedders/openai.ts +66 -0
  25. package/src/memory/extractor.ts +189 -0
  26. package/src/memory/policy.ts +325 -0
  27. package/src/memory/project-identity.ts +51 -0
  28. package/src/memory/runtime.ts +70 -0
  29. package/src/memory/service.ts +761 -0
  30. package/src/memory/turso-backend.ts +716 -0
  31. package/src/memory/types.ts +192 -0
  32. package/src/notifications.ts +11 -0
  33. package/src/queue.ts +42 -0
  34. package/src/session.ts +72 -0
  35. package/src/tools.ts +172 -0
  36. package/src/types.ts +81 -0
  37. package/src/utils.ts +68 -0
  38. package/src/web/dev.ts +7 -0
  39. package/src/web/index.html +1963 -0
  40. package/src/web/manager.ts +92 -0
  41. package/src/web/run.ts +33 -0
  42. package/src/web/server.ts +212 -0
  43. package/tsconfig.json +17 -0
@@ -0,0 +1,1963 @@
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Noodle</title>
7
+ <link
8
+ rel="icon"
9
+ type="image/svg+xml"
10
+ href="data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26.0645 21.6113' fill='%2374A4BC'%3E%3Cpath d='M5.44922 15.8594C7.82227 15.8594 9.27734 14.2773 9.27734 12.2266C9.27734 11.582 9.00391 10.8496 8.62305 10.4785C8.18359 10.0586 7.90039 9.94141 7.90039 9.45312C7.90039 9.05273 8.20312 8.7793 8.66211 8.7793C8.99414 8.7793 9.20898 8.87695 9.56055 9.17969C9.86328 9.44336 10.127 9.77539 10.3125 10.166C11.8164 9.81445 12.3828 8.89648 12.3828 7.42188C12.3828 6.99219 12.7441 6.63086 13.1738 6.63086C13.6035 6.63086 13.9551 6.99219 13.9551 7.42188C13.9648 9.58008 12.9688 11.084 10.7812 11.6406C10.8008 11.8359 10.8203 12.041 10.8203 12.2559C10.8203 14.082 10.0293 15.5566 8.65234 16.4844C9.62891 17.0898 10.8789 17.4316 12.1582 17.4316C12.4023 17.4316 12.6758 17.4121 13.1934 17.3926C13.0859 16.9043 13.0273 16.4453 13.0273 16.0059C13.0273 10.2051 21.25 10.8398 21.25 5.32227C21.25 3.02734 19.0234 1.45508 16.8555 1.45508C16.6602 1.45508 16.3867 1.49414 16.1133 1.55273C15.332 0.585938 14.082 0 12.9102 0C10.8398 0 9.46289 1.34766 9.39453 3.21289C9.375 3.71094 9.04297 3.99414 8.61328 3.99414C8.16406 3.99414 7.82227 3.65234 7.8418 3.14453C7.87109 2.44141 8.01758 1.79688 8.27148 1.23047C7.8125 1.10352 7.35352 1.04492 6.89453 1.04492C4.53125 1.04492 2.76367 2.53906 2.76367 4.36523C2.76367 5.58594 3.67188 6.5918 4.99023 6.5918C5.41992 6.5918 5.77148 6.95312 5.77148 7.37305C5.77148 7.80273 5.41992 8.16406 4.99023 8.16406C3.28125 8.16406 2.03125 7.37305 1.45508 6.13281C0.537109 7.19727 0 8.60352 0 10.0586C0 13.4082 2.1582 15.8594 5.44922 15.8594ZM21.1914 21.6016C23.6328 21.6016 24.8242 18.6328 24.8242 15.7422C24.8242 15.3027 24.8047 14.8926 24.7949 14.5117C23.9355 14.9512 22.8906 15.1465 21.6895 15.0684C21.25 15.0391 20.8984 14.7266 20.8984 14.2871C20.8984 13.8574 21.25 13.4668 21.6895 13.5059C24.0918 13.6914 25.7031 12.3438 25.7031 10.1465C25.7031 8.19336 24.6484 6.5918 22.9492 5.78125C21.709 12.207 14.6875 11.3477 14.4727 15.7227C14.4043 17.1973 15.5273 18.3594 17.1289 18.3594C17.6953 18.3594 17.9785 18.3398 18.2324 18.3301C18.457 19.9902 19.502 21.6016 21.1914 21.6016Z'/%3E%3C/svg%3E"
11
+ />
12
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
13
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
+ <link
15
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=JetBrains+Mono:wght@400;500;600&amp;display=swap"
16
+ rel="stylesheet"
17
+ />
18
+ <style>
19
+ /* ─────────────────────────────────────────────
20
+ TOKENS
21
+ ───────────────────────────────────────────── */
22
+ :root {
23
+ --bg: #f7f7f5;
24
+ --surface: #ffffff;
25
+ --surface-2: #f0f0ee;
26
+ --surface-hover: #ececea;
27
+ --border: #e2e2dd;
28
+ --border-strong: #cdcdc6;
29
+ --text: #161613;
30
+ --text-secondary: #4a4a45;
31
+ --text-muted: #8a8a82;
32
+ --accent: #74a4bc;
33
+ --accent-soft: rgba(116, 164, 188, 0.14);
34
+ --accent-bg: #74a4bc;
35
+ --accent-fg: #0e1418;
36
+ --warning: #9c5500;
37
+ --warning-bg: #f5a623;
38
+ --warning-fg: #1a1206;
39
+ --violet: #6d4ec7;
40
+ --violet-bg: #b78cf5;
41
+ --info: #1c5fb3;
42
+ --info-bg: #6cb8ff;
43
+ --danger: #b8262b;
44
+ --danger-bg: #ef5454;
45
+ --neutral-bg: #d6d6d0;
46
+ --neutral-fg: #2a2a26;
47
+ }
48
+ .dark {
49
+ --bg: #080808;
50
+ --surface: #0d0d0d;
51
+ --surface-2: #121212;
52
+ --surface-hover: #181818;
53
+ --border: #1d1d1d;
54
+ --border-strong: #2c2c2c;
55
+ --text: #e8e8e6;
56
+ --text-secondary: #a3a39d;
57
+ --text-muted: #5a5a55;
58
+ --accent: #74a4bc;
59
+ --accent-soft: rgba(116, 164, 188, 0.14);
60
+ --accent-bg: #2c4452;
61
+ --accent-fg: #d5e6ef;
62
+ --warning: #f5a623;
63
+ --warning-bg: #6b4815;
64
+ --warning-fg: #f5d18a;
65
+ --violet: #b78cf5;
66
+ --violet-bg: #3b2b6b;
67
+ --info: #6cb8ff;
68
+ --info-bg: #1c3c66;
69
+ --danger: #ef5454;
70
+ --danger-bg: #5a1f1f;
71
+ --neutral-bg: #1f1f1d;
72
+ --neutral-fg: #a3a39d;
73
+ }
74
+
75
+ *,
76
+ *::before,
77
+ *::after {
78
+ box-sizing: border-box;
79
+ margin: 0;
80
+ padding: 0;
81
+ }
82
+ html {
83
+ -webkit-font-smoothing: antialiased;
84
+ -moz-osx-font-smoothing: grayscale;
85
+ text-rendering: optimizeLegibility;
86
+ }
87
+ body {
88
+ font-family:
89
+ "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
90
+ background: var(--bg);
91
+ color: var(--text);
92
+ font-size: 13px;
93
+ line-height: 1.5;
94
+ height: 100vh;
95
+ height: 100dvh;
96
+ overflow: hidden;
97
+ }
98
+ ::selection {
99
+ background: var(--accent);
100
+ color: var(--accent-fg);
101
+ }
102
+ button {
103
+ font-family: inherit;
104
+ color: inherit;
105
+ }
106
+ .mono {
107
+ font-family:
108
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
109
+ }
110
+
111
+ /* ─────────────────────────────────────────────
112
+ LAYOUT
113
+ ───────────────────────────────────────────── */
114
+ .shell {
115
+ display: grid;
116
+ grid-template-columns: 232px 1fr;
117
+ height: 100%;
118
+ overflow: hidden;
119
+ }
120
+
121
+ /* ─────────────────────────────────────────────
122
+ SIDEBAR
123
+ ───────────────────────────────────────────── */
124
+ .sidebar {
125
+ background: var(--surface);
126
+ border-right: 1px solid var(--border);
127
+ display: flex;
128
+ flex-direction: column;
129
+ min-height: 0;
130
+ overflow: hidden;
131
+ }
132
+ .sidebar-header {
133
+ padding: 14px 16px;
134
+ border-bottom: 1px solid var(--border);
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: space-between;
138
+ flex-shrink: 0;
139
+ }
140
+ .brand {
141
+ display: inline-flex;
142
+ align-items: center;
143
+ gap: 10px;
144
+ font-family:
145
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
146
+ font-size: 12px;
147
+ font-weight: 600;
148
+ letter-spacing: 0.12em;
149
+ text-transform: uppercase;
150
+ color: var(--text);
151
+ }
152
+ .brand-mark {
153
+ width: 10px;
154
+ height: 10px;
155
+ background: var(--accent);
156
+ flex-shrink: 0;
157
+ }
158
+
159
+ .icon-btn {
160
+ width: 26px;
161
+ height: 26px;
162
+ border: 1px solid var(--border);
163
+ background: transparent;
164
+ color: var(--text-secondary);
165
+ cursor: pointer;
166
+ display: grid;
167
+ place-items: center;
168
+ transition: background 0.1s, color 0.1s, border-color 0.1s;
169
+ }
170
+ .icon-btn:hover {
171
+ background: var(--surface-hover);
172
+ color: var(--text);
173
+ border-color: var(--border-strong);
174
+ }
175
+ .icon-btn svg {
176
+ width: 13px;
177
+ height: 13px;
178
+ }
179
+
180
+ .sidebar-body {
181
+ flex: 1;
182
+ overflow-y: auto;
183
+ min-height: 0;
184
+ }
185
+
186
+ .sidebar-section {
187
+ border-bottom: 1px solid var(--border);
188
+ }
189
+ .sidebar-section:last-child {
190
+ border-bottom: none;
191
+ }
192
+ .sidebar-section-head {
193
+ padding: 12px 16px 6px;
194
+ display: flex;
195
+ align-items: center;
196
+ justify-content: space-between;
197
+ }
198
+ .sidebar-section-title {
199
+ font-family:
200
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
201
+ font-size: 10.5px;
202
+ font-weight: 600;
203
+ letter-spacing: 0.12em;
204
+ text-transform: uppercase;
205
+ color: var(--text-muted);
206
+ }
207
+ .sidebar-section-clear {
208
+ font-family:
209
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
210
+ font-size: 10.5px;
211
+ text-transform: uppercase;
212
+ letter-spacing: 0.08em;
213
+ color: var(--accent);
214
+ background: none;
215
+ border: none;
216
+ cursor: pointer;
217
+ padding: 0;
218
+ }
219
+ .sidebar-section-clear:hover {
220
+ text-decoration: underline;
221
+ }
222
+ .sidebar-section-clear[hidden] {
223
+ display: none;
224
+ }
225
+ .sidebar-section-body {
226
+ padding-bottom: 10px;
227
+ }
228
+
229
+ .nav-item,
230
+ .facet-item {
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 10px;
234
+ width: 100%;
235
+ padding: 6px 16px;
236
+ border: none;
237
+ background: transparent;
238
+ color: var(--text-secondary);
239
+ font: inherit;
240
+ font-size: 13px;
241
+ text-align: left;
242
+ cursor: pointer;
243
+ transition: background 0.1s, color 0.1s;
244
+ position: relative;
245
+ }
246
+ .nav-item:hover,
247
+ .facet-item:hover {
248
+ background: var(--surface-hover);
249
+ color: var(--text);
250
+ }
251
+ .nav-item.active,
252
+ .facet-item.active {
253
+ background: var(--surface-hover);
254
+ color: var(--text);
255
+ }
256
+ .nav-item.active::before,
257
+ .facet-item.active::before {
258
+ content: "";
259
+ position: absolute;
260
+ left: 0;
261
+ top: 0;
262
+ bottom: 0;
263
+ width: 2px;
264
+ background: var(--accent);
265
+ }
266
+ .nav-icon {
267
+ width: 14px;
268
+ height: 14px;
269
+ flex-shrink: 0;
270
+ color: var(--text-muted);
271
+ }
272
+ .nav-item.active .nav-icon {
273
+ color: var(--accent);
274
+ }
275
+ .nav-label,
276
+ .facet-label {
277
+ flex: 1;
278
+ overflow: hidden;
279
+ text-overflow: ellipsis;
280
+ white-space: nowrap;
281
+ }
282
+ .facet-item {
283
+ font-size: 12.5px;
284
+ padding: 4px 16px 4px 16px;
285
+ }
286
+ .facet-label {
287
+ font-family:
288
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
289
+ font-size: 12px;
290
+ }
291
+ .nav-count,
292
+ .facet-count {
293
+ font-family:
294
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
295
+ font-size: 11px;
296
+ color: var(--text-muted);
297
+ font-variant-numeric: tabular-nums;
298
+ }
299
+ .nav-item.active .nav-count,
300
+ .facet-item.active .facet-count {
301
+ color: var(--text-secondary);
302
+ }
303
+
304
+ .sidebar-foot {
305
+ padding: 10px 16px;
306
+ border-top: 1px solid var(--border);
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: space-between;
310
+ gap: 10px;
311
+ flex-shrink: 0;
312
+ font-family:
313
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
314
+ font-size: 10.5px;
315
+ color: var(--text-muted);
316
+ letter-spacing: 0.06em;
317
+ text-transform: uppercase;
318
+ white-space: nowrap;
319
+ line-height: 1;
320
+ }
321
+ .sidebar-foot > * {
322
+ min-width: 0;
323
+ overflow: hidden;
324
+ text-overflow: ellipsis;
325
+ }
326
+ .live {
327
+ display: inline-flex;
328
+ align-items: center;
329
+ gap: 7px;
330
+ min-width: 0;
331
+ }
332
+ .live span:last-child {
333
+ overflow: hidden;
334
+ text-overflow: ellipsis;
335
+ }
336
+ .live-dot {
337
+ width: 6px;
338
+ height: 6px;
339
+ background: var(--text-muted);
340
+ flex-shrink: 0;
341
+ }
342
+ .live-dot.connected {
343
+ background: var(--accent);
344
+ box-shadow: 0 0 0 1px var(--accent-soft);
345
+ animation: pulse-dot 2s ease-in-out infinite;
346
+ }
347
+ @keyframes pulse-dot {
348
+ 0%, 100% {
349
+ opacity: 1;
350
+ }
351
+ 50% {
352
+ opacity: 0.6;
353
+ }
354
+ }
355
+
356
+ /* ─────────────────────────────────────────────
357
+ MAIN
358
+ ───────────────────────────────────────────── */
359
+ .main {
360
+ display: flex;
361
+ flex-direction: column;
362
+ min-width: 0;
363
+ min-height: 0;
364
+ background: var(--bg);
365
+ }
366
+ .topbar {
367
+ display: flex;
368
+ align-items: center;
369
+ gap: 16px;
370
+ padding: 0 20px;
371
+ height: 56px;
372
+ border-bottom: 1px solid var(--border);
373
+ background: var(--surface);
374
+ flex-shrink: 0;
375
+ }
376
+ .topbar-title {
377
+ display: flex;
378
+ align-items: center;
379
+ gap: 10px;
380
+ margin-right: auto;
381
+ min-width: 0;
382
+ }
383
+ .topbar-glyph {
384
+ color: var(--accent);
385
+ font-size: 14px;
386
+ flex-shrink: 0;
387
+ }
388
+ .topbar-title h1 {
389
+ font-size: 16px;
390
+ font-weight: 600;
391
+ letter-spacing: -0.005em;
392
+ color: var(--text);
393
+ }
394
+ .topbar-meta {
395
+ font-family:
396
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
397
+ font-size: 11.5px;
398
+ color: var(--text-muted);
399
+ letter-spacing: 0.04em;
400
+ text-transform: uppercase;
401
+ }
402
+
403
+ .search-wrap {
404
+ position: relative;
405
+ width: 280px;
406
+ max-width: 36vw;
407
+ }
408
+ .search-wrap svg {
409
+ position: absolute;
410
+ left: 9px;
411
+ top: 50%;
412
+ transform: translateY(-50%);
413
+ width: 13px;
414
+ height: 13px;
415
+ color: var(--text-muted);
416
+ pointer-events: none;
417
+ }
418
+ .kbd {
419
+ position: absolute;
420
+ right: 6px;
421
+ top: 50%;
422
+ transform: translateY(-50%);
423
+ font-family:
424
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
425
+ font-size: 10px;
426
+ color: var(--text-muted);
427
+ background: var(--surface-2);
428
+ border: 1px solid var(--border);
429
+ padding: 1px 5px;
430
+ pointer-events: none;
431
+ }
432
+ .input {
433
+ width: 100%;
434
+ height: 30px;
435
+ border: 1px solid var(--border);
436
+ background: var(--bg);
437
+ color: var(--text);
438
+ font: inherit;
439
+ font-size: 12.5px;
440
+ padding: 0 56px 0 28px;
441
+ outline: none;
442
+ transition: border-color 0.12s;
443
+ }
444
+ .input::placeholder {
445
+ color: var(--text-muted);
446
+ }
447
+ .input:focus {
448
+ border-color: var(--accent);
449
+ }
450
+ .input:focus + .kbd {
451
+ opacity: 0;
452
+ }
453
+ .sort-select {
454
+ height: 30px;
455
+ padding: 0 24px 0 10px;
456
+ border: 1px solid var(--border);
457
+ background: var(--bg);
458
+ color: var(--text);
459
+ font: inherit;
460
+ font-family:
461
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
462
+ font-size: 11px;
463
+ text-transform: uppercase;
464
+ letter-spacing: 0.05em;
465
+ cursor: pointer;
466
+ appearance: none;
467
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
468
+ background-repeat: no-repeat;
469
+ background-position: right 8px center;
470
+ }
471
+ .sort-select:focus {
472
+ outline: none;
473
+ border-color: var(--accent);
474
+ }
475
+ .topbar-btn {
476
+ height: 30px;
477
+ padding: 0 10px;
478
+ border: 1px solid var(--border);
479
+ background: var(--bg);
480
+ color: var(--text-secondary);
481
+ font: inherit;
482
+ font-family:
483
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
484
+ font-size: 11px;
485
+ text-transform: uppercase;
486
+ letter-spacing: 0.05em;
487
+ cursor: pointer;
488
+ }
489
+ .topbar-btn:hover {
490
+ color: var(--text);
491
+ border-color: var(--border-strong);
492
+ background: var(--surface-hover);
493
+ }
494
+
495
+ /* Filter bar */
496
+ .filter-bar {
497
+ display: flex;
498
+ align-items: center;
499
+ gap: 6px;
500
+ padding: 8px 20px;
501
+ border-bottom: 1px solid var(--border);
502
+ background: var(--surface);
503
+ flex-shrink: 0;
504
+ min-height: 36px;
505
+ flex-wrap: wrap;
506
+ }
507
+ .filter-bar-label {
508
+ font-family:
509
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
510
+ font-size: 10.5px;
511
+ color: var(--text-muted);
512
+ letter-spacing: 0.08em;
513
+ text-transform: uppercase;
514
+ margin-right: 4px;
515
+ }
516
+ .filter-chip {
517
+ display: inline-flex;
518
+ align-items: center;
519
+ gap: 4px;
520
+ padding: 2px 3px 2px 7px;
521
+ background: var(--bg);
522
+ border: 1px solid var(--border-strong);
523
+ font-family:
524
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
525
+ font-size: 11px;
526
+ color: var(--text);
527
+ }
528
+ .filter-chip-key {
529
+ color: var(--text-muted);
530
+ text-transform: uppercase;
531
+ letter-spacing: 0.04em;
532
+ font-size: 10.5px;
533
+ }
534
+ .filter-chip-x {
535
+ width: 16px;
536
+ height: 16px;
537
+ background: transparent;
538
+ border: none;
539
+ cursor: pointer;
540
+ color: var(--text-muted);
541
+ display: grid;
542
+ place-items: center;
543
+ padding: 0;
544
+ }
545
+ .filter-chip-x:hover {
546
+ background: var(--surface-hover);
547
+ color: var(--text);
548
+ }
549
+ .filter-chip-x svg {
550
+ width: 9px;
551
+ height: 9px;
552
+ }
553
+ .filter-bar-empty {
554
+ font-family:
555
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
556
+ font-size: 11px;
557
+ color: var(--text-muted);
558
+ letter-spacing: 0.04em;
559
+ text-transform: uppercase;
560
+ }
561
+ .filter-clear-all {
562
+ margin-left: auto;
563
+ font-family:
564
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
565
+ font-size: 10.5px;
566
+ text-transform: uppercase;
567
+ letter-spacing: 0.08em;
568
+ color: var(--text-muted);
569
+ background: none;
570
+ border: none;
571
+ cursor: pointer;
572
+ padding: 2px 4px;
573
+ }
574
+ .filter-clear-all:hover {
575
+ color: var(--text);
576
+ }
577
+
578
+ /* ─────────────────────────────────────────────
579
+ CONTENT
580
+ ───────────────────────────────────────────── */
581
+ .content {
582
+ flex: 1;
583
+ overflow: auto;
584
+ min-height: 0;
585
+ }
586
+ .view-panel {
587
+ display: none;
588
+ }
589
+ .view-panel.active {
590
+ display: block;
591
+ }
592
+
593
+ /* ─────────────────────────────────────────────
594
+ DATA TABLE
595
+ ───────────────────────────────────────────── */
596
+ .data-table {
597
+ width: 100%;
598
+ border-collapse: separate;
599
+ border-spacing: 0;
600
+ font-size: 13px;
601
+ }
602
+ .data-table thead th {
603
+ position: sticky;
604
+ top: 0;
605
+ z-index: 2;
606
+ text-align: left;
607
+ padding: 9px 14px;
608
+ font-family:
609
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
610
+ font-size: 10.5px;
611
+ font-weight: 500;
612
+ color: var(--text-muted);
613
+ letter-spacing: 0.1em;
614
+ text-transform: uppercase;
615
+ background: var(--surface);
616
+ border-bottom: 1px solid var(--border-strong);
617
+ border-right: 1px solid var(--border);
618
+ white-space: nowrap;
619
+ user-select: none;
620
+ }
621
+ .data-table thead th:last-child {
622
+ border-right: none;
623
+ }
624
+ .data-table thead th.num {
625
+ text-align: right;
626
+ }
627
+ .data-table tbody td {
628
+ padding: 11px 14px;
629
+ border-bottom: 1px solid var(--border);
630
+ border-right: 1px solid var(--border);
631
+ vertical-align: middle;
632
+ color: var(--text);
633
+ }
634
+ .data-table tbody td:last-child {
635
+ border-right: none;
636
+ }
637
+ .data-table tbody tr {
638
+ position: relative;
639
+ }
640
+ .data-table tbody tr.row:hover td {
641
+ background: var(--surface);
642
+ }
643
+ .data-table tbody tr.row.expanded > td {
644
+ background: var(--surface);
645
+ }
646
+ .data-table tbody tr.row.expanded > td:first-child {
647
+ box-shadow: inset 2px 0 0 var(--accent);
648
+ }
649
+
650
+ .col-expand {
651
+ width: 28px;
652
+ padding: 0 !important;
653
+ text-align: center;
654
+ cursor: pointer;
655
+ }
656
+ .caret {
657
+ width: 28px;
658
+ height: 100%;
659
+ min-height: 36px;
660
+ display: grid;
661
+ place-items: center;
662
+ background: none;
663
+ border: none;
664
+ cursor: pointer;
665
+ color: var(--text-muted);
666
+ transition: color 0.12s;
667
+ }
668
+ .caret:hover {
669
+ color: var(--text);
670
+ }
671
+ .caret svg {
672
+ width: 9px;
673
+ height: 9px;
674
+ transition: transform 0.18s ease;
675
+ }
676
+ tr.row.expanded .caret svg {
677
+ transform: rotate(90deg);
678
+ color: var(--accent);
679
+ }
680
+ tr.row.expanded .caret {
681
+ color: var(--accent);
682
+ }
683
+
684
+ .col-memory {
685
+ max-width: 0; /* let table layout do its thing */
686
+ }
687
+ .row-text {
688
+ display: -webkit-box;
689
+ -webkit-line-clamp: 2;
690
+ -webkit-box-orient: vertical;
691
+ overflow: hidden;
692
+ line-height: 1.45;
693
+ color: var(--text);
694
+ }
695
+ tr.row.expanded .row-text {
696
+ display: block;
697
+ -webkit-line-clamp: unset;
698
+ white-space: pre-wrap;
699
+ word-break: break-word;
700
+ }
701
+
702
+ .col-source,
703
+ .col-agent {
704
+ width: 130px;
705
+ }
706
+ .col-num {
707
+ width: 80px;
708
+ text-align: right;
709
+ }
710
+ .col-created {
711
+ width: 110px;
712
+ }
713
+
714
+ /* Source pill — solid block, mono uppercase */
715
+ .pill {
716
+ display: inline-flex;
717
+ align-items: center;
718
+ height: 18px;
719
+ padding: 0 6px;
720
+ background: var(--neutral-bg);
721
+ color: var(--neutral-fg);
722
+ font-family:
723
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
724
+ font-size: 10.5px;
725
+ font-weight: 500;
726
+ letter-spacing: 0.05em;
727
+ text-transform: uppercase;
728
+ white-space: nowrap;
729
+ }
730
+ .pill.explicit {
731
+ background: var(--accent-bg);
732
+ color: var(--accent-fg);
733
+ }
734
+ .pill.heuristic {
735
+ background: var(--warning-bg);
736
+ color: var(--warning-fg);
737
+ }
738
+ .pill.repetition {
739
+ background: var(--info-bg);
740
+ color: var(--info-fg, var(--info));
741
+ }
742
+ .dark .pill.repetition {
743
+ color: #d4ebff;
744
+ }
745
+ .pill.llm_extracted {
746
+ background: var(--violet-bg);
747
+ color: #ece1ff;
748
+ }
749
+ .pill.consolidated {
750
+ background: var(--accent-soft);
751
+ color: var(--accent);
752
+ }
753
+
754
+ .agent-text {
755
+ font-family:
756
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
757
+ font-size: 12px;
758
+ color: var(--text);
759
+ overflow: hidden;
760
+ text-overflow: ellipsis;
761
+ white-space: nowrap;
762
+ display: inline-block;
763
+ max-width: 110px;
764
+ vertical-align: middle;
765
+ }
766
+ .num-cell {
767
+ font-family:
768
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
769
+ font-size: 12.5px;
770
+ color: var(--text);
771
+ font-variant-numeric: tabular-nums;
772
+ }
773
+ .num-cell.zero {
774
+ color: var(--text-muted);
775
+ }
776
+ .num-cell.hot {
777
+ color: var(--warning);
778
+ }
779
+ .time-cell {
780
+ font-family:
781
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
782
+ font-size: 11.5px;
783
+ color: var(--text-secondary);
784
+ font-variant-numeric: tabular-nums;
785
+ }
786
+
787
+ /* ─────────────────────────────────────────────
788
+ DETAIL ROW
789
+ ───────────────────────────────────────────── */
790
+ tr.row-detail > td {
791
+ padding: 0 !important;
792
+ background: var(--surface) !important;
793
+ border-bottom: 1px solid var(--border-strong);
794
+ border-right: none !important;
795
+ box-shadow: inset 2px 0 0 var(--accent);
796
+ }
797
+ .detail {
798
+ padding: 16px 20px 20px;
799
+ display: grid;
800
+ grid-template-columns: minmax(0, 1fr) 320px;
801
+ gap: 28px;
802
+ }
803
+ .detail-section-title {
804
+ font-family:
805
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
806
+ font-size: 10px;
807
+ font-weight: 600;
808
+ letter-spacing: 0.12em;
809
+ text-transform: uppercase;
810
+ color: var(--text-muted);
811
+ margin-bottom: 8px;
812
+ }
813
+ .detail-text {
814
+ font-size: 13.5px;
815
+ line-height: 1.6;
816
+ white-space: pre-wrap;
817
+ word-break: break-word;
818
+ color: var(--text);
819
+ margin-bottom: 18px;
820
+ }
821
+ .detail-categories {
822
+ display: flex;
823
+ flex-wrap: wrap;
824
+ gap: 4px;
825
+ margin-bottom: 18px;
826
+ }
827
+ .cat-tag {
828
+ font-family:
829
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
830
+ font-size: 11px;
831
+ padding: 1px 6px;
832
+ background: var(--surface-2);
833
+ border: 1px solid var(--border);
834
+ color: var(--text-secondary);
835
+ cursor: pointer;
836
+ }
837
+ .cat-tag:hover {
838
+ background: var(--surface-hover);
839
+ color: var(--text);
840
+ border-color: var(--border-strong);
841
+ }
842
+
843
+ .detail-side {
844
+ border-left: 1px solid var(--border);
845
+ padding-left: 24px;
846
+ }
847
+ .kv-grid {
848
+ display: grid;
849
+ grid-template-columns: max-content 1fr;
850
+ gap: 5px 14px;
851
+ font-size: 11.5px;
852
+ margin-bottom: 18px;
853
+ }
854
+ .kv-grid dt {
855
+ font-family:
856
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
857
+ color: var(--text-muted);
858
+ letter-spacing: 0.04em;
859
+ text-transform: lowercase;
860
+ }
861
+ .kv-grid dd {
862
+ font-family:
863
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
864
+ color: var(--text);
865
+ word-break: break-word;
866
+ overflow-wrap: anywhere;
867
+ }
868
+ .kv-grid dd .copy-id {
869
+ background: none;
870
+ border: none;
871
+ color: var(--accent);
872
+ cursor: pointer;
873
+ padding: 0 0 0 6px;
874
+ font: inherit;
875
+ font-size: 10.5px;
876
+ text-transform: uppercase;
877
+ letter-spacing: 0.06em;
878
+ opacity: 0.7;
879
+ }
880
+ .kv-grid dd .copy-id:hover {
881
+ opacity: 1;
882
+ }
883
+
884
+ .meter {
885
+ display: inline-flex;
886
+ align-items: center;
887
+ gap: 6px;
888
+ font-family:
889
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
890
+ font-size: 11px;
891
+ color: var(--text-secondary);
892
+ }
893
+ .meter-track {
894
+ width: 56px;
895
+ height: 4px;
896
+ background: var(--surface-2);
897
+ border: 1px solid var(--border);
898
+ position: relative;
899
+ }
900
+ .meter-fill {
901
+ position: absolute;
902
+ inset: 0 auto 0 0;
903
+ background: var(--accent);
904
+ }
905
+ .meter-fill.low {
906
+ background: var(--warning);
907
+ }
908
+
909
+ .json-block {
910
+ font-family:
911
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
912
+ font-size: 11px;
913
+ line-height: 1.55;
914
+ padding: 10px 12px;
915
+ background: var(--bg);
916
+ border: 1px solid var(--border);
917
+ color: var(--text-secondary);
918
+ white-space: pre-wrap;
919
+ word-break: break-word;
920
+ max-height: 240px;
921
+ overflow: auto;
922
+ }
923
+
924
+ /* Empty / error */
925
+ .empty {
926
+ padding: 80px 24px;
927
+ text-align: center;
928
+ color: var(--text-muted);
929
+ font-size: 13px;
930
+ }
931
+ .empty strong {
932
+ display: block;
933
+ color: var(--text);
934
+ font-size: 14px;
935
+ font-weight: 600;
936
+ margin-bottom: 6px;
937
+ font-family:
938
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
939
+ letter-spacing: 0.06em;
940
+ text-transform: uppercase;
941
+ }
942
+ .error {
943
+ padding: 40px 24px;
944
+ text-align: center;
945
+ color: var(--danger);
946
+ font-family:
947
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
948
+ font-size: 11.5px;
949
+ text-transform: uppercase;
950
+ letter-spacing: 0.08em;
951
+ border-bottom: 1px solid var(--border);
952
+ }
953
+
954
+ /* Skeleton */
955
+ .skel {
956
+ background: var(--surface-hover);
957
+ animation: pulse 1.5s ease-in-out infinite;
958
+ }
959
+ @keyframes pulse {
960
+ 0%, 100% { opacity: 1; }
961
+ 50% { opacity: 0.5; }
962
+ }
963
+
964
+ /* Entities table — reuse data-table styles */
965
+
966
+ /* Responsive */
967
+ @media (max-width: 960px) {
968
+ .shell {
969
+ grid-template-columns: 1fr;
970
+ }
971
+ .sidebar {
972
+ display: none;
973
+ }
974
+ .col-source,
975
+ .col-agent {
976
+ display: none;
977
+ }
978
+ .detail {
979
+ grid-template-columns: 1fr;
980
+ }
981
+ .detail-side {
982
+ border-left: none;
983
+ border-top: 1px solid var(--border);
984
+ padding-left: 0;
985
+ padding-top: 16px;
986
+ }
987
+ }
988
+ </style>
989
+ </head>
990
+ <body>
991
+ <div class="shell">
992
+ <aside class="sidebar">
993
+ <div class="sidebar-header">
994
+ <span class="brand">
995
+ <span class="brand-mark"></span>
996
+ Noodle
997
+ </span>
998
+ <button
999
+ class="icon-btn"
1000
+ id="theme-toggle"
1001
+ type="button"
1002
+ aria-label="Toggle theme"
1003
+ title="Toggle theme"
1004
+ >
1005
+ <svg
1006
+ id="icon-moon"
1007
+ xmlns="http://www.w3.org/2000/svg"
1008
+ viewBox="0 0 24 24"
1009
+ fill="none"
1010
+ stroke="currentColor"
1011
+ stroke-width="2"
1012
+ stroke-linecap="round"
1013
+ stroke-linejoin="round"
1014
+ >
1015
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
1016
+ </svg>
1017
+ <svg
1018
+ id="icon-sun"
1019
+ xmlns="http://www.w3.org/2000/svg"
1020
+ viewBox="0 0 24 24"
1021
+ fill="none"
1022
+ stroke="currentColor"
1023
+ stroke-width="2"
1024
+ stroke-linecap="round"
1025
+ stroke-linejoin="round"
1026
+ style="display: none"
1027
+ >
1028
+ <circle cx="12" cy="12" r="4" />
1029
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
1030
+ </svg>
1031
+ </button>
1032
+ </div>
1033
+
1034
+ <div class="sidebar-body">
1035
+ <div class="sidebar-section">
1036
+ <div class="sidebar-section-head">
1037
+ <span class="sidebar-section-title">View</span>
1038
+ </div>
1039
+ <div class="sidebar-section-body">
1040
+ <button type="button" class="nav-item active" data-view="memories">
1041
+ <svg
1042
+ class="nav-icon"
1043
+ xmlns="http://www.w3.org/2000/svg"
1044
+ viewBox="0 0 24 24"
1045
+ fill="none"
1046
+ stroke="currentColor"
1047
+ stroke-width="1.75"
1048
+ stroke-linecap="round"
1049
+ stroke-linejoin="round"
1050
+ >
1051
+ <rect x="3" y="3" width="7" height="7" />
1052
+ <rect x="14" y="3" width="7" height="7" />
1053
+ <rect x="3" y="14" width="7" height="7" />
1054
+ <rect x="14" y="14" width="7" height="7" />
1055
+ </svg>
1056
+ <span class="nav-label">Memories</span>
1057
+ <span class="nav-count" id="nav-count-memories">—</span>
1058
+ </button>
1059
+ <button type="button" class="nav-item" data-view="entities">
1060
+ <svg
1061
+ class="nav-icon"
1062
+ xmlns="http://www.w3.org/2000/svg"
1063
+ viewBox="0 0 24 24"
1064
+ fill="none"
1065
+ stroke="currentColor"
1066
+ stroke-width="1.75"
1067
+ stroke-linecap="round"
1068
+ stroke-linejoin="round"
1069
+ >
1070
+ <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
1071
+ <circle cx="9" cy="7" r="4" />
1072
+ <path d="M22 21v-2a4 4 0 0 0-3-3.87" />
1073
+ <path d="M16 3.13a4 4 0 0 1 0 7.75" />
1074
+ </svg>
1075
+ <span class="nav-label">Entities</span>
1076
+ <span class="nav-count" id="nav-count-entities">—</span>
1077
+ </button>
1078
+ </div>
1079
+ </div>
1080
+
1081
+ <div class="sidebar-section">
1082
+ <div class="sidebar-section-head">
1083
+ <span class="sidebar-section-title">Category</span>
1084
+ <button
1085
+ type="button"
1086
+ class="sidebar-section-clear"
1087
+ data-clear="categories"
1088
+ hidden
1089
+ >
1090
+ Clear
1091
+ </button>
1092
+ </div>
1093
+ <div class="sidebar-section-body" id="facet-categories"></div>
1094
+ </div>
1095
+
1096
+ <div class="sidebar-section">
1097
+ <div class="sidebar-section-head">
1098
+ <span class="sidebar-section-title">Source</span>
1099
+ <button
1100
+ type="button"
1101
+ class="sidebar-section-clear"
1102
+ data-clear="sources"
1103
+ hidden
1104
+ >
1105
+ Clear
1106
+ </button>
1107
+ </div>
1108
+ <div class="sidebar-section-body" id="facet-sources"></div>
1109
+ </div>
1110
+
1111
+ <div class="sidebar-section">
1112
+ <div class="sidebar-section-head">
1113
+ <span class="sidebar-section-title">Agent</span>
1114
+ <button
1115
+ type="button"
1116
+ class="sidebar-section-clear"
1117
+ data-clear="agents"
1118
+ hidden
1119
+ >
1120
+ Clear
1121
+ </button>
1122
+ </div>
1123
+ <div class="sidebar-section-body" id="facet-agents"></div>
1124
+ </div>
1125
+ </div>
1126
+
1127
+ <div class="sidebar-foot">
1128
+ <span
1129
+ class="live"
1130
+ id="live-status"
1131
+ title="Live link to the local explorer server. Drops when the server stops or the connection is interrupted."
1132
+ >
1133
+ <span class="live-dot" id="live-dot"></span>
1134
+ <span id="live-label">Connecting</span>
1135
+ </span>
1136
+ <span id="last-updated" title="Last synced"></span>
1137
+ </div>
1138
+ </aside>
1139
+
1140
+ <main class="main">
1141
+ <div class="topbar">
1142
+ <div class="topbar-title">
1143
+ <span class="topbar-glyph">▸</span>
1144
+ <h1 id="page-title">Memories</h1>
1145
+ <span class="topbar-meta" id="result-label"></span>
1146
+ </div>
1147
+ <div class="search-wrap">
1148
+ <svg
1149
+ xmlns="http://www.w3.org/2000/svg"
1150
+ viewBox="0 0 24 24"
1151
+ fill="none"
1152
+ stroke="currentColor"
1153
+ stroke-width="2"
1154
+ stroke-linecap="round"
1155
+ stroke-linejoin="round"
1156
+ >
1157
+ <circle cx="11" cy="11" r="8" />
1158
+ <path d="m21 21-4.3-4.3" />
1159
+ </svg>
1160
+ <input
1161
+ type="text"
1162
+ id="search"
1163
+ class="input"
1164
+ placeholder="Filter by text, scope, source…"
1165
+ autocomplete="off"
1166
+ spellcheck="false"
1167
+ />
1168
+ <span class="kbd">/</span>
1169
+ </div>
1170
+ <button type="button" id="review-auto-saved" class="topbar-btn">Review auto-saved</button>
1171
+ <select id="sort" class="sort-select" aria-label="Sort">
1172
+ <option value="recent">Newest</option>
1173
+ <option value="oldest">Oldest</option>
1174
+ <option value="retrieved">Most retrieved</option>
1175
+ <option value="lastRetrieved">Recently retrieved</option>
1176
+ </select>
1177
+ </div>
1178
+
1179
+ <div class="filter-bar" id="filter-bar">
1180
+ <span class="filter-bar-empty" id="filter-bar-empty">
1181
+ No filters applied
1182
+ </span>
1183
+ </div>
1184
+
1185
+ <div class="content">
1186
+ <div id="view-memories" class="view-panel active">
1187
+ <table class="data-table" id="memories-table">
1188
+ <thead>
1189
+ <tr>
1190
+ <th class="col-expand"></th>
1191
+ <th>Memory</th>
1192
+ <th class="col-source">Source</th>
1193
+ <th class="col-agent">Agent</th>
1194
+ <th class="col-num num">Retrievals</th>
1195
+ <th class="col-created">Created</th>
1196
+ </tr>
1197
+ </thead>
1198
+ <tbody id="memories"></tbody>
1199
+ </table>
1200
+ </div>
1201
+
1202
+ <div id="view-entities" class="view-panel">
1203
+ <table class="data-table">
1204
+ <thead>
1205
+ <tr>
1206
+ <th>Agent</th>
1207
+ <th>User</th>
1208
+ <th>Session</th>
1209
+ <th class="col-num num">Memories</th>
1210
+ <th class="col-num num">Retrievals</th>
1211
+ </tr>
1212
+ </thead>
1213
+ <tbody id="entities"></tbody>
1214
+ </table>
1215
+ </div>
1216
+ </div>
1217
+ </main>
1218
+ </div>
1219
+
1220
+ <script>
1221
+ // ─────────────────────────────────────────────
1222
+ // State
1223
+ // ─────────────────────────────────────────────
1224
+ const state = {
1225
+ memories: [],
1226
+ activeView: "memories",
1227
+ query: "",
1228
+ sort: "recent",
1229
+ selected: {
1230
+ categories: new Set(),
1231
+ sources: new Set(),
1232
+ agents: new Set(),
1233
+ },
1234
+ expanded: new Set(),
1235
+ };
1236
+
1237
+ let ws,
1238
+ pingTimer,
1239
+ shuttingDown = false;
1240
+
1241
+ // ─────────────────────────────────────────────
1242
+ // WebSocket
1243
+ // ─────────────────────────────────────────────
1244
+ function setLive(connected) {
1245
+ const dot = document.getElementById("live-dot");
1246
+ const label = document.getElementById("live-label");
1247
+ dot.classList.toggle("connected", !!connected);
1248
+ label.textContent = connected ? "Connected" : "Reconnecting";
1249
+ }
1250
+ function disconnect() {
1251
+ shuttingDown = true;
1252
+ if (pingTimer) clearInterval(pingTimer);
1253
+ pingTimer = null;
1254
+ if (ws && ws.readyState === WebSocket.OPEN) {
1255
+ try {
1256
+ ws.send(JSON.stringify({ type: "bye" }));
1257
+ } catch (_) {}
1258
+ ws.close(1000, "tab closed");
1259
+ }
1260
+ }
1261
+ function connect() {
1262
+ if (shuttingDown) return;
1263
+ ws = new WebSocket("ws://" + location.host);
1264
+ ws.onopen = () => {
1265
+ setLive(true);
1266
+ if (pingTimer) clearInterval(pingTimer);
1267
+ pingTimer = setInterval(() => {
1268
+ if (ws.readyState === WebSocket.OPEN)
1269
+ ws.send(JSON.stringify({ type: "ping" }));
1270
+ }, 4000);
1271
+ };
1272
+ ws.onmessage = (e) => {
1273
+ try {
1274
+ const msg = JSON.parse(e.data);
1275
+ if (msg.type === "reload") location.reload();
1276
+ } catch (_) {}
1277
+ };
1278
+ ws.onclose = () => {
1279
+ setLive(false);
1280
+ if (pingTimer) clearInterval(pingTimer);
1281
+ pingTimer = null;
1282
+ if (!shuttingDown && document.visibilityState !== "hidden")
1283
+ setTimeout(connect, 1000);
1284
+ };
1285
+ }
1286
+ connect();
1287
+ window.addEventListener("pagehide", disconnect);
1288
+ window.addEventListener("beforeunload", disconnect);
1289
+
1290
+ // ─────────────────────────────────────────────
1291
+ // Theme
1292
+ // ─────────────────────────────────────────────
1293
+ function applyTheme(dark) {
1294
+ document.documentElement.classList.toggle("dark", dark);
1295
+ document.getElementById("icon-moon").style.display = dark
1296
+ ? "block"
1297
+ : "none";
1298
+ document.getElementById("icon-sun").style.display = dark
1299
+ ? "none"
1300
+ : "block";
1301
+ }
1302
+ document.getElementById("theme-toggle").addEventListener("click", () => {
1303
+ const next = !document.documentElement.classList.contains("dark");
1304
+ applyTheme(next);
1305
+ localStorage.setItem("theme", next ? "dark" : "light");
1306
+ });
1307
+ if (localStorage.getItem("theme") === "light") applyTheme(false);
1308
+
1309
+ // ─────────────────────────────────────────────
1310
+ // Utilities
1311
+ // ─────────────────────────────────────────────
1312
+ function escapeHtml(t) {
1313
+ return String(t == null ? "" : t).replace(/[<>&"']/g, (c) => ({
1314
+ "<": "&lt;",
1315
+ ">": "&gt;",
1316
+ "&": "&amp;",
1317
+ '"': "&quot;",
1318
+ "'": "&#39;",
1319
+ })[c]);
1320
+ }
1321
+ function formatRelative(value) {
1322
+ if (!value) return "";
1323
+ const diff = (Date.now() - new Date(value).getTime()) / 1000;
1324
+ if (diff < 60) return "now";
1325
+ if (diff < 3600) return Math.floor(diff / 60) + "m";
1326
+ if (diff < 86400) return Math.floor(diff / 3600) + "h";
1327
+ if (diff < 86400 * 30) return Math.floor(diff / 86400) + "d";
1328
+ if (diff < 86400 * 365)
1329
+ return Math.floor(diff / (86400 * 30)) + "mo";
1330
+ return Math.floor(diff / (86400 * 365)) + "y";
1331
+ }
1332
+ function formatAbsolute(value) {
1333
+ if (!value) return "";
1334
+ return new Date(value).toLocaleString(undefined, {
1335
+ year: "numeric",
1336
+ month: "short",
1337
+ day: "numeric",
1338
+ hour: "numeric",
1339
+ minute: "2-digit",
1340
+ });
1341
+ }
1342
+ function agentOf(m) {
1343
+ return (m.scope && m.scope.assistantId) || "default";
1344
+ }
1345
+ function sourceOf(m) {
1346
+ return (m.metadata && m.metadata.source) || "unknown";
1347
+ }
1348
+ function confidenceOf(m) {
1349
+ const v = m.metadata && m.metadata.confidence;
1350
+ return typeof v === "number" ? v : null;
1351
+ }
1352
+
1353
+ // ─────────────────────────────────────────────
1354
+ // Data load
1355
+ // ─────────────────────────────────────────────
1356
+ function showSkeleton() {
1357
+ const host = document.getElementById("memories");
1358
+ const row = `
1359
+ <tr class="row">
1360
+ <td class="col-expand"></td>
1361
+ <td><div class="skel" style="height:12px;width:80%"></div><div class="skel" style="height:10px;width:60%;margin-top:6px"></div></td>
1362
+ <td class="col-source"><div class="skel" style="height:14px;width:60px"></div></td>
1363
+ <td class="col-agent"><div class="skel" style="height:12px;width:80px"></div></td>
1364
+ <td class="col-num num"><div class="skel" style="height:12px;width:24px;margin-left:auto"></div></td>
1365
+ <td class="col-created"><div class="skel" style="height:12px;width:48px"></div></td>
1366
+ </tr>`;
1367
+ host.innerHTML = row.repeat(5);
1368
+ }
1369
+
1370
+ async function load() {
1371
+ showSkeleton();
1372
+ try {
1373
+ const r = await fetch("/api/memories");
1374
+ if (!r.ok) throw new Error("HTTP " + r.status);
1375
+ const data = await r.json();
1376
+ if (!Array.isArray(data)) throw new Error("Invalid response");
1377
+ state.memories = data;
1378
+ document.getElementById("last-updated").textContent =
1379
+ new Date().toLocaleTimeString(undefined, {
1380
+ hour: "2-digit",
1381
+ minute: "2-digit",
1382
+ second: "2-digit",
1383
+ hour12: false,
1384
+ });
1385
+ renderFacets();
1386
+ renderAll();
1387
+ } catch (err) {
1388
+ console.error("Failed to load memories:", err);
1389
+ document.getElementById("memories").innerHTML =
1390
+ '<tr><td colspan="6"><div class="error">Failed to load · check that the database is accessible</div></td></tr>';
1391
+ document.getElementById("result-label").textContent = "Load error";
1392
+ }
1393
+ }
1394
+
1395
+ // ─────────────────────────────────────────────
1396
+ // Filter / sort
1397
+ // ─────────────────────────────────────────────
1398
+ function applyFilters(list) {
1399
+ const q = state.query.toLowerCase().trim();
1400
+ return list.filter((m) => {
1401
+ if (q) {
1402
+ const hay = [
1403
+ m.text,
1404
+ ...(m.categories || []),
1405
+ agentOf(m),
1406
+ sourceOf(m),
1407
+ m.scope && m.scope.userId,
1408
+ m.scope && m.scope.sessionId,
1409
+ ]
1410
+ .filter(Boolean)
1411
+ .join(" ")
1412
+ .toLowerCase();
1413
+ if (!hay.includes(q)) return false;
1414
+ }
1415
+ if (state.selected.categories.size) {
1416
+ const cats = m.categories || [];
1417
+ let hit = false;
1418
+ for (const c of cats)
1419
+ if (state.selected.categories.has(c)) {
1420
+ hit = true;
1421
+ break;
1422
+ }
1423
+ if (!hit) return false;
1424
+ }
1425
+ if (
1426
+ state.selected.sources.size &&
1427
+ !state.selected.sources.has(sourceOf(m))
1428
+ )
1429
+ return false;
1430
+ if (
1431
+ state.selected.agents.size &&
1432
+ !state.selected.agents.has(agentOf(m))
1433
+ )
1434
+ return false;
1435
+ return true;
1436
+ });
1437
+ }
1438
+ function sortMemories(list) {
1439
+ const arr = [...list];
1440
+ switch (state.sort) {
1441
+ case "oldest":
1442
+ arr.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
1443
+ break;
1444
+ case "retrieved":
1445
+ arr.sort(
1446
+ (a, b) => (b.retrievalCount || 0) - (a.retrievalCount || 0),
1447
+ );
1448
+ break;
1449
+ case "lastRetrieved":
1450
+ arr.sort(
1451
+ (a, b) => (b.lastRetrieved || 0) - (a.lastRetrieved || 0),
1452
+ );
1453
+ break;
1454
+ default:
1455
+ arr.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
1456
+ }
1457
+ return arr;
1458
+ }
1459
+
1460
+ // ─────────────────────────────────────────────
1461
+ // Facets
1462
+ // ─────────────────────────────────────────────
1463
+ function countBy(items, getter) {
1464
+ const map = new Map();
1465
+ items.forEach((it) => {
1466
+ const vs = getter(it);
1467
+ (Array.isArray(vs) ? vs : [vs]).forEach((v) => {
1468
+ if (!v) return;
1469
+ map.set(v, (map.get(v) || 0) + 1);
1470
+ });
1471
+ });
1472
+ return Array.from(map.entries()).sort((a, b) => b[1] - a[1]);
1473
+ }
1474
+ function renderFacets() {
1475
+ document.getElementById("nav-count-memories").textContent =
1476
+ state.memories.length;
1477
+ document.getElementById("nav-count-entities").textContent =
1478
+ computeEntities().length;
1479
+
1480
+ const cats = countBy(state.memories, (m) => m.categories || []);
1481
+ const srcs = countBy(state.memories, (m) => sourceOf(m));
1482
+ const agents = countBy(state.memories, (m) => agentOf(m));
1483
+
1484
+ document.getElementById("facet-categories").innerHTML =
1485
+ renderFacetList(cats, "categories");
1486
+ document.getElementById("facet-sources").innerHTML = renderFacetList(
1487
+ srcs,
1488
+ "sources",
1489
+ );
1490
+ document.getElementById("facet-agents").innerHTML = renderFacetList(
1491
+ agents,
1492
+ "agents",
1493
+ );
1494
+ document.querySelectorAll(".sidebar-section-clear").forEach((btn) => {
1495
+ btn.hidden = !state.selected[btn.getAttribute("data-clear")].size;
1496
+ });
1497
+ }
1498
+ function renderFacetList(entries, group) {
1499
+ if (!entries.length)
1500
+ return `<div style="padding:4px 16px;font-family:var(--mono);font-size:11px;color:var(--text-muted)">—</div>`;
1501
+ return entries
1502
+ .map(([key, count]) => {
1503
+ const active = state.selected[group].has(key);
1504
+ return `<button type="button" class="facet-item${active ? " active" : ""}" data-facet="${group}" data-key="${escapeHtml(key)}">
1505
+ <span class="facet-label">${escapeHtml(key)}</span>
1506
+ <span class="facet-count">${count}</span>
1507
+ </button>`;
1508
+ })
1509
+ .join("");
1510
+ }
1511
+ function toggleFacet(group, key) {
1512
+ const set = state.selected[group];
1513
+ if (set.has(key)) set.delete(key);
1514
+ else set.add(key);
1515
+ renderAll();
1516
+ renderFacets();
1517
+ }
1518
+ document
1519
+ .getElementById("facet-categories")
1520
+ .addEventListener("click", onFacetClick);
1521
+ document
1522
+ .getElementById("facet-sources")
1523
+ .addEventListener("click", onFacetClick);
1524
+ document
1525
+ .getElementById("facet-agents")
1526
+ .addEventListener("click", onFacetClick);
1527
+ function onFacetClick(e) {
1528
+ const btn = e.target.closest("[data-facet]");
1529
+ if (!btn) return;
1530
+ toggleFacet(
1531
+ btn.getAttribute("data-facet"),
1532
+ btn.getAttribute("data-key"),
1533
+ );
1534
+ }
1535
+ document.querySelectorAll(".sidebar-section-clear").forEach((btn) =>
1536
+ btn.addEventListener("click", () => {
1537
+ state.selected[btn.getAttribute("data-clear")].clear();
1538
+ renderAll();
1539
+ renderFacets();
1540
+ }),
1541
+ );
1542
+
1543
+ // ─────────────────────────────────────────────
1544
+ // Filter bar
1545
+ // ─────────────────────────────────────────────
1546
+ const FACET_LABELS = {
1547
+ categories: "CAT",
1548
+ sources: "SRC",
1549
+ agents: "AGENT",
1550
+ };
1551
+ function renderActiveFilters() {
1552
+ const bar = document.getElementById("filter-bar");
1553
+ const chips = [];
1554
+ for (const group of ["categories", "sources", "agents"]) {
1555
+ for (const val of state.selected[group]) {
1556
+ chips.push(
1557
+ `<span class="filter-chip"><span class="filter-chip-key">${FACET_LABELS[group]}</span>${escapeHtml(val)}<button type="button" class="filter-chip-x" data-remove-facet="${group}" data-key="${escapeHtml(val)}" aria-label="Remove">
1558
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
1559
+ </button></span>`,
1560
+ );
1561
+ }
1562
+ }
1563
+ if (state.query.trim()) {
1564
+ chips.push(
1565
+ `<span class="filter-chip"><span class="filter-chip-key">QUERY</span>${escapeHtml(state.query.trim())}<button type="button" class="filter-chip-x" data-clear-search aria-label="Clear">
1566
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
1567
+ </button></span>`,
1568
+ );
1569
+ }
1570
+ if (!chips.length) {
1571
+ bar.innerHTML = `<span class="filter-bar-empty">${state.memories.length} ${state.memories.length === 1 ? "record" : "records"}</span>`;
1572
+ return;
1573
+ }
1574
+ bar.innerHTML =
1575
+ `<span class="filter-bar-label">Filters</span>` +
1576
+ chips.join("") +
1577
+ `<button type="button" class="filter-clear-all" id="clear-all-filters">Reset</button>`;
1578
+ }
1579
+ document.getElementById("filter-bar").addEventListener("click", (e) => {
1580
+ if (e.target.closest("#clear-all-filters")) {
1581
+ state.selected.categories.clear();
1582
+ state.selected.sources.clear();
1583
+ state.selected.agents.clear();
1584
+ state.query = "";
1585
+ document.getElementById("search").value = "";
1586
+ renderAll();
1587
+ renderFacets();
1588
+ return;
1589
+ }
1590
+ const x = e.target.closest("[data-remove-facet]");
1591
+ if (x) {
1592
+ state.selected[x.getAttribute("data-remove-facet")].delete(
1593
+ x.getAttribute("data-key"),
1594
+ );
1595
+ renderAll();
1596
+ renderFacets();
1597
+ return;
1598
+ }
1599
+ if (e.target.closest("[data-clear-search]")) {
1600
+ state.query = "";
1601
+ document.getElementById("search").value = "";
1602
+ renderAll();
1603
+ }
1604
+ });
1605
+
1606
+ // ─────────────────────────────────────────────
1607
+ // Memory rows
1608
+ // ─────────────────────────────────────────────
1609
+ function renderConfMeter(c) {
1610
+ const pct = Math.round(c * 100);
1611
+ const cls = pct < 60 ? "low" : "";
1612
+ return `<span class="meter" title="confidence ${pct}%">
1613
+ <span class="meter-track"><span class="meter-fill ${cls}" style="width:${pct}%"></span></span>
1614
+ <span>${pct}%</span>
1615
+ </span>`;
1616
+ }
1617
+
1618
+ function renderMemoryRow(m) {
1619
+ const isExpanded = state.expanded.has(m.id);
1620
+ const source = sourceOf(m);
1621
+ const sourceClass = source.replace(/\W+/g, "_");
1622
+ const retrievals = m.retrievalCount || 0;
1623
+ const numClass =
1624
+ retrievals === 0 ? "num-cell zero" : retrievals >= 5 ? "num-cell hot" : "num-cell";
1625
+
1626
+ const main = `
1627
+ <tr class="row${isExpanded ? " expanded" : ""}" data-id="${escapeHtml(m.id || "")}">
1628
+ <td class="col-expand">
1629
+ <button type="button" class="caret" data-toggle="${escapeHtml(m.id)}" aria-label="${isExpanded ? "Collapse" : "Expand"}">
1630
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 6 6 6-6 6"/></svg>
1631
+ </button>
1632
+ </td>
1633
+ <td>
1634
+ <div class="row-text">${escapeHtml(m.text)}</div>
1635
+ </td>
1636
+ <td class="col-source"><span class="pill ${sourceClass}">${escapeHtml(source)}</span></td>
1637
+ <td class="col-agent"><span class="agent-text" title="${escapeHtml(agentOf(m))}">${escapeHtml(agentOf(m))}</span></td>
1638
+ <td class="col-num num"><span class="${numClass}">${retrievals}</span></td>
1639
+ <td class="col-created"><span class="time-cell" title="${escapeHtml(formatAbsolute(m.createdAt))}">${formatRelative(m.createdAt)}</span></td>
1640
+ </tr>`;
1641
+
1642
+ if (!isExpanded) return main;
1643
+
1644
+ return main + renderDetailRow(m);
1645
+ }
1646
+
1647
+ function renderDetailRow(m) {
1648
+ const meta = m.metadata || {};
1649
+ const conf = confidenceOf(m);
1650
+ const categories = m.categories || [];
1651
+
1652
+ // Build kv grid — scope, lifecycle, then metadata keys
1653
+ const rows = [];
1654
+ if (m.id) rows.push(["id", `<span>${escapeHtml(m.id)}</span><button type="button" class="copy-id" data-copy-id="${escapeHtml(m.id)}">copy</button>`]);
1655
+ const userId = m.scope && m.scope.userId;
1656
+ const sessionId = m.scope && m.scope.sessionId;
1657
+ if (userId) rows.push(["user", escapeHtml(userId)]);
1658
+ if (sessionId) rows.push(["session", escapeHtml(sessionId)]);
1659
+ rows.push(["created", escapeHtml(formatAbsolute(m.createdAt))]);
1660
+ if (m.lastRetrieved)
1661
+ rows.push(["retrieved", escapeHtml(formatAbsolute(m.lastRetrieved))]);
1662
+ if (conf !== null) rows.push(["confidence", renderConfMeter(conf)]);
1663
+
1664
+ const META_ORDER = [
1665
+ "durability",
1666
+ "auto_saved",
1667
+ "signal_count",
1668
+ "trigger",
1669
+ "trigger_reasons",
1670
+ "reason",
1671
+ "consolidated_into",
1672
+ "session_file",
1673
+ ];
1674
+ const seen = new Set(["source", "confidence"]);
1675
+ META_ORDER.forEach((k) => {
1676
+ if (k in meta && !seen.has(k)) {
1677
+ rows.push([k, escapeHtml(formatMetaValue(k, meta[k]))]);
1678
+ seen.add(k);
1679
+ }
1680
+ });
1681
+ Object.keys(meta).forEach((k) => {
1682
+ if (seen.has(k)) return;
1683
+ rows.push([k, escapeHtml(formatMetaValue(k, meta[k]))]);
1684
+ });
1685
+
1686
+ const grid = rows
1687
+ .map(([k, v]) => `<dt>${escapeHtml(k)}</dt><dd>${v}</dd>`)
1688
+ .join("");
1689
+
1690
+ const tags = categories.length
1691
+ ? categories
1692
+ .map(
1693
+ (c) =>
1694
+ `<button type="button" class="cat-tag" data-add-cat="${escapeHtml(c)}">${escapeHtml(c)}</button>`,
1695
+ )
1696
+ .join("")
1697
+ : `<span style="color:var(--text-muted);font-size:11.5px">No categories</span>`;
1698
+
1699
+ const json = Object.keys(meta).length
1700
+ ? `<div>
1701
+ <div class="detail-section-title">Raw metadata</div>
1702
+ <pre class="json-block">${escapeHtml(JSON.stringify(meta, null, 2))}</pre>
1703
+ </div>`
1704
+ : "";
1705
+
1706
+ return `
1707
+ <tr class="row-detail">
1708
+ <td colspan="6">
1709
+ <div class="detail">
1710
+ <div>
1711
+ <div class="detail-section-title">Content</div>
1712
+ <div class="detail-text">${escapeHtml(m.text)}</div>
1713
+ <div style="display:flex;gap:8px;margin-bottom:18px">
1714
+ <button type="button" class="topbar-btn" data-edit-memory="${escapeHtml(m.id)}">Edit</button>
1715
+ <button type="button" class="topbar-btn" data-delete-memory="${escapeHtml(m.id)}">Delete</button>
1716
+ </div>
1717
+ <div class="detail-section-title">Categories</div>
1718
+ <div class="detail-categories">${tags}</div>
1719
+ ${json}
1720
+ </div>
1721
+ <div class="detail-side">
1722
+ <div class="detail-section-title">Properties</div>
1723
+ <dl class="kv-grid">${grid}</dl>
1724
+ </div>
1725
+ </div>
1726
+ </td>
1727
+ </tr>`;
1728
+ }
1729
+
1730
+ function formatMetaValue(key, value) {
1731
+ if (value === null || value === undefined) return "—";
1732
+ if (key === "confidence" && typeof value === "number")
1733
+ return Math.round(value * 100) + "%";
1734
+ if (typeof value === "boolean") return value ? "true" : "false";
1735
+ if (Array.isArray(value)) return value.map(String).join(", ");
1736
+ if (typeof value === "object") return JSON.stringify(value);
1737
+ return String(value);
1738
+ }
1739
+
1740
+ // ─────────────────────────────────────────────
1741
+ // Render
1742
+ // ─────────────────────────────────────────────
1743
+ function renderAll() {
1744
+ if (state.activeView === "entities") renderEntities();
1745
+ else renderMemories();
1746
+ renderActiveFilters();
1747
+ }
1748
+ function renderMemories() {
1749
+ const filtered = sortMemories(applyFilters(state.memories));
1750
+ const total = state.memories.length;
1751
+ const label = document.getElementById("result-label");
1752
+ if (filtered.length === total)
1753
+ label.textContent =
1754
+ total === 0
1755
+ ? ""
1756
+ : `${total} ${total === 1 ? "record" : "records"}`;
1757
+ else label.textContent = `${filtered.length} / ${total}`;
1758
+
1759
+ const host = document.getElementById("memories");
1760
+ if (!filtered.length) {
1761
+ host.innerHTML = total
1762
+ ? `<tr><td colspan="6"><div class="empty"><strong>No matches</strong>Adjust filters or search to find memories.</div></td></tr>`
1763
+ : `<tr><td colspan="6"><div class="empty"><strong>No memories yet</strong>Capture a conversation or add one via the CLI.</div></td></tr>`;
1764
+ return;
1765
+ }
1766
+ host.innerHTML = filtered.map(renderMemoryRow).join("");
1767
+ }
1768
+
1769
+ function computeEntities() {
1770
+ const map = new Map();
1771
+ state.memories.forEach((m) => {
1772
+ const agent = agentOf(m);
1773
+ const user = (m.scope && m.scope.userId) || null;
1774
+ const session = (m.scope && m.scope.sessionId) || null;
1775
+ const key = agent + "\0" + user + "\0" + session;
1776
+ if (!map.has(key))
1777
+ map.set(key, {
1778
+ agent,
1779
+ user,
1780
+ session,
1781
+ count: 0,
1782
+ retrievals: 0,
1783
+ });
1784
+ const row = map.get(key);
1785
+ row.count++;
1786
+ row.retrievals += m.retrievalCount || 0;
1787
+ });
1788
+ return Array.from(map.values()).sort((a, b) => b.count - a.count);
1789
+ }
1790
+ function renderEntities() {
1791
+ const entities = computeEntities();
1792
+ document.getElementById("result-label").textContent =
1793
+ entities.length +
1794
+ " " +
1795
+ (entities.length === 1 ? "entity" : "entities");
1796
+ const host = document.getElementById("entities");
1797
+ if (!entities.length) {
1798
+ host.innerHTML = `<tr><td colspan="5"><div class="empty"><strong>No entities</strong>Capture some memories to populate scopes.</div></td></tr>`;
1799
+ return;
1800
+ }
1801
+ host.innerHTML = entities
1802
+ .map(
1803
+ (e) => `<tr class="row">
1804
+ <td><span class="agent-text">${escapeHtml(e.agent)}</span></td>
1805
+ <td><span class="agent-text">${e.user ? escapeHtml(e.user) : '<span style="color:var(--text-muted)">—</span>'}</span></td>
1806
+ <td><span class="agent-text">${e.session ? escapeHtml(e.session) : '<span style="color:var(--text-muted)">—</span>'}</span></td>
1807
+ <td class="col-num num"><span class="num-cell">${e.count}</span></td>
1808
+ <td class="col-num num"><span class="num-cell">${e.retrievals}</span></td>
1809
+ </tr>`,
1810
+ )
1811
+ .join("");
1812
+ }
1813
+
1814
+ // ─────────────────────────────────────────────
1815
+ // Views
1816
+ // ─────────────────────────────────────────────
1817
+ function setView(view) {
1818
+ state.activeView = view;
1819
+ document.querySelectorAll(".nav-item[data-view]").forEach((el) => {
1820
+ el.classList.toggle(
1821
+ "active",
1822
+ el.getAttribute("data-view") === view,
1823
+ );
1824
+ });
1825
+ document
1826
+ .getElementById("view-memories")
1827
+ .classList.toggle("active", view === "memories");
1828
+ document
1829
+ .getElementById("view-entities")
1830
+ .classList.toggle("active", view === "entities");
1831
+ document.getElementById("page-title").textContent =
1832
+ view === "entities" ? "Entities" : "Memories";
1833
+ renderAll();
1834
+ }
1835
+ document.querySelectorAll(".nav-item[data-view]").forEach((el) => {
1836
+ el.addEventListener("click", () =>
1837
+ setView(el.getAttribute("data-view")),
1838
+ );
1839
+ });
1840
+
1841
+ // ─────────────────────────────────────────────
1842
+ // Search / sort
1843
+ // ─────────────────────────────────────────────
1844
+ const searchInput = document.getElementById("search");
1845
+ searchInput.addEventListener("input", (e) => {
1846
+ state.query = e.target.value;
1847
+ renderAll();
1848
+ });
1849
+ document.getElementById("sort").addEventListener("change", (e) => {
1850
+ state.sort = e.target.value;
1851
+ renderAll();
1852
+ });
1853
+ document.addEventListener("keydown", (e) => {
1854
+ if (
1855
+ e.key === "/" &&
1856
+ document.activeElement.tagName !== "INPUT" &&
1857
+ document.activeElement.tagName !== "TEXTAREA"
1858
+ ) {
1859
+ e.preventDefault();
1860
+ searchInput.focus();
1861
+ searchInput.select();
1862
+ }
1863
+ if (e.key === "Escape" && document.activeElement === searchInput)
1864
+ searchInput.blur();
1865
+ });
1866
+
1867
+ // ─────────────────────────────────────────────
1868
+ // Row interactions
1869
+ // ─────────────────────────────────────────────
1870
+ function toggleExpanded(id) {
1871
+ if (!id) return;
1872
+ if (state.expanded.has(id)) state.expanded.delete(id);
1873
+ else state.expanded.add(id);
1874
+ renderMemories();
1875
+ }
1876
+
1877
+ async function updateMemory(id, text) {
1878
+ const res = await fetch(`/api/memories/${encodeURIComponent(id)}`, {
1879
+ method: "PATCH",
1880
+ headers: { "Content-Type": "application/json" },
1881
+ body: JSON.stringify({ text }),
1882
+ });
1883
+ if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
1884
+ }
1885
+
1886
+ async function deleteMemory(id) {
1887
+ const res = await fetch(`/api/memories/${encodeURIComponent(id)}`, { method: "DELETE" });
1888
+ if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
1889
+ }
1890
+
1891
+ document.getElementById("memories").addEventListener("click", async (e) => {
1892
+ const toggle = e.target.closest("[data-toggle]");
1893
+ if (toggle) {
1894
+ toggleExpanded(toggle.getAttribute("data-toggle"));
1895
+ return;
1896
+ }
1897
+ const editBtn = e.target.closest("[data-edit-memory]");
1898
+ if (editBtn) {
1899
+ const id = editBtn.getAttribute("data-edit-memory");
1900
+ const memory = state.memories.find((m) => m.id === id);
1901
+ if (!memory) return;
1902
+ const next = window.prompt("Edit memory", memory.text);
1903
+ if (next === null) return;
1904
+ const trimmed = next.trim();
1905
+ if (!trimmed || trimmed === memory.text) return;
1906
+ try {
1907
+ await updateMemory(id, trimmed);
1908
+ await load();
1909
+ } catch (err) {
1910
+ window.alert(`Failed to update memory: ${err.message || err}`);
1911
+ }
1912
+ return;
1913
+ }
1914
+ const deleteBtn = e.target.closest("[data-delete-memory]");
1915
+ if (deleteBtn) {
1916
+ const id = deleteBtn.getAttribute("data-delete-memory");
1917
+ const memory = state.memories.find((m) => m.id === id);
1918
+ if (!memory) return;
1919
+ if (!window.confirm(`Delete this memory?\n\n${memory.text}`)) return;
1920
+ try {
1921
+ await deleteMemory(id);
1922
+ state.expanded.delete(id);
1923
+ await load();
1924
+ } catch (err) {
1925
+ window.alert(`Failed to delete memory: ${err.message || err}`);
1926
+ }
1927
+ return;
1928
+ }
1929
+ // Click anywhere on the row (not on inner buttons) to expand
1930
+ const row = e.target.closest("tr.row");
1931
+ if (row && !e.target.closest("button, a, .cat-tag, .copy-id")) {
1932
+ toggleExpanded(row.getAttribute("data-id"));
1933
+ return;
1934
+ }
1935
+ const cat = e.target.closest("[data-add-cat]");
1936
+ if (cat) {
1937
+ state.selected.categories.add(cat.getAttribute("data-add-cat"));
1938
+ renderAll();
1939
+ renderFacets();
1940
+ return;
1941
+ }
1942
+ const copy = e.target.closest("[data-copy-id]");
1943
+ if (copy) {
1944
+ const id = copy.getAttribute("data-copy-id");
1945
+ navigator.clipboard?.writeText(id);
1946
+ const orig = copy.textContent;
1947
+ copy.textContent = "copied";
1948
+ setTimeout(() => (copy.textContent = orig), 1000);
1949
+ }
1950
+ });
1951
+
1952
+ document.getElementById("review-auto-saved").addEventListener("click", () => {
1953
+ state.selected.sources = new Set(["heuristic", "repetition", "llm_extracted"]);
1954
+ state.sort = "recent";
1955
+ document.getElementById("sort").value = "recent";
1956
+ setView("memories");
1957
+ renderFacets();
1958
+ });
1959
+
1960
+ load();
1961
+ </script>
1962
+ </body>
1963
+ </html>