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,815 @@
1
+ """
2
+ CCT UI — ExtensionsPanel (v0.7.9.11 VS Code-style).
3
+
4
+ Full VS Code-inspired marketplace/manager for Gestures / CAT Vision /
5
+ Personalize and future third-party extensions.
6
+
7
+ Features per spec:
8
+ - Search (name, creator, category, keywords)
9
+ - Cards: icon, name, description, creator, verified, size, version,
10
+ category, trusted, stars, Install/Enable/Disable/Uninstall
11
+ - Details page on click
12
+ - States: NOT_INSTALLED, INSTALLING, INSTALLED, ENABLED, DISABLED,
13
+ UNINSTALLING, Error, Update Available
14
+ - Size from filesystem (extensions.get_size)
15
+ - Creator Kazi Zillani ✓ Verified, trusted, future company support
16
+ - Main menu integration via extensions.should_show_in_menu
17
+ - Persistence via extensions registry, error isolation
18
+
19
+ Design: CAT's own design system, not copied Microsoft assets.
20
+ """
21
+
22
+ if __name__ == "__main__":
23
+ print("This is a library file and is not meant to be run directly.")
24
+ import sys
25
+ sys.exit(1)
26
+
27
+ import time
28
+
29
+ TEXTUAL_AVAILABLE = True
30
+ try:
31
+ from textual.screen import Screen
32
+ from textual.containers import Vertical, Horizontal, VerticalScroll
33
+ from textual.widgets import Static, Button, Input, LoadingIndicator
34
+ from textual.binding import Binding
35
+ except Exception:
36
+ TEXTUAL_AVAILABLE = False
37
+ LoadingIndicator = None
38
+
39
+ if TEXTUAL_AVAILABLE:
40
+ try:
41
+ from .. import extensions as ext_mgr
42
+ except Exception:
43
+ ext_mgr = None
44
+ try:
45
+ from . import theme_css
46
+ except Exception:
47
+ theme_css = None
48
+
49
+ def _hex(role):
50
+ try:
51
+ return theme_css.current_hex(role) if theme_css else "#888"
52
+ except Exception:
53
+ return "#888"
54
+
55
+ class _ExtCard(Horizontal):
56
+ """VS Code-style card: icon | info | actions"""
57
+ def __init__(self, ext):
58
+ super().__init__(classes="ext-card")
59
+ self.ext = ext
60
+ self.ext_id = ext.get("id") or ext.get("key")
61
+
62
+ def compose(self):
63
+ ext = self.ext
64
+ name = ext.get("display_name") or ext.get("name") or ext.get("id")
65
+ desc = ext.get("description", "")
66
+ icon = ext.get("icon", "▣")
67
+ creator = ext.get("publisher") or ext.get("creator") or "Unknown"
68
+ verified = bool(ext.get("publisher_verified") or ext.get("verified"))
69
+ trusted = bool(ext.get("trusted"))
70
+ version = ext.get("version", "1.0.0")
71
+ category = ext.get("category", "General")
72
+ size = ext.get("size_display") or ext.get("size", "?")
73
+ state = ext.get("state") or ("INSTALLED_ENABLED" if ext.get("enabled") and ext.get("installed") else "NOT_INSTALLED" if not ext.get("installed") else "INSTALLED_DISABLED")
74
+ # fallback state
75
+ if ext.get("installed") and ext.get("enabled"):
76
+ state = "INSTALLED_ENABLED"
77
+ elif ext.get("installed") and not ext.get("enabled"):
78
+ state = "INSTALLED_DISABLED"
79
+ elif not ext.get("installed"):
80
+ state = "NOT_INSTALLED"
81
+ # check busy animation state from parent screen
82
+ busy = None
83
+ try:
84
+ busy = getattr(self.screen, "_busy", {}).get(self.ext_id) # type: ignore
85
+ except Exception:
86
+ busy = None
87
+ if busy:
88
+ # override state for animation — map to proper ING form
89
+ _busy_map = {"install": "INSTALLING", "uninstall": "UNINSTALLING", "enable": "ENABLING", "disable": "DISABLING"}
90
+ state = _busy_map.get(busy, "INSTALLING")
91
+
92
+ # left icon
93
+ with Vertical(classes="ext-card-icon"):
94
+ if state in ("INSTALLING", "UNINSTALLING"):
95
+ if LoadingIndicator:
96
+ yield LoadingIndicator(classes="ext-loading")
97
+ else:
98
+ yield Static("⟳", classes="ext-icon ext-busy")
99
+ else:
100
+ yield Static(icon, classes="ext-icon")
101
+
102
+ # center info
103
+ with Vertical(classes="ext-card-info"):
104
+ # title row: name + verified + trusted
105
+ with Horizontal(classes="ext-title-row"):
106
+ yield Static(name, classes="ext-name")
107
+ if verified:
108
+ yield Static("✓ Verified", classes="ext-verified")
109
+ if trusted:
110
+ yield Static("Trusted", classes="ext-trusted")
111
+ yield Static(desc, classes="ext-desc")
112
+ # meta row: creator, size, version, category
113
+ yield Static(f"{creator} · {size} · v{version} · {category}", classes="ext-meta")
114
+ # status row — with installing animation
115
+ status_map = {
116
+ "NOT_INSTALLED": ("○ Not Installed", "text-faint"),
117
+ "INSTALLED_ENABLED": ("● Enabled", "success"),
118
+ "INSTALLED_DISABLED": ("○ Disabled", "warning"),
119
+ "INSTALLING": ("⟳ Installing…", "warning"),
120
+ "UNINSTALLING": ("⟳ Uninstalling…", "warning"),
121
+ "ENABLING": ("⟳ Enabling…", "warning"),
122
+ "DISABLING": ("⟳ Disabling…", "warning"),
123
+ "ENABLEING": ("⟳ Enabling…", "warning"),
124
+ "DISABLEING": ("⟳ Disabling…", "warning"),
125
+ }
126
+ status_text, tone = status_map.get(state, ("Unknown", "text-faint"))
127
+ yield Static(f"[{_hex(tone)}]{status_text}[/]", classes="ext-status" + (" ext-busy-label" if busy else ""), markup=True)
128
+
129
+ # right actions — show spinner/disabled during busy
130
+ with Vertical(classes="ext-card-actions"):
131
+ if busy:
132
+ if busy == "install":
133
+ yield Button("⟳ Installing…", id=f"ext-busy-{self.ext_id}", variant="primary", classes="cct-btn-sm", disabled=True)
134
+ elif busy == "uninstall":
135
+ yield Button("⟳ Removing…", id=f"ext-busy-{self.ext_id}", variant="error", classes="cct-btn-sm", disabled=True)
136
+ elif busy == "enable":
137
+ yield Button("⟳ Enabling…", id=f"ext-busy-{self.ext_id}", variant="primary", classes="cct-btn-sm", disabled=True)
138
+ elif busy == "disable":
139
+ yield Button("⟳ Disabling…", id=f"ext-busy-{self.ext_id}", classes="cct-btn-sm", disabled=True)
140
+ else:
141
+ yield Button("⟳ Working…", id=f"ext-busy-{self.ext_id}", disabled=True, classes="cct-btn-sm")
142
+ else:
143
+ if state == "NOT_INSTALLED":
144
+ yield Button("Install", id=f"ext-install-{self.ext_id}", variant="primary", classes="cct-btn-sm")
145
+ elif state == "INSTALLED_DISABLED":
146
+ yield Button("Enable", id=f"ext-enable-{self.ext_id}", variant="primary", classes="cct-btn-sm")
147
+ yield Button("Uninstall", id=f"ext-uninstall-{self.ext_id}", variant="error", classes="cct-btn-sm")
148
+ elif state == "INSTALLED_ENABLED":
149
+ yield Button("Disable", id=f"ext-disable-{self.ext_id}", classes="cct-btn-sm")
150
+ yield Button("Uninstall", id=f"ext-uninstall-{self.ext_id}", variant="error", classes="cct-btn-sm")
151
+ else:
152
+ yield Button("Install", id=f"ext-install-{self.ext_id}", variant="primary", classes="cct-btn-sm")
153
+
154
+ def on_click(self, event):
155
+ # clicking the card itself opens details
156
+ # but buttons handle their own clicks, so only handle card background
157
+ if getattr(event, "widget", None) is self:
158
+ try:
159
+ self.screen._open_details(self.ext_id)
160
+ except Exception:
161
+ pass
162
+
163
+ class _DetailsPanel(Screen):
164
+ """Extension details page (VS Code style)."""
165
+ CSS = """
166
+ _DetailsPanel { align: center middle; background: $surface-darken-2 70%; }
167
+ #det-box { width: 72; max-width: 96%; height: auto; max-height: 46; background: $surface; border: tall $border; padding: 0; layout: vertical; overflow: hidden; }
168
+ #det-titlebar { height: 3; padding: 1 2 0 2; border-bottom: solid $border; layout: horizontal; }
169
+ #det-title { width: 1fr; text-style: bold; }
170
+ #det-body { height: 1fr; overflow-y: auto; padding: 1 2; }
171
+ .det-icon { width: 100%; height: 3; text-align: center; text-style: bold; }
172
+ .det-name { width: 100%; height: 1; text-style: bold; text-align: center; }
173
+ .det-creator { width: 100%; height: 1; color: $text-muted; text-align: center; }
174
+ .det-verified { width: 100%; height: 1; color: $success; text-align: center; }
175
+ .det-meta { width: 100%; height: auto; color: $text-faint; text-align: center; padding: 1 0; }
176
+ .det-desc { width: 100%; height: auto; color: $text; padding: 1 0; }
177
+ .det-section { width: 100%; height: 1; color: $text-faint; text-style: bold; padding-top: 1; border-top: solid $border; margin-top: 1; }
178
+ #det-actions { height: auto; padding: 1 2; border-top: solid $border; layout: horizontal; }
179
+ #det-actions Button { margin-right: 1; }
180
+ """
181
+
182
+ def __init__(self, ext_id):
183
+ super().__init__()
184
+ self.ext_id = ext_id
185
+ self.ext = ext_mgr.get_metadata(ext_id) if ext_mgr else None
186
+ self._busy = None
187
+
188
+ def compose(self):
189
+ ext = self.ext or {}
190
+ name = ext.get("display_name") or ext.get("name") or self.ext_id
191
+ icon = ext.get("icon", "▣")
192
+ desc = ext.get("description", "")
193
+ creator = ext.get("publisher") or ext.get("creator") or "Unknown"
194
+ verified = bool(ext.get("publisher_verified") or ext.get("verified"))
195
+ trusted = bool(ext.get("trusted"))
196
+ version = ext.get("version", "1.0.0")
197
+ size = ext.get("size_display") or ext.get("size", "?")
198
+ category = ext.get("category", "General")
199
+ state = ext.get("state") or "NOT_INSTALLED"
200
+ # show busy animation if pending
201
+ if self._busy:
202
+ state = {"install":"INSTALLING","uninstall":"UNINSTALLING","enable":"ENABLING","disable":"DISABLING"}.get(self._busy, state)
203
+ with Vertical(id="det-box"):
204
+ with Horizontal(id="det-titlebar"):
205
+ yield Static(f"{icon} {name}", id="det-title")
206
+ yield Button("✕", id="det-close", classes="cct-ctrl")
207
+ with VerticalScroll(id="det-body"):
208
+ if state in ("INSTALLING", "UNINSTALLING") and LoadingIndicator:
209
+ yield LoadingIndicator(classes="det-loading")
210
+ yield Static(icon, classes="det-icon")
211
+ yield Static(name, classes="det-name")
212
+ yield Static(creator, classes="det-creator")
213
+ if verified:
214
+ yield Static("✓ Verified", classes="det-verified")
215
+ else:
216
+ yield Static("Unverified", classes="det-verified")
217
+ if trusted:
218
+ yield Static("Trusted Extension", classes="det-verified")
219
+ if state == "INSTALLING":
220
+ yield Static("⟳ Installing…", classes="det-verified")
221
+ elif state == "UNINSTALLING":
222
+ yield Static("⟳ Uninstalling…", classes="det-verified")
223
+ yield Static(f"{size} · v{version} · {category}", classes="det-meta")
224
+ yield Static(desc, classes="det-desc")
225
+ yield Static("Publisher", classes="det-section")
226
+ pub_type = ext.get("publisher_type", "individual")
227
+ yield Static(f"{creator} ({pub_type}) {'✓ Verified' if verified else ''}", classes="det-desc")
228
+ yield Static("Permissions", classes="det-section")
229
+ yield Static("filesystem, workspace access", classes="det-desc")
230
+ with Horizontal(id="det-actions"):
231
+ if self._busy:
232
+ if self._busy == "install":
233
+ yield Button("⟳ Installing…", id="det-install", variant="primary", disabled=True)
234
+ elif self._busy == "uninstall":
235
+ yield Button("⟳ Removing…", id="det-uninstall", variant="error", disabled=True)
236
+ elif self._busy == "enable":
237
+ yield Button("⟳ Enabling…", id="det-enable", variant="primary", disabled=True)
238
+ elif self._busy == "disable":
239
+ yield Button("⟳ Disabling…", id="det-disable", disabled=True)
240
+ yield Button("Close", id="det-close2", classes="cct-ctrl", disabled=True)
241
+ else:
242
+ if state == "NOT_INSTALLED":
243
+ yield Button("Install", id="det-install", variant="primary")
244
+ elif state == "INSTALLED_DISABLED":
245
+ yield Button("Enable", id="det-enable", variant="primary")
246
+ yield Button("Uninstall", id="det-uninstall", variant="error")
247
+ elif state == "INSTALLED_ENABLED":
248
+ yield Button("Disable", id="det-disable")
249
+ yield Button("Uninstall", id="det-uninstall", variant="error")
250
+ yield Button("Close", id="det-close2", classes="cct-ctrl")
251
+
252
+ def _det_refresh_busy(self):
253
+ try:
254
+ # re-compose body actions to show busy
255
+ box = self.query_one("#det-box")
256
+ # simple refresh by re-mounting actions row
257
+ actions = self.query_one("#det-actions")
258
+ actions.remove_children()
259
+ if self._busy:
260
+ if self._busy == "install":
261
+ actions.mount(Button("⟳ Installing…", id="det-install", variant="primary", disabled=True))
262
+ elif self._busy == "uninstall":
263
+ actions.mount(Button("⟳ Removing…", id="det-uninstall", variant="error", disabled=True))
264
+ elif self._busy == "enable":
265
+ actions.mount(Button("⟳ Enabling…", id="det-enable", variant="primary", disabled=True))
266
+ elif self._busy == "disable":
267
+ actions.mount(Button("⟳ Disabling…", id="det-disable", disabled=True))
268
+ actions.mount(Button("Close", id="det-close2", classes="cct-ctrl", disabled=True))
269
+ else:
270
+ ext = ext_mgr.get_metadata(self.ext_id) if ext_mgr else {}
271
+ state = (ext or {}).get("state") or "NOT_INSTALLED"
272
+ if state == "NOT_INSTALLED":
273
+ actions.mount(Button("Install", id="det-install", variant="primary"))
274
+ elif state == "INSTALLED_DISABLED":
275
+ actions.mount(Button("Enable", id="det-enable", variant="primary"))
276
+ actions.mount(Button("Uninstall", id="det-uninstall", variant="error"))
277
+ elif state == "INSTALLED_ENABLED":
278
+ actions.mount(Button("Disable", id="det-disable"))
279
+ actions.mount(Button("Uninstall", id="det-uninstall", variant="error"))
280
+ actions.mount(Button("Close", id="det-close2", classes="cct-ctrl"))
281
+ except Exception:
282
+ pass
283
+
284
+ def _finish_det(self, action: str):
285
+ try:
286
+ if action == "install" and ext_mgr:
287
+ ext_mgr.install(self.ext_id)
288
+ self.app._system_note(f"✓ {self.ext_id} installed")
289
+ self.dismiss("installed")
290
+ elif action == "enable" and ext_mgr:
291
+ ext_mgr.enable(self.ext_id)
292
+ self.app._system_note(f"✓ {self.ext_id} enabled")
293
+ self.dismiss("enabled")
294
+ elif action == "disable" and ext_mgr:
295
+ ext_mgr.disable(self.ext_id)
296
+ self.app._system_note(f"✓ {self.ext_id} disabled")
297
+ self.dismiss("disabled")
298
+ elif action == "uninstall" and ext_mgr:
299
+ ext_mgr.uninstall(self.ext_id)
300
+ self.app._system_note(f"✓ {self.ext_id} uninstalled")
301
+ self.dismiss("uninstalled")
302
+ except Exception:
303
+ try:
304
+ self.dismiss(None)
305
+ except Exception:
306
+ pass
307
+
308
+ def on_button_pressed(self, event):
309
+ bid = event.button.id
310
+ if bid in ("det-close", "det-close2"):
311
+ self.dismiss(None)
312
+ elif bid == "det-install":
313
+ self._busy = "install"
314
+ try:
315
+ event.button.disabled = True
316
+ event.button.label = "⟳ Installing…"
317
+ except Exception:
318
+ pass
319
+ self._det_refresh_busy()
320
+ try:
321
+ self.set_timer(0.85, lambda: self._finish_det("install"))
322
+ except Exception:
323
+ self._finish_det("install")
324
+ elif bid == "det-enable":
325
+ self._busy = "enable"
326
+ try:
327
+ event.button.disabled = True
328
+ event.button.label = "⟳ Enabling…"
329
+ except Exception:
330
+ pass
331
+ self._det_refresh_busy()
332
+ try:
333
+ self.set_timer(0.6, lambda: self._finish_det("enable"))
334
+ except Exception:
335
+ self._finish_det("enable")
336
+ elif bid == "det-disable":
337
+ self._busy = "disable"
338
+ try:
339
+ event.button.disabled = True
340
+ event.button.label = "⟳ Disabling…"
341
+ except Exception:
342
+ pass
343
+ self._det_refresh_busy()
344
+ try:
345
+ self.set_timer(0.6, lambda: self._finish_det("disable"))
346
+ except Exception:
347
+ self._finish_det("disable")
348
+ elif bid == "det-uninstall":
349
+ self._busy = "uninstall"
350
+ try:
351
+ event.button.disabled = True
352
+ event.button.label = "⟳ Removing…"
353
+ except Exception:
354
+ pass
355
+ self._det_refresh_busy()
356
+ try:
357
+ self.set_timer(0.85, lambda: self._finish_det("uninstall"))
358
+ except Exception:
359
+ self._finish_det("uninstall")
360
+
361
+ def on_key(self, event):
362
+ if event.key == "escape":
363
+ self.dismiss(None)
364
+
365
+ class _CreateExtensionModal(Screen):
366
+ """Modal dialog to create and add a custom extension."""
367
+ CSS = """
368
+ _CreateExtensionModal { align: center middle; background: rgba(0, 0, 0, 0.7); }
369
+ #create-ext-box {
370
+ width: 72; max-width: 96%; height: auto; max-height: 44;
371
+ background: $surface; border: tall $border; padding: 0;
372
+ layout: vertical; overflow: hidden;
373
+ }
374
+ #create-ext-titlebar { height: 3; padding: 1 2 0 2; border-bottom: solid $border; layout: horizontal; align: center middle; }
375
+ #create-ext-title { width: 1fr; text-style: bold; color: $text; }
376
+ #create-ext-body { height: auto; padding: 1 2; layout: vertical; }
377
+ .create-field-label { width: 100%; height: 1; color: $text-muted; margin-top: 1; }
378
+ .create-field-label:first-child { margin-top: 0; }
379
+ #create-ext-body Input { width: 100%; height: 3; border: tall $border; background: $surface; }
380
+ #create-ext-body Input:focus { border: tall $accent; }
381
+ #create-ext-row { height: auto; layout: horizontal; }
382
+ #create-ext-row > Vertical { width: 1fr; margin-right: 1; }
383
+ #create-ext-row > Vertical:last-child { margin-right: 0; }
384
+ #create-ext-error { width: 100%; height: 1; color: $error; display: none; margin-top: 1; }
385
+ #create-ext-error.show { display: block; }
386
+ #create-ext-actions { height: 4; min-height: 4; padding: 0 2; border-top: solid $border; background: $surface; layout: horizontal; align: right middle; }
387
+ #create-ext-actions Button { margin-left: 1; min-width: 12; height: 3; }
388
+ """
389
+
390
+ def compose(self):
391
+ with Vertical(id="create-ext-box"):
392
+ with Horizontal(id="create-ext-titlebar"):
393
+ yield Static("⚡ Create Custom Extension", id="create-ext-title")
394
+ yield Button("✕", id="create-close", classes="cct-ctrl")
395
+ with Vertical(id="create-ext-body"):
396
+ yield Static("Extension Name *", classes="create-field-label")
397
+ yield Input(placeholder="e.g. Code Formatter Pro", id="input-ext-name")
398
+ with Horizontal(id="create-ext-row"):
399
+ with Vertical():
400
+ yield Static("Extension ID (slug) *", classes="create-field-label")
401
+ yield Input(placeholder="e.g. code_formatter", id="input-ext-id")
402
+ with Vertical():
403
+ yield Static("Icon (emoji/symbol)", classes="create-field-label")
404
+ yield Input(value="⚡", placeholder="⚡", id="input-ext-icon")
405
+ yield Static("Description", classes="create-field-label")
406
+ yield Input(placeholder="e.g. Formats code with custom rules", id="input-ext-desc")
407
+ yield Static("Category", classes="create-field-label")
408
+ yield Input(value="Productivity", placeholder="Category", id="input-ext-cat")
409
+ yield Static("", id="create-ext-error")
410
+ with Horizontal(id="create-ext-actions"):
411
+ yield Button("Cancel", id="create-cancel")
412
+ yield Button("Create & Add", id="create-submit", variant="primary")
413
+
414
+ def on_button_pressed(self, event):
415
+ bid = event.button.id or ""
416
+ if bid in ("create-close", "create-cancel"):
417
+ self.dismiss(None)
418
+ return
419
+ if bid == "create-submit":
420
+ name = self.query_one("#input-ext-name", Input).value.strip()
421
+ raw_id = self.query_one("#input-ext-id", Input).value.strip()
422
+ desc = self.query_one("#input-ext-desc", Input).value.strip()
423
+ icon = self.query_one("#input-ext-icon", Input).value.strip() or "⚡"
424
+ cat = self.query_one("#input-ext-cat", Input).value.strip() or "Productivity"
425
+ err = self.query_one("#create-ext-error", Static)
426
+
427
+ if not name:
428
+ err.update("Please enter an extension name.")
429
+ err.add_class("show")
430
+ return
431
+ ext_id = raw_id.lower().replace("-", "_").replace(" ", "_") if raw_id else name.lower().replace("-", "_").replace(" ", "_")
432
+ import re
433
+ ext_id = re.sub(r"[^a-z0-9_]", "", ext_id)
434
+ if not ext_id:
435
+ err.update("Please enter a valid extension ID.")
436
+ err.add_class("show")
437
+ return
438
+
439
+ if ext_mgr:
440
+ ok, res = ext_mgr.create_extension(
441
+ extension_id=ext_id,
442
+ name=name,
443
+ description=desc,
444
+ icon=icon,
445
+ publisher="You (Custom)",
446
+ category=cat,
447
+ version="1.0.0"
448
+ )
449
+ if ok:
450
+ self.dismiss(("created", ext_id, name))
451
+ else:
452
+ err_msg = res if isinstance(res, str) else (res or {}).get("message", "Failed to create extension.")
453
+ err.update(err_msg)
454
+ err.add_class("show")
455
+ else:
456
+ self.dismiss(None)
457
+
458
+ def on_key(self, event):
459
+ if event.key == "escape":
460
+ self.dismiss(None)
461
+
462
+ class ExtensionsPanel(Screen):
463
+ """VS Code-style Extensions marketplace — professional, minimal, responsive."""
464
+
465
+ CSS = """
466
+ ExtensionsPanel { align: center middle; background: $surface-darken-2 60%; }
467
+ #ext-box {
468
+ width: 84; max-width: 96%; height: auto; max-height: 75%;
469
+ background: $surface; border: tall $border; padding: 0;
470
+ layout: vertical; overflow: hidden;
471
+ }
472
+ #ext-titlebar { height: 3; min-height: 3; padding: 1 2 0 2; border-bottom: solid $border; layout: horizontal; align: center middle; }
473
+ #ext-title { width: 1fr; text-style: bold; color: $text; }
474
+ #ext-search { height: 3; padding: 0 2; margin: 0; }
475
+ #ext-search Input { width: 1fr; height: 3; border: tall $border; background: $surface; }
476
+ #ext-search Input:focus { border: tall $accent; }
477
+ #ext-subtitle { color: $text-muted; height: 1; padding: 0 2; display: none; }
478
+ #ext-list { height: 1fr; min-height: 4; max-height: 62%; overflow-y: auto; scrollbar-gutter: stable; scrollbar-size: 1 1; scrollbar-color: $border $surface; scrollbar-color-hover: $accent $surface; margin: 0 1; padding: 1 0 0 0; }
479
+ .ext-card { height: auto; min-height: 6; padding: 1 2; border: tall $border; background: $surface; margin: 0 0 1 0; layout: horizontal; overflow: hidden; }
480
+ .ext-card:hover { border: tall $accent; }
481
+ .ext-card.ext-busy { background: $surface; border: tall $accent; }
482
+ .ext-card-icon { width: 6; height: auto; text-align: center; text-style: bold; content-align: center middle; background: $accent 8%; border: tall $border; margin-right: 1; padding: 1 0; }
483
+ .ext-icon { width: 100%; height: 1; text-align: center; content-align: center middle; }
484
+ .ext-loading { width: 6; height: 3; color: $accent; content-align: center middle; }
485
+ .ext-card-info { width: 1fr; height: auto; min-width: 16; }
486
+ .ext-title-row { height: 1; layout: horizontal; }
487
+ .ext-name { width: auto; height: 1; text-style: bold; margin-right: 1; color: $text; }
488
+ .ext-verified { width: auto; height: 1; color: $success; }
489
+ .ext-trusted { width: auto; height: 1; color: $success; margin-left: 1; }
490
+ .ext-desc { width: 100%; height: auto; color: $text-muted; max-height: 2; overflow: hidden; }
491
+ .ext-meta { width: 100%; height: 1; color: $text-muted; }
492
+ .ext-status { width: 100%; height: 1; }
493
+ .ext-busy { color: $accent; }
494
+ .ext-busy-label { color: $warning; text-style: bold; }
495
+ .ext-card-actions { width: auto; min-width: 28; height: auto; layout: horizontal; align: center middle; }
496
+ .ext-card-actions Button { min-width: 12; height: 3; min-height: 3; padding: 0 1; margin-left: 1; }
497
+ .ext-card-actions Button:first-child { margin-left: 0; }
498
+ .ext-card-actions Button:disabled { opacity: 0.7; }
499
+ #ext-actions { height: 4; min-height: 4; padding: 0 2; border-top: solid $border; background: $surface; layout: horizontal; align: center middle; overflow: hidden; }
500
+ #ext-actions-left { width: 1fr; height: auto; layout: horizontal; align: center middle; }
501
+ #ext-actions-left Button { margin-right: 1; min-width: 11; height: 3; min-height: 3; padding: 0 1; }
502
+ #ext-actions Button { min-width: 9; height: 3; min-height: 3; padding: 0 1; }
503
+ #ext-hint { display: none; }
504
+ #ext-error { color: $error; background: $surface; height: 1; padding: 0 2; display: none; }
505
+ #ext-error.show { display: block; }
506
+ .cct-compact #ext-box { width: 96; max-height: 88%; }
507
+ .cct-compact #ext-titlebar { height: 2; min-height: 2; }
508
+ .cct-compact #ext-list { min-height: 6; }
509
+ .cct-compact .ext-card { layout: vertical; min-height: 7; }
510
+ .cct-compact .ext-card-info { width: 100%; }
511
+ .cct-compact .ext-card-actions { width: 100%; layout: horizontal; align: center middle; margin-top: 1; }
512
+ .cct-compact .ext-card-actions Button { width: 1fr; min-width: 10; padding: 0 1; margin-top: 0; margin-right: 1; margin-left: 0; }
513
+ .cct-compact .ext-card-actions Button:last-child { margin-right: 0; }
514
+ .cct-compact #ext-actions { padding: 0 1; }
515
+ """
516
+
517
+ BINDINGS = [Binding("escape", "cancel", "Cancel")]
518
+
519
+ def __init__(self):
520
+ super().__init__()
521
+ self._busy: dict = {}
522
+ self._spinner_index: int = 0
523
+ self._search_query: str = ""
524
+ self._busy_timer = None
525
+
526
+ def compose(self):
527
+ with Vertical(id="ext-box"):
528
+ with Horizontal(id="ext-titlebar"):
529
+ yield Static("▣ Extensions", id="ext-title")
530
+ yield Button("✕", id="ext-close", classes="cct-ctrl")
531
+ with Vertical(id="ext-search"):
532
+ yield Input(placeholder="Search extensions...", id="ext-search-input")
533
+ yield Static("Install to enable. Disable hides from menu.", id="ext-subtitle")
534
+ yield Static("", id="ext-error")
535
+ with VerticalScroll(id="ext-list"):
536
+ for ext in self._filtered():
537
+ yield _ExtCard(ext)
538
+ with Horizontal(id="ext-actions"):
539
+ with Horizontal(id="ext-actions-left"):
540
+ yield Button("+ Create Extension", id="ext-create-btn", classes="cct-btn-sm")
541
+ yield Button("Enable All", id="ext-enable-all", classes="cct-btn-sm")
542
+ yield Button("Disable All", id="ext-disable-all", classes="cct-btn-sm")
543
+ yield Button("Close", id="ext-close2", variant="primary", classes="cct-btn-sm")
544
+
545
+ def on_mount(self):
546
+ try:
547
+ self.query_one("#ext-box").add_class("open")
548
+ except Exception:
549
+ pass
550
+ if self.size.width < 86:
551
+ try:
552
+ self.query_one("#ext-box").add_class("cct-compact")
553
+ except Exception:
554
+ pass
555
+ try:
556
+ self.query_one("#ext-search-input", Input).focus()
557
+ except Exception:
558
+ pass
559
+ # start spinner tick for install animations (tracked so
560
+ # re-opening the panel cannot stack orphaned tickers).
561
+ try:
562
+ from . import design_system
563
+ design_system.stop_timer(getattr(self, "_spin_timer", None))
564
+ self._spin_timer = self.set_interval(0.12, self._tick_spinner)
565
+ except Exception:
566
+ try:
567
+ self._spin_timer = self.set_interval(0.12, self._tick_spinner)
568
+ except Exception:
569
+ self._spin_timer = None
570
+
571
+ def on_unmount(self):
572
+ try:
573
+ from . import design_system
574
+ design_system.stop_timer(getattr(self, "_spin_timer", None))
575
+ except Exception:
576
+ pass
577
+ self._spin_timer = None
578
+
579
+ def _filtered(self):
580
+ try:
581
+ query = ""
582
+ try:
583
+ query = self.query_one("#ext-search-input", Input).value.strip().lower()
584
+ except Exception:
585
+ query = getattr(self, "_search_query", "") or ""
586
+ all_exts = ext_mgr.list_extensions() if ext_mgr else []
587
+ if not query:
588
+ return all_exts
589
+ out = []
590
+ for ext in all_exts:
591
+ hay = " ".join([
592
+ str(ext.get("name", "")),
593
+ str(ext.get("display_name", "")),
594
+ str(ext.get("publisher", "")),
595
+ str(ext.get("creator", "")),
596
+ str(ext.get("category", "")),
597
+ str(ext.get("description", "")),
598
+ " ".join(ext.get("keywords", [])),
599
+ ]).lower()
600
+ if query in hay:
601
+ out.append(ext)
602
+ return out
603
+ except Exception:
604
+ return ext_mgr.list_extensions() if ext_mgr else []
605
+
606
+ def _refresh(self, msg=None):
607
+ # preserve scroll to avoid glitch/jump during install animation
608
+ _saved_y = 0
609
+ try:
610
+ lst = self.query_one("#ext-list", VerticalScroll)
611
+ try:
612
+ _saved_y = lst.scroll_y
613
+ except Exception:
614
+ _saved_y = 0
615
+ lst.remove_children()
616
+ for ext in self._filtered():
617
+ lst.mount(_ExtCard(ext))
618
+ # restore scroll without animation
619
+ try:
620
+ lst.scroll_y = _saved_y
621
+ except Exception:
622
+ pass
623
+ except Exception:
624
+ pass
625
+ if msg:
626
+ try:
627
+ self.app._system_note(msg)
628
+ except Exception:
629
+ pass
630
+
631
+ def _show_error(self, msg=""):
632
+ try:
633
+ lbl = self.query_one("#ext-error", Static)
634
+ if msg:
635
+ lbl.update(msg)
636
+ lbl.add_class("show")
637
+ else:
638
+ lbl.remove_class("show")
639
+ except Exception:
640
+ pass
641
+
642
+ def _open_details(self, ext_id):
643
+ try:
644
+ self.app.push_screen(_DetailsPanel(ext_id), lambda res: self._refresh())
645
+ except Exception as e:
646
+ self._show_error(str(e))
647
+
648
+ # ---- install animations (LoadingIndicator handles spin, no full refresh to avoid glitch) ----
649
+ def _tick_spinner(self):
650
+ # kept for compatibility — LoadingIndicator animates automatically, no list refresh to prevent flicker
651
+ if not getattr(self, "_busy", None) or not self._busy:
652
+ return
653
+ self._spinner_index = (self._spinner_index + 1) % 4
654
+ # no _refresh() here — would cause scroll jump/glitch; status is static "Installing…" and LoadingIndicator spins on its own
655
+
656
+ def _start_busy(self, ext_id: str, action: str):
657
+ self._busy[ext_id] = action
658
+ # ensure spinner loop is running
659
+ self._refresh()
660
+ # add busy class to card for CSS highlight
661
+ try:
662
+ for card in self.query("#ext-list _ExtCard"):
663
+ if getattr(card, "ext_id", None) == ext_id:
664
+ card.add_class("ext-busy")
665
+ except Exception:
666
+ pass
667
+
668
+ def _finish_busy(self, ext_id: str):
669
+ try:
670
+ self._busy.pop(ext_id, None)
671
+ except Exception:
672
+ pass
673
+ try:
674
+ self._refresh()
675
+ except Exception:
676
+ pass
677
+
678
+ def _do_install(self, ext_id: str):
679
+ try:
680
+ if ext_mgr:
681
+ ok, msg = ext_mgr.install(ext_id)
682
+ self._finish_busy(ext_id)
683
+ self._refresh(msg)
684
+ if not ok:
685
+ self._show_error(msg)
686
+ else:
687
+ self._show_error("")
688
+ except Exception as e:
689
+ self._finish_busy(ext_id)
690
+ self._show_error(str(e))
691
+
692
+ def _do_uninstall(self, ext_id: str):
693
+ try:
694
+ if ext_mgr:
695
+ ok, msg = ext_mgr.uninstall(ext_id)
696
+ self._finish_busy(ext_id)
697
+ self._refresh(msg)
698
+ if not ok:
699
+ self._show_error(msg)
700
+ except Exception as e:
701
+ self._finish_busy(ext_id)
702
+ self._show_error(str(e))
703
+
704
+ def _do_enable(self, ext_id: str):
705
+ try:
706
+ if ext_mgr:
707
+ ok, msg = ext_mgr.enable(ext_id)
708
+ self._finish_busy(ext_id)
709
+ self._refresh(msg)
710
+ if not ok:
711
+ self._show_error(msg)
712
+ except Exception as e:
713
+ self._finish_busy(ext_id)
714
+ self._show_error(str(e))
715
+
716
+ def _do_disable(self, ext_id: str):
717
+ try:
718
+ if ext_mgr:
719
+ ok, msg = ext_mgr.disable(ext_id)
720
+ self._finish_busy(ext_id)
721
+ self._refresh(msg)
722
+ if not ok:
723
+ self._show_error(msg)
724
+ except Exception as e:
725
+ self._finish_busy(ext_id)
726
+ self._show_error(str(e))
727
+
728
+ def on_input_changed(self, event):
729
+ if getattr(event.input, "id", "") == "ext-search-input":
730
+ self._search_query = event.value
731
+ self._refresh()
732
+
733
+ def on_click(self, event):
734
+ widget = getattr(event, "widget", None)
735
+ if isinstance(widget, _ExtCard):
736
+ self._open_details(widget.ext_id)
737
+
738
+ def on_button_pressed(self, event):
739
+ bid = event.button.id or ""
740
+ if bid in ("ext-close", "ext-close2"):
741
+ self.dismiss(None)
742
+ return
743
+ if bid == "ext-create-btn":
744
+ self.app.push_screen(_CreateExtensionModal(), self._on_created_extension)
745
+ return
746
+ if bid == "ext-enable-all":
747
+ if ext_mgr:
748
+ for ext in ext_mgr.list_extensions():
749
+ if ext.get("installed") and not ext.get("enabled"):
750
+ ext_mgr.enable(ext["id"])
751
+ self._refresh("✓ All extensions enabled")
752
+ return
753
+ if bid == "ext-disable-all":
754
+ if ext_mgr:
755
+ for ext in ext_mgr.list_extensions():
756
+ if ext.get("enabled"):
757
+ ext_mgr.disable(ext["id"])
758
+ self._refresh("✓ All extensions disabled")
759
+ return
760
+ if bid.startswith("ext-install-"):
761
+ key = bid.replace("ext-install-", "")
762
+ if key in self._busy:
763
+ return
764
+ self._start_busy(key, "install")
765
+ try:
766
+ self.set_timer(0.85, lambda: self._do_install(key))
767
+ except Exception:
768
+ self._do_install(key)
769
+ return
770
+ if bid.startswith("ext-enable-"):
771
+ key = bid.replace("ext-enable-", "")
772
+ if key in self._busy:
773
+ return
774
+ self._start_busy(key, "enable")
775
+ try:
776
+ self.set_timer(0.6, lambda: self._do_enable(key))
777
+ except Exception:
778
+ self._do_enable(key)
779
+ return
780
+ if bid.startswith("ext-disable-"):
781
+ key = bid.replace("ext-disable-", "")
782
+ if key in self._busy:
783
+ return
784
+ self._start_busy(key, "disable")
785
+ try:
786
+ self.set_timer(0.6, lambda: self._do_disable(key))
787
+ except Exception:
788
+ self._do_disable(key)
789
+ return
790
+ if bid.startswith("ext-uninstall-"):
791
+ key = bid.replace("ext-uninstall-", "")
792
+ if key in self._busy:
793
+ return
794
+ self._start_busy(key, "uninstall")
795
+ try:
796
+ self.set_timer(0.85, lambda: self._do_uninstall(key))
797
+ except Exception:
798
+ self._do_uninstall(key)
799
+ return
800
+
801
+ def _on_created_extension(self, res):
802
+ if res and isinstance(res, tuple) and res[0] == "created":
803
+ self._refresh(f"✓ Extension '{res[2]}' created & added!")
804
+
805
+ def action_cancel(self):
806
+ self.dismiss(None)
807
+
808
+ def on_key(self, event):
809
+ if event.key == "escape":
810
+ self.dismiss(None)
811
+
812
+ else:
813
+ ExtensionsPanel = None
814
+ _DetailsPanel = None
815
+ _CreateExtensionModal = None