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,258 @@
1
+ """
2
+ CAT v0.8.a — Cursor / Pointer Tracking & Context Inference.
3
+
4
+ Records Pointer Events (mouse/pen/touch) with throttling, dwell detection,
5
+ and probabilistic attention inference — never "mind reading".
6
+
7
+ Spec 10/11: pointer metadata + Cursor Context Inference.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ import time
14
+ from dataclasses import dataclass, field, asdict
15
+ from enum import Enum
16
+ from typing import Any, Dict, List, Optional, Tuple
17
+
18
+
19
+ class PointerType(str, Enum):
20
+ MOUSE = "mouse"
21
+ PEN = "pen"
22
+ TOUCH = "touch"
23
+ UNKNOWN = "unknown"
24
+
25
+
26
+ @dataclass
27
+ class PointerEvent:
28
+ timestamp: float
29
+ x: float # normalized 0.0-1.0
30
+ y: float # normalized 0.0-1.0
31
+ pointer_id: int = 0
32
+ pointer_type: str = "mouse"
33
+ button: int = 0
34
+ pressure: float = 0.5
35
+ movement_x: float = 0.0
36
+ movement_y: float = 0.0
37
+ is_click: bool = False
38
+ is_double_click: bool = False
39
+ is_drag: bool = False
40
+ is_scroll: bool = False
41
+ scroll_delta: float = 0.0
42
+
43
+ def to_dict(self) -> Dict[str, Any]:
44
+ return {
45
+ "timestamp": self.timestamp,
46
+ "x": round(self.x, 4),
47
+ "y": round(self.y, 4),
48
+ "pointerId": self.pointer_id,
49
+ "pointerType": self.pointer_type,
50
+ "button": self.button,
51
+ "pressure": round(self.pressure, 3),
52
+ "movement": [round(self.movement_x, 4), round(self.movement_y, 4)],
53
+ "click": self.is_click,
54
+ "doubleClick": self.is_double_click,
55
+ "drag": self.is_drag,
56
+ "scroll": self.is_scroll,
57
+ }
58
+
59
+
60
+ @dataclass
61
+ class DwellRegion:
62
+ x: float
63
+ y: float
64
+ radius: float
65
+ dwell_ms: float
66
+ event_count: int
67
+ started_at: float
68
+ ended_at: float
69
+
70
+ def to_dict(self) -> Dict[str, Any]:
71
+ return {
72
+ "x": round(self.x, 4),
73
+ "y": round(self.y, 4),
74
+ "radius": round(self.radius, 4),
75
+ "dwell_ms": round(self.dwell_ms, 1),
76
+ "event_count": self.event_count,
77
+ "started_at": self.started_at,
78
+ "ended_at": self.ended_at,
79
+ }
80
+
81
+
82
+ class CursorTracker:
83
+ """
84
+ Throttled pointer tracking + dwell detection + attention inference.
85
+
86
+ - Throttle: coalesce high-rate pointermove (16ms min interval)
87
+ - Dwell: cursor stays within radius for >= dwell_threshold_ms
88
+ - Inference: probabilistic observations, never mind-reading
89
+ """
90
+
91
+ def __init__(self,
92
+ throttle_ms: float = 16.0,
93
+ dwell_threshold_ms: float = 800.0,
94
+ dwell_radius: float = 0.03,
95
+ max_events: int = 2000):
96
+ self.throttle_ms = throttle_ms
97
+ self.dwell_threshold_ms = dwell_threshold_ms
98
+ self.dwell_radius = dwell_radius
99
+ self.max_events = max_events
100
+
101
+ self._events: List[PointerEvent] = []
102
+ self._last_emit_time: float = 0.0
103
+ self._last_pos: Tuple[float, float] = (0.0, 0.0)
104
+ self._dwell_start: Optional[float] = None
105
+ self._dwell_center: Tuple[float, float] = (0.0, 0.0)
106
+ self._dwells: List[DwellRegion] = []
107
+ self._click_count: int = 0
108
+ self._scroll_total: float = 0.0
109
+
110
+ # -- ingestion --
111
+
112
+ def add_event(self, evt: PointerEvent) -> bool:
113
+ """Add event with throttling. Returns True if stored."""
114
+ now = evt.timestamp or time.time()
115
+ # throttle pointermove without click/drag
116
+ is_move_only = not (evt.is_click or evt.is_drag or evt.is_scroll or evt.is_double_click)
117
+ if is_move_only and self._events:
118
+ elapsed_ms = (now - self._last_emit_time) * 1000
119
+ if elapsed_ms < self.throttle_ms:
120
+ # coalesce: update last move position instead of appending
121
+ if self._events and not self._events[-1].is_click:
122
+ self._events[-1].x = evt.x
123
+ self._events[-1].y = evt.y
124
+ self._events[-1].timestamp = now
125
+ self._events[-1].movement_x = evt.movement_x
126
+ self._events[-1].movement_y = evt.movement_y
127
+ self._update_dwell(evt.x, evt.y, now)
128
+ return False
129
+ self._events.append(evt)
130
+ self._last_emit_time = now
131
+ self._last_pos = (evt.x, evt.y)
132
+ if len(self._events) > self.max_events:
133
+ self._events = self._events[-self.max_events:]
134
+ if evt.is_click:
135
+ self._click_count += 1
136
+ if evt.is_scroll:
137
+ self._scroll_total += abs(evt.scroll_delta)
138
+ self._update_dwell(evt.x, evt.y, now)
139
+ return True
140
+
141
+ def add_raw(self, x: float, y: float,
142
+ pointer_type: str = "mouse",
143
+ pointer_id: int = 0,
144
+ button: int = 0,
145
+ pressure: float = 0.5,
146
+ movement_x: float = 0.0,
147
+ movement_y: float = 0.0,
148
+ is_click: bool = False,
149
+ is_double_click: bool = False,
150
+ is_drag: bool = False,
151
+ is_scroll: bool = False,
152
+ scroll_delta: float = 0.0,
153
+ timestamp: Optional[float] = None) -> bool:
154
+ return self.add_event(PointerEvent(
155
+ timestamp=timestamp or time.time(),
156
+ x=max(0.0, min(1.0, x)),
157
+ y=max(0.0, min(1.0, y)),
158
+ pointer_id=pointer_id,
159
+ pointer_type=pointer_type,
160
+ button=button,
161
+ pressure=pressure,
162
+ movement_x=movement_x,
163
+ movement_y=movement_y,
164
+ is_click=is_click,
165
+ is_double_click=is_double_click,
166
+ is_drag=is_drag,
167
+ is_scroll=is_scroll,
168
+ scroll_delta=scroll_delta,
169
+ ))
170
+
171
+ # -- dwell --
172
+
173
+ def _update_dwell(self, x: float, y: float, now: float):
174
+ if self._dwell_start is None:
175
+ self._dwell_start = now
176
+ self._dwell_center = (x, y)
177
+ return
178
+ dist = math.hypot(x - self._dwell_center[0], y - self._dwell_center[1])
179
+ if dist > self.dwell_radius:
180
+ # moved away: finalize previous dwell if significant
181
+ dwell_ms = (now - self._dwell_start) * 1000
182
+ if dwell_ms >= self.dwell_threshold_ms:
183
+ self._dwells.append(DwellRegion(
184
+ x=self._dwell_center[0],
185
+ y=self._dwell_center[1],
186
+ radius=self.dwell_radius,
187
+ dwell_ms=dwell_ms,
188
+ event_count=1,
189
+ started_at=self._dwell_start,
190
+ ended_at=now,
191
+ ))
192
+ if len(self._dwells) > 100:
193
+ self._dwells = self._dwells[-50:]
194
+ self._dwell_start = now
195
+ self._dwell_center = (x, y)
196
+ # else still dwelling — no action until move away or query
197
+
198
+ def get_current_dwell_ms(self) -> float:
199
+ if self._dwell_start is None:
200
+ return 0.0
201
+ return (time.time() - self._dwell_start) * 1000
202
+
203
+ # -- queries --
204
+
205
+ @property
206
+ def events(self) -> List[PointerEvent]:
207
+ return list(self._events)
208
+
209
+ @property
210
+ def dwells(self) -> List[DwellRegion]:
211
+ return list(self._dwells)
212
+
213
+ def recent_events(self, n: int = 50) -> List[Dict]:
214
+ return [e.to_dict() for e in self._events[-n:]]
215
+
216
+ def get_cursor_context(self) -> Dict[str, Any]:
217
+ """Probabilistic cursor context for AI — never mind-reading."""
218
+ if not self._events:
219
+ return {"has_data": False}
220
+ latest = self._events[-1]
221
+ dwell_ms = self.get_current_dwell_ms()
222
+ recent = self._events[-20:]
223
+ # attention inference
224
+ observations: List[str] = []
225
+ inferences: List[str] = []
226
+ if dwell_ms >= self.dwell_threshold_ms:
227
+ observations.append(f"Cursor remained near ({latest.x:.2f}, {latest.y:.2f}) for {dwell_ms:.0f}ms.")
228
+ inferences.append("User is likely directing attention to this region.")
229
+ if self._click_count >= 2:
230
+ clicks_near = [e for e in recent if e.is_click and math.hypot(e.x - latest.x, e.y - latest.y) < 0.05]
231
+ if len(clicks_near) >= 2:
232
+ observations.append(f"User clicked near ({latest.x:.2f}, {latest.y:.2f}) {len(clicks_near)} times.")
233
+ inferences.append("Repeated clicks suggest the target may be unresponsive or needs attention.")
234
+ if any(e.is_drag for e in recent):
235
+ observations.append("User performed a drag gesture recently.")
236
+ inferences.append("User may be repositioning or selecting content.")
237
+ if abs(self._scroll_total) > 0.5:
238
+ observations.append(f"User scrolled (total delta {self._scroll_total:.1f}).")
239
+ return {
240
+ "has_data": True,
241
+ "latest": latest.to_dict(),
242
+ "dwell_ms": round(dwell_ms, 1),
243
+ "is_dwelling": dwell_ms >= self.dwell_threshold_ms,
244
+ "click_count": self._click_count,
245
+ "scroll_total": round(self._scroll_total, 2),
246
+ "recent_dwell_regions": [d.to_dict() for d in self._dwells[-5:]],
247
+ "observations": observations,
248
+ "inferences": inferences,
249
+ "cursor_label": f"Cursor: X {int(latest.x*1920)} Y {int(latest.y*1080)} (normalized {latest.x:.3f}, {latest.y:.3f})",
250
+ }
251
+
252
+ def clear(self):
253
+ self._events.clear()
254
+ self._dwells.clear()
255
+ self._click_count = 0
256
+ self._scroll_total = 0
257
+ self._dwell_start = None
258
+ self._last_emit_time = 0.0
@@ -0,0 +1,66 @@
1
+ """
2
+ CAT v0.8.a — Vision Event System (spec 41).
3
+
4
+ Shared event names + helper to emit on both the backend bus (eventbus)
5
+ and the UI event stream (event_stream) where appropriate.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from typing import Any, Dict, Optional
12
+
13
+ # Vision event names (spec 41)
14
+ VISION_SESSION_STARTED = "VISION_SESSION_STARTED"
15
+ VISION_SESSION_STOPPED = "VISION_SESSION_STOPPED"
16
+ VISION_CAPTURE_STARTED = "VISION_CAPTURE_STARTED"
17
+ VISION_CAPTURE_ENDED = "VISION_CAPTURE_ENDED"
18
+ VISION_FRAME_SELECTED = "VISION_FRAME_SELECTED"
19
+ VISION_FRAME_ANALYZED = "VISION_FRAME_ANALYZED"
20
+ VISION_CURSOR_EVENT = "VISION_CURSOR_EVENT"
21
+ VISION_ANNOTATION_CREATED = "VISION_ANNOTATION_CREATED"
22
+ VISION_ANNOTATION_UPDATED = "VISION_ANNOTATION_UPDATED"
23
+ VISION_ANNOTATION_DELETED = "VISION_ANNOTATION_DELETED"
24
+ VISION_USER_MESSAGE = "VISION_USER_MESSAGE"
25
+ VISION_AI_ANALYSIS = "VISION_AI_ANALYSIS"
26
+ VISION_FIX_PROPOSED = "VISION_FIX_PROPOSED"
27
+ VISION_FIX_APPLIED = "VISION_FIX_APPLIED"
28
+ VISION_PREVIEW_REFRESHED = "VISION_PREVIEW_REFRESHED"
29
+ VISION_VERIFICATION_COMPLETE = "VISION_VERIFICATION_COMPLETE"
30
+
31
+ ALL_VISION_EVENTS = [
32
+ VISION_SESSION_STARTED,
33
+ VISION_SESSION_STOPPED,
34
+ VISION_CAPTURE_STARTED,
35
+ VISION_CAPTURE_ENDED,
36
+ VISION_FRAME_SELECTED,
37
+ VISION_FRAME_ANALYZED,
38
+ VISION_CURSOR_EVENT,
39
+ VISION_ANNOTATION_CREATED,
40
+ VISION_ANNOTATION_UPDATED,
41
+ VISION_ANNOTATION_DELETED,
42
+ VISION_USER_MESSAGE,
43
+ VISION_AI_ANALYSIS,
44
+ VISION_FIX_PROPOSED,
45
+ VISION_FIX_APPLIED,
46
+ VISION_PREVIEW_REFRESHED,
47
+ VISION_VERIFICATION_COMPLETE,
48
+ ]
49
+
50
+
51
+ def emit_vision_event(event: str, **data: Any):
52
+ """Emit a vision event on the backend bus + event_stream (best effort)."""
53
+ data.setdefault("ts", time.time())
54
+ data.setdefault("source", "vision")
55
+ # backend bus (sync pub/sub) — ui/app.py can subscribe
56
+ try:
57
+ from .. import eventbus
58
+ eventbus.bus.publish(event, **data)
59
+ except Exception:
60
+ pass
61
+ # event_stream (bounded buffer with subscribers)
62
+ try:
63
+ from .. import event_stream
64
+ event_stream.stream.emit(event, **data)
65
+ except Exception:
66
+ pass
@@ -0,0 +1,259 @@
1
+ """
2
+ CAT v0.8.a — Frame Pipeline.
3
+
4
+ Adaptive sampling, change detection, quality scoring, and frame selection.
5
+ The pipeline decides which frames are worth sending to the AI model,
6
+ discards duplicates/blur, and maintains a sliding window of context frames.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import hashlib
13
+ import time
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum
16
+ from typing import Any, Dict, List, Optional, Tuple
17
+
18
+
19
+ class ChangeLevel(str, Enum):
20
+ NONE = "none" # No meaningful change
21
+ MINOR = "minor" # Small UI update
22
+ MODERATE = "moderate" # Noticeable change, worth keeping
23
+ MAJOR = "major" # Significant change, must keep
24
+ CRITICAL = "critical" # Error/screen change, always keep
25
+
26
+
27
+ @dataclass
28
+ class FrameMeta:
29
+ timestamp: float = 0.0
30
+ width: int = 0
31
+ height: int = 0
32
+ pointer_x: float = 0.0
33
+ pointer_y: float = 0.0
34
+ pointer_down: bool = False
35
+ scroll_y: int = 0
36
+ active_element: str = ""
37
+ url: str = ""
38
+ title: str = ""
39
+ annotations: List[Dict] = field(default_factory=list)
40
+
41
+ def to_dict(self) -> Dict[str, Any]:
42
+ return {
43
+ "timestamp": self.timestamp,
44
+ "width": self.width,
45
+ "height": self.height,
46
+ "pointer_x": self.pointer_x,
47
+ "pointer_y": self.pointer_y,
48
+ "pointer_down": self.pointer_down,
49
+ "scroll_y": self.scroll_y,
50
+ "active_element": self.active_element,
51
+ "url": self.url,
52
+ "title": self.title,
53
+ "annotations": self.annotations,
54
+ }
55
+
56
+
57
+ @dataclass
58
+ class FrameEntry:
59
+ frame_b64: str
60
+ meta: FrameMeta
61
+ index: int
62
+ change_level: ChangeLevel
63
+ quality_score: float
64
+ hash: str
65
+ kept: bool = True
66
+
67
+
68
+ class FramePipeline:
69
+ """Manages frame ingestion, change detection, and selection."""
70
+
71
+ def __init__(self, config: Any = None):
72
+ self._config = config
73
+ self._frames: List[FrameEntry] = []
74
+ self._last_hash: str = ""
75
+ self._last_change_time: float = 0.0
76
+ self._frame_count: int = 0
77
+ self._change_threshold = getattr(config, "change_threshold", 0.05)
78
+ self._max_frames = getattr(config, "max_frames", 500)
79
+ self._saved_max = getattr(config, "saved_frames_max", 20)
80
+ self._context_window = getattr(config, "context_window_frames", 5)
81
+ self._min_frame_interval = 1.0 / max(getattr(config, "fps", 1.0), 0.1)
82
+
83
+ @property
84
+ def frame_count(self) -> int:
85
+ return self._frame_count
86
+
87
+ def ingest(self, frame_b64: str,
88
+ meta: Optional[FrameMeta] = None) -> Optional[Dict[str, Any]]:
89
+ """Ingest a frame, detect change, decide whether to keep it.
90
+ Returns frame info dict if the frame was selected, None otherwise.
91
+ """
92
+ if meta is None:
93
+ meta = FrameMeta(timestamp=time.time())
94
+
95
+ self._frame_count += 1
96
+ frame_hash = self._compute_hash(frame_b64)
97
+ change_level = self._detect_change(frame_b64, frame_hash)
98
+ quality = self._score_quality(frame_b64, meta)
99
+
100
+ if change_level == ChangeLevel.NONE and quality < 0.3:
101
+ return None
102
+
103
+ entry = FrameEntry(
104
+ frame_b64=frame_b64,
105
+ meta=meta,
106
+ index=self._frame_count,
107
+ change_level=change_level,
108
+ quality_score=quality,
109
+ hash=frame_hash,
110
+ kept=True,
111
+ )
112
+
113
+ if len(self._frames) >= self._max_frames:
114
+ self._prune()
115
+
116
+ self._frames.append(entry)
117
+ self._last_hash = frame_hash
118
+ if change_level.value in ("moderate", "major", "critical"):
119
+ self._last_change_time = time.time()
120
+
121
+ return {
122
+ "frame_index": entry.index,
123
+ "change_level": change_level.value,
124
+ "quality_score": round(quality, 3),
125
+ "kept": True,
126
+ "meta": meta.to_dict(),
127
+ "total_frames": self._frame_count,
128
+ "stored_frames": len(self._frames),
129
+ }
130
+
131
+ def get_context_frames(self, n: Optional[int] = None) -> List[Dict]:
132
+ """Return the last N frames for AI context (without full base64)."""
133
+ n = n or self._context_window
134
+ recent = [f for f in self._frames if f.kept][-n:]
135
+ return [
136
+ {
137
+ "frame_index": f.index,
138
+ "change_level": f.change_level.value,
139
+ "quality_score": round(f.quality_score, 3),
140
+ "meta": f.meta.to_dict(),
141
+ "has_annotations": bool(f.meta.annotations),
142
+ }
143
+ for f in recent
144
+ ]
145
+
146
+ def get_latest_frame(self) -> Optional[FrameEntry]:
147
+ kept = [f for f in self._frames if f.kept]
148
+ return kept[-1] if kept else None
149
+
150
+ def get_frame_b64(self, frame_index: int) -> Optional[str]:
151
+ for f in self._frames:
152
+ if f.index == frame_index:
153
+ return f.frame_b64
154
+ return None
155
+
156
+ def get_best_frame(self) -> Optional[FrameEntry]:
157
+ """Select the best frame for AI analysis: highest change + quality."""
158
+ kept = [f for f in self._frames if f.kept]
159
+ if not kept:
160
+ return None
161
+ ranked = sorted(
162
+ kept,
163
+ key=lambda f: (
164
+ list(ChangeLevel).index(f.change_level) * 10
165
+ + f.quality_score
166
+ ),
167
+ reverse=True,
168
+ )
169
+ return ranked[0]
170
+
171
+ def get_summary(self) -> Dict[str, Any]:
172
+ kept = [f for f in self._frames if f.kept]
173
+ return {
174
+ "total_frames": self._frame_count,
175
+ "stored_frames": len(self._frames),
176
+ "kept_frames": len(kept),
177
+ "change_levels": {
178
+ level.value: sum(
179
+ 1 for f in kept if f.change_level == level
180
+ )
181
+ for level in ChangeLevel
182
+ },
183
+ "avg_quality": round(
184
+ sum(f.quality_score for f in kept) / max(len(kept), 1), 3
185
+ ),
186
+ "context_window": self._context_window,
187
+ }
188
+
189
+ def clear(self):
190
+ self._frames.clear()
191
+ self._last_hash = ""
192
+ self._frame_count = 0
193
+
194
+ def _compute_hash(self, frame_b64: str) -> str:
195
+ sample = frame_b64[:4096] if len(frame_b64) > 4096 else frame_b64
196
+ return hashlib.md5(sample.encode("ascii", errors="ignore")).hexdigest()
197
+
198
+ def _detect_change(self, frame_b64: str, frame_hash: str) -> ChangeLevel:
199
+ if not self._last_hash:
200
+ return ChangeLevel.MAJOR
201
+
202
+ if frame_hash == self._last_hash:
203
+ return ChangeLevel.NONE
204
+
205
+ similarity = self._compare_hashes(self._last_hash, frame_hash)
206
+ if similarity > 0.97:
207
+ return ChangeLevel.NONE
208
+ elif similarity > 0.90:
209
+ return ChangeLevel.MINOR
210
+ elif similarity > 0.75:
211
+ return ChangeLevel.MODERATE
212
+ elif similarity > 0.50:
213
+ return ChangeLevel.MAJOR
214
+ else:
215
+ return ChangeLevel.CRITICAL
216
+
217
+ def _compare_hashes(self, h1: str, h2: str) -> float:
218
+ """Compare two MD5 hashes by hex char similarity."""
219
+ if not h1 or not h2 or len(h1) != len(h2):
220
+ return 0.0
221
+ matches = sum(a == b for a, b in zip(h1, h2))
222
+ return matches / len(h1)
223
+
224
+ def _score_quality(self, frame_b64: str, meta: FrameMeta) -> float:
225
+ """Score frame quality 0.0-1.0. Higher = clearer, more useful."""
226
+ score = 0.5
227
+ data_len = len(frame_b64)
228
+ if data_len < 1000:
229
+ return 0.1
230
+ if 5000 < data_len < 500000:
231
+ score += 0.15
232
+ elif data_len > 500000:
233
+ score += 0.1
234
+ if meta.width > 800 and meta.height > 600:
235
+ score += 0.15
236
+ elif meta.width > 400 and meta.height > 300:
237
+ score += 0.05
238
+ if meta.pointer_down:
239
+ score += 0.1
240
+ if meta.active_element:
241
+ score += 0.1
242
+ if meta.annotations:
243
+ score += 0.1
244
+ return min(score, 1.0)
245
+
246
+ def _prune(self):
247
+ """Remove oldest low-priority frames when at capacity."""
248
+ if len(self._frames) <= self._max_frames:
249
+ return
250
+ removable = [
251
+ f for f in self._frames
252
+ if f.change_level in (ChangeLevel.NONE, ChangeLevel.MINOR)
253
+ ]
254
+ removable.sort(key=lambda f: f.index)
255
+ to_remove = len(self._frames) - self._max_frames + 50
256
+ for f in removable[:to_remove]:
257
+ self._frames.remove(f)
258
+ if len(self._frames) > self._max_frames:
259
+ self._frames = self._frames[-self._max_frames:]