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,160 @@
1
+ """CAT Code Editor — Command Registry (calc_terminal/editor/commands.py).
2
+
3
+ Centralized registration and execution of all editor, preview, and workbench commands.
4
+ UI components, buttons, menus, and shortcuts invoke commands through this registry.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import inspect
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Callable, Dict, List, Optional
12
+
13
+
14
+ @dataclass
15
+ class CATCommand:
16
+ """Definition of an executable command inside CAT IDE."""
17
+ id: str
18
+ title: str
19
+ category: str = "General"
20
+ handler: Optional[Callable[[Optional[dict]], Any]] = None
21
+ can_execute: Optional[Callable[[Optional[dict]], bool]] = None
22
+ default_keybinding: Optional[str] = None
23
+ description: str = ""
24
+
25
+
26
+ class CommandRegistry:
27
+ """Central registry of all CAT IDE commands."""
28
+
29
+ def __init__(self) -> None:
30
+ self._commands: Dict[str, CATCommand] = {}
31
+ self._register_defaults()
32
+
33
+ def register(self, command: CATCommand) -> None:
34
+ """Register or override a command."""
35
+ self._commands[command.id] = command
36
+
37
+ def unregister(self, command_id: str) -> bool:
38
+ """Unregister a command by ID."""
39
+ if command_id in self._commands:
40
+ del self._commands[command_id]
41
+ return True
42
+ return False
43
+
44
+ def get(self, command_id: str) -> Optional[CATCommand]:
45
+ """Look up a command by its ID."""
46
+ return self._commands.get(command_id)
47
+
48
+ def can_execute(self, command_id: str, context: Optional[dict] = None) -> bool:
49
+ """Check if a command can currently execute."""
50
+ cmd = self.get(command_id)
51
+ if not cmd:
52
+ return False
53
+ if cmd.can_execute is not None:
54
+ try:
55
+ return bool(cmd.can_execute(context))
56
+ except Exception:
57
+ return False
58
+ return cmd.handler is not None
59
+
60
+ def execute(self, command_id: str, context: Optional[dict] = None) -> Any:
61
+ """Execute a registered command by its ID."""
62
+ cmd = self.get(command_id)
63
+ if not cmd:
64
+ raise KeyError(f"Command '{command_id}' is not registered.")
65
+ if not self.can_execute(command_id, context):
66
+ return None
67
+ if cmd.handler is None:
68
+ return None
69
+ try:
70
+ sig = inspect.signature(cmd.handler)
71
+ if len(sig.parameters) == 0:
72
+ return cmd.handler()
73
+ return cmd.handler(context)
74
+ except TypeError:
75
+ try:
76
+ return cmd.handler(context)
77
+ except Exception:
78
+ return cmd.handler()
79
+
80
+ def list_commands(self, category: Optional[str] = None) -> List[CATCommand]:
81
+ """Return a sorted list of registered commands, optionally filtered by category."""
82
+ cmds = list(self._commands.values())
83
+ if category:
84
+ cmds = [c for c in cmds if c.category.lower() == category.lower()]
85
+ cmds.sort(key=lambda c: (c.category, c.title))
86
+ return cmds
87
+
88
+ def _register_defaults(self) -> None:
89
+ """Register default editor, preview, and workbench commands."""
90
+ defaults = [
91
+ # Editor File Actions
92
+ CATCommand("editor.save", "File: Save", "File", default_keybinding="ctrl+s", description="Save the active file to disk"),
93
+ CATCommand("editor.saveAs", "File: Save As...", "File", default_keybinding="ctrl+shift+s", description="Save active file with a new name"),
94
+ CATCommand("editor.closeTab", "View: Close Current Tab", "View", default_keybinding="ctrl+w", description="Close active editor or viewer tab"),
95
+ CATCommand("editor.reopenClosedTab", "View: Reopen Closed Tab", "View", default_keybinding="ctrl+shift+t", description="Reopen recently closed tab"),
96
+ CATCommand("editor.quickOpen", "Go to File... (Quick Open)", "Go", default_keybinding="ctrl+p", description="Quickly open a file from workspace"),
97
+ CATCommand("editor.commandPalette", "View: Command Palette", "View", default_keybinding="ctrl+shift+p", description="Open CAT Command Palette"),
98
+
99
+ # Editor Navigation & Search
100
+ CATCommand("editor.find", "Edit: Find", "Edit", default_keybinding="ctrl+f", description="Find text in active editor"),
101
+ CATCommand("editor.replace", "Edit: Replace", "Edit", default_keybinding="ctrl+h", description="Find and replace text in active editor"),
102
+ CATCommand("workspace.search", "Search: Find in Files", "Search", default_keybinding="ctrl+shift+f", description="Search text across workspace"),
103
+ CATCommand("editor.goToDefinition", "Go: Go to Definition", "Go", default_keybinding="f12", description="Jump to symbol definition"),
104
+ CATCommand("editor.findReferences", "Go: Find References", "Go", default_keybinding="shift+f12", description="Find all references of symbol"),
105
+ CATCommand("editor.rename", "Edit: Rename Symbol", "Edit", default_keybinding="f2", description="Rename symbol across file"),
106
+
107
+ # Line & Text Manipulation
108
+ CATCommand("editor.toggleComment", "Edit: Toggle Line Comment", "Edit", default_keybinding="ctrl+/", description="Comment or uncomment current line"),
109
+ CATCommand("editor.selectNextOccurrence", "Selection: Add Next Occurrence", "Selection", default_keybinding="ctrl+d", description="Select next matching text occurrence"),
110
+ CATCommand("editor.deleteLine", "Edit: Delete Line", "Edit", default_keybinding="ctrl+shift+k", description="Delete entire current line"),
111
+ CATCommand("editor.moveLineUp", "Edit: Move Line Up", "Edit", default_keybinding="alt+up", description="Move active line or selection up"),
112
+ CATCommand("editor.moveLineDown", "Edit: Move Line Down", "Edit", default_keybinding="alt+down", description="Move active line or selection down"),
113
+ CATCommand("editor.copyLineUp", "Edit: Copy Line Up", "Edit", default_keybinding="shift+alt+up", description="Duplicate active line upwards"),
114
+ CATCommand("editor.copyLineDown", "Edit: Copy Line Down", "Edit", default_keybinding="shift+alt+down", description="Duplicate active line downwards"),
115
+ CATCommand("editor.insertLineBelow", "Edit: Insert Line Below", "Edit", default_keybinding="ctrl+enter", description="Insert line below and move cursor"),
116
+ CATCommand("editor.insertLineAbove", "Edit: Insert Line Above", "Edit", default_keybinding="ctrl+shift+enter", description="Insert line above and move cursor"),
117
+ CATCommand("editor.triggerCompletion", "Edit: Trigger Suggestion", "Edit", default_keybinding="ctrl+space", description="Trigger code completion / suggestions"),
118
+ CATCommand("editor.formatDocument", "Edit: Format Document", "Edit", default_keybinding="shift+alt+f", description="Format source code"),
119
+
120
+ # Live Preview Commands
121
+ CATCommand("preview.open", "Preview: Run / Open Live Preview", "Preview", default_keybinding="shift+enter", description="Start or reuse local development server and open live preview"),
122
+ CATCommand("preview.reload", "Preview: Reload Live Preview", "Preview", default_keybinding="ctrl+r", description="Reload active live preview"),
123
+ CATCommand("preview.stop", "Preview: Stop Development Server", "Preview", description="Stop the running local development server"),
124
+ CATCommand("preview.restart", "Preview: Restart Development Server", "Preview", description="Restart the local development server"),
125
+ CATCommand("preview.toggleSplit", "View: Toggle Split Editor / Preview", "View", default_keybinding="ctrl+shift+s", description="Toggle side-by-side split between editor and preview"),
126
+
127
+ # Workbench & Sidebar
128
+ CATCommand("workbench.action.toggleSidebar", "View: Toggle Primary Sidebar", "View", default_keybinding="ctrl+b", description="Show or hide primary sidebar / explorer"),
129
+ CATCommand("workbench.action.terminal.toggle", "View: Toggle Terminal", "View", default_keybinding="ctrl+`", description="Toggle integrated terminal"),
130
+ CATCommand("workbench.view.explorer", "View: Show Explorer", "View", default_keybinding="ctrl+shift+e", description="Focus workspace file explorer"),
131
+ CATCommand("workbench.view.scm", "View: Show Source Control", "View", default_keybinding="ctrl+shift+g", description="Open source control / git panel"),
132
+ CATCommand("workbench.action.closeOverlays", "View: Close Active Overlays", "View", default_keybinding="escape", description="Dismiss active find bar, popups, or dialogs"),
133
+
134
+ # Viewer Controls
135
+ CATCommand("viewer.zoomIn", "Viewer: Zoom In", "Viewer", default_keybinding="ctrl+=", description="Zoom in active viewer"),
136
+ CATCommand("viewer.zoomOut", "Viewer: Zoom Out", "Viewer", default_keybinding="ctrl+-", description="Zoom out active viewer"),
137
+ CATCommand("viewer.fitPage", "Viewer: Fit to Screen", "Viewer", description="Fit document or image to screen"),
138
+ CATCommand("viewer.fitWidth", "Viewer: Fit to Width", "Viewer", description="Fit document or image to width"),
139
+ CATCommand("viewer.fullscreen", "Viewer: Toggle Fullscreen", "Viewer", default_keybinding="f11", description="Toggle fullscreen right pane"),
140
+
141
+ # AI Code Actions
142
+ CATCommand("ai.explainSelection", "CAT AI: Explain Selection", "AI", description="Ask CAT AI to explain the selected code"),
143
+ CATCommand("ai.fixSelection", "CAT AI: Fix Selection", "AI", description="Ask CAT AI to fix bugs or errors in selection"),
144
+ CATCommand("ai.refactorSelection", "CAT AI: Refactor Selection", "AI", description="Refactor selected code with AI diff"),
145
+ CATCommand("ai.generateTests", "CAT AI: Generate Unit Tests", "AI", description="Generate unit tests for selected code"),
146
+ ]
147
+ for cmd in defaults:
148
+ self.register(cmd)
149
+
150
+
151
+ # Global singleton registry
152
+ _GLOBAL_REGISTRY: Optional[CommandRegistry] = None
153
+
154
+
155
+ def get_command_registry() -> CommandRegistry:
156
+ """Return the global CommandRegistry singleton."""
157
+ global _GLOBAL_REGISTRY
158
+ if _GLOBAL_REGISTRY is None:
159
+ _GLOBAL_REGISTRY = CommandRegistry()
160
+ return _GLOBAL_REGISTRY
@@ -0,0 +1,226 @@
1
+ """CAT Code Editor — Shortcut & Keybinding Manager (calc_terminal/editor/shortcuts.py).
2
+
3
+ Centralized keybinding registration, normalization, conflict detection, and resolution.
4
+ Enforces VS Code keyboard compatibility with customizable bindings.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from dataclasses import asdict, dataclass
12
+ from typing import Dict, List, Optional
13
+
14
+
15
+ @dataclass
16
+ class Keybinding:
17
+ """Represents a key combination mapped to a command ID."""
18
+ command_id: str
19
+ key: str
20
+ when: Optional[str] = None # e.g., "editor", "preview", "global"
21
+ priority: int = 0
22
+ enabled: bool = True
23
+
24
+
25
+ def normalize_key(key: str) -> str:
26
+ """Normalize key combinations into canonical lowercase format: [ctrl+][alt+][shift+]key."""
27
+ if not key:
28
+ return ""
29
+ parts = [p.strip().lower() for p in key.replace("-", "+").split("+")]
30
+ modifiers = set()
31
+ base_key = ""
32
+ for p in parts:
33
+ if p in ("ctrl", "control"):
34
+ modifiers.add("ctrl")
35
+ elif p in ("alt", "option"):
36
+ modifiers.add("alt")
37
+ elif p in ("shift",):
38
+ modifiers.add("shift")
39
+ elif p:
40
+ base_key = p
41
+ ordered = []
42
+ if "ctrl" in modifiers:
43
+ ordered.append("ctrl")
44
+ if "alt" in modifiers:
45
+ ordered.append("alt")
46
+ if "shift" in modifiers:
47
+ ordered.append("shift")
48
+ if base_key:
49
+ ordered.append(base_key)
50
+ return "+".join(ordered)
51
+
52
+
53
+ class ShortcutManager:
54
+ """Manages all keyboard shortcuts across CAT IDE."""
55
+
56
+ CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".cat_keybindings.json")
57
+
58
+ def __init__(self) -> None:
59
+ self._bindings: List[Keybinding] = []
60
+ self._register_default_bindings()
61
+ self.load_user_bindings()
62
+
63
+ def _register_default_bindings(self) -> None:
64
+ """Register default VS Code-compatible keybindings."""
65
+ defaults = [
66
+ # File Operations
67
+ Keybinding("editor.save", "ctrl+s", when="editor", priority=10),
68
+ Keybinding("editor.save", "ctrl+s", when="global", priority=5),
69
+ Keybinding("editor.saveAs", "ctrl+shift+s", when="editor", priority=10),
70
+ Keybinding("editor.closeTab", "ctrl+w", when="editor", priority=10),
71
+ Keybinding("editor.reopenClosedTab", "ctrl+shift+t", when="global", priority=5),
72
+ Keybinding("editor.quickOpen", "ctrl+p", when="global", priority=10),
73
+ Keybinding("editor.commandPalette", "ctrl+shift+p", when="global", priority=10),
74
+
75
+ # Search & Navigation
76
+ Keybinding("editor.find", "ctrl+f", when="editor", priority=10),
77
+ Keybinding("editor.replace", "ctrl+h", when="editor", priority=10),
78
+ Keybinding("workspace.search", "ctrl+shift+f", when="global", priority=5),
79
+ Keybinding("editor.goToDefinition", "f12", when="editor", priority=10),
80
+ Keybinding("editor.findReferences", "shift+f12", when="editor", priority=10),
81
+ Keybinding("editor.rename", "f2", when="editor", priority=10),
82
+
83
+ # Code Editing
84
+ Keybinding("editor.toggleComment", "ctrl+/", when="editor", priority=10),
85
+ Keybinding("editor.selectNextOccurrence", "ctrl+d", when="editor", priority=10),
86
+ Keybinding("editor.deleteLine", "ctrl+shift+k", when="editor", priority=10),
87
+ Keybinding("editor.moveLineUp", "alt+up", when="editor", priority=10),
88
+ Keybinding("editor.moveLineDown", "alt+down", when="editor", priority=10),
89
+ Keybinding("editor.copyLineUp", "shift+alt+up", when="editor", priority=10),
90
+ Keybinding("editor.copyLineDown", "shift+alt+down", when="editor", priority=10),
91
+ Keybinding("editor.insertLineBelow", "alt+enter", when="editor", priority=10),
92
+ Keybinding("editor.insertLineAbove", "alt+shift+enter", when="editor", priority=10),
93
+ Keybinding("editor.triggerCompletion", "ctrl+space", when="editor", priority=10),
94
+
95
+ # Live Preview
96
+ Keybinding("preview.open", "shift+enter", when="editor", priority=20),
97
+ Keybinding("preview.open", "ctrl+shift+enter", when="editor", priority=20),
98
+ Keybinding("preview.open", "cmd+shift+enter", when="editor", priority=20),
99
+ Keybinding("preview.open", "super+shift+enter", when="editor", priority=20),
100
+ Keybinding("preview.open", "ctrl+enter", when="editor", priority=20),
101
+ Keybinding("preview.open", "shift+enter", when="global", priority=5),
102
+ Keybinding("preview.open", "ctrl+shift+enter", when="global", priority=5),
103
+ Keybinding("preview.reload", "ctrl+r", when="preview", priority=10),
104
+ Keybinding("preview.toggleSplit", "ctrl+shift+s", when="preview", priority=10),
105
+
106
+ # Workbench & Shell
107
+ Keybinding("workbench.action.toggleSidebar", "ctrl+b", when="global", priority=10),
108
+ Keybinding("workbench.action.terminal.toggle", "ctrl+`", when="global", priority=10),
109
+ Keybinding("workbench.view.explorer", "ctrl+shift+e", when="global", priority=10),
110
+ Keybinding("workbench.view.scm", "ctrl+shift+g", when="global", priority=10),
111
+ Keybinding("workbench.action.closeOverlays", "escape", when="global", priority=10),
112
+
113
+ # Viewer
114
+ Keybinding("viewer.fullscreen", "f11", when="global", priority=10),
115
+ Keybinding("viewer.zoomIn", "ctrl+=", when="viewer", priority=10),
116
+ Keybinding("viewer.zoomOut", "ctrl+-", when="viewer", priority=10),
117
+ ]
118
+ for kb in defaults:
119
+ self.register(kb)
120
+
121
+ def register(self, binding: Keybinding) -> None:
122
+ """Register a keybinding with normalized key representation."""
123
+ binding.key = normalize_key(binding.key)
124
+ self._bindings.append(binding)
125
+
126
+ def unregister(self, command_id: str, key: Optional[str] = None) -> None:
127
+ """Remove a keybinding by command ID and optionally key string."""
128
+ norm_key = normalize_key(key) if key else None
129
+ self._bindings = [
130
+ b for b in self._bindings
131
+ if not (b.command_id == command_id and (norm_key is None or b.key == norm_key))
132
+ ]
133
+
134
+ def detect_conflicts(self, key: str, when: Optional[str] = None) -> List[Keybinding]:
135
+ """Detect any existing enabled keybindings that share the same key and compatible context."""
136
+ norm_key = normalize_key(key)
137
+ conflicts = []
138
+ for b in self._bindings:
139
+ if not b.enabled:
140
+ continue
141
+ if b.key == norm_key:
142
+ if when is None or b.when is None or b.when == "global" or when == "global" or b.when == when:
143
+ conflicts.append(b)
144
+ return conflicts
145
+
146
+ def resolve(self, raw_key: str, context: Optional[str] = None) -> Optional[str]:
147
+ return self._resolve_impl(raw_key, context)
148
+
149
+ match = resolve
150
+
151
+ def _resolve_impl(self, raw_key: str, context: Optional[str] = None) -> Optional[str]:
152
+ """Resolve a raw keyboard input event into the winning command ID."""
153
+ norm_key = normalize_key(raw_key)
154
+ matching = []
155
+ for b in self._bindings:
156
+ if not b.enabled or b.key != norm_key:
157
+ continue
158
+ # Context match: exact context wins, or global matches if no specific binding
159
+ if context and b.when == context:
160
+ matching.append((b.priority + 20, b.command_id))
161
+ elif b.when in (None, "global"):
162
+ matching.append((b.priority, b.command_id))
163
+ elif not context:
164
+ matching.append((b.priority, b.command_id))
165
+ if not matching:
166
+ return None
167
+ # Highest score wins
168
+ matching.sort(key=lambda item: item[0], reverse=True)
169
+ return matching[0][1]
170
+
171
+ def get_keybindings_for_command(self, command_id: str) -> List[str]:
172
+ """Return all active key combination strings for a command ID."""
173
+ keys = []
174
+ for b in self._bindings:
175
+ if b.command_id == command_id and b.enabled and b.key not in keys:
176
+ keys.append(b.key)
177
+ return keys
178
+
179
+ def list_bindings(self) -> List[Keybinding]:
180
+ """Return all registered keybindings."""
181
+ return list(self._bindings)
182
+
183
+ def load_user_bindings(self) -> None:
184
+ """Load user-configured keybinding overrides from ~/.cat_keybindings.json."""
185
+ if not os.path.exists(self.CONFIG_PATH):
186
+ return
187
+ try:
188
+ with open(self.CONFIG_PATH, "r", encoding="utf-8") as f:
189
+ items = json.load(f)
190
+ if isinstance(items, list):
191
+ for item in items:
192
+ cmd = item.get("command_id")
193
+ key = item.get("key")
194
+ if cmd and key:
195
+ # Higher priority for user customizations
196
+ self.register(Keybinding(
197
+ command_id=cmd,
198
+ key=key,
199
+ when=item.get("when"),
200
+ priority=item.get("priority", 50),
201
+ enabled=item.get("enabled", True),
202
+ ))
203
+ except Exception:
204
+ pass
205
+
206
+ def save_user_bindings(self) -> bool:
207
+ """Persist user custom keybindings to disk."""
208
+ try:
209
+ user_bindings = [asdict(b) for b in self._bindings if b.priority >= 50]
210
+ with open(self.CONFIG_PATH, "w", encoding="utf-8") as f:
211
+ json.dump(user_bindings, f, indent=2)
212
+ return True
213
+ except Exception:
214
+ return False
215
+
216
+
217
+ # Global singleton manager
218
+ _GLOBAL_SHORTCUT_MANAGER: Optional[ShortcutManager] = None
219
+
220
+
221
+ def get_shortcut_manager() -> ShortcutManager:
222
+ """Return the global ShortcutManager singleton."""
223
+ global _GLOBAL_SHORTCUT_MANAGER
224
+ if _GLOBAL_SHORTCUT_MANAGER is None:
225
+ _GLOBAL_SHORTCUT_MANAGER = ShortcutManager()
226
+ return _GLOBAL_SHORTCUT_MANAGER
@@ -0,0 +1,259 @@
1
+ import shutil
2
+ import sys
3
+ import textwrap
4
+ import time
5
+
6
+ from . import theme
7
+ from .mathtext import compose, mathify
8
+
9
+
10
+ def term_width(min_w=60, max_w=100, margin=1, fallback=78):
11
+ """The real, current terminal width, clamped to a sane range.
12
+
13
+ Every box/panel in CCT is drawn at a fixed width — if that width is
14
+ wider than the actual terminal, the border characters wrap onto the
15
+ next line and the box looks broken (this was reported as a "border
16
+ problem": the right-hand `│` shows up in a different, wrong column
17
+ on every row, because every long line wraps at a different point).
18
+ Sizing to the real terminal at startup fixes that in the vast
19
+ majority of cases without needing a live-resize architecture.
20
+ """
21
+ try:
22
+ cols = shutil.get_terminal_size(fallback=(fallback, 24)).columns
23
+ except Exception:
24
+ cols = fallback
25
+ return max(min_w, min(max_w, cols - margin))
26
+
27
+
28
+ WIDTH = term_width()
29
+
30
+ ENGINE_STEPS = [
31
+ "Initializing Chemistry Engine...",
32
+ "Loading Formula Library...",
33
+ "Searching Database...",
34
+ "Checking Units...",
35
+ "Generating Notebook...",
36
+ "Rendering Output...",
37
+ ]
38
+
39
+ DERIVE_STEPS = [
40
+ "Setting up the differential rate law...",
41
+ "Separating variables...",
42
+ "Integrating both sides...",
43
+ "Simplifying the result...",
44
+ "Typesetting derivation...",
45
+ ]
46
+
47
+ IMAGE_STEPS = [
48
+ "Reading Image...",
49
+ "Recognizing Formula...",
50
+ "Extracting Text...",
51
+ "Preparing Solution...",
52
+ ]
53
+
54
+ SPINNER = "\u280b\u2819\u2839\u2838\u283c\u2834\u2826\u2827\u2807\u280f"
55
+
56
+
57
+ def run_steps(steps, step_time=0.35):
58
+ """Print a sequential fake-AI processing log with a spinner + checkmark."""
59
+ for step in steps:
60
+ frames = max(3, int(step_time / 0.05))
61
+ for i in range(frames):
62
+ frame = SPINNER[i % len(SPINNER)]
63
+ sys.stdout.write(f"\r {theme.cyan(frame)} {theme.dim(step)}" + " " * 10)
64
+ sys.stdout.flush()
65
+ time.sleep(0.05)
66
+ sys.stdout.write(f"\r {theme.green(chr(0x2713))} {theme.dim(step)}" + " " * 10 + "\n")
67
+ sys.stdout.flush()
68
+ print(f" {theme.green(chr(0x2713) + ' Completed', bold=True)}")
69
+ time.sleep(0.15)
70
+
71
+
72
+ def _wrap(s, width=WIDTH - 10):
73
+ return textwrap.wrap(s, width=width) or [""]
74
+
75
+
76
+ # ---------------------------------------------------------------- reveal --
77
+ # Animation is on by default; app.py can flip this off (e.g. non-tty runs
78
+ # or a user "fast mode" setting) without touching call sites.
79
+ ANIMATE = True
80
+
81
+
82
+ def set_animation(on):
83
+ global ANIMATE
84
+ ANIMATE = bool(on)
85
+
86
+
87
+ def _border(width, top=True, color=theme.CYAN):
88
+ ch = theme.TL + theme.H * (width - 2) + theme.TR if top else theme.BL + theme.H * (width - 2) + theme.BR
89
+ return theme.fg(ch, color)
90
+
91
+
92
+ def _row(text_line, width, color=theme.CYAN):
93
+ inner = width - 2
94
+ pad = max(0, inner - theme.vlen(text_line) - 2)
95
+ return theme.fg(theme.V, color) + " " + text_line + " " * pad + theme.fg(theme.V, color)
96
+
97
+
98
+ def _title_row(title, width, color):
99
+ inner = width - 2
100
+ tag = f" {title} "
101
+ left = 3
102
+ right = max(0, inner - left - theme.vlen(tag))
103
+ return theme.fg(theme.TL + theme.H * left, color) + theme.fg(tag, color, bold=True) + theme.fg(theme.H * right + theme.TR, color)
104
+
105
+
106
+ def animate_panel(lines, title=None, color=theme.CYAN, width=WIDTH, stage_pause=0.10, line_pause=0.014):
107
+ """Reveal a panel progressively: border draws in, then each line
108
+ fades in top-to-bottom, with a slightly longer pause on section
109
+ headers (lines containing the bullet marker) so every response
110
+ feels like it's being worked out live rather than dumped instantly.
111
+ Falls back to an instant static panel when ANIMATE is off."""
112
+ if not ANIMATE:
113
+ print(theme.panel(lines, title=title, color=color, width=width))
114
+ return
115
+ try:
116
+ print(_title_row(title, width, color) if title else _border(width, top=True, color=color))
117
+ for line in lines:
118
+ print(_row(line, width, color))
119
+ time.sleep(stage_pause if "\u25cf" in line else line_pause)
120
+ print(_border(width, top=False, color=color))
121
+ except Exception:
122
+ # any terminal-width edge case -> never break the actual output
123
+ print(theme.panel(lines, title=title, color=color, width=width))
124
+
125
+
126
+ def render_notebook(nb):
127
+ """Print a full step-by-step notebook panel for a generated numerical,
128
+ revealed one stage at a time (animated)."""
129
+ lines = []
130
+ lines.append(theme.purple(nb["topic"].upper(), bold=True))
131
+ for w in _wrap(nb["question"]):
132
+ lines.append(theme.text(w, bold=True))
133
+ lines.append("")
134
+
135
+ def stage(label, body_lines, final=False):
136
+ color = theme.GREEN if final else theme.CYAN
137
+ lines.append(theme.fg("\u25cf " + label, color, bold=True))
138
+ for bl in body_lines:
139
+ lines.append(" " + bl)
140
+ lines.append("")
141
+
142
+ stage("GIVEN DATA", [theme.dim("\u2022 ") + theme.text(g) for g in nb["given"]])
143
+ stage("FIND", [theme.text(nb["find"])])
144
+ stage("FORMULA", [theme.text(l) for l in compose(nb["formula"]).split("\n")])
145
+ stage("SUBSTITUTION", [theme.text(l) for l in compose(nb["substitution"]).split("\n")])
146
+ stage("CALCULATION", [theme.dim(f"{i+1}. ") + theme.text(mathify(c)) for i, c in enumerate(nb["calculation"])])
147
+
148
+ unit_lines = []
149
+ for w in _wrap(mathify(nb["unit"])):
150
+ unit_lines.append(theme.dim(w))
151
+ stage("UNITS", unit_lines)
152
+
153
+ ver_lines = []
154
+ for w in _wrap(mathify(nb["verification"])):
155
+ ver_lines.append(theme.dim(w))
156
+ stage("VERIFICATION", ver_lines)
157
+
158
+ stage("FINAL ANSWER", [theme.green(nb["final_answer"], bold=True)], final=True)
159
+
160
+ animate_panel(lines, title="notebook", color=theme.CYAN, width=WIDTH)
161
+
162
+
163
+ import re as _re
164
+
165
+ # A header line is either a short markdown-ish "## SOME HEADER" line, or a
166
+ # short, mostly-uppercase line (with or without a trailing colon) — the
167
+ # style CCT Agent's system prompt asks the model to write in (GIVEN DATA,
168
+ # FORMULA, FINAL ANSWER...). Kept permissive since it's parsing free-form
169
+ # LLM text, not a fixed schema.
170
+ _AGENT_HEADER_RE = _re.compile(
171
+ r"^\s*(?:#{1,4}\s*|\*\*)?([A-Z][A-Z0-9 /&()\-]{2,40}?)(?:\*\*)?\s*:?\s*$"
172
+ )
173
+
174
+
175
+ def _is_agent_header(line):
176
+ s = line.strip()
177
+ if not s or len(s) > 48:
178
+ return None
179
+ m = _AGENT_HEADER_RE.match(s)
180
+ if not m:
181
+ return None
182
+ label = m.group(1).strip()
183
+ letters = [c for c in label if c.isalpha()]
184
+ if len(letters) < 3:
185
+ return None
186
+ # Require it to actually look shouted (mostly uppercase), not just any
187
+ # short capitalized sentence fragment.
188
+ upper = sum(1 for c in letters if c.isupper())
189
+ if upper / len(letters) < 0.8:
190
+ return None
191
+ return label
192
+
193
+
194
+ def render_agent_answer(text, tag="CAT Agent", color=theme.PURPLE):
195
+ """Render CAT Agent's free-form "final" text as a notebook-style
196
+ panel — detecting the ALL-CAPS section headers (GIVEN DATA, FORMULA,
197
+ FINAL ANSWER, OVERVIEW, EXPLANATION...) the agent's system prompt asks
198
+ for, and falling back to a single plain ANSWER section for any text
199
+ that doesn't use them (e.g. a short aside or an older-style reply), so
200
+ /agent always reads like a worked notebook page rather than a wall of
201
+ unstructured prose."""
202
+ if not text or not text.strip():
203
+ return
204
+ raw_lines = text.strip().split("\n")
205
+ sections = [] # [(header, [body_lines])]
206
+ current_body = []
207
+ for line in raw_lines:
208
+ header = _is_agent_header(line)
209
+ if header:
210
+ sections.append([header, []])
211
+ current_body = sections[-1][1]
212
+ else:
213
+ if not sections:
214
+ sections.append(["ANSWER", []])
215
+ current_body = sections[-1][1]
216
+ if line.strip():
217
+ current_body.append(line.strip())
218
+
219
+ lines = [theme.badge(tag, theme.BG_AI, fgcolor=(225, 210, 255)), ""]
220
+ for header, body in sections:
221
+ is_final = "FINAL" in header
222
+ hcolor = theme.GREEN if is_final else theme.CYAN
223
+ lines.append(theme.fg("\u25cf " + header, hcolor, bold=True))
224
+ if not body:
225
+ body = [""]
226
+ for b in body:
227
+ for w in _wrap(b):
228
+ lines.append(" " + (theme.green(w, bold=True) if is_final else theme.text(w)))
229
+ lines.append("")
230
+
231
+ animate_panel(lines, title="agent notebook", color=color, width=WIDTH, stage_pause=0.10)
232
+
233
+
234
+ def render_derivation(dv):
235
+ """Print a full step-by-step DERIVATION panel (real calculus, not a
236
+ numeric example) for the given derivation dict from derivations.py,
237
+ revealed step-by-step so the logic visibly builds instead of
238
+ appearing all at once."""
239
+ lines = []
240
+ lines.append(theme.purple(dv["topic"].upper(), bold=True))
241
+ for w in _wrap(dv["goal"]):
242
+ lines.append(theme.text(w, bold=True))
243
+ lines.append("")
244
+
245
+ for i, (label, parts) in enumerate(dv["steps"], 1):
246
+ lines.append(theme.fg(f"\u25cf Step {i} \u2014 {label}", theme.CYAN, bold=True))
247
+ for l in compose(parts).split("\n"):
248
+ lines.append(" " + theme.text(l))
249
+ lines.append("")
250
+
251
+ lines.append(theme.fg("\u25cf RESULT", theme.GREEN, bold=True))
252
+ lines.append(" " + theme.green(dv["result"], bold=True))
253
+ lines.append("")
254
+
255
+ lines.append(theme.fg("\u25cf WHAT IT MEANS", theme.ORANGE, bold=True))
256
+ for w in _wrap(dv["note"]):
257
+ lines.append(" " + theme.dim(w))
258
+
259
+ animate_panel(lines, title="derivation", color=theme.PURPLE, width=WIDTH, stage_pause=0.16)