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
cli/ui_nano.html ADDED
@@ -0,0 +1,503 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>C3 Nano</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.9/babel.min.js"></script>
10
+ <style>
11
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=DM+Sans:wght@400;500;600;700&display=swap');
12
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13
+ html, body, #root { width: 100%; height: 100%; overflow: hidden; }
14
+ body { font-family: 'DM Sans', -apple-system, sans-serif; }
15
+ .mono { font-family: 'JetBrains Mono', monospace; }
16
+ ::-webkit-scrollbar { width: 5px; }
17
+ ::-webkit-scrollbar-track { background: transparent; }
18
+ ::-webkit-scrollbar-thumb { border-radius: 3px; }
19
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
20
+ @keyframes fadeUp { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
21
+ .fade-up { animation: fadeUp 0.25s ease forwards; }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <div id="root"></div>
26
+ <script type="text/babel">
27
+ const { useState, useEffect, useCallback } = React;
28
+
29
+ // ─── Theme ───
30
+ const DARK = {
31
+ bg: "#0a0e14", surface: "#0f1720", surfaceAlt: "#141e2b", border: "#1e2733",
32
+ text: "#c5cdd8", dim: "#6b7d95", accent: "#36d7b7", blue: "#65a9ff",
33
+ warn: "#ffbf57", error: "#ff6f7a", ok: "#50e39f", purple: "#b38aff",
34
+ };
35
+ const LIGHT = {
36
+ bg: "#edf2f8", surface: "#ffffff", surfaceAlt: "#f7f9fd", border: "#cfd9e8",
37
+ text: "#172436", dim: "#5e738f", accent: "#008a76", blue: "#1f6ed4",
38
+ warn: "#b07a00", error: "#c6374b", ok: "#1f9d5b", purple: "#7c5cbf",
39
+ };
40
+
41
+ // ─── API ───
42
+ const API = window.location.origin;
43
+ const parse = async (r) => {
44
+ const ct = (r.headers.get("content-type") || "").toLowerCase();
45
+ if (ct.includes("application/json")) {
46
+ const j = await r.json().catch(() => null);
47
+ if (!r.ok) throw new Error(j?.error || j?.message || `HTTP ${r.status}`);
48
+ return j || {};
49
+ }
50
+ const t = await r.text().catch(() => "");
51
+ if (!r.ok) throw new Error(t || `HTTP ${r.status}`);
52
+ return {};
53
+ };
54
+ const api = {
55
+ get: async (p) => parse(await fetch(`${API}${p}`)),
56
+ post: async (p, b = {}) => parse(await fetch(`${API}${p}`, {
57
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(b),
58
+ })),
59
+ };
60
+
61
+ // ─── Helpers ───
62
+ const timeAgo = (iso) => {
63
+ if (!iso) return "-";
64
+ const s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
65
+ if (isNaN(s) || s < 0) return "-";
66
+ if (s < 60) return `${s}s`;
67
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
68
+ if (s < 86400) return `${Math.floor(s / 3600)}h`;
69
+ return `${Math.floor(s / 86400)}d`;
70
+ };
71
+ const localTime = (iso) => {
72
+ if (!iso) return "-";
73
+ const d = new Date(iso);
74
+ return isNaN(d.getTime()) ? "-" : d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
75
+ };
76
+ const fmtDuration = (iso) => {
77
+ if (!iso) return "-";
78
+ const sec = Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 1000));
79
+ const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
80
+ return h > 0 ? `${h}h ${m}m` : `${m}m ${s}s`;
81
+ };
82
+ const fmtK = n => n >= 1000 ? (n / 1000).toFixed(1) + "K" : String(n);
83
+
84
+ // ─── Tiny Components ───
85
+ const Dot = ({ color, pulse }) => (
86
+ <span style={{
87
+ width: 7, height: 7, borderRadius: "50%", background: color, display: "inline-block",
88
+ boxShadow: `0 0 8px ${color}80`, animation: pulse ? "pulse 1.2s linear infinite" : "none",
89
+ }} />
90
+ );
91
+
92
+ const Badge = ({ color, children }) => (
93
+ <span className="mono" style={{
94
+ fontSize: 9, fontWeight: 600, color, background: `${color}15`, border: `1px solid ${color}30`,
95
+ padding: "1px 5px", borderRadius: 4, letterSpacing: 0.2,
96
+ }}>{children}</span>
97
+ );
98
+
99
+ const MiniBar = ({ value, max = 100, color, height = 4 }) => (
100
+ <div style={{ height, borderRadius: height, background: `${color}20`, overflow: "hidden" }}>
101
+ <div style={{ height: "100%", width: `${Math.min(100, max > 0 ? (value / max) * 100 : 0)}%`, background: color, borderRadius: height, transition: "width 0.3s" }} />
102
+ </div>
103
+ );
104
+
105
+ const Stat = ({ t, label, value, color }) => (
106
+ <div style={{ background: `${color}10`, border: `1px solid ${color}25`, borderRadius: 6, padding: "6px 8px" }}>
107
+ <div className="mono" style={{ fontSize: 9, color: t.dim, textTransform: "uppercase", letterSpacing: 0.8 }}>{label}</div>
108
+ <div className="mono" style={{ fontSize: 16, color, fontWeight: 700, marginTop: 1 }}>{value}</div>
109
+ </div>
110
+ );
111
+
112
+ const Card = ({ t, title, badge, children }) => (
113
+ <section className="fade-up" style={{
114
+ background: t.surface, border: `1px solid ${t.border}`, borderRadius: 8,
115
+ padding: 12, display: "flex", flexDirection: "column", gap: 8,
116
+ }}>
117
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
118
+ <span style={{ fontSize: 11, fontWeight: 700, color: t.dim, textTransform: "uppercase", letterSpacing: 1, flex: 1 }}>{title}</span>
119
+ {badge}
120
+ </div>
121
+ {children}
122
+ </section>
123
+ );
124
+
125
+ // ─── App ───
126
+ function App() {
127
+ const [dark, setDark] = useState(() => localStorage.getItem("c3-nano-theme") !== "light");
128
+ const t = dark ? DARK : LIGHT;
129
+
130
+ const [connected, setConnected] = useState(true);
131
+ const [loading, setLoading] = useState(false);
132
+ const [health, setHealth] = useState(null);
133
+ const [stats, setStats] = useState({});
134
+ const [session, setSession] = useState(null);
135
+ const [activity, setActivity] = useState([]);
136
+ const [notifications, setNotifications] = useState([]);
137
+ const [actionMsg, setActionMsg] = useState("");
138
+ const [showShutdown, setShowShutdown] = useState(false);
139
+ const [shutdownBusy, setShutdownBusy] = useState(false);
140
+ const [shutdownForce, setShutdownForce] = useState(false);
141
+ const [tick, setTick] = useState(0);
142
+
143
+ useEffect(() => {
144
+ localStorage.setItem("c3-nano-theme", dark ? "dark" : "light");
145
+ }, [dark]);
146
+
147
+ const loadHealth = useCallback(async () => {
148
+ try { const h = await api.get("/api/health"); setHealth(h); setConnected(true); } catch { setConnected(false); }
149
+ }, []);
150
+ const loadStats = useCallback(async () => {
151
+ try { const s = await api.get("/api/stats"); setStats(s); setConnected(true); } catch { setConnected(false); }
152
+ }, []);
153
+ const loadSession = useCallback(async () => {
154
+ try { setSession(await api.get("/api/sessions/current") || null); } catch {}
155
+ }, []);
156
+ const loadActivity = useCallback(async () => {
157
+ try { const a = await api.get("/api/activity?limit=12"); setActivity(Array.isArray(a) ? a : []); } catch {}
158
+ }, []);
159
+ const loadNotifications = useCallback(async () => {
160
+ try { const n = await api.get("/api/notifications?limit=30"); setNotifications(Array.isArray(n) ? n : []); } catch {}
161
+ }, []);
162
+
163
+ const refreshAll = useCallback(async () => {
164
+ setLoading(true);
165
+ await Promise.all([loadHealth(), loadStats(), loadSession(), loadActivity(), loadNotifications()]);
166
+ setLoading(false);
167
+ }, [loadHealth, loadStats, loadSession, loadActivity, loadNotifications]);
168
+
169
+ useEffect(() => { refreshAll(); }, [refreshAll]);
170
+ useEffect(() => {
171
+ const fast = setInterval(() => { loadSession(); loadActivity(); }, 5000);
172
+ const mid = setInterval(() => { loadNotifications(); loadStats(); }, 12000);
173
+ const slow = setInterval(loadHealth, 20000);
174
+ const ticker = setInterval(() => setTick(x => x + 1), 1000);
175
+ return () => { clearInterval(fast); clearInterval(mid); clearInterval(slow); clearInterval(ticker); };
176
+ }, [loadSession, loadActivity, loadNotifications, loadStats, loadHealth]);
177
+
178
+ const ackOne = async (id) => { try { await api.post("/api/notifications/ack", { id }); setNotifications(p => p.filter(n => n.id !== id)); } catch {} };
179
+ const ackAll = async () => { try { await api.post("/api/notifications/ack-all"); setNotifications([]); } catch {} };
180
+ const rebuildIndex = async () => {
181
+ setActionMsg("Rebuilding index...");
182
+ try { await api.post("/api/index/rebuild", {}); setActionMsg("Index rebuilt."); loadActivity(); } catch { setActionMsg("Rebuild failed."); }
183
+ setTimeout(() => setActionMsg(""), 3000);
184
+ };
185
+ const confirmShutdown = async (force = false) => {
186
+ setShutdownBusy(true); setShutdownForce(force);
187
+ try { await api.post("/api/shutdown", { force_close_terminal: force }); } catch {}
188
+ setConnected(false);
189
+ setTimeout(() => { try { window.close(); } catch {} window.location.href = "about:blank"; }, 450);
190
+ };
191
+
192
+ const eventSummary = (e) => {
193
+ if (!e) return "-";
194
+ if (e.type === "tool_call") return (e.tool || "tool") + (e.result_summary ? `: ${e.result_summary}` : "");
195
+ if (e.type === "decision") return e.decision || e.data || "decision";
196
+ if (e.type === "file_change") return e.file || e.data || "file change";
197
+ if (e.type === "session_start") return `session started`;
198
+ if (e.type === "session_save") return `session saved`;
199
+ return e.type || "event";
200
+ };
201
+
202
+ // Derived
203
+ const projectName = stats?.project_path ? stats.project_path.split(/[/\\]/).pop() : "C3";
204
+ const orig = stats?.total_original_tokens || 0;
205
+ const comp = stats?.total_compressed_tokens || 0;
206
+ const saved = orig - comp;
207
+ const pct = stats?.savings_pct || 0;
208
+ const convUsage = stats?.conversation_token_usage;
209
+ const sourceUsage = convUsage?.sources || {};
210
+ const sourceEntries = Object.entries(sourceUsage).sort((a, b) => (b[1]?.total_tokens || 0) - (a[1]?.total_tokens || 0));
211
+ const totalSourceTokens = sourceEntries.reduce((sum, [, d]) => sum + (d?.total_tokens || 0), 0);
212
+ const budget = stats?.context_budget;
213
+ const hasBudget = budget && budget.call_count > 0;
214
+ const budgetPct = hasBudget ? Math.min(100, Math.round((budget.response_tokens / (budget.threshold || 35000)) * 100)) : 0;
215
+ const budgetColor = budgetPct >= 90 ? t.error : budgetPct >= 70 ? t.warn : t.accent;
216
+ const sessionDuration = session?.live ? fmtDuration(session.started) : (session?.duration || "-");
217
+ const sourceLabel = session?.source_system || session?.source_ide || "unknown";
218
+
219
+ return (
220
+ <div style={{ width: "100%", height: "100%", display: "flex", flexDirection: "column", background: t.bg, color: t.text }}>
221
+ {/* ── Header ── */}
222
+ <header style={{
223
+ borderBottom: `1px solid ${t.border}`, background: t.surface, padding: "8px 14px",
224
+ display: "flex", alignItems: "center", gap: 10, flexShrink: 0,
225
+ }}>
226
+ <Dot color={connected ? t.ok : t.error} pulse={connected} />
227
+ <div style={{ flex: 1, minWidth: 0 }}>
228
+ <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
229
+ <span style={{ fontSize: 14, fontWeight: 700 }}>
230
+ C<span style={{ color: t.accent }}>3</span> <span style={{ fontWeight: 400, color: t.dim }}>Nano</span>
231
+ </span>
232
+ <span className="mono" style={{ fontSize: 10, color: t.dim }}>{projectName}</span>
233
+ </div>
234
+ </div>
235
+
236
+ {/* Source badges */}
237
+ <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
238
+ {(health?.sources ? Object.entries(health.sources) : []).map(([k, v]) => (
239
+ <Badge key={k} color={v ? t.ok : t.error}>{k}</Badge>
240
+ ))}
241
+ </div>
242
+
243
+ {/* Actions */}
244
+ <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
245
+ <button onClick={refreshAll} disabled={loading}
246
+ className="mono" style={{
247
+ height: 26, padding: "0 8px", borderRadius: 5, border: `1px solid ${t.border}`,
248
+ background: "transparent", color: t.dim, fontSize: 10, cursor: "pointer",
249
+ }}>
250
+ {loading ? "..." : "Refresh"}
251
+ </button>
252
+ <button onClick={() => setDark(v => !v)}
253
+ className="mono" style={{
254
+ height: 26, padding: "0 8px", borderRadius: 5, border: `1px solid ${t.border}`,
255
+ background: "transparent", color: t.dim, fontSize: 10, cursor: "pointer",
256
+ }}>
257
+ {dark ? "Light" : "Dark"}
258
+ </button>
259
+ <button onClick={async () => {
260
+ try { const h = await api.get('/api/hub/info'); window.location.href = h.url; }
261
+ catch { window.location.href = 'http://localhost:3330'; }
262
+ }}
263
+ className="mono" style={{
264
+ height: 26, padding: "0 8px", borderRadius: 5, border: `1px solid ${t.warn}40`,
265
+ background: `${t.warn}12`, color: t.warn, fontSize: 10, fontWeight: 700, cursor: "pointer",
266
+ }}>
267
+ Hub
268
+ </button>
269
+ <a href="/" className="mono" style={{
270
+ height: 26, padding: "0 10px", borderRadius: 5, border: `1px solid ${t.purple}40`,
271
+ background: `${t.purple}12`, color: t.purple, fontSize: 10, fontWeight: 700,
272
+ textDecoration: "none", display: "flex", alignItems: "center",
273
+ }}>
274
+ Full UI
275
+ </a>
276
+ <a href="/edits" className="mono" style={{
277
+ height: 26, padding: "0 10px", borderRadius: 5, border: `1px solid ${t.accent}40`,
278
+ background: `${t.accent}12`, color: t.accent, fontSize: 10, fontWeight: 700,
279
+ textDecoration: "none", display: "flex", alignItems: "center",
280
+ }}>
281
+ Ledger
282
+ </a>
283
+ <button onClick={() => setShowShutdown(true)}
284
+ className="mono" style={{
285
+ height: 26, padding: "0 8px", borderRadius: 5, border: `1px solid ${t.error}40`,
286
+ background: `${t.error}12`, color: t.error, fontSize: 10, fontWeight: 600, cursor: "pointer",
287
+ }}>
288
+ Shut Down
289
+ </button>
290
+ </div>
291
+ </header>
292
+
293
+ {/* ── Content ── */}
294
+ <main style={{ flex: 1, overflow: "auto", padding: 12 }}>
295
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", gap: 10 }}>
296
+
297
+ {/* Card: Overview */}
298
+ <Card t={t} title="Overview" badge={<Badge color={t.accent}>{pct}% savings</Badge>}>
299
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 6 }}>
300
+ <Stat t={t} label="Saved" value={fmtK(saved)} color={t.accent} />
301
+ <Stat t={t} label="Files" value={stats?.index?.files_indexed || 0} color={t.blue} />
302
+ <Stat t={t} label="Sessions" value={stats?.sessions_count || 0} color={t.warn} />
303
+ </div>
304
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
305
+ <span className="mono" style={{ fontSize: 9, color: t.dim, flexShrink: 0 }}>Savings</span>
306
+ <MiniBar value={pct} color={t.accent} />
307
+ <span className="mono" style={{ fontSize: 10, color: t.accent, fontWeight: 600, flexShrink: 0 }}>{pct}%</span>
308
+ </div>
309
+ {hasBudget && (
310
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
311
+ <span className="mono" style={{ fontSize: 9, color: t.dim, flexShrink: 0 }}>Budget</span>
312
+ <MiniBar value={budgetPct} color={budgetColor} />
313
+ <span className="mono" style={{ fontSize: 10, color: budgetColor, fontWeight: 600, flexShrink: 0 }}>{budgetPct}%</span>
314
+ </div>
315
+ )}
316
+ </Card>
317
+
318
+ {/* Card: Current Session */}
319
+ <Card t={t} title="Current Session" badge={<Badge color={t.blue}>source:{sourceLabel}</Badge>}>
320
+ {!session ? (
321
+ <div style={{ color: t.dim, fontSize: 12 }}>No active session.</div>
322
+ ) : (
323
+ <>
324
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }}>
325
+ <Stat t={t} label="Session" value={(session.id || "-").slice(0, 12)} color={t.blue} />
326
+ <Stat t={t} label="Duration" value={sessionDuration} color={t.accent} />
327
+ </div>
328
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 6 }}>
329
+ <Stat t={t} label="Tools" value={Array.isArray(session.tool_calls) ? session.tool_calls.length : (session.tool_calls || 0)} color={t.warn} />
330
+ <Stat t={t} label="Decisions" value={Array.isArray(session.decisions) ? session.decisions.length : (session.decisions || 0)} color={t.purple} />
331
+ <Stat t={t} label="Files" value={Array.isArray(session.files_touched) ? session.files_touched.length : (session.files_touched || 0)} color={t.ok} />
332
+ </div>
333
+ <div className="mono" style={{ fontSize: 9, color: t.dim }}>
334
+ started {localTime(session.started)} ({timeAgo(session.started)} ago)
335
+ </div>
336
+ </>
337
+ )}
338
+ </Card>
339
+
340
+ {/* Card: Token Usage by Source (provider-agnostic) */}
341
+ {sourceEntries.length > 0 && (
342
+ <Card t={t} title="Token Usage" badge={<Badge color={t.purple}>{fmtK(totalSourceTokens)} total</Badge>}>
343
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
344
+ {sourceEntries.map(([name, data], idx) => {
345
+ const tokens = data?.total_tokens || 0;
346
+ const input = data?.input_tokens || 0;
347
+ const output = data?.output_tokens || 0;
348
+ const calls = data?.call_count || data?.sessions || 0;
349
+ const colors = [t.accent, t.blue, t.purple, t.warn, t.error];
350
+ const color = colors[idx % colors.length];
351
+ const sourcePct = totalSourceTokens > 0 ? Math.round((tokens / totalSourceTokens) * 100) : 0;
352
+ return (
353
+ <div key={name} style={{
354
+ background: t.surfaceAlt, border: `1px solid ${t.border}`, borderRadius: 6, padding: "7px 10px",
355
+ }}>
356
+ <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 4 }}>
357
+ <Dot color={color} />
358
+ <span className="mono" style={{ fontSize: 11, fontWeight: 600, color: t.text, flex: 1 }}>{name}</span>
359
+ <span className="mono" style={{ fontSize: 10, color }}>{fmtK(tokens)}</span>
360
+ {calls > 0 && <span className="mono" style={{ fontSize: 9, color: t.dim }}>{calls} calls</span>}
361
+ <Badge color={color}>{sourcePct}%</Badge>
362
+ </div>
363
+ <MiniBar value={sourcePct} color={color} />
364
+ {(input > 0 || output > 0) && (
365
+ <div style={{ display: "flex", gap: 10, marginTop: 4 }}>
366
+ <span className="mono" style={{ fontSize: 9, color: t.dim }}>{fmtK(input)} in</span>
367
+ <span className="mono" style={{ fontSize: 9, color: t.dim }}>{fmtK(output)} out</span>
368
+ </div>
369
+ )}
370
+ </div>
371
+ );
372
+ })}
373
+ </div>
374
+ </Card>
375
+ )}
376
+
377
+ {/* Card: Activity */}
378
+ <Card t={t} title="Activity" badge={<Badge color={t.blue}>{activity.length}</Badge>}>
379
+ <div style={{ display: "flex", flexDirection: "column", gap: 4, maxHeight: 220, overflow: "auto" }}>
380
+ {activity.length === 0 && <div style={{ color: t.dim, fontSize: 12 }}>No recent activity.</div>}
381
+ {activity.map((e, i) => (
382
+ <div key={i} style={{
383
+ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px",
384
+ borderRadius: 5, background: t.surfaceAlt, border: `1px solid ${t.border}`,
385
+ }}>
386
+ <Badge color={t.blue}>{e.type || "event"}</Badge>
387
+ <span className="mono" style={{ fontSize: 10, color: t.text, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
388
+ {eventSummary(e)}
389
+ </span>
390
+ <span className="mono" style={{ fontSize: 9, color: t.dim, flexShrink: 0 }}>{timeAgo(e.timestamp)}</span>
391
+ </div>
392
+ ))}
393
+ </div>
394
+ </Card>
395
+
396
+ {/* Card: Notifications */}
397
+ <Card t={t} title="Notifications"
398
+ badge={
399
+ <div style={{ display: "flex", gap: 4 }}>
400
+ <Badge color={t.warn}>{notifications.length}</Badge>
401
+ {notifications.length > 0 && (
402
+ <button onClick={ackAll} className="mono" style={{
403
+ border: `1px solid ${t.border}`, borderRadius: 4, background: "transparent",
404
+ color: t.dim, fontSize: 9, padding: "1px 5px", cursor: "pointer",
405
+ }}>Dismiss all</button>
406
+ )}
407
+ </div>
408
+ }>
409
+ <div style={{ display: "flex", flexDirection: "column", gap: 4, maxHeight: 220, overflow: "auto" }}>
410
+ {notifications.length === 0 && <div style={{ color: t.dim, fontSize: 12 }}>No notifications.</div>}
411
+ {notifications.slice(0, 15).map((n, i) => {
412
+ const sev = (n.severity || "info").toLowerCase();
413
+ const sevColor = sev === "critical" ? t.error : sev === "warning" ? t.warn : t.blue;
414
+ return (
415
+ <div key={n.id || i} style={{
416
+ display: "flex", alignItems: "center", gap: 6, padding: "5px 8px",
417
+ borderRadius: 5, background: t.surfaceAlt, borderLeft: `3px solid ${sevColor}`,
418
+ }}>
419
+ <Badge color={sevColor}>{sev}</Badge>
420
+ <span className="mono" style={{ fontSize: 10, color: t.text, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
421
+ {n.title || "notification"}{n.message ? ` - ${n.message}` : ""}
422
+ </span>
423
+ <button onClick={() => ackOne(n.id)} style={{
424
+ border: "none", background: "transparent", color: t.dim, fontSize: 9, cursor: "pointer", padding: "2px",
425
+ }}>x</button>
426
+ </div>
427
+ );
428
+ })}
429
+ </div>
430
+ </Card>
431
+
432
+ {/* Card: Quick Actions */}
433
+ <Card t={t} title="Actions">
434
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }}>
435
+ <button onClick={rebuildIndex} className="mono" style={{
436
+ height: 34, borderRadius: 6, border: `1px solid ${t.blue}40`,
437
+ background: `${t.blue}12`, color: t.blue, fontWeight: 700, fontSize: 11, cursor: "pointer",
438
+ }}>Rebuild Index</button>
439
+ <button onClick={loadHealth} className="mono" style={{
440
+ height: 34, borderRadius: 6, border: `1px solid ${t.accent}40`,
441
+ background: `${t.accent}12`, color: t.accent, fontWeight: 700, fontSize: 11, cursor: "pointer",
442
+ }}>Refresh Health</button>
443
+ </div>
444
+ {actionMsg && (
445
+ <div className="mono" style={{ fontSize: 10, color: actionMsg.includes("fail") ? t.error : t.dim }}>
446
+ {actionMsg}
447
+ </div>
448
+ )}
449
+ </Card>
450
+
451
+ </div>
452
+ </main>
453
+
454
+ {/* ── Shutdown Modal ── */}
455
+ {showShutdown && (
456
+ <div style={{
457
+ position: "fixed", inset: 0, background: "rgba(4,10,18,0.6)",
458
+ display: "flex", alignItems: "center", justifyContent: "center", zIndex: 9999,
459
+ }}>
460
+ <div style={{
461
+ width: "min(400px, calc(100vw - 24px))",
462
+ background: t.surface, border: `1px solid ${t.border}`, borderRadius: 8,
463
+ boxShadow: "0 16px 48px rgba(0,0,0,0.3)", padding: 16, display: "flex", flexDirection: "column", gap: 10,
464
+ }}>
465
+ <div style={{ fontSize: 14, fontWeight: 700, color: t.text }}>Shut Down C3?</div>
466
+ <div style={{ fontSize: 12, color: t.dim, lineHeight: 1.5 }}>
467
+ Stops the C3 server and ends the current session.
468
+ </div>
469
+ <div style={{ display: "flex", justifyContent: "flex-end", gap: 6, marginTop: 4 }}>
470
+ <button onClick={() => { if (!shutdownBusy) setShowShutdown(false); }}
471
+ disabled={shutdownBusy} className="mono" style={{
472
+ height: 30, padding: "0 10px", borderRadius: 5, border: `1px solid ${t.border}`,
473
+ background: "transparent", color: t.dim, fontSize: 10, cursor: shutdownBusy ? "default" : "pointer",
474
+ opacity: shutdownBusy ? 0.5 : 1,
475
+ }}>Cancel</button>
476
+ <button onClick={() => confirmShutdown(false)} disabled={shutdownBusy}
477
+ className="mono" style={{
478
+ height: 30, padding: "0 10px", borderRadius: 5, border: `1px solid ${t.error}60`,
479
+ background: `${t.error}15`, color: t.error, fontSize: 10, fontWeight: 700, cursor: "pointer",
480
+ opacity: shutdownBusy ? 0.7 : 1,
481
+ }}>
482
+ {shutdownBusy && !shutdownForce ? "Stopping..." : "Shut Down"}
483
+ </button>
484
+ <button onClick={() => confirmShutdown(true)} disabled={shutdownBusy}
485
+ className="mono" style={{
486
+ height: 30, padding: "0 10px", borderRadius: 5, border: `1px solid ${t.error}80`,
487
+ background: `${t.error}22`, color: t.error, fontSize: 10, fontWeight: 700, cursor: "pointer",
488
+ opacity: shutdownBusy ? 0.7 : 1,
489
+ }}>
490
+ {shutdownBusy && shutdownForce ? "Closing..." : "Shut Down + Close Terminal"}
491
+ </button>
492
+ </div>
493
+ </div>
494
+ </div>
495
+ )}
496
+ </div>
497
+ );
498
+ }
499
+
500
+ ReactDOM.render(React.createElement(App), document.getElementById("root"));
501
+ </script>
502
+ </body>
503
+ </html>