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,606 @@
|
|
|
1
|
+
// SessionsPanel — merged Sessions + ActivityLog sub-views
|
|
2
|
+
// Globals: T, I, GlowDot, Badge, StatBox, Btn, api, timeAgo, localTime,
|
|
3
|
+
// getToolColor, typeColors, useLiveDuration, useState, useEffect, useCallback
|
|
4
|
+
|
|
5
|
+
const SessionsPanel = () => {
|
|
6
|
+
const [subView, setSubView] = useState("sessions"); // "sessions" | "activity"
|
|
7
|
+
|
|
8
|
+
// ── Sessions state ─────────────────────────────────────────────────────────
|
|
9
|
+
const [sessions, setSessions] = useState([]);
|
|
10
|
+
const [currentSession, setCurrentSession] = useState(null);
|
|
11
|
+
const [expanded, setExpanded] = useState(null);
|
|
12
|
+
const [detail, setDetail] = useState(null);
|
|
13
|
+
const [sessionsLoading, setSessionsLoading] = useState(true);
|
|
14
|
+
|
|
15
|
+
// ── Real Claude Code token/cost stats (from Stop hook + live transcript) ──
|
|
16
|
+
const [claudeStats, setClaudeStats] = useState(null); // .c3/session_stats.jsonl aggregates
|
|
17
|
+
const [liveTokens, setLiveTokens] = useState(null); // current transcript live counts
|
|
18
|
+
|
|
19
|
+
const liveDuration = useLiveDuration(currentSession?.started, currentSession?.live);
|
|
20
|
+
|
|
21
|
+
const loadClaudeStats = useCallback(async () => {
|
|
22
|
+
try { setClaudeStats(await api.get('/api/session-stats?limit=50')); } catch {}
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const loadLiveTokens = useCallback(async () => {
|
|
26
|
+
try { setLiveTokens(await api.get('/api/session-stats/live')); } catch {}
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const loadSessions = useCallback(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const s = await api.get("/api/sessions");
|
|
32
|
+
setSessions(s);
|
|
33
|
+
setSessionsLoading(false);
|
|
34
|
+
} catch (e) { setSessionsLoading(false); }
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const loadCurrentSession = useCallback(async () => {
|
|
38
|
+
try {
|
|
39
|
+
const cur = await api.get("/api/sessions/current");
|
|
40
|
+
setCurrentSession(cur);
|
|
41
|
+
} catch (e) {}
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
loadSessions();
|
|
46
|
+
loadCurrentSession();
|
|
47
|
+
loadClaudeStats();
|
|
48
|
+
loadLiveTokens();
|
|
49
|
+
}, [loadSessions, loadCurrentSession, loadClaudeStats, loadLiveTokens]);
|
|
50
|
+
|
|
51
|
+
// Auto-refresh sessions list + current session + expanded detail every 5s
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const iv = setInterval(async () => {
|
|
54
|
+
await loadSessions();
|
|
55
|
+
await loadCurrentSession();
|
|
56
|
+
await loadClaudeStats();
|
|
57
|
+
await loadLiveTokens();
|
|
58
|
+
if (expanded !== null && detail?.id) {
|
|
59
|
+
try {
|
|
60
|
+
const d = await api.get(`/api/sessions/${detail.id}`);
|
|
61
|
+
if ((!d.tool_calls || d.tool_calls.length === 0) && d.started) {
|
|
62
|
+
const params = new URLSearchParams({ type: "tool_call", limit: "200", since: d.started });
|
|
63
|
+
if (d.ended) params.set("until", d.ended);
|
|
64
|
+
const activityTools = await api.get(`/api/activity?${params}`);
|
|
65
|
+
if (activityTools.length > 0) {
|
|
66
|
+
d.tool_calls = activityTools.map(e => ({
|
|
67
|
+
tool: e.tool || "unknown",
|
|
68
|
+
args: e.args || {},
|
|
69
|
+
result_summary: e.result_summary || "",
|
|
70
|
+
timestamp: e.timestamp || "",
|
|
71
|
+
})).reverse();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
setDetail(d);
|
|
75
|
+
} catch (e) {}
|
|
76
|
+
}
|
|
77
|
+
}, 5000);
|
|
78
|
+
return () => clearInterval(iv);
|
|
79
|
+
}, [expanded, detail?.id, loadSessions, loadCurrentSession]);
|
|
80
|
+
|
|
81
|
+
const handleExpand = async (i, id) => {
|
|
82
|
+
if (expanded === i) { setExpanded(null); setDetail(null); return; }
|
|
83
|
+
setExpanded(i);
|
|
84
|
+
if (i === "current") return;
|
|
85
|
+
try {
|
|
86
|
+
const d = await api.get(`/api/sessions/${id}`);
|
|
87
|
+
if ((!d.tool_calls || d.tool_calls.length === 0) && d.started) {
|
|
88
|
+
const params = new URLSearchParams({ type: "tool_call", limit: "200", since: d.started });
|
|
89
|
+
if (d.ended) params.set("until", d.ended);
|
|
90
|
+
const activityTools = await api.get(`/api/activity?${params}`);
|
|
91
|
+
if (activityTools.length > 0) {
|
|
92
|
+
d.tool_calls = activityTools.map(e => ({
|
|
93
|
+
tool: e.tool || "unknown",
|
|
94
|
+
args: e.args || {},
|
|
95
|
+
result_summary: e.result_summary || "",
|
|
96
|
+
timestamp: e.timestamp || "",
|
|
97
|
+
})).reverse();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
setDetail(d);
|
|
101
|
+
} catch (e) { setDetail(null); }
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// ── Activity state ──────────────────────────────────────────────────────────
|
|
105
|
+
const [events, setEvents] = useState([]);
|
|
106
|
+
const [actStats, setActStats] = useState(null);
|
|
107
|
+
const [actFilter, setActFilter] = useState("");
|
|
108
|
+
const [actLoading, setActLoading] = useState(true);
|
|
109
|
+
const [actExpanded, setActExpanded] = useState(null);
|
|
110
|
+
const [autoRefresh, setAutoRefresh] = useState(false);
|
|
111
|
+
|
|
112
|
+
const eventTypes = ["tool_call", "decision", "file_change", "fact_stored", "session_start", "session_save"];
|
|
113
|
+
|
|
114
|
+
const loadActivity = useCallback(async () => {
|
|
115
|
+
try {
|
|
116
|
+
const params = new URLSearchParams({ limit: "100" });
|
|
117
|
+
if (actFilter) params.set("type", actFilter);
|
|
118
|
+
const [ev, st] = await Promise.all([
|
|
119
|
+
api.get(`/api/activity?${params}`),
|
|
120
|
+
api.get("/api/activity/stats"),
|
|
121
|
+
]);
|
|
122
|
+
setEvents(ev);
|
|
123
|
+
setActStats(st);
|
|
124
|
+
} catch (e) {}
|
|
125
|
+
setActLoading(false);
|
|
126
|
+
}, [actFilter]);
|
|
127
|
+
|
|
128
|
+
useEffect(() => { loadActivity(); }, [loadActivity]);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!autoRefresh) return;
|
|
132
|
+
const iv = setInterval(loadActivity, 5000);
|
|
133
|
+
return () => clearInterval(iv);
|
|
134
|
+
}, [autoRefresh, loadActivity]);
|
|
135
|
+
|
|
136
|
+
const todayCount = events.filter(e => {
|
|
137
|
+
if (!e.timestamp) return false;
|
|
138
|
+
return new Date(e.timestamp).toDateString() === new Date().toDateString();
|
|
139
|
+
}).length;
|
|
140
|
+
|
|
141
|
+
const eventSummary = (e) => {
|
|
142
|
+
switch (e.type) {
|
|
143
|
+
case "tool_call": return `${e.tool || "unknown"} — ${e.result_summary || JSON.stringify(e.args || {}).slice(0, 60)}`;
|
|
144
|
+
case "decision": return e.decision || "";
|
|
145
|
+
case "file_change": return `${e.file || ""} (${e.change_type || "modified"})`;
|
|
146
|
+
case "fact_stored": return `[${e.category || "general"}] ${e.fact || ""}`;
|
|
147
|
+
case "session_start":return `Session ${e.session_id || ""} — ${e.description || ""}`;
|
|
148
|
+
case "session_save": return `Session ${e.session_id || ""} saved${e.summary ? ": " + e.summary : ""}`;
|
|
149
|
+
default: return JSON.stringify(e).slice(0, 80);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// ── Sub-view toggle ─────────────────────────────────────────────────────────
|
|
154
|
+
const tabStyle = (active) => ({
|
|
155
|
+
padding: "6px 16px",
|
|
156
|
+
borderRadius: 6,
|
|
157
|
+
border: `1px solid ${active ? T.accent + "60" : T.border}`,
|
|
158
|
+
background: active ? T.accentDim : "transparent",
|
|
159
|
+
color: active ? T.accent : T.textMuted,
|
|
160
|
+
fontSize: 12,
|
|
161
|
+
fontFamily: "'JetBrains Mono', monospace",
|
|
162
|
+
cursor: "pointer",
|
|
163
|
+
transition: "all 0.15s",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ── Real Claude Code token block (live from transcript or stop hook) ────────
|
|
167
|
+
const fmtTok = n => n >= 1000 ? (n / 1000).toFixed(1) + 'K' : String(n || 0);
|
|
168
|
+
|
|
169
|
+
const renderClaudeTokens = (tokData, label) => {
|
|
170
|
+
if (!tokData || !tokData.input_tokens) return null;
|
|
171
|
+
const inp = tokData.input_tokens || 0;
|
|
172
|
+
const out = tokData.output_tokens || 0;
|
|
173
|
+
const cr = tokData.cache_read_tokens || 0;
|
|
174
|
+
const cost = tokData.cost_usd;
|
|
175
|
+
const turns = tokData.turns;
|
|
176
|
+
return (
|
|
177
|
+
<div style={{ marginBottom: 8, padding: '10px 12px', background: `${T.blue}08`, border: `1px solid ${T.blue}20`, borderRadius: 6 }}>
|
|
178
|
+
<div style={{ fontSize: 9, fontWeight: 700, color: T.blue, textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8, display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
179
|
+
<I name="zap" size={10} color={T.blue} />{label || 'Claude Code Tokens'}
|
|
180
|
+
{turns != null && <span style={{ color: T.textDim }}>{turns} turns</span>}
|
|
181
|
+
</div>
|
|
182
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
183
|
+
{[{ label: 'In', value: fmtTok(inp), color: T.blue },
|
|
184
|
+
{ label: 'Out', value: fmtTok(out), color: T.accent },
|
|
185
|
+
{ label: 'Cache-rd', value: fmtTok(cr), color: T.purple },
|
|
186
|
+
...(cost != null ? [{ label: 'Cost', value: `$${cost.toFixed(4)}`, color: T.warn }] : []),
|
|
187
|
+
].map(s => (
|
|
188
|
+
<div key={s.label} style={{ background: `${s.color}12`, border: `1px solid ${s.color}25`, borderRadius: 5, padding: '5px 10px', textAlign: 'center', minWidth: 72 }}>
|
|
189
|
+
<div className="mono" style={{ fontSize: 13, fontWeight: 700, color: s.color }}>{s.value}</div>
|
|
190
|
+
<div style={{ fontSize: 9, color: T.textDim }}>{s.label}</div>
|
|
191
|
+
</div>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// ── C3 Token Usage block (MCP overhead — shared between current + past session detail) ─────
|
|
199
|
+
const renderTokenUsage = (d, isLive) => {
|
|
200
|
+
const cb = d.context_budget || {};
|
|
201
|
+
const bt = cb.by_tool || {};
|
|
202
|
+
const respTok = cb.response_tokens || 0;
|
|
203
|
+
if (respTok === 0 && !isLive) return null;
|
|
204
|
+
const toolEntries = Object.entries(bt).sort((a, b) => b[1] - a[1]);
|
|
205
|
+
return (
|
|
206
|
+
<div style={{ marginTop: 8 }}>
|
|
207
|
+
{/* Real Claude Code tokens — show liveTokens for live sessions */}
|
|
208
|
+
{isLive && renderClaudeTokens(liveTokens, 'Live Session Tokens')}
|
|
209
|
+
|
|
210
|
+
{/* C3 MCP overhead */}
|
|
211
|
+
{respTok > 0 && (
|
|
212
|
+
<>
|
|
213
|
+
<strong style={{ color: T.textDim, fontSize: 11 }}>C3 MCP overhead:</strong>
|
|
214
|
+
<div style={{ display: "flex", gap: 12, flexWrap: "wrap", marginTop: 4, marginBottom: 6, padding: "8px 10px", background: T.surfaceAlt, borderRadius: 6 }}>
|
|
215
|
+
<div style={{ textAlign: "center", minWidth: 70 }}>
|
|
216
|
+
<div className="mono" style={{ fontSize: 14, fontWeight: 700, color: T.blue }}>{(respTok / 1000).toFixed(1)}K</div>
|
|
217
|
+
<div style={{ fontSize: 9, color: T.textDim }}>resp tokens</div>
|
|
218
|
+
</div>
|
|
219
|
+
<div style={{ textAlign: "center", minWidth: 70 }}>
|
|
220
|
+
<div className="mono" style={{ fontSize: 14, fontWeight: 700, color: T.accent }}>{cb.call_count || 0}</div>
|
|
221
|
+
<div style={{ fontSize: 9, color: T.textDim }}>C3 calls</div>
|
|
222
|
+
</div>
|
|
223
|
+
{isLive ? (
|
|
224
|
+
<div style={{ textAlign: "center", minWidth: 70 }}>
|
|
225
|
+
<div className="mono" style={{ fontSize: 14, fontWeight: 700, color: cb.over_budget ? T.error : T.accent }}>{cb.over_budget ? "OVER" : "OK"}</div>
|
|
226
|
+
<div style={{ fontSize: 9, color: T.textDim }}>budget</div>
|
|
227
|
+
</div>
|
|
228
|
+
) : (
|
|
229
|
+
<div style={{ textAlign: "center", minWidth: 70 }}>
|
|
230
|
+
<div className="mono" style={{ fontSize: 14, fontWeight: 700, color: T.purple }}>{cb.compression_level || 0}</div>
|
|
231
|
+
<div style={{ fontSize: 9, color: T.textDim }}>compress lvl</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
<div style={{ textAlign: "center", minWidth: 70 }}>
|
|
235
|
+
<div className="mono" style={{ fontSize: 14, fontWeight: 700, color: T.warn }}>{cb.peak_tokens ? (cb.peak_tokens / 1000).toFixed(1) + "K" : "0"}</div>
|
|
236
|
+
<div style={{ fontSize: 9, color: T.textDim }}>peak resp</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
{toolEntries.length > 0 && (
|
|
240
|
+
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 6 }}>
|
|
241
|
+
{toolEntries.map(([tool, tokens]) => (
|
|
242
|
+
<Badge key={tool} color={getToolColor(tool)}>{tool}: {tokens >= 1000 ? (tokens / 1000).toFixed(1) + "K" : tokens}</Badge>
|
|
243
|
+
))}
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
</>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// ── Tool calls list (shared between current + past session detail) ──────────
|
|
253
|
+
const renderToolCalls = (toolCalls) => {
|
|
254
|
+
if (!toolCalls || toolCalls.length === 0) return null;
|
|
255
|
+
return (
|
|
256
|
+
<div style={{ marginTop: 8 }}>
|
|
257
|
+
<strong style={{ color: T.text }}>Tool Calls ({toolCalls.length}):</strong>
|
|
258
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 4, marginTop: 6 }}>
|
|
259
|
+
{toolCalls.map((tc, j) => (
|
|
260
|
+
<div key={j} style={{ display: "flex", alignItems: "center", gap: 8, padding: "4px 8px", borderRadius: 4, background: T.surfaceAlt }}>
|
|
261
|
+
<Badge color={getToolColor(tc.tool)}>{tc.tool}</Badge>
|
|
262
|
+
<span className="mono" style={{ fontSize: 11, color: T.textMuted, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
263
|
+
{typeof tc.args === "object" ? JSON.stringify(tc.args).slice(0, 60) : String(tc.args || "").slice(0, 60)}
|
|
264
|
+
</span>
|
|
265
|
+
{(tc.result_summary || tc.result) && (
|
|
266
|
+
<span className="mono" style={{ fontSize: 10, color: T.textDim }}>
|
|
267
|
+
{String(tc.result_summary || tc.result).slice(0, 40)}
|
|
268
|
+
</span>
|
|
269
|
+
)}
|
|
270
|
+
{tc.timestamp && <span className="mono" style={{ fontSize: 10, color: T.textDim }}>{localTime(tc.timestamp)}</span>}
|
|
271
|
+
</div>
|
|
272
|
+
))}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// ── Session expanded detail body ────────────────────────────────────────────
|
|
279
|
+
const renderSessionDetail = (d, isLive, displayDuration) => {
|
|
280
|
+
const decisions = Array.isArray(d.decisions) ? d.decisions : [];
|
|
281
|
+
const filesTouched = Array.isArray(d.files_touched) ? d.files_touched : [];
|
|
282
|
+
const toolCalls = Array.isArray(d.tool_calls) ? d.tool_calls : [];
|
|
283
|
+
return (
|
|
284
|
+
<div style={{ fontSize: 12, color: T.textMuted, lineHeight: 1.8 }}>
|
|
285
|
+
<div style={{ display: "flex", gap: 16, marginBottom: 8, padding: "8px 12px", background: T.surfaceAlt, borderRadius: 6, fontSize: 11, flexWrap: "wrap" }} className="mono">
|
|
286
|
+
<span><strong style={{ color: T.text }}>Start:</strong> {d.started ? new Date(d.started).toLocaleString() : "—"}</span>
|
|
287
|
+
<span><strong style={{ color: T.text }}>End:</strong> {d.ended ? new Date(d.ended).toLocaleString() : (isLive ? "Running" : "—")}</span>
|
|
288
|
+
{displayDuration && <span><strong style={{ color: T.text }}>Duration:</strong> {displayDuration}</span>}
|
|
289
|
+
</div>
|
|
290
|
+
{decisions.length > 0 && (
|
|
291
|
+
<div style={{ marginBottom: 6 }}>
|
|
292
|
+
<strong style={{ color: T.text }}>Decisions:</strong>
|
|
293
|
+
{decisions.map((dd, j) => <div key={j} style={{ paddingLeft: 12 }}>• {dd.decision}</div>)}
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
{filesTouched.length > 0 && (
|
|
297
|
+
<div><strong style={{ color: T.text }}>Files:</strong> {filesTouched.map(f => f.file).join(", ")}</div>
|
|
298
|
+
)}
|
|
299
|
+
{(d.context_notes || []).length > 0 && (
|
|
300
|
+
<div style={{ marginTop: 4 }}><strong style={{ color: T.text }}>Notes:</strong> {d.context_notes.join("; ")}</div>
|
|
301
|
+
)}
|
|
302
|
+
{renderToolCalls(toolCalls)}
|
|
303
|
+
{renderTokenUsage(d, isLive)}
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// ── Sessions sub-view ───────────────────────────────────────────────────────
|
|
309
|
+
const renderSessions = () => (
|
|
310
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
311
|
+
{/* Stats row */}
|
|
312
|
+
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
|
|
313
|
+
<StatBox label="Total Sessions" value={sessions.length} color={T.blue} loading={sessionsLoading} />
|
|
314
|
+
<StatBox label="Decisions" value={sessions.reduce((s, x) => s + (x.decisions || 0), 0)} color={T.purple} loading={sessionsLoading} />
|
|
315
|
+
<StatBox label="Total Duration" value={(() => {
|
|
316
|
+
const t = sessions.reduce((s, x) => s + (x.duration_seconds || 0), 0);
|
|
317
|
+
if (t < 60) return t + "s";
|
|
318
|
+
const m = Math.floor(t / 60);
|
|
319
|
+
if (m < 60) return m + "m";
|
|
320
|
+
return Math.floor(m / 60) + "h " + (m % 60) + "m";
|
|
321
|
+
})()} color={T.accent} loading={sessionsLoading} />
|
|
322
|
+
<StatBox label="Total Tool Calls" value={sessions.reduce((s, x) => s + (x.tool_calls || 0), 0)} color={T.warn} loading={sessionsLoading} />
|
|
323
|
+
<StatBox
|
|
324
|
+
label="Total Cost"
|
|
325
|
+
value={claudeStats && claudeStats.total_sessions > 0 ? `$${claudeStats.total_cost_usd.toFixed(3)}` : '—'}
|
|
326
|
+
sub={
|
|
327
|
+
!claudeStats ? 'run: c3 install-mcp' :
|
|
328
|
+
claudeStats.total_sessions === 0 ? 'grows after next session' :
|
|
329
|
+
`${claudeStats.total_sessions} sessions captured`
|
|
330
|
+
}
|
|
331
|
+
color={claudeStats && claudeStats.total_sessions > 0 ? T.warn : T.textMuted}
|
|
332
|
+
loading={sessionsLoading}
|
|
333
|
+
/>
|
|
334
|
+
<StatBox
|
|
335
|
+
label="Claude Tokens"
|
|
336
|
+
value={claudeStats && claudeStats.total_sessions > 0 ? fmtTok((claudeStats.total_input_tokens || 0) + (claudeStats.total_output_tokens || 0)) : '—'}
|
|
337
|
+
sub={claudeStats && claudeStats.total_sessions > 0 ? `${fmtTok(claudeStats.total_input_tokens)} in · ${fmtTok(claudeStats.total_output_tokens)} out` : ''}
|
|
338
|
+
color={claudeStats && claudeStats.total_sessions > 0 ? T.blue : T.textMuted}
|
|
339
|
+
loading={sessionsLoading}
|
|
340
|
+
/>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{/* Hook bootstrap notice */}
|
|
344
|
+
{claudeStats && claudeStats.total_sessions === 0 && (
|
|
345
|
+
<div style={{
|
|
346
|
+
padding: '10px 14px', borderRadius: 7,
|
|
347
|
+
background: `${T.blue}08`, border: `1px dashed ${T.blue}30`,
|
|
348
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
349
|
+
}}>
|
|
350
|
+
<I name="info" size={13} color={T.blue} />
|
|
351
|
+
<span style={{ fontSize: 12, color: T.textMuted, lineHeight: 1.5 }}>
|
|
352
|
+
<strong style={{ color: T.blue }}>Cost & token tracking active.</strong>{' '}
|
|
353
|
+
Data appears here after your next Claude Code session ends.
|
|
354
|
+
Each session is captured automatically via the Stop hook.
|
|
355
|
+
</span>
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
{!claudeStats && (
|
|
359
|
+
<div style={{
|
|
360
|
+
padding: '10px 14px', borderRadius: 7,
|
|
361
|
+
background: `${T.textDim}08`, border: `1px dashed ${T.border}`,
|
|
362
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
363
|
+
}}>
|
|
364
|
+
<I name="info" size={13} color={T.textMuted} />
|
|
365
|
+
<span style={{ fontSize: 12, color: T.textMuted }}>
|
|
366
|
+
Run <code style={{ fontFamily: 'JetBrains Mono', background: T.surfaceAlt, padding: '1px 5px', borderRadius: 3 }}>c3 install-mcp</code>{' '}
|
|
367
|
+
to enable per-session cost & token tracking.
|
|
368
|
+
</span>
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
|
|
372
|
+
{/* Current Session Card */}
|
|
373
|
+
{currentSession && (() => {
|
|
374
|
+
const cur = currentSession;
|
|
375
|
+
const isLive = cur.live === true;
|
|
376
|
+
const decisions = Array.isArray(cur.decisions) ? cur.decisions : [];
|
|
377
|
+
const filesTouched = Array.isArray(cur.files_touched) ? cur.files_touched : [];
|
|
378
|
+
const toolCalls = Array.isArray(cur.tool_calls) ? cur.tool_calls : [];
|
|
379
|
+
const displayDuration = isLive ? liveDuration : (cur.duration || "");
|
|
380
|
+
return (
|
|
381
|
+
<div style={{
|
|
382
|
+
background: T.surface,
|
|
383
|
+
border: `1px solid ${isLive ? T.accent : T.border}30`,
|
|
384
|
+
borderLeft: `3px solid ${isLive ? T.accent : T.textMuted}`,
|
|
385
|
+
borderRadius: 8,
|
|
386
|
+
padding: 18,
|
|
387
|
+
}}>
|
|
388
|
+
<div style={{
|
|
389
|
+
fontSize: 10, fontWeight: 700, color: isLive ? T.accent : T.textMuted,
|
|
390
|
+
textTransform: "uppercase", letterSpacing: 1.2, marginBottom: 10,
|
|
391
|
+
display: "flex", alignItems: "center", gap: 6,
|
|
392
|
+
}}>
|
|
393
|
+
{isLive && <GlowDot color={T.accent} size={6} />}
|
|
394
|
+
{isLive ? "Live Session" : "Last Session"}
|
|
395
|
+
</div>
|
|
396
|
+
<div style={{ fontSize: 14, fontWeight: 600, color: T.text, marginBottom: 4 }}>
|
|
397
|
+
{cur.description || cur.summary || "No summary"}
|
|
398
|
+
</div>
|
|
399
|
+
{cur.description && cur.summary && cur.summary !== cur.description && (
|
|
400
|
+
<div style={{ fontSize: 12, color: T.textMuted, marginBottom: 6 }}>{cur.summary}</div>
|
|
401
|
+
)}
|
|
402
|
+
<div className="mono" style={{ fontSize: 11, color: T.textMuted, marginBottom: 12 }}>
|
|
403
|
+
{cur.started ? new Date(cur.started).toLocaleString() : "—"}
|
|
404
|
+
{displayDuration && <span style={{ color: T.accent, marginLeft: 10 }}>{displayDuration}</span>}
|
|
405
|
+
</div>
|
|
406
|
+
<div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
|
|
407
|
+
<div style={{ flex: 1, padding: "8px 10px", background: T.surfaceAlt, borderRadius: 6, textAlign: "center" }}>
|
|
408
|
+
<div className="mono" style={{ fontSize: 16, fontWeight: 700, color: T.purple }}>{decisions.length}</div>
|
|
409
|
+
<div style={{ fontSize: 9, color: T.textDim }}>decisions</div>
|
|
410
|
+
</div>
|
|
411
|
+
<div style={{ flex: 1, padding: "8px 10px", background: T.surfaceAlt, borderRadius: 6, textAlign: "center" }}>
|
|
412
|
+
<div className="mono" style={{ fontSize: 16, fontWeight: 700, color: T.blue }}>{filesTouched.length}</div>
|
|
413
|
+
<div style={{ fontSize: 9, color: T.textDim }}>files</div>
|
|
414
|
+
</div>
|
|
415
|
+
<div style={{ flex: 1, padding: "8px 10px", background: T.surfaceAlt, borderRadius: 6, textAlign: "center" }}>
|
|
416
|
+
<div className="mono" style={{ fontSize: 16, fontWeight: 700, color: T.warn }}>{toolCalls.length}</div>
|
|
417
|
+
<div style={{ fontSize: 9, color: T.textDim }}>tool calls</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
{/* Live token tracker — updates each exchange via transcript polling */}
|
|
421
|
+
{liveTokens && liveTokens.input_tokens > 0 && isLive && (
|
|
422
|
+
<div style={{ marginBottom: 10 }}>
|
|
423
|
+
{renderClaudeTokens(liveTokens, 'Live Tokens')}
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
426
|
+
<button
|
|
427
|
+
onClick={() => handleExpand("current", cur.id)}
|
|
428
|
+
style={{
|
|
429
|
+
padding: "6px 12px", background: "none", border: `1px solid ${T.border}`,
|
|
430
|
+
borderRadius: 5, color: T.accent, fontSize: 11, cursor: "pointer",
|
|
431
|
+
fontFamily: "'JetBrains Mono', monospace",
|
|
432
|
+
}}
|
|
433
|
+
>
|
|
434
|
+
{expanded === "current" ? "Collapse details" : "View details →"}
|
|
435
|
+
</button>
|
|
436
|
+
{expanded === "current" && (
|
|
437
|
+
<div style={{ marginTop: 12, paddingTop: 12, borderTop: `1px solid ${T.border}` }}>
|
|
438
|
+
{renderSessionDetail(cur, isLive, displayDuration)}
|
|
439
|
+
</div>
|
|
440
|
+
)}
|
|
441
|
+
</div>
|
|
442
|
+
);
|
|
443
|
+
})()}
|
|
444
|
+
|
|
445
|
+
{sessions.length === 0 && !currentSession && !sessionsLoading && (
|
|
446
|
+
<div style={{
|
|
447
|
+
background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8,
|
|
448
|
+
padding: 30, textAlign: "center", color: T.textMuted, fontSize: 13,
|
|
449
|
+
}}>
|
|
450
|
+
No sessions yet. Sessions are created automatically when the MCP server starts.
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
|
|
454
|
+
{/* Past Sessions */}
|
|
455
|
+
{sessions.length > 0 && (
|
|
456
|
+
<div>
|
|
457
|
+
<div style={{
|
|
458
|
+
fontSize: 12, fontWeight: 600, color: T.textMuted, textTransform: "uppercase",
|
|
459
|
+
letterSpacing: 1, marginBottom: 10, display: "flex", alignItems: "center", gap: 8,
|
|
460
|
+
}}>
|
|
461
|
+
Saved Sessions <Badge>{sessions.length}</Badge>
|
|
462
|
+
</div>
|
|
463
|
+
<div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, overflow: "hidden" }}>
|
|
464
|
+
{sessions.map((s, i) => {
|
|
465
|
+
const idx = i;
|
|
466
|
+
return (
|
|
467
|
+
<div key={s.id} style={{ borderBottom: i < sessions.length - 1 ? `1px solid ${T.border}` : "none" }}>
|
|
468
|
+
<div
|
|
469
|
+
onClick={() => handleExpand(idx, s.id)}
|
|
470
|
+
style={{ padding: "14px 18px", display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "pointer", transition: "background 0.15s" }}
|
|
471
|
+
onMouseEnter={e => e.currentTarget.style.background = T.surfaceAlt}
|
|
472
|
+
onMouseLeave={e => e.currentTarget.style.background = "transparent"}
|
|
473
|
+
>
|
|
474
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
475
|
+
<I name="chevron" size={14} color={T.textMuted} style={{ transition: "transform 0.2s", transform: expanded === idx ? "rotate(90deg)" : "" }} />
|
|
476
|
+
<div>
|
|
477
|
+
<div style={{ fontSize: 13, color: T.text, fontWeight: 500 }}>{s.summary || s.description || "No summary"}</div>
|
|
478
|
+
<div className="mono" style={{ fontSize: 11, color: T.textMuted, marginTop: 2 }}>{s.started ? new Date(s.started).toLocaleString() : "—"}</div>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "flex-end" }}>
|
|
482
|
+
{s.duration && <Badge color={T.accent}>{s.duration}</Badge>}
|
|
483
|
+
<Badge color={T.purple}>{s.decisions} decisions</Badge>
|
|
484
|
+
<Badge color={T.blue}>{s.files} files</Badge>
|
|
485
|
+
{s.tool_calls > 0 && <Badge color={T.warn}>{s.tool_calls} tools</Badge>}
|
|
486
|
+
{s.response_tokens > 0 && <Badge color={T.blue}>{(s.response_tokens / 1000).toFixed(1)}K tok</Badge>}
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
{expanded === idx && detail && (
|
|
490
|
+
<div style={{ padding: "0 18px 14px 42px" }}>
|
|
491
|
+
{renderSessionDetail(detail, false, detail.duration)}
|
|
492
|
+
</div>
|
|
493
|
+
)}
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
})}
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
// ── Activity sub-view ───────────────────────────────────────────────────────
|
|
504
|
+
const renderActivity = () => (
|
|
505
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
|
|
506
|
+
{/* Stats row */}
|
|
507
|
+
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
|
|
508
|
+
<StatBox
|
|
509
|
+
label="Total Events"
|
|
510
|
+
value={actStats?.total || 0}
|
|
511
|
+
sub={`since ${actStats?.first ? new Date(actStats.first).toLocaleDateString() : "—"}`}
|
|
512
|
+
color={T.accent}
|
|
513
|
+
loading={actLoading}
|
|
514
|
+
/>
|
|
515
|
+
<StatBox label="Today" value={todayCount} sub="events today" color={T.blue} loading={actLoading} />
|
|
516
|
+
{Object.entries(actStats?.by_type || {}).slice(0, 3).map(([type, count]) => (
|
|
517
|
+
<StatBox key={type} label={type.replace("_", " ")} value={count} color={typeColors[type] || T.textMuted} />
|
|
518
|
+
))}
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
{/* Controls */}
|
|
522
|
+
<div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
|
|
523
|
+
<select
|
|
524
|
+
value={actFilter}
|
|
525
|
+
onChange={e => setActFilter(e.target.value)}
|
|
526
|
+
className="mono"
|
|
527
|
+
style={{
|
|
528
|
+
padding: "7px 12px", borderRadius: 6, background: T.surfaceAlt,
|
|
529
|
+
border: `1px solid ${T.border}`, color: T.text, fontSize: 12, outline: "none",
|
|
530
|
+
}}
|
|
531
|
+
>
|
|
532
|
+
<option value="">All types</option>
|
|
533
|
+
{eventTypes.map(t => <option key={t} value={t}>{t}</option>)}
|
|
534
|
+
</select>
|
|
535
|
+
<button
|
|
536
|
+
onClick={() => setAutoRefresh(!autoRefresh)}
|
|
537
|
+
className="mono"
|
|
538
|
+
style={{
|
|
539
|
+
padding: "7px 14px", borderRadius: 6,
|
|
540
|
+
border: `1px solid ${autoRefresh ? T.accent + "60" : T.border}`,
|
|
541
|
+
background: autoRefresh ? T.accentDim : "transparent",
|
|
542
|
+
color: autoRefresh ? T.accent : T.textMuted,
|
|
543
|
+
fontSize: 12, cursor: "pointer",
|
|
544
|
+
}}
|
|
545
|
+
>
|
|
546
|
+
{autoRefresh ? "Auto-refresh ON" : "Auto-refresh OFF"}
|
|
547
|
+
</button>
|
|
548
|
+
<Btn variant="outline" onClick={loadActivity}><I name="refresh" size={13} /> Refresh</Btn>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
{/* Event timeline */}
|
|
552
|
+
<div style={{ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, overflow: "hidden" }}>
|
|
553
|
+
{events.length === 0 && !actLoading && (
|
|
554
|
+
<div style={{ padding: 30, textAlign: "center", color: T.textMuted, fontSize: 13 }}>
|
|
555
|
+
No activity logged yet. Use MCP tools to generate events.
|
|
556
|
+
</div>
|
|
557
|
+
)}
|
|
558
|
+
{events.map((e, i) => (
|
|
559
|
+
<div
|
|
560
|
+
key={i}
|
|
561
|
+
onClick={() => setActExpanded(actExpanded === i ? null : i)}
|
|
562
|
+
style={{
|
|
563
|
+
padding: "10px 16px",
|
|
564
|
+
borderBottom: i < events.length - 1 ? `1px solid ${T.border}` : "none",
|
|
565
|
+
cursor: "pointer",
|
|
566
|
+
background: actExpanded === i ? T.surfaceAlt : "transparent",
|
|
567
|
+
transition: "background 0.15s",
|
|
568
|
+
}}
|
|
569
|
+
>
|
|
570
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
571
|
+
<span className="mono" style={{ fontSize: 10, color: T.textDim, minWidth: 55, flexShrink: 0 }}>{timeAgo(e.timestamp)}</span>
|
|
572
|
+
<Badge color={typeColors[e.type] || T.textMuted}>{e.type}</Badge>
|
|
573
|
+
{e.type === "tool_call" && e.tool && <Badge color={getToolColor(e.tool)}>{e.tool}</Badge>}
|
|
574
|
+
<span className="mono" style={{ fontSize: 11, color: T.textMuted, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
575
|
+
{e.type === "tool_call" ? (e.result_summary || JSON.stringify(e.args || {}).slice(0, 60)) : eventSummary(e)}
|
|
576
|
+
</span>
|
|
577
|
+
<I name="chevron" size={12} color={T.textDim} style={{ transform: actExpanded === i ? "rotate(90deg)" : "none", transition: "transform 0.15s", flexShrink: 0 }} />
|
|
578
|
+
</div>
|
|
579
|
+
{actExpanded === i && (
|
|
580
|
+
<pre className="mono fade-up" style={{
|
|
581
|
+
marginTop: 8, padding: 10, background: T.bg, borderRadius: 6,
|
|
582
|
+
fontSize: 11, color: T.textMuted, overflow: "auto", maxHeight: 200,
|
|
583
|
+
whiteSpace: "pre-wrap", wordBreak: "break-all",
|
|
584
|
+
}}>
|
|
585
|
+
{JSON.stringify(e, null, 2)}
|
|
586
|
+
</pre>
|
|
587
|
+
)}
|
|
588
|
+
</div>
|
|
589
|
+
))}
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
// ── Render ──────────────────────────────────────────────────────────────────
|
|
595
|
+
return (
|
|
596
|
+
<div className="fade-up" style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
597
|
+
{/* Sub-view toggle */}
|
|
598
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
599
|
+
<button style={tabStyle(subView === "sessions")} onClick={() => setSubView("sessions")}>Sessions</button>
|
|
600
|
+
<button style={tabStyle(subView === "activity")} onClick={() => setSubView("activity")}>Activity</button>
|
|
601
|
+
</div>
|
|
602
|
+
|
|
603
|
+
{subView === "sessions" ? renderSessions() : renderActivity()}
|
|
604
|
+
</div>
|
|
605
|
+
);
|
|
606
|
+
};
|