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/tools/validate.py ADDED
@@ -0,0 +1,310 @@
1
+ """c3_validate — Deterministic syntax validation using native language parsers."""
2
+
3
+ import asyncio
4
+ import os
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ async def _deep_type_check(full: Path, ext: str, timeout: int = 15) -> list[str]:
13
+ """Run pyright (Python) or tsc (TypeScript) if available. Returns advisory type-warning strings."""
14
+ warnings: list[str] = []
15
+
16
+ def _popen_no_window() -> dict:
17
+ kw: dict = {}
18
+ if sys.platform == "win32":
19
+ kw["creationflags"] = subprocess.CREATE_NO_WINDOW
20
+ return kw
21
+
22
+ if ext == ".py":
23
+ exe = shutil.which("pyright")
24
+ if not exe:
25
+ return []
26
+ try:
27
+ proc = await asyncio.to_thread(
28
+ subprocess.run,
29
+ [exe, "--outputjson", str(full)],
30
+ capture_output=True, text=True, timeout=timeout,
31
+ stdin=subprocess.DEVNULL,
32
+ **_popen_no_window(),
33
+ )
34
+ try:
35
+ data = __import__("json").loads(proc.stdout)
36
+ diags = data.get("generalDiagnostics", [])
37
+ errors = [d for d in diags if d.get("severity") == "error"]
38
+ warns = [d for d in diags if d.get("severity") == "warning"]
39
+ if errors or warns:
40
+ parts = []
41
+ if errors:
42
+ parts.append(f"{len(errors)} type error(s)")
43
+ if warns:
44
+ parts.append(f"{len(warns)} warning(s)")
45
+ warnings.append(f"pyright: {', '.join(parts)}")
46
+ for d in errors[:3]:
47
+ ln = (d.get("range") or {}).get("start", {}).get("line", "?")
48
+ warnings.append(f" L{ln}: {d.get('message', '')[:80]}")
49
+ except Exception:
50
+ # Fallback: count error lines from plain text output
51
+ lines = (proc.stdout + proc.stderr).splitlines()
52
+ errs = [l for l in lines if " - error:" in l or " error " in l.lower()]
53
+ if errs:
54
+ warnings.append(f"pyright: {len(errs)} issue(s) — run pyright {full.name} for details")
55
+ except (subprocess.TimeoutExpired, Exception):
56
+ pass
57
+
58
+ elif ext in (".ts", ".tsx"):
59
+ exe = shutil.which("tsc")
60
+ if not exe:
61
+ exe = shutil.which("npx")
62
+ tsc_args = [exe, "tsc", "--noEmit", "--strict", str(full)] if exe else []
63
+ else:
64
+ tsc_args = [exe, "--noEmit", "--strict", str(full)]
65
+ if not tsc_args:
66
+ return []
67
+ try:
68
+ proc = await asyncio.to_thread(
69
+ subprocess.run,
70
+ tsc_args,
71
+ capture_output=True, text=True, timeout=timeout,
72
+ stdin=subprocess.DEVNULL, cwd=str(full.parent),
73
+ **_popen_no_window(),
74
+ )
75
+ output = proc.stdout + proc.stderr
76
+ # tsc output: "file.ts(10,5): error TS2304: Cannot find..."
77
+ errs = re.findall(r"error TS\d+:", output)
78
+ warns = re.findall(r"warning TS\d+:", output)
79
+ if errs or warns:
80
+ parts = []
81
+ if errs:
82
+ parts.append(f"{len(errs)} type error(s)")
83
+ if warns:
84
+ parts.append(f"{len(warns)} warning(s)")
85
+ warnings.append(f"tsc: {', '.join(parts)}")
86
+ for line in output.splitlines()[:3]:
87
+ if "error TS" in line:
88
+ warnings.append(f" {line.strip()[:100]}")
89
+ except (subprocess.TimeoutExpired, Exception):
90
+ pass
91
+
92
+ return warnings
93
+
94
+
95
+ async def handle_validate(file_path: str, svc, finalize) -> str:
96
+ # Support comma-separated paths for batch validation
97
+ paths = [p.strip() for p in file_path.split(",") if p.strip()]
98
+
99
+ if len(paths) == 1:
100
+ return await _validate_single(paths[0], svc, finalize)
101
+
102
+ # Batch: validate all files in parallel
103
+ cfg = (svc.hybrid_config or {}).get("agent_workflows", {})
104
+ max_files = max(1, int(cfg.get("batch_validate_max_files", 10)))
105
+ paths = paths[:max_files]
106
+
107
+ tasks = [_validate_one(p, svc) for p in paths]
108
+ results = await asyncio.gather(*tasks, return_exceptions=True)
109
+
110
+ pass_count = 0
111
+ fail_count = 0
112
+ skip_count = 0
113
+ lines = []
114
+ for fp, res in zip(paths, results):
115
+ if isinstance(res, Exception):
116
+ lines.append(f"? {fp} — ERROR: {res}")
117
+ skip_count += 1
118
+ continue
119
+ status_word, detail = res
120
+ if status_word == "PASS":
121
+ lines.append(f"\u2713 {fp} \u2014 {detail}")
122
+ pass_count += 1
123
+ elif status_word == "FAIL":
124
+ lines.append(f"\u2717 {fp} \u2014 {detail}")
125
+ fail_count += 1
126
+ else:
127
+ lines.append(f"? {fp} \u2014 {detail}")
128
+ skip_count += 1
129
+
130
+ body = "\n".join(lines)
131
+ summary = f"{len(paths)} files: {pass_count} pass, {fail_count} fail"
132
+ if skip_count:
133
+ summary += f", {skip_count} skip"
134
+ return finalize("c3_validate", {"file_path": file_path, "batch": True}, body, summary)
135
+
136
+
137
+ async def _validate_one(file_path: str, svc) -> tuple:
138
+ """Validate a single file and return (status_word, detail_line)."""
139
+ full = Path(svc.project_path) / file_path
140
+ if not full.exists():
141
+ full = Path(file_path)
142
+ if not full.exists():
143
+ return ("SKIP", f"NOT_FOUND: {file_path}")
144
+
145
+ ext = full.suffix.lower()
146
+ lang = ext.lstrip('.').upper() if ext else 'unknown'
147
+ hybrid_cfg = svc.hybrid_config or {}
148
+ timeout_seconds = max(1, int(hybrid_cfg.get("validate_timeout_seconds", 35) or 35))
149
+
150
+ # Resolve relative path once (used for cache get/put)
151
+ try:
152
+ rel = str(full.resolve().relative_to(Path(svc.project_path).resolve()))
153
+ except Exception:
154
+ rel = file_path
155
+
156
+ # Try cached result first
157
+ vcache = getattr(svc, "validation_cache", None)
158
+ cached_hit = False
159
+ result = None
160
+ if vcache:
161
+ try:
162
+ cached = vcache.get(rel)
163
+ if cached is not None:
164
+ result = cached
165
+ cached_hit = True
166
+ except Exception:
167
+ pass
168
+
169
+ if not cached_hit:
170
+ try:
171
+ content = await asyncio.to_thread(full.read_text, encoding="utf-8", errors="replace")
172
+ except Exception as e:
173
+ return ("SKIP", f"READ_ERROR {lang}: {e}")
174
+
175
+ from services.parser import check_syntax_native_with_timeout
176
+
177
+ try:
178
+ result = await asyncio.to_thread(
179
+ check_syntax_native_with_timeout, content, ext, timeout_seconds,
180
+ )
181
+ except Exception:
182
+ result = {"status": "checker_failed", "checker": "native", "errors": [],
183
+ "detail": "Validation failed unexpectedly."}
184
+
185
+ if vcache:
186
+ try:
187
+ st = os.stat(str(full))
188
+ vcache.put(rel, result, st.st_mtime, st.st_size)
189
+ except Exception:
190
+ pass
191
+
192
+ outcome = result.get("status", "checker_failed")
193
+ checker = result.get("checker", "native")
194
+ errors = result.get("errors", []) or []
195
+ detail_text = result.get("detail", "")
196
+ cache_tag = " [cached]" if cached_hit else ""
197
+
198
+ if outcome == "clean":
199
+ return ("PASS", f"PASS {lang}")
200
+ elif outcome == "syntax_error":
201
+ err_lines = "; ".join(f"L{e['line']}: {e['text']}" for e in errors[:3])
202
+ more = f" (+{len(errors) - 3} more)" if len(errors) > 3 else ""
203
+ return ("FAIL", f"FAIL {lang}: {err_lines}{more}")
204
+ elif outcome == "checker_unavailable":
205
+ return ("SKIP", f"SKIP {lang}: {checker} not found{cache_tag}")
206
+ elif outcome == "checker_timeout":
207
+ return ("SKIP", f"TIMEOUT {lang}: exceeded {timeout_seconds}s{cache_tag}")
208
+ elif outcome == "unsupported":
209
+ return ("SKIP", f"SKIP {lang}: unsupported type{cache_tag}")
210
+ else:
211
+ return ("SKIP", f"ERROR {lang}: {detail_text}{cache_tag}")
212
+
213
+
214
+ async def _validate_single(file_path: str, svc, finalize) -> str:
215
+ """Original single-file validation path."""
216
+ full = Path(svc.project_path) / file_path
217
+ if not full.exists():
218
+ full = Path(file_path)
219
+ if not full.exists():
220
+ return f"Error: File not found: {file_path}"
221
+
222
+ ext = full.suffix.lower()
223
+ lang = ext.lstrip('.').upper() if ext else 'unknown'
224
+ hybrid_cfg = svc.hybrid_config or {}
225
+ timeout_seconds = max(1, int(hybrid_cfg.get("validate_timeout_seconds", 35) or 35))
226
+
227
+ # Try cached result first (populated by background watcher).
228
+ cached_hit = False
229
+ vcache = getattr(svc, "validation_cache", None)
230
+ if vcache:
231
+ try:
232
+ rel = str(full.resolve().relative_to(Path(svc.project_path).resolve()))
233
+ cached = vcache.get(rel)
234
+ if cached is not None:
235
+ result = cached
236
+ cached_hit = True
237
+ except Exception:
238
+ pass
239
+
240
+ if not cached_hit:
241
+ try:
242
+ content = await asyncio.to_thread(full.read_text, encoding="utf-8", errors="replace")
243
+ except Exception as e:
244
+ return f"Error: Could not read {file_path}: {e}"
245
+
246
+ from services.parser import check_syntax_native_with_timeout
247
+
248
+ try:
249
+ result = await asyncio.to_thread(
250
+ check_syntax_native_with_timeout, content, ext, timeout_seconds,
251
+ )
252
+ except Exception:
253
+ result = {"status": "checker_failed", "checker": "native", "errors": [],
254
+ "detail": "Validation failed unexpectedly."}
255
+
256
+ # Store result in cache for future calls.
257
+ if vcache:
258
+ try:
259
+ rel = str(full.resolve().relative_to(Path(svc.project_path).resolve()))
260
+ st = os.stat(str(full))
261
+ vcache.put(rel, result, st.st_mtime, st.st_size)
262
+ except Exception:
263
+ pass
264
+
265
+ checker = result.get("checker", "native")
266
+ detail = result.get("detail", "")
267
+ errors = result.get("errors", []) or []
268
+ outcome = result.get("status", "checker_failed")
269
+
270
+ cache_tag = " [cached]" if cached_hit else ""
271
+
272
+ if outcome == "clean":
273
+ # LSP-inspired deep type check (advisory, never blocks PASS)
274
+ type_warns = await _deep_type_check(full, ext)
275
+ if type_warns:
276
+ status = f"PASS {lang} [type-warnings]\n" + "\n".join(type_warns)
277
+ else:
278
+ status = f"PASS {lang}"
279
+ summary = "pass"
280
+ elif outcome == "syntax_error":
281
+ status = f"FAIL {lang}:\n"
282
+ for err in errors[:10]:
283
+ status += f"- L{err['line']}, Col {err['column']}: {err['text']}\n"
284
+ if len(errors) > 10:
285
+ status += f"- ... and {len(errors) - 10} more errors.\n"
286
+ if detail:
287
+ status += f"[detail] {detail}"
288
+ summary = f"validated syntax_error via {checker}"
289
+ elif outcome == "checker_unavailable":
290
+ status = f"SKIP {lang}: {checker} not found on PATH — install it to enable validation. [checker:{checker}]"
291
+ if detail:
292
+ status += f"\n[detail] {detail}"
293
+ summary = f"validated checker_unavailable via {checker}"
294
+ elif outcome == "checker_timeout":
295
+ status = f"TIMEOUT {lang}: validation exceeded {timeout_seconds}s. [checker:{checker}]"
296
+ if detail:
297
+ status += f"\n[detail] {detail}"
298
+ summary = f"validated checker_timeout via {checker}"
299
+ elif outcome == "unsupported":
300
+ status = f"SKIP {lang}: unsupported file type for native validation. [checker:{checker}]"
301
+ if detail:
302
+ status += f"\n[detail] {detail}"
303
+ summary = f"validated unsupported via {checker}"
304
+ else:
305
+ status = f"ERROR {lang}: validator failed. [checker:{checker}]"
306
+ if detail:
307
+ status += f"\n[detail] {detail}"
308
+ summary = f"validated checker_failed via {checker}"
309
+
310
+ return finalize("c3_validate", {"file_path": file_path}, status, summary)
cli/ui/api.js ADDED
@@ -0,0 +1,36 @@
1
+ // ─── API Client ───────────────────────────
2
+ const API = window.location.origin;
3
+ const parseApiResponse = async (r) => {
4
+ const ct = (r.headers.get('content-type') || '').toLowerCase();
5
+ let json = null;
6
+ let text = '';
7
+ if (ct.includes('application/json')) {
8
+ try { json = await r.json(); } catch { json = null; }
9
+ } else {
10
+ try { text = await r.text(); } catch { text = ''; }
11
+ if (text) {
12
+ try { json = JSON.parse(text); } catch { json = null; }
13
+ }
14
+ }
15
+ if (!r.ok) {
16
+ const msg = (json && (json.error || json.message)) || text || `HTTP ${r.status}`;
17
+ const err = new Error(msg);
18
+ err.status = r.status;
19
+ err.payload = json;
20
+ throw err;
21
+ }
22
+ if (json !== null) return json;
23
+ return {};
24
+ };
25
+ const api = {
26
+ get: async (path) => parseApiResponse(await fetch(`${API}${path}`)),
27
+ post: async (path, body) => parseApiResponse(await fetch(`${API}${path}`, {
28
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify(body),
30
+ })),
31
+ put: async (path, body) => parseApiResponse(await fetch(`${API}${path}`, {
32
+ method: 'PUT', headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify(body),
34
+ })),
35
+ del: async (path) => parseApiResponse(await fetch(`${API}${path}`, { method: 'DELETE' })),
36
+ };
cli/ui/app.js ADDED
@@ -0,0 +1,207 @@
1
+ // ─── Main App ─────────────────────────────
2
+ const BUILD_TIME = "2026-04-10 UI-v2";
3
+ const { useState, useEffect, useCallback, useRef } = React;
4
+
5
+ const tabs = [
6
+ { id: "dashboard", label: "Dashboard", icon: "gauge" },
7
+ { id: "chat", label: "Chat", icon: "messageSquare" },
8
+ { id: "sessions", label: "Sessions", icon: "clock" },
9
+ { id: "memory", label: "Memory", icon: "bookmark" },
10
+ { id: "edits", label: "Edits", icon: "edit" },
11
+ { id: "instructions", label: "Instructions", icon: "fileText" },
12
+ { id: "settings", label: "Settings", icon: "settings" },
13
+ ];
14
+
15
+ function App() {
16
+ const [tab, setTab] = useState("dashboard");
17
+ const [stats, setStats] = useState({});
18
+ const [connected, setConnected] = useState(false);
19
+ const [loading, setLoading] = useState(true);
20
+ const [health, setHealth] = useState(null);
21
+ const [healthChecking, setHealthChecking] = useState(false);
22
+ const [notifications, setNotifications] = useState([]);
23
+ const [registry, setRegistry] = useState([]);
24
+
25
+ // Sidebar state
26
+ const [sidebarPinned, setSidebarPinned] = useState(() => {
27
+ try { return localStorage.getItem("c3-sidebar-pinned") === "true"; } catch { return true; }
28
+ });
29
+ const [sidebarHover, setSidebarHover] = useState(false);
30
+ const sidebarOpen = sidebarPinned || sidebarHover;
31
+
32
+ const toggleSidebarPin = () => {
33
+ const next = !sidebarPinned;
34
+ setSidebarPinned(next);
35
+ try { localStorage.setItem("c3-sidebar-pinned", String(next)); } catch { }
36
+ };
37
+
38
+ // Theme toggle
39
+ const [darkMode, setDarkMode] = useState(true);
40
+ T = darkMode ? DARK : LIGHT;
41
+ // Sync data-theme for CSS-based theming (code blocks, markdown)
42
+ React.useEffect(() => {
43
+ document.body.dataset.theme = darkMode ? 'dark' : 'light';
44
+ const dEl = document.getElementById('hljs-theme-dark');
45
+ const lEl = document.getElementById('hljs-theme-light');
46
+ if (dEl) dEl.disabled = !darkMode;
47
+ if (lEl) lEl.disabled = darkMode;
48
+ }, [darkMode]);
49
+
50
+ // Data loading
51
+ const loadHealth = useCallback(async () => {
52
+ setHealthChecking(true);
53
+ try { const h = await api.get('/api/health'); setHealth(h); } catch { }
54
+ setHealthChecking(false);
55
+ }, []);
56
+
57
+ const loadNotifications = useCallback(async () => {
58
+ try { const n = await api.get('/api/notifications'); setNotifications(Array.isArray(n) ? n : []); } catch { }
59
+ }, []);
60
+
61
+ const loadRegistry = useCallback(async () => {
62
+ try { const r = await api.get('/api/registry'); setRegistry(Array.isArray(r) ? r : []); } catch { }
63
+ }, []);
64
+
65
+ const ackNotification = async (id) => {
66
+ try { await api.post('/api/notifications/ack', { id }); } catch { }
67
+ loadNotifications();
68
+ };
69
+
70
+ const ackAllNotifications = async () => {
71
+ try { await api.post('/api/notifications/ack-all'); } catch { }
72
+ setNotifications([]);
73
+ };
74
+
75
+ // Single consolidated poll
76
+ useEffect(() => {
77
+ const load = async () => {
78
+ try {
79
+ const [s] = await Promise.all([api.get('/api/stats')]);
80
+ setStats(s);
81
+ setConnected(true);
82
+ } catch { setConnected(false); }
83
+ setLoading(false);
84
+ };
85
+ load();
86
+ loadHealth();
87
+ loadNotifications();
88
+ loadRegistry();
89
+ const iv = setInterval(() => {
90
+ load();
91
+ loadNotifications();
92
+ }, 15000);
93
+ const hv = setInterval(loadHealth, 30000);
94
+ const rv = setInterval(loadRegistry, 30000);
95
+ return () => { clearInterval(iv); clearInterval(hv); clearInterval(rv); };
96
+ }, [loadNotifications, loadHealth, loadRegistry]);
97
+
98
+ const renderPanel = () => {
99
+ // Use display:none instead of unmounting to preserve state
100
+ return (
101
+ <>
102
+ <div style={{ display: tab === "dashboard" ? "block" : "none", height: "100%" }}>
103
+ <Dashboard stats={stats} loading={loading} notifications={notifications}
104
+ ackNotification={ackNotification} ackAllNotifications={ackAllNotifications} />
105
+ </div>
106
+ <div style={{ display: tab === "chat" ? "flex" : "none", height: "100%", flexDirection: "column" }}>
107
+ <ChatPanel />
108
+ </div>
109
+ <div style={{ display: tab === "sessions" ? "block" : "none", height: "100%" }}>
110
+ <SessionsPanel />
111
+ </div>
112
+ <div style={{ display: tab === "memory" ? "block" : "none", height: "100%" }}>
113
+ <Memory />
114
+ </div>
115
+ <div style={{ display: tab === "edits" ? "block" : "none", height: "100%" }}>
116
+ <EditsPanel />
117
+ </div>
118
+ <div style={{ display: tab === "instructions" ? "block" : "none", height: "100%" }}>
119
+ <Instructions />
120
+ </div>
121
+ <div style={{ display: tab === "settings" ? "block" : "none", height: "100%" }}>
122
+ <SettingsPanel stats={stats} />
123
+ </div>
124
+ </>
125
+ );
126
+ };
127
+
128
+ return (
129
+ <div style={{ display: "flex", height: "100vh", width: "100vw", background: T.bg, overflow: "hidden" }}>
130
+ <Sidebar
131
+ tab={tab} setTab={setTab} tabs={tabs}
132
+ sidebarOpen={sidebarOpen} sidebarPinned={sidebarPinned}
133
+ toggleSidebarPin={toggleSidebarPin} setSidebarHover={setSidebarHover}
134
+ connected={connected} health={health} healthChecking={healthChecking}
135
+ loadHealth={loadHealth} registry={registry}
136
+ />
137
+
138
+ {/* Main content */}
139
+ <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
140
+ {/* Top bar */}
141
+ <div style={{
142
+ padding: "10px 20px", borderBottom: `1px solid ${T.border}`,
143
+ display: "flex", alignItems: "center", justifyContent: "space-between",
144
+ background: T.surface, flexShrink: 0
145
+ }}>
146
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
147
+ <I name={tabs.find(t => t.id === tab)?.icon || "gauge"} size={16} color={T.accent} />
148
+ <span style={{ fontSize: 14, fontWeight: 600, color: T.text }}>
149
+ {tabs.find(t => t.id === tab)?.label}
150
+ </span>
151
+ </div>
152
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
153
+ {notifications.length > 0 && (
154
+ <Badge color={T.warn}>{notifications.length} alerts</Badge>
155
+ )}
156
+ <button onClick={async () => {
157
+ try { const h = await api.get('/api/hub/info'); window.location.href = h.url; }
158
+ catch { window.location.href = 'http://localhost:3330'; }
159
+ }} title="Back to Projects Hub"
160
+ style={{
161
+ height: 28, padding: "0 10px", borderRadius: 6, border: `1px solid ${T.warn}40`,
162
+ background: `${T.warn}12`, color: T.warn, cursor: "pointer",
163
+ display: "flex", alignItems: "center", gap: 4, fontSize: 11,
164
+ fontFamily: "'JetBrains Mono', monospace", fontWeight: 600
165
+ }}>
166
+ Hub
167
+ </button>
168
+ <a href="/nano" title="Switch to Nano view"
169
+ style={{
170
+ height: 28, padding: "0 10px", borderRadius: 6, border: `1px solid ${T.accent}40`,
171
+ background: `${T.accent}12`, color: T.accent, textDecoration: "none",
172
+ display: "flex", alignItems: "center", gap: 4, fontSize: 11,
173
+ fontFamily: "'JetBrains Mono', monospace", fontWeight: 600
174
+ }}>
175
+ Nano
176
+ </a>
177
+ <a href="/edits" title="Edit Ledger — version timeline"
178
+ style={{
179
+ height: 28, padding: "0 10px", borderRadius: 6, border: `1px solid ${T.purple}40`,
180
+ background: `${T.purple}12`, color: T.purple, textDecoration: "none",
181
+ display: "flex", alignItems: "center", gap: 4, fontSize: 11,
182
+ fontFamily: "'JetBrains Mono', monospace", fontWeight: 600
183
+ }}>
184
+ Ledger
185
+ </a>
186
+ <button onClick={() => setDarkMode(!darkMode)} title="Toggle theme"
187
+ style={{
188
+ width: 28, height: 28, borderRadius: 6, border: `1px solid ${T.border}`,
189
+ background: "transparent", cursor: "pointer", display: "flex",
190
+ alignItems: "center", justifyContent: "center"
191
+ }}>
192
+ <I name={darkMode ? "sun" : "moon"} size={13} color={T.textMuted} />
193
+ </button>
194
+ <span className="mono" style={{ fontSize: 9, color: T.textDim }}>{BUILD_TIME}</span>
195
+ </div>
196
+ </div>
197
+
198
+ {/* Content area */}
199
+ <div style={{ flex: 1, overflow: "auto", padding: 20 }}>
200
+ {renderPanel()}
201
+ </div>
202
+ </div>
203
+ </div>
204
+ );
205
+ }
206
+
207
+ ReactDOM.render(<App />, document.getElementById("root"));