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
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
// ─── Dashboard ────────────────────────────
|
|
2
|
+
// Globals used: T, I, GlowDot, Badge, StatBox, ProgressBar, Btn, Section, api,
|
|
3
|
+
// timeAgo, localTime, localDate, getToolColor, toolColors, useLiveDuration,
|
|
4
|
+
// useState, useEffect, useCallback (React hooks via Babel standalone)
|
|
5
|
+
|
|
6
|
+
const Dashboard = ({ stats, loading, notifications = [], ackNotification, ackAllNotifications }) => {
|
|
7
|
+
// ── State ──
|
|
8
|
+
const [memFacts, setMemFacts] = useState([]);
|
|
9
|
+
const [mcpStatus, setMcpStatus] = useState(null);
|
|
10
|
+
const [meta, setMeta] = useState({});
|
|
11
|
+
const [editing, setEditing] = useState(false);
|
|
12
|
+
const [editForm, setEditForm] = useState({});
|
|
13
|
+
const [saving, setSaving] = useState(false);
|
|
14
|
+
const [showUsage, setShowUsage] = useState(false);
|
|
15
|
+
const [session, setSession] = useState(null);
|
|
16
|
+
const [activity, setActivity] = useState([]);
|
|
17
|
+
const [health, setHealth] = useState(null);
|
|
18
|
+
const [watcher, setWatcher] = useState([]);
|
|
19
|
+
const [actionMsg, setActionMsg] = useState("");
|
|
20
|
+
const [liveTokens, setLiveTokens] = useState(null); // live Claude transcript tokens
|
|
21
|
+
|
|
22
|
+
// ── Data fetch on mount + polling ──
|
|
23
|
+
const loadSession = useCallback(async () => {
|
|
24
|
+
try { setSession(await api.get('/api/sessions/current') || null); } catch {}
|
|
25
|
+
}, []);
|
|
26
|
+
const loadActivity = useCallback(async () => {
|
|
27
|
+
try { const a = await api.get('/api/activity?limit=8'); setActivity(Array.isArray(a) ? a : []); } catch {}
|
|
28
|
+
}, []);
|
|
29
|
+
const loadHealth = useCallback(async () => {
|
|
30
|
+
try { setHealth(await api.get('/api/health')); } catch {}
|
|
31
|
+
}, []);
|
|
32
|
+
const loadWatcher = useCallback(async () => {
|
|
33
|
+
try { const w = await api.get('/api/watcher/changes'); setWatcher(Array.isArray(w) ? w : []); } catch {}
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
api.get('/api/memory/facts').then(setMemFacts).catch(() => {});
|
|
38
|
+
api.get('/api/mcp/status').then(setMcpStatus).catch(() => {});
|
|
39
|
+
api.get('/api/project/meta').then(setMeta).catch(() => {});
|
|
40
|
+
loadSession();
|
|
41
|
+
loadActivity();
|
|
42
|
+
loadHealth();
|
|
43
|
+
loadWatcher();
|
|
44
|
+
api.get('/api/session-stats/live').then(setLiveTokens).catch(() => {});
|
|
45
|
+
const fast = setInterval(() => {
|
|
46
|
+
loadSession(); loadActivity();
|
|
47
|
+
api.get('/api/session-stats/live').then(setLiveTokens).catch(() => {});
|
|
48
|
+
}, 5000);
|
|
49
|
+
const slow = setInterval(() => { loadHealth(); loadWatcher(); }, 20000);
|
|
50
|
+
return () => { clearInterval(fast); clearInterval(slow); };
|
|
51
|
+
}, [loadSession, loadActivity, loadHealth, loadWatcher]);
|
|
52
|
+
|
|
53
|
+
// ── Project meta edit helpers ──
|
|
54
|
+
const saveMeta = async () => {
|
|
55
|
+
setSaving(true);
|
|
56
|
+
try {
|
|
57
|
+
const updated = await api.put('/api/project/meta', editForm);
|
|
58
|
+
setMeta(updated);
|
|
59
|
+
setEditing(false);
|
|
60
|
+
} catch (e) {}
|
|
61
|
+
setSaving(false);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const openEdit = () => {
|
|
65
|
+
setEditForm({
|
|
66
|
+
name: meta.name || projectName,
|
|
67
|
+
tech_stack: meta.tech_stack || stats?.tech_stack || '',
|
|
68
|
+
description: meta.description || '',
|
|
69
|
+
});
|
|
70
|
+
setEditing(true);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ── Quick Actions ──
|
|
74
|
+
const rebuildIndex = async () => {
|
|
75
|
+
setActionMsg("Rebuilding index...");
|
|
76
|
+
try { await api.post('/api/index/rebuild', {}); setActionMsg("Index rebuilt."); loadActivity(); }
|
|
77
|
+
catch { setActionMsg("Rebuild failed."); }
|
|
78
|
+
setTimeout(() => setActionMsg(""), 3000);
|
|
79
|
+
};
|
|
80
|
+
const openExplorer = async () => {
|
|
81
|
+
try { await api.post('/api/projects/open', { path: stats?.project_path }); }
|
|
82
|
+
catch {}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ── Derived values ──
|
|
86
|
+
const projectName = stats?.project_path ? stats.project_path.split(/[/\\]/).pop() : "\u2014";
|
|
87
|
+
const orig = stats?.total_original_tokens || 0;
|
|
88
|
+
const comp = stats?.total_compressed_tokens || 0;
|
|
89
|
+
const saved = orig - comp;
|
|
90
|
+
const pct = stats?.savings_pct || 0;
|
|
91
|
+
const cost = (saved * 0.003 / 1000).toFixed(2);
|
|
92
|
+
const totalLines = stats?.total_lines || 0;
|
|
93
|
+
|
|
94
|
+
// Provider-agnostic token usage from conversation sources
|
|
95
|
+
const convUsage = stats?.conversation_token_usage;
|
|
96
|
+
const sourceUsage = convUsage?.sources || {};
|
|
97
|
+
const sourceEntries = Object.entries(sourceUsage).sort((a, b) => (b[1]?.total_tokens || 0) - (a[1]?.total_tokens || 0));
|
|
98
|
+
const totalSourceTokens = sourceEntries.reduce((sum, [, d]) => sum + (d?.total_tokens || 0), 0);
|
|
99
|
+
const topSourceSummary = sourceEntries.slice(0, 3)
|
|
100
|
+
.map(([name, d]) => `${name}:${(((d?.total_tokens || 0) / 1000) || 0).toFixed(1)}K`)
|
|
101
|
+
.join(" \u00b7 ");
|
|
102
|
+
|
|
103
|
+
// Budget
|
|
104
|
+
const b = stats?.context_budget;
|
|
105
|
+
const hasBudget = b && b.call_count > 0;
|
|
106
|
+
const overBudget = b && b.response_tokens >= (b.threshold || 35000);
|
|
107
|
+
const budgetPct = hasBudget
|
|
108
|
+
? Math.min(100, Math.round((b.response_tokens / (b.threshold || 35000)) * 100))
|
|
109
|
+
: 0;
|
|
110
|
+
const budgetColor = overBudget ? T.error : budgetPct >= 70 ? T.warn : T.accent;
|
|
111
|
+
|
|
112
|
+
const fmtK = n => n >= 1000 ? (n / 1000).toFixed(1) + "K" : String(n);
|
|
113
|
+
|
|
114
|
+
// Session
|
|
115
|
+
const sessionDuration = useLiveDuration(session?.started, session?.live);
|
|
116
|
+
|
|
117
|
+
// File type distribution from stats
|
|
118
|
+
const files = stats?.files || [];
|
|
119
|
+
const extCounts = {};
|
|
120
|
+
files.forEach(f => { extCounts[f.type] = (extCounts[f.type] || 0) + 1; });
|
|
121
|
+
const topExts = Object.entries(extCounts).sort((a, bb) => bb[1] - a[1]).slice(0, 6);
|
|
122
|
+
const extColorMap = {
|
|
123
|
+
tsx: T.blue, ts: T.purple, py: T.accent, jsx: T.blue, js: T.warn,
|
|
124
|
+
yaml: T.warn, json: T.textMuted, md: T.textMuted, css: T.purple,
|
|
125
|
+
html: T.error, r: T.accent, sh: T.warn, go: T.blue, rs: T.error,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Health sources
|
|
129
|
+
const healthSources = health?.sources ? Object.entries(health.sources).filter(([k]) => k !== "mcp_mode") : [];
|
|
130
|
+
|
|
131
|
+
// ── Local Section component (collapsible header) ──
|
|
132
|
+
const SectionBlock = ({ label, icon, color, open, onToggle, badge, children }) => (
|
|
133
|
+
<div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, overflow: "hidden" }}>
|
|
134
|
+
<div
|
|
135
|
+
onClick={onToggle}
|
|
136
|
+
style={{ display: "flex", alignItems: "center", gap: 8, padding: "12px 16px", cursor: "pointer", userSelect: "none" }}
|
|
137
|
+
>
|
|
138
|
+
<I name={icon} size={13} color={color || T.textMuted} />
|
|
139
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1, flex: 1 }}>
|
|
140
|
+
{label}
|
|
141
|
+
</span>
|
|
142
|
+
{badge}
|
|
143
|
+
<I
|
|
144
|
+
name="chevron"
|
|
145
|
+
size={12}
|
|
146
|
+
color={T.textMuted}
|
|
147
|
+
style={{ transform: open ? "rotate(90deg)" : "rotate(0deg)", transition: "transform 0.15s" }}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
{open && <div style={{ padding: "0 16px 16px" }}>{children}</div>}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Event summary helper
|
|
155
|
+
const eventSummary = (e) => {
|
|
156
|
+
if (!e) return "-";
|
|
157
|
+
if (e.type === "tool_call") return (e.tool || "tool") + (e.result_summary ? `: ${e.result_summary}` : "");
|
|
158
|
+
if (e.type === "decision") return e.decision || e.data || "decision";
|
|
159
|
+
if (e.type === "file_change") return e.file || e.data || "file change";
|
|
160
|
+
if (e.type === "session_start") return "session started";
|
|
161
|
+
if (e.type === "session_save") return "session saved";
|
|
162
|
+
return e.type || "event";
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// ── Render ──
|
|
166
|
+
return (
|
|
167
|
+
<div className="fade-up" style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
|
168
|
+
|
|
169
|
+
{/* ── Row 1: Project header ── */}
|
|
170
|
+
{editing ? (
|
|
171
|
+
<div style={{
|
|
172
|
+
background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8,
|
|
173
|
+
padding: "14px 16px", display: "flex", flexDirection: "column", gap: 10,
|
|
174
|
+
}}>
|
|
175
|
+
<div style={{ fontSize: 10, fontWeight: 700, color: T.textDim, textTransform: "uppercase", letterSpacing: 1 }}>
|
|
176
|
+
Edit Project Info
|
|
177
|
+
</div>
|
|
178
|
+
<div style={{ display: "flex", gap: 10 }}>
|
|
179
|
+
<div style={{ flex: 2 }}>
|
|
180
|
+
<div style={{ fontSize: 10, color: T.textDim, marginBottom: 3 }}>Name</div>
|
|
181
|
+
<input
|
|
182
|
+
value={editForm.name || ''}
|
|
183
|
+
onChange={e => setEditForm(p => ({ ...p, name: e.target.value }))}
|
|
184
|
+
onKeyDown={e => { if (e.key === 'Enter') saveMeta(); if (e.key === 'Escape') setEditing(false); }}
|
|
185
|
+
style={{
|
|
186
|
+
width: "100%", background: T.bg, border: `1px solid ${T.border}`, borderRadius: 4,
|
|
187
|
+
padding: "5px 8px", fontSize: 13, color: T.text, fontFamily: "'JetBrains Mono', monospace", outline: "none",
|
|
188
|
+
}}
|
|
189
|
+
placeholder={projectName}
|
|
190
|
+
autoFocus
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
<div style={{ flex: 1 }}>
|
|
194
|
+
<div style={{ fontSize: 10, color: T.textDim, marginBottom: 3 }}>Tech Stack</div>
|
|
195
|
+
<input
|
|
196
|
+
value={editForm.tech_stack || ''}
|
|
197
|
+
onChange={e => setEditForm(p => ({ ...p, tech_stack: e.target.value }))}
|
|
198
|
+
onKeyDown={e => { if (e.key === 'Enter') saveMeta(); if (e.key === 'Escape') setEditing(false); }}
|
|
199
|
+
style={{
|
|
200
|
+
width: "100%", background: T.bg, border: `1px solid ${T.border}`, borderRadius: 4,
|
|
201
|
+
padding: "5px 8px", fontSize: 13, color: T.text, fontFamily: "'JetBrains Mono', monospace", outline: "none",
|
|
202
|
+
}}
|
|
203
|
+
placeholder="e.g. Python | React"
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<div>
|
|
208
|
+
<div style={{ fontSize: 10, color: T.textDim, marginBottom: 3 }}>Description</div>
|
|
209
|
+
<input
|
|
210
|
+
value={editForm.description || ''}
|
|
211
|
+
onChange={e => setEditForm(p => ({ ...p, description: e.target.value }))}
|
|
212
|
+
onKeyDown={e => { if (e.key === 'Enter') saveMeta(); if (e.key === 'Escape') setEditing(false); }}
|
|
213
|
+
style={{
|
|
214
|
+
width: "100%", background: T.bg, border: `1px solid ${T.border}`, borderRadius: 4,
|
|
215
|
+
padding: "5px 8px", fontSize: 13, color: T.text, fontFamily: "'DM Sans', sans-serif", outline: "none",
|
|
216
|
+
}}
|
|
217
|
+
placeholder="Short project description"
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
|
221
|
+
<button
|
|
222
|
+
onClick={() => setEditing(false)}
|
|
223
|
+
style={{
|
|
224
|
+
padding: "5px 12px", borderRadius: 4, border: `1px solid ${T.border}`, background: "transparent",
|
|
225
|
+
color: T.textMuted, fontSize: 12, cursor: "pointer", fontFamily: "'DM Sans', sans-serif",
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
Cancel
|
|
229
|
+
</button>
|
|
230
|
+
<button
|
|
231
|
+
onClick={saveMeta}
|
|
232
|
+
disabled={saving}
|
|
233
|
+
style={{
|
|
234
|
+
padding: "5px 12px", borderRadius: 4, border: "none", background: T.accent, color: T.bg,
|
|
235
|
+
fontSize: 12, fontWeight: 600, cursor: saving ? "default" : "pointer", opacity: saving ? 0.7 : 1,
|
|
236
|
+
display: "flex", alignItems: "center", gap: 5, fontFamily: "'DM Sans', sans-serif",
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
<I name={saving ? "refresh" : "save"} size={11} color={T.bg}
|
|
240
|
+
style={saving ? { animation: "spin 0.6s linear infinite" } : {}} />
|
|
241
|
+
{saving ? "Saving..." : "Save"}
|
|
242
|
+
</button>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
) : (
|
|
246
|
+
<div style={{ display: "flex", alignItems: "center", gap: 12, padding: "6px 0" }}>
|
|
247
|
+
<span className="mono" style={{ fontSize: 17, fontWeight: 700, color: T.accent }}>
|
|
248
|
+
{meta.name || projectName}
|
|
249
|
+
</span>
|
|
250
|
+
{(meta.tech_stack || stats?.tech_stack) && (
|
|
251
|
+
<Badge color={T.purple}>{meta.tech_stack || stats?.tech_stack}</Badge>
|
|
252
|
+
)}
|
|
253
|
+
{meta.description && (
|
|
254
|
+
<span style={{ fontSize: 12, color: T.textDim, fontStyle: "italic" }}>{meta.description}</span>
|
|
255
|
+
)}
|
|
256
|
+
<Badge color={mcpStatus?.configured ? T.accent : T.textMuted}>
|
|
257
|
+
{mcpStatus?.configured ? "MCP Active" : "MCP Off"}
|
|
258
|
+
</Badge>
|
|
259
|
+
<span style={{ flex: 1 }} />
|
|
260
|
+
<span className="mono" style={{ fontSize: 11, color: T.textDim }}>{stats?.project_path || ""}</span>
|
|
261
|
+
<button
|
|
262
|
+
onClick={openEdit}
|
|
263
|
+
title="Edit project info"
|
|
264
|
+
style={{
|
|
265
|
+
padding: "3px 6px", borderRadius: 4, border: `1px solid ${T.border}`, background: "transparent",
|
|
266
|
+
cursor: "pointer", display: "flex", alignItems: "center", flexShrink: 0,
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
<I name="edit" size={11} color={T.textDim} />
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
|
|
274
|
+
{/* ── Row 2: Hero stats strip ── */}
|
|
275
|
+
<div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
|
|
276
|
+
<StatBox
|
|
277
|
+
label="Tokens Saved"
|
|
278
|
+
value={saved >= 1000 ? (saved / 1000).toFixed(1) + "K" : saved}
|
|
279
|
+
sub={`${pct}% rate \u00b7 $${cost} saved`}
|
|
280
|
+
color={T.accent}
|
|
281
|
+
loading={loading}
|
|
282
|
+
/>
|
|
283
|
+
<StatBox
|
|
284
|
+
label="Files"
|
|
285
|
+
value={stats?.index?.files_indexed || 0}
|
|
286
|
+
sub={`${stats?.index?.total_chunks || 0} chunks \u00b7 ${((orig / 1000) || 0).toFixed(1)}K tokens`}
|
|
287
|
+
color={T.blue}
|
|
288
|
+
loading={loading}
|
|
289
|
+
/>
|
|
290
|
+
<StatBox
|
|
291
|
+
label="Sessions"
|
|
292
|
+
value={stats?.sessions_count || 0}
|
|
293
|
+
sub={`${stats?.total_decisions || 0} decisions \u00b7 ${memFacts.length} facts`}
|
|
294
|
+
color={T.warn}
|
|
295
|
+
loading={loading}
|
|
296
|
+
/>
|
|
297
|
+
{sourceEntries.length > 0 && (
|
|
298
|
+
<StatBox
|
|
299
|
+
label="Token Sources"
|
|
300
|
+
value={fmtK(totalSourceTokens)}
|
|
301
|
+
sub={topSourceSummary}
|
|
302
|
+
color={T.purple}
|
|
303
|
+
/>
|
|
304
|
+
)}
|
|
305
|
+
{hasBudget && (
|
|
306
|
+
<StatBox
|
|
307
|
+
label="Budget"
|
|
308
|
+
value={`${budgetPct}%`}
|
|
309
|
+
sub={`${fmtK(b.response_tokens)} C3 tokens \u00b7 ${b.call_count} calls`}
|
|
310
|
+
color={budgetColor}
|
|
311
|
+
/>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{/* ── Row 3: Savings + Budget progress bars ── */}
|
|
316
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
317
|
+
<span style={{ fontSize: 11, color: T.textMuted, flexShrink: 0 }}>Savings</span>
|
|
318
|
+
<ProgressBar value={pct} max={100} color={T.accent} height={6} />
|
|
319
|
+
<span className="mono" style={{ fontSize: 12, color: T.accent, fontWeight: 600, flexShrink: 0 }}>{pct}%</span>
|
|
320
|
+
{hasBudget && (
|
|
321
|
+
<>
|
|
322
|
+
<span style={{ color: T.border, margin: "0 4px" }}>|</span>
|
|
323
|
+
<span style={{ fontSize: 11, color: T.textMuted, flexShrink: 0 }}>Budget</span>
|
|
324
|
+
<ProgressBar value={b.response_tokens} max={b.threshold || 35000} color={budgetColor} height={6} />
|
|
325
|
+
<span className="mono" style={{ fontSize: 12, color: budgetColor, fontWeight: 600, flexShrink: 0 }}>
|
|
326
|
+
{fmtK(b.response_tokens)}
|
|
327
|
+
</span>
|
|
328
|
+
</>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
{/* ── Row 4: Two-column layout: Session + Health | Codebase + Actions ── */}
|
|
333
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
|
|
334
|
+
|
|
335
|
+
{/* Left: Current Session */}
|
|
336
|
+
<div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: "14px 16px" }}>
|
|
337
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
|
|
338
|
+
<I name="clock" size={13} color={T.blue} />
|
|
339
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1 }}>
|
|
340
|
+
Current Session
|
|
341
|
+
</span>
|
|
342
|
+
<span style={{ flex: 1 }} />
|
|
343
|
+
{session?.live && <Badge color={T.accent}>Live</Badge>}
|
|
344
|
+
{session && (
|
|
345
|
+
<Badge color={T.blue}>
|
|
346
|
+
{session.source_system || session.source_ide || "unknown"}
|
|
347
|
+
</Badge>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
{!session ? (
|
|
351
|
+
<div style={{ color: T.textDim, fontSize: 12, padding: "8px 0" }}>No active session.</div>
|
|
352
|
+
) : (
|
|
353
|
+
<>
|
|
354
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 8, marginBottom: 10 }}>
|
|
355
|
+
{[
|
|
356
|
+
{ label: "Duration", value: sessionDuration || "-", color: T.accent },
|
|
357
|
+
{ label: "Tools", value: Array.isArray(session.tool_calls) ? session.tool_calls.length : (session.tool_calls || 0), color: T.warn },
|
|
358
|
+
{ label: "Decisions", value: Array.isArray(session.decisions) ? session.decisions.length : (session.decisions || 0), color: T.purple },
|
|
359
|
+
{ label: "Files", value: Array.isArray(session.files_touched) ? session.files_touched.length : (session.files_touched || 0), color: T.blue },
|
|
360
|
+
].map(s => (
|
|
361
|
+
<div key={s.label} style={{ background: `${s.color}10`, border: `1px solid ${s.color}25`, borderRadius: 6, padding: "6px 8px" }}>
|
|
362
|
+
<div className="mono" style={{ fontSize: 9, color: T.textDim, textTransform: "uppercase", letterSpacing: 0.8 }}>{s.label}</div>
|
|
363
|
+
<div className="mono" style={{ fontSize: 16, fontWeight: 700, color: s.color, marginTop: 2 }}>{s.value}</div>
|
|
364
|
+
</div>
|
|
365
|
+
))}
|
|
366
|
+
</div>
|
|
367
|
+
<div className="mono" style={{ fontSize: 10, color: T.textDim }}>
|
|
368
|
+
ID: {(session.id || "-").slice(0, 16)} \u00b7 started {localTime(session.started)}
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
{/* Live Claude Code token ticker — updates each exchange */}
|
|
372
|
+
{liveTokens && liveTokens.input_tokens > 0 && (
|
|
373
|
+
<div style={{ marginTop: 8, padding: '7px 10px', background: `${T.blue}08`, border: `1px solid ${T.blue}20`, borderRadius: 6 }}>
|
|
374
|
+
<div style={{ fontSize: 9, color: T.blue, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 5 }}>
|
|
375
|
+
Live Tokens · {liveTokens.turns || 0} turns
|
|
376
|
+
</div>
|
|
377
|
+
<div style={{ display: 'flex', gap: 12 }} className="mono">
|
|
378
|
+
{[['In', liveTokens.input_tokens, T.blue],
|
|
379
|
+
['Out', liveTokens.output_tokens, T.accent],
|
|
380
|
+
['Cache-rd', liveTokens.cache_read_tokens, T.purple],
|
|
381
|
+
].map(([label, val, color]) => (
|
|
382
|
+
<span key={label} style={{ fontSize: 11 }}>
|
|
383
|
+
<span style={{ color: T.textDim }}>{label}: </span>
|
|
384
|
+
<span style={{ color, fontWeight: 600 }}>
|
|
385
|
+
{val >= 1000 ? (val / 1000).toFixed(1) + 'K' : val}
|
|
386
|
+
</span>
|
|
387
|
+
</span>
|
|
388
|
+
))}
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
)} </>
|
|
392
|
+
)}
|
|
393
|
+
|
|
394
|
+
{/* Health sources inline */}
|
|
395
|
+
{healthSources.length > 0 && (
|
|
396
|
+
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 10, paddingTop: 10, borderTop: `1px solid ${T.border}` }}>
|
|
397
|
+
<span style={{ fontSize: 10, color: T.textDim, alignSelf: "center", marginRight: 2 }}>Services:</span>
|
|
398
|
+
{healthSources.map(([name, ok]) => (
|
|
399
|
+
<span key={name} className="mono" style={{
|
|
400
|
+
padding: "2px 6px", borderRadius: 4, fontSize: 9,
|
|
401
|
+
background: ok ? `${T.accent}15` : `${T.error}15`,
|
|
402
|
+
color: ok ? T.accent : T.error,
|
|
403
|
+
border: `1px solid ${ok ? T.accent : T.error}30`,
|
|
404
|
+
}}>{name}</span>
|
|
405
|
+
))}
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
{/* Right: Codebase + Quick Actions */}
|
|
411
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
|
412
|
+
{/* Codebase Overview */}
|
|
413
|
+
<div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: "14px 16px" }}>
|
|
414
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
|
|
415
|
+
<I name="layers" size={13} color={T.accent} />
|
|
416
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1 }}>
|
|
417
|
+
Codebase
|
|
418
|
+
</span>
|
|
419
|
+
<span style={{ flex: 1 }} />
|
|
420
|
+
<span className="mono" style={{ fontSize: 10, color: T.textDim }}>
|
|
421
|
+
{fmtK(totalLines)} lines
|
|
422
|
+
</span>
|
|
423
|
+
</div>
|
|
424
|
+
{topExts.length > 0 ? (
|
|
425
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
426
|
+
{topExts.map(([ext, count]) => {
|
|
427
|
+
const total = files.length;
|
|
428
|
+
const extPct = total > 0 ? Math.round((count / total) * 100) : 0;
|
|
429
|
+
const color = extColorMap[ext] || T.textMuted;
|
|
430
|
+
return (
|
|
431
|
+
<div key={ext} style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
432
|
+
<span className="mono" style={{ fontSize: 10, color, width: 36, textAlign: "right", flexShrink: 0 }}>.{ext}</span>
|
|
433
|
+
<ProgressBar value={extPct} max={100} color={color} height={5} />
|
|
434
|
+
<span className="mono" style={{ fontSize: 10, color: T.textDim, width: 50, flexShrink: 0 }}>
|
|
435
|
+
{count} ({extPct}%)
|
|
436
|
+
</span>
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
})}
|
|
440
|
+
</div>
|
|
441
|
+
) : (
|
|
442
|
+
<div style={{ color: T.textDim, fontSize: 12 }}>No files indexed.</div>
|
|
443
|
+
)}
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
{/* Quick Actions */}
|
|
447
|
+
<div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: "14px 16px" }}>
|
|
448
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
|
|
449
|
+
<I name="zap" size={13} color={T.accent} />
|
|
450
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1 }}>
|
|
451
|
+
Quick Actions
|
|
452
|
+
</span>
|
|
453
|
+
</div>
|
|
454
|
+
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|
455
|
+
<button onClick={rebuildIndex} style={{
|
|
456
|
+
padding: "6px 14px", borderRadius: 6, border: `1px solid ${T.blue}40`,
|
|
457
|
+
background: `${T.blue}10`, color: T.blue, fontSize: 11, fontWeight: 600,
|
|
458
|
+
cursor: "pointer", fontFamily: "'JetBrains Mono', monospace",
|
|
459
|
+
display: "flex", alignItems: "center", gap: 5,
|
|
460
|
+
}}>
|
|
461
|
+
<I name="refresh" size={11} color={T.blue} /> Rebuild Index
|
|
462
|
+
</button>
|
|
463
|
+
<button onClick={openExplorer} style={{
|
|
464
|
+
padding: "6px 14px", borderRadius: 6, border: `1px solid ${T.accent}40`,
|
|
465
|
+
background: `${T.accent}10`, color: T.accent, fontSize: 11, fontWeight: 600,
|
|
466
|
+
cursor: "pointer", fontFamily: "'JetBrains Mono', monospace",
|
|
467
|
+
display: "flex", alignItems: "center", gap: 5,
|
|
468
|
+
}}>
|
|
469
|
+
<I name="folderOpen" size={11} color={T.accent} /> Open Folder
|
|
470
|
+
</button>
|
|
471
|
+
<a href="/docs" target="_blank" style={{
|
|
472
|
+
padding: "6px 14px", borderRadius: 6, border: `1px solid ${T.purple}40`,
|
|
473
|
+
background: `${T.purple}10`, color: T.purple, fontSize: 11, fontWeight: 600,
|
|
474
|
+
textDecoration: "none", fontFamily: "'JetBrains Mono', monospace",
|
|
475
|
+
display: "flex", alignItems: "center", gap: 5,
|
|
476
|
+
}}>
|
|
477
|
+
<I name="external" size={11} color={T.purple} /> API Docs
|
|
478
|
+
</a>
|
|
479
|
+
</div>
|
|
480
|
+
{actionMsg && (
|
|
481
|
+
<div className="mono" style={{ fontSize: 10, color: actionMsg.includes("fail") ? T.error : T.accent, marginTop: 8 }}>
|
|
482
|
+
{actionMsg}
|
|
483
|
+
</div>
|
|
484
|
+
)}
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
{/* ── Row 5: Token Usage by Source (provider-agnostic) ── */}
|
|
490
|
+
{sourceEntries.length > 0 && (
|
|
491
|
+
<SectionBlock
|
|
492
|
+
label="Token Usage by Source"
|
|
493
|
+
icon="zap"
|
|
494
|
+
color={T.purple}
|
|
495
|
+
open={showUsage}
|
|
496
|
+
onToggle={() => setShowUsage(!showUsage)}
|
|
497
|
+
badge={<Badge color={T.purple}>{fmtK(totalSourceTokens)} total \u00b7 {sourceEntries.length} sources</Badge>}
|
|
498
|
+
>
|
|
499
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
500
|
+
{sourceEntries.map(([name, data]) => {
|
|
501
|
+
const tokens = data?.total_tokens || 0;
|
|
502
|
+
const input = data?.input_tokens || 0;
|
|
503
|
+
const output = data?.output_tokens || 0;
|
|
504
|
+
const calls = data?.call_count || data?.sessions || 0;
|
|
505
|
+
const sourcePct = totalSourceTokens > 0 ? Math.round((tokens / totalSourceTokens) * 100) : 0;
|
|
506
|
+
const colors = [T.accent, T.blue, T.purple, T.warn, T.error];
|
|
507
|
+
const color = colors[sourceEntries.findIndex(([n]) => n === name) % colors.length];
|
|
508
|
+
|
|
509
|
+
return (
|
|
510
|
+
<div key={name} style={{
|
|
511
|
+
background: T.surfaceAlt, border: `1px solid ${T.border}`, borderRadius: 6, padding: "10px 12px",
|
|
512
|
+
}}>
|
|
513
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
|
|
514
|
+
<GlowDot color={color} size={6} />
|
|
515
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: T.text, flex: 1 }}>{name}</span>
|
|
516
|
+
<span className="mono" style={{ fontSize: 11, color }}>{fmtK(tokens)} tokens</span>
|
|
517
|
+
{calls > 0 && (
|
|
518
|
+
<span className="mono" style={{ fontSize: 10, color: T.textDim }}>{calls} calls</span>
|
|
519
|
+
)}
|
|
520
|
+
<Badge color={color}>{sourcePct}%</Badge>
|
|
521
|
+
</div>
|
|
522
|
+
<ProgressBar value={sourcePct} max={100} color={color} height={5} />
|
|
523
|
+
{(input > 0 || output > 0) && (
|
|
524
|
+
<div style={{ display: "flex", gap: 12, marginTop: 6 }}>
|
|
525
|
+
<span className="mono" style={{ fontSize: 10, color: T.textDim }}>
|
|
526
|
+
{fmtK(input)} in
|
|
527
|
+
</span>
|
|
528
|
+
<span className="mono" style={{ fontSize: 10, color: T.textDim }}>
|
|
529
|
+
{fmtK(output)} out
|
|
530
|
+
</span>
|
|
531
|
+
</div>
|
|
532
|
+
)}
|
|
533
|
+
</div>
|
|
534
|
+
);
|
|
535
|
+
})}
|
|
536
|
+
</div>
|
|
537
|
+
</SectionBlock>
|
|
538
|
+
)}
|
|
539
|
+
|
|
540
|
+
{/* ── Row 5b: Agent Workflows ── */}
|
|
541
|
+
{(() => {
|
|
542
|
+
const wf = stats?.hybrid_config?.agent_workflows;
|
|
543
|
+
if (!wf) return null;
|
|
544
|
+
const workflows = ["prefetch", "batch_validate", "compound_compress", "delegate_chain"];
|
|
545
|
+
const cfgItems = [
|
|
546
|
+
{ label: "Prefetch", value: wf.prefetch_max_files ?? "-" },
|
|
547
|
+
{ label: "Batch", value: wf.batch_validate_max_files ?? "-" },
|
|
548
|
+
{ label: "Compound", value: wf.compound_max_compress ?? "-" },
|
|
549
|
+
];
|
|
550
|
+
return (
|
|
551
|
+
<div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: "14px 16px" }}>
|
|
552
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
|
|
553
|
+
<I name="layers" size={13} color={T.purple} />
|
|
554
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1 }}>
|
|
555
|
+
Agent Workflows
|
|
556
|
+
</span>
|
|
557
|
+
<Badge color={wf.enabled ? T.accent : T.error}>{wf.enabled ? "Enabled" : "Disabled"}</Badge>
|
|
558
|
+
{wf.delegate_in_workflows && <Badge color={T.blue}>Delegate</Badge>}
|
|
559
|
+
</div>
|
|
560
|
+
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 10 }}>
|
|
561
|
+
{workflows.map(w => (
|
|
562
|
+
<span key={w} className="mono" style={{
|
|
563
|
+
padding: "2px 8px", borderRadius: 4, fontSize: 10,
|
|
564
|
+
background: wf.enabled ? `${T.purple}15` : `${T.textMuted}15`,
|
|
565
|
+
color: wf.enabled ? T.purple : T.textDim,
|
|
566
|
+
border: `1px solid ${wf.enabled ? T.purple : T.textMuted}30`,
|
|
567
|
+
}}>{w.replace(/_/g, " ")}</span>
|
|
568
|
+
))}
|
|
569
|
+
</div>
|
|
570
|
+
<div style={{ display: "flex", gap: 12 }}>
|
|
571
|
+
{cfgItems.map(c => (
|
|
572
|
+
<span key={c.label} className="mono" style={{ fontSize: 10, color: T.textDim }}>
|
|
573
|
+
{c.label}: <span style={{ color: T.text, fontWeight: 600 }}>{c.value}</span>
|
|
574
|
+
</span>
|
|
575
|
+
))}
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
);
|
|
579
|
+
})()}
|
|
580
|
+
|
|
581
|
+
{/* ── Row 6: Recent Activity ── */}
|
|
582
|
+
<div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: "14px 16px" }}>
|
|
583
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
|
|
584
|
+
<I name="terminal" size={13} color={T.blue} />
|
|
585
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1 }}>
|
|
586
|
+
Recent Activity
|
|
587
|
+
</span>
|
|
588
|
+
<Badge color={T.blue}>{activity.length}</Badge>
|
|
589
|
+
<span style={{ flex: 1 }} />
|
|
590
|
+
{watcher.length > 0 && (
|
|
591
|
+
<Badge color={T.warn}>{watcher.length} file changes</Badge>
|
|
592
|
+
)}
|
|
593
|
+
</div>
|
|
594
|
+
{activity.length === 0 ? (
|
|
595
|
+
<div style={{ color: T.textDim, fontSize: 12, padding: "4px 0" }}>No recent activity.</div>
|
|
596
|
+
) : (
|
|
597
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
|
598
|
+
{activity.map((e, i) => {
|
|
599
|
+
const typeColor = e.type === "tool_call" ? T.blue
|
|
600
|
+
: e.type === "decision" ? T.purple
|
|
601
|
+
: e.type === "file_change" ? T.accent
|
|
602
|
+
: e.type === "session_start" || e.type === "session_save" ? "#4ade80"
|
|
603
|
+
: T.textMuted;
|
|
604
|
+
return (
|
|
605
|
+
<div key={i} style={{
|
|
606
|
+
display: "flex", alignItems: "center", gap: 8, padding: "5px 10px",
|
|
607
|
+
borderRadius: 6, background: T.surfaceAlt,
|
|
608
|
+
}}>
|
|
609
|
+
<Badge color={typeColor}>{e.type || "event"}</Badge>
|
|
610
|
+
<span className="mono" style={{
|
|
611
|
+
fontSize: 11, color: T.text, flex: 1,
|
|
612
|
+
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
|
|
613
|
+
}}>
|
|
614
|
+
{eventSummary(e)}
|
|
615
|
+
</span>
|
|
616
|
+
<span className="mono" style={{ fontSize: 10, color: T.textDim, flexShrink: 0 }}>
|
|
617
|
+
{timeAgo(e.timestamp)}
|
|
618
|
+
</span>
|
|
619
|
+
</div>
|
|
620
|
+
);
|
|
621
|
+
})}
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
{/* ── Row 7: Notifications (only if present) ── */}
|
|
627
|
+
{notifications.length > 0 && (
|
|
628
|
+
<div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: "12px 16px" }}>
|
|
629
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
|
|
630
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
631
|
+
<I name="zap" size={13} color={T.warn} />
|
|
632
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase", letterSpacing: 1 }}>
|
|
633
|
+
Notifications
|
|
634
|
+
</span>
|
|
635
|
+
<Badge color={T.warn}>{notifications.length}</Badge>
|
|
636
|
+
</div>
|
|
637
|
+
<button
|
|
638
|
+
onClick={ackAllNotifications}
|
|
639
|
+
style={{
|
|
640
|
+
padding: "3px 8px", borderRadius: 4, border: `1px solid ${T.border}`, background: "transparent",
|
|
641
|
+
color: T.textMuted, fontSize: 10, cursor: "pointer", fontFamily: "'JetBrains Mono', monospace",
|
|
642
|
+
}}
|
|
643
|
+
>
|
|
644
|
+
Dismiss all
|
|
645
|
+
</button>
|
|
646
|
+
</div>
|
|
647
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
|
648
|
+
{notifications.slice(0, 5).map((n, i) => {
|
|
649
|
+
const sevColor = n.severity === "critical" ? T.error : n.severity === "warning" ? T.warn : T.blue;
|
|
650
|
+
return (
|
|
651
|
+
<div
|
|
652
|
+
key={n.id || i}
|
|
653
|
+
style={{
|
|
654
|
+
display: "flex", alignItems: "center", gap: 8, padding: "5px 10px",
|
|
655
|
+
borderRadius: 6, background: T.surfaceAlt, borderLeft: `3px solid ${sevColor}`,
|
|
656
|
+
}}
|
|
657
|
+
>
|
|
658
|
+
<Badge color={sevColor}>{n.severity}</Badge>
|
|
659
|
+
{n.ai_enhanced && <Badge color="#b38aff">AI</Badge>}
|
|
660
|
+
<span style={{ fontSize: 10, color: T.accent, fontWeight: 600, fontFamily: "'JetBrains Mono', monospace" }}>
|
|
661
|
+
{n.agent}
|
|
662
|
+
</span>
|
|
663
|
+
<span className="mono" style={{ fontSize: 11, color: T.text, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
664
|
+
{n.title}{n.message ? ` - ${n.message}` : ""}
|
|
665
|
+
</span>
|
|
666
|
+
<button
|
|
667
|
+
onClick={() => ackNotification(n.id)}
|
|
668
|
+
style={{
|
|
669
|
+
width: 20, height: 20, borderRadius: 4, border: "none", background: "transparent",
|
|
670
|
+
cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
|
|
671
|
+
}}
|
|
672
|
+
>
|
|
673
|
+
<I name="xSmall" size={12} color={T.textMuted} />
|
|
674
|
+
</button>
|
|
675
|
+
</div>
|
|
676
|
+
);
|
|
677
|
+
})}
|
|
678
|
+
{notifications.length > 5 && (
|
|
679
|
+
<div style={{ fontSize: 11, color: T.textMuted, textAlign: "center", padding: 4 }}>
|
|
680
|
+
+{notifications.length - 5} more
|
|
681
|
+
</div>
|
|
682
|
+
)}
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
)}
|
|
686
|
+
|
|
687
|
+
</div>
|
|
688
|
+
);
|
|
689
|
+
};
|