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 @@
1
+ """CAT Live Preview Infrastructure (dev server, preview manager, live reload, diagnostics)."""
@@ -0,0 +1,303 @@
1
+ """CAT Live Preview — Dev Server Manager (calc_terminal/preview/dev_server.py).
2
+
3
+ Owns development server lifecycle:
4
+ - Detects project configuration (Static HTML, Vite, Next.js, CRA, Node).
5
+ - Manages available port detection and avoids collisions.
6
+ - Starts, stops, restarts real local HTTP server binding 127.0.0.1 strictly.
7
+ - Prevents duplicate server processes for the same workspace.
8
+ - Verifies server readiness before declaring live state.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import socket
16
+ import subprocess
17
+ import threading
18
+ import time
19
+ from dataclasses import dataclass, field
20
+ from typing import Callable, Dict, List, Optional, Tuple
21
+
22
+
23
+ class ServerState:
24
+ STOPPED = "stopped"
25
+ STARTING = "starting"
26
+ RUNNING = "running"
27
+ ERROR = "error"
28
+ RESTARTING = "restarting"
29
+
30
+
31
+ @dataclass
32
+ class ProjectInfo:
33
+ kind: str # "static", "vite", "next", "cra", "node"
34
+ label: str
35
+ root: str
36
+ entry_file: Optional[str] = None
37
+ start_command: Optional[List[str]] = None
38
+ is_framework: bool = False
39
+
40
+
41
+ def detect_project_type(root: str) -> ProjectInfo:
42
+ """Analyze workspace root to determine project type and start strategy."""
43
+ abs_root = os.path.abspath(os.path.expanduser(root or ""))
44
+ if not os.path.isdir(abs_root):
45
+ return ProjectInfo(kind="static", label="Static Web", root=abs_root)
46
+
47
+ pkg_path = os.path.join(abs_root, "package.json")
48
+ if os.path.isfile(pkg_path):
49
+ try:
50
+ with open(pkg_path, "r", encoding="utf-8", errors="replace") as f:
51
+ pkg = json.load(f)
52
+ scripts = pkg.get("scripts", {})
53
+ deps = {}
54
+ deps.update(pkg.get("dependencies", {}))
55
+ deps.update(pkg.get("devDependencies", {}))
56
+
57
+ # Vite detection
58
+ for cfg in ("vite.config.js", "vite.config.ts", "vite.config.mjs", "vite.config.cjs"):
59
+ if os.path.isfile(os.path.join(abs_root, cfg)):
60
+ cmd = ["npm", "run", "dev"] if "dev" in scripts else ["npx", "vite"]
61
+ return ProjectInfo(kind="vite", label="Vite", root=abs_root, start_command=cmd, is_framework=True)
62
+ if "vite" in deps and "dev" in scripts:
63
+ return ProjectInfo(kind="vite", label="Vite", root=abs_root, start_command=["npm", "run", "dev"], is_framework=True)
64
+
65
+ # Next.js detection
66
+ for cfg in ("next.config.js", "next.config.ts", "next.config.mjs", "next.config.cjs"):
67
+ if os.path.isfile(os.path.join(abs_root, cfg)):
68
+ cmd = ["npm", "run", "dev"] if "dev" in scripts else ["npx", "next", "dev"]
69
+ return ProjectInfo(kind="next", label="Next.js", root=abs_root, start_command=cmd, is_framework=True)
70
+ if "next" in deps and "dev" in scripts:
71
+ return ProjectInfo(kind="next", label="Next.js", root=abs_root, start_command=["npm", "run", "dev"], is_framework=True)
72
+
73
+ # Create React App detection
74
+ if "react-scripts" in deps:
75
+ cmd = ["npm", "start"]
76
+ return ProjectInfo(kind="cra", label="Create React App", root=abs_root, start_command=cmd, is_framework=True)
77
+
78
+ # Generic Node dev script
79
+ if "dev" in scripts:
80
+ return ProjectInfo(kind="node", label="Node Dev", root=abs_root, start_command=["npm", "run", "dev"], is_framework=True)
81
+ if "start" in scripts:
82
+ return ProjectInfo(kind="node", label="Node Start", root=abs_root, start_command=["npm", "start"], is_framework=True)
83
+ except Exception:
84
+ pass
85
+
86
+ # Static project fallback
87
+ index_file = None
88
+ for cand in ("index.html", "index.htm", "main.html", "app.html"):
89
+ p = os.path.join(abs_root, cand)
90
+ if os.path.isfile(p):
91
+ index_file = p
92
+ break
93
+ return ProjectInfo(kind="static", label="Static Web", root=abs_root, entry_file=index_file)
94
+
95
+
96
+ def is_port_available(port: int, host: str = "127.0.0.1") -> bool:
97
+ """Check if a port is free to bind on localhost."""
98
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
99
+ try:
100
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
101
+ s.bind((host, port))
102
+ return True
103
+ except OSError:
104
+ return False
105
+
106
+
107
+ def find_available_port(preferred: int = 5173, fallback_ports: Optional[List[int]] = None) -> int:
108
+ """Find an available port starting with preferred, then fallbacks, then dynamic bind."""
109
+ if is_port_available(preferred):
110
+ return preferred
111
+ fallbacks = fallback_ports or [5174, 5175, 5176, 3000, 3001, 8080, 8000]
112
+ for p in fallbacks:
113
+ if is_port_available(p):
114
+ return p
115
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
116
+ s.bind(("127.0.0.1", 0))
117
+ return s.getsockname()[1]
118
+
119
+
120
+ class DevServerManager:
121
+ """Manages local dev server instances with zero duplicate processes and clean lifecycle."""
122
+
123
+ _active_servers: Dict[str, DevServerManager] = {}
124
+ _lock = threading.RLock()
125
+
126
+ @classmethod
127
+ def get_or_create(cls, root: str) -> DevServerManager:
128
+ """Get running server for workspace root or create a new manager."""
129
+ norm_root = os.path.abspath(os.path.expanduser(root or ""))
130
+ with cls._lock:
131
+ if norm_root in cls._active_servers:
132
+ return cls._active_servers[norm_root]
133
+ mgr = cls(norm_root)
134
+ cls._active_servers[norm_root] = mgr
135
+ return mgr
136
+
137
+ @classmethod
138
+ def cleanup_all(cls) -> None:
139
+ """Terminate and clean up all running development servers across CAT."""
140
+ with cls._lock:
141
+ for mgr in list(cls._active_servers.values()):
142
+ try:
143
+ mgr.stop()
144
+ except Exception:
145
+ pass
146
+ cls._active_servers.clear()
147
+
148
+ def __init__(self, root: str) -> None:
149
+ self.root = os.path.abspath(os.path.expanduser(root or ""))
150
+ self.project_info = detect_project_type(self.root)
151
+ self.state = ServerState.STOPPED
152
+ self.port: Optional[int] = None
153
+ self.url: Optional[str] = None
154
+ self.last_error: str = ""
155
+ self._server_instance: Any = None # LiveServer instance or subprocess.Popen
156
+ self._proc: Optional[subprocess.Popen] = None
157
+ self._logs: List[str] = []
158
+ self._on_state_change: Optional[Callable[[str, str], None]] = None
159
+
160
+ def set_state_listener(self, cb: Callable[[str, str], None]) -> None:
161
+ self._on_state_change = cb
162
+
163
+ def _notify(self, state: str, error: str = "") -> None:
164
+ self.state = state
165
+ if error:
166
+ self.last_error = error
167
+ if self._on_state_change:
168
+ try:
169
+ self._on_state_change(state, error)
170
+ except Exception:
171
+ pass
172
+
173
+ def is_running(self) -> bool:
174
+ if self.state != ServerState.RUNNING:
175
+ return False
176
+ if self._proc is not None:
177
+ return self._proc.poll() is None
178
+ if self._server_instance is not None:
179
+ return getattr(self._server_instance, "running", False)
180
+ return False
181
+
182
+ def start(self, preferred_port: Optional[int] = None) -> Tuple[bool, str]:
183
+ """Start the local development server for this project."""
184
+ if self.is_running():
185
+ return True, self.url or f"http://127.0.0.1:{self.port}/"
186
+
187
+ self._notify(ServerState.STARTING)
188
+ self.project_info = detect_project_type(self.root)
189
+
190
+ if not self.project_info.is_framework:
191
+ return self._start_static(preferred_port)
192
+ else:
193
+ return self._start_framework(preferred_port)
194
+
195
+ def _start_static(self, preferred_port: Optional[int] = None) -> Tuple[bool, str]:
196
+ """Start CAT's internal LiveServer with hot-reload."""
197
+ try:
198
+ from ..browser.server import LiveServer
199
+ self.port = find_available_port(preferred_port or 5173)
200
+ # Create LiveServer bound to root
201
+ srv = LiveServer(self.root)
202
+ srv.port = self.port
203
+ started = srv.start()
204
+ if not started:
205
+ self._notify(ServerState.ERROR, f"Port {self.port} could not be bound.")
206
+ return False, self.last_error
207
+
208
+ self._server_instance = srv
209
+ self.port = srv.port
210
+ self.url = f"http://127.0.0.1:{self.port}/"
211
+ self._notify(ServerState.RUNNING)
212
+ self._logs.append(f"Static dev server started at {self.url}")
213
+ return True, self.url
214
+ except Exception as e:
215
+ err = f"Failed to start static server: {e}"
216
+ self._notify(ServerState.ERROR, err)
217
+ return False, err
218
+
219
+ def _start_framework(self, preferred_port: Optional[int] = None) -> Tuple[bool, str]:
220
+ """Launch project's own dev server (e.g. Vite, Next.js)."""
221
+ cmd = self.project_info.start_command
222
+ if not cmd:
223
+ return self._start_static(preferred_port)
224
+
225
+ self.port = find_available_port(preferred_port or 5173)
226
+ try:
227
+ # Launch dev process safely
228
+ env = os.environ.copy()
229
+ env["PORT"] = str(self.port)
230
+ self._proc = subprocess.Popen(
231
+ cmd,
232
+ cwd=self.root,
233
+ env=env,
234
+ stdout=subprocess.PIPE,
235
+ stderr=subprocess.STDOUT,
236
+ text=True,
237
+ bufsize=1,
238
+ )
239
+
240
+ # Monitor startup in background thread
241
+ def _reader():
242
+ while self._proc and self._proc.poll() is None:
243
+ line = self._proc.stdout.readline()
244
+ if line:
245
+ self._logs.append(line.rstrip())
246
+ if len(self._logs) > 500:
247
+ del self._logs[:-500]
248
+
249
+ t = threading.Thread(target=_reader, daemon=True)
250
+ t.start()
251
+
252
+ # Wait briefly for process to initialize or report url
253
+ start_time = time.monotonic()
254
+ while time.monotonic() - start_time < 5.0:
255
+ if self._proc.poll() is not None:
256
+ err = "\n".join(self._logs[-10:]) or "Process exited prematurely."
257
+ self._notify(ServerState.ERROR, err)
258
+ return False, err
259
+ if not is_port_available(self.port):
260
+ # Port is now bound by dev server!
261
+ break
262
+ time.sleep(0.2)
263
+
264
+ self.url = f"http://127.0.0.1:{self.port}/"
265
+ self._notify(ServerState.RUNNING)
266
+ return True, self.url
267
+ except Exception as e:
268
+ err = f"Failed to spawn dev command {cmd}: {e}"
269
+ self._notify(ServerState.ERROR, err)
270
+ return False, err
271
+
272
+ def stop(self) -> None:
273
+ """Cleanly stop the running development server."""
274
+ if self._server_instance is not None:
275
+ try:
276
+ self._server_instance.shutdown()
277
+ except Exception:
278
+ pass
279
+ self._server_instance = None
280
+
281
+ if self._proc is not None:
282
+ try:
283
+ self._proc.terminate()
284
+ self._proc.wait(timeout=2.0)
285
+ except Exception:
286
+ try:
287
+ self._proc.kill()
288
+ except Exception:
289
+ pass
290
+ self._proc = None
291
+
292
+ self._notify(ServerState.STOPPED)
293
+
294
+ def restart(self) -> Tuple[bool, str]:
295
+ """Restart the running server."""
296
+ self._notify(ServerState.RESTARTING)
297
+ self.stop()
298
+ time.sleep(0.3)
299
+ return self.start(self.port)
300
+
301
+ def get_logs(self, limit: int = 50) -> List[str]:
302
+ """Return latest captured stdout/stderr lines."""
303
+ return list(self._logs[-limit:])
@@ -0,0 +1,131 @@
1
+ """CAT Live Preview — Diagnostics & Error Mapping (calc_terminal/preview/diagnostics.py).
2
+
3
+ Unified diagnostic model and error navigation:
4
+ - Normalizes errors from browser runtime, dev server, build, and editor.
5
+ - Extracts file, line, and column coordinates from error messages.
6
+ - Supports jumping from Problems/Console errors directly into the source code.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import re
13
+ import uuid
14
+ from dataclasses import dataclass
15
+ from typing import List, Optional
16
+
17
+
18
+ @dataclass
19
+ class SourceLocation:
20
+ """Exact source code target of a diagnostic error."""
21
+ file: str
22
+ line: Optional[int] = None
23
+ column: Optional[int] = None
24
+
25
+
26
+ @dataclass
27
+ class Diagnostic:
28
+ """Normalized diagnostic entry across browser, server, build, and editor."""
29
+ id: str
30
+ severity: str # "info", "warning", "error"
31
+ message: str
32
+ file: Optional[str] = None
33
+ line: Optional[int] = None
34
+ column: Optional[int] = None
35
+ source: str = "editor" # "browser", "server", "build", "editor"
36
+ timestamp: float = 0.0
37
+
38
+
39
+ # Regex patterns to detect file:line:col in runtime and build errors
40
+ # e.g., main.js:42:17, ./src/App.tsx (12:5), File "app.py", line 123
41
+ _LOCATION_PATTERNS = [
42
+ re.compile(r'([a-zA-Z0-9_\-\.\/\\]+\.(?:js|jsx|ts|tsx|py|html|css)):(\d+):(\d+)'),
43
+ re.compile(r'([a-zA-Z0-9_\-\.\/\\]+\.(?:js|jsx|ts|tsx|py|html|css)):(\d+)'),
44
+ re.compile(r'File "([^"]+)", line (\d+)'),
45
+ re.compile(r'([a-zA-Z0-9_\-\.\/\\]+\.(?:js|jsx|ts|tsx|py|html|css))\s*\((\d+):(\d+)\)'),
46
+ ]
47
+
48
+
49
+ def parse_error_location(error_text: str, workspace_root: Optional[str] = None) -> Optional[SourceLocation]:
50
+ """Extract source file, line, and column from an error string."""
51
+ if not error_text:
52
+ return None
53
+ for pattern in _LOCATION_PATTERNS:
54
+ match = pattern.search(error_text)
55
+ if match:
56
+ groups = match.groups()
57
+ raw_path = groups[0]
58
+ line = int(groups[1]) if len(groups) > 1 and groups[1] else None
59
+ col = int(groups[2]) if len(groups) > 2 and groups[2] else None
60
+
61
+ # Resolve relative paths against workspace root if provided
62
+ abs_path = raw_path
63
+ if workspace_root and not os.path.isabs(raw_path):
64
+ cand = os.path.join(workspace_root, raw_path)
65
+ if os.path.exists(cand):
66
+ abs_path = os.path.abspath(cand)
67
+
68
+ return SourceLocation(file=abs_path, line=line, column=col)
69
+ return None
70
+
71
+
72
+ class DiagnosticsManager:
73
+ """Collects and organizes diagnostics from all runtime and build systems."""
74
+
75
+ def __init__(self) -> None:
76
+ self._diagnostics: List[Diagnostic] = []
77
+
78
+ def add(self, severity: str, message: str, file: Optional[str] = None,
79
+ line: Optional[int] = None, column: Optional[int] = None,
80
+ source: str = "editor") -> Diagnostic:
81
+ """Add a new diagnostic item."""
82
+ # Auto-parse location if not explicitly provided
83
+ if not file or line is None:
84
+ loc = parse_error_location(message)
85
+ if loc:
86
+ file = file or loc.file
87
+ line = line if line is not None else loc.line
88
+ column = column if column is not None else loc.column
89
+
90
+ diag = Diagnostic(
91
+ id=uuid.uuid4().hex[:8],
92
+ severity=severity.lower(),
93
+ message=message.strip(),
94
+ file=file,
95
+ line=line,
96
+ column=column,
97
+ source=source,
98
+ timestamp=__import__("time").time(),
99
+ )
100
+ self._diagnostics.append(diag)
101
+ if len(self._diagnostics) > 200:
102
+ del self._diagnostics[:-200]
103
+ return diag
104
+
105
+ def list_diagnostics(self, severity: Optional[str] = None, source: Optional[str] = None) -> List[Diagnostic]:
106
+ """Return matching diagnostics filtered by severity and/or source."""
107
+ results = list(self._diagnostics)
108
+ if severity:
109
+ results = [d for d in results if d.severity == severity.lower()]
110
+ if source:
111
+ results = [d for d in results if d.source == source.lower()]
112
+ return results
113
+
114
+ def clear(self, source: Optional[str] = None) -> None:
115
+ """Clear diagnostics, optionally for a specific source."""
116
+ if source:
117
+ self._diagnostics = [d for d in self._diagnostics if d.source != source.lower()]
118
+ else:
119
+ self._diagnostics.clear()
120
+
121
+
122
+ # Global diagnostics singleton
123
+ _GLOBAL_DIAGNOSTICS_MANAGER: Optional[DiagnosticsManager] = None
124
+
125
+
126
+ def get_diagnostics_manager() -> DiagnosticsManager:
127
+ """Return global DiagnosticsManager singleton."""
128
+ global _GLOBAL_DIAGNOSTICS_MANAGER
129
+ if _GLOBAL_DIAGNOSTICS_MANAGER is None:
130
+ _GLOBAL_DIAGNOSTICS_MANAGER = DiagnosticsManager()
131
+ return _GLOBAL_DIAGNOSTICS_MANAGER
@@ -0,0 +1,66 @@
1
+ """CAT Live Preview — Live Reload Manager (calc_terminal/preview/live_reload.py).
2
+
3
+ Debounced change aggregation and hot-reload dispatcher for local web projects.
4
+ Avoids restarting dev servers on ordinary keystrokes / edits.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import threading
11
+ import time
12
+ from typing import Callable, List, Optional, Set
13
+
14
+
15
+ class LiveReloadManager:
16
+ """Aggregates file modification events and emits debounced reload requests."""
17
+
18
+ def __init__(self, debounce_sec: float = 0.25, on_reload: Optional[Callable[[List[str], bool], None]] = None) -> None:
19
+ self.debounce_sec = debounce_sec
20
+ self.on_reload = on_reload # (changed_files, is_css_only) -> None
21
+ self._pending_files: Set[str] = set()
22
+ self._timer: Optional[threading.Timer] = None
23
+ self._lock = threading.Lock()
24
+ self._last_reload_time: float = 0.0
25
+
26
+ def notify_file_changed(self, file_path: str) -> None:
27
+ """Register that a file in the workspace was saved or edited."""
28
+ abs_path = os.path.abspath(file_path)
29
+ with self._lock:
30
+ self._pending_files.add(abs_path)
31
+ if self._timer is not None:
32
+ self._timer.cancel()
33
+ self._timer = threading.Timer(self.debounce_sec, self._flush)
34
+ self._timer.daemon = True
35
+ self._timer.start()
36
+
37
+ def _flush(self) -> None:
38
+ """Process pending changes and invoke reload callback."""
39
+ with self._lock:
40
+ if not self._pending_files:
41
+ return
42
+ files = list(self._pending_files)
43
+ self._pending_files.clear()
44
+ self._timer = None
45
+
46
+ # Ignore if reloaded too frequently (avoid reload loops)
47
+ now = time.monotonic()
48
+ if now - self._last_reload_time < 0.1:
49
+ return
50
+ self._last_reload_time = now
51
+
52
+ # Determine if CSS only
53
+ is_css_only = all(f.lower().endswith(".css") for f in files)
54
+ if self.on_reload:
55
+ try:
56
+ self.on_reload(files, is_css_only)
57
+ except Exception:
58
+ pass
59
+
60
+ def cancel(self) -> None:
61
+ """Cancel any pending debounce timer."""
62
+ with self._lock:
63
+ if self._timer is not None:
64
+ self._timer.cancel()
65
+ self._timer = None
66
+ self._pending_files.clear()
@@ -0,0 +1,129 @@
1
+ """CAT Live Preview — Preview Manager (calc_terminal/preview/manager.py).
2
+
3
+ High-level session coordinator according to Preview API contract:
4
+ - start(), stop(), restart(), reload(), get(), get_active().
5
+ - Coordinates between DevServerManager, LiveReloadManager, and UI.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ import uuid
12
+ from dataclasses import dataclass
13
+ from typing import Dict, Optional
14
+
15
+ from .dev_server import DevServerManager, ServerState
16
+
17
+
18
+ @dataclass
19
+ class PreviewSession:
20
+ """Represents an active preview session for a project."""
21
+ id: str
22
+ project_path: str
23
+ url: str
24
+ port: int
25
+ status: str
26
+ framework: Optional[str] = None
27
+ created_at: float = 0.0
28
+
29
+
30
+ class PreviewManager:
31
+ """Manages active preview sessions across CAT IDE."""
32
+
33
+ def __init__(self) -> None:
34
+ self._sessions: Dict[str, PreviewSession] = {}
35
+ self._path_to_session_id: Dict[str, str] = {}
36
+
37
+ def start(self, project_path: str, options: Optional[dict] = None) -> PreviewSession:
38
+ """Start or reuse an existing preview session for a project path."""
39
+ norm_path = DevServerManager(project_path).root
40
+ existing = self.get_active(norm_path)
41
+ if existing and existing.status == ServerState.RUNNING:
42
+ return existing
43
+
44
+ dev_mgr = DevServerManager.get_or_create(norm_path)
45
+ ok, url = dev_mgr.start()
46
+ session_id = self._path_to_session_id.get(norm_path) or uuid.uuid4().hex
47
+
48
+ status = dev_mgr.state
49
+ port = dev_mgr.port or 5173
50
+ framework = dev_mgr.project_info.kind if dev_mgr.project_info.is_framework else None
51
+
52
+ session = PreviewSession(
53
+ id=session_id,
54
+ project_path=norm_path,
55
+ url=url if ok else "",
56
+ port=port,
57
+ status=status,
58
+ framework=framework,
59
+ created_at=time.time(),
60
+ )
61
+ self._sessions[session_id] = session
62
+ self._path_to_session_id[norm_path] = session_id
63
+ return session
64
+
65
+ def stop(self, session_id: str) -> None:
66
+ """Stop a preview session."""
67
+ session = self.get(session_id)
68
+ if session:
69
+ dev_mgr = DevServerManager.get_or_create(session.project_path)
70
+ dev_mgr.stop()
71
+ session.status = ServerState.STOPPED
72
+
73
+ def restart(self, session_id: str) -> Optional[PreviewSession]:
74
+ """Restart a preview session."""
75
+ session = self.get(session_id)
76
+ if not session:
77
+ return None
78
+ dev_mgr = DevServerManager.get_or_create(session.project_path)
79
+ ok, url = dev_mgr.restart()
80
+ session.status = dev_mgr.state
81
+ session.url = url
82
+ return session
83
+
84
+ def reload(self, session_id: str) -> bool:
85
+ """Trigger reload of the active preview."""
86
+ session = self.get(session_id)
87
+ if not session:
88
+ return False
89
+ # Notify connected preview clients through LiveServer ws channel if static
90
+ dev_mgr = DevServerManager.get_or_create(session.project_path)
91
+ if dev_mgr._server_instance is not None and hasattr(dev_mgr._server_instance, "broadcast"):
92
+ try:
93
+ dev_mgr._server_instance.broadcast({"cmd": "reload"})
94
+ return True
95
+ except Exception:
96
+ pass
97
+ return False
98
+
99
+ def get(self, session_id: str) -> Optional[PreviewSession]:
100
+ """Look up a preview session by its ID."""
101
+ return self._sessions.get(session_id)
102
+
103
+ def get_active(self, project_path: str) -> Optional[PreviewSession]:
104
+ """Return the active preview session for a project path if any exists."""
105
+ norm_path = DevServerManager(project_path).root
106
+ sid = self._path_to_session_id.get(norm_path)
107
+ if sid:
108
+ return self._sessions.get(sid)
109
+ return None
110
+
111
+ def cleanup_all(self) -> None:
112
+ """Clean up all active preview sessions and dev servers."""
113
+ for sid in list(self._sessions.keys()):
114
+ self.stop(sid)
115
+ self._sessions.clear()
116
+ self._path_to_session_id.clear()
117
+ DevServerManager.cleanup_all()
118
+
119
+
120
+ # Global preview manager singleton
121
+ _GLOBAL_PREVIEW_MANAGER: Optional[PreviewManager] = None
122
+
123
+
124
+ def get_preview_manager() -> PreviewManager:
125
+ """Return the global PreviewManager singleton."""
126
+ global _GLOBAL_PREVIEW_MANAGER
127
+ if _GLOBAL_PREVIEW_MANAGER is None:
128
+ _GLOBAL_PREVIEW_MANAGER = PreviewManager()
129
+ return _GLOBAL_PREVIEW_MANAGER