code-context-control 2.28.0__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.
Files changed (150) hide show
  1. cli/__init__.py +1 -0
  2. cli/_hook_utils.py +99 -0
  3. cli/c3.py +6152 -0
  4. cli/commands/__init__.py +1 -0
  5. cli/commands/common.py +312 -0
  6. cli/commands/parser.py +286 -0
  7. cli/docs.html +3178 -0
  8. cli/edits.html +878 -0
  9. cli/hook_auto_snapshot.py +142 -0
  10. cli/hook_c3_signal.py +61 -0
  11. cli/hook_c3read.py +116 -0
  12. cli/hook_edit_ledger.py +213 -0
  13. cli/hook_edit_unlock.py +170 -0
  14. cli/hook_filter.py +130 -0
  15. cli/hook_ghost_files.py +238 -0
  16. cli/hook_pretool_enforce.py +334 -0
  17. cli/hook_read.py +200 -0
  18. cli/hook_session_stats.py +62 -0
  19. cli/hook_terse_advisor.py +190 -0
  20. cli/hub.html +3764 -0
  21. cli/hub_server.py +1619 -0
  22. cli/mcp_proxy.py +428 -0
  23. cli/mcp_server.py +660 -0
  24. cli/server.py +2985 -0
  25. cli/tools/__init__.py +4 -0
  26. cli/tools/_helpers.py +65 -0
  27. cli/tools/agent.py +1165 -0
  28. cli/tools/compress.py +215 -0
  29. cli/tools/delegate.py +1184 -0
  30. cli/tools/edit.py +313 -0
  31. cli/tools/edits.py +118 -0
  32. cli/tools/filter.py +285 -0
  33. cli/tools/impact.py +163 -0
  34. cli/tools/memory.py +469 -0
  35. cli/tools/read.py +224 -0
  36. cli/tools/search.py +337 -0
  37. cli/tools/session.py +95 -0
  38. cli/tools/shell.py +193 -0
  39. cli/tools/status.py +306 -0
  40. cli/tools/validate.py +310 -0
  41. cli/ui/api.js +36 -0
  42. cli/ui/app.js +207 -0
  43. cli/ui/components/chat.js +758 -0
  44. cli/ui/components/dashboard.js +689 -0
  45. cli/ui/components/edits.js +220 -0
  46. cli/ui/components/instructions.js +481 -0
  47. cli/ui/components/memory.js +626 -0
  48. cli/ui/components/sessions.js +606 -0
  49. cli/ui/components/settings.js +1404 -0
  50. cli/ui/components/sidebar.js +156 -0
  51. cli/ui/icons.js +51 -0
  52. cli/ui/shared.js +119 -0
  53. cli/ui/theme.js +22 -0
  54. cli/ui.html +168 -0
  55. cli/ui_legacy.html +6797 -0
  56. cli/ui_nano.html +503 -0
  57. code_context_control-2.28.0.dist-info/METADATA +248 -0
  58. code_context_control-2.28.0.dist-info/RECORD +150 -0
  59. code_context_control-2.28.0.dist-info/WHEEL +5 -0
  60. code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
  61. code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
  62. code_context_control-2.28.0.dist-info/top_level.txt +5 -0
  63. core/__init__.py +75 -0
  64. core/config.py +269 -0
  65. core/ide.py +188 -0
  66. oracle/__init__.py +1 -0
  67. oracle/config.py +75 -0
  68. oracle/oracle.html +3900 -0
  69. oracle/oracle_server.py +663 -0
  70. oracle/services/__init__.py +1 -0
  71. oracle/services/c3_bridge.py +210 -0
  72. oracle/services/chat_engine.py +1103 -0
  73. oracle/services/chat_store.py +155 -0
  74. oracle/services/cross_memory.py +154 -0
  75. oracle/services/federated_graph.py +463 -0
  76. oracle/services/health_checker.py +117 -0
  77. oracle/services/insight_engine.py +307 -0
  78. oracle/services/memory_reader.py +106 -0
  79. oracle/services/memory_writer.py +182 -0
  80. oracle/services/ollama_bridge.py +332 -0
  81. oracle/services/project_scanner.py +87 -0
  82. oracle/services/review_agent.py +206 -0
  83. services/__init__.py +1 -0
  84. services/activity_log.py +93 -0
  85. services/agent_base.py +124 -0
  86. services/agents.py +1529 -0
  87. services/auto_memory.py +407 -0
  88. services/bench/__init__.py +6 -0
  89. services/bench/external/__init__.py +29 -0
  90. services/bench/external/aider_polyglot.py +405 -0
  91. services/bench/external/swe_bench.py +485 -0
  92. services/benchmark_dashboard.py +596 -0
  93. services/claude_md.py +785 -0
  94. services/compressor.py +592 -0
  95. services/context_snapshot.py +356 -0
  96. services/conversation_store.py +870 -0
  97. services/doc_index.py +537 -0
  98. services/e2e_benchmark.py +2884 -0
  99. services/e2e_evaluator.py +396 -0
  100. services/e2e_tasks.py +743 -0
  101. services/edit_ledger.py +459 -0
  102. services/embedding_index.py +341 -0
  103. services/error_reporting.py +123 -0
  104. services/file_memory.py +734 -0
  105. services/hub_service.py +585 -0
  106. services/indexer.py +712 -0
  107. services/memory.py +318 -0
  108. services/memory_consolidator.py +538 -0
  109. services/memory_graph.py +382 -0
  110. services/memory_grounder.py +304 -0
  111. services/memory_scorer.py +246 -0
  112. services/metrics.py +86 -0
  113. services/notifications.py +209 -0
  114. services/ollama_client.py +201 -0
  115. services/output_filter.py +488 -0
  116. services/parser.py +1238 -0
  117. services/project_manager.py +579 -0
  118. services/protocol.py +306 -0
  119. services/proxy_state.py +152 -0
  120. services/retrieval_broker.py +129 -0
  121. services/router.py +414 -0
  122. services/runtime.py +326 -0
  123. services/session_benchmark.py +1945 -0
  124. services/session_manager.py +1026 -0
  125. services/session_preloader.py +251 -0
  126. services/text_index.py +90 -0
  127. services/tool_classifier.py +176 -0
  128. services/transcript_index.py +340 -0
  129. services/validation_cache.py +155 -0
  130. services/vector_store.py +299 -0
  131. services/version_tracker.py +271 -0
  132. services/watcher.py +192 -0
  133. tui/__init__.py +0 -0
  134. tui/backend.py +59 -0
  135. tui/main.py +145 -0
  136. tui/screens/__init__.py +1 -0
  137. tui/screens/benchmark_view.py +109 -0
  138. tui/screens/claudemd_view.py +46 -0
  139. tui/screens/compress_view.py +52 -0
  140. tui/screens/index_view.py +74 -0
  141. tui/screens/init_view.py +82 -0
  142. tui/screens/mcp_view.py +73 -0
  143. tui/screens/optimize_view.py +41 -0
  144. tui/screens/pipe_view.py +46 -0
  145. tui/screens/projects_view.py +355 -0
  146. tui/screens/search_view.py +55 -0
  147. tui/screens/session_view.py +143 -0
  148. tui/screens/stats.py +158 -0
  149. tui/screens/ui_view.py +54 -0
  150. tui/theme.tcss +335 -0
@@ -0,0 +1,626 @@
1
+ // ─── Memory ───────────────────────────────
2
+ // Globals: T, I, GlowDot, Badge, StatBox, Btn, api, timeAgo, localDate, useState, useEffect, useRef
3
+
4
+ const CAT_COLORS_GRAPH = {
5
+ general: "#9aa0a6",
6
+ architecture: "#4c8bf5",
7
+ convention: "#b388ff",
8
+ bug: "#ef5350",
9
+ preference: "#ffb74d",
10
+ };
11
+
12
+ const GRAPH_LEGEND = [
13
+ { key: "general", label: "General", desc: "uncategorized facts" },
14
+ { key: "architecture", label: "Architecture", desc: "system design, structure" },
15
+ { key: "convention", label: "Convention", desc: "coding style, patterns" },
16
+ { key: "bug", label: "Bug", desc: "defects, issues" },
17
+ { key: "preference", label: "Preference", desc: "user choices" },
18
+ ];
19
+
20
+ const MemoryGraph = ({ onSelectFact }) => {
21
+ const containerRef = useRef(null);
22
+ const cyRef = useRef(null);
23
+ const [stats, setStats] = useState(null);
24
+ const [minWeight, setMinWeight] = useState(0);
25
+ const [includeNonFact, setIncludeNonFact] = useState(false);
26
+ const [selected, setSelected] = useState(null);
27
+ const [loading, setLoading] = useState(true);
28
+ const [showHelp, setShowHelp] = useState(true);
29
+
30
+ const fitGraph = () => { if (cyRef.current) cyRef.current.fit(undefined, 40); };
31
+ const relayout = () => {
32
+ if (!cyRef.current) return;
33
+ try { cyRef.current.layout({ name: 'fcose', animate: true, quality: 'default', nodeRepulsion: 4500 }).run(); }
34
+ catch (e) { cyRef.current.layout({ name: 'cose', animate: true }).run(); }
35
+ };
36
+
37
+ const loadGraph = async () => {
38
+ try {
39
+ const qs = `?min_weight=${minWeight}&include_non_fact=${includeNonFact ? 1 : 0}`;
40
+ const data = await api.get('/api/memory/graph' + qs);
41
+ setStats(data.stats || {});
42
+
43
+ const elements = [
44
+ ...data.nodes.map(n => ({
45
+ data: {
46
+ id: n.id,
47
+ label: n.label || n.id,
48
+ kind: n.kind,
49
+ category: n.category || "general",
50
+ relevance: n.relevance || 0,
51
+ },
52
+ })),
53
+ ...data.edges.map((e, i) => ({
54
+ data: {
55
+ id: `e${i}`,
56
+ source: e.src,
57
+ target: e.dst,
58
+ type: e.type,
59
+ weight: e.weight,
60
+ },
61
+ })),
62
+ ];
63
+
64
+ if (!cyRef.current && containerRef.current && window.cytoscape) {
65
+ cyRef.current = window.cytoscape({
66
+ container: containerRef.current,
67
+ elements,
68
+ style: [
69
+ {
70
+ selector: 'node',
71
+ style: {
72
+ 'background-color': ele => {
73
+ const kind = ele.data('kind');
74
+ if (kind === 'fact') return CAT_COLORS_GRAPH[ele.data('category')] || CAT_COLORS_GRAPH.general;
75
+ if (kind === 'file') return '#5f6368';
76
+ return '#3c4043';
77
+ },
78
+ 'label': 'data(label)',
79
+ 'color': '#e8eaed',
80
+ 'font-size': 9,
81
+ 'text-wrap': 'ellipsis',
82
+ 'text-max-width': 120,
83
+ 'text-valign': 'bottom',
84
+ 'text-margin-y': 4,
85
+ 'width': ele => 12 + Math.min(20, (ele.data('relevance') || 0) * 2),
86
+ 'height': ele => 12 + Math.min(20, (ele.data('relevance') || 0) * 2),
87
+ 'border-width': 1,
88
+ 'border-color': '#1a1a1a',
89
+ },
90
+ },
91
+ {
92
+ selector: 'node:selected',
93
+ style: { 'border-width': 3, 'border-color': '#ffd54f' },
94
+ },
95
+ {
96
+ selector: 'edge',
97
+ style: {
98
+ 'curve-style': 'bezier',
99
+ 'width': ele => Math.max(0.5, Math.min(4, (ele.data('weight') || 1) * 1.2)),
100
+ 'line-color': ele => ele.data('type') === 'co_recalled' ? '#4c8bf5' : '#9aa0a6',
101
+ 'opacity': 0.55,
102
+ 'target-arrow-shape': 'none',
103
+ },
104
+ },
105
+ ],
106
+ });
107
+ cyRef.current.on('tap', 'node', evt => {
108
+ const id = evt.target.data('id');
109
+ setSelected(id);
110
+ if (onSelectFact) onSelectFact(id);
111
+ });
112
+ } else if (cyRef.current) {
113
+ cyRef.current.elements().remove();
114
+ cyRef.current.add(elements);
115
+ }
116
+
117
+ if (cyRef.current && elements.length) {
118
+ try {
119
+ cyRef.current.layout({ name: 'fcose', animate: false, quality: 'default', nodeRepulsion: 4500 }).run();
120
+ } catch (e) {
121
+ cyRef.current.layout({ name: 'cose', animate: false }).run();
122
+ }
123
+ }
124
+ setLoading(false);
125
+ } catch (e) {
126
+ setLoading(false);
127
+ }
128
+ };
129
+
130
+ useEffect(() => { loadGraph(); }, [minWeight, includeNonFact]);
131
+ useEffect(() => () => { if (cyRef.current) { cyRef.current.destroy(); cyRef.current = null; } }, []);
132
+
133
+ return (
134
+ <div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: 14, display: "flex", flexDirection: "column", gap: 10 }}>
135
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 10 }}>
136
+ <div style={{ fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1 }}>
137
+ Memory Graph
138
+ {stats && (
139
+ <span className="mono" style={{ fontSize: 10, color: T.textDim, marginLeft: 10, textTransform: "none", letterSpacing: 0 }}>
140
+ {stats.total_nodes} nodes · {stats.total_edges} edges · {stats.clusters} clusters
141
+ </span>
142
+ )}
143
+ </div>
144
+ <div style={{ display: "flex", gap: 12, alignItems: "center", fontSize: 11, color: T.textMuted }}>
145
+ <label style={{ display: "flex", alignItems: "center", gap: 6 }}>
146
+ min-weight
147
+ <input type="range" min="0" max="5" step="0.1" value={minWeight}
148
+ onChange={e => setMinWeight(parseFloat(e.target.value))} />
149
+ <span className="mono" style={{ width: 28 }}>{minWeight.toFixed(1)}</span>
150
+ </label>
151
+ <label style={{ display: "flex", alignItems: "center", gap: 4 }}>
152
+ <input type="checkbox" checked={includeNonFact}
153
+ onChange={e => setIncludeNonFact(e.target.checked)} />
154
+ files/symbols
155
+ </label>
156
+ <Btn color={T.blue} onClick={loadGraph}><I name="refresh" size={12} /> Reload</Btn>
157
+ <Btn color={T.accent} onClick={relayout}><I name="git-branch" size={12} /> Relayout</Btn>
158
+ <Btn color={T.textMuted} onClick={fitGraph}><I name="search" size={12} /> Fit</Btn>
159
+ <button onClick={() => setShowHelp(!showHelp)}
160
+ title="Toggle legend & help"
161
+ style={{ background: "none", border: `1px solid ${T.border}`, color: T.textMuted, padding: "4px 8px", borderRadius: 4, cursor: "pointer", fontSize: 11 }}>
162
+ {showHelp ? "Hide help" : "Show help"}
163
+ </button>
164
+ </div>
165
+ </div>
166
+
167
+ {showHelp && (
168
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, padding: 12, background: T.surfaceAlt, borderRadius: 6, border: `1px solid ${T.border}` }}>
169
+ <div>
170
+ <div style={{ fontSize: 10, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1, marginBottom: 8 }}>
171
+ Node colors (fact category)
172
+ </div>
173
+ <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
174
+ {GRAPH_LEGEND.map(g => (
175
+ <div key={g.key} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 11, color: T.text }}>
176
+ <span style={{ width: 12, height: 12, borderRadius: "50%", background: CAT_COLORS_GRAPH[g.key], border: "1px solid #1a1a1a", flexShrink: 0 }} />
177
+ <span style={{ fontWeight: 600, minWidth: 90 }}>{g.label}</span>
178
+ <span style={{ color: T.textMuted }}>{g.desc}</span>
179
+ </div>
180
+ ))}
181
+ <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 11, color: T.text, marginTop: 6 }}>
182
+ <span style={{ width: 12, height: 12, borderRadius: "50%", background: "#5f6368", border: "1px solid #1a1a1a" }} />
183
+ <span style={{ fontWeight: 600, minWidth: 90 }}>File / symbol</span>
184
+ <span style={{ color: T.textMuted }}>shown when "files/symbols" toggled</span>
185
+ </div>
186
+ </div>
187
+ <div style={{ fontSize: 10, color: T.textDim, marginTop: 8, lineHeight: 1.5 }}>
188
+ <strong style={{ color: T.textMuted }}>Node size</strong> = recall count (how often the fact has been retrieved).
189
+ Bigger = more relevant.
190
+ </div>
191
+ </div>
192
+ <div>
193
+ <div style={{ fontSize: 10, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1, marginBottom: 8 }}>
194
+ Edges
195
+ </div>
196
+ <div style={{ display: "flex", flexDirection: "column", gap: 6, fontSize: 11, color: T.text }}>
197
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
198
+ <span style={{ width: 26, height: 2, background: "#4c8bf5" }} />
199
+ <span><strong>co-recalled</strong> — facts retrieved together</span>
200
+ </div>
201
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
202
+ <span style={{ width: 26, height: 2, background: "#9aa0a6" }} />
203
+ <span><strong>touches / other</strong> — file or symbol links</span>
204
+ </div>
205
+ <div style={{ color: T.textDim, fontSize: 10, marginTop: 4, lineHeight: 1.5 }}>
206
+ <strong style={{ color: T.textMuted }}>Thickness</strong> = edge weight (decays over time, strengthens with co-recall).
207
+ Use <strong style={{ color: T.textMuted }}>min-weight</strong> slider to hide weak/stale links.
208
+ </div>
209
+ </div>
210
+ <div style={{ fontSize: 10, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1, margin: "12px 0 6px" }}>
211
+ Interactions
212
+ </div>
213
+ <ul style={{ margin: 0, padding: "0 0 0 16px", fontSize: 10, color: T.textDim, lineHeight: 1.6 }}>
214
+ <li><strong style={{ color: T.textMuted }}>Click node</strong> → open fact + neighbors in side panel</li>
215
+ <li><strong style={{ color: T.textMuted }}>Drag</strong> node to reposition · <strong style={{ color: T.textMuted }}>drag canvas</strong> to pan</li>
216
+ <li><strong style={{ color: T.textMuted }}>Scroll</strong> to zoom · <strong style={{ color: T.textMuted }}>Fit</strong> recenters view</li>
217
+ <li><strong style={{ color: T.textMuted }}>Relayout</strong> re-runs force-directed layout</li>
218
+ <li><strong style={{ color: T.textMuted }}>Ground</strong> (side panel) verifies a fact against current code</li>
219
+ </ul>
220
+ </div>
221
+ </div>
222
+ )}
223
+
224
+ <div
225
+ ref={containerRef}
226
+ style={{ width: "100%", height: 520, background: T.surfaceAlt, borderRadius: 6, border: `1px solid ${T.border}` }}
227
+ />
228
+ {loading && <div style={{ fontSize: 11, color: T.textDim }}>Loading graph...</div>}
229
+ {stats && stats.total_nodes === 0 && !loading && (
230
+ <div style={{ padding: 16, textAlign: "center", color: T.textMuted, fontSize: 12 }}>
231
+ No graph edges yet. Co-recall edges form after facts are retrieved together.
232
+ </div>
233
+ )}
234
+ </div>
235
+ );
236
+ };
237
+
238
+ const Memory = () => {
239
+ const [view, setView] = useState("list");
240
+ const [selectedFactId, setSelectedFactId] = useState(null);
241
+ const [selectedDetail, setSelectedDetail] = useState(null);
242
+
243
+ useEffect(() => {
244
+ if (!selectedFactId) { setSelectedDetail(null); return; }
245
+ api.get(`/api/memory/fact/${selectedFactId}`)
246
+ .then(d => setSelectedDetail(d))
247
+ .catch(() => setSelectedDetail(null));
248
+ }, [selectedFactId]);
249
+ const [facts, setFacts] = useState([]);
250
+ const [loading, setLoading] = useState(true);
251
+ const [newFact, setNewFact] = useState("");
252
+ const [category, setCategory] = useState("general");
253
+ const [storing, setStoring] = useState(false);
254
+ const [searchQuery, setSearchQuery] = useState("");
255
+ const [searchResults, setSearchResults] = useState(null);
256
+ const [searching, setSearching] = useState(false);
257
+ const [decisions, setDecisions] = useState([]);
258
+ const [decisionsExpanded, setDecisionsExpanded] = useState(false);
259
+
260
+ const categories = ["general", "architecture", "convention", "bug", "preference"];
261
+ const catColors = {
262
+ general: T.textMuted,
263
+ architecture: T.blue,
264
+ convention: T.purple,
265
+ bug: T.error,
266
+ preference: T.warn,
267
+ };
268
+
269
+ const loadFacts = () => {
270
+ api.get('/api/memory/facts')
271
+ .then(f => { setFacts(f); setLoading(false); })
272
+ .catch(() => setLoading(false));
273
+ };
274
+
275
+ const loadDecisions = () => {
276
+ api.get('/api/activity?type=decision&limit=50')
277
+ .then(d => setDecisions(d))
278
+ .catch(() => {});
279
+ };
280
+
281
+ useEffect(() => {
282
+ loadFacts();
283
+ loadDecisions();
284
+ const iv = setInterval(() => { loadFacts(); loadDecisions(); }, 5000);
285
+ return () => clearInterval(iv);
286
+ }, []);
287
+
288
+ const handleRemember = async () => {
289
+ if (!newFact.trim()) return;
290
+ setStoring(true);
291
+ try {
292
+ await api.post('/api/memory/remember', { fact: newFact, category });
293
+ setNewFact("");
294
+ loadFacts();
295
+ } catch (e) {}
296
+ setStoring(false);
297
+ };
298
+
299
+ const handleSearch = async () => {
300
+ if (!searchQuery.trim()) return;
301
+ setSearching(true);
302
+ try {
303
+ const r = await api.post('/api/memory/recall', { query: searchQuery, top_k: 10 });
304
+ setSearchResults(r);
305
+ } catch (e) {}
306
+ setSearching(false);
307
+ };
308
+
309
+ const handleDelete = async (id) => {
310
+ await api.del(`/api/memory/facts/${id}`);
311
+ loadFacts();
312
+ if (searchResults) {
313
+ setSearchResults(searchResults.filter(f => f.id !== id));
314
+ }
315
+ };
316
+
317
+ const [exportMsg, setExportMsg] = useState(null);
318
+ const handleExport = async () => {
319
+ try {
320
+ const r = await api.get('/api/memory/export');
321
+ await navigator.clipboard.writeText(r.markdown);
322
+ setExportMsg(`Copied ${r.count} facts as markdown`);
323
+ setTimeout(() => setExportMsg(null), 3000);
324
+ } catch (e) {
325
+ setExportMsg("Export failed");
326
+ setTimeout(() => setExportMsg(null), 3000);
327
+ }
328
+ };
329
+
330
+ const totalRecalls = facts.reduce((s, f) => s + (f.relevance_count || 0), 0);
331
+
332
+ // Group facts by category
333
+ const grouped = {};
334
+ facts.forEach(f => {
335
+ const cat = f.category || "general";
336
+ if (!grouped[cat]) grouped[cat] = [];
337
+ grouped[cat].push(f);
338
+ });
339
+
340
+ return (
341
+ <div className="fade-up" style={{ display: "flex", flexDirection: "column", gap: 16 }}>
342
+
343
+ {/* Stats row */}
344
+ <div style={{ display: "flex", gap: 12, flexWrap: "wrap", alignItems: "center" }}>
345
+ <StatBox label="Stored Facts" value={facts.length} color={T.purple} loading={loading} />
346
+ <StatBox label="Total Recalls" value={totalRecalls} sub="relevance score sum" color={T.accent} loading={loading} />
347
+ <StatBox label="Decisions" value={decisions.length} sub="from sessions" color={T.blue} />
348
+ <div style={{ marginLeft: "auto", display: "flex", gap: 4, padding: 3, background: T.surfaceAlt, borderRadius: 6, border: `1px solid ${T.border}` }}>
349
+ {["list", "graph"].map(v => (
350
+ <button key={v} onClick={() => setView(v)}
351
+ style={{
352
+ padding: "6px 12px", borderRadius: 4, border: "none", cursor: "pointer",
353
+ background: view === v ? T.surface : "transparent",
354
+ color: view === v ? T.text : T.textMuted,
355
+ fontSize: 11, textTransform: "uppercase", letterSpacing: 1, fontWeight: 600,
356
+ }}>
357
+ <I name={v === "graph" ? "git-branch" : "list"} size={11} /> {v}
358
+ </button>
359
+ ))}
360
+ </div>
361
+ </div>
362
+
363
+ {view === "graph" && (
364
+ <div style={{ display: "grid", gridTemplateColumns: selectedDetail ? "1fr 320px" : "1fr", gap: 12 }}>
365
+ <MemoryGraph onSelectFact={setSelectedFactId} />
366
+ {selectedDetail && (
367
+ <div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: 14, height: "fit-content" }}>
368
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
369
+ <Badge color={catColors[selectedDetail.fact?.category] || T.textMuted}>
370
+ {selectedDetail.fact?.category || "general"}
371
+ </Badge>
372
+ <button onClick={() => setSelectedFactId(null)} style={{ background: "none", border: "none", cursor: "pointer", color: T.textMuted }}>
373
+ <I name="x" size={14} />
374
+ </button>
375
+ </div>
376
+ <div style={{ fontSize: 13, color: T.text, lineHeight: 1.5, marginBottom: 10 }}>
377
+ {selectedDetail.fact?.fact}
378
+ </div>
379
+ <div className="mono" style={{ fontSize: 10, color: T.textDim, marginBottom: 12 }}>
380
+ recalls: {selectedDetail.fact?.relevance_count} · confidence: {(selectedDetail.fact?.confidence ?? 1).toFixed(2)}
381
+ </div>
382
+ <div style={{ fontSize: 10, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1, marginBottom: 6 }}>
383
+ Neighbors ({selectedDetail.neighbors?.length || 0})
384
+ </div>
385
+ <div style={{ display: "flex", flexDirection: "column", gap: 4, maxHeight: 260, overflowY: "auto" }}>
386
+ {(selectedDetail.neighbors || []).map(n => (
387
+ <div key={n.id} onClick={() => n.kind === "fact" && setSelectedFactId(n.id)}
388
+ style={{ padding: "6px 8px", borderRadius: 4, background: T.surfaceAlt, cursor: n.kind === "fact" ? "pointer" : "default", fontSize: 11, color: T.text }}>
389
+ <div>{n.label}</div>
390
+ <div className="mono" style={{ fontSize: 9, color: T.textDim, marginTop: 2 }}>
391
+ {n.type} · w={n.weight?.toFixed(2)}
392
+ </div>
393
+ </div>
394
+ ))}
395
+ {(!selectedDetail.neighbors || selectedDetail.neighbors.length === 0) && (
396
+ <div style={{ fontSize: 11, color: T.textDim, fontStyle: "italic" }}>No neighbors yet.</div>
397
+ )}
398
+ </div>
399
+ <div style={{ display: "flex", gap: 6, marginTop: 12 }}>
400
+ <Btn color={T.accent} onClick={async () => {
401
+ try { const r = await api.post(`/api/memory/ground/${selectedFactId}`, {}); alert(r.grounded ? "Grounded ✓" : `Issues: ${(r.issues || []).join(", ")}`); } catch (e) {}
402
+ }}><I name="check" size={12} /> Ground</Btn>
403
+ <Btn color={T.error} onClick={async () => {
404
+ if (!confirm("Delete fact?")) return;
405
+ await api.del(`/api/memory/facts/${selectedFactId}`);
406
+ setSelectedFactId(null);
407
+ loadFacts();
408
+ }}><I name="trash" size={12} /> Delete</Btn>
409
+ </div>
410
+ </div>
411
+ )}
412
+ </div>
413
+ )}
414
+
415
+ {view === "list" && <>
416
+
417
+
418
+ {/* Remember form */}
419
+ <div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: 18 }}>
420
+ <div style={{ fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1, marginBottom: 12 }}>
421
+ Remember a Fact
422
+ </div>
423
+ <div style={{ display: "flex", gap: 10, flexWrap: "wrap", alignItems: "flex-end" }}>
424
+ <div style={{ flex: 1, minWidth: 250 }}>
425
+ <input
426
+ value={newFact}
427
+ onChange={e => setNewFact(e.target.value)}
428
+ onKeyDown={e => e.key === "Enter" && handleRemember()}
429
+ placeholder="Enter a fact to remember..."
430
+ className="mono"
431
+ style={{
432
+ width: "100%", padding: "9px 12px", borderRadius: 6,
433
+ background: T.surfaceAlt, border: `1px solid ${T.border}`,
434
+ color: T.text, fontSize: 12, outline: "none",
435
+ }}
436
+ />
437
+ </div>
438
+ <select
439
+ value={category}
440
+ onChange={e => setCategory(e.target.value)}
441
+ className="mono"
442
+ style={{
443
+ padding: "9px 12px", borderRadius: 6,
444
+ background: T.surfaceAlt, border: `1px solid ${T.border}`,
445
+ color: T.text, fontSize: 12, outline: "none",
446
+ }}
447
+ >
448
+ {categories.map(c => <option key={c} value={c}>{c}</option>)}
449
+ </select>
450
+ <Btn color={T.purple} onClick={handleRemember} disabled={!newFact.trim() || storing}>
451
+ <I name="bookmark" size={14} /> {storing ? "Storing..." : "Remember"}
452
+ </Btn>
453
+ </div>
454
+ </div>
455
+
456
+ {/* Search */}
457
+ <div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: 18 }}>
458
+ <div style={{ fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1, marginBottom: 12 }}>
459
+ Search Facts
460
+ </div>
461
+ <div style={{ display: "flex", gap: 10 }}>
462
+ <div style={{
463
+ flex: 1, display: "flex", alignItems: "center", gap: 8,
464
+ padding: "0 14px", borderRadius: 6,
465
+ background: T.surfaceAlt, border: `1px solid ${T.border}`,
466
+ }}>
467
+ <I name="search" size={14} color={T.textMuted} />
468
+ <input
469
+ value={searchQuery}
470
+ onChange={e => setSearchQuery(e.target.value)}
471
+ onKeyDown={e => e.key === "Enter" && handleSearch()}
472
+ placeholder="Search stored facts..."
473
+ className="mono"
474
+ style={{
475
+ flex: 1, padding: "10px 0", background: "transparent",
476
+ border: "none", color: T.text, fontSize: 13, outline: "none",
477
+ }}
478
+ />
479
+ </div>
480
+ <Btn color={T.blue} onClick={handleSearch} disabled={searching || !searchQuery.trim()}>
481
+ <I name="search" size={14} /> {searching ? "Searching..." : "Search"}
482
+ </Btn>
483
+ </div>
484
+ {searchResults && (
485
+ <div style={{ marginTop: 12, display: "flex", flexDirection: "column", gap: 6 }}>
486
+ {searchResults.length === 0 && (
487
+ <div style={{ padding: 16, textAlign: "center", color: T.textMuted, fontSize: 13 }}>
488
+ No matching facts found.
489
+ </div>
490
+ )}
491
+ {searchResults.map((f, i) => (
492
+ <div
493
+ key={f.id}
494
+ style={{
495
+ display: "flex", alignItems: "center", gap: 10,
496
+ padding: "10px 12px", borderRadius: 6,
497
+ background: T.surfaceAlt, border: `1px solid ${T.border}`,
498
+ }}
499
+ >
500
+ <div style={{ flex: 1 }}>
501
+ <div style={{ fontSize: 13, color: T.text, marginBottom: 4 }}>{f.fact}</div>
502
+ <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
503
+ <Badge color={catColors[f.category] || T.textMuted}>{f.category}</Badge>
504
+ <span className="mono" style={{ fontSize: 10, color: T.textDim }}>recalls: {f.relevance_count}</span>
505
+ {f.score !== undefined && <Badge color={T.accent}>score: {f.score}</Badge>}
506
+ </div>
507
+ </div>
508
+ <button onClick={() => handleDelete(f.id)} style={{ background: "none", border: "none", cursor: "pointer", padding: 4 }}>
509
+ <I name="trash" size={14} color={T.error} />
510
+ </button>
511
+ </div>
512
+ ))}
513
+ </div>
514
+ )}
515
+ </div>
516
+
517
+ {/* Decisions (collapsible) */}
518
+ {decisions.length > 0 && (
519
+ <div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, overflow: "hidden" }}>
520
+ <div
521
+ onClick={() => setDecisionsExpanded(!decisionsExpanded)}
522
+ style={{
523
+ display: "flex", alignItems: "center", justifyContent: "space-between",
524
+ padding: "12px 18px", background: T.surfaceAlt, cursor: "pointer",
525
+ borderBottom: decisionsExpanded ? `1px solid ${T.border}` : "none",
526
+ }}
527
+ >
528
+ <span style={{
529
+ fontSize: 12, fontWeight: 600, color: T.textMuted,
530
+ textTransform: "uppercase", letterSpacing: 1,
531
+ display: "flex", alignItems: "center", gap: 6,
532
+ }}>
533
+ <I name="brain" size={13} color={T.blue} /> Decisions
534
+ <Badge color={T.blue}>{decisions.length}</Badge>
535
+ </span>
536
+ <I
537
+ name="chevron"
538
+ size={14}
539
+ color={T.textMuted}
540
+ style={{ transform: decisionsExpanded ? "rotate(90deg)" : "none", transition: "transform 0.15s" }}
541
+ />
542
+ </div>
543
+ {decisionsExpanded && (
544
+ <div style={{ padding: 14, display: "flex", flexDirection: "column", gap: 6 }}>
545
+ {decisions.map((d, i) => (
546
+ <div
547
+ key={i}
548
+ style={{
549
+ display: "flex", gap: 10, padding: "10px 12px",
550
+ borderRadius: 6, background: T.surfaceAlt,
551
+ border: `1px solid ${T.border}20`,
552
+ }}
553
+ >
554
+ <GlowDot color={T.blue} size={6} />
555
+ <div style={{ flex: 1 }}>
556
+ <div style={{ fontSize: 12, color: T.text, lineHeight: 1.5 }}>{d.decision}</div>
557
+ {d.reasoning && (
558
+ <div style={{ fontSize: 11, color: T.textMuted, marginTop: 4, fontStyle: "italic" }}>
559
+ {d.reasoning}
560
+ </div>
561
+ )}
562
+ <div className="mono" style={{ fontSize: 10, color: T.textDim, marginTop: 4 }}>
563
+ {timeAgo(d.timestamp)}
564
+ </div>
565
+ </div>
566
+ </div>
567
+ ))}
568
+ </div>
569
+ )}
570
+ </div>
571
+ )}
572
+
573
+ {/* All facts grouped by category */}
574
+ <div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: 18 }}>
575
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 14 }}>
576
+ <div style={{ fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1 }}>
577
+ All Facts
578
+ </div>
579
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
580
+ {exportMsg && <span style={{ fontSize: 11, color: T.accent }}>{exportMsg}</span>}
581
+ <Btn color={T.purple} onClick={handleExport} disabled={facts.length === 0}>
582
+ <I name="copy" size={13} /> Export Markdown
583
+ </Btn>
584
+ </div>
585
+ </div>
586
+ {facts.length === 0 && !loading && (
587
+ <div style={{ padding: 20, textAlign: "center", color: T.textMuted, fontSize: 13 }}>
588
+ No facts stored yet. Use the form above or the MCP remember tool.
589
+ </div>
590
+ )}
591
+ {Object.entries(grouped).map(([cat, items]) => (
592
+ <div key={cat} style={{ marginBottom: 14 }}>
593
+ <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
594
+ <Badge color={catColors[cat] || T.textMuted}>{cat}</Badge>
595
+ <span style={{ fontSize: 11, color: T.textDim }}>({items.length})</span>
596
+ </div>
597
+ <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
598
+ {items.map(f => (
599
+ <div
600
+ key={f.id}
601
+ style={{
602
+ display: "flex", alignItems: "center", gap: 10,
603
+ padding: "8px 12px", borderRadius: 6, background: T.surfaceAlt,
604
+ }}
605
+ >
606
+ <div style={{ flex: 1 }}>
607
+ <div style={{ fontSize: 12, color: T.text }}>{f.fact}</div>
608
+ <div className="mono" style={{ fontSize: 10, color: T.textDim, marginTop: 2 }}>
609
+ {localDate(f.timestamp)} | recalls: {f.relevance_count}
610
+ {f.source_session && <> | session: {f.source_session.slice(0, 8)}</>}
611
+ </div>
612
+ </div>
613
+ <button onClick={() => handleDelete(f.id)} style={{ background: "none", border: "none", cursor: "pointer", padding: 4 }}>
614
+ <I name="trash" size={14} color={T.error} />
615
+ </button>
616
+ </div>
617
+ ))}
618
+ </div>
619
+ </div>
620
+ ))}
621
+ </div>
622
+ </>}
623
+
624
+ </div>
625
+ );
626
+ };