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,831 @@
1
+ """
2
+ CCT UI — ui/workspace.py (spec v0.7 Workspace section, distinct from
3
+ calc_terminal/workspace.py which stores *generated artifacts*, not the
4
+ IDE's open-folder state).
5
+
6
+ Adaptive workspace layout: Explorer sidebar on the left, a CHAT COLUMN
7
+ in the middle, and an independent RIGHT PANE that switches between the
8
+ Code Editor and the Live Web Preview:
9
+
10
+ ┌──────────────┬─┬───────────────────┬─┬──────────────────┐
11
+ │ FILE EXPLORER│║│ CHAT / AI ║│ CODE ⇄ WEB │
12
+ │ │║│ (ConversationView)║│ (EditorPane / │
13
+ │ (draggable │║│ ║│ PreviewPanel) │
14
+ │ divider) │║├───────────────────┤╵│ (drag divider) │
15
+ │ │║│ grip ║│ │
16
+ │ │║├───────────────────┤╵│ │
17
+ │ │║│ CHAT COMPOSER ║│ │
18
+ └──────────────┴─┴───────────────────┴─┴──────────────────┘
19
+
20
+ v0.7.10 layout contract:
21
+ * THREE independent multitasking sections (spec section 1): the File
22
+ Pane, the Chat Area (with its own composer INSIDE its column), and
23
+ the Code/Preview pane each own their geometry — the chat box is
24
+ physically unable to overlap the editor or preview because they are
25
+ siblings of separate columns, never overlays.
26
+ * The right pane preserves its width across Code⇄Preview switches and
27
+ is horizontally resizable (RightPaneResizeHandle, persisted).
28
+ * Both halves of the right pane stay MOUNTED at all times — switching
29
+ modes only flips `display`, so editor tabs/cursor/unsaved state and
30
+ preview navigation/history all survive every switch.
31
+ * Closing the last editor tab never touches Web Preview (the server/
32
+ browser belong to the workspace session, not a tab).
33
+
34
+ Narrow terminals fall back to the historical stacked behavior: a
35
+ Chat/Files switcher row shows exactly one section at a time.
36
+ """
37
+
38
+ if __name__ == "__main__":
39
+ print("This is a library file and is not meant to be run directly.")
40
+ import sys
41
+ sys.exit(1)
42
+
43
+ import os
44
+
45
+ from .sidebar import Explorer
46
+ from .editor import EditorPane
47
+ from .preview_panel import PreviewPanel
48
+ from .resizers import (ExplorerResizeHandle, ComposerResizeHandle,
49
+ RightPaneResizeHandle, SplitPreviewResizer,
50
+ load_layout, save_layout,
51
+ EXPLORER_DEFAULT_WIDTH, RIGHTPANE_DEFAULT_WIDTH)
52
+
53
+ TEXTUAL_AVAILABLE = True
54
+ try:
55
+ from ..browser import WorkspaceMode
56
+ from textual.containers import Horizontal, Vertical
57
+ from textual.widgets import Button, Static
58
+ except Exception:
59
+ TEXTUAL_AVAILABLE = False
60
+
61
+
62
+ if TEXTUAL_AVAILABLE:
63
+
64
+ class WorkspaceShell(Horizontal):
65
+ """Explorer (left) + adaptive main area (right) that keeps the
66
+ chat, the editor AND the web preview mounted at all times."""
67
+
68
+ # main-area width at which the chat and the right pane sit side
69
+ # by side instead of stacking behind the switcher. Textual CSS
70
+ # has no media queries, so this is decided in Python on Resize.
71
+ SPLIT_MIN_WIDTH = 68
72
+
73
+ def __init__(self, conversation_view, composer=None, id="cct-workspace"):
74
+ super().__init__(id=id)
75
+ self._conversation_view = conversation_view
76
+ self._composer = composer
77
+ self.workspace_root = None
78
+ self._files_count = 0
79
+ self._showing = "chat" # "chat" | "files" (stack mode only)
80
+ self._mode = WorkspaceMode.CODE
81
+ self._fullscreen = False
82
+ self._fullscreen_restore = None
83
+
84
+ def compose(self):
85
+ yield Explorer()
86
+ yield ExplorerResizeHandle(self)
87
+ with Vertical(id="cct-workspace-main"):
88
+ with Horizontal(id="cct-workspace-switcher"):
89
+ yield Button("\u25b8", id="cct-sidebar-expand-btn", classes="cct-switch", tooltip="Open Sidebar")
90
+ yield Button("Chat", id="cct-switch-chat", classes="cct-switch")
91
+ yield Button("Files", id="cct-switch-files", classes="cct-switch")
92
+ with Horizontal(id="cct-workspace-body"):
93
+ with Vertical(id="cct-chat-col"):
94
+ with Horizontal(id="cct-chat-nav"):
95
+ yield Button("▸ Explorer", id="cct-chat-sidebar-toggle", classes="cct-chat-nav-btn", tooltip="Open Sidebar (Ctrl+B)")
96
+ yield self._conversation_view
97
+ if self._composer is not None:
98
+ yield ComposerResizeHandle(self)
99
+ yield self._composer
100
+ yield RightPaneResizeHandle(self)
101
+ with Vertical(id="cct-right-pane"):
102
+ # In split mode these become horizontal side-by-side with resizer between
103
+ yield EditorPane()
104
+ yield SplitPreviewResizer(self)
105
+ yield PreviewPanel()
106
+
107
+ def on_mount(self):
108
+ # No folder open yet: the Explorer stays hidden so the app
109
+ # looks like a clean chat until the first Open Folder.
110
+ self.explorer.display = False
111
+ self.preview_panel.display = False
112
+ try:
113
+ saved = load_layout()
114
+ w = saved.get("explorer_width")
115
+ if w:
116
+ self.explorer.set_width(w)
117
+ rw = saved.get("rightpane_width")
118
+ if rw:
119
+ self.set_right_width(rw)
120
+ except Exception:
121
+ pass
122
+ self.sync_resizer()
123
+ self.sync_right_resizer()
124
+ self._relayout()
125
+ try:
126
+ self.apply_custom_layout()
127
+ except Exception:
128
+ pass
129
+ self._sync_sidebar_toggle()
130
+
131
+ # ------------------------------------------------- properties --
132
+ @property
133
+ def fullscreen(self):
134
+ return self._fullscreen
135
+
136
+ @property
137
+ def mode(self) -> "WorkspaceMode":
138
+ return self._mode
139
+
140
+ @property
141
+ def explorer(self):
142
+ try:
143
+ return self.query_one(Explorer)
144
+ except Exception:
145
+ return None
146
+
147
+ @property
148
+ def editor(self):
149
+ try:
150
+ return self.query_one(EditorPane)
151
+ except Exception:
152
+ return None
153
+
154
+ @property
155
+ def preview_panel(self):
156
+ try:
157
+ return self.query_one(PreviewPanel)
158
+ except Exception:
159
+ return None
160
+
161
+ @property
162
+ def right_pane(self):
163
+ """The #cct-right-pane container (width owner for resizing),
164
+ or None before mount."""
165
+ try:
166
+ return self.query_one("#cct-right-pane", Vertical)
167
+ except Exception:
168
+ return None
169
+
170
+ @property
171
+ def chat_column(self):
172
+ try:
173
+ return self.query_one("#cct-chat-col", Vertical)
174
+ except Exception:
175
+ return None
176
+
177
+ # ------------------------------------------------- resize handles --
178
+ def sync_resizer(self):
179
+ """The explorer-divider must exist exactly when the explorer
180
+ does — hidden together, never left floating in between."""
181
+ try:
182
+ handle = self.query_one(ExplorerResizeHandle)
183
+ except Exception:
184
+ return
185
+ visible = self.explorer.display and self.explorer.width > 0
186
+ handle.display = visible
187
+
188
+ def sync_right_resizer(self):
189
+ """The right-divider exists only while the right pane is a
190
+ real, visible column (split mode, not fullscreen)."""
191
+ try:
192
+ handle = self.query_one(RightPaneResizeHandle)
193
+ pane = self.right_pane
194
+ except Exception:
195
+ return
196
+ if pane is None:
197
+ handle.display = False
198
+ return
199
+ handle.display = bool(
200
+ pane.display and not self._fullscreen
201
+ and self.chat_visible)
202
+
203
+ @property
204
+ def chat_visible(self):
205
+ chat = self._conversation_view
206
+ return bool(chat.display) if chat is not None else False
207
+
208
+ def on_sidebar_toggled(self, event):
209
+ """Explorer collapse/expand crossed through here on its way
210
+ up to CCTApp — keep the divider in lockstep."""
211
+ self.sync_resizer()
212
+ self._relayout()
213
+ self._sync_sidebar_toggle()
214
+
215
+ def persist_explorer_width(self):
216
+ save_layout(explorer_width=self.explorer.width or None)
217
+
218
+ def on_resize(self):
219
+ # Resize storms (rapid terminal drags) are coalesced by a
220
+ # leading-edge + trailing debouncer: the first event lays
221
+ # out immediately (interactive feel preserved), the burst
222
+ # settles into one final layout instead of N.
223
+ try:
224
+ from . import design_system
225
+ deb = getattr(self, "_relayout_debouncer", None)
226
+ if deb is None or not isinstance(
227
+ deb, design_system.RelayoutDebouncer):
228
+ deb = design_system.RelayoutDebouncer(delay=0.06)
229
+ self._relayout_debouncer = deb
230
+ deb.request(self, self._do_resize_layout)
231
+ except Exception:
232
+ self._do_resize_layout()
233
+
234
+ def _do_resize_layout(self):
235
+ # While the right pane is fullscreen the split/stack logic
236
+ # must not fight the expanded layout.
237
+ self._clamp_explorer_to_window()
238
+ if not self._fullscreen:
239
+ self._relayout()
240
+ self._sync_sidebar_toggle()
241
+
242
+ def _clamp_explorer_to_window(self):
243
+ """Window got smaller? Shrink the explorer proportionally so
244
+ the chat pane always keeps a usable width."""
245
+ try:
246
+ from .resizers import EXPLORER_MIN_WIDTH, EXPLORER_MAX_FRACTION
247
+ explorer = self.explorer
248
+ if not explorer.display or explorer.width <= 0:
249
+ return
250
+ ceiling = max(EXPLORER_MIN_WIDTH + 4,
251
+ int(self.size.width * EXPLORER_MAX_FRACTION))
252
+ if explorer.width > ceiling:
253
+ explorer.set_width(ceiling)
254
+ self.sync_resizer()
255
+ except Exception:
256
+ pass
257
+
258
+ # ------------------------------------------- right pane mode --
259
+ def set_right_mode(self, mode: "WorkspaceMode"):
260
+ """CODE ⇄ PREVIEW ⇄ SPLIT switch. Pure display flips: the editor
261
+ keeps its tabs/state, the preview keeps navigation/history,
262
+ and the right pane's WIDTH never changes across switches.
263
+ SPLIT shows both side-by-side, resizable."""
264
+ if isinstance(mode, str):
265
+ low = mode.lower()
266
+ if low.startswith("split"):
267
+ mode = WorkspaceMode.SPLIT
268
+ elif low.startswith("prev"):
269
+ mode = WorkspaceMode.PREVIEW
270
+ else:
271
+ mode = WorkspaceMode.CODE
272
+ self._mode = mode
273
+ editor = self.editor
274
+ panel = self.preview_panel
275
+ right = self.right_pane
276
+ if editor is not None:
277
+ if mode is WorkspaceMode.SPLIT:
278
+ editor.display = True
279
+ else:
280
+ editor.display = (mode is WorkspaceMode.CODE)
281
+ if panel is not None:
282
+ if mode is WorkspaceMode.SPLIT:
283
+ panel.display = True
284
+ else:
285
+ panel.display = (mode is WorkspaceMode.PREVIEW)
286
+ # Split layout: horizontal side-by-side inside right pane
287
+ # Handle split resizer visibility
288
+ try:
289
+ from .resizers import SplitPreviewResizer
290
+ resizer = self.query_one(SplitPreviewResizer)
291
+ resizer.display = (mode is WorkspaceMode.SPLIT and not self._fullscreen)
292
+ except Exception:
293
+ pass
294
+ if right is not None:
295
+ try:
296
+ if mode is WorkspaceMode.SPLIT:
297
+ right.styles.layout = "horizontal"
298
+ # Editor and preview each take half
299
+ if editor is not None:
300
+ editor.styles.width = "1fr"
301
+ editor.styles.height = "100%"
302
+ if panel is not None:
303
+ panel.styles.width = "1fr"
304
+ panel.styles.height = "100%"
305
+ panel.styles.border_left = "none"
306
+ else:
307
+ right.styles.layout = "vertical"
308
+ if editor is not None:
309
+ editor.styles.width = "100%"
310
+ editor.styles.height = "1fr" if mode is WorkspaceMode.CODE else "100%"
311
+ if panel is not None:
312
+ panel.styles.width = "100%"
313
+ panel.styles.height = "1fr" if mode is WorkspaceMode.PREVIEW else "100%"
314
+ panel.styles.border_left = "none"
315
+ except Exception:
316
+ pass
317
+ # In stacked narrow mode the Files entry means the right
318
+ # pane whichever half it currently shows.
319
+ if not self._fullscreen:
320
+ self._relayout()
321
+
322
+ # -------------------------------------------- fullscreen pane --
323
+ def set_fullscreen(self, active):
324
+ """⿻ — expands whatever the right pane currently shows
325
+ (Code OR Web Preview) to the whole viewport. Nothing is
326
+ destroyed: Explorer/chat are display-hidden and the exact
327
+ previous configuration is restored on exit."""
328
+ try:
329
+ main = self.query_one("#cct-workspace-main", Vertical)
330
+ switcher = self.query_one("#cct-workspace-switcher")
331
+ except Exception:
332
+ return
333
+ right = self.right_pane
334
+ if right is None:
335
+ return
336
+ chat_col = self.chat_column
337
+ explorer = self.explorer
338
+ if active:
339
+ if self._fullscreen:
340
+ return
341
+ self._fullscreen = True
342
+ self._fullscreen_restore = {
343
+ "explorer": explorer.display,
344
+ "showing": self._showing,
345
+ "chat_display": chat_col.display if chat_col else True,
346
+ "right_width": right.size.width or None,
347
+ }
348
+ explorer.display = False
349
+ self.sync_resizer()
350
+ switcher.display = False
351
+ if chat_col is not None:
352
+ chat_col.display = False
353
+ try:
354
+ self.query_one(RightPaneResizeHandle).display = False
355
+ except Exception:
356
+ pass
357
+ right.styles.width = "1fr"
358
+ main.styles.width = "1fr"
359
+ else:
360
+ if not self._fullscreen:
361
+ return
362
+ self._fullscreen = False
363
+ restore = self._fullscreen_restore or {}
364
+ explorer.display = bool(restore.get("explorer"))
365
+ self._showing = restore.get("showing", "chat")
366
+ main.styles.width = "1fr"
367
+ if chat_col is not None:
368
+ chat_col.display = True
369
+ saved_w = restore.get("right_width")
370
+ right.styles.width = int(saved_w) if saved_w \
371
+ else RIGHTPANE_DEFAULT_WIDTH
372
+ self.sync_resizer()
373
+ self.sync_right_resizer()
374
+ self._relayout()
375
+
376
+ # --------------------------------------------- adaptive layout --
377
+ def _relayout(self):
378
+ """Split vs. stacked vs. chat-only, driven by the width of
379
+ the main area and whether there is anything to show in the
380
+ right pane (editor tabs or an active preview)."""
381
+ try:
382
+ main = self.query_one("#cct-workspace-main", Vertical)
383
+ switcher = self.query_one("#cct-workspace-switcher")
384
+ except Exception:
385
+ return # resize can fire before compose() lands
386
+ right = self.right_pane
387
+ chat_col = self.chat_column
388
+ if right is None or chat_col is None:
389
+ return
390
+ n = self._files_count
391
+ has_preview = self._mode in (WorkspaceMode.PREVIEW, WorkspaceMode.SPLIT)
392
+ has_files = n > 0 or (self.editor is not None and bool(getattr(self.editor, "_open_paths", {})))
393
+ explorer_w = self.explorer.width if self.explorer.display else 0
394
+ usable = max(0, self.size.width - explorer_w)
395
+ split_possible = usable >= self.SPLIT_MIN_WIDTH
396
+
397
+ editor_disabled = False
398
+ try:
399
+ from .. import customization as _cust
400
+ if _cust.is_active():
401
+ cust_mgr = _cust.get_manager()
402
+ ed_cfg = cust_mgr.get_component("code_editor") or cust_mgr.get_component("editor") or {}
403
+ if not ed_cfg.get("visible", True):
404
+ editor_disabled = True
405
+ except Exception:
406
+ pass
407
+
408
+ # Responsive tier (design_system breakpoints): drives the
409
+ # .cat-bp-* CSS classes that strip secondary chrome on
410
+ # narrow/short terminals. Display flags below stay fully
411
+ # owned by _relayout (recomputed every resize), so user
412
+ # state (explorer visibility, _showing) is never mutated.
413
+ tiny = False
414
+ try:
415
+ from . import design_system
416
+ bp = design_system.breakpoint_for(
417
+ self.size.width, self.size.height)
418
+ for _cls in ("cat-bp-large", "cat-bp-medium",
419
+ "cat-bp-small", "cat-bp-tiny"):
420
+ self.set_class(bp == _cls[7:], _cls)
421
+ try:
422
+ short = (self.size.height or 24) <= 20
423
+ except Exception:
424
+ short = False
425
+ self.set_class(short, "cat-short")
426
+ tiny = (bp == "tiny")
427
+ except Exception:
428
+ pass
429
+
430
+ if (not has_files and not has_preview) or editor_disabled:
431
+ # Nothing to show on the right or editor disabled: chat alone, full width.
432
+ right.display = False
433
+ self.sync_right_resizer()
434
+ switcher.display = False
435
+ chat_col.display = True
436
+ chat_col.styles.width = "1fr"
437
+ self._sync_switcher()
438
+ self._sync_sidebar_toggle()
439
+ return
440
+
441
+ if tiny:
442
+ # Extremely small terminal: clean chat-first fallback.
443
+ # No switcher, no right pane, no overflow — the chat
444
+ # column (min-width 24, overflow hidden) always fits.
445
+ switcher.display = False
446
+ chat_col.display = True
447
+ right.display = False
448
+ chat_col.styles.width = "1fr"
449
+ self.sync_right_resizer()
450
+ self._sync_switcher()
451
+ self._sync_sidebar_toggle()
452
+ return
453
+
454
+ if split_possible:
455
+ # Both columns visible, side by side.
456
+ switcher.display = False
457
+ chat_col.display = True
458
+ right.display = True
459
+ chat_col.styles.width = "1fr"
460
+ else:
461
+ # Stacked: switcher on top, exactly one section below.
462
+ switcher.display = True
463
+ chat_col.styles.width = "1fr"
464
+ chat_col.display = self._showing == "chat"
465
+ right.display = self._showing == "files"
466
+ self.sync_right_resizer()
467
+ self._sync_switcher()
468
+ self._sync_sidebar_toggle()
469
+ try:
470
+ exp_btn = self.query_one("#cct-sidebar-expand-btn", Button)
471
+ exp_btn.display = (not self.explorer.display or self.explorer.width == 0)
472
+ except Exception:
473
+ pass
474
+
475
+ def _sync_switcher(self):
476
+ """Highlights whichever of Chat/Files is currently shown in
477
+ stacked mode (CSS decides the exact look)."""
478
+ try:
479
+ self.query_one("#cct-switch-chat", Button).set_class(
480
+ self._showing == "chat", "-active")
481
+ self.query_one("#cct-switch-files", Button).set_class(
482
+ self._showing == "files", "-active")
483
+ except Exception:
484
+ pass
485
+
486
+ def on_editor_tabs_changed(self, event):
487
+ self._files_count = event.count
488
+ self._relayout()
489
+
490
+ def on_chat_requested(self, event):
491
+ """Esc in the editor (or any other ChatRequested sender):
492
+ bring the chat forward in stacked mode. The app handles
493
+ focusing the composer after this bubbles up to it."""
494
+ self._showing = "chat"
495
+ self._relayout()
496
+
497
+ def on_button_pressed(self, event):
498
+ if event.button.id == "cct-switch-chat":
499
+ self._showing = "chat"
500
+ self._relayout()
501
+ elif event.button.id == "cct-switch-files":
502
+ self._showing = "files"
503
+ self._relayout()
504
+ elif event.button.id in ("cct-sidebar-expand-btn", "cct-chat-sidebar-toggle"):
505
+ event.stop()
506
+ self.toggle_sidebar()
507
+ elif event.button.id == "cct-chat-clear-btn":
508
+ event.stop()
509
+ if hasattr(self.app, "_new_chat_session"):
510
+ self.app._new_chat_session()
511
+ elif hasattr(self.app, "conversation"):
512
+ self.app.conversation.clear()
513
+ elif event.button.id == "cct-chat-export-btn":
514
+ event.stop()
515
+ if hasattr(self.app, "_export_chat"):
516
+ self.app._export_chat()
517
+ elif event.button.id == "cct-chat-reload-btn":
518
+ event.stop()
519
+ if hasattr(self.app, "_retry_last_turn"):
520
+ self.app._retry_last_turn()
521
+
522
+ def toggle_sidebar(self):
523
+ if self.explorer:
524
+ if not self.explorer.display or self.explorer.width == 0:
525
+ self.explorer.display = True
526
+ self.explorer.set_width(32)
527
+ else:
528
+ self.explorer.toggle()
529
+ self.sync_resizer()
530
+ self._relayout()
531
+ self._sync_sidebar_toggle()
532
+
533
+ def _sync_sidebar_toggle(self):
534
+ try:
535
+ nav = self.query_one("#cct-chat-nav", Horizontal)
536
+ btn = self.query_one("#cct-chat-sidebar-toggle", Button)
537
+ collapsed = (not self.explorer.display or self.explorer.width == 0)
538
+ nav.display = collapsed
539
+ if collapsed:
540
+ btn.label = "▸ Explorer"
541
+ btn.tooltip = "Open Sidebar (Ctrl+B)"
542
+ except Exception:
543
+ pass
544
+
545
+ def close_workspace(self):
546
+ self.workspace_root = None
547
+ if self.explorer:
548
+ try:
549
+ self.explorer.close_workspace()
550
+ except Exception:
551
+ pass
552
+ self.sync_resizer()
553
+ self._relayout()
554
+ self._sync_sidebar_toggle()
555
+
556
+ # --------------------------------------------------------- API --
557
+ def set_right_width(self, width):
558
+ """Width owner for the CODE/PREVIEW pane (both halves share
559
+ one geometry, so switching modes can never resize it). The
560
+ CLAMPED value is remembered so persistence never depends on
561
+ a layout pass having landed."""
562
+ pane = self.right_pane
563
+ try:
564
+ from .resizers import (RIGHTPANE_MIN_WIDTH,
565
+ RIGHTPANE_MAX_FRACTION)
566
+ total = max(30, self.size.width)
567
+ ceiling = max(RIGHTPANE_MIN_WIDTH + 4,
568
+ int(total * RIGHTPANE_MAX_FRACTION))
569
+ width = max(RIGHTPANE_MIN_WIDTH, min(ceiling, int(width)))
570
+ except Exception:
571
+ pass
572
+ self._rightpane_width = width
573
+ if pane is not None:
574
+ pane.styles.width = width
575
+ # Stretching the editor can make chat too narrow → re-evaluate
576
+ # split vs stacked so dashboard never gets overlapped.
577
+ if not self._fullscreen:
578
+ try:
579
+ self._relayout()
580
+ except Exception:
581
+ pass
582
+
583
+ def persist_rightpane_width(self):
584
+ save_layout(rightpane_width=getattr(self, "_rightpane_width",
585
+ None))
586
+
587
+ def open_folder(self, path):
588
+ path = os.path.abspath(os.path.expanduser(path))
589
+ if not os.path.isdir(path):
590
+ return False
591
+ self.workspace_root = path
592
+ self.explorer.display = True
593
+ self.explorer.open_folder(path)
594
+ self.sync_resizer()
595
+ self._relayout()
596
+ self._sync_sidebar_toggle()
597
+ return True
598
+
599
+ def open_file(self, path):
600
+ """Open a file in the editor WITHOUT disturbing a running
601
+ Web Preview (spec section 11: tabs and preview are
602
+ independent). Only when the right pane is already in CODE
603
+ mode do we bring it forward in stacked layouts."""
604
+ editor = self.editor
605
+ if editor is None:
606
+ return False
607
+ if self._mode is WorkspaceMode.CODE and not self._fullscreen:
608
+ self._showing = "files"
609
+ self._relayout()
610
+ return editor.open_file(path)
611
+
612
+ def open_file_at(self, path, line=1, col=1):
613
+ """Open a file in the editor and navigate cursor to (line, col)."""
614
+ editor = self.editor
615
+ if editor is None:
616
+ return False
617
+ if self._mode is WorkspaceMode.CODE and not self._fullscreen:
618
+ self._showing = "files"
619
+ self._relayout()
620
+ if hasattr(editor, "open_file_at"):
621
+ return editor.open_file_at(path, line, col)
622
+ return editor.open_file(path)
623
+
624
+ def show_chat(self):
625
+ self._showing = "chat"
626
+ self._relayout()
627
+
628
+ def refresh_explorer(self, changed_paths=None):
629
+ exp = self.explorer
630
+ if exp:
631
+ try:
632
+ exp.refresh_tree()
633
+ except Exception:
634
+ pass
635
+ ed = self.editor
636
+ if ed:
637
+ for p in (changed_paths or []):
638
+ try:
639
+ ed.reload_if_open(p)
640
+ except Exception:
641
+ pass
642
+
643
+ def apply_custom_layout(self, layout_cfg=None):
644
+ """CAT Customization: apply positional, sizing, ordering, and visibility layout."""
645
+ try:
646
+ from .. import customization as _cust
647
+ active = _cust.is_active()
648
+ except Exception:
649
+ active = False
650
+
651
+ try:
652
+ main = self.query_one("#cct-workspace-main", Vertical)
653
+ body = self.query_one("#cct-workspace-body", Horizontal)
654
+ explorer = self.explorer
655
+ resizer = self.query_one(ExplorerResizeHandle)
656
+ chat_col = self.chat_column
657
+ right_pane = self.right_pane
658
+ right_resizer = self.query_one(RightPaneResizeHandle)
659
+ except Exception:
660
+ return
661
+
662
+ if not active:
663
+ # Customization is disabled or not installed: revert to default core layout
664
+ self.styles.layout = "horizontal"
665
+ body.styles.layout = "horizontal"
666
+ try:
667
+ self.move_child(explorer, before=main)
668
+ self.move_child(resizer, before=main)
669
+ except Exception:
670
+ pass
671
+ try:
672
+ body.move_child(chat_col, before=right_pane)
673
+ body.move_child(right_resizer, before=right_pane)
674
+ except Exception:
675
+ pass
676
+ if self.workspace_root is None:
677
+ explorer.display = False
678
+ chat_col.display = True
679
+ chat_col.styles.width = "1fr"
680
+ self.sync_resizer()
681
+ self.sync_right_resizer()
682
+ return
683
+
684
+ # Active customization layout
685
+ if layout_cfg is None:
686
+ try:
687
+ layout_cfg = _cust.get_manager().get_layout()
688
+ except Exception:
689
+ layout_cfg = {}
690
+
691
+ sidebar_cfg = layout_cfg.get("sidebar") or layout_cfg.get("file_explorer") or {}
692
+ chat_cfg = layout_cfg.get("chat", {})
693
+ editor_cfg = layout_cfg.get("editor") or layout_cfg.get("code_editor") or {}
694
+
695
+ # 1. SIDEBAR POSITION & SIZING & VISIBILITY (Sections 4, 8, 9)
696
+ sb_pos = str(sidebar_cfg.get("position", "left")).lower()
697
+ sb_vis = sidebar_cfg.get("visible", True)
698
+
699
+ # Sizing (Section 8)
700
+ sb_w = sidebar_cfg.get("width")
701
+ if sb_w is not None:
702
+ try:
703
+ if isinstance(sb_w, (int, float)):
704
+ explorer.set_width(int(sb_w))
705
+ elif isinstance(sb_w, str) and sb_w.endswith("%"):
706
+ pct = float(sb_w[:-1].strip())
707
+ total_w = max(40, self.size.width or 100)
708
+ explorer.set_width(max(14, int(total_w * pct / 100)))
709
+ elif isinstance(sb_w, str) and sb_w.isdigit():
710
+ explorer.set_width(int(sb_w))
711
+ except Exception:
712
+ pass
713
+
714
+ sb_order = sidebar_cfg.get("order", 0)
715
+ chat_order = chat_cfg.get("order", 1)
716
+ editor_order = editor_cfg.get("order", 2)
717
+
718
+ # Position sidebar: left, right, top, bottom
719
+ if sb_pos == "right" or (sb_order > chat_order and sb_order > editor_order and sb_pos != "left"):
720
+ self.styles.layout = "horizontal"
721
+ try:
722
+ self.move_child(main, before=resizer)
723
+ self.move_child(main, before=explorer)
724
+ except Exception:
725
+ pass
726
+ elif sb_pos == "top":
727
+ self.styles.layout = "vertical"
728
+ try:
729
+ self.move_child(explorer, before=main)
730
+ self.move_child(resizer, before=main)
731
+ except Exception:
732
+ pass
733
+ elif sb_pos == "bottom":
734
+ self.styles.layout = "vertical"
735
+ try:
736
+ self.move_child(main, before=resizer)
737
+ self.move_child(main, before=explorer)
738
+ except Exception:
739
+ pass
740
+ else: # left (default)
741
+ self.styles.layout = "horizontal"
742
+ try:
743
+ self.move_child(explorer, before=main)
744
+ self.move_child(resizer, before=main)
745
+ except Exception:
746
+ pass
747
+
748
+ # Visibility: releases space if hidden
749
+ if not sb_vis:
750
+ explorer.display = False
751
+ resizer.display = False
752
+ else:
753
+ explorer.display = True
754
+ self.sync_resizer()
755
+
756
+ # 2. CHAT & CODE EDITOR POSITIONS & SIZING & VISIBILITY (Sections 5, 6, 7, 8, 9, 10)
757
+ chat_pos = str(chat_cfg.get("position", "center")).lower()
758
+ editor_pos = str(editor_cfg.get("position", "right")).lower()
759
+ chat_vis = chat_cfg.get("visible", True)
760
+ editor_vis = editor_cfg.get("visible", True)
761
+
762
+ # Sizes
763
+ chat_w = chat_cfg.get("width")
764
+ if chat_w is not None and str(chat_w).lower() not in ("auto", "1fr", "remaining"):
765
+ try:
766
+ if isinstance(chat_w, str) and chat_w.endswith("%"):
767
+ chat_col.styles.width = chat_w
768
+ else:
769
+ chat_col.styles.width = int(chat_w)
770
+ except Exception:
771
+ chat_col.styles.width = "1fr"
772
+ else:
773
+ chat_col.styles.width = "1fr"
774
+
775
+ editor_w = editor_cfg.get("width")
776
+ if editor_w is not None and str(editor_w).lower() not in ("auto", "1fr", "remaining"):
777
+ try:
778
+ if isinstance(editor_w, str) and editor_w.endswith("%"):
779
+ right_pane.styles.width = editor_w
780
+ else:
781
+ right_pane.styles.width = int(editor_w)
782
+ except Exception:
783
+ pass
784
+
785
+ # Ordering / Position swap between Chat and Editor:
786
+ if (chat_pos == "right" and editor_pos in ("left", "center")) or (editor_pos == "left" and chat_pos != "left") or (editor_order < chat_order and chat_pos != "left"):
787
+ body.styles.layout = "horizontal"
788
+ try:
789
+ body.move_child(right_pane, before=chat_col)
790
+ body.move_child(right_resizer, before=chat_col)
791
+ except Exception:
792
+ pass
793
+ elif chat_pos == "top" and editor_pos == "bottom":
794
+ body.styles.layout = "vertical"
795
+ try:
796
+ body.move_child(chat_col, before=right_pane)
797
+ body.move_child(right_resizer, before=right_pane)
798
+ except Exception:
799
+ pass
800
+ elif chat_pos == "bottom" and editor_pos == "top":
801
+ body.styles.layout = "vertical"
802
+ try:
803
+ body.move_child(right_pane, before=chat_col)
804
+ body.move_child(right_resizer, before=chat_col)
805
+ except Exception:
806
+ pass
807
+ else: # Chat on left / center, editor on right
808
+ body.styles.layout = "horizontal"
809
+ try:
810
+ body.move_child(chat_col, before=right_pane)
811
+ body.move_child(right_resizer, before=right_pane)
812
+ except Exception:
813
+ pass
814
+
815
+ # Visibility: releases space if hidden
816
+ chat_col.display = bool(chat_vis)
817
+ has_preview = getattr(self, "_mode", None) in (WorkspaceMode.PREVIEW, WorkspaceMode.SPLIT)
818
+ has_files = getattr(self, "_files_count", 0) > 0 or (self.editor is not None and bool(getattr(self.editor, "_open_paths", {})))
819
+ if not editor_vis or (not has_files and not has_preview):
820
+ right_pane.display = False
821
+ right_resizer.display = False
822
+ else:
823
+ right_pane.display = True
824
+ self.sync_right_resizer()
825
+
826
+ self.sync_resizer()
827
+ self.sync_right_resizer()
828
+ self._sync_sidebar_toggle()
829
+
830
+ else:
831
+ WorkspaceShell = None