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,395 @@
1
+ """
2
+ CAT Browser Launcher - creates CAT's own browser window.
3
+
4
+ Entry point for CAT's embedded browser subsystem.
5
+ Creates a QWebEngineView-based browser window (same process).
6
+
7
+ Usage:
8
+ from calc_terminal.host.launcher import launch_cat_host, can_launch_host
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import sys
15
+ import pathlib
16
+
17
+ def _has_module(name: str) -> bool:
18
+ try:
19
+ __import__(name)
20
+ return True
21
+ except ImportError:
22
+ return False
23
+
24
+
25
+ def can_launch_host() -> bool:
26
+ return _has_module("PySide6") or _has_module("PyQt6")
27
+
28
+
29
+ def get_host_status() -> dict:
30
+ engines = []
31
+ if _has_module("PySide6"):
32
+ engines.append("PySide6 (Qt WebEngine)")
33
+ if _has_module("PyQt6"):
34
+ engines.append("PyQt6 (Qt WebEngine)")
35
+ return {
36
+ "available": bool(engines),
37
+ "engines": engines,
38
+ "preferred": engines[0] if engines else None,
39
+ "reason": "ready" if engines else "no browser engine - install: pip install PySide6",
40
+ "webview2_runtime": _webview2_runtime_version(),
41
+ }
42
+
43
+
44
+ def _webview2_runtime_version() -> str:
45
+ if sys.platform != "win32":
46
+ return ""
47
+ try:
48
+ import winreg
49
+ for path in [
50
+ r"SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
51
+ r"SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
52
+ ]:
53
+ try:
54
+ k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path)
55
+ pv, _ = winreg.QueryValueEx(k, "pv")
56
+ winreg.CloseKey(k)
57
+ return str(pv)
58
+ except FileNotFoundError:
59
+ continue
60
+ except Exception:
61
+ pass
62
+ return ""
63
+
64
+
65
+ _PENDING_VERIFICATION_PATH = pathlib.Path.home() / ".cat_pending_verification.json"
66
+
67
+ def _store_pending_verification(url: str):
68
+ try:
69
+ import json, time
70
+ payload = json.dumps({"url": url, "ts": time.time()})
71
+ tmp_p = pathlib.Path.home() / ".cat_pending_verification.tmp"
72
+ tmp_p.write_text(payload, encoding="utf-8")
73
+ tmp_p.replace(_PENDING_VERIFICATION_PATH)
74
+ except Exception:
75
+ try:
76
+ _PENDING_VERIFICATION_PATH.write_text(
77
+ json.dumps({"url": url, "ts": time.time()}), encoding="utf-8"
78
+ )
79
+ except Exception:
80
+ pass
81
+
82
+
83
+ def _ensure_qt_app():
84
+ """Create or get existing QApplication. Returns (app, created_new)."""
85
+ # Fix Qt WebEngine cache permission errors
86
+ import tempfile
87
+ _cache_path = os.path.join(tempfile.gettempdir(), "cat_qt_cache")
88
+ os.makedirs(_cache_path, exist_ok=True)
89
+ os.environ["QTWEBENGINE_CACHE_PATH"] = _cache_path
90
+ # Performance: keep GPU enabled for smooth rendering, add safe flags
91
+ os.environ.setdefault("QTWEBENGINE_CHROMIUM_FLAGS",
92
+ "--disable-dev-shm-usage --enable-gpu-rasterization "
93
+ "--ignore-gpu-blocklist --enable-zero-copy "
94
+ "--disable-background-networking --disable-default-apps")
95
+ os.environ.setdefault("QTWEBENGINE_DISABLE_SANDBOX", "1")
96
+
97
+ try:
98
+ from PySide6.QtWidgets import QApplication
99
+ except ImportError:
100
+ from PyQt6.QtWidgets import QApplication # type: ignore
101
+ app = QApplication.instance()
102
+ if app is None:
103
+ app = QApplication(sys.argv)
104
+ return app, True
105
+ return app, False
106
+
107
+
108
+ def _is_pid_alive(pid: int) -> bool:
109
+ if pid <= 0:
110
+ return False
111
+ if sys.platform == "win32":
112
+ try:
113
+ import ctypes, ctypes.wintypes
114
+ h = ctypes.windll.kernel32.OpenProcess(0x1000, False, pid)
115
+ if h:
116
+ code = ctypes.wintypes.DWORD()
117
+ ctypes.windll.kernel32.GetExitCodeProcess(h, ctypes.byref(code))
118
+ ctypes.windll.kernel32.CloseHandle(h)
119
+ return code.value == 259
120
+ return False
121
+ except Exception:
122
+ return False
123
+ else:
124
+ try:
125
+ os.kill(pid, 0)
126
+ return True
127
+ except OSError:
128
+ return False
129
+
130
+
131
+ def launch_cat_host(
132
+ start_browser_url: str | None = None,
133
+ start_mode: str = "terminal",
134
+ repl=None,
135
+ block: bool = True,
136
+ ) -> int:
137
+ """Launch CAT's own browser window.
138
+
139
+ block=True: creates app + window + runs event loop (BLOCKS until closed).
140
+ block=False: navigates existing window to URL (no new app, returns fast).
141
+ """
142
+ if start_browser_url in ("fatty", "--fatty", "cat web"):
143
+ start_browser_url = "http://localhost:8765/"
144
+ start_mode = "browser"
145
+
146
+ if start_browser_url and "8765" in start_browser_url:
147
+ try:
148
+ from ..web.server import ensure_fatty_server
149
+ ensure_fatty_server(timeout=5.0, auto_start=True)
150
+ except Exception:
151
+ pass
152
+
153
+ if not block:
154
+ url = start_browser_url or "about:home"
155
+ _store_pending_verification(url)
156
+ # 1. Check if running in current process
157
+ try:
158
+ try:
159
+ from PySide6.QtWidgets import QApplication as _QApp
160
+ except ImportError:
161
+ from PyQt6.QtWidgets import QApplication as _QApp # type: ignore
162
+ app = _QApp.instance()
163
+ if app is not None:
164
+ for w in app.topLevelWidgets():
165
+ try:
166
+ if w.__class__.__name__ == "CATDesktopWindow" and hasattr(w, "browser"):
167
+ w.navigate(url)
168
+ if hasattr(w, "_bring_to_front"):
169
+ w._bring_to_front()
170
+ else:
171
+ w.show()
172
+ w.raise_()
173
+ w.activateWindow()
174
+ return 0
175
+ except Exception:
176
+ continue
177
+ except Exception:
178
+ pass
179
+
180
+ # 2. Check if an external CAT Browser process is actively running (heartbeat within 3.5s)
181
+ try:
182
+ import time, json
183
+ hb_path = pathlib.Path.home() / ".cat_browser_heartbeat"
184
+ if hb_path.exists():
185
+ is_running = False
186
+ hwnd = 0
187
+ try:
188
+ raw = hb_path.read_text(encoding="utf-8").strip()
189
+ if raw:
190
+ if raw.startswith("{"):
191
+ data = json.loads(raw)
192
+ hb_ts = float(data.get("ts", 0))
193
+ pid = int(data.get("pid", 0))
194
+ hwnd = int(data.get("hwnd", 0))
195
+ else:
196
+ hb_ts = float(raw)
197
+ pid = 0
198
+ if (time.time() - hb_ts) < 3.5:
199
+ if pid > 0:
200
+ is_running = _is_pid_alive(pid)
201
+ else:
202
+ is_running = True
203
+ except Exception:
204
+ is_running = False
205
+
206
+ if is_running:
207
+ # Active window will pick up _store_pending_verification within 250ms
208
+ # On Windows, immediately restore & bring it to foreground over maximized terminal!
209
+ if sys.platform == "win32":
210
+ try:
211
+ import ctypes
212
+ if hwnd <= 0 and pid > 0:
213
+ def _enum_cb(h, extra):
214
+ lp = ctypes.c_ulong()
215
+ ctypes.windll.user32.GetWindowThreadProcessId(h, ctypes.byref(lp))
216
+ if lp.value == pid and ctypes.windll.user32.IsWindowVisible(h):
217
+ extra.append(h)
218
+ return False
219
+ return True
220
+ WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_int, ctypes.py_object)
221
+ found = []
222
+ ctypes.windll.user32.EnumWindows(WNDENUMPROC(_enum_cb), found)
223
+ if found:
224
+ hwnd = found[0]
225
+
226
+ if hwnd > 0:
227
+ user32 = ctypes.windll.user32
228
+ kernel32 = ctypes.windll.kernel32
229
+ user32.ShowWindow(hwnd, 9) # SW_RESTORE
230
+ fg_hwnd = user32.GetForegroundWindow()
231
+ fg_thread = user32.GetWindowThreadProcessId(fg_hwnd, None) if fg_hwnd else 0
232
+ cur_thread = kernel32.GetCurrentThreadId()
233
+ attached = False
234
+ if fg_thread and fg_thread != cur_thread:
235
+ attached = bool(user32.AttachThreadInput(cur_thread, fg_thread, True))
236
+ swp_flags = 0x0001 | 0x0002 | 0x0040 # SWP_NOSIZE | SWP_NOMOVE | SWP_SHOWWINDOW
237
+ user32.SetWindowPos(hwnd, -1, 0, 0, 0, 0, swp_flags) # HWND_TOPMOST
238
+ user32.SetWindowPos(hwnd, -2, 0, 0, 0, 0, swp_flags) # HWND_NOTOPMOST
239
+ user32.BringWindowToTop(hwnd)
240
+ user32.SetForegroundWindow(hwnd)
241
+ if attached:
242
+ user32.AttachThreadInput(cur_thread, fg_thread, False)
243
+ except Exception:
244
+ pass
245
+ return 0
246
+ else:
247
+ # Stale heartbeat file from dead/crashed process -> clean it up
248
+ hb_path.unlink(missing_ok=True)
249
+ except Exception:
250
+ pass
251
+
252
+ # 3. No active CAT Browser window found -> spawn in background
253
+ has_qt = _has_module("PySide6") or _has_module("PyQt6")
254
+ if has_qt:
255
+ import subprocess
256
+ exe = sys.executable
257
+ if sys.platform == "win32":
258
+ pw = exe.replace("python.exe", "pythonw.exe")
259
+ if os.path.exists(pw):
260
+ exe = pw
261
+ # Source-checkout support only: a pip-installed CAT already has
262
+ # `calc_terminal` importable, so never force PYTHONPATH/cwd to a
263
+ # repo path that does not exist on the user's machine.
264
+ candidate_root = pathlib.Path(__file__).resolve().parent.parent.parent
265
+ is_source_checkout = (candidate_root / "calc_terminal").is_dir() and (
266
+ candidate_root / "main.py"
267
+ ).exists()
268
+ env = os.environ.copy()
269
+ popen_kwargs: dict = {}
270
+ if is_source_checkout:
271
+ ws_root = str(candidate_root)
272
+ ppath = env.get("PYTHONPATH", "")
273
+ env["PYTHONPATH"] = ws_root + (os.pathsep + ppath if ppath else "")
274
+ popen_kwargs["cwd"] = ws_root
275
+ cmd = [exe, "-m", "calc_terminal.host.launcher", "--url", url, "--mode", "browser"]
276
+ creationflags = 0
277
+ if sys.platform == "win32":
278
+ creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
279
+ try:
280
+ subprocess.Popen(
281
+ cmd,
282
+ env=env,
283
+ creationflags=creationflags,
284
+ close_fds=(sys.platform != "win32"),
285
+ stdout=subprocess.DEVNULL,
286
+ stderr=subprocess.DEVNULL,
287
+ **popen_kwargs,
288
+ )
289
+ return 0
290
+ except Exception as e:
291
+ print(f"[CAT Browser] failed to spawn background browser: {e}", file=sys.stderr)
292
+ return 0
293
+
294
+ has_qt = _has_module("PySide6") or _has_module("PyQt6")
295
+ if has_qt:
296
+ return _launch_with_qt(start_browser_url, start_mode, repl)
297
+
298
+ print("\n[CAT Browser] No browser engine found.", file=sys.stderr)
299
+ print("Install PySide6 for real HTML/CSS/JS rendering:", file=sys.stderr)
300
+ print(" pip install PySide6", file=sys.stderr)
301
+ return 2
302
+
303
+
304
+ def _launch_with_qt(start_browser_url, start_mode, repl) -> int:
305
+ """Create the browser window AND run the event loop. BLOCKS until closed."""
306
+ try:
307
+ os.environ.setdefault(
308
+ "QTWEBENGINE_CHROMIUM_FLAGS",
309
+ "--disable-dev-shm-usage --enable-gpu-rasterization "
310
+ "--ignore-gpu-blocklist --enable-zero-copy "
311
+ "--disable-background-networking --disable-default-apps"
312
+ )
313
+ app, created = _ensure_qt_app()
314
+
315
+ from .desktop import CATDesktopWindow
316
+ initial_url = start_browser_url if (start_mode == "browser" or start_browser_url) else None
317
+ w = CATDesktopWindow(repl=repl, start_browser_url=initial_url)
318
+ w.show()
319
+ try:
320
+ if hasattr(w, "_bring_to_front"):
321
+ w._bring_to_front()
322
+ else:
323
+ w.raise_()
324
+ w.activateWindow()
325
+ except Exception:
326
+ pass
327
+
328
+ if created:
329
+ return app.exec() # type: ignore
330
+ return 0
331
+ except Exception as e:
332
+ import traceback
333
+ print(f"[CAT Browser] launch failed: {e}", file=sys.stderr)
334
+ traceback.print_exc()
335
+ return 1
336
+
337
+
338
+ def create_browser_window(start_browser_url=None, start_mode="browser", repl=None):
339
+ """Create the browser window WITHOUT starting the event loop.
340
+
341
+ Returns (app, window, created_new).
342
+ Caller MUST call app.exec() after setting up other Qt objects.
343
+ """
344
+ os.environ.setdefault(
345
+ "QTWEBENGINE_CHROMIUM_FLAGS",
346
+ "--disable-dev-shm-usage --enable-gpu-rasterization "
347
+ "--ignore-gpu-blocklist --enable-zero-copy "
348
+ "--disable-background-networking --disable-default-apps"
349
+ )
350
+ app, created = _ensure_qt_app()
351
+
352
+ from .desktop import CATDesktopWindow
353
+ initial_url = start_browser_url if (start_mode == "browser" or start_browser_url) else None
354
+ w = CATDesktopWindow(repl=repl, start_browser_url=initial_url)
355
+ w.show()
356
+ try:
357
+ w.raise_()
358
+ w.activateWindow()
359
+ except Exception:
360
+ pass
361
+ return app, w, created
362
+
363
+
364
+ def main():
365
+ import argparse
366
+ p = argparse.ArgumentParser(description="CAT Browser - embedded Chromium")
367
+ p.add_argument("url_pos", nargs="?", default=None, help="URL to open (positional)")
368
+ p.add_argument("--url", default=None, help="URL to open")
369
+ p.add_argument("--mode", choices=["terminal", "browser"], default="browser")
370
+ p.add_argument("--no-block", action="store_true")
371
+ p.add_argument("--status", action="store_true")
372
+ p.add_argument("--fatty", action="store_true", help="Open Fatty CAT Web interface")
373
+ args = p.parse_args()
374
+
375
+ if args.status:
376
+ import json
377
+ print(json.dumps(get_host_status(), indent=2))
378
+ return 0
379
+
380
+ start_url = args.url or args.url_pos
381
+ if args.fatty:
382
+ start_url = "http://localhost:8765/"
383
+ args.mode = "browser"
384
+ elif start_url and "localhost" in start_url and args.mode == "terminal":
385
+ args.mode = "browser"
386
+
387
+ sys.exit(launch_cat_host(
388
+ start_browser_url=start_url,
389
+ start_mode=args.mode,
390
+ block=not args.no_block,
391
+ ))
392
+
393
+
394
+ if __name__ == "__main__":
395
+ main()
@@ -0,0 +1,279 @@
1
+ """
2
+ CAT Host — Terminal Embed
3
+
4
+ Provides the terminal pane inside the graphical CAT Host window.
5
+
6
+ The host window is a real OS window (Qt/QWebEngine on Windows). This module
7
+ gives it a terminal surface that preserves existing CAT CLI behaviour:
8
+
9
+ PowerShell
10
+ ↓
11
+ cat (launcher detects Windows, launches CAT Host)
12
+ ↓
13
+ CAT Host OS Window
14
+ ├── [Terminal] ← this widget (CAT REPL preserved)
15
+ ├── [Browser] ← real QWebEngineView (WebView2/Chromium)
16
+ ├── [IDE]
17
+ └── ...
18
+
19
+ Design decisions:
20
+ * The widget is a plain Qt widget (no external PTY, no xterm.js dependency)
21
+ so it works with only PySide6 installed.
22
+ * It hosts the existing calc_terminal.app.App REPL logic — the same
23
+ handle()/handle_question() dispatch the Textual and fallback CLI use.
24
+ Input from the GUI line edit is fed to App.handle(); output (print())
25
+ is captured and appended to the read-only log view.
26
+ * ANSI escape sequences from calc_terminal.theme are stripped for the
27
+ QPlainTextEdit (Qt handles rich text separately). The behaviour and
28
+ command set are 100% identical to the classic terminal.
29
+ * If the REPL raises, the widget stays alive — the error is shown inline
30
+ and the prompt returns (same contract as the fallback terminal).
31
+
32
+ Fomoji: the REPL instance passed in is the same one cli.bootstrap()
33
+ created, so `cat --auth` state is shared.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import io
39
+ import re
40
+ import sys
41
+ import contextlib
42
+ import threading
43
+ import traceback
44
+
45
+ _ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]|\x1b\].*?\x07|\x1b\(B")
46
+
47
+ def _strip_ansi(s: str) -> str:
48
+ return _ANSI_RE.sub("", s)
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Fallback when Qt not installed — import-time safe so the host package
53
+ # can be imported on headless CI.
54
+ # ---------------------------------------------------------------------------
55
+ try:
56
+ from PySide6.QtWidgets import (
57
+ QWidget, QVBoxLayout, QHBoxLayout, QPlainTextEdit,
58
+ QLineEdit, QLabel, QPushButton
59
+ )
60
+ from PySide6.QtCore import Qt, Signal, Slot
61
+ from PySide6.QtGui import QFont, QTextCursor
62
+ _QT_AVAILABLE = True
63
+ except ImportError:
64
+ try:
65
+ from PyQt6.QtWidgets import (
66
+ QWidget, QVBoxLayout, QHBoxLayout, QPlainTextEdit,
67
+ QLineEdit, QLabel, QPushButton
68
+ )
69
+ from PyQt6.QtCore import Qt, pyqtSignal as Signal, pyqtSlot as Slot
70
+ from PyQt6.QtGui import QFont, QTextCursor
71
+ _QT_AVAILABLE = True
72
+ except ImportError:
73
+ _QT_AVAILABLE = False
74
+ QWidget = object # type: ignore
75
+ Signal = lambda *a, **k: None # type: ignore
76
+
77
+
78
+ if _QT_AVAILABLE:
79
+
80
+ class CATTerminalWidget(QWidget):
81
+ """
82
+ Graphical terminal pane for CAT Host.
83
+
84
+ Hosts calc_terminal.app.App (the REPL/product logic) and exposes
85
+ it through a Qt input line + scrollback. The host window owns the
86
+ layout; this widget owns only the terminal chrome.
87
+
88
+ Usage:
89
+ repl = App() # from calc_terminal.app
90
+ term = CATTerminalWidget(repl)
91
+ stacked.addWidget(term)
92
+ """
93
+
94
+ # Emitted on every command so the host can mirror to status bar
95
+ commandEntered = Signal(str)
96
+
97
+ def __init__(self, repl=None, parent=None):
98
+ super().__init__(parent)
99
+ self.repl = repl
100
+ self._history: list[str] = []
101
+ self._history_idx: int = -1
102
+ self._setup_ui()
103
+ self._print_boot()
104
+
105
+ def _setup_ui(self):
106
+ layout = QVBoxLayout(self)
107
+ layout.setContentsMargins(6, 6, 6, 6)
108
+ layout.setSpacing(6)
109
+
110
+ # Header — shows we are the real CAT terminal, not a plain shell
111
+ header = QLabel("CAT TERMINAL — type /help · /browser · /agent · Ctrl+Shift+B → Browser")
112
+ header.setStyleSheet("color: #878caf; font-size: 11px; padding: 2px 4px;")
113
+ layout.addWidget(header)
114
+
115
+ # Scrollback — read-only, selectable
116
+ self.log = QPlainTextEdit()
117
+ self.log.setReadOnly(True)
118
+ self.log.setLineWrapMode(QPlainTextEdit.LineWrapMode.WidgetWidth)
119
+ font = QFont("Consolas" if sys.platform == "win32" else "Monospace", 10)
120
+ font.setStyleHint(QFont.StyleHint.Monospace)
121
+ self.log.setFont(font)
122
+ # Dark terminal palette matching CAT themes
123
+ self.log.setStyleSheet(
124
+ "QPlainTextEdit { background: #1a1b26; color: #e8ebfa; "
125
+ "border: 1px solid #2a2e48; border-radius: 6px; padding: 6px; "
126
+ "selection-background-color: #82aaff; }"
127
+ )
128
+ layout.addWidget(self.log, 1)
129
+
130
+ # Input row — glyph + line edit + send
131
+ row = QHBoxLayout()
132
+ row.setSpacing(6)
133
+ glyph = QLabel("❯")
134
+ glyph.setStyleSheet("color: #82aaff; font-weight: bold; font-size: 14px; padding-left: 4px;")
135
+ glyph.setFixedWidth(22)
136
+ row.addWidget(glyph)
137
+
138
+ self.input = QLineEdit()
139
+ self.input.setPlaceholderText("Type a command or question… (/help for list)")
140
+ self.input.setStyleSheet(
141
+ "QLineEdit { background: #1e2030; color: #e8ebfa; "
142
+ "border: 1px solid #2a2e48; border-radius: 999px; padding: 6px 12px; "
143
+ "selection-background-color: #82aaff; }"
144
+ "QLineEdit:focus { border: 1px solid #82aaff; }"
145
+ )
146
+ self.input.returnPressed.connect(self._on_submit)
147
+ row.addWidget(self.input, 1)
148
+
149
+ send = QPushButton("↵")
150
+ send.setFixedSize(32, 28)
151
+ send.setToolTip("Send (Enter)")
152
+ send.setStyleSheet(
153
+ "QPushButton { background: #82aaff; color: #1a1b26; border: none; "
154
+ "border-radius: 14px; font-weight: bold; }"
155
+ "QPushButton:hover { background: #9ab8ff; }"
156
+ )
157
+ send.clicked.connect(self._on_submit)
158
+ row.addWidget(send)
159
+
160
+ layout.addLayout(row)
161
+ self.input.setFocus()
162
+
163
+ # --------------------------------------------------------------- I/O --
164
+
165
+ def _append(self, text: str):
166
+ """Append text to scrollback, stripping ANSI for Qt."""
167
+ if not text:
168
+ return
169
+ clean = _strip_ansi(text)
170
+ # QPlainTextEdit.appendPlainText adds a newline; for multi-line
171
+ # captures we insert as single block to preserve formatting.
172
+ self.log.appendPlainText(clean.rstrip("\n"))
173
+ # Autoscroll
174
+ cursor = self.log.textCursor()
175
+ cursor.movePosition(QTextCursor.MoveOperation.End)
176
+ self.log.setTextCursor(cursor)
177
+
178
+ def _print_boot(self):
179
+ if self.repl is None:
180
+ self._append("CAT terminal ready — no REPL attached (UI preview mode).")
181
+ self._append("Run `cat` from PowerShell to attach the real session.")
182
+ return
183
+ # Capture the classic boot/home output (same as fallback CLI)
184
+ buf = io.StringIO()
185
+ try:
186
+ with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
187
+ # theme.enable_windows_ansi + load_saved_theme are already
188
+ # done by cli.bootstrap(); just print home.
189
+ try:
190
+ self.repl.print_home()
191
+ except Exception:
192
+ # Fallback if print_home fails early
193
+ print(f"CAT v{getattr(self.repl, 'VERSION', '?')} — ready", file=buf)
194
+ except Exception as e:
195
+ buf.write(f"[boot warning: {e}]\n")
196
+ out = buf.getvalue()
197
+ if out.strip():
198
+ self._append(out)
199
+ else:
200
+ self._append(f"CAT v{getattr(self.repl, 'VERSION', '0.7.9')} — ready. Type /help")
201
+
202
+ def _on_submit(self):
203
+ raw = self.input.text()
204
+ if raw is None:
205
+ return
206
+ raw = raw.strip()
207
+ if not raw:
208
+ return
209
+ # History
210
+ self._history.append(raw)
211
+ del self._history[:-200]
212
+ self._history_idx = len(self._history)
213
+ self.input.clear()
214
+ self._append(f"\n❯ {raw}")
215
+ self.commandEntered.emit(raw)
216
+
217
+ if self.repl is None:
218
+ self._append("(no REPL — echo only)")
219
+ return
220
+
221
+ # Special: clear
222
+ if raw.lower() in ("/clear", "/home", "clear", "cls"):
223
+ self.log.clear()
224
+ self._print_boot()
225
+ return
226
+
227
+ # Dispatch to the real REPL, capturing its print() output
228
+ buf = io.StringIO()
229
+ try:
230
+ with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
231
+ self.repl.handle(raw)
232
+ except SystemExit:
233
+ # /exit /quit — don't kill host window, just note it
234
+ self._append("\n[Use the window close button or Ctrl+Q to quit CAT Host]")
235
+ # Still show any buffered output
236
+ except Exception as e:
237
+ buf.write(f"\n[error: {type(e).__name__}: {e}]\n")
238
+ traceback.print_exc(file=buf)
239
+ out = buf.getvalue()
240
+ if out:
241
+ self._append(out)
242
+
243
+ # --------------------------------------------------------- public API --
244
+
245
+ def focus_input(self):
246
+ self.input.setFocus()
247
+ self.input.selectAll()
248
+
249
+ def execute(self, raw: str):
250
+ """Programmatic entry — used by host for /browser shortcuts."""
251
+ self.input.setText(raw)
252
+ self._on_submit()
253
+
254
+ def keyPressEvent(self, event):
255
+ # History navigation with Up/Down when input is focused
256
+ try:
257
+ if event.key() == Qt.Key.Key_Up:
258
+ if self.input.hasFocus() and self._history:
259
+ self._history_idx = max(0, self._history_idx - 1)
260
+ self.input.setText(self._history[self._history_idx])
261
+ return
262
+ elif event.key() == Qt.Key.Key_Down:
263
+ if self.input.hasFocus() and self._history:
264
+ if self._history_idx >= len(self._history) - 1:
265
+ self._history_idx = len(self._history)
266
+ self.input.clear()
267
+ else:
268
+ self._history_idx += 1
269
+ self.input.setText(self._history[self._history_idx])
270
+ return
271
+ except Exception:
272
+ pass
273
+ super().keyPressEvent(event)
274
+
275
+ else:
276
+ # Stub so `from .terminal import CATTerminalWidget` never crashes
277
+ class CATTerminalWidget: # type: ignore
278
+ def __init__(self, *a, **k):
279
+ raise ImportError("Qt not installed — CAT Host requires PySide6 (pip install PySide6)")