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,1034 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT UI — header.py: the persistent NavBar docked at the very top of the
|
|
3
|
+
screen (v0.7.4 "AI-native IDE navigation" redesign).
|
|
4
|
+
|
|
5
|
+
Three sections, and only three (spec's own words: "The navigation bar
|
|
6
|
+
must contain ONLY three sections"):
|
|
7
|
+
|
|
8
|
+
LEFT animated menu button (MenuButton) + the CCT mark (Logo)
|
|
9
|
+
CENTER the Permission pill (PermissionPill) — a real button, not a
|
|
10
|
+
form dropdown, with no triangle glyph
|
|
11
|
+
RIGHT the current workspace path, and nothing else — no GPU/API/
|
|
12
|
+
clock cluster (those used to live in `refresh_right`; that
|
|
13
|
+
method's contract is now "one path string", see below)
|
|
14
|
+
|
|
15
|
+
Opening the menu button no longer silently toggles the Explorer (that
|
|
16
|
+
surprised people who had no folder open yet, and conflated "browse the
|
|
17
|
+
IDE's own features" with "browse this project's files" — two different
|
|
18
|
+
jobs). It now opens NavPanel: a real, keyboard-navigable list of every
|
|
19
|
+
top-level destination in the app (Dashboard, Open Folder, Recent
|
|
20
|
+
Workspaces, MCP Servers, Personalize, Diff Viewer, Settings, Themes,
|
|
21
|
+
Extensions, Keyboard Shortcuts, Help). Only "Open Folder" reaches for
|
|
22
|
+
the system folder picker; everything else routes through
|
|
23
|
+
`events.NavAction` to CCTApp.on_nav_action, which is the one place
|
|
24
|
+
allowed to decide what each destination actually does — same one-way
|
|
25
|
+
event-flow convention every other component in ui/ already follows.
|
|
26
|
+
|
|
27
|
+
"Open Folder" and "Recent Workspaces" (the two destinations that end
|
|
28
|
+
in a system folder picker) KEEP the panel open when picked — the main
|
|
29
|
+
menu stays up after a folder is opened, per v0.7.4 — while every other
|
|
30
|
+
item dismisses as usual. The menu button's ✕ morph doubles as a close
|
|
31
|
+
control: clicking it while the panel is open closes the menu.
|
|
32
|
+
|
|
33
|
+
Explorer's own show/hide toggle still exists — it's just reached via
|
|
34
|
+
Ctrl+B (CCTApp.action_toggle_sidebar) or a NavPanel item, not by
|
|
35
|
+
re-purposing the header's menu glyph for it.
|
|
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 .. import permissions as perm
|
|
46
|
+
from .events import PermissionModeChanged, NavAction
|
|
47
|
+
|
|
48
|
+
TEXTUAL_AVAILABLE = True
|
|
49
|
+
try:
|
|
50
|
+
from textual.containers import Vertical, Horizontal
|
|
51
|
+
from textual.screen import Screen
|
|
52
|
+
from textual.widgets import Static, Button
|
|
53
|
+
except Exception:
|
|
54
|
+
TEXTUAL_AVAILABLE = False
|
|
55
|
+
|
|
56
|
+
_MARK = "\u2b21" # WHITE HEXAGON — a molecule/benzene-ring silhouette,
|
|
57
|
+
# a cleaner and more legible brand mark at 1-cell size
|
|
58
|
+
# than the previous alembic glyph.
|
|
59
|
+
|
|
60
|
+
# spec v0.7 Header section: three selectable modes.
|
|
61
|
+
_MODE_ORDER = ("ask", "restricted", "full")
|
|
62
|
+
|
|
63
|
+
_MODE_DESCRIPTIONS = {
|
|
64
|
+
"ask": "Ask me before protected actions.",
|
|
65
|
+
"restricted": "Analyze and help, but block protected actions.",
|
|
66
|
+
"full": "Work automatically within my allowed workspace.",
|
|
67
|
+
}
|
|
68
|
+
_MODE_TONE = {"ask": "text", "restricted": "warning", "full": "success"}
|
|
69
|
+
|
|
70
|
+
# Permission mode icons for the selector
|
|
71
|
+
_MODE_ICONS = {"ask": "\U0001f510", "restricted": "\U0001f6e1", "full": "\u26a1"}
|
|
72
|
+
|
|
73
|
+
# NavPanel items: (action id, icon, label). v0.7.8.1 cleanup: "New
|
|
74
|
+
# Workspace" / "Save Workspace" / "Close Workspace" are gone — opening a
|
|
75
|
+
# folder already records it in Recent Workspaces automatically, so those
|
|
76
|
+
# three were redundant in a terminal IDE. The menu is now exactly:
|
|
77
|
+
# Open Folder / Recent Workspaces / Chats, then Integrations, then
|
|
78
|
+
# Settings / Account section.
|
|
79
|
+
# v0.7.9.10: Gestures / Vision / Personalize are Extensions — they appear
|
|
80
|
+
# in the main menu only when enabled via Extensions panel (see
|
|
81
|
+
# extensions.should_show_in_menu). This keeps the main menu uncluttered
|
|
82
|
+
# and lets users install/uninstall via Extensions.
|
|
83
|
+
_NAV_ITEMS_BASE = (
|
|
84
|
+
("open_folder", "\U0001f4c1", "Open Folder"),
|
|
85
|
+
("recent_workspaces", "\U0001f553", "Recent Workspaces"),
|
|
86
|
+
("chats", "\U0001f4ac", "Chats"),
|
|
87
|
+
("sep1", "", ""),
|
|
88
|
+
("mcp_servers", "\U0001f50c", "MCP Servers"),
|
|
89
|
+
("backup_providers", "\U0001f4e1", "Backup Providers"),
|
|
90
|
+
("sep2", "", ""),
|
|
91
|
+
("customize_ai", "🎨", "Personalize"),
|
|
92
|
+
("themes", "\u25d1", "Themes"),
|
|
93
|
+
("gestures", "\u270b", "Gestures"),
|
|
94
|
+
("extensions", "\u25a3", "Extensions"),
|
|
95
|
+
("sep3", "", ""),
|
|
96
|
+
("user", "\u263a", "Users"),
|
|
97
|
+
("signout", "\u238b", "Sign Out"),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def get_nav_items():
|
|
101
|
+
"""Filtered NAV_ITEMS for current extensions state.
|
|
102
|
+
|
|
103
|
+
v0.7.9.12: Gestures / CAT Vision / Personalize are now grouped in the
|
|
104
|
+
dedicated 'Downloaded Extensions' section at the bottom of the main
|
|
105
|
+
menu instead of being scattered in the top list. This removes the
|
|
106
|
+
previous duplication where they appeared both in the top scattered
|
|
107
|
+
positions AND again in Downloaded (2×). Install → appears in
|
|
108
|
+
Downloaded as '●', disable → stays in Downloaded as '○' (still
|
|
109
|
+
reachable to re-enable), uninstall → removed. Other items use
|
|
110
|
+
should_show_in_menu filtering as before.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
from .. import extensions as _ext
|
|
114
|
+
# --- base items (excluding builtin extensions — they are grouped) ---
|
|
115
|
+
# Builtin extension actions are handled exclusively in the Downloaded
|
|
116
|
+
# section to avoid scattering + duplication. Compute dynamically so
|
|
117
|
+
# future builtins are also grouped without hard-coding.
|
|
118
|
+
try:
|
|
119
|
+
builtin_actions = {v.get("action") for v in _ext.BUILTIN_EXTENSIONS.values() if v.get("action")}
|
|
120
|
+
except Exception:
|
|
121
|
+
builtin_actions = {"gestures", "vision", "customize_ai"}
|
|
122
|
+
out = []
|
|
123
|
+
for action, icon, label in _NAV_ITEMS_BASE:
|
|
124
|
+
if action.startswith("sep"):
|
|
125
|
+
out.append((action, icon, label))
|
|
126
|
+
continue
|
|
127
|
+
if action in builtin_actions:
|
|
128
|
+
# grouped in Downloaded Extensions — skip scattered position
|
|
129
|
+
continue
|
|
130
|
+
if not _ext.should_show_in_menu(action):
|
|
131
|
+
continue
|
|
132
|
+
out.append((action, icon, label))
|
|
133
|
+
|
|
134
|
+
# Check customization main_menu settings (Section 14)
|
|
135
|
+
try:
|
|
136
|
+
from .. import customization as _cust
|
|
137
|
+
if _cust.is_active():
|
|
138
|
+
_mm = _cust.get_manager().get_main_menu()
|
|
139
|
+
_mm_vis = _mm.get("visibility", {})
|
|
140
|
+
_mm_order = _mm.get("order", [])
|
|
141
|
+
if _mm_vis:
|
|
142
|
+
out = [it for it in out if it[0].startswith("sep") or _mm_vis.get(it[0], True) is not False]
|
|
143
|
+
if _mm_order:
|
|
144
|
+
def _key(it):
|
|
145
|
+
act = it[0]
|
|
146
|
+
return _mm_order.index(act) if act in _mm_order else 999
|
|
147
|
+
seps = [it for it in out if it[0].startswith("sep")]
|
|
148
|
+
non_seps = [it for it in out if not it[0].startswith("sep")]
|
|
149
|
+
non_seps.sort(key=_key)
|
|
150
|
+
out = non_seps + seps
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# collapse consecutive seps and trim leading/trailing seps
|
|
155
|
+
filtered = []
|
|
156
|
+
prev_sep = True # treat start as sep to trim leading
|
|
157
|
+
for item in out:
|
|
158
|
+
is_sep = item[0].startswith("sep")
|
|
159
|
+
if is_sep and prev_sep:
|
|
160
|
+
continue
|
|
161
|
+
filtered.append(item)
|
|
162
|
+
prev_sep = is_sep
|
|
163
|
+
while filtered and filtered[-1][0].startswith("sep"):
|
|
164
|
+
filtered.pop()
|
|
165
|
+
|
|
166
|
+
# --- Downloaded Extensions section (v0.7.9.12) ---
|
|
167
|
+
# Grouped section at the bottom that always appears so users can
|
|
168
|
+
# see what is downloaded/installed and jump directly to it.
|
|
169
|
+
# Enabled builtins use their original action id (so nav_has checks
|
|
170
|
+
# still pass and clicking opens the panel directly); disabled
|
|
171
|
+
# builtins and all custom extensions use 'ext:<id>' with ○ hint
|
|
172
|
+
# (so disabled does not count as visible for should_show_in_menu
|
|
173
|
+
# tests but remains reachable to re-enable).
|
|
174
|
+
try:
|
|
175
|
+
installed = []
|
|
176
|
+
if hasattr(_ext, "list_installed"):
|
|
177
|
+
try:
|
|
178
|
+
installed = _ext.list_installed()
|
|
179
|
+
except Exception:
|
|
180
|
+
installed = []
|
|
181
|
+
if not installed:
|
|
182
|
+
try:
|
|
183
|
+
installed = [m for m in _ext.list_extensions() if m.get("installed")]
|
|
184
|
+
except Exception:
|
|
185
|
+
installed = []
|
|
186
|
+
try:
|
|
187
|
+
installed = sorted(installed, key=lambda m: (m.get("display_name") or m.get("name") or m.get("id", "")).lower())
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
dl_extra = []
|
|
192
|
+
if filtered and not filtered[-1][0].startswith("sep"):
|
|
193
|
+
dl_extra.append(("sep_dl", "", ""))
|
|
194
|
+
count = len(installed)
|
|
195
|
+
hdr_label = f"Downloaded Extensions ({count})" if count else "Downloaded Extensions"
|
|
196
|
+
dl_extra.append(("__section_dl", "📦", hdr_label))
|
|
197
|
+
if installed:
|
|
198
|
+
for m in installed:
|
|
199
|
+
eid = m.get("id")
|
|
200
|
+
if not eid:
|
|
201
|
+
continue
|
|
202
|
+
icon = m.get("icon", "▣")
|
|
203
|
+
name = m.get("display_name") or m.get("name") or eid
|
|
204
|
+
enabled = bool(m.get("enabled"))
|
|
205
|
+
# For builtins use original action when enabled (so
|
|
206
|
+
# main-menu tests and direct panel open still work),
|
|
207
|
+
# otherwise use ext: prefix with disabled hint.
|
|
208
|
+
builtin_meta = _ext.BUILTIN_EXTENSIONS.get(eid) if hasattr(_ext, "BUILTIN_EXTENSIONS") else None
|
|
209
|
+
if builtin_meta and enabled:
|
|
210
|
+
action_id = builtin_meta.get("action") or f"ext:{eid}"
|
|
211
|
+
# no suffix needed — enabled is the normal visible state
|
|
212
|
+
label = name
|
|
213
|
+
else:
|
|
214
|
+
suffix = " \u25cf" if enabled else " \u25cb"
|
|
215
|
+
max_len = 28 - len(suffix)
|
|
216
|
+
if len(name) > max_len:
|
|
217
|
+
name = name[:max_len - 1] + "\u2026"
|
|
218
|
+
label = f"{name}{suffix}"
|
|
219
|
+
action_id = f"ext:{eid}"
|
|
220
|
+
dl_extra.append((action_id, icon, label))
|
|
221
|
+
else:
|
|
222
|
+
dl_extra.append(("__placeholder_dl", "", "No extensions installed"))
|
|
223
|
+
dl_extra.append(("__hint_dl", "", "Install from Extensions \u2192"))
|
|
224
|
+
filtered = filtered + dl_extra
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
return tuple(filtered)
|
|
229
|
+
except Exception:
|
|
230
|
+
return _NAV_ITEMS_BASE
|
|
231
|
+
|
|
232
|
+
# Backward compat: NAV_ITEMS remains as filtered view for old importers
|
|
233
|
+
# that do `from header import NAV_ITEMS`. At import time extensions are
|
|
234
|
+
# all enabled (empty config), so this equals the filtered view anyway.
|
|
235
|
+
NAV_ITEMS = get_nav_items()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if TEXTUAL_AVAILABLE:
|
|
239
|
+
|
|
240
|
+
class _Clickable(Static):
|
|
241
|
+
"""A Static that reports clicks to a callback — the same minimal
|
|
242
|
+
pattern footer.py's `_Badge` already uses, kept local here so
|
|
243
|
+
header.py doesn't need to import from footer.py for one thing."""
|
|
244
|
+
|
|
245
|
+
def __init__(self, text, widget_id, on_click=None, classes=""):
|
|
246
|
+
super().__init__(text, id=widget_id, classes=classes)
|
|
247
|
+
self._on_click = on_click
|
|
248
|
+
|
|
249
|
+
def on_mouse_down(self, event):
|
|
250
|
+
self.add_class("-active")
|
|
251
|
+
|
|
252
|
+
def on_mouse_up(self, event):
|
|
253
|
+
self.remove_class("-active")
|
|
254
|
+
|
|
255
|
+
def on_leave(self, event):
|
|
256
|
+
self.remove_class("-active")
|
|
257
|
+
|
|
258
|
+
def on_blur(self, event):
|
|
259
|
+
self.remove_class("-active")
|
|
260
|
+
|
|
261
|
+
def on_click(self, event):
|
|
262
|
+
self.remove_class("-active")
|
|
263
|
+
if self._on_click:
|
|
264
|
+
self._on_click()
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------- nav panel --
|
|
267
|
+
class _NavRow(Static):
|
|
268
|
+
"""One row of NavPanel: icon + label. Hover is free via CSS
|
|
269
|
+
`:hover`; keyboard-driven highlight is applied explicitly via
|
|
270
|
+
the `selected` class from the parent screen (same split
|
|
271
|
+
PermissionModeMenu's `_PermMenuRow` already uses, since arrow-key
|
|
272
|
+
movement isn't a real mouse hover)."""
|
|
273
|
+
|
|
274
|
+
def __init__(self, action_id, icon, label, index):
|
|
275
|
+
super().__init__("", classes="cct-navmenu-row")
|
|
276
|
+
self.action_id = action_id
|
|
277
|
+
self._icon = icon
|
|
278
|
+
self._label = label
|
|
279
|
+
self.index = index
|
|
280
|
+
|
|
281
|
+
def on_mount(self):
|
|
282
|
+
self.redraw()
|
|
283
|
+
|
|
284
|
+
def redraw(self):
|
|
285
|
+
from . import theme_css
|
|
286
|
+
text_color = theme_css.current_hex("text")
|
|
287
|
+
self.update(f"[{text_color}]{self._icon} {self._label}[/]")
|
|
288
|
+
|
|
289
|
+
def on_click(self, event):
|
|
290
|
+
# v0.7.6 Patch 1: only act while the panel is still the
|
|
291
|
+
# active screen (see _PermMenuRow.on_click for the
|
|
292
|
+
# re-delivered-click explanation).
|
|
293
|
+
if self.app.screen is self.screen:
|
|
294
|
+
self.screen.on_row_picked(self.action_id)
|
|
295
|
+
|
|
296
|
+
class NavPanel(Screen):
|
|
297
|
+
"""The structured navigation panel opened by MenuButton (spec
|
|
298
|
+
v0.7.4 "PRIMARY GOALS #2"). Top-left-anchored so it visually
|
|
299
|
+
drops down from the menu button rather than reading as a
|
|
300
|
+
centered dialog — same real-Screen-push-as-modal pattern this
|
|
301
|
+
package already uses for PermissionModeMenu, _AttachPrompt, and
|
|
302
|
+
_PathPrompt.
|
|
303
|
+
|
|
304
|
+
v0.7.4's "Main Menu stays open after opening a folder": the two
|
|
305
|
+
destinations that lead to a system folder picker ('open_folder',
|
|
306
|
+
'recent_workspaces') keep the panel open and post NavAction
|
|
307
|
+
directly, so the user can immediately pick another destination
|
|
308
|
+
when the picker returns. Every other item dismisses the panel as
|
|
309
|
+
usual (the dismiss callback then posts NavAction).
|
|
310
|
+
|
|
311
|
+
v0.7.9.10: Gestures / Vision / Personalize are Extensions — this
|
|
312
|
+
panel now builds from get_nav_items() so disabled extensions are
|
|
313
|
+
hidden here until re-enabled via Extensions panel."""
|
|
314
|
+
|
|
315
|
+
# Destinations that keep the menu open instead of dismissing.
|
|
316
|
+
_KEEP_OPEN = {"open_folder", "recent_workspaces"}
|
|
317
|
+
|
|
318
|
+
CSS = """
|
|
319
|
+
NavPanel { align: left top; background: $app-background 0%; }
|
|
320
|
+
#cct-navmenu-box {
|
|
321
|
+
margin-top: 3; margin-left: 1;
|
|
322
|
+
width: 42; height: auto;
|
|
323
|
+
max-height: 88%;
|
|
324
|
+
background: $surface;
|
|
325
|
+
border-top: tall $surface-highlight;
|
|
326
|
+
border-left: tall $surface-highlight;
|
|
327
|
+
border-bottom: tall $surface-dark;
|
|
328
|
+
border-right: tall $surface-dark;
|
|
329
|
+
padding: 1 0;
|
|
330
|
+
opacity: 1; offset-y: 0;
|
|
331
|
+
overflow-y: auto;
|
|
332
|
+
scrollbar-gutter: stable;
|
|
333
|
+
scrollbar-size: 1 1;
|
|
334
|
+
scrollbar-color: $border $surface;
|
|
335
|
+
}
|
|
336
|
+
#cct-navmenu-box.open { opacity: 1; offset-y: 0; }
|
|
337
|
+
.cct-navmenu-row {
|
|
338
|
+
height: 1; padding: 0 2;
|
|
339
|
+
background: $surface;
|
|
340
|
+
color: $text;
|
|
341
|
+
border-left: thick $surface;
|
|
342
|
+
border-right: thick $surface;
|
|
343
|
+
}
|
|
344
|
+
.cct-navmenu-row:hover {
|
|
345
|
+
background: $surface-alt;
|
|
346
|
+
color: $text;
|
|
347
|
+
border-left: thick $accent;
|
|
348
|
+
border-right: thick $surface-alt;
|
|
349
|
+
text-style: none;
|
|
350
|
+
}
|
|
351
|
+
.cct-navmenu-row-selected {
|
|
352
|
+
background: $surface-highlight;
|
|
353
|
+
color: $text;
|
|
354
|
+
border-left: thick $accent;
|
|
355
|
+
border-right: thick $surface-highlight;
|
|
356
|
+
text-style: none;
|
|
357
|
+
}
|
|
358
|
+
.cct-navmenu-sep {
|
|
359
|
+
height: 1; background: $surface; color: $border; padding: 0 2;
|
|
360
|
+
border-left: thick $surface; border-right: thick $surface;
|
|
361
|
+
}
|
|
362
|
+
.cct-navmenu-section {
|
|
363
|
+
height: 1; color: $accent; text-style: bold; padding: 0 2;
|
|
364
|
+
background: $surface-alt;
|
|
365
|
+
border-bottom: solid $border;
|
|
366
|
+
border-left: thick $surface-alt; border-right: thick $surface-alt;
|
|
367
|
+
}
|
|
368
|
+
.cct-navmenu-placeholder {
|
|
369
|
+
height: 1; background: $surface; color: $text-muted; padding: 0 2; text-style: italic;
|
|
370
|
+
border-left: thick $surface; border-right: thick $surface;
|
|
371
|
+
}
|
|
372
|
+
.cct-navmenu-hint {
|
|
373
|
+
height: 1; background: $surface; color: $text-faint; padding: 0 2;
|
|
374
|
+
border-left: thick $surface; border-right: thick $surface;
|
|
375
|
+
}
|
|
376
|
+
.cct-touch-mode .cct-navmenu-row { min-height: 2; height: 2; content-align: left middle; }
|
|
377
|
+
.cct-touch-mode .cct-navmenu-row:hover,
|
|
378
|
+
.cct-touch-mode .cct-navmenu-row:focus {
|
|
379
|
+
background: $surface-alt; color: $text;
|
|
380
|
+
border-left: thick $accent; border-right: thick $surface-alt;
|
|
381
|
+
}
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
def __init__(self):
|
|
385
|
+
super().__init__()
|
|
386
|
+
self._selected = None # action_id of selected row
|
|
387
|
+
self._nav_cache = None
|
|
388
|
+
|
|
389
|
+
def _get_filtered(self):
|
|
390
|
+
try:
|
|
391
|
+
return get_nav_items()
|
|
392
|
+
except Exception:
|
|
393
|
+
return _NAV_ITEMS_BASE
|
|
394
|
+
|
|
395
|
+
def compose(self):
|
|
396
|
+
items = self._get_filtered()
|
|
397
|
+
self._nav_cache = items
|
|
398
|
+
# No default forced text selection on open; keyboard navigation activates it on demand
|
|
399
|
+
with Vertical(id="cct-navmenu-box"):
|
|
400
|
+
for i, (action_id, icon, label) in enumerate(items):
|
|
401
|
+
if action_id.startswith("sep"):
|
|
402
|
+
yield Static("\u2500" * 30, id=f"cct-navmenu-sep-{i}",
|
|
403
|
+
classes="cct-navmenu-sep")
|
|
404
|
+
elif action_id.startswith("__section"):
|
|
405
|
+
yield Static(f"{icon} {label}", id=f"cct-navmenu-section-{i}",
|
|
406
|
+
classes="cct-navmenu-section")
|
|
407
|
+
elif action_id.startswith("__placeholder") or action_id.startswith("__hint"):
|
|
408
|
+
cls = "cct-navmenu-placeholder" if "placeholder" in action_id else "cct-navmenu-hint"
|
|
409
|
+
yield Static(label, id=f"cct-navmenu-placeholder-{i}", classes=cls)
|
|
410
|
+
else:
|
|
411
|
+
yield _NavRow(action_id, icon, label, i)
|
|
412
|
+
|
|
413
|
+
def on_mount(self):
|
|
414
|
+
self._sync_selected_class()
|
|
415
|
+
try:
|
|
416
|
+
self.query_one("#cct-navmenu-box").add_class("open")
|
|
417
|
+
except Exception:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
def _sync_selected_class(self):
|
|
421
|
+
for row in self.query(_NavRow):
|
|
422
|
+
row.set_class(row.action_id == self._selected, "cct-navmenu-row-selected")
|
|
423
|
+
|
|
424
|
+
def on_row_picked(self, action_id):
|
|
425
|
+
"""Shared by mouse clicks and the Enter key. Keep-open
|
|
426
|
+
destinations post NavAction and leave the panel up; the
|
|
427
|
+
rest dismiss, which triggers the usual push callback."""
|
|
428
|
+
if action_id in self._KEEP_OPEN:
|
|
429
|
+
self.post_message(NavAction(action_id))
|
|
430
|
+
return
|
|
431
|
+
self.dismiss(action_id)
|
|
432
|
+
|
|
433
|
+
def _nav_items(self):
|
|
434
|
+
"""Return only the selectable (non-separator) filtered items."""
|
|
435
|
+
items = self._get_filtered()
|
|
436
|
+
return [(i, a, ic, l) for i, (a, ic, l) in enumerate(items)
|
|
437
|
+
if not a.startswith("sep") and not a.startswith("__")]
|
|
438
|
+
|
|
439
|
+
def on_key(self, event):
|
|
440
|
+
items = self._nav_items()
|
|
441
|
+
if not items:
|
|
442
|
+
return
|
|
443
|
+
cur_ids = [a for _, a, _, _ in items]
|
|
444
|
+
if event.key == "escape":
|
|
445
|
+
self.dismiss(None)
|
|
446
|
+
elif event.key in ("down", "tab"):
|
|
447
|
+
if self._selected not in cur_ids:
|
|
448
|
+
cur_idx = 0
|
|
449
|
+
else:
|
|
450
|
+
cur_idx = (cur_ids.index(self._selected) + 1) % len(items)
|
|
451
|
+
self._selected = items[cur_idx][1]
|
|
452
|
+
self._sync_selected_class()
|
|
453
|
+
elif event.key in ("up", "shift+tab"):
|
|
454
|
+
if self._selected not in cur_ids:
|
|
455
|
+
cur_idx = len(items) - 1
|
|
456
|
+
else:
|
|
457
|
+
cur_idx = (cur_ids.index(self._selected) - 1) % len(items)
|
|
458
|
+
self._selected = items[cur_idx][1]
|
|
459
|
+
self._sync_selected_class()
|
|
460
|
+
elif event.key == "enter":
|
|
461
|
+
if self._selected:
|
|
462
|
+
self.on_row_picked(self._selected)
|
|
463
|
+
|
|
464
|
+
def on_click(self, event):
|
|
465
|
+
# Click on the dimmed backdrop (outside the box) closes the
|
|
466
|
+
# panel without picking anything — rows handle their own
|
|
467
|
+
# clicks (see _NavRow.on_click). v0.7.6 Patch 1: only
|
|
468
|
+
# dismiss while the panel is still the active screen.
|
|
469
|
+
if getattr(event, "widget", None) in (self, None) and self.app.screen is self:
|
|
470
|
+
self.dismiss(None)
|
|
471
|
+
|
|
472
|
+
class MenuButton(_Clickable):
|
|
473
|
+
"""Left-of-header menu button (spec: "Modern animated menu
|
|
474
|
+
button, replace the existing hamburger icon"). Morphs
|
|
475
|
+
≡ -> ✕ while NavPanel is open, with a smooth color transition
|
|
476
|
+
on both states and on hover, instead of a static glyph. While
|
|
477
|
+
the panel is open the button is that ✕: clicking it closes the
|
|
478
|
+
menu (toggle behavior), so the ✕ is a real close control.
|
|
479
|
+
|
|
480
|
+
The closed-state glyph is U+2261 IDENTICAL TO (≡) — a cleaner
|
|
481
|
+
three-bar menu mark than the U+2630 TRIGRAM (☰) it replaces:
|
|
482
|
+
the trigram renders with uneven bar lengths and baseline
|
|
483
|
+
offsets in several common terminal fonts, which read as a
|
|
484
|
+
glitchy/off-center hamburger; ≡ is three uniform, crisp lines
|
|
485
|
+
in every font that has it (and it's far more widely covered
|
|
486
|
+
than the trigram block)."""
|
|
487
|
+
|
|
488
|
+
# U+2261 — clean three-bar menu mark.
|
|
489
|
+
_MENU_GLYPH = "\u2261"
|
|
490
|
+
_CLOSE_GLYPH = "\u2715"
|
|
491
|
+
|
|
492
|
+
def __init__(self, id="cct-menu-btn"):
|
|
493
|
+
super().__init__(self._MENU_GLYPH, widget_id=id, classes="cct-menu-btn")
|
|
494
|
+
self._open = False
|
|
495
|
+
|
|
496
|
+
def on_click(self, event):
|
|
497
|
+
# v0.7.6 Patch 1: stop the opening click from bubbling to
|
|
498
|
+
# the freshly pushed NavPanel's backdrop.
|
|
499
|
+
event.stop()
|
|
500
|
+
if self._open:
|
|
501
|
+
self.close_menu()
|
|
502
|
+
else:
|
|
503
|
+
self.open_menu()
|
|
504
|
+
|
|
505
|
+
def open_menu(self):
|
|
506
|
+
"""Opens NavPanel — shared by the click handler and the
|
|
507
|
+
Ctrl+M binding, so keyboard and mouse go through one path
|
|
508
|
+
and the button's ≡→✕ morph stays in sync either way."""
|
|
509
|
+
if self._open:
|
|
510
|
+
return
|
|
511
|
+
self._open = True
|
|
512
|
+
self.update(self._CLOSE_GLYPH)
|
|
513
|
+
self.add_class("cct-menu-btn-active")
|
|
514
|
+
self.app.push_screen(NavPanel(), self._on_closed)
|
|
515
|
+
|
|
516
|
+
def close_menu(self):
|
|
517
|
+
"""Closes the open NavPanel (used by the ✕-morph click and
|
|
518
|
+
Ctrl+M's close branch) — the panel's dismiss callback
|
|
519
|
+
restores the ≡ glyph and posts the picked action."""
|
|
520
|
+
for screen in list(self.app.screen_stack):
|
|
521
|
+
if isinstance(screen, NavPanel):
|
|
522
|
+
screen.dismiss(None)
|
|
523
|
+
return
|
|
524
|
+
|
|
525
|
+
def _on_closed(self, action_id):
|
|
526
|
+
self._open = False
|
|
527
|
+
self.update(self._MENU_GLYPH)
|
|
528
|
+
self.remove_class("cct-menu-btn-active")
|
|
529
|
+
if action_id:
|
|
530
|
+
self.post_message(NavAction(action_id))
|
|
531
|
+
|
|
532
|
+
class Logo(Static):
|
|
533
|
+
"""The CAT mark (spec: "Redesigned logo with a cleaner, more
|
|
534
|
+
recognizable identity"). A single hexagon glyph + bold wordmark,
|
|
535
|
+
no subtitle crowding the compact one-line nav bar — the fuller
|
|
536
|
+
"Coding Agent Terminal" subtitle still appears once on the
|
|
537
|
+
Welcome Dashboard (conversation.py), not here."""
|
|
538
|
+
|
|
539
|
+
def __init__(self, id="cct-logo"):
|
|
540
|
+
super().__init__("", id=id, classes="cct-logo")
|
|
541
|
+
|
|
542
|
+
def on_mount(self):
|
|
543
|
+
self.redraw()
|
|
544
|
+
|
|
545
|
+
def redraw(self):
|
|
546
|
+
from . import theme_css
|
|
547
|
+
accent = theme_css.current_hex("accent")
|
|
548
|
+
self.update(f"[{accent} b]{_MARK} CAT[/]")
|
|
549
|
+
|
|
550
|
+
# ------------------------------------------------------ permission pill --
|
|
551
|
+
class _PermMenuRow(Static):
|
|
552
|
+
"""One row of the permission dropdown: icon + label (colored
|
|
553
|
+
per-mode) on top, faint description underneath."""
|
|
554
|
+
|
|
555
|
+
def __init__(self, mode, index):
|
|
556
|
+
super().__init__("", classes="cct-permmenu-row")
|
|
557
|
+
self.mode = mode
|
|
558
|
+
self.index = index
|
|
559
|
+
|
|
560
|
+
def on_mount(self):
|
|
561
|
+
self.redraw()
|
|
562
|
+
|
|
563
|
+
def redraw(self):
|
|
564
|
+
from . import theme_css
|
|
565
|
+
color = theme_css.current_hex(_MODE_TONE[self.mode])
|
|
566
|
+
faint = theme_css.current_hex("text-faint")
|
|
567
|
+
label = perm.MODE_LABELS[self.mode]
|
|
568
|
+
desc = _MODE_DESCRIPTIONS[self.mode]
|
|
569
|
+
check = "\u25cf " if self.mode == perm.manager.mode else "\u25cb "
|
|
570
|
+
self.update(f"[{color} b]{check}{label}[/]\n [{faint}]{desc}[/]")
|
|
571
|
+
|
|
572
|
+
def on_click(self, event):
|
|
573
|
+
# v0.7.6 Patch 1: guard against a click being delivered to
|
|
574
|
+
# this menu after it was already dismissed (the same click
|
|
575
|
+
# that closed it can be re-delivered to the popped screen —
|
|
576
|
+
# double-dismiss then hits an empty screen stack). A row
|
|
577
|
+
# click only acts while the menu is still the active screen.
|
|
578
|
+
if self.app.screen is self.screen:
|
|
579
|
+
self.screen.dismiss(self.mode)
|
|
580
|
+
|
|
581
|
+
class PermissionModeMenu(Screen):
|
|
582
|
+
"""The dropdown itself (spec: "Options: Ask Every Time /
|
|
583
|
+
Restricted / Full Access", each with icon/description/color,
|
|
584
|
+
hover + selection + keyboard-nav + open/close animation).
|
|
585
|
+
|
|
586
|
+
Real anchored popup, not a centered dialog: takes the trigger
|
|
587
|
+
widget (the PermissionPill) at construction time and, on mount
|
|
588
|
+
(and again on every terminal resize while it's open), reads
|
|
589
|
+
that widget's live `region` — screen-absolute coordinates,
|
|
590
|
+
recomputed fresh each time rather than cached — to place the
|
|
591
|
+
box directly under the pill via `styles.offset`. Previously
|
|
592
|
+
this used `align: center top` + a fixed `margin-top: 3`, which
|
|
593
|
+
is why the popup always rendered centered/floating in the
|
|
594
|
+
middle of the screen instead of next to the button, and never
|
|
595
|
+
adapted to resizes or the button's real position — the root
|
|
596
|
+
cause of the reported positioning bugs. Edge collision is
|
|
597
|
+
handled explicitly: the box is clamped so it never renders
|
|
598
|
+
past the right or bottom edge of the terminal, and flips to
|
|
599
|
+
open *above* the pill instead of below when there isn't enough
|
|
600
|
+
room underneath. `align: left top` on the Screen makes (0, 0)
|
|
601
|
+
the coordinate origin so the computed offset is truly
|
|
602
|
+
absolute, rather than fighting a centering rule."""
|
|
603
|
+
|
|
604
|
+
CSS = """
|
|
605
|
+
PermissionModeMenu {
|
|
606
|
+
align: left top;
|
|
607
|
+
background: $app-background 60%;
|
|
608
|
+
}
|
|
609
|
+
#cct-permmenu-box {
|
|
610
|
+
width: 50;
|
|
611
|
+
height: auto;
|
|
612
|
+
background: $surface;
|
|
613
|
+
border-top: tall $surface-highlight;
|
|
614
|
+
border-left: tall $surface-highlight;
|
|
615
|
+
border-bottom: tall $surface-dark;
|
|
616
|
+
border-right: tall $surface-dark;
|
|
617
|
+
padding: 1 1;
|
|
618
|
+
opacity: 1; offset-y: 0;
|
|
619
|
+
}
|
|
620
|
+
#cct-permmenu-box.open { opacity: 1; offset-y: 0; }
|
|
621
|
+
.cct-permmenu-title {
|
|
622
|
+
color: $accent;
|
|
623
|
+
text-style: bold;
|
|
624
|
+
padding: 0 1 1 1;
|
|
625
|
+
border-bottom: solid $border;
|
|
626
|
+
margin-bottom: 1;
|
|
627
|
+
}
|
|
628
|
+
.cct-permmenu-row {
|
|
629
|
+
height: 3;
|
|
630
|
+
padding: 0 1;
|
|
631
|
+
background: $surface;
|
|
632
|
+
border-left: thick $surface;
|
|
633
|
+
border-right: thick $surface;
|
|
634
|
+
}
|
|
635
|
+
.cct-permmenu-row:hover {
|
|
636
|
+
background: $surface-alt;
|
|
637
|
+
color: $accent;
|
|
638
|
+
border-left: thick $accent;
|
|
639
|
+
border-right: thick $surface-alt;
|
|
640
|
+
}
|
|
641
|
+
.cct-permmenu-row-selected {
|
|
642
|
+
background: $surface-highlight;
|
|
643
|
+
border-left: thick $accent;
|
|
644
|
+
border-right: thick $surface-highlight;
|
|
645
|
+
text-style: bold;
|
|
646
|
+
}
|
|
647
|
+
.cct-permmenu-row-focused {
|
|
648
|
+
background: $surface-alt;
|
|
649
|
+
border-left: thick $accent;
|
|
650
|
+
border-right: thick $surface-alt;
|
|
651
|
+
}
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
_BOX_WIDTH = 48
|
|
655
|
+
# Fallback estimate for the box's height before Textual has had
|
|
656
|
+
# a chance to lay it out for real (title row + 3 cells/mode row).
|
|
657
|
+
_ESTIMATED_HEIGHT = 1 + 2 + len(_MODE_ORDER) * 3
|
|
658
|
+
|
|
659
|
+
def __init__(self, anchor=None):
|
|
660
|
+
super().__init__()
|
|
661
|
+
self._selected = _MODE_ORDER.index(perm.manager.mode)
|
|
662
|
+
# The PermissionPill that triggered this menu, so the popup
|
|
663
|
+
# can anchor to its actual on-screen position instead of
|
|
664
|
+
# being centered. `None` is a safe fallback (falls back to
|
|
665
|
+
# the old centered-under-header placement) for any caller
|
|
666
|
+
# that doesn't have a trigger widget handy.
|
|
667
|
+
self._anchor = anchor
|
|
668
|
+
|
|
669
|
+
def compose(self):
|
|
670
|
+
with Vertical(id="cct-permmenu-box"):
|
|
671
|
+
yield Static("\U0001f510 Access", classes="cct-permmenu-title")
|
|
672
|
+
for i, mode in enumerate(_MODE_ORDER):
|
|
673
|
+
yield _PermMenuRow(mode, i)
|
|
674
|
+
|
|
675
|
+
def on_mount(self):
|
|
676
|
+
self._sync_selected_class()
|
|
677
|
+
self._reposition()
|
|
678
|
+
self.call_after_refresh(self._reposition) # box has real height now
|
|
679
|
+
self.call_after_refresh(lambda: self.query_one("#cct-permmenu-box").add_class("open"))
|
|
680
|
+
|
|
681
|
+
def on_resize(self, event):
|
|
682
|
+
"""Terminal resized while the menu is open: re-read the
|
|
683
|
+
anchor's (possibly moved) region and re-clamp so the popup
|
|
684
|
+
never ends up outside the new terminal bounds."""
|
|
685
|
+
self._reposition()
|
|
686
|
+
|
|
687
|
+
def _reposition(self):
|
|
688
|
+
try:
|
|
689
|
+
box = self.query_one("#cct-permmenu-box")
|
|
690
|
+
except Exception:
|
|
691
|
+
return
|
|
692
|
+
screen_w, screen_h = self.size.width, self.size.height
|
|
693
|
+
if not screen_w or not screen_h:
|
|
694
|
+
return
|
|
695
|
+
box_w = self._BOX_WIDTH
|
|
696
|
+
box_h = box.size.height or self._ESTIMATED_HEIGHT
|
|
697
|
+
|
|
698
|
+
if self._anchor is not None:
|
|
699
|
+
region = self._anchor.region
|
|
700
|
+
x, y = region.x, region.y + region.height
|
|
701
|
+
else:
|
|
702
|
+
x, y = (screen_w - box_w) // 2, 3
|
|
703
|
+
|
|
704
|
+
# Edge collision, horizontal: never let the box run past
|
|
705
|
+
# the right edge (or, on a very narrow terminal, the left).
|
|
706
|
+
x = max(0, min(x, max(0, screen_w - box_w)))
|
|
707
|
+
|
|
708
|
+
# Edge collision, vertical: prefer opening below the
|
|
709
|
+
# anchor, but flip to above it when there isn't enough
|
|
710
|
+
# room underneath; then clamp to the terminal's height.
|
|
711
|
+
if self._anchor is not None and y + box_h > screen_h:
|
|
712
|
+
above = region.y - box_h
|
|
713
|
+
y = above if above >= 0 else max(0, screen_h - box_h)
|
|
714
|
+
y = max(0, min(y, max(0, screen_h - box_h)))
|
|
715
|
+
|
|
716
|
+
box.styles.offset = (x, y)
|
|
717
|
+
|
|
718
|
+
def _sync_selected_class(self):
|
|
719
|
+
for row in self.query(_PermMenuRow):
|
|
720
|
+
row.set_class(row.index == self._selected, "cct-permmenu-row-selected")
|
|
721
|
+
|
|
722
|
+
def on_key(self, event):
|
|
723
|
+
if event.key == "escape":
|
|
724
|
+
self.dismiss(None)
|
|
725
|
+
elif event.key in ("down", "tab"):
|
|
726
|
+
self._selected = (self._selected + 1) % len(_MODE_ORDER)
|
|
727
|
+
self._sync_selected_class()
|
|
728
|
+
elif event.key in ("up", "shift+tab"):
|
|
729
|
+
self._selected = (self._selected - 1) % len(_MODE_ORDER)
|
|
730
|
+
self._sync_selected_class()
|
|
731
|
+
elif event.key == "enter":
|
|
732
|
+
self.dismiss(_MODE_ORDER[self._selected])
|
|
733
|
+
|
|
734
|
+
def on_click(self, event):
|
|
735
|
+
# v0.7.6 Patch 1: only dismiss while this menu is still the
|
|
736
|
+
# active screen. The click that opened the menu can be
|
|
737
|
+
# re-delivered here after the menu already popped; a second
|
|
738
|
+
# dismiss would then hit an empty screen stack and raise
|
|
739
|
+
# ScreenStackError ("Can't pop screen").
|
|
740
|
+
if getattr(event, "widget", None) in (self, None) and self.app.screen is self:
|
|
741
|
+
self.dismiss(None)
|
|
742
|
+
|
|
743
|
+
class FullAccessConfirmScreen(Screen):
|
|
744
|
+
CSS = """
|
|
745
|
+
FullAccessConfirmScreen { align: center middle; background: $app-background 60%; }
|
|
746
|
+
#fullaccess-box {
|
|
747
|
+
width: 60; height: auto; background: $surface; border: round $warning;
|
|
748
|
+
padding: 1 2; align: center middle;
|
|
749
|
+
}
|
|
750
|
+
#fullaccess-title { color: $warning; text-style: bold; height: 1; }
|
|
751
|
+
#fullaccess-body { color: $text; height: auto; padding: 1 0; }
|
|
752
|
+
#fullaccess-btns { height: 3; align: center middle; }
|
|
753
|
+
#fullaccess-btns Button { margin: 0 1; }
|
|
754
|
+
"""
|
|
755
|
+
def compose(self):
|
|
756
|
+
with Vertical(id="fullaccess-box"):
|
|
757
|
+
yield Static("Enable Full Access?", id="fullaccess-title")
|
|
758
|
+
yield Static(
|
|
759
|
+
"CAT will be able to perform permitted workspace actions\n"
|
|
760
|
+
"without asking each time. Scope: current CAT workspace.\n"
|
|
761
|
+
"You can change this at any time.",
|
|
762
|
+
id="fullaccess-body")
|
|
763
|
+
with Horizontal(id="fullaccess-btns"):
|
|
764
|
+
yield Button("Cancel", id="fullaccess-cancel", classes="cct-btn")
|
|
765
|
+
yield Button("Enable Access", id="fullaccess-ok", variant="primary", classes="cct-btn")
|
|
766
|
+
def on_button_pressed(self, event):
|
|
767
|
+
self.dismiss(event.button.id == "fullaccess-ok")
|
|
768
|
+
def on_key(self, event):
|
|
769
|
+
if event.key == "escape":
|
|
770
|
+
self.dismiss(False)
|
|
771
|
+
|
|
772
|
+
class PermissionPill(_Clickable):
|
|
773
|
+
"""Center-of-header permission control (spec: "large rounded
|
|
774
|
+
Permission button... Remove the default dropdown triangle...
|
|
775
|
+
Style it as a primary action button rather than a form
|
|
776
|
+
widget"). No ▲/▼ glyph in either state — the pill's rounded
|
|
777
|
+
border + background swap on click/focus is what communicates
|
|
778
|
+
"this opens something", the same way a real button in Warp or
|
|
779
|
+
VS Code's command palette doesn't need a caret to read as
|
|
780
|
+
pressable."""
|
|
781
|
+
|
|
782
|
+
def __init__(self, id="cct-perm-pill"):
|
|
783
|
+
super().__init__("", widget_id=id, classes="cct-perm-pill")
|
|
784
|
+
self._open = False
|
|
785
|
+
|
|
786
|
+
def on_mount(self):
|
|
787
|
+
self.refresh_label()
|
|
788
|
+
|
|
789
|
+
def on_click(self, event):
|
|
790
|
+
# v0.7.6 Patch 1: stop the opening click from bubbling —
|
|
791
|
+
# otherwise the new menu screen can receive the same click
|
|
792
|
+
# as a backdrop hit and instantly close itself.
|
|
793
|
+
event.stop()
|
|
794
|
+
if self._open:
|
|
795
|
+
return
|
|
796
|
+
self._open = True
|
|
797
|
+
self.add_class("cct-perm-pill-active")
|
|
798
|
+
self.refresh_label()
|
|
799
|
+
self.app.push_screen(PermissionModeMenu(anchor=self), self._on_menu_closed)
|
|
800
|
+
|
|
801
|
+
def _on_menu_closed(self, chosen):
|
|
802
|
+
if not self._open:
|
|
803
|
+
return
|
|
804
|
+
self._open = False
|
|
805
|
+
self.remove_class("cct-perm-pill-active")
|
|
806
|
+
if chosen and chosen != perm.manager.mode:
|
|
807
|
+
if chosen == "full":
|
|
808
|
+
# Full Access requires confirmation
|
|
809
|
+
self.app.push_screen(FullAccessConfirmScreen(), lambda ok: self._apply_mode(chosen) if ok else self.refresh_label())
|
|
810
|
+
return
|
|
811
|
+
self._apply_mode(chosen)
|
|
812
|
+
self.refresh_label()
|
|
813
|
+
|
|
814
|
+
def _apply_mode(self, mode):
|
|
815
|
+
perm.manager.set_mode(mode)
|
|
816
|
+
self.post_message(PermissionModeChanged(mode))
|
|
817
|
+
self.refresh_label()
|
|
818
|
+
try:
|
|
819
|
+
from .. import workspace as _ws
|
|
820
|
+
ws = _ws.root_dir() if hasattr(_ws, 'root_dir') else "workspace"
|
|
821
|
+
scope = ws if mode == "full" else ""
|
|
822
|
+
note = f"Access: {perm.manager.mode_label()}" + (f" Scope: {scope}" if scope else "")
|
|
823
|
+
self.app._system_note(note)
|
|
824
|
+
except Exception:
|
|
825
|
+
pass
|
|
826
|
+
|
|
827
|
+
def refresh_label(self):
|
|
828
|
+
from . import theme_css
|
|
829
|
+
mode = perm.manager.mode
|
|
830
|
+
color = theme_css.current_hex(_MODE_TONE[mode])
|
|
831
|
+
self.update(f"[{color} b]{perm.manager.mode_label()}[/]")
|
|
832
|
+
# tooltip for current mode
|
|
833
|
+
try:
|
|
834
|
+
self.tooltip = _MODE_DESCRIPTIONS.get(mode, "")
|
|
835
|
+
except Exception:
|
|
836
|
+
pass
|
|
837
|
+
|
|
838
|
+
# Back-compat aliases: older code (and any external caller) that
|
|
839
|
+
# still imports the pre-redesign names keeps working unchanged.
|
|
840
|
+
HeaderPermissionSelector = PermissionPill
|
|
841
|
+
SidebarToggleGlyph = MenuButton
|
|
842
|
+
|
|
843
|
+
class BrandHeader(Vertical):
|
|
844
|
+
"""The NavBar. Exactly one row, height 3: MenuButton + Logo
|
|
845
|
+
(left) / PermissionPill (center) / workspace path (right).
|
|
846
|
+
|
|
847
|
+
The old second breadcrumb row (Notebook mode · model · context)
|
|
848
|
+
is gone — the spec is explicit that the bar holds only three
|
|
849
|
+
sections and the right side shows only the workspace path.
|
|
850
|
+
`refresh_breadcrumb`/`refresh_right` are kept as thin methods
|
|
851
|
+
(rather than deleted outright) so callers elsewhere in app.py
|
|
852
|
+
don't need simultaneous surgery just to keep importing cleanly;
|
|
853
|
+
`refresh_right` now expects a single path string, not a list of
|
|
854
|
+
status fields.
|
|
855
|
+
"""
|
|
856
|
+
|
|
857
|
+
def __init__(self, subtitle="Coding Agent Terminal", id="cct-brandheader"):
|
|
858
|
+
super().__init__(id=id)
|
|
859
|
+
self._subtitle = subtitle
|
|
860
|
+
self._workspace_path = None # None = never painted, forces first render
|
|
861
|
+
self._current_chat_title = "New Chat"
|
|
862
|
+
|
|
863
|
+
def _show_workspace_info(self):
|
|
864
|
+
try:
|
|
865
|
+
info_lines = [f"Path: {self._workspace_path or 'No workspace open'}"]
|
|
866
|
+
try:
|
|
867
|
+
from .. import workspace as _ws
|
|
868
|
+
from ..workspace_index import index_project
|
|
869
|
+
root = self._workspace_path
|
|
870
|
+
if root and root != "No workspace open" and os.path.isdir(root):
|
|
871
|
+
idx = index_project(root)
|
|
872
|
+
info_lines.append(f"Files: {idx.get('file_count', '?')}")
|
|
873
|
+
langs = idx.get("languages") or []
|
|
874
|
+
if langs:
|
|
875
|
+
info_lines.append("Languages: " + ", ".join(langs))
|
|
876
|
+
pms = idx.get("package_managers") or []
|
|
877
|
+
if pms:
|
|
878
|
+
info_lines.append("Managers: " + ", ".join(pms))
|
|
879
|
+
except Exception:
|
|
880
|
+
pass
|
|
881
|
+
from .nav_screens import InfoPanel
|
|
882
|
+
self.app.push_screen(InfoPanel("Workspace", info_lines))
|
|
883
|
+
except Exception:
|
|
884
|
+
pass
|
|
885
|
+
|
|
886
|
+
def _on_chat_title_click(self):
|
|
887
|
+
try:
|
|
888
|
+
self.post_message(NavAction("chats"))
|
|
889
|
+
except Exception:
|
|
890
|
+
pass
|
|
891
|
+
|
|
892
|
+
def set_chat_title(self, title: str):
|
|
893
|
+
"""Updates the current chat title shown in the header pill."""
|
|
894
|
+
clean = (title or "New Chat").strip()
|
|
895
|
+
if len(clean) > 28:
|
|
896
|
+
clean = clean[:27] + "\u2026"
|
|
897
|
+
self._current_chat_title = title or "New Chat"
|
|
898
|
+
try:
|
|
899
|
+
w = self.query_one("#cct-chat-title-pill", Static)
|
|
900
|
+
from . import theme_css
|
|
901
|
+
accent = theme_css.current_hex("accent")
|
|
902
|
+
w.update(f"[{accent}]\U0001f4ac[/] {clean}")
|
|
903
|
+
try:
|
|
904
|
+
w.tooltip = f"Chat: {title}\nClick to view all chats"
|
|
905
|
+
except Exception:
|
|
906
|
+
pass
|
|
907
|
+
except Exception:
|
|
908
|
+
pass
|
|
909
|
+
|
|
910
|
+
def compose(self):
|
|
911
|
+
with Horizontal(id="cct-header-top"):
|
|
912
|
+
with Horizontal(id="cct-header-left"):
|
|
913
|
+
yield MenuButton()
|
|
914
|
+
yield Logo()
|
|
915
|
+
yield Static("", id="cct-header-spacer-left")
|
|
916
|
+
yield PermissionPill()
|
|
917
|
+
yield Static("", id="cct-header-spacer-right")
|
|
918
|
+
yield _Clickable("", widget_id="cct-header-right", on_click=self._show_workspace_info)
|
|
919
|
+
|
|
920
|
+
def on_mount(self):
|
|
921
|
+
self.refresh_brand()
|
|
922
|
+
self.apply_custom_header()
|
|
923
|
+
# Force initial workspace paint (fixes blank on launch)
|
|
924
|
+
try:
|
|
925
|
+
self.refresh_right(self._workspace_path or "No workspace open")
|
|
926
|
+
except Exception:
|
|
927
|
+
pass
|
|
928
|
+
|
|
929
|
+
def apply_custom_header(self, hdr_cfg=None):
|
|
930
|
+
"""Apply header customization (height, visibility, logo visibility)."""
|
|
931
|
+
try:
|
|
932
|
+
from .. import customization as _cust
|
|
933
|
+
if not _cust.is_active():
|
|
934
|
+
self.display = True
|
|
935
|
+
self.styles.height = 4
|
|
936
|
+
try:
|
|
937
|
+
logo = self.query_one(Logo)
|
|
938
|
+
if logo:
|
|
939
|
+
logo.display = True
|
|
940
|
+
except Exception:
|
|
941
|
+
pass
|
|
942
|
+
return
|
|
943
|
+
if hdr_cfg is None:
|
|
944
|
+
hdr_cfg = _cust.get_manager().get_component("header") or {}
|
|
945
|
+
# Visibility (releases space if hidden)
|
|
946
|
+
vis = hdr_cfg.get("visible", True)
|
|
947
|
+
self.display = bool(vis)
|
|
948
|
+
# Height: #cct-brandheader has border-bottom: solid $border (1 row),
|
|
949
|
+
# so 3 rows of content (pill height: 3) requires header height: 4.
|
|
950
|
+
h = hdr_cfg.get("height")
|
|
951
|
+
if h is not None:
|
|
952
|
+
try:
|
|
953
|
+
val = int(h)
|
|
954
|
+
self.styles.height = max(4, min(10, val if val >= 4 else val + 1))
|
|
955
|
+
except Exception:
|
|
956
|
+
self.styles.height = 4
|
|
957
|
+
else:
|
|
958
|
+
self.styles.height = 4
|
|
959
|
+
# Logo visibility
|
|
960
|
+
logo_vis = hdr_cfg.get("logo_visible", True)
|
|
961
|
+
try:
|
|
962
|
+
logo = self.query_one(Logo)
|
|
963
|
+
if logo:
|
|
964
|
+
logo.display = bool(logo_vis)
|
|
965
|
+
except Exception:
|
|
966
|
+
pass
|
|
967
|
+
except Exception:
|
|
968
|
+
pass
|
|
969
|
+
|
|
970
|
+
def refresh_brand(self):
|
|
971
|
+
"""Re-renders the mark/pill colors from the current theme —
|
|
972
|
+
content doesn't change, only the accent color it's painted
|
|
973
|
+
with. Called once on mount and again whenever `/theme`
|
|
974
|
+
toggles."""
|
|
975
|
+
try:
|
|
976
|
+
self.query_one(Logo).redraw()
|
|
977
|
+
except Exception:
|
|
978
|
+
pass
|
|
979
|
+
try:
|
|
980
|
+
self.query_one(PermissionPill).refresh_label()
|
|
981
|
+
except Exception:
|
|
982
|
+
pass
|
|
983
|
+
try:
|
|
984
|
+
self.set_chat_title(self._current_chat_title)
|
|
985
|
+
except Exception:
|
|
986
|
+
pass
|
|
987
|
+
|
|
988
|
+
def refresh_breadcrumb(self, *args, **kwargs):
|
|
989
|
+
"""No-op retained for backward compatibility — the NavBar no
|
|
990
|
+
longer has a breadcrumb row (spec: only 3 sections)."""
|
|
991
|
+
return
|
|
992
|
+
|
|
993
|
+
def refresh_right(self, workspace_path):
|
|
994
|
+
"""`workspace_path` is the single string CCTApp now passes
|
|
995
|
+
(the open folder's absolute path, or a "No workspace open"
|
|
996
|
+
placeholder) — the right side shows this and nothing else
|
|
997
|
+
(no GPU/API/clock cluster).
|
|
998
|
+
|
|
999
|
+
v0.7.6 Patch 1 (header flicker fix): CCTApp's idle
|
|
1000
|
+
animation calls this every 1.6s while nothing streams, and
|
|
1001
|
+
Static.update() with byte-identical text still marks the
|
|
1002
|
+
widget dirty and triggers a full repaint of that row every
|
|
1003
|
+
tick — which is the steady "header flickering" everyone
|
|
1004
|
+
reported. Guarded here: no change, no update() call, no
|
|
1005
|
+
repaint. The first call still renders unconditionally."""
|
|
1006
|
+
from . import theme_css
|
|
1007
|
+
new_text = workspace_path or "No workspace open"
|
|
1008
|
+
if new_text == self._workspace_path:
|
|
1009
|
+
return
|
|
1010
|
+
self._workspace_path = new_text
|
|
1011
|
+
accent = theme_css.current_hex("accent")
|
|
1012
|
+
faint = theme_css.current_hex("text-faint")
|
|
1013
|
+
# Extract just the folder name for compact display
|
|
1014
|
+
short_name = os.path.basename(new_text) if new_text and new_text != "No workspace open" else "No workspace"
|
|
1015
|
+
# Workspace indicator: 📁 prefix + short name, with full path as tooltip
|
|
1016
|
+
display = f"[{accent}]\U0001f4c1[/] [b]{short_name}[/b]"
|
|
1017
|
+
try:
|
|
1018
|
+
w = self.query_one("#cct-header-right", Static)
|
|
1019
|
+
w.update(display)
|
|
1020
|
+
try:
|
|
1021
|
+
w.tooltip = new_text
|
|
1022
|
+
except Exception:
|
|
1023
|
+
pass
|
|
1024
|
+
except Exception:
|
|
1025
|
+
pass
|
|
1026
|
+
|
|
1027
|
+
else:
|
|
1028
|
+
BrandHeader = None
|
|
1029
|
+
HeaderPermissionSelector = None
|
|
1030
|
+
SidebarToggleGlyph = None
|
|
1031
|
+
NavPanel = None
|
|
1032
|
+
MenuButton = None
|
|
1033
|
+
Logo = None
|
|
1034
|
+
PermissionPill = None
|