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,449 @@
1
+ """CAT browser/ — the local live development server.
2
+
3
+ A threaded HTTP server that serves the CURRENT WORKSPACE only:
4
+
5
+ http://127.0.0.1:<auto port>/
6
+ ├── index.html (directory -> index resolution)
7
+ ├── style.css / app.js (correct MIME types)
8
+ ├── images / fonts / svg (static assets)
9
+ └── /__cat__/ws (WebSocket hot-reload channel)
10
+
11
+ Hard guarantees:
12
+ * binds 127.0.0.1 ONLY (never 0.0.0.0) — the workspace is not exposed
13
+ to the network unless a future feature explicitly opts in;
14
+ * the port is chosen by the OS at bind time (port 0), so collisions are
15
+ impossible by construction; `url` reports the real one;
16
+ * every served path is resolved and verified to live UNDER the workspace
17
+ root — no `..` traversal can turn this into a file-sharing server;
18
+ * responses carry no-cache headers so Chromium always revalidates against
19
+ the files CAT's AI is actively rewriting;
20
+ * shutdown() stops listener + threads cleanly (idempotent), so CAT exit
21
+ never leaves an orphan socket/process behind.
22
+
23
+ The WebSocket endpoint is a minimal RFC6455 server (text frames only)
24
+ implemented with the stdlib — no extra dependency. Browsers connect
25
+ through the injected hot-reload snippet (server.inject_hot_reload)
26
+ and receive JSON events: {"cmd": "reload"} or {"cmd": "hot_css",
27
+ "hrefs": [...]} for CSS-only updates.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import base64
33
+ import hashlib
34
+ import json
35
+ import os
36
+ import socket
37
+ import struct
38
+ import threading
39
+ import time
40
+ import webbrowser # noqa: F401 (deliberately NOT used — terminal-first)
41
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
42
+
43
+ # ------------------------------------------------------------------ mimes --
44
+ _MIME = {
45
+ ".html": "text/html; charset=utf-8", ".htm": "text/html; charset=utf-8",
46
+ ".css": "text/css; charset=utf-8",
47
+ ".js": "application/javascript; charset=utf-8",
48
+ ".mjs": "application/javascript; charset=utf-8",
49
+ ".json": "application/json; charset=utf-8",
50
+ ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg",
51
+ ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp",
52
+ ".ico": "image/x-icon", ".bmp": "image/bmp", ".avif": "image/avif",
53
+ ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf",
54
+ ".otf": "font/otf", ".eot": "application/vnd.ms-fontobject",
55
+ ".txt": "text/plain; charset=utf-8", ".md": "text/plain; charset=utf-8",
56
+ ".xml": "application/xml", ".pdf": "application/pdf",
57
+ ".wasm": "application/wasm", ".map": "application/json",
58
+ ".mp3": "audio/mpeg", ".wav": "audio/wav", ".ogg": "audio/ogg",
59
+ ".mp4": "video/mp4", ".webm": "video/webm",
60
+ }
61
+
62
+ WEB_EXTENSIONS = {".html", ".htm", ".css", ".js", ".mjs"}
63
+
64
+ HOT_RELOAD_SNIPPET = """
65
+ <script>
66
+ (function () {
67
+ if (window.__CAT_HOT__) return; window.__CAT_HOT__ = true;
68
+ function connect() {
69
+ var ws;
70
+ try { ws = new WebSocket("ws://127.0.0.1:" + location.port + "/__cat__/ws"); }
71
+ catch (e) { return; }
72
+ ws.onmessage = function (ev) {
73
+ try {
74
+ var msg = JSON.parse(ev.data);
75
+ if (msg.cmd === "reload") { location.reload(); return; }
76
+ if (msg.cmd === "hot_css") {
77
+ (msg.hrefs || []).forEach(function (href) {
78
+ var links = document.querySelectorAll('link[rel="stylesheet"]');
79
+ links.forEach(function (l) {
80
+ var key = l.getAttribute('href') || '';
81
+ if (!href || key.indexOf(href.split('?')[0]) !== -1 ||
82
+ href.indexOf(key.split('?')[0]) !== -1) {
83
+ var clone = l.cloneNode();
84
+ var bust = "href=" + l.href.replace(/[?&]__cat=\\d+/, '') +
85
+ (l.href.indexOf("?") === -1 ? "?" : "&") +
86
+ "__cat=" + Date.now();
87
+ clone.setAttribute("href", bust);
88
+ l.parentNode.replaceChild(clone, l);
89
+ }
90
+ });
91
+ });
92
+ }
93
+ } catch (e) {}
94
+ };
95
+ ws.onclose = function () { setTimeout(connect, 700); };
96
+ }
97
+ connect();
98
+ })();
99
+ </script>
100
+ """
101
+
102
+
103
+ def find_free_port() -> int:
104
+ """Ask the OS for a free localhost port (bind :0). The port stays
105
+ free only momentarily — LiveServer binds immediately after."""
106
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
107
+ s.bind(("127.0.0.1", 0))
108
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
109
+ return s.getsockname()[1]
110
+
111
+
112
+ class _WSClient:
113
+ """One connected preview browser (minimal RFC6455 text-frame peer)."""
114
+
115
+ def __init__(self, sock):
116
+ self.sock = sock
117
+ self.alive = True
118
+ self._lock = threading.Lock()
119
+
120
+ def send_text(self, text: str) -> bool:
121
+ try:
122
+ payload = text.encode("utf-8")
123
+ header = bytearray([0x81]) # FIN + text opcode
124
+ n = len(payload)
125
+ if n < 126:
126
+ header.append(n)
127
+ elif n < 65536:
128
+ header.append(126)
129
+ header += struct.pack(">H", n)
130
+ else:
131
+ header.append(127)
132
+ header += struct.pack(">Q", n)
133
+ with self._lock:
134
+ self.sock.sendall(bytes(header) + payload)
135
+ return True
136
+ except Exception:
137
+ self.alive = False
138
+ return False
139
+
140
+ def close(self):
141
+ self.alive = False
142
+ try:
143
+ self.sock.close()
144
+ except Exception:
145
+ pass
146
+
147
+
148
+ def _ws_accept(key: str) -> str:
149
+ guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
150
+ digest = hashlib.sha1((key + guid).encode("ascii")).digest()
151
+ return base64.b64encode(digest).decode("ascii")
152
+
153
+
154
+ class _Handler(BaseHTTPRequestHandler):
155
+ """Request handler bound to its LiveServer via the server class."""
156
+
157
+ protocol_version = "HTTP/1.1"
158
+
159
+ # ------------------------------------------------------------ utils --
160
+ def log_message(self, fmt, *args): # silence stderr spam
161
+ pass
162
+
163
+ def _no_cache(self):
164
+ self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
165
+ self.send_header("Pragma", "no-cache")
166
+ self.send_header("Expires", "0")
167
+
168
+ def _safe_path(self, url_path: str):
169
+ """Resolve a URL path to a real file under the root. Returns
170
+ (filesystem_path_or_None, status_code)."""
171
+ root = self.server.cat_root
172
+ clean = url_path.split("?", 1)[0].split("#", 1)[0]
173
+ clean = clean.replace("\\", "/").lstrip("/")
174
+ try:
175
+ candidate = os.path.normpath(os.path.join(root, clean))
176
+ # normcase both sides so Windows casing can't smuggle a path
177
+ # outside the root either.
178
+ if os.path.normcase(candidate).startswith(
179
+ os.path.normcase(root.rstrip(os.sep) + os.sep)) \
180
+ or os.path.normcase(candidate) == os.path.normcase(root):
181
+ if os.path.isdir(candidate):
182
+ for name in ("index.html", "index.htm"):
183
+ p = os.path.join(candidate, name)
184
+ if os.path.isfile(p):
185
+ return p, 200
186
+ return None, 404
187
+ if os.path.isfile(candidate):
188
+ return candidate, 200
189
+ return None, 404
190
+ except Exception:
191
+ pass
192
+ return None, 400
193
+
194
+ def _inject(self, body: bytes) -> bytes:
195
+ """Append the hot-reload snippet to HTML responses."""
196
+ lower = bytes(body[-64:]).lower()
197
+ marker = b"</body>"
198
+ idx = bytes(body).lower().rfind(marker)
199
+ if idx == -1:
200
+ # plain append still works for sloppy HTML
201
+ return body + HOT_RELOAD_SNIPPET.encode("utf-8")
202
+ out = body[:idx] + HOT_RELOAD_SNIPPET.encode("utf-8") + body[idx:]
203
+ del lower
204
+ return out
205
+
206
+ # ----------------------------------------------------------- verbs --
207
+ def do_GET(self):
208
+ # ---- WebSocket upgrade -----------------------------------------
209
+ if self.path.startswith("/__cat__/ws"):
210
+ key = self.headers.get("Sec-WebSocket-Key", "")
211
+ if key:
212
+ self.send_response(101, "Switching Protocols")
213
+ self.send_header("Upgrade", "websocket")
214
+ self.send_header("Connection", "Upgrade")
215
+ self.send_header("Sec-WebSocket-Accept", _ws_accept(key))
216
+ self.end_headers()
217
+ self.close_connection = True
218
+ try:
219
+ self.connection.settimeout(600)
220
+ client = _WSClient(self.connection)
221
+ self.server.cat_ws_register(client)
222
+ self.server.cat_ws_reader(client)
223
+ except Exception:
224
+ pass
225
+ else:
226
+ self.send_error(400)
227
+ return
228
+
229
+ path, status = self._safe_path(self.path)
230
+ if status != 200 or path is None:
231
+ body = (f"<h1>404 — Not in workspace</h1>"
232
+ f"<p>{self.path} does not exist under "
233
+ f"{self.server.cat_root}</p>").encode("utf-8")
234
+ self.send_response(404)
235
+ self.send_header("Content-Type", "text/html; charset=utf-8")
236
+ self._no_cache()
237
+ self.send_header("Content-Length", str(len(body)))
238
+ self.end_headers()
239
+ self.wfile.write(body)
240
+ self.server.cat_note_hit(self.path, 404)
241
+ return
242
+
243
+ ext = os.path.splitext(path)[1].lower()
244
+ ctype = _MIME.get(ext, "application/octet-stream")
245
+ try:
246
+ with open(path, "rb") as f:
247
+ body = f.read()
248
+ except OSError as e:
249
+ body = f"<h1>500</h1><p>{e}</p>".encode("utf-8")
250
+ self.send_response(500)
251
+ self.send_header("Content-Type", "text/html; charset=utf-8")
252
+ self._no_cache()
253
+ self.send_header("Content-Length", str(len(body)))
254
+ self.end_headers()
255
+ self.wfile.write(body)
256
+ return
257
+ if ext in (".html", ".htm"):
258
+ body = self._inject(body)
259
+ ctype = "text/html; charset=utf-8"
260
+ self.send_response(200)
261
+ self.send_header("Content-Type", ctype)
262
+ self._no_cache()
263
+ self.send_header("Content-Length", str(len(body)))
264
+ self.end_headers()
265
+ try:
266
+ self.wfile.write(body)
267
+ except (BrokenPipeError, ConnectionResetError,
268
+ ConnectionAbortedError, OSError):
269
+ pass # preview tab navigated away mid-response — harmless
270
+ self.server.cat_note_hit(self.path, 200)
271
+
272
+
273
+ class LiveServer:
274
+ """One running dev server for ONE workspace root.
275
+
276
+ Usage:
277
+ srv = LiveServer(root)
278
+ srv.start() # binds 127.0.0.1:<auto>
279
+ srv.url # 'http://127.0.0.1:5173/'
280
+ srv.stop() # idempotent, joins threads
281
+ """
282
+
283
+ def __init__(self, root: str, server_class=ThreadingHTTPServer,
284
+ handler_class=_Handler):
285
+ self.root = os.path.abspath(os.path.expanduser(root))
286
+ self._httpd = None
287
+ self._thread = None
288
+ self._state_lock = threading.Lock()
289
+ self.port = None
290
+ self.hits = [] # [(path, status)] recent
291
+ self.ws_clients = []
292
+ self._ws_lock = threading.Lock()
293
+ # wire the handler-facing attributes onto the server class
294
+ base = server_class
295
+
296
+ class _S(base):
297
+ cat_root = self.root
298
+ cat_server = self
299
+
300
+ def handle_error(srv, request, client_address):
301
+ # Preview tabs disconnect constantly (navigation, reload,
302
+ # shutdown) — a dropped connection is never an error worth
303
+ # printing into CAT's terminal.
304
+ pass
305
+
306
+ def cat_note_hit(srv, path, status):
307
+ self._note(path, status)
308
+
309
+ def cat_ws_register(srv, client):
310
+ with self._ws_lock:
311
+ self.ws_clients = [c for c in self.ws_clients if c.alive]
312
+ self.ws_clients.append(client)
313
+
314
+ def cat_ws_reader(srv, client):
315
+ # drain frames (we never require client messages); any
316
+ # error just marks the client dead.
317
+ try:
318
+ while client.alive:
319
+ hdr = client.sock.recv(2)
320
+ if len(hdr) < 2:
321
+ break
322
+ opcode = hdr[0] & 0x0F
323
+ masked = hdr[1] & 0x80
324
+ length = hdr[1] & 0x7F
325
+ if length == 126:
326
+ ext = client.sock.recv(2)
327
+ length = struct.unpack(">H", ext)[0]
328
+ elif length == 127:
329
+ ext = client.sock.recv(8)
330
+ length = struct.unpack(">Q", ext)[0]
331
+ mask = client.sock.recv(4) if masked else b""
332
+ payload = client.sock.recv(length) if length else b""
333
+ if masked and mask:
334
+ payload = bytes(b ^ mask[i % 4]
335
+ for i, b in enumerate(payload))
336
+ if opcode == 0x8: # close
337
+ break
338
+ if opcode == 0x9: # ping → pong
339
+ try:
340
+ frame = bytearray([0x8A, len(payload)])
341
+ client.sock.sendall(bytes(frame) + payload)
342
+ except Exception:
343
+ break
344
+ except Exception:
345
+ pass
346
+ finally:
347
+ client.alive = False
348
+
349
+ self._server_cls = _S
350
+
351
+ # ------------------------------------------------------------ props --
352
+ @property
353
+ def url(self) -> str:
354
+ return f"http://127.0.0.1:{self.port}/"
355
+
356
+ @property
357
+ def running(self) -> bool:
358
+ return self._httpd is not None
359
+
360
+ def _note(self, path, status):
361
+ self.hits.append((path, status))
362
+ del self.hits[:-200:]
363
+
364
+ # ----------------------------------------------------------- start --
365
+ def start(self) -> bool:
366
+ """Bind 127.0.0.1 on an OS-assigned port and serve forever on a
367
+ daemon thread. Returns False (without raising) when binding
368
+ failed for any reason."""
369
+ if self._httpd is not None:
370
+ return True
371
+ try:
372
+ port = find_free_port()
373
+ self._httpd = self._server_cls(("127.0.0.1", port), _Handler)
374
+ self._httpd.daemon_threads = True
375
+ self.port = self._httpd.server_address[1]
376
+ except Exception:
377
+ self._httpd = None
378
+ return False
379
+ self._thread = threading.Thread(
380
+ target=self._serve_loop, name="cat-live-server", daemon=True)
381
+ self._thread.start()
382
+ return True
383
+
384
+ def _serve_loop(self):
385
+ try:
386
+ self._httpd.serve_forever(poll_interval=0.25)
387
+ except Exception:
388
+ pass
389
+
390
+ def wait_ready(self, timeout=3.0) -> bool:
391
+ """Block until the socket answers (or timeout). Keeps start-up
392
+ ordering honest for the engine's first navigation."""
393
+ deadline = time.time() + timeout
394
+ while time.time() < deadline:
395
+ if self.port is not None:
396
+ try:
397
+ with socket.create_connection(
398
+ ("127.0.0.1", self.port), timeout=0.4):
399
+ return True
400
+ except OSError:
401
+ pass
402
+ time.sleep(0.03)
403
+ return False
404
+
405
+ # ---------------------------------------------------- broadcast API --
406
+ def broadcast(self, obj: dict) -> int:
407
+ """Send one JSON event to every connected preview browser.
408
+ Returns the number of live clients it reached."""
409
+ data = json.dumps(obj)
410
+ with self._ws_lock:
411
+ clients = [c for c in self.ws_clients if c.alive]
412
+ sent = 0
413
+ for c in clients:
414
+ if c.send_text(data):
415
+ sent += 1
416
+ with self._ws_lock:
417
+ self.ws_clients = [c for c in self.ws_clients if c.alive]
418
+ return sent
419
+
420
+ def notify_reload(self):
421
+ # counted so tests/probes can verify debounce coalescing
422
+ self._reload_count = getattr(self, "_reload_count", 0) + 1
423
+ return self.broadcast({"cmd": "reload"})
424
+
425
+ def notify_hot_css(self, hrefs):
426
+ return self.broadcast({"cmd": "hot_css", "hrefs": list(hrefs or [])})
427
+
428
+ # ------------------------------------------------------------- stop --
429
+ def stop(self):
430
+ """Idempotent full teardown: WebSocket peers, listener, thread."""
431
+ with self._ws_lock:
432
+ clients = list(self.ws_clients)
433
+ self.ws_clients = []
434
+ for c in clients:
435
+ c.close()
436
+ httpd, self._httpd = self._httpd, None
437
+ if httpd is not None:
438
+ try:
439
+ httpd.shutdown()
440
+ except Exception:
441
+ pass
442
+ try:
443
+ httpd.server_close()
444
+ except Exception:
445
+ pass
446
+ thread, self._thread = self._thread, None
447
+ if thread is not None and thread.is_alive():
448
+ thread.join(timeout=2.0)
449
+ self.port = None
@@ -0,0 +1,75 @@
1
+ """CAT browser/ — the UI state model (spec section 22).
2
+
3
+ One place owns the vocabulary for the right pane. The UI derives its
4
+ layout from these states instead of scattered booleans:
5
+
6
+ WorkspaceMode CODE | PREVIEW what the right pane shows
7
+ PaneMode DEFAULT | FULLSCREEN right pane vs whole window
8
+ PreviewState STOPPED | STARTING | RUNNING | ERROR
9
+ ServerState STOPPED | STARTING | RUNNING | ERROR
10
+ EditorState EMPTY | HAS_TABS editor tab strip state
11
+ """
12
+
13
+ from dataclasses import dataclass, field
14
+ from enum import Enum
15
+ from typing import Optional, Tuple
16
+
17
+
18
+ class WorkspaceMode(Enum):
19
+ CODE = "code"
20
+ PREVIEW = "preview"
21
+ SPLIT = "split"
22
+
23
+
24
+ class PaneMode(Enum):
25
+ DEFAULT = "default"
26
+ FULLSCREEN = "fullscreen"
27
+
28
+
29
+ class PreviewState(Enum):
30
+ STOPPED = "stopped"
31
+ STARTING = "starting"
32
+ RUNNING = "running"
33
+ ERROR = "error"
34
+
35
+
36
+ class ServerState(Enum):
37
+ STOPPED = "stopped"
38
+ STARTING = "starting"
39
+ RUNNING = "running"
40
+ ERROR = "error"
41
+
42
+
43
+ class EditorState(Enum):
44
+ EMPTY = "empty"
45
+ HAS_TABS = "has_tabs"
46
+
47
+
48
+ @dataclass
49
+ class PreviewSnapshot:
50
+ """One immutable view of a loaded page — everything the preview
51
+ panel renders. Produced by BrowserEngine after every navigation /
52
+ reload / hot update."""
53
+ url: str = ""
54
+ title: str = ""
55
+ ok: bool = True
56
+ error: str = ""
57
+ # Structured outline extracted from the LIVE DOM (headings, text,
58
+ # links, buttons...) so the terminal can show what Chromium sees.
59
+ outline_lines: Tuple[str, ...] = ()
60
+ console_tail: Tuple[str, ...] = () # recent browser console lines
61
+ js_errors: Tuple[str, ...] = ()
62
+ elements: int = 0
63
+ images_loaded: int = 0
64
+ images_broken: int = 0
65
+ status_code: Optional[int] = None
66
+ extra: dict = field(default_factory=dict)
67
+
68
+ @property
69
+ def summary_line(self):
70
+ if not self.ok and self.error:
71
+ return f"✗ {self.error}"
72
+ bits = [f"{self.elements} elements"]
73
+ if self.images_loaded or self.images_broken:
74
+ bits.append(f"{self.images_loaded}✓/{self.images_broken}✗ images")
75
+ return " · ".join(bits)
@@ -0,0 +1,99 @@
1
+ """CAT browser/ — debounced workspace watch for WEB files.
2
+
3
+ Reuses fs_watcher.WorkspaceWatcher's proven watchdog/poller machinery,
4
+ narrowed to what a website preview cares about:
5
+
6
+ .html .htm .css .js .mjs .json .svg (+ image/font assets)
7
+
8
+ and re-debounced to PREVIEW_DEBOUNCE (150 ms, spec section 13) so a
9
+ burst of AI writes coalesces into ONE preview update:
10
+
11
+ AI writes index.html ─┐
12
+ AI writes style.css ─┼─► quiet 150ms ─► callback({paths}) ─► reload
13
+ AI writes app.js ─┘
14
+
15
+ The callback always fires on a background thread (same contract as
16
+ fs_watcher) and receives absolute paths.
17
+ """
18
+
19
+ import os
20
+ import threading
21
+
22
+ from ..fs_watcher import WorkspaceWatcher
23
+ from .server import WEB_EXTENSIONS
24
+
25
+ PREVIEW_DEBOUNCE = 0.15 # spec: wait 100–200 ms after the last change
26
+ ASSET_EXTENSIONS = {
27
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".bmp", ".avif",
28
+ ".woff", ".woff2", ".ttf", ".otf", ".svg",
29
+ }
30
+
31
+
32
+ def is_web_file(path: str) -> bool:
33
+ _, ext = os.path.splitext(str(path).lower())
34
+ return ext in WEB_EXTENSIONS or ext in ASSET_EXTENSIONS
35
+
36
+
37
+ class PreviewFileWatcher(WorkspaceWatcher):
38
+ """WorkspaceWatcher tuned for live preview:
39
+
40
+ * DEBOUNCE = 150 ms (the Explorer's copy stays at 350 ms);
41
+ * only web-relevant paths reach the callback;
42
+ * `on_activity` fires per accepted change batch so the UI can show
43
+ '● Writing style.css' style build activity without polling.
44
+ """
45
+
46
+ DEBOUNCE = PREVIEW_DEBOUNCE
47
+
48
+ def __init__(self, callback, on_activity=None):
49
+ self._user_callback = callback
50
+ self._activity = on_activity
51
+ super().__init__(self._filtered_flush)
52
+
53
+ def _filtered_flush(self, batch):
54
+ web = {p for p in batch if is_web_file(p)}
55
+ if not web:
56
+ return
57
+ try:
58
+ if self._activity is not None:
59
+ for p in sorted(web)[:12]:
60
+ self._activity(p)
61
+ except Exception:
62
+ pass
63
+ try:
64
+ self._user_callback(web)
65
+ except Exception:
66
+ pass
67
+
68
+
69
+ class Debouncer:
70
+ """Generic trailing-edge debounce usable by tests and the controller:
71
+ schedule(fn) resets a 150 ms timer; fn runs once after quiet."""
72
+
73
+ def __init__(self, delay=PREVIEW_DEBOUNCE):
74
+ self.delay = delay
75
+ self._timer = None
76
+ self._lock = threading.Lock()
77
+
78
+ def schedule(self, fn):
79
+ with self._lock:
80
+ if self._timer is not None:
81
+ self._timer.cancel()
82
+ timer = threading.Timer(self.delay, self._run, args=(fn,))
83
+ timer.daemon = True
84
+ self._timer = timer
85
+ timer.start()
86
+
87
+ def _run(self, fn):
88
+ with self._lock:
89
+ self._timer = None
90
+ try:
91
+ fn()
92
+ except Exception:
93
+ pass
94
+
95
+ def cancel(self):
96
+ with self._lock:
97
+ if self._timer is not None:
98
+ self._timer.cancel()
99
+ self._timer = None
@@ -0,0 +1 @@
1
+ """CAT Browser GUI package — real embedded browser (Chromium/WebView2)."""
@@ -0,0 +1,3 @@
1
+ from .launcher import main
2
+ if __name__ == "__main__":
3
+ main()