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
@@ -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
+ }