@agentmemory/agentmemory 0.9.28 → 0.9.29
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/.env.example +15 -6
- package/AGENTS.md +6 -6
- package/README.md +220 -152
- package/dist/.env.example +15 -6
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +233 -35
- package/dist/cli.mjs.map +1 -1
- package/dist/{connect-JnsJXc68.mjs → connect-Qaeq15TQ.mjs} +599 -41
- package/dist/connect-Qaeq15TQ.mjs.map +1 -0
- package/dist/docker-compose.yml +2 -2
- package/dist/hooks/antigravity-bridge.d.mts +8 -0
- package/dist/hooks/antigravity-bridge.d.mts.map +1 -0
- package/dist/hooks/antigravity-bridge.mjs +114 -0
- package/dist/hooks/antigravity-bridge.mjs.map +1 -0
- package/dist/hooks/notification.mjs +19 -4
- package/dist/hooks/notification.mjs.map +1 -1
- package/dist/hooks/post-commit.mjs +13 -1
- package/dist/hooks/post-commit.mjs.map +1 -1
- package/dist/hooks/post-tool-failure.mjs +14 -3
- package/dist/hooks/post-tool-failure.mjs.map +1 -1
- package/dist/hooks/post-tool-use.mjs +14 -3
- package/dist/hooks/post-tool-use.mjs.map +1 -1
- package/dist/hooks/pre-compact.mjs +12 -2
- package/dist/hooks/pre-compact.mjs.map +1 -1
- package/dist/hooks/pre-tool-use.mjs +1 -1
- package/dist/hooks/pre-tool-use.mjs.map +1 -1
- package/dist/hooks/prompt-submit.mjs +14 -3
- package/dist/hooks/prompt-submit.mjs.map +1 -1
- package/dist/hooks/session-end.mjs +82 -18
- package/dist/hooks/session-end.mjs.map +1 -1
- package/dist/hooks/session-start.mjs +22 -4
- package/dist/hooks/session-start.mjs.map +1 -1
- package/dist/hooks/stop.mjs +1 -7
- package/dist/hooks/stop.mjs.map +1 -1
- package/dist/hooks/subagent-start.mjs +14 -3
- package/dist/hooks/subagent-start.mjs.map +1 -1
- package/dist/hooks/subagent-stop.mjs +14 -3
- package/dist/hooks/subagent-stop.mjs.map +1 -1
- package/dist/hooks/task-completed.mjs +14 -3
- package/dist/hooks/task-completed.mjs.map +1 -1
- package/dist/{image-refs-C2YU7BIm.mjs → image-refs-CJgZmb9X.mjs} +2 -2
- package/dist/{image-refs-C2YU7BIm.mjs.map → image-refs-CJgZmb9X.mjs.map} +1 -1
- package/dist/index.mjs +1557 -755
- package/dist/index.mjs.map +1 -1
- package/dist/schema-BFZhNTF7.mjs +197 -0
- package/dist/schema-BFZhNTF7.mjs.map +1 -0
- package/dist/{src-CzgoepGU.mjs → src-G7yt8gGm.mjs} +1531 -883
- package/dist/src-G7yt8gGm.mjs.map +1 -0
- package/dist/{standalone-YsZFFQBM.mjs → standalone-BqNJDJ04.mjs} +26 -15
- package/dist/standalone-BqNJDJ04.mjs.map +1 -0
- package/dist/standalone.d.mts.map +1 -1
- package/dist/standalone.mjs +50 -14
- package/dist/standalone.mjs.map +1 -1
- package/dist/{tools-registry-D3NEoe5m.mjs → tools-registry-BV_2bu7z.mjs} +293 -3
- package/dist/tools-registry-BV_2bu7z.mjs.map +1 -0
- package/dist/viewer/favicon.svg +35 -1
- package/dist/viewer/index.html +473 -83
- package/docker-compose.yml +2 -2
- package/integrations/pi/README.md +77 -0
- package/integrations/pi/index.ts +403 -0
- package/integrations/pi/package.json +32 -0
- package/integrations/pi/security.ts +35 -0
- package/package.json +3 -4
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +2 -2
- package/plugin/.devin-plugin/plugin.json +32 -0
- package/plugin/cursor/hooks.json +41 -0
- package/plugin/cursor/mcp.json +12 -0
- package/plugin/hooks/hooks.antigravity.json +43 -0
- package/plugin/hooks/hooks.devin.json +67 -0
- package/plugin/hooks/hooks.droid.json +55 -0
- package/plugin/opencode/README.md +1 -1
- package/plugin/opencode/agentmemory-capture.ts +73 -13
- package/plugin/opencode/plugin.json +1 -1
- package/plugin/plugin.json +2 -2
- package/plugin/scripts/antigravity-bridge.d.mts +8 -0
- package/plugin/scripts/antigravity-bridge.d.mts.map +1 -0
- package/plugin/scripts/antigravity-bridge.mjs +114 -0
- package/plugin/scripts/antigravity-bridge.mjs.map +1 -0
- package/plugin/scripts/notification.mjs +19 -4
- package/plugin/scripts/notification.mjs.map +1 -1
- package/plugin/scripts/post-commit.mjs +13 -1
- package/plugin/scripts/post-commit.mjs.map +1 -1
- package/plugin/scripts/post-tool-failure.mjs +14 -3
- package/plugin/scripts/post-tool-failure.mjs.map +1 -1
- package/plugin/scripts/post-tool-use.mjs +14 -3
- package/plugin/scripts/post-tool-use.mjs.map +1 -1
- package/plugin/scripts/pre-compact.mjs +12 -2
- package/plugin/scripts/pre-compact.mjs.map +1 -1
- package/plugin/scripts/pre-tool-use.mjs +1 -1
- package/plugin/scripts/pre-tool-use.mjs.map +1 -1
- package/plugin/scripts/prompt-submit.mjs +14 -3
- package/plugin/scripts/prompt-submit.mjs.map +1 -1
- package/plugin/scripts/session-end.mjs +82 -18
- package/plugin/scripts/session-end.mjs.map +1 -1
- package/plugin/scripts/session-start.mjs +22 -4
- package/plugin/scripts/session-start.mjs.map +1 -1
- package/plugin/scripts/stop.mjs +1 -7
- package/plugin/scripts/stop.mjs.map +1 -1
- package/plugin/scripts/subagent-start.mjs +14 -3
- package/plugin/scripts/subagent-start.mjs.map +1 -1
- package/plugin/scripts/subagent-stop.mjs +14 -3
- package/plugin/scripts/subagent-stop.mjs.map +1 -1
- package/plugin/scripts/task-completed.mjs +14 -3
- package/plugin/scripts/task-completed.mjs.map +1 -1
- package/plugin/skills/agentmemory-agents/REFERENCE.md +6 -3
- package/plugin/skills/agentmemory-config/REFERENCE.md +4 -1
- package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +3 -2
- package/plugin/skills/agentmemory-rest-api/REFERENCE.md +14 -1
- package/plugin/skills/forget/SKILL.md +4 -1
- package/plugin/skills/lesson/SKILL.md +64 -0
- package/plugin/skills/memory-discipline/SKILL.md +57 -0
- package/plugin/skills/recall/SKILL.md +4 -1
- package/plugin/skills/remember/SKILL.md +6 -1
- package/dist/config-ChC9EtgV.mjs +0 -259
- package/dist/config-ChC9EtgV.mjs.map +0 -1
- package/dist/connect-JnsJXc68.mjs.map +0 -1
- package/dist/schema-CNiCWzV9.mjs +0 -78
- package/dist/schema-CNiCWzV9.mjs.map +0 -1
- package/dist/src-CzgoepGU.mjs.map +0 -1
- package/dist/standalone-YsZFFQBM.mjs.map +0 -1
- package/dist/tools-registry-D3NEoe5m.mjs.map +0 -1
- package/plugin/scripts/diagnostics.d.mts +0 -17
- package/plugin/scripts/diagnostics.d.mts.map +0 -1
- package/plugin/scripts/diagnostics.mjs.map +0 -1
package/dist/viewer/index.html
CHANGED
|
@@ -50,22 +50,21 @@
|
|
|
50
50
|
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
|
|
51
51
|
}
|
|
52
52
|
html[data-theme="dark"] {
|
|
53
|
-
--bg: #
|
|
54
|
-
--bg-alt: #
|
|
55
|
-
--bg-subtle: #
|
|
56
|
-
--bg-inset: #
|
|
57
|
-
--border: #
|
|
58
|
-
--border-light: #
|
|
59
|
-
--border-heavy: #
|
|
60
|
-
--ink: #
|
|
61
|
-
--ink-secondary: #
|
|
62
|
-
--ink-muted: #
|
|
63
|
-
--ink-faint: #
|
|
53
|
+
--bg: #121316;
|
|
54
|
+
--bg-alt: #1a1c20;
|
|
55
|
+
--bg-subtle: #17181b;
|
|
56
|
+
--bg-inset: #222428;
|
|
57
|
+
--border: #33363b;
|
|
58
|
+
--border-light: #26282c;
|
|
59
|
+
--border-heavy: #c9cbd1;
|
|
60
|
+
--ink: #eef0f3;
|
|
61
|
+
--ink-secondary: #c6c9ce;
|
|
62
|
+
--ink-muted: #94979d;
|
|
63
|
+
--ink-faint: #6d7076;
|
|
64
|
+
--accent: #f2555a;
|
|
65
|
+
--accent-light: #ff7a70;
|
|
64
66
|
--cream: #2a2520;
|
|
65
67
|
}
|
|
66
|
-
html[data-theme="dark"] body {
|
|
67
|
-
background-image: radial-gradient(circle, #3a3a42 0.5px, transparent 0.5px);
|
|
68
|
-
}
|
|
69
68
|
html[data-theme="dark"] .graph-tooltip {
|
|
70
69
|
background: rgba(30,30,35,0.92);
|
|
71
70
|
border-color: rgba(255,255,255,0.1);
|
|
@@ -80,6 +79,16 @@
|
|
|
80
79
|
color: var(--bg);
|
|
81
80
|
}
|
|
82
81
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
82
|
+
#bg-dither {
|
|
83
|
+
position: fixed;
|
|
84
|
+
inset: 0;
|
|
85
|
+
width: 100%;
|
|
86
|
+
height: 100%;
|
|
87
|
+
z-index: 0;
|
|
88
|
+
pointer-events: none;
|
|
89
|
+
opacity: 0.5;
|
|
90
|
+
}
|
|
91
|
+
.app-header, .tab-bar, .view, .flags-banner, footer, .app-footer { position: relative; z-index: 1; }
|
|
83
92
|
body {
|
|
84
93
|
font-family: var(--font-body);
|
|
85
94
|
background: var(--bg);
|
|
@@ -89,8 +98,6 @@
|
|
|
89
98
|
height: 100vh;
|
|
90
99
|
display: flex;
|
|
91
100
|
flex-direction: column;
|
|
92
|
-
background-image: radial-gradient(circle, #D4D4CF 0.5px, transparent 0.5px);
|
|
93
|
-
background-size: 16px 16px;
|
|
94
101
|
}
|
|
95
102
|
::-webkit-scrollbar { width: 6px; }
|
|
96
103
|
::-webkit-scrollbar-track { background: var(--bg); }
|
|
@@ -139,6 +146,11 @@
|
|
|
139
146
|
align-items: center;
|
|
140
147
|
gap: 12px;
|
|
141
148
|
}
|
|
149
|
+
@media (max-width: 720px) {
|
|
150
|
+
.app-header { flex-wrap: wrap; row-gap: 6px; padding: 10px 16px; }
|
|
151
|
+
.app-header .dateline { display: none; }
|
|
152
|
+
.view { overflow-x: auto; }
|
|
153
|
+
}
|
|
142
154
|
.ws-status {
|
|
143
155
|
font-size: 10px;
|
|
144
156
|
padding: 3px 10px;
|
|
@@ -158,7 +170,11 @@
|
|
|
158
170
|
display: inline-block;
|
|
159
171
|
}
|
|
160
172
|
.ws-status.connected { border-color: var(--green); color: var(--green); }
|
|
161
|
-
.ws-status.connected::before { background: var(--green); }
|
|
173
|
+
.ws-status.connected::before { background: var(--green); animation: live-pulse 2.4s ease-in-out infinite; }
|
|
174
|
+
@keyframes live-pulse {
|
|
175
|
+
0%, 100% { opacity: 1; }
|
|
176
|
+
50% { opacity: 0.35; }
|
|
177
|
+
}
|
|
162
178
|
.ws-status.disconnected { border-color: var(--ink-faint); color: var(--ink-faint); }
|
|
163
179
|
.ws-status.disconnected::before { background: var(--ink-faint); }
|
|
164
180
|
|
|
@@ -195,7 +211,15 @@
|
|
|
195
211
|
}
|
|
196
212
|
|
|
197
213
|
.view { display: none; flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 24px; }
|
|
198
|
-
.view.active { display: block; }
|
|
214
|
+
.view.active { display: block; animation: view-in 160ms ease-out; }
|
|
215
|
+
@keyframes view-in {
|
|
216
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
217
|
+
to { opacity: 1; transform: translateY(0); }
|
|
218
|
+
}
|
|
219
|
+
@media (prefers-reduced-motion: reduce) {
|
|
220
|
+
.view.active { animation: none; }
|
|
221
|
+
.ws-status.connected::before { animation: none; }
|
|
222
|
+
}
|
|
199
223
|
|
|
200
224
|
.stats-grid {
|
|
201
225
|
display: grid;
|
|
@@ -211,6 +235,14 @@
|
|
|
211
235
|
border-bottom: 1px solid var(--border-light);
|
|
212
236
|
}
|
|
213
237
|
.stat-card:last-child { border-right: none; }
|
|
238
|
+
.stat-card[data-action] {
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
transition: background 0.15s ease-out;
|
|
241
|
+
}
|
|
242
|
+
.stat-card[data-action]:hover { background: var(--bg-alt); }
|
|
243
|
+
.stat-card[data-action]:hover .label { color: var(--accent); }
|
|
244
|
+
.stat-card[data-action]:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
245
|
+
.stat-card[data-action]:active { background: var(--bg-inset); }
|
|
214
246
|
.stat-card .label {
|
|
215
247
|
font-size: 9px;
|
|
216
248
|
color: var(--ink-muted);
|
|
@@ -294,6 +326,7 @@
|
|
|
294
326
|
border-collapse: collapse;
|
|
295
327
|
font-size: 13px;
|
|
296
328
|
font-family: var(--font-body);
|
|
329
|
+
font-variant-numeric: tabular-nums;
|
|
297
330
|
}
|
|
298
331
|
th {
|
|
299
332
|
text-align: left;
|
|
@@ -333,7 +366,7 @@
|
|
|
333
366
|
align-items: center;
|
|
334
367
|
flex-wrap: wrap;
|
|
335
368
|
}
|
|
336
|
-
.toolbar input, .toolbar select {
|
|
369
|
+
.toolbar input, .toolbar select, .search-input {
|
|
337
370
|
background: var(--bg);
|
|
338
371
|
border: 1px solid var(--border);
|
|
339
372
|
color: var(--ink);
|
|
@@ -342,13 +375,13 @@
|
|
|
342
375
|
outline: none;
|
|
343
376
|
font-family: var(--font-ui);
|
|
344
377
|
}
|
|
345
|
-
.toolbar input:focus, .toolbar select:focus {
|
|
378
|
+
.toolbar input:focus, .toolbar select:focus, .search-input:focus {
|
|
346
379
|
border-color: var(--ink);
|
|
347
380
|
box-shadow: 2px 2px 0px 0px var(--border);
|
|
348
381
|
}
|
|
349
382
|
.toolbar input { flex: 1; min-width: 200px; }
|
|
350
383
|
|
|
351
|
-
.btn {
|
|
384
|
+
.btn, .toolbar button {
|
|
352
385
|
background: var(--bg);
|
|
353
386
|
border: 1px solid var(--border);
|
|
354
387
|
color: var(--ink);
|
|
@@ -361,8 +394,8 @@
|
|
|
361
394
|
text-transform: uppercase;
|
|
362
395
|
letter-spacing: 0.06em;
|
|
363
396
|
}
|
|
364
|
-
.btn:hover { box-shadow: 3px 3px 0px 0px var(--border); transform: translate(-1px, -1px); }
|
|
365
|
-
.btn:active { box-shadow: none; transform: translate(0, 0); }
|
|
397
|
+
.btn:hover, .toolbar button:hover { box-shadow: 3px 3px 0px 0px var(--border); transform: translate(-1px, -1px); }
|
|
398
|
+
.btn:active, .toolbar button:active { box-shadow: none; transform: translate(0, 0); }
|
|
366
399
|
.btn-danger { border-color: var(--accent); color: var(--accent); }
|
|
367
400
|
.btn-danger:hover { background: var(--accent); color: white; box-shadow: 3px 3px 0px 0px var(--border); }
|
|
368
401
|
.btn-primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
@@ -370,7 +403,8 @@
|
|
|
370
403
|
|
|
371
404
|
.graph-container {
|
|
372
405
|
display: flex;
|
|
373
|
-
height: calc(100vh -
|
|
406
|
+
height: calc(100vh - 178px);
|
|
407
|
+
min-height: 460px;
|
|
374
408
|
margin: -24px;
|
|
375
409
|
border-top: 1px solid var(--border-light);
|
|
376
410
|
}
|
|
@@ -523,18 +557,36 @@
|
|
|
523
557
|
}
|
|
524
558
|
.tag.file-tag { border-color: var(--green); color: var(--green); }
|
|
525
559
|
|
|
560
|
+
/* Two-pane sessions: list left, detail pinned right on wide screens.
|
|
561
|
+
The detail panel previously rendered below the full list — selecting
|
|
562
|
+
a session on any real corpus put the response off-screen. */
|
|
563
|
+
.sessions-layout {
|
|
564
|
+
display: grid;
|
|
565
|
+
grid-template-columns: minmax(300px, 400px) minmax(0, 1fr);
|
|
566
|
+
gap: 20px;
|
|
567
|
+
align-items: start;
|
|
568
|
+
}
|
|
569
|
+
.sessions-layout #session-detail { position: sticky; top: 0; min-width: 0; }
|
|
570
|
+
.sessions-layout #session-detail .detail-panel { margin-top: 0; }
|
|
571
|
+
@media (max-width: 1100px) {
|
|
572
|
+
.sessions-layout { grid-template-columns: 1fr; }
|
|
573
|
+
.sessions-layout #session-detail { position: static; }
|
|
574
|
+
}
|
|
526
575
|
.session-list { display: flex; flex-direction: column; gap: 0; }
|
|
527
576
|
.session-item {
|
|
528
577
|
background: var(--bg);
|
|
529
578
|
border: 1px solid var(--border-light);
|
|
530
579
|
border-bottom: none;
|
|
580
|
+
border-left: 3px solid transparent;
|
|
531
581
|
padding: 14px 20px;
|
|
532
582
|
cursor: pointer;
|
|
533
|
-
transition: background 0.
|
|
583
|
+
transition: background 0.15s ease-out, border-color 0.15s ease-out;
|
|
534
584
|
}
|
|
535
585
|
.session-item:last-child { border-bottom: 1px solid var(--border-light); }
|
|
536
|
-
.session-item:hover { background: var(--bg-alt); }
|
|
537
|
-
.session-item
|
|
586
|
+
.session-item:hover { background: var(--bg-alt); border-left-color: var(--border-light); }
|
|
587
|
+
.session-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
588
|
+
.session-item:active { background: var(--bg-inset); }
|
|
589
|
+
.session-item.selected { background: var(--bg-alt); border-left-color: var(--accent); }
|
|
538
590
|
.session-item .session-top {
|
|
539
591
|
display: flex;
|
|
540
592
|
justify-content: space-between;
|
|
@@ -975,6 +1027,7 @@
|
|
|
975
1027
|
</style>
|
|
976
1028
|
</head>
|
|
977
1029
|
<body>
|
|
1030
|
+
<canvas id="bg-dither" aria-hidden="true"></canvas>
|
|
978
1031
|
<div class="app-header">
|
|
979
1032
|
<a class="brand" href="#dashboard" data-tab-link="dashboard" aria-label="Open dashboard">
|
|
980
1033
|
<h1>agentmemory</h1>
|
|
@@ -1112,12 +1165,12 @@
|
|
|
1112
1165
|
activeTab: 'dashboard',
|
|
1113
1166
|
dashboard: { loaded: false, health: null, sessions: [], memories: [], graphStats: null, recentAudit: [], lessons: [], crystals: [] },
|
|
1114
1167
|
graph: { loaded: false, nodes: [], edges: [], stats: null, filters: {}, selectedNode: null, queryError: null, truncated: false, totalNodes: 0, totalEdges: 0 },
|
|
1115
|
-
memories: { loaded: false, items: [], search: '', typeFilter: '' },
|
|
1168
|
+
memories: { loaded: false, items: [], search: '', typeFilter: '', selectedId: null },
|
|
1116
1169
|
timeline: { loaded: false, observations: [], sessionId: '', minImportance: 0, page: 0, pageSize: 50 },
|
|
1117
1170
|
sessions: { loaded: false, items: [], selectedId: null },
|
|
1118
1171
|
audit: { loaded: false, entries: [], opFilter: '' },
|
|
1119
1172
|
activity: { loaded: false, observations: [], sessions: [], typeFilter: '' },
|
|
1120
|
-
lessons: { loaded: false, items: [], search: '' },
|
|
1173
|
+
lessons: { loaded: false, items: [], search: '', selectedId: null },
|
|
1121
1174
|
actions: { loaded: false, items: [], frontier: [], statusFilter: '', search: '' },
|
|
1122
1175
|
crystals: { loaded: false, items: [], search: '', lessonMap: {} },
|
|
1123
1176
|
profile: { loaded: false, projects: [], selectedProject: '', data: null },
|
|
@@ -1141,6 +1194,45 @@
|
|
|
1141
1194
|
if (!ts) return '';
|
|
1142
1195
|
try { return new Date(ts).toLocaleTimeString(); } catch { return ts; }
|
|
1143
1196
|
}
|
|
1197
|
+
// Observation subtitles are often the raw tool input serialized as
|
|
1198
|
+
// JSON ('{"file_path":"src/x.ts"}'). Pull the human-meaningful field
|
|
1199
|
+
// out for display; anything unparseable renders as-is.
|
|
1200
|
+
// Health alerts/notes arrive as compact machine slugs
|
|
1201
|
+
// (memory_heap_tight_93%_rss111mb). Translate the known families
|
|
1202
|
+
// into sentences; unknown slugs render as-is.
|
|
1203
|
+
function humanizeHealthFlag(f) {
|
|
1204
|
+
var m;
|
|
1205
|
+
if ((m = /^memory_heap_tight_(\d+)%_rss(\d+)mb$/.exec(f)))
|
|
1206
|
+
return 'Heap is running tight: ' + m[1] + '% of allocated heap in use (process memory ' + m[2] + ' MB). Informational — Node grows the heap on demand.';
|
|
1207
|
+
if ((m = /^memory_(warn|critical)_(\d+)%_rss(\d+)mb$/.exec(f)))
|
|
1208
|
+
return 'Memory ' + (m[1] === 'critical' ? 'critically high' : 'elevated') + ': ' + m[2] + '% of heap in use, process memory ' + m[3] + ' MB.';
|
|
1209
|
+
if ((m = /^cpu_(warn|critical)_(\d+)%$/.exec(f)))
|
|
1210
|
+
return 'CPU ' + (m[1] === 'critical' ? 'critically high' : 'elevated') + ': ' + m[2] + '%.';
|
|
1211
|
+
if ((m = /^event_loop_lag_(warn|critical)_(\d+)ms$/.exec(f)))
|
|
1212
|
+
return 'Event loop ' + (m[1] === 'critical' ? 'severely delayed' : 'delayed') + ': ' + m[2] + ' ms behind. The worker is busy or blocked.';
|
|
1213
|
+
if (f === 'connection_reconnecting')
|
|
1214
|
+
return 'Engine connection lost — reconnecting.';
|
|
1215
|
+
if ((m = /^connection_(.+)$/.exec(f)))
|
|
1216
|
+
return 'Engine connection state: ' + m[1] + '.';
|
|
1217
|
+
return f;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function humanizeSubtitle(s) {
|
|
1221
|
+
if (typeof s !== 'string') return '';
|
|
1222
|
+
var t = s.trim();
|
|
1223
|
+
if (!t.startsWith('{')) return s;
|
|
1224
|
+
try {
|
|
1225
|
+
var o = JSON.parse(t);
|
|
1226
|
+
if (o && typeof o === 'object') {
|
|
1227
|
+
var keys = ['file_path', 'filepath', 'filePath', 'path', 'file', 'command', 'pattern', 'url', 'query', 'prompt'];
|
|
1228
|
+
for (var i = 0; i < keys.length; i++) {
|
|
1229
|
+
if (typeof o[keys[i]] === 'string' && o[keys[i]].length > 0) return o[keys[i]];
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
} catch (_) {}
|
|
1233
|
+
return s;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1144
1236
|
function truncate(s, n) {
|
|
1145
1237
|
if (!s) return '';
|
|
1146
1238
|
return s.length > n ? s.slice(0, n) + '...' : s;
|
|
@@ -1150,7 +1242,15 @@
|
|
|
1150
1242
|
}
|
|
1151
1243
|
function shortSessionId(s, n) {
|
|
1152
1244
|
var id = sessionId(s);
|
|
1153
|
-
|
|
1245
|
+
if (!id) return '';
|
|
1246
|
+
var max = n || 8;
|
|
1247
|
+
if (id.length <= max) return id;
|
|
1248
|
+
// Session ids share a long common prefix (demo_msuaboq7_...); the
|
|
1249
|
+
// distinguishing part is the tail. Keep head + tail so truncated
|
|
1250
|
+
// ids stay tellable apart in lists and dropdowns.
|
|
1251
|
+
var tail = 6;
|
|
1252
|
+
var head = Math.max(2, max - tail - 1);
|
|
1253
|
+
return id.slice(0, head) + '…' + id.slice(-tail);
|
|
1154
1254
|
}
|
|
1155
1255
|
function sessionDisplayName(s) {
|
|
1156
1256
|
var project = s && s.project ? String(s.project).split('/').pop() : '';
|
|
@@ -1244,11 +1344,24 @@
|
|
|
1244
1344
|
headers.Authorization = 'Bearer ' + viewerToken;
|
|
1245
1345
|
}
|
|
1246
1346
|
var fetchOpts = Object.assign({}, opts || {}, { headers: headers });
|
|
1347
|
+
var readErrorBody = fetchOpts.readErrorBody;
|
|
1348
|
+
delete fetchOpts.readErrorBody;
|
|
1247
1349
|
var res = await fetch(url, fetchOpts);
|
|
1248
1350
|
if (!res.ok) {
|
|
1249
1351
|
if (res.status === 401) showViewerAuthPrompt();
|
|
1250
1352
|
console.warn('[viewer] API ' + (fetchOpts.method || 'GET') + ' ' + path + ' returned ' + res.status);
|
|
1251
|
-
|
|
1353
|
+
// Non-2xx responses resolve to null so callers can keep treating
|
|
1354
|
+
// null as "request failed" (e.g. loadGraph's disabled/error state).
|
|
1355
|
+
// The health endpoint opts out via readErrorBody: it intentionally
|
|
1356
|
+
// responds 503 with a valid JSON body when status is "critical"
|
|
1357
|
+
// (see #1019), and the dashboard badge needs that body.
|
|
1358
|
+
if (!readErrorBody) return null;
|
|
1359
|
+
try {
|
|
1360
|
+
return await res.json();
|
|
1361
|
+
} catch (parseErr) {
|
|
1362
|
+
console.debug('[viewer] API ' + path + ' non-2xx body was not JSON:', parseErr);
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1252
1365
|
}
|
|
1253
1366
|
hideViewerAuthPrompt();
|
|
1254
1367
|
return await res.json();
|
|
@@ -1322,29 +1435,44 @@
|
|
|
1322
1435
|
loadTab(tab);
|
|
1323
1436
|
}
|
|
1324
1437
|
|
|
1438
|
+
// Per-tab freshness stamps. Tabs refetch on entry (the loaded-once
|
|
1439
|
+
// model went stale the moment anything wrote through the API), but a
|
|
1440
|
+
// short window stops rapid tab flipping from re-issuing the full
|
|
1441
|
+
// fan-out (dashboard alone is ~10 requests) on every click.
|
|
1442
|
+
var tabFetchedAt = {};
|
|
1443
|
+
var TAB_FRESH_MS = 5000;
|
|
1444
|
+
|
|
1325
1445
|
async function loadTab(tab) {
|
|
1446
|
+
var now = Date.now();
|
|
1447
|
+
if (tab !== 'replay' && tabFetchedAt[tab] && now - tabFetchedAt[tab] < TAB_FRESH_MS) {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
tabFetchedAt[tab] = now;
|
|
1326
1451
|
switch(tab) {
|
|
1327
|
-
case 'dashboard':
|
|
1328
|
-
case 'graph':
|
|
1329
|
-
case 'memories':
|
|
1330
|
-
case 'timeline':
|
|
1331
|
-
case 'sessions':
|
|
1332
|
-
case 'lessons':
|
|
1333
|
-
case 'actions':
|
|
1334
|
-
case 'crystals':
|
|
1335
|
-
case 'audit':
|
|
1336
|
-
case 'activity':
|
|
1337
|
-
case 'profile':
|
|
1452
|
+
case 'dashboard': await loadDashboard(); break;
|
|
1453
|
+
case 'graph': await loadGraph(); break;
|
|
1454
|
+
case 'memories': await loadMemories(); break;
|
|
1455
|
+
case 'timeline': await loadTimeline(); break;
|
|
1456
|
+
case 'sessions': await loadSessions(); break;
|
|
1457
|
+
case 'lessons': await loadLessons(); break;
|
|
1458
|
+
case 'actions': await loadActions(); break;
|
|
1459
|
+
case 'crystals': await loadCrystals(); break;
|
|
1460
|
+
case 'audit': await loadAudit(); break;
|
|
1461
|
+
case 'activity': await loadActivity(); break;
|
|
1462
|
+
case 'profile': await loadProfile(); break;
|
|
1463
|
+
// Replay stays fetch-once: reloading it would reset playback
|
|
1464
|
+
// timer and cursor state mid-session; its toolbar has an explicit
|
|
1465
|
+
// Refresh button instead.
|
|
1338
1466
|
case 'replay': if (!state.replay.loaded) await loadReplay(); break;
|
|
1339
1467
|
}
|
|
1340
1468
|
}
|
|
1341
1469
|
|
|
1342
1470
|
async function loadDashboard() {
|
|
1343
1471
|
var el = document.getElementById('view-dashboard');
|
|
1344
|
-
el.innerHTML = '<div class="loading">Loading dashboard...</div>';
|
|
1472
|
+
if (!state.dashboard.loaded) el.innerHTML = '<div class="loading">Loading dashboard...</div>';
|
|
1345
1473
|
try {
|
|
1346
1474
|
var results = await Promise.all([
|
|
1347
|
-
|
|
1475
|
+
api('health', { readErrorBody: true }),
|
|
1348
1476
|
apiGet('sessions'),
|
|
1349
1477
|
apiGet('memories?latest=true&limit=500'),
|
|
1350
1478
|
apiGet('graph/stats'),
|
|
@@ -1415,13 +1543,13 @@
|
|
|
1415
1543
|
'</div>';
|
|
1416
1544
|
}
|
|
1417
1545
|
html += '<div class="stats-grid">';
|
|
1418
|
-
html += '<div class="stat-card"><div class="label">Sessions</div><div class="value">' + d.sessions.length + '</div><div class="sub">' + activeSessions + ' active</div></div>';
|
|
1419
|
-
html += '<div class="stat-card"><div class="label">Memories</div><div class="value">' + d.memories.length + '</div><div class="sub">latest versions</div></div>';
|
|
1546
|
+
html += '<div class="stat-card" data-action="goto-tab" data-tab="sessions" role="link" tabindex="0"><div class="label">Sessions</div><div class="value">' + d.sessions.length + '</div><div class="sub">' + activeSessions + ' active</div></div>';
|
|
1547
|
+
html += '<div class="stat-card" data-action="goto-tab" data-tab="memories" role="link" tabindex="0"><div class="label">Memories</div><div class="value">' + d.memories.length + '</div><div class="sub">latest versions</div></div>';
|
|
1420
1548
|
var lessonCount = (d.lessons || []).length;
|
|
1421
1549
|
var crystalCount = (d.crystals || []).length;
|
|
1422
|
-
html += '<div class="stat-card"><div class="label">Lessons</div><div class="value">' + lessonCount + '</div><div class="sub">confidence-scored</div></div>';
|
|
1423
|
-
html += '<div class="stat-card"><div class="label">Crystals</div><div class="value">' + crystalCount + '</div><div class="sub">action digests</div></div>';
|
|
1424
|
-
html += '<div class="stat-card"><div class="label">Graph Nodes</div><div class="value">' + nodeCount + '</div><div class="sub">' + edgeCount + ' edges</div></div>';
|
|
1550
|
+
html += '<div class="stat-card" data-action="goto-tab" data-tab="lessons" role="link" tabindex="0"><div class="label">Lessons</div><div class="value">' + lessonCount + '</div><div class="sub">confidence-scored</div></div>';
|
|
1551
|
+
html += '<div class="stat-card" data-action="goto-tab" data-tab="crystals" role="link" tabindex="0"><div class="label">Crystals</div><div class="value">' + crystalCount + '</div><div class="sub">action digests</div></div>';
|
|
1552
|
+
html += '<div class="stat-card" data-action="goto-tab" data-tab="graph" role="link" tabindex="0"><div class="label">Graph Nodes</div><div class="value">' + nodeCount + '</div><div class="sub">' + edgeCount + ' edges</div></div>';
|
|
1425
1553
|
html += '<div class="stat-card"><div class="label">Health</div><div class="value"><div class="health-bar"><span class="health-dot ' + dotClass + '"></span> ' + esc(healthStatus) + '</div></div>';
|
|
1426
1554
|
html += '<div class="sub">' + esc(snap.connectionState || 'unknown') + '</div></div>';
|
|
1427
1555
|
var totalCalls = fMetrics.reduce(function(a, m) { return a + (m.totalCalls || 0); }, 0);
|
|
@@ -1482,7 +1610,8 @@
|
|
|
1482
1610
|
|
|
1483
1611
|
if (snap.alerts && snap.alerts.length > 0) {
|
|
1484
1612
|
html += '<div class="card" style="margin-bottom:16px;border-color:var(--accent);border-width:2px;"><div class="card-title" style="color:var(--accent);border-bottom-color:var(--accent);">Alerts (' + snap.alerts.length + ')</div>';
|
|
1485
|
-
snap.alerts.forEach(function(
|
|
1613
|
+
snap.alerts.forEach(function(alRaw) {
|
|
1614
|
+
var al = humanizeHealthFlag(alRaw);
|
|
1486
1615
|
html += '<div style="font-size:12px;color:var(--accent);padding:4px 0;border-bottom:1px solid var(--border-light);font-family:var(--font-ui);">' + esc(al) + '</div>';
|
|
1487
1616
|
});
|
|
1488
1617
|
html += '</div>';
|
|
@@ -1491,7 +1620,7 @@
|
|
|
1491
1620
|
if (snap.notes && snap.notes.length > 0) {
|
|
1492
1621
|
html += '<div class="card" style="margin-bottom:16px;"><div class="card-title" style="color:var(--ink-muted);">Notes (' + snap.notes.length + ')</div>';
|
|
1493
1622
|
snap.notes.forEach(function(n) {
|
|
1494
|
-
html += '<div style="font-size:12px;color:var(--ink-muted);padding:4px 0;border-bottom:1px solid var(--border-light);font-family:var(--font-ui);">' + esc(n) + '</div>';
|
|
1623
|
+
html += '<div style="font-size:12px;color:var(--ink-muted);padding:4px 0;border-bottom:1px solid var(--border-light);font-family:var(--font-ui);">' + esc(humanizeHealthFlag(n)) + '</div>';
|
|
1495
1624
|
});
|
|
1496
1625
|
html += '</div>';
|
|
1497
1626
|
}
|
|
@@ -1625,6 +1754,9 @@
|
|
|
1625
1754
|
html += '<div class="consolidation-row"><span class="cl">Semantic facts</span><span class="cv">' + semFacts.length + '</span></div>';
|
|
1626
1755
|
html += '<div class="consolidation-row"><span class="cl">Procedures</span><span class="cv">' + procItems.length + '</span></div>';
|
|
1627
1756
|
html += '<div class="consolidation-row"><span class="cl">Relations</span><span class="cv">' + relItems.length + '</span></div>';
|
|
1757
|
+
if (semFacts.length === 0 && procItems.length === 0 && relItems.length === 0) {
|
|
1758
|
+
html += '<div style="font-size:12px;color:var(--ink-faint);margin-top:8px;line-height:1.5;">Consolidation distills session observations into durable facts and repeatable procedures. It runs on a schedule when <code>CONSOLIDATION_ENABLED=true</code> and an LLM provider key are set, or on demand via <code>memory_consolidate</code>.</div>';
|
|
1759
|
+
}
|
|
1628
1760
|
html += '</div>';
|
|
1629
1761
|
|
|
1630
1762
|
if (relItems.length > 0) {
|
|
@@ -1682,9 +1814,24 @@
|
|
|
1682
1814
|
|
|
1683
1815
|
var results = await Promise.all([
|
|
1684
1816
|
apiPost('graph/query', { limit: GRAPH_INITIAL_LIMIT }),
|
|
1685
|
-
|
|
1817
|
+
api('graph/stats', { readErrorBody: true })
|
|
1686
1818
|
]);
|
|
1687
1819
|
var queryResult = results[0];
|
|
1820
|
+
var statsResult = results[1];
|
|
1821
|
+
if (statsResult && statsResult.error && statsResult.flag) {
|
|
1822
|
+
// 503 with a structured body = the feature is off, not broken.
|
|
1823
|
+
// Rendering this as "query failed / Retry" sends users hunting
|
|
1824
|
+
// through server logs for an error that isn't one.
|
|
1825
|
+
state.graph.disabledInfo = statsResult;
|
|
1826
|
+
state.graph.queryError = null;
|
|
1827
|
+
state.graph.nodes = [];
|
|
1828
|
+
state.graph.edges = [];
|
|
1829
|
+
state.graph.stats = {};
|
|
1830
|
+
state.graph.loaded = true;
|
|
1831
|
+
renderGraphSidebar();
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
state.graph.disabledInfo = null;
|
|
1688
1835
|
if (queryResult === null) {
|
|
1689
1836
|
// api() returns null only on non-2xx or a transport error; an
|
|
1690
1837
|
// empty graph would come back as { nodes: [], edges: [] }.
|
|
@@ -1751,6 +1898,17 @@
|
|
|
1751
1898
|
|
|
1752
1899
|
var html = '';
|
|
1753
1900
|
|
|
1901
|
+
if (state.graph.disabledInfo) {
|
|
1902
|
+
html += '<div style="margin:8px 0;padding:10px 12px;border:1px solid var(--border);background:var(--bg-alt);font-size:12px;color:var(--ink);line-height:1.5;">';
|
|
1903
|
+
html += '<div style="font-weight:600;margin-bottom:4px;">Knowledge graph is off</div>';
|
|
1904
|
+
html += '<div style="font-size:11px;color:var(--ink-muted);">' + esc(state.graph.disabledInfo.enableHow || 'Set ' + (state.graph.disabledInfo.flag || 'GRAPH_EXTRACTION_ENABLED') + '=true and restart.') + '</div>';
|
|
1905
|
+
if (state.graph.disabledInfo.docsHref) {
|
|
1906
|
+
html += '<a href="' + esc(state.graph.disabledInfo.docsHref) + '" target="_blank" rel="noopener" style="font-size:11px;">docs →</a>';
|
|
1907
|
+
}
|
|
1908
|
+
html += '</div>';
|
|
1909
|
+
sb.innerHTML = html;
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1754
1912
|
// #753: error banner stays above the search box so a failed
|
|
1755
1913
|
// graph/query doesn't read as "0 nodes".
|
|
1756
1914
|
if (state.graph.queryError) {
|
|
@@ -1783,7 +1941,10 @@
|
|
|
1783
1941
|
html += '<label class="filter-item"><input type="checkbox" checked data-type="' + esc(type) + '"><span class="filter-dot" style="background:' + color + '"></span>' + esc(type) + '</label>';
|
|
1784
1942
|
});
|
|
1785
1943
|
|
|
1786
|
-
|
|
1944
|
+
if (state.graph.nodes.length > 0 && state.graph.edges.length === 0) {
|
|
1945
|
+
html += '<div style="margin:10px 0;padding:10px 12px;border:1px solid var(--border-light);background:var(--bg-subtle);font-size:11px;color:var(--ink-muted);line-height:1.5;">Entities extracted, no relations between them yet. Nodes are grouped by kind; edges appear as extraction sees entities acting on each other across more sessions (larger models find them faster).</div>';
|
|
1946
|
+
}
|
|
1947
|
+
html += '<div class="graph-legend" style="display:none"><h3>Legend</h3>';
|
|
1787
1948
|
var shapeLabels = { rect: '▭', circle: '●', diamond: '◆', hexagon: '⬢' };
|
|
1788
1949
|
var shownShapes = {};
|
|
1789
1950
|
Object.keys(NODE_COLORS).forEach(function(type) {
|
|
@@ -1843,21 +2004,38 @@
|
|
|
1843
2004
|
edgeMap[e.targetNodeId] = (edgeMap[e.targetNodeId] || 0) + 1;
|
|
1844
2005
|
});
|
|
1845
2006
|
|
|
2007
|
+
// With no (or few) edges a pure force layout is a meaningless
|
|
2008
|
+
// scatter. Anchor each type to its own cluster center so the
|
|
2009
|
+
// picture reads as "here is what memory knows, grouped by kind";
|
|
2010
|
+
// once real relations exist the edge springs dominate instead.
|
|
2011
|
+
var types = [];
|
|
2012
|
+
state.graph.nodes.forEach(function(n) {
|
|
2013
|
+
if (types.indexOf(n.type) === -1) types.push(n.type);
|
|
2014
|
+
});
|
|
2015
|
+
var typeCenters = {};
|
|
2016
|
+
types.forEach(function(t, ti) {
|
|
2017
|
+
var a = (2 * Math.PI * ti) / Math.max(types.length, 1) - Math.PI / 2;
|
|
2018
|
+
var cr = types.length > 1 ? Math.min(cw, ch) * 0.28 : 0;
|
|
2019
|
+
typeCenters[t] = { x: Math.cos(a) * cr, y: Math.sin(a) * cr };
|
|
2020
|
+
});
|
|
2021
|
+
graphSim.typeCenters = typeCenters;
|
|
2022
|
+
graphSim.clustered = state.graph.edges.length < state.graph.nodes.length / 2;
|
|
2023
|
+
|
|
1846
2024
|
graphSim.nodes = state.graph.nodes.map(function(n, i) {
|
|
1847
|
-
var angle = (2 * Math.PI * i) / Math.max(state.graph.nodes.length, 1);
|
|
1848
|
-
var radius = Math.min(cw, ch) * 0.3;
|
|
1849
2025
|
var deg = edgeMap[n.id] || 0;
|
|
2026
|
+
var c = typeCenters[n.type] || { x: 0, y: 0 };
|
|
1850
2027
|
return {
|
|
1851
2028
|
id: n.id, type: n.type, name: n.name, properties: n.properties,
|
|
1852
|
-
x:
|
|
1853
|
-
y:
|
|
2029
|
+
x: c.x + (Math.random() - 0.5) * 150,
|
|
2030
|
+
y: c.y + (Math.random() - 0.5) * 150,
|
|
1854
2031
|
vx: 0, vy: 0,
|
|
1855
|
-
r: Math.max(
|
|
2032
|
+
r: Math.max(11, Math.min(22, 11 + deg * 2.5))
|
|
1856
2033
|
};
|
|
1857
2034
|
});
|
|
1858
2035
|
graphSim.edges = state.graph.edges.slice();
|
|
1859
2036
|
graphSim.running = true;
|
|
1860
2037
|
graphSim.dragNode = null;
|
|
2038
|
+
graphSim.autoFitPending = true;
|
|
1861
2039
|
|
|
1862
2040
|
setupGraphInteraction(canvas);
|
|
1863
2041
|
runSimulation();
|
|
@@ -2071,8 +2249,14 @@
|
|
|
2071
2249
|
fx += (dx / dist) * force;
|
|
2072
2250
|
fy += (dy / dist) * force;
|
|
2073
2251
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2252
|
+
if (graphSim.clustered && graphSim.typeCenters && graphSim.typeCenters[n.type]) {
|
|
2253
|
+
var tc = graphSim.typeCenters[n.type];
|
|
2254
|
+
fx += (tc.x - n.x) * 0.006;
|
|
2255
|
+
fy += (tc.y - n.y) * 0.006;
|
|
2256
|
+
} else {
|
|
2257
|
+
fx -= n.x * centerGravity;
|
|
2258
|
+
fy -= n.y * centerGravity;
|
|
2259
|
+
}
|
|
2076
2260
|
var nvx = (n.vx + fx) * damping;
|
|
2077
2261
|
var nvy = (n.vy + fy) * damping;
|
|
2078
2262
|
// Velocity cap (#563): keep any single node from being launched
|
|
@@ -2105,6 +2289,10 @@
|
|
|
2105
2289
|
totalKineticEnergy += n.vx * n.vx + n.vy * n.vy;
|
|
2106
2290
|
});
|
|
2107
2291
|
|
|
2292
|
+
if (graphSim.autoFitPending && graphSim.tickCount > 45) {
|
|
2293
|
+
graphSim.autoFitPending = false;
|
|
2294
|
+
fitGraphToView();
|
|
2295
|
+
}
|
|
2108
2296
|
// park the simulation when the layout is quiet to save CPU.
|
|
2109
2297
|
// Pick up again when a drag/interaction wakes the loop.
|
|
2110
2298
|
var rmsVelocity = nodes.length > 0 ? Math.sqrt(totalKineticEnergy / nodes.length) : 0;
|
|
@@ -2162,6 +2350,27 @@
|
|
|
2162
2350
|
}
|
|
2163
2351
|
}
|
|
2164
2352
|
|
|
2353
|
+
function fitGraphToView() {
|
|
2354
|
+
var canvas = graphSim.canvas;
|
|
2355
|
+
if (!canvas || graphSim.nodes.length === 0) return;
|
|
2356
|
+
var w = canvas.width / window.devicePixelRatio;
|
|
2357
|
+
var h = canvas.height / window.devicePixelRatio;
|
|
2358
|
+
var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
2359
|
+
graphSim.nodes.forEach(function(n) {
|
|
2360
|
+
if (n.x < minX) minX = n.x;
|
|
2361
|
+
if (n.x > maxX) maxX = n.x;
|
|
2362
|
+
if (n.y < minY) minY = n.y;
|
|
2363
|
+
if (n.y > maxY) maxY = n.y;
|
|
2364
|
+
});
|
|
2365
|
+
var pad = 150;
|
|
2366
|
+
var spanX = (maxX - minX) + pad * 2;
|
|
2367
|
+
var spanY = (maxY - minY) + pad * 2;
|
|
2368
|
+
var z = Math.min(w / spanX, h / spanY);
|
|
2369
|
+
graphSim.zoom = Math.max(0.35, Math.min(1.4, z));
|
|
2370
|
+
graphSim.panX = w / 2 - ((minX + maxX) / 2) * graphSim.zoom;
|
|
2371
|
+
graphSim.panY = h / 2 - ((minY + maxY) / 2) * graphSim.zoom;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2165
2374
|
function renderGraph() {
|
|
2166
2375
|
var ctx = graphSim.ctx;
|
|
2167
2376
|
var canvas = graphSim.canvas;
|
|
@@ -2192,6 +2401,15 @@
|
|
|
2192
2401
|
graphSim.nodes.forEach(function(n) { nodeMap[n.id] = n; });
|
|
2193
2402
|
|
|
2194
2403
|
var searchActive = graphSearchTerm.length > 0;
|
|
2404
|
+
var drawnLabelRects = [];
|
|
2405
|
+
function labelFits(x, y, w2, h2) {
|
|
2406
|
+
for (var li = 0; li < drawnLabelRects.length; li++) {
|
|
2407
|
+
var r = drawnLabelRects[li];
|
|
2408
|
+
if (x < r.x + r.w && x + w2 > r.x && y < r.y + r.h && y + h2 > r.y) return false;
|
|
2409
|
+
}
|
|
2410
|
+
drawnLabelRects.push({ x: x, y: y, w: w2, h: h2 });
|
|
2411
|
+
return true;
|
|
2412
|
+
}
|
|
2195
2413
|
var totalVisible = graphSim.nodes.filter(function(n) { return state.graph.filters[n.type]; }).length;
|
|
2196
2414
|
var isDense = totalVisible > 40;
|
|
2197
2415
|
var labelZoomThreshold = isDense ? 1.5 : 0.5;
|
|
@@ -2290,7 +2508,7 @@
|
|
|
2290
2508
|
var isSelected = selectedId === n.id;
|
|
2291
2509
|
var isHovered = hoverNodeId === n.id;
|
|
2292
2510
|
var matchesSearch = !searchActive || n.name.toLowerCase().includes(graphSearchTerm);
|
|
2293
|
-
var isFocusFaded = focusNodeId && n.id !== focusNodeId && !graphSim.edges.some(function(ed) {
|
|
2511
|
+
var isFocusFaded = graphSim.edges.length > 0 && focusNodeId && n.id !== focusNodeId && !graphSim.edges.some(function(ed) {
|
|
2294
2512
|
return (ed.sourceNodeId === focusNodeId && ed.targetNodeId === n.id) ||
|
|
2295
2513
|
(ed.targetNodeId === focusNodeId && ed.sourceNodeId === n.id);
|
|
2296
2514
|
});
|
|
@@ -2343,6 +2561,7 @@
|
|
|
2343
2561
|
var showLabel = matchesSearch && !isFocusFaded && (
|
|
2344
2562
|
isSelected || isHovered ||
|
|
2345
2563
|
(searchActive && matchesSearch) ||
|
|
2564
|
+
(totalVisible <= 30 && graphSim.zoom > 0.5) ||
|
|
2346
2565
|
(!isDense && graphSim.zoom > labelZoomThreshold) ||
|
|
2347
2566
|
(isDense && graphSim.zoom > labelZoomThreshold && n.r > 10)
|
|
2348
2567
|
);
|
|
@@ -2357,7 +2576,13 @@
|
|
|
2357
2576
|
var labelW = textW + (16 * zoomInv);
|
|
2358
2577
|
var labelH = 20 * zoomInv;
|
|
2359
2578
|
var labelY = n.y + n.r + (8 * zoomInv); // Top of the background pill
|
|
2360
|
-
|
|
2579
|
+
// Selected/hovered labels always win; others draw only into
|
|
2580
|
+
// free space so zoomed-out views degrade to fewer labels
|
|
2581
|
+
// instead of a pile of overlapping pills.
|
|
2582
|
+
if (!isSelected && !isHovered && !labelFits(n.x - labelW / 2, labelY, labelW, labelH)) {
|
|
2583
|
+
ctx.restore();
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2361
2586
|
ctx.fillStyle = isDarkMode() ? 'rgba(30,30,35,0.92)' : 'rgba(255,255,255,0.92)';
|
|
2362
2587
|
ctx.beginPath();
|
|
2363
2588
|
ctx.roundRect ? ctx.roundRect(n.x - labelW / 2, labelY, labelW, labelH, 4 * zoomInv) : ctx.rect(n.x - labelW / 2, labelY, labelW, labelH);
|
|
@@ -2388,7 +2613,7 @@
|
|
|
2388
2613
|
|
|
2389
2614
|
async function loadMemories() {
|
|
2390
2615
|
var el = document.getElementById('view-memories');
|
|
2391
|
-
el.innerHTML = '<div class="loading">Loading memories...</div>';
|
|
2616
|
+
if (!state.memories.loaded) el.innerHTML = '<div class="loading">Loading memories...</div>';
|
|
2392
2617
|
// cap at 2000 so the viewer remains responsive on large
|
|
2393
2618
|
// corpora. Older endpoints returned the full unbounded list which
|
|
2394
2619
|
// hit the iii invocation timeout and the UI fell through to 0.
|
|
@@ -2471,7 +2696,8 @@
|
|
|
2471
2696
|
var strength = Math.round(rawStrength <= 1 ? rawStrength * 100 : rawStrength * 10);
|
|
2472
2697
|
if (strength > 100) strength = 100;
|
|
2473
2698
|
var barColor = strength > 70 ? 'var(--green)' : strength > 40 ? 'var(--yellow)' : 'var(--red)';
|
|
2474
|
-
|
|
2699
|
+
var expanded = state.memories.selectedId === m.id;
|
|
2700
|
+
html += '<tr class="memory-row' + (expanded ? ' selected' : '') + '" data-action="select-memory" data-memory-id="' + esc(m.id) + '" role="button" tabindex="0" aria-expanded="' + (expanded ? 'true' : 'false') + '" style="cursor:pointer;">';
|
|
2475
2701
|
var preview = (m.content || '').split('\n').slice(0, 2).join(' ').trim();
|
|
2476
2702
|
var previewHtml = esc(truncate(preview, 150));
|
|
2477
2703
|
if (search && search.length > 2) {
|
|
@@ -2492,6 +2718,24 @@
|
|
|
2492
2718
|
html += '<td style="font-size:11px;color:var(--ink-faint);font-family:var(--font-mono);">' + esc(formatTime(m.updatedAt)) + '</td>';
|
|
2493
2719
|
html += '<td><button class="btn btn-danger" style="font-size:9px;padding:2px 8px;" data-action="delete-memory" data-memory-id="' + esc(m.id) + '" data-memory-title="' + esc(m.title || '') + '">Delete</button></td>';
|
|
2494
2720
|
html += '</tr>';
|
|
2721
|
+
if (expanded) {
|
|
2722
|
+
html += '<tr class="memory-detail-row"><td colspan="6" style="background:var(--bg-alt);padding:14px 16px;">';
|
|
2723
|
+
html += '<div style="font-size:13px;color:var(--ink);white-space:pre-wrap;line-height:1.5;margin-bottom:10px;">' + esc(m.content || '') + '</div>';
|
|
2724
|
+
html += '<div style="display:flex;gap:16px;flex-wrap:wrap;font-size:11px;color:var(--ink-muted);font-family:var(--font-mono);margin-bottom:10px;">';
|
|
2725
|
+
html += '<span>id: ' + esc(m.id) + '</span>';
|
|
2726
|
+
if (m.origin && m.origin.channel) {
|
|
2727
|
+
html += '<span>origin: ' + esc(m.origin.channel) + (m.origin.detail ? ' (' + esc(m.origin.detail) + ')' : '') + '</span>';
|
|
2728
|
+
}
|
|
2729
|
+
if (m.project) html += '<span>project: ' + esc(m.project) + '</span>';
|
|
2730
|
+
if (m.createdAt) html += '<span>created: ' + esc(formatTime(m.createdAt)) + '</span>';
|
|
2731
|
+
if (m.supersedes && m.supersedes.length > 0) html += '<span>supersedes: ' + esc(m.supersedes.join(', ')) + '</span>';
|
|
2732
|
+
if (m.files && m.files.length > 0) html += '<span>files: ' + esc(m.files.join(', ')) + '</span>';
|
|
2733
|
+
if (m.sessionIds && m.sessionIds.length > 0) html += '<span>sessions: ' + m.sessionIds.length + '</span>';
|
|
2734
|
+
html += '</div>';
|
|
2735
|
+
html += '<details><summary style="cursor:pointer;font-size:11px;color:var(--ink-faint);font-family:var(--font-mono);">raw record</summary>';
|
|
2736
|
+
html += '<pre style="margin-top:8px;padding:10px;background:var(--bg);border:1px solid var(--border);font-size:11px;line-height:1.5;overflow-x:auto;">' + esc(JSON.stringify(m, null, 2)) + '</pre></details>';
|
|
2737
|
+
html += '</td></tr>';
|
|
2738
|
+
}
|
|
2495
2739
|
});
|
|
2496
2740
|
html += '</table>';
|
|
2497
2741
|
}
|
|
@@ -2533,13 +2777,19 @@
|
|
|
2533
2777
|
|
|
2534
2778
|
async function loadTimeline() {
|
|
2535
2779
|
var el = document.getElementById('view-timeline');
|
|
2536
|
-
el.innerHTML = '<div class="loading">Loading timeline...</div>';
|
|
2780
|
+
if (!state.timeline.loaded) el.innerHTML = '<div class="loading">Loading timeline...</div>';
|
|
2537
2781
|
var sessResult = await apiGet('sessions');
|
|
2538
2782
|
var sessions = (sessResult && sessResult.sessions) || [];
|
|
2539
2783
|
state.timeline.loaded = true;
|
|
2540
2784
|
|
|
2541
2785
|
if (sessions.length > 0 && !state.timeline.sessionId) {
|
|
2542
|
-
|
|
2786
|
+
// Default to the session with the most observations — the newest
|
|
2787
|
+
// one is often a sparse just-started session, which made the tab
|
|
2788
|
+
// look empty on first open.
|
|
2789
|
+
var sorted = sessions.slice().sort(function(a, b) {
|
|
2790
|
+
return (b.observationCount || 0) - (a.observationCount || 0) ||
|
|
2791
|
+
(b.startedAt || '').localeCompare(a.startedAt || '');
|
|
2792
|
+
});
|
|
2543
2793
|
var firstSelectable = sorted.find(function(s) { return sessionId(s); });
|
|
2544
2794
|
state.timeline.sessionId = firstSelectable ? sessionId(firstSelectable) : '';
|
|
2545
2795
|
}
|
|
@@ -2679,7 +2929,7 @@
|
|
|
2679
2929
|
html += '<span class="obs-time">' + esc(shortTime(o.timestamp)) + '</span>';
|
|
2680
2930
|
html += '</div></div>';
|
|
2681
2931
|
|
|
2682
|
-
if (o.subtitle) html += '<div class="obs-subtitle">' + esc(o.subtitle) + '</div>';
|
|
2932
|
+
if (o.subtitle) html += '<div class="obs-subtitle">' + esc(humanizeSubtitle(o.subtitle)) + '</div>';
|
|
2683
2933
|
|
|
2684
2934
|
html += '<div style="margin-top:4px;">';
|
|
2685
2935
|
html += '<span class="badge" style="border-color:' + typeColor + ';color:' + typeColor + ';margin-right:4px;">' + esc(type.replace(/_/g, ' ')) + '</span>';
|
|
@@ -2759,7 +3009,7 @@
|
|
|
2759
3009
|
|
|
2760
3010
|
async function loadActivity() {
|
|
2761
3011
|
var el = document.getElementById('view-activity');
|
|
2762
|
-
el.innerHTML = '<div class="loading">Loading activity...</div>';
|
|
3012
|
+
if (!state.activity.loaded) el.innerHTML = '<div class="loading">Loading activity...</div>';
|
|
2763
3013
|
var results = await Promise.all([
|
|
2764
3014
|
apiGet('sessions'),
|
|
2765
3015
|
apiGet('audit?limit=200')
|
|
@@ -2896,7 +3146,7 @@
|
|
|
2896
3146
|
|
|
2897
3147
|
async function loadSessions() {
|
|
2898
3148
|
var el = document.getElementById('view-sessions');
|
|
2899
|
-
el.innerHTML = '<div class="loading">Loading sessions...</div>';
|
|
3149
|
+
if (!state.sessions.loaded) el.innerHTML = '<div class="loading">Loading sessions...</div>';
|
|
2900
3150
|
var result = await apiGet('sessions');
|
|
2901
3151
|
state.sessions.items = (result && result.sessions) || [];
|
|
2902
3152
|
state.sessions.loaded = true;
|
|
@@ -2909,7 +3159,7 @@
|
|
|
2909
3159
|
return (b.startedAt || '').localeCompare(a.startedAt || '');
|
|
2910
3160
|
});
|
|
2911
3161
|
|
|
2912
|
-
var html = '<div class="session-list">';
|
|
3162
|
+
var html = '<div class="sessions-layout"><div class="session-list">';
|
|
2913
3163
|
if (items.length === 0) {
|
|
2914
3164
|
html += '<div class="empty-state"><div class="empty-icon">🗒</div><p>No sessions</p></div>';
|
|
2915
3165
|
} else {
|
|
@@ -2917,7 +3167,7 @@
|
|
|
2917
3167
|
var statusBadge = s.status === 'active' ? 'badge-green' : s.status === 'completed' ? 'badge-blue' : 'badge-muted';
|
|
2918
3168
|
var id = sessionId(s);
|
|
2919
3169
|
var selected = id && state.sessions.selectedId === id;
|
|
2920
|
-
html += '<div class="session-item' + (selected ? ' selected' : '') + '"' + (id ? ' data-action="select-session" data-session-id="' + esc(id) + '"' : '') + '>';
|
|
3170
|
+
html += '<div class="session-item' + (selected ? ' selected' : '') + '"' + (id ? ' data-action="select-session" data-session-id="' + esc(id) + '" role="button" tabindex="0" aria-expanded="' + (selected ? 'true' : 'false') + '"' : '') + '>';
|
|
2921
3171
|
html += '<div class="session-top"><span class="session-project">' + esc(sessionDisplayName(s)) + '</span>';
|
|
2922
3172
|
html += '<span class="badge ' + statusBadge + '">' + esc(s.status) + '</span></div>';
|
|
2923
3173
|
var preview = s.firstPrompt || s.summary || '';
|
|
@@ -2931,7 +3181,7 @@
|
|
|
2931
3181
|
});
|
|
2932
3182
|
}
|
|
2933
3183
|
html += '</div>';
|
|
2934
|
-
html += '<div id="session-detail"></div>';
|
|
3184
|
+
html += '<div id="session-detail"></div></div>';
|
|
2935
3185
|
el.innerHTML = html;
|
|
2936
3186
|
|
|
2937
3187
|
if (state.sessions.selectedId) renderSessionDetail();
|
|
@@ -2940,6 +3190,18 @@
|
|
|
2940
3190
|
function selectSession(id) {
|
|
2941
3191
|
state.sessions.selectedId = state.sessions.selectedId === id ? null : id;
|
|
2942
3192
|
renderSessions();
|
|
3193
|
+
// On the stacked layout (narrow screens) the detail renders below
|
|
3194
|
+
// the list; bring it into view. The wide two-pane layout keeps the
|
|
3195
|
+
// panel sticky beside the list, so no scroll is needed there.
|
|
3196
|
+
if (
|
|
3197
|
+
state.sessions.selectedId &&
|
|
3198
|
+
window.matchMedia('(max-width: 1100px)').matches
|
|
3199
|
+
) {
|
|
3200
|
+
var panel = document.getElementById('session-detail');
|
|
3201
|
+
if (panel && panel.scrollIntoView) {
|
|
3202
|
+
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
2943
3205
|
}
|
|
2944
3206
|
|
|
2945
3207
|
async function renderSessionDetail() {
|
|
@@ -3063,7 +3325,7 @@
|
|
|
3063
3325
|
|
|
3064
3326
|
async function loadLessons() {
|
|
3065
3327
|
var el = document.getElementById('view-lessons');
|
|
3066
|
-
el.innerHTML = '<div class="loading">Loading lessons...</div>';
|
|
3328
|
+
if (!state.lessons.loaded) el.innerHTML = '<div class="loading">Loading lessons...</div>';
|
|
3067
3329
|
var result = await apiGet('lessons');
|
|
3068
3330
|
state.lessons.items = (result && result.lessons) || [];
|
|
3069
3331
|
state.lessons.loaded = true;
|
|
@@ -3095,16 +3357,17 @@
|
|
|
3095
3357
|
html += '<div class="empty-state">' +
|
|
3096
3358
|
'<div class="empty-icon">💡</div>' +
|
|
3097
3359
|
'<div class="empty-title">No lessons yet</div>' +
|
|
3098
|
-
'<div class="empty-lead">Lessons are
|
|
3360
|
+
'<div class="empty-lead">Lessons are short imperative rules (always/never/prefer/avoid) learned from past work — things you corrected once that the agent should never repeat. Confidence grows when they hold and decays when unused.</div>' +
|
|
3099
3361
|
'<pre class="empty-cmd"># Save a lesson explicitly\nmemory_lesson_save { rule, reason, confidence }\n\n# Or: Replay tab → Import JSONL auto-extracts lessons\n# from your past Claude Code sessions</pre>' +
|
|
3100
3362
|
'<div><a class="empty-link" href="https://github.com/rohitg00/agentmemory#lessons" target="_blank" rel="noopener">Lesson decay & scoring →</a></div>' +
|
|
3101
3363
|
'</div>';
|
|
3102
3364
|
} else {
|
|
3103
|
-
html += '<table><thead><tr><th>Lesson</th><th>Confidence</th><th>
|
|
3365
|
+
html += '<table><thead><tr><th>Lesson</th><th title="How battle-tested the rule is. Grows each time it is reinforced, decays weekly when unused.">Confidence</th><th title="Times the agent re-confirmed this rule.">Uses</th><th>Source</th><th>Project</th><th>Updated</th></tr></thead><tbody>';
|
|
3104
3366
|
items.forEach(function(l) {
|
|
3105
3367
|
var confPct = Math.round(l.confidence * 100);
|
|
3106
3368
|
var confColor = confPct >= 70 ? 'var(--green)' : confPct >= 40 ? 'var(--yellow)' : 'var(--red)';
|
|
3107
|
-
|
|
3369
|
+
var expanded = state.lessons.selectedId === l.id;
|
|
3370
|
+
html += '<tr class="lesson-row' + (expanded ? ' selected' : '') + '" data-action="select-lesson" data-lesson-id="' + esc(l.id) + '" role="button" tabindex="0" aria-expanded="' + (expanded ? 'true' : 'false') + '" style="cursor:pointer;">';
|
|
3108
3371
|
html += '<td style="max-width:400px;">' + esc(truncate(l.content, 120)) + (l.context ? '<div style="font-size:11px;color:var(--ink-faint);margin-top:2px;">' + esc(truncate(l.context, 80)) + '</div>' : '') + '</td>';
|
|
3109
3372
|
html += '<td><div class="gauge" style="min-width:80px;"><div class="gauge-bar"><div class="gauge-fill" style="width:' + confPct + '%;background:' + confColor + '"></div></div><span class="gauge-value" style="font-size:11px;">' + confPct + '%</span></div></td>';
|
|
3110
3373
|
html += '<td style="text-align:center;">' + (l.reinforcements || 0) + '</td>';
|
|
@@ -3112,6 +3375,21 @@
|
|
|
3112
3375
|
html += '<td style="font-size:12px;color:var(--ink-muted);">' + esc(l.project || '-') + '</td>';
|
|
3113
3376
|
html += '<td style="font-size:12px;color:var(--ink-muted);">' + shortTime(l.updatedAt) + '</td>';
|
|
3114
3377
|
html += '</tr>';
|
|
3378
|
+
if (expanded) {
|
|
3379
|
+
html += '<tr class="lesson-detail-row"><td colspan="6" style="background:var(--bg-alt);padding:14px 16px;">';
|
|
3380
|
+
html += '<div style="font-size:13px;color:var(--ink);white-space:pre-wrap;line-height:1.5;margin-bottom:8px;">' + esc(l.content) + '</div>';
|
|
3381
|
+
if (l.context) html += '<div style="font-size:12px;color:var(--ink-muted);margin-bottom:8px;"><span style="font-family:var(--font-ui);font-size:10px;text-transform:uppercase;letter-spacing:0.1em;">Why learned</span><br>' + esc(l.context) + '</div>';
|
|
3382
|
+
html += '<div style="display:flex;gap:16px;flex-wrap:wrap;font-size:11px;color:var(--ink-muted);font-family:var(--font-mono);margin-bottom:8px;">';
|
|
3383
|
+
html += '<span>id: ' + esc(l.id) + '</span>';
|
|
3384
|
+
if (l.tags && l.tags.length) html += '<span>tags: ' + esc(l.tags.join(', ')) + '</span>';
|
|
3385
|
+
if (l.createdAt) html += '<span>learned: ' + esc(formatTime(l.createdAt)) + '</span>';
|
|
3386
|
+
if (l.lastReinforcedAt) html += '<span>last confirmed: ' + esc(formatTime(l.lastReinforcedAt)) + '</span>';
|
|
3387
|
+
if (l.sourceIds && l.sourceIds.length) html += '<span>from ' + l.sourceIds.length + ' session(s)</span>';
|
|
3388
|
+
html += '</div>';
|
|
3389
|
+
html += '<details><summary style="cursor:pointer;font-size:11px;color:var(--ink-faint);font-family:var(--font-mono);">raw record</summary>';
|
|
3390
|
+
html += '<pre style="margin-top:8px;padding:10px;background:var(--bg);border:1px solid var(--border);font-size:11px;line-height:1.5;overflow-x:auto;">' + esc(JSON.stringify(l, null, 2)) + '</pre></details>';
|
|
3391
|
+
html += '</td></tr>';
|
|
3392
|
+
}
|
|
3115
3393
|
});
|
|
3116
3394
|
html += '</tbody></table>';
|
|
3117
3395
|
}
|
|
@@ -3125,7 +3403,7 @@
|
|
|
3125
3403
|
|
|
3126
3404
|
async function loadActions() {
|
|
3127
3405
|
var el = document.getElementById('view-actions');
|
|
3128
|
-
el.innerHTML = '<div class="loading">Loading actions...</div>';
|
|
3406
|
+
if (!state.actions.loaded) el.innerHTML = '<div class="loading">Loading actions...</div>';
|
|
3129
3407
|
var results = await Promise.all([apiGet('actions'), apiGet('frontier')]);
|
|
3130
3408
|
state.actions.items = (results[0] && results[0].actions) || [];
|
|
3131
3409
|
state.actions.frontier = (results[1] && (results[1].frontier || results[1].actions)) || [];
|
|
@@ -3136,6 +3414,10 @@
|
|
|
3136
3414
|
function renderActions() {
|
|
3137
3415
|
var el = document.getElementById('view-actions');
|
|
3138
3416
|
var items = state.actions.items;
|
|
3417
|
+
var introCard = '<div class="card" style="margin-bottom:12px;padding:12px;background:var(--bg-subtle);">' +
|
|
3418
|
+
'<div style="font-size:13px;color:var(--ink-muted);line-height:1.5;">' +
|
|
3419
|
+
'<strong>Actions</strong> are follow-ups the agent surfaced during sessions — decisions to revisit, files to inspect, tasks blocked on input. Status flows pending → active → done/blocked; the <em>frontier</em> marks what is unblocked and ready to pick up next.' +
|
|
3420
|
+
'</div></div>';
|
|
3139
3421
|
var search = state.actions.search.toLowerCase();
|
|
3140
3422
|
var statusFilter = state.actions.statusFilter;
|
|
3141
3423
|
var frontierIds = new Set((state.actions.frontier || []).map(function(a) { return a.id; }));
|
|
@@ -3149,7 +3431,8 @@
|
|
|
3149
3431
|
items = items.filter(function(a) { return a.status === statusFilter; });
|
|
3150
3432
|
}
|
|
3151
3433
|
|
|
3152
|
-
var html =
|
|
3434
|
+
var html = introCard;
|
|
3435
|
+
html += '<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;">';
|
|
3153
3436
|
html += '<input id="actions-search" class="search-input" type="text" placeholder="Search actions..." value="' + esc(state.actions.search) + '" style="flex:1;min-width:200px" />';
|
|
3154
3437
|
html += '<select id="actions-status-filter" style="padding:4px 8px;font-size:12px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--ink);">';
|
|
3155
3438
|
html += '<option value="">All statuses</option>';
|
|
@@ -3200,7 +3483,7 @@
|
|
|
3200
3483
|
|
|
3201
3484
|
async function loadCrystals() {
|
|
3202
3485
|
var el = document.getElementById('view-crystals');
|
|
3203
|
-
el.innerHTML = '<div class="loading">Loading crystals...</div>';
|
|
3486
|
+
if (!state.crystals.loaded) el.innerHTML = '<div class="loading">Loading crystals...</div>';
|
|
3204
3487
|
var results = await Promise.all([apiGet('crystals'), apiGet('lessons')]);
|
|
3205
3488
|
state.crystals.items = (results[0] && results[0].crystals) || [];
|
|
3206
3489
|
var lessonMap = {};
|
|
@@ -3251,7 +3534,7 @@
|
|
|
3251
3534
|
html += '<div class="empty-state">' +
|
|
3252
3535
|
'<div class="empty-icon">💎</div>' +
|
|
3253
3536
|
'<div class="empty-title">No crystals yet</div>' +
|
|
3254
|
-
'<div class="empty-lead">Crystals are
|
|
3537
|
+
'<div class="empty-lead">Crystals are frozen snapshots of completed work — one session’s narrative, key outcomes, files touched, and lessons surfaced, kept after raw observations are pruned so the next session gets fast context.</div>' +
|
|
3255
3538
|
'<pre class="empty-cmd"># Auto: import a JSONL transcript\n# Replay tab → Import JSONL\n\n# Manual: crystallize a specific session\nmemory_crystallize { sessionId }</pre>' +
|
|
3256
3539
|
'<div><a class="empty-link" href="https://github.com/rohitg00/agentmemory#crystals" target="_blank" rel="noopener">Crystal pipeline →</a></div>' +
|
|
3257
3540
|
'</div>';
|
|
@@ -3313,7 +3596,7 @@
|
|
|
3313
3596
|
|
|
3314
3597
|
async function loadAudit() {
|
|
3315
3598
|
var el = document.getElementById('view-audit');
|
|
3316
|
-
el.innerHTML = '<div class="loading">Loading audit log...</div>';
|
|
3599
|
+
if (!state.audit.loaded) el.innerHTML = '<div class="loading">Loading audit log...</div>';
|
|
3317
3600
|
var result = await apiGet('audit?limit=100');
|
|
3318
3601
|
state.audit.entries = (result && result.entries) || [];
|
|
3319
3602
|
state.audit.loaded = true;
|
|
@@ -3437,7 +3720,7 @@
|
|
|
3437
3720
|
html += '<div class="card"><div class="card-title">Top Concepts</div>';
|
|
3438
3721
|
var concepts = p.topConcepts || [];
|
|
3439
3722
|
if (concepts.length === 0) {
|
|
3440
|
-
html += '<div style="font-size:13px;color:var(--ink-faint);
|
|
3723
|
+
html += '<div style="font-size:13px;color:var(--ink-faint);line-height:1.5;">No concepts yet. Concepts are tagged when observations are compressed by an LLM (<code>AGENTMEMORY_AUTO_COMPRESS=true</code> + provider key) or attached to saved memories.</div>';
|
|
3441
3724
|
} else {
|
|
3442
3725
|
var maxC = Math.max.apply(null, concepts.map(function(c) { return c.frequency; })) || 1;
|
|
3443
3726
|
html += '<div class="bar-chart">';
|
|
@@ -3846,6 +4129,17 @@
|
|
|
3846
4129
|
}
|
|
3847
4130
|
});
|
|
3848
4131
|
fetchFlags();
|
|
4132
|
+
// Keyboard activation for role="button" cards (session items). Click
|
|
4133
|
+
// delegation alone leaves them unreachable for keyboard and AT users.
|
|
4134
|
+
document.addEventListener('keydown', function(e) {
|
|
4135
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
4136
|
+
if (!(e.target instanceof Element)) return;
|
|
4137
|
+
var card = e.target.closest('[data-action][role="button"], [data-action][role="link"]');
|
|
4138
|
+
if (!card) return;
|
|
4139
|
+
e.preventDefault();
|
|
4140
|
+
card.click();
|
|
4141
|
+
});
|
|
4142
|
+
|
|
3849
4143
|
document.addEventListener('click', function(e) {
|
|
3850
4144
|
if (!(e.target instanceof Element)) return;
|
|
3851
4145
|
var target = e.target.closest('[data-action]');
|
|
@@ -3926,6 +4220,27 @@
|
|
|
3926
4220
|
if (sessionId) selectSession(sessionId);
|
|
3927
4221
|
return;
|
|
3928
4222
|
}
|
|
4223
|
+
if (action === 'goto-tab') {
|
|
4224
|
+
var gotoTab = target.getAttribute('data-tab');
|
|
4225
|
+
if (gotoTab) switchTab(gotoTab);
|
|
4226
|
+
return;
|
|
4227
|
+
}
|
|
4228
|
+
if (action === 'select-lesson') {
|
|
4229
|
+
var lsnId = target.getAttribute('data-lesson-id');
|
|
4230
|
+
if (lsnId) {
|
|
4231
|
+
state.lessons.selectedId = state.lessons.selectedId === lsnId ? null : lsnId;
|
|
4232
|
+
renderLessons();
|
|
4233
|
+
}
|
|
4234
|
+
return;
|
|
4235
|
+
}
|
|
4236
|
+
if (action === 'select-memory') {
|
|
4237
|
+
var memId = target.getAttribute('data-memory-id');
|
|
4238
|
+
if (memId) {
|
|
4239
|
+
state.memories.selectedId = state.memories.selectedId === memId ? null : memId;
|
|
4240
|
+
renderMemories();
|
|
4241
|
+
}
|
|
4242
|
+
return;
|
|
4243
|
+
}
|
|
3929
4244
|
if (action === 'end-session') {
|
|
3930
4245
|
var endSessionId = target.getAttribute('data-session-id');
|
|
3931
4246
|
if (endSessionId) endSession(endSessionId);
|
|
@@ -4175,8 +4490,83 @@
|
|
|
4175
4490
|
});
|
|
4176
4491
|
|
|
4177
4492
|
switchTab(tabFromRoute(), { replaceRoute: true });
|
|
4178
|
-
|
|
4493
|
+
// Resolve the stream WebSocket target from the server before the first
|
|
4494
|
+
// connect. The old viewerPort-1 arithmetic breaks whenever the viewer
|
|
4495
|
+
// binds a fallback port (3113 taken → viewer on 3114 → 3113 is another
|
|
4496
|
+
// HTTP server, not the streams endpoint) — every retry then fails and
|
|
4497
|
+
// the viewer silently degrades to 10s polling.
|
|
4498
|
+
(async function initWs() {
|
|
4499
|
+
try {
|
|
4500
|
+
var live = await api('livez', { signal: AbortSignal.timeout(5000) });
|
|
4501
|
+
if (live && typeof live.streamsPort === 'number' && live.streamsPort > 0) {
|
|
4502
|
+
WS_URL = wsProto + '//' + window.location.hostname + ':' + live.streamsPort;
|
|
4503
|
+
WS_DIRECT_URL = WS_URL + '/stream/mem-live/viewer';
|
|
4504
|
+
}
|
|
4505
|
+
} catch (_) {}
|
|
4506
|
+
connectWs();
|
|
4507
|
+
})();
|
|
4179
4508
|
startDashboardAutoRefresh();
|
|
4509
|
+
|
|
4510
|
+
// Ambient background: an ordered-dither dot field that drifts very
|
|
4511
|
+
// slowly — the print-halftone cousin of the old static dot grid.
|
|
4512
|
+
// Renders at quarter resolution and ~12fps; a single static frame
|
|
4513
|
+
// under prefers-reduced-motion.
|
|
4514
|
+
(function ditherField() {
|
|
4515
|
+
var cv = document.getElementById('bg-dither');
|
|
4516
|
+
if (!cv || !cv.getContext) return;
|
|
4517
|
+
var ctx = cv.getContext('2d');
|
|
4518
|
+
var BAYER = [
|
|
4519
|
+
[0, 8, 2, 10],
|
|
4520
|
+
[12, 4, 14, 6],
|
|
4521
|
+
[3, 11, 1, 9],
|
|
4522
|
+
[15, 7, 13, 5]
|
|
4523
|
+
];
|
|
4524
|
+
var CELL = 7;
|
|
4525
|
+
var t = 0;
|
|
4526
|
+
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
4527
|
+
|
|
4528
|
+
function dotColor() {
|
|
4529
|
+
return document.documentElement.getAttribute('data-theme') === 'dark'
|
|
4530
|
+
? 'rgba(150, 154, 162, 0.16)'
|
|
4531
|
+
: 'rgba(90, 90, 84, 0.14)';
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
function draw() {
|
|
4535
|
+
var w = cv.width = Math.ceil(window.innerWidth / 2);
|
|
4536
|
+
var h = cv.height = Math.ceil(window.innerHeight / 2);
|
|
4537
|
+
ctx.clearRect(0, 0, w, h);
|
|
4538
|
+
ctx.fillStyle = dotColor();
|
|
4539
|
+
var cols = Math.ceil(w / CELL) + 1;
|
|
4540
|
+
var rows = Math.ceil(h / CELL) + 1;
|
|
4541
|
+
for (var y = 0; y < rows; y++) {
|
|
4542
|
+
for (var x = 0; x < cols; x++) {
|
|
4543
|
+
// Slow luminance swells drifting diagonally; the Bayer
|
|
4544
|
+
// threshold turns them into stable ordered-dither dots.
|
|
4545
|
+
var v =
|
|
4546
|
+
0.5 +
|
|
4547
|
+
0.5 *
|
|
4548
|
+
Math.sin(x * 0.11 + t * 0.013) *
|
|
4549
|
+
Math.cos(y * 0.13 - t * 0.011);
|
|
4550
|
+
if (v * 16 > BAYER[y % 4][x % 4] + 6.5) {
|
|
4551
|
+
ctx.fillRect(x * CELL, y * CELL, 1.4, 1.4);
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
draw();
|
|
4558
|
+
if (!reduced) {
|
|
4559
|
+
setInterval(function () {
|
|
4560
|
+
t++;
|
|
4561
|
+
draw();
|
|
4562
|
+
}, 85);
|
|
4563
|
+
}
|
|
4564
|
+
window.addEventListener('resize', draw);
|
|
4565
|
+
new MutationObserver(draw).observe(document.documentElement, {
|
|
4566
|
+
attributes: true,
|
|
4567
|
+
attributeFilter: ['data-theme']
|
|
4568
|
+
});
|
|
4569
|
+
})();
|
|
4180
4570
|
</script>
|
|
4181
4571
|
</body>
|
|
4182
4572
|
</html>
|