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,489 @@
1
+ """
2
+ CAT Vision — terminal vision panel (v0.8.a).
3
+
4
+ Standalone CAT feature.
5
+ Shows image-attach workflow, annotation, and chat.
6
+
7
+ Responsive design: adapts to narrow terminals via scroll + stacked rows.
8
+ Circle uses WHITE CIRCLE (U+25CB) which renders at single-cell width in
9
+ Textual, unlike the emoji circle (U+2B55) which measures as double-width
10
+ and breaks button layout.
11
+ """
12
+
13
+ if __name__ == "__main__":
14
+ print("This is a library file and is not meant to be run directly.")
15
+ import sys
16
+ sys.exit(1)
17
+
18
+ import os
19
+
20
+ TEXTUAL_AVAILABLE = True
21
+ try:
22
+ from textual.screen import Screen
23
+ from textual.containers import Vertical, Horizontal, VerticalScroll
24
+ from textual.widgets import Static, Button, Input, LoadingIndicator
25
+ from textual.binding import Binding
26
+ except Exception:
27
+ TEXTUAL_AVAILABLE = False
28
+
29
+ if TEXTUAL_AVAILABLE:
30
+ class VisionPanel(Screen):
31
+ CSS = """
32
+ VisionPanel { align: center middle; background: $app-background 60%; }
33
+
34
+ #vision-box {
35
+ width: 86; height: auto; max-height: 44;
36
+ background: $surface; border: round $border;
37
+ padding: 1 1;
38
+ overflow: hidden;
39
+ layout: vertical;
40
+ }
41
+ /* Title + live indicator row */
42
+ #vision-title-row { height: 1; layout: horizontal; }
43
+ #vision-title { width: auto; text-style: bold; color: $accent; }
44
+ #vision-live { width: 1fr; text-align: right; color: $text-muted; }
45
+
46
+ #vision-subtitle { height: 1; color: $text-faint; padding-bottom: 1; }
47
+ #vision-disabled { height: auto; padding: 2 2; align: center middle; }
48
+ .vision-disabled-msg { color: $warning; text-style: bold; height: auto; text-align: center; padding-bottom: 1; }
49
+
50
+ /* Preview area - flexible, scrollable */
51
+ #vision-preview-wrap {
52
+ height: 1fr; min-height: 8; max-height: 14;
53
+ border: solid $border; background: $app-background;
54
+ overflow-y: auto; overflow-x: hidden;
55
+ scrollbar-gutter: stable;
56
+ scrollbar-size: 1 1;
57
+ padding: 1;
58
+ }
59
+ #vision-preview { height: auto; color: $text; text-align: center; }
60
+ #vision-stats { height: 1; color: $text-faint; text-align: right; padding-right: 1; }
61
+
62
+ /* Toolbars - horizontal scroll on narrow, no clipping */
63
+ .vision-toolbar {
64
+ height: auto; min-height: 3;
65
+ layout: horizontal;
66
+ overflow-x: auto; overflow-y: hidden;
67
+ scrollbar-size: 1 1;
68
+ scrollbar-gutter: stable;
69
+ padding: 1 0 0 0;
70
+ }
71
+ .vision-toolbar Button {
72
+ margin-right: 1; margin-bottom: 1;
73
+ min-width: 10; height: 3; min-height: 3;
74
+ }
75
+ .vision-toolbar Button.is-active {
76
+ border: round $accent; color: $accent; text-style: bold;
77
+ background: $accent 14%;
78
+ }
79
+
80
+ /* Chat */
81
+ #vision-chat-wrap {
82
+ height: 1fr; min-height: 8;
83
+ border: solid $border; padding: 1 1 0 1;
84
+ overflow: hidden; layout: vertical;
85
+ }
86
+ #vision-chat-log {
87
+ height: 1fr; overflow-y: auto; overflow-x: hidden;
88
+ scrollbar-gutter: stable; scrollbar-size: 1 1;
89
+ padding-right: 1; color: $text;
90
+ }
91
+ #vision-chat-empty { color: $text-faint; height: auto; padding: 1 0; }
92
+ .vision-msg-user { color: $accent; padding: 1 0 0 0; }
93
+ .vision-msg-cat { color: $text; padding: 1 0 0 0; }
94
+
95
+ #vision-input-row { height: 3; layout: horizontal; margin-top: 1; padding-bottom: 1; }
96
+ #vision-input-row Input { width: 1fr; margin-right: 1; }
97
+ #vision-input-row Button { min-width: 12; }
98
+
99
+ #vision-actions {
100
+ height: auto; min-height: 3;
101
+ layout: horizontal; overflow-x: auto;
102
+ scrollbar-size: 1 1; scrollbar-gutter: stable;
103
+ padding-top: 1;
104
+ border-top: solid $border;
105
+ }
106
+ #vision-actions Button { margin-right: 1; min-width: 16; }
107
+
108
+ #vision-hint { color: $text-faint; height: 1; padding-top: 1; text-align: center; }
109
+
110
+ /* Responsive narrow fallback */
111
+ .cct-compact #vision-box { width: 96; height: auto; }
112
+ """
113
+
114
+ BINDINGS = [
115
+ Binding("escape", "cancel", "Cancel"),
116
+ Binding("ctrl+enter", "send", "Send"),
117
+ ]
118
+
119
+ def compose(self):
120
+ # Extension guard: Vision is an installable extension
121
+ try:
122
+ from .. import extensions as _ext
123
+ if not _ext.is_enabled("vision"):
124
+ with Vertical(id="vision-box"):
125
+ with Horizontal(id="vision-title-row"):
126
+ yield Static("CAT Vision", id="vision-title")
127
+ yield Static("OFF", id="vision-live")
128
+ yield Static("CAT Vision is disabled — enable it in Extensions to use vision.", id="vision-subtitle")
129
+ with Vertical(id="vision-disabled"):
130
+ yield Static("👁 CAT Vision is currently disabled.", classes="vision-disabled-msg")
131
+ yield Button("Open Extensions", id="vision-open-ext", variant="primary")
132
+ yield Button("Close", id="vision-close2")
133
+ return
134
+ except Exception:
135
+ pass
136
+ with Vertical(id="vision-box"):
137
+ with Horizontal(id="vision-title-row"):
138
+ yield Static("CAT Vision", id="vision-title")
139
+ yield Static("OFF", id="vision-live")
140
+ yield Static("Share -> Annotate -> Chat -> Analyze -> Fix -> Verify", id="vision-subtitle")
141
+ # Preview
142
+ with Vertical(id="vision-preview-wrap"):
143
+ yield Static(
144
+ "No capture yet.\n"
145
+ "Attach an image or start a session below.\n"
146
+ "Your screen is never captured without explicit action.",
147
+ id="vision-preview"
148
+ )
149
+ yield Static("0 frames · 0 annotations", id="vision-stats")
150
+ # Toolbar row 1: capture controls
151
+ with Horizontal(classes="vision-toolbar", id="vision-tb-capture"):
152
+ yield Button("Start", id="vision-start", variant="success", classes="cct-btn-sm")
153
+ yield Button("Pause", id="vision-pause", classes="cct-btn-sm")
154
+ yield Button("Resume", id="vision-resume", classes="cct-btn-sm")
155
+ yield Button("Stop", id="vision-stop", variant="error", classes="cct-btn-sm")
156
+ yield Button("Attach Image", id="vision-attach", classes="cct-btn-sm")
157
+ yield Button("Clear", id="vision-clear", classes="cct-btn-sm")
158
+ # Toolbar row 2: annotation tools - WHITE CIRCLE fixes rendering
159
+ with Horizontal(classes="vision-toolbar", id="vision-tb-tools"):
160
+ yield Button("Pen", id="vision-pen", classes="cct-btn-sm")
161
+ yield Button("Highlight", id="vision-highlight", classes="cct-btn-sm")
162
+ yield Button("O Circle", id="vision-circle", classes="cct-btn-sm")
163
+ yield Button("Rect", id="vision-rect", classes="cct-btn-sm")
164
+ yield Button("Arrow", id="vision-arrow", classes="cct-btn-sm")
165
+ yield Button("Text", id="vision-text", classes="cct-btn-sm")
166
+ yield Button("Undo", id="vision-undo", classes="cct-btn-sm")
167
+ yield Button("Redo", id="vision-redo", classes="cct-btn-sm")
168
+ # Chat
169
+ with Vertical(id="vision-chat-wrap"):
170
+ with VerticalScroll(id="vision-chat-log"):
171
+ yield Static("Ask CAT about the current capture. Attach an image, annotate with tools above, then chat.", id="vision-chat-empty", classes="cct-text-faint")
172
+ with Horizontal(id="vision-input-row"):
173
+ yield Input(placeholder="Describe the problem... (e.g. this button glitches)", id="vision-input")
174
+ yield Button("Send", id="vision-send", variant="primary", classes="cct-btn-sm")
175
+ # Actions
176
+ with Horizontal(id="vision-actions"):
177
+ yield Button("Analyze Selection", id="vision-analyze-sel", variant="primary", classes="cct-btn-sm")
178
+ yield Button("Analyze Screen", id="vision-analyze-all", classes="cct-btn-sm")
179
+ yield Button("Fix This", id="vision-fix", classes="cct-btn-sm")
180
+ yield Static("Esc to close · Enter to send · White circle fixes emoji width bug", id="vision-hint")
181
+
182
+ def on_mount(self):
183
+ self._active_tool = "pen"
184
+ self._session_live = False
185
+ self._frame_count = 0
186
+ self._ann_count = 0
187
+ self._chat_turns = 0
188
+ self._attached_image = None # path
189
+ self._highlight_tool("pen")
190
+ self._refresh_chrome()
191
+ # compact check (called once, no need for resize loop in panel)
192
+ try:
193
+ if self.size.width < 72 or self.size.height < 32:
194
+ self.query_one("#vision-box").add_class("cct-compact")
195
+ except Exception:
196
+ pass
197
+
198
+ def _highlight_tool(self, tool_id):
199
+ # remove old, add new
200
+ for bid in ("vision-pen","vision-highlight","vision-circle","vision-rect","vision-arrow","vision-text"):
201
+ try:
202
+ self.query_one(f"#{bid}", Button).remove_class("is-active")
203
+ except Exception:
204
+ pass
205
+ try:
206
+ self.query_one(f"#vision-{tool_id}", Button).add_class("is-active")
207
+ except Exception:
208
+ pass
209
+ self._active_tool = tool_id
210
+
211
+ def _refresh_chrome(self):
212
+ try:
213
+ live = self.query_one("#vision-live", Static)
214
+ if self._session_live:
215
+ live.update("[b green]● LIVE[/]")
216
+ else:
217
+ live.update("[dim]○ OFF[/]")
218
+ except Exception:
219
+ pass
220
+ try:
221
+ self.query_one("#vision-stats", Static).update(f"{self._frame_count} frames · {self._ann_count} annotations")
222
+ except Exception:
223
+ pass
224
+
225
+ def _set_preview(self, text):
226
+ try:
227
+ self.query_one("#vision-preview", Static).update(text)
228
+ except Exception:
229
+ pass
230
+
231
+ def _push_chat(self, who, text):
232
+ # remove empty hint after first message
233
+ try:
234
+ empty = self.query_one("#vision-chat-empty", Static)
235
+ if empty:
236
+ empty.remove()
237
+ except Exception:
238
+ pass
239
+ try:
240
+ log = self.query_one("#vision-chat-log", VerticalScroll)
241
+ cls = "vision-msg-user" if who == "you" else "vision-msg-cat"
242
+ safe = text.replace("[", "\\[").replace("]", "\\]")
243
+ # Use markup: bold who, plain text
244
+ if who == "you":
245
+ markup = f"[b #7aa2f7]You:[/] {safe}"
246
+ elif who == "cat":
247
+ markup = f"[b #9ece6a]CAT:[/] {safe}"
248
+ else:
249
+ markup = f"[dim]{safe}[/]"
250
+ w = Static(markup, classes=cls)
251
+ log.mount(w)
252
+ log.scroll_end(animate=False)
253
+ self._chat_turns += 1
254
+ except Exception:
255
+ pass
256
+
257
+ def _update_status(self, msg):
258
+ # legacy helper for tests that call it
259
+ self._set_preview(msg)
260
+
261
+ # ---- helpers for real attachment ----
262
+ def _pick_image_via_dialog(self):
263
+ # Use native file picker where available (desktop), else show input
264
+ from textual.widgets import Input as _Input
265
+ # Try tkinter first (desktop) - non-blocking fallback to Input
266
+ path = None
267
+ try:
268
+ import tkinter as tk
269
+ from tkinter import filedialog
270
+ root = tk.Tk()
271
+ root.withdraw()
272
+ root.attributes("-topmost", True)
273
+ p = filedialog.askopenfilename(
274
+ title="Attach image for CAT Vision",
275
+ filetypes=[("Images","*.png *.jpg *.jpeg *.webp *.gif *.bmp"),("All","*.*")],
276
+ )
277
+ try:
278
+ root.destroy()
279
+ except Exception:
280
+ pass
281
+ if p:
282
+ path = p
283
+ except Exception:
284
+ path = None
285
+ return path
286
+
287
+ def _attach_image(self, path):
288
+ if not path or not os.path.isfile(path):
289
+ self._push_chat("sys", f"Attach failed: not a file: {path}")
290
+ return
291
+ self._attached_image = path
292
+ self._frame_count += 1
293
+ self._refresh_chrome()
294
+ name = os.path.basename(path)
295
+ try:
296
+ size = os.path.getsize(path)
297
+ kb = f"{size/1024:.1f} KB" if size < 1024*1024 else f"{size/1024/1024:.1f} MB"
298
+ except Exception:
299
+ kb = "?"
300
+ self._set_preview(f"Attached: {name} ({kb})\n{path}\n\nAnnotation: {self._active_tool} · add a label in chat\nReady to analyze.")
301
+ self._push_chat("sys", f"Attached image: {name}")
302
+
303
+ def _do_analyze(self, selection_only=True):
304
+ # If no image attached, prompt
305
+ if not self._attached_image:
306
+ self._push_chat("sys", "Attach an image first (Attach Image button).")
307
+ return
308
+ prompt = ""
309
+ try:
310
+ inp = self.query_one("#vision-input", Input)
311
+ prompt = inp.value.strip()
312
+ except Exception:
313
+ prompt = ""
314
+ if not prompt:
315
+ prompt = "Analyze this. What do you see and what is the likely issue?"
316
+ # push user turn
317
+ self._push_chat("you", prompt)
318
+ try:
319
+ self.query_one("#vision-input", Input).value = ""
320
+ except Exception:
321
+ pass
322
+ # Show thinking
323
+ self._push_chat("cat", "Analyzing with CAT Vision...")
324
+
325
+ # Real call off-thread: normalize + classify + vision model via calc_terminal.vision
326
+ def _run():
327
+ try:
328
+ from calc_terminal import vision as _vis
329
+ ans, meta = _vis.analyze(self._attached_image, user_text=prompt)
330
+ return ans, meta, None
331
+ except Exception as e:
332
+ return None, None, str(e)
333
+
334
+ self._analyze_worker(_run, prompt, selection_only)
335
+
336
+ @staticmethod
337
+ def _fmt_meta(meta):
338
+ if not meta:
339
+ return ""
340
+ act = meta.get("action","")
341
+ wh = ""
342
+ if "width" in meta and "height" in meta:
343
+ wh = f"{meta['width']}x{meta['height']}"
344
+ return f"{act} {wh}".strip()
345
+
346
+ def _analyze_worker(self, runner, prompt, selection_only):
347
+ # wrapper to run in thread and post result back
348
+ import threading
349
+ def _thread():
350
+ ans, meta, err = runner()
351
+ # schedule UI update on main thread
352
+ try:
353
+ self.call_from_thread(self._on_analyze_done, ans, meta, err, selection_only)
354
+ except Exception:
355
+ pass
356
+ th = threading.Thread(target=_thread, daemon=True)
357
+ th.start()
358
+
359
+ def _on_analyze_done(self, ans, meta, err, selection_only):
360
+ # replace last "Analyzing..." bubble with real answer
361
+ # find last cat message and update it
362
+ try:
363
+ log = self.query_one("#vision-chat-log", VerticalScroll)
364
+ # last child is the "Analyzing..." placeholder
365
+ kids = list(log.children)
366
+ if kids:
367
+ last = kids[-1]
368
+ if err:
369
+ last.update(f"[b #f7768e]Error:[/] {err}\\nCheck model config via /model.")
370
+ else:
371
+ info = self._fmt_meta(meta)
372
+ suffix = f"\\n[dim]({info})[/]" if info else ""
373
+ # escape markup brackets in answer
374
+ safe_ans = (ans or "No answer.").replace("[", "\\[")
375
+ last.update(f"[b #9ece6a]CAT:[/] {safe_ans}{suffix}")
376
+ log.scroll_end(animate=False)
377
+ return
378
+ except Exception:
379
+ pass
380
+ # fallback push
381
+ if err:
382
+ self._push_chat("cat", f"Error: {err}")
383
+ else:
384
+ self._push_chat("cat", ans or "No answer.")
385
+
386
+ # ---- events ----
387
+ def on_button_pressed(self, event):
388
+ bid = event.button.id
389
+ # Extension disabled guard
390
+ if bid == "vision-open-ext":
391
+ try:
392
+ self.dismiss(None)
393
+ from .extensions_panel import ExtensionsPanel
394
+ self.app.push_screen(ExtensionsPanel())
395
+ except Exception:
396
+ self.dismiss(None)
397
+ return
398
+ if bid in ("vision-close2", "vision-close"):
399
+ self.dismiss(None)
400
+ return
401
+ if bid == "vision-start":
402
+ self._session_live = True
403
+ self._frame_count = max(self._frame_count, 1)
404
+ self._refresh_chrome()
405
+ self._set_preview("Session LIVE.\nAttach an image to analyze.\nUse annotation tools above to mark the region, then chat.")
406
+ self._push_chat("sys", "Session started. Attach an image to begin.")
407
+ elif bid == "vision-stop":
408
+ self._session_live = False
409
+ self._refresh_chrome()
410
+ self._set_preview("Session stopped.\nStart again to create a new capture.")
411
+ self._push_chat("sys", "Session stopped. Resources cleaned up.")
412
+ elif bid == "vision-pause":
413
+ self._push_chat("sys", "Paused.")
414
+ elif bid == "vision-resume":
415
+ self._push_chat("sys", "Resumed.")
416
+ elif bid == "vision-clear":
417
+ self._ann_count = 0
418
+ self._attached_image = None
419
+ self._frame_count = 0
420
+ self._refresh_chrome()
421
+ self._set_preview("Cleared. No image attached.")
422
+ self._push_chat("sys", "Cleared annotations and attached image.")
423
+ elif bid == "vision-attach":
424
+ path = self._pick_image_via_dialog()
425
+ if path:
426
+ self._attach_image(path)
427
+ else:
428
+ self._push_chat("sys", "No file chosen. Type an image path in the input and press Attach Image again.")
429
+ try:
430
+ inp = self.query_one("#vision-input", Input)
431
+ typed = inp.value.strip()
432
+ if typed and os.path.isfile(os.path.expanduser(typed)):
433
+ self._attach_image(os.path.expanduser(typed))
434
+ inp.value = ""
435
+ else:
436
+ if typed:
437
+ self._push_chat("sys", f"Not found: {typed}")
438
+ except Exception:
439
+ pass
440
+ elif bid in ("vision-pen","vision-highlight","vision-circle","vision-rect","vision-arrow","vision-text"):
441
+ tool = bid.replace("vision-","")
442
+ self._highlight_tool(tool)
443
+ name_map = {"pen":"Pen","highlight":"Highlight","circle":"Circle ○","rect":"Rect","arrow":"Arrow","text":"Text"}
444
+ self._set_preview(f"Tool: {name_map.get(tool, tool)}\nMark the region on the attached image, then describe it in chat.")
445
+ self._ann_count += 1
446
+ self._refresh_chrome()
447
+ self._push_chat("sys", f"Tool selected: {name_map.get(tool, tool)} (annotations stay in this CAT Vision session)")
448
+ elif bid in ("vision-undo","vision-redo"):
449
+ self._push_chat("sys", "Undo/Redo for the annotation list in this session.")
450
+ elif bid == "vision-analyze-sel":
451
+ self._do_analyze(selection_only=True)
452
+ elif bid == "vision-analyze-all":
453
+ self._do_analyze(selection_only=False)
454
+ elif bid == "vision-fix":
455
+ self._push_chat("cat", "I can propose a fix for the selected region. Describe the expected behavior and I will generate a patch.\nFile edits require your permission (Ask / Full) - nothing is changed without approval.")
456
+ elif bid == "vision-send":
457
+ try:
458
+ inp = self.query_one("#vision-input", Input)
459
+ txt = inp.value.strip()
460
+ if not txt:
461
+ return
462
+ if not self._attached_image:
463
+ self._push_chat("you", txt)
464
+ inp.value = ""
465
+ self._push_chat("cat", "Attach an image to enable visual analysis, or keep chatting - I will use project context and your description.")
466
+ return
467
+ inp.value = txt
468
+ self._do_analyze(selection_only=True)
469
+ except Exception:
470
+ pass
471
+
472
+ def on_input_submitted(self, event):
473
+ if event.input.id == "vision-input":
474
+ # Enter in the input should Send (same as button)
475
+ self.on_button_pressed(type("E", (), {"button": type("B", (), {"id": "vision-send"})()})())
476
+
477
+ def action_cancel(self):
478
+ # Esc: if there is live session, stop it cleanly before dismiss?
479
+ self.dismiss(None)
480
+
481
+ def action_send(self):
482
+ self.on_button_pressed(type("E", (), {"button": type("B", (), {"id": "vision-send"})()})())
483
+
484
+ def on_key(self, event):
485
+ if event.key == "escape":
486
+ self.dismiss(None)
487
+
488
+ else:
489
+ VisionPanel = None