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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. cli/__init__.py +1 -0
  2. cli/_hook_utils.py +99 -0
  3. cli/c3.py +6152 -0
  4. cli/commands/__init__.py +1 -0
  5. cli/commands/common.py +312 -0
  6. cli/commands/parser.py +286 -0
  7. cli/docs.html +3178 -0
  8. cli/edits.html +878 -0
  9. cli/hook_auto_snapshot.py +142 -0
  10. cli/hook_c3_signal.py +61 -0
  11. cli/hook_c3read.py +116 -0
  12. cli/hook_edit_ledger.py +213 -0
  13. cli/hook_edit_unlock.py +170 -0
  14. cli/hook_filter.py +130 -0
  15. cli/hook_ghost_files.py +238 -0
  16. cli/hook_pretool_enforce.py +334 -0
  17. cli/hook_read.py +200 -0
  18. cli/hook_session_stats.py +62 -0
  19. cli/hook_terse_advisor.py +190 -0
  20. cli/hub.html +3764 -0
  21. cli/hub_server.py +1619 -0
  22. cli/mcp_proxy.py +428 -0
  23. cli/mcp_server.py +660 -0
  24. cli/server.py +2985 -0
  25. cli/tools/__init__.py +4 -0
  26. cli/tools/_helpers.py +65 -0
  27. cli/tools/agent.py +1165 -0
  28. cli/tools/compress.py +215 -0
  29. cli/tools/delegate.py +1184 -0
  30. cli/tools/edit.py +313 -0
  31. cli/tools/edits.py +118 -0
  32. cli/tools/filter.py +285 -0
  33. cli/tools/impact.py +163 -0
  34. cli/tools/memory.py +469 -0
  35. cli/tools/read.py +224 -0
  36. cli/tools/search.py +337 -0
  37. cli/tools/session.py +95 -0
  38. cli/tools/shell.py +193 -0
  39. cli/tools/status.py +306 -0
  40. cli/tools/validate.py +310 -0
  41. cli/ui/api.js +36 -0
  42. cli/ui/app.js +207 -0
  43. cli/ui/components/chat.js +758 -0
  44. cli/ui/components/dashboard.js +689 -0
  45. cli/ui/components/edits.js +220 -0
  46. cli/ui/components/instructions.js +481 -0
  47. cli/ui/components/memory.js +626 -0
  48. cli/ui/components/sessions.js +606 -0
  49. cli/ui/components/settings.js +1404 -0
  50. cli/ui/components/sidebar.js +156 -0
  51. cli/ui/icons.js +51 -0
  52. cli/ui/shared.js +119 -0
  53. cli/ui/theme.js +22 -0
  54. cli/ui.html +168 -0
  55. cli/ui_legacy.html +6797 -0
  56. cli/ui_nano.html +503 -0
  57. code_context_control-2.28.0.dist-info/METADATA +248 -0
  58. code_context_control-2.28.0.dist-info/RECORD +150 -0
  59. code_context_control-2.28.0.dist-info/WHEEL +5 -0
  60. code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
  61. code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
  62. code_context_control-2.28.0.dist-info/top_level.txt +5 -0
  63. core/__init__.py +75 -0
  64. core/config.py +269 -0
  65. core/ide.py +188 -0
  66. oracle/__init__.py +1 -0
  67. oracle/config.py +75 -0
  68. oracle/oracle.html +3900 -0
  69. oracle/oracle_server.py +663 -0
  70. oracle/services/__init__.py +1 -0
  71. oracle/services/c3_bridge.py +210 -0
  72. oracle/services/chat_engine.py +1103 -0
  73. oracle/services/chat_store.py +155 -0
  74. oracle/services/cross_memory.py +154 -0
  75. oracle/services/federated_graph.py +463 -0
  76. oracle/services/health_checker.py +117 -0
  77. oracle/services/insight_engine.py +307 -0
  78. oracle/services/memory_reader.py +106 -0
  79. oracle/services/memory_writer.py +182 -0
  80. oracle/services/ollama_bridge.py +332 -0
  81. oracle/services/project_scanner.py +87 -0
  82. oracle/services/review_agent.py +206 -0
  83. services/__init__.py +1 -0
  84. services/activity_log.py +93 -0
  85. services/agent_base.py +124 -0
  86. services/agents.py +1529 -0
  87. services/auto_memory.py +407 -0
  88. services/bench/__init__.py +6 -0
  89. services/bench/external/__init__.py +29 -0
  90. services/bench/external/aider_polyglot.py +405 -0
  91. services/bench/external/swe_bench.py +485 -0
  92. services/benchmark_dashboard.py +596 -0
  93. services/claude_md.py +785 -0
  94. services/compressor.py +592 -0
  95. services/context_snapshot.py +356 -0
  96. services/conversation_store.py +870 -0
  97. services/doc_index.py +537 -0
  98. services/e2e_benchmark.py +2884 -0
  99. services/e2e_evaluator.py +396 -0
  100. services/e2e_tasks.py +743 -0
  101. services/edit_ledger.py +459 -0
  102. services/embedding_index.py +341 -0
  103. services/error_reporting.py +123 -0
  104. services/file_memory.py +734 -0
  105. services/hub_service.py +585 -0
  106. services/indexer.py +712 -0
  107. services/memory.py +318 -0
  108. services/memory_consolidator.py +538 -0
  109. services/memory_graph.py +382 -0
  110. services/memory_grounder.py +304 -0
  111. services/memory_scorer.py +246 -0
  112. services/metrics.py +86 -0
  113. services/notifications.py +209 -0
  114. services/ollama_client.py +201 -0
  115. services/output_filter.py +488 -0
  116. services/parser.py +1238 -0
  117. services/project_manager.py +579 -0
  118. services/protocol.py +306 -0
  119. services/proxy_state.py +152 -0
  120. services/retrieval_broker.py +129 -0
  121. services/router.py +414 -0
  122. services/runtime.py +326 -0
  123. services/session_benchmark.py +1945 -0
  124. services/session_manager.py +1026 -0
  125. services/session_preloader.py +251 -0
  126. services/text_index.py +90 -0
  127. services/tool_classifier.py +176 -0
  128. services/transcript_index.py +340 -0
  129. services/validation_cache.py +155 -0
  130. services/vector_store.py +299 -0
  131. services/version_tracker.py +271 -0
  132. services/watcher.py +192 -0
  133. tui/__init__.py +0 -0
  134. tui/backend.py +59 -0
  135. tui/main.py +145 -0
  136. tui/screens/__init__.py +1 -0
  137. tui/screens/benchmark_view.py +109 -0
  138. tui/screens/claudemd_view.py +46 -0
  139. tui/screens/compress_view.py +52 -0
  140. tui/screens/index_view.py +74 -0
  141. tui/screens/init_view.py +82 -0
  142. tui/screens/mcp_view.py +73 -0
  143. tui/screens/optimize_view.py +41 -0
  144. tui/screens/pipe_view.py +46 -0
  145. tui/screens/projects_view.py +355 -0
  146. tui/screens/search_view.py +55 -0
  147. tui/screens/session_view.py +143 -0
  148. tui/screens/stats.py +158 -0
  149. tui/screens/ui_view.py +54 -0
  150. tui/theme.tcss +335 -0
cli/edits.html ADDED
@@ -0,0 +1,878 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>C3 Edit Ledger</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.9/babel.min.js"></script>
10
+ <style>
11
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=DM+Sans:wght@400;500;600;700&display=swap');
12
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13
+ html, body, #root { width: 100%; height: 100%; overflow: hidden; }
14
+ body { font-family: 'DM Sans', -apple-system, sans-serif; }
15
+ .mono { font-family: 'JetBrains Mono', monospace; }
16
+ ::-webkit-scrollbar { width: 5px; }
17
+ ::-webkit-scrollbar-track { background: transparent; }
18
+ ::-webkit-scrollbar-thumb { border-radius: 3px; }
19
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
20
+ @keyframes fadeUp { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
21
+ .fade-up { animation: fadeUp 0.25s ease forwards; }
22
+ @keyframes slideIn { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div id="root"></div>
27
+ <script type="text/babel">
28
+ const { useState, useEffect, useCallback, useMemo, useRef } = React;
29
+
30
+ // ─── Theme ───
31
+ const DARK = {
32
+ bg: "#0a0e14", surface: "#0f1720", surfaceAlt: "#141e2b", border: "#1e2733",
33
+ text: "#c5cdd8", dim: "#6b7d95", accent: "#36d7b7", blue: "#65a9ff",
34
+ warn: "#ffbf57", error: "#ff6f7a", ok: "#50e39f", purple: "#b38aff",
35
+ created: "#22c55e", modified: "#3b82f6", deleted: "#ef4444", renamed: "#f59e0b",
36
+ };
37
+ const LIGHT = {
38
+ bg: "#edf2f8", surface: "#ffffff", surfaceAlt: "#f7f9fd", border: "#cfd9e8",
39
+ text: "#172436", dim: "#5e738f", accent: "#008a76", blue: "#1f6ed4",
40
+ warn: "#b07a00", error: "#c6374b", ok: "#1f9d5b", purple: "#7c5cbf",
41
+ created: "#16a34a", modified: "#2563eb", deleted: "#dc2626", renamed: "#d97706",
42
+ };
43
+
44
+ const API = window.location.origin;
45
+ const api = async (path) => {
46
+ const r = await fetch(`${API}${path}`);
47
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
48
+ return r.json();
49
+ };
50
+
51
+ const timeAgo = (iso) => {
52
+ if (!iso) return "";
53
+ const s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
54
+ if (s < 60) return `${s}s ago`;
55
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
56
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
57
+ return `${Math.floor(s / 86400)}d ago`;
58
+ };
59
+
60
+ const fmtDate = (iso) => {
61
+ if (!iso) return "";
62
+ return iso.slice(0, 19).replace("T", " ");
63
+ };
64
+
65
+ const extOf = (path) => {
66
+ const m = path.match(/\.(\w+)$/);
67
+ return m ? m[1] : "";
68
+ };
69
+
70
+ const dirOf = (path) => {
71
+ const i = path.lastIndexOf("/");
72
+ return i > 0 ? path.slice(0, i) : ".";
73
+ };
74
+
75
+ // ─── Tiny Components ───
76
+ const Badge = ({ color, children, style, title }) =>
77
+ React.createElement("span", {
78
+ className: "mono", title,
79
+ style: { display: "inline-block", padding: "2px 7px", borderRadius: 4, fontSize: 10, fontWeight: 600, background: `${color}20`, color, ...style }
80
+ }, children);
81
+
82
+ const StatCard = ({ label, value, sub, color, T }) =>
83
+ React.createElement("div", {
84
+ style: { background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8, padding: "14px 18px", minWidth: 110, flex: "1 1 110px" }
85
+ },
86
+ React.createElement("div", { style: { fontSize: 22, fontWeight: 700, color: color || T.accent } }, value),
87
+ React.createElement("div", { style: { fontSize: 11, color: T.dim, marginTop: 4, fontWeight: 500 } }, label),
88
+ sub && React.createElement("div", { className: "mono", style: { fontSize: 9, color: T.dim, marginTop: 2 } }, sub)
89
+ );
90
+
91
+ const Pill = ({ color, children, T, active, onClick }) =>
92
+ React.createElement("button", {
93
+ onClick,
94
+ style: {
95
+ padding: "4px 10px", borderRadius: 12, fontSize: 10, fontWeight: 600, cursor: "pointer",
96
+ border: `1px solid ${active ? color : T.border}`,
97
+ background: active ? `${color}18` : "transparent",
98
+ color: active ? color : T.dim, transition: "all 0.15s ease",
99
+ }
100
+ }, children);
101
+
102
+ // ─── Diff Renderer (VSCode/Claude unified style) ───
103
+ const renderDiff = (oldStr, newStr, T) => {
104
+ const oldLines = (oldStr || "").split("\n");
105
+ const newLines = (newStr || "").split("\n");
106
+
107
+ const makeLines = (lines, prefix, color, startNum) =>
108
+ lines.map((line, i) =>
109
+ React.createElement("div", {
110
+ key: `${prefix}${i}`,
111
+ style: { display: "flex", minHeight: 18, background: `${color}0f` }
112
+ },
113
+ React.createElement("span", {
114
+ className: "mono",
115
+ style: {
116
+ display: "inline-block", minWidth: 36, textAlign: "right",
117
+ paddingRight: 6, paddingLeft: 4, color: `${color}55`, fontSize: 10,
118
+ userSelect: "none", flexShrink: 0, borderRight: `2px solid ${color}30`
119
+ }
120
+ }, startNum + i),
121
+ React.createElement("span", {
122
+ className: "mono",
123
+ style: {
124
+ display: "inline-block", width: 18, textAlign: "center",
125
+ color, fontWeight: 700, fontSize: 11, flexShrink: 0
126
+ }
127
+ }, prefix),
128
+ React.createElement("span", {
129
+ className: "mono",
130
+ style: {
131
+ flex: 1, paddingLeft: 4, paddingRight: 8, color,
132
+ fontSize: 10, whiteSpace: "pre-wrap", wordBreak: "break-all",
133
+ lineHeight: 1.6
134
+ }
135
+ }, line || " ")
136
+ )
137
+ );
138
+
139
+ const rows = [];
140
+ if (oldStr) rows.push(...makeLines(oldLines, "−", T.error, 1));
141
+ if (oldStr && newStr) rows.push(
142
+ React.createElement("div", {
143
+ key: "sep",
144
+ style: { height: 1, background: T.border, margin: "0" }
145
+ })
146
+ );
147
+ if (newStr) rows.push(...makeLines(newLines, "+", T.ok, 1));
148
+ return rows;
149
+ };
150
+
151
+ // ─── Timeline Entry ───
152
+ const TimelineEntry = ({ entry, T, isLast, onFileClick, expanded, onToggle }) => {
153
+ const c = T[entry.change_type] || T.dim;
154
+ const isAuto = entry.tags?.includes("auto");
155
+ const ext = extOf(entry.file || "");
156
+ return React.createElement("div", {
157
+ className: "fade-up",
158
+ style: { display: "flex", gap: 16, position: "relative", minHeight: 56 }
159
+ },
160
+ // Timeline line + dot
161
+ React.createElement("div", {
162
+ style: { display: "flex", flexDirection: "column", alignItems: "center", width: 20, flexShrink: 0 }
163
+ },
164
+ React.createElement("div", {
165
+ style: { width: 10, height: 10, borderRadius: "50%", background: c, border: `2px solid ${T.bg}`, boxShadow: `0 0 6px ${c}60`, flexShrink: 0, marginTop: 4 }
166
+ }),
167
+ !isLast && React.createElement("div", {
168
+ style: { width: 1, flex: 1, background: `${T.border}`, marginTop: 2 }
169
+ })
170
+ ),
171
+ // Content
172
+ React.createElement("div", {
173
+ style: { flex: 1, paddingBottom: 14 }
174
+ },
175
+ // Header row
176
+ React.createElement("div", {
177
+ style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }
178
+ },
179
+ React.createElement("span", {
180
+ className: "mono",
181
+ style: { fontSize: 12, fontWeight: 600, color: T.accent, cursor: "pointer", textDecoration: "underline", textDecorationColor: `${T.accent}40`, textUnderlineOffset: 2 },
182
+ onClick: () => onFileClick(entry.file),
183
+ title: "Show file versions"
184
+ }, entry.file),
185
+ React.createElement(Badge, { color: T.purple }, entry.version || "v?"),
186
+ React.createElement(Badge, { color: c }, entry.change_type),
187
+ ext && React.createElement(Badge, { color: T.dim, style: { fontSize: 9 } }, `.${ext}`),
188
+ entry.diff_summary && React.createElement(Badge, { color: T.blue, style: { fontSize: 9 }, title: "Git diff summary" }, entry.diff_summary),
189
+ entry.git?.commit && React.createElement("span", {
190
+ className: "mono", style: { fontSize: 10, color: T.dim, cursor: "default" },
191
+ title: entry.git.subject || ""
192
+ }, entry.git.commit.slice(0, 7)),
193
+ isAuto
194
+ ? React.createElement(Badge, { color: T.ok, style: { fontSize: 8 }, title: "Auto-captured by PostToolUse hook" }, "HOOK")
195
+ : React.createElement(Badge, { color: T.warn, style: { fontSize: 8 }, title: "Manually logged via c3_edits" }, "MANUAL"),
196
+ ),
197
+ // Summary
198
+ entry.summary && React.createElement("div", {
199
+ style: { fontSize: 12, color: T.text, marginTop: 4, lineHeight: 1.5 }
200
+ }, entry.summary),
201
+ // Detail (old/new strings for Edit tool)
202
+ entry.detail && React.createElement("button", {
203
+ onClick: onToggle,
204
+ className: "mono",
205
+ style: { fontSize: 10, color: T.blue, background: "none", border: "none", cursor: "pointer", marginTop: 4, padding: 0 }
206
+ }, expanded ? "Hide diff" : "Show diff"),
207
+ expanded && entry.detail && React.createElement("div", {
208
+ style: {
209
+ marginTop: 6, border: `1px solid ${T.border}`, borderRadius: 6,
210
+ overflow: "auto", maxHeight: 400, background: T.surfaceAlt
211
+ }
212
+ },
213
+ ...renderDiff(entry.detail.old_string, entry.detail.new_string, T)
214
+ ),
215
+ // Tags
216
+ entry.tags?.length > 0 && React.createElement("div", {
217
+ style: { display: "flex", gap: 4, marginTop: 4 }
218
+ }, entry.tags.filter(t => t !== "auto").map((tag, i) =>
219
+ React.createElement(Badge, { key: i, color: T.warn }, tag)
220
+ )),
221
+ // Footer: timestamp + session
222
+ React.createElement("div", {
223
+ style: { display: "flex", alignItems: "center", gap: 10, marginTop: 4 }
224
+ },
225
+ React.createElement("span", { className: "mono", style: { fontSize: 10, color: T.dim } },
226
+ `${fmtDate(entry.timestamp)} \u00b7 ${timeAgo(entry.timestamp)}`
227
+ ),
228
+ entry.session_id && React.createElement(Badge, {
229
+ color: T.blue, style: { fontSize: 8 }, title: `Session: ${entry.session_id}`
230
+ }, `session:${entry.session_id.slice(-6)}`)
231
+ )
232
+ )
233
+ );
234
+ };
235
+
236
+ // ─── File Version Tree ───
237
+ const FileVersionPanel = ({ file, versions, T, onClose }) => {
238
+ return React.createElement("div", {
239
+ style: { background: T.surface, border: `1px solid ${T.border}`, borderRadius: 10, padding: 20, marginBottom: 16 }
240
+ },
241
+ React.createElement("div", {
242
+ style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }
243
+ },
244
+ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } },
245
+ React.createElement("span", { style: { fontSize: 15, fontWeight: 700, color: T.text } }, "Version History"),
246
+ React.createElement("span", { className: "mono", style: { fontSize: 13, color: T.accent } }, file),
247
+ React.createElement(Badge, { color: T.purple }, `${versions.length} versions`)
248
+ ),
249
+ React.createElement("button", {
250
+ onClick: onClose,
251
+ style: { background: "none", border: "none", color: T.dim, cursor: "pointer", fontSize: 18, padding: "2px 6px" }
252
+ }, "\u2715")
253
+ ),
254
+ React.createElement("div", {
255
+ style: { display: "flex", flexDirection: "column", gap: 0 }
256
+ },
257
+ versions.map((v, i) => {
258
+ const c = T[v.change_type] || T.dim;
259
+ return React.createElement("div", {
260
+ key: v.id || i,
261
+ style: { display: "flex", alignItems: "flex-start", gap: 12, position: "relative", minHeight: 44 }
262
+ },
263
+ React.createElement("div", {
264
+ style: { display: "flex", flexDirection: "column", alignItems: "center", width: 50, flexShrink: 0 }
265
+ },
266
+ React.createElement("div", {
267
+ className: "mono",
268
+ style: { fontSize: 12, fontWeight: 700, color: T.purple, background: `${T.purple}15`, padding: "3px 8px", borderRadius: 4, width: 40, textAlign: "center" }
269
+ }, v.version),
270
+ i < versions.length - 1 && React.createElement("div", { style: { width: 1, height: 16, background: T.border } })
271
+ ),
272
+ React.createElement("div", { style: { flex: 1, paddingBottom: 10 } },
273
+ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
274
+ React.createElement(Badge, { color: c }, v.change_type),
275
+ v.diff_summary && React.createElement("span", { className: "mono", style: { fontSize: 10, color: T.dim } }, v.diff_summary),
276
+ v.git?.commit && React.createElement("span", { className: "mono", style: { fontSize: 10, color: T.dim } }, v.git.commit.slice(0, 7))
277
+ ),
278
+ v.summary && React.createElement("div", { style: { fontSize: 12, color: T.text, marginTop: 3 } }, v.summary),
279
+ React.createElement("div", { className: "mono", style: { fontSize: 10, color: T.dim, marginTop: 2 } },
280
+ fmtDate(v.timestamp)
281
+ )
282
+ )
283
+ );
284
+ })
285
+ )
286
+ );
287
+ };
288
+
289
+ // ─── Activity Heatmap ───
290
+ const ActivityHeatmap = ({ edits, T }) => {
291
+ const days = useMemo(() => {
292
+ const map = {};
293
+ edits.forEach(e => {
294
+ const d = e.timestamp?.slice(0, 10);
295
+ if (d) map[d] = (map[d] || 0) + 1;
296
+ });
297
+ const result = [];
298
+ const now = new Date();
299
+ for (let i = 29; i >= 0; i--) {
300
+ const dt = new Date(now);
301
+ dt.setDate(dt.getDate() - i);
302
+ const key = dt.toISOString().slice(0, 10);
303
+ result.push({ date: key, count: map[key] || 0 });
304
+ }
305
+ return result;
306
+ }, [edits]);
307
+
308
+ const max = Math.max(1, ...days.map(d => d.count));
309
+
310
+ return React.createElement("div", {
311
+ style: { background: T.surface, border: `1px solid ${T.border}`, borderRadius: 10, padding: "14px 18px", marginBottom: 16 }
312
+ },
313
+ React.createElement("div", { style: { fontSize: 13, fontWeight: 600, color: T.text, marginBottom: 10 } }, "30-Day Activity"),
314
+ React.createElement("div", {
315
+ style: { display: "flex", gap: 3, alignItems: "flex-end", height: 60 }
316
+ },
317
+ days.map((d) => {
318
+ const h = Math.max(4, (d.count / max) * 56);
319
+ const opacity = d.count === 0 ? 0.1 : 0.3 + (d.count / max) * 0.7;
320
+ return React.createElement("div", {
321
+ key: d.date,
322
+ title: `${d.date}: ${d.count} edits`,
323
+ style: {
324
+ flex: 1, height: h, borderRadius: 2,
325
+ background: T.accent, opacity,
326
+ transition: "height 0.3s ease, opacity 0.3s ease",
327
+ cursor: "default",
328
+ }
329
+ });
330
+ })
331
+ ),
332
+ React.createElement("div", {
333
+ style: { display: "flex", justifyContent: "space-between", marginTop: 4 }
334
+ },
335
+ React.createElement("span", { className: "mono", style: { fontSize: 9, color: T.dim } }, days[0]?.date),
336
+ React.createElement("span", { className: "mono", style: { fontSize: 9, color: T.dim } }, days[days.length - 1]?.date)
337
+ )
338
+ );
339
+ };
340
+
341
+ // ─── Hourly Activity Chart ───
342
+ const HourlyChart = ({ edits, T }) => {
343
+ const hours = useMemo(() => {
344
+ const bins = Array(24).fill(0);
345
+ edits.forEach(e => {
346
+ if (!e.timestamp) return;
347
+ const h = new Date(e.timestamp).getHours();
348
+ if (!isNaN(h)) bins[h]++;
349
+ });
350
+ return bins;
351
+ }, [edits]);
352
+
353
+ const max = Math.max(1, ...hours);
354
+
355
+ return React.createElement("div", {
356
+ style: { background: T.surface, border: `1px solid ${T.border}`, borderRadius: 10, padding: "14px 18px" }
357
+ },
358
+ React.createElement("div", { style: { fontSize: 13, fontWeight: 600, color: T.text, marginBottom: 10 } }, "Edits by Hour"),
359
+ React.createElement("div", {
360
+ style: { display: "flex", gap: 2, alignItems: "flex-end", height: 50 }
361
+ },
362
+ hours.map((count, h) => {
363
+ const ht = Math.max(3, (count / max) * 46);
364
+ return React.createElement("div", {
365
+ key: h,
366
+ title: `${String(h).padStart(2, "0")}:00 \u2013 ${count} edits`,
367
+ style: {
368
+ flex: 1, height: ht, borderRadius: 2,
369
+ background: T.blue, opacity: count === 0 ? 0.1 : 0.3 + (count / max) * 0.7,
370
+ cursor: "default",
371
+ }
372
+ });
373
+ })
374
+ ),
375
+ React.createElement("div", {
376
+ style: { display: "flex", justifyContent: "space-between", marginTop: 4 }
377
+ },
378
+ React.createElement("span", { className: "mono", style: { fontSize: 9, color: T.dim } }, "00:00"),
379
+ React.createElement("span", { className: "mono", style: { fontSize: 9, color: T.dim } }, "12:00"),
380
+ React.createElement("span", { className: "mono", style: { fontSize: 9, color: T.dim } }, "23:00")
381
+ )
382
+ );
383
+ };
384
+
385
+ // ─── Most Edited Bar Chart ───
386
+ const MostEditedChart = ({ data, T }) => {
387
+ if (!data?.length) return null;
388
+ const max = Math.max(1, data[0]?.count || 1);
389
+ return React.createElement("div", {
390
+ style: { background: T.surface, border: `1px solid ${T.border}`, borderRadius: 10, padding: "14px 18px" }
391
+ },
392
+ React.createElement("div", { style: { fontSize: 13, fontWeight: 600, color: T.text, marginBottom: 10 } }, "Most Edited Files"),
393
+ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 6 } },
394
+ data.slice(0, 10).map((m, i) =>
395
+ React.createElement("div", { key: i, style: { display: "flex", alignItems: "center", gap: 10 } },
396
+ React.createElement("span", { className: "mono", style: { fontSize: 11, color: T.accent, width: 220, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flexShrink: 0 } }, m.file),
397
+ React.createElement("div", {
398
+ style: { flex: 1, height: 14, background: T.surfaceAlt, borderRadius: 3, overflow: "hidden" }
399
+ },
400
+ React.createElement("div", {
401
+ style: { height: "100%", width: `${(m.count / max) * 100}%`, background: `linear-gradient(90deg, ${T.accent}, ${T.blue})`, borderRadius: 3, transition: "width 0.5s ease" }
402
+ })
403
+ ),
404
+ React.createElement("span", { className: "mono", style: { fontSize: 10, color: T.dim, width: 30, textAlign: "right" } }, m.count)
405
+ )
406
+ )
407
+ )
408
+ );
409
+ };
410
+
411
+ // ─── Type Distribution ───
412
+ const TypeDistribution = ({ byType, total, T }) => {
413
+ if (!byType || !total) return null;
414
+ const types = Object.entries(byType).sort((a, b) => b[1] - a[1]);
415
+ return React.createElement("div", {
416
+ style: { background: T.surface, border: `1px solid ${T.border}`, borderRadius: 10, padding: "14px 18px" }
417
+ },
418
+ React.createElement("div", { style: { fontSize: 13, fontWeight: 600, color: T.text, marginBottom: 10 } }, "Change Types"),
419
+ React.createElement("div", {
420
+ style: { display: "flex", height: 20, borderRadius: 4, overflow: "hidden", marginBottom: 10 }
421
+ },
422
+ types.map(([type, count]) =>
423
+ React.createElement("div", {
424
+ key: type,
425
+ title: `${type}: ${count}`,
426
+ style: { width: `${(count / total) * 100}%`, background: T[type] || T.dim, transition: "width 0.5s ease" }
427
+ })
428
+ )
429
+ ),
430
+ React.createElement("div", { style: { display: "flex", gap: 14, flexWrap: "wrap" } },
431
+ types.map(([type, count]) =>
432
+ React.createElement("div", { key: type, style: { display: "flex", alignItems: "center", gap: 5 } },
433
+ React.createElement("div", { style: { width: 8, height: 8, borderRadius: 2, background: T[type] || T.dim } }),
434
+ React.createElement("span", { style: { fontSize: 11, color: T.text } }, type),
435
+ React.createElement("span", { className: "mono", style: { fontSize: 10, color: T.dim } }, `${count} (${Math.round(count / total * 100)}%)`)
436
+ )
437
+ )
438
+ )
439
+ );
440
+ };
441
+
442
+ // ─── Directory Breakdown ───
443
+ const DirectoryBreakdown = ({ edits, T }) => {
444
+ const dirs = useMemo(() => {
445
+ const map = {};
446
+ edits.forEach(e => {
447
+ const d = dirOf(e.file || "");
448
+ map[d] = (map[d] || 0) + 1;
449
+ });
450
+ return Object.entries(map).sort((a, b) => b[1] - a[1]).slice(0, 10);
451
+ }, [edits]);
452
+
453
+ if (!dirs.length) return null;
454
+ const max = Math.max(1, dirs[0][1]);
455
+
456
+ return React.createElement("div", {
457
+ style: { background: T.surface, border: `1px solid ${T.border}`, borderRadius: 10, padding: "14px 18px" }
458
+ },
459
+ React.createElement("div", { style: { fontSize: 13, fontWeight: 600, color: T.text, marginBottom: 10 } }, "Directories"),
460
+ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 5 } },
461
+ dirs.map(([dir, count]) =>
462
+ React.createElement("div", { key: dir, style: { display: "flex", alignItems: "center", gap: 10 } },
463
+ React.createElement("span", { className: "mono", style: { fontSize: 11, color: T.purple, width: 180, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flexShrink: 0 } }, dir),
464
+ React.createElement("div", {
465
+ style: { flex: 1, height: 12, background: T.surfaceAlt, borderRadius: 3, overflow: "hidden" }
466
+ },
467
+ React.createElement("div", {
468
+ style: { height: "100%", width: `${(count / max) * 100}%`, background: `linear-gradient(90deg, ${T.purple}80, ${T.blue}80)`, borderRadius: 3 }
469
+ })
470
+ ),
471
+ React.createElement("span", { className: "mono", style: { fontSize: 10, color: T.dim, width: 30, textAlign: "right" } }, count)
472
+ )
473
+ )
474
+ )
475
+ );
476
+ };
477
+
478
+ // ─── Insights Panel ───
479
+ const InsightsPanel = ({ edits, stats, T }) => {
480
+ const insights = useMemo(() => {
481
+ const result = [];
482
+ if (!edits.length) return result;
483
+
484
+ // Auto vs manual
485
+ const autoCount = edits.filter(e => e.tags?.includes("auto")).length;
486
+ const manualCount = edits.length - autoCount;
487
+ if (autoCount > 0 || manualCount > 0) {
488
+ result.push({ label: "Capture source", value: `${autoCount} auto-hook, ${manualCount} manual`, color: T.blue });
489
+ }
490
+
491
+ // Edit velocity (edits per session)
492
+ const sessions = new Set(edits.map(e => e.session_id).filter(Boolean));
493
+ if (sessions.size > 0) {
494
+ const rate = (edits.length / sessions.size).toFixed(1);
495
+ result.push({ label: "Edit velocity", value: `${rate} edits/session across ${sessions.size} sessions`, color: T.accent });
496
+ }
497
+
498
+ // Extensions breakdown
499
+ const exts = {};
500
+ edits.forEach(e => {
501
+ const ex = extOf(e.file || "");
502
+ if (ex) exts[ex] = (exts[ex] || 0) + 1;
503
+ });
504
+ const topExts = Object.entries(exts).sort((a, b) => b[1] - a[1]).slice(0, 5);
505
+ if (topExts.length) {
506
+ result.push({ label: "Top languages", value: topExts.map(([e, c]) => `.${e}: ${c}`).join(", "), color: T.purple });
507
+ }
508
+
509
+ // Most active hour
510
+ const hours = Array(24).fill(0);
511
+ edits.forEach(e => {
512
+ if (!e.timestamp) return;
513
+ const h = new Date(e.timestamp).getHours();
514
+ if (!isNaN(h)) hours[h]++;
515
+ });
516
+ const peakHour = hours.indexOf(Math.max(...hours));
517
+ if (Math.max(...hours) > 0) {
518
+ result.push({ label: "Peak hour", value: `${String(peakHour).padStart(2, "0")}:00 (${hours[peakHour]} edits)`, color: T.warn });
519
+ }
520
+
521
+ // Last edit
522
+ const last = edits[edits.length - 1];
523
+ if (last) {
524
+ result.push({ label: "Latest edit", value: `${last.file} ${last.version} \u2013 ${timeAgo(last.timestamp)}`, color: T.ok });
525
+ }
526
+
527
+ return result;
528
+ }, [edits, T]);
529
+
530
+ if (!insights.length) return null;
531
+
532
+ return React.createElement("div", {
533
+ style: { background: T.surface, border: `1px solid ${T.border}`, borderRadius: 10, padding: "14px 18px", marginBottom: 16 }
534
+ },
535
+ React.createElement("div", { style: { fontSize: 13, fontWeight: 600, color: T.text, marginBottom: 10 } }, "Insights"),
536
+ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 6 } },
537
+ insights.map((ins, i) =>
538
+ React.createElement("div", {
539
+ key: i,
540
+ style: { display: "flex", alignItems: "center", gap: 10, padding: "6px 10px", background: T.surfaceAlt, borderRadius: 6, border: `1px solid ${T.border}` }
541
+ },
542
+ React.createElement("span", { style: { fontSize: 11, fontWeight: 600, color: ins.color, width: 120, flexShrink: 0 } }, ins.label),
543
+ React.createElement("span", { className: "mono", style: { fontSize: 11, color: T.text } }, ins.value)
544
+ )
545
+ )
546
+ )
547
+ );
548
+ };
549
+
550
+ // ─── Empty State ───
551
+ const EmptyState = ({ T }) =>
552
+ React.createElement("div", {
553
+ style: { textAlign: "center", padding: "60px 20px", color: T.dim }
554
+ },
555
+ React.createElement("div", { style: { fontSize: 40, marginBottom: 16, opacity: 0.3 } }, "\ud83d\udcdd"),
556
+ React.createElement("div", { style: { fontSize: 16, fontWeight: 600, color: T.text, marginBottom: 8 } }, "No edits recorded yet"),
557
+ React.createElement("div", { style: { fontSize: 13, lineHeight: 1.7, maxWidth: 480, margin: "0 auto" } },
558
+ "Edits appear here automatically when the PostToolUse hook captures Edit and Write tool calls. ",
559
+ "You can also log edits manually with ",
560
+ React.createElement("code", { className: "mono", style: { fontSize: 11, color: T.accent, background: `${T.accent}15`, padding: "2px 5px", borderRadius: 3 } },
561
+ "c3_edits(action='log', file='...', summary='...')"
562
+ ),
563
+ "."
564
+ ),
565
+ React.createElement("div", {
566
+ style: { marginTop: 20, padding: "12px 16px", background: T.surfaceAlt, borderRadius: 8, border: `1px solid ${T.border}`, display: "inline-block", textAlign: "left" }
567
+ },
568
+ React.createElement("div", { className: "mono", style: { fontSize: 10, color: T.dim, marginBottom: 6 } }, "Hook setup in .claude/settings.local.json:"),
569
+ React.createElement("code", { className: "mono", style: { fontSize: 10, color: T.text, lineHeight: 1.6 } },
570
+ '"PostToolUse": [{ "matcher": "Edit", "hooks": [{ "type": "command", "command": "python hook_edit_ledger.py" }] }]'
571
+ )
572
+ )
573
+ );
574
+
575
+ // ─── App ───
576
+ const App = () => {
577
+ const [darkMode, setDarkMode] = useState(() => localStorage.getItem("c3-edits-theme") !== "light");
578
+ const [edits, setEdits] = useState([]);
579
+ const [stats, setStats] = useState(null);
580
+ const [view, setView] = useState("timeline"); // timeline | stats | files
581
+ const [filter, setFilter] = useState("");
582
+ const [typeFilter, setTypeFilter] = useState("all");
583
+ const [sourceFilter, setSourceFilter] = useState("all"); // all | auto | manual
584
+ const [selectedFile, setSelectedFile] = useState(null);
585
+ const [fileVersions, setFileVersions] = useState([]);
586
+ const [autoRefresh, setAutoRefresh] = useState(true);
587
+ const [lastUpdate, setLastUpdate] = useState(null);
588
+ const [expandedIds, setExpandedIds] = useState(new Set());
589
+ const scrollRef = useRef(null);
590
+
591
+ useEffect(() => {
592
+ localStorage.setItem("c3-edits-theme", darkMode ? "dark" : "light");
593
+ }, [darkMode]);
594
+
595
+ const T = darkMode ? DARK : LIGHT;
596
+
597
+ const load = useCallback(async () => {
598
+ try {
599
+ const [e, s] = await Promise.all([
600
+ api("/api/edits?limit=500"),
601
+ api("/api/edits/stats"),
602
+ ]);
603
+ setEdits(Array.isArray(e) ? e : []);
604
+ setStats(s);
605
+ setLastUpdate(new Date());
606
+ } catch {}
607
+ }, []);
608
+
609
+ useEffect(() => { load(); }, [load]);
610
+ useEffect(() => {
611
+ if (!autoRefresh) return;
612
+ const iv = setInterval(load, 10000);
613
+ return () => clearInterval(iv);
614
+ }, [autoRefresh, load]);
615
+
616
+ const openFileVersions = useCallback(async (file) => {
617
+ try {
618
+ const v = await api(`/api/edits/versions/${encodeURIComponent(file)}`);
619
+ setFileVersions(Array.isArray(v) ? v : []);
620
+ setSelectedFile(file);
621
+ } catch { setFileVersions([]); }
622
+ }, []);
623
+
624
+ const toggleExpand = useCallback((id) => {
625
+ setExpandedIds(prev => {
626
+ const next = new Set(prev);
627
+ if (next.has(id)) next.delete(id);
628
+ else next.add(id);
629
+ return next;
630
+ });
631
+ }, []);
632
+
633
+ // Filtered edits
634
+ const filtered = useMemo(() => {
635
+ let list = edits;
636
+ if (filter) list = list.filter(e =>
637
+ e.file?.toLowerCase().includes(filter.toLowerCase()) ||
638
+ e.summary?.toLowerCase().includes(filter.toLowerCase()) ||
639
+ e.tags?.some(t => t.toLowerCase().includes(filter.toLowerCase()))
640
+ );
641
+ if (typeFilter !== "all") list = list.filter(e => e.change_type === typeFilter);
642
+ if (sourceFilter === "auto") list = list.filter(e => e.tags?.includes("auto"));
643
+ if (sourceFilter === "manual") list = list.filter(e => !e.tags?.includes("auto"));
644
+ return list;
645
+ }, [edits, filter, typeFilter, sourceFilter]);
646
+
647
+ const reversed = useMemo(() => [...filtered].reverse(), [filtered]);
648
+
649
+ // Unique files
650
+ const fileGroups = useMemo(() => {
651
+ const map = {};
652
+ edits.forEach(e => {
653
+ if (!map[e.file]) map[e.file] = { file: e.file, edits: [], latestVersion: "v0", types: new Set(), ext: extOf(e.file || "") };
654
+ map[e.file].edits.push(e);
655
+ map[e.file].latestVersion = e.version || map[e.file].latestVersion;
656
+ map[e.file].types.add(e.change_type);
657
+ });
658
+ return Object.values(map).sort((a, b) => b.edits.length - a.edits.length);
659
+ }, [edits]);
660
+
661
+ const typeOptions = useMemo(() => {
662
+ const types = new Set(edits.map(e => e.change_type));
663
+ return ["all", ...types];
664
+ }, [edits]);
665
+
666
+ const btnStyle = (active) => ({
667
+ padding: "6px 14px", borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: "pointer",
668
+ border: `1px solid ${active ? T.accent : T.border}`,
669
+ background: active ? `${T.accent}18` : "transparent",
670
+ color: active ? T.accent : T.dim,
671
+ transition: "all 0.15s ease",
672
+ });
673
+
674
+ return React.createElement("div", {
675
+ style: { width: "100%", height: "100%", background: T.bg, color: T.text, display: "flex", flexDirection: "column", overflow: "hidden" }
676
+ },
677
+ // ─── Header ───
678
+ React.createElement("div", {
679
+ style: { padding: "12px 24px", borderBottom: `1px solid ${T.border}`, display: "flex", justifyContent: "space-between", alignItems: "center", flexShrink: 0 }
680
+ },
681
+ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14 } },
682
+ React.createElement("span", { style: { fontSize: 18, fontWeight: 700 } },
683
+ React.createElement("span", { style: { color: T.text } }, "C"),
684
+ React.createElement("span", { style: { color: T.accent } }, "3"),
685
+ React.createElement("span", { style: { color: T.text } }, " Edit Ledger")
686
+ ),
687
+ stats && React.createElement(Badge, { color: T.accent }, `${stats.total || 0} edits`),
688
+ stats && React.createElement(Badge, { color: T.blue }, `${stats.files || 0} files`),
689
+ lastUpdate && React.createElement("span", { className: "mono", style: { fontSize: 10, color: T.dim } },
690
+ `Updated ${lastUpdate.toLocaleTimeString()}`
691
+ ),
692
+ autoRefresh && React.createElement("div", {
693
+ style: { width: 6, height: 6, borderRadius: "50%", background: T.ok, animation: "pulse 2s infinite" }
694
+ })
695
+ ),
696
+ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
697
+ React.createElement("button", {
698
+ className: "mono",
699
+ onClick: async () => {
700
+ try { const h = await api("/api/hub/info"); window.location.href = h.url; }
701
+ catch { window.location.href = "http://localhost:3330"; }
702
+ },
703
+ style: { height: 26, padding: "0 10px", borderRadius: 6, border: `1px solid ${T.warn}40`, background: `${T.warn}12`, color: T.warn, fontSize: 11, fontWeight: 600, cursor: "pointer" }
704
+ }, "Hub"),
705
+ React.createElement("a", {
706
+ href: "/", className: "mono",
707
+ style: { height: 26, padding: "0 10px", borderRadius: 6, border: `1px solid ${T.accent}40`, background: `${T.accent}12`, color: T.accent, fontSize: 11, fontWeight: 600, textDecoration: "none", display: "flex", alignItems: "center" }
708
+ }, "Full UI"),
709
+ React.createElement("a", {
710
+ href: "/nano", className: "mono",
711
+ style: { height: 26, padding: "0 10px", borderRadius: 6, border: `1px solid ${T.blue}40`, background: `${T.blue}12`, color: T.blue, fontSize: 11, fontWeight: 600, textDecoration: "none", display: "flex", alignItems: "center" }
712
+ }, "Nano"),
713
+ React.createElement("button", {
714
+ onClick: () => setAutoRefresh(!autoRefresh),
715
+ style: { ...btnStyle(autoRefresh), padding: "4px 10px", fontSize: 11 }
716
+ }, autoRefresh ? "Live" : "Paused"),
717
+ React.createElement("button", {
718
+ onClick: () => setDarkMode(!darkMode),
719
+ style: { background: "none", border: `1px solid ${T.border}`, borderRadius: 6, padding: "4px 10px", cursor: "pointer", color: T.dim, fontSize: 11 }
720
+ }, darkMode ? "Light" : "Dark")
721
+ )
722
+ ),
723
+
724
+ // ─── Toolbar ───
725
+ React.createElement("div", {
726
+ style: { padding: "10px 24px", borderBottom: `1px solid ${T.border}`, display: "flex", justifyContent: "space-between", alignItems: "center", flexShrink: 0, flexWrap: "wrap", gap: 8 }
727
+ },
728
+ React.createElement("div", { style: { display: "flex", gap: 6 } },
729
+ ["timeline", "stats", "files"].map(v =>
730
+ React.createElement("button", { key: v, onClick: () => setView(v), style: btnStyle(view === v) },
731
+ v.charAt(0).toUpperCase() + v.slice(1)
732
+ )
733
+ )
734
+ ),
735
+ React.createElement("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" } },
736
+ // Source filter pills
737
+ React.createElement("div", { style: { display: "flex", gap: 4 } },
738
+ ["all", "auto", "manual"].map(s =>
739
+ React.createElement(Pill, {
740
+ key: s, T, color: s === "auto" ? T.ok : s === "manual" ? T.warn : T.dim,
741
+ active: sourceFilter === s,
742
+ onClick: () => setSourceFilter(s)
743
+ }, s === "all" ? "All sources" : s === "auto" ? "Hook" : "Manual")
744
+ )
745
+ ),
746
+ React.createElement("select", {
747
+ value: typeFilter,
748
+ onChange: (e) => setTypeFilter(e.target.value),
749
+ style: {
750
+ padding: "5px 8px", borderRadius: 6, fontSize: 11, border: `1px solid ${T.border}`,
751
+ background: T.surface, color: T.text, fontFamily: "'JetBrains Mono', monospace", cursor: "pointer"
752
+ }
753
+ }, typeOptions.map(t => React.createElement("option", { key: t, value: t }, t === "all" ? "All types" : t))),
754
+ React.createElement("input", {
755
+ type: "text", placeholder: "Search files, summaries, tags...", value: filter,
756
+ onChange: (e) => setFilter(e.target.value),
757
+ style: {
758
+ width: 260, padding: "6px 12px", borderRadius: 6, fontSize: 12,
759
+ border: `1px solid ${T.border}`, background: T.surface, color: T.text,
760
+ fontFamily: "'JetBrains Mono', monospace"
761
+ }
762
+ }),
763
+ filtered.length !== edits.length && React.createElement("span", {
764
+ className: "mono", style: { fontSize: 10, color: T.dim }
765
+ }, `${filtered.length}/${edits.length}`)
766
+ )
767
+ ),
768
+
769
+ // ─── Content ───
770
+ React.createElement("div", {
771
+ ref: scrollRef,
772
+ style: { flex: 1, overflow: "auto", padding: 24 }
773
+ },
774
+ // File version panel (overlay)
775
+ selectedFile && React.createElement(FileVersionPanel, {
776
+ file: selectedFile, versions: fileVersions, T,
777
+ onClose: () => { setSelectedFile(null); setFileVersions([]); }
778
+ }),
779
+
780
+ // ─── Timeline View ───
781
+ view === "timeline" && React.createElement("div", null,
782
+ // Quick stats row
783
+ stats && React.createElement("div", {
784
+ style: { display: "flex", gap: 12, flexWrap: "wrap", marginBottom: 20 }
785
+ },
786
+ React.createElement(StatCard, { T, label: "Total Edits", value: stats.total || 0, color: T.accent }),
787
+ React.createElement(StatCard, { T, label: "Files Tracked", value: stats.files || 0, color: T.blue }),
788
+ ...(stats.by_type ? Object.entries(stats.by_type).map(([k, v]) =>
789
+ React.createElement(StatCard, { key: k, T, label: k, value: v, color: T[k] || T.dim })
790
+ ) : [])
791
+ ),
792
+ // Activity heatmap
793
+ edits.length > 0 && React.createElement(ActivityHeatmap, { edits, T }),
794
+ // Insights
795
+ edits.length > 0 && React.createElement(InsightsPanel, { edits, stats, T }),
796
+ // Timeline
797
+ React.createElement("div", { style: { marginTop: 8 } },
798
+ reversed.length === 0
799
+ ? React.createElement(EmptyState, { T })
800
+ : reversed.map((e, i) =>
801
+ React.createElement(TimelineEntry, {
802
+ key: e.id || i, entry: e, T,
803
+ isLast: i === reversed.length - 1,
804
+ onFileClick: openFileVersions,
805
+ expanded: expandedIds.has(e.id),
806
+ onToggle: () => toggleExpand(e.id)
807
+ })
808
+ )
809
+ )
810
+ ),
811
+
812
+ // ─── Stats View ───
813
+ view === "stats" && stats && React.createElement("div", {
814
+ style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }
815
+ },
816
+ React.createElement("div", { style: { gridColumn: "1 / -1" } },
817
+ React.createElement("div", { style: { display: "flex", gap: 12, flexWrap: "wrap", marginBottom: 16 } },
818
+ React.createElement(StatCard, { T, label: "Total Edits", value: stats.total || 0, color: T.accent }),
819
+ React.createElement(StatCard, { T, label: "Files Tracked", value: stats.files || 0, color: T.blue }),
820
+ ...(stats.by_type ? Object.entries(stats.by_type).map(([k, v]) =>
821
+ React.createElement(StatCard, { key: k, T, label: k, value: v, color: T[k] || T.dim })
822
+ ) : [])
823
+ )
824
+ ),
825
+ React.createElement("div", { style: { gridColumn: "1 / -1" } },
826
+ edits.length > 0 && React.createElement(InsightsPanel, { edits, stats, T })
827
+ ),
828
+ React.createElement(TypeDistribution, { byType: stats.by_type, total: stats.total, T }),
829
+ React.createElement(MostEditedChart, { data: stats.most_edited, T }),
830
+ React.createElement(DirectoryBreakdown, { edits, T }),
831
+ React.createElement(HourlyChart, { edits, T }),
832
+ React.createElement("div", { style: { gridColumn: "1 / -1" } },
833
+ React.createElement(ActivityHeatmap, { edits, T })
834
+ )
835
+ ),
836
+
837
+ // ─── Files View ───
838
+ view === "files" && React.createElement("div", {
839
+ style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(340px, 1fr))", gap: 12 }
840
+ },
841
+ fileGroups.length === 0 && React.createElement(EmptyState, { T }),
842
+ fileGroups.map(fg =>
843
+ React.createElement("div", {
844
+ key: fg.file,
845
+ onClick: () => openFileVersions(fg.file),
846
+ style: {
847
+ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8,
848
+ padding: "12px 16px", cursor: "pointer", transition: "border-color 0.15s",
849
+ }
850
+ },
851
+ React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" } },
852
+ React.createElement("span", { className: "mono", style: { fontSize: 12, color: T.accent, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1 } }, fg.file),
853
+ React.createElement("div", { style: { display: "flex", gap: 6, flexShrink: 0, marginLeft: 8 } },
854
+ React.createElement(Badge, { color: T.purple }, fg.latestVersion),
855
+ React.createElement(Badge, { color: T.blue }, `${fg.edits.length} edits`),
856
+ fg.ext && React.createElement(Badge, { color: T.dim }, `.${fg.ext}`)
857
+ )
858
+ ),
859
+ // Change type badges
860
+ React.createElement("div", { style: { display: "flex", gap: 4, marginTop: 6 } },
861
+ [...fg.types].map(type =>
862
+ React.createElement(Badge, { key: type, color: T[type] || T.dim }, type)
863
+ )
864
+ ),
865
+ fg.edits.length > 0 && React.createElement("div", {
866
+ style: { fontSize: 11, color: T.dim, marginTop: 6 }
867
+ }, `Last: ${fg.edits[fg.edits.length - 1].summary || fg.edits[fg.edits.length - 1].change_type} \u00b7 ${timeAgo(fg.edits[fg.edits.length - 1].timestamp)}`)
868
+ )
869
+ )
870
+ )
871
+ )
872
+ );
873
+ };
874
+
875
+ ReactDOM.createRoot(document.getElementById("root")).render(React.createElement(App));
876
+ </script>
877
+ </body>
878
+ </html>