code-context-control 2.28.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. cli/__init__.py +1 -0
  2. cli/_hook_utils.py +99 -0
  3. cli/c3.py +6152 -0
  4. cli/commands/__init__.py +1 -0
  5. cli/commands/common.py +312 -0
  6. cli/commands/parser.py +286 -0
  7. cli/docs.html +3178 -0
  8. cli/edits.html +878 -0
  9. cli/hook_auto_snapshot.py +142 -0
  10. cli/hook_c3_signal.py +61 -0
  11. cli/hook_c3read.py +116 -0
  12. cli/hook_edit_ledger.py +213 -0
  13. cli/hook_edit_unlock.py +170 -0
  14. cli/hook_filter.py +130 -0
  15. cli/hook_ghost_files.py +238 -0
  16. cli/hook_pretool_enforce.py +334 -0
  17. cli/hook_read.py +200 -0
  18. cli/hook_session_stats.py +62 -0
  19. cli/hook_terse_advisor.py +190 -0
  20. cli/hub.html +3764 -0
  21. cli/hub_server.py +1619 -0
  22. cli/mcp_proxy.py +428 -0
  23. cli/mcp_server.py +660 -0
  24. cli/server.py +2985 -0
  25. cli/tools/__init__.py +4 -0
  26. cli/tools/_helpers.py +65 -0
  27. cli/tools/agent.py +1165 -0
  28. cli/tools/compress.py +215 -0
  29. cli/tools/delegate.py +1184 -0
  30. cli/tools/edit.py +313 -0
  31. cli/tools/edits.py +118 -0
  32. cli/tools/filter.py +285 -0
  33. cli/tools/impact.py +163 -0
  34. cli/tools/memory.py +469 -0
  35. cli/tools/read.py +224 -0
  36. cli/tools/search.py +337 -0
  37. cli/tools/session.py +95 -0
  38. cli/tools/shell.py +193 -0
  39. cli/tools/status.py +306 -0
  40. cli/tools/validate.py +310 -0
  41. cli/ui/api.js +36 -0
  42. cli/ui/app.js +207 -0
  43. cli/ui/components/chat.js +758 -0
  44. cli/ui/components/dashboard.js +689 -0
  45. cli/ui/components/edits.js +220 -0
  46. cli/ui/components/instructions.js +481 -0
  47. cli/ui/components/memory.js +626 -0
  48. cli/ui/components/sessions.js +606 -0
  49. cli/ui/components/settings.js +1404 -0
  50. cli/ui/components/sidebar.js +156 -0
  51. cli/ui/icons.js +51 -0
  52. cli/ui/shared.js +119 -0
  53. cli/ui/theme.js +22 -0
  54. cli/ui.html +168 -0
  55. cli/ui_legacy.html +6797 -0
  56. cli/ui_nano.html +503 -0
  57. code_context_control-2.28.0.dist-info/METADATA +248 -0
  58. code_context_control-2.28.0.dist-info/RECORD +150 -0
  59. code_context_control-2.28.0.dist-info/WHEEL +5 -0
  60. code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
  61. code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
  62. code_context_control-2.28.0.dist-info/top_level.txt +5 -0
  63. core/__init__.py +75 -0
  64. core/config.py +269 -0
  65. core/ide.py +188 -0
  66. oracle/__init__.py +1 -0
  67. oracle/config.py +75 -0
  68. oracle/oracle.html +3900 -0
  69. oracle/oracle_server.py +663 -0
  70. oracle/services/__init__.py +1 -0
  71. oracle/services/c3_bridge.py +210 -0
  72. oracle/services/chat_engine.py +1103 -0
  73. oracle/services/chat_store.py +155 -0
  74. oracle/services/cross_memory.py +154 -0
  75. oracle/services/federated_graph.py +463 -0
  76. oracle/services/health_checker.py +117 -0
  77. oracle/services/insight_engine.py +307 -0
  78. oracle/services/memory_reader.py +106 -0
  79. oracle/services/memory_writer.py +182 -0
  80. oracle/services/ollama_bridge.py +332 -0
  81. oracle/services/project_scanner.py +87 -0
  82. oracle/services/review_agent.py +206 -0
  83. services/__init__.py +1 -0
  84. services/activity_log.py +93 -0
  85. services/agent_base.py +124 -0
  86. services/agents.py +1529 -0
  87. services/auto_memory.py +407 -0
  88. services/bench/__init__.py +6 -0
  89. services/bench/external/__init__.py +29 -0
  90. services/bench/external/aider_polyglot.py +405 -0
  91. services/bench/external/swe_bench.py +485 -0
  92. services/benchmark_dashboard.py +596 -0
  93. services/claude_md.py +785 -0
  94. services/compressor.py +592 -0
  95. services/context_snapshot.py +356 -0
  96. services/conversation_store.py +870 -0
  97. services/doc_index.py +537 -0
  98. services/e2e_benchmark.py +2884 -0
  99. services/e2e_evaluator.py +396 -0
  100. services/e2e_tasks.py +743 -0
  101. services/edit_ledger.py +459 -0
  102. services/embedding_index.py +341 -0
  103. services/error_reporting.py +123 -0
  104. services/file_memory.py +734 -0
  105. services/hub_service.py +585 -0
  106. services/indexer.py +712 -0
  107. services/memory.py +318 -0
  108. services/memory_consolidator.py +538 -0
  109. services/memory_graph.py +382 -0
  110. services/memory_grounder.py +304 -0
  111. services/memory_scorer.py +246 -0
  112. services/metrics.py +86 -0
  113. services/notifications.py +209 -0
  114. services/ollama_client.py +201 -0
  115. services/output_filter.py +488 -0
  116. services/parser.py +1238 -0
  117. services/project_manager.py +579 -0
  118. services/protocol.py +306 -0
  119. services/proxy_state.py +152 -0
  120. services/retrieval_broker.py +129 -0
  121. services/router.py +414 -0
  122. services/runtime.py +326 -0
  123. services/session_benchmark.py +1945 -0
  124. services/session_manager.py +1026 -0
  125. services/session_preloader.py +251 -0
  126. services/text_index.py +90 -0
  127. services/tool_classifier.py +176 -0
  128. services/transcript_index.py +340 -0
  129. services/validation_cache.py +155 -0
  130. services/vector_store.py +299 -0
  131. services/version_tracker.py +271 -0
  132. services/watcher.py +192 -0
  133. tui/__init__.py +0 -0
  134. tui/backend.py +59 -0
  135. tui/main.py +145 -0
  136. tui/screens/__init__.py +1 -0
  137. tui/screens/benchmark_view.py +109 -0
  138. tui/screens/claudemd_view.py +46 -0
  139. tui/screens/compress_view.py +52 -0
  140. tui/screens/index_view.py +74 -0
  141. tui/screens/init_view.py +82 -0
  142. tui/screens/mcp_view.py +73 -0
  143. tui/screens/optimize_view.py +41 -0
  144. tui/screens/pipe_view.py +46 -0
  145. tui/screens/projects_view.py +355 -0
  146. tui/screens/search_view.py +55 -0
  147. tui/screens/session_view.py +143 -0
  148. tui/screens/stats.py +158 -0
  149. tui/screens/ui_view.py +54 -0
  150. tui/theme.tcss +335 -0
@@ -0,0 +1,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
+ };