code-context-control 2.28.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/__init__.py +1 -0
- cli/_hook_utils.py +99 -0
- cli/c3.py +6152 -0
- cli/commands/__init__.py +1 -0
- cli/commands/common.py +312 -0
- cli/commands/parser.py +286 -0
- cli/docs.html +3178 -0
- cli/edits.html +878 -0
- cli/hook_auto_snapshot.py +142 -0
- cli/hook_c3_signal.py +61 -0
- cli/hook_c3read.py +116 -0
- cli/hook_edit_ledger.py +213 -0
- cli/hook_edit_unlock.py +170 -0
- cli/hook_filter.py +130 -0
- cli/hook_ghost_files.py +238 -0
- cli/hook_pretool_enforce.py +334 -0
- cli/hook_read.py +200 -0
- cli/hook_session_stats.py +62 -0
- cli/hook_terse_advisor.py +190 -0
- cli/hub.html +3764 -0
- cli/hub_server.py +1619 -0
- cli/mcp_proxy.py +428 -0
- cli/mcp_server.py +660 -0
- cli/server.py +2985 -0
- cli/tools/__init__.py +4 -0
- cli/tools/_helpers.py +65 -0
- cli/tools/agent.py +1165 -0
- cli/tools/compress.py +215 -0
- cli/tools/delegate.py +1184 -0
- cli/tools/edit.py +313 -0
- cli/tools/edits.py +118 -0
- cli/tools/filter.py +285 -0
- cli/tools/impact.py +163 -0
- cli/tools/memory.py +469 -0
- cli/tools/read.py +224 -0
- cli/tools/search.py +337 -0
- cli/tools/session.py +95 -0
- cli/tools/shell.py +193 -0
- cli/tools/status.py +306 -0
- cli/tools/validate.py +310 -0
- cli/ui/api.js +36 -0
- cli/ui/app.js +207 -0
- cli/ui/components/chat.js +758 -0
- cli/ui/components/dashboard.js +689 -0
- cli/ui/components/edits.js +220 -0
- cli/ui/components/instructions.js +481 -0
- cli/ui/components/memory.js +626 -0
- cli/ui/components/sessions.js +606 -0
- cli/ui/components/settings.js +1404 -0
- cli/ui/components/sidebar.js +156 -0
- cli/ui/icons.js +51 -0
- cli/ui/shared.js +119 -0
- cli/ui/theme.js +22 -0
- cli/ui.html +168 -0
- cli/ui_legacy.html +6797 -0
- cli/ui_nano.html +503 -0
- code_context_control-2.28.0.dist-info/METADATA +248 -0
- code_context_control-2.28.0.dist-info/RECORD +150 -0
- code_context_control-2.28.0.dist-info/WHEEL +5 -0
- code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
- code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
- code_context_control-2.28.0.dist-info/top_level.txt +5 -0
- core/__init__.py +75 -0
- core/config.py +269 -0
- core/ide.py +188 -0
- oracle/__init__.py +1 -0
- oracle/config.py +75 -0
- oracle/oracle.html +3900 -0
- oracle/oracle_server.py +663 -0
- oracle/services/__init__.py +1 -0
- oracle/services/c3_bridge.py +210 -0
- oracle/services/chat_engine.py +1103 -0
- oracle/services/chat_store.py +155 -0
- oracle/services/cross_memory.py +154 -0
- oracle/services/federated_graph.py +463 -0
- oracle/services/health_checker.py +117 -0
- oracle/services/insight_engine.py +307 -0
- oracle/services/memory_reader.py +106 -0
- oracle/services/memory_writer.py +182 -0
- oracle/services/ollama_bridge.py +332 -0
- oracle/services/project_scanner.py +87 -0
- oracle/services/review_agent.py +206 -0
- services/__init__.py +1 -0
- services/activity_log.py +93 -0
- services/agent_base.py +124 -0
- services/agents.py +1529 -0
- services/auto_memory.py +407 -0
- services/bench/__init__.py +6 -0
- services/bench/external/__init__.py +29 -0
- services/bench/external/aider_polyglot.py +405 -0
- services/bench/external/swe_bench.py +485 -0
- services/benchmark_dashboard.py +596 -0
- services/claude_md.py +785 -0
- services/compressor.py +592 -0
- services/context_snapshot.py +356 -0
- services/conversation_store.py +870 -0
- services/doc_index.py +537 -0
- services/e2e_benchmark.py +2884 -0
- services/e2e_evaluator.py +396 -0
- services/e2e_tasks.py +743 -0
- services/edit_ledger.py +459 -0
- services/embedding_index.py +341 -0
- services/error_reporting.py +123 -0
- services/file_memory.py +734 -0
- services/hub_service.py +585 -0
- services/indexer.py +712 -0
- services/memory.py +318 -0
- services/memory_consolidator.py +538 -0
- services/memory_graph.py +382 -0
- services/memory_grounder.py +304 -0
- services/memory_scorer.py +246 -0
- services/metrics.py +86 -0
- services/notifications.py +209 -0
- services/ollama_client.py +201 -0
- services/output_filter.py +488 -0
- services/parser.py +1238 -0
- services/project_manager.py +579 -0
- services/protocol.py +306 -0
- services/proxy_state.py +152 -0
- services/retrieval_broker.py +129 -0
- services/router.py +414 -0
- services/runtime.py +326 -0
- services/session_benchmark.py +1945 -0
- services/session_manager.py +1026 -0
- services/session_preloader.py +251 -0
- services/text_index.py +90 -0
- services/tool_classifier.py +176 -0
- services/transcript_index.py +340 -0
- services/validation_cache.py +155 -0
- services/vector_store.py +299 -0
- services/version_tracker.py +271 -0
- services/watcher.py +192 -0
- tui/__init__.py +0 -0
- tui/backend.py +59 -0
- tui/main.py +145 -0
- tui/screens/__init__.py +1 -0
- tui/screens/benchmark_view.py +109 -0
- tui/screens/claudemd_view.py +46 -0
- tui/screens/compress_view.py +52 -0
- tui/screens/index_view.py +74 -0
- tui/screens/init_view.py +82 -0
- tui/screens/mcp_view.py +73 -0
- tui/screens/optimize_view.py +41 -0
- tui/screens/pipe_view.py +46 -0
- tui/screens/projects_view.py +355 -0
- tui/screens/search_view.py +55 -0
- tui/screens/session_view.py +143 -0
- tui/screens/stats.py +158 -0
- tui/screens/ui_view.py +54 -0
- tui/theme.tcss +335 -0
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"));
|