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,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT UI — the "/" command palette. Filters calc_terminal/commands_data.py's
|
|
3
|
+
COMMANDS (the SAME list the fallback CLI's /help prints — one registry,
|
|
4
|
+
not a second hand-maintained copy), renders the dropdown, and posts
|
|
5
|
+
CommandExecuted when a command is chosen. Deciding what a command does
|
|
6
|
+
is calc_terminal/app.py's job, not this module's.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
print("This is a library file and is not meant to be run directly.")
|
|
11
|
+
import sys
|
|
12
|
+
sys.exit(1)
|
|
13
|
+
|
|
14
|
+
from ..commands_data import COMMANDS
|
|
15
|
+
|
|
16
|
+
TEXTUAL_AVAILABLE = True
|
|
17
|
+
try:
|
|
18
|
+
from textual.containers import Vertical
|
|
19
|
+
from textual.widgets import Static
|
|
20
|
+
except Exception:
|
|
21
|
+
TEXTUAL_AVAILABLE = False
|
|
22
|
+
|
|
23
|
+
PROMPT_SUGGESTIONS = [
|
|
24
|
+
# Kinetics & Numerical Chemistry
|
|
25
|
+
("what is a first order reaction", "Quick conceptual answer · Kinetics"),
|
|
26
|
+
("half life of a first order reaction", "Numerical calculation · Kinetics"),
|
|
27
|
+
("explain the arrhenius equation simply", "Conceptual explanation · Physical Chem"),
|
|
28
|
+
("what's the difference between molarity and molality", "Concentration comparison · Solutions"),
|
|
29
|
+
("quick mole concept refresher", "Core principles & formulas · Class 11"),
|
|
30
|
+
("how to balance redox reactions", "Ion-electron oxidation-reduction method"),
|
|
31
|
+
("calculate molar mass of glucose", "Step-by-step formula C6H12O6 mass"),
|
|
32
|
+
("activation energy numerical", "Arrhenius rate constant equation"),
|
|
33
|
+
("zero order reaction half life", "Zero order rate formula & graph"),
|
|
34
|
+
("second order kinetics rate constant", "Integrated rate equation derivation"),
|
|
35
|
+
("calculate gibbs free energy delta G", "Thermodynamics spontaneity calculation"),
|
|
36
|
+
("calculate ph of weak acid solution", "Equilibrium pH & dissociation constant"),
|
|
37
|
+
# Physics, Solves, Derivations & Simulations
|
|
38
|
+
("solve ideal gas law with P=1, n=2, T=300", "Symbolic solve + full notebook"),
|
|
39
|
+
("derive the first order rate law step by step", "Full derivation, notebook format"),
|
|
40
|
+
("plot the arrhenius graph and export it", "Interactive plot + PNG export"),
|
|
41
|
+
("plot y = sin(x)*exp(-x/5) from 0 to 20", "Damped wave function plot"),
|
|
42
|
+
("simulate an iron atom in 3d", "Live 3D Bohr electron simulation"),
|
|
43
|
+
("show me the 2p orbital cloud", "Quantum orbital 3D probability density"),
|
|
44
|
+
("explain redox titration in detail", "Full reaction mechanism & calculations"),
|
|
45
|
+
("integrate x*exp(-x^2) dx from 0 to infinity", "Calculus definite integration"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SuggestionEngine:
|
|
50
|
+
"""Pure filtering logic — no widgets, so it's independently testable
|
|
51
|
+
and reusable anywhere a command or prompt list needs fuzzy filtering."""
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def fuzzy_score(query, candidate):
|
|
55
|
+
"""Every character of `query` (lowercased) must appear in
|
|
56
|
+
`candidate` in order. Returns a score (lower is better) or None
|
|
57
|
+
if it doesn't match — a subsequence match is good enough for a
|
|
58
|
+
few dozen commands without pulling in a dependency."""
|
|
59
|
+
query = query.lower()
|
|
60
|
+
candidate = candidate.lower()
|
|
61
|
+
if not query:
|
|
62
|
+
return 0
|
|
63
|
+
if query in candidate:
|
|
64
|
+
return candidate.index(query)
|
|
65
|
+
pos = 0
|
|
66
|
+
gaps = 0
|
|
67
|
+
for ch in query:
|
|
68
|
+
found = candidate.find(ch, pos)
|
|
69
|
+
if found == -1:
|
|
70
|
+
return None
|
|
71
|
+
gaps += found - pos
|
|
72
|
+
pos = found + 1
|
|
73
|
+
return 100 + gaps
|
|
74
|
+
|
|
75
|
+
_HIDDEN_IN_CHAT = {"/notebook", "/workspace", "/kinetics", "/orbitals"}
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def match(cls, query, limit=30):
|
|
79
|
+
query = (query or "").strip()
|
|
80
|
+
scored = []
|
|
81
|
+
is_cmd_query = query.startswith("/")
|
|
82
|
+
search_term = query[1:].strip() if is_cmd_query else query
|
|
83
|
+
|
|
84
|
+
# 1. Match slash commands
|
|
85
|
+
for cmd, desc in COMMANDS:
|
|
86
|
+
if cmd in cls._HIDDEN_IN_CHAT:
|
|
87
|
+
continue
|
|
88
|
+
if is_cmd_query:
|
|
89
|
+
# Slash query: match against command name
|
|
90
|
+
score = cls.fuzzy_score(search_term, cmd[1:])
|
|
91
|
+
elif search_term:
|
|
92
|
+
# Plain text: match against command name as fallback
|
|
93
|
+
score = cls.fuzzy_score(search_term, cmd[1:])
|
|
94
|
+
if score is not None:
|
|
95
|
+
score += 25
|
|
96
|
+
else:
|
|
97
|
+
score = None
|
|
98
|
+
|
|
99
|
+
if score is not None:
|
|
100
|
+
scored.append((score, cmd, desc))
|
|
101
|
+
|
|
102
|
+
# 2. Match prompt suggestions
|
|
103
|
+
for prompt_text, desc in PROMPT_SUGGESTIONS:
|
|
104
|
+
if not search_term:
|
|
105
|
+
# Top prompt suggestions after commands
|
|
106
|
+
score = 35 if is_cmd_query else 10
|
|
107
|
+
else:
|
|
108
|
+
score = cls.fuzzy_score(search_term, prompt_text)
|
|
109
|
+
if score is not None and is_cmd_query:
|
|
110
|
+
score += 20 # Prefer slash commands when user explicitly typed '/'
|
|
111
|
+
if score is None:
|
|
112
|
+
words = search_term.lower().split()
|
|
113
|
+
if words and all(w in prompt_text.lower() or w in desc.lower() for w in words):
|
|
114
|
+
score = 50 if is_cmd_query else 40
|
|
115
|
+
if score is not None:
|
|
116
|
+
scored.append((score, prompt_text, desc))
|
|
117
|
+
|
|
118
|
+
scored.sort(key=lambda t: t[0])
|
|
119
|
+
return [(item, desc) for _, item, desc in scored[:limit]]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if TEXTUAL_AVAILABLE:
|
|
123
|
+
from textual.containers import VerticalScroll
|
|
124
|
+
|
|
125
|
+
PAGE_SIZE = 6
|
|
126
|
+
|
|
127
|
+
class CommandPalette(VerticalScroll):
|
|
128
|
+
"""The command and prompt suggestions palette — mounted inline inside
|
|
129
|
+
the composer card. ComposerInput drives it: Up/Down/PageUp/PageDown
|
|
130
|
+
move selection, Tab/Enter accept it, Esc closes it, and clicking
|
|
131
|
+
any item selects it."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, id="cct-cmdpalette"):
|
|
134
|
+
super().__init__(id=id)
|
|
135
|
+
self._matches = []
|
|
136
|
+
self._selected = 0
|
|
137
|
+
|
|
138
|
+
def compose(self):
|
|
139
|
+
yield from ()
|
|
140
|
+
|
|
141
|
+
def is_open(self):
|
|
142
|
+
return self.has_class("open")
|
|
143
|
+
|
|
144
|
+
def open_for_query(self, query):
|
|
145
|
+
self._matches = SuggestionEngine.match(query)
|
|
146
|
+
if not self._matches:
|
|
147
|
+
self.close()
|
|
148
|
+
return
|
|
149
|
+
self._selected = min(self._selected, len(self._matches) - 1)
|
|
150
|
+
self._redraw()
|
|
151
|
+
self.add_class("open")
|
|
152
|
+
# Dynamically size between 3 and 7 rows
|
|
153
|
+
self.styles.height = min(7, max(3, len(self._matches)))
|
|
154
|
+
self.scroll_home(animate=False)
|
|
155
|
+
if self.parent and hasattr(self.parent, "_on_palette_state_changed"):
|
|
156
|
+
self.parent._on_palette_state_changed(True)
|
|
157
|
+
|
|
158
|
+
def close(self):
|
|
159
|
+
was_open = self.is_open()
|
|
160
|
+
self._matches = []
|
|
161
|
+
self._selected = 0
|
|
162
|
+
self.remove_class("open")
|
|
163
|
+
for child in list(self.children):
|
|
164
|
+
child.remove()
|
|
165
|
+
if was_open and self.parent and hasattr(self.parent, "_on_palette_state_changed"):
|
|
166
|
+
self.parent._on_palette_state_changed(False)
|
|
167
|
+
|
|
168
|
+
def on_click(self, event):
|
|
169
|
+
try:
|
|
170
|
+
for i, child in enumerate(self.children):
|
|
171
|
+
if child == event.widget or child in getattr(event.widget, "ancestors", []):
|
|
172
|
+
self._selected = i
|
|
173
|
+
self._redraw()
|
|
174
|
+
if self.parent and hasattr(self.parent, "on_palette_selected"):
|
|
175
|
+
self.parent.on_palette_selected(self.selected_command())
|
|
176
|
+
break
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
def move(self, delta):
|
|
181
|
+
if not self._matches:
|
|
182
|
+
return
|
|
183
|
+
self._selected = (self._selected + delta) % len(self._matches)
|
|
184
|
+
self._redraw()
|
|
185
|
+
self._scroll_selected_into_view()
|
|
186
|
+
|
|
187
|
+
def move_page(self, direction):
|
|
188
|
+
if not self._matches:
|
|
189
|
+
return
|
|
190
|
+
last = len(self._matches) - 1
|
|
191
|
+
self._selected = max(0, min(last, self._selected + direction * PAGE_SIZE))
|
|
192
|
+
self._redraw()
|
|
193
|
+
self._scroll_selected_into_view()
|
|
194
|
+
|
|
195
|
+
def selected_command(self):
|
|
196
|
+
if not self._matches:
|
|
197
|
+
return None
|
|
198
|
+
return self._matches[self._selected][0]
|
|
199
|
+
|
|
200
|
+
def _scroll_selected_into_view(self):
|
|
201
|
+
try:
|
|
202
|
+
rows = list(self.query(".cct-cmd-row"))
|
|
203
|
+
if 0 <= self._selected < len(rows):
|
|
204
|
+
self.scroll_to_widget(rows[self._selected], animate=True)
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
def _redraw(self):
|
|
209
|
+
from . import theme_css
|
|
210
|
+
accent = theme_css.current_hex("accent")
|
|
211
|
+
for child in list(self.children):
|
|
212
|
+
child.remove()
|
|
213
|
+
for i, (item, desc) in enumerate(self._matches):
|
|
214
|
+
classes = "cct-cmd-row" + (" cct-cmd-row-selected" if i == self._selected else "")
|
|
215
|
+
if item.startswith("/"):
|
|
216
|
+
self.mount(Static(f"[{accent} b]{item}[/] [dim]{desc}[/dim]", classes=classes))
|
|
217
|
+
else:
|
|
218
|
+
self.mount(Static(f"[{accent}]\u2726[/] {item} [dim]{desc}[/dim]", classes=classes))
|
|
219
|
+
|
|
220
|
+
else:
|
|
221
|
+
CommandPalette = None
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT UI — the single permission renderer. Two widgets, both reading and
|
|
3
|
+
writing through calc_terminal/permissions.py's shared PermissionManager
|
|
4
|
+
(the one source of truth for permission state — this module never
|
|
5
|
+
tracks its own copy):
|
|
6
|
+
|
|
7
|
+
* PermissionCard — an inline "AI wants permission" request, mounted
|
|
8
|
+
as a ConversationItem in the conversation stream (per the redesign
|
|
9
|
+
brief: "Permission requests become part of conversation history",
|
|
10
|
+
never a modal/popup).
|
|
11
|
+
* PermissionsSettingsPanel — the full on/off checklist, opened from
|
|
12
|
+
the footer's Permissions button.
|
|
13
|
+
|
|
14
|
+
Both post events (PermissionGranted/PermissionDenied/PermissionCancelled)
|
|
15
|
+
rather than deciding what a decision means — calc_terminal/permissions.py
|
|
16
|
+
records it, calc_terminal/ui/app.py resolves whatever backend call was
|
|
17
|
+
waiting.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
print("This is a library file and is not meant to be run directly.")
|
|
22
|
+
import sys
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
from .. import permissions as perm
|
|
26
|
+
from .events import PermissionGranted, PermissionDenied, PermissionCancelled
|
|
27
|
+
|
|
28
|
+
TEXTUAL_AVAILABLE = True
|
|
29
|
+
try:
|
|
30
|
+
from textual.containers import Horizontal, Vertical
|
|
31
|
+
from textual.widgets import Static, Button, Switch, Label
|
|
32
|
+
except Exception:
|
|
33
|
+
TEXTUAL_AVAILABLE = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if TEXTUAL_AVAILABLE:
|
|
37
|
+
|
|
38
|
+
class PermissionCard(Vertical):
|
|
39
|
+
"""Inline 'AI wants permission' request — mounted into the
|
|
40
|
+
conversation log as its own item, never a popup."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, request_id, key, action_text, reason_text, id=None, path=None):
|
|
43
|
+
super().__init__(id=id, classes="cct-permcard")
|
|
44
|
+
self.request_id = request_id
|
|
45
|
+
self.key = key
|
|
46
|
+
self._action_text = action_text
|
|
47
|
+
self._reason_text = reason_text
|
|
48
|
+
self._path = path
|
|
49
|
+
self._resolved = False
|
|
50
|
+
|
|
51
|
+
def compose(self):
|
|
52
|
+
# Restricted mode (spec Mode 2) reviews this as a suggested
|
|
53
|
+
# patch — Accept/Reject — instead of Ask mode's (spec Mode 1)
|
|
54
|
+
# Allow-Once/Always-Allow/Deny. Same request, same event
|
|
55
|
+
# contract either way; only the labels/verb change.
|
|
56
|
+
self._restricted = perm.manager.restricted_review(self.key)
|
|
57
|
+
title = "Suggested action \u2014 review required" if self._restricted else "Permission required"
|
|
58
|
+
yield Static(title, classes="cct-permcard-title")
|
|
59
|
+
yield Static(f"The AI wants to [b]{self._action_text}[/b]")
|
|
60
|
+
if self._path:
|
|
61
|
+
yield Static(f"Path: {self._path}", classes="cct-permcard-path")
|
|
62
|
+
yield Static(f"Reason: {self._reason_text}", classes="cct-permcard-reason")
|
|
63
|
+
with Horizontal(classes="cct-permcard-buttons"):
|
|
64
|
+
if self._restricted:
|
|
65
|
+
yield Button("Accept", id="allow-once", classes="cct-ctrl")
|
|
66
|
+
yield Button("Reject", id="deny", classes="cct-ctrl")
|
|
67
|
+
else:
|
|
68
|
+
# v0.7.7 spec section 3: the dialog offers five
|
|
69
|
+
# choices — Allow Once / Always Allow / Deny /
|
|
70
|
+
# Always Deny / Cancel (wrapped on a second row so
|
|
71
|
+
# the card stays compact).
|
|
72
|
+
yield Button("Allow Once", id="allow-once", classes="cct-ctrl")
|
|
73
|
+
yield Button("Always Allow", id="always-allow", classes="cct-ctrl")
|
|
74
|
+
yield Button("Deny", id="deny", classes="cct-ctrl")
|
|
75
|
+
if not self._restricted:
|
|
76
|
+
with Horizontal(classes="cct-permcard-buttons"):
|
|
77
|
+
yield Button("Always Deny", id="always-deny", classes="cct-ctrl")
|
|
78
|
+
yield Button("Cancel", id="cancel", classes="cct-ctrl")
|
|
79
|
+
|
|
80
|
+
def on_button_pressed(self, event: Button.Pressed):
|
|
81
|
+
if self._resolved:
|
|
82
|
+
return
|
|
83
|
+
self._resolved = True
|
|
84
|
+
bid = event.button.id
|
|
85
|
+
for b in self.query(Button):
|
|
86
|
+
b.disabled = True
|
|
87
|
+
from . import theme_css
|
|
88
|
+
if bid == "always-allow":
|
|
89
|
+
self.post_message(PermissionGranted(self.request_id, self.key, remember=True))
|
|
90
|
+
tone = "success"
|
|
91
|
+
verb = "always allow"
|
|
92
|
+
elif bid == "allow-once":
|
|
93
|
+
self.post_message(PermissionGranted(self.request_id, self.key, remember=False))
|
|
94
|
+
tone = "success"
|
|
95
|
+
verb = "accept" if getattr(self, "_restricted", False) else "allow once"
|
|
96
|
+
elif bid == "always-deny":
|
|
97
|
+
# v0.7.7: permanent (session) refusal — recorded in the
|
|
98
|
+
# shared PermissionManager so future requests for this
|
|
99
|
+
# key are refused without prompting at all.
|
|
100
|
+
self.post_message(PermissionDenied(self.request_id, self.key, remember=True))
|
|
101
|
+
tone = "error"
|
|
102
|
+
verb = "always deny"
|
|
103
|
+
elif bid == "cancel":
|
|
104
|
+
self.post_message(PermissionCancelled(self.request_id, self.key))
|
|
105
|
+
tone = "warn"
|
|
106
|
+
verb = "cancel"
|
|
107
|
+
else:
|
|
108
|
+
self.post_message(PermissionDenied(self.request_id, self.key, remember=False))
|
|
109
|
+
tone = "error"
|
|
110
|
+
verb = "reject" if getattr(self, "_restricted", False) else "deny"
|
|
111
|
+
color = theme_css.current_hex(tone)
|
|
112
|
+
self.mount(Static(f"[{color}]\u2192 {verb}[/]", classes="cct-permcard-verdict"))
|
|
113
|
+
|
|
114
|
+
class Toggle3D(Button):
|
|
115
|
+
"""Skeuomorphic 3D On/Off tactile slider switch with physical rail,
|
|
116
|
+
depression animation, and glowing state indicator."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, key, is_on=False, on_toggle=None):
|
|
119
|
+
self._key = key
|
|
120
|
+
self.is_on = is_on
|
|
121
|
+
self._on_toggle = on_toggle
|
|
122
|
+
super().__init__(
|
|
123
|
+
self._format_label(is_on),
|
|
124
|
+
id=f"perm-toggle-{key}",
|
|
125
|
+
classes="cct-toggle-3d " + ("-on" if is_on else "-off"),
|
|
126
|
+
)
|
|
127
|
+
self.can_focus = True
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _format_label(is_on: bool) -> str:
|
|
131
|
+
# Tactile physical slider switch with 3D track and knob
|
|
132
|
+
return "[──● ON ]" if is_on else "[○── OFF]"
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def is_enabled(self) -> bool:
|
|
136
|
+
return self.is_on
|
|
137
|
+
|
|
138
|
+
def on_button_pressed(self, event: Button.Pressed):
|
|
139
|
+
event.stop()
|
|
140
|
+
self.is_on = not self.is_on
|
|
141
|
+
self.set_class(self.is_on, "-on")
|
|
142
|
+
self.set_class(not self.is_on, "-off")
|
|
143
|
+
self.label = self._format_label(self.is_on)
|
|
144
|
+
perm.manager.set(self._key, self.is_on)
|
|
145
|
+
if self._on_toggle:
|
|
146
|
+
self._on_toggle(self._key, self.is_on)
|
|
147
|
+
|
|
148
|
+
class PermissionRow(Horizontal):
|
|
149
|
+
"""One checklist line inside PermissionsSettingsPanel: a label
|
|
150
|
+
and a 3D tactile On/Off switch."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, key, label, is_on):
|
|
153
|
+
super().__init__(classes="cct-perm-row")
|
|
154
|
+
self._key = key
|
|
155
|
+
self._label_text = label
|
|
156
|
+
self._is_on = is_on
|
|
157
|
+
|
|
158
|
+
def compose(self):
|
|
159
|
+
from . import theme_css
|
|
160
|
+
color = theme_css.current_hex("success" if self._is_on else "text-muted")
|
|
161
|
+
mark = "●" if self._is_on else "○"
|
|
162
|
+
yield Static(f"[{color}]{mark}[/] {self._label_text}", classes="cct-perm-label")
|
|
163
|
+
yield Toggle3D(self._key, self._is_on, on_toggle=self._on_toggle_changed)
|
|
164
|
+
|
|
165
|
+
def _on_toggle_changed(self, key, value):
|
|
166
|
+
from . import theme_css
|
|
167
|
+
self._is_on = value
|
|
168
|
+
color = theme_css.current_hex("success" if value else "text-muted")
|
|
169
|
+
mark = "●" if value else "○"
|
|
170
|
+
try:
|
|
171
|
+
self.query_one(".cct-perm-label", Static).update(f"[{color}]{mark}[/] {self._label_text}")
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
class PermissionsSettingsPanel(Vertical):
|
|
176
|
+
"""Redesigned 3D Skeuomorphic Permissions & Settings Dashboard with
|
|
177
|
+
header bar, security mode cycler, close button, and 2-column organized grid."""
|
|
178
|
+
|
|
179
|
+
def __init__(self, id="cct-permission-panel"):
|
|
180
|
+
super().__init__(id=id)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def is_open(self) -> bool:
|
|
184
|
+
return self.has_class("open")
|
|
185
|
+
|
|
186
|
+
def _mode_badge_text(self):
|
|
187
|
+
mode = getattr(perm.manager, "mode", "ask")
|
|
188
|
+
return perm.MODE_LABELS.get(mode, "🔒 Ask Every Time")
|
|
189
|
+
|
|
190
|
+
def compose(self):
|
|
191
|
+
# 1. Header Bar: Icon, Title, Mode Badge Cycler, and Close Button
|
|
192
|
+
with Horizontal(id="cct-perm-header"):
|
|
193
|
+
with Horizontal(id="cct-perm-title-box"):
|
|
194
|
+
yield Static("🛡", id="cct-perm-icon")
|
|
195
|
+
yield Static("Permissions & Security Controls", id="cct-perm-title")
|
|
196
|
+
yield Button(self._mode_badge_text(), id="cct-perm-mode-btn", tooltip="Click to cycle Security Mode (Ask / Restricted / Full)")
|
|
197
|
+
yield Static("", id="cct-perm-spacer")
|
|
198
|
+
yield Button("✕", id="cct-perm-close", classes="cct-icon-btn", tooltip="Close permissions panel (Esc)")
|
|
199
|
+
|
|
200
|
+
# 2. Body: Two organized 3D columns
|
|
201
|
+
exec_keys = {"read_files", "write_files", "execute_python", "shell_commands", "install_packages"}
|
|
202
|
+
tools_keys = {"device_control", "internet", "formula_library", "notebook", "calculator"}
|
|
203
|
+
|
|
204
|
+
with Horizontal(id="cct-perm-grid"):
|
|
205
|
+
with Vertical(classes="cct-perm-col"):
|
|
206
|
+
yield Static("⚡ Files & Execution", classes="cct-perm-col-title")
|
|
207
|
+
for key, label, is_on in perm.manager.snapshot():
|
|
208
|
+
if key in exec_keys:
|
|
209
|
+
yield PermissionRow(key, label, is_on)
|
|
210
|
+
|
|
211
|
+
with Vertical(classes="cct-perm-col"):
|
|
212
|
+
yield Static("🌐 Network & Tools", classes="cct-perm-col-title")
|
|
213
|
+
for key, label, is_on in perm.manager.snapshot():
|
|
214
|
+
if key in tools_keys:
|
|
215
|
+
yield PermissionRow(key, label, is_on)
|
|
216
|
+
|
|
217
|
+
def on_button_pressed(self, event: Button.Pressed):
|
|
218
|
+
bid = event.button.id
|
|
219
|
+
if bid == "cct-perm-close":
|
|
220
|
+
event.stop()
|
|
221
|
+
self.close_panel()
|
|
222
|
+
elif bid == "cct-perm-mode-btn":
|
|
223
|
+
event.stop()
|
|
224
|
+
modes = list(perm.MODES)
|
|
225
|
+
curr = getattr(perm.manager, "mode", "ask")
|
|
226
|
+
next_mode = modes[(modes.index(curr) + 1) % len(modes)]
|
|
227
|
+
perm.manager.set_mode(next_mode)
|
|
228
|
+
event.button.label = self._mode_badge_text()
|
|
229
|
+
|
|
230
|
+
def on_key(self, event) -> None:
|
|
231
|
+
if event.key == "escape":
|
|
232
|
+
event.stop()
|
|
233
|
+
try:
|
|
234
|
+
event.prevent_default()
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
self.close_panel()
|
|
238
|
+
|
|
239
|
+
def close_panel(self):
|
|
240
|
+
self.remove_class("open")
|
|
241
|
+
if self.parent and hasattr(self.parent, "_on_permission_panel_toggled"):
|
|
242
|
+
self.parent._on_permission_panel_toggled(False)
|
|
243
|
+
|
|
244
|
+
def refresh_rows(self):
|
|
245
|
+
from . import theme_css
|
|
246
|
+
try:
|
|
247
|
+
mode_btn = self.query_one("#cct-perm-mode-btn", Button)
|
|
248
|
+
mode_btn.label = self._mode_badge_text()
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
for key, label, is_on in perm.manager.snapshot():
|
|
252
|
+
try:
|
|
253
|
+
toggle = self.query_one(f"#perm-toggle-{key}", Toggle3D)
|
|
254
|
+
toggle.is_on = is_on
|
|
255
|
+
toggle.set_class(is_on, "-on")
|
|
256
|
+
toggle.set_class(not is_on, "-off")
|
|
257
|
+
toggle.label = Toggle3D._format_label(is_on)
|
|
258
|
+
row = toggle.parent
|
|
259
|
+
if row:
|
|
260
|
+
color = theme_css.current_hex("success" if is_on else "text-muted")
|
|
261
|
+
mark = "●" if is_on else "○"
|
|
262
|
+
row.query_one(".cct-perm-label", Static).update(f"[{color}]{mark}[/] {label}")
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
else:
|
|
267
|
+
PermissionCard = None
|
|
268
|
+
PermissionRow = None
|
|
269
|
+
PermissionsSettingsPanel = None
|