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,226 @@
1
+ """
2
+ CCT UI — thinking.py: the "Advanced AI Response Animation System" (spec
3
+ item #25). Pure logic + formatting helpers only — no Textual dependency,
4
+ no widgets. composer.py's live status bar and conversation.py's
5
+ completion-summary line are the only two things that render what this
6
+ module computes; this file just decides *what* to show (stage label,
7
+ color, spinner frame, elapsed-time text) and never *how* to paint it.
8
+
9
+ Kept dependency-free on purpose: stage detection and timer formatting
10
+ are simple enough to unit test directly, and staying importable outside
11
+ Textual means the fallback CLI could reuse the same stage vocabulary
12
+ later without dragging in ui/ at all.
13
+ """
14
+
15
+ if __name__ == "__main__":
16
+ print("This is a library file and is not meant to be run directly.")
17
+ import sys
18
+ sys.exit(1)
19
+
20
+ import re
21
+
22
+ # ---------------------------------------------------------- spinner styles
23
+ # Animation Style 1 / 2 / 3 from the spec.
24
+ DOT_FRAMES = ["\u25cf\u25cb\u25cb\u25cb\u25cb", "\u25cf\u25cf\u25cb\u25cb\u25cb",
25
+ "\u25cf\u25cf\u25cf\u25cb\u25cb", "\u25cf\u25cf\u25cf\u25cf\u25cb",
26
+ "\u25cf\u25cf\u25cf\u25cf\u25cf"]
27
+ BRAILLE_FRAMES = ["\u280b", "\u2819", "\u2839", "\u2838", "\u283c", "\u2834",
28
+ "\u2826", "\u2827", "\u2807", "\u280f"]
29
+ BLOCK_FRAMES = ["\u25a1\u25a1\u25a1\u25a1\u25a1\u25a1\u25a1\u25a1",
30
+ "\u25a0\u25a0\u25a1\u25a1\u25a1\u25a1\u25a1\u25a1",
31
+ "\u25a0\u25a0\u25a0\u25a0\u25a1\u25a1\u25a1\u25a1",
32
+ "\u25a0\u25a0\u25a0\u25a0\u25a0\u25a0\u25a1\u25a1",
33
+ "\u25a0\u25a0\u25a0\u25a0\u25a0\u25a0\u25a0\u25a0"]
34
+
35
+ STYLES = {"dots": DOT_FRAMES, "braille": BRAILLE_FRAMES, "blocks": BLOCK_FRAMES}
36
+
37
+
38
+ def spinner_frame(style, tick):
39
+ """Loops continuously, per spec ('Loop until completion')."""
40
+ frames = STYLES.get(style, BRAILLE_FRAMES)
41
+ return frames[tick % len(frames)]
42
+
43
+
44
+ # --------------------------------------------------------------- stages --
45
+ # stage_key -> (display label, COLOR ROLE, spinner style).
46
+ #
47
+ # v0.7.9.0 LIGHT-MODE FIX: the second tuple slot used to be a hardcoded
48
+ # Tokyo-Night hex (#7aa2f7, #9ece6a, #e0af68...). Those pastels are
49
+ # unreadable on the light theme's white surfaces — several fall below
50
+ # 2:1 contrast. Colors are now semantic ROLES resolved through
51
+ # theme.role_hex() at CALL time (see stage_info), so dark keeps its
52
+ # exact Tokyo Night values and light automatically gets its darker,
53
+ # readable palette. `style` is just which of the three animation styles
54
+ # best fits that stage's feel.
55
+ STAGES = {
56
+ "preparing": ("Preparing", "blue", "braille"),
57
+ "thinking": ("Thinking", "blue", "braille"),
58
+ "web": ("Searching Web", "cyan", "dots"),
59
+ "knowledge": ("Searching Knowledge", "cyan", "dots"),
60
+ "notebook": ("Searching Notebook", "cyan", "dots"),
61
+ "formula_db": ("Searching Formula Database", "cyan", "dots"),
62
+ "quantum_lib": ("Searching Quantum Library", "cyan", "dots"),
63
+ "analyzing": ("Analyzing Question", "blue", "braille"),
64
+ "calc": ("Running Calculation", "amber", "blocks"),
65
+ "simulation": ("Running Simulation", "purple", "blocks"),
66
+ "sim3d": ("Rendering 3D Simulation", "purple", "blocks"),
67
+ "graph": ("Rendering Graph", "purple", "blocks"),
68
+ "html": ("Generating HTML", "magenta", "braille"),
69
+ "code": ("Executing Code", "magenta", "braille"),
70
+ "building": ("Building Response", "blue", "braille"),
71
+ "formatting": ("Formatting Result", "blue", "dots"),
72
+ "finalizing": ("Finalizing", "blue", "dots"),
73
+ "debugging": ("Debugging", "red", "braille"),
74
+ "installing": ("Installing Package", "amber", "blocks"),
75
+ "done": ("Completed", "green", "dots"),
76
+ "error": ("Error", "red", "dots"),
77
+ }
78
+
79
+ # Role -> theme.color-role used by theme.role_hex(). "magenta" maps to
80
+ # the purple family in both palettes.
81
+ _ROLE_MAP = {
82
+ "blue": "blue", "cyan": "cyan", "purple": "purple",
83
+ "magenta": "purple", "amber": "amber", "green": "green",
84
+ "red": "red", "gray": "gray",
85
+ }
86
+
87
+ DEFAULT_STAGE = "thinking"
88
+
89
+ # Elapsed-time fallback used once no keyword signal fires — keeps the
90
+ # indicator visibly alive on a plain, keyword-free question instead of
91
+ # freezing on one word the whole time it's waiting on the model.
92
+ _PRE_TOKEN_PROGRESSION = ["preparing", "thinking", "analyzing"]
93
+ _POST_TOKEN_PROGRESSION = ["building", "formatting", "finalizing"]
94
+
95
+ # "Smart Status Detection": prompt keywords -> stage. Order matters —
96
+ # first match wins, most specific first.
97
+ _KEYWORD_STAGES = [
98
+ (("/research", "deep research"), "web"),
99
+ (("/websearch", "web search", "search the web", "search online"), "web"),
100
+ (("/install", "/packages", "install ", "pip install", "npm install", "setup "), "installing"),
101
+ (("/debug", "debug this", "fix this bug", "error message", "traceback", "why is this failing"), "debugging"),
102
+ (("/pipeline", "build me a", "build us an", "build a full"), "building"),
103
+ (("/orbitals", "orbital", "quantum library", "electron cloud"), "quantum_lib"),
104
+ (("/atomsim", "/sim3d", "3d simulation", "atomic simulation"), "sim3d"),
105
+ (("/react", "reaction sim", "kinetics ode", "equilibrium ode", "simulate"), "simulation"),
106
+ (("/graph", "plot ", "plot a", "graph of", "draw a graph"), "graph"),
107
+ (("/codepad", "```", "write code", "generate code", "run python"), "code"),
108
+ (("render html", "generate html", "html report"), "html"),
109
+ (("/formulas", "formula database", "formula library"), "formula_db"),
110
+ (("/history", "notebook", "past numericals"), "notebook"),
111
+ (("/solve", "calculate", "compute", "solve for", "/calculator"), "calc"),
112
+ ]
113
+
114
+
115
+ def detect_stage(prompt, elapsed, chunk_count=0):
116
+ """Best-effort stage for a moment in time. `prompt` drives an early,
117
+ strong keyword match (what's about to happen is real signal); once
118
+ tokens are already streaming (`chunk_count > 0`) with nothing more
119
+ specific detected, elapsed time alone walks the generic
120
+ building -> formatting -> finalizing tail so the status bar keeps
121
+ moving instead of sitting still for the rest of the response."""
122
+ text = (prompt or "").lower()
123
+ for keywords, stage in _KEYWORD_STAGES:
124
+ if any(k in text for k in keywords):
125
+ return stage
126
+
127
+ progression = _POST_TOKEN_PROGRESSION if chunk_count else _PRE_TOKEN_PROGRESSION
128
+ idx = min(int(elapsed // 1.4), len(progression) - 1)
129
+ return progression[idx]
130
+
131
+
132
+ def stage_info(stage_key):
133
+ """(label, hex_color, spinner_style) for a stage key, defaulting to
134
+ 'thinking' for anything unrecognized so callers never need a guard.
135
+
136
+ The hex is resolved from the CURRENT theme at call time (dark =
137
+ Tokyo Night values as before; light = its darker, readable
138
+ palette), so a live /theme switch recolors every spinner/status on
139
+ the next tick without any extra wiring."""
140
+ label, role, style = STAGES.get(stage_key, STAGES[DEFAULT_STAGE])
141
+ try:
142
+ from .. import theme
143
+ color = theme.role_hex(_ROLE_MAP.get(role, "blue"))
144
+ except Exception:
145
+ # Dependency-free fallbacks matching the DARK theme (the
146
+ # classic terminal may import this module before theme setup).
147
+ _DARK_FALLBACK = {"blue": "#7aa2f7", "cyan": "#7dcfff",
148
+ "purple": "#bb9af7", "magenta": "#bb9af7",
149
+ "amber": "#e0af68", "green": "#9ece6a",
150
+ "red": "#f7768e"}
151
+ color = _DARK_FALLBACK.get(role, "#7aa2f7")
152
+ return label, color, style
153
+
154
+
155
+ def format_timer(elapsed):
156
+ """0.1 s / 1.2 s / 5.8 s under a minute; mm:ss beyond, matching the
157
+ spec's live-timer examples exactly for the common case."""
158
+ if elapsed < 0:
159
+ elapsed = 0.0
160
+ if elapsed < 60:
161
+ return f"{elapsed:.1f} s"
162
+ minutes, seconds = divmod(int(elapsed), 60)
163
+ return f"{minutes}:{seconds:02d}"
164
+
165
+
166
+ # ---------------------------------------------------- idle chemistry icons
167
+ IDLE_ICONS = ["\u269b", "\U0001f9ea", "\U0001f9ec", "\u2601"] # ⚛ 🧪 🧬 ☁
168
+
169
+
170
+ def idle_icon(tick):
171
+ return IDLE_ICONS[tick % len(IDLE_ICONS)]
172
+
173
+
174
+ # --------------------------------------------------------- command usage --
175
+ _SLASH_CMD_RE = re.compile(r"(?<!\S)/[a-zA-Z][a-zA-Z\-]*")
176
+
177
+
178
+ def commands_used(*texts):
179
+ """Slash commands mentioned across the given texts (prompt +
180
+ response), de-duplicated and sorted, for the completion summary's
181
+ 'Commands Used' line."""
182
+ found = set()
183
+ for text in texts:
184
+ if not text:
185
+ continue
186
+ found.update(m.group(0) for m in _SLASH_CMD_RE.finditer(text))
187
+ return sorted(found)
188
+
189
+
190
+ # ------------------------------------------------------- completion block
191
+ def completion_summary_lines(*, provider=None, model=None, duration=None,
192
+ tokens=None, commands=None, extras=None):
193
+ """Rich-markup lines for the 'Message Completion' block the spec
194
+ describes (✓ Response Complete / Provider / Model / Time / Tokens /
195
+ Commands Used / feature flags). Returns a list of markup strings —
196
+ rendering them is the caller's job (conversation.py turns each into
197
+ a Text.from_markup line under the finished bubble).
198
+
199
+ v0.7.9.0: the ✓ color resolves from the CURRENT theme (was a
200
+ hardcoded dark-palette green that washed out in light mode)."""
201
+ try:
202
+ from .. import theme
203
+ ok_hex = theme.role_hex("green")
204
+ except Exception:
205
+ ok_hex = "#9ece6a"
206
+ lines = [f"[{ok_hex}]\u2713 Response Complete[/]"]
207
+
208
+ meta = []
209
+ if provider:
210
+ meta.append(f"Provider: {provider}")
211
+ if model:
212
+ meta.append(f"Model: {model}")
213
+ if duration is not None:
214
+ meta.append(f"Time: {format_timer(duration)}")
215
+ if tokens:
216
+ meta.append(f"Tokens: {tokens}")
217
+ if meta:
218
+ lines.append("[dim]" + " \u00b7 ".join(meta) + "[/dim]")
219
+
220
+ if commands:
221
+ lines.append("[dim]Commands used: " + ", ".join(commands) + "[/dim]")
222
+
223
+ if extras:
224
+ lines.append("[dim]" + " \u00b7 ".join(extras) + "[/dim]")
225
+
226
+ return lines
@@ -0,0 +1,102 @@
1
+ """
2
+ CCT UI — timeline_panel.py: Session Timeline panel (v0.7.2 roadmap).
3
+ Real Screen push (same family as context_menu.py/header.py's
4
+ PermissionModeMenu — this package's standing "no floating popup
5
+ windows" rule), showing calc_terminal/timeline.py's event log with a
6
+ live search box and per-topic filter chips. "Jump to event" opens the
7
+ related file in the Editor if it still exists on disk; "Restore
8
+ previous state" is the roadmap's own named FUTURE feature and isn't
9
+ implemented here.
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
+ import os
18
+
19
+ from .. import timeline
20
+ from .events import FileOpenRequested
21
+
22
+ TEXTUAL_AVAILABLE = True
23
+ try:
24
+ from textual.containers import Vertical, Horizontal, VerticalScroll
25
+ from textual.screen import Screen
26
+ from textual.widgets import Static, Input, Button
27
+ except Exception:
28
+ TEXTUAL_AVAILABLE = False
29
+
30
+
31
+ if TEXTUAL_AVAILABLE:
32
+
33
+ class _TimelineRow(Static):
34
+ def __init__(self, event):
35
+ super().__init__(event.line(), classes="cct-timeline-row")
36
+ self._event = event
37
+
38
+ def on_click(self, ev):
39
+ if self._event.related_file and os.path.isfile(self._event.related_file):
40
+ self.post_message(FileOpenRequested(self._event.related_file))
41
+
42
+ class TimelinePanel(Screen):
43
+ """Full event log. `_filter_topic`, when set, narrows to one
44
+ eventbus topic; the search box narrows further by substring
45
+ (timeline.search) on top of that. Both apply together, list
46
+ re-rendered via _render_rows() rather than a full recompose."""
47
+
48
+ CSS = """
49
+ TimelinePanel { align: center middle; background: $app-background 60%; }
50
+ #cct-timeline-box {
51
+ width: 84%; height: 80%; background: $surface;
52
+ border: round $border; padding: 1 2;
53
+ }
54
+ #cct-timeline-header { height: 1; }
55
+ #cct-timeline-search { margin-bottom: 1; }
56
+ #cct-timeline-list { height: 1fr; }
57
+ .cct-timeline-row { height: 1; color: $text; padding: 0 1; }
58
+ .cct-timeline-row:hover { color: $accent; }
59
+ #cct-timeline-empty { color: $text-faint; padding: 1; }
60
+ """
61
+
62
+ def __init__(self):
63
+ super().__init__()
64
+ self._query = ""
65
+
66
+ def compose(self):
67
+ with Vertical(id="cct-timeline-box"):
68
+ with Horizontal(id="cct-timeline-header"):
69
+ yield Static("[b]Session Timeline[/b]")
70
+ yield Button("Close", id="cct-timeline-close")
71
+ yield Input(placeholder="Search timeline\u2026", id="cct-timeline-search")
72
+ yield VerticalScroll(id="cct-timeline-list")
73
+
74
+ def on_mount(self):
75
+ self._render_rows()
76
+
77
+ def _render_rows(self):
78
+ box = self.query_one("#cct-timeline-list", VerticalScroll)
79
+ for child in list(box.children):
80
+ child.remove()
81
+ evs = timeline.search(self._query)
82
+ if not evs:
83
+ box.mount(Static("No matching activity yet.", id="cct-timeline-empty"))
84
+ return
85
+ for ev in evs[:200]:
86
+ box.mount(_TimelineRow(ev))
87
+
88
+ def on_input_changed(self, event):
89
+ if event.input.id == "cct-timeline-search":
90
+ self._query = event.value
91
+ self._render_rows()
92
+
93
+ def on_button_pressed(self, event):
94
+ if event.button.id == "cct-timeline-close":
95
+ self.dismiss(None)
96
+
97
+ def on_key(self, event):
98
+ if event.key == "escape":
99
+ self.dismiss(None)
100
+
101
+ else:
102
+ TimelinePanel = None
@@ -0,0 +1,193 @@
1
+ """
2
+ CCT UI — todo_panel.py: AI Todo Manager panel (v0.7.2 roadmap). Real
3
+ Screen push over calc_terminal/todos.py's per-workspace JSON store —
4
+ add, complete/reopen, delete, search, and filter by status, plus a
5
+ live progress bar. Priority is settable at creation (cycles low/
6
+ normal/high via a click on the priority badge); due dates and full
7
+ in-place text editing are accepted from todos.update_todo() but this
8
+ pass's UI only exposes add/toggle/delete/priority-cycle/search/filter
9
+ — see the module docstring in todos.py for what's genuinely
10
+ implemented vs. deferred.
11
+
12
+ The roadmap's own mockup nests this under a "Main Menu" (Workspace /
13
+ Recent Projects / AI Todo / Settings / Extensions / Documentation)
14
+ that doesn't exist anywhere in this codebase yet — header.py's
15
+ hamburger glyph is a plain sidebar-visibility toggle, and Settings/
16
+ Extensions/Documentation aren't panels here either. Building a whole
17
+ new main-menu hierarchy just to host this one panel would be scope
18
+ creep well beyond "add a Todo Manager", so this is exposed the same
19
+ way TimelinePanel is: a dedicated `/todo` command, following this
20
+ codebase's existing pattern for every other panel (/permissions,
21
+ /workspace, /model, etc.) rather than a menu system that has no other
22
+ reason to exist yet.
23
+ """
24
+
25
+ if __name__ == "__main__":
26
+ print("This is a library file and is not meant to be run directly.")
27
+ import sys
28
+ sys.exit(1)
29
+
30
+ from .. import todos
31
+
32
+ TEXTUAL_AVAILABLE = True
33
+ try:
34
+ from textual.containers import Vertical, Horizontal, VerticalScroll
35
+ from textual.screen import Screen
36
+ from textual.widgets import Static, Input, Button, ProgressBar
37
+ except Exception:
38
+ TEXTUAL_AVAILABLE = False
39
+
40
+ _PRIORITY_GLYPH = {"low": "\u25bd", "normal": "\u25cb", "high": "\u25b3"}
41
+
42
+
43
+ if TEXTUAL_AVAILABLE:
44
+
45
+ class _TodoRow(Horizontal):
46
+ def __init__(self, todo):
47
+ super().__init__(classes="cct-todo-row")
48
+ self.todo = todo
49
+
50
+ def compose(self):
51
+ box = "\u2611" if self.todo["status"] == "done" else "\u2610"
52
+ style = "strike dim" if self.todo["status"] == "done" else ""
53
+ source = " [dim](AI)[/]" if self.todo.get("source") == "ai" else ""
54
+ yield Static(box, classes="cct-todo-check", id=f"cct-todo-check-{self.todo['id']}")
55
+ yield Static(_PRIORITY_GLYPH.get(self.todo["priority"], "\u25cb"),
56
+ classes="cct-todo-priority", id=f"cct-todo-pri-{self.todo['id']}")
57
+ label = f"[{style}]{self.todo['text']}[/]" if style else self.todo["text"]
58
+ yield Static(label + source, classes="cct-todo-text")
59
+ yield Button("\u2715", classes="cct-todo-delete", id=f"cct-todo-del-{self.todo['id']}")
60
+
61
+ class TodoPanel(Screen):
62
+ """AI Todo Manager. `workspace` scopes the list to the currently
63
+ open project (or None for the global/no-folder list) — see
64
+ todos.py's module docstring on why todos are per-workspace."""
65
+
66
+ CSS = """
67
+ TodoPanel { align: center middle; background: $app-background 60%; }
68
+ #cct-todo-box {
69
+ width: 84%; height: 80%; background: $surface;
70
+ border: round $border; padding: 1 2;
71
+ }
72
+ #cct-todo-header { height: 1; }
73
+ #cct-todo-progress-row { height: 1; padding: 0 0 1 0; }
74
+ #cct-todo-add-row { height: 3; padding-bottom: 1; }
75
+ #cct-todo-add-row Input { width: 1fr; }
76
+ #cct-todo-filters { height: 1; padding-bottom: 1; }
77
+ #cct-todo-filters Button { margin-right: 1; min-width: 10; }
78
+ #cct-todo-search { margin-bottom: 1; }
79
+ #cct-todo-list { height: 1fr; }
80
+ .cct-todo-row { height: 1; padding: 0 1; }
81
+ .cct-todo-row:hover { color: $accent; }
82
+ .cct-todo-check { width: 3; }
83
+ .cct-todo-priority { width: 3; color: $warning; }
84
+ .cct-todo-text { width: 1fr; }
85
+ .cct-todo-delete { min-width: 3; height: 1; border: none; color: $error; }
86
+ #cct-todo-empty { color: $text-faint; padding: 1; }
87
+ """
88
+
89
+ def __init__(self, workspace=None):
90
+ super().__init__()
91
+ self._workspace = workspace
92
+ self._query = ""
93
+ self._filter = "all" # all | pending | done
94
+
95
+ def compose(self):
96
+ with Vertical(id="cct-todo-box"):
97
+ with Horizontal(id="cct-todo-header"):
98
+ yield Static("[b]AI Todo[/b]")
99
+ yield Button("Close", id="cct-todo-close")
100
+ with Horizontal(id="cct-todo-progress-row"):
101
+ yield Static("", id="cct-todo-progress-label")
102
+ yield ProgressBar(id="cct-todo-progress", show_eta=False)
103
+ with Horizontal(id="cct-todo-add-row"):
104
+ yield Input(placeholder="Add a task and press Enter\u2026", id="cct-todo-add")
105
+ with Horizontal(id="cct-todo-filters"):
106
+ yield Button("All", id="cct-todo-filter-all", variant="primary")
107
+ yield Button("Pending", id="cct-todo-filter-pending")
108
+ yield Button("Done", id="cct-todo-filter-done")
109
+ yield Input(placeholder="Search\u2026", id="cct-todo-search")
110
+ yield VerticalScroll(id="cct-todo-list")
111
+
112
+ def on_mount(self):
113
+ self._render()
114
+
115
+ def _render(self):
116
+ frac, done, total = todos.progress(self._workspace)
117
+ self.query_one("#cct-todo-progress-label", Static).update(
118
+ f"{done}/{total} complete" if total else "No tasks yet")
119
+ try:
120
+ self.query_one("#cct-todo-progress", ProgressBar).update(total=1.0, progress=frac)
121
+ except Exception:
122
+ pass
123
+ items = todos.list_todos(self._workspace)
124
+ if self._filter == "pending":
125
+ items = [t for t in items if t["status"] != "done"]
126
+ elif self._filter == "done":
127
+ items = [t for t in items if t["status"] == "done"]
128
+ if self._query:
129
+ q = self._query.lower()
130
+ items = [t for t in items if q in t["text"].lower()]
131
+ box = self.query_one("#cct-todo-list", VerticalScroll)
132
+ for child in list(box.children):
133
+ child.remove()
134
+ if not items:
135
+ box.mount(Static("Nothing here.", id="cct-todo-empty"))
136
+ return
137
+ for t in items:
138
+ box.mount(_TodoRow(t))
139
+
140
+ def on_input_submitted(self, event):
141
+ if event.input.id == "cct-todo-add":
142
+ todos.add_todo(event.value, workspace=self._workspace, source="manual")
143
+ event.input.value = ""
144
+ self._render()
145
+
146
+ def on_input_changed(self, event):
147
+ if event.input.id == "cct-todo-search":
148
+ self._query = event.value
149
+ self._render()
150
+
151
+ def on_button_pressed(self, event):
152
+ bid = event.button.id or ""
153
+ if bid == "cct-todo-close":
154
+ self.dismiss(None)
155
+ return
156
+ if bid.startswith("cct-todo-filter-"):
157
+ self._filter = bid.rsplit("-", 1)[1]
158
+ for b in self.query("#cct-todo-filters Button"):
159
+ b.variant = "primary" if b.id == bid else "default"
160
+ self._render()
161
+ return
162
+ if bid.startswith("cct-todo-del-"):
163
+ todo_id = int(bid.rsplit("-", 1)[1])
164
+ todos.delete_todo(todo_id, workspace=self._workspace)
165
+ self._render()
166
+ return
167
+
168
+ def on_click(self, event):
169
+ widget = getattr(event, "widget", None)
170
+ wid = getattr(widget, "id", "") or ""
171
+ if wid.startswith("cct-todo-check-"):
172
+ todo_id = int(wid.rsplit("-", 1)[1])
173
+ current = next((t for t in todos.list_todos(self._workspace) if t["id"] == todo_id), None)
174
+ if current:
175
+ new_status = "pending" if current["status"] == "done" else "done"
176
+ todos.set_status(todo_id, new_status, workspace=self._workspace)
177
+ self._render()
178
+ elif wid.startswith("cct-todo-pri-"):
179
+ todo_id = int(wid.rsplit("-", 1)[1])
180
+ current = next((t for t in todos.list_todos(self._workspace) if t["id"] == todo_id), None)
181
+ if current:
182
+ order = ("low", "normal", "high")
183
+ nxt = order[(order.index(current["priority"]) + 1) % len(order)] \
184
+ if current["priority"] in order else "normal"
185
+ todos.update_todo(todo_id, workspace=self._workspace, priority=nxt)
186
+ self._render()
187
+
188
+ def on_key(self, event):
189
+ if event.key == "escape":
190
+ self.dismiss(None)
191
+
192
+ else:
193
+ TodoPanel = None
@@ -0,0 +1,136 @@
1
+ """
2
+ CAT CLI — CATRootViewport: Shared Responsive Viewport Engine.
3
+
4
+ Provides unified terminal dimension measuring, responsive breakpoints,
5
+ and full-screen layout constraints (100% width/height) to eliminate
6
+ the top-left corner shrinkage glitch across Provider, User, and Settings screens.
7
+ """
8
+
9
+ from typing import Optional, Callable
10
+ import shutil
11
+
12
+ try:
13
+ from textual.screen import Screen
14
+ from textual.containers import Container
15
+ from textual.reactive import reactive
16
+ from textual.events import Resize
17
+ TEXTUAL_OK = True
18
+ except ImportError:
19
+ Screen = object # type: ignore
20
+ Container = object # type: ignore
21
+ TEXTUAL_OK = False
22
+
23
+
24
+ # Breakpoint definitions
25
+ BP_LARGE = 100
26
+ BP_MEDIUM = 80
27
+ BP_SMALL = 60
28
+
29
+
30
+ def get_terminal_size_class(width: Optional[int] = None) -> str:
31
+ """Return responsive size class ('large', 'medium', 'small', 'very_small')
32
+ based on current terminal or specified width."""
33
+ if width is None:
34
+ try:
35
+ cols, _ = shutil.get_terminal_size((80, 24))
36
+ width = cols
37
+ except Exception:
38
+ width = 80
39
+
40
+ if width >= BP_LARGE:
41
+ return "large"
42
+ elif width >= BP_MEDIUM:
43
+ return "medium"
44
+ elif width >= BP_SMALL:
45
+ return "small"
46
+ return "very_small"
47
+
48
+
49
+ if TEXTUAL_OK:
50
+ class CATRootViewport(Container):
51
+ """Root container that guarantees 100% width and height and adapts
52
+
53
+ its child layout to the terminal viewport dimensions.
54
+ """
55
+ DEFAULT_CSS = """
56
+ CATRootViewport {
57
+ width: 100%;
58
+ height: 100%;
59
+ min-width: 100%;
60
+ min-height: 100%;
61
+ layout: vertical;
62
+ padding: 0;
63
+ margin: 0;
64
+ overflow: hidden;
65
+ }
66
+ """
67
+
68
+ size_class = reactive("large")
69
+
70
+ def __init__(self, *children, on_size_change: Optional[Callable[[str], None]] = None, **kwargs):
71
+ super().__init__(*children, **kwargs)
72
+ self._on_size_change = on_size_change
73
+
74
+ def on_mount(self) -> None:
75
+ self._update_size_class()
76
+
77
+ def on_resize(self, event: Resize) -> None:
78
+ self._update_size_class(event.size.width)
79
+
80
+ def _update_size_class(self, width: Optional[int] = None) -> None:
81
+ w = width if width is not None else (self.size.width or 80)
82
+ new_cls = get_terminal_size_class(w)
83
+ if new_cls != self.size_class:
84
+ self.size_class = new_cls
85
+ # Update CSS classes for styling hooks (.viewport-large, etc.)
86
+ for cls_name in ("viewport-large", "viewport-medium", "viewport-small", "viewport-very-small"):
87
+ self.remove_class(cls_name)
88
+ self.add_class(f"viewport-{new_cls.replace('_', '-')}")
89
+ if self._on_size_change:
90
+ try:
91
+ self._on_size_change(new_cls)
92
+ except Exception:
93
+ pass
94
+
95
+
96
+ class CATViewportScreenMixin:
97
+ """Mixin for Textual Screen subclasses ensuring full terminal viewport
98
+
99
+ inheritance, proper resize dispatch, and lifecycle cleanup.
100
+ """
101
+ DEFAULT_CSS = """
102
+ Screen {
103
+ width: 100%;
104
+ height: 100%;
105
+ min-width: 100%;
106
+ min-height: 100%;
107
+ padding: 0;
108
+ margin: 0;
109
+ }
110
+ """
111
+
112
+ def get_current_size_class(self) -> str:
113
+ w = getattr(self.size, "width", 0) or getattr(getattr(self, "app", None), "size", None)
114
+ if hasattr(w, "width"):
115
+ width_val = w.width
116
+ elif isinstance(w, int) and w > 0:
117
+ width_val = w
118
+ else:
119
+ cols, _ = shutil.get_terminal_size((80, 24))
120
+ width_val = cols
121
+ return get_terminal_size_class(width_val)
122
+
123
+ def on_resize(self, event: Resize) -> None:
124
+ size_class = get_terminal_size_class(event.size.width)
125
+ if hasattr(self, "on_viewport_resized"):
126
+ try:
127
+ self.on_viewport_resized(size_class, event.size.width, event.size.height)
128
+ except Exception:
129
+ pass
130
+
131
+ else:
132
+ class CATRootViewport: # type: ignore
133
+ pass
134
+
135
+ class CATViewportScreenMixin: # type: ignore
136
+ pass