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.
- cli/__init__.py +1 -0
- cli/_hook_utils.py +99 -0
- cli/c3.py +6152 -0
- cli/commands/__init__.py +1 -0
- cli/commands/common.py +312 -0
- cli/commands/parser.py +286 -0
- cli/docs.html +3178 -0
- cli/edits.html +878 -0
- cli/hook_auto_snapshot.py +142 -0
- cli/hook_c3_signal.py +61 -0
- cli/hook_c3read.py +116 -0
- cli/hook_edit_ledger.py +213 -0
- cli/hook_edit_unlock.py +170 -0
- cli/hook_filter.py +130 -0
- cli/hook_ghost_files.py +238 -0
- cli/hook_pretool_enforce.py +334 -0
- cli/hook_read.py +200 -0
- cli/hook_session_stats.py +62 -0
- cli/hook_terse_advisor.py +190 -0
- cli/hub.html +3764 -0
- cli/hub_server.py +1619 -0
- cli/mcp_proxy.py +428 -0
- cli/mcp_server.py +660 -0
- cli/server.py +2985 -0
- cli/tools/__init__.py +4 -0
- cli/tools/_helpers.py +65 -0
- cli/tools/agent.py +1165 -0
- cli/tools/compress.py +215 -0
- cli/tools/delegate.py +1184 -0
- cli/tools/edit.py +313 -0
- cli/tools/edits.py +118 -0
- cli/tools/filter.py +285 -0
- cli/tools/impact.py +163 -0
- cli/tools/memory.py +469 -0
- cli/tools/read.py +224 -0
- cli/tools/search.py +337 -0
- cli/tools/session.py +95 -0
- cli/tools/shell.py +193 -0
- cli/tools/status.py +306 -0
- cli/tools/validate.py +310 -0
- cli/ui/api.js +36 -0
- cli/ui/app.js +207 -0
- cli/ui/components/chat.js +758 -0
- cli/ui/components/dashboard.js +689 -0
- cli/ui/components/edits.js +220 -0
- cli/ui/components/instructions.js +481 -0
- cli/ui/components/memory.js +626 -0
- cli/ui/components/sessions.js +606 -0
- cli/ui/components/settings.js +1404 -0
- cli/ui/components/sidebar.js +156 -0
- cli/ui/icons.js +51 -0
- cli/ui/shared.js +119 -0
- cli/ui/theme.js +22 -0
- cli/ui.html +168 -0
- cli/ui_legacy.html +6797 -0
- cli/ui_nano.html +503 -0
- code_context_control-2.28.0.dist-info/METADATA +248 -0
- code_context_control-2.28.0.dist-info/RECORD +150 -0
- code_context_control-2.28.0.dist-info/WHEEL +5 -0
- code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
- code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
- code_context_control-2.28.0.dist-info/top_level.txt +5 -0
- core/__init__.py +75 -0
- core/config.py +269 -0
- core/ide.py +188 -0
- oracle/__init__.py +1 -0
- oracle/config.py +75 -0
- oracle/oracle.html +3900 -0
- oracle/oracle_server.py +663 -0
- oracle/services/__init__.py +1 -0
- oracle/services/c3_bridge.py +210 -0
- oracle/services/chat_engine.py +1103 -0
- oracle/services/chat_store.py +155 -0
- oracle/services/cross_memory.py +154 -0
- oracle/services/federated_graph.py +463 -0
- oracle/services/health_checker.py +117 -0
- oracle/services/insight_engine.py +307 -0
- oracle/services/memory_reader.py +106 -0
- oracle/services/memory_writer.py +182 -0
- oracle/services/ollama_bridge.py +332 -0
- oracle/services/project_scanner.py +87 -0
- oracle/services/review_agent.py +206 -0
- services/__init__.py +1 -0
- services/activity_log.py +93 -0
- services/agent_base.py +124 -0
- services/agents.py +1529 -0
- services/auto_memory.py +407 -0
- services/bench/__init__.py +6 -0
- services/bench/external/__init__.py +29 -0
- services/bench/external/aider_polyglot.py +405 -0
- services/bench/external/swe_bench.py +485 -0
- services/benchmark_dashboard.py +596 -0
- services/claude_md.py +785 -0
- services/compressor.py +592 -0
- services/context_snapshot.py +356 -0
- services/conversation_store.py +870 -0
- services/doc_index.py +537 -0
- services/e2e_benchmark.py +2884 -0
- services/e2e_evaluator.py +396 -0
- services/e2e_tasks.py +743 -0
- services/edit_ledger.py +459 -0
- services/embedding_index.py +341 -0
- services/error_reporting.py +123 -0
- services/file_memory.py +734 -0
- services/hub_service.py +585 -0
- services/indexer.py +712 -0
- services/memory.py +318 -0
- services/memory_consolidator.py +538 -0
- services/memory_graph.py +382 -0
- services/memory_grounder.py +304 -0
- services/memory_scorer.py +246 -0
- services/metrics.py +86 -0
- services/notifications.py +209 -0
- services/ollama_client.py +201 -0
- services/output_filter.py +488 -0
- services/parser.py +1238 -0
- services/project_manager.py +579 -0
- services/protocol.py +306 -0
- services/proxy_state.py +152 -0
- services/retrieval_broker.py +129 -0
- services/router.py +414 -0
- services/runtime.py +326 -0
- services/session_benchmark.py +1945 -0
- services/session_manager.py +1026 -0
- services/session_preloader.py +251 -0
- services/text_index.py +90 -0
- services/tool_classifier.py +176 -0
- services/transcript_index.py +340 -0
- services/validation_cache.py +155 -0
- services/vector_store.py +299 -0
- services/version_tracker.py +271 -0
- services/watcher.py +192 -0
- tui/__init__.py +0 -0
- tui/backend.py +59 -0
- tui/main.py +145 -0
- tui/screens/__init__.py +1 -0
- tui/screens/benchmark_view.py +109 -0
- tui/screens/claudemd_view.py +46 -0
- tui/screens/compress_view.py +52 -0
- tui/screens/index_view.py +74 -0
- tui/screens/init_view.py +82 -0
- tui/screens/mcp_view.py +73 -0
- tui/screens/optimize_view.py +41 -0
- tui/screens/pipe_view.py +46 -0
- tui/screens/projects_view.py +355 -0
- tui/screens/search_view.py +55 -0
- tui/screens/session_view.py +143 -0
- tui/screens/stats.py +158 -0
- tui/screens/ui_view.py +54 -0
- 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>
|