agentcache-core 0.9.9__py3-none-any.whl
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.
- agentcache/__init__.py +29 -0
- agentcache/app.py +312 -0
- agentcache/cli.py +346 -0
- agentcache/connect.py +724 -0
- agentcache/core/__init__.py +19 -0
- agentcache/core/audit_log.py +104 -0
- agentcache/core/config.py +51 -0
- agentcache/core/context_builder.py +209 -0
- agentcache/core/graph.py +120 -0
- agentcache/core/image_store.py +111 -0
- agentcache/core/infer.py +142 -0
- agentcache/core/kv_scopes.py +86 -0
- agentcache/core/lessons.py +242 -0
- agentcache/core/llm.py +596 -0
- agentcache/core/memory_store.py +207 -0
- agentcache/core/observation_store.py +576 -0
- agentcache/core/privacy.py +34 -0
- agentcache/core/project_profile.py +625 -0
- agentcache/core/search_service.py +444 -0
- agentcache/core/session_store.py +382 -0
- agentcache/core/slots.py +504 -0
- agentcache/db.py +359 -0
- agentcache/import_data.py +86 -0
- agentcache/legacy.py +94 -0
- agentcache/mcp_stdio.py +141 -0
- agentcache/py.typed +0 -0
- agentcache/replay_import.py +680 -0
- agentcache/routes/__init__.py +29 -0
- agentcache/routes/_deps.py +46 -0
- agentcache/routes/auth.py +68 -0
- agentcache/routes/graph.py +81 -0
- agentcache/routes/health.py +149 -0
- agentcache/routes/mcp.py +614 -0
- agentcache/routes/memories.py +116 -0
- agentcache/routes/migration.py +32 -0
- agentcache/routes/observations.py +253 -0
- agentcache/routes/search.py +80 -0
- agentcache/search.py +935 -0
- agentcache/storage/__init__.py +29 -0
- agentcache/storage/images.py +102 -0
- agentcache/storage/paths.py +116 -0
- agentcache/storage/scopes.py +9 -0
- agentcache/viewer/favicon.svg +1 -0
- agentcache/viewer/index.html +4235 -0
- agentcache/viewer_helpers.py +61 -0
- agentcache/workers.py +192 -0
- agentcache_core-0.9.9.dist-info/METADATA +194 -0
- agentcache_core-0.9.9.dist-info/RECORD +52 -0
- agentcache_core-0.9.9.dist-info/WHEEL +5 -0
- agentcache_core-0.9.9.dist-info/entry_points.txt +2 -0
- agentcache_core-0.9.9.dist-info/licenses/LICENSE +190 -0
- agentcache_core-0.9.9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,4235 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>agentcache viewer</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
+
<!-- Removed Google Fonts <link> in #323: the viewer CSP is strict
|
|
9
|
+
(default-src 'none', style-src 'unsafe-inline', font-src 'self')
|
|
10
|
+
and external stylesheets from fonts.googleapis.com were blocked,
|
|
11
|
+
producing console CSP violations that some browsers (Windows
|
|
12
|
+
Edge in particular) escalate into broader script-execution
|
|
13
|
+
issues. System-font fallbacks are already declared on every
|
|
14
|
+
--font-* CSS variable below, so the viewer renders fine with
|
|
15
|
+
Georgia / system serif / system sans / system mono. -->
|
|
16
|
+
<style>
|
|
17
|
+
:root {
|
|
18
|
+
--bg: #F9F9F7;
|
|
19
|
+
--bg-alt: #F0F0EC;
|
|
20
|
+
--bg-subtle: #F4F4F0;
|
|
21
|
+
--bg-inset: #E8E8E3;
|
|
22
|
+
--border: #111111;
|
|
23
|
+
--border-light: #D4D4CF;
|
|
24
|
+
--border-heavy: #111111;
|
|
25
|
+
--ink: #111111;
|
|
26
|
+
--ink-secondary: #333333;
|
|
27
|
+
--ink-muted: #666666;
|
|
28
|
+
--ink-faint: #999999;
|
|
29
|
+
--accent: #CC0000;
|
|
30
|
+
--accent-light: #FF1A1A;
|
|
31
|
+
--cream: #F5F0E8;
|
|
32
|
+
--node-file: #2D6A4F;
|
|
33
|
+
--node-function: #1D4E89;
|
|
34
|
+
--node-concept: #B8860B;
|
|
35
|
+
--node-error: #CC0000;
|
|
36
|
+
--node-decision: #6B3FA0;
|
|
37
|
+
--node-pattern: #2563EB;
|
|
38
|
+
--node-library: #C2410C;
|
|
39
|
+
--node-person: #111111;
|
|
40
|
+
--green: #2D6A4F;
|
|
41
|
+
--blue: #1D4E89;
|
|
42
|
+
--yellow: #B8860B;
|
|
43
|
+
--red: #CC0000;
|
|
44
|
+
--purple: #6B3FA0;
|
|
45
|
+
--orange: #C2410C;
|
|
46
|
+
--cyan: #0E7490;
|
|
47
|
+
--font-display: 'Playfair Display', Georgia, 'Times New Roman', serif;
|
|
48
|
+
--font-body: 'Lora', Georgia, serif;
|
|
49
|
+
--font-ui: 'Inter', -apple-system, sans-serif;
|
|
50
|
+
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
|
|
51
|
+
}
|
|
52
|
+
html[data-theme="dark"] {
|
|
53
|
+
--bg: #1a1a1e;
|
|
54
|
+
--bg-alt: #232328;
|
|
55
|
+
--bg-subtle: #1f1f24;
|
|
56
|
+
--bg-inset: #2a2a30;
|
|
57
|
+
--border: #444;
|
|
58
|
+
--border-light: #3a3a42;
|
|
59
|
+
--border-heavy: #ccc;
|
|
60
|
+
--ink: #eee;
|
|
61
|
+
--ink-secondary: #ccc;
|
|
62
|
+
--ink-muted: #999;
|
|
63
|
+
--ink-faint: #777;
|
|
64
|
+
--cream: #2a2520;
|
|
65
|
+
}
|
|
66
|
+
html[data-theme="dark"] body {
|
|
67
|
+
background-image: radial-gradient(circle, #3a3a42 0.5px, transparent 0.5px);
|
|
68
|
+
}
|
|
69
|
+
html[data-theme="dark"] .graph-tooltip {
|
|
70
|
+
background: rgba(30,30,35,0.92);
|
|
71
|
+
border-color: rgba(255,255,255,0.1);
|
|
72
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
73
|
+
}
|
|
74
|
+
html[data-theme="dark"] .graph-controls button {
|
|
75
|
+
background: rgba(30,30,35,0.92);
|
|
76
|
+
border-color: rgba(255,255,255,0.1);
|
|
77
|
+
}
|
|
78
|
+
html[data-theme="dark"] .graph-controls button:hover {
|
|
79
|
+
background: var(--ink);
|
|
80
|
+
color: var(--bg);
|
|
81
|
+
}
|
|
82
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
83
|
+
body {
|
|
84
|
+
font-family: var(--font-body);
|
|
85
|
+
background: var(--bg);
|
|
86
|
+
color: var(--ink-secondary);
|
|
87
|
+
line-height: 1.6;
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
height: 100vh;
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
background-image: radial-gradient(circle, #D4D4CF 0.5px, transparent 0.5px);
|
|
93
|
+
background-size: 16px 16px;
|
|
94
|
+
}
|
|
95
|
+
::-webkit-scrollbar { width: 6px; }
|
|
96
|
+
::-webkit-scrollbar-track { background: var(--bg); }
|
|
97
|
+
::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 0; }
|
|
98
|
+
::-webkit-scrollbar-thumb:hover { background: var(--ink-muted); }
|
|
99
|
+
|
|
100
|
+
.app-header {
|
|
101
|
+
padding: 10px 24px;
|
|
102
|
+
border-bottom: 4px solid var(--border-heavy);
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: space-between;
|
|
106
|
+
background: var(--bg);
|
|
107
|
+
flex: 0 0 auto;
|
|
108
|
+
position: relative;
|
|
109
|
+
z-index: 3;
|
|
110
|
+
}
|
|
111
|
+
.app-header .brand {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: baseline;
|
|
114
|
+
gap: 10px;
|
|
115
|
+
color: inherit;
|
|
116
|
+
text-decoration: none;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
}
|
|
119
|
+
.app-header .brand:hover h1,
|
|
120
|
+
.app-header .brand:focus-visible h1 { color: var(--accent); }
|
|
121
|
+
.app-header .brand:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
|
|
122
|
+
.app-header .brand h1 {
|
|
123
|
+
font-size: 22px;
|
|
124
|
+
color: var(--ink);
|
|
125
|
+
font-weight: 900;
|
|
126
|
+
font-family: var(--font-display);
|
|
127
|
+
letter-spacing: -0.02em;
|
|
128
|
+
text-transform: lowercase;
|
|
129
|
+
}
|
|
130
|
+
.app-header .brand .version {
|
|
131
|
+
font-size: 10px;
|
|
132
|
+
color: var(--ink-faint);
|
|
133
|
+
font-family: var(--font-mono);
|
|
134
|
+
text-transform: uppercase;
|
|
135
|
+
letter-spacing: 0.1em;
|
|
136
|
+
}
|
|
137
|
+
.header-right {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
gap: 12px;
|
|
141
|
+
}
|
|
142
|
+
.ws-status {
|
|
143
|
+
font-size: 10px;
|
|
144
|
+
padding: 3px 10px;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: 5px;
|
|
148
|
+
font-family: var(--font-ui);
|
|
149
|
+
text-transform: uppercase;
|
|
150
|
+
letter-spacing: 0.08em;
|
|
151
|
+
font-weight: 600;
|
|
152
|
+
border: 1px solid var(--border-light);
|
|
153
|
+
}
|
|
154
|
+
.ws-status::before {
|
|
155
|
+
content: '';
|
|
156
|
+
width: 6px;
|
|
157
|
+
height: 6px;
|
|
158
|
+
display: inline-block;
|
|
159
|
+
}
|
|
160
|
+
.ws-status.connected { border-color: var(--green); color: var(--green); }
|
|
161
|
+
.ws-status.connected::before { background: var(--green); }
|
|
162
|
+
.ws-status.disconnected { border-color: var(--ink-faint); color: var(--ink-faint); }
|
|
163
|
+
.ws-status.disconnected::before { background: var(--ink-faint); }
|
|
164
|
+
|
|
165
|
+
.tab-bar {
|
|
166
|
+
display: flex;
|
|
167
|
+
height: 48px;
|
|
168
|
+
flex-shrink: 0;
|
|
169
|
+
border-bottom: 1px solid var(--border-light);
|
|
170
|
+
background: var(--bg);
|
|
171
|
+
overflow-x: auto;
|
|
172
|
+
flex: 0 0 auto;
|
|
173
|
+
position: relative;
|
|
174
|
+
z-index: 2;
|
|
175
|
+
}
|
|
176
|
+
.tab-bar button {
|
|
177
|
+
background: none;
|
|
178
|
+
border: none;
|
|
179
|
+
color: var(--ink-muted);
|
|
180
|
+
padding: 10px 20px;
|
|
181
|
+
font-size: 11px;
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
border-bottom: 2px solid transparent;
|
|
184
|
+
white-space: nowrap;
|
|
185
|
+
font-family: var(--font-ui);
|
|
186
|
+
text-transform: uppercase;
|
|
187
|
+
letter-spacing: 0.12em;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
transition: color 0.15s, border-color 0.15s;
|
|
190
|
+
}
|
|
191
|
+
.tab-bar button:hover { color: var(--ink); }
|
|
192
|
+
.tab-bar button.active {
|
|
193
|
+
color: var(--ink);
|
|
194
|
+
border-bottom-color: var(--accent);
|
|
195
|
+
}
|
|
196
|
+
.tab-bar button:focus-visible {
|
|
197
|
+
outline: 2px solid var(--accent);
|
|
198
|
+
outline-offset: -2px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* -- B6.3 Loading skeleton states --------------------------------- */
|
|
202
|
+
@keyframes shimmer {
|
|
203
|
+
0% { background-position: -200% 0; }
|
|
204
|
+
100% { background-position: 200% 0; }
|
|
205
|
+
}
|
|
206
|
+
.skeleton {
|
|
207
|
+
background: linear-gradient(90deg,
|
|
208
|
+
var(--bg-inset) 25%,
|
|
209
|
+
var(--bg-alt) 50%,
|
|
210
|
+
var(--bg-inset) 75%);
|
|
211
|
+
background-size: 200% 100%;
|
|
212
|
+
animation: shimmer 1.5s infinite;
|
|
213
|
+
border-radius: 3px;
|
|
214
|
+
}
|
|
215
|
+
.skeleton-card { height: 80px; margin-bottom: 8px; }
|
|
216
|
+
.skeleton-line { height: 14px; margin-bottom: 6px; }
|
|
217
|
+
.skeleton-line.short { width: 60%; }
|
|
218
|
+
|
|
219
|
+
/* -- B1 Command palette ------------------------------------------- */
|
|
220
|
+
#cmd-palette { display: contents; }
|
|
221
|
+
#cmd-palette[hidden] { display: none !important; }
|
|
222
|
+
#cmd-backdrop {
|
|
223
|
+
position: fixed; inset: 0;
|
|
224
|
+
background: rgba(0,0,0,0.45);
|
|
225
|
+
backdrop-filter: blur(4px);
|
|
226
|
+
z-index: 9998;
|
|
227
|
+
}
|
|
228
|
+
#cmd-panel {
|
|
229
|
+
position: fixed;
|
|
230
|
+
top: 18%;
|
|
231
|
+
left: 50%;
|
|
232
|
+
transform: translateX(-50%) translateY(-8px);
|
|
233
|
+
width: min(640px, 92vw);
|
|
234
|
+
background: var(--bg);
|
|
235
|
+
border: 1px solid var(--border);
|
|
236
|
+
border-radius: 8px;
|
|
237
|
+
z-index: 9999;
|
|
238
|
+
overflow: hidden;
|
|
239
|
+
box-shadow: 0 24px 64px rgba(0,0,0,0.35);
|
|
240
|
+
animation: cmdSlideIn 0.14s ease forwards;
|
|
241
|
+
}
|
|
242
|
+
@keyframes cmdSlideIn {
|
|
243
|
+
from { opacity: 0; transform: translateX(-50%) translateY(-16px); }
|
|
244
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
245
|
+
}
|
|
246
|
+
#cmd-input {
|
|
247
|
+
width: 100%; padding: 14px 18px;
|
|
248
|
+
font-size: 15px; font-family: var(--font-ui);
|
|
249
|
+
border: none; border-bottom: 1px solid var(--border-light);
|
|
250
|
+
background: transparent; color: var(--ink); outline: none;
|
|
251
|
+
}
|
|
252
|
+
#cmd-input::placeholder { color: var(--ink-faint); }
|
|
253
|
+
#cmd-results {
|
|
254
|
+
list-style: none; margin: 0; padding: 6px 0;
|
|
255
|
+
max-height: 380px; overflow-y: auto;
|
|
256
|
+
}
|
|
257
|
+
#cmd-results li {
|
|
258
|
+
padding: 9px 18px; cursor: pointer;
|
|
259
|
+
display: flex; align-items: center; gap: 10px;
|
|
260
|
+
font-family: var(--font-ui); font-size: 13px;
|
|
261
|
+
color: var(--ink-secondary);
|
|
262
|
+
}
|
|
263
|
+
#cmd-results li[aria-selected="true"],
|
|
264
|
+
#cmd-results li:hover { background: var(--bg-inset); color: var(--ink); }
|
|
265
|
+
.cmd-badge {
|
|
266
|
+
font-size: 10px; padding: 2px 6px; border-radius: 3px;
|
|
267
|
+
background: var(--bg-alt); color: var(--ink-muted);
|
|
268
|
+
border: 1px solid var(--border-light); flex-shrink: 0;
|
|
269
|
+
}
|
|
270
|
+
.cmd-main { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
271
|
+
.cmd-sub { font-size: 11px; color: var(--ink-faint); flex-shrink: 0; }
|
|
272
|
+
|
|
273
|
+
/* -- B5 MCP Tool Tester tab --------------------------------------- */
|
|
274
|
+
.tools-list { display: flex; flex-direction: column; gap: 0; max-width: 900px; }
|
|
275
|
+
.tool-row {
|
|
276
|
+
border: 1px solid var(--border-light); border-top: none;
|
|
277
|
+
padding: 10px 16px; cursor: pointer;
|
|
278
|
+
display: flex; align-items: center; gap: 12px;
|
|
279
|
+
font-family: var(--font-ui); font-size: 13px;
|
|
280
|
+
}
|
|
281
|
+
.tool-row:first-child { border-top: 1px solid var(--border-light); }
|
|
282
|
+
.tool-row:hover, .tool-row.selected { background: var(--bg-inset); }
|
|
283
|
+
.tool-name { font-weight: 600; color: var(--ink); min-width: 220px; font-family: var(--font-mono); font-size: 12px; }
|
|
284
|
+
.tool-desc { color: var(--ink-muted); }
|
|
285
|
+
#tool-panel { max-width: 900px; margin-top: 24px; }
|
|
286
|
+
#tool-panel h3 { font-family: var(--font-ui); font-size: 14px; font-weight: 600; margin-bottom: 8px; color: var(--ink); }
|
|
287
|
+
#tool-args-editor {
|
|
288
|
+
width: 100%; min-height: 120px; padding: 10px 14px;
|
|
289
|
+
font-family: var(--font-mono); font-size: 13px;
|
|
290
|
+
border: 1px solid var(--border-light); background: var(--bg-inset);
|
|
291
|
+
color: var(--ink); resize: vertical; border-radius: 3px;
|
|
292
|
+
}
|
|
293
|
+
#tool-run-btn {
|
|
294
|
+
margin-top: 8px; padding: 8px 20px;
|
|
295
|
+
background: var(--accent); color: #fff; border: none;
|
|
296
|
+
font-family: var(--font-ui); font-size: 13px; cursor: pointer; border-radius: 3px;
|
|
297
|
+
}
|
|
298
|
+
#tool-run-btn:hover { opacity: 0.85; }
|
|
299
|
+
#tool-response {
|
|
300
|
+
margin-top: 12px; padding: 12px 14px;
|
|
301
|
+
background: var(--bg-inset); border: 1px solid var(--border-light);
|
|
302
|
+
font-family: var(--font-mono); font-size: 12px;
|
|
303
|
+
white-space: pre-wrap; word-break: break-all;
|
|
304
|
+
max-height: 300px; overflow-y: auto; border-radius: 3px;
|
|
305
|
+
color: var(--ink);
|
|
306
|
+
}
|
|
307
|
+
#tool-response.error { color: var(--accent); border-color: var(--accent); }
|
|
308
|
+
.tool-history { margin-top: 20px; }
|
|
309
|
+
.tool-history summary {
|
|
310
|
+
font-family: var(--font-ui); font-size: 12px; cursor: pointer;
|
|
311
|
+
color: var(--ink-muted); padding: 6px 0;
|
|
312
|
+
}
|
|
313
|
+
.tool-history-entry {
|
|
314
|
+
border: 1px solid var(--border-light); margin-bottom: 4px; border-radius: 3px;
|
|
315
|
+
}
|
|
316
|
+
.tool-history-entry summary {
|
|
317
|
+
padding: 8px 12px; display: flex; align-items: center; gap: 8px;
|
|
318
|
+
font-family: var(--font-mono); font-size: 12px; color: var(--ink);
|
|
319
|
+
}
|
|
320
|
+
.badge-ok { color: var(--green); }
|
|
321
|
+
.badge-err { color: var(--accent); }
|
|
322
|
+
|
|
323
|
+
/* -- B4 Onboarding card ------------------------------------------- */
|
|
324
|
+
.onboarding-card {
|
|
325
|
+
border: 2px solid var(--border); padding: 28px 32px;
|
|
326
|
+
max-width: 640px; margin: 32px auto; background: var(--cream);
|
|
327
|
+
}
|
|
328
|
+
.onboarding-card h2 { font-family: var(--font-display); font-size: 22px; margin-bottom: 6px; color: var(--ink); }
|
|
329
|
+
.onboarding-card p { font-size: 14px; color: var(--ink-muted); margin-bottom: 20px; font-family: var(--font-ui); }
|
|
330
|
+
.onboarding-step { margin-bottom: 20px; }
|
|
331
|
+
.onboarding-step .step-header {
|
|
332
|
+
display: flex; align-items: center; gap: 10px; margin-bottom: 8px;
|
|
333
|
+
}
|
|
334
|
+
.step-num {
|
|
335
|
+
width: 24px; height: 24px; border-radius: 50%;
|
|
336
|
+
background: var(--ink); color: var(--bg);
|
|
337
|
+
display: flex; align-items: center; justify-content: center;
|
|
338
|
+
font-family: var(--font-ui); font-size: 12px; font-weight: 700; flex-shrink: 0;
|
|
339
|
+
}
|
|
340
|
+
.step-num.done { background: var(--green); }
|
|
341
|
+
.step-title { font-family: var(--font-ui); font-weight: 600; font-size: 13px; color: var(--ink); }
|
|
342
|
+
.step-code-wrap { position: relative; }
|
|
343
|
+
.step-code {
|
|
344
|
+
display: block; background: var(--bg-inset); border: 1px solid var(--border-light);
|
|
345
|
+
padding: 8px 12px; font-family: var(--font-mono); font-size: 11px;
|
|
346
|
+
color: var(--ink); overflow-x: auto; white-space: pre;
|
|
347
|
+
}
|
|
348
|
+
.copy-btn {
|
|
349
|
+
position: absolute; right: 6px; top: 6px;
|
|
350
|
+
background: var(--bg); border: 1px solid var(--border-light);
|
|
351
|
+
padding: 2px 8px; font-size: 10px; font-family: var(--font-ui);
|
|
352
|
+
cursor: pointer; color: var(--ink-muted); border-radius: 2px;
|
|
353
|
+
}
|
|
354
|
+
.copy-btn:hover { background: var(--bg-inset); }
|
|
355
|
+
.onboarding-dismiss {
|
|
356
|
+
margin-top: 20px; padding: 8px 20px;
|
|
357
|
+
background: none; border: 1px solid var(--border);
|
|
358
|
+
font-family: var(--font-ui); font-size: 13px; cursor: pointer; color: var(--ink);
|
|
359
|
+
}
|
|
360
|
+
.onboarding-dismiss:hover { background: var(--bg-inset); }
|
|
361
|
+
|
|
362
|
+
.view { display: none; flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 24px; }
|
|
363
|
+
.view.active { display: block; }
|
|
364
|
+
|
|
365
|
+
.stats-grid {
|
|
366
|
+
display: grid;
|
|
367
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
368
|
+
gap: 0;
|
|
369
|
+
margin-bottom: 24px;
|
|
370
|
+
border: 1px solid var(--border);
|
|
371
|
+
}
|
|
372
|
+
.stat-card {
|
|
373
|
+
background: var(--bg);
|
|
374
|
+
padding: 16px 20px;
|
|
375
|
+
border-right: 1px solid var(--border-light);
|
|
376
|
+
border-bottom: 1px solid var(--border-light);
|
|
377
|
+
}
|
|
378
|
+
.stat-card:last-child { border-right: none; }
|
|
379
|
+
.stat-card .label {
|
|
380
|
+
font-size: 9px;
|
|
381
|
+
color: var(--ink-muted);
|
|
382
|
+
text-transform: uppercase;
|
|
383
|
+
letter-spacing: 0.15em;
|
|
384
|
+
margin-bottom: 4px;
|
|
385
|
+
font-family: var(--font-ui);
|
|
386
|
+
font-weight: 600;
|
|
387
|
+
}
|
|
388
|
+
.stat-card .value {
|
|
389
|
+
font-size: 32px;
|
|
390
|
+
font-weight: 900;
|
|
391
|
+
color: var(--ink);
|
|
392
|
+
font-family: var(--font-display);
|
|
393
|
+
line-height: 1.1;
|
|
394
|
+
}
|
|
395
|
+
.stat-card .sub {
|
|
396
|
+
font-size: 11px;
|
|
397
|
+
color: var(--ink-faint);
|
|
398
|
+
margin-top: 2px;
|
|
399
|
+
font-family: var(--font-ui);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.card {
|
|
403
|
+
background: var(--bg);
|
|
404
|
+
border: 1px solid var(--border);
|
|
405
|
+
padding: 20px;
|
|
406
|
+
margin-bottom: 16px;
|
|
407
|
+
transition: box-shadow 0.15s;
|
|
408
|
+
}
|
|
409
|
+
.card:hover {
|
|
410
|
+
box-shadow: 4px 4px 0px 0px var(--border);
|
|
411
|
+
}
|
|
412
|
+
.card-title {
|
|
413
|
+
font-size: 13px;
|
|
414
|
+
font-weight: 700;
|
|
415
|
+
color: var(--ink);
|
|
416
|
+
margin-bottom: 12px;
|
|
417
|
+
font-family: var(--font-display);
|
|
418
|
+
text-transform: uppercase;
|
|
419
|
+
letter-spacing: 0.06em;
|
|
420
|
+
padding-bottom: 8px;
|
|
421
|
+
border-bottom: 1px solid var(--border-light);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.health-bar {
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 8px;
|
|
428
|
+
margin-bottom: 8px;
|
|
429
|
+
}
|
|
430
|
+
.health-dot {
|
|
431
|
+
width: 10px;
|
|
432
|
+
height: 10px;
|
|
433
|
+
}
|
|
434
|
+
.health-dot.healthy { background: var(--green); }
|
|
435
|
+
.health-dot.degraded { background: var(--yellow); }
|
|
436
|
+
.health-dot.critical { background: var(--accent); }
|
|
437
|
+
|
|
438
|
+
.badge {
|
|
439
|
+
display: inline-block;
|
|
440
|
+
font-size: 9px;
|
|
441
|
+
padding: 2px 8px;
|
|
442
|
+
font-weight: 600;
|
|
443
|
+
font-family: var(--font-ui);
|
|
444
|
+
text-transform: uppercase;
|
|
445
|
+
letter-spacing: 0.08em;
|
|
446
|
+
border: 1px solid;
|
|
447
|
+
}
|
|
448
|
+
.badge-blue { border-color: var(--blue); color: var(--blue); background: transparent; }
|
|
449
|
+
.badge-green { border-color: var(--green); color: var(--green); background: transparent; }
|
|
450
|
+
.badge-yellow { border-color: var(--yellow); color: var(--yellow); background: transparent; }
|
|
451
|
+
.badge-red { border-color: var(--accent); color: var(--accent); background: transparent; }
|
|
452
|
+
.badge-purple { border-color: var(--purple); color: var(--purple); background: transparent; }
|
|
453
|
+
.badge-orange { border-color: var(--orange); color: var(--orange); background: transparent; }
|
|
454
|
+
.badge-cyan { border-color: var(--cyan); color: var(--cyan); background: transparent; }
|
|
455
|
+
.badge-muted { border-color: var(--border-light); color: var(--ink-muted); background: transparent; }
|
|
456
|
+
|
|
457
|
+
table {
|
|
458
|
+
width: 100%;
|
|
459
|
+
border-collapse: collapse;
|
|
460
|
+
font-size: 13px;
|
|
461
|
+
font-family: var(--font-body);
|
|
462
|
+
}
|
|
463
|
+
th {
|
|
464
|
+
text-align: left;
|
|
465
|
+
padding: 8px 12px;
|
|
466
|
+
border-bottom: 2px solid var(--border);
|
|
467
|
+
color: var(--ink);
|
|
468
|
+
font-size: 9px;
|
|
469
|
+
text-transform: uppercase;
|
|
470
|
+
letter-spacing: 0.12em;
|
|
471
|
+
font-weight: 600;
|
|
472
|
+
font-family: var(--font-ui);
|
|
473
|
+
}
|
|
474
|
+
td {
|
|
475
|
+
padding: 8px 12px;
|
|
476
|
+
border-bottom: 1px solid var(--border-light);
|
|
477
|
+
vertical-align: top;
|
|
478
|
+
}
|
|
479
|
+
tr:hover td { background: var(--bg-alt); }
|
|
480
|
+
|
|
481
|
+
.strength-bar {
|
|
482
|
+
width: 60px;
|
|
483
|
+
height: 4px;
|
|
484
|
+
background: var(--bg-inset);
|
|
485
|
+
overflow: hidden;
|
|
486
|
+
display: inline-block;
|
|
487
|
+
vertical-align: middle;
|
|
488
|
+
}
|
|
489
|
+
.strength-bar .fill {
|
|
490
|
+
height: 100%;
|
|
491
|
+
transition: width 0.3s;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.toolbar {
|
|
495
|
+
display: flex;
|
|
496
|
+
gap: 10px;
|
|
497
|
+
margin-bottom: 20px;
|
|
498
|
+
align-items: center;
|
|
499
|
+
flex-wrap: wrap;
|
|
500
|
+
}
|
|
501
|
+
.toolbar input, .toolbar select {
|
|
502
|
+
background: var(--bg);
|
|
503
|
+
border: 1px solid var(--border);
|
|
504
|
+
color: var(--ink);
|
|
505
|
+
padding: 7px 12px;
|
|
506
|
+
font-size: 13px;
|
|
507
|
+
outline: none;
|
|
508
|
+
font-family: var(--font-ui);
|
|
509
|
+
}
|
|
510
|
+
.toolbar input:focus, .toolbar select:focus {
|
|
511
|
+
border-color: var(--ink);
|
|
512
|
+
box-shadow: 2px 2px 0px 0px var(--border);
|
|
513
|
+
}
|
|
514
|
+
.toolbar input { flex: 1; min-width: 200px; }
|
|
515
|
+
|
|
516
|
+
.btn {
|
|
517
|
+
background: var(--bg);
|
|
518
|
+
border: 1px solid var(--border);
|
|
519
|
+
color: var(--ink);
|
|
520
|
+
padding: 7px 16px;
|
|
521
|
+
font-size: 11px;
|
|
522
|
+
cursor: pointer;
|
|
523
|
+
transition: box-shadow 0.1s, transform 0.1s;
|
|
524
|
+
font-family: var(--font-ui);
|
|
525
|
+
font-weight: 600;
|
|
526
|
+
text-transform: uppercase;
|
|
527
|
+
letter-spacing: 0.06em;
|
|
528
|
+
}
|
|
529
|
+
.btn:hover { box-shadow: 3px 3px 0px 0px var(--border); transform: translate(-1px, -1px); }
|
|
530
|
+
.btn:active { box-shadow: none; transform: translate(0, 0); }
|
|
531
|
+
.btn-danger { border-color: var(--accent); color: var(--accent); }
|
|
532
|
+
.btn-danger:hover { background: var(--accent); color: white; box-shadow: 3px 3px 0px 0px var(--border); }
|
|
533
|
+
.btn-primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
534
|
+
.btn-primary:hover { background: var(--ink-secondary); box-shadow: 3px 3px 0px 0px var(--ink-muted); }
|
|
535
|
+
|
|
536
|
+
.graph-container {
|
|
537
|
+
display: flex;
|
|
538
|
+
height: calc(100vh - 130px);
|
|
539
|
+
margin: -24px;
|
|
540
|
+
border-top: 1px solid var(--border-light);
|
|
541
|
+
}
|
|
542
|
+
.graph-canvas-wrap {
|
|
543
|
+
flex: 1;
|
|
544
|
+
position: relative;
|
|
545
|
+
overflow: hidden;
|
|
546
|
+
background: var(--bg);
|
|
547
|
+
}
|
|
548
|
+
.graph-canvas-wrap canvas {
|
|
549
|
+
display: block;
|
|
550
|
+
width: 100%;
|
|
551
|
+
height: 100%;
|
|
552
|
+
}
|
|
553
|
+
.graph-sidebar {
|
|
554
|
+
width: 260px;
|
|
555
|
+
border-left: 2px solid var(--border);
|
|
556
|
+
padding: 20px;
|
|
557
|
+
overflow-y: auto;
|
|
558
|
+
background: var(--bg);
|
|
559
|
+
}
|
|
560
|
+
.graph-sidebar h3 {
|
|
561
|
+
font-size: 9px;
|
|
562
|
+
color: var(--ink);
|
|
563
|
+
text-transform: uppercase;
|
|
564
|
+
letter-spacing: 0.15em;
|
|
565
|
+
margin-bottom: 12px;
|
|
566
|
+
font-family: var(--font-ui);
|
|
567
|
+
font-weight: 600;
|
|
568
|
+
padding-bottom: 6px;
|
|
569
|
+
border-bottom: 1px solid var(--border-light);
|
|
570
|
+
}
|
|
571
|
+
.filter-item {
|
|
572
|
+
display: flex;
|
|
573
|
+
align-items: center;
|
|
574
|
+
gap: 6px;
|
|
575
|
+
padding: 4px 0;
|
|
576
|
+
font-size: 12px;
|
|
577
|
+
cursor: pointer;
|
|
578
|
+
font-family: var(--font-ui);
|
|
579
|
+
}
|
|
580
|
+
.filter-item input[type="checkbox"] {
|
|
581
|
+
accent-color: var(--ink);
|
|
582
|
+
}
|
|
583
|
+
.filter-dot {
|
|
584
|
+
width: 8px;
|
|
585
|
+
height: 8px;
|
|
586
|
+
display: inline-block;
|
|
587
|
+
}
|
|
588
|
+
.graph-info {
|
|
589
|
+
margin-top: 16px;
|
|
590
|
+
padding-top: 16px;
|
|
591
|
+
border-top: 1px solid var(--border-light);
|
|
592
|
+
}
|
|
593
|
+
.graph-info .info-row {
|
|
594
|
+
display: flex;
|
|
595
|
+
justify-content: space-between;
|
|
596
|
+
font-size: 12px;
|
|
597
|
+
padding: 3px 0;
|
|
598
|
+
font-family: var(--font-ui);
|
|
599
|
+
}
|
|
600
|
+
.graph-info .info-row .info-label { color: var(--ink-muted); }
|
|
601
|
+
.graph-info .info-row .info-value { color: var(--ink); font-weight: 600; font-family: var(--font-mono); }
|
|
602
|
+
|
|
603
|
+
.obs-card {
|
|
604
|
+
background: var(--bg);
|
|
605
|
+
border: 1px solid var(--border-light);
|
|
606
|
+
padding: 16px 20px;
|
|
607
|
+
margin-bottom: 12px;
|
|
608
|
+
border-left: 3px solid var(--border-light);
|
|
609
|
+
transition: box-shadow 0.15s;
|
|
610
|
+
min-width: 0;
|
|
611
|
+
max-width: 100%;
|
|
612
|
+
overflow: hidden;
|
|
613
|
+
}
|
|
614
|
+
.obs-card:hover { box-shadow: 3px 3px 0px 0px var(--border-light); }
|
|
615
|
+
.obs-card.imp-high { border-left-color: var(--accent); }
|
|
616
|
+
.obs-card.imp-med { border-left-color: var(--yellow); }
|
|
617
|
+
.obs-card.imp-low { border-left-color: var(--green); }
|
|
618
|
+
.obs-card .obs-head {
|
|
619
|
+
display: grid;
|
|
620
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
621
|
+
align-items: start;
|
|
622
|
+
gap: 12px;
|
|
623
|
+
margin-bottom: 6px;
|
|
624
|
+
}
|
|
625
|
+
.obs-card .obs-title-row {
|
|
626
|
+
display: flex;
|
|
627
|
+
align-items: center;
|
|
628
|
+
gap: 6px;
|
|
629
|
+
min-width: 0;
|
|
630
|
+
}
|
|
631
|
+
.obs-card .obs-meta {
|
|
632
|
+
display: flex;
|
|
633
|
+
align-items: center;
|
|
634
|
+
gap: 8px;
|
|
635
|
+
flex: 0 0 auto;
|
|
636
|
+
white-space: nowrap;
|
|
637
|
+
}
|
|
638
|
+
.obs-card .obs-type-icon {
|
|
639
|
+
flex: 0 0 auto;
|
|
640
|
+
}
|
|
641
|
+
.obs-card .obs-title {
|
|
642
|
+
font-size: 14px;
|
|
643
|
+
font-weight: 700;
|
|
644
|
+
color: var(--ink);
|
|
645
|
+
font-family: var(--font-display);
|
|
646
|
+
min-width: 0;
|
|
647
|
+
overflow: hidden;
|
|
648
|
+
text-overflow: ellipsis;
|
|
649
|
+
white-space: nowrap;
|
|
650
|
+
}
|
|
651
|
+
.obs-card .obs-time {
|
|
652
|
+
font-size: 10px;
|
|
653
|
+
color: var(--ink-faint);
|
|
654
|
+
font-family: var(--font-mono);
|
|
655
|
+
letter-spacing: 0.04em;
|
|
656
|
+
}
|
|
657
|
+
.obs-card .obs-narrative {
|
|
658
|
+
font-size: 13px;
|
|
659
|
+
color: var(--ink-muted);
|
|
660
|
+
margin-bottom: 6px;
|
|
661
|
+
overflow-wrap: anywhere;
|
|
662
|
+
word-break: break-word;
|
|
663
|
+
}
|
|
664
|
+
.obs-card pre {
|
|
665
|
+
max-width: 100%;
|
|
666
|
+
white-space: pre-wrap;
|
|
667
|
+
overflow-wrap: anywhere;
|
|
668
|
+
word-break: break-word;
|
|
669
|
+
}
|
|
670
|
+
.obs-card .obs-facts {
|
|
671
|
+
margin: 6px 0 6px 16px;
|
|
672
|
+
font-size: 12px;
|
|
673
|
+
color: var(--ink-muted);
|
|
674
|
+
}
|
|
675
|
+
.obs-card .obs-facts li { margin-bottom: 2px; }
|
|
676
|
+
.tag-list { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 6px; }
|
|
677
|
+
mark { background: rgba(204, 0, 0, 0.12); color: var(--ink); padding: 0 2px; border-radius: 2px; }
|
|
678
|
+
.tag {
|
|
679
|
+
font-size: 10px;
|
|
680
|
+
padding: 1px 6px;
|
|
681
|
+
border: 1px solid var(--blue);
|
|
682
|
+
color: var(--blue);
|
|
683
|
+
font-family: var(--font-mono);
|
|
684
|
+
font-weight: 500;
|
|
685
|
+
max-width: 100%;
|
|
686
|
+
overflow: hidden;
|
|
687
|
+
text-overflow: ellipsis;
|
|
688
|
+
}
|
|
689
|
+
.tag.file-tag { border-color: var(--green); color: var(--green); }
|
|
690
|
+
|
|
691
|
+
.session-list { display: flex; flex-direction: column; gap: 0; }
|
|
692
|
+
.session-item {
|
|
693
|
+
background: var(--bg);
|
|
694
|
+
border: 1px solid var(--border-light);
|
|
695
|
+
border-bottom: none;
|
|
696
|
+
padding: 14px 20px;
|
|
697
|
+
cursor: pointer;
|
|
698
|
+
transition: background 0.1s;
|
|
699
|
+
}
|
|
700
|
+
.session-item:last-child { border-bottom: 1px solid var(--border-light); }
|
|
701
|
+
.session-item:hover { background: var(--bg-alt); }
|
|
702
|
+
.session-item.selected { background: var(--bg-alt); border-left: 3px solid var(--accent); }
|
|
703
|
+
.session-item .session-top {
|
|
704
|
+
display: flex;
|
|
705
|
+
justify-content: space-between;
|
|
706
|
+
align-items: center;
|
|
707
|
+
margin-bottom: 4px;
|
|
708
|
+
}
|
|
709
|
+
.session-item .session-project {
|
|
710
|
+
font-weight: 700;
|
|
711
|
+
color: var(--ink);
|
|
712
|
+
font-size: 14px;
|
|
713
|
+
font-family: var(--font-display);
|
|
714
|
+
}
|
|
715
|
+
.session-item .session-meta {
|
|
716
|
+
font-size: 11px;
|
|
717
|
+
color: var(--ink-muted);
|
|
718
|
+
font-family: var(--font-mono);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.detail-panel {
|
|
722
|
+
background: var(--bg);
|
|
723
|
+
border: 1px solid var(--border);
|
|
724
|
+
padding: 24px;
|
|
725
|
+
margin-top: 20px;
|
|
726
|
+
}
|
|
727
|
+
.detail-panel h3 {
|
|
728
|
+
font-size: 15px;
|
|
729
|
+
font-weight: 700;
|
|
730
|
+
color: var(--ink);
|
|
731
|
+
margin-bottom: 16px;
|
|
732
|
+
font-family: var(--font-display);
|
|
733
|
+
text-transform: uppercase;
|
|
734
|
+
letter-spacing: 0.04em;
|
|
735
|
+
padding-bottom: 8px;
|
|
736
|
+
border-bottom: 2px solid var(--border);
|
|
737
|
+
}
|
|
738
|
+
.detail-row {
|
|
739
|
+
display: flex;
|
|
740
|
+
padding: 6px 0;
|
|
741
|
+
font-size: 13px;
|
|
742
|
+
border-bottom: 1px solid var(--bg-inset);
|
|
743
|
+
}
|
|
744
|
+
.detail-row .dl { color: var(--ink-muted); width: 140px; flex-shrink: 0; font-family: var(--font-ui); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; padding-top: 2px; }
|
|
745
|
+
.detail-row .dv { color: var(--ink); font-family: var(--font-body); }
|
|
746
|
+
|
|
747
|
+
.audit-entry {
|
|
748
|
+
padding: 12px 0;
|
|
749
|
+
border-bottom: 1px solid var(--border-light);
|
|
750
|
+
font-size: 13px;
|
|
751
|
+
}
|
|
752
|
+
.audit-entry:last-child { border-bottom: none; }
|
|
753
|
+
.audit-head {
|
|
754
|
+
display: flex;
|
|
755
|
+
align-items: center;
|
|
756
|
+
gap: 8px;
|
|
757
|
+
margin-bottom: 4px;
|
|
758
|
+
}
|
|
759
|
+
.audit-detail {
|
|
760
|
+
font-size: 12px;
|
|
761
|
+
color: var(--ink-faint);
|
|
762
|
+
margin-top: 4px;
|
|
763
|
+
max-height: 0;
|
|
764
|
+
overflow: hidden;
|
|
765
|
+
transition: max-height 0.2s;
|
|
766
|
+
}
|
|
767
|
+
.audit-detail.open { max-height: 200px; }
|
|
768
|
+
.audit-detail pre {
|
|
769
|
+
font-family: var(--font-mono);
|
|
770
|
+
font-size: 11px;
|
|
771
|
+
background: var(--bg-alt);
|
|
772
|
+
padding: 10px;
|
|
773
|
+
border: 1px solid var(--border-light);
|
|
774
|
+
overflow-x: auto;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.bar-chart { margin-top: 8px; }
|
|
778
|
+
.bar-row {
|
|
779
|
+
display: flex;
|
|
780
|
+
align-items: center;
|
|
781
|
+
gap: 8px;
|
|
782
|
+
margin-bottom: 6px;
|
|
783
|
+
font-size: 12px;
|
|
784
|
+
}
|
|
785
|
+
.bar-label { width: 120px; color: var(--ink-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-mono); font-size: 11px; }
|
|
786
|
+
.bar-track {
|
|
787
|
+
flex: 1;
|
|
788
|
+
height: 6px;
|
|
789
|
+
background: var(--bg-inset);
|
|
790
|
+
overflow: hidden;
|
|
791
|
+
}
|
|
792
|
+
.bar-fill {
|
|
793
|
+
height: 100%;
|
|
794
|
+
transition: width 0.3s;
|
|
795
|
+
}
|
|
796
|
+
.bar-value { width: 30px; text-align: right; color: var(--ink-muted); font-size: 11px; font-family: var(--font-mono); font-weight: 500; }
|
|
797
|
+
|
|
798
|
+
.empty-state {
|
|
799
|
+
text-align: center;
|
|
800
|
+
padding: 60px 20px;
|
|
801
|
+
color: var(--ink-faint);
|
|
802
|
+
}
|
|
803
|
+
.empty-state .empty-icon { font-size: 36px; margin-bottom: 10px; opacity: 0.4; }
|
|
804
|
+
.empty-state p { font-size: 14px; font-family: var(--font-body); font-style: italic; }
|
|
805
|
+
.empty-state .empty-title { font-size: 16px; font-weight: 600; font-style: normal; color: var(--ink-muted); margin-bottom: 8px; }
|
|
806
|
+
.empty-state .empty-lead { font-style: normal; font-size: 14px; color: var(--ink-muted); max-width: 520px; margin: 0 auto 14px; line-height: 1.5; }
|
|
807
|
+
.empty-state pre.empty-cmd {
|
|
808
|
+
display: inline-block; margin: 10px auto 12px; padding: 10px 14px;
|
|
809
|
+
background: var(--bg-alt); border: 1px solid var(--border);
|
|
810
|
+
border-radius: 4px; font-family: var(--font-mono); font-size: 12px;
|
|
811
|
+
color: var(--ink); text-align: left; font-style: normal; white-space: pre;
|
|
812
|
+
}
|
|
813
|
+
.empty-state .empty-link { color: var(--accent); text-decoration: underline; font-size: 13px; font-style: normal; }
|
|
814
|
+
|
|
815
|
+
/* Feature flag banner system -- compact collapsed by default */
|
|
816
|
+
.flag-banners {
|
|
817
|
+
padding: 0 12px 10px 12px;
|
|
818
|
+
background: var(--bg);
|
|
819
|
+
flex: 0 0 auto;
|
|
820
|
+
position: relative;
|
|
821
|
+
z-index: 1;
|
|
822
|
+
}
|
|
823
|
+
.flag-banners:empty { display: none; }
|
|
824
|
+
button.flag-summary {
|
|
825
|
+
display: flex; align-items: center; gap: 12px;
|
|
826
|
+
padding: 8px 14px; border-radius: 4px;
|
|
827
|
+
border: 1px solid var(--border);
|
|
828
|
+
background: var(--bg-subtle);
|
|
829
|
+
font-family: var(--font-ui); font-size: 12px;
|
|
830
|
+
color: var(--ink-muted);
|
|
831
|
+
cursor: pointer; user-select: none;
|
|
832
|
+
width: 100%; text-align: left;
|
|
833
|
+
appearance: none;
|
|
834
|
+
flex: 1 1 auto;
|
|
835
|
+
}
|
|
836
|
+
button.flag-summary:hover,
|
|
837
|
+
button.flag-summary:focus-visible { background: var(--bg-alt); outline: 2px solid var(--border); outline-offset: 1px; }
|
|
838
|
+
.flag-summary .flag-count { color: var(--ink); font-weight: 600; }
|
|
839
|
+
.flag-summary .flag-pill {
|
|
840
|
+
display: inline-block; padding: 1px 8px; border-radius: 10px;
|
|
841
|
+
background: #f59e0b20; color: #d97706; font-size: 11px; font-weight: 600;
|
|
842
|
+
margin-right: 6px;
|
|
843
|
+
}
|
|
844
|
+
.flag-summary .flag-pill.info { background: var(--border-light); color: var(--ink-muted); }
|
|
845
|
+
.flag-summary .flag-toggle { margin-left: auto; font-size: 11px; opacity: 0.7; }
|
|
846
|
+
.flag-list {
|
|
847
|
+
display: none; flex-direction: column; gap: 6px;
|
|
848
|
+
margin-top: 6px;
|
|
849
|
+
max-height: min(30vh, 260px);
|
|
850
|
+
overflow-y: auto;
|
|
851
|
+
}
|
|
852
|
+
.flag-list.open { display: flex; }
|
|
853
|
+
.flag-banner {
|
|
854
|
+
display: flex; align-items: flex-start; gap: 10px;
|
|
855
|
+
padding: 10px 14px; border-radius: 3px;
|
|
856
|
+
border: 1px solid var(--border);
|
|
857
|
+
background: var(--bg-subtle);
|
|
858
|
+
font-family: var(--font-ui); font-size: 12px;
|
|
859
|
+
}
|
|
860
|
+
.flag-banner.warn { border-left: 3px solid #f59e0b; }
|
|
861
|
+
.flag-banner.info { border-left: 3px solid var(--ink-muted); }
|
|
862
|
+
.flag-banner .flag-icon { flex-shrink: 0; font-size: 14px; line-height: 1.3; }
|
|
863
|
+
.flag-banner .flag-body { flex: 1; min-width: 0; }
|
|
864
|
+
.flag-banner .flag-title { font-weight: 600; color: var(--ink); margin-bottom: 2px; font-size: 12px; }
|
|
865
|
+
.flag-banner .flag-title code { font-family: var(--font-mono); font-size: 10px; color: var(--ink-muted); font-weight: 400; margin-left: 4px; }
|
|
866
|
+
.flag-banner .flag-desc { color: var(--ink-muted); margin-bottom: 4px; line-height: 1.4; font-size: 12px; }
|
|
867
|
+
.flag-banner .flag-enable {
|
|
868
|
+
display: block; margin-top: 2px; padding: 5px 8px;
|
|
869
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 3px;
|
|
870
|
+
font-family: var(--font-mono); font-size: 10px; color: var(--ink);
|
|
871
|
+
white-space: pre-wrap; word-break: break-all;
|
|
872
|
+
}
|
|
873
|
+
.flag-close {
|
|
874
|
+
background: none; border: none; color: var(--ink-faint); cursor: pointer;
|
|
875
|
+
font-size: 16px; line-height: 1; padding: 0 4px; font-family: inherit;
|
|
876
|
+
flex: 0 0 auto;
|
|
877
|
+
}
|
|
878
|
+
.flag-close:hover,
|
|
879
|
+
.flag-close:focus-visible { color: var(--ink); outline: 2px solid var(--border); outline-offset: 1px; }
|
|
880
|
+
|
|
881
|
+
.viewer-auth {
|
|
882
|
+
display: none;
|
|
883
|
+
padding: 0;
|
|
884
|
+
background: var(--bg);
|
|
885
|
+
flex: 0 0 auto;
|
|
886
|
+
position: relative;
|
|
887
|
+
z-index: 10;
|
|
888
|
+
}
|
|
889
|
+
.viewer-auth.open { display: block; }
|
|
890
|
+
.viewer-auth-panel {
|
|
891
|
+
display: grid;
|
|
892
|
+
grid-template-columns: minmax(0, 1fr) minmax(200px, 360px) auto;
|
|
893
|
+
gap: 10px;
|
|
894
|
+
align-items: center;
|
|
895
|
+
width: 100%;
|
|
896
|
+
max-width: 100%;
|
|
897
|
+
padding: 12px 24px;
|
|
898
|
+
border-top: none;
|
|
899
|
+
border-bottom: 3px solid var(--accent);
|
|
900
|
+
background: #fff8f0;
|
|
901
|
+
font-family: var(--font-ui);
|
|
902
|
+
font-size: 12px;
|
|
903
|
+
box-shadow: 0 2px 8px rgba(204,0,0,0.10);
|
|
904
|
+
}
|
|
905
|
+
html[data-theme="dark"] .viewer-auth-panel { background: #2a1a1a; }
|
|
906
|
+
.viewer-auth-title { font-weight: 700; color: var(--accent); margin-bottom: 2px; font-size: 13px; }
|
|
907
|
+
.viewer-auth-desc { color: var(--ink-muted); line-height: 1.4; }
|
|
908
|
+
.viewer-auth-desc code { font-family: var(--font-mono); font-size: 10px; color: var(--ink); }
|
|
909
|
+
.viewer-auth input {
|
|
910
|
+
width: 100%;
|
|
911
|
+
min-width: 0;
|
|
912
|
+
padding: 8px 10px;
|
|
913
|
+
border: 1px solid var(--accent);
|
|
914
|
+
background: var(--bg);
|
|
915
|
+
color: var(--ink);
|
|
916
|
+
font-family: var(--font-mono);
|
|
917
|
+
font-size: 13px;
|
|
918
|
+
}
|
|
919
|
+
@media (max-width: 900px) {
|
|
920
|
+
.viewer-auth-panel { grid-template-columns: 1fr; padding: 12px 16px; }
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/* Viewer footer */
|
|
924
|
+
.viewer-footer {
|
|
925
|
+
margin-top: 48px; padding: 16px 0 24px;
|
|
926
|
+
border-top: 1px solid var(--border-light);
|
|
927
|
+
display: flex; align-items: center; gap: 10px;
|
|
928
|
+
font-family: var(--font-ui); font-size: 11px;
|
|
929
|
+
color: var(--ink-faint); letter-spacing: 0.05em;
|
|
930
|
+
}
|
|
931
|
+
.viewer-footer a { color: var(--ink-muted); text-decoration: none; }
|
|
932
|
+
.viewer-footer a:hover { color: var(--ink); text-decoration: underline; }
|
|
933
|
+
.viewer-footer .footer-sep { color: var(--ink-faint); opacity: 0.5; }
|
|
934
|
+
|
|
935
|
+
.loading { color: var(--ink-faint); padding: 20px; text-align: center; font-style: italic; font-family: var(--font-body); }
|
|
936
|
+
.empty { color: var(--ink-muted); padding: 24px; text-align: center; font-family: var(--font-body); font-style: italic; border: 1px dashed var(--border); }
|
|
937
|
+
|
|
938
|
+
.replay-controls { display: flex; align-items: center; gap: 6px; padding: 10px 0; flex-wrap: wrap; font-family: var(--font-ui); font-size: 12px; }
|
|
939
|
+
.replay-controls button { padding: 4px 10px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); cursor: pointer; font-family: var(--font-ui); font-size: 12px; }
|
|
940
|
+
.replay-controls button:hover { background: var(--bg-alt); }
|
|
941
|
+
.replay-controls button.active { background: var(--ink); color: var(--bg); }
|
|
942
|
+
.replay-controls .sep { width: 12px; }
|
|
943
|
+
.replay-progress { height: 3px; background: var(--border-light); margin: 4px 0 12px 0; }
|
|
944
|
+
.replay-progress-bar { height: 100%; background: var(--ink); transition: width 100ms linear; }
|
|
945
|
+
.replay-grid { display: grid; grid-template-columns: 340px 1fr; gap: 16px; align-items: start; }
|
|
946
|
+
.replay-list { max-height: 60vh; overflow-y: auto; border: 1px solid var(--border); }
|
|
947
|
+
.replay-event { display: grid; grid-template-columns: 90px 1fr 60px; gap: 8px; padding: 6px 10px; border-bottom: 1px solid var(--border-light); font-family: var(--font-ui); font-size: 11px; cursor: default; }
|
|
948
|
+
.replay-event:hover { background: var(--bg-alt); }
|
|
949
|
+
.replay-event-active { background: var(--bg-alt); border-left: 2px solid var(--ink); }
|
|
950
|
+
.replay-event-kind { font-size: 9px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-muted); align-self: center; }
|
|
951
|
+
.replay-event-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
952
|
+
.replay-event-time { text-align: right; font-family: var(--font-mono); color: var(--ink-muted); }
|
|
953
|
+
.replay-event-prompt .replay-event-kind { color: var(--blue, #0366d6); }
|
|
954
|
+
.replay-event-response .replay-event-kind { color: var(--green, #2ea043); }
|
|
955
|
+
.replay-event-tool_call .replay-event-kind { color: var(--orange, #bf8700); }
|
|
956
|
+
.replay-event-tool_result .replay-event-kind { color: var(--ink-muted); }
|
|
957
|
+
.replay-event-tool_error .replay-event-kind { color: var(--red, #cf222e); }
|
|
958
|
+
.replay-detail { border: 1px solid var(--border); padding: 14px; max-height: 60vh; overflow-y: auto; font-family: var(--font-body); font-size: 13px; }
|
|
959
|
+
.replay-detail-header { margin-bottom: 6px; }
|
|
960
|
+
.replay-body { background: var(--bg-alt); padding: 10px; white-space: pre-wrap; word-break: break-word; font-family: var(--font-mono); font-size: 12px; }
|
|
961
|
+
.replay-tool { margin-top: 10px; font-family: var(--font-ui); font-size: 12px; }
|
|
962
|
+
.replay-tool-block { margin-top: 8px; }
|
|
963
|
+
.replay-tool-block pre { background: var(--bg-alt); padding: 10px; max-height: 240px; overflow: auto; font-family: var(--font-mono); font-size: 11px; white-space: pre-wrap; word-break: break-word; }
|
|
964
|
+
.muted { color: var(--ink-muted); font-size: 11px; }
|
|
965
|
+
|
|
966
|
+
.metric-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
967
|
+
.metric-table th { padding: 6px 8px; font-size: 9px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-muted); border-bottom: 2px solid var(--border); text-align: left; font-family: var(--font-ui); font-weight: 600; }
|
|
968
|
+
.metric-table td { padding: 5px 8px; border-bottom: 1px solid var(--border-light); }
|
|
969
|
+
.metric-table tr:hover td { background: var(--bg-alt); }
|
|
970
|
+
.metric-fn { font-family: var(--font-mono); font-size: 11px; color: var(--blue); }
|
|
971
|
+
.metric-num { font-family: var(--font-mono); color: var(--ink); text-align: right; }
|
|
972
|
+
|
|
973
|
+
.cb-indicator { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; font-size: 10px; font-weight: 600; font-family: var(--font-ui); text-transform: uppercase; letter-spacing: 0.08em; border: 1px solid; }
|
|
974
|
+
.cb-closed { border-color: var(--green); color: var(--green); }
|
|
975
|
+
.cb-open { border-color: var(--accent); color: var(--accent); }
|
|
976
|
+
.cb-half-open { border-color: var(--yellow); color: var(--yellow); }
|
|
977
|
+
|
|
978
|
+
.worker-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; font-family: var(--font-ui); }
|
|
979
|
+
.worker-dot { width: 8px; height: 8px; }
|
|
980
|
+
.worker-dot.running { background: var(--green); }
|
|
981
|
+
.worker-dot.stopped { background: var(--accent); }
|
|
982
|
+
.worker-dot.starting { background: var(--yellow); }
|
|
983
|
+
|
|
984
|
+
.gauge { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
985
|
+
.gauge-bar { flex: 1; height: 6px; background: var(--bg-inset); overflow: hidden; }
|
|
986
|
+
.gauge-fill { height: 100%; transition: width 0.5s; }
|
|
987
|
+
.gauge-label { width: 90px; font-size: 10px; color: var(--ink-muted); font-family: var(--font-ui); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; }
|
|
988
|
+
.gauge-value { width: 70px; font-size: 11px; color: var(--ink); text-align: right; font-family: var(--font-mono); }
|
|
989
|
+
|
|
990
|
+
.obs-type-icon { font-size: 16px; margin-right: 4px; }
|
|
991
|
+
.obs-subtitle { font-size: 12px; color: var(--ink-faint); margin-top: 2px; font-style: italic; }
|
|
992
|
+
.obs-importance { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; font-size: 11px; font-weight: 700; font-family: var(--font-mono); border: 1px solid; }
|
|
993
|
+
.imp-1, .imp-2, .imp-3 { border-color: var(--green); color: var(--green); }
|
|
994
|
+
.imp-4, .imp-5, .imp-6 { border-color: var(--yellow); color: var(--yellow); }
|
|
995
|
+
.imp-7, .imp-8, .imp-9, .imp-10 { border-color: var(--accent); color: var(--accent); }
|
|
996
|
+
|
|
997
|
+
.three-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
|
|
998
|
+
@media (max-width: 1100px) { .three-col { grid-template-columns: 1fr 1fr; } }
|
|
999
|
+
@media (max-width: 768px) { .three-col { grid-template-columns: 1fr; } }
|
|
1000
|
+
|
|
1001
|
+
.pagination {
|
|
1002
|
+
display: flex;
|
|
1003
|
+
justify-content: center;
|
|
1004
|
+
gap: 8px;
|
|
1005
|
+
margin-top: 20px;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.modal-overlay {
|
|
1009
|
+
display: none;
|
|
1010
|
+
position: fixed;
|
|
1011
|
+
inset: 0;
|
|
1012
|
+
background: rgba(0,0,0,0.3);
|
|
1013
|
+
z-index: 100;
|
|
1014
|
+
align-items: center;
|
|
1015
|
+
justify-content: center;
|
|
1016
|
+
}
|
|
1017
|
+
.modal-overlay.open { display: flex; }
|
|
1018
|
+
.modal {
|
|
1019
|
+
background: var(--bg);
|
|
1020
|
+
border: 2px solid var(--border);
|
|
1021
|
+
padding: 28px;
|
|
1022
|
+
max-width: 460px;
|
|
1023
|
+
width: 90%;
|
|
1024
|
+
box-shadow: 6px 6px 0px 0px var(--border);
|
|
1025
|
+
}
|
|
1026
|
+
.modal h3 {
|
|
1027
|
+
font-size: 18px;
|
|
1028
|
+
font-weight: 700;
|
|
1029
|
+
color: var(--ink);
|
|
1030
|
+
margin-bottom: 12px;
|
|
1031
|
+
font-family: var(--font-display);
|
|
1032
|
+
}
|
|
1033
|
+
.modal p { font-size: 13px; color: var(--ink-muted); margin-bottom: 16px; }
|
|
1034
|
+
.modal-actions {
|
|
1035
|
+
display: flex;
|
|
1036
|
+
justify-content: flex-end;
|
|
1037
|
+
gap: 8px;
|
|
1038
|
+
}
|
|
1039
|
+
.selected-node-info {
|
|
1040
|
+
margin-top: 16px;
|
|
1041
|
+
padding-top: 16px;
|
|
1042
|
+
border-top: 1px solid var(--border-light);
|
|
1043
|
+
}
|
|
1044
|
+
.selected-node-info h4 {
|
|
1045
|
+
font-size: 13px;
|
|
1046
|
+
font-weight: 700;
|
|
1047
|
+
color: var(--ink);
|
|
1048
|
+
margin-bottom: 6px;
|
|
1049
|
+
font-family: var(--font-display);
|
|
1050
|
+
}
|
|
1051
|
+
.selected-node-info .prop {
|
|
1052
|
+
font-size: 12px;
|
|
1053
|
+
color: var(--ink-muted);
|
|
1054
|
+
padding: 2px 0;
|
|
1055
|
+
font-family: var(--font-ui);
|
|
1056
|
+
}
|
|
1057
|
+
.two-col {
|
|
1058
|
+
display: grid;
|
|
1059
|
+
grid-template-columns: 1fr 1fr;
|
|
1060
|
+
gap: 16px;
|
|
1061
|
+
}
|
|
1062
|
+
@media (max-width: 768px) {
|
|
1063
|
+
.two-col { grid-template-columns: 1fr; }
|
|
1064
|
+
.graph-sidebar { width: 200px; }
|
|
1065
|
+
.stats-grid { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
.section-rule {
|
|
1069
|
+
border: none;
|
|
1070
|
+
border-top: 1px solid var(--border-light);
|
|
1071
|
+
margin: 20px 0;
|
|
1072
|
+
}
|
|
1073
|
+
.dateline {
|
|
1074
|
+
font-family: var(--font-mono);
|
|
1075
|
+
font-size: 10px;
|
|
1076
|
+
color: var(--ink-faint);
|
|
1077
|
+
text-transform: uppercase;
|
|
1078
|
+
letter-spacing: 0.1em;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.timeline-container { position: relative; padding: 20px 0; }
|
|
1082
|
+
.timeline-container::before { content: ''; position: absolute; left: 50%; top: 0; bottom: 0; width: 2px; background: var(--border-light); transform: translateX(-50%); }
|
|
1083
|
+
.timeline-item { position: relative; width: 45%; margin-bottom: 20px; min-width: 0; }
|
|
1084
|
+
.timeline-item.tl-left { margin-left: 0; margin-right: auto; text-align: right; padding-right: 30px; }
|
|
1085
|
+
.timeline-item.tl-right { margin-left: auto; margin-right: 0; padding-left: 30px; }
|
|
1086
|
+
.timeline-dot { position: absolute; width: 12px; height: 12px; border-radius: 50%; top: 16px; z-index: 1; border: 2px solid var(--bg); }
|
|
1087
|
+
.timeline-item.tl-left .timeline-dot { right: -6px; transform: translateX(50%); }
|
|
1088
|
+
.timeline-item.tl-right .timeline-dot { left: -6px; transform: translateX(-50%); }
|
|
1089
|
+
.timeline-connector { position: absolute; top: 21px; height: 1px; background: var(--border-light); width: 24px; }
|
|
1090
|
+
.timeline-item.tl-left .timeline-connector { right: 0; }
|
|
1091
|
+
.timeline-item.tl-right .timeline-connector { left: 0; }
|
|
1092
|
+
.timeline-date-marker { text-align: center; position: relative; margin: 24px 0 16px; z-index: 2; }
|
|
1093
|
+
.timeline-date-marker span { background: var(--bg); padding: 4px 16px; font-size: 10px; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-muted); border: 1px solid var(--border-light); }
|
|
1094
|
+
|
|
1095
|
+
.heatmap-wrap { overflow-x: auto; padding: 8px 0; }
|
|
1096
|
+
.heatmap-grid { display: grid; grid-template-rows: repeat(7, 1fr); grid-auto-flow: column; grid-auto-columns: 12px; gap: 2px; }
|
|
1097
|
+
.heatmap-cell { width: 10px; height: 10px; background: var(--bg-inset); cursor: default; }
|
|
1098
|
+
.heatmap-cell[title] { cursor: pointer; }
|
|
1099
|
+
.heatmap-cell.level-1 { background: rgba(45,106,79,0.2); }
|
|
1100
|
+
.heatmap-cell.level-2 { background: rgba(45,106,79,0.4); }
|
|
1101
|
+
.heatmap-cell.level-3 { background: rgba(45,106,79,0.65); }
|
|
1102
|
+
.heatmap-cell.level-4 { background: var(--green); }
|
|
1103
|
+
.heatmap-labels { display: flex; gap: 2px; font-size: 9px; color: var(--ink-faint); font-family: var(--font-mono); margin-bottom: 4px; }
|
|
1104
|
+
|
|
1105
|
+
.graph-search { width: 100%; background: var(--bg); border: 1px solid var(--border); padding: 7px 12px; font-size: 12px; font-family: var(--font-ui); margin-bottom: 12px; outline: none; }
|
|
1106
|
+
.graph-search:focus { border-color: var(--ink); box-shadow: 2px 2px 0px 0px var(--border); }
|
|
1107
|
+
.graph-legend { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-light); }
|
|
1108
|
+
.graph-legend-item { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 11px; font-family: var(--font-ui); color: var(--ink-muted); }
|
|
1109
|
+
.graph-legend-shape { width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; }
|
|
1110
|
+
.graph-tooltip { position: absolute; background: rgba(255,255,255,0.88); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(17,17,17,0.08); padding: 12px 16px; font-size: 11px; font-family: var(--font-ui); pointer-events: none; z-index: 10; box-shadow: 0 8px 32px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06); max-width: 260px; display: none; border-radius: 8px; transition: opacity 0.15s ease; }
|
|
1111
|
+
.graph-tooltip.visible { display: block; opacity: 1; }
|
|
1112
|
+
.graph-tooltip .tt-name { font-weight: 700; color: var(--ink); margin-bottom: 4px; font-family: var(--font-display); font-size: 13px; }
|
|
1113
|
+
.graph-tooltip .tt-type { font-size: 9px; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 6px; font-weight: 600; padding: 2px 6px; border-radius: 3px; display: inline-block; }
|
|
1114
|
+
.graph-tooltip .tt-prop { font-size: 10px; color: var(--ink-muted); padding: 1px 0; }
|
|
1115
|
+
.graph-tooltip .tt-conns { font-size: 10px; color: var(--ink-faint); margin-top: 6px; border-top: 1px solid rgba(17,17,17,0.08); padding-top: 6px; font-family: var(--font-mono); }
|
|
1116
|
+
.graph-controls { position: absolute; bottom: 16px; right: 16px; display: flex; flex-direction: column; gap: 2px; z-index: 5; }
|
|
1117
|
+
.graph-controls button { width: 36px; height: 36px; font-size: 18px; cursor: pointer; background: rgba(255,255,255,0.92); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border: 1px solid rgba(17,17,17,0.1); color: var(--ink); display: flex; align-items: center; justify-content: center; font-weight: 500; font-family: var(--font-ui); border-radius: 6px; transition: all 0.15s ease; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
|
1118
|
+
.graph-controls button:hover { background: var(--ink); color: var(--bg); transform: scale(1.05); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
|
1119
|
+
.graph-controls .ctrl-divider { height: 1px; background: var(--border-light); margin: 2px 4px; }
|
|
1120
|
+
|
|
1121
|
+
.type-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
1122
|
+
.type-chip { font-size: 10px; padding: 3px 10px; border: 1px solid var(--border-light); cursor: pointer; font-family: var(--font-ui); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; transition: all 0.15s; background: var(--bg); }
|
|
1123
|
+
.type-chip:hover { border-color: var(--ink); }
|
|
1124
|
+
.type-chip.active { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
1125
|
+
|
|
1126
|
+
.memory-fact { padding: 8px 0; border-bottom: 1px solid var(--border-light); font-size: 13px; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
|
1127
|
+
.memory-fact:last-child { border-bottom: none; }
|
|
1128
|
+
.procedure-item { padding: 10px 0; border-bottom: 1px solid var(--border-light); }
|
|
1129
|
+
.procedure-item:last-child { border-bottom: none; }
|
|
1130
|
+
.procedure-steps { margin: 6px 0 0 16px; font-size: 12px; color: var(--ink-muted); }
|
|
1131
|
+
.procedure-steps li { margin-bottom: 2px; }
|
|
1132
|
+
.consolidation-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 12px; font-family: var(--font-ui); }
|
|
1133
|
+
.consolidation-row .cl { color: var(--ink-muted); }
|
|
1134
|
+
.consolidation-row .cv { color: var(--ink); font-weight: 600; font-family: var(--font-mono); }
|
|
1135
|
+
|
|
1136
|
+
.activity-feed-item { display: flex; gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--border-light); font-size: 13px; }
|
|
1137
|
+
.activity-feed-item:last-child { border-bottom: none; }
|
|
1138
|
+
.activity-feed-icon { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; border: 1px solid var(--border-light); }
|
|
1139
|
+
.activity-feed-body { flex: 1; min-width: 0; }
|
|
1140
|
+
.activity-feed-title { font-weight: 600; color: var(--ink); font-family: var(--font-display); font-size: 13px; }
|
|
1141
|
+
.activity-feed-meta { font-size: 10px; color: var(--ink-faint); font-family: var(--font-mono); margin-top: 2px; }
|
|
1142
|
+
|
|
1143
|
+
/* Personal Second Brain styles */
|
|
1144
|
+
.personal-grid {
|
|
1145
|
+
display: grid;
|
|
1146
|
+
grid-template-columns: 280px 1fr;
|
|
1147
|
+
gap: 20px;
|
|
1148
|
+
align-items: start;
|
|
1149
|
+
}
|
|
1150
|
+
@media (max-width: 768px) {
|
|
1151
|
+
.personal-grid {
|
|
1152
|
+
grid-template-columns: 1fr;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
.personal-sidebar {
|
|
1156
|
+
display: flex;
|
|
1157
|
+
flex-direction: column;
|
|
1158
|
+
gap: 12px;
|
|
1159
|
+
}
|
|
1160
|
+
.personal-search-wrapper {
|
|
1161
|
+
position: relative;
|
|
1162
|
+
}
|
|
1163
|
+
.personal-search-input {
|
|
1164
|
+
width: 100%;
|
|
1165
|
+
background: var(--bg);
|
|
1166
|
+
border: 1px solid var(--border);
|
|
1167
|
+
padding: 10px 14px;
|
|
1168
|
+
font-size: 13px;
|
|
1169
|
+
font-family: var(--font-ui);
|
|
1170
|
+
outline: none;
|
|
1171
|
+
transition: box-shadow 0.15s;
|
|
1172
|
+
}
|
|
1173
|
+
.personal-search-input:focus {
|
|
1174
|
+
box-shadow: 2px 2px 0px 0px var(--border);
|
|
1175
|
+
}
|
|
1176
|
+
.personal-file-list {
|
|
1177
|
+
display: flex;
|
|
1178
|
+
flex-direction: column;
|
|
1179
|
+
border: 1px solid var(--border-light);
|
|
1180
|
+
background: var(--bg);
|
|
1181
|
+
max-height: 500px;
|
|
1182
|
+
overflow-y: auto;
|
|
1183
|
+
}
|
|
1184
|
+
.personal-file-item {
|
|
1185
|
+
padding: 12px 16px;
|
|
1186
|
+
cursor: pointer;
|
|
1187
|
+
border-bottom: 1px solid var(--border-light);
|
|
1188
|
+
transition: background 0.1s, border-left 0.1s;
|
|
1189
|
+
font-family: var(--font-ui);
|
|
1190
|
+
font-size: 13px;
|
|
1191
|
+
color: var(--ink);
|
|
1192
|
+
display: flex;
|
|
1193
|
+
align-items: center;
|
|
1194
|
+
gap: 8px;
|
|
1195
|
+
}
|
|
1196
|
+
.personal-file-item:last-child {
|
|
1197
|
+
border-bottom: none;
|
|
1198
|
+
}
|
|
1199
|
+
.personal-file-item:hover {
|
|
1200
|
+
background: var(--bg-alt);
|
|
1201
|
+
}
|
|
1202
|
+
.personal-file-item.selected {
|
|
1203
|
+
background: var(--bg-alt);
|
|
1204
|
+
border-left: 3px solid var(--accent);
|
|
1205
|
+
font-weight: 600;
|
|
1206
|
+
}
|
|
1207
|
+
.personal-file-name {
|
|
1208
|
+
flex: 1;
|
|
1209
|
+
white-space: nowrap;
|
|
1210
|
+
overflow: hidden;
|
|
1211
|
+
text-overflow: ellipsis;
|
|
1212
|
+
}
|
|
1213
|
+
.personal-file-size {
|
|
1214
|
+
font-size: 10px;
|
|
1215
|
+
color: var(--ink-faint);
|
|
1216
|
+
font-family: var(--font-mono);
|
|
1217
|
+
}
|
|
1218
|
+
.personal-editor-card {
|
|
1219
|
+
background: var(--bg);
|
|
1220
|
+
border: 1px solid var(--border);
|
|
1221
|
+
padding: 24px;
|
|
1222
|
+
min-height: 450px;
|
|
1223
|
+
display: flex;
|
|
1224
|
+
flex-direction: column;
|
|
1225
|
+
gap: 16px;
|
|
1226
|
+
}
|
|
1227
|
+
.personal-editor-header {
|
|
1228
|
+
border-bottom: 1px solid var(--border-light);
|
|
1229
|
+
padding-bottom: 12px;
|
|
1230
|
+
display: flex;
|
|
1231
|
+
justify-content: space-between;
|
|
1232
|
+
align-items: center;
|
|
1233
|
+
flex-wrap: wrap;
|
|
1234
|
+
gap: 10px;
|
|
1235
|
+
}
|
|
1236
|
+
.personal-editor-title {
|
|
1237
|
+
font-size: 16px;
|
|
1238
|
+
font-weight: 700;
|
|
1239
|
+
color: var(--ink);
|
|
1240
|
+
font-family: var(--font-display);
|
|
1241
|
+
}
|
|
1242
|
+
.personal-editor-meta {
|
|
1243
|
+
font-size: 11px;
|
|
1244
|
+
color: var(--ink-muted);
|
|
1245
|
+
font-family: var(--font-mono);
|
|
1246
|
+
}
|
|
1247
|
+
.personal-editor-textarea {
|
|
1248
|
+
width: 100%;
|
|
1249
|
+
height: 380px;
|
|
1250
|
+
background: var(--bg);
|
|
1251
|
+
color: var(--ink);
|
|
1252
|
+
border: 1px solid var(--border-light);
|
|
1253
|
+
padding: 14px;
|
|
1254
|
+
font-family: var(--font-mono);
|
|
1255
|
+
font-size: 13px;
|
|
1256
|
+
line-height: 1.5;
|
|
1257
|
+
resize: vertical;
|
|
1258
|
+
outline: none;
|
|
1259
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
1260
|
+
}
|
|
1261
|
+
.personal-editor-textarea:focus {
|
|
1262
|
+
border-color: var(--border);
|
|
1263
|
+
box-shadow: 2px 2px 0px 0px var(--border);
|
|
1264
|
+
}
|
|
1265
|
+
.personal-actions {
|
|
1266
|
+
display: flex;
|
|
1267
|
+
justify-content: flex-end;
|
|
1268
|
+
align-items: center;
|
|
1269
|
+
gap: 12px;
|
|
1270
|
+
}
|
|
1271
|
+
.personal-status-indicator {
|
|
1272
|
+
font-size: 12px;
|
|
1273
|
+
color: var(--green);
|
|
1274
|
+
font-weight: 500;
|
|
1275
|
+
font-family: var(--font-ui);
|
|
1276
|
+
opacity: 0;
|
|
1277
|
+
transition: opacity 0.35s ease;
|
|
1278
|
+
}
|
|
1279
|
+
.personal-status-indicator.visible {
|
|
1280
|
+
opacity: 1;
|
|
1281
|
+
}
|
|
1282
|
+
.personal-status-indicator.error {
|
|
1283
|
+
color: var(--accent);
|
|
1284
|
+
}
|
|
1285
|
+
</style>
|
|
1286
|
+
</head>
|
|
1287
|
+
<body>
|
|
1288
|
+
<div class="app-header">
|
|
1289
|
+
<a class="brand" href="#folders" data-tab-link="folders" aria-label="Open folders view">
|
|
1290
|
+
<h1>agentcache</h1>
|
|
1291
|
+
<span class="version">v__AGENTCACHE_VERSION__</span>
|
|
1292
|
+
</a>
|
|
1293
|
+
<div class="header-right">
|
|
1294
|
+
<span class="dateline" id="dateline"></span>
|
|
1295
|
+
<button id="theme-toggle" class="btn" style="font-size:9px;padding:3px 10px;letter-spacing:0.1em;margin-right:8px;" data-action="toggle-theme">DARK</button>
|
|
1296
|
+
<span id="ws-status" class="ws-status disconnected">live updates off</span>
|
|
1297
|
+
</div>
|
|
1298
|
+
</div>
|
|
1299
|
+
|
|
1300
|
+
<div class="tab-bar" id="tab-bar" role="tablist" aria-label="Main navigation">
|
|
1301
|
+
<button class="active" data-tab="folders" role="tab" aria-selected="true" aria-controls="view-folders" id="tab-btn-folders">Folders</button>
|
|
1302
|
+
<button data-tab="memories" role="tab" aria-selected="false" aria-controls="view-memories" id="tab-btn-memories">Memories</button>
|
|
1303
|
+
<button data-tab="graph" role="tab" aria-selected="false" aria-controls="view-graph" id="tab-btn-graph">Graph</button>
|
|
1304
|
+
<button data-tab="timeline" role="tab" aria-selected="false" aria-controls="view-timeline" id="tab-btn-timeline">Timeline</button>
|
|
1305
|
+
<button data-tab="tools" role="tab" aria-selected="false" aria-controls="view-tools" id="tab-btn-tools">Tools</button>
|
|
1306
|
+
</div>
|
|
1307
|
+
|
|
1308
|
+
<div id="flag-banners" class="flag-banners"></div>
|
|
1309
|
+
<div id="viewer-auth" class="viewer-auth"></div>
|
|
1310
|
+
|
|
1311
|
+
<div id="view-folders" class="view active" role="tabpanel" aria-labelledby="tab-btn-folders"></div>
|
|
1312
|
+
<div id="view-graph" class="view" role="tabpanel" aria-labelledby="tab-btn-graph"></div>
|
|
1313
|
+
<div id="view-memories" class="view" role="tabpanel" aria-labelledby="tab-btn-memories"></div>
|
|
1314
|
+
<div id="view-timeline" class="view" role="tabpanel" aria-labelledby="tab-btn-timeline"></div>
|
|
1315
|
+
<div id="view-tools" class="view" role="tabpanel" aria-labelledby="tab-btn-tools"></div>
|
|
1316
|
+
<!-- legacy view containers removed -- backed by endpoints that no longer exist -->
|
|
1317
|
+
|
|
1318
|
+
<!-- Command Palette (B1) -->
|
|
1319
|
+
<div id="cmd-palette" hidden role="dialog" aria-modal="true" aria-label="Command Palette">
|
|
1320
|
+
<div id="cmd-backdrop"></div>
|
|
1321
|
+
<div id="cmd-panel">
|
|
1322
|
+
<input id="cmd-input" type="text" placeholder="Search folders, memories, tabs..." autocomplete="off" aria-label="Command palette search">
|
|
1323
|
+
<ul id="cmd-results" role="listbox" aria-label="Search results"></ul>
|
|
1324
|
+
</div>
|
|
1325
|
+
</div>
|
|
1326
|
+
|
|
1327
|
+
<div id="modal-overlay" class="modal-overlay">
|
|
1328
|
+
<div class="modal" id="modal"></div>
|
|
1329
|
+
</div>
|
|
1330
|
+
|
|
1331
|
+
<footer id="viewer-footer" class="viewer-footer">
|
|
1332
|
+
<span>agentcache viewer · <span id="footer-version">loading...</span></span>
|
|
1333
|
+
<span class="footer-sep">·</span>
|
|
1334
|
+
<a href="https://github.com/rohitg00/agentmemory" target="_blank" rel="noopener">github</a>
|
|
1335
|
+
<span class="footer-sep">·</span>
|
|
1336
|
+
<a href="https://github.com/rohitg00/agentmemory#readme" target="_blank" rel="noopener">docs</a>
|
|
1337
|
+
<span class="footer-sep">·</span>
|
|
1338
|
+
<a id="footer-feedback" href="#" target="_blank" rel="noopener">report issue →</a>
|
|
1339
|
+
</footer>
|
|
1340
|
+
|
|
1341
|
+
<script nonce="__AGENTCACHE_VIEWER_NONCE__">
|
|
1342
|
+
var safeStorage = {
|
|
1343
|
+
_mem: {},
|
|
1344
|
+
getItem: function(key) {
|
|
1345
|
+
try { return localStorage.getItem(key); } catch (_) { return this._mem[key] || null; }
|
|
1346
|
+
},
|
|
1347
|
+
setItem: function(key, val) {
|
|
1348
|
+
try { localStorage.setItem(key, val); } catch (_) { this._mem[key] = String(val); }
|
|
1349
|
+
},
|
|
1350
|
+
removeItem: function(key) {
|
|
1351
|
+
try { localStorage.removeItem(key); } catch (_) { delete this._mem[key]; }
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
var params = new URLSearchParams(window.location.search);
|
|
1355
|
+
var paramPort = params.get('port');
|
|
1356
|
+
var locPort = window.location.port;
|
|
1357
|
+
var wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1358
|
+
var REST, WS_URL, WS_DIRECT_URL, wsPort;
|
|
1359
|
+
if (paramPort) {
|
|
1360
|
+
var resolvedPort = paramPort;
|
|
1361
|
+
REST = window.location.protocol + '//' + window.location.hostname + ':' + resolvedPort;
|
|
1362
|
+
wsPort = params.get('wsPort') || resolvedPort;
|
|
1363
|
+
WS_URL = wsProto + '//' + window.location.hostname + ':' + wsPort;
|
|
1364
|
+
WS_DIRECT_URL = WS_URL + '/stream/mem-live/viewer';
|
|
1365
|
+
} else if (locPort) {
|
|
1366
|
+
var resolvedPort = locPort;
|
|
1367
|
+
REST = window.location.protocol + '//' + window.location.hostname + ':' + resolvedPort;
|
|
1368
|
+
wsPort = params.get('wsPort') || resolvedPort;
|
|
1369
|
+
WS_URL = wsProto + '//' + window.location.hostname + ':' + wsPort;
|
|
1370
|
+
WS_DIRECT_URL = WS_URL + '/stream/mem-live/viewer';
|
|
1371
|
+
} else {
|
|
1372
|
+
REST = window.location.origin;
|
|
1373
|
+
wsPort = params.get('wsPort');
|
|
1374
|
+
WS_URL = wsPort
|
|
1375
|
+
? wsProto + '//' + window.location.hostname + ':' + wsPort
|
|
1376
|
+
: wsProto + '//' + window.location.host;
|
|
1377
|
+
WS_DIRECT_URL = WS_URL + '/stream/mem-live/viewer';
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
var dateEl = document.getElementById('dateline');
|
|
1381
|
+
if (dateEl) dateEl.textContent = new Date().toLocaleDateString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' });
|
|
1382
|
+
|
|
1383
|
+
function isDarkMode() { return document.documentElement.dataset.theme === 'dark'; }
|
|
1384
|
+
function applyTheme(dark, persist) {
|
|
1385
|
+
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
|
|
1386
|
+
var btn = document.getElementById('theme-toggle');
|
|
1387
|
+
if (btn) btn.textContent = dark ? 'LIGHT' : 'DARK';
|
|
1388
|
+
if (persist) safeStorage.setItem('agentcache-theme', dark ? 'dark' : 'light');
|
|
1389
|
+
}
|
|
1390
|
+
window.toggleTheme = function() { applyTheme(!isDarkMode(), true); };
|
|
1391
|
+
var savedTheme = safeStorage.getItem('agentcache-theme');
|
|
1392
|
+
if (savedTheme) {
|
|
1393
|
+
applyTheme(savedTheme === 'dark', false);
|
|
1394
|
+
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
1395
|
+
applyTheme(true, false);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
var NODE_COLORS = {
|
|
1399
|
+
file: '#2D6A4F', function: '#1D4E89', concept: '#B8860B', error: '#CC0000',
|
|
1400
|
+
decision: '#6B3FA0', pattern: '#2563EB', library: '#C2410C', person: '#111111',
|
|
1401
|
+
folder: '#4F46E5'
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
function folderColor(id) {
|
|
1405
|
+
var h = 0;
|
|
1406
|
+
for (var i = 0; i < id.length; i++) { h = (h * 31 + id.charCodeAt(i)) & 0xfffffff; }
|
|
1407
|
+
var hue = (h % 360 + 360) % 360;
|
|
1408
|
+
var sat = 55 + (h % 25);
|
|
1409
|
+
var lig = isDarkMode() ? 52 + (h % 16) : 38 + (h % 14);
|
|
1410
|
+
// Return hex so parseInt(color.slice(1,3),16) works in gradient code
|
|
1411
|
+
var s = sat / 100, l = lig / 100;
|
|
1412
|
+
var a = s * Math.min(l, 1 - l);
|
|
1413
|
+
function f(n) {
|
|
1414
|
+
var k = (n + hue / 30) % 12;
|
|
1415
|
+
var c = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
1416
|
+
return Math.round(255 * c).toString(16).padStart(2, '0');
|
|
1417
|
+
}
|
|
1418
|
+
return '#' + f(0) + f(8) + f(4);
|
|
1419
|
+
}
|
|
1420
|
+
var OP_BADGES = {
|
|
1421
|
+
observe: 'badge-blue', compress: 'badge-cyan', remember: 'badge-green',
|
|
1422
|
+
forget: 'badge-red', evolve: 'badge-purple', consolidate: 'badge-yellow',
|
|
1423
|
+
share: 'badge-orange', delete: 'badge-red', import: 'badge-blue', export: 'badge-blue'
|
|
1424
|
+
};
|
|
1425
|
+
var TYPE_BADGES = {
|
|
1426
|
+
pattern: 'badge-purple', preference: 'badge-blue', architecture: 'badge-cyan',
|
|
1427
|
+
bug: 'badge-red', workflow: 'badge-green', fact: 'badge-yellow'
|
|
1428
|
+
};
|
|
1429
|
+
var OBS_TYPE_COLORS = {
|
|
1430
|
+
file_read: '#1D4E89', file_write: '#2D6A4F', file_edit: '#B8860B',
|
|
1431
|
+
command_run: '#C2410C', search: '#2563EB', web_fetch: '#6B3FA0',
|
|
1432
|
+
conversation: '#111111', error: '#CC0000', decision: '#B8860B',
|
|
1433
|
+
discovery: '#2D6A4F', subagent: '#6B3FA0', notification: '#0E7490',
|
|
1434
|
+
task: '#1D4E89', other: '#666666'
|
|
1435
|
+
};
|
|
1436
|
+
var OBS_TYPE_ICONS = {
|
|
1437
|
+
file_read: '📄', file_write: '✏', file_edit: '📝',
|
|
1438
|
+
command_run: '⚡', search: '🔎', web_fetch: '🌐',
|
|
1439
|
+
conversation: '💬', error: '⚠', decision: '🤔',
|
|
1440
|
+
discovery: '💡', subagent: '🤖', notification: '🔔',
|
|
1441
|
+
task: '☑', other: '📄'
|
|
1442
|
+
};
|
|
1443
|
+
var CB_STATE_COLORS = { closed: 'badge-green', open: 'badge-red', 'half-open': 'badge-yellow' };
|
|
1444
|
+
var TAB_IDS = ['folders', 'memories', 'graph', 'timeline', 'tools'];
|
|
1445
|
+
var VIEWER_TOKEN_STORAGE_KEY = 'agentcache-viewer-token';
|
|
1446
|
+
|
|
1447
|
+
var state = {
|
|
1448
|
+
activeTab: 'folders',
|
|
1449
|
+
graph: { loaded: false, nodes: [], edges: [], stats: null, filters: {}, selectedNode: null, queryError: null, truncated: false, totalNodes: 0, totalEdges: 0 },
|
|
1450
|
+
memories: { loaded: false, items: [], search: '', typeFilter: '', folderFilter: '' },
|
|
1451
|
+
timeline: { loaded: false, observations: [], sessionId: '', minImportance: 0, page: 0, pageSize: 50 },
|
|
1452
|
+
audit: { loaded: false, entries: [], opFilter: '' },
|
|
1453
|
+
flagsConfig: null,
|
|
1454
|
+
flagsDismissed: {},
|
|
1455
|
+
ws: null
|
|
1456
|
+
};
|
|
1457
|
+
|
|
1458
|
+
function esc(s) {
|
|
1459
|
+
if (!s) return '';
|
|
1460
|
+
var d = document.createElement('div');
|
|
1461
|
+
d.textContent = String(s);
|
|
1462
|
+
return d.innerHTML;
|
|
1463
|
+
}
|
|
1464
|
+
function formatTime(ts) {
|
|
1465
|
+
if (!ts) return '';
|
|
1466
|
+
try { return new Date(ts).toLocaleString(); } catch { return ts; }
|
|
1467
|
+
}
|
|
1468
|
+
function shortTime(ts) {
|
|
1469
|
+
if (!ts) return '';
|
|
1470
|
+
try { return new Date(ts).toLocaleTimeString(); } catch { return ts; }
|
|
1471
|
+
}
|
|
1472
|
+
function truncate(s, n) {
|
|
1473
|
+
if (!s) return '';
|
|
1474
|
+
return s.length > n ? s.slice(0, n) + '...' : s;
|
|
1475
|
+
}
|
|
1476
|
+
function sessionId(s) {
|
|
1477
|
+
return s && s.id !== undefined && s.id !== null ? String(s.id) : '';
|
|
1478
|
+
}
|
|
1479
|
+
function shortSessionId(s, n) {
|
|
1480
|
+
var id = sessionId(s);
|
|
1481
|
+
return id ? id.slice(0, n || 8) : '';
|
|
1482
|
+
}
|
|
1483
|
+
function sessionDisplayName(s) {
|
|
1484
|
+
var folder = s && s.project ? String(s.project).split(/[\\/]/).pop() : '';
|
|
1485
|
+
var title = s && s.title ? String(s.title).trim() : '';
|
|
1486
|
+
if (title && folder) {
|
|
1487
|
+
return title + ' (' + folder + ')';
|
|
1488
|
+
} else if (title) {
|
|
1489
|
+
return title;
|
|
1490
|
+
} else if (folder) {
|
|
1491
|
+
return folder;
|
|
1492
|
+
}
|
|
1493
|
+
return shortSessionId(s, 8) || 'Unknown session';
|
|
1494
|
+
}
|
|
1495
|
+
function sessionLabel(s) {
|
|
1496
|
+
var id = shortSessionId(s, 8);
|
|
1497
|
+
var name = sessionDisplayName(s);
|
|
1498
|
+
return id ? name + ' (' + id + ')' : name + ' (missing id)';
|
|
1499
|
+
}
|
|
1500
|
+
function debounce(fn, ms) {
|
|
1501
|
+
var t;
|
|
1502
|
+
return function() {
|
|
1503
|
+
var args = arguments, ctx = this;
|
|
1504
|
+
clearTimeout(t);
|
|
1505
|
+
t = setTimeout(function() { fn.apply(ctx, args); }, ms);
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// IME_SAFE_SEARCH_V2
|
|
1510
|
+
function bindImeSafeSearch(input, ms, onSearch) {
|
|
1511
|
+
var composing = false;
|
|
1512
|
+
var justCommitted = false;
|
|
1513
|
+
var run = debounce(function(value) { onSearch(value); }, ms);
|
|
1514
|
+
input.addEventListener('compositionstart', function() { composing = true; });
|
|
1515
|
+
input.addEventListener('compositionend', function() {
|
|
1516
|
+
composing = false;
|
|
1517
|
+
justCommitted = true;
|
|
1518
|
+
onSearch(input.value);
|
|
1519
|
+
setTimeout(function() { justCommitted = false; }, 0);
|
|
1520
|
+
});
|
|
1521
|
+
input.addEventListener('input', function(e) {
|
|
1522
|
+
if (composing || e.isComposing) return;
|
|
1523
|
+
if (justCommitted) return;
|
|
1524
|
+
run(input.value);
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
function captureSearchFocus(ids) {
|
|
1528
|
+
var a = document.activeElement;
|
|
1529
|
+
if (!a || ids.indexOf(a.id) < 0) return null;
|
|
1530
|
+
return { id: a.id, start: a.selectionStart, end: a.selectionEnd };
|
|
1531
|
+
}
|
|
1532
|
+
function restoreSearchFocus(focus) {
|
|
1533
|
+
if (!focus) return;
|
|
1534
|
+
var el = document.getElementById(focus.id);
|
|
1535
|
+
if (!el) return;
|
|
1536
|
+
el.focus();
|
|
1537
|
+
if (typeof el.setSelectionRange === 'function') {
|
|
1538
|
+
try { el.setSelectionRange(focus.start, focus.end); } catch (e) {}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
(function() {
|
|
1542
|
+
var autoToken = '__AGENTCACHE_AUTO_TOKEN__';
|
|
1543
|
+
if (autoToken && autoToken.length > 0 && autoToken !== '__AGENTCACHE_AUTO_TOKEN__') {
|
|
1544
|
+
try { sessionStorage.setItem(VIEWER_TOKEN_STORAGE_KEY, autoToken); } catch (_) {}
|
|
1545
|
+
}
|
|
1546
|
+
})();
|
|
1547
|
+
function getViewerToken() {
|
|
1548
|
+
try { return sessionStorage.getItem(VIEWER_TOKEN_STORAGE_KEY) || ''; } catch (_) { return ''; }
|
|
1549
|
+
}
|
|
1550
|
+
function setViewerToken(token) {
|
|
1551
|
+
try {
|
|
1552
|
+
if (token) sessionStorage.setItem(VIEWER_TOKEN_STORAGE_KEY, token);
|
|
1553
|
+
else sessionStorage.removeItem(VIEWER_TOKEN_STORAGE_KEY);
|
|
1554
|
+
} catch (_) {}
|
|
1555
|
+
}
|
|
1556
|
+
function showViewerAuthPrompt() {
|
|
1557
|
+
var host = document.getElementById('viewer-auth');
|
|
1558
|
+
if (!host) return;
|
|
1559
|
+
host.classList.add('open');
|
|
1560
|
+
host.innerHTML =
|
|
1561
|
+
'<div class="viewer-auth-panel">' +
|
|
1562
|
+
'<div>' +
|
|
1563
|
+
'<div class="viewer-auth-title">Viewer authorization required</div>' +
|
|
1564
|
+
'<div id="viewer-auth-desc" class="viewer-auth-desc">Enter <code>AGENTCACHE_SECRET</code> to unlock viewer API access.</div>' +
|
|
1565
|
+
'</div>' +
|
|
1566
|
+
'<input id="viewer-auth-token" type="password" autocomplete="off" spellcheck="false" aria-label="AGENTCACHE_SECRET" aria-describedby="viewer-auth-desc" placeholder="AGENTCACHE_SECRET" />' +
|
|
1567
|
+
'<button class="btn" data-action="save-viewer-token">Unlock</button>' +
|
|
1568
|
+
'</div>';
|
|
1569
|
+
var input = document.getElementById('viewer-auth-token');
|
|
1570
|
+
if (input && typeof input.focus === 'function') input.focus();
|
|
1571
|
+
}
|
|
1572
|
+
function hideViewerAuthPrompt() {
|
|
1573
|
+
var host = document.getElementById('viewer-auth');
|
|
1574
|
+
if (!host) return;
|
|
1575
|
+
host.classList.remove('open');
|
|
1576
|
+
host.innerHTML = '';
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
async function api(path, opts) {
|
|
1580
|
+
try {
|
|
1581
|
+
var url = REST + '/agentcache/' + path;
|
|
1582
|
+
var headers = Object.assign({ 'Cache-Control': 'no-cache' }, (opts && opts.headers) || {});
|
|
1583
|
+
var viewerToken = getViewerToken();
|
|
1584
|
+
if (viewerToken && !headers.Authorization && !headers.authorization) {
|
|
1585
|
+
headers.Authorization = 'Bearer ' + viewerToken;
|
|
1586
|
+
}
|
|
1587
|
+
var fetchOpts = Object.assign({}, opts || {}, { headers: headers });
|
|
1588
|
+
var res = await fetch(url, fetchOpts);
|
|
1589
|
+
if (!res.ok) {
|
|
1590
|
+
if (res.status === 401) showViewerAuthPrompt();
|
|
1591
|
+
console.warn('[viewer] API ' + (fetchOpts.method || 'GET') + ' ' + path + ' returned ' + res.status);
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
hideViewerAuthPrompt();
|
|
1595
|
+
return await res.json();
|
|
1596
|
+
} catch (err) {
|
|
1597
|
+
console.warn('[viewer] API error on ' + path + ':', err);
|
|
1598
|
+
return null;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
async function apiGet(path) { return api(path); }
|
|
1602
|
+
async function apiPost(path, body) {
|
|
1603
|
+
return api(path, {
|
|
1604
|
+
method: 'POST',
|
|
1605
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1606
|
+
body: JSON.stringify(body || {})
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
async function apiDelete(path, body) {
|
|
1610
|
+
return api(path, {
|
|
1611
|
+
method: 'DELETE',
|
|
1612
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1613
|
+
body: JSON.stringify(body || {})
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
function normalizeTab(tab) {
|
|
1618
|
+
var normalized = String(tab || '').replace(/^#/, '').toLowerCase();
|
|
1619
|
+
return TAB_IDS.indexOf(normalized) >= 0 ? normalized : 'folders';
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function tabFromRoute() {
|
|
1623
|
+
try {
|
|
1624
|
+
return normalizeTab(decodeURIComponent(window.location.hash.slice(1)));
|
|
1625
|
+
} catch (_) {
|
|
1626
|
+
return 'folders';
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function updateTabRoute(tab, replace) {
|
|
1631
|
+
var target = '#' + tab;
|
|
1632
|
+
if (window.location.hash === target) return;
|
|
1633
|
+
if (replace) {
|
|
1634
|
+
history.replaceState(null, '', target);
|
|
1635
|
+
} else {
|
|
1636
|
+
history.pushState(null, '', target);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function switchTab(tab, opts) {
|
|
1641
|
+
opts = opts || {};
|
|
1642
|
+
tab = normalizeTab(tab);
|
|
1643
|
+
if (!opts.skipRoute) {
|
|
1644
|
+
updateTabRoute(tab, !!opts.replaceRoute);
|
|
1645
|
+
}
|
|
1646
|
+
state.activeTab = tab;
|
|
1647
|
+
document.querySelectorAll('.tab-bar button').forEach(function(b) {
|
|
1648
|
+
var isActive = b.dataset.tab === tab;
|
|
1649
|
+
b.classList.toggle('active', isActive);
|
|
1650
|
+
b.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
1651
|
+
if (isActive) {
|
|
1652
|
+
b.setAttribute('aria-current', 'page');
|
|
1653
|
+
} else {
|
|
1654
|
+
b.removeAttribute('aria-current');
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
document.querySelectorAll('.view').forEach(function(v) {
|
|
1658
|
+
v.classList.toggle('active', v.id === 'view-' + tab);
|
|
1659
|
+
});
|
|
1660
|
+
if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
|
|
1661
|
+
loadTab(tab);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
async function loadTab(tab) {
|
|
1665
|
+
switch(tab) {
|
|
1666
|
+
case 'folders': await loadFolders(); break;
|
|
1667
|
+
case 'graph': await loadGraph(); break;
|
|
1668
|
+
case 'memories': await loadMemories(); break;
|
|
1669
|
+
case 'timeline': await loadTimeline(); break;
|
|
1670
|
+
case 'tools': await loadTools(); break;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
async function loadDashboard() {
|
|
1675
|
+
// Dashboard tab was removed; redirect to the Folders tab which is the
|
|
1676
|
+
// active landing page backed by real endpoints (/agentcache/folders).
|
|
1677
|
+
switchTab('folders');
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// =====================================================================
|
|
1681
|
+
// Folders Tab
|
|
1682
|
+
// =====================================================================
|
|
1683
|
+
async function loadFolders() {
|
|
1684
|
+
var el = document.getElementById('view-folders');
|
|
1685
|
+
if (!el) return;
|
|
1686
|
+
el.innerHTML = '<div style="padding:24px;"><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div>';
|
|
1687
|
+
try {
|
|
1688
|
+
var result = await apiGet('folders');
|
|
1689
|
+
var folders = (result && result.folders) || [];
|
|
1690
|
+
window._cachedFolders = folders; // cache for command palette
|
|
1691
|
+
renderFolderList(folders);
|
|
1692
|
+
} catch(err) {
|
|
1693
|
+
el.innerHTML = '<div class="loading" style="color:var(--accent);">Failed to load folders: ' + esc(String(err)) + '</div>';
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function renderFolderList(folders) {
|
|
1698
|
+
var el = document.getElementById('view-folders');
|
|
1699
|
+
if (!el) return;
|
|
1700
|
+
if (folders.length === 0) {
|
|
1701
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-icon">📁</div><p>No folder memories yet.</p><p style="font-size:13px;color:var(--ink-muted);margin-top:8px;">Use <code>agent_observe</code> with a <code>folderPath</code> to start recording.</p></div>';
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
var html = '<div style="padding:20px 24px;">';
|
|
1705
|
+
html += '<h2 style="font-family:var(--font-display);font-size:18px;font-weight:700;color:var(--ink);margin-bottom:16px;">Folder Memories</h2>';
|
|
1706
|
+
html += '<table style="width:100%;border-collapse:collapse;font-family:var(--font-ui);font-size:13px;">';
|
|
1707
|
+
html += '<thead><tr style="border-bottom:2px solid var(--border);">';
|
|
1708
|
+
html += '<th style="text-align:left;padding:8px 12px;color:var(--ink-muted);font-weight:600;">Folder</th>';
|
|
1709
|
+
html += '<th style="text-align:left;padding:8px 12px;color:var(--ink-muted);font-weight:600;">Agent</th>';
|
|
1710
|
+
html += '<th style="text-align:right;padding:8px 12px;color:var(--ink-muted);font-weight:600;">Observations</th>';
|
|
1711
|
+
html += '<th style="text-align:left;padding:8px 12px;color:var(--ink-muted);font-weight:600;">Last Updated</th>';
|
|
1712
|
+
html += '</tr></thead><tbody>';
|
|
1713
|
+
folders.forEach(function(f) {
|
|
1714
|
+
var fp = f.folderPath || '';
|
|
1715
|
+
var aid = f.agentId || '';
|
|
1716
|
+
var obs = f.obsCount || 0;
|
|
1717
|
+
var updated = f.lastUpdated ? new Date(f.lastUpdated).toLocaleString() : '\u2014';
|
|
1718
|
+
html += '<tr class="folder-row" data-folder="' + esc(fp) + '" data-agent="' + esc(aid) + '" style="border-bottom:1px solid var(--border-light);cursor:pointer;" onmouseover="this.style.background=\'var(--bg-subtle)\'" onmouseout="this.style.background=\'\'">';
|
|
1719
|
+
html += '<td style="padding:10px 12px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + esc(fp) + '"><span style="font-family:var(--font-mono);font-size:12px;">' + esc(fp) + '</span></td>';
|
|
1720
|
+
html += '<td style="padding:10px 12px;"><span style="background:var(--bg-inset);padding:2px 8px;border-radius:3px;font-size:11px;font-weight:600;">' + esc(aid) + '</span></td>';
|
|
1721
|
+
html += '<td style="padding:10px 12px;text-align:right;font-weight:600;color:var(--accent);">' + obs + '</td>';
|
|
1722
|
+
html += '<td style="padding:10px 12px;color:var(--ink-muted);font-size:12px;">' + esc(updated) + '</td>';
|
|
1723
|
+
html += '</tr>';
|
|
1724
|
+
});
|
|
1725
|
+
html += '</tbody></table></div>';
|
|
1726
|
+
el.innerHTML = html;
|
|
1727
|
+
|
|
1728
|
+
el.querySelectorAll('.folder-row').forEach(function(row) {
|
|
1729
|
+
row.addEventListener('click', function() {
|
|
1730
|
+
loadFolderDetail(row.dataset.folder, row.dataset.agent);
|
|
1731
|
+
});
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// Per-folder-detail state for bulk selection
|
|
1736
|
+
var _folderDetailFp = '';
|
|
1737
|
+
var _folderDetailAid = '';
|
|
1738
|
+
var _selectedObsIds = new Set();
|
|
1739
|
+
|
|
1740
|
+
async function loadFolderDetail(folderPath, agentId) {
|
|
1741
|
+
var el = document.getElementById('view-folders');
|
|
1742
|
+
if (!el) return;
|
|
1743
|
+
_folderDetailFp = folderPath;
|
|
1744
|
+
_folderDetailAid = agentId;
|
|
1745
|
+
_selectedObsIds = new Set();
|
|
1746
|
+
el.innerHTML = '<div style="padding:24px;"><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div>';
|
|
1747
|
+
try {
|
|
1748
|
+
var qp = 'folderPath=' + encodeURIComponent(folderPath) + '&agentId=' + encodeURIComponent(agentId);
|
|
1749
|
+
var result = await apiFetchRaw('/agentcache/folder/observations?' + qp);
|
|
1750
|
+
var obs = (result && result.observations) || [];
|
|
1751
|
+
|
|
1752
|
+
var html = '<div id="folder-detail-root" style="padding:20px 24px;">';
|
|
1753
|
+
|
|
1754
|
+
// ── Header row ──────────────────────────────────────────────────
|
|
1755
|
+
html += '<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap;">';
|
|
1756
|
+
html += '<button class="btn" data-action="back-to-folders">← Back</button>';
|
|
1757
|
+
html += '<span style="font-family:var(--font-mono);font-size:13px;color:var(--ink);">' + esc(folderPath) + '</span>';
|
|
1758
|
+
html += '<span style="background:var(--bg-inset);padding:2px 8px;border-radius:3px;font-size:11px;font-weight:600;">' + esc(agentId) + '</span>';
|
|
1759
|
+
html += '<span style="font-size:12px;color:var(--ink-muted);">' + obs.length + ' observation' + (obs.length !== 1 ? 's' : '') + '</span>';
|
|
1760
|
+
html += '<span style="flex:1;"></span>';
|
|
1761
|
+
html += '<button class="btn btn-danger" data-action="delete-folder" data-folder-path="' + esc(folderPath) + '" data-agent-id="' + esc(agentId) + '">Delete folder</button>';
|
|
1762
|
+
html += '</div>';
|
|
1763
|
+
|
|
1764
|
+
if (obs.length === 0) {
|
|
1765
|
+
html += '<div class="empty-state"><div class="empty-icon">📋</div><p>No observations found.</p></div>';
|
|
1766
|
+
} else {
|
|
1767
|
+
// ── Bulk-delete toolbar ─────────────────────────────────────────
|
|
1768
|
+
html += '<div id="obs-bulk-toolbar" style="display:flex;align-items:center;gap:10px;margin-bottom:14px;padding:10px 14px;background:var(--bg-alt);border:1px solid var(--border-light);">';
|
|
1769
|
+
html += '<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-family:var(--font-ui);font-size:12px;">';
|
|
1770
|
+
html += '<input type="checkbox" id="obs-select-all" style="accent-color:var(--ink);"> Select all</label>';
|
|
1771
|
+
html += '<button class="btn btn-danger" id="obs-delete-selected-btn" data-action="delete-obs-selected" disabled style="font-size:11px;padding:4px 12px;">Delete selected (0)</button>';
|
|
1772
|
+
html += '<button class="btn" data-action="run-dedup" data-folder-path="' + esc(folderPath) + '" data-agent-id="' + esc(agentId) + '" style="font-size:11px;padding:4px 12px;" title="Remove duplicate observations automatically">✨ Dedup</button>';
|
|
1773
|
+
html += '</div>';
|
|
1774
|
+
|
|
1775
|
+
// ── Observation cards ───────────────────────────────────────────
|
|
1776
|
+
html += '<div id="obs-list">';
|
|
1777
|
+
obs.forEach(function(o) {
|
|
1778
|
+
var ts = o.timestamp ? new Date(o.timestamp).toLocaleString() : '';
|
|
1779
|
+
var typeColor = {'file_edit':'var(--blue)','command_run':'var(--orange)','search':'var(--green)','error':'var(--red)','conversation':'var(--purple)'}[o.type] || 'var(--ink-muted)';
|
|
1780
|
+
html += '<div class="obs-detail-card" data-obs-id="' + esc(o.id) + '" style="border:1px solid var(--border-light);border-radius:4px;padding:12px 16px;margin-bottom:8px;background:var(--bg);display:flex;gap:10px;align-items:flex-start;">';
|
|
1781
|
+
// checkbox
|
|
1782
|
+
html += '<label style="padding-top:2px;cursor:pointer;flex-shrink:0;">';
|
|
1783
|
+
html += '<input type="checkbox" class="obs-checkbox" data-obs-id="' + esc(o.id) + '" style="accent-color:var(--ink);">';
|
|
1784
|
+
html += '</label>';
|
|
1785
|
+
// content
|
|
1786
|
+
html += '<div style="flex:1;min-width:0;">';
|
|
1787
|
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap;">';
|
|
1788
|
+
html += '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:' + typeColor + ';background:var(--bg-subtle);padding:2px 7px;border-radius:3px;">' + esc(o.type || 'other') + '</span>';
|
|
1789
|
+
html += '<span style="font-size:11px;color:var(--ink-muted);font-family:var(--font-mono);">' + esc(ts) + '</span>';
|
|
1790
|
+
html += '<span style="flex:1;"></span>';
|
|
1791
|
+
html += '<button class="btn btn-danger" data-action="delete-obs" data-obs-id="' + esc(o.id) + '" data-folder-path="' + esc(folderPath) + '" data-agent-id="' + esc(agentId) + '" style="font-size:9px;padding:2px 8px;">Delete</button>';
|
|
1792
|
+
html += '</div>';
|
|
1793
|
+
if (o.title) html += '<div style="font-weight:600;font-size:14px;color:var(--ink);margin-bottom:4px;">' + esc(o.title) + '</div>';
|
|
1794
|
+
if (o.text) html += '<div style="font-size:13px;color:var(--ink-secondary);line-height:1.5;white-space:pre-wrap;">' + esc(o.text.length > 300 ? o.text.slice(0, 300) + '\u2026' : o.text) + '</div>';
|
|
1795
|
+
html += '</div>';
|
|
1796
|
+
html += '</div>';
|
|
1797
|
+
});
|
|
1798
|
+
html += '</div>'; // #obs-list
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
html += '</div>'; // #folder-detail-root
|
|
1802
|
+
el.innerHTML = html;
|
|
1803
|
+
|
|
1804
|
+
// Wire up checkbox logic after innerHTML is set
|
|
1805
|
+
_wireFolderDetailCheckboxes();
|
|
1806
|
+
|
|
1807
|
+
} catch(err) {
|
|
1808
|
+
el.innerHTML = '<div style="padding:20px;">' +
|
|
1809
|
+
'<button class="btn" data-action="back-to-folders">← Back</button>' +
|
|
1810
|
+
'<div style="margin-top:16px;color:var(--accent);">Error loading observations: ' + esc(String(err)) + '</div></div>';
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function _wireFolderDetailCheckboxes() {
|
|
1815
|
+
var selectAll = document.getElementById('obs-select-all');
|
|
1816
|
+
var deleteBtn = document.getElementById('obs-delete-selected-btn');
|
|
1817
|
+
if (!selectAll || !deleteBtn) return;
|
|
1818
|
+
|
|
1819
|
+
function updateDeleteBtn() {
|
|
1820
|
+
deleteBtn.textContent = 'Delete selected (' + _selectedObsIds.size + ')';
|
|
1821
|
+
deleteBtn.disabled = _selectedObsIds.size === 0;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
selectAll.addEventListener('change', function() {
|
|
1825
|
+
document.querySelectorAll('.obs-checkbox').forEach(function(cb) {
|
|
1826
|
+
cb.checked = selectAll.checked;
|
|
1827
|
+
var oid = cb.getAttribute('data-obs-id');
|
|
1828
|
+
if (selectAll.checked) { _selectedObsIds.add(oid); }
|
|
1829
|
+
else { _selectedObsIds.delete(oid); }
|
|
1830
|
+
});
|
|
1831
|
+
updateDeleteBtn();
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
document.querySelectorAll('.obs-checkbox').forEach(function(cb) {
|
|
1835
|
+
cb.addEventListener('change', function() {
|
|
1836
|
+
var oid = cb.getAttribute('data-obs-id');
|
|
1837
|
+
if (cb.checked) { _selectedObsIds.add(oid); }
|
|
1838
|
+
else { _selectedObsIds.delete(oid); }
|
|
1839
|
+
// Update select-all indeterminate state
|
|
1840
|
+
var total = document.querySelectorAll('.obs-checkbox').length;
|
|
1841
|
+
selectAll.indeterminate = _selectedObsIds.size > 0 && _selectedObsIds.size < total;
|
|
1842
|
+
selectAll.checked = _selectedObsIds.size === total;
|
|
1843
|
+
updateDeleteBtn();
|
|
1844
|
+
});
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function confirmDeleteFolder(fp, aid) {
|
|
1849
|
+
var modal = document.getElementById('modal');
|
|
1850
|
+
var overlay = document.getElementById('modal-overlay');
|
|
1851
|
+
modal.innerHTML = '<h3>Delete Folder Memory</h3>' +
|
|
1852
|
+
'<p>Delete <strong>all observations</strong> for<br><code style="font-size:11px;">' + esc(fp) + '</code> / <code style="font-size:11px;">' + esc(aid) + '</code>?<br><br>This cannot be undone.</p>' +
|
|
1853
|
+
'<div class="modal-actions">' +
|
|
1854
|
+
'<button class="btn" data-action="close-modal">Cancel</button>' +
|
|
1855
|
+
'<button class="btn btn-danger" data-action="confirm-delete-folder" data-folder-path="' + esc(fp) + '" data-agent-id="' + esc(aid) + '">Delete all</button>' +
|
|
1856
|
+
'</div>';
|
|
1857
|
+
overlay.classList.add('open');
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
async function execDeleteFolder(fp, aid) {
|
|
1861
|
+
closeModal();
|
|
1862
|
+
try {
|
|
1863
|
+
await apiPost('forget', { folderPath: fp, agentId: aid });
|
|
1864
|
+
loadFolders();
|
|
1865
|
+
} catch(err) {
|
|
1866
|
+
alert('Delete failed: ' + String(err));
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function confirmDeleteObs(obsId, fp, aid) {
|
|
1871
|
+
var modal = document.getElementById('modal');
|
|
1872
|
+
var overlay = document.getElementById('modal-overlay');
|
|
1873
|
+
modal.innerHTML = '<h3>Delete Observation</h3>' +
|
|
1874
|
+
'<p>Delete this observation? This cannot be undone.</p>' +
|
|
1875
|
+
'<div class="modal-actions">' +
|
|
1876
|
+
'<button class="btn" data-action="close-modal">Cancel</button>' +
|
|
1877
|
+
'<button class="btn btn-danger" data-action="confirm-delete-obs" data-obs-id="' + esc(obsId) + '" data-folder-path="' + esc(fp) + '" data-agent-id="' + esc(aid) + '">Delete</button>' +
|
|
1878
|
+
'</div>';
|
|
1879
|
+
overlay.classList.add('open');
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
async function execDeleteObs(obsId, fp, aid) {
|
|
1883
|
+
closeModal();
|
|
1884
|
+
try {
|
|
1885
|
+
await apiPost('forget', { folderPath: fp, agentId: aid, observationIds: [obsId] });
|
|
1886
|
+
// Remove card from DOM without reload
|
|
1887
|
+
var card = document.querySelector('.obs-detail-card[data-obs-id="' + obsId + '"]');
|
|
1888
|
+
if (card) card.remove();
|
|
1889
|
+
_selectedObsIds.delete(obsId);
|
|
1890
|
+
// Update count in header
|
|
1891
|
+
var remaining = document.querySelectorAll('.obs-detail-card').length;
|
|
1892
|
+
var countEl = document.querySelector('#folder-detail-root span[style*="ink-muted"]');
|
|
1893
|
+
if (countEl) countEl.textContent = remaining + ' observation' + (remaining !== 1 ? 's' : '');
|
|
1894
|
+
} catch(err) {
|
|
1895
|
+
alert('Delete failed: ' + String(err));
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
async function execDeleteObsSelected() {
|
|
1900
|
+
var ids = Array.from(_selectedObsIds);
|
|
1901
|
+
if (!ids.length) return;
|
|
1902
|
+
closeModal();
|
|
1903
|
+
try {
|
|
1904
|
+
await apiPost('forget', { folderPath: _folderDetailFp, agentId: _folderDetailAid, observationIds: ids });
|
|
1905
|
+
ids.forEach(function(oid) {
|
|
1906
|
+
var card = document.querySelector('.obs-detail-card[data-obs-id="' + oid + '"]');
|
|
1907
|
+
if (card) card.remove();
|
|
1908
|
+
_selectedObsIds.delete(oid);
|
|
1909
|
+
});
|
|
1910
|
+
var remaining = document.querySelectorAll('.obs-detail-card').length;
|
|
1911
|
+
var countEl = document.querySelector('#folder-detail-root span[style*="ink-muted"]');
|
|
1912
|
+
if (countEl) countEl.textContent = remaining + ' observation' + (remaining !== 1 ? 's' : '');
|
|
1913
|
+
var deleteBtn = document.getElementById('obs-delete-selected-btn');
|
|
1914
|
+
if (deleteBtn) { deleteBtn.textContent = 'Delete selected (0)'; deleteBtn.disabled = true; }
|
|
1915
|
+
var selectAll = document.getElementById('obs-select-all');
|
|
1916
|
+
if (selectAll) { selectAll.checked = false; selectAll.indeterminate = false; }
|
|
1917
|
+
} catch(err) {
|
|
1918
|
+
alert('Bulk delete failed: ' + String(err));
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
async function apiFetchRaw(path, opts) {
|
|
1923
|
+
opts = opts || {};
|
|
1924
|
+
var headers = Object.assign({ 'Content-Type': 'application/json' }, opts.headers || {});
|
|
1925
|
+
var viewerToken = getViewerToken();
|
|
1926
|
+
if (viewerToken && !headers.Authorization) headers['Authorization'] = 'Bearer ' + viewerToken;
|
|
1927
|
+
// Always use REST base so cross-origin ?port= param works correctly
|
|
1928
|
+
var url = path.startsWith('http') ? path
|
|
1929
|
+
: REST + (path.startsWith('/') ? path : '/agentcache/' + path);
|
|
1930
|
+
var res = await fetch(url, Object.assign({}, opts, { headers: headers }));
|
|
1931
|
+
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
|
1932
|
+
return res.json();
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
async function apiFetch(path, opts) {
|
|
1936
|
+
return apiFetchRaw(path, opts);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
var graphSim = { nodes: [], edges: [], running: false, canvas: null, ctx: null, raf: null, panX: 0, panY: 0, zoom: 1, dragNode: null, mouseX: 0, mouseY: 0, tickCount: 0, quietTicks: 0 };
|
|
1942
|
+
function wakeGraphSim() {
|
|
1943
|
+
graphSim.quietTicks = 0;
|
|
1944
|
+
if (graphSim.running && !graphSim.raf) {
|
|
1945
|
+
graphSim.raf = requestAnimationFrame(runSimulation);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
var GRAPH_INITIAL_LIMIT = 500;
|
|
1950
|
+
|
|
1951
|
+
async function loadGraph() {
|
|
1952
|
+
var el = document.getElementById('view-graph');
|
|
1953
|
+
el.innerHTML = '<div class="graph-container"><div class="graph-canvas-wrap"><canvas id="graph-canvas"></canvas><div class="graph-controls"><button title="Zoom In" data-action="zoom-graph" data-dir="1">+</button><button title="Zoom Out" data-action="zoom-graph" data-dir="-1">−</button><div class="ctrl-divider"></div><button title="Recenter" data-action="recenter-graph">⌖</button></div><div class="graph-tooltip" id="graph-tooltip"></div></div><div class="graph-sidebar" id="graph-sidebar"></div></div>';
|
|
1954
|
+
|
|
1955
|
+
state.graph.queryError = null;
|
|
1956
|
+
var graphData;
|
|
1957
|
+
try {
|
|
1958
|
+
graphData = await apiFetchRaw('/agentcache/graph');
|
|
1959
|
+
} catch(err) {
|
|
1960
|
+
state.graph.queryError = String(err);
|
|
1961
|
+
graphData = { nodes: [], edges: [] };
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// Map server nodes to viewer format
|
|
1965
|
+
var folders = (graphData.nodes || []).map(function(n) {
|
|
1966
|
+
return {
|
|
1967
|
+
id: n.folderPath || n.id,
|
|
1968
|
+
name: n.label || (n.folderPath || n.id).split(/[\\/]/).pop() || n.id,
|
|
1969
|
+
fullPath: n.folderPath || n.id,
|
|
1970
|
+
type: 'folder',
|
|
1971
|
+
agentIds: n.agentIds || [],
|
|
1972
|
+
obsCount: n.obsCount || 0,
|
|
1973
|
+
color: n.color || folderColor(n.folderPath || n.id),
|
|
1974
|
+
properties: {
|
|
1975
|
+
description: 'Agents: ' + (n.agentIds || []).join(', ') + '<br>Observations: ' + (n.obsCount || 0),
|
|
1976
|
+
fullPath: n.folderPath || n.id,
|
|
1977
|
+
obsCount: n.obsCount || 0
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
// Map server edges to viewer format
|
|
1983
|
+
var edges = (graphData.edges || []).map(function(e) {
|
|
1984
|
+
return {
|
|
1985
|
+
id: e.source + '->' + e.target + ':' + e.type,
|
|
1986
|
+
sourceNodeId: e.source,
|
|
1987
|
+
targetNodeId: e.target,
|
|
1988
|
+
type: e.type || 'connection',
|
|
1989
|
+
agentId: e.agentId,
|
|
1990
|
+
weight: e.type === 'cross-ref' ? 2 : 1
|
|
1991
|
+
};
|
|
1992
|
+
});
|
|
1993
|
+
|
|
1994
|
+
|
|
1995
|
+
state.graph.nodes = folders;
|
|
1996
|
+
state.graph.edges = edges;
|
|
1997
|
+
state.graph.truncated = false;
|
|
1998
|
+
state.graph.totalNodes = folders.length;
|
|
1999
|
+
state.graph.totalEdges = edges.length;
|
|
2000
|
+
state.graph.stats = { totalNodes: folders.length, totalEdges: edges.length };
|
|
2001
|
+
|
|
2002
|
+
state.graph.loaded = true;
|
|
2003
|
+
var types = {};
|
|
2004
|
+
state.graph.nodes.forEach(function(n) { types[n.type] = true; });
|
|
2005
|
+
state.graph.filters = types;
|
|
2006
|
+
|
|
2007
|
+
renderGraphSidebar();
|
|
2008
|
+
initGraph();
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
var NODE_SHAPES = {
|
|
2012
|
+
file: 'rect', function: 'circle', concept: 'circle', error: 'diamond',
|
|
2013
|
+
decision: 'diamond', pattern: 'circle', library: 'hexagon', person: 'circle',
|
|
2014
|
+
folder: 'rect'
|
|
2015
|
+
};
|
|
2016
|
+
var graphSearchTerm = '';
|
|
2017
|
+
|
|
2018
|
+
function renderGraphSidebar() {
|
|
2019
|
+
var sb = document.getElementById('graph-sidebar');
|
|
2020
|
+
if (!sb) return;
|
|
2021
|
+
var gs = state.graph.stats || {};
|
|
2022
|
+
var nodeCount = gs.totalNodes !== undefined ? gs.totalNodes : (gs.nodes !== undefined ? gs.nodes : (gs.nodeCount || state.graph.nodes.length));
|
|
2023
|
+
var edgeCount = gs.totalEdges !== undefined ? gs.totalEdges : (gs.edges !== undefined ? gs.edges : (gs.edgeCount || state.graph.edges.length));
|
|
2024
|
+
|
|
2025
|
+
var html = '';
|
|
2026
|
+
|
|
2027
|
+
if (state.graph.queryError) {
|
|
2028
|
+
html += '<div style="margin:8px 0;padding:10px 12px;border:1px solid var(--accent);background:var(--bg-alt);font-size:12px;color:var(--ink);line-height:1.4;">';
|
|
2029
|
+
html += '<div style="font-weight:600;margin-bottom:4px;">Graph query failed</div>';
|
|
2030
|
+
html += '<div style="font-size:11px;color:var(--ink-muted);">' + esc(state.graph.queryError) + '</div>';
|
|
2031
|
+
html += '<button class="btn" data-action="rebuild-graph" style="margin-top:8px;font-size:11px;">Retry</button>';
|
|
2032
|
+
html += '</div>';
|
|
2033
|
+
} else if (state.graph.truncated) {
|
|
2034
|
+
html += '<div style="margin:8px 0;padding:10px 12px;border:1px solid var(--border);background:var(--bg-alt);font-size:11px;color:var(--ink-muted);line-height:1.4;">';
|
|
2035
|
+
html += 'Showing ' + state.graph.nodes.length + ' of ' + state.graph.totalNodes + ' nodes (most-connected first). The full graph is too large to render at once.';
|
|
2036
|
+
html += '</div>';
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
html += '<input type="text" class="graph-search" id="graph-search" placeholder="Search nodes...">';
|
|
2040
|
+
|
|
2041
|
+
html += '<h3 style="margin-top:16px;font-size:10px;text-transform:uppercase;letter-spacing:0.12em;color:var(--ink-muted);font-family:var(--font-ui);font-weight:700;">Graph Stats</h3>';
|
|
2042
|
+
html += '<div style="display:flex;gap:20px;margin:10px 0 16px;padding:12px;background:var(--bg-alt);border:1px solid var(--border-light);border-radius:4px;">';
|
|
2043
|
+
html += '<div style="text-align:center;flex:1;"><span style="font-size:28px;font-weight:900;font-family:var(--font-display);color:var(--ink);line-height:1;">' + nodeCount + '</span><div style="font-size:8px;color:var(--ink-faint);text-transform:uppercase;letter-spacing:0.12em;font-family:var(--font-ui);font-weight:600;margin-top:4px;">Nodes</div></div>';
|
|
2044
|
+
html += '<div style="width:1px;background:var(--border-light);"></div>';
|
|
2045
|
+
html += '<div style="text-align:center;flex:1;"><span style="font-size:28px;font-weight:900;font-family:var(--font-display);color:var(--ink);line-height:1;">' + edgeCount + '</span><div style="font-size:8px;color:var(--ink-faint);text-transform:uppercase;letter-spacing:0.12em;font-family:var(--font-ui);font-weight:600;margin-top:4px;">Edges</div></div>';
|
|
2046
|
+
html += '</div>';
|
|
2047
|
+
|
|
2048
|
+
html += '<h3 style="margin-top:12px;font-size:10px;text-transform:uppercase;letter-spacing:0.12em;color:var(--ink-muted);font-family:var(--font-ui);font-weight:700;">Filter by Type</h3>';
|
|
2049
|
+
Object.keys(state.graph.filters).forEach(function(type) {
|
|
2050
|
+
var color = NODE_COLORS[type] || '#666666';
|
|
2051
|
+
html += '<label class="filter-item"><input type="checkbox" checked data-type="' + esc(type) + '"><span class="filter-dot" style="background:' + color + '"></span>' + esc(type) + '</label>';
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
html += '<div class="graph-legend"><h3>Legend</h3>';
|
|
2055
|
+
var shapeLabels = { rect: '▭', circle: '●', diamond: '◆', hexagon: '⬢' };
|
|
2056
|
+
var shownShapes = {};
|
|
2057
|
+
Object.keys(NODE_COLORS).forEach(function(type) {
|
|
2058
|
+
var shape = NODE_SHAPES[type] || 'circle';
|
|
2059
|
+
var color = NODE_COLORS[type];
|
|
2060
|
+
var key = type;
|
|
2061
|
+
if (shownShapes[key]) return;
|
|
2062
|
+
shownShapes[key] = true;
|
|
2063
|
+
html += '<div class="graph-legend-item"><span class="graph-legend-shape" style="color:' + color + ';font-size:14px;">' + (shapeLabels[shape] || '●') + '</span><span>' + esc(type) + '</span></div>';
|
|
2064
|
+
});
|
|
2065
|
+
html += '</div>';
|
|
2066
|
+
|
|
2067
|
+
html += '<button class="btn" style="margin-top:14px;width:100%;font-size:11px;padding:8px;letter-spacing:0.06em;transition:all 0.15s ease;" data-action="rebuild-graph">↻ Rebuild Graph</button>';
|
|
2068
|
+
html += '<div id="selected-node-panel"></div>';
|
|
2069
|
+
var __focus = captureSearchFocus(['graph-search']);
|
|
2070
|
+
sb.innerHTML = html;
|
|
2071
|
+
|
|
2072
|
+
sb.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
|
|
2073
|
+
cb.addEventListener('change', function() {
|
|
2074
|
+
state.graph.filters[this.dataset.type] = this.checked;
|
|
2075
|
+
renderGraph();
|
|
2076
|
+
});
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
var searchInput = document.getElementById('graph-search');
|
|
2080
|
+
if (searchInput) {
|
|
2081
|
+
bindImeSafeSearch(searchInput, 200, function(v){ graphSearchTerm = v.toLowerCase(); renderGraph(); });
|
|
2082
|
+
}
|
|
2083
|
+
restoreSearchFocus(__focus);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function initGraph() {
|
|
2087
|
+
var canvas = document.getElementById('graph-canvas');
|
|
2088
|
+
if (!canvas) return;
|
|
2089
|
+
graphSim.canvas = canvas;
|
|
2090
|
+
graphSim.ctx = canvas.getContext('2d');
|
|
2091
|
+
|
|
2092
|
+
function resize() {
|
|
2093
|
+
var r = canvas.parentElement.getBoundingClientRect();
|
|
2094
|
+
canvas.width = r.width * window.devicePixelRatio;
|
|
2095
|
+
canvas.height = r.height * window.devicePixelRatio;
|
|
2096
|
+
canvas.style.width = r.width + 'px';
|
|
2097
|
+
canvas.style.height = r.height + 'px';
|
|
2098
|
+
graphSim.ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0);
|
|
2099
|
+
}
|
|
2100
|
+
resize();
|
|
2101
|
+
window.addEventListener('resize', resize);
|
|
2102
|
+
|
|
2103
|
+
var cw = canvas.width / window.devicePixelRatio;
|
|
2104
|
+
var ch = canvas.height / window.devicePixelRatio;
|
|
2105
|
+
graphSim.panX = cw / 2;
|
|
2106
|
+
graphSim.panY = ch / 2;
|
|
2107
|
+
|
|
2108
|
+
var edgeMap = {};
|
|
2109
|
+
state.graph.edges.forEach(function(e) {
|
|
2110
|
+
edgeMap[e.sourceNodeId] = (edgeMap[e.sourceNodeId] || 0) + 1;
|
|
2111
|
+
edgeMap[e.targetNodeId] = (edgeMap[e.targetNodeId] || 0) + 1;
|
|
2112
|
+
});
|
|
2113
|
+
|
|
2114
|
+
graphSim.nodes = state.graph.nodes.map(function(n, i) {
|
|
2115
|
+
var angle = (2 * Math.PI * i) / Math.max(state.graph.nodes.length, 1);
|
|
2116
|
+
var radius = Math.min(cw, ch) * 0.38;
|
|
2117
|
+
var deg = edgeMap[n.id] || 0;
|
|
2118
|
+
var activity = ((n.sessions && n.sessions.length) || 0) + ((n.memories && n.memories.length) || 0);
|
|
2119
|
+
var nodeR = n.type === 'folder'
|
|
2120
|
+
? Math.max(24, Math.min(52, 24 + Math.sqrt(activity + 1) * 3.5))
|
|
2121
|
+
: Math.max(10, Math.min(26, 10 + deg * 2.5));
|
|
2122
|
+
return {
|
|
2123
|
+
id: n.id, type: n.type, name: n.name, properties: n.properties,
|
|
2124
|
+
sessions: n.sessions, memories: n.memories,
|
|
2125
|
+
x: Math.cos(angle) * radius,
|
|
2126
|
+
y: Math.sin(angle) * radius,
|
|
2127
|
+
vx: 0, vy: 0,
|
|
2128
|
+
r: nodeR
|
|
2129
|
+
};
|
|
2130
|
+
});
|
|
2131
|
+
graphSim.edges = state.graph.edges.slice();
|
|
2132
|
+
graphSim.running = true;
|
|
2133
|
+
graphSim.dragNode = null;
|
|
2134
|
+
|
|
2135
|
+
setupGraphInteraction(canvas);
|
|
2136
|
+
runSimulation();
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
function setupGraphInteraction(canvas) {
|
|
2140
|
+
var isPanning = false;
|
|
2141
|
+
var lastMX = 0, lastMY = 0;
|
|
2142
|
+
|
|
2143
|
+
function canvasCoords(e) {
|
|
2144
|
+
var rect = canvas.getBoundingClientRect();
|
|
2145
|
+
return {
|
|
2146
|
+
x: (e.clientX - rect.left - graphSim.panX) / graphSim.zoom,
|
|
2147
|
+
y: (e.clientY - rect.top - graphSim.panY) / graphSim.zoom
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function findNode(cx, cy) {
|
|
2151
|
+
for (var i = graphSim.nodes.length - 1; i >= 0; i--) {
|
|
2152
|
+
var n = graphSim.nodes[i];
|
|
2153
|
+
if (!state.graph.filters[n.type]) continue;
|
|
2154
|
+
var dx = n.x - cx, dy = n.y - cy;
|
|
2155
|
+
if (dx * dx + dy * dy < n.r * n.r + 25) return n;
|
|
2156
|
+
}
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
canvas.addEventListener('mousedown', function(e) {
|
|
2161
|
+
var c = canvasCoords(e);
|
|
2162
|
+
var node = findNode(c.x, c.y);
|
|
2163
|
+
if (node) {
|
|
2164
|
+
graphSim.dragNode = node;
|
|
2165
|
+
} else {
|
|
2166
|
+
isPanning = true;
|
|
2167
|
+
}
|
|
2168
|
+
lastMX = e.clientX;
|
|
2169
|
+
lastMY = e.clientY;
|
|
2170
|
+
wakeGraphSim();
|
|
2171
|
+
});
|
|
2172
|
+
canvas.addEventListener('mousemove', function(e) {
|
|
2173
|
+
var dx = e.clientX - lastMX;
|
|
2174
|
+
var dy = e.clientY - lastMY;
|
|
2175
|
+
if (graphSim.dragNode) {
|
|
2176
|
+
graphSim.dragNode.x += dx / graphSim.zoom;
|
|
2177
|
+
graphSim.dragNode.y += dy / graphSim.zoom;
|
|
2178
|
+
graphSim.dragNode.vx = 0;
|
|
2179
|
+
graphSim.dragNode.vy = 0;
|
|
2180
|
+
} else if (isPanning) {
|
|
2181
|
+
graphSim.panX += dx;
|
|
2182
|
+
graphSim.panY += dy;
|
|
2183
|
+
}
|
|
2184
|
+
lastMX = e.clientX;
|
|
2185
|
+
lastMY = e.clientY;
|
|
2186
|
+
graphSim.mouseX = e.clientX;
|
|
2187
|
+
graphSim.mouseY = e.clientY;
|
|
2188
|
+
|
|
2189
|
+
var c = canvasCoords(e);
|
|
2190
|
+
var hoverNode = findNode(c.x, c.y);
|
|
2191
|
+
var tooltip = document.getElementById('graph-tooltip');
|
|
2192
|
+
if (tooltip) {
|
|
2193
|
+
if (hoverNode && !graphSim.dragNode && !isPanning) {
|
|
2194
|
+
var conns = graphSim.edges.filter(function(ed) { return ed.sourceNodeId === hoverNode.id || ed.targetNodeId === hoverNode.id; }).length;
|
|
2195
|
+
var ttHtml = '<div class="tt-name">' + esc(hoverNode.name) + '</div>';
|
|
2196
|
+
ttHtml += '<div class="tt-type" style="color:' + (NODE_COLORS[hoverNode.type] || '#666') + '">' + esc(hoverNode.type) + '</div>';
|
|
2197
|
+
if (hoverNode.type === 'folder' && hoverNode.properties) {
|
|
2198
|
+
if (hoverNode.properties.fullPath) {
|
|
2199
|
+
ttHtml += '<div class="tt-prop">Path: ' + esc(truncate(hoverNode.properties.fullPath, 45)) + '</div>';
|
|
2200
|
+
}
|
|
2201
|
+
if (typeof hoverNode.properties.sessionCount === 'number') {
|
|
2202
|
+
ttHtml += '<div class="tt-prop">Sessions: ' + hoverNode.properties.sessionCount + '</div>';
|
|
2203
|
+
}
|
|
2204
|
+
if (typeof hoverNode.properties.memoryCount === 'number') {
|
|
2205
|
+
ttHtml += '<div class="tt-prop">Memories: ' + hoverNode.properties.memoryCount + '</div>';
|
|
2206
|
+
}
|
|
2207
|
+
} else if (hoverNode.properties) {
|
|
2208
|
+
var propKeys = Object.keys(hoverNode.properties).slice(0, 3);
|
|
2209
|
+
propKeys.forEach(function(k) {
|
|
2210
|
+
ttHtml += '<div class="tt-prop">' + esc(k) + ': ' + esc(truncate(String(hoverNode.properties[k]), 30)) + '</div>';
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
ttHtml += '<div class="tt-conns">' + conns + ' connection' + (conns !== 1 ? 's' : '') + '</div>';
|
|
2214
|
+
tooltip.innerHTML = ttHtml;
|
|
2215
|
+
var rect = canvas.getBoundingClientRect();
|
|
2216
|
+
tooltip.style.left = (e.clientX - rect.left + 12) + 'px';
|
|
2217
|
+
tooltip.style.top = (e.clientY - rect.top + 12) + 'px';
|
|
2218
|
+
tooltip.classList.add('visible');
|
|
2219
|
+
canvas.style.cursor = 'pointer';
|
|
2220
|
+
} else {
|
|
2221
|
+
tooltip.classList.remove('visible');
|
|
2222
|
+
canvas.style.cursor = graphSim.dragNode || isPanning ? 'grabbing' : 'grab';
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
canvas.addEventListener('mouseup', function(e) {
|
|
2227
|
+
if (graphSim.dragNode && !isPanning) {
|
|
2228
|
+
selectGraphNode(graphSim.dragNode);
|
|
2229
|
+
}
|
|
2230
|
+
graphSim.dragNode = null;
|
|
2231
|
+
isPanning = false;
|
|
2232
|
+
});
|
|
2233
|
+
canvas.addEventListener('wheel', function(e) {
|
|
2234
|
+
e.preventDefault();
|
|
2235
|
+
var factor = e.deltaY > 0 ? 0.9 : 1.1;
|
|
2236
|
+
graphSim.zoom = Math.max(0.1, Math.min(5, graphSim.zoom * factor));
|
|
2237
|
+
wakeGraphSim();
|
|
2238
|
+
}, { passive: false });
|
|
2239
|
+
canvas.addEventListener('dblclick', function(e) {
|
|
2240
|
+
var c = canvasCoords(e);
|
|
2241
|
+
var node = findNode(c.x, c.y);
|
|
2242
|
+
if (node) {
|
|
2243
|
+
selectGraphNode(node);
|
|
2244
|
+
expandNode(node.id);
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
window.zoomGraph = function(dir) {
|
|
2250
|
+
var factor = dir > 0 ? 1.25 : 0.8;
|
|
2251
|
+
graphSim.zoom = Math.max(0.1, Math.min(5, graphSim.zoom * factor));
|
|
2252
|
+
wakeGraphSim();
|
|
2253
|
+
};
|
|
2254
|
+
window.recenterGraph = function() {
|
|
2255
|
+
graphSim.zoom = 1;
|
|
2256
|
+
if (graphSim.canvas) {
|
|
2257
|
+
var cw = graphSim.canvas.width / window.devicePixelRatio;
|
|
2258
|
+
var ch = graphSim.canvas.height / window.devicePixelRatio;
|
|
2259
|
+
graphSim.panX = cw / 2;
|
|
2260
|
+
graphSim.panY = ch / 2;
|
|
2261
|
+
}
|
|
2262
|
+
wakeGraphSim();
|
|
2263
|
+
};
|
|
2264
|
+
|
|
2265
|
+
function selectGraphNode(simNode) {
|
|
2266
|
+
state.graph.selectedNode = simNode;
|
|
2267
|
+
var panel = document.getElementById('selected-node-panel');
|
|
2268
|
+
if (!panel) return;
|
|
2269
|
+
var color = NODE_COLORS[simNode.type] || '#666666';
|
|
2270
|
+
var html = '<div class="selected-node-info">';
|
|
2271
|
+
html += '<h4 style="color:' + color + '">' + esc(simNode.name) + '</h4>';
|
|
2272
|
+
html += '<div class="prop">Type: ' + esc(simNode.type) + '</div>';
|
|
2273
|
+
|
|
2274
|
+
if (simNode.type === 'folder' && simNode.properties && simNode.properties.description) {
|
|
2275
|
+
html += '<div class="prop-desc" style="margin-top:10px;font-size:12px;color:var(--ink);line-height:1.5;max-height:300px;overflow-y:auto;border-top:1px solid var(--border);padding-top:10px;">' + simNode.properties.description + '</div>';
|
|
2276
|
+
} else if (simNode.properties) {
|
|
2277
|
+
Object.keys(simNode.properties).forEach(function(k) {
|
|
2278
|
+
html += '<div class="prop">' + esc(k) + ': ' + esc(truncate(simNode.properties[k], 50)) + '</div>';
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
var conns = graphSim.edges.filter(function(e) { return e.sourceNodeId === simNode.id || e.targetNodeId === simNode.id; }).length;
|
|
2283
|
+
html += '<div class="prop" style="margin-top:10px;">Connections: ' + conns + '</div>';
|
|
2284
|
+
if (simNode.type !== 'folder') {
|
|
2285
|
+
html += '<button class="btn btn-primary" style="margin-top:8px;width:100%;" data-action="expand-node" data-node-id="' + esc(simNode.id) + '">Expand neighbors</button>';
|
|
2286
|
+
}
|
|
2287
|
+
html += '</div>';
|
|
2288
|
+
panel.innerHTML = html;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
async function expandNode(nodeId) {
|
|
2292
|
+
var result = await apiPost('graph/query', { startNodeId: nodeId, maxDepth: 1 });
|
|
2293
|
+
if (!result) return;
|
|
2294
|
+
var existingIds = {};
|
|
2295
|
+
graphSim.nodes.forEach(function(n) { existingIds[n.id] = true; });
|
|
2296
|
+
var parentNode = graphSim.nodes.find(function(n) { return n.id === nodeId; });
|
|
2297
|
+
var px = parentNode ? parentNode.x : 0;
|
|
2298
|
+
var py = parentNode ? parentNode.y : 0;
|
|
2299
|
+
|
|
2300
|
+
(result.nodes || []).forEach(function(n) {
|
|
2301
|
+
if (!existingIds[n.id]) {
|
|
2302
|
+
state.graph.nodes.push(n);
|
|
2303
|
+
if (!state.graph.filters.hasOwnProperty(n.type)) state.graph.filters[n.type] = true;
|
|
2304
|
+
var angle = Math.random() * Math.PI * 2;
|
|
2305
|
+
graphSim.nodes.push({
|
|
2306
|
+
id: n.id, type: n.type, name: n.name, properties: n.properties,
|
|
2307
|
+
x: px + Math.cos(angle) * 80,
|
|
2308
|
+
y: py + Math.sin(angle) * 80,
|
|
2309
|
+
vx: 0, vy: 0, r: 8
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
var existingEdges = {};
|
|
2315
|
+
graphSim.edges.forEach(function(e) { existingEdges[e.id] = true; });
|
|
2316
|
+
(result.edges || []).forEach(function(e) {
|
|
2317
|
+
if (!existingEdges[e.id]) {
|
|
2318
|
+
state.graph.edges.push(e);
|
|
2319
|
+
graphSim.edges.push(e);
|
|
2320
|
+
}
|
|
2321
|
+
});
|
|
2322
|
+
renderGraphSidebar();
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
function runSimulation() {
|
|
2326
|
+
if (!graphSim.running) return;
|
|
2327
|
+
var nodes = graphSim.nodes;
|
|
2328
|
+
var edges = graphSim.edges;
|
|
2329
|
+
var nodeCount = nodes.length;
|
|
2330
|
+
graphSim.tickCount = (graphSim.tickCount || 0) + 1;
|
|
2331
|
+
var coolBoost = Math.min(0.4, graphSim.tickCount / 1500);
|
|
2332
|
+
var damping = 0.9 - coolBoost;
|
|
2333
|
+
var repulsion = nodeCount > 1000 ? 3000 : nodeCount > 100 ? 2000 : nodeCount > 50 ? 1800 : nodeCount > 10 ? 8000 : 18000;
|
|
2334
|
+
var attraction = nodeCount > 100 ? 0.002 : 0.005;
|
|
2335
|
+
var centerGravity = nodeCount > 1000 ? 0.012 : nodeCount > 100 ? 0.005 : 0.01;
|
|
2336
|
+
var velocityCap = nodeCount > 1000 ? 6 : nodeCount > 200 ? 12 : 24;
|
|
2337
|
+
|
|
2338
|
+
var nodeMap = {};
|
|
2339
|
+
nodes.forEach(function(n) { nodeMap[n.id] = n; });
|
|
2340
|
+
|
|
2341
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
2342
|
+
if (graphSim.dragNode === nodes[i]) continue;
|
|
2343
|
+
var n = nodes[i];
|
|
2344
|
+
var fx = 0, fy = 0;
|
|
2345
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
2346
|
+
if (i === j) continue;
|
|
2347
|
+
var dx = n.x - nodes[j].x;
|
|
2348
|
+
var dy = n.y - nodes[j].y;
|
|
2349
|
+
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
2350
|
+
var force = repulsion / (dist * dist);
|
|
2351
|
+
fx += (dx / dist) * force;
|
|
2352
|
+
fy += (dy / dist) * force;
|
|
2353
|
+
}
|
|
2354
|
+
fx -= n.x * centerGravity;
|
|
2355
|
+
fy -= n.y * centerGravity;
|
|
2356
|
+
var nvx = (n.vx + fx) * damping;
|
|
2357
|
+
var nvy = (n.vy + fy) * damping;
|
|
2358
|
+
if (nvx > velocityCap) nvx = velocityCap; else if (nvx < -velocityCap) nvx = -velocityCap;
|
|
2359
|
+
if (nvy > velocityCap) nvy = velocityCap; else if (nvy < -velocityCap) nvy = -velocityCap;
|
|
2360
|
+
n.vx = nvx;
|
|
2361
|
+
n.vy = nvy;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
edges.forEach(function(e) {
|
|
2365
|
+
var s = nodeMap[e.sourceNodeId];
|
|
2366
|
+
var t = nodeMap[e.targetNodeId];
|
|
2367
|
+
if (!s || !t) return;
|
|
2368
|
+
var dx = t.x - s.x;
|
|
2369
|
+
var dy = t.y - s.y;
|
|
2370
|
+
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
2371
|
+
var f = (dist - (s.type === 'folder' || t.type === 'folder' ? 220 : 100)) * attraction;
|
|
2372
|
+
var fx = (dx / dist) * f;
|
|
2373
|
+
var fy = (dy / dist) * f;
|
|
2374
|
+
if (graphSim.dragNode !== s) { s.vx += fx; s.vy += fy; }
|
|
2375
|
+
if (graphSim.dragNode !== t) { t.vx -= fx; t.vy -= fy; }
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
var totalKineticEnergy = 0;
|
|
2379
|
+
nodes.forEach(function(n) {
|
|
2380
|
+
if (graphSim.dragNode === n) return;
|
|
2381
|
+
n.x += n.vx;
|
|
2382
|
+
n.y += n.vy;
|
|
2383
|
+
totalKineticEnergy += n.vx * n.vx + n.vy * n.vy;
|
|
2384
|
+
});
|
|
2385
|
+
|
|
2386
|
+
var rmsVelocity = nodes.length > 0 ? Math.sqrt(totalKineticEnergy / nodes.length) : 0;
|
|
2387
|
+
if (rmsVelocity < 0.05 && graphSim.tickCount > 60 && !graphSim.dragNode) {
|
|
2388
|
+
graphSim.quietTicks = (graphSim.quietTicks || 0) + 1;
|
|
2389
|
+
} else {
|
|
2390
|
+
graphSim.quietTicks = 0;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
renderGraph();
|
|
2394
|
+
if (graphSim.quietTicks > 30) {
|
|
2395
|
+
graphSim.raf = null;
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
graphSim.raf = requestAnimationFrame(runSimulation);
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
async function rebuildGraph() {
|
|
2402
|
+
var sb = document.getElementById('graph-sidebar');
|
|
2403
|
+
if (sb) sb.innerHTML = '<h3>Graph</h3><p style="font-size:12px;color:var(--ink-faint);font-style:italic;">Rebuilding graph from observations...</p>';
|
|
2404
|
+
await apiPost('graph/build', {});
|
|
2405
|
+
state.graph.loaded = false;
|
|
2406
|
+
loadGraph();
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
function drawNodeShape(ctx, x, y, r, type) {
|
|
2410
|
+
var shape = NODE_SHAPES[type] || 'circle';
|
|
2411
|
+
switch(shape) {
|
|
2412
|
+
case 'rect':
|
|
2413
|
+
ctx.beginPath();
|
|
2414
|
+
ctx.rect(x - r, y - r * 0.75, r * 2, r * 1.5);
|
|
2415
|
+
break;
|
|
2416
|
+
case 'diamond':
|
|
2417
|
+
ctx.beginPath();
|
|
2418
|
+
ctx.moveTo(x, y - r);
|
|
2419
|
+
ctx.lineTo(x + r, y);
|
|
2420
|
+
ctx.lineTo(x, y + r);
|
|
2421
|
+
ctx.lineTo(x - r, y);
|
|
2422
|
+
ctx.closePath();
|
|
2423
|
+
break;
|
|
2424
|
+
case 'hexagon':
|
|
2425
|
+
ctx.beginPath();
|
|
2426
|
+
for (var i = 0; i < 6; i++) {
|
|
2427
|
+
var angle = (Math.PI / 3) * i - Math.PI / 2;
|
|
2428
|
+
var hx = x + r * Math.cos(angle);
|
|
2429
|
+
var hy = y + r * Math.sin(angle);
|
|
2430
|
+
if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy);
|
|
2431
|
+
}
|
|
2432
|
+
ctx.closePath();
|
|
2433
|
+
break;
|
|
2434
|
+
default:
|
|
2435
|
+
ctx.beginPath();
|
|
2436
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
2437
|
+
break;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
function renderGraph() {
|
|
2442
|
+
var ctx = graphSim.ctx;
|
|
2443
|
+
var canvas = graphSim.canvas;
|
|
2444
|
+
if (!ctx || !canvas) return;
|
|
2445
|
+
var w = canvas.width / window.devicePixelRatio;
|
|
2446
|
+
var h = canvas.height / window.devicePixelRatio;
|
|
2447
|
+
|
|
2448
|
+
ctx.clearRect(0, 0, w, h);
|
|
2449
|
+
|
|
2450
|
+
var gridSize = 24;
|
|
2451
|
+
ctx.save();
|
|
2452
|
+
ctx.strokeStyle = isDarkMode() ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.04)';
|
|
2453
|
+
ctx.lineWidth = 0.5;
|
|
2454
|
+
for (var gx = 0; gx < w; gx += gridSize) {
|
|
2455
|
+
ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, h); ctx.stroke();
|
|
2456
|
+
}
|
|
2457
|
+
for (var gy = 0; gy < h; gy += gridSize) {
|
|
2458
|
+
ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(w, gy); ctx.stroke();
|
|
2459
|
+
}
|
|
2460
|
+
ctx.restore();
|
|
2461
|
+
|
|
2462
|
+
ctx.save();
|
|
2463
|
+
ctx.translate(graphSim.panX, graphSim.panY);
|
|
2464
|
+
ctx.scale(graphSim.zoom, graphSim.zoom);
|
|
2465
|
+
|
|
2466
|
+
var nodeMap = {};
|
|
2467
|
+
graphSim.nodes.forEach(function(n) { nodeMap[n.id] = n; });
|
|
2468
|
+
|
|
2469
|
+
var searchActive = graphSearchTerm.length > 0;
|
|
2470
|
+
var totalVisible = graphSim.nodes.filter(function(n) { return state.graph.filters[n.type]; }).length;
|
|
2471
|
+
var isDense = totalVisible > 40;
|
|
2472
|
+
var labelZoomThreshold = isDense ? 1.5 : 0.5;
|
|
2473
|
+
var edgeLabelZoomThreshold = isDense ? 2.5 : 1.2;
|
|
2474
|
+
var selectedId = state.graph.selectedNode ? state.graph.selectedNode.id : null;
|
|
2475
|
+
|
|
2476
|
+
var hoverNodeId = null;
|
|
2477
|
+
if (!graphSim.dragNode && graphSim.canvas) {
|
|
2478
|
+
var rect = graphSim.canvas.getBoundingClientRect();
|
|
2479
|
+
var hx = (graphSim.mouseX - rect.left - graphSim.panX) / graphSim.zoom;
|
|
2480
|
+
var hy = (graphSim.mouseY - rect.top - graphSim.panY) / graphSim.zoom;
|
|
2481
|
+
for (var hi = graphSim.nodes.length - 1; hi >= 0; hi--) {
|
|
2482
|
+
var hn = graphSim.nodes[hi];
|
|
2483
|
+
if (!state.graph.filters[hn.type]) continue;
|
|
2484
|
+
var hdx = hn.x - hx, hdy = hn.y - hy;
|
|
2485
|
+
if (hdx * hdx + hdy * hdy < hn.r * hn.r + 25) { hoverNodeId = hn.id; break; }
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
var focusNodeId = selectedId || hoverNodeId;
|
|
2489
|
+
|
|
2490
|
+
graphSim.edges.forEach(function(e) {
|
|
2491
|
+
var s = nodeMap[e.sourceNodeId];
|
|
2492
|
+
var t = nodeMap[e.targetNodeId];
|
|
2493
|
+
if (!s || !t) return;
|
|
2494
|
+
if (!state.graph.filters[s.type] || !state.graph.filters[t.type]) return;
|
|
2495
|
+
|
|
2496
|
+
var edgeDimmed = searchActive && !(s.name.toLowerCase().includes(graphSearchTerm) || t.name.toLowerCase().includes(graphSearchTerm));
|
|
2497
|
+
var isConnectedToFocus = focusNodeId && (e.sourceNodeId === focusNodeId || e.targetNodeId === focusNodeId);
|
|
2498
|
+
var isFocusActive = focusNodeId !== null;
|
|
2499
|
+
var weight = typeof e.weight === 'number' ? e.weight : 0.5;
|
|
2500
|
+
var lineWidth = isConnectedToFocus ? 2 + weight * 2 : 1 + weight * 1.5;
|
|
2501
|
+
|
|
2502
|
+
var dx = t.x - s.x;
|
|
2503
|
+
var dy = t.y - s.y;
|
|
2504
|
+
var len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
2505
|
+
var curveOffset = isDense ? 12 : 18;
|
|
2506
|
+
var offsetX = -dy / len * curveOffset;
|
|
2507
|
+
var offsetY = dx / len * curveOffset;
|
|
2508
|
+
var cpx = (s.x + t.x) / 2 + offsetX;
|
|
2509
|
+
var cpy = (s.y + t.y) / 2 + offsetY;
|
|
2510
|
+
|
|
2511
|
+
var edgeColor = s.type === 'folder' ? folderColor(s.id) : (NODE_COLORS[s.type] || '#666666');
|
|
2512
|
+
var edgeAlpha;
|
|
2513
|
+
if (edgeDimmed) {
|
|
2514
|
+
edgeAlpha = 0.06;
|
|
2515
|
+
} else if (isFocusActive && isConnectedToFocus) {
|
|
2516
|
+
edgeAlpha = 0.65;
|
|
2517
|
+
} else if (isFocusActive && !isConnectedToFocus) {
|
|
2518
|
+
edgeAlpha = 0.06;
|
|
2519
|
+
} else {
|
|
2520
|
+
edgeAlpha = isDense ? 0.15 : 0.25;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
ctx.beginPath();
|
|
2524
|
+
ctx.moveTo(s.x, s.y);
|
|
2525
|
+
ctx.quadraticCurveTo(cpx, cpy, t.x, t.y);
|
|
2526
|
+
var r = parseInt(edgeColor.slice(1,3), 16);
|
|
2527
|
+
var g = parseInt(edgeColor.slice(3,5), 16);
|
|
2528
|
+
var b = parseInt(edgeColor.slice(5,7), 16);
|
|
2529
|
+
ctx.strokeStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + edgeAlpha + ')';
|
|
2530
|
+
ctx.lineWidth = lineWidth;
|
|
2531
|
+
ctx.stroke();
|
|
2532
|
+
|
|
2533
|
+
if (!isDense || isConnectedToFocus) {
|
|
2534
|
+
var arrowAngle = Math.atan2(t.y - cpy, t.x - cpx);
|
|
2535
|
+
var arrowLen = 5 + lineWidth;
|
|
2536
|
+
ctx.beginPath();
|
|
2537
|
+
ctx.moveTo(t.x - t.r * Math.cos(arrowAngle), t.y - t.r * Math.sin(arrowAngle));
|
|
2538
|
+
ctx.lineTo(t.x - (t.r + arrowLen) * Math.cos(arrowAngle - 0.3), t.y - (t.r + arrowLen) * Math.sin(arrowAngle - 0.3));
|
|
2539
|
+
ctx.lineTo(t.x - (t.r + arrowLen) * Math.cos(arrowAngle + 0.3), t.y - (t.r + arrowLen) * Math.sin(arrowAngle + 0.3));
|
|
2540
|
+
ctx.closePath();
|
|
2541
|
+
ctx.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + (edgeDimmed ? 0.06 : isConnectedToFocus ? 0.6 : 0.2) + ')';
|
|
2542
|
+
ctx.fill();
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
var showEdgeLabel = e.type && !edgeDimmed && isConnectedToFocus && graphSim.zoom > 0.4;
|
|
2546
|
+
if (showEdgeLabel) {
|
|
2547
|
+
var zoomInv = 1 / graphSim.zoom;
|
|
2548
|
+
ctx.save();
|
|
2549
|
+
ctx.fillStyle = isDarkMode() ? (isConnectedToFocus ? 'rgba(238,238,238,0.9)' : 'rgba(180,180,180,0.7)') : (isConnectedToFocus ? 'rgba(17,17,17,0.85)' : 'rgba(80,80,80,0.7)');
|
|
2550
|
+
ctx.font = (isConnectedToFocus ? '600 ' : '500 ') + (11 * zoomInv).toFixed(1) + 'px Inter, sans-serif';
|
|
2551
|
+
ctx.textAlign = 'center';
|
|
2552
|
+
ctx.fillText(e.type, cpx, cpy - (4 * zoomInv));
|
|
2553
|
+
ctx.restore();
|
|
2554
|
+
}
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
graphSim.nodes.forEach(function(n) {
|
|
2558
|
+
if (!state.graph.filters[n.type]) return;
|
|
2559
|
+
var color = n.type === 'folder' ? folderColor(n.id) : (NODE_COLORS[n.type] || '#666666');
|
|
2560
|
+
var isSelected = selectedId === n.id;
|
|
2561
|
+
var isHovered = hoverNodeId === n.id;
|
|
2562
|
+
var matchesSearch = !searchActive || n.name.toLowerCase().includes(graphSearchTerm);
|
|
2563
|
+
var isFocusFaded = focusNodeId && n.id !== focusNodeId && !graphSim.edges.some(function(ed) {
|
|
2564
|
+
return (ed.sourceNodeId === focusNodeId && ed.targetNodeId === n.id) ||
|
|
2565
|
+
(ed.targetNodeId === focusNodeId && ed.sourceNodeId === n.id);
|
|
2566
|
+
});
|
|
2567
|
+
|
|
2568
|
+
var nodeAlpha = !matchesSearch ? 0.12 : (isFocusFaded ? 0.2 : 1);
|
|
2569
|
+
|
|
2570
|
+
ctx.save();
|
|
2571
|
+
ctx.globalAlpha = nodeAlpha;
|
|
2572
|
+
|
|
2573
|
+
if (matchesSearch && !isFocusFaded && (isSelected || isHovered || !searchActive)) {
|
|
2574
|
+
ctx.shadowColor = color;
|
|
2575
|
+
ctx.shadowBlur = isSelected ? 20 : isHovered ? 16 : (isDense ? 4 : 8);
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
drawNodeShape(ctx, n.x, n.y, n.r, n.type);
|
|
2579
|
+
var grad = ctx.createRadialGradient(n.x - n.r * 0.3, n.y - n.r * 0.3, 0, n.x, n.y, n.r * 1.2);
|
|
2580
|
+
var cr = parseInt(color.slice(1,3), 16);
|
|
2581
|
+
var cg = parseInt(color.slice(3,5), 16);
|
|
2582
|
+
var cb = parseInt(color.slice(5,7), 16);
|
|
2583
|
+
grad.addColorStop(0, 'rgba(' + Math.min(255, cr + 60) + ',' + Math.min(255, cg + 60) + ',' + Math.min(255, cb + 60) + ',0.95)');
|
|
2584
|
+
grad.addColorStop(1, color);
|
|
2585
|
+
ctx.fillStyle = grad;
|
|
2586
|
+
ctx.fill();
|
|
2587
|
+
ctx.restore();
|
|
2588
|
+
|
|
2589
|
+
if (isSelected) {
|
|
2590
|
+
ctx.save();
|
|
2591
|
+
drawNodeShape(ctx, n.x, n.y, n.r + 3, n.type);
|
|
2592
|
+
ctx.strokeStyle = color;
|
|
2593
|
+
ctx.lineWidth = 3;
|
|
2594
|
+
ctx.shadowColor = color;
|
|
2595
|
+
ctx.shadowBlur = 12;
|
|
2596
|
+
ctx.stroke();
|
|
2597
|
+
ctx.restore();
|
|
2598
|
+
} else if (isHovered) {
|
|
2599
|
+
drawNodeShape(ctx, n.x, n.y, n.r + 2, n.type);
|
|
2600
|
+
ctx.strokeStyle = color;
|
|
2601
|
+
ctx.lineWidth = 2;
|
|
2602
|
+
ctx.stroke();
|
|
2603
|
+
} else if (searchActive && matchesSearch) {
|
|
2604
|
+
drawNodeShape(ctx, n.x, n.y, n.r, n.type);
|
|
2605
|
+
ctx.strokeStyle = '#CC0000';
|
|
2606
|
+
ctx.lineWidth = 2;
|
|
2607
|
+
ctx.stroke();
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
var showLabel = !isFocusFaded && (
|
|
2611
|
+
n.type === 'folder' ||
|
|
2612
|
+
isSelected || isHovered ||
|
|
2613
|
+
(searchActive && matchesSearch) ||
|
|
2614
|
+
(!isDense && graphSim.zoom > labelZoomThreshold) ||
|
|
2615
|
+
(isDense && graphSim.zoom > labelZoomThreshold && n.r > 10)
|
|
2616
|
+
);
|
|
2617
|
+
if (showLabel) {
|
|
2618
|
+
var zoomInv = 1 / graphSim.zoom;
|
|
2619
|
+
ctx.save();
|
|
2620
|
+
ctx.font = (isSelected || isHovered ? '600 ' : '500 ') + (13 * zoomInv).toFixed(1) + 'px Inter, sans-serif';
|
|
2621
|
+
ctx.textAlign = 'center';
|
|
2622
|
+
|
|
2623
|
+
var label = truncate(n.name, 18);
|
|
2624
|
+
var textW = ctx.measureText(label).width;
|
|
2625
|
+
var labelW = textW + (16 * zoomInv);
|
|
2626
|
+
var labelH = 20 * zoomInv;
|
|
2627
|
+
var labelY = n.y + n.r + (8 * zoomInv);
|
|
2628
|
+
|
|
2629
|
+
ctx.fillStyle = isDarkMode() ? 'rgba(30,30,35,0.92)' : 'rgba(255,255,255,0.92)';
|
|
2630
|
+
ctx.beginPath();
|
|
2631
|
+
ctx.roundRect ? ctx.roundRect(n.x - labelW / 2, labelY, labelW, labelH, 4 * zoomInv) : ctx.rect(n.x - labelW / 2, labelY, labelW, labelH);
|
|
2632
|
+
ctx.fill();
|
|
2633
|
+
|
|
2634
|
+
ctx.strokeStyle = isDarkMode() ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)';
|
|
2635
|
+
ctx.lineWidth = 1 * zoomInv;
|
|
2636
|
+
ctx.stroke();
|
|
2637
|
+
|
|
2638
|
+
ctx.fillStyle = isDarkMode() ? (isSelected || isHovered ? '#eeeeee' : '#bbbbbb') : (isSelected || isHovered ? '#111111' : '#444444');
|
|
2639
|
+
ctx.fillText(label, n.x, labelY + (14 * zoomInv));
|
|
2640
|
+
ctx.restore();
|
|
2641
|
+
}
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2644
|
+
ctx.restore();
|
|
2645
|
+
|
|
2646
|
+
if (graphSim.nodes.length === 0) {
|
|
2647
|
+
ctx.fillStyle = '#999999';
|
|
2648
|
+
ctx.font = '14px Lora, Georgia, serif';
|
|
2649
|
+
ctx.textAlign = 'center';
|
|
2650
|
+
ctx.fillText('No graph data yet.', w / 2, h / 2 - 16);
|
|
2651
|
+
ctx.font = '12px Inter, sans-serif';
|
|
2652
|
+
ctx.fillText('Set GRAPH_EXTRACTION_ENABLED=true to enable knowledge graph extraction.', w / 2, h / 2 + 8);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
async function loadMemories() {
|
|
2657
|
+
var el = document.getElementById('view-memories');
|
|
2658
|
+
el.innerHTML = '<div style="padding:24px;"><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div>';
|
|
2659
|
+
var result = await apiGet('memories?latest=true&limit=2000');
|
|
2660
|
+
var items = (result && result.memories) || [];
|
|
2661
|
+
items.sort(function(a, b) {
|
|
2662
|
+
var ac = (a && a.createdAt) || (a && a.updatedAt) || '';
|
|
2663
|
+
var bc = (b && b.createdAt) || (b && b.updatedAt) || '';
|
|
2664
|
+
return bc.localeCompare(ac);
|
|
2665
|
+
});
|
|
2666
|
+
state.memories.items = items;
|
|
2667
|
+
state.memories.total = (result && typeof result.total === 'number') ? result.total : items.length;
|
|
2668
|
+
state.memories.loaded = true;
|
|
2669
|
+
renderMemories();
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
function renderMemories() {
|
|
2673
|
+
var el = document.getElementById('view-memories');
|
|
2674
|
+
var items = state.memories.items;
|
|
2675
|
+
var search = state.memories.search.toLowerCase();
|
|
2676
|
+
var typeFilter = state.memories.typeFilter;
|
|
2677
|
+
var folderFilter = state.memories.folderFilter || '';
|
|
2678
|
+
|
|
2679
|
+
var projects = [];
|
|
2680
|
+
var hasNoFolder = false;
|
|
2681
|
+
items.forEach(function(m) {
|
|
2682
|
+
var pPath = m.project ? String(m.project).trim() : '';
|
|
2683
|
+
if (pPath) {
|
|
2684
|
+
if (!projects.includes(pPath)) {
|
|
2685
|
+
projects.push(pPath);
|
|
2686
|
+
}
|
|
2687
|
+
} else {
|
|
2688
|
+
hasNoFolder = true;
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2691
|
+
projects.sort();
|
|
2692
|
+
|
|
2693
|
+
var filtered = items.filter(function(m) {
|
|
2694
|
+
if (typeFilter && m.type !== typeFilter) return false;
|
|
2695
|
+
var pPath = m.project ? String(m.project).trim() : '';
|
|
2696
|
+
if (folderFilter) {
|
|
2697
|
+
if (folderFilter === '_none_') {
|
|
2698
|
+
if (pPath) return false;
|
|
2699
|
+
} else if (pPath !== folderFilter) {
|
|
2700
|
+
return false;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
const normalizedSearch = (search || '').normalize("NFKC").toLowerCase();
|
|
2704
|
+
const normalizedTitle = (m.title || '').normalize("NFKC").toLowerCase();
|
|
2705
|
+
const normalizedContent = (m.content || '').normalize("NFKC").toLowerCase();
|
|
2706
|
+
if (search && !normalizedTitle.includes(normalizedSearch) && !normalizedContent.includes(normalizedSearch)) {
|
|
2707
|
+
return false;
|
|
2708
|
+
}
|
|
2709
|
+
return true;
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
var types = {};
|
|
2713
|
+
items.forEach(function(m) { types[m.type] = true; });
|
|
2714
|
+
var typeOptions = Object.keys(types).sort();
|
|
2715
|
+
|
|
2716
|
+
var html = '<div class="card" style="margin-bottom:12px;padding:12px;background:var(--bg-subtle);">';
|
|
2717
|
+
html += '<div style="font-size:13px;color:var(--ink-muted);line-height:1.5;">';
|
|
2718
|
+
html += '<strong>Memories</strong> are durable facts, architecture notes, conventions, and lessons saved via <code>agent_cache</code> MCP tool or the <code>/agentcache/remember</code> endpoint. They survive across sessions and supersede each other as v1, v2, etc. ';
|
|
2719
|
+
html += '<span style="color:var(--ink-faint);">Shown: ' + items.length + ' total.</span>';
|
|
2720
|
+
html += '</div></div>';
|
|
2721
|
+
|
|
2722
|
+
html += '<div class="toolbar">';
|
|
2723
|
+
html += '<input type="text" id="mem-search" placeholder="Search memories..." value="' + esc(state.memories.search) + '">';
|
|
2724
|
+
html += '<select id="mem-type-filter"><option value="">All types</option>';
|
|
2725
|
+
typeOptions.forEach(function(t) {
|
|
2726
|
+
html += '<option value="' + esc(t) + '"' + (typeFilter === t ? ' selected' : '') + '>' + esc(t) + '</option>';
|
|
2727
|
+
});
|
|
2728
|
+
html += '</select>';
|
|
2729
|
+
html += '<select id="mem-folder-filter"><option value="">All folders</option>';
|
|
2730
|
+
if (hasNoFolder) {
|
|
2731
|
+
html += '<option value="_none_"' + (folderFilter === '_none_' ? ' selected' : '') + '>Uncategorized (No Folder)</option>';
|
|
2732
|
+
}
|
|
2733
|
+
projects.forEach(function(p) {
|
|
2734
|
+
html += '<option value="' + esc(p) + '"' + (folderFilter === p ? ' selected' : '') + '>' + esc(p) + '</option>';
|
|
2735
|
+
});
|
|
2736
|
+
html += '</select>';
|
|
2737
|
+
html += '</div>';
|
|
2738
|
+
|
|
2739
|
+
if (filtered.length === 0) {
|
|
2740
|
+
html += '<div class="empty-state">' +
|
|
2741
|
+
'<div class="empty-icon">📚</div>' +
|
|
2742
|
+
'<div class="empty-title">No memories yet</div>' +
|
|
2743
|
+
'<div class="empty-lead">Memories are the distilled facts agentcache keeps across sessions — things like file paths, architectural decisions, and user preferences. Hooks capture them automatically during coding sessions; you can also save one directly.</div>' +
|
|
2744
|
+
'<pre class="empty-cmd">agent_cache {\n title: "auth uses jose middleware",\n content: "src/middleware/auth.ts handles JWT validation",\n type: "architecture"\n}</pre>' +
|
|
2745
|
+
'<div><a class="empty-link" href="https://github.com/rohitg00/agentmemory#memories" target="_blank" rel="noopener">Memory types →</a></div>' +
|
|
2746
|
+
'</div>';
|
|
2747
|
+
} else {
|
|
2748
|
+
html += '<table><tr><th>Title</th><th>Type</th><th>Strength</th><th>Version</th><th>Updated</th><th>Actions</th></tr>';
|
|
2749
|
+
filtered.forEach(function(m) {
|
|
2750
|
+
var badgeClass = TYPE_BADGES[m.type] || 'badge-muted';
|
|
2751
|
+
var rawStrength = m.strength || 0;
|
|
2752
|
+
var strength = Math.round(rawStrength <= 1 ? rawStrength * 100 : rawStrength * 10);
|
|
2753
|
+
if (strength > 100) strength = 100;
|
|
2754
|
+
var barColor = strength > 70 ? 'var(--green)' : strength > 40 ? 'var(--yellow)' : 'var(--red)';
|
|
2755
|
+
html += '<tr>';
|
|
2756
|
+
var preview = (m.content || '').split('\n').slice(0, 2).join(' ').trim();
|
|
2757
|
+
var previewHtml = esc(truncate(preview, 150));
|
|
2758
|
+
if (search && search.length > 2) {
|
|
2759
|
+
var re = new RegExp('(' + search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
|
|
2760
|
+
previewHtml = previewHtml.replace(re, '<mark>$1</mark>');
|
|
2761
|
+
}
|
|
2762
|
+
html += '<td><span style="color:var(--ink);font-weight:600;">' + esc(truncate(m.title, 50)) + '</span>';
|
|
2763
|
+
html += '<div style="font-size:12px;color:var(--ink-muted);margin-top:3px;line-height:1.4;max-height:34px;overflow:hidden;">' + previewHtml + '</div>';
|
|
2764
|
+
if (m.concepts && m.concepts.length > 0) {
|
|
2765
|
+
html += '<div style="margin-top:3px;display:flex;gap:4px;flex-wrap:wrap;">';
|
|
2766
|
+
m.concepts.slice(0, 4).forEach(function(c) { html += '<span class="tag">' + esc(c) + '</span>'; });
|
|
2767
|
+
html += '</div>';
|
|
2768
|
+
}
|
|
2769
|
+
html += '</td>';
|
|
2770
|
+
html += '<td><span class="badge ' + badgeClass + '">' + esc(m.type) + '</span></td>';
|
|
2771
|
+
html += '<td><div class="strength-bar"><div class="fill" style="width:' + strength + '%;background:' + barColor + '"></div></div> <span style="font-size:10px;color:var(--ink-faint);font-family:var(--font-mono);">' + strength + '%</span></td>';
|
|
2772
|
+
html += '<td style="color:var(--ink-muted);font-family:var(--font-mono);font-size:12px;">v' + (m.version || 1) + '</td>';
|
|
2773
|
+
html += '<td style="font-size:11px;color:var(--ink-faint);font-family:var(--font-mono);">' + esc(formatTime(m.updatedAt)) + '</td>';
|
|
2774
|
+
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>';
|
|
2775
|
+
html += '</tr>';
|
|
2776
|
+
});
|
|
2777
|
+
html += '</table>';
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
var __focus = captureSearchFocus(['mem-search']);
|
|
2781
|
+
el.innerHTML = html;
|
|
2782
|
+
|
|
2783
|
+
var searchInput = document.getElementById('mem-search');
|
|
2784
|
+
if (searchInput) {
|
|
2785
|
+
bindImeSafeSearch(searchInput, 200, function(v){ state.memories.search = v; renderMemories(); });
|
|
2786
|
+
}
|
|
2787
|
+
var typeSelect = document.getElementById('mem-type-filter');
|
|
2788
|
+
if (typeSelect) {
|
|
2789
|
+
typeSelect.addEventListener('change', function() {
|
|
2790
|
+
state.memories.typeFilter = this.value;
|
|
2791
|
+
renderMemories();
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
var folderSelect = document.getElementById('mem-folder-filter');
|
|
2795
|
+
if (folderSelect) {
|
|
2796
|
+
folderSelect.addEventListener('change', function() {
|
|
2797
|
+
state.memories.folderFilter = this.value;
|
|
2798
|
+
renderMemories();
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
restoreSearchFocus(__focus);
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
function deleteMemory(id, title) {
|
|
2805
|
+
var modal = document.getElementById('modal');
|
|
2806
|
+
var overlay = document.getElementById('modal-overlay');
|
|
2807
|
+
modal.innerHTML = '<h3>Delete Memory</h3><p>Are you sure you want to delete "' + esc(title) + '"? This action cannot be undone.</p><div class="modal-actions"><button class="btn" data-action="close-modal">Cancel</button><button class="btn btn-danger" data-action="confirm-delete-memory" data-memory-id="' + esc(id) + '">Delete</button></div>';
|
|
2808
|
+
overlay.classList.add('open');
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
async function confirmDeleteMemory(id) {
|
|
2812
|
+
closeModal();
|
|
2813
|
+
await apiPost('forget', { memoryId: id });
|
|
2814
|
+
state.memories.loaded = false;
|
|
2815
|
+
loadMemories();
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
function closeModal() {
|
|
2819
|
+
document.getElementById('modal-overlay').classList.remove('open');
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// =====================================================================
|
|
2823
|
+
// Timeline Tab — folder activity feed
|
|
2824
|
+
// =====================================================================
|
|
2825
|
+
var tlFolderFilter = '';
|
|
2826
|
+
var tlAgentFilter = '';
|
|
2827
|
+
|
|
2828
|
+
async function loadTimeline(folderPath, agentId) {
|
|
2829
|
+
var el = document.getElementById('view-timeline');
|
|
2830
|
+
if (!el) return;
|
|
2831
|
+
el.innerHTML = '<div style="padding:24px;"><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div>';
|
|
2832
|
+
try {
|
|
2833
|
+
var body = { limit: 100 };
|
|
2834
|
+
if (folderPath) body.folderPath = folderPath;
|
|
2835
|
+
if (agentId) body.agentId = agentId;
|
|
2836
|
+
var result = await apiFetchRaw('/agentcache/timeline', {
|
|
2837
|
+
method: 'POST',
|
|
2838
|
+
body: JSON.stringify(body)
|
|
2839
|
+
});
|
|
2840
|
+
var obs = (result && result.observations) || [];
|
|
2841
|
+
renderTimeline(obs);
|
|
2842
|
+
} catch(err) {
|
|
2843
|
+
el.innerHTML = '<div class="loading" style="color:var(--accent);">Failed to load timeline: ' + esc(String(err)) + '</div>';
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
function renderTimeline(obs) {
|
|
2848
|
+
var el = document.getElementById('view-timeline');
|
|
2849
|
+
if (!el) return;
|
|
2850
|
+
|
|
2851
|
+
// Collect unique agents for dropdown
|
|
2852
|
+
var agents = [];
|
|
2853
|
+
var agentSeen = {};
|
|
2854
|
+
obs.forEach(function(o) {
|
|
2855
|
+
if (o.agentId && !agentSeen[o.agentId]) {
|
|
2856
|
+
agentSeen[o.agentId] = true;
|
|
2857
|
+
agents.push(o.agentId);
|
|
2858
|
+
}
|
|
2859
|
+
});
|
|
2860
|
+
|
|
2861
|
+
var html = '<div style="padding:16px 24px;">';
|
|
2862
|
+
// Filter bar
|
|
2863
|
+
html += '<div style="display:flex;gap:12px;align-items:center;margin-bottom:16px;flex-wrap:wrap;">';
|
|
2864
|
+
html += '<input id="tl-folder-filter" type="text" placeholder="Filter by folder path..." value="' + esc(tlFolderFilter) + '" style="flex:1;min-width:200px;padding:7px 12px;border:1px solid var(--border-light);background:var(--bg);color:var(--ink);font-family:var(--font-mono);font-size:12px;">';
|
|
2865
|
+
html += '<select id="tl-agent-filter" style="padding:7px 12px;border:1px solid var(--border-light);background:var(--bg);color:var(--ink);font-family:var(--font-ui);font-size:12px;">';
|
|
2866
|
+
html += '<option value="">All agents</option>';
|
|
2867
|
+
agents.forEach(function(a) {
|
|
2868
|
+
html += '<option value="' + esc(a) + '"' + (tlAgentFilter === a ? ' selected' : '') + '>' + esc(a) + '</option>';
|
|
2869
|
+
});
|
|
2870
|
+
html += '</select>';
|
|
2871
|
+
html += '<span style="font-size:12px;color:var(--ink-muted);">' + obs.length + ' observations</span>';
|
|
2872
|
+
html += '</div>';
|
|
2873
|
+
|
|
2874
|
+
// Cards
|
|
2875
|
+
html += '<div id="tl-cards">';
|
|
2876
|
+
if (obs.length === 0) {
|
|
2877
|
+
html += '<div class="empty-state"><div class="empty-icon">🕑</div><p>No observations found.</p></div>';
|
|
2878
|
+
} else {
|
|
2879
|
+
obs.forEach(function(o) {
|
|
2880
|
+
html += renderTimelineCard(o);
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
html += '</div></div>';
|
|
2884
|
+
el.innerHTML = html;
|
|
2885
|
+
|
|
2886
|
+
// Wire up filters
|
|
2887
|
+
var folderInput = document.getElementById('tl-folder-filter');
|
|
2888
|
+
var agentSelect = document.getElementById('tl-agent-filter');
|
|
2889
|
+
if (folderInput) {
|
|
2890
|
+
folderInput.addEventListener('input', function() {
|
|
2891
|
+
tlFolderFilter = this.value;
|
|
2892
|
+
loadTimeline(tlFolderFilter || undefined, tlAgentFilter || undefined);
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
if (agentSelect) {
|
|
2896
|
+
agentSelect.addEventListener('change', function() {
|
|
2897
|
+
tlAgentFilter = this.value;
|
|
2898
|
+
loadTimeline(tlFolderFilter || undefined, tlAgentFilter || undefined);
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
function renderTimelineCard(o) {
|
|
2904
|
+
var ts = o.timestamp ? new Date(o.timestamp).toLocaleString() : '';
|
|
2905
|
+
var typeColors = {
|
|
2906
|
+
'file_edit': 'var(--blue)', 'file_write': 'var(--blue)', 'file_read': 'var(--cyan)',
|
|
2907
|
+
'command_run': 'var(--orange)', 'search': 'var(--green)',
|
|
2908
|
+
'error': 'var(--red)', 'conversation': 'var(--purple)', 'subagent': 'var(--yellow)'
|
|
2909
|
+
};
|
|
2910
|
+
var typeColor = typeColors[o.type] || 'var(--ink-muted)';
|
|
2911
|
+
var fp = o.folderPath || '';
|
|
2912
|
+
var fpShort = fp.split(/[\\/]/).slice(-2).join('/');
|
|
2913
|
+
var html = '<div style="border:1px solid var(--border-light);border-radius:4px;padding:12px 16px;margin-bottom:8px;background:var(--bg);">';
|
|
2914
|
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap;">';
|
|
2915
|
+
html += '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:' + typeColor + ';background:var(--bg-subtle);padding:2px 7px;border-radius:3px;">' + esc(o.type || 'other') + '</span>';
|
|
2916
|
+
html += '<span style="font-size:10px;font-weight:600;background:var(--bg-inset);padding:2px 7px;border-radius:3px;">' + esc(o.agentId || '') + '</span>';
|
|
2917
|
+
html += '<span style="font-size:11px;color:var(--ink-muted);font-family:var(--font-mono);" title="' + esc(fp) + '">' + esc(fpShort) + '</span>';
|
|
2918
|
+
html += '<span style="margin-left:auto;font-size:11px;color:var(--ink-faint);font-family:var(--font-mono);">' + esc(ts) + '</span>';
|
|
2919
|
+
html += '</div>';
|
|
2920
|
+
if (o.title) html += '<div style="font-weight:600;font-size:13px;color:var(--ink);margin-bottom:3px;">' + esc(o.title) + '</div>';
|
|
2921
|
+
if (o.text) {
|
|
2922
|
+
var excerpt = o.text.length > 200 ? o.text.slice(0, 200) + '\u2026' : o.text;
|
|
2923
|
+
html += '<div style="font-size:12px;color:var(--ink-secondary);line-height:1.5;">' + esc(excerpt) + '</div>';
|
|
2924
|
+
}
|
|
2925
|
+
html += '</div>';
|
|
2926
|
+
return html;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
function prependTimelineCard(obs) {
|
|
2930
|
+
var cards = document.getElementById('tl-cards');
|
|
2931
|
+
if (!cards) return;
|
|
2932
|
+
var div = document.createElement('div');
|
|
2933
|
+
div.innerHTML = renderTimelineCard(obs);
|
|
2934
|
+
cards.insertBefore(div.firstChild, cards.firstChild);
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
function prependFolderObservationCard(o) {
|
|
2938
|
+
var list = document.getElementById('obs-list');
|
|
2939
|
+
if (!list) return;
|
|
2940
|
+
var div = document.createElement('div');
|
|
2941
|
+
var ts = o.timestamp ? new Date(o.timestamp).toLocaleString() : '';
|
|
2942
|
+
var typeColor = {'file_edit':'var(--blue)','command_run':'var(--orange)','search':'var(--green)','error':'var(--red)','conversation':'var(--purple)'}[o.type] || 'var(--ink-muted)';
|
|
2943
|
+
var html = '';
|
|
2944
|
+
html += '<div class="obs-detail-card" data-obs-id="' + esc(o.id) + '" style="border:1px solid var(--border-light);border-radius:4px;padding:12px 16px;margin-bottom:8px;background:var(--bg);display:flex;gap:10px;align-items:flex-start;">';
|
|
2945
|
+
// checkbox
|
|
2946
|
+
html += '<label style="padding-top:2px;cursor:pointer;flex-shrink:0;">';
|
|
2947
|
+
html += '<input type="checkbox" class="obs-checkbox" data-obs-id="' + esc(o.id) + '" style="accent-color:var(--ink);">';
|
|
2948
|
+
html += '</label>';
|
|
2949
|
+
// content
|
|
2950
|
+
html += '<div style="flex:1;min-width:0;">';
|
|
2951
|
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap;">';
|
|
2952
|
+
html += '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:' + typeColor + ';background:var(--bg-subtle);padding:2px 7px;border-radius:3px;">' + esc(o.type || 'other') + '</span>';
|
|
2953
|
+
html += '<span style="font-size:11px;color:var(--ink-muted);font-family:var(--font-mono);">' + esc(ts) + '</span>';
|
|
2954
|
+
html += '<span style="flex:1;"></span>';
|
|
2955
|
+
html += '<button class="btn btn-danger" data-action="delete-obs" data-obs-id="' + esc(o.id) + '" data-folder-path="' + esc(_folderDetailFp) + '" data-agent-id="' + esc(_folderDetailAid) + '" style="font-size:9px;padding:2px 8px;">Delete</button>';
|
|
2956
|
+
html += '</div>';
|
|
2957
|
+
if (o.title) html += '<div style="font-weight:600;font-size:14px;color:var(--ink);margin-bottom:4px;">' + esc(o.title) + '</div>';
|
|
2958
|
+
if (o.text) html += '<div style="font-size:13px;color:var(--ink-secondary);line-height:1.5;white-space:pre-wrap;">' + esc(o.text.length > 300 ? o.text.slice(0, 300) + '\u2026' : o.text) + '</div>';
|
|
2959
|
+
html += '</div>';
|
|
2960
|
+
html += '</div>';
|
|
2961
|
+
div.innerHTML = html;
|
|
2962
|
+
list.insertBefore(div.firstChild, list.firstChild);
|
|
2963
|
+
|
|
2964
|
+
// Update count in header
|
|
2965
|
+
var remaining = document.querySelectorAll('.obs-detail-card').length;
|
|
2966
|
+
var countEl = document.querySelector('#folder-detail-root span[style*="ink-muted"]');
|
|
2967
|
+
if (countEl) countEl.textContent = remaining + ' observation' + (remaining !== 1 ? 's' : '');
|
|
2968
|
+
|
|
2969
|
+
_wireFolderDetailCheckboxes();
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
function updateFolderBadge(folderPath, agentId) {
|
|
2973
|
+
var rows = document.querySelectorAll('.folder-row');
|
|
2974
|
+
rows.forEach(function(row) {
|
|
2975
|
+
if (row.dataset.folder === folderPath && row.dataset.agent === agentId) {
|
|
2976
|
+
var countCell = row.querySelector('td:nth-child(3)');
|
|
2977
|
+
if (countCell) {
|
|
2978
|
+
var cur = parseInt(countCell.textContent) || 0;
|
|
2979
|
+
countCell.textContent = cur + 1;
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
var tlTypeFilter = '';
|
|
2986
|
+
|
|
2987
|
+
function renderObservations() {
|
|
2988
|
+
var content = document.getElementById('tl-content');
|
|
2989
|
+
if (!content) return;
|
|
2990
|
+
var obs = state.timeline.observations;
|
|
2991
|
+
var minImp = state.timeline.minImportance;
|
|
2992
|
+
var filtered = minImp > 0 ? obs.filter(function(o) { return (o.importance || 0) >= minImp; }) : obs;
|
|
2993
|
+
|
|
2994
|
+
var TOOL_TYPE_MAP = { Read: 'file_read', Write: 'file_write', Edit: 'file_edit', Bash: 'command_run', Grep: 'search', Glob: 'search', WebFetch: 'web_fetch', WebSearch: 'web_fetch', AskUserQuestion: 'conversation', Task: 'subagent' };
|
|
2995
|
+
|
|
2996
|
+
var typeCounts = {};
|
|
2997
|
+
filtered.forEach(function(o) {
|
|
2998
|
+
var t = o.type || TOOL_TYPE_MAP[o.toolName] || (o.hookType ? o.hookType.replace(/_/g, ' ') : 'other');
|
|
2999
|
+
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
3000
|
+
});
|
|
3001
|
+
var typeList = Object.keys(typeCounts).sort(function(a, b) { return typeCounts[b] - typeCounts[a]; });
|
|
3002
|
+
|
|
3003
|
+
if (tlTypeFilter) {
|
|
3004
|
+
filtered = filtered.filter(function(o) {
|
|
3005
|
+
var t = o.type || TOOL_TYPE_MAP[o.toolName] || (o.hookType ? o.hookType.replace(/_/g, ' ') : 'other');
|
|
3006
|
+
return t === tlTypeFilter;
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
var pageSize = state.timeline.pageSize;
|
|
3011
|
+
var page = state.timeline.page;
|
|
3012
|
+
var start = page * pageSize;
|
|
3013
|
+
var paged = filtered.slice(start, start + pageSize);
|
|
3014
|
+
var totalPages = Math.ceil(filtered.length / pageSize);
|
|
3015
|
+
|
|
3016
|
+
var html = '<div class="type-chips">';
|
|
3017
|
+
html += '<span class="type-chip' + (!tlTypeFilter ? ' active' : '') + '" data-action="timeline-filter" data-type-filter="">All (' + obs.length + ')</span>';
|
|
3018
|
+
typeList.forEach(function(t) {
|
|
3019
|
+
var color = OBS_TYPE_COLORS[t] || '#666666';
|
|
3020
|
+
html += '<span class="type-chip' + (tlTypeFilter === t ? ' active' : '') + '" data-action="timeline-filter" data-type-filter="' + esc(t) + '" style="' + (tlTypeFilter === t ? 'background:' + color + ';border-color:' + color + ';' : 'border-color:' + color + ';color:' + color + ';') + '">' + esc(t.replace(/_/g, ' ')) + ' (' + typeCounts[t] + ')</span>';
|
|
3021
|
+
});
|
|
3022
|
+
html += '</div>';
|
|
3023
|
+
|
|
3024
|
+
if (paged.length === 0) {
|
|
3025
|
+
html += '<div class="empty-state"><div class="empty-icon">🕑</div><p>No observations' + (obs.length > 0 ? ' match the filter (' + obs.length + ' total)' : ' for this session') + '</p></div>';
|
|
3026
|
+
content.innerHTML = html;
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
html += '<div style="font-size:11px;color:var(--ink-faint);margin-bottom:16px;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:0.06em;">' + filtered.length + ' observations shown</div>';
|
|
3031
|
+
|
|
3032
|
+
html += '<div class="timeline-container">';
|
|
3033
|
+
|
|
3034
|
+
var lastDateGroup = '';
|
|
3035
|
+
paged.forEach(function(o, idx) {
|
|
3036
|
+
var isCompressed = !!o.narrative || !!o.type;
|
|
3037
|
+
var isRaw = !isCompressed;
|
|
3038
|
+
var type = o.type || TOOL_TYPE_MAP[o.toolName] || 'other';
|
|
3039
|
+
var impVal = typeof o.importance === 'number' ? o.importance : 5;
|
|
3040
|
+
var impClass = impVal >= 7 ? 'high' : impVal >= 4 ? 'med' : 'low';
|
|
3041
|
+
var title = o.title || o.toolName || (o.hookType ? o.hookType.replace(/_/g, ' ') : 'Observation');
|
|
3042
|
+
var typeColor = OBS_TYPE_COLORS[type] || '#666666';
|
|
3043
|
+
var icon = OBS_TYPE_ICONS[type] || '📄';
|
|
3044
|
+
|
|
3045
|
+
var dateGroup = '';
|
|
3046
|
+
try {
|
|
3047
|
+
var d = new Date(o.timestamp);
|
|
3048
|
+
dateGroup = d.toLocaleDateString() + ' ' + d.getHours() + ':00';
|
|
3049
|
+
} catch(e) { dateGroup = ''; }
|
|
3050
|
+
|
|
3051
|
+
if (dateGroup && dateGroup !== lastDateGroup) {
|
|
3052
|
+
html += '<div class="timeline-date-marker"><span>' + esc(dateGroup) + '</span></div>';
|
|
3053
|
+
lastDateGroup = dateGroup;
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
var side = idx % 2 === 0 ? 'tl-left' : 'tl-right';
|
|
3057
|
+
|
|
3058
|
+
html += '<div class="timeline-item ' + side + '">';
|
|
3059
|
+
html += '<div class="timeline-dot" style="background:' + typeColor + ';"></div>';
|
|
3060
|
+
html += '<div class="timeline-connector"></div>';
|
|
3061
|
+
|
|
3062
|
+
html += '<div class="obs-card imp-' + impClass + '" style="border-left-color:' + typeColor + ';text-align:left;">';
|
|
3063
|
+
html += '<div class="obs-head">';
|
|
3064
|
+
html += '<div class="obs-title-row">';
|
|
3065
|
+
html += '<span class="obs-type-icon">' + icon + '</span>';
|
|
3066
|
+
html += '<span class="obs-title" title="' + esc(title) + '">' + esc(title) + '</span>';
|
|
3067
|
+
if (isRaw) html += '<span class="badge badge-muted" style="font-size:8px;margin-left:4px;">raw</span>';
|
|
3068
|
+
html += '</div>';
|
|
3069
|
+
html += '<div class="obs-meta">';
|
|
3070
|
+
if (isCompressed) html += '<span class="obs-importance imp-' + impVal + '" title="Importance: ' + impVal + '/10">' + impVal + '</span>';
|
|
3071
|
+
html += '<span class="obs-time">' + esc(shortTime(o.timestamp)) + '</span>';
|
|
3072
|
+
html += '</div></div>';
|
|
3073
|
+
|
|
3074
|
+
if (o.subtitle) html += '<div class="obs-subtitle">' + esc(o.subtitle) + '</div>';
|
|
3075
|
+
|
|
3076
|
+
html += '<div style="margin-top:4px;">';
|
|
3077
|
+
html += '<span class="badge" style="border-color:' + typeColor + ';color:' + typeColor + ';margin-right:4px;">' + esc(type.replace(/_/g, ' ')) + '</span>';
|
|
3078
|
+
if (o.hookType) html += '<span class="badge badge-muted" style="margin-right:4px;">' + esc(o.hookType) + '</span>';
|
|
3079
|
+
html += '</div>';
|
|
3080
|
+
|
|
3081
|
+
if (isRaw && o.toolInput) {
|
|
3082
|
+
var inputStr = typeof o.toolInput === 'string' ? o.toolInput : JSON.stringify(o.toolInput);
|
|
3083
|
+
html += '<div style="margin-top:6px;"><span style="font-size:10px;color:var(--ink-muted);font-weight:600;font-family:var(--font-ui);text-transform:uppercase;letter-spacing:0.08em;">Input:</span>';
|
|
3084
|
+
html += '<pre style="font-size:11px;color:var(--ink-muted);background:var(--bg-alt);padding:8px 10px;border:1px solid var(--border-light);margin-top:3px;overflow-x:auto;max-height:80px;font-family:var(--font-mono);">' + esc(truncate(inputStr, 300)) + '</pre></div>';
|
|
3085
|
+
}
|
|
3086
|
+
if (isRaw && o.toolOutput) {
|
|
3087
|
+
var outputStr = typeof o.toolOutput === 'string' ? o.toolOutput : JSON.stringify(o.toolOutput);
|
|
3088
|
+
html += '<div style="margin-top:4px;"><span style="font-size:10px;color:var(--ink-muted);font-weight:600;font-family:var(--font-ui);text-transform:uppercase;letter-spacing:0.08em;">Output:</span>';
|
|
3089
|
+
html += '<div class="obs-narrative" style="margin-top:3px;">' + esc(truncate(outputStr, 300)) + '</div></div>';
|
|
3090
|
+
}
|
|
3091
|
+
if (o.narrative) html += '<div class="obs-narrative" style="margin-top:8px;">' + esc(o.narrative) + '</div>';
|
|
3092
|
+
if (o.facts && o.facts.length > 0) {
|
|
3093
|
+
html += '<ul class="obs-facts">';
|
|
3094
|
+
o.facts.forEach(function(f) { html += '<li>' + esc(f) + '</li>'; });
|
|
3095
|
+
html += '</ul>';
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
var hasTags = (o.concepts && o.concepts.length) || (o.files && o.files.length);
|
|
3099
|
+
if (hasTags) {
|
|
3100
|
+
html += '<div class="tag-list">';
|
|
3101
|
+
(o.concepts || []).forEach(function(c) { html += '<span class="tag">' + esc(c) + '</span>'; });
|
|
3102
|
+
(o.files || []).forEach(function(f) {
|
|
3103
|
+
var short = f.split('/').pop();
|
|
3104
|
+
html += '<span class="tag file-tag" title="' + esc(f) + '">' + esc(short) + '</span>';
|
|
3105
|
+
});
|
|
3106
|
+
html += '</div>';
|
|
3107
|
+
}
|
|
3108
|
+
if (isRaw && o.toolInput) {
|
|
3109
|
+
var files = [];
|
|
3110
|
+
var ti = o.toolInput;
|
|
3111
|
+
if (typeof ti === 'object' && ti !== null) {
|
|
3112
|
+
if (ti.file_path) files.push(ti.file_path);
|
|
3113
|
+
if (ti.path) files.push(ti.path);
|
|
3114
|
+
}
|
|
3115
|
+
if (files.length > 0) {
|
|
3116
|
+
html += '<div class="tag-list">';
|
|
3117
|
+
files.forEach(function(f) {
|
|
3118
|
+
var short = String(f).split('/').pop();
|
|
3119
|
+
html += '<span class="tag file-tag" title="' + esc(f) + '">' + esc(short) + '</span>';
|
|
3120
|
+
});
|
|
3121
|
+
html += '</div>';
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
html += '</div>';
|
|
3125
|
+
html += '</div>';
|
|
3126
|
+
});
|
|
3127
|
+
|
|
3128
|
+
html += '</div>';
|
|
3129
|
+
|
|
3130
|
+
if (totalPages > 1) {
|
|
3131
|
+
html += '<div class="pagination">';
|
|
3132
|
+
if (page > 0) html += '<button class="btn" data-action="timeline-page" data-page="' + (page - 1) + '">Prev</button>';
|
|
3133
|
+
html += '<span style="color:var(--ink-faint);font-size:12px;padding:6px;font-family:var(--font-mono);">Page ' + (page + 1) + ' of ' + totalPages + ' (' + filtered.length + ' total)</span>';
|
|
3134
|
+
if (page < totalPages - 1) html += '<button class="btn" data-action="timeline-page" data-page="' + (page + 1) + '">Next</button>';
|
|
3135
|
+
html += '</div>';
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
content.innerHTML = html;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
function setTlTypeFilter(type) {
|
|
3142
|
+
tlTypeFilter = type;
|
|
3143
|
+
state.timeline.page = 0;
|
|
3144
|
+
renderObservations();
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
function tlPage(p) {
|
|
3148
|
+
state.timeline.page = p;
|
|
3149
|
+
renderObservations();
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// loadActivity removed — /agentcache/sessions endpoint does not exist.
|
|
3153
|
+
async function loadActivity() { return; }
|
|
3154
|
+
function renderActivity() { return; }
|
|
3155
|
+
|
|
3156
|
+
// loadSessions removed — /agentcache/sessions endpoint does not exist.
|
|
3157
|
+
async function loadSessions() { return; }
|
|
3158
|
+
function renderSessions() { return; }
|
|
3159
|
+
function selectSession(id) { return; }
|
|
3160
|
+
async function renderSessionDetail() { return; }
|
|
3161
|
+
async function endSession(id) { return; }
|
|
3162
|
+
async function summarizeSession(id, btn) { return; }
|
|
3163
|
+
|
|
3164
|
+
// All dead legacy load/render functions (loadSessions, renderSessions, loadLessons,
|
|
3165
|
+
// renderLessons, loadActions, renderActions, loadCrystals, renderCrystals,
|
|
3166
|
+
// loadAudit, renderAudit, loadProfile, renderProfile, loadPersonal, renderPersonal,
|
|
3167
|
+
// renderPersonalFileListOnly, loadReplay, renderReplay) have been removed.
|
|
3168
|
+
// These called endpoints that don't exist in this Python backend.
|
|
3169
|
+
// The audit state is still used for the command-palette; keep a minimal helper:
|
|
3170
|
+
async function loadAuditSilent() {
|
|
3171
|
+
try {
|
|
3172
|
+
var result = await apiGet('audit?limit=100');
|
|
3173
|
+
state.audit.entries = (result && result.entries) || [];
|
|
3174
|
+
state.audit.loaded = true;
|
|
3175
|
+
} catch(_) {}
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
function toggleAuditDetail(idx) {
|
|
3179
|
+
var el = document.getElementById('audit-detail-' + idx);
|
|
3180
|
+
if (el) el.classList.toggle('open');
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
var wsReconnectTimer = null;
|
|
3184
|
+
var wsRetries = 0;
|
|
3185
|
+
var WS_MAX_RETRIES = 4;
|
|
3186
|
+
var directFailed = false;
|
|
3187
|
+
var directFailures = 0;
|
|
3188
|
+
var DIRECT_FAILURE_THRESHOLD = 2;
|
|
3189
|
+
var pollTimer = null;
|
|
3190
|
+
var POLL_INTERVAL_MS = 10000;
|
|
3191
|
+
|
|
3192
|
+
function setWsStatus(text, cls) {
|
|
3193
|
+
var el = document.getElementById('ws-status');
|
|
3194
|
+
if (!el) return;
|
|
3195
|
+
el.textContent = text;
|
|
3196
|
+
el.className = 'ws-status ' + cls;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
var WS_REPROBE_EVERY_TICKS = 6;
|
|
3200
|
+
|
|
3201
|
+
function startPolling() {
|
|
3202
|
+
if (pollTimer) return;
|
|
3203
|
+
setWsStatus('polling · ' + (POLL_INTERVAL_MS / 1000) + 's', 'disconnected');
|
|
3204
|
+
var tick = 0;
|
|
3205
|
+
pollTimer = setInterval(function() {
|
|
3206
|
+
tick++;
|
|
3207
|
+
// Only refresh tabs backed by real endpoints
|
|
3208
|
+
if (state.activeTab === 'folders') {
|
|
3209
|
+
loadFolders();
|
|
3210
|
+
} else if (state.activeTab === 'memories') {
|
|
3211
|
+
state.memories.loaded = false;
|
|
3212
|
+
loadMemories();
|
|
3213
|
+
} else if (state.activeTab === 'timeline') {
|
|
3214
|
+
loadTimeline(tlFolderFilter || undefined, tlAgentFilter || undefined);
|
|
3215
|
+
}
|
|
3216
|
+
if (tick % WS_REPROBE_EVERY_TICKS === 0) {
|
|
3217
|
+
var ws = state.ws;
|
|
3218
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
3219
|
+
wsRetries = 0;
|
|
3220
|
+
directFailures = 0;
|
|
3221
|
+
directFailed = false;
|
|
3222
|
+
connectWs();
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
}, POLL_INTERVAL_MS);
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
function stopPolling() {
|
|
3229
|
+
if (!pollTimer) return;
|
|
3230
|
+
clearInterval(pollTimer);
|
|
3231
|
+
pollTimer = null;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
var WS_CONNECT_TIMEOUT_MS = 5000;
|
|
3235
|
+
|
|
3236
|
+
function connectWs() {
|
|
3237
|
+
if (wsRetries >= WS_MAX_RETRIES) {
|
|
3238
|
+
startPolling();
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
var useDirect = !directFailed;
|
|
3242
|
+
var token = getViewerToken();
|
|
3243
|
+
var suffix = token ? '?token=' + encodeURIComponent(token) : '';
|
|
3244
|
+
var ws;
|
|
3245
|
+
try {
|
|
3246
|
+
ws = new WebSocket((useDirect ? WS_DIRECT_URL : WS_URL) + suffix);
|
|
3247
|
+
ws.__direct = useDirect;
|
|
3248
|
+
} catch (_) {
|
|
3249
|
+
ws = new WebSocket(WS_URL + suffix);
|
|
3250
|
+
ws.__direct = false;
|
|
3251
|
+
}
|
|
3252
|
+
var connectTimer = setTimeout(function() {
|
|
3253
|
+
if (ws.readyState === WebSocket.CONNECTING) {
|
|
3254
|
+
try { ws.close(); } catch {}
|
|
3255
|
+
}
|
|
3256
|
+
}, WS_CONNECT_TIMEOUT_MS);
|
|
3257
|
+
try {
|
|
3258
|
+
ws.onopen = function() {
|
|
3259
|
+
clearTimeout(connectTimer);
|
|
3260
|
+
if (state.ws !== ws) return;
|
|
3261
|
+
wsRetries = 0;
|
|
3262
|
+
stopPolling();
|
|
3263
|
+
if (ws.__direct) {
|
|
3264
|
+
directFailures = 0;
|
|
3265
|
+
directFailed = false;
|
|
3266
|
+
}
|
|
3267
|
+
if (!ws.__direct) {
|
|
3268
|
+
ws.send(JSON.stringify({
|
|
3269
|
+
type: 'join',
|
|
3270
|
+
data: {
|
|
3271
|
+
subscriptionId: 'viewer-' + Date.now(),
|
|
3272
|
+
streamName: 'mem-live',
|
|
3273
|
+
groupId: 'viewer'
|
|
3274
|
+
}
|
|
3275
|
+
}));
|
|
3276
|
+
}
|
|
3277
|
+
setWsStatus('live', 'connected');
|
|
3278
|
+
};
|
|
3279
|
+
ws.onmessage = function(e) {
|
|
3280
|
+
if (state.ws !== ws) return;
|
|
3281
|
+
try {
|
|
3282
|
+
var msg = JSON.parse(e.data);
|
|
3283
|
+
if (msg.type === 'folder_observation') {
|
|
3284
|
+
// New folder-based broadcast
|
|
3285
|
+
if (state.activeTab === 'timeline') {
|
|
3286
|
+
prependTimelineCard(msg.data || {});
|
|
3287
|
+
}
|
|
3288
|
+
if (state.activeTab === 'folders') {
|
|
3289
|
+
updateFolderBadge(msg.folderPath, msg.agentId);
|
|
3290
|
+
if (_folderDetailFp === msg.folderPath && _folderDetailAid === msg.agentId) {
|
|
3291
|
+
prependFolderObservationCard(msg.data || {});
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
} else if (msg.type === 'memory_created' || msg.type === 'memory_deleted') {
|
|
3295
|
+
if (state.activeTab === 'memories') {
|
|
3296
|
+
state.memories.loaded = false;
|
|
3297
|
+
loadMemories();
|
|
3298
|
+
}
|
|
3299
|
+
} else if (msg.type === 'folder_deleted') {
|
|
3300
|
+
if (state.activeTab === 'folders') {
|
|
3301
|
+
if (_folderDetailFp === msg.folderPath && _folderDetailAid === msg.agentId) {
|
|
3302
|
+
loadFolders(); // go back
|
|
3303
|
+
} else {
|
|
3304
|
+
var row = document.querySelector('.folder-row[data-folder="' + esc(msg.folderPath) + '"][data-agent="' + esc(msg.agentId) + '"]');
|
|
3305
|
+
if (row) row.remove();
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
} else if (msg.type === 'observations_deleted') {
|
|
3309
|
+
if (state.activeTab === 'timeline') {
|
|
3310
|
+
var oids = new Set(msg.observationIds || []);
|
|
3311
|
+
state.timeline.observations = state.timeline.observations.filter(function(o) {
|
|
3312
|
+
return !oids.has(o.id);
|
|
3313
|
+
});
|
|
3314
|
+
renderObservations();
|
|
3315
|
+
}
|
|
3316
|
+
if (state.activeTab === 'folders') {
|
|
3317
|
+
if (_folderDetailFp === msg.folderPath && _folderDetailAid === msg.agentId) {
|
|
3318
|
+
var oids = new Set(msg.observationIds || []);
|
|
3319
|
+
oids.forEach(function(oid) {
|
|
3320
|
+
var card = document.querySelector('.obs-detail-card[data-obs-id="' + oid + '"]');
|
|
3321
|
+
if (card) card.remove();
|
|
3322
|
+
_selectedObsIds.delete(oid);
|
|
3323
|
+
});
|
|
3324
|
+
var remaining = document.querySelectorAll('.obs-detail-card').length;
|
|
3325
|
+
var countEl = document.querySelector('#folder-detail-root span[style*="ink-muted"]');
|
|
3326
|
+
if (countEl) countEl.textContent = remaining + ' observation' + (remaining !== 1 ? 's' : '');
|
|
3327
|
+
} else {
|
|
3328
|
+
var rows = document.querySelectorAll('.folder-row');
|
|
3329
|
+
rows.forEach(function(row) {
|
|
3330
|
+
if (row.dataset.folder === msg.folderPath && row.dataset.agent === msg.agentId) {
|
|
3331
|
+
var countCell = row.querySelector('td:nth-child(3)');
|
|
3332
|
+
if (countCell) {
|
|
3333
|
+
var cur = parseInt(countCell.textContent) || 0;
|
|
3334
|
+
countCell.textContent = Math.max(0, cur - (msg.observationIds || []).length);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
} else if (msg.type === 'raw_observation' || msg.type === 'compressed_observation') {
|
|
3341
|
+
if (msg.data && msg.data.observation) {
|
|
3342
|
+
routeWsMessage({ observation: msg.data.observation });
|
|
3343
|
+
}
|
|
3344
|
+
} else if (msg.type === 'stream' && msg.event) {
|
|
3345
|
+
handleStreamEvent(msg);
|
|
3346
|
+
} else if (msg.event_type && msg.data) {
|
|
3347
|
+
handleStreamEvent({ event: { type: 'create', data: msg.data, event_type: msg.event_type } });
|
|
3348
|
+
}
|
|
3349
|
+
} catch {}
|
|
3350
|
+
};
|
|
3351
|
+
ws.onclose = function(e) {
|
|
3352
|
+
clearTimeout(connectTimer);
|
|
3353
|
+
if (state.ws !== ws) return;
|
|
3354
|
+
if (e && e.code === 1008) {
|
|
3355
|
+
showViewerAuthPrompt();
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
if (ws.__direct) {
|
|
3359
|
+
directFailures += 1;
|
|
3360
|
+
if (directFailures >= DIRECT_FAILURE_THRESHOLD) {
|
|
3361
|
+
directFailed = true;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
wsRetries++;
|
|
3365
|
+
if (wsRetries < WS_MAX_RETRIES) {
|
|
3366
|
+
setWsStatus('connecting...', 'disconnected');
|
|
3367
|
+
wsReconnectTimer = setTimeout(connectWs, 2000 + Math.min(wsRetries * 1000, 8000));
|
|
3368
|
+
} else {
|
|
3369
|
+
startPolling();
|
|
3370
|
+
}
|
|
3371
|
+
};
|
|
3372
|
+
ws.onerror = function() {
|
|
3373
|
+
if (state.ws !== ws) return;
|
|
3374
|
+
try { ws.close(); } catch {}
|
|
3375
|
+
};
|
|
3376
|
+
state.ws = ws;
|
|
3377
|
+
} catch {
|
|
3378
|
+
wsRetries++;
|
|
3379
|
+
if (wsRetries < WS_MAX_RETRIES) {
|
|
3380
|
+
wsReconnectTimer = setTimeout(connectWs, 2000 + Math.min(wsRetries * 1000, 8000));
|
|
3381
|
+
} else {
|
|
3382
|
+
startPolling();
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
function looksLikeObservation(obj) {
|
|
3388
|
+
return !!(obj && typeof obj === 'object' && obj.id && obj.timestamp);
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
function handleStreamEvent(msg) {
|
|
3392
|
+
var evt = msg.event;
|
|
3393
|
+
var observation;
|
|
3394
|
+
if (!evt) return;
|
|
3395
|
+
if (evt.event_type && evt.event_type !== 'observation' && evt.event_type !== 'create' && evt.event_type !== 'update') {
|
|
3396
|
+
return;
|
|
3397
|
+
}
|
|
3398
|
+
if (evt.type === 'event' && evt.data) {
|
|
3399
|
+
observation = evt.data.observation || evt.data;
|
|
3400
|
+
if (looksLikeObservation(observation)) {
|
|
3401
|
+
routeWsMessage({ observation: observation });
|
|
3402
|
+
}
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
if ((evt.type === 'create' || evt.type === 'update') && evt.data) {
|
|
3406
|
+
var payload = evt.data;
|
|
3407
|
+
observation = payload.observation || payload;
|
|
3408
|
+
if (looksLikeObservation(observation)) {
|
|
3409
|
+
routeWsMessage({ observation: observation });
|
|
3410
|
+
}
|
|
3411
|
+
} else if (evt.type === 'sync') {
|
|
3412
|
+
var items = Array.isArray(evt.data) ? evt.data : [];
|
|
3413
|
+
items.forEach(function(item) {
|
|
3414
|
+
var payload = item.data || item;
|
|
3415
|
+
observation = payload.observation || payload;
|
|
3416
|
+
if (looksLikeObservation(observation)) {
|
|
3417
|
+
routeWsMessage({ observation: observation });
|
|
3418
|
+
}
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
function routeWsMessage(msg) {
|
|
3424
|
+
if (state.activeTab === 'timeline' && msg.observation) {
|
|
3425
|
+
var existing = state.timeline.observations.findIndex(function(o) { return o.id === msg.observation.id; });
|
|
3426
|
+
if (existing >= 0) {
|
|
3427
|
+
state.timeline.observations[existing] = msg.observation;
|
|
3428
|
+
} else {
|
|
3429
|
+
state.timeline.observations.unshift(msg.observation);
|
|
3430
|
+
}
|
|
3431
|
+
renderObservations();
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
document.getElementById('tab-bar').addEventListener('click', function(e) {
|
|
3436
|
+
var btn = e.target instanceof Element ? e.target.closest('button[data-tab]') : null;
|
|
3437
|
+
if (btn) switchTab(btn.dataset.tab);
|
|
3438
|
+
});
|
|
3439
|
+
|
|
3440
|
+
document.querySelectorAll('[data-tab-link]').forEach(function(link) {
|
|
3441
|
+
link.addEventListener('click', function(e) {
|
|
3442
|
+
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
3443
|
+
e.preventDefault();
|
|
3444
|
+
switchTab(link.getAttribute('data-tab-link'));
|
|
3445
|
+
});
|
|
3446
|
+
});
|
|
3447
|
+
|
|
3448
|
+
function syncTabFromRoute() {
|
|
3449
|
+
switchTab(tabFromRoute(), { replaceRoute: true });
|
|
3450
|
+
}
|
|
3451
|
+
window.addEventListener('hashchange', syncTabFromRoute);
|
|
3452
|
+
window.addEventListener('popstate', syncTabFromRoute);
|
|
3453
|
+
|
|
3454
|
+
function getDismissedFlags() {
|
|
3455
|
+
if (!state.flagsDismissed) state.flagsDismissed = {};
|
|
3456
|
+
return state.flagsDismissed;
|
|
3457
|
+
}
|
|
3458
|
+
function dismissFlags(keys) {
|
|
3459
|
+
var dismissed = getDismissedFlags();
|
|
3460
|
+
keys.forEach(function(key) {
|
|
3461
|
+
if (key) dismissed[key] = true;
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
function renderFlagBanners(cfg) {
|
|
3465
|
+
var host = document.getElementById('flag-banners');
|
|
3466
|
+
if (!host) return;
|
|
3467
|
+
var dismissed = getDismissedFlags();
|
|
3468
|
+
var banners = [];
|
|
3469
|
+
(cfg.flags || []).forEach(function(f) {
|
|
3470
|
+
if (f.enabled) return;
|
|
3471
|
+
if (dismissed[f.key]) return;
|
|
3472
|
+
var tabsAffected = (f.affects || []).map(function(t) { return t.toLowerCase(); });
|
|
3473
|
+
if (tabsAffected.length && tabsAffected.indexOf(state.activeTab) === -1) return;
|
|
3474
|
+
banners.push({
|
|
3475
|
+
kind: 'warn',
|
|
3476
|
+
icon: '⚠',
|
|
3477
|
+
title: f.label,
|
|
3478
|
+
keyLabel: f.key,
|
|
3479
|
+
desc: f.description + (f.needsLlm ? ' Requires an LLM provider key (ANTHROPIC_API_KEY, GEMINI_API_KEY, etc.).' : ''),
|
|
3480
|
+
enable: f.enableHow,
|
|
3481
|
+
docs: f.docsHref,
|
|
3482
|
+
dismissKey: f.key,
|
|
3483
|
+
});
|
|
3484
|
+
});
|
|
3485
|
+
if (cfg.provider === 'noop' && !dismissed['__provider_noop']) {
|
|
3486
|
+
banners.unshift({
|
|
3487
|
+
kind: 'warn',
|
|
3488
|
+
icon: '🔒',
|
|
3489
|
+
title: 'No LLM provider key set',
|
|
3490
|
+
keyLabel: 'ANTHROPIC_API_KEY',
|
|
3491
|
+
desc: 'Compression, summarization, and graph extraction stay disabled until a key is provided.',
|
|
3492
|
+
enable: 'export ANTHROPIC_API_KEY=sk-ant-...\n# then restart: npx @agentcache/agentcache',
|
|
3493
|
+
docs: 'https://github.com/rohitg00/agentmemory#quick-start',
|
|
3494
|
+
dismissKey: '__provider_noop',
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
if (cfg.embeddingProvider === 'none' && !dismissed['__embedding_none']) {
|
|
3498
|
+
banners.push({
|
|
3499
|
+
kind: 'info',
|
|
3500
|
+
icon: '⚙',
|
|
3501
|
+
title: 'Running in BM25-only mode',
|
|
3502
|
+
keyLabel: 'OPENAI_API_KEY',
|
|
3503
|
+
desc: 'Semantic vector search is off. BM25 keyword search is active and good for exact matches.',
|
|
3504
|
+
enable: 'export OPENAI_API_KEY=sk-...\n# or VOYAGE_API_KEY, COHERE_API_KEY, OLLAMA_HOST',
|
|
3505
|
+
docs: 'https://github.com/rohitg00/agentmemory#embedding-providers',
|
|
3506
|
+
dismissKey: '__embedding_none',
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
if (banners.length === 0) { host.innerHTML = ''; return; }
|
|
3510
|
+
var warnCount = banners.filter(function(b) { return b.kind === 'warn'; }).length;
|
|
3511
|
+
var infoCount = banners.filter(function(b) { return b.kind === 'info'; }).length;
|
|
3512
|
+
var expanded = host.getAttribute('data-expanded') === '1';
|
|
3513
|
+
var pills = '';
|
|
3514
|
+
if (warnCount) pills += '<span class="flag-pill">' + warnCount + ' off</span>';
|
|
3515
|
+
if (infoCount) pills += '<span class="flag-pill info">' + infoCount + ' note</span>';
|
|
3516
|
+
var escHtml = function(s) {
|
|
3517
|
+
return String(s).replace(/[<>&"]/g, function(c) {
|
|
3518
|
+
return { '<': '<', '>': '>', '&': '&', '"': '"' }[c];
|
|
3519
|
+
});
|
|
3520
|
+
};
|
|
3521
|
+
var listHtml = banners.map(function(b) {
|
|
3522
|
+
return '<div class="flag-banner ' + b.kind + '" data-flag="' + escHtml(b.dismissKey) + '">' +
|
|
3523
|
+
'<span class="flag-icon">' + b.icon + '</span>' +
|
|
3524
|
+
'<div class="flag-body">' +
|
|
3525
|
+
'<div class="flag-title">' + escHtml(b.title) + ' <code>' + escHtml(b.keyLabel) + '</code></div>' +
|
|
3526
|
+
'<div class="flag-desc">' + escHtml(b.desc) + '</div>' +
|
|
3527
|
+
'<code class="flag-enable">' + escHtml(b.enable) + '</code>' +
|
|
3528
|
+
(b.docs ? ' <a class="empty-link" href="' + escHtml(b.docs) + '" target="_blank" rel="noopener">Learn more →</a>' : '') +
|
|
3529
|
+
'</div>' +
|
|
3530
|
+
'<button type="button" class="flag-close" data-dismiss-flag="' + escHtml(b.dismissKey) + '" aria-label="Dismiss">×</button>' +
|
|
3531
|
+
'</div>';
|
|
3532
|
+
}).join('');
|
|
3533
|
+
host.innerHTML = '<button type="button" class="flag-summary" data-action="toggle-flags" aria-expanded="' + (expanded ? 'true' : 'false') + '" aria-controls="flag-list">' +
|
|
3534
|
+
pills +
|
|
3535
|
+
'<span class="flag-count">Feature flags</span>' +
|
|
3536
|
+
'<span style="color:var(--ink-faint);">— click to ' + (expanded ? 'collapse' : 'expand') + '</span>' +
|
|
3537
|
+
'<span class="flag-toggle" aria-hidden="true">' + (expanded ? '▲' : '▼') + '</span>' +
|
|
3538
|
+
'</button>' +
|
|
3539
|
+
'<div class="flag-list' + (expanded ? ' open' : '') + '" id="flag-list">' + listHtml + '</div>';
|
|
3540
|
+
}
|
|
3541
|
+
async function fetchFlags() {
|
|
3542
|
+
var res = await apiGet('config/flags');
|
|
3543
|
+
if (!res) return;
|
|
3544
|
+
state.flagsConfig = res;
|
|
3545
|
+
renderFlagBanners(res);
|
|
3546
|
+
updateFooter(res);
|
|
3547
|
+
}
|
|
3548
|
+
function updateFooter(cfg) {
|
|
3549
|
+
var vEl = document.getElementById('footer-version');
|
|
3550
|
+
if (vEl) vEl.textContent = 'v' + (cfg.version || '?');
|
|
3551
|
+
var fbEl = document.getElementById('footer-feedback');
|
|
3552
|
+
if (fbEl) {
|
|
3553
|
+
var flagSummary = (cfg.flags || []).map(function(f) { return f.key + '=' + (f.enabled ? 'on' : 'off'); }).join(', ');
|
|
3554
|
+
var body = encodeURIComponent(
|
|
3555
|
+
'**Version:** ' + (cfg.version || '?') + '\n' +
|
|
3556
|
+
'**Provider:** ' + (cfg.provider || '?') + '\n' +
|
|
3557
|
+
'**Embedding:** ' + (cfg.embeddingProvider || '?') + '\n' +
|
|
3558
|
+
'**Flags:** ' + flagSummary + '\n' +
|
|
3559
|
+
'**User agent:** ' + navigator.userAgent + '\n\n' +
|
|
3560
|
+
'### What went wrong\n\n' +
|
|
3561
|
+
'(describe the issue)\n\n' +
|
|
3562
|
+
'### Steps to reproduce\n\n' +
|
|
3563
|
+
'1. \n2. \n3. \n'
|
|
3564
|
+
);
|
|
3565
|
+
fbEl.href = 'https://github.com/rohitg00/agentcache/issues/new?title=' +
|
|
3566
|
+
encodeURIComponent('[viewer] ') + '&body=' + body;
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
document.addEventListener('click', function(e) {
|
|
3570
|
+
if (!(e.target instanceof Element)) return;
|
|
3571
|
+
var btn = e.target.closest('[data-dismiss-flag]');
|
|
3572
|
+
if (btn) {
|
|
3573
|
+
e.preventDefault();
|
|
3574
|
+
e.stopPropagation();
|
|
3575
|
+
var key = btn.getAttribute('data-dismiss-flag');
|
|
3576
|
+
dismissFlags([key]);
|
|
3577
|
+
if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
var toggle = e.target.closest('[data-action="toggle-flags"]');
|
|
3581
|
+
if (toggle) {
|
|
3582
|
+
var host = document.getElementById('flag-banners');
|
|
3583
|
+
var cur = host.getAttribute('data-expanded') === '1';
|
|
3584
|
+
host.setAttribute('data-expanded', cur ? '0' : '1');
|
|
3585
|
+
if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
|
|
3586
|
+
}
|
|
3587
|
+
});
|
|
3588
|
+
fetchFlags();
|
|
3589
|
+
document.addEventListener('click', function(e) {
|
|
3590
|
+
if (!(e.target instanceof Element)) return;
|
|
3591
|
+
var target = e.target.closest('[data-action]');
|
|
3592
|
+
if (!target) return;
|
|
3593
|
+
var action = target.getAttribute('data-action');
|
|
3594
|
+
if (!action) return;
|
|
3595
|
+
|
|
3596
|
+
if (action === 'toggle-theme') {
|
|
3597
|
+
toggleTheme();
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
if (action === 'refresh-dashboard') {
|
|
3601
|
+
refreshDashboard();
|
|
3602
|
+
return;
|
|
3603
|
+
}
|
|
3604
|
+
if (action === 'zoom-graph') {
|
|
3605
|
+
zoomGraph(parseInt(target.getAttribute('data-dir') || '0', 10));
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
if (action === 'recenter-graph') {
|
|
3609
|
+
recenterGraph();
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3612
|
+
if (action === 'rebuild-graph') {
|
|
3613
|
+
rebuildGraph();
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3616
|
+
if (action === 'expand-node') {
|
|
3617
|
+
var nodeId = target.getAttribute('data-node-id');
|
|
3618
|
+
if (nodeId) expandNode(nodeId);
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3621
|
+
if (action === 'delete-memory') {
|
|
3622
|
+
deleteMemory(
|
|
3623
|
+
target.getAttribute('data-memory-id') || '',
|
|
3624
|
+
target.getAttribute('data-memory-title') || '',
|
|
3625
|
+
);
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
if (action === 'close-modal') {
|
|
3629
|
+
closeModal();
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
if (action === 'confirm-delete-memory') {
|
|
3633
|
+
var memoryId = target.getAttribute('data-memory-id');
|
|
3634
|
+
if (memoryId) confirmDeleteMemory(memoryId);
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3637
|
+
if (action === 'back-to-folders') {
|
|
3638
|
+
loadFolders();
|
|
3639
|
+
return;
|
|
3640
|
+
}
|
|
3641
|
+
if (action === 'delete-folder') {
|
|
3642
|
+
var fp = target.getAttribute('data-folder-path');
|
|
3643
|
+
var aid = target.getAttribute('data-agent-id');
|
|
3644
|
+
if (fp && aid) confirmDeleteFolder(fp, aid);
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
if (action === 'confirm-delete-folder') {
|
|
3648
|
+
var fp = target.getAttribute('data-folder-path');
|
|
3649
|
+
var aid = target.getAttribute('data-agent-id');
|
|
3650
|
+
if (fp && aid) execDeleteFolder(fp, aid);
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
if (action === 'delete-obs') {
|
|
3654
|
+
var obsId = target.getAttribute('data-obs-id');
|
|
3655
|
+
var fp = target.getAttribute('data-folder-path');
|
|
3656
|
+
var aid = target.getAttribute('data-agent-id');
|
|
3657
|
+
if (obsId && fp && aid) confirmDeleteObs(obsId, fp, aid);
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
if (action === 'confirm-delete-obs') {
|
|
3661
|
+
var obsId = target.getAttribute('data-obs-id');
|
|
3662
|
+
var fp = target.getAttribute('data-folder-path');
|
|
3663
|
+
var aid = target.getAttribute('data-agent-id');
|
|
3664
|
+
if (obsId && fp && aid) execDeleteObs(obsId, fp, aid);
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
if (action === 'delete-obs-selected') {
|
|
3668
|
+
if (_selectedObsIds.size > 0) execDeleteObsSelected();
|
|
3669
|
+
return;
|
|
3670
|
+
}
|
|
3671
|
+
if (action === 'run-dedup') {
|
|
3672
|
+
var fp = target.getAttribute('data-folder-path');
|
|
3673
|
+
var aid = target.getAttribute('data-agent-id');
|
|
3674
|
+
if (!fp || !aid) return;
|
|
3675
|
+
target.textContent = '⏳ Running…';
|
|
3676
|
+
target.disabled = true;
|
|
3677
|
+
apiPost('folder/dedup', { folderPath: fp, agentId: aid }).then(function(res) {
|
|
3678
|
+
var removed = res && res.deduplicated != null ? res.deduplicated : '?';
|
|
3679
|
+
target.textContent = '✓ Removed ' + removed + ' dupes';
|
|
3680
|
+
setTimeout(function() { loadFolderDetail(fp, aid); }, 1200);
|
|
3681
|
+
}).catch(function() {
|
|
3682
|
+
target.textContent = '✗ Failed';
|
|
3683
|
+
target.disabled = false;
|
|
3684
|
+
});
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
if (action === 'save-viewer-token') {
|
|
3688
|
+
var tokenInput = document.getElementById('viewer-auth-token');
|
|
3689
|
+
var token = tokenInput ? tokenInput.value.trim() : '';
|
|
3690
|
+
if (token) {
|
|
3691
|
+
setViewerToken(token);
|
|
3692
|
+
hideViewerAuthPrompt();
|
|
3693
|
+
fetchFlags();
|
|
3694
|
+
if (state[state.activeTab] && typeof state[state.activeTab] === 'object') {
|
|
3695
|
+
state[state.activeTab].loaded = false;
|
|
3696
|
+
}
|
|
3697
|
+
loadTab(state.activeTab);
|
|
3698
|
+
}
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
if (action === 'timeline-filter') {
|
|
3702
|
+
setTlTypeFilter(target.getAttribute('data-type-filter') || '');
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
if (action === 'timeline-page') {
|
|
3706
|
+
var page = parseInt(target.getAttribute('data-page') || '', 10);
|
|
3707
|
+
if (!Number.isNaN(page)) tlPage(page);
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
if (action === 'select-session') {
|
|
3711
|
+
var sessionId = target.getAttribute('data-session-id');
|
|
3712
|
+
if (sessionId) selectSession(sessionId);
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
if (action === 'end-session') {
|
|
3716
|
+
var endSessionId = target.getAttribute('data-session-id');
|
|
3717
|
+
if (endSessionId) endSession(endSessionId);
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
3720
|
+
if (action === 'summarize-session') {
|
|
3721
|
+
var summarizeSessionId = target.getAttribute('data-session-id');
|
|
3722
|
+
if (summarizeSessionId) summarizeSession(summarizeSessionId, target);
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
if (action === 'toggle-audit') {
|
|
3726
|
+
var auditIndex = parseInt(target.getAttribute('data-audit-index') || '', 10);
|
|
3727
|
+
if (!Number.isNaN(auditIndex)) toggleAuditDetail(auditIndex);
|
|
3728
|
+
}
|
|
3729
|
+
if (action === 'replay-select') {
|
|
3730
|
+
var rSid = target.getAttribute('data-session-id');
|
|
3731
|
+
if (rSid) selectReplaySession(rSid);
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
if (action === 'replay-toggle-play') { toggleReplayPlay(); return; }
|
|
3735
|
+
if (action === 'replay-step') {
|
|
3736
|
+
var d = parseInt(target.getAttribute('data-dir') || '1', 10);
|
|
3737
|
+
stepReplay(d);
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
if (action === 'replay-speed') {
|
|
3741
|
+
var sp = parseFloat(target.getAttribute('data-speed') || '1');
|
|
3742
|
+
setReplaySpeed(sp);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
if (action === 'replay-reset') { resetReplay(); return; }
|
|
3746
|
+
if (action === 'replay-import') { runReplayImport(); return; }
|
|
3747
|
+
if (action === 'replay-refresh') { refreshReplaySessions(); return; }
|
|
3748
|
+
});
|
|
3749
|
+
document.getElementById('modal-overlay').addEventListener('click', function(e) {
|
|
3750
|
+
if (e.target === this) closeModal();
|
|
3751
|
+
});
|
|
3752
|
+
|
|
3753
|
+
async function loadReplay() { return; }
|
|
3754
|
+
|
|
3755
|
+
async function refreshReplaySessions() { return; }
|
|
3756
|
+
|
|
3757
|
+
function renderReplay() { return; }
|
|
3758
|
+
|
|
3759
|
+
function renderReplayDetail(ev) { return; }
|
|
3760
|
+
|
|
3761
|
+
async function selectReplaySession(sessionId) { return; }
|
|
3762
|
+
|
|
3763
|
+
function toggleReplayPlay() { return; }
|
|
3764
|
+
|
|
3765
|
+
function startReplayTimer() { return; }
|
|
3766
|
+
|
|
3767
|
+
function stopReplayTimer() { return; }
|
|
3768
|
+
|
|
3769
|
+
function stepReplay(dir) { return; }
|
|
3770
|
+
|
|
3771
|
+
function setReplaySpeed(sp) { return; }
|
|
3772
|
+
|
|
3773
|
+
function resetReplay() { return; }
|
|
3774
|
+
|
|
3775
|
+
async function runReplayImport() { return; }
|
|
3776
|
+
|
|
3777
|
+
document.addEventListener('keydown', function(e) {
|
|
3778
|
+
// Replay keyboard shortcuts removed — replay tab no longer exists
|
|
3779
|
+
if (false) { return; }
|
|
3780
|
+
});
|
|
3781
|
+
|
|
3782
|
+
switchTab(tabFromRoute(), { replaceRoute: true });
|
|
3783
|
+
connectWs();
|
|
3784
|
+
|
|
3785
|
+
// ==========================================================================
|
|
3786
|
+
// B4.3 — copyToClipboard utility
|
|
3787
|
+
// ==========================================================================
|
|
3788
|
+
function copyToClipboard(text, btn) {
|
|
3789
|
+
function showCopied() {
|
|
3790
|
+
if (!btn) return;
|
|
3791
|
+
var orig = btn.textContent;
|
|
3792
|
+
btn.textContent = 'Copied!';
|
|
3793
|
+
btn.style.color = 'var(--green)';
|
|
3794
|
+
setTimeout(function() { btn.textContent = orig; btn.style.color = ''; }, 1500);
|
|
3795
|
+
}
|
|
3796
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
3797
|
+
navigator.clipboard.writeText(text).then(showCopied).catch(fallback);
|
|
3798
|
+
} else { fallback(); }
|
|
3799
|
+
function fallback() {
|
|
3800
|
+
var ta = document.createElement('textarea');
|
|
3801
|
+
ta.value = text;
|
|
3802
|
+
ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none;';
|
|
3803
|
+
document.body.appendChild(ta);
|
|
3804
|
+
ta.select();
|
|
3805
|
+
try { document.execCommand('copy'); showCopied(); } catch(_) {}
|
|
3806
|
+
document.body.removeChild(ta);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
// ==========================================================================
|
|
3811
|
+
// B4 -- Onboarding card (empty state detection)
|
|
3812
|
+
// ==========================================================================
|
|
3813
|
+
function maybeShowOnboarding(health, flags) {
|
|
3814
|
+
if (safeStorage.getItem('agentcache_onboarding_dismissed')) return;
|
|
3815
|
+
if ((health.observationCount || 0) > 0 || (health.memoryCount || 0) > 0) return;
|
|
3816
|
+
var el = document.getElementById('view-folders');
|
|
3817
|
+
if (!el) return;
|
|
3818
|
+
|
|
3819
|
+
var flagMap = {};
|
|
3820
|
+
((flags && flags.flags) || []).forEach(function(f) { flagMap[f.key] = f.enabled; });
|
|
3821
|
+
|
|
3822
|
+
function step(num, done, title, snippet) {
|
|
3823
|
+
var doneClass = done ? ' done' : '';
|
|
3824
|
+
return '<div class="onboarding-step">' +
|
|
3825
|
+
'<div class="step-header">' +
|
|
3826
|
+
'<div class="step-num' + doneClass + '">' + (done ? '\u2713' : num) + '</div>' +
|
|
3827
|
+
'<div class="step-title">' + title + '</div>' +
|
|
3828
|
+
'</div>' +
|
|
3829
|
+
'<div class="step-code-wrap">' +
|
|
3830
|
+
'<code class="step-code">' + esc(snippet) + '</code>' +
|
|
3831
|
+
'<button class="copy-btn" data-snippet="' + esc(snippet) + '">Copy</button>' +
|
|
3832
|
+
'</div></div>';
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
var secretSet = false; // AGENTCACHE_SECRET status not exposed by flags endpoint
|
|
3836
|
+
var geminiSet = !!(flags && flags.embeddingProvider && flags.embeddingProvider !== 'none');
|
|
3837
|
+
|
|
3838
|
+
var card = document.createElement('div');
|
|
3839
|
+
card.className = 'onboarding-card';
|
|
3840
|
+
card.innerHTML =
|
|
3841
|
+
'<h2>Welcome to agentcache</h2>' +
|
|
3842
|
+
'<p>No observations yet. Complete these steps to start recording agent cache.</p>' +
|
|
3843
|
+
step(1, secretSet, 'Set AGENTCACHE_SECRET (optional auth)',
|
|
3844
|
+
'echo "AGENTCACHE_SECRET=your-secret-here" >> ~/.agentcache/.env') +
|
|
3845
|
+
step(2, geminiSet, 'Add GEMINI_API_KEY for semantic search',
|
|
3846
|
+
'echo "GEMINI_API_KEY=your-key" >> ~/.agentcache/.env') +
|
|
3847
|
+
step(3, false, 'Point your agent at agentcache',
|
|
3848
|
+
'curl -X POST http://localhost:3111/agentcache/agent/observe \\\n' +
|
|
3849
|
+
' -H "Content-Type: application/json" \\\n' +
|
|
3850
|
+
' -d \'{"folderPath":"/your/project","agentId":"kiro","text":"Hello",' +
|
|
3851
|
+
'"timestamp":"' + new Date().toISOString() + '"}\'') +
|
|
3852
|
+
'<button class="onboarding-dismiss">Got it</button>';
|
|
3853
|
+
|
|
3854
|
+
// Wire copy buttons
|
|
3855
|
+
card.querySelectorAll('.copy-btn').forEach(function(btn) {
|
|
3856
|
+
btn.addEventListener('click', function() {
|
|
3857
|
+
copyToClipboard(btn.dataset.snippet, btn);
|
|
3858
|
+
});
|
|
3859
|
+
});
|
|
3860
|
+
card.querySelector('.onboarding-dismiss').addEventListener('click', function() {
|
|
3861
|
+
safeStorage.setItem('agentcache_onboarding_dismissed', '1');
|
|
3862
|
+
card.remove();
|
|
3863
|
+
});
|
|
3864
|
+
|
|
3865
|
+
el.insertAdjacentElement('afterbegin', card);
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
// Hook into existing folder load to check empty state (B4 onboarding)
|
|
3869
|
+
// Uses a closure over the original loadFolders instead of hoisted re-declaration
|
|
3870
|
+
(function() {
|
|
3871
|
+
var _origLoadFolders = loadFolders;
|
|
3872
|
+
loadFolders = async function() {
|
|
3873
|
+
await _origLoadFolders.apply(this, arguments);
|
|
3874
|
+
// After load, check health for onboarding card
|
|
3875
|
+
try {
|
|
3876
|
+
var h = await apiFetchRaw('/agentcache/health');
|
|
3877
|
+
var f = state.flagsConfig;
|
|
3878
|
+
maybeShowOnboarding(h, f);
|
|
3879
|
+
} catch(_) {}
|
|
3880
|
+
};
|
|
3881
|
+
})();
|
|
3882
|
+
|
|
3883
|
+
// ==========================================================================
|
|
3884
|
+
// B5 — MCP Tool Tester tab
|
|
3885
|
+
// ==========================================================================
|
|
3886
|
+
var _toolHistory = [];
|
|
3887
|
+
var _selectedTool = null;
|
|
3888
|
+
|
|
3889
|
+
async function loadTools() {
|
|
3890
|
+
var el = document.getElementById('view-tools');
|
|
3891
|
+
if (!el) return;
|
|
3892
|
+
// Skeleton while loading
|
|
3893
|
+
el.innerHTML = '<div style="padding:24px;max-width:900px;">' +
|
|
3894
|
+
'<div class="skeleton skeleton-card"></div>' +
|
|
3895
|
+
'<div class="skeleton skeleton-card"></div>' +
|
|
3896
|
+
'<div class="skeleton skeleton-card"></div>' +
|
|
3897
|
+
'</div>';
|
|
3898
|
+
try {
|
|
3899
|
+
var data = await apiFetch('/agentcache/mcp/tools');
|
|
3900
|
+
var tools = data.tools || [];
|
|
3901
|
+
renderToolsTab(el, tools);
|
|
3902
|
+
} catch(err) {
|
|
3903
|
+
el.innerHTML = '<div style="padding:24px;color:var(--accent);">Error loading tools: ' + esc(String(err)) + '</div>';
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
function renderToolsTab(el, tools) {
|
|
3908
|
+
var html = '<div style="padding:0 0 24px;">';
|
|
3909
|
+
html += '<h2 style="font-family:var(--font-display);font-size:22px;margin-bottom:16px;color:var(--ink);">MCP Tool Tester</h2>';
|
|
3910
|
+
html += '<div class="tools-list" id="tools-list">';
|
|
3911
|
+
tools.forEach(function(t) {
|
|
3912
|
+
html += '<div class="tool-row" data-name="' + esc(t.name) + '">' +
|
|
3913
|
+
'<span class="tool-name">' + esc(t.name) + '</span>' +
|
|
3914
|
+
'<span class="tool-desc">' + esc(t.description || '') + '</span>' +
|
|
3915
|
+
'</div>';
|
|
3916
|
+
});
|
|
3917
|
+
html += '</div>';
|
|
3918
|
+
html += '<div id="tool-panel" style="display:none;">' +
|
|
3919
|
+
'<h3 id="tool-panel-title"></h3>' +
|
|
3920
|
+
'<textarea id="tool-args-editor" spellcheck="false">{}</textarea>' +
|
|
3921
|
+
'<div><button id="tool-run-btn">Run ▶</button></div>' +
|
|
3922
|
+
'<div id="tool-response" style="display:none;"></div>' +
|
|
3923
|
+
'<div class="tool-history" id="tool-history-wrap"></div>' +
|
|
3924
|
+
'</div>';
|
|
3925
|
+
html += '</div>';
|
|
3926
|
+
el.innerHTML = html;
|
|
3927
|
+
|
|
3928
|
+
var toolMap = {};
|
|
3929
|
+
tools.forEach(function(t) { toolMap[t.name] = t; });
|
|
3930
|
+
|
|
3931
|
+
el.querySelectorAll('.tool-row').forEach(function(row) {
|
|
3932
|
+
row.addEventListener('click', function() {
|
|
3933
|
+
el.querySelectorAll('.tool-row').forEach(function(r) { r.classList.remove('selected'); });
|
|
3934
|
+
row.classList.add('selected');
|
|
3935
|
+
var name = row.dataset.name;
|
|
3936
|
+
_selectedTool = name;
|
|
3937
|
+
var tool = toolMap[name];
|
|
3938
|
+
var panel = document.getElementById('tool-panel');
|
|
3939
|
+
var title = document.getElementById('tool-panel-title');
|
|
3940
|
+
var editor = document.getElementById('tool-args-editor');
|
|
3941
|
+
panel.style.display = '';
|
|
3942
|
+
title.textContent = name;
|
|
3943
|
+
// Pre-fill required keys
|
|
3944
|
+
var args = {};
|
|
3945
|
+
if (tool.inputSchema && tool.inputSchema.properties) {
|
|
3946
|
+
Object.keys(tool.inputSchema.properties).forEach(function(k) {
|
|
3947
|
+
args[k] = '';
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
editor.value = JSON.stringify(args, null, 2);
|
|
3951
|
+
document.getElementById('tool-response').style.display = 'none';
|
|
3952
|
+
renderToolHistory();
|
|
3953
|
+
});
|
|
3954
|
+
});
|
|
3955
|
+
|
|
3956
|
+
document.getElementById('tool-run-btn').addEventListener('click', async function() {
|
|
3957
|
+
var btn = this;
|
|
3958
|
+
var name = _selectedTool;
|
|
3959
|
+
if (!name) return;
|
|
3960
|
+
var argsText = document.getElementById('tool-args-editor').value;
|
|
3961
|
+
var respEl = document.getElementById('tool-response');
|
|
3962
|
+
var args;
|
|
3963
|
+
try { args = JSON.parse(argsText); } catch(e) {
|
|
3964
|
+
respEl.textContent = 'Invalid JSON: ' + e.message;
|
|
3965
|
+
respEl.className = 'error';
|
|
3966
|
+
respEl.style.display = '';
|
|
3967
|
+
return;
|
|
3968
|
+
}
|
|
3969
|
+
btn.textContent = 'Running...';
|
|
3970
|
+
btn.disabled = true;
|
|
3971
|
+
try {
|
|
3972
|
+
// Use raw fetch so we can call .text() on the Response — apiFetchRaw
|
|
3973
|
+
// already parses JSON and returns a plain object, not a Response.
|
|
3974
|
+
var _toolToken = getViewerToken();
|
|
3975
|
+
var _toolHeaders = { 'Content-Type': 'application/json' };
|
|
3976
|
+
if (_toolToken) _toolHeaders['Authorization'] = 'Bearer ' + _toolToken;
|
|
3977
|
+
var rawRes = await fetch(REST + '/agentcache/mcp/tools', {
|
|
3978
|
+
method: 'POST',
|
|
3979
|
+
headers: _toolHeaders,
|
|
3980
|
+
body: JSON.stringify({ name: name, arguments: args })
|
|
3981
|
+
});
|
|
3982
|
+
var text = await rawRes.text();
|
|
3983
|
+
var ok = rawRes.ok;
|
|
3984
|
+
respEl.textContent = text;
|
|
3985
|
+
respEl.className = ok ? '' : 'error';
|
|
3986
|
+
respEl.style.display = '';
|
|
3987
|
+
// Add to history
|
|
3988
|
+
_toolHistory.unshift({ name: name, ts: new Date().toISOString(), ok: ok, args: args, response: text });
|
|
3989
|
+
if (_toolHistory.length > 10) _toolHistory.length = 10;
|
|
3990
|
+
renderToolHistory();
|
|
3991
|
+
} catch(err) {
|
|
3992
|
+
respEl.textContent = String(err);
|
|
3993
|
+
respEl.className = 'error';
|
|
3994
|
+
respEl.style.display = '';
|
|
3995
|
+
} finally {
|
|
3996
|
+
btn.textContent = 'Run \u25b6';
|
|
3997
|
+
btn.disabled = false;
|
|
3998
|
+
}
|
|
3999
|
+
});
|
|
4000
|
+
|
|
4001
|
+
renderToolHistory();
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
function renderToolHistory() {
|
|
4005
|
+
var wrap = document.getElementById('tool-history-wrap');
|
|
4006
|
+
if (!wrap || _toolHistory.length === 0) return;
|
|
4007
|
+
var html = '<details class="tool-history"><summary>History (' + _toolHistory.length + ')</summary>';
|
|
4008
|
+
_toolHistory.forEach(function(h) {
|
|
4009
|
+
var badge = h.ok
|
|
4010
|
+
? '<span class="badge-ok">✓ ok</span>'
|
|
4011
|
+
: '<span class="badge-err">✗ err</span>';
|
|
4012
|
+
html += '<details class="tool-history-entry">' +
|
|
4013
|
+
'<summary>' + badge + ' <span style="font-weight:600;">' + esc(h.name) + '</span>' +
|
|
4014
|
+
' <span style="color:var(--ink-faint);font-size:10px;">' + h.ts.slice(11,19) + '</span></summary>' +
|
|
4015
|
+
'<div style="padding:8px 12px;font-family:var(--font-mono);font-size:11px;white-space:pre-wrap;max-height:150px;overflow:auto;">' +
|
|
4016
|
+
esc(h.response) + '</div>' +
|
|
4017
|
+
'</details>';
|
|
4018
|
+
});
|
|
4019
|
+
html += '</details>';
|
|
4020
|
+
wrap.innerHTML = html;
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
// ==========================================================================
|
|
4024
|
+
// B1 — Command Palette (Ctrl+K / Cmd+K)
|
|
4025
|
+
// ==========================================================================
|
|
4026
|
+
var _cmdOpen = false;
|
|
4027
|
+
var _cmdSelectedIdx = -1;
|
|
4028
|
+
|
|
4029
|
+
function openCommandPalette() {
|
|
4030
|
+
var palette = document.getElementById('cmd-palette');
|
|
4031
|
+
if (!palette) return;
|
|
4032
|
+
_cmdOpen = true;
|
|
4033
|
+
_cmdSelectedIdx = -1;
|
|
4034
|
+
palette.hidden = false;
|
|
4035
|
+
var input = document.getElementById('cmd-input');
|
|
4036
|
+
if (input) { input.value = ''; input.focus(); }
|
|
4037
|
+
renderCmdResults('');
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
function closeCommandPalette() {
|
|
4041
|
+
var palette = document.getElementById('cmd-palette');
|
|
4042
|
+
if (!palette) return;
|
|
4043
|
+
_cmdOpen = false;
|
|
4044
|
+
palette.hidden = true;
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
function renderCmdResults(query) {
|
|
4048
|
+
var list = document.getElementById('cmd-results');
|
|
4049
|
+
if (!list) return;
|
|
4050
|
+
var q = query.toLowerCase().trim();
|
|
4051
|
+
var items = [];
|
|
4052
|
+
|
|
4053
|
+
// Tab names
|
|
4054
|
+
['Folders','Memories','Graph','Timeline','Tools'].forEach(function(name) {
|
|
4055
|
+
if (!q || name.toLowerCase().includes(q)) {
|
|
4056
|
+
items.push({ type: 'Tab', label: name, sub: '', action: function(n) { return function() { closeCommandPalette(); switchTab(n.toLowerCase()); }; }(name) });
|
|
4057
|
+
}
|
|
4058
|
+
});
|
|
4059
|
+
|
|
4060
|
+
// Folders from cache
|
|
4061
|
+
((window._cachedFolders) || []).forEach(function(f) {
|
|
4062
|
+
var fp = f.folderPath || '';
|
|
4063
|
+
var aid = f.agentId || '';
|
|
4064
|
+
if (!q || fp.toLowerCase().includes(q) || aid.toLowerCase().includes(q)) {
|
|
4065
|
+
items.push({ type: 'Folder', label: fp, sub: aid, action: function(fp2, aid2) {
|
|
4066
|
+
return function() { closeCommandPalette(); switchTab('folders'); };
|
|
4067
|
+
}(fp, aid) });
|
|
4068
|
+
}
|
|
4069
|
+
});
|
|
4070
|
+
|
|
4071
|
+
// Memories from cache
|
|
4072
|
+
((window._cachedMemories) || []).forEach(function(m) {
|
|
4073
|
+
var content = (m.content || '').slice(0, 80);
|
|
4074
|
+
if (!q || content.toLowerCase().includes(q)) {
|
|
4075
|
+
items.push({ type: 'Memory', label: content, sub: m.type || '', action: function() {
|
|
4076
|
+
closeCommandPalette(); switchTab('memories');
|
|
4077
|
+
} });
|
|
4078
|
+
}
|
|
4079
|
+
});
|
|
4080
|
+
|
|
4081
|
+
items = items.slice(0, 30);
|
|
4082
|
+
_cmdSelectedIdx = items.length > 0 ? 0 : -1;
|
|
4083
|
+
|
|
4084
|
+
list.innerHTML = items.map(function(item, i) {
|
|
4085
|
+
return '<li role="option" aria-selected="' + (i === 0 ? 'true' : 'false') + '" data-idx="' + i + '">' +
|
|
4086
|
+
'<span class="cmd-badge">' + esc(item.type) + '</span>' +
|
|
4087
|
+
'<span class="cmd-main">' + esc(item.label) + '</span>' +
|
|
4088
|
+
(item.sub ? '<span class="cmd-sub">' + esc(item.sub) + '</span>' : '') +
|
|
4089
|
+
'</li>';
|
|
4090
|
+
}).join('');
|
|
4091
|
+
|
|
4092
|
+
// Store actions
|
|
4093
|
+
list._cmdItems = items;
|
|
4094
|
+
|
|
4095
|
+
list.querySelectorAll('li').forEach(function(li) {
|
|
4096
|
+
li.addEventListener('click', function() {
|
|
4097
|
+
var idx = parseInt(li.dataset.idx, 10);
|
|
4098
|
+
if (list._cmdItems && list._cmdItems[idx]) {
|
|
4099
|
+
list._cmdItems[idx].action();
|
|
4100
|
+
}
|
|
4101
|
+
});
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
// Global keydown handler for command palette
|
|
4106
|
+
document.addEventListener('keydown', function(e) {
|
|
4107
|
+
// Ctrl+K or Cmd+K -- open palette
|
|
4108
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
4109
|
+
e.preventDefault();
|
|
4110
|
+
if (_cmdOpen) { closeCommandPalette(); } else { openCommandPalette(); }
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
if (!_cmdOpen) return;
|
|
4114
|
+
|
|
4115
|
+
var list = document.getElementById('cmd-results');
|
|
4116
|
+
var items = (list && list._cmdItems) || [];
|
|
4117
|
+
|
|
4118
|
+
if (e.key === 'Escape') {
|
|
4119
|
+
e.preventDefault();
|
|
4120
|
+
closeCommandPalette();
|
|
4121
|
+
} else if (e.key === 'ArrowDown') {
|
|
4122
|
+
e.preventDefault();
|
|
4123
|
+
_cmdSelectedIdx = Math.min(_cmdSelectedIdx + 1, items.length - 1);
|
|
4124
|
+
updateCmdSelection();
|
|
4125
|
+
} else if (e.key === 'ArrowUp') {
|
|
4126
|
+
e.preventDefault();
|
|
4127
|
+
_cmdSelectedIdx = Math.max(_cmdSelectedIdx - 1, 0);
|
|
4128
|
+
updateCmdSelection();
|
|
4129
|
+
} else if (e.key === 'Enter') {
|
|
4130
|
+
e.preventDefault();
|
|
4131
|
+
if (_cmdSelectedIdx >= 0 && items[_cmdSelectedIdx]) {
|
|
4132
|
+
items[_cmdSelectedIdx].action();
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
});
|
|
4136
|
+
|
|
4137
|
+
function updateCmdSelection() {
|
|
4138
|
+
var list = document.getElementById('cmd-results');
|
|
4139
|
+
if (!list) return;
|
|
4140
|
+
list.querySelectorAll('li').forEach(function(li, i) {
|
|
4141
|
+
li.setAttribute('aria-selected', i === _cmdSelectedIdx ? 'true' : 'false');
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
// Wire palette input
|
|
4146
|
+
var cmdInput = document.getElementById('cmd-input');
|
|
4147
|
+
if (cmdInput) {
|
|
4148
|
+
cmdInput.addEventListener('input', function() {
|
|
4149
|
+
renderCmdResults(this.value);
|
|
4150
|
+
});
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
// Close on backdrop click
|
|
4154
|
+
var cmdBackdrop = document.getElementById('cmd-backdrop');
|
|
4155
|
+
if (cmdBackdrop) {
|
|
4156
|
+
cmdBackdrop.addEventListener('click', closeCommandPalette);
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
// ==========================================================================
|
|
4160
|
+
// B6.2 — Tab keyboard navigation (arrow keys)
|
|
4161
|
+
// ==========================================================================
|
|
4162
|
+
var tabBar = document.getElementById('tab-bar');
|
|
4163
|
+
if (tabBar) {
|
|
4164
|
+
tabBar.addEventListener('keydown', function(e) {
|
|
4165
|
+
var tabs = Array.from(tabBar.querySelectorAll('button[role="tab"]'));
|
|
4166
|
+
var idx = tabs.indexOf(document.activeElement);
|
|
4167
|
+
if (idx === -1) return;
|
|
4168
|
+
if (e.key === 'ArrowRight') {
|
|
4169
|
+
e.preventDefault();
|
|
4170
|
+
tabs[(idx + 1) % tabs.length].focus();
|
|
4171
|
+
} else if (e.key === 'ArrowLeft') {
|
|
4172
|
+
e.preventDefault();
|
|
4173
|
+
tabs[(idx - 1 + tabs.length) % tabs.length].focus();
|
|
4174
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
4175
|
+
e.preventDefault();
|
|
4176
|
+
tabs[idx].click();
|
|
4177
|
+
}
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
// ==========================================================================
|
|
4182
|
+
// B3 — VirtualList helper
|
|
4183
|
+
// ==========================================================================
|
|
4184
|
+
function VirtualList(opts) {
|
|
4185
|
+
this.container = opts.container;
|
|
4186
|
+
this.renderItem = opts.renderItem;
|
|
4187
|
+
this.loadMore = opts.loadMore || null;
|
|
4188
|
+
this.items = [];
|
|
4189
|
+
this._renderedCount = 0;
|
|
4190
|
+
|
|
4191
|
+
this.sentinel = document.createElement('div');
|
|
4192
|
+
this.sentinel.style.height = '1px';
|
|
4193
|
+
this.container.appendChild(this.sentinel);
|
|
4194
|
+
|
|
4195
|
+
var self = this;
|
|
4196
|
+
this._observer = new IntersectionObserver(function(entries) {
|
|
4197
|
+
if (entries[0].isIntersecting && self.loadMore) { self.loadMore(); }
|
|
4198
|
+
}, { root: null, threshold: 0.1 });
|
|
4199
|
+
this._observer.observe(this.sentinel);
|
|
4200
|
+
}
|
|
4201
|
+
VirtualList.prototype.setItems = function(items) {
|
|
4202
|
+
this.items = items;
|
|
4203
|
+
this._renderedCount = 0;
|
|
4204
|
+
// Clear all children except sentinel
|
|
4205
|
+
while (this.container.firstChild && this.container.firstChild !== this.sentinel) {
|
|
4206
|
+
this.container.removeChild(this.container.firstChild);
|
|
4207
|
+
}
|
|
4208
|
+
this.render();
|
|
4209
|
+
};
|
|
4210
|
+
VirtualList.prototype.addItems = function(newItems) {
|
|
4211
|
+
this.items = this.items.concat(newItems);
|
|
4212
|
+
this.render();
|
|
4213
|
+
};
|
|
4214
|
+
VirtualList.prototype.render = function() {
|
|
4215
|
+
var frag = document.createDocumentFragment();
|
|
4216
|
+
for (var i = this._renderedCount; i < this.items.length; i++) {
|
|
4217
|
+
frag.appendChild(this.renderItem(this.items[i]));
|
|
4218
|
+
}
|
|
4219
|
+
this._renderedCount = this.items.length;
|
|
4220
|
+
this.container.insertBefore(frag, this.sentinel);
|
|
4221
|
+
};
|
|
4222
|
+
VirtualList.prototype.destroy = function() { this._observer.disconnect(); };
|
|
4223
|
+
|
|
4224
|
+
// Cache for command palette
|
|
4225
|
+
window._cachedFolders = window._cachedFolders || [];
|
|
4226
|
+
window._cachedMemories = window._cachedMemories || [];
|
|
4227
|
+
|
|
4228
|
+
// Hook into existing loadMemories to cache results for command palette
|
|
4229
|
+
var _origLoadMemories = typeof loadMemories === 'function' ? loadMemories : null;
|
|
4230
|
+
|
|
4231
|
+
</script>
|
|
4232
|
+
</body>
|
|
4233
|
+
</html>
|
|
4234
|
+
|
|
4235
|
+
|