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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from screens.stats import Card
|
|
5
|
+
from textual import work
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.containers import Vertical
|
|
8
|
+
from textual.widgets import Button, Log
|
|
9
|
+
|
|
10
|
+
from services.activity_log import ActivityLog
|
|
11
|
+
from services.session_manager import SessionManager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SessionView(Vertical):
|
|
15
|
+
def compose(self) -> ComposeResult:
|
|
16
|
+
with Card("Recent Sessions", ""):
|
|
17
|
+
yield Log(id="session_list_log", highlight=True)
|
|
18
|
+
yield Button("Refresh List", id="refresh_btn")
|
|
19
|
+
|
|
20
|
+
with Card("Active Session Log", ""):
|
|
21
|
+
yield Log(id="active_log", highlight=True)
|
|
22
|
+
|
|
23
|
+
def on_mount(self) -> None:
|
|
24
|
+
self.load_sessions()
|
|
25
|
+
self.set_interval(5.0, self.load_sessions)
|
|
26
|
+
|
|
27
|
+
@work(exclusive=True, thread=True)
|
|
28
|
+
def load_sessions(self) -> None:
|
|
29
|
+
project_path = Path.cwd()
|
|
30
|
+
session_mgr = SessionManager(str(project_path))
|
|
31
|
+
sessions = session_mgr.list_sessions(10)
|
|
32
|
+
current = self._current_session(project_path)
|
|
33
|
+
self.app.call_from_thread(self._render_sessions, sessions, current)
|
|
34
|
+
|
|
35
|
+
def _current_session(self, project_path: Path) -> dict | None:
|
|
36
|
+
activity = ActivityLog(str(project_path))
|
|
37
|
+
starts = activity.get_recent(limit=1, event_type="session_start")
|
|
38
|
+
if not starts:
|
|
39
|
+
return None
|
|
40
|
+
start_event = starts[0]
|
|
41
|
+
session_id = start_event.get("session_id")
|
|
42
|
+
started = start_event.get("timestamp")
|
|
43
|
+
if not session_id or not started:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
saves = activity.get_recent(limit=1, event_type="session_save")
|
|
47
|
+
if saves and saves[0].get("session_id") == session_id:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
events = activity.get_recent(limit=200, since=started)
|
|
51
|
+
tool_calls = [e for e in events if e.get("type") == "tool_call"]
|
|
52
|
+
decisions = [e for e in events if e.get("type") == "decision"]
|
|
53
|
+
files = [e for e in events if e.get("type") == "file_change"]
|
|
54
|
+
last_event = events[0] if events else start_event
|
|
55
|
+
try:
|
|
56
|
+
started_dt = datetime.fromisoformat(started)
|
|
57
|
+
duration = datetime.now(timezone.utc) - started_dt
|
|
58
|
+
duration_seconds = max(0, int(duration.total_seconds()))
|
|
59
|
+
except Exception:
|
|
60
|
+
duration_seconds = 0
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"id": session_id,
|
|
64
|
+
"started": started,
|
|
65
|
+
"description": start_event.get("description", ""),
|
|
66
|
+
"source_system": start_event.get("source_system", ""),
|
|
67
|
+
"source_ide": start_event.get("source_ide", ""),
|
|
68
|
+
"tool_calls": len(tool_calls),
|
|
69
|
+
"decisions": len(decisions),
|
|
70
|
+
"files": len(files),
|
|
71
|
+
"last_activity": last_event.get("timestamp", started),
|
|
72
|
+
"recent_events": list(reversed(events[:12])),
|
|
73
|
+
"duration": self._format_duration(duration_seconds),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def _render_sessions(self, sessions: list, current: dict | None) -> None:
|
|
77
|
+
session_log = self.query_one("#session_list_log", Log)
|
|
78
|
+
active_log = self.query_one("#active_log", Log)
|
|
79
|
+
session_log.clear()
|
|
80
|
+
active_log.clear()
|
|
81
|
+
|
|
82
|
+
if current:
|
|
83
|
+
session_log.write_line(
|
|
84
|
+
f"LIVE {current['id']} {current['duration']} "
|
|
85
|
+
f"{current['tool_calls']} tools {current['decisions']} decisions {current['files']} files"
|
|
86
|
+
)
|
|
87
|
+
if current.get("description"):
|
|
88
|
+
session_log.write_line(f" {current['description']}")
|
|
89
|
+
session_log.write_line("")
|
|
90
|
+
active_log.write_line(f"Session: {current['id']}")
|
|
91
|
+
active_log.write_line(f"Started: {current['started']}")
|
|
92
|
+
active_log.write_line(f"Last activity: {current['last_activity']}")
|
|
93
|
+
active_log.write_line(
|
|
94
|
+
f"Source: {current.get('source_system') or '-'} / {current.get('source_ide') or '-'}"
|
|
95
|
+
)
|
|
96
|
+
active_log.write_line(
|
|
97
|
+
f"Activity: {current['tool_calls']} tools, {current['decisions']} decisions, {current['files']} files"
|
|
98
|
+
)
|
|
99
|
+
if current.get("description"):
|
|
100
|
+
active_log.write_line(f"Description: {current['description']}")
|
|
101
|
+
active_log.write_line("")
|
|
102
|
+
active_log.write_line("Recent events:")
|
|
103
|
+
for event in current["recent_events"]:
|
|
104
|
+
label = event.get("type", "event")
|
|
105
|
+
detail = (
|
|
106
|
+
event.get("tool")
|
|
107
|
+
or event.get("decision")
|
|
108
|
+
or event.get("file")
|
|
109
|
+
or event.get("data")
|
|
110
|
+
or ""
|
|
111
|
+
)
|
|
112
|
+
active_log.write_line(f"{event.get('timestamp', '')} {label} {detail}")
|
|
113
|
+
else:
|
|
114
|
+
active_log.write_line("No live session detected.")
|
|
115
|
+
|
|
116
|
+
if sessions:
|
|
117
|
+
if current:
|
|
118
|
+
session_log.write_line("Saved sessions:")
|
|
119
|
+
for session in sessions:
|
|
120
|
+
status = "DONE" if session.get("ended") else "IDLE"
|
|
121
|
+
summary = session.get("summary") or session.get("description") or ""
|
|
122
|
+
session_log.write_line(
|
|
123
|
+
f"{status:<5} {session.get('id', '')} "
|
|
124
|
+
f"{session.get('duration') or '-':<8} "
|
|
125
|
+
f"{session.get('tool_calls', 0)} tools "
|
|
126
|
+
f"{summary[:80]}"
|
|
127
|
+
)
|
|
128
|
+
elif not current:
|
|
129
|
+
session_log.write_line("No session history found.")
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def _format_duration(seconds: int) -> str:
|
|
133
|
+
if seconds < 60:
|
|
134
|
+
return f"{seconds}s"
|
|
135
|
+
minutes, secs = divmod(seconds, 60)
|
|
136
|
+
if minutes < 60:
|
|
137
|
+
return f"{minutes}m {secs}s" if secs else f"{minutes}m"
|
|
138
|
+
hours, mins = divmod(minutes, 60)
|
|
139
|
+
return f"{hours}h {mins}m" if mins else f"{hours}h"
|
|
140
|
+
|
|
141
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
142
|
+
if event.button.id == "refresh_btn":
|
|
143
|
+
self.load_sessions()
|
tui/screens/stats.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import socket
|
|
3
|
+
import sys
|
|
4
|
+
import webbrowser
|
|
5
|
+
|
|
6
|
+
from backend import get_c3_path
|
|
7
|
+
from textual import work
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Vertical
|
|
10
|
+
from textual.widgets import Button, DataTable, Label, Static
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Card(Vertical):
|
|
14
|
+
def __init__(self, title: str, content: str, **kwargs):
|
|
15
|
+
super().__init__(**kwargs)
|
|
16
|
+
self.card_title = title
|
|
17
|
+
self.card_content = content
|
|
18
|
+
self.add_class("card")
|
|
19
|
+
|
|
20
|
+
def compose(self) -> ComposeResult:
|
|
21
|
+
yield Label(self.card_title, classes="card-title")
|
|
22
|
+
if self.card_content:
|
|
23
|
+
yield Static(self.card_content)
|
|
24
|
+
|
|
25
|
+
class StatsWidget(Vertical):
|
|
26
|
+
def compose(self) -> ComposeResult:
|
|
27
|
+
with Card("Quick Actions", ""):
|
|
28
|
+
yield Button("Project Init", id="quick_init_btn", variant="primary", classes="quick-btn")
|
|
29
|
+
yield Button("MCP Install", id="quick_mcp_btn", variant="primary", classes="quick-btn")
|
|
30
|
+
yield Button("Refresh Stats", id="refresh_stats_btn", classes="quick-btn")
|
|
31
|
+
|
|
32
|
+
yield Label("System Metrics", classes="card-title")
|
|
33
|
+
yield DataTable(id="stats-table")
|
|
34
|
+
yield Button("Web UI Status: Checking...", id="web_ui_status_btn", classes="ui-btn-offline")
|
|
35
|
+
yield Button("Terminate UI", id="terminate_ui_btn", variant="error", classes="quick-btn")
|
|
36
|
+
|
|
37
|
+
def on_mount(self) -> None:
|
|
38
|
+
table = self.query_one("#stats-table", DataTable)
|
|
39
|
+
table.add_columns("Component", "Metric", "Value")
|
|
40
|
+
table.cursor_type = "row"
|
|
41
|
+
table.zebra_stripes = True
|
|
42
|
+
|
|
43
|
+
self.query_one("#terminate_ui_btn").display = False
|
|
44
|
+
|
|
45
|
+
self.load_stats()
|
|
46
|
+
self.check_ui_status()
|
|
47
|
+
self.set_interval(5.0, self.check_ui_status)
|
|
48
|
+
|
|
49
|
+
@work(exclusive=True, thread=True)
|
|
50
|
+
def check_ui_status(self) -> None:
|
|
51
|
+
port = 3333
|
|
52
|
+
is_open = False
|
|
53
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
54
|
+
s.settimeout(0.5)
|
|
55
|
+
is_open = (s.connect_ex(('localhost', port)) == 0)
|
|
56
|
+
|
|
57
|
+
self.app.call_from_thread(self._update_ui_btn, is_open, port)
|
|
58
|
+
|
|
59
|
+
def _update_ui_btn(self, is_open: bool, port: int) -> None:
|
|
60
|
+
btn = self.query_one("#web_ui_status_btn", Button)
|
|
61
|
+
term_btn = self.query_one("#terminate_ui_btn", Button)
|
|
62
|
+
if is_open:
|
|
63
|
+
btn.label = f"Web UI: Running (Port {port})"
|
|
64
|
+
btn.remove_class("ui-btn-offline")
|
|
65
|
+
btn.add_class("ui-btn-online")
|
|
66
|
+
term_btn.display = True
|
|
67
|
+
self._current_ui_url = f"http://localhost:{port}"
|
|
68
|
+
else:
|
|
69
|
+
btn.label = "Web UI: Offline"
|
|
70
|
+
btn.remove_class("ui-btn-online")
|
|
71
|
+
btn.add_class("ui-btn-offline")
|
|
72
|
+
term_btn.display = False
|
|
73
|
+
self._current_ui_url = None
|
|
74
|
+
|
|
75
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
76
|
+
if event.button.id == "web_ui_status_btn" and getattr(self, "_current_ui_url", None):
|
|
77
|
+
webbrowser.open(self._current_ui_url)
|
|
78
|
+
elif event.button.id == "terminate_ui_btn":
|
|
79
|
+
self.terminate_web_ui()
|
|
80
|
+
elif event.button.id == "refresh_stats_btn":
|
|
81
|
+
table = self.query_one("#stats-table", DataTable)
|
|
82
|
+
table.clear()
|
|
83
|
+
self.load_stats()
|
|
84
|
+
elif event.button.id == "quick_init_btn":
|
|
85
|
+
self.app.mount_view("init", "Init")
|
|
86
|
+
self.app.query_one("#nav_list").index = 9
|
|
87
|
+
elif event.button.id == "quick_mcp_btn":
|
|
88
|
+
self.app.mount_view("mcp", "MCP")
|
|
89
|
+
self.app.query_one("#nav_list").index = 10
|
|
90
|
+
|
|
91
|
+
@work(exclusive=True, thread=True)
|
|
92
|
+
def terminate_web_ui(self) -> None:
|
|
93
|
+
port = 3333
|
|
94
|
+
try:
|
|
95
|
+
import subprocess
|
|
96
|
+
kwargs = {}
|
|
97
|
+
if sys.platform == "win32":
|
|
98
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
99
|
+
if sys.platform == "win32":
|
|
100
|
+
result = subprocess.run(f"netstat -ano | findstr :{port}", shell=True, capture_output=True, text=True, **kwargs)
|
|
101
|
+
lines = result.stdout.strip().split('\n')
|
|
102
|
+
pids = set()
|
|
103
|
+
for line in lines:
|
|
104
|
+
if f":{port}" in line and "LISTENING" in line:
|
|
105
|
+
parts = line.strip().split()
|
|
106
|
+
if len(parts) >= 5:
|
|
107
|
+
pids.add(parts[-1])
|
|
108
|
+
for pid in pids:
|
|
109
|
+
subprocess.run(f"taskkill /PID {pid} /F", shell=True, capture_output=True, **kwargs)
|
|
110
|
+
else:
|
|
111
|
+
subprocess.run(f"lsof -ti:{port} | xargs kill -9", shell=True, capture_output=True)
|
|
112
|
+
self.check_ui_status()
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
@work(exclusive=True, thread=True)
|
|
117
|
+
def load_stats(self) -> None:
|
|
118
|
+
try:
|
|
119
|
+
import subprocess
|
|
120
|
+
c3_path, root_dir = get_c3_path()
|
|
121
|
+
env = os.environ.copy()
|
|
122
|
+
env["PYTHONPATH"] = root_dir
|
|
123
|
+
env["NO_COLOR"] = "1"
|
|
124
|
+
env["TERM"] = "dumb"
|
|
125
|
+
|
|
126
|
+
kwargs = {}
|
|
127
|
+
if sys.platform == "win32":
|
|
128
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
129
|
+
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
[sys.executable, c3_path, "stats"],
|
|
132
|
+
stdout=subprocess.PIPE,
|
|
133
|
+
stderr=subprocess.STDOUT,
|
|
134
|
+
env=env,
|
|
135
|
+
text=True,
|
|
136
|
+
encoding="utf-8",
|
|
137
|
+
errors="replace",
|
|
138
|
+
**kwargs
|
|
139
|
+
)
|
|
140
|
+
self.app.call_from_thread(self._render_stats, result.stdout)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
def _render_stats(self, output: str) -> None:
|
|
145
|
+
try:
|
|
146
|
+
table = self.query_one("#stats-table", DataTable)
|
|
147
|
+
table.clear()
|
|
148
|
+
|
|
149
|
+
for line in output.splitlines():
|
|
150
|
+
if not line.strip() or line.startswith("+") or line.startswith("System") or "C3 Statistics" in line:
|
|
151
|
+
continue
|
|
152
|
+
parts = [p.strip() for p in line.split("|")]
|
|
153
|
+
parts = [p for p in parts if p] # Remove empty splits
|
|
154
|
+
|
|
155
|
+
if len(parts) >= 3 and parts[0] != "Component" and not parts[0].startswith("-"):
|
|
156
|
+
table.add_row(parts[0], parts[1], parts[2])
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
tui/screens/ui_view.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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 UIView(Vertical):
|
|
14
|
+
def compose(self) -> ComposeResult:
|
|
15
|
+
with Card("Web Dashboard Control", ""):
|
|
16
|
+
with Horizontal(classes="input-row"):
|
|
17
|
+
yield Label("Port: ", classes="input-label")
|
|
18
|
+
yield Input("3333", id="port_input")
|
|
19
|
+
with Horizontal(classes="input-row"):
|
|
20
|
+
yield Checkbox("Nano Mode", id="nano_check")
|
|
21
|
+
with Horizontal(classes="action-row"):
|
|
22
|
+
yield Button("Launch Web UI", id="run_btn", variant="primary")
|
|
23
|
+
|
|
24
|
+
with Card("Server Status", ""):
|
|
25
|
+
yield Log(id="output_log", highlight=True)
|
|
26
|
+
|
|
27
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
28
|
+
if event.button.id == "run_btn":
|
|
29
|
+
port = self.query_one("#port_input").value or "3333"
|
|
30
|
+
nano = self.query_one("#nano_check").value
|
|
31
|
+
self.query_one("#output_log").clear()
|
|
32
|
+
self.launch_ui(port, nano)
|
|
33
|
+
|
|
34
|
+
@work(exclusive=True)
|
|
35
|
+
async def launch_ui(self, port: str, nano: bool) -> None:
|
|
36
|
+
c3_path, root_dir = get_c3_path()
|
|
37
|
+
args = ["ui", "--port", port]
|
|
38
|
+
if nano: args.append("--nano")
|
|
39
|
+
args.append("--no-browser") # TUI handle manually or just show URL
|
|
40
|
+
|
|
41
|
+
cmd = [sys.executable, c3_path] + args
|
|
42
|
+
env = os.environ.copy()
|
|
43
|
+
env["PYTHONPATH"] = root_dir
|
|
44
|
+
|
|
45
|
+
log_widget = self.query_one("#output_log")
|
|
46
|
+
log_widget.write_line(f"Starting Web UI on http://localhost:{port}...")
|
|
47
|
+
|
|
48
|
+
# We don't wait for this one to finish as it's a server
|
|
49
|
+
process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, env=env)
|
|
50
|
+
|
|
51
|
+
while True:
|
|
52
|
+
line = await process.stdout.readline()
|
|
53
|
+
if not line: break
|
|
54
|
+
log_widget.write_line(line.decode(errors="replace").rstrip())
|
tui/theme.tcss
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
Screen.theme-dark {
|
|
2
|
+
background: #2E3440;
|
|
3
|
+
color: #ECEFF4;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
Screen.theme-light {
|
|
7
|
+
background: #F4F1EA;
|
|
8
|
+
color: #2A2118;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
Screen.theme-dark #sidebar {
|
|
12
|
+
width: 20;
|
|
13
|
+
dock: left;
|
|
14
|
+
height: 100%;
|
|
15
|
+
background: #242933;
|
|
16
|
+
border-right: solid #3B4252;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Screen.theme-light #sidebar {
|
|
20
|
+
width: 20;
|
|
21
|
+
dock: left;
|
|
22
|
+
height: 100%;
|
|
23
|
+
background: #E8E1D5;
|
|
24
|
+
border-right: solid #C9B9A3;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#nav_list {
|
|
28
|
+
background: transparent;
|
|
29
|
+
border: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#nav_list > ListItem {
|
|
33
|
+
padding: 0 1;
|
|
34
|
+
background: transparent;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Screen.theme-dark #nav_list > ListItem:hover {
|
|
38
|
+
background: #3B4252;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Screen.theme-light #nav_list > ListItem:hover {
|
|
42
|
+
background: #D5C9B8;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Screen.theme-dark #nav_list > ListItem.-active {
|
|
46
|
+
background: #434C5E;
|
|
47
|
+
color: #88C0D0;
|
|
48
|
+
text-style: bold;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Screen.theme-light #nav_list > ListItem.-active {
|
|
52
|
+
background: #C4B29A;
|
|
53
|
+
color: #0F766E;
|
|
54
|
+
text-style: bold;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#content {
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: 100%;
|
|
60
|
+
padding: 0 1;
|
|
61
|
+
overflow-y: auto;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Screen.theme-dark .card {
|
|
65
|
+
border: solid #3B4252;
|
|
66
|
+
padding: 0 1;
|
|
67
|
+
margin-bottom: 0;
|
|
68
|
+
background: #2E3440;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Screen.theme-light .card {
|
|
72
|
+
border: solid #C9B9A3;
|
|
73
|
+
padding: 0 1;
|
|
74
|
+
margin-bottom: 0;
|
|
75
|
+
background: #F4F1EA;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Screen.theme-dark .card-title {
|
|
79
|
+
color: #88C0D0;
|
|
80
|
+
text-style: bold;
|
|
81
|
+
margin-bottom: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Screen.theme-light .card-title {
|
|
85
|
+
color: #0F766E;
|
|
86
|
+
text-style: bold;
|
|
87
|
+
margin-bottom: 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Screen.theme-dark Header {
|
|
91
|
+
background: #242933;
|
|
92
|
+
color: #88C0D0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Screen.theme-light Header {
|
|
96
|
+
background: #E8E1D5;
|
|
97
|
+
color: #0F766E;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Screen.theme-dark Footer {
|
|
101
|
+
background: #242933;
|
|
102
|
+
color: #4C566A;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Screen.theme-light Footer {
|
|
106
|
+
background: #E8E1D5;
|
|
107
|
+
color: #756250;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Screen.theme-dark Footer > .footer--key {
|
|
111
|
+
color: #88C0D0;
|
|
112
|
+
text-style: bold;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Screen.theme-light Footer > .footer--key {
|
|
116
|
+
color: #0F766E;
|
|
117
|
+
text-style: bold;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
Screen.theme-dark LoadingIndicator {
|
|
121
|
+
color: #88C0D0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
Screen.theme-light LoadingIndicator {
|
|
125
|
+
color: #0F766E;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.input-row { height: auto; min-height: 1; margin-bottom: 1; }
|
|
129
|
+
.action-row { height: 1; margin-bottom: 1; }
|
|
130
|
+
#index-config-row { height: 1; margin-bottom: 1; }
|
|
131
|
+
#index-actions-row { height: 1; margin-bottom: 1; }
|
|
132
|
+
|
|
133
|
+
.input-label { padding-top: 0; margin-right: 1; }
|
|
134
|
+
|
|
135
|
+
Screen.theme-dark Input {
|
|
136
|
+
width: 20;
|
|
137
|
+
background: #3B4252;
|
|
138
|
+
border: none;
|
|
139
|
+
height: 1;
|
|
140
|
+
min-height: 1;
|
|
141
|
+
padding: 0 1;
|
|
142
|
+
color: #ECEFF4;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Screen.theme-light Input {
|
|
146
|
+
width: 20;
|
|
147
|
+
background: #D5C9B8;
|
|
148
|
+
border: none;
|
|
149
|
+
height: 1;
|
|
150
|
+
min-height: 1;
|
|
151
|
+
padding: 0 1;
|
|
152
|
+
color: #2A2118;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Screen.theme-dark Input:focus {
|
|
156
|
+
background: #434C5E;
|
|
157
|
+
color: #88C0D0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Screen.theme-light Input:focus {
|
|
161
|
+
background: #C4B29A;
|
|
162
|
+
color: #0F766E;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
Screen.theme-dark Button {
|
|
166
|
+
margin-right: 2;
|
|
167
|
+
border: none;
|
|
168
|
+
height: 1;
|
|
169
|
+
min-height: 1;
|
|
170
|
+
padding: 0 1;
|
|
171
|
+
background: #3B4252;
|
|
172
|
+
color: #ECEFF4;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Screen.theme-light Button {
|
|
176
|
+
margin-right: 2;
|
|
177
|
+
border: none;
|
|
178
|
+
height: 1;
|
|
179
|
+
min-height: 1;
|
|
180
|
+
padding: 0 1;
|
|
181
|
+
background: #D5C9B8;
|
|
182
|
+
color: #2A2118;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
Screen.theme-dark Button.-primary {
|
|
186
|
+
background: #88C0D0;
|
|
187
|
+
color: #2E3440;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
Screen.theme-light Button.-primary {
|
|
191
|
+
background: #0F766E;
|
|
192
|
+
color: #F4F1EA;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
Screen.theme-dark Button.-error {
|
|
196
|
+
background: #BF616A;
|
|
197
|
+
color: #ECEFF4;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
Screen.theme-light Button.-error {
|
|
201
|
+
background: #B42318;
|
|
202
|
+
color: #F4F1EA;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
Screen.theme-dark Log {
|
|
206
|
+
height: 100%;
|
|
207
|
+
border: none;
|
|
208
|
+
background: #242933;
|
|
209
|
+
margin-top: 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
Screen.theme-light Log {
|
|
213
|
+
height: 100%;
|
|
214
|
+
border: none;
|
|
215
|
+
background: #E8E1D5;
|
|
216
|
+
margin-top: 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Checkbox { height: 1; min-height: 1; border: none; padding: 0; margin-right: 2; }
|
|
220
|
+
Checkbox > .toggle--label { padding-left: 1; }
|
|
221
|
+
Checkbox:focus { background: transparent; }
|
|
222
|
+
|
|
223
|
+
Screen.theme-dark #cwd_label {
|
|
224
|
+
dock: bottom;
|
|
225
|
+
text-align: right;
|
|
226
|
+
width: 100%;
|
|
227
|
+
color: #88C0D0;
|
|
228
|
+
background: transparent;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
Screen.theme-light #cwd_label {
|
|
232
|
+
dock: bottom;
|
|
233
|
+
text-align: right;
|
|
234
|
+
width: 100%;
|
|
235
|
+
color: #0F766E;
|
|
236
|
+
background: transparent;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
Screen.theme-dark #right-sidebar {
|
|
240
|
+
width: 35;
|
|
241
|
+
dock: right;
|
|
242
|
+
height: 100%;
|
|
243
|
+
background: #242933;
|
|
244
|
+
border-left: solid #3B4252;
|
|
245
|
+
padding: 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
Screen.theme-light #right-sidebar {
|
|
249
|
+
width: 35;
|
|
250
|
+
dock: right;
|
|
251
|
+
height: 100%;
|
|
252
|
+
background: #E8E1D5;
|
|
253
|
+
border-left: solid #C9B9A3;
|
|
254
|
+
padding: 1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
Screen.theme-dark .info-text {
|
|
258
|
+
color: #6B7280;
|
|
259
|
+
margin-bottom: 1;
|
|
260
|
+
text-style: italic;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
Screen.theme-light .info-text {
|
|
264
|
+
color: #756250;
|
|
265
|
+
margin-bottom: 1;
|
|
266
|
+
text-style: italic;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#web_ui_status_btn { width: 100%; margin-top: 1; text-align: center; }
|
|
270
|
+
|
|
271
|
+
Screen.theme-dark .ui-btn-online { background: #A3BE8C; color: #2E3440; }
|
|
272
|
+
Screen.theme-light .ui-btn-online { background: #4D7C0F; color: #F4F1EA; }
|
|
273
|
+
Screen.theme-dark .ui-btn-offline { background: #4C566A; color: #ECEFF4; }
|
|
274
|
+
Screen.theme-light .ui-btn-offline { background: #756250; color: #F4F1EA; }
|
|
275
|
+
|
|
276
|
+
.quick-btn { width: 100%; margin-bottom: 1; text-align: center; }
|
|
277
|
+
|
|
278
|
+
.proj-add-row { height: auto; min-height: 1; margin-bottom: 1; }
|
|
279
|
+
.proj-toolbar-row { height: auto; min-height: 1; margin-bottom: 1; }
|
|
280
|
+
.proj-action-row { height: 1; margin-bottom: 1; }
|
|
281
|
+
.proj-path-input { width: 40; }
|
|
282
|
+
.proj-name-input { width: 20; }
|
|
283
|
+
|
|
284
|
+
DataTable { height: 1fr; }
|
|
285
|
+
|
|
286
|
+
Screen.theme-dark DataTable > .datatable--header {
|
|
287
|
+
color: #88C0D0;
|
|
288
|
+
text-style: bold;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
Screen.theme-light DataTable > .datatable--header {
|
|
292
|
+
color: #0F766E;
|
|
293
|
+
text-style: bold;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
Screen.theme-dark DataTable > .datatable--cursor {
|
|
297
|
+
background: #434C5E;
|
|
298
|
+
color: #ECEFF4;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
Screen.theme-light DataTable > .datatable--cursor {
|
|
302
|
+
background: #C4B29A;
|
|
303
|
+
color: #2A2118;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#projects_grid {
|
|
307
|
+
height: auto;
|
|
308
|
+
display: none;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.project-grid-row {
|
|
312
|
+
height: auto;
|
|
313
|
+
min-height: 7;
|
|
314
|
+
margin-bottom: 1;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.project-card {
|
|
318
|
+
width: 1fr;
|
|
319
|
+
height: auto;
|
|
320
|
+
min-height: 7;
|
|
321
|
+
margin-right: 1;
|
|
322
|
+
content-align: left top;
|
|
323
|
+
text-align: left;
|
|
324
|
+
padding: 1 2;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
Screen.theme-dark .projects-empty-state {
|
|
328
|
+
padding: 1 0;
|
|
329
|
+
color: #6B7280;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
Screen.theme-light .projects-empty-state {
|
|
333
|
+
padding: 1 0;
|
|
334
|
+
color: #756250;
|
|
335
|
+
}
|