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
services/watcher.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File Watcher Service
|
|
3
|
+
|
|
4
|
+
Watches project files for changes and tracks modifications:
|
|
5
|
+
- Daemon thread monitors file system events
|
|
6
|
+
- Filters by code extensions, skips node_modules/.git/etc.
|
|
7
|
+
- Accumulates changes for session logging
|
|
8
|
+
- Triggers index rebuild when enough changes accumulate
|
|
9
|
+
"""
|
|
10
|
+
import threading
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from watchdog.events import FileSystemEventHandler
|
|
15
|
+
from watchdog.observers import Observer
|
|
16
|
+
|
|
17
|
+
# Extensions to watch
|
|
18
|
+
CODE_EXTENSIONS = {
|
|
19
|
+
'.py', '.js', '.ts', '.tsx', '.jsx', '.r', '.R',
|
|
20
|
+
'.css', '.html', '.json', '.yaml', '.yml', '.md',
|
|
21
|
+
'.sh', '.sql', '.go', '.rs', '.java', '.cpp', '.c', '.h',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Directories to skip
|
|
25
|
+
SKIP_DIRS = {
|
|
26
|
+
'node_modules', '.git', '__pycache__', '.c3', 'venv',
|
|
27
|
+
'env', '.venv', 'dist', 'build', '.next', '.cache',
|
|
28
|
+
'coverage', '.pytest_cache',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _ChangeHandler(FileSystemEventHandler):
|
|
33
|
+
"""Collects file change events."""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
super().__init__()
|
|
37
|
+
self._lock = threading.Lock()
|
|
38
|
+
self._changes = []
|
|
39
|
+
|
|
40
|
+
def _should_track(self, path: str) -> bool:
|
|
41
|
+
p = Path(path)
|
|
42
|
+
if any(skip in p.parts for skip in SKIP_DIRS):
|
|
43
|
+
return False
|
|
44
|
+
return p.suffix.lower() in CODE_EXTENSIONS
|
|
45
|
+
|
|
46
|
+
def _record(self, event_type: str, path: str):
|
|
47
|
+
if not self._should_track(path):
|
|
48
|
+
return
|
|
49
|
+
with self._lock:
|
|
50
|
+
self._changes.append({
|
|
51
|
+
"type": event_type,
|
|
52
|
+
"path": path,
|
|
53
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
def on_modified(self, event):
|
|
57
|
+
if not event.is_directory:
|
|
58
|
+
self._record("modified", event.src_path)
|
|
59
|
+
|
|
60
|
+
def on_created(self, event):
|
|
61
|
+
if not event.is_directory:
|
|
62
|
+
self._record("created", event.src_path)
|
|
63
|
+
|
|
64
|
+
def on_deleted(self, event):
|
|
65
|
+
if not event.is_directory:
|
|
66
|
+
self._record("deleted", event.src_path)
|
|
67
|
+
|
|
68
|
+
def on_moved(self, event):
|
|
69
|
+
if not event.is_directory:
|
|
70
|
+
self._record("moved", event.src_path)
|
|
71
|
+
|
|
72
|
+
def get_and_clear(self) -> list:
|
|
73
|
+
with self._lock:
|
|
74
|
+
changes = list(self._changes)
|
|
75
|
+
self._changes.clear()
|
|
76
|
+
return changes
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def change_count(self) -> int:
|
|
80
|
+
with self._lock:
|
|
81
|
+
return len(self._changes)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CodeWatcher:
|
|
85
|
+
"""Watches project files for changes on a daemon thread."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, project_path: str):
|
|
88
|
+
self.project_path = str(Path(project_path).resolve())
|
|
89
|
+
self._handler = _ChangeHandler()
|
|
90
|
+
self._observer = Observer()
|
|
91
|
+
self._observer.daemon = True
|
|
92
|
+
self._file_memory = None
|
|
93
|
+
self._compressor = None
|
|
94
|
+
self._worker_thread = None
|
|
95
|
+
self._stop_event = threading.Event()
|
|
96
|
+
self._update_queue = set()
|
|
97
|
+
self._queue_lock = threading.Lock()
|
|
98
|
+
|
|
99
|
+
def set_backends(self, file_memory, compressor, validation_cache=None):
|
|
100
|
+
"""Set backends for proactive background updates."""
|
|
101
|
+
self._file_memory = file_memory
|
|
102
|
+
self._compressor = compressor
|
|
103
|
+
self._validation_cache = validation_cache
|
|
104
|
+
|
|
105
|
+
def _background_worker(self):
|
|
106
|
+
import time
|
|
107
|
+
from pathlib import Path
|
|
108
|
+
# Debounce tracking: {abs_path: last_enqueue_time}
|
|
109
|
+
pending_validation: dict[str, float] = {}
|
|
110
|
+
while not self._stop_event.is_set():
|
|
111
|
+
time.sleep(1.0)
|
|
112
|
+
now = time.time()
|
|
113
|
+
paths_to_update = []
|
|
114
|
+
with self._queue_lock:
|
|
115
|
+
if self._update_queue:
|
|
116
|
+
paths_to_update = list(self._update_queue)
|
|
117
|
+
self._update_queue.clear()
|
|
118
|
+
|
|
119
|
+
for path in paths_to_update:
|
|
120
|
+
if self._stop_event.is_set():
|
|
121
|
+
break
|
|
122
|
+
try:
|
|
123
|
+
rel_path = str(Path(path).resolve().relative_to(self.project_path))
|
|
124
|
+
# Pre-emptively update structural map
|
|
125
|
+
if self._file_memory:
|
|
126
|
+
self._file_memory.update(rel_path)
|
|
127
|
+
# Pre-emptively compress
|
|
128
|
+
if self._compressor:
|
|
129
|
+
self._compressor.compress_file(str(Path(path)), "smart")
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
# Track for debounced validation
|
|
133
|
+
if self._validation_cache:
|
|
134
|
+
pending_validation[path] = now
|
|
135
|
+
|
|
136
|
+
# Run debounced validation for files that haven't changed recently
|
|
137
|
+
if self._validation_cache and pending_validation:
|
|
138
|
+
debounce = self._validation_cache.debounce_seconds
|
|
139
|
+
ready = [p for p, t in pending_validation.items() if now - t >= debounce]
|
|
140
|
+
for path in ready:
|
|
141
|
+
if self._stop_event.is_set():
|
|
142
|
+
break
|
|
143
|
+
pending_validation.pop(path, None)
|
|
144
|
+
try:
|
|
145
|
+
rel_path = str(Path(path).resolve().relative_to(self.project_path))
|
|
146
|
+
self._validation_cache.validate_file(rel_path)
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def start(self):
|
|
151
|
+
"""Start watching (non-blocking, daemon thread)."""
|
|
152
|
+
self._observer.schedule(self._handler, self.project_path, recursive=True)
|
|
153
|
+
self._observer.start()
|
|
154
|
+
|
|
155
|
+
# Start background worker for proactive mapping
|
|
156
|
+
self._worker_thread = threading.Thread(target=self._background_worker, daemon=True)
|
|
157
|
+
self._worker_thread.start()
|
|
158
|
+
|
|
159
|
+
def stop(self):
|
|
160
|
+
"""Stop watching."""
|
|
161
|
+
self._stop_event.set()
|
|
162
|
+
self._observer.stop()
|
|
163
|
+
self._observer.join(timeout=2)
|
|
164
|
+
if self._worker_thread:
|
|
165
|
+
self._worker_thread.join(timeout=2)
|
|
166
|
+
|
|
167
|
+
def get_changes(self) -> list:
|
|
168
|
+
"""Return accumulated changes and clear the buffer."""
|
|
169
|
+
changes = self._handler.get_and_clear()
|
|
170
|
+
|
|
171
|
+
# Enqueue modified files for background update
|
|
172
|
+
with self._queue_lock:
|
|
173
|
+
for c in changes:
|
|
174
|
+
if c["type"] in ("modified", "created"):
|
|
175
|
+
self._update_queue.add(c["path"])
|
|
176
|
+
elif c["type"] == "deleted" and self._validation_cache:
|
|
177
|
+
try:
|
|
178
|
+
rel = str(Path(c["path"]).resolve().relative_to(self.project_path))
|
|
179
|
+
self._validation_cache.evict(rel)
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
return changes
|
|
184
|
+
|
|
185
|
+
def rebuild_if_needed(self, indexer, threshold: int = 10) -> dict | None:
|
|
186
|
+
"""Trigger index rebuild if enough changes have accumulated."""
|
|
187
|
+
if self._handler.change_count >= threshold:
|
|
188
|
+
changes = self.get_changes()
|
|
189
|
+
result = indexer.build_index()
|
|
190
|
+
result["triggered_by_changes"] = len(changes)
|
|
191
|
+
return result
|
|
192
|
+
return None
|
tui/__init__.py
ADDED
|
File without changes
|
tui/backend.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_c3_path():
|
|
9
|
+
# Resolves to the root directory's cli/c3.py
|
|
10
|
+
current_dir = Path(__file__).resolve().parent
|
|
11
|
+
root_dir = current_dir.parent
|
|
12
|
+
c3_path = root_dir / "cli" / "c3.py"
|
|
13
|
+
return str(c3_path), str(root_dir)
|
|
14
|
+
async def run_cmd_async(*args):
|
|
15
|
+
"""Runs a c3 command asynchronously and returns stdout."""
|
|
16
|
+
c3_path, root_dir = get_c3_path()
|
|
17
|
+
|
|
18
|
+
env = os.environ.copy()
|
|
19
|
+
env["PYTHONPATH"] = root_dir
|
|
20
|
+
|
|
21
|
+
cmd = [sys.executable, c3_path] + list(args)
|
|
22
|
+
|
|
23
|
+
kwargs = {}
|
|
24
|
+
if sys.platform == "win32":
|
|
25
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
26
|
+
|
|
27
|
+
process = await asyncio.create_subprocess_exec(
|
|
28
|
+
*cmd,
|
|
29
|
+
stdout=asyncio.subprocess.PIPE,
|
|
30
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
31
|
+
env=env,
|
|
32
|
+
**kwargs
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
stdout, _ = await process.communicate()
|
|
36
|
+
return stdout.decode("utf-8", errors="replace")
|
|
37
|
+
|
|
38
|
+
def run_cmd(*args):
|
|
39
|
+
"""Runs a c3 command synchronously."""
|
|
40
|
+
c3_path, root_dir = get_c3_path()
|
|
41
|
+
|
|
42
|
+
env = os.environ.copy()
|
|
43
|
+
env["PYTHONPATH"] = root_dir
|
|
44
|
+
|
|
45
|
+
cmd = [sys.executable, c3_path] + list(args)
|
|
46
|
+
|
|
47
|
+
kwargs = {}
|
|
48
|
+
if sys.platform == "win32":
|
|
49
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
50
|
+
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
cmd,
|
|
53
|
+
stdout=subprocess.PIPE,
|
|
54
|
+
stderr=subprocess.STDOUT,
|
|
55
|
+
env=env,
|
|
56
|
+
text=True,
|
|
57
|
+
**kwargs
|
|
58
|
+
)
|
|
59
|
+
return result.stdout
|
tui/main.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from textual.app import App, ComposeResult
|
|
4
|
+
from textual.binding import Binding
|
|
5
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
6
|
+
from textual.widgets import Button, Footer, Header, Label, ListItem, ListView
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SidebarItem(ListItem):
|
|
10
|
+
def __init__(self, label: str, view_id: str, **kwargs):
|
|
11
|
+
super().__init__(**kwargs)
|
|
12
|
+
self.label = label
|
|
13
|
+
self.view_id = view_id
|
|
14
|
+
|
|
15
|
+
def compose(self) -> ComposeResult:
|
|
16
|
+
yield Label(self.label)
|
|
17
|
+
|
|
18
|
+
class C3App(App):
|
|
19
|
+
CSS_PATH = "theme.tcss"
|
|
20
|
+
BINDINGS = [
|
|
21
|
+
Binding("q", "quit", "Quit", show=True),
|
|
22
|
+
Binding("tab", "toggle_focus", "Focus: NAV/CONTENT", show=True),
|
|
23
|
+
Binding("ctrl+t", "toggle_theme", "Theme", show=True),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
def compose(self) -> ComposeResult:
|
|
27
|
+
from screens.stats import StatsWidget
|
|
28
|
+
yield Header(show_clock=True)
|
|
29
|
+
with Horizontal():
|
|
30
|
+
with Vertical(id="sidebar"):
|
|
31
|
+
yield ListView(
|
|
32
|
+
SidebarItem("Projects", "projects"),
|
|
33
|
+
SidebarItem("Index", "index"),
|
|
34
|
+
SidebarItem("Search", "search"),
|
|
35
|
+
SidebarItem("Compress", "compress"),
|
|
36
|
+
SidebarItem("Session", "session"),
|
|
37
|
+
SidebarItem("ClaudeMD", "claudemd"),
|
|
38
|
+
SidebarItem("Pipe", "pipe"),
|
|
39
|
+
SidebarItem("Optimize", "optimize"),
|
|
40
|
+
SidebarItem("Init", "init"),
|
|
41
|
+
SidebarItem("MCP", "mcp"),
|
|
42
|
+
SidebarItem("Benchmark", "benchmark"),
|
|
43
|
+
SidebarItem("Web UI", "web_ui"),
|
|
44
|
+
id="nav_list"
|
|
45
|
+
)
|
|
46
|
+
with Container(id="content"):
|
|
47
|
+
pass
|
|
48
|
+
with Vertical(id="right-sidebar"):
|
|
49
|
+
yield StatsWidget()
|
|
50
|
+
yield Button("Theme: Dark", id="theme_toggle_btn", classes="quick-btn")
|
|
51
|
+
yield Footer()
|
|
52
|
+
yield Label(f" {os.getcwd()} ", id="cwd_label")
|
|
53
|
+
|
|
54
|
+
def on_mount(self) -> None:
|
|
55
|
+
self.title = "C3 COMPANION"
|
|
56
|
+
self.sub_title = "v2.28.0 (Textual)"
|
|
57
|
+
self.theme_mode = "dark"
|
|
58
|
+
self.screen.add_class("theme-dark")
|
|
59
|
+
self._sync_theme_button()
|
|
60
|
+
|
|
61
|
+
# Open Projects hub by default
|
|
62
|
+
nav_list = self.query_one("#nav_list", ListView)
|
|
63
|
+
nav_list.index = 0
|
|
64
|
+
self.mount_view("projects", "Projects")
|
|
65
|
+
self.action_toggle_focus()
|
|
66
|
+
|
|
67
|
+
def action_toggle_focus(self) -> None:
|
|
68
|
+
nav_list = self.query_one("#nav_list")
|
|
69
|
+
if self.focused == nav_list:
|
|
70
|
+
self.query_one("#content").focus()
|
|
71
|
+
else:
|
|
72
|
+
nav_list.focus()
|
|
73
|
+
|
|
74
|
+
def action_toggle_theme(self) -> None:
|
|
75
|
+
if self.theme_mode == "dark":
|
|
76
|
+
self.theme_mode = "light"
|
|
77
|
+
self.screen.remove_class("theme-dark")
|
|
78
|
+
self.screen.add_class("theme-light")
|
|
79
|
+
else:
|
|
80
|
+
self.theme_mode = "dark"
|
|
81
|
+
self.screen.remove_class("theme-light")
|
|
82
|
+
self.screen.add_class("theme-dark")
|
|
83
|
+
self._sync_theme_button()
|
|
84
|
+
self.notify(f"{self.theme_mode.title()} mode enabled", timeout=1.5)
|
|
85
|
+
|
|
86
|
+
def _sync_theme_button(self) -> None:
|
|
87
|
+
self.query_one("#theme_toggle_btn", Button).label = (
|
|
88
|
+
f"Theme: {self.theme_mode.title()}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
|
92
|
+
item = event.item
|
|
93
|
+
if isinstance(item, SidebarItem):
|
|
94
|
+
self.mount_view(item.view_id, item.label)
|
|
95
|
+
|
|
96
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
97
|
+
if event.button.id == "theme_toggle_btn":
|
|
98
|
+
self.action_toggle_theme()
|
|
99
|
+
|
|
100
|
+
def mount_view(self, view_id: str, label: str) -> None:
|
|
101
|
+
content_container = self.query_one("#content")
|
|
102
|
+
# Remove existing view
|
|
103
|
+
content_container.query("*").remove()
|
|
104
|
+
|
|
105
|
+
# Mount new view
|
|
106
|
+
if view_id == "projects":
|
|
107
|
+
from screens.projects_view import ProjectsView
|
|
108
|
+
content_container.mount(ProjectsView())
|
|
109
|
+
elif view_id == "index":
|
|
110
|
+
from screens.index_view import IndexWidget
|
|
111
|
+
content_container.mount(IndexWidget())
|
|
112
|
+
elif view_id == "search":
|
|
113
|
+
from screens.search_view import SearchView
|
|
114
|
+
content_container.mount(SearchView())
|
|
115
|
+
elif view_id == "compress":
|
|
116
|
+
from screens.compress_view import CompressView
|
|
117
|
+
content_container.mount(CompressView())
|
|
118
|
+
elif view_id == "session":
|
|
119
|
+
from screens.session_view import SessionView
|
|
120
|
+
content_container.mount(SessionView())
|
|
121
|
+
elif view_id == "claudemd":
|
|
122
|
+
from screens.claudemd_view import ClaudeMDView
|
|
123
|
+
content_container.mount(ClaudeMDView())
|
|
124
|
+
elif view_id == "pipe":
|
|
125
|
+
from screens.pipe_view import PipeView
|
|
126
|
+
content_container.mount(PipeView())
|
|
127
|
+
elif view_id == "optimize":
|
|
128
|
+
from screens.optimize_view import OptimizeView
|
|
129
|
+
content_container.mount(OptimizeView())
|
|
130
|
+
elif view_id == "init":
|
|
131
|
+
from screens.init_view import InitView
|
|
132
|
+
content_container.mount(InitView())
|
|
133
|
+
elif view_id == "mcp":
|
|
134
|
+
from screens.mcp_view import MCPView
|
|
135
|
+
content_container.mount(MCPView())
|
|
136
|
+
elif view_id == "benchmark":
|
|
137
|
+
from screens.benchmark_view import BenchmarkView
|
|
138
|
+
content_container.mount(BenchmarkView())
|
|
139
|
+
elif view_id == "web_ui":
|
|
140
|
+
from screens.ui_view import UIView
|
|
141
|
+
content_container.mount(UIView())
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
app = C3App()
|
|
145
|
+
app.run()
|
tui/screens/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Package init
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from backend import get_c3_path
|
|
6
|
+
from screens.stats import Card
|
|
7
|
+
from textual import work
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.widgets import Button, Checkbox, Input, Label, Log
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BenchmarkView(Vertical):
|
|
14
|
+
def compose(self) -> ComposeResult:
|
|
15
|
+
with Card("Benchmark Configuration", ""):
|
|
16
|
+
yield Label("Runs a series of tests to measure C3 performance.", classes="card-title")
|
|
17
|
+
with Horizontal(classes="input-row"):
|
|
18
|
+
yield Label("Path:", classes="input-label")
|
|
19
|
+
yield Input(".", id="project_path")
|
|
20
|
+
yield Label("Sample Size:", classes="input-label")
|
|
21
|
+
yield Input("50", placeholder="e.g. 50", id="sample_size")
|
|
22
|
+
|
|
23
|
+
with Horizontal(classes="input-row"):
|
|
24
|
+
yield Label("Min Tokens:", classes="input-label")
|
|
25
|
+
yield Input("100", placeholder="e.g. 100", id="min_tokens")
|
|
26
|
+
yield Label("Top K:", classes="input-label")
|
|
27
|
+
yield Input("5", placeholder="e.g. 5", id="top_k")
|
|
28
|
+
|
|
29
|
+
with Horizontal(classes="input-row"):
|
|
30
|
+
yield Label("Max Tokens:", classes="input-label")
|
|
31
|
+
yield Input("10000", placeholder="e.g. 10000", id="max_tokens")
|
|
32
|
+
yield Label("System Name:", classes="input-label")
|
|
33
|
+
yield Input("c3", placeholder="e.g. codex", id="sys_name")
|
|
34
|
+
|
|
35
|
+
with Horizontal(classes="input-row"):
|
|
36
|
+
yield Label("Sys Label:", classes="input-label")
|
|
37
|
+
yield Input(placeholder="e.g. OpenAI", id="sys_label")
|
|
38
|
+
yield Label("Sys Version:", classes="input-label")
|
|
39
|
+
yield Input(placeholder="e.g. v1.0", id="sys_version")
|
|
40
|
+
|
|
41
|
+
with Horizontal(classes="input-row"):
|
|
42
|
+
yield Label("JSON Out:", classes="input-label")
|
|
43
|
+
yield Input(placeholder="out.json", id="out_json")
|
|
44
|
+
yield Label("HTML Out:", classes="input-label")
|
|
45
|
+
yield Input(placeholder="report.html", id="out_html")
|
|
46
|
+
|
|
47
|
+
with Horizontal(classes="action-row"):
|
|
48
|
+
yield Checkbox("Print JSON", id="json_check")
|
|
49
|
+
yield Checkbox("No HTML", id="no_html_check")
|
|
50
|
+
yield Button("Start Benchmark", id="run_btn", variant="primary")
|
|
51
|
+
|
|
52
|
+
with Card("Results", ""):
|
|
53
|
+
yield Log(id="output_log", highlight=True)
|
|
54
|
+
|
|
55
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
56
|
+
if event.button.id == "run_btn":
|
|
57
|
+
self.query_one("#output_log").clear()
|
|
58
|
+
self.query_one("#run_btn").disabled = True
|
|
59
|
+
|
|
60
|
+
# Gather all kwargs
|
|
61
|
+
args = []
|
|
62
|
+
|
|
63
|
+
# Options with values
|
|
64
|
+
val_map = {
|
|
65
|
+
"--sample-size": "sample_size",
|
|
66
|
+
"--min-tokens": "min_tokens",
|
|
67
|
+
"--top-k": "top_k",
|
|
68
|
+
"--max-tokens": "max_tokens",
|
|
69
|
+
"--system-name": "sys_name",
|
|
70
|
+
"--system-label": "sys_label",
|
|
71
|
+
"--system-version": "sys_version",
|
|
72
|
+
"--output": "out_json",
|
|
73
|
+
"--html-output": "out_html"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for flag, widget_id in val_map.items():
|
|
77
|
+
val = self.query_one(f"#{widget_id}").value
|
|
78
|
+
if val:
|
|
79
|
+
args.extend([flag, val])
|
|
80
|
+
|
|
81
|
+
# Flags
|
|
82
|
+
if self.query_one("#json_check").value:
|
|
83
|
+
args.append("--json")
|
|
84
|
+
if self.query_one("#no_html_check").value:
|
|
85
|
+
args.append("--no-html")
|
|
86
|
+
|
|
87
|
+
# Positional path
|
|
88
|
+
path = self.query_one("#project_path").value
|
|
89
|
+
if path:
|
|
90
|
+
args.append(path)
|
|
91
|
+
|
|
92
|
+
self.run_benchmark(args)
|
|
93
|
+
|
|
94
|
+
@work(exclusive=True)
|
|
95
|
+
async def run_benchmark(self, args: list) -> None:
|
|
96
|
+
c3_path, root_dir = get_c3_path()
|
|
97
|
+
cmd = [sys.executable, c3_path, "benchmark"] + args
|
|
98
|
+
env = os.environ.copy()
|
|
99
|
+
env["PYTHONPATH"] = root_dir
|
|
100
|
+
|
|
101
|
+
process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, env=env)
|
|
102
|
+
log_widget = self.query_one("#output_log")
|
|
103
|
+
while True:
|
|
104
|
+
line = await process.stdout.readline()
|
|
105
|
+
if not line: break
|
|
106
|
+
log_widget.write_line(line.decode(errors="replace").rstrip())
|
|
107
|
+
await process.wait()
|
|
108
|
+
|
|
109
|
+
self.query_one("#run_btn").disabled = False
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from backend import get_c3_path
|
|
6
|
+
from screens.stats import Card
|
|
7
|
+
from textual import work
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.widgets import Button, Label, Log
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClaudeMDView(Vertical):
|
|
14
|
+
def compose(self) -> ComposeResult:
|
|
15
|
+
with Card("Documentation Generator", ""):
|
|
16
|
+
yield Label("Generates or updates CLAUDE.md / GEMINI.md instructions.")
|
|
17
|
+
with Horizontal(classes="action-row"):
|
|
18
|
+
yield Button("Generate CLAUDE.md", id="gen_btn", variant="primary")
|
|
19
|
+
yield Button("Check Health", id="check_btn")
|
|
20
|
+
|
|
21
|
+
with Card("Process Output", ""):
|
|
22
|
+
yield Log(id="output_log", highlight=True)
|
|
23
|
+
|
|
24
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
25
|
+
log = self.query_one("#output_log")
|
|
26
|
+
log.clear()
|
|
27
|
+
|
|
28
|
+
if event.button.id == "gen_btn":
|
|
29
|
+
self.run_task("claudemd", "generate")
|
|
30
|
+
elif event.button.id == "check_btn":
|
|
31
|
+
self.run_task("claudemd", "check")
|
|
32
|
+
|
|
33
|
+
@work(exclusive=True)
|
|
34
|
+
async def run_task(self, *args) -> None:
|
|
35
|
+
c3_path, root_dir = get_c3_path()
|
|
36
|
+
cmd = [sys.executable, c3_path] + list(args)
|
|
37
|
+
env = os.environ.copy()
|
|
38
|
+
env["PYTHONPATH"] = root_dir
|
|
39
|
+
|
|
40
|
+
process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, env=env)
|
|
41
|
+
log_widget = self.query_one("#output_log")
|
|
42
|
+
while True:
|
|
43
|
+
line = await process.stdout.readline()
|
|
44
|
+
if not line: break
|
|
45
|
+
log_widget.write_line(line.decode(errors="replace").rstrip())
|
|
46
|
+
await process.wait()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from backend import get_c3_path
|
|
6
|
+
from screens.stats import Card
|
|
7
|
+
from textual import work
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.widgets import Button, Input, Label, Log, Select
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CompressView(Vertical):
|
|
14
|
+
def compose(self) -> ComposeResult:
|
|
15
|
+
with Card("Token Efficiency", ""):
|
|
16
|
+
with Horizontal(classes="input-row"):
|
|
17
|
+
yield Label("File Path: ", classes="input-label")
|
|
18
|
+
yield Input(placeholder="path/to/file.py", id="file_input")
|
|
19
|
+
with Horizontal(classes="input-row"):
|
|
20
|
+
yield Label("Mode: ", classes="input-label")
|
|
21
|
+
yield Select([("Smart", "smart"), ("Map", "map"), ("Dense Map", "dense_map"), ("Diff", "diff")], value="smart", id="mode_select")
|
|
22
|
+
with Horizontal(classes="action-row"):
|
|
23
|
+
yield Button("Compress File", id="run_btn", variant="primary")
|
|
24
|
+
|
|
25
|
+
with Card("Compressed Output", ""):
|
|
26
|
+
yield Log(id="output_log", highlight=True)
|
|
27
|
+
|
|
28
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
29
|
+
if event.button.id == "run_btn":
|
|
30
|
+
path = self.query_one("#file_input").value
|
|
31
|
+
mode = self.query_one("#mode_select").value
|
|
32
|
+
if not path: return
|
|
33
|
+
|
|
34
|
+
self.query_one("#output_log").clear()
|
|
35
|
+
self.query_one("#run_btn").disabled = True
|
|
36
|
+
self.run_compress(path, mode)
|
|
37
|
+
|
|
38
|
+
@work(exclusive=True)
|
|
39
|
+
async def run_compress(self, path: str, mode: str) -> None:
|
|
40
|
+
c3_path, root_dir = get_c3_path()
|
|
41
|
+
cmd = [sys.executable, c3_path, "compress", path, "--mode", mode]
|
|
42
|
+
env = os.environ.copy()
|
|
43
|
+
env["PYTHONPATH"] = root_dir
|
|
44
|
+
|
|
45
|
+
process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, env=env)
|
|
46
|
+
log_widget = self.query_one("#output_log")
|
|
47
|
+
while True:
|
|
48
|
+
line = await process.stdout.readline()
|
|
49
|
+
if not line: break
|
|
50
|
+
log_widget.write_line(line.decode(errors="replace").rstrip())
|
|
51
|
+
await process.wait()
|
|
52
|
+
self.query_one("#run_btn").disabled = False
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from backend import get_c3_path
|
|
6
|
+
from screens.stats import Card
|
|
7
|
+
from textual import work
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.widgets import Button, Input, Label, Log
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IndexWidget(Vertical):
|
|
14
|
+
def compose(self) -> ComposeResult:
|
|
15
|
+
with Card("Configuration", ""):
|
|
16
|
+
with Horizontal(id="index-config-row"):
|
|
17
|
+
yield Label("Max Files to Index: ", classes="input-label")
|
|
18
|
+
yield Input("500", placeholder="500", id="max_files_input")
|
|
19
|
+
with Horizontal(id="index-actions-row"):
|
|
20
|
+
yield Button("Start Indexing", id="start_btn", variant="primary")
|
|
21
|
+
yield Button("Stop", id="stop_btn", variant="error")
|
|
22
|
+
|
|
23
|
+
with Card("Status Output", ""):
|
|
24
|
+
yield Log(id="index_log", highlight=True)
|
|
25
|
+
|
|
26
|
+
def on_mount(self) -> None:
|
|
27
|
+
self.process = None
|
|
28
|
+
self.query_one("#stop_btn").disabled = True
|
|
29
|
+
|
|
30
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
31
|
+
if event.button.id == "start_btn":
|
|
32
|
+
max_files = self.query_one("#max_files_input").value or "500"
|
|
33
|
+
self.query_one("#index_log").clear()
|
|
34
|
+
self.query_one("#index_log").write_line(f"Starting indexer with max files: {max_files}...")
|
|
35
|
+
self.query_one("#start_btn").disabled = True
|
|
36
|
+
self.query_one("#stop_btn").disabled = False
|
|
37
|
+
self.run_indexer(max_files)
|
|
38
|
+
elif event.button.id == "stop_btn":
|
|
39
|
+
if self.process:
|
|
40
|
+
self.process.terminate()
|
|
41
|
+
self.query_one("#start_btn").disabled = False
|
|
42
|
+
self.query_one("#stop_btn").disabled = True
|
|
43
|
+
self.query_one("#index_log").write_line("[bold red]Indexing stopped by user.[/]")
|
|
44
|
+
|
|
45
|
+
@work(exclusive=True, thread=False)
|
|
46
|
+
async def run_indexer(self, max_files: str) -> None:
|
|
47
|
+
c3_path, root_dir = get_c3_path()
|
|
48
|
+
env = os.environ.copy()
|
|
49
|
+
env["PYTHONPATH"] = root_dir
|
|
50
|
+
|
|
51
|
+
cmd = [sys.executable, c3_path, "index", "--max-files", max_files]
|
|
52
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
53
|
+
*cmd,
|
|
54
|
+
stdout=asyncio.subprocess.PIPE,
|
|
55
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
56
|
+
env=env
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
log_widget = self.query_one("#index_log")
|
|
60
|
+
|
|
61
|
+
while True:
|
|
62
|
+
line = await self.process.stdout.readline()
|
|
63
|
+
if not line:
|
|
64
|
+
break
|
|
65
|
+
log_widget.write_line(line.decode(errors="replace").rstrip())
|
|
66
|
+
|
|
67
|
+
await self.process.wait()
|
|
68
|
+
|
|
69
|
+
# Only update if not already stopped/reset by the Stop button
|
|
70
|
+
if self.process is not None:
|
|
71
|
+
self.query_one("#start_btn").disabled = False
|
|
72
|
+
self.query_one("#stop_btn").disabled = True
|
|
73
|
+
log_widget.write_line("[bold green]Indexing completed.[/]")
|
|
74
|
+
self.process = None
|