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,165 @@
1
+ """
2
+ CCT — live_automation.py: Real-time file change synchronization (v0.7.10 spec #13).
3
+
4
+ Monitors file changes and synchronizes them across:
5
+ - Editor tabs (auto-reload modified files)
6
+ - Preview panel (trigger hot reload)
7
+ - File explorer (refresh tree)
8
+ - AI activity (notify of changes)
9
+
10
+ Architecture:
11
+ FS Watcher -> LiveAutomationManager -> [Editor, Preview, Explorer]
12
+ """
13
+
14
+ if __name__ == "__main__":
15
+ print("This is a library file and is not meant to be run directly.")
16
+ import sys
17
+ sys.exit(1)
18
+
19
+ import os
20
+ import threading
21
+ import time
22
+ from typing import Callable, Optional, Set
23
+ from dataclasses import dataclass, field
24
+
25
+
26
+ @dataclass
27
+ class FileChangeEvent:
28
+ """Represents a file system change event."""
29
+ path: str
30
+ change_type: str # "created", "modified", "deleted", "renamed"
31
+ timestamp: float = field(default_factory=time.time)
32
+ is_web_file: bool = False
33
+ is_code_file: bool = False
34
+
35
+
36
+ class LiveAutomationManager:
37
+ """Manages real-time file synchronization across CAT components.
38
+
39
+ This class coordinates file change events and triggers appropriate
40
+ updates in the editor, preview, and explorer.
41
+ """
42
+
43
+ # File extensions that trigger web preview updates
44
+ WEB_EXTENSIONS = {
45
+ ".html", ".htm", ".css", ".js", ".mjs", ".json", ".svg",
46
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".bmp",
47
+ ".woff", ".woff2", ".ttf", ".otf",
48
+ }
49
+
50
+ # File extensions that are code files (for editor updates)
51
+ CODE_EXTENSIONS = {
52
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".html", ".css", ".scss",
53
+ ".json", ".yaml", ".yml", ".toml", ".md", ".rs", ".go", ".java",
54
+ ".xml", ".sql", ".sh", ".bash",
55
+ }
56
+
57
+ def __init__(self, root: str):
58
+ self.root = os.path.abspath(root)
59
+ self._callbacks = {
60
+ "editor": [],
61
+ "preview": [],
62
+ "explorer": [],
63
+ "activity": [],
64
+ }
65
+ self._debounce_timers = {}
66
+ self._lock = threading.Lock()
67
+ self._pending_events = {}
68
+ self._debounce_interval = 0.15 # 150ms debounce
69
+
70
+ def register_callback(self, component: str, callback: Callable):
71
+ """Register a callback for a specific component type."""
72
+ if component in self._callbacks:
73
+ self._callbacks[component].append(callback)
74
+
75
+ def on_file_changed(self, path: str, change_type: str = "modified"):
76
+ """Handle a file change event with debouncing."""
77
+ path = os.path.abspath(path)
78
+ event = FileChangeEvent(
79
+ path=path,
80
+ change_type=change_type,
81
+ is_web_file=self._is_web_file(path),
82
+ is_code_file=self._is_code_file(path),
83
+ )
84
+
85
+ with self._lock:
86
+ # Debounce rapid changes to the same file
87
+ if path in self._debounce_timers:
88
+ self._debounce_timers[path].cancel()
89
+
90
+ self._pending_events[path] = event
91
+ timer = threading.Timer(
92
+ self._debounce_interval,
93
+ self._process_event,
94
+ args=(path,)
95
+ )
96
+ timer.daemon = True
97
+ self._debounce_timers[path] = timer
98
+ timer.start()
99
+
100
+ def _process_event(self, path: str):
101
+ """Process a debounced file change event."""
102
+ with self._lock:
103
+ event = self._pending_events.pop(path, None)
104
+ self._debounce_timers.pop(path, None)
105
+
106
+ if event is None:
107
+ return
108
+
109
+ # Notify appropriate components
110
+ if event.is_code_file:
111
+ self._notify("editor", event)
112
+ if event.is_web_file:
113
+ self._notify("preview", event)
114
+ self._notify("explorer", event)
115
+ self._notify("activity", event)
116
+
117
+ def _notify(self, component: str, event: FileChangeEvent):
118
+ """Notify all callbacks for a component type."""
119
+ for callback in self._callbacks.get(component, []):
120
+ try:
121
+ callback(event)
122
+ except Exception:
123
+ pass
124
+
125
+ def _is_web_file(self, path: str) -> bool:
126
+ """Check if a file is a web file (HTML, CSS, JS, etc.)."""
127
+ ext = os.path.splitext(path)[1].lower()
128
+ return ext in self.WEB_EXTENSIONS
129
+
130
+ def _is_code_file(self, path: str) -> bool:
131
+ """Check if a file is a code file."""
132
+ ext = os.path.splitext(path)[1].lower()
133
+ return ext in self.CODE_EXTENSIONS
134
+
135
+ def sync_editor(self, editor, event: FileChangeEvent):
136
+ """Synchronize editor with file changes."""
137
+ if event.change_type == "modified":
138
+ editor.reload_if_open(event.path)
139
+ elif event.change_type == "deleted":
140
+ # Close tab if file was deleted
141
+ if hasattr(editor, '_open_paths') and event.path in editor._open_paths:
142
+ tab_id = editor._open_paths[event.path]
143
+ editor.close_active(tab_id)
144
+
145
+ def sync_preview(self, preview_controller, event: FileChangeEvent):
146
+ """Synchronize preview with file changes."""
147
+ if event.is_web_file and preview_controller:
148
+ preview_controller.notify_ai_wrote(event.path)
149
+
150
+ def sync_explorer(self, explorer, event: FileChangeEvent):
151
+ """Synchronize explorer tree with file changes."""
152
+ if hasattr(explorer, 'refresh_tree'):
153
+ explorer.refresh_tree()
154
+
155
+
156
+ # Global instance
157
+ _automation_manager = None
158
+
159
+
160
+ def get_automation_manager(root: str = None) -> LiveAutomationManager:
161
+ """Get or create the global automation manager."""
162
+ global _automation_manager
163
+ if _automation_manager is None or (root and _automation_manager.root != root):
164
+ _automation_manager = LiveAutomationManager(root or os.getcwd())
165
+ return _automation_manager
@@ -0,0 +1,433 @@
1
+ """
2
+ Notebook-style math rendering for the terminal: stacked fractions,
3
+ subscripts and superscripts using Unicode, so output never looks like
4
+ a raw inline "R0/R" — it looks handwritten, e.g.:
5
+
6
+ 2.303
7
+ k = ───────────────── log(R0/R)
8
+ t
9
+ """
10
+
11
+ import re as _re
12
+
13
+ from . import theme
14
+
15
+ _MUL_RE = _re.compile(r"(?<!\*)\*(?!\*)")
16
+ _DIV_RE = _re.compile(r"(?<=[\w\)\]\u2080-\u2089])\s*/\s*(?=[\w\(\[])")
17
+
18
+
19
+ def mathify(s):
20
+ """House style, applied to any plain (already-computed) display line:
21
+ never a literal '*' for multiplication (use 'x') and never a literal
22
+ '/' for division (use the fraction slash \u2044, which reads as a
23
+ proper divide rather than a path separator). Formula/substitution
24
+ rows should prefer compose()+frac_part() for a full stacked bar —
25
+ this is the lightweight fallback for single-line calculation text.
26
+
27
+ Also normalizes explicit ^ / _ notation and Greek-letter words to
28
+ their Unicode form. This only fires on explicit markup (a literal
29
+ ``^``/``_``, or a whole Greek word), never on bare digits, so
30
+ ordinary numbers like "15" are always left alone.
31
+ """
32
+ s = _MUL_RE.sub(" x ", str(s))
33
+ s = _DIV_RE.sub("\u2044", s)
34
+ s = convert_explicit_superscripts(s)
35
+ s = convert_explicit_subscripts(s)
36
+ s = convert_greek(s)
37
+ return s
38
+
39
+ SUB = {"0": "\u2080", "1": "\u2081", "2": "\u2082", "3": "\u2083", "4": "\u2084",
40
+ "5": "\u2085", "6": "\u2086", "7": "\u2087", "8": "\u2088", "9": "\u2089"}
41
+ SUP = {"0": "\u2070", "1": "\u00b9", "2": "\u00b2", "3": "\u00b3", "4": "\u2074",
42
+ "5": "\u2075", "6": "\u2076", "7": "\u2077", "8": "\u2078", "9": "\u2079",
43
+ "-": "\u207b", "+": "\u207a"}
44
+
45
+
46
+ def sub(n):
47
+ return "".join(SUB.get(c, c) for c in str(n))
48
+
49
+
50
+ def sup(n):
51
+ return "".join(SUP.get(c, c) for c in str(n))
52
+
53
+
54
+ def frac_part(num, den):
55
+ """Return a ('frac', num, den) tuple for use in compose()."""
56
+ return ("frac", str(num), str(den))
57
+
58
+
59
+ def compose(parts, color=theme.TEXT):
60
+ """
61
+ Compose a mixed list of plain strings and ('frac', num, den) tuples
62
+ into a 3-row, baseline-aligned block of text, colored uniformly.
63
+ """
64
+ top_row, mid_row, bot_row = [], [], []
65
+ for p in parts:
66
+ if isinstance(p, tuple) and p[0] == "frac":
67
+ _, num, den = p
68
+ w = max(len(num), len(den))
69
+ top_row.append(" " + num.center(w) + " ")
70
+ mid_row.append(" " + ("\u2500" * w) + " ")
71
+ bot_row.append(" " + den.center(w) + " ")
72
+ else:
73
+ s = str(p)
74
+ top_row.append(" " * len(s))
75
+ mid_row.append(s)
76
+ bot_row.append(" " * len(s))
77
+
78
+ top = "".join(top_row).rstrip()
79
+ mid = "".join(mid_row).rstrip()
80
+ bot = "".join(bot_row).rstrip()
81
+
82
+ lines = [l for l in (top, mid, bot) if l.strip() != ""]
83
+ return "\n".join(theme.fg(l, color) for l in lines) if lines else ""
84
+
85
+
86
+ # --------------------------------------------------------------------- #
87
+ # Scientific rendering engine (v0.6.2) #
88
+ # #
89
+ # Everything below extends the module above without touching it: the #
90
+ # original mathify/sub/sup/frac_part/compose keep their exact old #
91
+ # behavior (mathify only gained two new, narrowly-scoped calls). The #
92
+ # goal of this section is a single entry point — render_math() — that #
93
+ # any AI-generated or user-facing text can be piped through so raw #
94
+ # Markdown/LaTeX markup never reaches the screen; textbook Unicode #
95
+ # comes out the other side instead. #
96
+ # --------------------------------------------------------------------- #
97
+
98
+ GREEK = {
99
+ "alpha": "\u03b1", "beta": "\u03b2", "gamma": "\u03b3", "delta": "\u0394",
100
+ "epsilon": "\u03b5", "zeta": "\u03b6", "eta": "\u03b7", "theta": "\u03b8",
101
+ "iota": "\u03b9", "kappa": "\u03ba", "lambda": "\u03bb", "mu": "\u03bc",
102
+ "nu": "\u03bd", "xi": "\u03be", "pi": "\u03c0", "rho": "\u03c1",
103
+ "sigma": "\u03c3", "tau": "\u03c4", "upsilon": "\u03c5", "phi": "\u03c6",
104
+ "chi": "\u03c7", "psi": "\u03c8", "omega": "\u03c9",
105
+ }
106
+ # Case-preserving alternates the spec calls out explicitly (e.g. capital
107
+ # Delta for change-in, lowercase everywhere else).
108
+ _GREEK_RE = _re.compile(
109
+ r"\\?(?<![A-Za-z])(" + "|".join(sorted(GREEK, key=len, reverse=True)) + r")(?![A-Za-z])",
110
+ _re.IGNORECASE,
111
+ )
112
+
113
+
114
+ def convert_greek(s):
115
+ """Replace bare Greek-letter words (optionally LaTeX-escaped, e.g.
116
+ 'delta' or '\\delta') with their Unicode symbol. Word-boundary
117
+ matched, so it never touches a word that merely contains one of
118
+ these as a substring."""
119
+ def _rep(m):
120
+ return GREEK[m.group(1).lower()]
121
+ return _GREEK_RE.sub(_rep, str(s))
122
+
123
+
124
+ _EXPL_SUP_BRACE_RE = _re.compile(r"\^\{([^{}]+)\}")
125
+ _EXPL_SUP_RE = _re.compile(r"\^(-?[0-9A-Za-z]+)")
126
+ _EXPL_SUB_BRACE_RE = _re.compile(r"_\{([^{}]+)\}")
127
+ _EXPL_SUB_RE = _re.compile(r"_(-?[0-9A-Za-z]+)")
128
+
129
+
130
+ def convert_explicit_superscripts(s):
131
+ """Turn explicit ``x^2``, ``x^{12}``, ``10^-5`` into Unicode
132
+ superscripts. Only fires on a literal caret, so plain numbers are
133
+ never touched."""
134
+ s = _EXPL_SUP_BRACE_RE.sub(lambda m: sup(m.group(1)), str(s))
135
+ s = _EXPL_SUP_RE.sub(lambda m: sup(m.group(1)), s)
136
+ return s
137
+
138
+
139
+ def convert_explicit_subscripts(s):
140
+ """Turn explicit ``H_2``, ``psi_{100}`` into Unicode subscripts.
141
+ Only fires on a literal underscore."""
142
+ s = _EXPL_SUB_BRACE_RE.sub(lambda m: sub(m.group(1)), str(s))
143
+ s = _EXPL_SUB_RE.sub(lambda m: sub(m.group(1)), s)
144
+ return s
145
+
146
+
147
+ _SCI_NOTATION_RE = _re.compile(r"(?<![\w.])(\d+(?:\.\d+)?)[eE]([+-]?\d+)(?!\w)")
148
+
149
+
150
+ def convert_scientific_notation(s):
151
+ """6.022e23 -> 6.022 x 10^23 (Unicode). Requires digits on both
152
+ sides of the 'e' so it can never fire on ordinary words."""
153
+ return _SCI_NOTATION_RE.sub(lambda m: f"{m.group(1)} \u00d7 10{sup(m.group(2))}", str(s))
154
+
155
+
156
+ _LATEX_SYMBOLS = {
157
+ r"\times": "\u00d7", r"\cdot": "\u00b7", r"\pm": "\u00b1", r"\mp": "\u2213",
158
+ r"\geq": "\u2265", r"\leq": "\u2264", r"\neq": "\u2260", r"\approx": "\u2248",
159
+ r"\infty": "\u221e", r"\rightarrow": "\u2192", r"\Rightarrow": "\u21d2",
160
+ r"\leftarrow": "\u2190", r"\to": "\u2192", r"\propto": "\u221d",
161
+ r"\partial": "\u2202", r"\nabla": "\u2207", r"\degree": "\u00b0",
162
+ r"\circ": "\u00b0", r"\perp": "\u22a5", r"\parallel": "\u2225",
163
+ r"\angle": "\u2220", r"\sim": "\u223c", r"\equiv": "\u2261",
164
+ # v0.7.2 roadmap additions: big operators (bounds already come for
165
+ # free — \sum_{i=1}^{n} still has the \sum replaced here, then the
166
+ # existing convert_explicit_sub/superscript passes below pick up
167
+ # the trailing _{i=1}/^{n} the same as they would on any other
168
+ # symbol) plus quantum-mechanics/set notation the spec calls out
169
+ # by name ("Quantum mechanics notation").
170
+ r"\sum": "\u2211", r"\prod": "\u220f", r"\int": "\u222b",
171
+ r"\iint": "\u222c", r"\iiint": "\u222d", r"\oint": "\u222e",
172
+ r"\hbar": "\u210f", r"\langle": "\u27e8", r"\rangle": "\u27e9",
173
+ r"\otimes": "\u2297", r"\oplus": "\u2295", r"\dagger": "\u2020",
174
+ r"\forall": "\u2200", r"\exists": "\u2203", r"\nexists": "\u2204",
175
+ r"\in": "\u2208", r"\notin": "\u2209", r"\subseteq": "\u2286",
176
+ r"\subset": "\u2282", r"\cup": "\u222a", r"\cap": "\u2229",
177
+ r"\emptyset": "\u2205", r"\wedge": "\u2227", r"\vee": "\u2228",
178
+ r"\neg": "\u00ac", r"\therefore": "\u2234", r"\because": "\u2235",
179
+ r"\ell": "\u2113", r"\Re": "\u211c", r"\Im": "\u2111",
180
+ r"\aleph": "\u2135",
181
+ }
182
+
183
+
184
+ def _strip_latex_noise(s):
185
+ """Replace common LaTeX macros with their Unicode glyph, and drop
186
+ the purely-cosmetic ones (\\left, \\right, \\, , \\; , \\quad,
187
+ stray $ / \\[ \\] delimiters) so nothing raw leaks through."""
188
+ for macro, glyph in _LATEX_SYMBOLS.items():
189
+ s = s.replace(macro, glyph)
190
+ for noise in (r"\left", r"\right", r"\quad", r"\qquad", r"\,", r"\;",
191
+ r"\!", "\\\n"):
192
+ s = s.replace(noise, "")
193
+ s = s.replace(r"\[", "").replace(r"\]", "")
194
+ s = _re.sub(r"(?<!\\)\$\$?", "", s)
195
+ return s
196
+
197
+
198
+ def _find_balanced(s, open_idx):
199
+ """s[open_idx] must be '{'. Returns the index just past the
200
+ matching '}' (handling one level of nesting), or None."""
201
+ depth = 0
202
+ for i in range(open_idx, len(s)):
203
+ if s[i] == "{":
204
+ depth += 1
205
+ elif s[i] == "}":
206
+ depth -= 1
207
+ if depth == 0:
208
+ return i + 1
209
+ return None
210
+
211
+
212
+ def convert_sqrt(s):
213
+ """\\sqrt{X} -> a Unicode radical with a combining overline across
214
+ X, e.g. \\sqrt{2KE/m} -> '\u221a(2KE/m)' with an overline on the
215
+ inside. Recurses so nested markup inside the radicand still gets
216
+ converted."""
217
+ s = str(s)
218
+ out, i = [], 0
219
+ while True:
220
+ j = s.find(r"\sqrt", i)
221
+ if j == -1:
222
+ out.append(s[i:])
223
+ break
224
+ out.append(s[i:j])
225
+ k = j + len(r"\sqrt")
226
+ if k < len(s) and s[k] == "{":
227
+ end = _find_balanced(s, k)
228
+ if end is None:
229
+ out.append(s[j:])
230
+ break
231
+ inner = render_inline(s[k + 1:end - 1])
232
+ out.append("\u221a\u0305" + "\u0305".join(inner) + "\u0305" if inner else "\u221a")
233
+ i = end
234
+ else:
235
+ out.append("\u221a")
236
+ i = k
237
+ return "".join(out)
238
+
239
+
240
+ def convert_fractions(s):
241
+ """\\frac{A}{B} -> a real 3-row stacked-bar block (via compose),
242
+ dropped in as its own fenced code section so the Markdown renderer
243
+ preserves the line breaks and monospace alignment a stacked
244
+ fraction depends on. Handles nested \\frac inside numerator or
245
+ denominator by recursing first."""
246
+ s = str(s)
247
+ out, i = [], 0
248
+ while True:
249
+ j = s.find(r"\frac", i)
250
+ if j == -1:
251
+ out.append(s[i:])
252
+ break
253
+ out.append(s[i:j])
254
+ k = j + len(r"\frac")
255
+ if k < len(s) and s[k] == "{":
256
+ num_end = _find_balanced(s, k)
257
+ if num_end is None:
258
+ out.append(s[j:])
259
+ break
260
+ num = s[k + 1:num_end - 1]
261
+ den_start = num_end
262
+ if den_start < len(s) and s[den_start] == "{":
263
+ den_end = _find_balanced(s, den_start)
264
+ if den_end is None:
265
+ out.append(s[j:den_start])
266
+ i = den_start
267
+ continue
268
+ den = s[den_start + 1:den_end - 1]
269
+ block = compose([frac_part(render_inline(num), render_inline(den))])
270
+ out.append("\n```\n" + _strip_ansi(block) + "\n```\n")
271
+ i = den_end
272
+ continue
273
+ out.append(s[j:k])
274
+ i = k
275
+ return "".join(out)
276
+
277
+
278
+ _ANSI_RE = _re.compile(r"\x1b\[[0-9;]*m")
279
+
280
+
281
+ def _strip_ansi(s):
282
+ """compose() colors its output with terminal ANSI codes for the
283
+ fallback CLI; inside a Markdown fenced block those codes would show
284
+ up as literal garbage, so strip them back to plain Unicode text."""
285
+ return _ANSI_RE.sub("", s)
286
+
287
+
288
+ def render_inline(s):
289
+ """Apply every *inline-safe* conversion (no fraction blocks, which
290
+ span multiple lines) — Greek, explicit sup/sub, latex symbol noise,
291
+ scientific notation. Used both standalone and as the recursive
292
+ step inside sqrt/frac/matrix parsing."""
293
+ s = _strip_latex_noise(str(s))
294
+ s = convert_greek(s)
295
+ s = convert_explicit_superscripts(s)
296
+ s = convert_explicit_subscripts(s)
297
+ s = convert_scientific_notation(s)
298
+ return s
299
+
300
+
301
+ _MATRIX_ENV_RE = _re.compile(
302
+ r"\\begin\{(matrix|bmatrix|pmatrix|vmatrix)\}(.*?)\\end\{\1\}", _re.DOTALL
303
+ )
304
+
305
+
306
+ def convert_matrices(s):
307
+ """\\begin{bmatrix} a & b \\\\ c & d \\end{bmatrix} -> a bracket-
308
+ bordered Unicode grid, e.g.:
309
+
310
+ ⎡a b⎤
311
+ ⎣c d⎦
312
+ """
313
+ def _rep(m):
314
+ env, body = m.group(1), m.group(2)
315
+ rows = [r.strip() for r in body.strip().split("\\\\") if r.strip()]
316
+ grid = [[render_inline(cell.strip()) for cell in row.split("&")] for row in rows]
317
+ if not grid:
318
+ return ""
319
+ widths = [max(len(row[c]) for row in grid) for c in range(len(grid[0]))]
320
+ lines = []
321
+ n = len(grid)
322
+ left_edges = {"matrix": ("", "", ""), "bmatrix": ("\u23a1", "\u23a2", "\u23a3"),
323
+ "pmatrix": ("\u239b", "\u239c", "\u239d"),
324
+ "vmatrix": ("\u2502", "\u2502", "\u2502")}
325
+ right_edges = {"matrix": ("", "", ""), "bmatrix": ("\u23a4", "\u23a5", "\u23a6"),
326
+ "pmatrix": ("\u239e", "\u239f", "\u23a0"),
327
+ "vmatrix": ("\u2502", "\u2502", "\u2502")}
328
+ lft, rgt = left_edges.get(env, ("", "", "")), right_edges.get(env, ("", "", ""))
329
+ for idx, row in enumerate(grid):
330
+ pos = 0 if n == 1 else (0 if idx == 0 else (2 if idx == n - 1 else 1))
331
+ cells = " ".join(cell.rjust(widths[c]) for c, cell in enumerate(row))
332
+ lines.append(f"{lft[pos]}{cells}{rgt[pos]}")
333
+ return "\n```\n" + "\n".join(lines) + "\n```\n"
334
+
335
+ return _MATRIX_ENV_RE.sub(_rep, str(s))
336
+
337
+
338
+ # Conservative chemistry-formula matcher: 2+ Element[digits] groups,
339
+ # optionally followed by a trailing ionic charge like ^2- or a bare +/-.
340
+ _CHEM_TOKEN_RE = _re.compile(
341
+ r"\b((?:[A-Z][a-z]?\d*){2,}(?:\^?\d*[+\-])?)\b"
342
+ )
343
+ _CHEM_ELEMENT_RE = _re.compile(r"([A-Z][a-z]?)(\d+)")
344
+ _CHEM_CHARGE_CARET_RE = _re.compile(r"\^(\d*)([+\-])$")
345
+ _CHEM_CHARGE_BARE_RE = _re.compile(r"([+\-])$")
346
+
347
+
348
+ def chem_formula(token):
349
+ """Render one chemistry token (e.g. 'SO4^2-', 'CaCO3', 'NH4+') with
350
+ Unicode subscripts for atom counts and a Unicode superscript for a
351
+ trailing ionic charge. A caret-prefixed digit (``^2-``) is the
352
+ charge magnitude; a bare trailing sign (``NH4+``) is charge 1, and
353
+ any digit right before it still belongs to the preceding element's
354
+ atom count, not the charge — so 'NH4+' reads N, H4, +1, never
355
+ N, H, charge 4. Meant to be called on a token already known to be
356
+ a formula, not blindly across arbitrary prose."""
357
+ charge = ""
358
+ m = _CHEM_CHARGE_CARET_RE.search(token)
359
+ if m:
360
+ n, sign = m.groups()
361
+ charge = sup((n or "") + sign)
362
+ token = token[:m.start()]
363
+ else:
364
+ m = _CHEM_CHARGE_BARE_RE.search(token)
365
+ if m:
366
+ charge = sup(m.group(1))
367
+ token = token[:m.start()]
368
+ body = _CHEM_ELEMENT_RE.sub(lambda m2: m2.group(1) + sub(m2.group(2)), token)
369
+ return body + charge
370
+
371
+
372
+ def find_latex_errors(text):
373
+ """Real (if intentionally narrow) error-highlighting pass — v0.7.2
374
+ roadmap's "Error highlighting" bullet. Checks for the mistakes that
375
+ are actually detectable from the text alone without a real LaTeX
376
+ parser: unbalanced braces after \\frac/\\sqrt, and an unclosed
377
+ \\begin{...} environment. Does NOT validate that a macro name is a
378
+ real LaTeX command (this file only ever converts a known allow-
379
+ list — see _LATEX_SYMBOLS/convert_* above — and silently leaves
380
+ anything else as plain text, which is the deliberately permissive
381
+ choice made throughout this module already: rendering "unknown
382
+ macro" warnings on every \\notarealcommand a user or AI might type
383
+ would be noisier than useful). Returns a list of short strings,
384
+ empty if nothing looks wrong."""
385
+ text = str(text)
386
+ errors = []
387
+ for macro in (r"\frac", r"\sqrt"):
388
+ i = 0
389
+ while True:
390
+ j = text.find(macro, i)
391
+ if j == -1:
392
+ break
393
+ k = j + len(macro)
394
+ if k >= len(text) or text[k] != "{":
395
+ i = k
396
+ continue
397
+ if _find_balanced(text, k) is None:
398
+ errors.append(f"Unbalanced {{}} after {macro}")
399
+ i = k + 1
400
+ for m in _re.finditer(r"\\begin\{([a-zA-Z*]+)\}", text):
401
+ env = m.group(1)
402
+ if f"\\end{{{env}}}" not in text[m.end():]:
403
+ errors.append(f"\\begin{{{env}}} has no matching \\end{{{env}}}")
404
+ return errors
405
+
406
+
407
+ def render_math(text):
408
+ """Master entry point: run a full block of AI/user-facing text
409
+ through the scientific rendering pipeline so LaTeX/Markdown math
410
+ markup never reaches the screen as raw text. Fenced code blocks
411
+ (real code, not math) are left completely untouched. Safe to call
412
+ on plain prose with no math in it at all — everything here only
413
+ fires on explicit markers (\\, ^, _, e-notation, a known Greek
414
+ word), so ordinary sentences pass through unchanged."""
415
+ text = str(text)
416
+ if "```" not in text:
417
+ return _render_math_segment(text)
418
+ parts = text.split("```")
419
+ for i in range(0, len(parts), 2): # even indices are outside fences
420
+ parts[i] = _render_math_segment(parts[i])
421
+ return "```".join(parts)
422
+
423
+
424
+ def _render_math_segment(segment):
425
+ segment = _strip_latex_noise(segment)
426
+ segment = convert_fractions(segment)
427
+ segment = convert_matrices(segment)
428
+ segment = convert_sqrt(segment)
429
+ segment = convert_greek(segment)
430
+ segment = convert_explicit_superscripts(segment)
431
+ segment = convert_explicit_subscripts(segment)
432
+ segment = convert_scientific_notation(segment)
433
+ return segment