total-recall-brain 3.1.2 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/extension/background.js +99 -0
  2. package/extension/content-script.css +16 -0
  3. package/extension/content-script.js +134 -0
  4. package/extension/icons/README.md +27 -0
  5. package/extension/icons/icon-128.png +0 -0
  6. package/extension/icons/icon-16.png +0 -0
  7. package/extension/icons/icon-48.png +0 -0
  8. package/extension/lib/brain-client.js +47 -0
  9. package/extension/manifest.json +39 -0
  10. package/extension/options/options.css +218 -0
  11. package/extension/options/options.html +72 -0
  12. package/extension/options/options.js +99 -0
  13. package/extension/popup/popup.css +221 -0
  14. package/extension/popup/popup.html +44 -0
  15. package/extension/popup/popup.js +107 -0
  16. package/extension/sidepanel/sidepanel.css +340 -0
  17. package/extension/sidepanel/sidepanel.html +38 -0
  18. package/extension/sidepanel/sidepanel.js +229 -0
  19. package/frontend/README.md +73 -0
  20. package/frontend/index.html +17 -0
  21. package/frontend/package.json +31 -0
  22. package/frontend/src/App.css +2 -0
  23. package/frontend/src/App.tsx +390 -0
  24. package/frontend/src/api.ts +688 -0
  25. package/frontend/src/assets/hero.png +0 -0
  26. package/frontend/src/assets/react.svg +1 -0
  27. package/frontend/src/assets/vite.svg +1 -0
  28. package/frontend/src/components/BrainSelector.tsx +272 -0
  29. package/frontend/src/components/DaemonLogsTab.tsx +54 -0
  30. package/frontend/src/components/Graph3D.tsx +850 -0
  31. package/frontend/src/components/MarkdownUtils.tsx +99 -0
  32. package/frontend/src/components/ResearchAgendaTab.tsx +707 -0
  33. package/frontend/src/components/TaskQueueTab.tsx +185 -0
  34. package/frontend/src/index.css +1589 -0
  35. package/frontend/src/main.tsx +10 -0
  36. package/frontend/src/pages/ApiKeysPage.tsx +288 -0
  37. package/frontend/src/pages/AutomationsPage.tsx +354 -0
  38. package/frontend/src/pages/ChatPage.tsx +831 -0
  39. package/frontend/src/pages/DeploymentsPage.tsx +275 -0
  40. package/frontend/src/pages/FilesPage.tsx +384 -0
  41. package/frontend/src/pages/HealthPage.tsx +91 -0
  42. package/frontend/src/pages/IntegrationsPage.tsx +382 -0
  43. package/frontend/src/pages/LoginPage.tsx +237 -0
  44. package/frontend/src/pages/MemoryPage.tsx +1192 -0
  45. package/frontend/src/pages/SandboxPage.tsx +113 -0
  46. package/frontend/src/pages/SettingsPage.tsx +662 -0
  47. package/frontend/src/pages/SkillsPage.tsx +618 -0
  48. package/frontend/src/pages/TasksPage.tsx +392 -0
  49. package/frontend/src/pages/UsagePage.tsx +260 -0
  50. package/frontend/src/types.ts +163 -0
  51. package/frontend/tsconfig.app.json +25 -0
  52. package/frontend/tsconfig.json +7 -0
  53. package/frontend/tsconfig.node.json +24 -0
  54. package/frontend/vite.config.ts +24 -0
  55. package/package.json +8 -1
  56. package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +35 -8
  57. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +13 -8
  58. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +28 -1
  59. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +28 -1
  60. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +830 -81
  61. package/src/cli/backup.mjs +172 -1
  62. package/src/cli/setup.mjs +24 -0
  63. package/src/server/rest.mjs +56 -7
@@ -0,0 +1,1192 @@
1
+ import { useState, useEffect, useCallback, useRef } from "react"
2
+ import { useSearchParams } from "react-router-dom"
3
+ import { listMemory, searchMemory, readMemory, fetchConflicts, resolveConflict, saveMemory, createMemory, deleteMemory, fetchSessions, deleteSession } from "../api"
4
+ import type { MemoryNode, Conflict } from "../types"
5
+ import type { SessionSummary } from "../api"
6
+
7
+ const CATEGORIES = [
8
+ "all", "invariants", "preferences", "anti-patterns", "patterns",
9
+ "decisions", "concepts", "facts", "lore"
10
+ ]
11
+
12
+ function renderMarkdown(md: string) {
13
+ if (!md) return "";
14
+ let html = md
15
+ .replace(/&/g, "&")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;");
18
+
19
+ // Headings
20
+ html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
21
+ html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
22
+ html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
23
+
24
+ // Bold & Italic
25
+ html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
26
+ html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
27
+
28
+ // Code blocks
29
+ html = html.replace(/```([\s\S]*?)```/g, "<pre style=\"background:rgba(0,0,0,0.3);padding:12px;border-radius:6px;border:1px solid rgba(255,255,255,0.06);font-family:monospace;overflow-x:auto;\"><code>$1</code></pre>");
30
+ html = html.replace(/`(.*?)`/g, "<code style=\"background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px;font-family:monospace;\">$1</code>");
31
+
32
+ // Blockquotes
33
+ html = html.replace(/^> (.*$)/gim, "<blockquote style=\"border-left:4px solid var(--purple-primary);padding-left:14px;color:var(--text-secondary);margin:16px 0;font-style:italic;\">$1</blockquote>");
34
+
35
+ // Lists
36
+ html = html.replace(/^- (.*$)/gim, "<li style=\"margin-left:20px;list-style-type:disc;\">$1</li>");
37
+
38
+ // Linebreaks
39
+ html = html.replace(/\n/g, "<br />");
40
+
41
+ return <div dangerouslySetInnerHTML={{ __html: html }} className="markdown-preview-block" />;
42
+ }
43
+
44
+ function markdownToHtml(md: string): string {
45
+ if (!md) return "";
46
+ let html = md
47
+ .replace(/&/g, "&amp;")
48
+ .replace(/</g, "&lt;")
49
+ .replace(/>/g, "&gt;");
50
+
51
+ // Headings
52
+ html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
53
+ html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
54
+ html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
55
+
56
+ // Bold & Italic
57
+ html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
58
+ html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
59
+
60
+ // Blockquotes
61
+ html = html.replace(/^> (.*$)/gim, "<blockquote>$1</blockquote>");
62
+
63
+ // Lists
64
+ html = html.replace(/^- (.*$)/gim, "<li>$1</li>");
65
+
66
+ // Newlines
67
+ html = html.replace(/\n/g, "<br />");
68
+
69
+ return html;
70
+ }
71
+
72
+ function htmlToMarkdown(html: string): string {
73
+ if (!html) return "";
74
+ let md = html;
75
+
76
+ // Replace <br> and <br /> with newline
77
+ md = md.replace(/<br\s*\/?>/gi, "\n");
78
+
79
+ // Headings
80
+ md = md.replace(/<h1>(.*?)<\/h1>/gi, "# $1\n");
81
+ md = md.replace(/<h2>(.*?)<\/h2>/gi, "## $1\n");
82
+ md = md.replace(/<h3>(.*?)<\/h3>/gi, "### $1\n");
83
+
84
+ // Bold & Italic
85
+ md = md.replace(/<strong>(.*?)<\/strong>/gi, "**$1**");
86
+ md = md.replace(/<b>(.*?)<\/b>/gi, "**$1**");
87
+ md = md.replace(/<em>(.*?)<\/em>/gi, "*$1*");
88
+ md = md.replace(/<i>(.*?)<\/i>/gi, "*$1*");
89
+
90
+ // Blockquotes
91
+ md = md.replace(/<blockquote>(.*?)<\/blockquote>/gi, "> $1\n");
92
+
93
+ // Lists
94
+ md = md.replace(/<li>(.*?)<\/li>/gi, "- $1\n");
95
+
96
+ // Clean up any remaining HTML tags safely
97
+ md = md.replace(/<[^>]+>/g, "");
98
+
99
+ // Decode HTML entities
100
+ md = md
101
+ .replace(/&amp;/g, "&")
102
+ .replace(/&lt;/g, "<")
103
+ .replace(/&gt;/g, ">");
104
+
105
+ return md.trim();
106
+ }
107
+
108
+ export default function MemoryPage({ activeBrainId }: { activeBrainId?: string }) {
109
+ const [searchParams, setSearchParams] = useSearchParams()
110
+ const urlSlug = searchParams.get("slug")
111
+
112
+ const [activeTab, setActiveTab] = useState<"vault" | "conflicts" | "sessions">("vault")
113
+ const [nodes, setNodes] = useState<MemoryNode[]>([])
114
+ const [selected, setSelected] = useState<MemoryNode | null>(null)
115
+ const [category, setCategory] = useState("all")
116
+ const [query, setQuery] = useState("")
117
+ const [loading, setLoading] = useState(false)
118
+ const [error, setError] = useState("")
119
+
120
+ // Sessions state
121
+ const [sessionsList, setSessionsList] = useState<SessionSummary[]>([])
122
+ const [sessionsTotal, setSessionsTotal] = useState(0)
123
+ const [sessionsLoading, setSessionsLoading] = useState(false)
124
+
125
+ // Conflicts state
126
+ const [conflicts, setConflicts] = useState<Conflict[]>([])
127
+ const [resolvingId, setResolvingId] = useState<string | null>(null)
128
+
129
+ // Rich inline editing states
130
+ const [isEditing, setIsEditing] = useState(false)
131
+ const [isCreating, setIsCreating] = useState(false)
132
+ const [editSlug, setEditSlug] = useState("")
133
+ const [editTitle, setEditTitle] = useState("")
134
+ const [editCategory, setEditCategory] = useState("invariants")
135
+ const [editContent, setEditContent] = useState("")
136
+ const [editStatus, setEditStatus] = useState("active")
137
+ const [editPriority, setEditPriority] = useState("low")
138
+ const [editModality, setEditModality] = useState("should")
139
+ const [editConfidence, setEditConfidence] = useState(1.0)
140
+ const [editImportance, setEditImportance] = useState(3)
141
+ const [editTags, setEditTags] = useState("")
142
+ const [editRelated, setEditRelated] = useState("")
143
+ const [editContradicts, setEditContradicts] = useState("")
144
+ const [editSupersedes, setEditSupersedes] = useState("")
145
+
146
+ const editorRef = useRef<HTMLDivElement>(null)
147
+
148
+ const execCommand = (command: string, value: string = "") => {
149
+ document.execCommand(command, false, value)
150
+ editorRef.current?.focus()
151
+ if (editorRef.current) {
152
+ setEditContent(htmlToMarkdown(editorRef.current.innerHTML))
153
+ }
154
+ }
155
+
156
+ useEffect(() => {
157
+ if ((isEditing || isCreating) && editorRef.current) {
158
+ editorRef.current.innerHTML = markdownToHtml(editContent)
159
+ }
160
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- only sync editor HTML on toggle edit/create
161
+ }, [isEditing, isCreating])
162
+
163
+ const fetchNodes = useCallback(async () => {
164
+ setLoading(true)
165
+ setError("")
166
+ try {
167
+ if (query.trim()) {
168
+ const results = await searchMemory(query, category === "all" ? undefined : category)
169
+ setNodes(results)
170
+ } else {
171
+ const results = await listMemory(activeBrainId, category === "all" ? undefined : category)
172
+ setNodes(results)
173
+ }
174
+ } catch (e) {
175
+ setError((e as Error).message)
176
+ setNodes([])
177
+ } finally {
178
+ setLoading(false)
179
+ }
180
+ }, [category, query, activeBrainId])
181
+
182
+
183
+ useEffect(() => {
184
+ if (selected) {
185
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate form synchronization
186
+ setEditSlug(selected.slug || "")
187
+ setEditTitle(selected.title || "")
188
+ setEditCategory(selected.category || "invariants")
189
+ setEditContent(selected.content || selected.body || "")
190
+ setEditStatus(selected.status || "active")
191
+ setEditPriority(selected.priority || "low")
192
+ setEditModality(selected.modality || "should")
193
+ setEditConfidence(selected.confidence ?? 1.0)
194
+ setEditImportance(selected.importance ?? 3)
195
+ setEditTags((selected.tags || []).join(", "))
196
+ setEditRelated((selected.related || []).join(", "))
197
+ setEditContradicts((selected.contradicts || []).join(", "))
198
+ setEditSupersedes((selected.supersedes || []).join(", "))
199
+ }
200
+ }, [selected])
201
+
202
+ const handleSave = async () => {
203
+ if (!selected) return
204
+ setLoading(true)
205
+ setError("")
206
+ try {
207
+ const tagsArr = editTags.split(",").map(t => t.trim()).filter(Boolean)
208
+ const relatedArr = editRelated.split(",").map(t => t.trim()).filter(Boolean)
209
+ const contradictsArr = editContradicts.split(",").map(t => t.trim()).filter(Boolean)
210
+ const supersedesArr = editSupersedes.split(",").map(t => t.trim()).filter(Boolean)
211
+
212
+ const updated = await saveMemory(selected.slug, {
213
+ title: editTitle,
214
+ category: editCategory,
215
+ content: editContent,
216
+ status: editStatus,
217
+ priority: editPriority,
218
+ modality: editModality,
219
+ confidence: editConfidence,
220
+ importance: editImportance,
221
+ tags: tagsArr,
222
+ related: relatedArr,
223
+ contradicts: contradictsArr,
224
+ supersedes: supersedesArr
225
+ })
226
+ setSelected(updated)
227
+ setIsEditing(false)
228
+ fetchNodes()
229
+ } catch (e) {
230
+ setError((e as Error).message)
231
+ } finally {
232
+ setLoading(false)
233
+ }
234
+ }
235
+
236
+ const handleCreate = async () => {
237
+ if (!editSlug.trim() || !editTitle.trim() || !editContent.trim()) {
238
+ setError("Slug, Title, and Content are required fields.")
239
+ return
240
+ }
241
+ setLoading(true)
242
+ setError("")
243
+ try {
244
+ const tagsArr = editTags.split(",").map(t => t.trim()).filter(Boolean)
245
+ const relatedArr = editRelated.split(",").map(t => t.trim()).filter(Boolean)
246
+ const contradictsArr = editContradicts.split(",").map(t => t.trim()).filter(Boolean)
247
+ const supersedesArr = editSupersedes.split(",").map(t => t.trim()).filter(Boolean)
248
+
249
+ const created = await createMemory({
250
+ slug: editSlug,
251
+ title: editTitle,
252
+ category: editCategory,
253
+ content: editContent,
254
+ status: editStatus,
255
+ priority: editPriority,
256
+ modality: editModality,
257
+ confidence: editConfidence,
258
+ importance: editImportance,
259
+ tags: tagsArr,
260
+ related: relatedArr,
261
+ contradicts: contradictsArr,
262
+ supersedes: supersedesArr
263
+ })
264
+ setIsCreating(false)
265
+ setSelected(created)
266
+ setSearchParams({ slug: created.slug })
267
+ fetchNodes()
268
+ } catch (e) {
269
+ setError((e as Error).message)
270
+ } finally {
271
+ setLoading(false)
272
+ }
273
+ }
274
+
275
+ const handleDelete = async (slug: string) => {
276
+ if (!window.confirm(`Are you sure you want to delete memory node "${slug}"?`)) return
277
+ setLoading(true)
278
+ setError("")
279
+ try {
280
+ await deleteMemory(slug)
281
+ setSelected(null)
282
+ setSearchParams({})
283
+ fetchNodes()
284
+ } catch (e) {
285
+ setError((e as Error).message)
286
+ } finally {
287
+ setLoading(false)
288
+ }
289
+ }
290
+
291
+ const fetchConflictsList = useCallback(async () => {
292
+ try {
293
+ const res = await fetchConflicts()
294
+ setConflicts(res.conflicts || [])
295
+ } catch (e) {
296
+ console.warn("Conflicts API error (enhanced features might be disabled):", e)
297
+ }
298
+ }, [])
299
+
300
+ useEffect(() => {
301
+ const timer = setTimeout(fetchNodes, 300)
302
+ return () => clearTimeout(timer)
303
+ }, [fetchNodes])
304
+
305
+ const loadSessions = useCallback(async () => {
306
+ setSessionsLoading(true)
307
+ try {
308
+ const data = await fetchSessions(100)
309
+ setSessionsList(data.sessions)
310
+ setSessionsTotal(data.total)
311
+ } catch (e) {
312
+ setError((e as Error).message)
313
+ } finally {
314
+ setSessionsLoading(false)
315
+ }
316
+ }, [])
317
+
318
+
319
+ useEffect(() => {
320
+ if (activeTab === "conflicts") {
321
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate tab data synchronization
322
+ fetchConflictsList()
323
+ } else if (activeTab === "sessions") {
324
+ loadSessions()
325
+ }
326
+ }, [activeTab, fetchConflictsList, loadSessions])
327
+
328
+ const handleDeleteSession = async (id: string) => {
329
+ if (!window.confirm(`Delete session "${id}"?`)) return
330
+ try {
331
+ await deleteSession(id)
332
+ loadSessions()
333
+ } catch (e) {
334
+ setError((e as Error).message)
335
+ }
336
+ }
337
+
338
+
339
+ useEffect(() => {
340
+ if (urlSlug) {
341
+ const loadUrlSlug = async () => {
342
+ setLoading(true)
343
+ setError("")
344
+ try {
345
+ const full = await readMemory(urlSlug)
346
+ setSelected(full)
347
+ } catch (e) {
348
+ setError(`Failed to load memory node '${urlSlug}': ${(e as Error).message}`)
349
+ setSelected(null)
350
+ } finally {
351
+ setLoading(false)
352
+ }
353
+ }
354
+ loadUrlSlug()
355
+ } else {
356
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate reset on url change
357
+ setSelected(null)
358
+ }
359
+ }, [urlSlug])
360
+
361
+ // Wire SSE Live Agent Monitor Streaming (Port 9111)
362
+ useEffect(() => {
363
+ const sseUrl = "http://localhost:9111/stream"
364
+ const eventSource = new EventSource(sseUrl)
365
+
366
+ eventSource.onmessage = (event) => {
367
+ try {
368
+ const logEntry = JSON.parse(event.data)
369
+ // If a conflict-detector log, memory ingestion, or surface rebuild log arrives, hot-refresh!
370
+ if (
371
+ logEntry.subsystem === "conflict-detector" ||
372
+ logEntry.subsystem === "rebuild" ||
373
+ logEntry.subsystem === "session-watcher"
374
+ ) {
375
+ fetchNodes()
376
+ fetchConflictsList()
377
+ }
378
+ } catch {
379
+ // ignore
380
+ }
381
+ }
382
+
383
+ eventSource.onerror = () => {
384
+ eventSource.close()
385
+ }
386
+
387
+ return () => {
388
+ eventSource.close()
389
+ }
390
+ }, [fetchNodes, fetchConflictsList])
391
+
392
+ const handleSelect = (node: MemoryNode) => {
393
+ setSearchParams({ slug: node.slug })
394
+ }
395
+
396
+ const handleResolveConflict = async (conflictId: string, action: "keep" | "supersede", winnerSlug: string) => {
397
+ setResolvingId(conflictId)
398
+ setError("")
399
+ try {
400
+ await resolveConflict(conflictId, action, winnerSlug)
401
+ await fetchConflictsList()
402
+ await fetchNodes()
403
+ } catch (e) {
404
+ setError((e as Error).message)
405
+ } finally {
406
+ setResolvingId(null)
407
+ }
408
+ }
409
+
410
+ const renderEditorForm = (onSubmit: () => void, onCancel: () => void, isNew: boolean) => {
411
+ return (
412
+ <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
413
+ {/* Form fields */}
414
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
415
+ {/* Left Column */}
416
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
417
+ <div>
418
+ <label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Slug (kebab-case, matches filename)</label>
419
+ <input
420
+ type="text"
421
+ placeholder="e.g. prefer-atomic-writes"
422
+ value={editSlug}
423
+ onChange={e => setEditSlug(e.target.value)}
424
+ disabled={!isNew}
425
+ style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
426
+ />
427
+ </div>
428
+ <div>
429
+ <label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Title (One-line description)</label>
430
+ <input
431
+ type="text"
432
+ placeholder="e.g. Always write files atomically (write-then-rename)"
433
+ value={editTitle}
434
+ onChange={e => setEditTitle(e.target.value)}
435
+ style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
436
+ />
437
+ </div>
438
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
439
+ <div>
440
+ <label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Category</label>
441
+ <select
442
+ value={editCategory}
443
+ onChange={e => setEditCategory(e.target.value)}
444
+ style={{ width: "100%", background: "rgba(15,23,42,0.9)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
445
+ >
446
+ <option value="invariants">invariants</option>
447
+ <option value="patterns">patterns</option>
448
+ <option value="anti-patterns">anti-patterns</option>
449
+ <option value="preferences">preferences</option>
450
+ <option value="decisions">decisions</option>
451
+ <option value="concepts">concepts</option>
452
+ <option value="facts">facts</option>
453
+ <option value="lore">lore</option>
454
+ </select>
455
+ </div>
456
+ <div>
457
+ <label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Modality</label>
458
+ <select
459
+ value={editModality}
460
+ onChange={e => setEditModality(e.target.value)}
461
+ style={{ width: "100%", background: "rgba(15,23,42,0.9)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
462
+ >
463
+ <option value="must">must</option>
464
+ <option value="must_not">must_not</option>
465
+ <option value="should">should</option>
466
+ <option value="should_not">should_not</option>
467
+ </select>
468
+ </div>
469
+ </div>
470
+ </div>
471
+
472
+ {/* Right Column */}
473
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
474
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
475
+ <div>
476
+ <label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Status</label>
477
+ <select
478
+ value={editStatus}
479
+ onChange={e => setEditStatus(e.target.value)}
480
+ style={{ width: "100%", background: "rgba(15,23,42,0.9)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
481
+ >
482
+ <option value="active">active</option>
483
+ <option value="draft">draft</option>
484
+ <option value="deprecated">deprecated</option>
485
+ <option value="superseded">superseded</option>
486
+ </select>
487
+ </div>
488
+ <div>
489
+ <label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Importance (1-5 Stars)</label>
490
+ <select
491
+ value={editImportance}
492
+ onChange={e => setEditImportance(Number(e.target.value))}
493
+ style={{ width: "100%", background: "rgba(15,23,42,0.9)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
494
+ >
495
+ <option value={1}>★ 1/5</option>
496
+ <option value={2}>★★ 2/5</option>
497
+ <option value={3}>★★★ 3/5</option>
498
+ <option value={4}>★★★★ 4/5</option>
499
+ <option value={5}>★★★★★ 5/5</option>
500
+ </select>
501
+ </div>
502
+ </div>
503
+ <div>
504
+ <label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>
505
+ Confidence: <strong>{Math.round(editConfidence * 100)}%</strong>
506
+ </label>
507
+ <input
508
+ type="range"
509
+ min="0"
510
+ max="1"
511
+ step="0.05"
512
+ value={editConfidence}
513
+ onChange={e => setEditConfidence(Number(e.target.value))}
514
+ style={{ width: "100%", height: 6, background: "rgba(255,255,255,0.1)", borderRadius: 3, outline: "none" }}
515
+ />
516
+ </div>
517
+ <div>
518
+ <label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Tags (comma separated)</label>
519
+ <input
520
+ type="text"
521
+ placeholder="e.g. filesystem, reliability, writes"
522
+ value={editTags}
523
+ onChange={e => setEditTags(e.target.value)}
524
+ style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
525
+ />
526
+ </div>
527
+ </div>
528
+ </div>
529
+
530
+ {/* Semantic Relationships */}
531
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 12 }}>
532
+ <div>
533
+ <label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Related Nodes (comma separated slugs)</label>
534
+ <input
535
+ type="text"
536
+ placeholder="e.g. use-atomic-writes, git-wiping"
537
+ value={editRelated}
538
+ onChange={e => setEditRelated(e.target.value)}
539
+ style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
540
+ />
541
+ </div>
542
+ <div>
543
+ <label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Contradicts Nodes (comma separated slugs)</label>
544
+ <input
545
+ type="text"
546
+ placeholder="e.g. write-anywhere-sync"
547
+ value={editContradicts}
548
+ onChange={e => setEditContradicts(e.target.value)}
549
+ style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
550
+ />
551
+ </div>
552
+ <div>
553
+ <label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Supersedes Nodes (comma separated slugs)</label>
554
+ <input
555
+ type="text"
556
+ placeholder="e.g. prefer-atomic-writes-v1"
557
+ value={editSupersedes}
558
+ onChange={e => setEditSupersedes(e.target.value)}
559
+ style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
560
+ />
561
+ </div>
562
+ </div>
563
+
564
+ {/* WYSIWYG visual HTML Editor - NO RAW CODE PREVIEW! EDIT Styled preview DIRECTLY */}
565
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
566
+ <label style={{ fontSize: 12, color: "var(--text-secondary)" }}>Visual WYSIWYG Memory Content Editor</label>
567
+ <div style={{ background: "rgba(10,15,30,0.5)", border: "1px solid var(--border)", borderRadius: 8, overflow: "hidden" }}>
568
+ {/* Rich Formatting Toolbar */}
569
+ <div style={{ display: "flex", gap: 8, padding: 8, background: "rgba(255,255,255,0.03)", borderBottom: "1px solid var(--border)", flexWrap: "wrap" }}>
570
+ <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("bold")} style={{ fontWeight: "bold", padding: "4px 10px" }}>B</button>
571
+ <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("italic")} style={{ fontStyle: "italic", padding: "4px 10px" }}>I</button>
572
+ <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("formatBlock", "<h1>")} style={{ padding: "4px 8px" }}>H1</button>
573
+ <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("formatBlock", "<h2>")} style={{ padding: "4px 8px" }}>H2</button>
574
+ <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("formatBlock", "<blockquote>")} style={{ padding: "4px 8px" }}>“ Quote</button>
575
+ <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("insertUnorderedList")} style={{ padding: "4px 8px" }}>• List</button>
576
+ <button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("removeFormat")} style={{ padding: "4px 8px", fontSize: 11 }}>Clear</button>
577
+ </div>
578
+
579
+ {/* Direct contentEditable Editor Pane */}
580
+ <div
581
+ ref={editorRef}
582
+ contentEditable
583
+ onInput={() => {
584
+ if (editorRef.current) {
585
+ setEditContent(htmlToMarkdown(editorRef.current.innerHTML))
586
+ }
587
+ }}
588
+ style={{
589
+ minHeight: 300,
590
+ padding: 16,
591
+ color: "#fff",
592
+ background: "#090d16",
593
+ outline: "none",
594
+ fontSize: 14,
595
+ lineHeight: 1.6,
596
+ overflowY: "auto"
597
+ }}
598
+ />
599
+ </div>
600
+ </div>
601
+
602
+ {/* Action Buttons */}
603
+ <div style={{ display: "flex", justifyContent: "flex-end", gap: 12, marginTop: 12 }}>
604
+ <button className="btn btn-ghost" type="button" onClick={onCancel} style={{ fontSize: 13 }}>Cancel</button>
605
+ <button className="btn btn-purple" type="button" onClick={onSubmit} style={{ fontSize: 13 }}>
606
+ {isNew ? "🚀 Create Memory Node" : "💾 Save Changes"}
607
+ </button>
608
+ </div>
609
+ </div>
610
+ )
611
+ }
612
+
613
+ const categoryCounts = nodes.reduce<Record<string, number>>((acc, n) => {
614
+ acc[n.category] = (acc[n.category] || 0) + 1
615
+ return acc
616
+ }, {})
617
+
618
+ return (
619
+ <div className="page">
620
+ <div className="page-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 20 }}>
621
+ <div>
622
+ <h1>Memory Vault</h1>
623
+ <p>Browse, audit, and resolve contradictions in your SSSS knowledge graph</p>
624
+ </div>
625
+
626
+ {/* Tab Selection */}
627
+ <div style={{ display: "flex", background: "rgba(255,255,255,0.03)", padding: 4, borderRadius: 8, border: "1px solid var(--border)" }}>
628
+ <button
629
+ className={`btn btn-sm ${activeTab === "vault" ? "btn-purple" : "btn-ghost"}`}
630
+ onClick={() => { setActiveTab("vault"); setSelected(null); setSearchParams({}); }}
631
+ style={{ borderRadius: 6, fontSize: 12, padding: "6px 12px" }}
632
+ >
633
+ 🧠 Active Nodes
634
+ </button>
635
+ <button
636
+ className={`btn btn-sm ${activeTab === "conflicts" ? "btn-cyan" : "btn-ghost"}`}
637
+ onClick={() => { setActiveTab("conflicts"); setSelected(null); setSearchParams({}); }}
638
+ style={{ borderRadius: 6, fontSize: 12, padding: "6px 12px", display: "flex", alignItems: "center", gap: 6 }}
639
+ >
640
+ ⚠️ Quarantined Conflicts
641
+ {conflicts.length > 0 && (
642
+ <span style={{ background: "#ef4444", color: "#fff", fontSize: 9, padding: "1px 5px", borderRadius: 10, fontWeight: 700 }}>
643
+ {conflicts.length}
644
+ </span>
645
+ )}
646
+ </button>
647
+ <button
648
+ className={`btn btn-sm ${activeTab === "sessions" ? "btn-purple" : "btn-ghost"}`}
649
+ onClick={() => { setActiveTab("sessions"); setSelected(null); setSearchParams({}); }}
650
+ style={{ borderRadius: 6, fontSize: 12, padding: "6px 12px" }}
651
+ >
652
+ 📝 Sessions
653
+ {sessionsTotal > 0 && (
654
+ <span style={{ background: "rgba(139,92,246,0.3)", color: "#a78bfa", fontSize: 9, padding: "1px 5px", borderRadius: 10, fontWeight: 700, marginLeft: 4 }}>
655
+ {sessionsTotal}
656
+ </span>
657
+ )}
658
+ </button>
659
+ </div>
660
+ </div>
661
+
662
+ {error && <div className="badge badge-error" style={{ alignSelf: "flex-start", marginBottom: 15 }}>⚠️ {error}</div>}
663
+
664
+ {activeTab === "vault" ? (
665
+ <div className="memory-layout">
666
+ <div className="memory-sidebar">
667
+ <h3 style={{ margin: "0 0 12px 0" }}>Categories</h3>
668
+ {CATEGORIES.map(c => (
669
+ <button
670
+ key={c}
671
+ className={`category-btn ${category === c ? "active" : ""}`}
672
+ onClick={() => { setCategory(c); setSearchParams({}); setIsCreating(false); setIsEditing(false); }}
673
+ >
674
+ <span>{c === "all" ? "📋 All Nodes" : c}</span>
675
+ {c === "all"
676
+ ? <span className="category-count">{nodes.length}</span>
677
+ : categoryCounts[c]
678
+ ? <span className="category-count">{categoryCounts[c]}</span>
679
+ : null
680
+ }
681
+ </button>
682
+ ))}
683
+
684
+ <button
685
+ className="btn btn-purple"
686
+ style={{ width: "100%", marginTop: 20, display: "flex", alignItems: "center", justifyContent: "center", gap: 8, fontSize: 13 }}
687
+ onClick={() => {
688
+ setSelected(null);
689
+ setSearchParams({});
690
+ setIsCreating(true);
691
+ setIsEditing(false);
692
+ setEditSlug("");
693
+ setEditTitle("");
694
+ setEditCategory("invariants");
695
+ setEditContent("");
696
+ setEditStatus("active");
697
+ setEditPriority("low");
698
+ setEditModality("should");
699
+ setEditConfidence(1.0);
700
+ setEditImportance(3);
701
+ setEditTags("");
702
+ setEditRelated("");
703
+ setEditContradicts("");
704
+ setEditSupersedes("");
705
+ }}
706
+ >
707
+ ➕ Create New Node
708
+ </button>
709
+ </div>
710
+ <div className="memory-main">
711
+ <div className="memory-search">
712
+ <input
713
+ id="memory-search"
714
+ type="text"
715
+ placeholder="Search memory nodes…"
716
+ value={query}
717
+ onChange={e => setQuery(e.target.value)}
718
+ />
719
+ <button className="btn btn-ghost btn-sm" onClick={fetchNodes}>Refresh</button>
720
+ </div>
721
+ {loading ? (
722
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
723
+ {[1, 2, 3].map(i => <div key={i} className="skeleton" style={{ height: 60 }} />)}
724
+ </div>
725
+ ) : isCreating ? (
726
+ <div className="memory-detail" style={{ background: "rgba(15,23,42,0.45)", backdropFilter: "blur(12px)", border: "1px solid var(--border)", borderRadius: 12, padding: 24 }}>
727
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 20 }}>
728
+ <h2 style={{ margin: 0, color: "#fff" }}>Create New SSSS Memory Node</h2>
729
+ <button className="btn btn-ghost btn-sm" onClick={() => setIsCreating(false)} style={{ fontSize: 12 }}>← Back to List</button>
730
+ </div>
731
+ {renderEditorForm(handleCreate, () => setIsCreating(false), true)}
732
+ </div>
733
+ ) : selected && isEditing ? (
734
+ <div className="memory-detail" style={{ background: "rgba(15,23,42,0.45)", backdropFilter: "blur(12px)", border: "1px solid var(--border)", borderRadius: 12, padding: 24 }}>
735
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 20 }}>
736
+ <h2 style={{ margin: 0, color: "#fff" }}>Edit Memory Node</h2>
737
+ <button className="btn btn-ghost btn-sm" onClick={() => setIsEditing(false)} style={{ fontSize: 12 }}>← Cancel Edit</button>
738
+ </div>
739
+ {renderEditorForm(handleSave, () => setIsEditing(false), false)}
740
+ </div>
741
+ ) : selected ? (
742
+ <div className="memory-detail" style={{
743
+ background: "rgba(15,23,42,0.45)",
744
+ backdropFilter: "blur(12px)",
745
+ border: selected.modality === "must"
746
+ ? "2px solid #10b981"
747
+ : selected.modality === "must_not"
748
+ ? "2px solid #ef4444"
749
+ : selected.modality === "should"
750
+ ? "1px solid #8b5cf6"
751
+ : selected.modality === "should_not"
752
+ ? "1px solid #f97316"
753
+ : "1px solid var(--border)",
754
+ boxShadow: selected.modality === "must"
755
+ ? "0 0 16px rgba(16, 185, 129, 0.1)"
756
+ : selected.modality === "must_not"
757
+ ? "0 0 16px rgba(239, 68, 68, 0.1)"
758
+ : undefined,
759
+ borderRadius: 12,
760
+ padding: 24,
761
+ }}>
762
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 16 }}>
763
+ <button className="btn btn-ghost btn-sm" onClick={() => setSearchParams({})} style={{ fontSize: 12 }}>← Back to List</button>
764
+ <div style={{ display: "flex", gap: 8 }}>
765
+ <button
766
+ className="btn btn-ghost btn-sm"
767
+ onClick={() => {
768
+ setEditSlug(selected.slug || "");
769
+ setEditTitle(selected.title || "");
770
+ setEditCategory(selected.category || "invariants");
771
+ setEditContent(selected.content || selected.body || "");
772
+ setEditStatus(selected.status || "active");
773
+ setEditPriority(selected.priority || "low");
774
+ setEditModality(selected.modality || "should");
775
+ setEditConfidence(selected.confidence ?? 1.0);
776
+ setEditImportance(selected.importance ?? 3);
777
+ setEditTags((selected.tags || []).join(", "));
778
+ setEditRelated((selected.related || []).join(", "));
779
+ setEditContradicts((selected.contradicts || []).join(", "));
780
+ setEditSupersedes((selected.supersedes || []).join(", "));
781
+ setIsEditing(true);
782
+ }}
783
+ style={{ fontSize: 12, border: "1px solid var(--border)" }}
784
+ >
785
+ ✏️ Edit Node
786
+ </button>
787
+ <button
788
+ className="btn btn-ghost btn-sm"
789
+ onClick={() => handleDelete(selected.slug)}
790
+ style={{ fontSize: 12, borderColor: "rgba(239,68,68,0.2)", color: "#fca5a5" }}
791
+ >
792
+ 🗑️ Delete
793
+ </button>
794
+ </div>
795
+ </div>
796
+
797
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
798
+ <h2 style={{ margin: 0, color: "#fff" }}>{selected.title}</h2>
799
+ {selected.modality && (
800
+ <span style={{
801
+ fontSize: 10,
802
+ fontWeight: 700,
803
+ textTransform: "uppercase",
804
+ padding: "4px 10px",
805
+ borderRadius: 6,
806
+ background: selected.modality === "must"
807
+ ? "rgba(16, 185, 129, 0.15)"
808
+ : selected.modality === "must_not"
809
+ ? "rgba(239, 68, 68, 0.15)"
810
+ : "rgba(255, 255, 255, 0.05)",
811
+ color: selected.modality === "must"
812
+ ? "#10b981"
813
+ : selected.modality === "must_not"
814
+ ? "#ef4444"
815
+ : "var(--text-secondary)",
816
+ border: selected.modality === "must"
817
+ ? "1px solid rgba(16, 185, 129, 0.3)"
818
+ : selected.modality === "must_not"
819
+ ? "1px solid rgba(239, 68, 68, 0.3)"
820
+ : "1px solid rgba(255, 255, 255, 0.1)"
821
+ }}>
822
+ {selected.modality}
823
+ </span>
824
+ )}
825
+ </div>
826
+
827
+ <dl className="frontmatter" style={{ display: "grid", gridTemplateColumns: "120px 1fr", gap: "10px 20px", marginBottom: 24, paddingBottom: 16, borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
828
+ <dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Slug</dt>
829
+ <dd style={{ margin: 0 }}><code>{selected.slug}</code></dd>
830
+
831
+ <dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Category</dt>
832
+ <dd style={{ margin: 0, textTransform: "capitalize" }}>{selected.category}</dd>
833
+
834
+ {selected.status && (
835
+ <>
836
+ <dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Status</dt>
837
+ <dd style={{ margin: 0 }}>
838
+ <span className="badge badge-success" style={{ background: selected.status === "active" ? "rgba(34, 197, 94, 0.15)" : "rgba(255,255,255,0.05)", color: selected.status === "active" ? "#22c55e" : "var(--text-secondary)" }}>
839
+ {selected.status}
840
+ </span>
841
+ </dd>
842
+ </>
843
+ )}
844
+
845
+ {selected.confidence !== undefined && (
846
+ <>
847
+ <dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Confidence</dt>
848
+ <dd style={{ margin: 0, display: "flex", alignItems: "center", gap: 12 }}>
849
+ <strong style={{ fontSize: 13 }}>{Math.round(selected.confidence * 100)}%</strong>
850
+ <div style={{ flex: 1, maxWidth: 150, height: 6, background: "rgba(255,255,255,0.06)", borderRadius: 3, overflow: "hidden", display: "inline-block" }}>
851
+ <div style={{ width: `${Math.round(selected.confidence * 100)}%`, height: "100%", background: "linear-gradient(90deg, #8b5cf6, #06b6d4)", borderRadius: 3 }} />
852
+ </div>
853
+ </dd>
854
+ </>
855
+ )}
856
+
857
+ {selected.importance !== undefined && (
858
+ <>
859
+ <dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Importance</dt>
860
+ <dd style={{ margin: 0, color: "#fbbf24", display: "flex", gap: 2 }}>
861
+ {Array.from({ length: 5 }).map((_, i) => (
862
+ <span key={i}>{i < (selected.importance || 0) ? "★" : "☆"}</span>
863
+ ))}
864
+ </dd>
865
+ </>
866
+ )}
867
+
868
+ {selected.tags?.length ? (
869
+ <>
870
+ <dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Tags</dt>
871
+ <dd style={{ margin: 0, display: "flex", gap: 6, flexWrap: "wrap" }}>
872
+ {selected.tags.map(t => (
873
+ <span key={t} style={{ background: "rgba(255,255,255,0.05)", color: "var(--text-secondary)", fontSize: 10, padding: "2px 8px", borderRadius: 4, border: "1px solid rgba(255,255,255,0.03)" }}>
874
+ {t}
875
+ </span>
876
+ ))}
877
+ </dd>
878
+ </>
879
+ ) : null}
880
+
881
+ {selected.related?.length ? (
882
+ <>
883
+ <dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Related</dt>
884
+ <dd style={{ margin: 0 }}><code>{selected.related.join(", ")}</code></dd>
885
+ </>
886
+ ) : null}
887
+
888
+ {selected.contradicts?.length ? (
889
+ <>
890
+ <dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Contradicts</dt>
891
+ <dd style={{ margin: 0, color: "#fca5a5" }}><code>{selected.contradicts.join(", ")}</code></dd>
892
+ </>
893
+ ) : null}
894
+
895
+ {selected.supersedes?.length ? (
896
+ <>
897
+ <dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Supersedes</dt>
898
+ <dd style={{ margin: 0, color: "#93c5fd" }}><code>{selected.supersedes.join(", ")}</code></dd>
899
+ </>
900
+ ) : null}
901
+ </dl>
902
+
903
+ <div style={{ marginTop: 20 }}>
904
+ <h3 style={{ fontSize: 12, textTransform: "uppercase", color: "var(--text-tertiary)", letterSpacing: 0.5, marginBottom: 12 }}>Memory Content (SSSS Body)</h3>
905
+ <div style={{ background: "rgba(0,0,0,0.15)", border: "1px solid rgba(255,255,255,0.04)", borderRadius: 8, padding: 20 }}>
906
+ {renderMarkdown(selected.body || selected.content || "")}
907
+ </div>
908
+ </div>
909
+ </div>
910
+ ) : (
911
+ <div className="memory-list" style={{ display: "grid", gridTemplateColumns: "1fr", gap: 12 }}>
912
+ {nodes.length === 0 && !loading && (
913
+ <div style={{ textAlign: "center", padding: 40, color: "var(--text-tertiary)" }}>
914
+ {query ? "No matching nodes found." : "No memory nodes loaded. Is the backend running?"}
915
+ </div>
916
+ )}
917
+ {nodes.map(n => {
918
+ const borderStyle = n.modality === "must"
919
+ ? "2px solid #10b981"
920
+ : n.modality === "must_not"
921
+ ? "2px solid #ef4444"
922
+ : n.modality === "should"
923
+ ? "1px solid #8b5cf6"
924
+ : n.modality === "should_not"
925
+ ? "1px solid #f97316"
926
+ : "1px solid rgba(255, 255, 255, 0.08)";
927
+
928
+ const glowStyle = n.modality === "must"
929
+ ? "0 0 12px rgba(16, 185, 129, 0.15)"
930
+ : n.modality === "must_not"
931
+ ? "0 0 12px rgba(239, 68, 68, 0.15)"
932
+ : undefined;
933
+
934
+ return (
935
+ <div
936
+ key={n.slug}
937
+ className="memory-node-card"
938
+ onClick={() => handleSelect(n)}
939
+ style={{
940
+ border: borderStyle,
941
+ boxShadow: glowStyle,
942
+ transition: "transform 0.2s ease, box-shadow 0.2s ease",
943
+ position: "relative"
944
+ }}
945
+ >
946
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 6 }}>
947
+ <h4 style={{ margin: 0, color: "#fff", fontSize: 15 }}>{n.title}</h4>
948
+ {n.modality && (
949
+ <span style={{
950
+ fontSize: 9,
951
+ fontWeight: 700,
952
+ textTransform: "uppercase",
953
+ padding: "2px 6px",
954
+ borderRadius: 4,
955
+ background: n.modality === "must"
956
+ ? "rgba(16, 185, 129, 0.15)"
957
+ : n.modality === "must_not"
958
+ ? "rgba(239, 68, 68, 0.15)"
959
+ : "rgba(255, 255, 255, 0.05)",
960
+ color: n.modality === "must"
961
+ ? "#10b981"
962
+ : n.modality === "must_not"
963
+ ? "#ef4444"
964
+ : "var(--text-secondary)"
965
+ }}>
966
+ {n.modality}
967
+ </span>
968
+ )}
969
+ </div>
970
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 16, fontSize: 11, color: "var(--text-tertiary)", marginTop: 8 }}>
971
+ <span>📂 {n.category}</span>
972
+ {n.importance !== undefined && (
973
+ <span style={{ color: "#fbbf24" }}>★ {n.importance}/5</span>
974
+ )}
975
+ {n.confidence !== undefined && (
976
+ <span style={{ display: "flex", alignItems: "center", gap: 4 }}>
977
+ 🧠 {Math.round(n.confidence * 100)}%
978
+ </span>
979
+ )}
980
+ </div>
981
+ </div>
982
+ );
983
+ })}
984
+ </div>
985
+ )}
986
+ </div>
987
+ </div>
988
+ ) : (
989
+ // SSE-driven Quarantined Conflicts override panel
990
+ <div style={{ width: "100%" }}>
991
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
992
+ <h3>Quarantined Contradiction Records</h3>
993
+ <span style={{ fontSize: 12, color: "var(--text-tertiary)", display: "flex", alignItems: "center", gap: 5 }}>
994
+ <span className="pulse" style={{ background: "#06b6d4" }} /> SSE Real-time Agent Monitor Connected
995
+ </span>
996
+ </div>
997
+
998
+ {conflicts.length === 0 ? (
999
+ <div style={{ textAlign: "center", padding: 60, background: "rgba(255,255,255,0.02)", borderRadius: 12, border: "1px dashed var(--border)", color: "var(--text-tertiary)" }}>
1000
+ <span style={{ fontSize: 32, display: "block", marginBottom: 12 }}>🛡️</span>
1001
+ <h4 style={{ color: "#fff", marginBottom: 6 }}>All Clear!</h4>
1002
+ No memory conflicts enqueued or quarantined in `.agent/memory-inbox/conflicts/`.
1003
+ </div>
1004
+ ) : (
1005
+ <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
1006
+ {conflicts.map((c) => {
1007
+ const isQuarantined = c.status === "pending";
1008
+
1009
+ return (
1010
+ <div
1011
+ key={c.conflict_id}
1012
+ style={{
1013
+ background: "rgba(15,23,42,0.45)",
1014
+ backdropFilter: "blur(12px)",
1015
+ border: isQuarantined ? "1px solid rgba(239, 68, 68, 0.25)" : "1px solid rgba(34, 197, 94, 0.2)",
1016
+ borderRadius: 12,
1017
+ padding: 24,
1018
+ boxShadow: "0 8px 32px rgba(0,0,0,0.15)"
1019
+ }}
1020
+ >
1021
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 16, borderBottom: "1px solid rgba(255,255,255,0.06)", paddingBottom: 12 }}>
1022
+ <div>
1023
+ <h4 style={{ margin: 0, fontSize: 16, color: "#fff", display: "flex", alignItems: "center", gap: 8 }}>
1024
+ Conflict: <code>{c.conflict_id}</code>
1025
+ <span style={{ fontSize: 11, padding: "2px 8px", borderRadius: 10, background: isQuarantined ? "rgba(239,68,68,0.15)" : "rgba(34,197,94,0.15)", color: isQuarantined ? "#ef4444" : "#22c55e", textTransform: "uppercase" }}>
1026
+ {c.status}
1027
+ </span>
1028
+ </h4>
1029
+ <span style={{ fontSize: 11, color: "var(--text-tertiary)" }}>Detected at: {new Date(c.detected_at).toLocaleString()}</span>
1030
+ </div>
1031
+ <div style={{ textAlign: "right" }}>
1032
+ <span style={{ fontSize: 12, color: "#ef4444", fontWeight: 700, display: "block" }}>
1033
+ Clash Similarity: {Math.round(c.similarity * 100)}%
1034
+ </span>
1035
+ <span style={{ fontSize: 10, color: "var(--text-tertiary)" }}>Modality polarity flip</span>
1036
+ </div>
1037
+ </div>
1038
+
1039
+ <p style={{ fontSize: 13, background: "rgba(239,68,68,0.03)", padding: "10px 14px", borderRadius: 6, borderLeft: "3px solid #ef4444", color: "#fca5a5", marginBottom: 20 }}>
1040
+ <strong>Reason:</strong> {c.reason}
1041
+ </p>
1042
+
1043
+ {/* Nodes Comparison Table */}
1044
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginBottom: 24 }}>
1045
+ {/* Existing Node */}
1046
+ <div style={{ background: "rgba(255,255,255,0.02)", padding: 16, borderRadius: 8, border: "1px solid rgba(255,255,255,0.05)" }}>
1047
+ <h5 style={{ margin: "0 0 10px 0", color: "#a78bfa", textTransform: "uppercase", fontSize: 11, letterSpacing: 0.5 }}>✓ Existing Node In Vault</h5>
1048
+ <div style={{ fontSize: 13, fontWeight: 700, color: "#fff", marginBottom: 8 }}><code>{c.existing_slug}</code></div>
1049
+ <dl style={{ display: "grid", gridTemplateColumns: "80px 1fr", gap: 4, margin: "0 0 12px 0", fontSize: 11, color: "var(--text-secondary)" }}>
1050
+ <dt>Modality:</dt><dd><code>{c._existing_node?.modality || "must"}</code></dd>
1051
+ <dt>Polarity:</dt><dd><code>{c._existing_node?.sentiment_polarity || "directive_must"}</code></dd>
1052
+ <dt>Confidence:</dt><dd>{c._existing_node?.confidence !== undefined ? `${Math.round(c._existing_node.confidence * 100)}%` : "N/A"}</dd>
1053
+ </dl>
1054
+ <div style={{ fontSize: 12, fontStyle: "italic", background: "rgba(0,0,0,0.2)", padding: 10, borderRadius: 6, color: "var(--text-tertiary)", minHeight: 60, whiteSpace: "pre-wrap" }}>
1055
+ {c._existing_node?.excerpt || c._existing_node?.body || "No preview content."}
1056
+ </div>
1057
+ </div>
1058
+
1059
+ {/* Candidate Node */}
1060
+ <div style={{ background: "rgba(255,255,255,0.02)", padding: 16, borderRadius: 8, border: "1px solid rgba(255,255,255,0.05)" }}>
1061
+ <h5 style={{ margin: "0 0 10px 0", color: "#06b6d4", textTransform: "uppercase", fontSize: 11, letterSpacing: 0.5 }}>⚠ New Ingestion Candidate</h5>
1062
+ <div style={{ fontSize: 13, fontWeight: 700, color: "#fff", marginBottom: 8 }}><code>{c.new_slug}</code></div>
1063
+ <dl style={{ display: "grid", gridTemplateColumns: "80px 1fr", gap: 4, margin: "0 0 12px 0", fontSize: 11, color: "var(--text-secondary)" }}>
1064
+ <dt>Modality:</dt><dd><code>{c._new_node?.modality || "must_not"}</code></dd>
1065
+ <dt>Polarity:</dt><dd><code>{c._new_node?.sentiment_polarity || "directive_must_not"}</code></dd>
1066
+ <dt>Confidence:</dt><dd>{c._new_node?.confidence !== undefined ? `${Math.round(c._new_node.confidence * 100)}%` : "N/A"}</dd>
1067
+ </dl>
1068
+ <div style={{ fontSize: 12, fontStyle: "italic", background: "rgba(0,0,0,0.2)", padding: 10, borderRadius: 6, color: "var(--text-tertiary)", minHeight: 60, whiteSpace: "pre-wrap" }}>
1069
+ {c._new_node?.excerpt || c._new_node?.body || "No preview content."}
1070
+ </div>
1071
+ </div>
1072
+ </div>
1073
+
1074
+ {/* Resolution Actions */}
1075
+ {isQuarantined ? (
1076
+ <div style={{ display: "flex", justifyContent: "flex-end", gap: 12 }}>
1077
+ <button
1078
+ className="btn btn-ghost btn-sm"
1079
+ disabled={resolvingId !== null}
1080
+ onClick={() => handleResolveConflict(c.conflict_id, "keep", c.existing_slug)}
1081
+ style={{ borderColor: "rgba(255,255,255,0.1)", fontSize: 12 }}
1082
+ >
1083
+ {resolvingId === c.conflict_id ? "Processing..." : `Keep Existing Node (${c.existing_slug})`}
1084
+ </button>
1085
+ <button
1086
+ className="btn btn-purple btn-sm"
1087
+ disabled={resolvingId !== null}
1088
+ onClick={() => handleResolveConflict(c.conflict_id, "supersede", c.new_slug)}
1089
+ style={{ fontSize: 12 }}
1090
+ >
1091
+ {resolvingId === c.conflict_id ? "Processing..." : `Supersede with New (${c.new_slug})`}
1092
+ </button>
1093
+ </div>
1094
+ ) : (
1095
+ <div style={{ fontSize: 12, color: "#22c55e", background: "rgba(34,197,94,0.06)", padding: "10px 14px", borderRadius: 6, border: "1px solid rgba(34,197,94,0.15)", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
1096
+ <span>✓ Resolved successfully. Choice: <strong>{c.resolution}</strong></span>
1097
+ <span style={{ fontSize: 11, color: "var(--text-tertiary)" }}>Resolved at: {c.resolved_at ? new Date(c.resolved_at).toLocaleString() : ""}</span>
1098
+ </div>
1099
+ )}
1100
+ </div>
1101
+ )
1102
+ })}
1103
+ </div>
1104
+ )}
1105
+ </div>
1106
+ )}
1107
+
1108
+ {activeTab === "sessions" && (
1109
+ <div className="health-log" style={{ background: "rgba(15,23,42,0.45)", backdropFilter: "blur(12px)", border: "1px solid var(--border)", borderRadius: 12, padding: 24 }}>
1110
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
1111
+ <h2 style={{ margin: 0, color: "#fff", fontSize: 16 }}>📝 IDE Sessions ({sessionsTotal})</h2>
1112
+ <button className="btn btn-ghost btn-sm" onClick={loadSessions} style={{ fontSize: 12 }}>🔄 Refresh</button>
1113
+ </div>
1114
+
1115
+ {sessionsLoading ? (
1116
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
1117
+ {[1, 2, 3].map(i => <div key={i} className="skeleton" style={{ height: 56 }} />)}
1118
+ </div>
1119
+ ) : sessionsList.length === 0 ? (
1120
+ <div style={{ textAlign: "center", padding: 40, color: "var(--text-tertiary)" }}>
1121
+ <span style={{ fontSize: 32, display: "block", marginBottom: 8 }}>📝</span>
1122
+ No sessions ingested yet. Sessions appear here when the relay ships IDE conversation data.
1123
+ </div>
1124
+ ) : (
1125
+ <table style={{ width: "100%", textAlign: "left", borderCollapse: "collapse" }}>
1126
+ <thead>
1127
+ <tr>
1128
+ <th style={{ padding: "10px 12px", borderBottom: "1px solid var(--border)", color: "var(--text-tertiary)", fontSize: 12, fontWeight: 600 }}>Title</th>
1129
+ <th style={{ padding: "10px 12px", borderBottom: "1px solid var(--border)", color: "var(--text-tertiary)", fontSize: 12, fontWeight: 600 }}>Date</th>
1130
+ <th style={{ padding: "10px 12px", borderBottom: "1px solid var(--border)", color: "var(--text-tertiary)", fontSize: 12, fontWeight: 600 }}>Source</th>
1131
+ <th style={{ padding: "10px 12px", borderBottom: "1px solid var(--border)", color: "var(--text-tertiary)", fontSize: 12, fontWeight: 600 }}>Messages</th>
1132
+ <th style={{ padding: "10px 12px", borderBottom: "1px solid var(--border)", color: "var(--text-tertiary)", fontSize: 12, fontWeight: 600, width: 60 }}></th>
1133
+ </tr>
1134
+ </thead>
1135
+ <tbody>
1136
+ {sessionsList.map(s => {
1137
+ const sourceColors: Record<string, string> = {
1138
+ antigravity: "#8b5cf6",
1139
+ "claude-code": "#f59e0b",
1140
+ codex: "#10b981",
1141
+ vscode: "#3b82f6",
1142
+ cursor: "#06b6d4",
1143
+ "gemini-cli": "#ec4899",
1144
+ }
1145
+ const color = sourceColors[s.source || ""] || "var(--text-secondary)"
1146
+ return (
1147
+ <tr key={s.id} style={{ borderBottom: "1px solid rgba(255,255,255,0.04)" }}>
1148
+ <td style={{ padding: "10px 12px", color: "#fff", fontSize: 13, maxWidth: 400, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
1149
+ {s.title || s.id}
1150
+ </td>
1151
+ <td style={{ padding: "10px 12px", color: "var(--text-secondary)", fontSize: 12, whiteSpace: "nowrap" }}>
1152
+ {s.date ? new Date(s.date).toLocaleString() : "-"}
1153
+ </td>
1154
+ <td style={{ padding: "10px 12px" }}>
1155
+ {s.source && (
1156
+ <span style={{
1157
+ fontSize: 10,
1158
+ fontWeight: 600,
1159
+ textTransform: "uppercase",
1160
+ padding: "2px 8px",
1161
+ borderRadius: 4,
1162
+ background: `${color}20`,
1163
+ color,
1164
+ border: `1px solid ${color}40`,
1165
+ }}>
1166
+ {s.source}
1167
+ </span>
1168
+ )}
1169
+ </td>
1170
+ <td style={{ padding: "10px 12px", color: "var(--text-secondary)", fontSize: 12 }}>
1171
+ {s.count}
1172
+ </td>
1173
+ <td style={{ padding: "10px 12px" }}>
1174
+ <button
1175
+ className="btn btn-ghost btn-sm"
1176
+ onClick={() => handleDeleteSession(s.id)}
1177
+ style={{ fontSize: 10, padding: "2px 6px", color: "#fca5a5", borderColor: "rgba(239,68,68,0.2)" }}
1178
+ >
1179
+ 🗑️
1180
+ </button>
1181
+ </td>
1182
+ </tr>
1183
+ )
1184
+ })}
1185
+ </tbody>
1186
+ </table>
1187
+ )}
1188
+ </div>
1189
+ )}
1190
+ </div>
1191
+ )
1192
+ }