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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- 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
|