cct-cli 0.7.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,536 @@
1
+ """CAT Model Center — Native Ollama & Local/Cloud AI Model Manager.
2
+ Creator: Kazi Zillani (CAT Platform).
3
+
4
+ Features:
5
+ - Curated authentic models & dynamic local discovery
6
+ - Deep Hardware Analyzer integration (CPU, RAM, GPU VRAM, CUDA/ROCm/Metal)
7
+ - Model Compatibility Engine with 5 standard tiers:
8
+ 🟢 Excellent, 🟢 Recommended, 🟡 Usable, 🟠 Heavy, 🔴 Not Recommended
9
+ - CAT Benchmark System integration with transparent source attribution
10
+ - Category filtering: All, Popular, Coding, Reasoning, Vision, Small, Large, Installed
11
+ - Memory footprint visualizer (Weights, KV-Cache, VRAM usage, System RAM offload, TPS)
12
+ - Full action controls: Download, Run/Select, Remove, Benchmark
13
+ """
14
+
15
+ if __name__ == "__main__":
16
+ import sys
17
+ sys.exit(1)
18
+
19
+ import time
20
+ import threading
21
+ from typing import Optional, Dict, Any, List
22
+
23
+ TEXTUAL_AVAILABLE = True
24
+ try:
25
+ from textual.screen import Screen
26
+ from textual.containers import Vertical, Horizontal, ScrollableContainer
27
+ from textual.widgets import Static, Button, Input, Label
28
+ from textual.binding import Binding
29
+ except Exception:
30
+ TEXTUAL_AVAILABLE = False
31
+
32
+ if TEXTUAL_AVAILABLE:
33
+ try:
34
+ from ..ollama_catalog import all_models, search_models, get_model, categories as get_categories
35
+ from ..hardware_analyzer import HardwareAnalyzer
36
+ from ..compatibility_engine import ModelCompatibilityEngine
37
+ from ..benchmark_system import CATBenchmarkSystem
38
+ from ..ollama_download import (
39
+ installed_models, pull_model, delete_model,
40
+ estimate_minutes, is_ollama_running, ensure_ollama_running
41
+ )
42
+ from .. import config as cct_config
43
+ from ..providers import provider_manager as pm
44
+ except Exception:
45
+ all_models = search_models = get_model = get_categories = None
46
+ HardwareAnalyzer = ModelCompatibilityEngine = CATBenchmarkSystem = None
47
+ installed_models = pull_model = delete_model = None
48
+ estimate_minutes = is_ollama_running = ensure_ollama_running = None
49
+ cct_config = pm = None
50
+
51
+ try:
52
+ from . import theme_css
53
+ except Exception:
54
+ theme_css = None
55
+
56
+ _CATEGORIES = ["all", "popular", "coding", "reasoning", "vision", "small", "large", "installed"]
57
+
58
+ def _hex(role):
59
+ if theme_css:
60
+ try:
61
+ return theme_css.current_hex(role)
62
+ except Exception:
63
+ pass
64
+ return "#888888"
65
+
66
+ class _ModelRow(Horizontal):
67
+ def __init__(self, m: Dict[str, Any], compat: Any, installed: bool = False):
68
+ super().__init__(classes="ollama-row")
69
+ self.m = m
70
+ self.compat = compat
71
+ self.installed = installed
72
+
73
+ def compose(self):
74
+ m = self.m
75
+ inst_tag = "[✓]" if self.installed else "○"
76
+ # Model name & tag
77
+ yield Static(f"{inst_tag} {m['name']}", classes="ollama-name")
78
+ yield Static(str(m.get("params", "?")), classes="ollama-params")
79
+ yield Static(f"{m.get('size_gb', 0):.1f}G", classes="ollama-size")
80
+
81
+ # Compatibility tier badge
82
+ tier = getattr(self.compat, "tier", "USABLE")
83
+ if tier in ("EXCELLENT", "RECOMMENDED"):
84
+ badge_markup = f"[{_hex('success')}]● {getattr(self.compat, 'tier_label_ascii', '[GOOD]')}[/]"
85
+ elif tier == "USABLE":
86
+ badge_markup = f"[{_hex('warning')}]● {getattr(self.compat, 'tier_label_ascii', '[USABLE]')}[/]"
87
+ else:
88
+ badge_markup = f"[{_hex('error')}]● {getattr(self.compat, 'tier_label_ascii', '[HEAVY]')}[/]"
89
+
90
+ yield Static(badge_markup, classes="ollama-compat")
91
+
92
+ class OllamaPanel(Screen):
93
+ """Dedicated CAT Model Center modal screen."""
94
+ CSS = """
95
+ OllamaPanel { align: center middle; background: $app-background 75%; }
96
+ #ollama-box {
97
+ width: 106; max-width: 98%; height: 46; max-height: 90%;
98
+ background: $surface; border: round $border; padding: 0;
99
+ layout: vertical;
100
+ }
101
+ #ollama-titlebar { height: 3; min-height: 3; padding: 0 2; border-bottom: solid $border; }
102
+ #ollama-title { text-style: bold; width: 1fr; }
103
+ #ollama-body { height: 1fr; min-height: 0; layout: horizontal; }
104
+ #ollama-left { width: 62; min-width: 48; height: 100%; border-right: solid $border; layout: vertical; }
105
+ #ollama-search { height: 3; min-height: 3; padding: 0 1; border-bottom: solid $border; }
106
+ #ollama-search Input { width: 1fr; height: 1; background: transparent; border: none; }
107
+ #ollama-filters { height: 3; min-height: 3; padding: 0 1; overflow-x: auto; }
108
+ #ollama-filters Button { margin-right: 1; }
109
+ #ollama-pcspec { height: auto; max-height: 4; padding: 1 1; border-bottom: solid $border; color: $text-faint; font-size: 85%; }
110
+ #ollama-list { height: 1fr; min-height: 0; overflow-y: auto; scrollbar-gutter: stable; }
111
+ .ollama-row { height: 1; min-height: 1; padding: 0 1; }
112
+ .ollama-row:hover { background: $accent 8%; }
113
+ .ollama-row.-selected { background: $accent 18%; color: $accent; }
114
+ .ollama-name { width: 28; height: 1; overflow: hidden; }
115
+ .ollama-params { width: 8; height: 1; color: $text-faint; text-align: center; }
116
+ .ollama-size { width: 8; height: 1; color: $text-faint; text-align: right; }
117
+ .ollama-compat { width: 16; height: 1; text-align: right; }
118
+ #ollama-right { width: 44; min-width: 36; height: 100%; padding: 1 2; overflow-y: auto; }
119
+ #ollama-detail-title { text-style: bold; color: $accent; height: 1; }
120
+ #ollama-detail-desc { color: $text; height: auto; padding: 1 0; }
121
+ #ollama-compat-card { height: auto; padding: 1 0; border-top: solid $border; }
122
+ #ollama-benchmark-card { height: auto; padding: 1 0; border-top: solid $border; }
123
+ #ollama-progress-wrap { height: 5; min-height: 5; padding: 1 0; display: none; }
124
+ #ollama-progress-wrap.show { display: block; }
125
+ #ollama-progress-bar { height: 1; color: $accent; }
126
+ #ollama-progress-text { height: 1; color: $text-faint; text-align: center; }
127
+ #ollama-progress-eta { height: 1; color: $warning; text-align: center; }
128
+ #ollama-actions { height: 3; min-height: 3; padding: 1 0; border-top: solid $border; }
129
+ #ollama-actions Button { margin-right: 1; }
130
+ #ollama-hint { height: 1; color: $text-faint; padding: 0 2; }
131
+ """
132
+
133
+ BINDINGS = [Binding("escape", "close", "Close")]
134
+
135
+ def __init__(self):
136
+ super().__init__()
137
+ self._catalog = []
138
+ self._installed = set()
139
+ self._filtered = []
140
+ self._selected = None
141
+ self._category = "all"
142
+ self._query = ""
143
+ self._hw = None
144
+ self._engine = None
145
+ self._bench = None
146
+ self._downloading = None
147
+ self._progress = {}
148
+ # Generation counter for the 8s self-rearming installed-
149
+ # models poll: opening the panel N times must not leave N
150
+ # chains polling after close. on_unmount bumps the token so
151
+ # stray chains stop re-arming.
152
+ self._poll_token = 0
153
+
154
+ def compose(self):
155
+ with Vertical(id="ollama-box"):
156
+ with Horizontal(id="ollama-titlebar"):
157
+ yield Static("⬢ CAT Model Center — Native Local AI & Model Platform", id="ollama-title")
158
+ yield Button("✕", id="ollama-close", classes="cct-popup-close")
159
+ with Horizontal(id="ollama-body"):
160
+ with Vertical(id="ollama-left"):
161
+ with Horizontal(id="ollama-search"):
162
+ yield Input(placeholder="🔍 Search models… (qwen, llama, coding, 3b)", id="ollama-search-input")
163
+ with Horizontal(id="ollama-filters"):
164
+ for cat in _CATEGORIES:
165
+ yield Button(cat.title(), id=f"ollama-cat-{cat}", classes="cct-btn cct-btn-sm")
166
+ yield Static("Hardware Analyzer: Profiling system…", id="ollama-pcspec")
167
+ yield ScrollableContainer(id="ollama-list")
168
+ with Vertical(id="ollama-right"):
169
+ yield Static("Select a model", id="ollama-detail-title")
170
+ yield Static("Explore curated and dynamic models. Hardware badges reflect live device compatibility.", id="ollama-detail-desc")
171
+ yield Static("", id="ollama-compat-card")
172
+ yield Static("", id="ollama-benchmark-card")
173
+ with Vertical(id="ollama-progress-wrap"):
174
+ yield Static("", id="ollama-progress-bar")
175
+ yield Static("", id="ollama-progress-text")
176
+ yield Static("", id="ollama-progress-eta")
177
+ with Horizontal(id="ollama-actions"):
178
+ yield Button("⬇ Download", id="ollama-download", variant="primary", classes="cct-btn cct-btn-sm")
179
+ yield Button("▶ Run", id="ollama-use", classes="cct-btn cct-btn-sm")
180
+ yield Button("🗑 Remove", id="ollama-remove", classes="cct-btn cct-btn-sm")
181
+ yield Button("⚡ Bench", id="ollama-bench", classes="cct-btn cct-btn-sm")
182
+ yield Static("Enter: run/select • Click: inspect • Esc: close", id="ollama-hint")
183
+
184
+ def on_mount(self):
185
+ self._init_systems()
186
+ self._load_catalog()
187
+ self.call_after_refresh(self._refresh_pcspec)
188
+ self.call_after_refresh(self._refresh_list)
189
+ self.call_after_refresh(self._poll_installed)
190
+
191
+ def _init_systems(self):
192
+ try:
193
+ if HardwareAnalyzer:
194
+ self._hw = HardwareAnalyzer.analyze()
195
+ if ModelCompatibilityEngine and self._hw:
196
+ self._engine = ModelCompatibilityEngine(hw=self._hw)
197
+ if CATBenchmarkSystem:
198
+ self._bench = CATBenchmarkSystem()
199
+ except Exception:
200
+ pass
201
+
202
+ def _load_catalog(self):
203
+ try:
204
+ self._catalog = all_models(include_dynamic=True) if all_models else []
205
+ except Exception:
206
+ self._catalog = []
207
+ try:
208
+ self._installed = set(installed_models()) if installed_models else set()
209
+ except Exception:
210
+ self._installed = set()
211
+ self._filtered = list(self._catalog)
212
+
213
+ def _refresh_pcspec(self):
214
+ try:
215
+ if self._hw:
216
+ cpu_txt = f"{self._hw.cpu_model} ({self._hw.cpu_threads}T)"
217
+ ram_txt = f"{self._hw.ram_total_gb:.1f}GB RAM"
218
+ gpu_txt = f"{self._hw.gpu_name} ({self._hw.vram_gb:.1f}GB VRAM via {self._hw.gpu_backend})" if self._hw.has_gpu else "CPU Only"
219
+ tier_txt = self._hw.local_ai_tier.value
220
+ txt = f"⚡ Device Profile: {gpu_txt} • {ram_txt} • {cpu_txt} • Tier: {tier_txt}"
221
+ else:
222
+ txt = "⚡ Hardware profile unavailable"
223
+ self.query_one("#ollama-pcspec", Static).update(txt)
224
+ except Exception:
225
+ pass
226
+
227
+ def on_unmount(self):
228
+ # Invalidate the poll chain: the in-flight callback (if
229
+ # any) will see a stale token and stop re-arming.
230
+ try:
231
+ self._poll_token = getattr(self, "_poll_token", 0) + 1
232
+ except Exception:
233
+ pass
234
+
235
+ def _poll_installed(self):
236
+ token = getattr(self, "_poll_token", 0)
237
+ try:
238
+ if not self.is_running:
239
+ return
240
+ except Exception:
241
+ pass
242
+ try:
243
+ if installed_models:
244
+ self._installed = set(installed_models())
245
+ except Exception:
246
+ pass
247
+ try:
248
+ if getattr(self, "_poll_token", 0) != token:
249
+ return
250
+ self.set_timer(8, self._poll_installed)
251
+ except Exception:
252
+ pass
253
+
254
+ def _refresh_list(self):
255
+ try:
256
+ cont = self.query_one("#ollama-list", ScrollableContainer)
257
+ cont.remove_children()
258
+ except Exception:
259
+ return
260
+
261
+ q = self._query.lower().strip()
262
+ cat = self._category.lower()
263
+
264
+ filtered = []
265
+ for m in self._catalog:
266
+ is_inst = m["name"] in self._installed
267
+ if cat == "installed" and not is_inst:
268
+ continue
269
+ if cat not in ("all", "installed"):
270
+ m_cats = [c.lower() for c in m.get("categories", [])]
271
+ if cat not in m_cats and cat not in m.get("caps", []) and cat != m.get("family"):
272
+ continue
273
+ if q:
274
+ n_match = q in m.get("name", "").lower()
275
+ d_match = q in m.get("desc", "").lower()
276
+ f_match = q in m.get("family", "").lower()
277
+ p_match = q in m.get("params", "").lower()
278
+ if not (n_match or d_match or f_match or p_match):
279
+ continue
280
+ filtered.append(m)
281
+
282
+ # Sort: Installed first, then higher compatibility, then smaller size
283
+ def _sort_key(item):
284
+ inst = item["name"] in self._installed
285
+ compat = self._engine.evaluate(item) if self._engine else None
286
+ tier_rank = {"EXCELLENT": 0, "RECOMMENDED": 1, "USABLE": 2, "HEAVY": 3, "NOT_RECOMMENDED": 4}
287
+ t_val = tier_rank.get(getattr(compat, "tier", "USABLE"), 2)
288
+ return (not inst, t_val, item.get("size_gb", 0))
289
+
290
+ filtered.sort(key=_sort_key)
291
+ self._filtered = filtered[:200]
292
+
293
+ for m in self._filtered:
294
+ compat = self._engine.evaluate(m) if self._engine else None
295
+ is_inst = m["name"] in self._installed
296
+ row = _ModelRow(m, compat=compat, installed=is_inst)
297
+ if self._selected and self._selected.get("name") == m["name"]:
298
+ row.add_class("-selected")
299
+ try:
300
+ cont.mount(row)
301
+ except Exception:
302
+ pass
303
+
304
+ if not self._filtered:
305
+ try:
306
+ cont.mount(Static("No matching models found. Try another search.", classes="pc-empty-msg"))
307
+ except Exception:
308
+ pass
309
+
310
+ def _show_detail(self, m: Dict[str, Any]):
311
+ self._selected = m
312
+ try:
313
+ name = m["name"]
314
+ self.query_one("#ollama-detail-title", Static).update(
315
+ f"{name} • {m.get('params', '?')} • {m.get('size_gb', 0):.1f}GB"
316
+ )
317
+ self.query_one("#ollama-detail-desc", Static).update(m.get("desc", ""))
318
+
319
+ # Compatibility Card
320
+ compat = self._engine.evaluate(m) if self._engine else None
321
+ if compat:
322
+ c_lines = [
323
+ f"[b]Device Compatibility: {compat.tier_label_ascii}[/b]",
324
+ f" Memory: {compat.total_memory_gb:.1f}GB (Weights: {compat.weights_gb:.1f}GB, KV: {compat.kv_cache_gb:.1f}GB)",
325
+ f" VRAM: {compat.vram_used_gb:.1f}GB • RAM Offload: {compat.ram_offload_gb:.1f}GB",
326
+ f" Speed: ~{compat.estimated_tps:.1f} tps • {compat.summary}",
327
+ ]
328
+ self.query_one("#ollama-compat-card", Static).update("\n".join(c_lines))
329
+ else:
330
+ self.query_one("#ollama-compat-card", Static).update("")
331
+
332
+ # Benchmark Card
333
+ bench_score = self._bench.get_score_for_model(name) if self._bench else None
334
+ if bench_score:
335
+ src = bench_score.source.value if hasattr(bench_score.source, "value") else str(bench_score.source)
336
+ b_lines = [
337
+ f"[b]CAT Benchmark:[/] Score {bench_score.overall:.1f}/100 [{src}]",
338
+ f" Coding: {bench_score.coding:.1f} • Reason: {bench_score.reasoning:.1f} • Tools: {bench_score.tool_use:.1f}",
339
+ ]
340
+ self.query_one("#ollama-benchmark-card", Static).update("\n".join(b_lines))
341
+ else:
342
+ self.query_one("#ollama-benchmark-card", Static).update(
343
+ "[dim]No CAT benchmark recorded. Click [Bench] to evaluate.[/dim]"
344
+ )
345
+
346
+ # Buttons
347
+ is_inst = name in self._installed
348
+ dl_btn = self.query_one("#ollama-download", Button)
349
+ use_btn = self.query_one("#ollama-use", Button)
350
+ rm_btn = self.query_one("#ollama-remove", Button)
351
+
352
+ if is_inst:
353
+ dl_btn.label = "✓ Installed"
354
+ dl_btn.disabled = True
355
+ use_btn.disabled = False
356
+ rm_btn.disabled = False
357
+ else:
358
+ dl_btn.label = "⬇ Download"
359
+ dl_btn.disabled = False
360
+ use_btn.disabled = True
361
+ rm_btn.disabled = True
362
+
363
+ self._refresh_list()
364
+ except Exception:
365
+ pass
366
+
367
+ def _start_download(self):
368
+ if not self._selected or not pull_model:
369
+ return
370
+ name = self._selected["name"]
371
+ if name in self._installed or self._downloading:
372
+ return
373
+
374
+ if ensure_ollama_running:
375
+ ok, emsg = ensure_ollama_running(timeout=6, auto_start=True)
376
+ if not ok:
377
+ self._show_progress_error(f"Cannot start Ollama: {emsg}")
378
+ return
379
+
380
+ self._downloading = name
381
+ try:
382
+ wrap = self.query_one("#ollama-progress-wrap")
383
+ wrap.add_class("show")
384
+ self.query_one("#ollama-progress-bar", Static).update("⟳ Connecting…")
385
+ self.query_one("#ollama-progress-text", Static).update("0%")
386
+ dl_btn = self.query_one("#ollama-download", Button)
387
+ dl_btn.label = "⏳ Pulling…"
388
+ dl_btn.disabled = True
389
+ except Exception:
390
+ pass
391
+
392
+ def on_prog(p):
393
+ def _upd():
394
+ try:
395
+ pct = p.get("percent")
396
+ st = p.get("status", "")
397
+ speed = p.get("speed")
398
+ bar_w = 26
399
+ if pct is not None:
400
+ filled = int(bar_w * pct / 100)
401
+ bar = "█" * filled + "░" * (bar_w - filled)
402
+ self.query_one("#ollama-progress-bar", Static).update(f"[{bar}]")
403
+ self.query_one("#ollama-progress-text", Static).update(f"{pct}% {st[:24]}")
404
+ else:
405
+ self.query_one("#ollama-progress-bar", Static).update(f"⟳ {st[:28]}")
406
+ if speed:
407
+ self.query_one("#ollama-progress-eta", Static).update(f"{speed/1024/1024:.1f} MB/s")
408
+ except Exception:
409
+ pass
410
+
411
+ try:
412
+ self.app.call_from_thread(_upd)
413
+ except Exception:
414
+ _upd()
415
+
416
+ def on_done(ok, msg):
417
+ def _done():
418
+ try:
419
+ wrap = self.query_one("#ollama-progress-wrap")
420
+ if ok:
421
+ wrap.remove_class("show")
422
+ self._installed.add(name)
423
+ self._downloading = None
424
+ self.query_one("#ollama-download", Button).label = "✓ Installed"
425
+ self.query_one("#ollama-use", Button).disabled = False
426
+ self.query_one("#ollama-remove", Button).disabled = False
427
+ self._use_model(name)
428
+ else:
429
+ self._show_progress_error(f"Download failed: {msg}")
430
+ self._downloading = None
431
+ dl = self.query_one("#ollama-download", Button)
432
+ dl.label = "⬇ Retry"
433
+ dl.disabled = False
434
+ self._refresh_list()
435
+ except Exception:
436
+ pass
437
+
438
+ try:
439
+ self.app.call_from_thread(_done)
440
+ except Exception:
441
+ _done()
442
+
443
+ try:
444
+ pull_model(name, on_progress=on_prog, on_done=on_done)
445
+ except Exception as e:
446
+ on_done(False, str(e))
447
+
448
+ def _show_progress_error(self, err_text: str):
449
+ try:
450
+ wrap = self.query_one("#ollama-progress-wrap")
451
+ wrap.add_class("show")
452
+ self.query_one("#ollama-progress-bar", Static).update(f"✗ Error")
453
+ self.query_one("#ollama-progress-text", Static).update(err_text[:36])
454
+ except Exception:
455
+ pass
456
+
457
+ def _remove_model(self):
458
+ if not self._selected or not delete_model:
459
+ return
460
+ name = self._selected["name"]
461
+ ok, msg = delete_model(name)
462
+ if ok:
463
+ self._installed.discard(name)
464
+ self._show_detail(self._selected)
465
+ self._refresh_list()
466
+
467
+ def _run_benchmark(self):
468
+ if not self._selected or not self._bench:
469
+ return
470
+ name = self._selected["name"]
471
+ score = self._bench.run_full_benchmark(name)
472
+ self._show_detail(self._selected)
473
+
474
+ def _use_model(self, name: str):
475
+ try:
476
+ if cct_config:
477
+ cc = cct_config.get_config()
478
+ cc.default_ai_provider = "ollama"
479
+ cc.default_model = name
480
+ cct_config.save_config(cc)
481
+
482
+ if pm:
483
+ cfg = pm.load_config()
484
+ cfg["provider"] = "ollama"
485
+ cfg["model"] = name
486
+ pm.save_config(cfg)
487
+
488
+ try:
489
+ self.app._system_note(f"✓ CAT Model activated: {name}")
490
+ except Exception:
491
+ pass
492
+ self.dismiss(name)
493
+ except Exception:
494
+ self.dismiss(name)
495
+
496
+ def on_button_pressed(self, event):
497
+ bid = event.button.id or ""
498
+ if bid == "ollama-close":
499
+ self.dismiss(None)
500
+ elif bid.startswith("ollama-cat-"):
501
+ self._category = bid.replace("ollama-cat-", "")
502
+ for b in self.query(Button):
503
+ if b.id and b.id.startswith("ollama-cat-"):
504
+ b.variant = "primary" if b.id == bid else "default"
505
+ self._refresh_list()
506
+ elif bid == "ollama-download":
507
+ self._start_download()
508
+ elif bid == "ollama-use":
509
+ if self._selected:
510
+ self._use_model(self._selected["name"])
511
+ elif bid == "ollama-remove":
512
+ self._remove_model()
513
+ elif bid == "ollama-bench":
514
+ self._run_benchmark()
515
+
516
+ def on_input_changed(self, event):
517
+ if event.input.id == "ollama-search-input":
518
+ self._query = event.value
519
+ self._refresh_list()
520
+
521
+ def on_click(self, event):
522
+ w = getattr(event, "widget", None)
523
+ cur = w
524
+ for _ in range(4):
525
+ if cur is None:
526
+ break
527
+ if isinstance(cur, _ModelRow):
528
+ self._show_detail(cur.m)
529
+ break
530
+ cur = getattr(cur, "parent", None)
531
+
532
+ def action_close(self):
533
+ self.dismiss(None)
534
+
535
+ else:
536
+ OllamaPanel = None