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,221 @@
1
+ """
2
+ CCT UI — the "/" command palette. Filters calc_terminal/commands_data.py's
3
+ COMMANDS (the SAME list the fallback CLI's /help prints — one registry,
4
+ not a second hand-maintained copy), renders the dropdown, and posts
5
+ CommandExecuted when a command is chosen. Deciding what a command does
6
+ is calc_terminal/app.py's job, not this module's.
7
+ """
8
+
9
+ if __name__ == "__main__":
10
+ print("This is a library file and is not meant to be run directly.")
11
+ import sys
12
+ sys.exit(1)
13
+
14
+ from ..commands_data import COMMANDS
15
+
16
+ TEXTUAL_AVAILABLE = True
17
+ try:
18
+ from textual.containers import Vertical
19
+ from textual.widgets import Static
20
+ except Exception:
21
+ TEXTUAL_AVAILABLE = False
22
+
23
+ PROMPT_SUGGESTIONS = [
24
+ # Kinetics & Numerical Chemistry
25
+ ("what is a first order reaction", "Quick conceptual answer · Kinetics"),
26
+ ("half life of a first order reaction", "Numerical calculation · Kinetics"),
27
+ ("explain the arrhenius equation simply", "Conceptual explanation · Physical Chem"),
28
+ ("what's the difference between molarity and molality", "Concentration comparison · Solutions"),
29
+ ("quick mole concept refresher", "Core principles & formulas · Class 11"),
30
+ ("how to balance redox reactions", "Ion-electron oxidation-reduction method"),
31
+ ("calculate molar mass of glucose", "Step-by-step formula C6H12O6 mass"),
32
+ ("activation energy numerical", "Arrhenius rate constant equation"),
33
+ ("zero order reaction half life", "Zero order rate formula & graph"),
34
+ ("second order kinetics rate constant", "Integrated rate equation derivation"),
35
+ ("calculate gibbs free energy delta G", "Thermodynamics spontaneity calculation"),
36
+ ("calculate ph of weak acid solution", "Equilibrium pH & dissociation constant"),
37
+ # Physics, Solves, Derivations & Simulations
38
+ ("solve ideal gas law with P=1, n=2, T=300", "Symbolic solve + full notebook"),
39
+ ("derive the first order rate law step by step", "Full derivation, notebook format"),
40
+ ("plot the arrhenius graph and export it", "Interactive plot + PNG export"),
41
+ ("plot y = sin(x)*exp(-x/5) from 0 to 20", "Damped wave function plot"),
42
+ ("simulate an iron atom in 3d", "Live 3D Bohr electron simulation"),
43
+ ("show me the 2p orbital cloud", "Quantum orbital 3D probability density"),
44
+ ("explain redox titration in detail", "Full reaction mechanism & calculations"),
45
+ ("integrate x*exp(-x^2) dx from 0 to infinity", "Calculus definite integration"),
46
+ ]
47
+
48
+
49
+ class SuggestionEngine:
50
+ """Pure filtering logic — no widgets, so it's independently testable
51
+ and reusable anywhere a command or prompt list needs fuzzy filtering."""
52
+
53
+ @staticmethod
54
+ def fuzzy_score(query, candidate):
55
+ """Every character of `query` (lowercased) must appear in
56
+ `candidate` in order. Returns a score (lower is better) or None
57
+ if it doesn't match — a subsequence match is good enough for a
58
+ few dozen commands without pulling in a dependency."""
59
+ query = query.lower()
60
+ candidate = candidate.lower()
61
+ if not query:
62
+ return 0
63
+ if query in candidate:
64
+ return candidate.index(query)
65
+ pos = 0
66
+ gaps = 0
67
+ for ch in query:
68
+ found = candidate.find(ch, pos)
69
+ if found == -1:
70
+ return None
71
+ gaps += found - pos
72
+ pos = found + 1
73
+ return 100 + gaps
74
+
75
+ _HIDDEN_IN_CHAT = {"/notebook", "/workspace", "/kinetics", "/orbitals"}
76
+
77
+ @classmethod
78
+ def match(cls, query, limit=30):
79
+ query = (query or "").strip()
80
+ scored = []
81
+ is_cmd_query = query.startswith("/")
82
+ search_term = query[1:].strip() if is_cmd_query else query
83
+
84
+ # 1. Match slash commands
85
+ for cmd, desc in COMMANDS:
86
+ if cmd in cls._HIDDEN_IN_CHAT:
87
+ continue
88
+ if is_cmd_query:
89
+ # Slash query: match against command name
90
+ score = cls.fuzzy_score(search_term, cmd[1:])
91
+ elif search_term:
92
+ # Plain text: match against command name as fallback
93
+ score = cls.fuzzy_score(search_term, cmd[1:])
94
+ if score is not None:
95
+ score += 25
96
+ else:
97
+ score = None
98
+
99
+ if score is not None:
100
+ scored.append((score, cmd, desc))
101
+
102
+ # 2. Match prompt suggestions
103
+ for prompt_text, desc in PROMPT_SUGGESTIONS:
104
+ if not search_term:
105
+ # Top prompt suggestions after commands
106
+ score = 35 if is_cmd_query else 10
107
+ else:
108
+ score = cls.fuzzy_score(search_term, prompt_text)
109
+ if score is not None and is_cmd_query:
110
+ score += 20 # Prefer slash commands when user explicitly typed '/'
111
+ if score is None:
112
+ words = search_term.lower().split()
113
+ if words and all(w in prompt_text.lower() or w in desc.lower() for w in words):
114
+ score = 50 if is_cmd_query else 40
115
+ if score is not None:
116
+ scored.append((score, prompt_text, desc))
117
+
118
+ scored.sort(key=lambda t: t[0])
119
+ return [(item, desc) for _, item, desc in scored[:limit]]
120
+
121
+
122
+ if TEXTUAL_AVAILABLE:
123
+ from textual.containers import VerticalScroll
124
+
125
+ PAGE_SIZE = 6
126
+
127
+ class CommandPalette(VerticalScroll):
128
+ """The command and prompt suggestions palette — mounted inline inside
129
+ the composer card. ComposerInput drives it: Up/Down/PageUp/PageDown
130
+ move selection, Tab/Enter accept it, Esc closes it, and clicking
131
+ any item selects it."""
132
+
133
+ def __init__(self, id="cct-cmdpalette"):
134
+ super().__init__(id=id)
135
+ self._matches = []
136
+ self._selected = 0
137
+
138
+ def compose(self):
139
+ yield from ()
140
+
141
+ def is_open(self):
142
+ return self.has_class("open")
143
+
144
+ def open_for_query(self, query):
145
+ self._matches = SuggestionEngine.match(query)
146
+ if not self._matches:
147
+ self.close()
148
+ return
149
+ self._selected = min(self._selected, len(self._matches) - 1)
150
+ self._redraw()
151
+ self.add_class("open")
152
+ # Dynamically size between 3 and 7 rows
153
+ self.styles.height = min(7, max(3, len(self._matches)))
154
+ self.scroll_home(animate=False)
155
+ if self.parent and hasattr(self.parent, "_on_palette_state_changed"):
156
+ self.parent._on_palette_state_changed(True)
157
+
158
+ def close(self):
159
+ was_open = self.is_open()
160
+ self._matches = []
161
+ self._selected = 0
162
+ self.remove_class("open")
163
+ for child in list(self.children):
164
+ child.remove()
165
+ if was_open and self.parent and hasattr(self.parent, "_on_palette_state_changed"):
166
+ self.parent._on_palette_state_changed(False)
167
+
168
+ def on_click(self, event):
169
+ try:
170
+ for i, child in enumerate(self.children):
171
+ if child == event.widget or child in getattr(event.widget, "ancestors", []):
172
+ self._selected = i
173
+ self._redraw()
174
+ if self.parent and hasattr(self.parent, "on_palette_selected"):
175
+ self.parent.on_palette_selected(self.selected_command())
176
+ break
177
+ except Exception:
178
+ pass
179
+
180
+ def move(self, delta):
181
+ if not self._matches:
182
+ return
183
+ self._selected = (self._selected + delta) % len(self._matches)
184
+ self._redraw()
185
+ self._scroll_selected_into_view()
186
+
187
+ def move_page(self, direction):
188
+ if not self._matches:
189
+ return
190
+ last = len(self._matches) - 1
191
+ self._selected = max(0, min(last, self._selected + direction * PAGE_SIZE))
192
+ self._redraw()
193
+ self._scroll_selected_into_view()
194
+
195
+ def selected_command(self):
196
+ if not self._matches:
197
+ return None
198
+ return self._matches[self._selected][0]
199
+
200
+ def _scroll_selected_into_view(self):
201
+ try:
202
+ rows = list(self.query(".cct-cmd-row"))
203
+ if 0 <= self._selected < len(rows):
204
+ self.scroll_to_widget(rows[self._selected], animate=True)
205
+ except Exception:
206
+ pass
207
+
208
+ def _redraw(self):
209
+ from . import theme_css
210
+ accent = theme_css.current_hex("accent")
211
+ for child in list(self.children):
212
+ child.remove()
213
+ for i, (item, desc) in enumerate(self._matches):
214
+ classes = "cct-cmd-row" + (" cct-cmd-row-selected" if i == self._selected else "")
215
+ if item.startswith("/"):
216
+ self.mount(Static(f"[{accent} b]{item}[/] [dim]{desc}[/dim]", classes=classes))
217
+ else:
218
+ self.mount(Static(f"[{accent}]\u2726[/] {item} [dim]{desc}[/dim]", classes=classes))
219
+
220
+ else:
221
+ CommandPalette = None
@@ -0,0 +1,269 @@
1
+ """
2
+ CCT UI — the single permission renderer. Two widgets, both reading and
3
+ writing through calc_terminal/permissions.py's shared PermissionManager
4
+ (the one source of truth for permission state — this module never
5
+ tracks its own copy):
6
+
7
+ * PermissionCard — an inline "AI wants permission" request, mounted
8
+ as a ConversationItem in the conversation stream (per the redesign
9
+ brief: "Permission requests become part of conversation history",
10
+ never a modal/popup).
11
+ * PermissionsSettingsPanel — the full on/off checklist, opened from
12
+ the footer's Permissions button.
13
+
14
+ Both post events (PermissionGranted/PermissionDenied/PermissionCancelled)
15
+ rather than deciding what a decision means — calc_terminal/permissions.py
16
+ records it, calc_terminal/ui/app.py resolves whatever backend call was
17
+ waiting.
18
+ """
19
+
20
+ if __name__ == "__main__":
21
+ print("This is a library file and is not meant to be run directly.")
22
+ import sys
23
+ sys.exit(1)
24
+
25
+ from .. import permissions as perm
26
+ from .events import PermissionGranted, PermissionDenied, PermissionCancelled
27
+
28
+ TEXTUAL_AVAILABLE = True
29
+ try:
30
+ from textual.containers import Horizontal, Vertical
31
+ from textual.widgets import Static, Button, Switch, Label
32
+ except Exception:
33
+ TEXTUAL_AVAILABLE = False
34
+
35
+
36
+ if TEXTUAL_AVAILABLE:
37
+
38
+ class PermissionCard(Vertical):
39
+ """Inline 'AI wants permission' request — mounted into the
40
+ conversation log as its own item, never a popup."""
41
+
42
+ def __init__(self, request_id, key, action_text, reason_text, id=None, path=None):
43
+ super().__init__(id=id, classes="cct-permcard")
44
+ self.request_id = request_id
45
+ self.key = key
46
+ self._action_text = action_text
47
+ self._reason_text = reason_text
48
+ self._path = path
49
+ self._resolved = False
50
+
51
+ def compose(self):
52
+ # Restricted mode (spec Mode 2) reviews this as a suggested
53
+ # patch — Accept/Reject — instead of Ask mode's (spec Mode 1)
54
+ # Allow-Once/Always-Allow/Deny. Same request, same event
55
+ # contract either way; only the labels/verb change.
56
+ self._restricted = perm.manager.restricted_review(self.key)
57
+ title = "Suggested action \u2014 review required" if self._restricted else "Permission required"
58
+ yield Static(title, classes="cct-permcard-title")
59
+ yield Static(f"The AI wants to [b]{self._action_text}[/b]")
60
+ if self._path:
61
+ yield Static(f"Path: {self._path}", classes="cct-permcard-path")
62
+ yield Static(f"Reason: {self._reason_text}", classes="cct-permcard-reason")
63
+ with Horizontal(classes="cct-permcard-buttons"):
64
+ if self._restricted:
65
+ yield Button("Accept", id="allow-once", classes="cct-ctrl")
66
+ yield Button("Reject", id="deny", classes="cct-ctrl")
67
+ else:
68
+ # v0.7.7 spec section 3: the dialog offers five
69
+ # choices — Allow Once / Always Allow / Deny /
70
+ # Always Deny / Cancel (wrapped on a second row so
71
+ # the card stays compact).
72
+ yield Button("Allow Once", id="allow-once", classes="cct-ctrl")
73
+ yield Button("Always Allow", id="always-allow", classes="cct-ctrl")
74
+ yield Button("Deny", id="deny", classes="cct-ctrl")
75
+ if not self._restricted:
76
+ with Horizontal(classes="cct-permcard-buttons"):
77
+ yield Button("Always Deny", id="always-deny", classes="cct-ctrl")
78
+ yield Button("Cancel", id="cancel", classes="cct-ctrl")
79
+
80
+ def on_button_pressed(self, event: Button.Pressed):
81
+ if self._resolved:
82
+ return
83
+ self._resolved = True
84
+ bid = event.button.id
85
+ for b in self.query(Button):
86
+ b.disabled = True
87
+ from . import theme_css
88
+ if bid == "always-allow":
89
+ self.post_message(PermissionGranted(self.request_id, self.key, remember=True))
90
+ tone = "success"
91
+ verb = "always allow"
92
+ elif bid == "allow-once":
93
+ self.post_message(PermissionGranted(self.request_id, self.key, remember=False))
94
+ tone = "success"
95
+ verb = "accept" if getattr(self, "_restricted", False) else "allow once"
96
+ elif bid == "always-deny":
97
+ # v0.7.7: permanent (session) refusal — recorded in the
98
+ # shared PermissionManager so future requests for this
99
+ # key are refused without prompting at all.
100
+ self.post_message(PermissionDenied(self.request_id, self.key, remember=True))
101
+ tone = "error"
102
+ verb = "always deny"
103
+ elif bid == "cancel":
104
+ self.post_message(PermissionCancelled(self.request_id, self.key))
105
+ tone = "warn"
106
+ verb = "cancel"
107
+ else:
108
+ self.post_message(PermissionDenied(self.request_id, self.key, remember=False))
109
+ tone = "error"
110
+ verb = "reject" if getattr(self, "_restricted", False) else "deny"
111
+ color = theme_css.current_hex(tone)
112
+ self.mount(Static(f"[{color}]\u2192 {verb}[/]", classes="cct-permcard-verdict"))
113
+
114
+ class Toggle3D(Button):
115
+ """Skeuomorphic 3D On/Off tactile slider switch with physical rail,
116
+ depression animation, and glowing state indicator."""
117
+
118
+ def __init__(self, key, is_on=False, on_toggle=None):
119
+ self._key = key
120
+ self.is_on = is_on
121
+ self._on_toggle = on_toggle
122
+ super().__init__(
123
+ self._format_label(is_on),
124
+ id=f"perm-toggle-{key}",
125
+ classes="cct-toggle-3d " + ("-on" if is_on else "-off"),
126
+ )
127
+ self.can_focus = True
128
+
129
+ @staticmethod
130
+ def _format_label(is_on: bool) -> str:
131
+ # Tactile physical slider switch with 3D track and knob
132
+ return "[──● ON ]" if is_on else "[○── OFF]"
133
+
134
+ @property
135
+ def is_enabled(self) -> bool:
136
+ return self.is_on
137
+
138
+ def on_button_pressed(self, event: Button.Pressed):
139
+ event.stop()
140
+ self.is_on = not self.is_on
141
+ self.set_class(self.is_on, "-on")
142
+ self.set_class(not self.is_on, "-off")
143
+ self.label = self._format_label(self.is_on)
144
+ perm.manager.set(self._key, self.is_on)
145
+ if self._on_toggle:
146
+ self._on_toggle(self._key, self.is_on)
147
+
148
+ class PermissionRow(Horizontal):
149
+ """One checklist line inside PermissionsSettingsPanel: a label
150
+ and a 3D tactile On/Off switch."""
151
+
152
+ def __init__(self, key, label, is_on):
153
+ super().__init__(classes="cct-perm-row")
154
+ self._key = key
155
+ self._label_text = label
156
+ self._is_on = is_on
157
+
158
+ def compose(self):
159
+ from . import theme_css
160
+ color = theme_css.current_hex("success" if self._is_on else "text-muted")
161
+ mark = "●" if self._is_on else "○"
162
+ yield Static(f"[{color}]{mark}[/] {self._label_text}", classes="cct-perm-label")
163
+ yield Toggle3D(self._key, self._is_on, on_toggle=self._on_toggle_changed)
164
+
165
+ def _on_toggle_changed(self, key, value):
166
+ from . import theme_css
167
+ self._is_on = value
168
+ color = theme_css.current_hex("success" if value else "text-muted")
169
+ mark = "●" if value else "○"
170
+ try:
171
+ self.query_one(".cct-perm-label", Static).update(f"[{color}]{mark}[/] {self._label_text}")
172
+ except Exception:
173
+ pass
174
+
175
+ class PermissionsSettingsPanel(Vertical):
176
+ """Redesigned 3D Skeuomorphic Permissions & Settings Dashboard with
177
+ header bar, security mode cycler, close button, and 2-column organized grid."""
178
+
179
+ def __init__(self, id="cct-permission-panel"):
180
+ super().__init__(id=id)
181
+
182
+ @property
183
+ def is_open(self) -> bool:
184
+ return self.has_class("open")
185
+
186
+ def _mode_badge_text(self):
187
+ mode = getattr(perm.manager, "mode", "ask")
188
+ return perm.MODE_LABELS.get(mode, "🔒 Ask Every Time")
189
+
190
+ def compose(self):
191
+ # 1. Header Bar: Icon, Title, Mode Badge Cycler, and Close Button
192
+ with Horizontal(id="cct-perm-header"):
193
+ with Horizontal(id="cct-perm-title-box"):
194
+ yield Static("🛡", id="cct-perm-icon")
195
+ yield Static("Permissions & Security Controls", id="cct-perm-title")
196
+ yield Button(self._mode_badge_text(), id="cct-perm-mode-btn", tooltip="Click to cycle Security Mode (Ask / Restricted / Full)")
197
+ yield Static("", id="cct-perm-spacer")
198
+ yield Button("✕", id="cct-perm-close", classes="cct-icon-btn", tooltip="Close permissions panel (Esc)")
199
+
200
+ # 2. Body: Two organized 3D columns
201
+ exec_keys = {"read_files", "write_files", "execute_python", "shell_commands", "install_packages"}
202
+ tools_keys = {"device_control", "internet", "formula_library", "notebook", "calculator"}
203
+
204
+ with Horizontal(id="cct-perm-grid"):
205
+ with Vertical(classes="cct-perm-col"):
206
+ yield Static("⚡ Files & Execution", classes="cct-perm-col-title")
207
+ for key, label, is_on in perm.manager.snapshot():
208
+ if key in exec_keys:
209
+ yield PermissionRow(key, label, is_on)
210
+
211
+ with Vertical(classes="cct-perm-col"):
212
+ yield Static("🌐 Network & Tools", classes="cct-perm-col-title")
213
+ for key, label, is_on in perm.manager.snapshot():
214
+ if key in tools_keys:
215
+ yield PermissionRow(key, label, is_on)
216
+
217
+ def on_button_pressed(self, event: Button.Pressed):
218
+ bid = event.button.id
219
+ if bid == "cct-perm-close":
220
+ event.stop()
221
+ self.close_panel()
222
+ elif bid == "cct-perm-mode-btn":
223
+ event.stop()
224
+ modes = list(perm.MODES)
225
+ curr = getattr(perm.manager, "mode", "ask")
226
+ next_mode = modes[(modes.index(curr) + 1) % len(modes)]
227
+ perm.manager.set_mode(next_mode)
228
+ event.button.label = self._mode_badge_text()
229
+
230
+ def on_key(self, event) -> None:
231
+ if event.key == "escape":
232
+ event.stop()
233
+ try:
234
+ event.prevent_default()
235
+ except Exception:
236
+ pass
237
+ self.close_panel()
238
+
239
+ def close_panel(self):
240
+ self.remove_class("open")
241
+ if self.parent and hasattr(self.parent, "_on_permission_panel_toggled"):
242
+ self.parent._on_permission_panel_toggled(False)
243
+
244
+ def refresh_rows(self):
245
+ from . import theme_css
246
+ try:
247
+ mode_btn = self.query_one("#cct-perm-mode-btn", Button)
248
+ mode_btn.label = self._mode_badge_text()
249
+ except Exception:
250
+ pass
251
+ for key, label, is_on in perm.manager.snapshot():
252
+ try:
253
+ toggle = self.query_one(f"#perm-toggle-{key}", Toggle3D)
254
+ toggle.is_on = is_on
255
+ toggle.set_class(is_on, "-on")
256
+ toggle.set_class(not is_on, "-off")
257
+ toggle.label = Toggle3D._format_label(is_on)
258
+ row = toggle.parent
259
+ if row:
260
+ color = theme_css.current_hex("success" if is_on else "text-muted")
261
+ mark = "●" if is_on else "○"
262
+ row.query_one(".cct-perm-label", Static).update(f"[{color}]{mark}[/] {label}")
263
+ except Exception:
264
+ pass
265
+
266
+ else:
267
+ PermissionCard = None
268
+ PermissionRow = None
269
+ PermissionsSettingsPanel = None