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,481 @@
1
+ // Instructions component — manages all IDE instruction documents
2
+ // Globals: T, I, GlowDot, Badge, StatBox, Btn, api, useState, useEffect
3
+
4
+ const Instructions = () => {
5
+ const IDE_DOCS = [
6
+ { key: 'claude', label: 'Claude', file: 'CLAUDE.md' },
7
+ { key: 'codex', label: 'Codex', file: 'AGENTS.md' },
8
+ { key: 'gemini', label: 'Gemini', file: 'GEMINI.md' },
9
+ { key: 'copilot', label: 'Copilot', file: '.github/copilot-instructions.md' },
10
+ ];
11
+
12
+ const [selectedKey, setSelectedKey] = useState('claude');
13
+ const [preview, setPreview] = useState('');
14
+ const [previewLoading, setPreviewLoading] = useState(false);
15
+ const [existsMap, setExistsMap] = useState({}); // key -> bool
16
+ const [busy, setBusy] = useState(false);
17
+ const [actionResult, setActionResult] = useState(null); // { type, data }
18
+ const [error, setError] = useState('');
19
+
20
+ // ── helpers ──────────────────────────────────────────────────────────────
21
+ const sevColor = (sev) => {
22
+ if (!sev) return T.textMuted;
23
+ const s = sev.toLowerCase();
24
+ if (s === 'error') return T.error;
25
+ if (s === 'warning') return '#ffb224';
26
+ return T.textMuted;
27
+ };
28
+
29
+ const lineCount = preview ? preview.split('\n').length : 0;
30
+ // rough token estimate: ~4 chars per token
31
+ const tokenEst = preview ? Math.round(preview.length / 4) : 0;
32
+
33
+ const selectedDoc = IDE_DOCS.find(d => d.key === selectedKey);
34
+
35
+ // ── data loading ─────────────────────────────────────────────────────────
36
+ const loadPreview = async () => {
37
+ setPreviewLoading(true);
38
+ setError('');
39
+ try {
40
+ const data = await api.get('/api/claudemd');
41
+ // backend may return { content } or a plain string
42
+ const content = typeof data === 'string' ? data : (data.content || data.generated || '');
43
+ setPreview(content);
44
+ // infer which docs exist from a check call (optional, best-effort)
45
+ try {
46
+ const check = await api.get('/api/claudemd/check');
47
+ if (check && check.docs_exist) {
48
+ setExistsMap(check.docs_exist);
49
+ }
50
+ } catch (_) {}
51
+ } catch (e) {
52
+ setError(e.message || 'Failed to load preview');
53
+ }
54
+ setPreviewLoading(false);
55
+ };
56
+
57
+ useEffect(() => {
58
+ loadPreview();
59
+ }, []);
60
+
61
+ // ── actions ───────────────────────────────────────────────────────────────
62
+ const runSyncAll = async () => {
63
+ setBusy(true);
64
+ setActionResult(null);
65
+ setError('');
66
+ try {
67
+ const res = await api.post('/api/claudemd/save', {});
68
+ setActionResult({ type: 'sync', data: res });
69
+ await loadPreview();
70
+ } catch (e) {
71
+ setError(e.message || 'Sync failed');
72
+ }
73
+ setBusy(false);
74
+ };
75
+
76
+ const runGenerate = async () => {
77
+ setBusy(true);
78
+ setActionResult(null);
79
+ setError('');
80
+ try {
81
+ const res = await api.post('/api/claudemd/save', {});
82
+ setActionResult({ type: 'generate', data: res });
83
+ await loadPreview();
84
+ } catch (e) {
85
+ setError(e.message || 'Generate failed');
86
+ }
87
+ setBusy(false);
88
+ };
89
+
90
+ const runHealthCheck = async () => {
91
+ setBusy(true);
92
+ setActionResult(null);
93
+ setError('');
94
+ try {
95
+ const res = await api.get('/api/claudemd/check');
96
+ setActionResult({ type: 'check', data: res });
97
+ } catch (e) {
98
+ setError(e.message || 'Health check failed');
99
+ }
100
+ setBusy(false);
101
+ };
102
+
103
+ const runCompact = async () => {
104
+ setBusy(true);
105
+ setActionResult(null);
106
+ setError('');
107
+ try {
108
+ const res = await api.post('/api/claudemd/compact', { target_lines: 150 });
109
+ setActionResult({ type: 'compact', data: res });
110
+ await loadPreview();
111
+ } catch (e) {
112
+ setError(e.message || 'Compact failed');
113
+ }
114
+ setBusy(false);
115
+ };
116
+
117
+ const runPromote = async () => {
118
+ setBusy(true);
119
+ setActionResult(null);
120
+ setError('');
121
+ try {
122
+ const res = await api.get('/api/claudemd/promote');
123
+ setActionResult({ type: 'promote', data: res });
124
+ } catch (e) {
125
+ setError(e.message || 'Promote failed');
126
+ }
127
+ setBusy(false);
128
+ };
129
+
130
+ // ── sub-renders ───────────────────────────────────────────────────────────
131
+ const renderActionResult = () => {
132
+ if (!actionResult) return null;
133
+ const { type, data } = actionResult;
134
+
135
+ if (type === 'check') {
136
+ const issues = data.issues || [];
137
+ return (
138
+ <div style={{ animation: 'fadeUp 0.25s ease' }}>
139
+ <div style={{
140
+ fontSize: 11, fontWeight: 700, color: T.textMuted,
141
+ textTransform: 'uppercase', letterSpacing: 1, marginBottom: 10
142
+ }}>
143
+ Health Check — {issues.length === 0 ? 'No issues' : `${issues.length} issue${issues.length > 1 ? 's' : ''}`}
144
+ </div>
145
+ {issues.length === 0 ? (
146
+ <div style={{
147
+ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px',
148
+ background: `${T.accent}12`, border: `1px solid ${T.accent}30`, borderRadius: 6,
149
+ fontSize: 12, color: T.accent
150
+ }}>
151
+ <I name="check" size={14} color={T.accent} /> All documents look healthy.
152
+ </div>
153
+ ) : (
154
+ issues.map((iss, i) => (
155
+ <div key={i} style={{
156
+ display: 'flex', gap: 10, alignItems: 'flex-start',
157
+ padding: '9px 12px', marginBottom: 6,
158
+ background: `${sevColor(iss.severity)}10`,
159
+ border: `1px solid ${sevColor(iss.severity)}30`,
160
+ borderLeft: `3px solid ${sevColor(iss.severity)}`,
161
+ borderRadius: '0 6px 6px 6px', fontSize: 12
162
+ }}>
163
+ <span style={{
164
+ fontSize: 9, padding: '2px 6px', borderRadius: 3, fontWeight: 700,
165
+ background: `${sevColor(iss.severity)}20`, color: sevColor(iss.severity),
166
+ textTransform: 'uppercase', letterSpacing: 0.5, flexShrink: 0, marginTop: 1
167
+ }}>
168
+ {iss.severity || 'info'}
169
+ </span>
170
+ <span style={{ color: T.text, lineHeight: 1.6 }}>{iss.message || JSON.stringify(iss)}</span>
171
+ </div>
172
+ ))
173
+ )}
174
+ </div>
175
+ );
176
+ }
177
+
178
+ if (type === 'compact') {
179
+ const before = data.lines_before ?? data.original_lines ?? '?';
180
+ const after = data.lines_after ?? data.compacted_lines ?? '?';
181
+ const saved = typeof before === 'number' && typeof after === 'number' ? before - after : null;
182
+ return (
183
+ <div style={{ animation: 'fadeUp 0.25s ease' }}>
184
+ <div style={{
185
+ fontSize: 11, fontWeight: 700, color: T.textMuted,
186
+ textTransform: 'uppercase', letterSpacing: 1, marginBottom: 10
187
+ }}>
188
+ Compact Result
189
+ </div>
190
+ <div style={{
191
+ display: 'flex', gap: 12, flexWrap: 'wrap'
192
+ }}>
193
+ <StatBox label="Before" value={before} color={T.warn} />
194
+ <StatBox label="After" value={after} color={T.accent} />
195
+ {saved !== null && (
196
+ <StatBox label="Saved" value={`-${saved}`} color={T.blue} />
197
+ )}
198
+ </div>
199
+ {data.message && (
200
+ <div style={{ marginTop: 12, fontSize: 12, color: T.textMuted, lineHeight: 1.6 }}>
201
+ {data.message}
202
+ </div>
203
+ )}
204
+ </div>
205
+ );
206
+ }
207
+
208
+ if (type === 'promote') {
209
+ const raw = data.candidates || data.sections || {};
210
+ // Backend returns { "Section Name": [items...] } dict, not an array
211
+ const entries = typeof raw === 'object' && !Array.isArray(raw)
212
+ ? Object.entries(raw)
213
+ : [];
214
+ const totalCount = data.total_candidates || entries.reduce((s, [, v]) => s + (Array.isArray(v) ? v.length : 0), 0);
215
+ return (
216
+ <div style={{ animation: 'fadeUp 0.25s ease' }}>
217
+ <div style={{
218
+ fontSize: 11, fontWeight: 700, color: T.textMuted,
219
+ textTransform: 'uppercase', letterSpacing: 1, marginBottom: 10
220
+ }}>
221
+ Promote Insights — {totalCount} candidate{totalCount !== 1 ? 's' : ''}
222
+ </div>
223
+ {data.message && (
224
+ <div style={{ fontSize: 12, color: T.textMuted, marginBottom: 10 }}>{data.message}</div>
225
+ )}
226
+ {totalCount === 0 ? (
227
+ <div style={{ fontSize: 12, color: T.textMuted, fontStyle: 'italic' }}>
228
+ No promotable insights found. Use C3 tools more to build up facts.
229
+ </div>
230
+ ) : (
231
+ entries.map(([section, items]) => (
232
+ <div key={section} style={{ marginBottom: 12 }}>
233
+ <div style={{
234
+ fontSize: 10, fontWeight: 700, color: T.purple,
235
+ textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 6
236
+ }}>
237
+ {section}
238
+ </div>
239
+ {(Array.isArray(items) ? items : []).map((c, i) => (
240
+ <div key={i} style={{
241
+ marginBottom: 6, padding: '8px 14px',
242
+ background: T.surface, border: `1px solid ${T.border}`,
243
+ borderLeft: `3px solid ${T.purple}60`,
244
+ borderRadius: '0 6px 6px 6px'
245
+ }}>
246
+ <div style={{ fontSize: 12, color: T.text, lineHeight: 1.6 }}>
247
+ {c.fact || c.text || c.insight || c.snippet || JSON.stringify(c)}
248
+ </div>
249
+ <div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
250
+ {c.category && <Badge color={T.purple}>{c.category}</Badge>}
251
+ {c.relevance_count > 0 && (
252
+ <span className="mono" style={{ fontSize: 9, color: T.textDim }}>
253
+ relevance: {c.relevance_count}
254
+ </span>
255
+ )}
256
+ </div>
257
+ </div>
258
+ ))}
259
+ </div>
260
+ ))
261
+ )}
262
+ </div>
263
+ );
264
+ }
265
+
266
+ // sync / generate — simple status message
267
+ const msg = data.message || data.status || (data.saved ? 'Documents saved.' : 'Done.');
268
+ return (
269
+ <div style={{
270
+ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px',
271
+ background: `${T.accent}12`, border: `1px solid ${T.accent}30`, borderRadius: 6,
272
+ fontSize: 12, color: T.accent, animation: 'fadeUp 0.25s ease'
273
+ }}>
274
+ <I name="check" size={14} color={T.accent} /> {msg}
275
+ </div>
276
+ );
277
+ };
278
+
279
+ // ── spinner ────────────────────────────────────────────────────────────────
280
+ const Spinner = () => (
281
+ <div style={{
282
+ display: 'inline-block', width: 14, height: 14, borderRadius: '50%',
283
+ border: `2px solid ${T.border}`, borderTopColor: T.accent,
284
+ animation: 'spin 0.7s linear infinite', flexShrink: 0
285
+ }} />
286
+ );
287
+
288
+ // ── layout ─────────────────────────────────────────────────────────────────
289
+ return (
290
+ <div style={{
291
+ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden',
292
+ background: T.bg
293
+ }}>
294
+ {/* Header */}
295
+ <div style={{
296
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
297
+ padding: '18px 24px 14px',
298
+ borderBottom: `1px solid ${T.border}`,
299
+ flexShrink: 0
300
+ }}>
301
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
302
+ <I name="file" size={18} color={T.accent} />
303
+ <span style={{ fontSize: 16, fontWeight: 700, color: T.text, letterSpacing: -0.3 }}>
304
+ Instruction Documents
305
+ </span>
306
+ <Badge color={T.blue}>{IDE_DOCS.length} IDEs</Badge>
307
+ </div>
308
+ <Btn
309
+ color={T.accent}
310
+ variant="solid"
311
+ onClick={runSyncAll}
312
+ disabled={busy}
313
+ style={{ gap: 6 }}
314
+ >
315
+ {busy ? <Spinner /> : <I name="refresh" size={13} />}
316
+ Sync All
317
+ </Btn>
318
+ </div>
319
+
320
+ {/* IDE selector */}
321
+ <div style={{
322
+ display: 'flex', gap: 6, padding: '12px 24px',
323
+ borderBottom: `1px solid ${T.border}`,
324
+ flexShrink: 0, flexWrap: 'wrap'
325
+ }}>
326
+ {IDE_DOCS.map(doc => {
327
+ const isActive = doc.key === selectedKey;
328
+ const docExists = existsMap[doc.key] ?? existsMap[doc.file] ?? null;
329
+ return (
330
+ <button
331
+ key={doc.key}
332
+ onClick={() => setSelectedKey(doc.key)}
333
+ style={{
334
+ display: 'flex', alignItems: 'center', gap: 6,
335
+ padding: '6px 14px', borderRadius: 6, cursor: 'pointer',
336
+ border: isActive ? `1px solid ${T.accent}` : `1px solid ${T.border}`,
337
+ background: isActive ? `${T.accent}18` : 'transparent',
338
+ color: isActive ? T.accent : T.textMuted,
339
+ fontSize: 12, fontWeight: 600,
340
+ transition: 'all 0.15s',
341
+ }}
342
+ onMouseEnter={e => { if (!isActive) { e.currentTarget.style.borderColor = T.borderHover; e.currentTarget.style.color = T.text; }}}
343
+ onMouseLeave={e => { if (!isActive) { e.currentTarget.style.borderColor = T.border; e.currentTarget.style.color = T.textMuted; }}}
344
+ >
345
+ {docExists === true && <GlowDot color={T.accent} size={6} />}
346
+ {docExists === false && <GlowDot color={T.textDim} size={6} />}
347
+ {docExists === null && <span style={{ width: 6, height: 6 }} />}
348
+ {doc.label}
349
+ </button>
350
+ );
351
+ })}
352
+ </div>
353
+
354
+ {/* Main body — scrollable */}
355
+ <div style={{ flex: 1, overflowY: 'auto', padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 20 }}>
356
+
357
+ {/* Error banner */}
358
+ {error && (
359
+ <div style={{
360
+ padding: '10px 14px', background: `${T.error}12`,
361
+ border: `1px solid ${T.error}30`, borderRadius: 6,
362
+ fontSize: 12, color: T.error, lineHeight: 1.5
363
+ }}>
364
+ {error}
365
+ </div>
366
+ )}
367
+
368
+ {/* Document info row */}
369
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
370
+ <span style={{ fontSize: 12, color: T.textMuted, fontFamily: "'JetBrains Mono', monospace" }}>
371
+ {selectedDoc?.file}
372
+ </span>
373
+ {!previewLoading && preview && (
374
+ <>
375
+ <Badge color={T.blue}>{lineCount} lines</Badge>
376
+ <Badge color={T.textMuted}>~{tokenEst} tokens</Badge>
377
+ </>
378
+ )}
379
+ {previewLoading && (
380
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: T.textMuted }}>
381
+ <Spinner /> Loading…
382
+ </div>
383
+ )}
384
+ </div>
385
+
386
+ {/* Preview */}
387
+ <div style={{
388
+ background: T.surface, border: `1px solid ${T.border}`, borderRadius: 8,
389
+ overflow: 'hidden', flexShrink: 0
390
+ }}>
391
+ <div style={{
392
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
393
+ padding: '8px 14px', borderBottom: `1px solid ${T.border}`,
394
+ background: T.surfaceAlt
395
+ }}>
396
+ <span style={{ fontSize: 11, fontWeight: 700, color: T.textMuted, textTransform: 'uppercase', letterSpacing: 1 }}>
397
+ Preview
398
+ </span>
399
+ <span style={{ fontSize: 10, color: T.textDim, fontFamily: "'JetBrains Mono', monospace" }}>
400
+ {selectedDoc?.file}
401
+ </span>
402
+ </div>
403
+ {previewLoading ? (
404
+ <div style={{
405
+ height: 200, display: 'flex', alignItems: 'center', justifyContent: 'center',
406
+ color: T.textDim, fontSize: 13, gap: 8
407
+ }}>
408
+ <Spinner /> Loading preview…
409
+ </div>
410
+ ) : preview ? (
411
+ <div style={{ overflowY: 'auto', maxHeight: 400 }}>
412
+ <table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
413
+ <tbody>
414
+ {preview.split('\n').map((line, idx) => (
415
+ <tr key={idx} style={{ verticalAlign: 'top' }}>
416
+ <td style={{
417
+ width: 48, paddingLeft: 10, paddingRight: 10,
418
+ textAlign: 'right', fontSize: 11, lineHeight: '20px',
419
+ color: T.textDim, userSelect: 'none', flexShrink: 0,
420
+ fontFamily: "'JetBrains Mono', monospace",
421
+ borderRight: `1px solid ${T.border}30`
422
+ }}>
423
+ {idx + 1}
424
+ </td>
425
+ <td style={{
426
+ paddingLeft: 12, paddingRight: 12,
427
+ fontSize: 12, lineHeight: '20px', color: T.text,
428
+ fontFamily: "'JetBrains Mono', monospace",
429
+ whiteSpace: 'pre-wrap', wordBreak: 'break-word'
430
+ }}>
431
+ {line || '\u00A0'}
432
+ </td>
433
+ </tr>
434
+ ))}
435
+ </tbody>
436
+ </table>
437
+ </div>
438
+ ) : (
439
+ <div style={{
440
+ height: 120, display: 'flex', alignItems: 'center', justifyContent: 'center',
441
+ color: T.textDim, fontSize: 13
442
+ }}>
443
+ No content — click Generate &amp; Save to create this document.
444
+ </div>
445
+ )}
446
+ </div>
447
+
448
+ {/* Action buttons */}
449
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
450
+ <Btn color={T.accent} variant="solid" onClick={runGenerate} disabled={busy}>
451
+ {busy ? <Spinner /> : <I name="save" size={13} />}
452
+ Generate &amp; Save
453
+ </Btn>
454
+ <Btn color={T.blue} variant="ghost" onClick={runHealthCheck} disabled={busy}>
455
+ {busy ? <Spinner /> : <I name="zap" size={13} />}
456
+ Health Check
457
+ </Btn>
458
+ <Btn color={T.warn} variant="ghost" onClick={runCompact} disabled={busy}>
459
+ {busy ? <Spinner /> : <I name="minimize" size={13} />}
460
+ Compact
461
+ </Btn>
462
+ <Btn color={T.purple} variant="ghost" onClick={runPromote} disabled={busy}>
463
+ {busy ? <Spinner /> : <I name="bookmark" size={13} />}
464
+ Promote Insights
465
+ </Btn>
466
+ </div>
467
+
468
+ {/* Results area */}
469
+ {actionResult && (
470
+ <div style={{
471
+ background: T.surface, border: `1px solid ${T.border}`,
472
+ borderRadius: 8, padding: '16px 18px'
473
+ }}>
474
+ {renderActionResult()}
475
+ </div>
476
+ )}
477
+
478
+ </div>
479
+ </div>
480
+ );
481
+ };