coreinsight-cli 0.2.6__tar.gz → 0.2.7__tar.gz
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.
- {coreinsight_cli-0.2.6/coreinsight_cli.egg-info → coreinsight_cli-0.2.7}/PKG-INFO +2 -1
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/config.py +3 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/main.py +196 -160
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/sandbox.py +4 -3
- coreinsight_cli-0.2.7/coreinsight/tui.py +650 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7/coreinsight_cli.egg-info}/PKG-INFO +2 -1
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight_cli.egg-info/SOURCES.txt +1 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight_cli.egg-info/requires.txt +1 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/pyproject.toml +2 -1
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/LICENSE +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/README.md +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/Dockerfile.cpp-sandbox +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/Dockerfile.python-sandbox +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/__init__.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/analyzer.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/demo/__init__.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/demo/bad_loop.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/demo/data_processor.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/demo/slow.cpp +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/hardware.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/indexer.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/memory.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/parser.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/profiler.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/prompts.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/scanner.py +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight_cli.egg-info/dependency_links.txt +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight_cli.egg-info/entry_points.txt +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight_cli.egg-info/top_level.txt +0 -0
- {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coreinsight-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Local-first AI performance profiler that mathematically verifies optimizations for Python, C++, and CUDA
|
|
5
5
|
Author: Varun Jani
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -32,6 +32,7 @@ Requires-Dist: langchain-anthropic>=0.1.0
|
|
|
32
32
|
Requires-Dist: pydantic>=2.0
|
|
33
33
|
Requires-Dist: chromadb>=0.5.0
|
|
34
34
|
Requires-Dist: sentence-transformers>=3.0.0
|
|
35
|
+
Requires-Dist: textual>=0.60.0
|
|
35
36
|
Requires-Dist: psutil>=5.9
|
|
36
37
|
Provides-Extra: compat
|
|
37
38
|
Requires-Dist: pysqlite3-binary>=0.5.0; extra == "compat"
|
|
@@ -18,6 +18,7 @@ FREE_TIER_LIMITS = {
|
|
|
18
18
|
"max_retries": 2,
|
|
19
19
|
"num_test_cases": 8,
|
|
20
20
|
"hardware_profiling": False,
|
|
21
|
+
"max_files": 2,
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
PRO_TIER_LIMITS = {
|
|
@@ -25,6 +26,7 @@ PRO_TIER_LIMITS = {
|
|
|
25
26
|
"max_retries": 5,
|
|
26
27
|
"num_test_cases": 15,
|
|
27
28
|
"hardware_profiling": True,
|
|
29
|
+
"max_files": None,
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
SMALL_MODELS = ["llama3.2:3b", "llama3.2:1b", "codellama:7b", "llama3:7b", "mistral:7b"]
|
|
@@ -168,6 +170,7 @@ def run_configure(pro_key: str = None, agent_mode: str = None):
|
|
|
168
170
|
if provider == "ollama":
|
|
169
171
|
config["model_name"] = Prompt.ask("Ollama model name", default=config.get("model_name", "llama3.2"))
|
|
170
172
|
elif provider == "local_server":
|
|
173
|
+
from rich.panel import Panel
|
|
171
174
|
console.print(Panel(
|
|
172
175
|
"[bold]Local inference server setup[/bold]\n\n"
|
|
173
176
|
"CoreInsight talks to any OpenAI-compatible local server.\n"
|
|
@@ -32,7 +32,7 @@ from rich.console import Group
|
|
|
32
32
|
from coreinsight.config import load_config, run_configure, is_pro, get_tier_limits, PRO_WAITLIST_URL, get_model_tier, get_agent_mode
|
|
33
33
|
from coreinsight.analyzer import AnalyzerAgent, BottleneckAgent, OptimizerAgent, HarnessAgent, TestCaseAgent
|
|
34
34
|
from coreinsight.parser import CodeParser
|
|
35
|
-
from coreinsight.sandbox import CodeSandbox
|
|
35
|
+
from coreinsight.sandbox import CodeSandbox, SANDBOX_SKIPPED_MSG
|
|
36
36
|
from coreinsight.profiler import HardwareProfiler
|
|
37
37
|
from coreinsight.memory import OptimizationMemory
|
|
38
38
|
from coreinsight.indexer import RepoIndexer
|
|
@@ -40,6 +40,7 @@ from coreinsight.hardware import HardwareDetector
|
|
|
40
40
|
from coreinsight.scanner import ProjectScanner
|
|
41
41
|
|
|
42
42
|
console = Console()
|
|
43
|
+
_default_console = console # kept for restore after TUI swaps it
|
|
43
44
|
|
|
44
45
|
# Thread locks to prevent garbled output when multiple threads finish simultaneously
|
|
45
46
|
print_lock = threading.Lock()
|
|
@@ -101,17 +102,20 @@ def _run_single_agent(
|
|
|
101
102
|
while not is_valid and retry_count < max_retries:
|
|
102
103
|
if getattr(sandbox, 'disabled', False):
|
|
103
104
|
break
|
|
105
|
+
error_hint = ""
|
|
104
106
|
if success:
|
|
105
107
|
if "N,Original_Time" not in logs:
|
|
106
|
-
|
|
108
|
+
error_hint = "\nERROR: Script ran but DID NOT print the CSV table. You MUST print the strict CSV format."
|
|
107
109
|
else:
|
|
108
|
-
|
|
109
|
-
harness_code
|
|
110
|
+
error_hint = "\nERROR: Optimized code was SLOWER. Rewrite to be faster."
|
|
111
|
+
harness_code = agent.fix_harness(func_name, original_code, harness_code, logs + error_hint, language, context=context)
|
|
110
112
|
success, logs, plot_data = sandbox.execute_benchmark(harness_code, language)
|
|
111
113
|
is_valid = _check_speedup_success(success, logs)
|
|
112
114
|
retry_count += 1
|
|
113
115
|
|
|
114
|
-
if
|
|
116
|
+
if getattr(sandbox, 'disabled', False):
|
|
117
|
+
pass # skipped intentionally — don't annotate as failed
|
|
118
|
+
elif is_valid and retry_count > 0:
|
|
115
119
|
logs = f"(Succeeded after {retry_count} retries)\n" + logs
|
|
116
120
|
elif not is_valid:
|
|
117
121
|
logs = f"(Failed after {retry_count} retries)\n" + logs
|
|
@@ -230,7 +234,8 @@ def process_function(func: dict, language: str, agent: AnalyzerAgent, sandbox: C
|
|
|
230
234
|
# 3. Verification + AI-free hardware profiling
|
|
231
235
|
verification = None
|
|
232
236
|
profiler_result = None
|
|
233
|
-
|
|
237
|
+
docker_active = not getattr(sandbox, 'disabled', False)
|
|
238
|
+
if is_valid_optimization and docker_active:
|
|
234
239
|
# Multi-agent: test cases already generated in parallel with harness
|
|
235
240
|
# Single-agent: generate them now
|
|
236
241
|
if agent_mode == "multi" and result.get("_test_cases") is not None:
|
|
@@ -313,6 +318,13 @@ def format_report_markdown(func_name: str, result: dict, sandbox_res: tuple, msg
|
|
|
313
318
|
|
|
314
319
|
# ── Normal sandbox path ─────────────────────────────────────────────
|
|
315
320
|
success, logs, plot_data = sandbox_res
|
|
321
|
+
|
|
322
|
+
if logs and SANDBOX_SKIPPED_MSG in logs:
|
|
323
|
+
md += f"### Verification: Skipped (--no-docker)\n"
|
|
324
|
+
md += f"> Sandbox and verification were disabled at runtime.\n\n"
|
|
325
|
+
md += "\n---\n"
|
|
326
|
+
return md
|
|
327
|
+
|
|
316
328
|
sandbox_icon = "🟢" if success else "❌"
|
|
317
329
|
md += f"### Verification: {sandbox_icon} {'Success' if success else 'Failed'}\n"
|
|
318
330
|
|
|
@@ -409,6 +421,15 @@ def print_console_report(func_name: str, result: dict, sandbox_res: tuple, msg:
|
|
|
409
421
|
# ── Normal sandbox path ─────────────────────────────────────────────
|
|
410
422
|
success, logs, plot_data = sandbox_res
|
|
411
423
|
|
|
424
|
+
# --no-docker path: render as skipped, not failed
|
|
425
|
+
if logs and SANDBOX_SKIPPED_MSG in logs:
|
|
426
|
+
content.append(Text.from_markup(
|
|
427
|
+
"[dim]Sandbox verification skipped (--no-docker). "
|
|
428
|
+
"The optimized code above has not been benchmarked or verified.[/dim]"
|
|
429
|
+
))
|
|
430
|
+
console.print(Panel(Group(*content), title=f"Analysis: [bold]{func_name}[/bold]", border_style=color))
|
|
431
|
+
return
|
|
432
|
+
|
|
412
433
|
csv_data = parse_csv_logs(logs)
|
|
413
434
|
if csv_data:
|
|
414
435
|
table = Table(show_header=True, header_style="bold magenta", expand=True)
|
|
@@ -541,168 +562,177 @@ def _preflight_checks(provider: str, model_name: str, no_docker: bool = False) -
|
|
|
541
562
|
|
|
542
563
|
return True
|
|
543
564
|
|
|
544
|
-
def run_analysis(file_path: str, no_docker: bool = False):
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
console.print(f"[red]Error: File '{file_path}' not found.[/red]")
|
|
548
|
-
sys.exit(1)
|
|
549
|
-
|
|
550
|
-
language = get_language_from_ext(path)
|
|
551
|
-
if language == "unknown":
|
|
552
|
-
console.print(f"[red]Error: Unsupported file type '{path.suffix}'. Use .cpp, .cu, or .py[/red]")
|
|
553
|
-
sys.exit(1)
|
|
554
|
-
|
|
555
|
-
config = load_config()
|
|
556
|
-
provider = config.get("provider", "ollama")
|
|
557
|
-
model_name = config.get("model_name", "llama3.2")
|
|
558
|
-
api_keys = config.get("api_keys", {})
|
|
559
|
-
tier_limits = get_tier_limits(config)
|
|
560
|
-
pro_user = is_pro(config)
|
|
561
|
-
|
|
562
|
-
# ── Preflight: fail fast before AST parsing, image building, or thread spawning ──
|
|
563
|
-
if not _preflight_checks(provider, model_name, no_docker=no_docker):
|
|
564
|
-
sys.exit(1)
|
|
565
|
-
|
|
566
|
-
tier_label = "[bold green]Pro[/bold green]" if pro_user else "[bold yellow]Free[/bold yellow]"
|
|
567
|
-
console.print(Panel.fit(f"🚀 CoreInsight: Profiling [bold cyan]{path.name}[/bold cyan] via [bold]{provider}[/bold] · {tier_label}"))
|
|
568
|
-
|
|
569
|
-
# 1. PARSE (Synchronous, since it's fast)
|
|
570
|
-
with console.status("[yellow]Parsing AST and extracting hot loops...[/yellow]"):
|
|
571
|
-
parser = CodeParser()
|
|
572
|
-
content_bytes = path.read_bytes()
|
|
573
|
-
file_content = content_bytes.decode("utf-8", errors="replace")
|
|
574
|
-
functions = parser.parse_file(str(path), content_bytes)
|
|
565
|
+
def run_analysis(file_path: str, no_docker: bool = False, tui_console=None):
|
|
566
|
+
global console
|
|
567
|
+
_prev_console = console
|
|
575
568
|
|
|
576
|
-
if not
|
|
577
|
-
console
|
|
578
|
-
|
|
569
|
+
if tui_console is not None:
|
|
570
|
+
console = tui_console
|
|
571
|
+
|
|
572
|
+
try:
|
|
573
|
+
path = Path(file_path)
|
|
574
|
+
if not path.exists() or not path.is_file():
|
|
575
|
+
console.print(f"[red]Error: File '{file_path}' not found.[/red]")
|
|
576
|
+
sys.exit(1)
|
|
577
|
+
|
|
578
|
+
language = get_language_from_ext(path)
|
|
579
|
+
if language == "unknown":
|
|
580
|
+
console.print(f"[red]Error: Unsupported file type '{path.suffix}'. Use .cpp, .cu, or .py[/red]")
|
|
581
|
+
sys.exit(1)
|
|
582
|
+
|
|
583
|
+
config = load_config()
|
|
584
|
+
provider = config.get("provider", "ollama")
|
|
585
|
+
model_name = config.get("model_name", "llama3.2")
|
|
586
|
+
api_keys = config.get("api_keys", {})
|
|
587
|
+
tier_limits = get_tier_limits(config)
|
|
588
|
+
pro_user = is_pro(config)
|
|
589
|
+
|
|
590
|
+
# ── Preflight: fail fast before AST parsing, image building, or thread spawning ──
|
|
591
|
+
if not _preflight_checks(provider, model_name, no_docker=no_docker):
|
|
592
|
+
sys.exit(1)
|
|
593
|
+
|
|
594
|
+
tier_label = "[bold green]Pro[/bold green]" if pro_user else "[bold yellow]Free[/bold yellow]"
|
|
595
|
+
console.print(Panel.fit(f"🚀 CoreInsight: Profiling [bold cyan]{path.name}[/bold cyan] via [bold]{provider}[/bold] · {tier_label}"))
|
|
596
|
+
|
|
597
|
+
# 1. PARSE (Synchronous, since it's fast)
|
|
598
|
+
with console.status("[yellow]Parsing AST and extracting hot loops...[/yellow]"):
|
|
599
|
+
parser = CodeParser()
|
|
600
|
+
content_bytes = path.read_bytes()
|
|
601
|
+
file_content = content_bytes.decode("utf-8", errors="replace")
|
|
602
|
+
functions = parser.parse_file(str(path), content_bytes)
|
|
603
|
+
|
|
604
|
+
if not functions:
|
|
605
|
+
console.print("[red]No parseable functions found in the file.[/red]")
|
|
606
|
+
sys.exit(1)
|
|
607
|
+
|
|
608
|
+
max_fn = tier_limits["max_functions"]
|
|
609
|
+
if max_fn is not None and len(functions) > max_fn:
|
|
610
|
+
console.print(
|
|
611
|
+
f"[yellow]⚠ Free tier: analysing the first {max_fn} of {len(functions)} functions. "
|
|
612
|
+
f"Upgrade to Pro for unlimited → [cyan underline]{PRO_WAITLIST_URL}[/cyan underline][/yellow]"
|
|
613
|
+
)
|
|
614
|
+
functions = functions[:max_fn]
|
|
579
615
|
|
|
580
|
-
|
|
581
|
-
if max_fn is not None and len(functions) > max_fn:
|
|
582
|
-
console.print(
|
|
583
|
-
f"[yellow]⚠ Free tier: analysing the first {max_fn} of {len(functions)} functions. "
|
|
584
|
-
f"Upgrade to Pro for unlimited → [cyan underline]{PRO_WAITLIST_URL}[/cyan underline][/yellow]"
|
|
585
|
-
)
|
|
586
|
-
functions = functions[:max_fn]
|
|
616
|
+
console.print(f"[green]✅ Extracted {len(functions)} functional kernels.[/green]\n")
|
|
587
617
|
|
|
588
|
-
|
|
618
|
+
# Initialize heavy lifters
|
|
619
|
+
try:
|
|
620
|
+
hardware_specs_dict = HardwareDetector.get_system_specs()
|
|
621
|
+
hardware_target_str = HardwareDetector.format_for_llm(hardware_specs_dict)
|
|
622
|
+
|
|
623
|
+
console.print(f"[dim]🎯 Target Hardware Detected: {hardware_target_str}[/dim]")
|
|
624
|
+
|
|
625
|
+
model_tier = get_model_tier(provider, model_name)
|
|
626
|
+
agent = AnalyzerAgent(provider=provider, model_name=model_name, api_keys=api_keys, model_tier=model_tier)
|
|
627
|
+
sandbox = CodeSandbox(disabled=no_docker)
|
|
628
|
+
db_path = path.parent / ".coreinsight_db"
|
|
629
|
+
indexer = RepoIndexer(str(path.parent)) if db_path.exists() else None
|
|
630
|
+
profiler = HardwareProfiler() if pro_user else None
|
|
631
|
+
memory = OptimizationMemory()
|
|
632
|
+
|
|
633
|
+
# Multi-agent setup — create specialized agents if mode requires it
|
|
634
|
+
agent_mode = get_agent_mode(config)
|
|
635
|
+
multi_agents = None
|
|
636
|
+
if agent_mode == "multi":
|
|
637
|
+
multi_agents = {
|
|
638
|
+
"bottleneck": BottleneckAgent(provider=provider, model_name=model_name, api_keys=api_keys, model_tier=model_tier),
|
|
639
|
+
"optimizer": OptimizerAgent( provider=provider, model_name=model_name, api_keys=api_keys, model_tier=model_tier),
|
|
640
|
+
"harness": HarnessAgent( provider=provider, model_name=model_name, api_keys=api_keys, model_tier=model_tier),
|
|
641
|
+
"testcase": TestCaseAgent( provider=provider, model_name=model_name, api_keys=api_keys, model_tier=model_tier),
|
|
642
|
+
}
|
|
643
|
+
except Exception as e:
|
|
644
|
+
console.print(f"[red]Initialization Error:[/red] {e}")
|
|
645
|
+
sys.exit(1)
|
|
589
646
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
hardware_specs_dict = HardwareDetector.get_system_specs()
|
|
593
|
-
hardware_target_str = HardwareDetector.format_for_llm(hardware_specs_dict)
|
|
594
|
-
|
|
595
|
-
console.print(f"[dim]🎯 Target Hardware Detected: {hardware_target_str}[/dim]")
|
|
596
|
-
|
|
597
|
-
model_tier = get_model_tier(provider, model_name)
|
|
598
|
-
agent = AnalyzerAgent(provider=provider, model_name=model_name, api_keys=api_keys, model_tier=model_tier)
|
|
599
|
-
sandbox = CodeSandbox(disabled=no_docker)
|
|
600
|
-
db_path = path.parent / ".coreinsight_db"
|
|
601
|
-
indexer = RepoIndexer(str(path.parent)) if db_path.exists() else None
|
|
602
|
-
profiler = HardwareProfiler() if pro_user else None
|
|
603
|
-
memory = OptimizationMemory()
|
|
604
|
-
|
|
605
|
-
# Multi-agent setup — create specialized agents if mode requires it
|
|
606
|
-
agent_mode = get_agent_mode(config)
|
|
607
|
-
multi_agents = None
|
|
608
|
-
if agent_mode == "multi":
|
|
609
|
-
multi_agents = {
|
|
610
|
-
"bottleneck": BottleneckAgent(provider=provider, model_name=model_name, api_keys=api_keys, model_tier=model_tier),
|
|
611
|
-
"optimizer": OptimizerAgent( provider=provider, model_name=model_name, api_keys=api_keys, model_tier=model_tier),
|
|
612
|
-
"harness": HarnessAgent( provider=provider, model_name=model_name, api_keys=api_keys, model_tier=model_tier),
|
|
613
|
-
"testcase": TestCaseAgent( provider=provider, model_name=model_name, api_keys=api_keys, model_tier=model_tier),
|
|
614
|
-
}
|
|
615
|
-
except Exception as e:
|
|
616
|
-
console.print(f"[red]Initialization Error:[/red] {e}")
|
|
617
|
-
sys.exit(1)
|
|
647
|
+
mode_label = "[bold cyan]Multi-Agent[/bold cyan]" if agent_mode == "multi" else "[dim]Single-Agent[/dim]"
|
|
648
|
+
console.print(f"[dim]⚙️ Agent mode: {mode_label}[/dim]")
|
|
618
649
|
|
|
619
|
-
|
|
620
|
-
|
|
650
|
+
mem_count = memory.stats().get("count", 0)
|
|
651
|
+
if mem_count > 0:
|
|
652
|
+
console.print(
|
|
653
|
+
f"[dim]⚡ Optimization memory: [bold cyan]{mem_count}[/bold cyan] "
|
|
654
|
+
f"verified optimization(s) in local store[/dim]"
|
|
655
|
+
)
|
|
621
656
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
f"
|
|
626
|
-
f"
|
|
627
|
-
|
|
657
|
+
# Prepare Live Markdown File
|
|
658
|
+
report_path = path.with_name(f"{path.stem}_coreinsight_report.md")
|
|
659
|
+
with open(report_path, "w", encoding="utf-8") as f:
|
|
660
|
+
f.write(f"# CoreInsight Performance Report: `{path.name}`\n\n")
|
|
661
|
+
f.write("> **Note:** This file updates live as the AI completes hardware verification.\n\n---\n\n")
|
|
662
|
+
|
|
663
|
+
console.print(f"[bold blue]📄 Live report created at:[/bold blue] [underline]{report_path.absolute()}[/underline]\n")
|
|
664
|
+
console.print("[dim]Analyzing functions in parallel. Results will appear as they complete...[/dim]\n")
|
|
628
665
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
666
|
+
# 2. PARALLEL EXECUTION
|
|
667
|
+
# Limit max_workers to 4 so we don't overwhelm local Docker engine or local Ollama GPU VRAM
|
|
668
|
+
max_workers = min(4, len(functions))
|
|
669
|
+
|
|
670
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
671
|
+
# Submit all functions to the thread pool
|
|
672
|
+
future_to_func = {
|
|
673
|
+
executor.submit(process_function, func, language, agent, sandbox, indexer, hardware_target_str, tier_limits, profiler, file_content, str(path.parent), memory, agent_mode, multi_agents): func
|
|
674
|
+
for func in functions
|
|
675
|
+
}
|
|
637
676
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
" • [cyan]Cloud models[/cyan] (GPT-4o, Claude, Gemini)\n"
|
|
698
|
-
" • [cyan]Unlimited functions[/cyan] per file\n"
|
|
699
|
-
" • [cyan]5 retry attempts[/cyan] + deeper test coverage\n"
|
|
700
|
-
" • [cyan]AI-free hardware profiling[/cyan] — cProfile + perf stat evidence in every report\n\n"
|
|
701
|
-
f"[bold yellow]Pro is free during beta.[/bold yellow] Request a key:\n"
|
|
702
|
-
f"[cyan underline]{PRO_WAITLIST_URL}[/cyan underline]",
|
|
703
|
-
title="⚡ Upgrade to Pro — free during beta",
|
|
704
|
-
border_style="yellow",
|
|
705
|
-
))
|
|
677
|
+
# As each thread finishes, process its output instantly
|
|
678
|
+
for future in concurrent.futures.as_completed(future_to_func):
|
|
679
|
+
func = future_to_func[future]
|
|
680
|
+
try:
|
|
681
|
+
func_name, result, sandbox_res, msg, verification, profiler_result, memory_hit, was_valid = future.result()
|
|
682
|
+
|
|
683
|
+
# Store if the benchmark actually ran and achieved real speedup.
|
|
684
|
+
# We use is_valid_optimization (≥1.05x measured speedup) rather
|
|
685
|
+
# than verification.speedup.verified because timer resolution at
|
|
686
|
+
# small N frequently causes the cross-check to flag a discrepancy
|
|
687
|
+
# even when the optimization is genuine.
|
|
688
|
+
if (
|
|
689
|
+
memory_hit is None
|
|
690
|
+
and was_valid
|
|
691
|
+
and result is not None
|
|
692
|
+
):
|
|
693
|
+
stored = memory.store(
|
|
694
|
+
original_code=func['code'],
|
|
695
|
+
func_name=func_name,
|
|
696
|
+
language=language,
|
|
697
|
+
result=result,
|
|
698
|
+
verification=verification,
|
|
699
|
+
profiler_result=profiler_result,
|
|
700
|
+
)
|
|
701
|
+
if stored:
|
|
702
|
+
_log(func_name, "💾 Stored in optimization memory", style="dim cyan")
|
|
703
|
+
|
|
704
|
+
# 1. Format and save to Markdown file
|
|
705
|
+
output_md = format_report_markdown(func_name, result, sandbox_res, msg, language, path.parent, verification, profiler_result, memory_hit)
|
|
706
|
+
|
|
707
|
+
# Write to File (Safely)
|
|
708
|
+
with file_lock:
|
|
709
|
+
with open(report_path, "a", encoding="utf-8") as f:
|
|
710
|
+
f.write(output_md)
|
|
711
|
+
|
|
712
|
+
# 2. Print beautiful native UI to the terminal
|
|
713
|
+
with print_lock:
|
|
714
|
+
print_console_report(func_name, result, sandbox_res, msg, language, verification, profiler_result, memory_hit)
|
|
715
|
+
|
|
716
|
+
except Exception as exc:
|
|
717
|
+
with print_lock:
|
|
718
|
+
console.print(f"[bold red]❌ Critical failure in thread processing {func['name']}:[/bold red] {exc}")
|
|
719
|
+
|
|
720
|
+
console.print(Panel.fit(f"✅ [bold green]Analysis Complete![/bold green] Final report saved to:\n{report_path.absolute()}"))
|
|
721
|
+
|
|
722
|
+
if not pro_user:
|
|
723
|
+
console.print(Panel(
|
|
724
|
+
"[bold]Enjoyed CoreInsight?[/bold] Pro unlocks:\n"
|
|
725
|
+
" • [cyan]Cloud models[/cyan] (GPT-4o, Claude, Gemini)\n"
|
|
726
|
+
" • [cyan]Unlimited functions[/cyan] per file\n"
|
|
727
|
+
" • [cyan]5 retry attempts[/cyan] + deeper test coverage\n"
|
|
728
|
+
" • [cyan]AI-free hardware profiling[/cyan] — cProfile + perf stat evidence in every report\n\n"
|
|
729
|
+
f"[bold yellow]Pro is free during beta.[/bold yellow] Request a key:\n"
|
|
730
|
+
f"[cyan underline]{PRO_WAITLIST_URL}[/cyan underline]",
|
|
731
|
+
title="⚡ Upgrade to Pro — free during beta",
|
|
732
|
+
border_style="yellow",
|
|
733
|
+
))
|
|
734
|
+
finally:
|
|
735
|
+
console = _prev_console
|
|
706
736
|
|
|
707
737
|
def run_demo(lang: str = "python", no_docker: bool = False):
|
|
708
738
|
import shutil
|
|
@@ -902,6 +932,9 @@ def main_cli():
|
|
|
902
932
|
memory_parser = subparsers.add_parser("memory", help="Inspect or clear the local optimization memory")
|
|
903
933
|
memory_parser.add_argument("--clear", action="store_true", help="Wipe the memory store")
|
|
904
934
|
|
|
935
|
+
view_parser = subparsers.add_parser("view", help="Launch the interactive TUI")
|
|
936
|
+
view_parser.add_argument("--dir", default=".", help="Starting directory (default: current)")
|
|
937
|
+
|
|
905
938
|
scan_parser = subparsers.add_parser("scan", help="Scan directory for complex hotspots")
|
|
906
939
|
scan_parser.add_argument("--dir", default=".", help="Directory to scan")
|
|
907
940
|
scan_parser.add_argument("--top", type=int, default=10, help="Number of hotspots to show")
|
|
@@ -913,6 +946,9 @@ def main_cli():
|
|
|
913
946
|
pro_key=getattr(args, "pro_key", None),
|
|
914
947
|
agent_mode=getattr(args, "agent_mode", None),
|
|
915
948
|
)
|
|
949
|
+
elif args.command == "view":
|
|
950
|
+
from coreinsight.tui import run_tui
|
|
951
|
+
run_tui(start_dir=getattr(args, "dir", "."))
|
|
916
952
|
elif args.command == "demo":
|
|
917
953
|
run_demo(getattr(args, "lang", "python"), no_docker=getattr(args, "no_docker", False))
|
|
918
954
|
elif args.command == "analyze":
|
|
@@ -26,6 +26,7 @@ DOCKERFILES = {
|
|
|
26
26
|
# ---------------------------------------------------------------------------
|
|
27
27
|
# Verification constants
|
|
28
28
|
# ---------------------------------------------------------------------------
|
|
29
|
+
SANDBOX_SKIPPED_MSG = "Verification skipped (--no-docker)."
|
|
29
30
|
SPEEDUP_DISCREPANCY_TOLERANCE = 0.05 # max relative delta: computed vs reported speedup
|
|
30
31
|
MIN_TIMING_ROWS = 2 # minimum CSV rows to trust timing statistics
|
|
31
32
|
FLOAT_RTOL = 1e-5 # relative tolerance for output comparison
|
|
@@ -203,7 +204,7 @@ class CodeSandbox:
|
|
|
203
204
|
|
|
204
205
|
def execute_benchmark(self, code: str, language: str = "cpp", timeout_seconds: int = 120) -> Tuple[bool, str, Optional[bytes]]:
|
|
205
206
|
if self.disabled:
|
|
206
|
-
return False,
|
|
207
|
+
return False, SANDBOX_SKIPPED_MSG, None
|
|
207
208
|
if not self.client:
|
|
208
209
|
return False, "Docker is not running on the host machine.", None
|
|
209
210
|
|
|
@@ -292,8 +293,8 @@ class CodeSandbox:
|
|
|
292
293
|
) -> VerificationResult:
|
|
293
294
|
if self.disabled:
|
|
294
295
|
return VerificationResult(
|
|
295
|
-
speedup=SpeedupVerification(verified=False, details=
|
|
296
|
-
correctness=CorrectnessVerification(verified=False, details=
|
|
296
|
+
speedup=SpeedupVerification(verified=False, details=SANDBOX_SKIPPED_MSG),
|
|
297
|
+
correctness=CorrectnessVerification(verified=False, details=SANDBOX_SKIPPED_MSG),
|
|
297
298
|
)
|
|
298
299
|
speedup_result = self._verify_speedup(csv_output)
|
|
299
300
|
correctness_result = self._verify_correctness(
|
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"""
|
|
2
|
+
coreinsight/tui.py — Interactive TUI for CoreInsight
|
|
3
|
+
Launch with: coreinsight view
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from textual import on, work
|
|
15
|
+
from textual.app import App, ComposeResult
|
|
16
|
+
from textual.binding import Binding
|
|
17
|
+
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
|
18
|
+
from textual.screen import ModalScreen
|
|
19
|
+
from textual.widgets import (
|
|
20
|
+
Button,
|
|
21
|
+
Checkbox,
|
|
22
|
+
DirectoryTree,
|
|
23
|
+
Footer,
|
|
24
|
+
Header,
|
|
25
|
+
Label,
|
|
26
|
+
RichLog,
|
|
27
|
+
Static,
|
|
28
|
+
Switch,
|
|
29
|
+
)
|
|
30
|
+
from textual.widgets._directory_tree import DirEntry
|
|
31
|
+
|
|
32
|
+
from coreinsight.config import (
|
|
33
|
+
load_config,
|
|
34
|
+
is_pro,
|
|
35
|
+
get_tier_limits,
|
|
36
|
+
PRO_WAITLIST_URL,
|
|
37
|
+
)
|
|
38
|
+
from coreinsight.memory import OptimizationMemory
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Sentinel used to detect --no-docker skips (imported for display logic)
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
try:
|
|
44
|
+
from coreinsight.sandbox import SANDBOX_SKIPPED_MSG
|
|
45
|
+
except ImportError:
|
|
46
|
+
SANDBOX_SKIPPED_MSG = "Verification skipped (--no-docker)."
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# TuiConsole — drop-in replacement for rich.Console that writes to RichLog
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
class _NoopStatus:
|
|
54
|
+
"""Stub so console.status(...) calls don't crash inside the TUI."""
|
|
55
|
+
def __enter__(self): return self
|
|
56
|
+
def __exit__(self, *_): pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TuiConsole:
|
|
60
|
+
"""
|
|
61
|
+
Mimics the rich.Console API used in main.py so run_analysis() output
|
|
62
|
+
is captured and displayed in the TUI's RichLog widget in real time.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, log_widget: RichLog):
|
|
66
|
+
self._log = log_widget
|
|
67
|
+
self._lock = threading.Lock()
|
|
68
|
+
|
|
69
|
+
# Main output method — handles str, Rich renderables, Panel, etc.
|
|
70
|
+
def print(self, *args, **kwargs):
|
|
71
|
+
with self._lock:
|
|
72
|
+
for arg in args:
|
|
73
|
+
try:
|
|
74
|
+
self._log.write(arg)
|
|
75
|
+
except Exception:
|
|
76
|
+
self._log.write(str(arg))
|
|
77
|
+
|
|
78
|
+
# console.log() is used by _log() helper in main.py
|
|
79
|
+
def log(self, *args, **kwargs):
|
|
80
|
+
self.print(*args)
|
|
81
|
+
|
|
82
|
+
# console.status() is used for spinners — no-op in TUI
|
|
83
|
+
def status(self, *args, **kwargs):
|
|
84
|
+
return _NoopStatus()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Memory modal
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
class MemoryModal(ModalScreen):
|
|
92
|
+
"""Full-screen modal showing the optimization memory store."""
|
|
93
|
+
|
|
94
|
+
BINDINGS = [Binding("escape,q", "dismiss", "Close")]
|
|
95
|
+
|
|
96
|
+
DEFAULT_CSS = """
|
|
97
|
+
MemoryModal {
|
|
98
|
+
align: center middle;
|
|
99
|
+
}
|
|
100
|
+
MemoryModal > Container {
|
|
101
|
+
width: 90%;
|
|
102
|
+
height: 80%;
|
|
103
|
+
background: $surface;
|
|
104
|
+
border: thick $primary;
|
|
105
|
+
padding: 1 2;
|
|
106
|
+
}
|
|
107
|
+
MemoryModal #memory-log {
|
|
108
|
+
height: 1fr;
|
|
109
|
+
}
|
|
110
|
+
MemoryModal #close-memory {
|
|
111
|
+
margin-top: 1;
|
|
112
|
+
width: 100%;
|
|
113
|
+
}
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def compose(self) -> ComposeResult:
|
|
117
|
+
with Container():
|
|
118
|
+
yield Label("Optimization Memory", id="memory-title")
|
|
119
|
+
yield RichLog(id="memory-log", highlight=True, markup=True)
|
|
120
|
+
yield Button("Close [Esc]", id="close-memory", variant="default")
|
|
121
|
+
|
|
122
|
+
def on_mount(self) -> None:
|
|
123
|
+
log = self.query_one("#memory-log", RichLog)
|
|
124
|
+
self._populate(log)
|
|
125
|
+
|
|
126
|
+
def _populate(self, log: RichLog) -> None:
|
|
127
|
+
from rich.table import Table
|
|
128
|
+
|
|
129
|
+
mem = OptimizationMemory()
|
|
130
|
+
stats = mem.stats()
|
|
131
|
+
count = stats.get("count", 0)
|
|
132
|
+
|
|
133
|
+
if count == 0:
|
|
134
|
+
log.write("[dim]No optimizations stored yet.[/dim]")
|
|
135
|
+
log.write(
|
|
136
|
+
f"\nRun [cyan]coreinsight analyze[/cyan] or use the Analyze action "
|
|
137
|
+
f"to start building your memory store."
|
|
138
|
+
)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
if not mem._ensure_db():
|
|
143
|
+
log.write("[red]Could not open memory store.[/red]")
|
|
144
|
+
return
|
|
145
|
+
all_records = mem._collection.get(include=["metadatas"])
|
|
146
|
+
metadatas = all_records.get("metadatas", []) or []
|
|
147
|
+
ids = all_records.get("ids", []) or []
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
log.write(f"[red]Failed to read memory store: {exc}[/red]")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
table = Table(
|
|
153
|
+
show_header=True, header_style="bold cyan",
|
|
154
|
+
expand=True, show_lines=True,
|
|
155
|
+
)
|
|
156
|
+
table.add_column("#", justify="right", style="dim", width=4)
|
|
157
|
+
table.add_column("Function", justify="left", style="bold white")
|
|
158
|
+
table.add_column("Language", justify="center", style="cyan", width=10)
|
|
159
|
+
table.add_column("Speedup", justify="right", style="bold green", width=9)
|
|
160
|
+
table.add_column("Severity", justify="center", width=10)
|
|
161
|
+
table.add_column("Issue", justify="left", style="dim white")
|
|
162
|
+
table.add_column("Verified", justify="left", style="dim", width=20)
|
|
163
|
+
|
|
164
|
+
severity_colors = {
|
|
165
|
+
"Critical": "red", "High": "yellow",
|
|
166
|
+
"Medium": "cyan", "Low": "green",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
paired = sorted(
|
|
170
|
+
zip(metadatas, ids),
|
|
171
|
+
key=lambda x: x[0].get("timestamp", ""),
|
|
172
|
+
reverse=True,
|
|
173
|
+
)
|
|
174
|
+
for i, (meta, rid) in enumerate(paired, start=1):
|
|
175
|
+
sev = meta.get("severity", "High")
|
|
176
|
+
sev_c = severity_colors.get(sev, "white")
|
|
177
|
+
ts = meta.get("timestamp", "")[:19].replace("T", " ")
|
|
178
|
+
issue = (meta.get("issue", "") or "—")[:55]
|
|
179
|
+
if len(meta.get("issue", "")) > 55:
|
|
180
|
+
issue += "…"
|
|
181
|
+
table.add_row(
|
|
182
|
+
str(i),
|
|
183
|
+
meta.get("func_name", rid[:12]),
|
|
184
|
+
meta.get("language", "?"),
|
|
185
|
+
f"{float(meta.get('avg_speedup', 0)):.2f}x",
|
|
186
|
+
f"[{sev_c}]{sev}[/{sev_c}]",
|
|
187
|
+
issue,
|
|
188
|
+
ts,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
log.write(table)
|
|
192
|
+
log.write(f"\n[dim]Store location: ~/.coreinsight/memory_db[/dim]")
|
|
193
|
+
|
|
194
|
+
@on(Button.Pressed, "#close-memory")
|
|
195
|
+
def close(self) -> None:
|
|
196
|
+
self.dismiss()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Confirm modal — used for destructive actions like memory clear
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
class ConfirmModal(ModalScreen[bool]):
|
|
204
|
+
"""Simple yes/no confirmation dialog."""
|
|
205
|
+
|
|
206
|
+
BINDINGS = [Binding("escape", "dismiss_false", "Cancel")]
|
|
207
|
+
|
|
208
|
+
DEFAULT_CSS = """
|
|
209
|
+
ConfirmModal {
|
|
210
|
+
align: center middle;
|
|
211
|
+
}
|
|
212
|
+
ConfirmModal > Container {
|
|
213
|
+
width: 50;
|
|
214
|
+
height: 10;
|
|
215
|
+
background: $surface;
|
|
216
|
+
border: thick $warning;
|
|
217
|
+
padding: 1 2;
|
|
218
|
+
align: center middle;
|
|
219
|
+
}
|
|
220
|
+
ConfirmModal Horizontal {
|
|
221
|
+
align: center middle;
|
|
222
|
+
margin-top: 1;
|
|
223
|
+
}
|
|
224
|
+
ConfirmModal Button {
|
|
225
|
+
margin: 0 1;
|
|
226
|
+
}
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(self, message: str, **kwargs):
|
|
230
|
+
super().__init__(**kwargs)
|
|
231
|
+
self._message = message
|
|
232
|
+
|
|
233
|
+
def compose(self) -> ComposeResult:
|
|
234
|
+
with Container():
|
|
235
|
+
yield Label(self._message, id="confirm-msg")
|
|
236
|
+
with Horizontal():
|
|
237
|
+
yield Button("Yes", id="confirm-yes", variant="error")
|
|
238
|
+
yield Button("No", id="confirm-no", variant="default")
|
|
239
|
+
|
|
240
|
+
@on(Button.Pressed, "#confirm-yes")
|
|
241
|
+
def yes(self) -> None:
|
|
242
|
+
self.dismiss(True)
|
|
243
|
+
|
|
244
|
+
@on(Button.Pressed, "#confirm-no")
|
|
245
|
+
def no(self) -> None:
|
|
246
|
+
self.dismiss(False)
|
|
247
|
+
|
|
248
|
+
def action_dismiss_false(self) -> None:
|
|
249
|
+
self.dismiss(False)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
# Settings modal
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
class SettingsModal(ModalScreen):
|
|
257
|
+
"""Read-only settings viewer. Directs user to coreinsight configure for changes."""
|
|
258
|
+
|
|
259
|
+
BINDINGS = [Binding("escape,q", "dismiss", "Close")]
|
|
260
|
+
|
|
261
|
+
DEFAULT_CSS = """
|
|
262
|
+
SettingsModal {
|
|
263
|
+
align: center middle;
|
|
264
|
+
}
|
|
265
|
+
SettingsModal > Container {
|
|
266
|
+
width: 60;
|
|
267
|
+
height: 22;
|
|
268
|
+
background: $surface;
|
|
269
|
+
border: thick $primary;
|
|
270
|
+
padding: 1 2;
|
|
271
|
+
}
|
|
272
|
+
SettingsModal .setting-row {
|
|
273
|
+
height: 1;
|
|
274
|
+
margin-bottom: 1;
|
|
275
|
+
}
|
|
276
|
+
SettingsModal #close-settings {
|
|
277
|
+
margin-top: 1;
|
|
278
|
+
width: 100%;
|
|
279
|
+
}
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def compose(self) -> ComposeResult:
|
|
283
|
+
config = load_config()
|
|
284
|
+
pro_user = is_pro(config)
|
|
285
|
+
provider = config.get("provider", "ollama")
|
|
286
|
+
model = config.get("model_name", "llama3.2")
|
|
287
|
+
tier = "Pro" if pro_user else "Free"
|
|
288
|
+
tier_col = "bold green" if pro_user else "bold yellow"
|
|
289
|
+
agent_mode = config.get("agent_mode", "auto")
|
|
290
|
+
|
|
291
|
+
with Container():
|
|
292
|
+
yield Label("Current Configuration", id="settings-title")
|
|
293
|
+
yield Static(f" Tier : [{tier_col}]{tier}[/{tier_col}]", classes="setting-row")
|
|
294
|
+
yield Static(f" Provider : [cyan]{provider}[/cyan]", classes="setting-row")
|
|
295
|
+
yield Static(f" Model : [cyan]{model}[/cyan]", classes="setting-row")
|
|
296
|
+
yield Static(f" Agent mode: [cyan]{agent_mode}[/cyan]", classes="setting-row")
|
|
297
|
+
yield Static("", classes="setting-row")
|
|
298
|
+
yield Static(
|
|
299
|
+
" To change settings, run:\n"
|
|
300
|
+
" [cyan]coreinsight configure[/cyan]",
|
|
301
|
+
classes="setting-row",
|
|
302
|
+
)
|
|
303
|
+
if not pro_user:
|
|
304
|
+
yield Static(
|
|
305
|
+
f"\n [yellow]Unlock Pro (free during beta):[/yellow]\n"
|
|
306
|
+
f" [cyan underline]{PRO_WAITLIST_URL}[/cyan underline]",
|
|
307
|
+
classes="setting-row",
|
|
308
|
+
)
|
|
309
|
+
yield Button("Close [Esc]", id="close-settings", variant="default")
|
|
310
|
+
|
|
311
|
+
@on(Button.Pressed, "#close-settings")
|
|
312
|
+
def close(self) -> None:
|
|
313
|
+
self.dismiss()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
# Main TUI App
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
class CoreInsightApp(App):
|
|
321
|
+
|
|
322
|
+
TITLE = "CoreInsight"
|
|
323
|
+
CSS_PATH = None # all CSS inline below
|
|
324
|
+
|
|
325
|
+
BINDINGS = [
|
|
326
|
+
Binding("q", "quit", "Quit"),
|
|
327
|
+
Binding("a", "analyze", "Analyze"),
|
|
328
|
+
Binding("i", "index", "Index"),
|
|
329
|
+
Binding("m", "view_memory", "Memory"),
|
|
330
|
+
Binding("s", "view_settings", "Settings"),
|
|
331
|
+
Binding("ctrl+c","quit", "Quit", show=False),
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
CSS = """
|
|
335
|
+
/* ── Layout ─────────────────────────────────────────────────────────── */
|
|
336
|
+
Screen {
|
|
337
|
+
layout: vertical;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#main-area {
|
|
341
|
+
layout: horizontal;
|
|
342
|
+
height: 1fr;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* ── Left panel ─────────────────────────────────────────────────────── */
|
|
346
|
+
#left-panel {
|
|
347
|
+
width: 32;
|
|
348
|
+
min-width: 24;
|
|
349
|
+
layout: vertical;
|
|
350
|
+
border-right: solid $primary-darken-2;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#file-panel {
|
|
354
|
+
height: 1fr;
|
|
355
|
+
border-bottom: solid $primary-darken-2;
|
|
356
|
+
padding: 0 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#file-label {
|
|
360
|
+
background: $primary-darken-2;
|
|
361
|
+
color: $text;
|
|
362
|
+
padding: 0 1;
|
|
363
|
+
height: 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#file-tree {
|
|
367
|
+
height: 1fr;
|
|
368
|
+
padding: 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
#selected-count {
|
|
372
|
+
height: 1;
|
|
373
|
+
padding: 0 1;
|
|
374
|
+
color: $text-muted;
|
|
375
|
+
background: $surface-darken-1;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* ── Action panel ───────────────────────────────────────────────────── */
|
|
379
|
+
#action-panel {
|
|
380
|
+
height: auto;
|
|
381
|
+
padding: 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#action-label {
|
|
385
|
+
color: $text-muted;
|
|
386
|
+
margin-bottom: 1;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#action-panel Button {
|
|
390
|
+
width: 100%;
|
|
391
|
+
margin-bottom: 1;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#no-docker-row {
|
|
395
|
+
height: 3;
|
|
396
|
+
align: left middle;
|
|
397
|
+
margin-top: 1;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#no-docker-label {
|
|
401
|
+
padding: 0 1;
|
|
402
|
+
color: $text-muted;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* ── Right panel (output) ───────────────────────────────────────────── */
|
|
406
|
+
#right-panel {
|
|
407
|
+
width: 1fr;
|
|
408
|
+
layout: vertical;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#output-label {
|
|
412
|
+
background: $primary-darken-2;
|
|
413
|
+
color: $text;
|
|
414
|
+
padding: 0 1;
|
|
415
|
+
height: 1;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
#output-log {
|
|
419
|
+
height: 1fr;
|
|
420
|
+
padding: 0 1;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* ── Status bar ─────────────────────────────────────────────────────── */
|
|
424
|
+
#status-bar {
|
|
425
|
+
height: 1;
|
|
426
|
+
background: $primary-darken-3;
|
|
427
|
+
color: $text-muted;
|
|
428
|
+
padding: 0 1;
|
|
429
|
+
dock: bottom;
|
|
430
|
+
}
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
def __init__(self, start_dir: str = "."):
|
|
434
|
+
super().__init__()
|
|
435
|
+
self._start_dir = str(Path(start_dir).resolve())
|
|
436
|
+
self._selected: set[str] = set()
|
|
437
|
+
self._busy = False
|
|
438
|
+
|
|
439
|
+
config = load_config()
|
|
440
|
+
self._pro = is_pro(config)
|
|
441
|
+
self._tier_limits = get_tier_limits(config)
|
|
442
|
+
|
|
443
|
+
# ── Compose ──────────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
def compose(self) -> ComposeResult:
|
|
446
|
+
yield Header(show_clock=True)
|
|
447
|
+
|
|
448
|
+
with Container(id="main-area"):
|
|
449
|
+
|
|
450
|
+
# Left panel
|
|
451
|
+
with Vertical(id="left-panel"):
|
|
452
|
+
with Vertical(id="file-panel"):
|
|
453
|
+
yield Label(" Files", id="file-label")
|
|
454
|
+
yield DirectoryTree(self._start_dir, id="file-tree")
|
|
455
|
+
yield Label("No files selected", id="selected-count")
|
|
456
|
+
|
|
457
|
+
with Vertical(id="action-panel"):
|
|
458
|
+
yield Label("Actions", id="action-label")
|
|
459
|
+
yield Button("Analyze [a]", id="btn-analyze", variant="success")
|
|
460
|
+
yield Button("Index [i]", id="btn-index", variant="primary")
|
|
461
|
+
yield Button("Memory [m]", id="btn-memory", variant="default")
|
|
462
|
+
yield Button("Settings [s]", id="btn-settings", variant="default")
|
|
463
|
+
with Horizontal(id="no-docker-row"):
|
|
464
|
+
yield Switch(value=False, id="no-docker-switch")
|
|
465
|
+
yield Label("Skip Docker", id="no-docker-label")
|
|
466
|
+
|
|
467
|
+
# Right panel — live output
|
|
468
|
+
with Vertical(id="right-panel"):
|
|
469
|
+
yield Label(" Output", id="output-label")
|
|
470
|
+
yield RichLog(
|
|
471
|
+
id="output-log",
|
|
472
|
+
highlight=True,
|
|
473
|
+
markup=True,
|
|
474
|
+
wrap=True,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
yield Label(" Ready — select files to begin.", id="status-bar")
|
|
478
|
+
yield Footer()
|
|
479
|
+
|
|
480
|
+
def on_mount(self) -> None:
|
|
481
|
+
log = self.query_one("#output-log", RichLog)
|
|
482
|
+
config = load_config()
|
|
483
|
+
pro_user = is_pro(config)
|
|
484
|
+
tier_str = "[bold green]Pro[/bold green]" if pro_user else "[bold yellow]Free[/bold yellow]"
|
|
485
|
+
|
|
486
|
+
log.write(f"[bold cyan]CoreInsight[/bold cyan] {tier_str}")
|
|
487
|
+
log.write("[dim]Select one or more files in the browser, then press Analyze.[/dim]")
|
|
488
|
+
if not pro_user:
|
|
489
|
+
log.write(
|
|
490
|
+
f"[dim]Free tier: up to 2 files per run. "
|
|
491
|
+
f"Pro is free during beta → [cyan]{PRO_WAITLIST_URL}[/cyan][/dim]"
|
|
492
|
+
)
|
|
493
|
+
log.write("")
|
|
494
|
+
|
|
495
|
+
# ── File selection ────────────────────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
@on(DirectoryTree.FileSelected)
|
|
498
|
+
def file_selected(self, event: DirectoryTree.FileSelected) -> None:
|
|
499
|
+
path = str(event.path)
|
|
500
|
+
|
|
501
|
+
# Only allow supported file types
|
|
502
|
+
supported = {".py", ".cpp", ".cc", ".h", ".hpp", ".cu", ".cuh"}
|
|
503
|
+
if Path(path).suffix.lower() not in supported:
|
|
504
|
+
self._set_status(f"Unsupported file type: {Path(path).suffix}")
|
|
505
|
+
return
|
|
506
|
+
|
|
507
|
+
# Toggle selection
|
|
508
|
+
if path in self._selected:
|
|
509
|
+
self._selected.discard(path)
|
|
510
|
+
else:
|
|
511
|
+
# Enforce free-tier file limit
|
|
512
|
+
max_files = self._tier_limits.get("max_files")
|
|
513
|
+
if max_files and len(self._selected) >= max_files:
|
|
514
|
+
log = self.query_one("#output-log", RichLog)
|
|
515
|
+
log.write(
|
|
516
|
+
f"\n[yellow]Free tier: max {max_files} file(s) per run. "
|
|
517
|
+
f"Upgrade to Pro for unlimited → [cyan]{PRO_WAITLIST_URL}[/cyan][/yellow]"
|
|
518
|
+
)
|
|
519
|
+
self._set_status(f"Free tier: max {max_files} files. Upgrade to Pro for unlimited.")
|
|
520
|
+
return
|
|
521
|
+
self._selected.add(path)
|
|
522
|
+
|
|
523
|
+
self._refresh_selected_label()
|
|
524
|
+
|
|
525
|
+
def _refresh_selected_label(self) -> None:
|
|
526
|
+
label = self.query_one("#selected-count", Label)
|
|
527
|
+
n = len(self._selected)
|
|
528
|
+
if n == 0:
|
|
529
|
+
label.update("No files selected")
|
|
530
|
+
elif n == 1:
|
|
531
|
+
name = Path(next(iter(self._selected))).name
|
|
532
|
+
label.update(f"1 selected: {name}")
|
|
533
|
+
else:
|
|
534
|
+
label.update(f"{n} files selected")
|
|
535
|
+
|
|
536
|
+
# ── Actions ───────────────────────────────────────────────────────────
|
|
537
|
+
|
|
538
|
+
@on(Button.Pressed, "#btn-analyze")
|
|
539
|
+
def action_analyze(self) -> None:
|
|
540
|
+
if self._busy:
|
|
541
|
+
self._set_status("Already running — please wait.")
|
|
542
|
+
return
|
|
543
|
+
if not self._selected:
|
|
544
|
+
self._set_status("Select at least one file first.")
|
|
545
|
+
return
|
|
546
|
+
no_docker = self.query_one("#no-docker-switch", Switch).value
|
|
547
|
+
self._start_analysis(list(self._selected), no_docker)
|
|
548
|
+
|
|
549
|
+
@on(Button.Pressed, "#btn-index")
|
|
550
|
+
def action_index(self) -> None:
|
|
551
|
+
if self._busy:
|
|
552
|
+
self._set_status("Already running — please wait.")
|
|
553
|
+
return
|
|
554
|
+
if not self._selected:
|
|
555
|
+
self._set_status("Select at least one file to determine its directory.")
|
|
556
|
+
return
|
|
557
|
+
# Index the parent directory of the first selected file
|
|
558
|
+
target_dir = str(Path(next(iter(self._selected))).parent)
|
|
559
|
+
self._start_index(target_dir)
|
|
560
|
+
|
|
561
|
+
@on(Button.Pressed, "#btn-memory")
|
|
562
|
+
def action_view_memory(self) -> None:
|
|
563
|
+
self.push_screen(MemoryModal())
|
|
564
|
+
|
|
565
|
+
@on(Button.Pressed, "#btn-settings")
|
|
566
|
+
def action_view_settings(self) -> None:
|
|
567
|
+
self.push_screen(SettingsModal())
|
|
568
|
+
|
|
569
|
+
# ── Workers ───────────────────────────────────────────────────────────
|
|
570
|
+
|
|
571
|
+
@work(thread=True)
|
|
572
|
+
def _start_analysis(self, files: list[str], no_docker: bool) -> None:
|
|
573
|
+
from coreinsight.main import run_analysis
|
|
574
|
+
|
|
575
|
+
log = self.query_one("#output-log", RichLog)
|
|
576
|
+
tui_console = TuiConsole(log)
|
|
577
|
+
self._busy = True
|
|
578
|
+
|
|
579
|
+
for i, file_path in enumerate(files, 1):
|
|
580
|
+
name = Path(file_path).name
|
|
581
|
+
self.call_from_thread(
|
|
582
|
+
self._set_status,
|
|
583
|
+
f"Analyzing {name} ({i}/{len(files)})...",
|
|
584
|
+
)
|
|
585
|
+
self.call_from_thread(
|
|
586
|
+
log.write,
|
|
587
|
+
f"\n[bold cyan]{'─' * 60}[/bold cyan]"
|
|
588
|
+
f"\n[bold]Analyzing:[/bold] [cyan]{name}[/cyan]\n",
|
|
589
|
+
)
|
|
590
|
+
try:
|
|
591
|
+
run_analysis(file_path, no_docker=no_docker, tui_console=tui_console)
|
|
592
|
+
except SystemExit:
|
|
593
|
+
# run_analysis calls sys.exit(1) on bad input — catch it
|
|
594
|
+
pass
|
|
595
|
+
except Exception as exc:
|
|
596
|
+
self.call_from_thread(
|
|
597
|
+
log.write,
|
|
598
|
+
f"[red]Unexpected error analyzing {name}: {exc}[/red]",
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
self._busy = False
|
|
602
|
+
self.call_from_thread(
|
|
603
|
+
self._set_status,
|
|
604
|
+
f"Done. {len(files)} file(s) analyzed.",
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
@work(thread=True)
|
|
608
|
+
def _start_index(self, target_dir: str) -> None:
|
|
609
|
+
from coreinsight.indexer import RepoIndexer
|
|
610
|
+
|
|
611
|
+
log = self.query_one("#output-log", RichLog)
|
|
612
|
+
self._busy = True
|
|
613
|
+
self.call_from_thread(
|
|
614
|
+
self._set_status,
|
|
615
|
+
f"Indexing {target_dir}...",
|
|
616
|
+
)
|
|
617
|
+
self.call_from_thread(
|
|
618
|
+
log.write,
|
|
619
|
+
f"\n[bold cyan]Indexing directory:[/bold cyan] [cyan]{target_dir}[/cyan]\n",
|
|
620
|
+
)
|
|
621
|
+
try:
|
|
622
|
+
indexer = RepoIndexer(target_dir)
|
|
623
|
+
indexer.index_repository()
|
|
624
|
+
self.call_from_thread(
|
|
625
|
+
log.write,
|
|
626
|
+
"[bold green]Indexing complete.[/bold green]\n",
|
|
627
|
+
)
|
|
628
|
+
except Exception as exc:
|
|
629
|
+
self.call_from_thread(
|
|
630
|
+
log.write,
|
|
631
|
+
f"[red]Indexing failed: {exc}[/red]\n",
|
|
632
|
+
)
|
|
633
|
+
finally:
|
|
634
|
+
self._busy = False
|
|
635
|
+
self.call_from_thread(self._set_status, "Indexing done.")
|
|
636
|
+
|
|
637
|
+
# ── Helpers ───────────────────────────────────────────────────────────
|
|
638
|
+
|
|
639
|
+
def _set_status(self, msg: str) -> None:
|
|
640
|
+
self.query_one("#status-bar", Label).update(f" {msg}")
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
# ---------------------------------------------------------------------------
|
|
644
|
+
# Entry point called from main.py
|
|
645
|
+
# ---------------------------------------------------------------------------
|
|
646
|
+
|
|
647
|
+
def run_tui(start_dir: str = ".") -> None:
|
|
648
|
+
"""Launch the CoreInsight TUI. Called by `coreinsight view`."""
|
|
649
|
+
app = CoreInsightApp(start_dir=start_dir)
|
|
650
|
+
app.run()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coreinsight-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Local-first AI performance profiler that mathematically verifies optimizations for Python, C++, and CUDA
|
|
5
5
|
Author: Varun Jani
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -32,6 +32,7 @@ Requires-Dist: langchain-anthropic>=0.1.0
|
|
|
32
32
|
Requires-Dist: pydantic>=2.0
|
|
33
33
|
Requires-Dist: chromadb>=0.5.0
|
|
34
34
|
Requires-Dist: sentence-transformers>=3.0.0
|
|
35
|
+
Requires-Dist: textual>=0.60.0
|
|
35
36
|
Requires-Dist: psutil>=5.9
|
|
36
37
|
Provides-Extra: compat
|
|
37
38
|
Requires-Dist: pysqlite3-binary>=0.5.0; extra == "compat"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "coreinsight-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.7"
|
|
8
8
|
description = "Local-first AI performance profiler that mathematically verifies optimizations for Python, C++, and CUDA"
|
|
9
9
|
license = {text = "GPL-3.0-or-later"}
|
|
10
10
|
authors = [
|
|
@@ -38,6 +38,7 @@ dependencies = [
|
|
|
38
38
|
"pydantic>=2.0",
|
|
39
39
|
"chromadb>=0.5.0",
|
|
40
40
|
"sentence-transformers>=3.0.0",
|
|
41
|
+
"textual>=0.60.0",
|
|
41
42
|
"psutil>=5.9",
|
|
42
43
|
]
|
|
43
44
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|