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
tui/screens/init_view.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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, Select
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InitView(Vertical):
|
|
14
|
+
def compose(self) -> ComposeResult:
|
|
15
|
+
with Card("Project Initialization", ""):
|
|
16
|
+
yield Label("Initialize or update C3 in a project.", classes="card-title")
|
|
17
|
+
yield Label("IDE sets the MCP config target. Force skips prompts. Clear removes C3 files.", classes="info-text")
|
|
18
|
+
with Horizontal(classes="input-row"):
|
|
19
|
+
yield Label("Path:", classes="input-label")
|
|
20
|
+
yield Input(".", id="path_input")
|
|
21
|
+
|
|
22
|
+
with Horizontal(classes="input-row"):
|
|
23
|
+
yield Label("IDE:", classes="input-label")
|
|
24
|
+
ide_options = [(ide, ide) for ide in ["auto", "claude", "vscode", "cursor", "codex", "gemini", "antigravity"]]
|
|
25
|
+
yield Select(ide_options, value="auto", id="ide_select")
|
|
26
|
+
yield Label("MCP Mode:", classes="input-label")
|
|
27
|
+
yield Select([("direct", "direct"), ("proxy", "proxy")], value="direct", id="mcp_mode_select")
|
|
28
|
+
|
|
29
|
+
with Horizontal(classes="input-row"):
|
|
30
|
+
yield Checkbox("Force re-init", id="force_check")
|
|
31
|
+
yield Checkbox("Clear all C3 files", id="clear_check")
|
|
32
|
+
yield Checkbox("Init Git repo", id="git_check")
|
|
33
|
+
|
|
34
|
+
with Horizontal(classes="action-row"):
|
|
35
|
+
yield Button("Initialize", id="run_btn", variant="primary")
|
|
36
|
+
|
|
37
|
+
with Card("Setup Logs", ""):
|
|
38
|
+
yield Log(id="output_log", highlight=True)
|
|
39
|
+
|
|
40
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
41
|
+
if event.button.id == "run_btn":
|
|
42
|
+
path = self.query_one("#path_input").value
|
|
43
|
+
force = self.query_one("#force_check").value
|
|
44
|
+
clear = self.query_one("#clear_check").value
|
|
45
|
+
git = self.query_one("#git_check").value
|
|
46
|
+
ide = self.query_one("#ide_select").value
|
|
47
|
+
mcp_mode = self.query_one("#mcp_mode_select").value
|
|
48
|
+
|
|
49
|
+
self.query_one("#output_log").clear()
|
|
50
|
+
self.query_one("#run_btn").disabled = True
|
|
51
|
+
|
|
52
|
+
args = []
|
|
53
|
+
if force: args.append("--force")
|
|
54
|
+
if clear: args.append("--clear")
|
|
55
|
+
if git: args.append("--git")
|
|
56
|
+
if ide and ide != "auto":
|
|
57
|
+
args.extend(["--ide", ide])
|
|
58
|
+
if mcp_mode and mcp_mode != "direct":
|
|
59
|
+
args.extend(["--mcp-mode", mcp_mode])
|
|
60
|
+
|
|
61
|
+
self.run_init(path, args)
|
|
62
|
+
|
|
63
|
+
@work(exclusive=True)
|
|
64
|
+
async def run_init(self, path: str, additional_args: list) -> None:
|
|
65
|
+
c3_path, root_dir = get_c3_path()
|
|
66
|
+
args = ["init"] + additional_args
|
|
67
|
+
if path:
|
|
68
|
+
args.append(path)
|
|
69
|
+
|
|
70
|
+
cmd = [sys.executable, c3_path] + args
|
|
71
|
+
env = os.environ.copy()
|
|
72
|
+
env["PYTHONPATH"] = root_dir
|
|
73
|
+
|
|
74
|
+
process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, env=env)
|
|
75
|
+
log_widget = self.query_one("#output_log")
|
|
76
|
+
while True:
|
|
77
|
+
line = await process.stdout.readline()
|
|
78
|
+
if not line: break
|
|
79
|
+
log_widget.write_line(line.decode(errors="replace").rstrip())
|
|
80
|
+
await process.wait()
|
|
81
|
+
|
|
82
|
+
self.query_one("#run_btn").disabled = False
|
tui/screens/mcp_view.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
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 MCPView(Vertical):
|
|
14
|
+
def compose(self) -> ComposeResult:
|
|
15
|
+
with Card("MCP Server Management", ""):
|
|
16
|
+
yield Label("Configure and register MCP servers.", classes="card-title")
|
|
17
|
+
yield Label("Targets: Optional project path and/or IDE shorthand (e.g. . claude)", classes="info-text")
|
|
18
|
+
with Horizontal(classes="input-row"):
|
|
19
|
+
yield Label("Targets (Path/IDE): ", classes="input-label")
|
|
20
|
+
yield Input(placeholder=". claude", id="targets_input")
|
|
21
|
+
|
|
22
|
+
with Horizontal(classes="input-row"):
|
|
23
|
+
yield Label("IDE:", classes="input-label")
|
|
24
|
+
ide_options = [(ide, ide) for ide in ["auto", "claude", "vscode", "cursor", "codex", "gemini", "antigravity"]]
|
|
25
|
+
yield Select(ide_options, value="auto", id="ide_select")
|
|
26
|
+
yield Label("MCP Mode:", classes="input-label")
|
|
27
|
+
yield Select([("direct", "direct"), ("proxy", "proxy")], value="direct", id="mcp_mode_select")
|
|
28
|
+
|
|
29
|
+
with Horizontal(classes="action-row"):
|
|
30
|
+
yield Button("Register Server", id="run_btn", variant="primary")
|
|
31
|
+
|
|
32
|
+
with Card("Status", ""):
|
|
33
|
+
yield Log(id="output_log", highlight=True)
|
|
34
|
+
|
|
35
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
36
|
+
if event.button.id == "run_btn":
|
|
37
|
+
targets_raw = self.query_one("#targets_input").value
|
|
38
|
+
ide = self.query_one("#ide_select").value
|
|
39
|
+
mcp_mode = self.query_one("#mcp_mode_select").value
|
|
40
|
+
|
|
41
|
+
self.query_one("#output_log").clear()
|
|
42
|
+
self.query_one("#run_btn").disabled = True
|
|
43
|
+
|
|
44
|
+
args = []
|
|
45
|
+
if ide and ide != "auto":
|
|
46
|
+
args.extend(["--ide", ide])
|
|
47
|
+
if mcp_mode and mcp_mode != "direct":
|
|
48
|
+
args.extend(["--mcp-mode", mcp_mode])
|
|
49
|
+
|
|
50
|
+
# Parse targets (space separated)
|
|
51
|
+
targets = []
|
|
52
|
+
if targets_raw:
|
|
53
|
+
targets = targets_raw.split()
|
|
54
|
+
|
|
55
|
+
self.run_mcp(args, targets)
|
|
56
|
+
|
|
57
|
+
@work(exclusive=True)
|
|
58
|
+
async def run_mcp(self, args: list, targets: list) -> None:
|
|
59
|
+
c3_path, root_dir = get_c3_path()
|
|
60
|
+
cmd_args = ["install-mcp"] + args + targets
|
|
61
|
+
cmd = [sys.executable, c3_path] + cmd_args
|
|
62
|
+
env = os.environ.copy()
|
|
63
|
+
env["PYTHONPATH"] = root_dir
|
|
64
|
+
|
|
65
|
+
process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, env=env)
|
|
66
|
+
log_widget = self.query_one("#output_log")
|
|
67
|
+
while True:
|
|
68
|
+
line = await process.stdout.readline()
|
|
69
|
+
if not line: break
|
|
70
|
+
log_widget.write_line(line.decode(errors="replace").rstrip())
|
|
71
|
+
await process.wait()
|
|
72
|
+
|
|
73
|
+
self.query_one("#run_btn").disabled = False
|
|
@@ -0,0 +1,41 @@
|
|
|
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 OptimizeView(Vertical):
|
|
14
|
+
def compose(self) -> ComposeResult:
|
|
15
|
+
with Card("Context Optimization", ""):
|
|
16
|
+
yield Label("Analyzes and optimizes the project index and context usage.")
|
|
17
|
+
with Horizontal(classes="action-row"):
|
|
18
|
+
yield Button("Run Optimization", id="run_btn", variant="primary")
|
|
19
|
+
|
|
20
|
+
with Card("Analysis Report", ""):
|
|
21
|
+
yield Log(id="output_log", highlight=True)
|
|
22
|
+
|
|
23
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
24
|
+
if event.button.id == "run_btn":
|
|
25
|
+
self.query_one("#output_log").clear()
|
|
26
|
+
self.run_optimize()
|
|
27
|
+
|
|
28
|
+
@work(exclusive=True)
|
|
29
|
+
async def run_optimize(self) -> None:
|
|
30
|
+
c3_path, root_dir = get_c3_path()
|
|
31
|
+
cmd = [sys.executable, c3_path, "optimize"]
|
|
32
|
+
env = os.environ.copy()
|
|
33
|
+
env["PYTHONPATH"] = root_dir
|
|
34
|
+
|
|
35
|
+
process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, env=env)
|
|
36
|
+
log_widget = self.query_one("#output_log")
|
|
37
|
+
while True:
|
|
38
|
+
line = await process.stdout.readline()
|
|
39
|
+
if not line: break
|
|
40
|
+
log_widget.write_line(line.decode(errors="replace").rstrip())
|
|
41
|
+
await process.wait()
|
tui/screens/pipe_view.py
ADDED
|
@@ -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, Input, Label, Log
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PipeView(Vertical):
|
|
14
|
+
def compose(self) -> ComposeResult:
|
|
15
|
+
with Card("Tool Pipeline", ""):
|
|
16
|
+
yield Label("Executes a full context-to-answer pipeline.")
|
|
17
|
+
with Horizontal(classes="input-row"):
|
|
18
|
+
yield Label("Query: ", classes="input-label")
|
|
19
|
+
yield Input(placeholder="How do I fix the TUI?", id="query_input")
|
|
20
|
+
with Horizontal(classes="action-row"):
|
|
21
|
+
yield Button("Run Pipeline", id="run_btn", variant="primary")
|
|
22
|
+
|
|
23
|
+
with Card("Pipeline Output", ""):
|
|
24
|
+
yield Log(id="output_log", highlight=True)
|
|
25
|
+
|
|
26
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
27
|
+
if event.button.id == "run_btn":
|
|
28
|
+
query = self.query_one("#query_input").value
|
|
29
|
+
if not query: return
|
|
30
|
+
self.query_one("#output_log").clear()
|
|
31
|
+
self.run_pipe(query)
|
|
32
|
+
|
|
33
|
+
@work(exclusive=True)
|
|
34
|
+
async def run_pipe(self, query: str) -> None:
|
|
35
|
+
c3_path, root_dir = get_c3_path()
|
|
36
|
+
cmd = [sys.executable, c3_path, "pipe", query]
|
|
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,355 @@
|
|
|
1
|
+
"""Project Hub - central manager for all C3 projects and their sessions."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import webbrowser
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
from screens.stats import Card
|
|
10
|
+
from textual import work
|
|
11
|
+
from textual.app import ComposeResult
|
|
12
|
+
from textual.containers import Horizontal, Vertical
|
|
13
|
+
from textual.widgets import Button, DataTable, Input, Label, Static
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProjectsView(Vertical):
|
|
17
|
+
"""Central project tracker and session manager."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, **kwargs):
|
|
20
|
+
super().__init__(**kwargs)
|
|
21
|
+
self._projects: list = []
|
|
22
|
+
self._view_mode = "table"
|
|
23
|
+
self._selected_project_path: str | None = None
|
|
24
|
+
|
|
25
|
+
def compose(self) -> ComposeResult:
|
|
26
|
+
with Card("Add / Register Project", ""):
|
|
27
|
+
with Horizontal(classes="proj-add-row"):
|
|
28
|
+
yield Label("Path:", classes="input-label")
|
|
29
|
+
yield Input(
|
|
30
|
+
placeholder="Absolute path to project (e.g. C:/projects/myapp)",
|
|
31
|
+
id="proj_path_input",
|
|
32
|
+
classes="proj-path-input",
|
|
33
|
+
)
|
|
34
|
+
yield Label("Name:", classes="input-label")
|
|
35
|
+
yield Input(
|
|
36
|
+
placeholder="Optional display name",
|
|
37
|
+
id="proj_name_input",
|
|
38
|
+
classes="proj-name-input",
|
|
39
|
+
)
|
|
40
|
+
yield Button("Add", id="add_btn", variant="primary")
|
|
41
|
+
|
|
42
|
+
with Card("Registered Projects", ""):
|
|
43
|
+
with Horizontal(classes="proj-toolbar-row"):
|
|
44
|
+
yield Static(
|
|
45
|
+
"Browse projects as a table or a card grid.",
|
|
46
|
+
classes="info-text",
|
|
47
|
+
)
|
|
48
|
+
yield Button("List View", id="table_view_btn", variant="primary")
|
|
49
|
+
yield Button("Grid View", id="grid_view_btn")
|
|
50
|
+
yield DataTable(id="projects_table")
|
|
51
|
+
yield Vertical(id="projects_grid")
|
|
52
|
+
|
|
53
|
+
with Card("Session Actions", ""):
|
|
54
|
+
with Horizontal(classes="proj-action-row"):
|
|
55
|
+
yield Button("Refresh", id="refresh_btn")
|
|
56
|
+
yield Button("Start Session", id="start_btn", variant="primary")
|
|
57
|
+
yield Button("Open Browser", id="open_btn")
|
|
58
|
+
yield Button("Stop Session", id="stop_btn", variant="error")
|
|
59
|
+
yield Button("Remove Project", id="remove_btn")
|
|
60
|
+
yield Static("Select a project row to manage it.", id="status_label")
|
|
61
|
+
|
|
62
|
+
def on_mount(self) -> None:
|
|
63
|
+
table = self.query_one("#projects_table", DataTable)
|
|
64
|
+
table.add_columns("Name", "Path", "IDE", "Status", "Port", "Last Session", "Added")
|
|
65
|
+
table.cursor_type = "row"
|
|
66
|
+
table.zebra_stripes = True
|
|
67
|
+
self.query_one("#projects_grid", Vertical).display = False
|
|
68
|
+
self.load_projects()
|
|
69
|
+
self.set_interval(15.0, self.load_projects)
|
|
70
|
+
|
|
71
|
+
@work(exclusive=True, thread=True)
|
|
72
|
+
def load_projects(self) -> None:
|
|
73
|
+
try:
|
|
74
|
+
pm = self._get_pm()
|
|
75
|
+
projects = pm.list_projects()
|
|
76
|
+
self.app.call_from_thread(self._render_projects, projects)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
self.app.call_from_thread(
|
|
79
|
+
self._set_status, f"[red]Error loading projects: {e}[/]"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _render_projects(self, projects: list) -> None:
|
|
83
|
+
self._projects = projects
|
|
84
|
+
table = self.query_one("#projects_table", DataTable)
|
|
85
|
+
grid = self.query_one("#projects_grid", Vertical)
|
|
86
|
+
table.clear()
|
|
87
|
+
|
|
88
|
+
if self._selected_project_path and not any(
|
|
89
|
+
p.get("path") == self._selected_project_path for p in projects
|
|
90
|
+
):
|
|
91
|
+
self._selected_project_path = None
|
|
92
|
+
if not self._selected_project_path and projects:
|
|
93
|
+
self._selected_project_path = projects[0].get("path")
|
|
94
|
+
|
|
95
|
+
for project in projects:
|
|
96
|
+
status = (
|
|
97
|
+
Text("IN PROGRESS", style="bold green")
|
|
98
|
+
if project.get("session_active")
|
|
99
|
+
else Text("UI ACTIVE", style="bold green")
|
|
100
|
+
if project.get("ui_active")
|
|
101
|
+
else Text("stopped", style="dim #6B7280")
|
|
102
|
+
)
|
|
103
|
+
port_str = str(project["port"]) if project.get("port") else "-"
|
|
104
|
+
last = (project.get("last_session") or "never")
|
|
105
|
+
if last != "never":
|
|
106
|
+
last = last[:10]
|
|
107
|
+
added = (project.get("added_at") or "")[:10]
|
|
108
|
+
path = project.get("path", "")
|
|
109
|
+
display_path = ("..." + path[-37:]) if len(path) > 40 else path
|
|
110
|
+
|
|
111
|
+
table.add_row(
|
|
112
|
+
project.get("name", "?"),
|
|
113
|
+
display_path,
|
|
114
|
+
project.get("ide", "?"),
|
|
115
|
+
status,
|
|
116
|
+
port_str,
|
|
117
|
+
last,
|
|
118
|
+
added,
|
|
119
|
+
key=project.get("path", ""),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
grid.query("*").remove()
|
|
123
|
+
if not projects:
|
|
124
|
+
grid.mount(
|
|
125
|
+
Static(
|
|
126
|
+
"No projects registered yet. Add one above to populate the hub.",
|
|
127
|
+
classes="projects-empty-state",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
for start in range(0, len(projects), 2):
|
|
132
|
+
row = Horizontal(classes="project-grid-row")
|
|
133
|
+
for index, project in enumerate(projects[start : start + 2], start=start):
|
|
134
|
+
row.mount(
|
|
135
|
+
Button(
|
|
136
|
+
self._project_card_label(project),
|
|
137
|
+
id=f"project_card__{index}",
|
|
138
|
+
classes="project-card",
|
|
139
|
+
variant=(
|
|
140
|
+
"primary"
|
|
141
|
+
if project.get("path") == self._selected_project_path
|
|
142
|
+
else "default"
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
grid.mount(row)
|
|
147
|
+
|
|
148
|
+
active_count = sum(1 for p in projects if p.get("active"))
|
|
149
|
+
self._set_status(
|
|
150
|
+
f"{len(projects)} project(s) registered - "
|
|
151
|
+
f"[bold green]{active_count} active[/] session(s). "
|
|
152
|
+
f"{self._selection_hint()}"
|
|
153
|
+
)
|
|
154
|
+
self._sync_view_mode_buttons()
|
|
155
|
+
self._update_view_visibility()
|
|
156
|
+
|
|
157
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
158
|
+
btn_id = event.button.id
|
|
159
|
+
|
|
160
|
+
if btn_id == "refresh_btn":
|
|
161
|
+
self.load_projects()
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
if btn_id == "table_view_btn":
|
|
165
|
+
self._view_mode = "table"
|
|
166
|
+
self._sync_view_mode_buttons()
|
|
167
|
+
self._update_view_visibility()
|
|
168
|
+
self._set_status("[green]Projects view switched to list mode.[/]")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
if btn_id == "grid_view_btn":
|
|
172
|
+
self._view_mode = "grid"
|
|
173
|
+
self._sync_view_mode_buttons()
|
|
174
|
+
self._update_view_visibility()
|
|
175
|
+
self._set_status("[green]Projects view switched to grid mode.[/]")
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
if btn_id == "add_btn":
|
|
179
|
+
path = self.query_one("#proj_path_input", Input).value.strip()
|
|
180
|
+
name = self.query_one("#proj_name_input", Input).value.strip() or None
|
|
181
|
+
if not path:
|
|
182
|
+
self._set_status("[yellow]Enter a project path first.[/]")
|
|
183
|
+
return
|
|
184
|
+
self._do_add(path, name)
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
if btn_id and btn_id.startswith("project_card__"):
|
|
188
|
+
index = int(btn_id.split("__", 1)[1])
|
|
189
|
+
if 0 <= index < len(self._projects):
|
|
190
|
+
project = self._projects[index]
|
|
191
|
+
self._selected_project_path = project.get("path")
|
|
192
|
+
self._render_projects(self._projects)
|
|
193
|
+
self._set_status(
|
|
194
|
+
f"[green]Selected [bold]{project.get('name', '?')}[/] in grid view.[/]"
|
|
195
|
+
)
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
proj = self._selected_project()
|
|
199
|
+
if proj is None:
|
|
200
|
+
self._set_status(f"[yellow]{self._selection_hint()}[/]")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
if btn_id == "start_btn":
|
|
204
|
+
if proj.get("ui_active"):
|
|
205
|
+
self._set_status(
|
|
206
|
+
f"[yellow]Session already running on port {proj['port']}.[/]"
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
self._do_launch(proj["path"])
|
|
210
|
+
|
|
211
|
+
elif btn_id == "open_btn":
|
|
212
|
+
if not proj.get("ui_active"):
|
|
213
|
+
self._set_status("[yellow]No active session. Start one first.[/]")
|
|
214
|
+
else:
|
|
215
|
+
webbrowser.open(f"http://localhost:{proj['port']}")
|
|
216
|
+
self._set_status(f"[green]Opened http://localhost:{proj['port']}[/]")
|
|
217
|
+
|
|
218
|
+
elif btn_id == "stop_btn":
|
|
219
|
+
if not proj.get("ui_active"):
|
|
220
|
+
self._set_status("[yellow]No active session to stop.[/]")
|
|
221
|
+
else:
|
|
222
|
+
self._do_stop(proj["port"])
|
|
223
|
+
|
|
224
|
+
elif btn_id == "remove_btn":
|
|
225
|
+
self._do_remove(proj["path"])
|
|
226
|
+
|
|
227
|
+
@work(exclusive=False, thread=True)
|
|
228
|
+
def _do_add(self, path: str, name) -> None:
|
|
229
|
+
try:
|
|
230
|
+
pm = self._get_pm()
|
|
231
|
+
entry = pm.add_project(path, name)
|
|
232
|
+
self.app.call_from_thread(
|
|
233
|
+
self._set_status,
|
|
234
|
+
f"[green]Registered: [bold]{entry['name']}[/] - {entry['path']}[/]",
|
|
235
|
+
)
|
|
236
|
+
self.app.call_from_thread(self._clear_add_inputs)
|
|
237
|
+
self.load_projects()
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.app.call_from_thread(self._set_status, f"[red]Error: {e}[/]")
|
|
240
|
+
|
|
241
|
+
@work(exclusive=False, thread=True)
|
|
242
|
+
def _do_launch(self, path: str) -> None:
|
|
243
|
+
try:
|
|
244
|
+
pm = self._get_pm()
|
|
245
|
+
ok = pm.launch_session(path)
|
|
246
|
+
if ok:
|
|
247
|
+
time.sleep(2.5)
|
|
248
|
+
self.load_projects()
|
|
249
|
+
self.app.call_from_thread(
|
|
250
|
+
self._set_status,
|
|
251
|
+
f"[green]Session launched for [bold]{path}[/][/]",
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
self.app.call_from_thread(
|
|
255
|
+
self._set_status, "[red]Failed to launch session.[/]"
|
|
256
|
+
)
|
|
257
|
+
except Exception as e:
|
|
258
|
+
self.app.call_from_thread(self._set_status, f"[red]Error: {e}[/]")
|
|
259
|
+
|
|
260
|
+
@work(exclusive=False, thread=True)
|
|
261
|
+
def _do_stop(self, port: int) -> None:
|
|
262
|
+
try:
|
|
263
|
+
pm = self._get_pm()
|
|
264
|
+
pm.stop_session(port)
|
|
265
|
+
time.sleep(1)
|
|
266
|
+
self.load_projects()
|
|
267
|
+
self.app.call_from_thread(
|
|
268
|
+
self._set_status, f"[green]Session on port {port} stopped.[/]"
|
|
269
|
+
)
|
|
270
|
+
except Exception as e:
|
|
271
|
+
self.app.call_from_thread(self._set_status, f"[red]Error: {e}[/]")
|
|
272
|
+
|
|
273
|
+
@work(exclusive=False, thread=True)
|
|
274
|
+
def _do_remove(self, path: str) -> None:
|
|
275
|
+
try:
|
|
276
|
+
pm = self._get_pm()
|
|
277
|
+
removed = pm.remove_project(path)
|
|
278
|
+
msg = (
|
|
279
|
+
f"[green]Removed project: {path}[/]"
|
|
280
|
+
if removed
|
|
281
|
+
else "[yellow]Project not found in registry.[/]"
|
|
282
|
+
)
|
|
283
|
+
self.app.call_from_thread(self._set_status, msg)
|
|
284
|
+
self.load_projects()
|
|
285
|
+
except Exception as e:
|
|
286
|
+
self.app.call_from_thread(self._set_status, f"[red]Error: {e}[/]")
|
|
287
|
+
|
|
288
|
+
def _get_pm(self):
|
|
289
|
+
root = str(Path(__file__).parent.parent.parent)
|
|
290
|
+
if root not in sys.path:
|
|
291
|
+
sys.path.insert(0, root)
|
|
292
|
+
from services.project_manager import ProjectManager
|
|
293
|
+
|
|
294
|
+
return ProjectManager()
|
|
295
|
+
|
|
296
|
+
def _selected_project(self):
|
|
297
|
+
if self._view_mode == "grid":
|
|
298
|
+
if not self._selected_project_path:
|
|
299
|
+
return None
|
|
300
|
+
for project in self._projects:
|
|
301
|
+
if project.get("path") == self._selected_project_path:
|
|
302
|
+
return project
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
table = self.query_one("#projects_table", DataTable)
|
|
306
|
+
idx = table.cursor_row
|
|
307
|
+
if idx is not None and self._projects and idx < len(self._projects):
|
|
308
|
+
return self._projects[idx]
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
def _project_card_label(self, project: dict) -> str:
|
|
312
|
+
if project.get("session_active"):
|
|
313
|
+
status = "IN PROGRESS"
|
|
314
|
+
elif project.get("ui_active"):
|
|
315
|
+
status = "UI ACTIVE"
|
|
316
|
+
else:
|
|
317
|
+
status = "stopped"
|
|
318
|
+
port = project.get("port") or "-"
|
|
319
|
+
last = (project.get("last_session") or "never")[:10]
|
|
320
|
+
added = (project.get("added_at") or "")[:10] or "unknown"
|
|
321
|
+
return (
|
|
322
|
+
f"{project.get('name', '?')}\n"
|
|
323
|
+
f"{project.get('ide', '?').upper()} {status}\n"
|
|
324
|
+
f"Port: {port} Last: {last}\n"
|
|
325
|
+
f"{project.get('path', '')}\n"
|
|
326
|
+
f"Added: {added}"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def _selection_hint(self) -> str:
|
|
330
|
+
if self._view_mode == "grid":
|
|
331
|
+
return "Select a card, then use the buttons below."
|
|
332
|
+
return "Select a row, then use the buttons below."
|
|
333
|
+
|
|
334
|
+
def _sync_view_mode_buttons(self) -> None:
|
|
335
|
+
table_btn = self.query_one("#table_view_btn", Button)
|
|
336
|
+
grid_btn = self.query_one("#grid_view_btn", Button)
|
|
337
|
+
if self._view_mode == "table":
|
|
338
|
+
table_btn.variant = "primary"
|
|
339
|
+
grid_btn.variant = "default"
|
|
340
|
+
else:
|
|
341
|
+
table_btn.variant = "default"
|
|
342
|
+
grid_btn.variant = "primary"
|
|
343
|
+
|
|
344
|
+
def _update_view_visibility(self) -> None:
|
|
345
|
+
table = self.query_one("#projects_table", DataTable)
|
|
346
|
+
grid = self.query_one("#projects_grid", Vertical)
|
|
347
|
+
table.display = self._view_mode == "table"
|
|
348
|
+
grid.display = self._view_mode == "grid"
|
|
349
|
+
|
|
350
|
+
def _set_status(self, msg: str) -> None:
|
|
351
|
+
self.query_one("#status_label", Static).update(msg)
|
|
352
|
+
|
|
353
|
+
def _clear_add_inputs(self) -> None:
|
|
354
|
+
self.query_one("#proj_path_input", Input).value = ""
|
|
355
|
+
self.query_one("#proj_name_input", Input).value = ""
|
|
@@ -0,0 +1,55 @@
|
|
|
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 SearchView(Vertical):
|
|
14
|
+
def compose(self) -> ComposeResult:
|
|
15
|
+
with Card("Context Search", ""):
|
|
16
|
+
with Horizontal(classes="input-row"):
|
|
17
|
+
yield Label("Query: ", classes="input-label")
|
|
18
|
+
yield Input(placeholder="e.g. how does auth work?", id="search_input")
|
|
19
|
+
with Horizontal(classes="action-row"):
|
|
20
|
+
yield Button("Search Context", id="search_btn", variant="primary")
|
|
21
|
+
|
|
22
|
+
with Card("Search Results", ""):
|
|
23
|
+
yield Log(id="search_log", highlight=True)
|
|
24
|
+
|
|
25
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
26
|
+
if event.button.id == "search_btn":
|
|
27
|
+
query = self.query_one("#search_input").value
|
|
28
|
+
if not query:
|
|
29
|
+
return
|
|
30
|
+
self.query_one("#search_log").clear()
|
|
31
|
+
self.query_one("#search_btn").disabled = True
|
|
32
|
+
self.run_search(query)
|
|
33
|
+
|
|
34
|
+
@work(exclusive=True)
|
|
35
|
+
async def run_search(self, query: str) -> None:
|
|
36
|
+
c3_path, root_dir = get_c3_path()
|
|
37
|
+
env = os.environ.copy()
|
|
38
|
+
env["PYTHONPATH"] = root_dir
|
|
39
|
+
|
|
40
|
+
cmd = [sys.executable, c3_path, "context", query]
|
|
41
|
+
process = await asyncio.create_subprocess_exec(
|
|
42
|
+
*cmd,
|
|
43
|
+
stdout=asyncio.subprocess.PIPE,
|
|
44
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
45
|
+
env=env
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
log_widget = self.query_one("#search_log")
|
|
49
|
+
while True:
|
|
50
|
+
line = await process.stdout.readline()
|
|
51
|
+
if not line: break
|
|
52
|
+
log_widget.write_line(line.decode(errors="replace").rstrip())
|
|
53
|
+
|
|
54
|
+
await process.wait()
|
|
55
|
+
self.query_one("#search_btn").disabled = False
|