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
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()
@@ -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