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.
Files changed (30) hide show
  1. {coreinsight_cli-0.2.6/coreinsight_cli.egg-info → coreinsight_cli-0.2.7}/PKG-INFO +2 -1
  2. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/config.py +3 -0
  3. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/main.py +196 -160
  4. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/sandbox.py +4 -3
  5. coreinsight_cli-0.2.7/coreinsight/tui.py +650 -0
  6. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7/coreinsight_cli.egg-info}/PKG-INFO +2 -1
  7. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight_cli.egg-info/SOURCES.txt +1 -0
  8. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight_cli.egg-info/requires.txt +1 -0
  9. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/pyproject.toml +2 -1
  10. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/LICENSE +0 -0
  11. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/README.md +0 -0
  12. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/Dockerfile.cpp-sandbox +0 -0
  13. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/Dockerfile.python-sandbox +0 -0
  14. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/__init__.py +0 -0
  15. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/analyzer.py +0 -0
  16. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/demo/__init__.py +0 -0
  17. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/demo/bad_loop.py +0 -0
  18. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/demo/data_processor.py +0 -0
  19. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/demo/slow.cpp +0 -0
  20. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/hardware.py +0 -0
  21. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/indexer.py +0 -0
  22. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/memory.py +0 -0
  23. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/parser.py +0 -0
  24. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/profiler.py +0 -0
  25. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/prompts.py +0 -0
  26. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight/scanner.py +0 -0
  27. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight_cli.egg-info/dependency_links.txt +0 -0
  28. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight_cli.egg-info/entry_points.txt +0 -0
  29. {coreinsight_cli-0.2.6 → coreinsight_cli-0.2.7}/coreinsight_cli.egg-info/top_level.txt +0 -0
  30. {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.6
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
- logs += "\nERROR: Script ran but DID NOT print the CSV table. You MUST print the strict CSV format."
108
+ error_hint = "\nERROR: Script ran but DID NOT print the CSV table. You MUST print the strict CSV format."
107
109
  else:
108
- logs += "\nERROR: Optimized code was SLOWER. Rewrite to be faster."
109
- harness_code = agent.fix_harness(func_name, original_code, harness_code, logs, language, context=context)
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 is_valid and retry_count > 0:
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
- if is_valid_optimization:
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
- path = Path(file_path)
546
- if not path.exists() or not path.is_file():
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 functions:
577
- console.print("[red]No parseable functions found in the file.[/red]")
578
- sys.exit(1)
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
- max_fn = tier_limits["max_functions"]
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
- console.print(f"[green]✅ Extracted {len(functions)} functional kernels.[/green]\n")
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
- # Initialize heavy lifters
591
- try:
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
- mode_label = "[bold cyan]Multi-Agent[/bold cyan]" if agent_mode == "multi" else "[dim]Single-Agent[/dim]"
620
- console.print(f"[dim]⚙️ Agent mode: {mode_label}[/dim]")
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
- mem_count = memory.stats().get("count", 0)
623
- if mem_count > 0:
624
- console.print(
625
- f"[dim]⚡ Optimization memory: [bold cyan]{mem_count}[/bold cyan] "
626
- f"verified optimization(s) in local store[/dim]"
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
- # Prepare Live Markdown File
630
- report_path = path.with_name(f"{path.stem}_coreinsight_report.md")
631
- with open(report_path, "w", encoding="utf-8") as f:
632
- f.write(f"# CoreInsight Performance Report: `{path.name}`\n\n")
633
- f.write("> **Note:** This file updates live as the AI completes hardware verification.\n\n---\n\n")
634
-
635
- console.print(f"[bold blue]📄 Live report created at:[/bold blue] [underline]{report_path.absolute()}[/underline]\n")
636
- console.print("[dim]Analyzing functions in parallel. Results will appear as they complete...[/dim]\n")
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
- # 2. PARALLEL EXECUTION
639
- # Limit max_workers to 4 so we don't overwhelm local Docker engine or local Ollama GPU VRAM
640
- max_workers = min(4, len(functions))
641
-
642
- with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
643
- # Submit all functions to the thread pool
644
- future_to_func = {
645
- 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
646
- for func in functions
647
- }
648
-
649
- # As each thread finishes, process its output instantly
650
- for future in concurrent.futures.as_completed(future_to_func):
651
- func = future_to_func[future]
652
- try:
653
- func_name, result, sandbox_res, msg, verification, profiler_result, memory_hit, was_valid = future.result()
654
-
655
- # Store if the benchmark actually ran and achieved real speedup.
656
- # We use is_valid_optimization (≥1.05x measured speedup) rather
657
- # than verification.speedup.verified because timer resolution at
658
- # small N frequently causes the cross-check to flag a discrepancy
659
- # even when the optimization is genuine.
660
- if (
661
- memory_hit is None
662
- and was_valid
663
- and result is not None
664
- ):
665
- stored = memory.store(
666
- original_code=func['code'],
667
- func_name=func_name,
668
- language=language,
669
- result=result,
670
- verification=verification,
671
- profiler_result=profiler_result,
672
- )
673
- if stored:
674
- _log(func_name, "💾 Stored in optimization memory", style="dim cyan")
675
-
676
- # 1. Format and save to Markdown file
677
- output_md = format_report_markdown(func_name, result, sandbox_res, msg, language, path.parent, verification, profiler_result, memory_hit)
678
-
679
- # Write to File (Safely)
680
- with file_lock:
681
- with open(report_path, "a", encoding="utf-8") as f:
682
- f.write(output_md)
683
-
684
- # 2. Print beautiful native UI to the terminal
685
- with print_lock:
686
- print_console_report(func_name, result, sandbox_res, msg, language, verification, profiler_result, memory_hit)
687
-
688
- except Exception as exc:
689
- with print_lock:
690
- console.print(f"[bold red] Critical failure in thread processing {func['name']}:[/bold red] {exc}")
691
-
692
- console.print(Panel.fit(f" [bold green]Analysis Complete![/bold green] Final report saved to:\n{report_path.absolute()}"))
693
-
694
- if not pro_user:
695
- console.print(Panel(
696
- "[bold]Enjoyed CoreInsight?[/bold] Pro unlocks:\n"
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, "Verification skipped (--no-docker).", None
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="Skipped (--no-docker)."),
296
- correctness=CorrectnessVerification(verified=False, details="Skipped (--no-docker)."),
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.6
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"
@@ -15,6 +15,7 @@ coreinsight/profiler.py
15
15
  coreinsight/prompts.py
16
16
  coreinsight/sandbox.py
17
17
  coreinsight/scanner.py
18
+ coreinsight/tui.py
18
19
  coreinsight/demo/__init__.py
19
20
  coreinsight/demo/bad_loop.py
20
21
  coreinsight/demo/data_processor.py
@@ -11,6 +11,7 @@ langchain-anthropic>=0.1.0
11
11
  pydantic>=2.0
12
12
  chromadb>=0.5.0
13
13
  sentence-transformers>=3.0.0
14
+ textual>=0.60.0
14
15
  psutil>=5.9
15
16
 
16
17
  [compat]
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "coreinsight-cli"
7
- version = "0.2.6"
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