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,1404 @@
1
+ // ─── SettingsPanel ────────────────────────
2
+ // Globals: T, I, GlowDot, Badge, StatBox, Btn, Section, api, timeAgo, renderBoolToggle, useState, useEffect, useCallback
3
+
4
+ const SettingsPanel = ({ stats }) => {
5
+ // ── Core state ──
6
+ const [msg, setMsg] = useState("");
7
+ const [rebuilding, setRebuilding] = useState(false);
8
+ const [generating, setGenerating] = useState(false);
9
+
10
+ // ── Budget ──
11
+ const [budgetCfg, setBudgetCfg] = useState(null);
12
+ const [savingBudget, setSavingBudget] = useState(false);
13
+
14
+ // ── Hybrid / Feature Flags ──
15
+ const [hybridCfg, setHybridCfg] = useState(null);
16
+
17
+ // ── Agents ──
18
+ const [agentsCfg, setAgentsCfg] = useState(null);
19
+ const [agentsStatus, setAgentsStatus] = useState({});
20
+ const [runningAgent, setRunningAgent] = useState("");
21
+
22
+ // ── Delegate ──
23
+ const [delegateCfg, setDelegateCfg] = useState(null);
24
+
25
+ // ── Proxy ──
26
+ const [proxyCfg, setProxyCfg] = useState(null);
27
+
28
+ // ── MCP ──
29
+ const [mcpStatus, setMcpStatus] = useState(null);
30
+ const [mcpIde, setMcpIde] = useState("auto");
31
+ const [installIde, setInstallIde] = useState("auto");
32
+ const [installMcpMode, setInstallMcpMode] = useState("direct");
33
+ const [installing, setInstalling] = useState(false);
34
+ const [showAddMcp, setShowAddMcp] = useState(false);
35
+ const [newMcpName, setNewMcpName] = useState("");
36
+ const [newMcpCmd, setNewMcpCmd] = useState("");
37
+ const [newMcpArgs, setNewMcpArgs] = useState("");
38
+
39
+ // ── Permissions ──
40
+ const [permsCfg, setPermsCfg] = useState(null);
41
+ const [savingPerms, setSavingPerms] = useState(false);
42
+
43
+ // ── Shared ──
44
+ const [ollamaModels, setOllamaModels] = useState([]);
45
+ const [busy, setBusyState] = useState({ agents: false, delegate: false, proxy: false });
46
+
47
+ // ── Project Data ──
48
+ const [dataSummary, setDataSummary] = useState(null);
49
+ const [dataLoading, setDataLoading] = useState(false);
50
+ const [dataWorking, setDataWorking] = useState(null);
51
+ const [dataConfirm, setDataConfirm] = useState(null);
52
+ const [dataMsg, setDataMsg] = useState(null);
53
+
54
+ // ── Section open state ──
55
+ const [sections, setSections] = useState({
56
+ project: true,
57
+ budget: false,
58
+ features: false,
59
+ editLedger: false,
60
+ agents: false,
61
+ delegate: false,
62
+ codex: false,
63
+ gemini: false,
64
+ workflows: false,
65
+ proxy: false,
66
+ mcp: false,
67
+ permissions: false,
68
+ data: false,
69
+ });
70
+
71
+ const toggleSection = (key) => setSections(prev => ({ ...prev, [key]: !prev[key] }));
72
+
73
+ // ── Helpers ──
74
+ const flashMsg = (text, delay = 3000) => {
75
+ setMsg(text);
76
+ if (delay) setTimeout(() => setMsg(""), delay);
77
+ };
78
+
79
+ const setBusy = (key, value) => setBusyState(prev => ({ ...prev, [key]: value }));
80
+
81
+ const inputStyle = {
82
+ width: "100%",
83
+ background: T.surfaceAlt,
84
+ border: `1px solid ${T.border}`,
85
+ borderRadius: 4,
86
+ padding: "5px 8px",
87
+ color: T.text,
88
+ fontFamily: "'JetBrains Mono', monospace",
89
+ fontSize: 12,
90
+ outline: "none",
91
+ };
92
+ const labelStyle = {
93
+ fontSize: 10,
94
+ color: T.textDim,
95
+ textTransform: "uppercase",
96
+ letterSpacing: 0.8,
97
+ marginBottom: 4,
98
+ };
99
+
100
+ const renderModelOptions = () => (
101
+ <>
102
+ <option value="">Auto-select</option>
103
+ {ollamaModels.map(m => <option key={m} value={m}>{m}</option>)}
104
+ </>
105
+ );
106
+
107
+ // ── Load functions ──
108
+ const loadMcpStatus = useCallback(async (ide = mcpIde) => {
109
+ try {
110
+ const q = ide && ide !== "auto" ? `?ide=${encodeURIComponent(ide)}` : "";
111
+ const s = await api.get(`/api/mcp/status${q}`);
112
+ setMcpStatus(s);
113
+ } catch (e) { }
114
+ }, [mcpIde]);
115
+
116
+ const loadAgentStatus = useCallback(async () => {
117
+ try {
118
+ const s = await api.get('/api/agents/status');
119
+ const mapped = {};
120
+ for (const item of (s?.agents || [])) mapped[item.name] = item;
121
+ setAgentsStatus(mapped);
122
+ } catch (e) { }
123
+ }, []);
124
+
125
+ const loadDataSummary = useCallback(async () => {
126
+ setDataLoading(true);
127
+ try { const s = await api.get('/api/data/summary'); setDataSummary(s); } catch (e) { }
128
+ setDataLoading(false);
129
+ }, []);
130
+
131
+ // ── Mount load ──
132
+ useEffect(() => {
133
+ const init = async () => {
134
+ try {
135
+ const [hybrid, budget, agents, delegate, proxy, models, perms] = await Promise.all([
136
+ api.get('/api/hybrid/config').catch(() => null),
137
+ api.get('/api/budget/config').catch(() => null),
138
+ api.get('/api/agents/config').catch(() => null),
139
+ api.get('/api/delegate/config').catch(() => null),
140
+ api.get('/api/proxy/config').catch(() => null),
141
+ api.get('/api/ollama/models').catch(() => ({ models: [] })),
142
+ api.get('/api/permissions').catch(() => null),
143
+ ]);
144
+ if (hybrid) setHybridCfg(hybrid);
145
+ if (budget) setBudgetCfg(budget);
146
+ if (agents) setAgentsCfg(agents);
147
+ if (delegate) setDelegateCfg(delegate);
148
+ if (proxy) setProxyCfg(proxy);
149
+ setOllamaModels(models?.models || []);
150
+ if (perms) setPermsCfg(perms);
151
+ } catch (e) { }
152
+ };
153
+ init();
154
+ loadMcpStatus();
155
+ loadAgentStatus();
156
+ loadDataSummary();
157
+ }, []);
158
+
159
+ // ── MCP ide watcher ──
160
+ useEffect(() => { loadMcpStatus(mcpIde); }, [mcpIde]);
161
+
162
+ // ── Agent status auto-refresh ──
163
+ useEffect(() => {
164
+ const iv = setInterval(loadAgentStatus, 15000);
165
+ return () => clearInterval(iv);
166
+ }, [loadAgentStatus]);
167
+
168
+ // ── Hybrid flag toggle ──
169
+ const toggleHybridFlag = async (flag) => {
170
+ if (!hybridCfg) return;
171
+ const next = !hybridCfg[flag];
172
+ try {
173
+ const updated = await api.put('/api/hybrid/config', { [flag]: next });
174
+ setHybridCfg(updated || {});
175
+ } catch (e) { flashMsg(`✗ Toggle ${flag}: ${e.message}`); }
176
+ };
177
+
178
+ // ── Budget save ──
179
+ const saveBudget = async () => {
180
+ if (!budgetCfg) return;
181
+ setSavingBudget(true);
182
+ try {
183
+ const updated = await api.put('/api/budget/config', budgetCfg);
184
+ setBudgetCfg(updated || {});
185
+ flashMsg("✓ Saved budget settings");
186
+ } catch (e) { flashMsg(`✗ Save budget: ${e.message}`); }
187
+ setSavingBudget(false);
188
+ };
189
+
190
+ // ── Agent field update ──
191
+ const updateAgentField = (agentName, key, value) => {
192
+ setAgentsCfg(prev => ({
193
+ ...(prev || {}),
194
+ [agentName]: { ...(prev?.[agentName] || {}), [key]: value },
195
+ }));
196
+ };
197
+
198
+ const saveAgent = async (agentName) => {
199
+ if (!agentsCfg?.[agentName]) return;
200
+ setBusy("agents", true);
201
+ try {
202
+ const updated = await api.put('/api/agents/config', { [agentName]: agentsCfg[agentName] });
203
+ setAgentsCfg(updated || {});
204
+ await loadAgentStatus();
205
+ flashMsg(`✓ Saved ${agentName}`);
206
+ } catch (e) { flashMsg(`✗ Save agent: ${e.message}`); }
207
+ setBusy("agents", false);
208
+ };
209
+
210
+ const runAgentNow = async (agentName) => {
211
+ setRunningAgent(agentName);
212
+ try {
213
+ await api.post(`/api/agents/run/${encodeURIComponent(agentName)}`, {});
214
+ await loadAgentStatus();
215
+ flashMsg(`✓ Ran ${agentName}`);
216
+ } catch (e) { flashMsg(`✗ Run agent: ${e.message}`); }
217
+ setRunningAgent("");
218
+ };
219
+
220
+ // ── Delegate field update ──
221
+ const updateDelegateField = (key, value) => {
222
+ setDelegateCfg(prev => ({ ...(prev || {}), [key]: value }));
223
+ };
224
+
225
+ const saveDelegate = async () => {
226
+ if (!delegateCfg) return;
227
+ setBusy("delegate", true);
228
+ try {
229
+ const updated = await api.put('/api/delegate/config', delegateCfg);
230
+ setDelegateCfg(updated || {});
231
+ flashMsg("✓ Saved delegate settings");
232
+ } catch (e) { flashMsg(`✗ Save delegate: ${e.message}`); }
233
+ setBusy("delegate", false);
234
+ };
235
+
236
+ // ── Proxy field update ──
237
+ const updateProxyField = (key, value) => {
238
+ setProxyCfg(prev => ({ ...(prev || {}), [key]: value }));
239
+ };
240
+
241
+ const saveProxy = async () => {
242
+ if (!proxyCfg) return;
243
+ setBusy("proxy", true);
244
+ try {
245
+ const payload = {
246
+ ...proxyCfg,
247
+ always_visible: Array.isArray(proxyCfg.always_visible)
248
+ ? proxyCfg.always_visible
249
+ : String(proxyCfg.always_visible || "").split(",").map(v => v.trim()).filter(Boolean),
250
+ };
251
+ const updated = await api.put('/api/proxy/config', payload);
252
+ setProxyCfg(updated || {});
253
+ flashMsg("✓ Saved proxy settings");
254
+ } catch (e) { flashMsg(`✗ Save proxy: ${e.message}`); }
255
+ setBusy("proxy", false);
256
+ };
257
+
258
+ // ── Permissions ──
259
+ const applyPermTier = async (tier) => {
260
+ setSavingPerms(true);
261
+ try {
262
+ const res = await api.put('/api/permissions', { tier });
263
+ // Reload full state after apply
264
+ const updated = await api.get('/api/permissions');
265
+ setPermsCfg(updated);
266
+ flashMsg(`✓ Applied '${tier}' permissions — restart Claude Code to activate`);
267
+ } catch (e) { flashMsg(`✗ Permissions: ${e.message}`); }
268
+ setSavingPerms(false);
269
+ };
270
+
271
+ // ── Project Data helpers ──
272
+ const flashData = (text, ok) => {
273
+ setDataMsg({ text, ok });
274
+ setTimeout(() => setDataMsg(null), 3500);
275
+ };
276
+
277
+ const doDataAction = async (key, fn) => {
278
+ setDataWorking(key);
279
+ try {
280
+ const r = await fn();
281
+ flashData(`✓ ${r?.cleared !== undefined ? r.cleared + ' items cleared' : 'Done'}`, true);
282
+ await loadDataSummary();
283
+ } catch (e) { flashData(`✗ ${e.message || 'Failed'}`, false); }
284
+ setDataWorking(null);
285
+ };
286
+
287
+ const confirmThenRun = (key, fn) => {
288
+ if (dataConfirm === key) {
289
+ setDataConfirm(null);
290
+ doDataAction(key, fn);
291
+ } else {
292
+ setDataConfirm(key);
293
+ setTimeout(() => setDataConfirm(c => c === key ? null : c), 3000);
294
+ }
295
+ };
296
+
297
+ // ── Project path / file count from stats ──
298
+ const projectPath = stats?.project_path || stats?.path || "—";
299
+ const filesIndexed = stats?.files_indexed ?? stats?.index?.count ?? "—";
300
+ const indexStatus = stats?.index_status || (stats?.index ? "ready" : "unknown");
301
+ const statusColor = indexStatus === "ready" ? T.accent : indexStatus === "building" ? T.warn : T.textMuted;
302
+
303
+ // ── Data rows ──
304
+ const dataRows = dataSummary ? [
305
+ {
306
+ key: 'index', label: 'Index', icon: 'search', color: T.blue,
307
+ count: `${dataSummary.index?.count ?? 0} files`, size: dataSummary.index?.size_kb ?? 0,
308
+ action: 'Rebuild',
309
+ onAction: () => doDataAction('index', () => api.post('/api/index/rebuild')),
310
+ },
311
+ {
312
+ key: 'sessions', label: 'Sessions', icon: 'clock', color: T.warn,
313
+ count: dataSummary.sessions?.count ?? 0, size: dataSummary.sessions?.size_kb ?? 0,
314
+ action: 'Keep last 5',
315
+ onAction: () => confirmThenRun('sessions', () => api.delete('/api/data/sessions?keep=5')),
316
+ },
317
+ {
318
+ key: 'cache', label: 'Compression Cache', icon: 'minimize', color: T.purple,
319
+ count: `${dataSummary.cache?.count ?? 0} files`, size: dataSummary.cache?.size_kb ?? 0,
320
+ action: 'Clear',
321
+ onAction: () => confirmThenRun('cache', () => api.delete('/api/data/cache')),
322
+ },
323
+ {
324
+ key: 'snapshots', label: 'Snapshots', icon: 'bookmark', color: T.accent,
325
+ count: dataSummary.snapshots?.count ?? 0, size: dataSummary.snapshots?.size_kb ?? 0,
326
+ action: 'Clear all',
327
+ onAction: () => confirmThenRun('snapshots', () => api.delete('/api/data/snapshots')),
328
+ },
329
+ {
330
+ key: 'file_memory', label: 'File Maps', icon: 'file', color: T.blue,
331
+ count: `${dataSummary.file_memory?.count ?? 0} maps`, size: dataSummary.file_memory?.size_kb ?? 0,
332
+ action: 'Clear',
333
+ onAction: () => confirmThenRun('file_memory', () => api.delete('/api/data/file-memory')),
334
+ },
335
+ {
336
+ key: 'notifications', label: 'Notifications', icon: 'zap', color: T.warn,
337
+ count: dataSummary.notifications?.count ?? 0, size: dataSummary.notifications?.size_kb ?? 0,
338
+ action: 'Clear',
339
+ onAction: () => confirmThenRun('notifications', () => api.delete('/api/data/notifications')),
340
+ },
341
+ {
342
+ key: 'sltm', label: 'SLTM Memory', icon: 'brain', color: T.purple,
343
+ count: `${dataSummary.sltm?.count ?? 0} records`, size: dataSummary.sltm?.size_kb ?? 0,
344
+ },
345
+ ] : [];
346
+
347
+ return (
348
+ <div className="fade-up" style={{ display: "flex", flexDirection: "column", gap: 10 }}>
349
+
350
+ {/* ── Global status message ── */}
351
+ {msg && (
352
+ <div className="mono fade-up" style={{
353
+ padding: "8px 12px", borderRadius: 6, fontSize: 11,
354
+ background: T.surfaceAlt,
355
+ color: msg.startsWith("✓") ? T.accent : T.error,
356
+ border: `1px solid ${msg.startsWith("✓") ? T.accent : T.error}30`,
357
+ }}>
358
+ {msg}
359
+ </div>
360
+ )}
361
+
362
+ {/* ══════════════════════════════════════════
363
+ 1. PROJECT INFO
364
+ ══════════════════════════════════════════ */}
365
+ <Section
366
+ label="Project Info"
367
+ icon="folder"
368
+ color={T.accent}
369
+ open={sections.project}
370
+ onToggle={() => toggleSection("project")}
371
+ >
372
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
373
+ {/* Path + status */}
374
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
375
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
376
+ <span style={{ color: T.textDim, fontSize: 11 }}>Project Path</span>
377
+ <Badge color={statusColor}>
378
+ <GlowDot color={statusColor} size={5} />
379
+ {indexStatus}
380
+ </Badge>
381
+ </div>
382
+ <div className="mono" style={{
383
+ fontSize: 11, color: T.textMuted, background: T.surfaceAlt,
384
+ padding: "6px 8px", borderRadius: 4, wordBreak: "break-all",
385
+ border: `1px solid ${T.border}`,
386
+ }}>
387
+ {projectPath}
388
+ </div>
389
+ </div>
390
+
391
+ {/* Files indexed */}
392
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "6px 0", borderBottom: `1px solid ${T.border}22` }}>
393
+ <span style={{ color: T.textMuted, fontSize: 12 }}>Files Indexed</span>
394
+ <span className="mono" style={{ color: T.text, fontSize: 13, fontWeight: 600 }}>{filesIndexed}</span>
395
+ </div>
396
+
397
+ {/* Quick actions */}
398
+ <div style={{ display: "flex", gap: 8, flexWrap: "wrap", paddingTop: 4 }}>
399
+ <Btn
400
+ color={T.blue}
401
+ onClick={async () => {
402
+ setRebuilding(true);
403
+ try {
404
+ await api.post('/api/index/rebuild');
405
+ flashMsg("✓ Index rebuild started");
406
+ } catch (e) { flashMsg(`✗ Rebuild: ${e.message}`); }
407
+ setRebuilding(false);
408
+ }}
409
+ disabled={rebuilding}
410
+ >
411
+ <I name="refresh" size={13} style={rebuilding ? { animation: "spin 0.6s linear infinite" } : {}} />
412
+ {rebuilding ? "Rebuilding..." : "Rebuild Index"}
413
+ </Btn>
414
+ <Btn
415
+ color={T.accent}
416
+ onClick={async () => {
417
+ setGenerating(true);
418
+ try {
419
+ await api.post('/api/claudemd/sync');
420
+ flashMsg("✓ Instructions saved");
421
+ } catch (e) { flashMsg(`✗ Save: ${e.message}`); }
422
+ setGenerating(false);
423
+ }}
424
+ disabled={generating}
425
+ >
426
+ <I name="save" size={13} />
427
+ {generating ? "Saving..." : "Save Instructions"}
428
+ </Btn>
429
+ </div>
430
+ </div>
431
+ </Section>
432
+
433
+ {/* ══════════════════════════════════════════
434
+ 2. BUDGET & GUIDANCE
435
+ ══════════════════════════════════════════ */}
436
+ <Section
437
+ label="Budget & Guidance"
438
+ icon="zap"
439
+ color={T.warn}
440
+ open={sections.budget}
441
+ onToggle={() => toggleSection("budget")}
442
+ >
443
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
444
+ {renderBoolToggle(
445
+ "Budget Nudges",
446
+ !!hybridCfg?.show_context_nudges,
447
+ () => toggleHybridFlag("show_context_nudges"),
448
+ "Append budget warning when over threshold. Tells the AI when to snapshot."
449
+ )}
450
+ {renderBoolToggle(
451
+ "Agent Alerts",
452
+ !!hybridCfg?.prepend_notifications,
453
+ () => toggleHybridFlag("prepend_notifications"),
454
+ "Prepend critical alerts inline so the AI sees and relays them."
455
+ )}
456
+
457
+ {budgetCfg && (
458
+ <>
459
+ <div style={{ paddingTop: 8, borderTop: `1px solid ${T.border}22`, marginTop: 4 }}>
460
+ <div style={labelStyle}>Budget Threshold (tokens)</div>
461
+ <input
462
+ type="number"
463
+ min="1000"
464
+ value={budgetCfg.threshold ?? ""}
465
+ onChange={e => setBudgetCfg(prev => ({ ...prev, threshold: parseInt(e.target.value || "0", 10) || 0 }))}
466
+ style={inputStyle}
467
+ />
468
+ </div>
469
+ <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 8 }}>
470
+ <Btn color={T.warn} onClick={saveBudget} disabled={savingBudget}>
471
+ <I name="save" size={13} />
472
+ {savingBudget ? "Saving..." : "Save Budget Settings"}
473
+ </Btn>
474
+ </div>
475
+ </>
476
+ )}
477
+ {!budgetCfg && <div style={{ color: T.textDim, fontSize: 12 }}>Loading budget settings...</div>}
478
+ </div>
479
+ </Section>
480
+
481
+ {/* ══════════════════════════════════════════
482
+ 3. FEATURE FLAGS
483
+ ══════════════════════════════════════════ */}
484
+ <Section
485
+ label="Feature Flags"
486
+ icon="settings"
487
+ color={T.purple}
488
+ open={sections.features}
489
+ onToggle={() => toggleSection("features")}
490
+ badge={<Badge color={T.purple}>Hybrid</Badge>}
491
+ >
492
+ {hybridCfg ? (
493
+ <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
494
+ {renderBoolToggle(
495
+ "Tier 1 Output Filter",
496
+ !hybridCfg.HYBRID_DISABLE_TIER1,
497
+ () => toggleHybridFlag("HYBRID_DISABLE_TIER1"),
498
+ "Compress and filter tool output to reduce token usage."
499
+ )}
500
+ {renderBoolToggle(
501
+ "Tier 2 Adaptive Router",
502
+ !hybridCfg.HYBRID_DISABLE_TIER2,
503
+ () => toggleHybridFlag("HYBRID_DISABLE_TIER2"),
504
+ "Route requests to the most efficient backend."
505
+ )}
506
+ {renderBoolToggle(
507
+ "Tier 3 SLTM Memory",
508
+ !hybridCfg.HYBRID_DISABLE_SLTM,
509
+ () => toggleHybridFlag("HYBRID_DISABLE_SLTM"),
510
+ "Enable semantic long-term memory recall."
511
+ )}
512
+ {renderBoolToggle(
513
+ "Auto-Memory",
514
+ hybridCfg?.auto_memory?.enabled !== false,
515
+ async () => {
516
+ const cur = hybridCfg?.auto_memory?.enabled !== false;
517
+ try {
518
+ const updated = await api.put('/api/hybrid/config', { auto_memory: { enabled: !cur } });
519
+ setHybridCfg(updated || {});
520
+ } catch (e) { flashMsg(`✗ Toggle auto-memory: ${e.message}`); }
521
+ },
522
+ "Learn from tool calls automatically."
523
+ )}
524
+
525
+ <div style={{ paddingTop: 8, borderTop: `1px solid ${T.border}22`, marginTop: 4 }}>
526
+ <div style={labelStyle}>Validate Timeout (seconds)</div>
527
+ <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
528
+ <input
529
+ type="number"
530
+ min="5"
531
+ value={hybridCfg?.validate_timeout ?? ""}
532
+ onChange={e => setHybridCfg(prev => ({ ...prev, validate_timeout: parseInt(e.target.value || "0", 10) || 0 }))}
533
+ style={{ ...inputStyle, flex: 1 }}
534
+ />
535
+ <Btn
536
+ color={T.purple}
537
+ onClick={async () => {
538
+ try {
539
+ const updated = await api.put('/api/hybrid/config', { validate_timeout: hybridCfg.validate_timeout });
540
+ setHybridCfg(updated || {});
541
+ flashMsg("✓ Saved validate timeout");
542
+ } catch (e) { flashMsg(`✗ Save timeout: ${e.message}`); }
543
+ }}
544
+ >
545
+ <I name="save" size={13} /> Save
546
+ </Btn>
547
+ </div>
548
+ </div>
549
+ </div>
550
+ ) : (
551
+ <div style={{ color: T.textDim, fontSize: 12 }}>Loading feature flags...</div>
552
+ )}
553
+ </Section>
554
+
555
+ {/* ══════════════════════════════════════════
556
+ 3b. EDIT LEDGER
557
+ ══════════════════════════════════════════ */}
558
+ <Section
559
+ label="Edit Ledger"
560
+ icon="edit"
561
+ color={T.purple}
562
+ open={sections.editLedger}
563
+ onToggle={() => toggleSection("editLedger")}
564
+ badge={<Badge color={T.purple}>Tracking</Badge>}
565
+ >
566
+ {hybridCfg ? (
567
+ <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
568
+ {renderBoolToggle(
569
+ "Edit Tracking",
570
+ hybridCfg?.edit_ledger?.enabled !== false,
571
+ async () => {
572
+ const cur = hybridCfg?.edit_ledger?.enabled !== false;
573
+ try {
574
+ const updated = await api.put('/api/hybrid/config', { edit_ledger: { ...hybridCfg?.edit_ledger, enabled: !cur } });
575
+ setHybridCfg(updated || {});
576
+ } catch (e) { flashMsg(`✗ Toggle edit tracking: ${e.message}`); }
577
+ },
578
+ "Auto-log all Edit/Write operations to the ledger."
579
+ )}
580
+ {renderBoolToggle(
581
+ "Auto-Tag",
582
+ hybridCfg?.edit_ledger?.auto_tag !== false,
583
+ async () => {
584
+ const cur = hybridCfg?.edit_ledger?.auto_tag !== false;
585
+ try {
586
+ const updated = await api.put('/api/hybrid/config', { edit_ledger: { ...hybridCfg?.edit_ledger, auto_tag: !cur } });
587
+ setHybridCfg(updated || {});
588
+ } catch (e) { flashMsg(`✗ Toggle auto-tag: ${e.message}`); }
589
+ },
590
+ "Automatically tag hook-generated entries."
591
+ )}
592
+ <div style={{ paddingTop: 8, borderTop: `1px solid ${T.border}22`, marginTop: 4 }}>
593
+ <div style={labelStyle}>Tracking Level</div>
594
+ <div style={{ display: "flex", gap: 6, marginTop: 4 }}>
595
+ {["minimal", "standard", "detailed"].map(level => (
596
+ <button
597
+ key={level}
598
+ onClick={async () => {
599
+ try {
600
+ const updated = await api.put('/api/hybrid/config', { edit_ledger: { ...hybridCfg?.edit_ledger, tracking_level: level } });
601
+ setHybridCfg(updated || {});
602
+ flashMsg(`Tracking level set to ${level}`);
603
+ } catch (e) { flashMsg(`✗ ${e.message}`); }
604
+ }}
605
+ style={{
606
+ padding: "5px 12px", borderRadius: 5, fontSize: 11, fontWeight: 600,
607
+ cursor: "pointer",
608
+ border: `1px solid ${(hybridCfg?.edit_ledger?.tracking_level || "standard") === level ? T.accent : T.border}`,
609
+ background: (hybridCfg?.edit_ledger?.tracking_level || "standard") === level ? `${T.accent}18` : "transparent",
610
+ color: (hybridCfg?.edit_ledger?.tracking_level || "standard") === level ? T.accent : T.dim,
611
+ }}
612
+ >
613
+ {level.charAt(0).toUpperCase() + level.slice(1)}
614
+ </button>
615
+ ))}
616
+ </div>
617
+ <div style={{ fontSize: 10, color: T.dim, marginTop: 4 }}>
618
+ Minimal: file + type only. Standard: + git info & diffs. Detailed: + code snippets.
619
+ </div>
620
+ </div>
621
+ <div style={{ paddingTop: 8, borderTop: `1px solid ${T.border}22`, marginTop: 4 }}>
622
+ <Btn onClick={() => window.open('/edits', '_blank')} style={{ fontSize: 11 }}>
623
+ Open Edit Ledger ↗
624
+ </Btn>
625
+ </div>
626
+ </div>
627
+ ) : (
628
+ <div style={{ color: T.dim, fontSize: 12 }}>Loading...</div>
629
+ )}
630
+ </Section>
631
+
632
+ {/* ══════════════════════════════════════════
633
+ 4. BACKGROUND AGENTS
634
+ ══════════════════════════════════════════ */}
635
+ <Section
636
+ label="Background Agents"
637
+ icon="cpu"
638
+ color={T.blue}
639
+ open={sections.agents}
640
+ onToggle={() => toggleSection("agents")}
641
+ >
642
+ {agentsCfg ? (
643
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 12 }}>
644
+ {Object.entries(agentsCfg).map(([agentName, cfg]) => {
645
+ const live = agentsStatus[agentName] || {};
646
+ const agentStatusColor = !cfg.enabled ? T.textMuted : live.running ? T.accent : T.warn;
647
+ const agentStatusLabel = !cfg.enabled ? "Disabled" : live.running ? "Running" : "Idle";
648
+ return (
649
+ <div key={agentName} style={{
650
+ background: T.surfaceAlt, border: `1px solid ${T.border}`,
651
+ borderRadius: 8, padding: 12, display: "flex", flexDirection: "column", gap: 10,
652
+ }}>
653
+ {/* Header */}
654
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
655
+ <span style={{ color: T.text, fontSize: 13, fontWeight: 600 }}>{agentName}</span>
656
+ <Badge color={agentStatusColor}>
657
+ <GlowDot color={agentStatusColor} size={5} />
658
+ {agentStatusLabel}
659
+ </Badge>
660
+ </div>
661
+
662
+ {/* Stats row */}
663
+ <div style={{ display: "flex", gap: 6 }}>
664
+ {[
665
+ { label: "Checks", value: live.check_count ?? 0 },
666
+ { label: "Errors", value: live.error_count ?? 0, valueColor: (live.error_count || 0) > 0 ? T.error : T.text },
667
+ { label: "Last Check", value: live.last_check ? timeAgo(new Date((live.last_check || 0) * 1000).toISOString()) : "Never", mono: false },
668
+ ].map(s => (
669
+ <div key={s.label} style={{
670
+ flex: 1, background: T.surface, border: `1px solid ${T.border}`,
671
+ borderRadius: 6, padding: "6px 8px",
672
+ }}>
673
+ <div style={{ color: T.textDim, fontSize: 10, textTransform: "uppercase", letterSpacing: 0.8 }}>{s.label}</div>
674
+ <div className={s.mono !== false ? "mono" : ""} style={{ color: s.valueColor || T.text, fontSize: s.mono !== false ? 13 : 11, marginTop: 2 }}>
675
+ {s.value}
676
+ </div>
677
+ </div>
678
+ ))}
679
+ </div>
680
+
681
+ {/* Toggles */}
682
+ {renderBoolToggle("Enabled", !!cfg.enabled, () => updateAgentField(agentName, "enabled", !cfg.enabled))}
683
+ {"use_ai" in cfg && renderBoolToggle("AI Enhancement", !!cfg.use_ai, () => updateAgentField(agentName, "use_ai", !cfg.use_ai))}
684
+
685
+ {/* Interval + model */}
686
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
687
+ <div>
688
+ <div style={labelStyle}>Interval (sec)</div>
689
+ <input
690
+ type="number" min="5"
691
+ value={cfg.interval ?? ""}
692
+ onChange={e => updateAgentField(agentName, "interval", parseInt(e.target.value || "0", 10) || 0)}
693
+ style={inputStyle}
694
+ />
695
+ </div>
696
+ {"ai_model" in cfg && (
697
+ <div>
698
+ <div style={labelStyle}>AI Model</div>
699
+ <select value={cfg.ai_model || ""} onChange={e => updateAgentField(agentName, "ai_model", e.target.value)} style={inputStyle}>
700
+ {renderModelOptions()}
701
+ </select>
702
+ </div>
703
+ )}
704
+ </div>
705
+
706
+ {/* Actions */}
707
+ <div style={{ display: "flex", gap: 8 }}>
708
+ <Btn color={T.blue} onClick={() => saveAgent(agentName)} disabled={busy.agents}>
709
+ <I name="save" size={13} /> {busy.agents ? "Saving..." : "Save"}
710
+ </Btn>
711
+ <Btn color={T.accent} variant="outline" onClick={() => runAgentNow(agentName)} disabled={runningAgent === agentName || !cfg.enabled}>
712
+ <I name="zap" size={13} color={runningAgent === agentName ? T.textMuted : T.accent} />
713
+ {runningAgent === agentName ? "Running..." : "Run Now"}
714
+ </Btn>
715
+ </div>
716
+ </div>
717
+ );
718
+ })}
719
+ </div>
720
+ ) : (
721
+ <div style={{ color: T.textDim, fontSize: 12 }}>Loading agent settings...</div>
722
+ )}
723
+ </Section>
724
+
725
+ {/* ══════════════════════════════════════════
726
+ 5. DELEGATE SETTINGS
727
+ ══════════════════════════════════════════ */}
728
+ <Section
729
+ label="Delegate Settings"
730
+ icon="share"
731
+ color={T.purple}
732
+ open={sections.delegate}
733
+ onToggle={() => toggleSection("delegate")}
734
+ badge={delegateCfg && <Badge color={delegateCfg.enabled ? T.accent : T.textMuted}>{delegateCfg.enabled ? "Enabled" : "Disabled"}</Badge>}
735
+ >
736
+ {delegateCfg ? (
737
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
738
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 12 }}>
739
+ {/* Policy toggles */}
740
+ <div style={{ background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 8, padding: 12 }}>
741
+ {renderBoolToggle("Delegate Enabled", !!delegateCfg.enabled, () => updateDelegateField("enabled", !delegateCfg.enabled))}
742
+ {renderBoolToggle("Threshold Policy", !!delegateCfg.threshold_enabled, () => updateDelegateField("threshold_enabled", !delegateCfg.threshold_enabled), "Delegate automatically once token threshold is met.")}
743
+ {renderBoolToggle("Fallback Models", !!delegateCfg.allow_model_fallback, () => updateDelegateField("allow_model_fallback", !delegateCfg.allow_model_fallback))}
744
+ </div>
745
+ {/* Auto toggles */}
746
+ <div style={{ background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 8, padding: 12 }}>
747
+ {renderBoolToggle("Auto-Compress", !!delegateCfg.auto_compress, () => updateDelegateField("auto_compress", !delegateCfg.auto_compress))}
748
+ {renderBoolToggle("Auto-Search", !!delegateCfg.auto_search, () => updateDelegateField("auto_search", !delegateCfg.auto_search))}
749
+ {renderBoolToggle("Auto-Vector Search", !!delegateCfg.auto_vector_search, () => updateDelegateField("auto_vector_search", !delegateCfg.auto_vector_search))}
750
+ {renderBoolToggle("Auto-Activity Log", !!delegateCfg.auto_activity_log, () => updateDelegateField("auto_activity_log", !delegateCfg.auto_activity_log))}
751
+ </div>
752
+ </div>
753
+
754
+ {/* Numeric inputs */}
755
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: 10 }}>
756
+ <div>
757
+ <div style={labelStyle}>Preferred Model</div>
758
+ <select value={delegateCfg.preferred_model || ""} onChange={e => updateDelegateField("preferred_model", e.target.value)} style={inputStyle}>
759
+ {renderModelOptions()}
760
+ </select>
761
+ </div>
762
+ <div>
763
+ <div style={labelStyle}>Max Tokens</div>
764
+ <input type="number" min="64" value={delegateCfg.max_tokens ?? ""} onChange={e => updateDelegateField("max_tokens", parseInt(e.target.value || "0", 10) || 0)} style={inputStyle} />
765
+ </div>
766
+ <div>
767
+ <div style={labelStyle}>Search Top K</div>
768
+ <input type="number" min="1" value={delegateCfg.search_top_k ?? ""} onChange={e => updateDelegateField("search_top_k", parseInt(e.target.value || "0", 10) || 0)} style={inputStyle} />
769
+ </div>
770
+ <div>
771
+ <div style={labelStyle}>Max Context Tokens</div>
772
+ <input type="number" min="100" value={delegateCfg.max_context_tokens ?? ""} onChange={e => updateDelegateField("max_context_tokens", parseInt(e.target.value || "0", 10) || 0)} style={inputStyle} />
773
+ </div>
774
+ <div>
775
+ <div style={labelStyle}>Threshold Min Tokens</div>
776
+ <input type="number" min="1" value={delegateCfg.threshold_min_total_tokens ?? ""} onChange={e => updateDelegateField("threshold_min_total_tokens", parseInt(e.target.value || "0", 10) || 0)} style={inputStyle} />
777
+ </div>
778
+ </div>
779
+
780
+ <div style={{ display: "flex", justifyContent: "flex-end" }}>
781
+ <Btn color={T.purple} onClick={saveDelegate} disabled={busy.delegate}>
782
+ <I name="save" size={13} /> {busy.delegate ? "Saving..." : "Save Delegate Settings"}
783
+ </Btn>
784
+ </div>
785
+ </div>
786
+ ) : (
787
+ <div style={{ color: T.textDim, fontSize: 12 }}>Loading delegate settings...</div>
788
+ )}
789
+ </Section>
790
+
791
+ {/* ══════════════════════════════════════════
792
+ 5b. CODEX INTEGRATION
793
+ ══════════════════════════════════════════ */}
794
+ <Section
795
+ label="Codex Integration"
796
+ icon="terminal"
797
+ color={T.orange || "#e67e22"}
798
+ open={sections.codex}
799
+ onToggle={() => toggleSection("codex")}
800
+ badge={delegateCfg && <Badge color={delegateCfg.codex_enabled ? T.accent : T.textMuted}>{delegateCfg.codex_enabled ? "Active" : "Inactive"}</Badge>}
801
+ >
802
+ {delegateCfg ? (
803
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
804
+ <div style={{ fontSize: 12, color: T.textMuted, marginBottom: 4 }}>
805
+ Cloud delegate backend via OpenAI Codex CLI. Enables deep code review, test generation, and consensus workflows using GPT-5.x models.
806
+ </div>
807
+
808
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 12 }}>
809
+ {/* Master toggles */}
810
+ <div style={{ background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 8, padding: 12 }}>
811
+ {renderBoolToggle("Codex Enabled", !!delegateCfg.codex_enabled, () => updateDelegateField("codex_enabled", !delegateCfg.codex_enabled), "Master switch for Codex as a delegate backend.")}
812
+ {renderBoolToggle("Verify Edits", !!delegateCfg.codex_verify_edits, () => updateDelegateField("codex_verify_edits", !delegateCfg.codex_verify_edits), "Auto-review edits via Codex after enrichment.")}
813
+ {renderBoolToggle("Memory Bridge", delegateCfg.codex_memory_bridge !== false, () => updateDelegateField("codex_memory_bridge", !(delegateCfg.codex_memory_bridge !== false)), "Auto-extract findings into c3_memory.")}
814
+ </div>
815
+ {/* Model & sandbox */}
816
+ <div style={{ background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 8, padding: 12 }}>
817
+ <div>
818
+ <div style={labelStyle}>Default Model</div>
819
+ <select value={delegateCfg.codex_default_model || "gpt-5.3-codex-spark"} onChange={e => updateDelegateField("codex_default_model", e.target.value)} style={inputStyle}>
820
+ <option value="gpt-5.4">gpt-5.4</option>
821
+ <option value="gpt-5.3-codex-spark">gpt-5.3-codex-spark</option>
822
+ <option value="gpt-5.3-codex">gpt-5.3-codex</option>
823
+ </select>
824
+ </div>
825
+ <div style={{ marginTop: 8 }}>
826
+ <div style={labelStyle}>Sandbox Mode</div>
827
+ <select value={delegateCfg.codex_default_sandbox || "read-only"} onChange={e => updateDelegateField("codex_default_sandbox", e.target.value)} style={inputStyle}>
828
+ <option value="read-only">read-only (safe)</option>
829
+ <option value="workspace-write">workspace-write (local edits)</option>
830
+ <option value="danger-full-access">danger-full-access (network)</option>
831
+ </select>
832
+ </div>
833
+ <div style={{ marginTop: 8 }}>
834
+ <div style={labelStyle}>Reasoning Effort</div>
835
+ <select value={delegateCfg.codex_reasoning_effort || "high"} onChange={e => updateDelegateField("codex_reasoning_effort", e.target.value)} style={inputStyle}>
836
+ <option value="xhigh">xhigh</option>
837
+ <option value="high">high</option>
838
+ <option value="medium">medium</option>
839
+ <option value="low">low</option>
840
+ </select>
841
+ </div>
842
+ </div>
843
+ </div>
844
+
845
+ {/* Numeric inputs */}
846
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: 10 }}>
847
+ <div>
848
+ <div style={labelStyle}>Timeout (seconds)</div>
849
+ <input type="number" min="30" max="600" value={delegateCfg.codex_timeout ?? 120} onChange={e => updateDelegateField("codex_timeout", parseInt(e.target.value || "120", 10) || 120)} style={inputStyle} />
850
+ </div>
851
+ <div>
852
+ <div style={labelStyle}>Max Context Tokens</div>
853
+ <input type="number" min="200" value={delegateCfg.codex_max_context_tokens ?? 4000} onChange={e => updateDelegateField("codex_max_context_tokens", parseInt(e.target.value || "4000", 10) || 4000)} style={inputStyle} />
854
+ </div>
855
+ </div>
856
+
857
+ {/* Auto-routed task types */}
858
+ <div style={{ background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 8, padding: 12 }}>
859
+ <div style={labelStyle}>Auto-Routed Task Types</div>
860
+ <div style={{ fontSize: 11, color: T.textMuted, marginBottom: 6 }}>
861
+ Tasks auto-routed to Codex when backend='auto'. Click to toggle.
862
+ </div>
863
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
864
+ {["review", "diagnose", "improve", "test", "explain", "summarize", "ask", "docstring"].map(tt => {
865
+ const active = (delegateCfg.codex_task_types || ["review", "diagnose", "improve", "test"]).includes(tt);
866
+ return <button key={tt} onClick={() => {
867
+ const current = delegateCfg.codex_task_types || ["review", "diagnose", "improve", "test"];
868
+ const next = active ? current.filter(t => t !== tt) : [...current, tt];
869
+ updateDelegateField("codex_task_types", next);
870
+ }} style={{
871
+ padding: "3px 10px", borderRadius: 12, border: `1px solid ${active ? (T.orange || T.accent) : T.border}`,
872
+ background: active ? (T.orange || T.accent) + "22" : "transparent",
873
+ color: active ? (T.orange || T.accent) : T.textMuted,
874
+ cursor: "pointer", fontSize: 11, fontWeight: active ? 600 : 400,
875
+ }}>{tt}</button>;
876
+ })}
877
+ </div>
878
+ </div>
879
+
880
+ <div style={{ display: "flex", justifyContent: "flex-end" }}>
881
+ <Btn color={T.orange || T.accent} onClick={saveDelegate} disabled={busy.delegate}>
882
+ <I name="save" size={13} /> {busy.delegate ? "Saving..." : "Save Codex Settings"}
883
+ </Btn>
884
+ </div>
885
+ </div>
886
+ ) : (
887
+ <div style={{ color: T.textDim, fontSize: 12 }}>Loading delegate settings...</div>
888
+ )}
889
+ </Section>
890
+
891
+ {/* ══════════════════════════════════════════
892
+ 5c. GEMINI INTEGRATION
893
+ ══════════════════════════════════════════ */}
894
+ <Section
895
+ label="Gemini Integration"
896
+ icon="cloud"
897
+ color={T.blue || "#3498db"}
898
+ open={sections.gemini}
899
+ onToggle={() => toggleSection("gemini")}
900
+ badge={delegateCfg && <Badge color={delegateCfg.gemini_enabled ? T.accent : T.textMuted}>{delegateCfg.gemini_enabled ? "Active" : "Inactive"}</Badge>}
901
+ >
902
+ {delegateCfg ? (
903
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
904
+ <div style={{ fontSize: 12, color: T.textMuted, marginBottom: 4 }}>
905
+ Cloud delegate backend via Google Gemini CLI. Enables code review, diagnosis, and tri-consensus workflows using Gemini 2.5 models.
906
+ </div>
907
+
908
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 12 }}>
909
+ <div style={{ background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 8, padding: 12 }}>
910
+ {renderBoolToggle("Gemini Enabled", !!delegateCfg.gemini_enabled, () => updateDelegateField("gemini_enabled", !delegateCfg.gemini_enabled), "Master switch for Gemini as a delegate backend.")}
911
+ {renderBoolToggle("Memory Bridge", delegateCfg.gemini_memory_bridge !== false, () => updateDelegateField("gemini_memory_bridge", !(delegateCfg.gemini_memory_bridge !== false)), "Auto-extract findings into c3_memory.")}
912
+ </div>
913
+ <div style={{ background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 8, padding: 12 }}>
914
+ <div>
915
+ <div style={labelStyle}>Default Model</div>
916
+ <select value={delegateCfg.gemini_default_model || "gemini-2.5-flash"} onChange={e => updateDelegateField("gemini_default_model", e.target.value)} style={inputStyle}>
917
+ <option value="gemini-2.5-flash">gemini-2.5-flash</option>
918
+ <option value="gemini-2.5-pro">gemini-2.5-pro</option>
919
+ </select>
920
+ </div>
921
+ </div>
922
+ </div>
923
+
924
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: 10 }}>
925
+ <div>
926
+ <div style={labelStyle}>Timeout (seconds)</div>
927
+ <input type="number" min="30" max="600" value={delegateCfg.gemini_timeout ?? 120} onChange={e => updateDelegateField("gemini_timeout", parseInt(e.target.value || "120", 10) || 120)} style={inputStyle} />
928
+ </div>
929
+ <div>
930
+ <div style={labelStyle}>Max Context Tokens</div>
931
+ <input type="number" min="200" value={delegateCfg.gemini_max_context_tokens ?? 8000} onChange={e => updateDelegateField("gemini_max_context_tokens", parseInt(e.target.value || "8000", 10) || 8000)} style={inputStyle} />
932
+ </div>
933
+ </div>
934
+
935
+ <div style={{ background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 8, padding: 12 }}>
936
+ <div style={labelStyle}>Auto-Routed Task Types</div>
937
+ <div style={{ fontSize: 11, color: T.textMuted, marginBottom: 6 }}>
938
+ Tasks auto-routed to Gemini when backend='auto'. Click to toggle.
939
+ </div>
940
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
941
+ {["review", "diagnose", "improve", "test", "explain", "summarize", "ask", "docstring"].map(tt => {
942
+ const active = (delegateCfg.gemini_task_types || ["review", "diagnose", "improve", "test"]).includes(tt);
943
+ return <button key={tt} onClick={() => {
944
+ const current = delegateCfg.gemini_task_types || ["review", "diagnose", "improve", "test"];
945
+ const next = active ? current.filter(t => t !== tt) : [...current, tt];
946
+ updateDelegateField("gemini_task_types", next);
947
+ }} style={{
948
+ padding: "3px 10px", borderRadius: 12, border: `1px solid ${active ? (T.blue || "#3498db") : T.border}`,
949
+ background: active ? (T.blue || "#3498db") + "22" : "transparent",
950
+ color: active ? (T.blue || "#3498db") : T.textMuted,
951
+ cursor: "pointer", fontSize: 11, fontWeight: active ? 600 : 400,
952
+ }}>{tt}</button>;
953
+ })}
954
+ </div>
955
+ </div>
956
+
957
+ <div style={{ display: "flex", justifyContent: "flex-end" }}>
958
+ <Btn color={T.blue || "#3498db"} onClick={saveDelegate} disabled={busy.delegate}>
959
+ <I name="save" size={13} /> {busy.delegate ? "Saving..." : "Save Gemini Settings"}
960
+ </Btn>
961
+ </div>
962
+ </div>
963
+ ) : (
964
+ <div style={{ color: T.textDim, fontSize: 12 }}>Loading delegate settings...</div>
965
+ )}
966
+ </Section>
967
+
968
+ {/* ══════════════════════════════════════════
969
+ 6. AGENT WORKFLOWS
970
+ ══════════════════════════════════════════ */}
971
+ <Section
972
+ label="Agent Workflows"
973
+ icon="zap"
974
+ color={T.accent}
975
+ open={sections.workflows}
976
+ onToggle={() => toggleSection("workflows")}
977
+ badge={<Badge color={hybridCfg?.agent_workflows?.enabled !== false ? T.accent : T.textMuted}>
978
+ {hybridCfg?.agent_workflows?.enabled !== false ? "Enabled" : "Disabled"}
979
+ </Badge>}
980
+ >
981
+ {hybridCfg ? (() => {
982
+ const wf = hybridCfg.agent_workflows || {};
983
+ const updateWfField = async (key, value) => {
984
+ try {
985
+ const updated = await api.put('/api/hybrid/config', {
986
+ agent_workflows: { ...wf, [key]: value }
987
+ });
988
+ setHybridCfg(updated || {});
989
+ } catch (e) { flashMsg(`✗ Update workflow setting: ${e.message}`); }
990
+ };
991
+ return (
992
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
993
+ <div style={{ color: T.textDim, fontSize: 12, marginBottom: 4 }}>
994
+ Compound pipelines that run multi-step C3 operations in a single call.
995
+ </div>
996
+ {renderBoolToggle(
997
+ "Workflows Enabled",
998
+ wf.enabled !== false,
999
+ () => updateWfField("enabled", !(wf.enabled !== false)),
1000
+ "Enable c3_agent compound workflows (review_changes, prepare_context, investigate, preflight)."
1001
+ )}
1002
+ {renderBoolToggle(
1003
+ "Delegate in Workflows",
1004
+ wf.delegate_in_workflows !== false,
1005
+ () => updateWfField("delegate_in_workflows", !(wf.delegate_in_workflows !== false)),
1006
+ "Allow Ollama AI delegation within compound workflows."
1007
+ )}
1008
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 10, paddingTop: 8, borderTop: `1px solid ${T.border}22` }}>
1009
+ <div>
1010
+ <div style={labelStyle}>Prefetch Max Files</div>
1011
+ <input type="number" min="1" max="10" value={wf.prefetch_max_files ?? 3}
1012
+ onChange={e => updateWfField("prefetch_max_files", parseInt(e.target.value || "3", 10) || 3)}
1013
+ style={inputStyle} />
1014
+ <div style={{ color: T.textDim, fontSize: 11 }}>Files auto-compressed on search prefetch</div>
1015
+ </div>
1016
+ <div>
1017
+ <div style={labelStyle}>Batch Validate Max</div>
1018
+ <input type="number" min="1" max="20" value={wf.batch_validate_max_files ?? 10}
1019
+ onChange={e => updateWfField("batch_validate_max_files", parseInt(e.target.value || "10", 10) || 10)}
1020
+ style={inputStyle} />
1021
+ <div style={{ color: T.textDim, fontSize: 11 }}>Max files per batch validate call</div>
1022
+ </div>
1023
+ <div>
1024
+ <div style={labelStyle}>Compound Max Compress</div>
1025
+ <input type="number" min="1" max="10" value={wf.compound_max_compress ?? 5}
1026
+ onChange={e => updateWfField("compound_max_compress", parseInt(e.target.value || "5", 10) || 5)}
1027
+ style={inputStyle} />
1028
+ <div style={{ color: T.textDim, fontSize: 11 }}>Max files compressed in agent workflows</div>
1029
+ </div>
1030
+ </div>
1031
+ </div>
1032
+ );
1033
+ })() : (
1034
+ <div style={{ color: T.textDim, fontSize: 12 }}>Loading workflow settings...</div>
1035
+ )}
1036
+ </Section>
1037
+
1038
+ {/* ══════════════════════════════════════════
1039
+ 7. PROXY SETTINGS
1040
+ ══════════════════════════════════════════ */}
1041
+ <Section
1042
+ label="Proxy Settings"
1043
+ icon="terminal"
1044
+ color={T.warn}
1045
+ open={sections.proxy}
1046
+ onToggle={() => toggleSection("proxy")}
1047
+ badge={proxyCfg && <Badge color={proxyCfg.PROXY_DISABLE ? T.textMuted : T.warn}>{proxyCfg.PROXY_DISABLE ? "Disabled" : "Available"}</Badge>}
1048
+ >
1049
+ {proxyCfg ? (
1050
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
1051
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 12 }}>
1052
+ {/* Boolean toggles */}
1053
+ <div style={{ background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 8, padding: 12 }}>
1054
+ {renderBoolToggle("Proxy Enabled", !proxyCfg.PROXY_DISABLE, () => updateProxyField("PROXY_DISABLE", !proxyCfg.PROXY_DISABLE))}
1055
+ {renderBoolToggle("Filter Tools", !!proxyCfg.filter_tools, () => updateProxyField("filter_tools", !proxyCfg.filter_tools))}
1056
+ {renderBoolToggle("Use SLM", !!proxyCfg.use_slm, () => updateProxyField("use_slm", !proxyCfg.use_slm))}
1057
+ {renderBoolToggle("Inject Context Summary", !!proxyCfg.inject_context_summary, () => updateProxyField("inject_context_summary", !proxyCfg.inject_context_summary))}
1058
+ </div>
1059
+ {/* Numeric + select inputs */}
1060
+ <div style={{ background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 8, padding: 12, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, alignContent: "start" }}>
1061
+ <div>
1062
+ <div style={labelStyle}>Max Tools</div>
1063
+ <input type="number" min="1" value={proxyCfg.max_tools ?? ""} onChange={e => updateProxyField("max_tools", parseInt(e.target.value || "0", 10) || 0)} style={inputStyle} />
1064
+ </div>
1065
+ <div>
1066
+ <div style={labelStyle}>Context Window</div>
1067
+ <input type="number" min="1" value={proxyCfg.context_window_size ?? ""} onChange={e => updateProxyField("context_window_size", parseInt(e.target.value || "0", 10) || 0)} style={inputStyle} />
1068
+ </div>
1069
+ <div style={{ gridColumn: "1 / -1" }}>
1070
+ <div style={labelStyle}>SLM Model</div>
1071
+ <select value={proxyCfg.slm_model || ""} onChange={e => updateProxyField("slm_model", e.target.value)} style={inputStyle}>
1072
+ {renderModelOptions()}
1073
+ </select>
1074
+ </div>
1075
+ <div style={{ gridColumn: "1 / -1" }}>
1076
+ <div style={labelStyle}>Always Visible Categories</div>
1077
+ <input
1078
+ value={Array.isArray(proxyCfg.always_visible) ? proxyCfg.always_visible.join(", ") : (proxyCfg.always_visible || "")}
1079
+ onChange={e => updateProxyField("always_visible", e.target.value)}
1080
+ style={inputStyle}
1081
+ placeholder="core, search, memory"
1082
+ />
1083
+ </div>
1084
+ </div>
1085
+ </div>
1086
+
1087
+ <div style={{ display: "flex", justifyContent: "flex-end" }}>
1088
+ <Btn color={T.warn} onClick={saveProxy} disabled={busy.proxy}>
1089
+ <I name="save" size={13} /> {busy.proxy ? "Saving..." : "Save Proxy Settings"}
1090
+ </Btn>
1091
+ </div>
1092
+ </div>
1093
+ ) : (
1094
+ <div style={{ color: T.textDim, fontSize: 12 }}>Loading proxy settings...</div>
1095
+ )}
1096
+ </Section>
1097
+
1098
+ {/* ══════════════════════════════════════════
1099
+ 8. MCP SERVERS
1100
+ ══════════════════════════════════════════ */}
1101
+ <Section
1102
+ label="MCP Servers"
1103
+ icon="cpu"
1104
+ color={T.blue}
1105
+ open={sections.mcp}
1106
+ onToggle={() => toggleSection("mcp")}
1107
+ badge={
1108
+ <Badge color={(mcpStatus?.active ?? mcpStatus?.configured) ? T.accent : T.textMuted}>
1109
+ <GlowDot color={(mcpStatus?.active ?? mcpStatus?.configured) ? T.accent : T.textDim} size={5} />
1110
+ {(mcpStatus?.active ?? mcpStatus?.configured) ? "Active" : "Inactive"}
1111
+ </Badge>
1112
+ }
1113
+ >
1114
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
1115
+ {/* Status row */}
1116
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "6px 0", borderBottom: `1px solid ${T.border}` }}>
1117
+ <span style={{ color: T.textMuted, fontSize: 13 }}>MCP Mode</span>
1118
+ <Badge color={mcpStatus?.mode === "proxy" ? T.warn : T.accent}>
1119
+ {mcpStatus?.mode === "proxy" ? "Proxy (Advanced)" : "Direct (Recommended)"}
1120
+ </Badge>
1121
+ </div>
1122
+
1123
+ {/* Target IDE */}
1124
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "6px 0", borderBottom: `1px solid ${T.border}` }}>
1125
+ <span style={{ color: T.textMuted, fontSize: 13 }}>Target IDE</span>
1126
+ <select value={mcpIde} onChange={e => setMcpIde(e.target.value)} style={{ ...inputStyle, width: "auto", minWidth: 170 }}>
1127
+ <option value="auto">Auto-detect</option>
1128
+ <option value="claude">Claude Code</option>
1129
+ <option value="gemini">Gemini CLI</option>
1130
+ <option value="vscode">VS Code Copilot</option>
1131
+ <option value="cursor">Cursor</option>
1132
+ <option value="codex">OpenAI Codex</option>
1133
+ <option value="antigravity">Google Antigravity</option>
1134
+ </select>
1135
+ </div>
1136
+
1137
+ {/* Installed MCPs */}
1138
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
1139
+ <span style={{ color: T.textMuted, fontSize: 12, fontWeight: 600 }}>Installed MCPs</span>
1140
+ <button onClick={() => setShowAddMcp(!showAddMcp)} style={{ background: "transparent", border: "none", color: T.accent, cursor: "pointer", fontSize: 12 }}>
1141
+ {showAddMcp ? "Cancel" : "+ Add Server"}
1142
+ </button>
1143
+ </div>
1144
+
1145
+ {/* Add server form */}
1146
+ {showAddMcp && (
1147
+ <div className="fade-up" style={{ display: "flex", flexDirection: "column", gap: 8, padding: 12, background: T.surfaceAlt, borderRadius: 6 }}>
1148
+ <input value={newMcpName} onChange={e => setNewMcpName(e.target.value)} placeholder="Server name (e.g. sqlite)" style={inputStyle} />
1149
+ <input value={newMcpCmd} onChange={e => setNewMcpCmd(e.target.value)} placeholder="Command (e.g. npx)" style={inputStyle} />
1150
+ <input value={newMcpArgs} onChange={e => setNewMcpArgs(e.target.value)} placeholder="Args comma-separated (e.g. -y,@modelcontextprotocol/server-sqlite)" style={inputStyle} />
1151
+ <Btn color={T.accent} onClick={async () => {
1152
+ if (!newMcpName || !newMcpCmd) return alert("Name and command are required");
1153
+ const args = newMcpArgs ? newMcpArgs.split(',').map(a => a.trim()) : [];
1154
+ try {
1155
+ await api.post('/api/mcp/servers', { ide: mcpIde, name: newMcpName, command: newMcpCmd, args });
1156
+ setNewMcpName(""); setNewMcpCmd(""); setNewMcpArgs(""); setShowAddMcp(false);
1157
+ loadMcpStatus(mcpIde);
1158
+ } catch (e) { alert("Failed to add: " + e.message); }
1159
+ }}>
1160
+ <I name="save" size={13} /> Save Server
1161
+ </Btn>
1162
+ </div>
1163
+ )}
1164
+
1165
+ {/* Server cards */}
1166
+ {mcpStatus?.config?.mcpServers && Object.keys(mcpStatus.config.mcpServers).length > 0 ? (
1167
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 8 }}>
1168
+ {Object.entries(mcpStatus.config.mcpServers).map(([name, srv]) => (
1169
+ <div key={name} style={{ background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 8, padding: 10, display: "flex", flexDirection: "column", gap: 6 }}>
1170
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
1171
+ <Badge color={name === "c3" ? T.accent : T.blue}>{name}</Badge>
1172
+ <button
1173
+ onClick={async () => {
1174
+ const confirmMsg = name === "c3"
1175
+ ? `Remove MCP server '${name}' from ${mcpIde}? This also removes related C3 files/hooks.`
1176
+ : `Remove MCP server '${name}' from ${mcpIde}?`;
1177
+ if (!confirm(confirmMsg)) return;
1178
+ try {
1179
+ await api.del(`/api/mcp/servers/${encodeURIComponent(name)}?ide=${encodeURIComponent(mcpIde)}&remove_files=1`);
1180
+ loadMcpStatus(mcpIde);
1181
+ } catch (e) { alert("Failed to remove: " + e.message); }
1182
+ }}
1183
+ title={`Remove ${name}`}
1184
+ style={{ background: "transparent", border: "none", color: T.error, cursor: "pointer", padding: 2, lineHeight: 0 }}
1185
+ >
1186
+ <I name="trash" size={14} color={T.error} />
1187
+ </button>
1188
+ </div>
1189
+ <div className="mono" style={{ fontSize: 10, color: T.textDim, wordBreak: "break-all" }}>{srv.command || "No command"}</div>
1190
+ <div className="mono" style={{ fontSize: 10, color: T.textDim, wordBreak: "break-all" }}>{(srv.args || []).join(" ")}</div>
1191
+ </div>
1192
+ ))}
1193
+ </div>
1194
+ ) : (
1195
+ <div style={{ padding: "8px 0", color: T.textDim, fontSize: 12 }}>No MCP servers configured for {mcpIde}.</div>
1196
+ )}
1197
+
1198
+ {/* C3 server found status */}
1199
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "6px 0", borderTop: `1px solid ${T.border}` }}>
1200
+ <span style={{ color: T.textMuted, fontSize: 13 }}>C3 Server Script</span>
1201
+ <Badge color={mcpStatus?.server_found ? T.accent : T.textDim}>{mcpStatus?.server_found ? "Found" : "Not found"}</Badge>
1202
+ </div>
1203
+
1204
+ {/* Install / reinstall */}
1205
+ <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
1206
+ <select value={installIde} onChange={e => setInstallIde(e.target.value)} style={{ ...inputStyle, flex: 1, minWidth: 140, width: "auto" }}>
1207
+ <option value="auto">Auto-detect IDE</option>
1208
+ <option value="claude">Claude Code</option>
1209
+ <option value="antigravity">Google Antigravity</option>
1210
+ <option value="gemini">Gemini CLI</option>
1211
+ <option value="vscode">VS Code Copilot</option>
1212
+ <option value="cursor">Cursor</option>
1213
+ <option value="codex">OpenAI Codex</option>
1214
+ </select>
1215
+ <select value={installMcpMode} onChange={e => setInstallMcpMode(e.target.value)} style={{ ...inputStyle, width: "auto" }}>
1216
+ <option value="direct">Direct</option>
1217
+ <option value="proxy">Proxy (Advanced)</option>
1218
+ </select>
1219
+ <Btn
1220
+ color={T.purple}
1221
+ onClick={async () => {
1222
+ setInstalling(true);
1223
+ try {
1224
+ const r = await api.post('/api/mcp/install', { ide: installIde, mcp_mode: installMcpMode });
1225
+ flashMsg(`✓ MCP Install: ${JSON.stringify(r).slice(0, 80)}`);
1226
+ const targetIde = installIde === "auto" ? (mcpStatus?.ide || mcpIde) : installIde;
1227
+ setMcpIde(targetIde);
1228
+ loadMcpStatus(targetIde);
1229
+ } catch (e) { flashMsg(`✗ MCP Install: ${e.message}`); }
1230
+ setInstalling(false);
1231
+ }}
1232
+ disabled={installing}
1233
+ >
1234
+ <I name="terminal" size={13} />
1235
+ {installing ? "Installing..." : mcpStatus?.configured ? "Reinstall C3 MCP" : "Install C3 MCP"}
1236
+ </Btn>
1237
+ </div>
1238
+ </div>
1239
+ </Section>
1240
+
1241
+ {/* ══════════════════════════════════════════
1242
+ 9. PERMISSIONS (Claude Code only)
1243
+ ══════════════════════════════════════════ */}
1244
+ {permsCfg?.supported !== false && <Section
1245
+ label="Permissions (Claude Code)"
1246
+ icon="lock"
1247
+ color={T.warn}
1248
+ open={sections.permissions}
1249
+ onToggle={() => toggleSection("permissions")}
1250
+ badge={permsCfg?.current_tier && <Badge color={
1251
+ permsCfg.current_tier === "read-only" ? T.blue :
1252
+ permsCfg.current_tier === "standard" ? T.accent :
1253
+ permsCfg.current_tier === "permissive" ? T.warn : T.textDim
1254
+ }>{permsCfg.current_tier}</Badge>}
1255
+ >
1256
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
1257
+ <div style={{ fontSize: 11, color: T.textDim, lineHeight: 1.5 }}>
1258
+ Control what Claude Code is allowed to do in this project via <code style={{ fontSize: 10, background: T.surfaceAlt, padding: "1px 4px", borderRadius: 3 }}>.claude/settings.local.json</code>.
1259
+ C3 MCP tools are always allowed in every tier.
1260
+ </div>
1261
+
1262
+ {permsCfg?.current_tier && (
1263
+ <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "6px 10px", borderRadius: 6, background: T.surfaceAlt, border: `1px solid ${T.border}` }}>
1264
+ <I name="shield" size={14} color={
1265
+ permsCfg.current_tier === "read-only" ? T.blue :
1266
+ permsCfg.current_tier === "standard" ? T.accent : T.warn
1267
+ } />
1268
+ <div>
1269
+ <div style={{ fontSize: 12, fontWeight: 600, color: T.text }}>{permsCfg.current_tier}</div>
1270
+ <div style={{ fontSize: 10, color: T.textDim }}>{permsCfg.allow_count} allow, {permsCfg.deny_count} deny rules</div>
1271
+ </div>
1272
+ </div>
1273
+ )}
1274
+
1275
+ {/* Tier selector */}
1276
+ {permsCfg?.tiers && Object.entries(permsCfg.tiers).map(([name, info]) => {
1277
+ const isActive = permsCfg.current_tier === name;
1278
+ const tierColor = name === "read-only" ? T.blue : name === "standard" ? T.accent : T.warn;
1279
+ return (
1280
+ <div key={name} style={{
1281
+ display: "flex", alignItems: "center", gap: 10, padding: "8px 10px", borderRadius: 6,
1282
+ border: `1px solid ${isActive ? tierColor + "60" : T.border}`,
1283
+ background: isActive ? tierColor + "10" : "transparent",
1284
+ cursor: savingPerms ? "default" : "pointer",
1285
+ transition: "all 0.15s",
1286
+ }} onClick={() => !savingPerms && !isActive && applyPermTier(name)}>
1287
+ <div style={{
1288
+ width: 14, height: 14, borderRadius: "50%",
1289
+ border: `2px solid ${isActive ? tierColor : T.textDim + "60"}`,
1290
+ background: isActive ? tierColor : "transparent",
1291
+ display: "flex", alignItems: "center", justifyContent: "center",
1292
+ flexShrink: 0,
1293
+ }}>
1294
+ {isActive && <div style={{ width: 6, height: 6, borderRadius: "50%", background: T.bg }} />}
1295
+ </div>
1296
+ <div style={{ flex: 1 }}>
1297
+ <div style={{ fontSize: 12, fontWeight: isActive ? 600 : 400, color: isActive ? tierColor : T.text }}>
1298
+ {name}
1299
+ </div>
1300
+ <div style={{ fontSize: 10, color: T.textDim }}>{info.description}</div>
1301
+ </div>
1302
+ <div className="mono" style={{ fontSize: 10, color: T.textDim, whiteSpace: "nowrap" }}>
1303
+ {info.allow_count} allow
1304
+ </div>
1305
+ </div>
1306
+ );
1307
+ })}
1308
+
1309
+ {savingPerms && (
1310
+ <div style={{ fontSize: 11, color: T.accent, textAlign: "center" }}>Applying permissions...</div>
1311
+ )}
1312
+ </div>
1313
+ </Section>}
1314
+
1315
+ {/* ══════════════════════════════════════════
1316
+ 10. PROJECT DATA
1317
+ ══════════════════════════════════════════ */}
1318
+ <Section
1319
+ label="Project Data"
1320
+ icon="folder"
1321
+ color={T.textMuted}
1322
+ open={sections.data}
1323
+ onToggle={() => toggleSection("data")}
1324
+ badge={dataSummary && <span className="mono" style={{ fontSize: 11, color: T.textDim }}>{dataSummary.total_kb} KB</span>}
1325
+ >
1326
+ <div>
1327
+ {/* Header row */}
1328
+ <div style={{ display: "flex", alignItems: "center", marginBottom: 10 }}>
1329
+ <div style={{ flex: 1 }} />
1330
+ <button onClick={loadDataSummary} title="Refresh" disabled={dataLoading}
1331
+ style={{ padding: "2px 6px", borderRadius: 4, border: `1px solid ${T.border}`, background: "transparent", cursor: "pointer", display: "flex", alignItems: "center" }}>
1332
+ <I name="refresh" size={10} color={T.textDim} style={dataLoading ? { animation: "spin 0.6s linear infinite" } : {}} />
1333
+ </button>
1334
+ </div>
1335
+
1336
+ {dataLoading && !dataSummary && <div style={{ fontSize: 12, color: T.textDim }}>Loading...</div>}
1337
+
1338
+ {dataSummary && (
1339
+ <>
1340
+ {/* Column headers */}
1341
+ <div style={{
1342
+ display: "grid", gridTemplateColumns: "1fr auto auto auto",
1343
+ gap: "0 10px", fontSize: 10, color: T.textDim,
1344
+ textTransform: "uppercase", letterSpacing: 1,
1345
+ padding: "0 0 6px", borderBottom: `1px solid ${T.border}`, marginBottom: 2,
1346
+ }}>
1347
+ <span>Category</span>
1348
+ <span style={{ textAlign: "right" }}>Items</span>
1349
+ <span style={{ textAlign: "right" }}>Size</span>
1350
+ <span></span>
1351
+ </div>
1352
+
1353
+ {/* Data rows */}
1354
+ {dataRows.map(row => (
1355
+ <div key={row.key} style={{
1356
+ display: "grid", gridTemplateColumns: "1fr auto auto auto",
1357
+ gap: "0 10px", alignItems: "center", padding: "7px 0",
1358
+ borderBottom: `1px solid ${T.border}22`,
1359
+ }}>
1360
+ <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
1361
+ <I name={row.icon} size={12} color={row.color || T.textDim} />
1362
+ <span style={{ fontSize: 13, color: T.textMuted }}>{row.label}</span>
1363
+ </div>
1364
+ <span className="mono" style={{ fontSize: 11, color: T.textDim, textAlign: "right" }}>{row.count}</span>
1365
+ <span className="mono" style={{ fontSize: 11, color: T.textDim, textAlign: "right", minWidth: 52 }}>
1366
+ {row.size > 0 ? `${row.size} KB` : "—"}
1367
+ </span>
1368
+ <div style={{ minWidth: 76, textAlign: "right" }}>
1369
+ {row.action && (
1370
+ <button
1371
+ onClick={row.onAction}
1372
+ disabled={dataWorking === row.key}
1373
+ style={{
1374
+ padding: "3px 8px", borderRadius: 4, fontSize: 10, cursor: dataWorking === row.key ? "default" : "pointer",
1375
+ fontFamily: "'JetBrains Mono', monospace",
1376
+ border: `1px solid ${dataConfirm === row.key ? T.error + "90" : T.border}`,
1377
+ background: dataConfirm === row.key ? `${T.error}18` : "transparent",
1378
+ color: dataConfirm === row.key ? T.error : T.textMuted,
1379
+ transition: "all 0.15s",
1380
+ }}
1381
+ >
1382
+ {dataWorking === row.key ? "..." : dataConfirm === row.key ? "Confirm?" : row.action}
1383
+ </button>
1384
+ )}
1385
+ </div>
1386
+ </div>
1387
+ ))}
1388
+ </>
1389
+ )}
1390
+
1391
+ {dataMsg && (
1392
+ <div className="mono fade-up" style={{
1393
+ marginTop: 10, padding: "6px 10px", borderRadius: 5,
1394
+ background: T.surfaceAlt, fontSize: 11, color: dataMsg.ok ? T.accent : T.error,
1395
+ }}>
1396
+ {dataMsg.text}
1397
+ </div>
1398
+ )}
1399
+ </div>
1400
+ </Section>
1401
+
1402
+ </div>
1403
+ );
1404
+ };