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,477 @@
1
+ """
2
+ CAT Gestures — manager.py: persistence + CRUD + event dispatch.
3
+
4
+ Storage: ~/.cct_gestures.json (list of gesture dicts)
5
+ If missing, defaults are seeded from bindings.DEFAULT_GESTURES.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import time
13
+ import uuid
14
+ from typing import Dict, List, Optional
15
+
16
+ from .bindings import DEFAULT_GESTURES, GESTURE_ACTIONS, GESTURE_TARGETS, GESTURE_TRIGGERS
17
+
18
+ STORE_PATH = os.path.join(os.path.expanduser("~"), ".cct_gestures.json")
19
+
20
+ # In-memory cache
21
+ _cache: Optional[List[Dict]] = None
22
+ _cache_mtime: float = 0
23
+
24
+
25
+ class Gesture(dict):
26
+ """Typed dict for a gesture — behaves like a plain dict for JSON compat."""
27
+
28
+
29
+ def _load() -> List[Dict]:
30
+ global _cache, _cache_mtime
31
+ try:
32
+ mtime = os.path.getmtime(STORE_PATH) if os.path.exists(STORE_PATH) else 0
33
+ if _cache is not None and mtime == _cache_mtime:
34
+ return list(_cache)
35
+ except Exception:
36
+ pass
37
+ if not os.path.exists(STORE_PATH):
38
+ data = [dict(g) for g in DEFAULT_GESTURES]
39
+ _save(data)
40
+ _cache = list(data)
41
+ try:
42
+ _cache_mtime = os.path.getmtime(STORE_PATH)
43
+ except Exception:
44
+ _cache_mtime = time.time()
45
+ return list(data)
46
+ try:
47
+ with open(STORE_PATH, "r", encoding="utf-8") as f:
48
+ data = json.load(f)
49
+ if isinstance(data, list):
50
+ _cache = list(data)
51
+ try:
52
+ _cache_mtime = os.path.getmtime(STORE_PATH)
53
+ except Exception:
54
+ _cache_mtime = time.time()
55
+ return list(data)
56
+ except Exception:
57
+ pass
58
+ return [dict(g) for g in DEFAULT_GESTURES]
59
+
60
+
61
+ def _save(data: List[Dict]) -> bool:
62
+ global _cache, _cache_mtime
63
+ try:
64
+ with open(STORE_PATH, "w", encoding="utf-8") as f:
65
+ json.dump(data, f, indent=2)
66
+ _cache = list(data)
67
+ try:
68
+ _cache_mtime = os.path.getmtime(STORE_PATH)
69
+ except Exception:
70
+ _cache_mtime = time.time()
71
+ return True
72
+ except Exception:
73
+ return False
74
+
75
+
76
+ def list_gestures() -> List[Dict]:
77
+ return _load()
78
+
79
+
80
+ def get_gesture(gesture_id: str) -> Optional[Dict]:
81
+ for g in _load():
82
+ if g.get("id") == gesture_id:
83
+ return g
84
+ return None
85
+
86
+
87
+ def find_gesture(trigger: str, target: str) -> Optional[Dict]:
88
+ """Find enabled gesture matching trigger + target."""
89
+ try:
90
+ from .. import extensions as _ext
91
+ if not _ext.is_enabled("gestures"):
92
+ return None
93
+ except Exception:
94
+ pass
95
+ for g in _load():
96
+ if g.get("trigger") == trigger and g.get("target") == target and g.get("enabled"):
97
+ return g
98
+ return None
99
+
100
+
101
+ def _is_valid_shortcut(keys: str) -> bool:
102
+ """Validate shortcut like ctrl+s, shift+enter, ctrl+shift+p, f5, alt+enter."""
103
+ import re
104
+ if not keys or not isinstance(keys, str):
105
+ return False
106
+ k = keys.strip().lower()
107
+ # normalize: allow combos with +, allow single keys f1-f12, letters, numbers, enter/space/tab/esc etc
108
+ pattern = r"^(?:(?:ctrl|shift|alt|cmd|meta)\+)*(?:[a-z0-9]|f(?:[1-9]|1[0-2])|enter|space|tab|esc|escape|backspace|delete|up|down|left|right|home|end|pageup|pagedown|insert)$"
109
+ return bool(re.match(pattern, k))
110
+
111
+ def add_gesture(
112
+ name: str,
113
+ trigger: str,
114
+ target: str,
115
+ action: str,
116
+ description: str = "",
117
+ enabled: bool = True,
118
+ keys: str = "",
119
+ ) -> Optional[Dict]:
120
+ if trigger not in GESTURE_TRIGGERS:
121
+ return None
122
+ if target not in GESTURE_TARGETS:
123
+ return None
124
+ if action not in GESTURE_ACTIONS:
125
+ return None
126
+ # shortcut trigger requires a key combo
127
+ if trigger == "shortcut":
128
+ if not keys or not _is_valid_shortcut(keys):
129
+ return None
130
+ # ensure keys unique among enabled shortcuts
131
+ for g in _load():
132
+ if g.get("trigger") == "shortcut" and g.get("keys","").lower() == keys.lower() and g.get("enabled"):
133
+ return None # duplicate
134
+ data = _load()
135
+ gesture = {
136
+ "id": f"gesture_{uuid.uuid4().hex[:8]}",
137
+ "name": name,
138
+ "trigger": trigger,
139
+ "target": target,
140
+ "action": action,
141
+ "enabled": bool(enabled),
142
+ "description": description or f"{trigger} on {target} → {action}",
143
+ "created_at": time.time(),
144
+ }
145
+ if trigger == "shortcut" and keys:
146
+ gesture["keys"] = keys.strip().lower()
147
+ gesture["description"] = f"{keys} ({target}) → {action}"
148
+ data.append(gesture)
149
+ _save(data)
150
+ _publish_gesture_event("gesture_created", gesture)
151
+ return gesture
152
+
153
+
154
+ def find_shortcut(keys: str) -> Optional[Dict]:
155
+ """Find enabled shortcut gesture matching keys (case-insensitive)."""
156
+ # Extension guard: Gestures disabled → inactive (spec section 5)
157
+ try:
158
+ from .. import extensions as _ext
159
+ if not _ext.is_enabled("gestures"):
160
+ return None
161
+ except Exception:
162
+ pass
163
+ k = (keys or "").strip().lower()
164
+ if not k:
165
+ return None
166
+ for g in _load():
167
+ if g.get("trigger") == "shortcut" and g.get("keys","").lower() == k and g.get("enabled"):
168
+ return g
169
+ return None
170
+
171
+
172
+ def handle_shortcut(keys: str, app=None, context: Optional[Dict] = None) -> bool:
173
+ """Dispatch a keyboard shortcut."""
174
+ # Extension guard: Gestures disabled → inactive
175
+ try:
176
+ from .. import extensions as _ext
177
+ if not _ext.is_enabled("gestures"):
178
+ return False
179
+ except Exception:
180
+ pass
181
+ g = find_shortcut(keys)
182
+ if not g:
183
+ return False
184
+ action = g.get("action")
185
+ if not action:
186
+ return False
187
+ try:
188
+ from .bindings import ACTION_HANDLERS
189
+ handler = ACTION_HANDLERS.get(action)
190
+ if handler is not None:
191
+ return bool(handler(app, context or {}, g))
192
+ return _builtin_handler(action, app, context or {}, g)
193
+ except Exception:
194
+ return False
195
+
196
+
197
+ def update_gesture(gesture_id: str, **fields) -> Optional[Dict]:
198
+ data = _load()
199
+ for g in data:
200
+ if g.get("id") == gesture_id:
201
+ for k in ("name", "trigger", "target", "action", "enabled", "description", "keys"):
202
+ if k in fields:
203
+ if k == "trigger" and fields[k] not in GESTURE_TRIGGERS:
204
+ continue
205
+ if k == "target" and fields[k] not in GESTURE_TARGETS:
206
+ continue
207
+ if k == "action" and fields[k] not in GESTURE_ACTIONS:
208
+ continue
209
+ if k == "keys" and fields[k]:
210
+ if not _is_valid_shortcut(str(fields[k])):
211
+ continue
212
+ fields[k] = str(fields[k]).strip().lower()
213
+ g[k] = fields[k]
214
+ _save(data)
215
+ _publish_gesture_event("gesture_updated", g)
216
+ return g
217
+ return None
218
+
219
+
220
+ def delete_gesture(gesture_id: str) -> bool:
221
+ data = _load()
222
+ remaining = [g for g in data if g.get("id") != gesture_id]
223
+ if len(remaining) == len(data):
224
+ return False
225
+ _save(remaining)
226
+ _publish_gesture_event("gesture_deleted", {"id": gesture_id})
227
+ return True
228
+
229
+
230
+ def toggle_gesture(gesture_id: str) -> Optional[Dict]:
231
+ data = _load()
232
+ for g in data:
233
+ if g.get("id") == gesture_id:
234
+ g["enabled"] = not bool(g.get("enabled", True))
235
+ _save(data)
236
+ _publish_gesture_event("gesture_toggled", g)
237
+ return g
238
+ return None
239
+
240
+
241
+ def reset_defaults() -> List[Dict]:
242
+ data = [dict(g) for g in DEFAULT_GESTURES]
243
+ _save(data)
244
+ _publish_gesture_event("gestures_reset", {"count": len(data)})
245
+ return list(data)
246
+
247
+
248
+ def _publish_gesture_event(topic: str, detail: Dict):
249
+ try:
250
+ from .. import eventbus
251
+
252
+ eventbus.bus.publish(topic, **detail)
253
+ except Exception:
254
+ pass
255
+
256
+
257
+ # ---------------------------------------------------------------- handler dispatch
258
+
259
+ def handle_gesture(trigger: str, target: str, app=None, context: Optional[Dict] = None) -> bool:
260
+ """
261
+ Dispatch a gesture: look up enabled gesture for (trigger, target),
262
+ then invoke its action handler.
263
+
264
+ Returns True if a handler ran, False if no matching gesture/handler.
265
+ """
266
+ try:
267
+ from .. import extensions as _ext
268
+ if not _ext.is_enabled("gestures"):
269
+ return False
270
+ except Exception:
271
+ pass
272
+ gesture = find_gesture(trigger, target)
273
+ if not gesture:
274
+ return False
275
+ action = gesture.get("action")
276
+ if not action:
277
+ return False
278
+ # Try to get handler from bindings
279
+ try:
280
+ from .bindings import ACTION_HANDLERS
281
+
282
+ handler = ACTION_HANDLERS.get(action)
283
+ if handler is not None:
284
+ try:
285
+ return bool(handler(app, context or {}, gesture))
286
+ except Exception:
287
+ return False
288
+ # Built-in fallback handlers (no app dependency)
289
+ return _builtin_handler(action, app, context or {}, gesture)
290
+ except Exception:
291
+ return False
292
+
293
+
294
+ def _builtin_handler(action: str, app, context: Dict, gesture: Dict) -> bool:
295
+ """Fallback handlers when no custom handler registered — covers spec examples."""
296
+ try:
297
+ # --- new shortcut actions ---
298
+ if action == "show_preview":
299
+ # Shift+Enter or toolbar ▷
300
+ if app and hasattr(app, "_workspace_shell") and app._workspace_shell:
301
+ try:
302
+ # find active file or any web file
303
+ editor = app._workspace_shell.editor
304
+ # delegate to EditorPane's preview if possible
305
+ if editor and hasattr(editor, "action_show_preview"):
306
+ editor.action_show_preview()
307
+ return True
308
+ # fallback: post PreviewRequested
309
+ from ..ui.events import PreviewRequested
310
+ # try to get path from context or active editor
311
+ path = (context.get("path") if context else None)
312
+ if not path and editor:
313
+ area = editor.active_text_area() if hasattr(editor, "active_text_area") else None
314
+ path = getattr(area, "path", None) if area else None
315
+ if path:
316
+ app.post_message(PreviewRequested(path))
317
+ return True
318
+ # try workspace root fallback
319
+ app.post_message(PreviewRequested(""))
320
+ return True
321
+ except Exception:
322
+ pass
323
+ return False
324
+ if action == "toggle_preview":
325
+ if app and hasattr(app, "action_toggle_preview"):
326
+ app.action_toggle_preview()
327
+ return True
328
+ return False
329
+ if action == "save_file":
330
+ if app and hasattr(app, "action_save_file"):
331
+ app.action_save_file()
332
+ return True
333
+ # fallback via editor
334
+ if app and hasattr(app, "_workspace_shell") and app._workspace_shell:
335
+ try:
336
+ app._workspace_shell.editor.save_active()
337
+ return True
338
+ except Exception:
339
+ pass
340
+ return False
341
+ if action == "toggle_sidebar":
342
+ if app and hasattr(app, "action_toggle_sidebar"):
343
+ app.action_toggle_sidebar()
344
+ return True
345
+ return False
346
+ if action == "open_palette":
347
+ if app and hasattr(app, "action_open_palette"):
348
+ app.action_open_palette()
349
+ return True
350
+ return False
351
+ if action == "open_gestures":
352
+ if app:
353
+ try:
354
+ from ..ui.gestures_panel import GesturesPanel
355
+ app.push_screen(GesturesPanel())
356
+ return True
357
+ except Exception:
358
+ pass
359
+ return False
360
+ if action == "open_vision":
361
+ if app:
362
+ try:
363
+ from ..ui.vision_panel import VisionPanel
364
+ app.push_screen(VisionPanel())
365
+ return True
366
+ except Exception:
367
+ pass
368
+ return False
369
+
370
+ if action == "open_vscode":
371
+ # Double-click editor → open in VS Code
372
+ path = (context.get("path") or context.get("workspace") or "") if context else ""
373
+ if app is not None and hasattr(app, "_workspace_root") and app._workspace_root:
374
+ path = path or app._workspace_root
375
+ if path:
376
+ import subprocess
377
+ import shutil
378
+ code_bin = shutil.which("code") or shutil.which("code.cmd")
379
+ if code_bin:
380
+ subprocess.Popen([code_bin, path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
381
+ if app:
382
+ try:
383
+ app._system_note(f"Opened {path} in VS Code")
384
+ except Exception:
385
+ pass
386
+ return True
387
+ if app:
388
+ try:
389
+ app._system_note("VS Code not found (install 'code' on PATH)")
390
+ except Exception:
391
+ pass
392
+ return False
393
+
394
+ if action == "fullscreen_preview":
395
+ if app and hasattr(app, "action_toggle_right_pane_fullscreen"):
396
+ app.action_toggle_right_pane_fullscreen()
397
+ return True
398
+ return False
399
+
400
+ if action == "fullscreen_editor":
401
+ if app and hasattr(app, "action_toggle_right_pane_fullscreen"):
402
+ app.action_toggle_right_pane_fullscreen()
403
+ return True
404
+ return False
405
+
406
+ if action == "quick_actions":
407
+ path = context.get("path", "") if context else ""
408
+ if app:
409
+ try:
410
+ from ..ui.context_menu import FileQuickActions
411
+
412
+ # Show quick actions via system note fallback if UI not available
413
+ app._system_note(f"Quick actions for: {path or 'file'}")
414
+ except Exception:
415
+ pass
416
+ return True
417
+
418
+ if action == "reveal_actions":
419
+ path = context.get("path", "") if context else ""
420
+ if app:
421
+ try:
422
+ app._system_note(f"Actions revealed for: {path}")
423
+ except Exception:
424
+ pass
425
+ return True
426
+
427
+ if action == "before_after":
428
+ if app and hasattr(app, "_workspace_shell"):
429
+ try:
430
+ editor = app._workspace_shell.editor
431
+ if editor:
432
+ editor.show_diff()
433
+ return True
434
+ except Exception:
435
+ pass
436
+ return False
437
+
438
+ if action == "edit_message":
439
+ turn_id = context.get("turn_id") if context else None
440
+ if turn_id and app:
441
+ try:
442
+ # Trigger rewrite via MessageContextAction
443
+ from ..ui.events import MessageContextAction
444
+
445
+ app.post_message(MessageContextAction("rewrite", turn_id))
446
+ return True
447
+ except Exception:
448
+ pass
449
+ return False
450
+
451
+ if action == "copy_path":
452
+ path = context.get("path", "") if context else ""
453
+ if path:
454
+ try:
455
+ from .. import code_editor
456
+
457
+ code_editor.copy_to_clipboard(path)
458
+ if app:
459
+ app._system_note(f"Copied path: {path}")
460
+ return True
461
+ except Exception:
462
+ pass
463
+ return False
464
+ except Exception:
465
+ return False
466
+ return False
467
+
468
+
469
+ # Convenience: register builtins on import
470
+ try:
471
+ from .bindings import ACTION_HANDLERS as _AH
472
+
473
+ for _act in ("open_vscode", "fullscreen_preview", "fullscreen_editor", "quick_actions", "reveal_actions", "before_after", "edit_message", "copy_path", "show_preview", "toggle_preview", "save_file", "toggle_sidebar", "open_palette", "open_gestures", "open_vision"):
474
+ if _act not in _AH:
475
+ _AH[_act] = lambda app, ctx, g, _a=_act: _builtin_handler(_a, app, ctx, g)
476
+ except Exception:
477
+ pass