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,1396 @@
1
+ """
2
+ CCT UI theme bridge — theme.py is the single palette source; this module
3
+ is the ONLY place that turns it into Textual CSS variables. There is no
4
+ second, hand-authored palette here (that was the old composer.py's
5
+ COMPOSER_CSS bug — a flat black-and-blue palette that drifted from the
6
+ rest of the app's Tokyo Night colors). Every color a Textual widget
7
+ uses comes from theme.py's module-level RGB tuples via `css_variables()`
8
+ below, so `/theme light` restyles the primary UI exactly the same way
9
+ it already restyles every print()-based panel.
10
+ """
11
+
12
+ if __name__ == "__main__":
13
+ print("This is a library file and is not meant to be run directly.")
14
+ import sys
15
+ sys.exit(1)
16
+
17
+ from .. import theme
18
+ from .. import ai_modes
19
+
20
+
21
+ def _patch_rich_divide_line():
22
+ """Defend against Rich/Textual ValueError: range() arg 3 must not be zero
23
+ when widgets are rendered with width <= line_pad * 2."""
24
+ try:
25
+ import rich._wrap
26
+ import rich.cells
27
+
28
+ orig_divide_line = getattr(rich._wrap, "_cat_orig_divide_line", None)
29
+ if orig_divide_line is None:
30
+ orig_divide_line = rich._wrap.divide_line
31
+ rich._wrap._cat_orig_divide_line = orig_divide_line
32
+
33
+ def safe_divide_line(text, width, fold=True):
34
+ if width <= 0:
35
+ return []
36
+ return orig_divide_line(text, width, fold=fold)
37
+
38
+ rich._wrap.divide_line = safe_divide_line
39
+
40
+ orig_chop_cells = getattr(rich.cells, "_cat_orig_chop_cells", None)
41
+ if orig_chop_cells is None:
42
+ orig_chop_cells = rich.cells.chop_cells
43
+ rich.cells._cat_orig_chop_cells = orig_chop_cells
44
+
45
+ def safe_chop_cells(text, width, unicode_version="auto"):
46
+ if width <= 0:
47
+ return [text] if text else []
48
+ return orig_chop_cells(text, width, unicode_version=unicode_version)
49
+
50
+ rich.cells.chop_cells = safe_chop_cells
51
+
52
+ try:
53
+ import textual.content
54
+ if hasattr(textual.content, "divide_line"):
55
+ textual.content.divide_line = rich._wrap.divide_line
56
+ except Exception:
57
+ pass
58
+ except Exception:
59
+ pass
60
+
61
+
62
+ _patch_rich_divide_line()
63
+
64
+
65
+ def _hex(rgb):
66
+ r, g, b = rgb
67
+ return f"#{r:02x}{g:02x}{b:02x}"
68
+
69
+
70
+ # Textual CSS variable name -> theme.py attribute name. One row per
71
+ # variable; add a color here, not a second copy of its value anywhere
72
+ # else. `panel`/`element`/`border*` reuse the OC_* set theme.py already
73
+ # maintains for exactly this "OpenCode-style" surface (dark AND light
74
+ # variants already defined there — see theme.py's _THEMES dict).
75
+ _VARIABLE_SOURCE = {
76
+ "surface": "OC_BG_PANEL",
77
+ "surface-alt": "OC_BG_ELEMENT",
78
+ "panel": "OC_BG_PANEL",
79
+ "panel-alt": "OC_BG_ELEMENT",
80
+ "border": "OC_BORDER",
81
+ "border-active": "OC_BORDER_ACTIVE",
82
+ "accent": "OC_PRIMARY",
83
+ "accent-secondary": "OC_SECONDARY",
84
+ "accent-highlight": "OC_ACCENT",
85
+ "text": "TEXT",
86
+ "text-muted": "DIM",
87
+ "text-faint": "FAINT",
88
+ "success": "GREEN",
89
+ "warning": "ORANGE",
90
+ "error": "RED",
91
+ }
92
+
93
+
94
+ _CSS_VAR_CACHE = {}
95
+ _LAST_THEME_MODE = None
96
+
97
+
98
+ def invalidate_css_cache():
99
+ global _LAST_THEME_MODE, _CSS_VAR_CACHE
100
+ _LAST_THEME_MODE = None
101
+ _CSS_VAR_CACHE.clear()
102
+
103
+
104
+ def bevel_colors(hex_color):
105
+ """Computes a lighter top-left highlight and a darker bottom-right shadow from a hex color."""
106
+ try:
107
+ c = str(hex_color).lstrip("#")
108
+ if len(c) == 3:
109
+ c = "".join(ch * 2 for ch in c)
110
+ r = int(c[0:2], 16)
111
+ g = int(c[2:4], 16)
112
+ b = int(c[4:6], 16)
113
+ # Highlight: blend +35% towards white
114
+ hr = min(255, int(r + (255 - r) * 0.35))
115
+ hg = min(255, int(g + (255 - g) * 0.35))
116
+ hb = min(255, int(b + (255 - b) * 0.35))
117
+ # Shadow: blend -45% towards dark black/navy
118
+ sr = max(0, int(r * 0.45))
119
+ sg = max(0, int(g * 0.45))
120
+ sb = max(0, int(b * 0.45))
121
+ return f"#{hr:02x}{hg:02x}{hb:02x}", f"#{sr:02x}{sg:02x}{sb:02x}"
122
+ except Exception:
123
+ return hex_color or "#38bdf8", hex_color or "#0f172a"
124
+
125
+
126
+ def css_variables():
127
+ """Returns {css_variable_name: '#rrggbb'} built from theme.py's
128
+ CURRENT palette (dark or light — whichever `theme.set_theme()` last
129
+ selected). Pass this from `CCTApp.get_css_variables()`.
130
+
131
+ On top of the dark/light base palette, the active AI mode
132
+ (ai_modes.current_mode() — spec section 3) repaints `accent` and
133
+ `accent-highlight` with that mode's color. This is the one thing
134
+ that makes "Agent mode changes... accent color / borders / status
135
+ indicators" (and the same for Build/Plan) true everywhere those two
136
+ variables are already used (BrandHeader's mark, focused-panel
137
+ borders, footer badges) — without a second, mode-specific palette
138
+ to hand-maintain alongside theme.py's dark/light one.
139
+
140
+ v0.7.9.0 LIGHT-MODE AUDIT: Textual's built-in defaults for a whole
141
+ family of component variables are DARK-MODE values (#E0E0E0 text on
142
+ buttons, reverse-video button focus, pale selection washes). In
143
+ dark they're invisible because our own rules override most of them;
144
+ in light they leak through as washed-out / glowing / unreadable
145
+ highlights. When the active theme is light we now emit explicit,
146
+ contrast-correct values for that family — dark emits nothing, so
147
+ dark mode stays byte-for-byte what it was."""
148
+ global _LAST_THEME_MODE, _CSS_VAR_CACHE
149
+ cur_theme = getattr(theme, "CURRENT_THEME_NAME", "tokyo-night")
150
+ cur_mode = ai_modes.current_mode() if hasattr(ai_modes, "current_mode") else "notebook"
151
+ current_key = (cur_theme, cur_mode)
152
+ if _LAST_THEME_MODE == current_key and _CSS_VAR_CACHE:
153
+ return _CSS_VAR_CACHE
154
+
155
+ out = {}
156
+ for var_name, attr_name in _VARIABLE_SOURCE.items():
157
+ rgb = getattr(theme, attr_name, None)
158
+ if rgb is not None:
159
+ out[var_name] = _hex(rgb)
160
+ bg = theme._TERMINAL_BG.get(theme.CURRENT_THEME_NAME, (0, 0, 0))
161
+ out["app-background"] = _hex(bg)
162
+ out["background"] = out["app-background"]
163
+ mode_accent = ai_modes.accent_hex()
164
+ out["accent"] = mode_accent
165
+ hi, sh = bevel_colors(mode_accent)
166
+ out["accent-highlight"] = hi
167
+ out["accent-shadow"] = sh
168
+ out["border-active"] = mode_accent
169
+ s_alt_rgb = getattr(theme, "OC_BG_ELEMENT", (30, 30, 46))
170
+ s_rgb = getattr(theme, "OC_BG_PANEL", (24, 24, 37))
171
+ out["surface-highlight"] = blend_hex(s_alt_rgb, (255, 255, 255), 0.18)
172
+ out["surface-shadow"] = blend_hex(s_rgb, (0, 0, 0), 0.45)
173
+ out["surface-dark"] = blend_hex(s_rgb, (0, 0, 0), 0.35)
174
+ out["surface-active"] = blend_hex(s_alt_rgb, (255, 255, 255), 0.08)
175
+ out["surface-light"] = out["surface-highlight"]
176
+ out["accent-light"] = out["accent-highlight"]
177
+ out["accent-muted"] = out["accent-shadow"]
178
+ out["accent-dark"] = out["accent-shadow"]
179
+ # Standard Textual core variable fallbacks (for Switch, DataTable, etc.)
180
+ out["foreground"] = out.get("text", "#e0e0e0")
181
+ out["primary"] = out.get("accent", "#38bdf8")
182
+ out["secondary"] = out.get("accent-secondary", "#818cf8")
183
+ out["border-blurred"] = out.get("border", "#30363d")
184
+ out["screen-selection-background"] = out.get("accent", "#38bdf8") + "33"
185
+ out["screen-selection-foreground"] = "#ffffff"
186
+
187
+ # v0.7.8 FEATURE 3 (Mode Gradients): the active mode's two-stop
188
+ # gradient, exposed for glass/shimmer accents (composer badge,
189
+ # welcome hero, header mark). `accent` stays the flat solid color
190
+ # for borders/bubbles; the gradient is used where a subtle wash is
191
+ # wanted — never heavy.
192
+ g_start, g_end = ai_modes.gradient()
193
+ out["accent-gradient-start"] = g_start
194
+ out["accent-gradient-end"] = g_end
195
+
196
+ # v0.7.9.5 THEME CATALOG: every component-level variable below is now
197
+ # derived from the ACTIVE THEME OBJECT (theme.get_theme_obj()) instead
198
+ # of hardcoded GitHub-Light hexes, and emitted whenever the theme is
199
+ # light. Dark themes keep Textual's own dark-mode defaults (unchanged
200
+ # behavior from the v0.7.9.0 audit). Because each theme's schema
201
+ # carries tuned selection/cursor/heading colors, ANY of the 22 themes
202
+ # gets correct, contrast-checked chrome — no theme can inherit another
203
+ # theme's colors anymore.
204
+ if theme.is_light():
205
+ t = theme.get_theme_obj()
206
+ text_hex = t.hex("text")
207
+ muted_hex = t.hex("text_muted")
208
+ blue_hex = t.hex("accent_alt")
209
+ cursor_bg = blend_hex(t.text, t.background, 0.12)
210
+ out.update({
211
+ # Buttons: Textual's default label color is #E0E0E0 (near-
212
+ # white), which vanishes on light surfaces.
213
+ "button-foreground": text_hex,
214
+ # Focus: replace the default `b reverse` full-block invert
215
+ # with plain bold — the active border already marks focus.
216
+ "button-focus-text-style": "bold",
217
+ # Text/inputs: subtle, readable selection — a soft solid
218
+ # wash with dark text, never a bright glowing block, and it
219
+ # cannot bleed into unselected neighbors.
220
+ "input-selection-background": t.hex("selection_background"),
221
+ "input-selection-foreground": t.hex("selection_text"),
222
+ "screen-selection-background": t.hex("selection_background"),
223
+ "screen-selection-foreground": t.hex("selection_text"),
224
+ # Cursors: clearly visible without being loud.
225
+ "input-cursor-background": cursor_bg,
226
+ "input-cursor-foreground": t.hex("background"),
227
+ "block-cursor-background": blue_hex,
228
+ "block-cursor-foreground": "auto 87%",
229
+ "block-cursor-blurred-background": blue_hex + "44",
230
+ "block-cursor-blurred-foreground": text_hex,
231
+ # Footer key caps: amber readable on light surfaces.
232
+ "footer-key-foreground": t.hex("warning"),
233
+ # Blurred borders stop assuming a near-black chrome color.
234
+ "border-blurred": t.hex("border"),
235
+ # Markdown headings in assistant bubbles: darker, WCAG-safe.
236
+ "markdown-h1-color": blue_hex,
237
+ "markdown-h2-color": blue_hex,
238
+ "markdown-h3-color": blue_hex,
239
+ "markdown-h4-color": text_hex,
240
+ "markdown-h5-color": text_hex,
241
+ # In light mode, surface tones must be soft and adaptive, never dark/black
242
+ "surface-highlight": blend_hex(t.background, (255, 255, 255), 0.60),
243
+ "accent-highlight": blue_hex,
244
+ "accent-light": blue_hex,
245
+ "surface-shadow": t.hex("border"),
246
+ "surface-dark": t.hex("border"),
247
+ "surface-active": blend_hex(t.background, t.text, 0.08),
248
+ "surface-alt": blend_hex(t.background, t.text, 0.04),
249
+ })
250
+ _LAST_THEME_MODE = current_key
251
+ _CSS_VAR_CACHE = out
252
+ return out
253
+
254
+
255
+ def blend_hex(a, b, t):
256
+ """Blend two RGB tuples (t=0 → a, t=1 → b) into a '#rrggbb' string."""
257
+ r1, g1, b1 = a
258
+ r2, g2, b2 = b
259
+ return "#{:02x}{:02x}{:02x}".format(
260
+ int(round(r1 + (r2 - r1) * t)),
261
+ int(round(g1 + (g2 - g1) * t)),
262
+ int(round(b1 + (b2 - b1) * t)))
263
+
264
+
265
+ def gradient_hex(key=None):
266
+ """Direct (start_hex, end_hex) lookup for the active (or named) AI
267
+ mode's gradient, for Rich-markup callers outside CSS."""
268
+ return ai_modes.gradient(key)
269
+
270
+
271
+ def current_hex(role):
272
+ """One-off lookup for widgets that need a raw hex string outside CSS
273
+ var resolution (e.g. an f-string building Rich markup). `role` is
274
+ one of the keys in `_VARIABLE_SOURCE` above, or 'app-background'."""
275
+ return css_variables().get(role, "#888888")
276
+
277
+
278
+ # Shared layout/spacing rules every ui/ widget file composes with. Pure
279
+ # structure — no literal colors — every color reference below is a Textual
280
+ # `$variable` resolved at runtime from css_variables(), so switching
281
+ # `/theme light` <-> dark restyles this without touching a single rule here.
282
+ BASE_CSS = """
283
+ Screen { background: $app-background; }
284
+
285
+ .cct-panel {
286
+ background: $surface;
287
+ border: heavy;
288
+ border-top: heavy $surface-highlight;
289
+ border-left: heavy $surface-highlight;
290
+ border-bottom: heavy $surface-dark;
291
+ border-right: heavy $surface-dark;
292
+ }
293
+
294
+ .cct-panel:focus-within {
295
+ border: heavy;
296
+ border-top: heavy #ffffff;
297
+ border-left: heavy #ffffff;
298
+ border-bottom: heavy $border-active;
299
+ border-right: heavy $border-active;
300
+ }
301
+
302
+ .cct-card-3d, .cct-panel-3d {
303
+ background: $surface;
304
+ border: heavy;
305
+ border-top: heavy $surface-highlight;
306
+ border-left: heavy $surface-highlight;
307
+ border-bottom: heavy $surface-dark;
308
+ border-right: heavy $surface-dark;
309
+ padding: 1 2;
310
+ }
311
+
312
+ .cct-text { color: $text; }
313
+ .cct-text-muted { color: $text-muted; }
314
+ .cct-text-faint { color: $text-faint; }
315
+ .cct-success { color: $success; }
316
+ .cct-warning { color: $warning; }
317
+ .cct-error { color: $error; }
318
+
319
+ /* Tooltip: themed, 3D raised border with legible contrast (fixes black-box hover bug) */
320
+ Tooltip {
321
+ background: $surface-alt;
322
+ color: $text;
323
+ border: heavy $border-active;
324
+ padding: 0 1;
325
+ text-style: none;
326
+ max-width: 60;
327
+ opacity: 0.98;
328
+ }
329
+
330
+ /* Toast: themed 3D raised notification card with beveled lighting */
331
+ Toast, Toast.-information, Toast.-warning, Toast.-error {
332
+ background: $surface;
333
+ color: $text;
334
+ border: heavy;
335
+ border-top: heavy #ffffff;
336
+ border-left: heavy #ffffff;
337
+ border-bottom: heavy $surface-dark;
338
+ border-right: heavy $surface-dark;
339
+ padding: 1 2;
340
+ }
341
+ Toast.-warning {
342
+ border-top: heavy #ffffff;
343
+ border-left: heavy #ffffff;
344
+ border-bottom: heavy $warning;
345
+ border-right: heavy $warning;
346
+ }
347
+ Toast.-error {
348
+ border-top: heavy #ffffff;
349
+ border-left: heavy #ffffff;
350
+ border-bottom: heavy $error;
351
+ border-right: heavy $error;
352
+ }
353
+ Toast.-information {
354
+ border-top: heavy #ffffff;
355
+ border-left: heavy #ffffff;
356
+ border-bottom: heavy $accent-shadow;
357
+ border-right: heavy $accent-shadow;
358
+ }
359
+
360
+ /* ==================================================== v0.7.8.1 ======
361
+ Shared DESIGN SYSTEM — button + popup conventions every dialog in
362
+ ui/ follows, so no panel ever ships its own ad-hoc sizes again.
363
+ ------------------------------------------------------------------ */
364
+
365
+ /* ---- Button system (v0.7.8.61): ONE shared definition for every
366
+ button in the app. Bare `Button` rules override Textual's own
367
+ default "hkey/tall" half-block design (▔▁ rails / ▊▔▎ boxes) at
368
+ equal specificity — app CSS is merged after widget CSS, so the tie
369
+ resolves to this block. Variants drop in via Textual's emitted
370
+ `.-primary` / `.-error` / `.-success` classes. The clickable region,
371
+ border, background, and text all share the same 3-cell footprint:
372
+ no button-specific positioning, spacing hacks, or fractional sizes.
373
+
374
+ v0.7.8.61 changes (washed-out / "highlighted" button look):
375
+ - `text-style: not bold` — Textual's default Button CSS force-bolds
376
+ every label (`Button.-style-default { text-style: bold }`), which
377
+ made ALL buttons read as selected/highlighted. The base resets it;
378
+ focus and the primary variant re-bold explicitly.
379
+ - `background: $surface-alt` — the old `$surface` background equals
380
+ the panel color, so buttons melted into their cards. `$surface-alt`
381
+ sits one step lighter and reads as a raised control everywhere.
382
+ - hover: accent wash + active border; focus: active border + bold,
383
+ with Textual's default `background-tint` cleared so focus never
384
+ smears the button.
385
+ - `.-active` (pressed): the button sinks one row (`offset-y: 1`) and
386
+ darkens via `tint` — a real press, not a palette swap.
387
+ - disabled buttons dim without losing their label.
388
+ The `Button:ansi.-style-default/-style-flat` duplicates keep this
389
+ block winning over Textual's widget defaults even in setups where
390
+ the `:ansi` pseudo-class matches. ---- */
391
+ Button, Button:ansi.-style-default, Button:ansi.-style-flat, .cct-btn {
392
+ height: 3; min-height: 3; max-height: 3; padding: 0 2;
393
+ min-width: 10;
394
+ border: heavy;
395
+ border-top: heavy $surface-highlight;
396
+ border-left: heavy $surface-highlight;
397
+ border-bottom: heavy $surface-dark;
398
+ border-right: heavy $surface-dark;
399
+ background: $surface-alt;
400
+ color: $text;
401
+ text-style: bold;
402
+ content-align: center middle;
403
+ transition: background 80ms, color 80ms, border 80ms, offset 80ms;
404
+ }
405
+ Button:hover, .cct-btn:hover {
406
+ background: $accent 24%;
407
+ border: heavy;
408
+ border-top: heavy #ffffff;
409
+ border-left: heavy $accent-highlight;
410
+ border-bottom: heavy $accent;
411
+ border-right: heavy $accent;
412
+ color: $text;
413
+ text-style: bold;
414
+ offset-y: 0;
415
+ }
416
+ Button:focus, .cct-btn:focus {
417
+ border: heavy;
418
+ border-top: heavy #ffffff;
419
+ border-left: heavy $accent-highlight;
420
+ border-bottom: heavy $accent;
421
+ border-right: heavy $accent;
422
+ text-style: bold;
423
+ background-tint: transparent;
424
+ }
425
+ Button:disabled, .cct-btn:disabled { opacity: 0.45; text-style: not bold; }
426
+ Button.-active, .cct-btn.-active {
427
+ offset-y: 1;
428
+ border: heavy;
429
+ border-top: heavy $surface-dark;
430
+ border-left: heavy $surface-dark;
431
+ border-bottom: heavy $surface-highlight;
432
+ border-right: heavy $surface-highlight;
433
+ tint: $app-background 20%;
434
+ }
435
+ .chats-filter-btn.-active,
436
+ Screen .chats-filter-btn.-active {
437
+ offset-y: 0;
438
+ offset-x: 0;
439
+ offset: 0 0;
440
+ }
441
+ Button.cct-dash-action, .cct-dash-action,
442
+ Button.cct-dash-action.-style-default,
443
+ Button.cct-dash-action:ansi.-style-default,
444
+ Button.cct-dash-action:ansi.-style-flat,
445
+ Screen Button.cct-dash-action {
446
+ height: 3; min-height: 3; max-height: 3; padding: 0;
447
+ min-width: 14; max-width: 24; width: 1fr; margin: 0 1;
448
+ background: $surface-alt;
449
+ color: #ffffff;
450
+ text-style: bold;
451
+ content-align: center middle;
452
+ border: heavy;
453
+ border-top: heavy #ffffff;
454
+ border-left: heavy #ffffff;
455
+ border-bottom: heavy $accent-shadow;
456
+ border-right: heavy $accent-shadow;
457
+ transition: background 80ms, color 80ms, border 80ms, offset 80ms;
458
+ }
459
+ Button.cct-dash-action:hover, .cct-dash-action:hover,
460
+ Button.cct-dash-action:focus, .cct-dash-action:focus,
461
+ Screen Button.cct-dash-action:hover, Screen Button.cct-dash-action:focus {
462
+ background: $accent 35%;
463
+ color: #ffffff;
464
+ border: heavy;
465
+ border-top: heavy #ffffff;
466
+ border-left: heavy #ffffff;
467
+ border-bottom: heavy $accent-highlight;
468
+ border-right: heavy $accent-highlight;
469
+ text-style: bold;
470
+ offset-y: 0;
471
+ }
472
+ Button.cct-dash-action.-active, .cct-dash-action.-active,
473
+ Screen Button.cct-dash-action.-active {
474
+ offset-y: 1;
475
+ background: $accent 50%;
476
+ color: #ffffff;
477
+ border: heavy;
478
+ border-top: heavy $accent-shadow;
479
+ border-left: heavy $accent-shadow;
480
+ border-bottom: heavy #ffffff;
481
+ border-right: heavy #ffffff;
482
+ }
483
+ .cct-btn-3d {
484
+ height: 3; min-height: 3; max-height: 3; padding: 0 2;
485
+ min-width: 10;
486
+ border: heavy;
487
+ border-top: heavy $accent-highlight;
488
+ border-left: heavy $accent-highlight;
489
+ border-bottom: heavy $accent-shadow;
490
+ border-right: heavy $accent-shadow;
491
+ background: $surface-alt;
492
+ color: $text;
493
+ text-style: bold;
494
+ content-align: center middle;
495
+ transition: background 80ms, color 80ms, border 80ms, offset 80ms;
496
+ }
497
+ .cct-btn-3d:hover {
498
+ background: $accent 24%;
499
+ border: heavy;
500
+ border-top: heavy #ffffff;
501
+ border-left: heavy #ffffff;
502
+ border-bottom: heavy $accent;
503
+ border-right: heavy $accent;
504
+ color: #ffffff;
505
+ text-style: bold;
506
+ offset-y: 0;
507
+ }
508
+ .cct-btn-3d:focus {
509
+ border: heavy;
510
+ border-top: heavy #ffffff;
511
+ border-left: heavy #ffffff;
512
+ border-bottom: heavy $accent-highlight;
513
+ border-right: heavy $accent-highlight;
514
+ text-style: bold;
515
+ background-tint: transparent;
516
+ }
517
+ .cct-btn-3d.-active {
518
+ offset-y: 1;
519
+ border: heavy;
520
+ border-top: heavy $accent-shadow;
521
+ border-left: heavy $accent-shadow;
522
+ border-bottom: heavy $accent-highlight;
523
+ border-right: heavy $accent-highlight;
524
+ tint: $app-background 20%;
525
+ }
526
+
527
+ /* Universal 3D Skeuomorphic Button Keycaps */
528
+ Button.cct-btn, Button.cct-modal-btn, Button.cct-btn-sm, Button.cct-btn-primary {
529
+ border: tall;
530
+ border-top: tall $surface-highlight;
531
+ border-left: tall $surface-highlight;
532
+ border-bottom: tall $surface-shadow;
533
+ border-right: tall $surface-shadow;
534
+ background: $surface;
535
+ color: $text;
536
+ text-style: bold;
537
+ transition: background 80ms, color 80ms, border 80ms, offset 60ms;
538
+ }
539
+ Button.cct-btn:hover, Button.cct-modal-btn:hover, Button.cct-btn-sm:hover {
540
+ border: tall;
541
+ border-top: tall $accent-highlight;
542
+ border-left: tall $accent-highlight;
543
+ border-bottom: tall $accent-shadow;
544
+ border-right: tall $accent-shadow;
545
+ background: $surface-alt;
546
+ color: #ffffff;
547
+ }
548
+ Button.cct-btn:focus, Button.cct-modal-btn:focus, Button.cct-btn-sm:focus {
549
+ border: tall;
550
+ border-top: tall $accent-highlight;
551
+ border-left: tall $accent-highlight;
552
+ border-bottom: tall $accent-shadow;
553
+ border-right: tall $accent-shadow;
554
+ background: $surface-alt;
555
+ color: #ffffff;
556
+ }
557
+ Button.cct-btn.-active, Button.cct-modal-btn.-active, Button.cct-btn-sm.-active {
558
+ offset-y: 1;
559
+ border: tall;
560
+ border-top: tall $surface-shadow;
561
+ border-left: tall $surface-shadow;
562
+ border-bottom: tall $surface-highlight;
563
+ border-right: tall $surface-highlight;
564
+ background: $surface-dark;
565
+ }
566
+ Button.cct-btn-primary, Button.-primary {
567
+ border: tall;
568
+ border-top: tall $accent-highlight;
569
+ border-left: tall $accent-highlight;
570
+ border-bottom: tall $accent-shadow;
571
+ border-right: tall $accent-shadow;
572
+ background: $accent;
573
+ color: #ffffff;
574
+ }
575
+ Button.cct-btn-primary:hover, Button.-primary:hover {
576
+ background: $accent-highlight;
577
+ color: #ffffff;
578
+ }
579
+ Button.cct-btn-primary.-active, Button.-primary.-active {
580
+ offset-y: 1;
581
+ border-top: tall $accent-shadow;
582
+ border-left: tall $accent-shadow;
583
+ border-bottom: tall $accent-highlight;
584
+ border-right: tall $accent-highlight;
585
+ background: $accent-shadow;
586
+ }
587
+
588
+ /* Universal 3D Skeuomorphic Sunken Tray Inputs / Search Bars */
589
+ Input.cct-search-input, .cct-input-3d, #search-input, #model-search, #pc-search Input, #ext-search Input, #cust-search Input, #mcp-search Input {
590
+ border: tall;
591
+ border-top: tall $surface-shadow;
592
+ border-left: tall $surface-shadow;
593
+ border-bottom: tall $surface-highlight;
594
+ border-right: tall $surface-highlight;
595
+ background: $app-background;
596
+ color: $text;
597
+ transition: border 80ms, background 80ms;
598
+ }
599
+ Input.cct-search-input:focus, .cct-input-3d:focus, #search-input:focus, #model-search:focus, #pc-search Input:focus, #ext-search Input:focus, #cust-search Input:focus, #mcp-search Input:focus {
600
+ border: tall;
601
+ border-top: tall $surface-shadow;
602
+ border-left: tall $surface-shadow;
603
+ border-bottom: tall $accent-highlight;
604
+ border-right: tall $accent-highlight;
605
+ background: $app-background;
606
+ }
607
+ Button.cct-ctrl, .cct-ctrl,
608
+ Button.cct-ctrl:ansi.-style-default,
609
+ Button.cct-ctrl:ansi.-style-flat {
610
+ height: 1; min-height: 1; max-height: 1;
611
+ min-width: 3; max-width: 3; width: 3; padding: 0; margin: 0;
612
+ background: transparent; color: $text-faint;
613
+ border: none;
614
+ content-align: center middle;
615
+ transition: color 80ms, background 80ms, offset 60ms;
616
+ }
617
+ Button.cct-ctrl:hover, .cct-ctrl:hover {
618
+ color: $error; background: transparent; border: none;
619
+ }
620
+
621
+ #cct-right-controls {
622
+ dock: right;
623
+ width: 12;
624
+ min-width: 12;
625
+ max-width: 12;
626
+ height: 1;
627
+ min-height: 1;
628
+ max-height: 1;
629
+ layout: horizontal;
630
+ align-vertical: middle;
631
+ align-horizontal: right;
632
+ margin: 0;
633
+ padding: 0;
634
+ overflow: hidden;
635
+ background: transparent;
636
+ }
637
+
638
+ Button.cct-icon-btn,
639
+ Button.cct-icon-btn:hover,
640
+ Button.cct-icon-btn:focus,
641
+ Button.cct-icon-btn.-active,
642
+ Button#btn-permissions,
643
+ Button#btn-permissions:hover,
644
+ Button#btn-permissions:focus,
645
+ Button#btn-permissions.-active,
646
+ Button#btn-attach,
647
+ Button#btn-attach:hover,
648
+ Button#btn-attach:focus,
649
+ Button#btn-attach.-active,
650
+ Button.cct-icon-send,
651
+ Button.cct-icon-send:hover,
652
+ Button.cct-icon-send:focus,
653
+ Button.cct-icon-send.-active,
654
+ Button#btn-send,
655
+ Button#btn-send:hover,
656
+ Button#btn-send:focus,
657
+ Button#btn-send.-active {
658
+ border: none;
659
+ offset: 0 0;
660
+ offset-x: 0;
661
+ offset-y: 0;
662
+ transition: none;
663
+ }
664
+
665
+ Button.cct-icon-btn, .cct-icon-btn,
666
+ Button.cct-icon-btn:ansi.-style-default,
667
+ Button.cct-icon-btn:ansi.-style-flat,
668
+ Button#btn-permissions,
669
+ Button#btn-permissions:ansi.-style-default {
670
+ height: 1; min-height: 1; max-height: 1;
671
+ min-width: 3; max-width: 3; width: 3; padding: 0; margin: 0;
672
+ background: transparent; color: $text;
673
+ border: none;
674
+ text-style: bold;
675
+ content-align: center middle;
676
+ }
677
+ Button#btn-attach,
678
+ Button#btn-attach:ansi.-style-default {
679
+ height: 1; min-height: 1; max-height: 1;
680
+ min-width: 4; max-width: 4; width: 4; padding: 0; margin: 0 0 0 1;
681
+ background: transparent; color: $text;
682
+ border: none;
683
+ text-style: bold;
684
+ content-align: center middle;
685
+ }
686
+ Button.cct-icon-btn:hover, Button#btn-permissions:hover, Button#btn-attach:hover {
687
+ color: $accent-highlight; background: $surface-highlight 40%; text-style: bold; border: none;
688
+ }
689
+ Button.cct-icon-btn:focus, Button#btn-permissions:focus, Button#btn-attach:focus {
690
+ color: $accent-highlight; background: $surface-highlight 40%; text-style: bold; border: none;
691
+ }
692
+ Button.cct-icon-btn.-active, Button#btn-permissions.-active, Button#btn-attach.-active {
693
+ background: $surface-highlight 70%; color: #ffffff; border: none; offset: 0 0;
694
+ }
695
+
696
+ Button.cct-icon-send, .cct-icon-send,
697
+ Button.cct-icon-send:ansi.-style-default,
698
+ Button.cct-icon-send:ansi.-style-flat,
699
+ Button#btn-send,
700
+ Button#btn-send:ansi.-style-default {
701
+ height: 1; min-height: 1; max-height: 1;
702
+ min-width: 3; max-width: 3; width: 3; padding: 0; margin: 0 0 0 1;
703
+ background: transparent; color: $accent;
704
+ border: none;
705
+ text-style: bold;
706
+ content-align: center middle;
707
+ }
708
+ Button.cct-icon-send:hover, Button#btn-send:hover {
709
+ color: #ffffff; background: $accent 60%; text-style: bold; border: none;
710
+ }
711
+ Button.cct-icon-send:focus, Button#btn-send:focus {
712
+ color: $accent-highlight; background: $surface-highlight 40%; text-style: bold; border: none;
713
+ }
714
+ Button.cct-icon-send.-active, Button#btn-send.-active {
715
+ background: $surface-highlight 70%; color: #ffffff; border: none; offset: 0 0;
716
+ }
717
+
718
+ /* Interrupt Button state during AI Streaming */
719
+ #cct-composer Button#btn-send.cct-btn-interrupt,
720
+ #cct-composer.cct-mode-notebook Button#btn-send.cct-btn-interrupt,
721
+ #cct-composer.cct-mode-research Button#btn-send.cct-btn-interrupt,
722
+ #cct-composer.cct-mode-plan Button#btn-send.cct-btn-interrupt,
723
+ #cct-composer.cct-mode-build Button#btn-send.cct-btn-interrupt,
724
+ #cct-composer.cct-mode-debugger Button#btn-send.cct-btn-interrupt,
725
+ #cct-composer.cct-mode-agent Button#btn-send.cct-btn-interrupt,
726
+ Button.cct-btn-interrupt, Button#btn-send.cct-btn-interrupt {
727
+ background: transparent;
728
+ color: #ef4444;
729
+ text-style: bold;
730
+ }
731
+ #cct-composer Button#btn-send.cct-btn-interrupt:hover,
732
+ Button.cct-btn-interrupt:hover, Button#btn-send.cct-btn-interrupt:hover {
733
+ background: #ef4444 25%;
734
+ color: #f87171;
735
+ text-style: bold;
736
+ }
737
+ #cct-composer Button#btn-send.cct-btn-interrupt:focus,
738
+ Button.cct-btn-interrupt:focus, Button#btn-send.cct-btn-interrupt:focus {
739
+ background: #ef4444 25%;
740
+ color: #f87171;
741
+ text-style: bold;
742
+ }
743
+ #cct-composer Button#btn-send.cct-btn-interrupt.-active,
744
+ Button.cct-btn-interrupt.-active, Button#btn-send.cct-btn-interrupt.-active {
745
+ background: #ef4444 45%;
746
+ color: #fca5a5;
747
+ }
748
+
749
+ /* AI Mode Dynamic 3D Chassis & Skeuomorphic Accent Theming */
750
+ #cct-composer,
751
+ #cct-composer:focus-within,
752
+ #cct-composer.cct-mode-notebook,
753
+ #cct-composer.cct-mode-research,
754
+ #cct-composer.cct-mode-plan,
755
+ #cct-composer.cct-mode-build,
756
+ #cct-composer.cct-mode-debugger,
757
+ #cct-composer.cct-mode-agent {
758
+ background: $app-background;
759
+ }
760
+
761
+ #cct-composer.cct-mode-notebook {
762
+ border: heavy;
763
+ border-top: heavy #89b4fa;
764
+ border-left: heavy #89b4fa;
765
+ border-bottom: heavy #3d59a1;
766
+ border-right: heavy #3d59a1;
767
+ }
768
+ #cct-composer.cct-mode-notebook:focus-within {
769
+ border: heavy;
770
+ border-top: heavy #b4befe;
771
+ border-left: heavy #b4befe;
772
+ border-bottom: heavy #5875b7;
773
+ border-right: heavy #5875b7;
774
+ }
775
+ #cct-composer.cct-mode-notebook #cct-prompt-glyph {
776
+ color: #7aa2f7;
777
+ }
778
+ #cct-composer.cct-mode-notebook #cct-prompt-row,
779
+ #cct-composer.cct-mode-notebook #cct-prompt-row:focus-within {
780
+ border: none;
781
+ }
782
+ #cct-composer.cct-mode-notebook Button#btn-send,
783
+ #cct-composer.cct-mode-notebook Button.cct-icon-send {
784
+ background: transparent;
785
+ color: #7aa2f7;
786
+ }
787
+ #cct-composer.cct-mode-notebook Button#btn-send:hover,
788
+ #cct-composer.cct-mode-notebook Button.cct-icon-send:hover {
789
+ background: #7aa2f7 25%;
790
+ color: #89b4fa;
791
+ }
792
+
793
+ #cct-composer.cct-mode-research {
794
+ border: heavy;
795
+ border-top: heavy #ff85c0;
796
+ border-left: heavy #ff85c0;
797
+ border-bottom: heavy #9e366a;
798
+ border-right: heavy #9e366a;
799
+ }
800
+ #cct-composer.cct-mode-research:focus-within {
801
+ border: heavy;
802
+ border-top: heavy #ffb3d9;
803
+ border-left: heavy #ffb3d9;
804
+ border-bottom: heavy #b8437d;
805
+ border-right: heavy #b8437d;
806
+ }
807
+ #cct-composer.cct-mode-research #cct-prompt-glyph {
808
+ color: #ff69b4;
809
+ }
810
+ #cct-composer.cct-mode-research #cct-prompt-row,
811
+ #cct-composer.cct-mode-research #cct-prompt-row:focus-within {
812
+ border: none;
813
+ }
814
+ #cct-composer.cct-mode-research Button#btn-send,
815
+ #cct-composer.cct-mode-research Button.cct-icon-send {
816
+ background: transparent;
817
+ color: #ff69b4;
818
+ }
819
+ #cct-composer.cct-mode-research Button#btn-send:hover,
820
+ #cct-composer.cct-mode-research Button.cct-icon-send:hover {
821
+ background: #ff69b4 25%;
822
+ color: #ff85c0;
823
+ }
824
+
825
+ #cct-composer.cct-mode-plan {
826
+ border: heavy;
827
+ border-top: heavy #a6e3a1;
828
+ border-left: heavy #a6e3a1;
829
+ border-bottom: heavy #567e30;
830
+ border-right: heavy #567e30;
831
+ }
832
+ #cct-composer.cct-mode-plan:focus-within {
833
+ border: heavy;
834
+ border-top: heavy #c5f3be;
835
+ border-left: heavy #c5f3be;
836
+ border-bottom: heavy #6e9d40;
837
+ border-right: heavy #6e9d40;
838
+ }
839
+ #cct-composer.cct-mode-plan #cct-prompt-glyph {
840
+ color: #9ece6a;
841
+ }
842
+ #cct-composer.cct-mode-plan #cct-prompt-row,
843
+ #cct-composer.cct-mode-plan #cct-prompt-row:focus-within {
844
+ border: none;
845
+ }
846
+ #cct-composer.cct-mode-plan Button#btn-send,
847
+ #cct-composer.cct-mode-plan Button.cct-icon-send {
848
+ background: transparent;
849
+ color: #9ece6a;
850
+ }
851
+ #cct-composer.cct-mode-plan Button#btn-send:hover,
852
+ #cct-composer.cct-mode-plan Button.cct-icon-send:hover {
853
+ background: #9ece6a 25%;
854
+ color: #a6e3a1;
855
+ }
856
+
857
+ #cct-composer.cct-mode-build {
858
+ border: heavy;
859
+ border-top: heavy #f0d3a7;
860
+ border-left: heavy #f0d3a7;
861
+ border-bottom: heavy #966b33;
862
+ border-right: heavy #966b33;
863
+ }
864
+ #cct-composer.cct-mode-build:focus-within {
865
+ border: heavy;
866
+ border-top: heavy #fae3b0;
867
+ border-left: heavy #fae3b0;
868
+ border-bottom: heavy #b88642;
869
+ border-right: heavy #b88642;
870
+ }
871
+ #cct-composer.cct-mode-build #cct-prompt-glyph {
872
+ color: #e0af68;
873
+ }
874
+ #cct-composer.cct-mode-build #cct-prompt-row,
875
+ #cct-composer.cct-mode-build #cct-prompt-row:focus-within {
876
+ border: none;
877
+ }
878
+ #cct-composer.cct-mode-build Button#btn-send,
879
+ #cct-composer.cct-mode-build Button.cct-icon-send {
880
+ background: transparent;
881
+ color: #e0af68;
882
+ }
883
+ #cct-composer.cct-mode-build Button#btn-send:hover,
884
+ #cct-composer.cct-mode-build Button.cct-icon-send:hover {
885
+ background: #e0af68 25%;
886
+ color: #fab387;
887
+ }
888
+
889
+ #cct-composer.cct-mode-debugger {
890
+ border: heavy;
891
+ border-top: heavy #f38ba8;
892
+ border-left: heavy #f38ba8;
893
+ border-bottom: heavy #991b1b;
894
+ border-right: heavy #991b1b;
895
+ }
896
+ #cct-composer.cct-mode-debugger:focus-within {
897
+ border: heavy;
898
+ border-top: heavy #fca5a5;
899
+ border-left: heavy #fca5a5;
900
+ border-bottom: heavy #b91c1c;
901
+ border-right: heavy #b91c1c;
902
+ }
903
+ #cct-composer.cct-mode-debugger #cct-prompt-glyph {
904
+ color: #dc143c;
905
+ }
906
+ #cct-composer.cct-mode-debugger #cct-prompt-row,
907
+ #cct-composer.cct-mode-debugger #cct-prompt-row:focus-within {
908
+ border: none;
909
+ }
910
+ #cct-composer.cct-mode-debugger Button#btn-send,
911
+ #cct-composer.cct-mode-debugger Button.cct-icon-send {
912
+ background: transparent;
913
+ color: #dc143c;
914
+ }
915
+ #cct-composer.cct-mode-debugger Button#btn-send:hover,
916
+ #cct-composer.cct-mode-debugger Button.cct-icon-send:hover {
917
+ background: #dc143c 25%;
918
+ color: #f38ba8;
919
+ }
920
+
921
+ #cct-composer.cct-mode-agent {
922
+ border: heavy;
923
+ border-top: heavy #cba6f7;
924
+ border-left: heavy #cba6f7;
925
+ border-bottom: heavy #7a58b8;
926
+ border-right: heavy #7a58b8;
927
+ }
928
+ #cct-composer.cct-mode-agent:focus-within {
929
+ border: heavy;
930
+ border-top: heavy #e0caff;
931
+ border-left: heavy #e0caff;
932
+ border-bottom: heavy #946fd4;
933
+ border-right: heavy #946fd4;
934
+ }
935
+ #cct-composer.cct-mode-agent #cct-prompt-glyph {
936
+ color: #bb9af7;
937
+ }
938
+ #cct-composer.cct-mode-agent #cct-prompt-row,
939
+ #cct-composer.cct-mode-agent #cct-prompt-row:focus-within {
940
+ border: none;
941
+ }
942
+ #cct-composer.cct-mode-agent Button#btn-send,
943
+ #cct-composer.cct-mode-agent Button.cct-icon-send {
944
+ background: transparent;
945
+ color: #bb9af7;
946
+ }
947
+ #cct-composer.cct-mode-agent Button#btn-send:hover,
948
+ #cct-composer.cct-mode-agent Button.cct-icon-send:hover {
949
+ background: #bb9af7 25%;
950
+ color: #cba6f7;
951
+ }
952
+
953
+ /* 3D Toggle Switch */
954
+ Button.cct-toggle-3d {
955
+ height: 1; min-height: 1; max-height: 1;
956
+ width: 9; min-width: 9; max-width: 9;
957
+ padding: 0; margin: 0;
958
+ content-align: center middle;
959
+ text-style: bold;
960
+ border: none;
961
+ transition: background 100ms, color 100ms, offset 60ms;
962
+ }
963
+ Button.cct-toggle-3d.-on {
964
+ background: #047857;
965
+ color: #ecfdf5;
966
+ }
967
+ Button.cct-toggle-3d.-on:hover {
968
+ background: #059669;
969
+ color: #ffffff;
970
+ }
971
+ Button.cct-toggle-3d.-off {
972
+ background: #1e293b;
973
+ color: #94a3b8;
974
+ }
975
+ Button.cct-toggle-3d.-off:hover {
976
+ background: #334155;
977
+ color: #f1f5f9;
978
+ }
979
+ Button.cct-toggle-3d.-active {
980
+ offset-y: 1;
981
+ }
982
+ Button.-primary, .cct-btn.-primary, Button.cct-btn-primary {
983
+ background: $accent 30%;
984
+ color: $text;
985
+ border-top: tall $accent-highlight;
986
+ border-left: tall $accent-highlight;
987
+ border-bottom: tall $accent;
988
+ border-right: tall $accent;
989
+ text-style: bold;
990
+ }
991
+ Button.-primary:hover, .cct-btn.-primary:hover, Button.cct-btn-primary:hover {
992
+ background: $accent 50%;
993
+ border-top: tall #ffffff;
994
+ border-left: tall $accent-highlight;
995
+ border-bottom: tall $accent;
996
+ border-right: tall $accent;
997
+ color: #ffffff;
998
+ offset-y: -1;
999
+ }
1000
+ Button.-primary:focus, .cct-btn.-primary:focus, Button.cct-btn-primary:focus {
1001
+ background: $accent 45%;
1002
+ border-top: tall #ffffff;
1003
+ border-left: tall $accent-highlight;
1004
+ border-bottom: tall $accent;
1005
+ border-right: tall $accent;
1006
+ color: #ffffff;
1007
+ text-style: bold;
1008
+ }
1009
+ Button.-primary.-active, .cct-btn.-primary.-active, Button.cct-btn-primary.-active {
1010
+ offset-y: 1;
1011
+ border-top: tall $accent;
1012
+ border-left: tall $accent;
1013
+ border-bottom: tall $accent-highlight;
1014
+ border-right: tall $accent-highlight;
1015
+ }
1016
+ Button.-error, .cct-btn.-error { background: $surface-alt; color: $error; border: tall $error #7f1d1d; text-style: bold; }
1017
+ Button.-error:hover, .cct-btn.-error:hover { background: $error 25%; color: #ffffff; border: tall #fca5a5 $error; }
1018
+ Button.-error:focus, .cct-btn.-error:focus { background: $error 15%; border: tall #fca5a5 $error; text-style: bold; }
1019
+ Button.-error.-active, .cct-btn.-error.-active { background: $error; color: #ffffff; border: tall #7f1d1d #fca5a5; offset-y: 1; }
1020
+ Button.-success, .cct-btn.-success { background: $surface-alt; color: $success; border: tall $success #14532d; text-style: bold; }
1021
+ Button.-success:hover, .cct-btn.-success:hover { background: $success 25%; color: #ffffff; border: tall #86efac $success; }
1022
+ Button.-success:focus, .cct-btn.-success:focus { background: $success 15%; border: tall #86efac $success; text-style: bold; }
1023
+ Button.-success.-active, .cct-btn.-success.-active { background: $success; color: #ffffff; border: tall #14532d #86efac; offset-y: 1; }
1024
+ Button.cct-btn-sm, .cct-btn-sm { min-width: 8; padding: 0 1; }
1025
+ Button.cct-btn-cta, .cct-btn-cta { min-width: 20; padding: 0 3; }
1026
+
1027
+ /* Shared form field look: same tall-nothing-to-hide rule — every
1028
+ Input app-wide gets the real single-line rounded border instead of
1029
+ Textual's default 3-cell "tall" block box. */
1030
+ Input {
1031
+ height: 3;
1032
+ border: tall;
1033
+ border-top: tall $surface-shadow;
1034
+ border-left: tall $surface-shadow;
1035
+ border-bottom: tall $surface-highlight;
1036
+ border-right: tall $surface-highlight;
1037
+ background: $app-background;
1038
+ color: $text;
1039
+ padding: 0 1;
1040
+ }
1041
+ Input:focus {
1042
+ border: tall;
1043
+ border-top: tall $accent-shadow;
1044
+ border-left: tall $accent-shadow;
1045
+ border-bottom: tall $accent-highlight;
1046
+ border-right: tall $accent-highlight;
1047
+ }
1048
+
1049
+ /* ---- Popup system: every centered modal uses .cct-popup (the dimmed
1050
+ full-screen overlay) + .cct-popup-box (the card). Fade + slide-up
1051
+ open animation, sharp 3D border, never wider than the terminal, and
1052
+ `Esc`/`Enter` handling lives in each Screen (shared convention). */
1053
+ .cct-popup {
1054
+ align: center middle;
1055
+ background: $app-background 65%;
1056
+ }
1057
+ .cct-popup-box {
1058
+ width: auto; height: auto; max-width: 100%; max-height: 100%;
1059
+ background: $surface;
1060
+ border: heavy;
1061
+ border-top: heavy $surface-highlight;
1062
+ border-left: heavy $surface-highlight;
1063
+ border-bottom: heavy $surface-shadow;
1064
+ border-right: heavy $surface-shadow;
1065
+ padding: 0;
1066
+ opacity: 0; offset-y: 1;
1067
+ transition: opacity 140ms, offset 160ms;
1068
+ }
1069
+ .cct-popup-box.open { opacity: 1; offset-y: 0; }
1070
+
1071
+ /* Title bar shared by every popup card. */
1072
+ .cct-popup-titlebar {
1073
+ height: 3; padding: 1 2 0 2;
1074
+ border-bottom: solid $border;
1075
+ }
1076
+ .cct-popup-title { text-style: bold; width: 1fr; }
1077
+ .cct-popup-subtitle { color: $text-faint; padding: 1 2 0 2; }
1078
+ .cct-popup-hint { color: $text-faint; padding: 0 2 1 2; }
1079
+ .cct-popup-close, Button.cct-popup-close,
1080
+ Button.cct-popup-close:ansi.-style-default,
1081
+ Button.cct-popup-close:ansi.-style-flat,
1082
+ Button.cct-popup-close:focus,
1083
+ Button#hc-close,
1084
+ Button#hc-close:ansi.-style-default,
1085
+ Button#hc-close:ansi.-style-flat,
1086
+ Button#ollama-close,
1087
+ Button#ollama-close:ansi.-style-default,
1088
+ Button#mcp-close,
1089
+ Button#mcp-close:ansi.-style-default,
1090
+ Button#bpp-close,
1091
+ Button#bpp-close:ansi.-style-default,
1092
+ Button#act-close,
1093
+ Button#act-close:ansi.-style-default,
1094
+ Button#pp-close,
1095
+ Button#pp-close:ansi.-style-default,
1096
+ Button#pc-close,
1097
+ Button#pc-close:ansi.-style-default,
1098
+ Button#ap-close,
1099
+ Button#ap-close:ansi.-style-default,
1100
+ Button#apf-close,
1101
+ Button#apf-close:ansi.-style-default,
1102
+ Button#amf-close,
1103
+ Button#amf-close:ansi.-style-default {
1104
+ width: 3; min-width: 3; max-width: 3;
1105
+ height: 1; min-height: 1; max-height: 1;
1106
+ padding: 0; margin: 0;
1107
+ background: transparent; color: $text-faint; border: none;
1108
+ content-align: center middle;
1109
+ }
1110
+ .cct-popup-close:hover, Button.cct-popup-close:hover,
1111
+ Button.cct-popup-close:hover:ansi.-style-default,
1112
+ Button#hc-close:hover,
1113
+ Button#ollama-close:hover,
1114
+ Button#mcp-close:hover,
1115
+ Button#bpp-close:hover,
1116
+ Button#act-close:hover,
1117
+ Button#pp-close:hover,
1118
+ Button#pc-close:hover,
1119
+ Button#ap-close:hover,
1120
+ Button#apf-close:hover,
1121
+ Button#amf-close:hover {
1122
+ color: $error;
1123
+ background: transparent;
1124
+ border: none;
1125
+ }
1126
+ .cct-popup-actions {
1127
+ height: 5; padding: 0 2 1 2; border-top: solid $border;
1128
+ align-horizontal: right;
1129
+ }
1130
+ .cct-popup-actions Button { margin-left: 1; }
1131
+
1132
+ /* Form fields inside popups — consistent height and spacing. */
1133
+ .cct-popup-box Input {
1134
+ height: 3;
1135
+ border: tall;
1136
+ border-top: tall $surface-shadow;
1137
+ border-left: tall $surface-shadow;
1138
+ border-bottom: tall $surface-highlight;
1139
+ border-right: tall $surface-highlight;
1140
+ background: $app-background;
1141
+ color: $text; margin: 0 0 0 0; padding: 0 1;
1142
+ }
1143
+ .cct-popup-box Input:focus {
1144
+ border: tall;
1145
+ border-top: tall $accent-shadow;
1146
+ border-left: tall $accent-shadow;
1147
+ border-bottom: tall $accent-highlight;
1148
+ border-right: tall $accent-highlight;
1149
+ }
1150
+ .cct-field-label { color: $text-faint; height: 2; padding-top: 1; }
1151
+
1152
+ /* ====================================================================
1153
+ CAT Customization — Dynamic UI & Button Styles (Section 11)
1154
+ Shapes: minimal, rounded, pill, square, ghost, outlined, filled, compact, large
1155
+ Sizes: small, medium, large, compact
1156
+ ==================================================================== */
1157
+ .cust-btn-shape-minimal Button, .cust-btn-shape-minimal Button:ansi.-style-default { border: none; background: transparent; }
1158
+ .cust-btn-shape-minimal Button:hover { background: $accent 15%; border: none; }
1159
+ .cust-btn-shape-rounded Button, .cust-btn-shape-rounded Button:ansi.-style-default { border: round $border; padding: 0 1; min-width: 10; min-height: 3; height: 3; }
1160
+ .cust-btn-shape-pill Button, .cust-btn-shape-pill Button:ansi.-style-default { border: round $border; padding: 0 1; min-width: 10; min-height: 3; height: 3; }
1161
+ .cust-btn-shape-square Button, .cust-btn-shape-square Button:ansi.-style-default { border: solid $border; padding: 0 1; min-width: 10; min-height: 3; height: 3; }
1162
+ .cust-btn-shape-ghost Button, .cust-btn-shape-ghost Button:ansi.-style-default { border: none; background: transparent; }
1163
+ .cust-btn-shape-ghost Button:hover { background: $accent 12%; }
1164
+ .cust-btn-shape-outlined Button, .cust-btn-shape-outlined Button:ansi.-style-default { border: solid $border; background: transparent; padding: 0 1; min-width: 10; min-height: 3; height: 3; }
1165
+ .cust-btn-shape-outlined Button:hover { background: $accent 15%; }
1166
+ .cust-btn-shape-filled Button, .cust-btn-shape-filled Button:ansi.-style-default { border: none; background: $surface-alt; }
1167
+ .cust-btn-shape-compact Button, .cust-btn-shape-compact Button:ansi.-style-default { height: 1; min-height: 1; min-width: 8; padding: 0 1; border: none; }
1168
+ .cust-btn-shape-large Button, .cust-btn-shape-large Button:ansi.-style-default { height: 4; min-height: 4; min-width: 16; padding: 1 3; }
1169
+
1170
+ .cust-btn-size-small Button, .cust-btn-size-small Button:ansi.-style-default { height: 1; min-height: 1; min-width: 8; padding: 0 1; border: none; }
1171
+ .cust-btn-size-medium Button, .cust-btn-size-medium Button:ansi.-style-default { height: 3; min-height: 3; min-width: 12; padding: 0 1; }
1172
+ .cust-btn-size-large Button, .cust-btn-size-large Button:ansi.-style-default { height: 4; min-height: 4; min-width: 16; padding: 1 3; }
1173
+ .cust-btn-size-compact Button, .cust-btn-size-compact Button:ansi.-style-default { height: 1; min-height: 1; min-width: 8; padding: 0 1; border: none; }
1174
+
1175
+ /* Preserve 3D Action & Dashboard Buttons from flat customization overrides */
1176
+ Button.cct-dash-action, Button.cct-dash-action.-style-default,
1177
+ Screen Button.cct-dash-action, Screen.cust-btn-shape-rounded Button.cct-dash-action,
1178
+ Screen.cust-btn-shape-pill Button.cct-dash-action, Screen.cust-btn-shape-square Button.cct-dash-action,
1179
+ Screen.cust-btn-shape-ghost Button.cct-dash-action, Screen.cust-btn-shape-outlined Button.cct-dash-action,
1180
+ Screen.cust-btn-shape-filled Button.cct-dash-action, Screen.cust-btn-shape-minimal Button.cct-dash-action,
1181
+ Button#cct-open-folder-btn, Screen Button#cct-open-folder-btn {
1182
+ border: heavy;
1183
+ border-top: heavy #ffffff;
1184
+ border-left: heavy #ffffff;
1185
+ border-bottom: heavy $accent-shadow;
1186
+ border-right: heavy $accent-shadow;
1187
+ }
1188
+
1189
+ /* ====================================================================
1190
+ CAT Touchscreen & Touch-Friendly UI Mode Rules (.cct-touch-mode)
1191
+ ==================================================================== */
1192
+ .cct-touch-mode Button, .cct-touch-mode .cct-btn, .cct-touch-mode .cct-ctrl {
1193
+ min-height: 3;
1194
+ min-width: 10;
1195
+ padding: 0 2;
1196
+ }
1197
+ .cct-touch-mode Button.cct-icon-btn {
1198
+ min-width: 4;
1199
+ min-height: 1;
1200
+ padding: 0 1;
1201
+ }
1202
+ .cct-touch-mode .cct-badge {
1203
+ padding: 0 1;
1204
+ min-width: 6;
1205
+ }
1206
+ .cct-touch-mode .cct-rws-row,
1207
+ .cct-touch-mode .cct-setting-row,
1208
+ .cct-touch-mode .cct-nav-item,
1209
+ .cct-touch-mode .cct-sidebar-project-row {
1210
+ min-height: 2;
1211
+ padding: 0 1;
1212
+ }
1213
+ .cct-touch-mode .cct-filetree {
1214
+ padding: 0 1;
1215
+ }
1216
+ .cct-touch-mode Button:hover,
1217
+ .cct-touch-mode Button:focus,
1218
+ .cct-touch-mode .cct-btn:hover,
1219
+ .cct-touch-mode .cct-btn:focus,
1220
+ .cct-touch-mode .cct-ctrl:hover,
1221
+ .cct-touch-mode .cct-ctrl:focus {
1222
+ background: $accent 30%;
1223
+ border: solid $accent;
1224
+ }
1225
+ .cct-touch-mode #cct-explorer-resizer,
1226
+ .cct-touch-mode #cct-rightpane-resizer {
1227
+ background: $border;
1228
+ }
1229
+ .cct-touch-mode #cct-explorer-resizer:hover,
1230
+ .cct-touch-mode #cct-explorer-resizer.dragging,
1231
+ .cct-touch-mode #cct-rightpane-resizer:hover,
1232
+ .cct-touch-mode #cct-rightpane-resizer.dragging {
1233
+ background: $accent;
1234
+ }
1235
+ .cct-touch-mode .cct-tb-pill {
1236
+ padding: 0 1;
1237
+ min-width: 3;
1238
+ }
1239
+ .cct-touch-mode .cct-tb-pill:hover,
1240
+ .cct-touch-mode .cct-tb-pill:focus {
1241
+ color: $accent;
1242
+ text-style: bold;
1243
+ }
1244
+ .cct-touch-mode .cct-tb-close {
1245
+ min-width: 4;
1246
+ padding: 0 1;
1247
+ }
1248
+ .cct-touch-mode .cct-bv-btn {
1249
+ min-width: 4;
1250
+ padding: 0 1;
1251
+ }
1252
+ .cct-touch-mode .cct-bv-btn:hover,
1253
+ .cct-touch-mode .cct-bv-btn:focus {
1254
+ color: $accent;
1255
+ background: $surface-alt;
1256
+ }
1257
+ """
1258
+
1259
+
1260
+ def fit_dialog(screen, box_id, scroll_id, fill_ids=()):
1261
+ """Size a centered dialog card to its content, capped to the terminal.
1262
+
1263
+ Textual CSS alone cannot express "card is content-sized, but when the
1264
+ terminal is too short the middle scrolls and the footer stays
1265
+ visible": an `1fr` middle always inflates an auto-height card to its
1266
+ max-height, and an auto middle overflows the capped card. So each
1267
+ dialog releases its card to natural auto sizing, waits for that
1268
+ layout to actually land, and then imposes ONE of two consistent
1269
+ states:
1270
+
1271
+ Release — every id this helper may have forced before (the card,
1272
+ its scroll area, and the fill ids) is reset back to its natural
1273
+ CSS height. The next layout passes therefore measure real natural
1274
+ sizes, never heights distorted by a previous fit.
1275
+
1276
+ Apply — v0.7.8.5: the card's natural total height is its own
1277
+ *measured region* in the released layout — Textual already stacks
1278
+ every child (header, scroll area, footer) at natural heights, so
1279
+ re-deriving the total from chrome/fixed/content sums is both
1280
+ unnecessary and wrong (that old math double-counted the scroll
1281
+ area's padding, so every dialog landed 1-2 rows short and the
1282
+ footer buttons were pushed past the box border and clipped).
1283
+ The card height is set to that measured natural total, capped at
1284
+ `vh - 4` and floored at 6. When capped, the scroll area switches
1285
+ to `1fr` (so the footer stays visible in the card) and every id
1286
+ in `fill_ids` switches from `auto` to `100%` (used by two-column
1287
+ dialogs whose columns must fill the card and scroll internally
1288
+ instead of overflowing it).
1289
+
1290
+ Textual never applies a changed CSS height instantly: the released
1291
+ box keeps its previous region for one layout pass, and content that
1292
+ mounts *after* a fit call (row lists rebuilt in on_mount/on_resize
1293
+ handlers) only lands a frame later. A single measure-then-set cycle
1294
+ can therefore snapshot a stale region and freeze the dialog at a
1295
+ too-small height with its footer pushed past the box border. So
1296
+ apply() loops: it waits until the released region no longer matches
1297
+ a height this helper previously imposed, measures, writes the new
1298
+ heights, and then runs one more release+measure pass — repeating
1299
+ only while consecutive measures disagree. Every cycle starts from
1300
+ the same released baseline, so fits converge to the stable natural
1301
+ size and cannot shrink or inflate the card frame by frame.
1302
+ """
1303
+ import os
1304
+
1305
+ # Per-screen memory of the height the last fit imposed on each box,
1306
+ # used to tell "the released layout hasn't landed yet" (region still
1307
+ # equals what we forced) apart from "the layout is stable at natural".
1308
+ memory = getattr(screen, "_cct_fit_memory", None)
1309
+ if memory is None:
1310
+ memory = {}
1311
+ try:
1312
+ screen._cct_fit_memory = memory
1313
+ except Exception:
1314
+ pass
1315
+
1316
+ def _q(ident):
1317
+ return screen.query_one(f"#{ident}")
1318
+
1319
+ def remember(value):
1320
+ memory[box_id] = value
1321
+
1322
+ def release():
1323
+ wanted = [box_id, scroll_id, *fill_ids]
1324
+ for ident in wanted:
1325
+ try:
1326
+ _q(ident).styles.height = "auto"
1327
+ except Exception:
1328
+ pass
1329
+
1330
+ def apply(attempts=0):
1331
+ if attempts > 14:
1332
+ return # cannot settle; leave whatever is on screen
1333
+ try:
1334
+ box = _q(box_id)
1335
+ scroll = _q(scroll_id)
1336
+ except Exception:
1337
+ return
1338
+ if not (box.is_attached and scroll.is_attached):
1339
+ return
1340
+ vh = screen.size.height
1341
+ # Short terminals: panels may hide chrome (subtitles/hints) via
1342
+ # the shared .cct-compact class; re-measure once it takes effect.
1343
+ want_compact = vh < 28
1344
+ if want_compact != box.has_class("cct-compact"):
1345
+ box.set_class(want_compact, "cct-compact")
1346
+ screen.call_after_refresh(lambda: apply(attempts))
1347
+ return
1348
+
1349
+ last_imposed = memory.get(box_id)
1350
+ if last_imposed is not None and box.region.height == last_imposed:
1351
+ # Still showing the previous fit — the released layout
1352
+ # hasn't landed yet. Wait one more frame (bounded; equal
1353
+ # values are also the stable end-state, so giving up here is
1354
+ # harmless).
1355
+ screen.call_after_refresh(lambda: apply(attempts + 1))
1356
+ return
1357
+
1358
+ natural_total = box.region.height
1359
+ if natural_total < 2:
1360
+ # Layout not ready (first frame); try again after the next one.
1361
+ screen.call_after_refresh(lambda: apply(attempts + 1))
1362
+ return
1363
+ capped = natural_total > vh - 4
1364
+ target = max(6, min(natural_total, vh - 4))
1365
+ if os.environ.get("CCT_FIT_DEBUG"):
1366
+ children = ", ".join(
1367
+ f"{c.id or type(c).__name__}={c.region.height}" for c in box.children)
1368
+ print(f"[fit] {box_id}@{vh}: box={box.region} natural={natural_total} "
1369
+ f"target={target} capped={capped} children=[{children}]")
1370
+ if target == last_imposed:
1371
+ # The released layout agrees with the previous fit: stable.
1372
+ # The styles below may still hold the release() reset from
1373
+ # the confirmation cycle above — re-assert the settled state
1374
+ # and stop.
1375
+ scroll.styles.height = "1fr" if capped else "auto"
1376
+ for child_id in fill_ids:
1377
+ try:
1378
+ _q(child_id).styles.height = "100%" if capped else "auto"
1379
+ except Exception:
1380
+ pass
1381
+ return
1382
+ box.styles.height = target
1383
+ scroll.styles.height = "1fr" if capped else "auto"
1384
+ for child_id in fill_ids:
1385
+ try:
1386
+ _q(child_id).styles.height = "100%" if capped else "auto"
1387
+ except Exception:
1388
+ pass
1389
+ remember(target)
1390
+ # Content may still be landing; confirm with one more
1391
+ # release + measure cycle before calling it settled.
1392
+ release()
1393
+ screen.call_after_refresh(lambda: apply(attempts + 1))
1394
+
1395
+ release()
1396
+ screen.call_after_refresh(apply)