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,160 @@
|
|
|
1
|
+
"""CAT Code Editor — Command Registry (calc_terminal/editor/commands.py).
|
|
2
|
+
|
|
3
|
+
Centralized registration and execution of all editor, preview, and workbench commands.
|
|
4
|
+
UI components, buttons, menus, and shortcuts invoke commands through this registry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class CATCommand:
|
|
16
|
+
"""Definition of an executable command inside CAT IDE."""
|
|
17
|
+
id: str
|
|
18
|
+
title: str
|
|
19
|
+
category: str = "General"
|
|
20
|
+
handler: Optional[Callable[[Optional[dict]], Any]] = None
|
|
21
|
+
can_execute: Optional[Callable[[Optional[dict]], bool]] = None
|
|
22
|
+
default_keybinding: Optional[str] = None
|
|
23
|
+
description: str = ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CommandRegistry:
|
|
27
|
+
"""Central registry of all CAT IDE commands."""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._commands: Dict[str, CATCommand] = {}
|
|
31
|
+
self._register_defaults()
|
|
32
|
+
|
|
33
|
+
def register(self, command: CATCommand) -> None:
|
|
34
|
+
"""Register or override a command."""
|
|
35
|
+
self._commands[command.id] = command
|
|
36
|
+
|
|
37
|
+
def unregister(self, command_id: str) -> bool:
|
|
38
|
+
"""Unregister a command by ID."""
|
|
39
|
+
if command_id in self._commands:
|
|
40
|
+
del self._commands[command_id]
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
def get(self, command_id: str) -> Optional[CATCommand]:
|
|
45
|
+
"""Look up a command by its ID."""
|
|
46
|
+
return self._commands.get(command_id)
|
|
47
|
+
|
|
48
|
+
def can_execute(self, command_id: str, context: Optional[dict] = None) -> bool:
|
|
49
|
+
"""Check if a command can currently execute."""
|
|
50
|
+
cmd = self.get(command_id)
|
|
51
|
+
if not cmd:
|
|
52
|
+
return False
|
|
53
|
+
if cmd.can_execute is not None:
|
|
54
|
+
try:
|
|
55
|
+
return bool(cmd.can_execute(context))
|
|
56
|
+
except Exception:
|
|
57
|
+
return False
|
|
58
|
+
return cmd.handler is not None
|
|
59
|
+
|
|
60
|
+
def execute(self, command_id: str, context: Optional[dict] = None) -> Any:
|
|
61
|
+
"""Execute a registered command by its ID."""
|
|
62
|
+
cmd = self.get(command_id)
|
|
63
|
+
if not cmd:
|
|
64
|
+
raise KeyError(f"Command '{command_id}' is not registered.")
|
|
65
|
+
if not self.can_execute(command_id, context):
|
|
66
|
+
return None
|
|
67
|
+
if cmd.handler is None:
|
|
68
|
+
return None
|
|
69
|
+
try:
|
|
70
|
+
sig = inspect.signature(cmd.handler)
|
|
71
|
+
if len(sig.parameters) == 0:
|
|
72
|
+
return cmd.handler()
|
|
73
|
+
return cmd.handler(context)
|
|
74
|
+
except TypeError:
|
|
75
|
+
try:
|
|
76
|
+
return cmd.handler(context)
|
|
77
|
+
except Exception:
|
|
78
|
+
return cmd.handler()
|
|
79
|
+
|
|
80
|
+
def list_commands(self, category: Optional[str] = None) -> List[CATCommand]:
|
|
81
|
+
"""Return a sorted list of registered commands, optionally filtered by category."""
|
|
82
|
+
cmds = list(self._commands.values())
|
|
83
|
+
if category:
|
|
84
|
+
cmds = [c for c in cmds if c.category.lower() == category.lower()]
|
|
85
|
+
cmds.sort(key=lambda c: (c.category, c.title))
|
|
86
|
+
return cmds
|
|
87
|
+
|
|
88
|
+
def _register_defaults(self) -> None:
|
|
89
|
+
"""Register default editor, preview, and workbench commands."""
|
|
90
|
+
defaults = [
|
|
91
|
+
# Editor File Actions
|
|
92
|
+
CATCommand("editor.save", "File: Save", "File", default_keybinding="ctrl+s", description="Save the active file to disk"),
|
|
93
|
+
CATCommand("editor.saveAs", "File: Save As...", "File", default_keybinding="ctrl+shift+s", description="Save active file with a new name"),
|
|
94
|
+
CATCommand("editor.closeTab", "View: Close Current Tab", "View", default_keybinding="ctrl+w", description="Close active editor or viewer tab"),
|
|
95
|
+
CATCommand("editor.reopenClosedTab", "View: Reopen Closed Tab", "View", default_keybinding="ctrl+shift+t", description="Reopen recently closed tab"),
|
|
96
|
+
CATCommand("editor.quickOpen", "Go to File... (Quick Open)", "Go", default_keybinding="ctrl+p", description="Quickly open a file from workspace"),
|
|
97
|
+
CATCommand("editor.commandPalette", "View: Command Palette", "View", default_keybinding="ctrl+shift+p", description="Open CAT Command Palette"),
|
|
98
|
+
|
|
99
|
+
# Editor Navigation & Search
|
|
100
|
+
CATCommand("editor.find", "Edit: Find", "Edit", default_keybinding="ctrl+f", description="Find text in active editor"),
|
|
101
|
+
CATCommand("editor.replace", "Edit: Replace", "Edit", default_keybinding="ctrl+h", description="Find and replace text in active editor"),
|
|
102
|
+
CATCommand("workspace.search", "Search: Find in Files", "Search", default_keybinding="ctrl+shift+f", description="Search text across workspace"),
|
|
103
|
+
CATCommand("editor.goToDefinition", "Go: Go to Definition", "Go", default_keybinding="f12", description="Jump to symbol definition"),
|
|
104
|
+
CATCommand("editor.findReferences", "Go: Find References", "Go", default_keybinding="shift+f12", description="Find all references of symbol"),
|
|
105
|
+
CATCommand("editor.rename", "Edit: Rename Symbol", "Edit", default_keybinding="f2", description="Rename symbol across file"),
|
|
106
|
+
|
|
107
|
+
# Line & Text Manipulation
|
|
108
|
+
CATCommand("editor.toggleComment", "Edit: Toggle Line Comment", "Edit", default_keybinding="ctrl+/", description="Comment or uncomment current line"),
|
|
109
|
+
CATCommand("editor.selectNextOccurrence", "Selection: Add Next Occurrence", "Selection", default_keybinding="ctrl+d", description="Select next matching text occurrence"),
|
|
110
|
+
CATCommand("editor.deleteLine", "Edit: Delete Line", "Edit", default_keybinding="ctrl+shift+k", description="Delete entire current line"),
|
|
111
|
+
CATCommand("editor.moveLineUp", "Edit: Move Line Up", "Edit", default_keybinding="alt+up", description="Move active line or selection up"),
|
|
112
|
+
CATCommand("editor.moveLineDown", "Edit: Move Line Down", "Edit", default_keybinding="alt+down", description="Move active line or selection down"),
|
|
113
|
+
CATCommand("editor.copyLineUp", "Edit: Copy Line Up", "Edit", default_keybinding="shift+alt+up", description="Duplicate active line upwards"),
|
|
114
|
+
CATCommand("editor.copyLineDown", "Edit: Copy Line Down", "Edit", default_keybinding="shift+alt+down", description="Duplicate active line downwards"),
|
|
115
|
+
CATCommand("editor.insertLineBelow", "Edit: Insert Line Below", "Edit", default_keybinding="ctrl+enter", description="Insert line below and move cursor"),
|
|
116
|
+
CATCommand("editor.insertLineAbove", "Edit: Insert Line Above", "Edit", default_keybinding="ctrl+shift+enter", description="Insert line above and move cursor"),
|
|
117
|
+
CATCommand("editor.triggerCompletion", "Edit: Trigger Suggestion", "Edit", default_keybinding="ctrl+space", description="Trigger code completion / suggestions"),
|
|
118
|
+
CATCommand("editor.formatDocument", "Edit: Format Document", "Edit", default_keybinding="shift+alt+f", description="Format source code"),
|
|
119
|
+
|
|
120
|
+
# Live Preview Commands
|
|
121
|
+
CATCommand("preview.open", "Preview: Run / Open Live Preview", "Preview", default_keybinding="shift+enter", description="Start or reuse local development server and open live preview"),
|
|
122
|
+
CATCommand("preview.reload", "Preview: Reload Live Preview", "Preview", default_keybinding="ctrl+r", description="Reload active live preview"),
|
|
123
|
+
CATCommand("preview.stop", "Preview: Stop Development Server", "Preview", description="Stop the running local development server"),
|
|
124
|
+
CATCommand("preview.restart", "Preview: Restart Development Server", "Preview", description="Restart the local development server"),
|
|
125
|
+
CATCommand("preview.toggleSplit", "View: Toggle Split Editor / Preview", "View", default_keybinding="ctrl+shift+s", description="Toggle side-by-side split between editor and preview"),
|
|
126
|
+
|
|
127
|
+
# Workbench & Sidebar
|
|
128
|
+
CATCommand("workbench.action.toggleSidebar", "View: Toggle Primary Sidebar", "View", default_keybinding="ctrl+b", description="Show or hide primary sidebar / explorer"),
|
|
129
|
+
CATCommand("workbench.action.terminal.toggle", "View: Toggle Terminal", "View", default_keybinding="ctrl+`", description="Toggle integrated terminal"),
|
|
130
|
+
CATCommand("workbench.view.explorer", "View: Show Explorer", "View", default_keybinding="ctrl+shift+e", description="Focus workspace file explorer"),
|
|
131
|
+
CATCommand("workbench.view.scm", "View: Show Source Control", "View", default_keybinding="ctrl+shift+g", description="Open source control / git panel"),
|
|
132
|
+
CATCommand("workbench.action.closeOverlays", "View: Close Active Overlays", "View", default_keybinding="escape", description="Dismiss active find bar, popups, or dialogs"),
|
|
133
|
+
|
|
134
|
+
# Viewer Controls
|
|
135
|
+
CATCommand("viewer.zoomIn", "Viewer: Zoom In", "Viewer", default_keybinding="ctrl+=", description="Zoom in active viewer"),
|
|
136
|
+
CATCommand("viewer.zoomOut", "Viewer: Zoom Out", "Viewer", default_keybinding="ctrl+-", description="Zoom out active viewer"),
|
|
137
|
+
CATCommand("viewer.fitPage", "Viewer: Fit to Screen", "Viewer", description="Fit document or image to screen"),
|
|
138
|
+
CATCommand("viewer.fitWidth", "Viewer: Fit to Width", "Viewer", description="Fit document or image to width"),
|
|
139
|
+
CATCommand("viewer.fullscreen", "Viewer: Toggle Fullscreen", "Viewer", default_keybinding="f11", description="Toggle fullscreen right pane"),
|
|
140
|
+
|
|
141
|
+
# AI Code Actions
|
|
142
|
+
CATCommand("ai.explainSelection", "CAT AI: Explain Selection", "AI", description="Ask CAT AI to explain the selected code"),
|
|
143
|
+
CATCommand("ai.fixSelection", "CAT AI: Fix Selection", "AI", description="Ask CAT AI to fix bugs or errors in selection"),
|
|
144
|
+
CATCommand("ai.refactorSelection", "CAT AI: Refactor Selection", "AI", description="Refactor selected code with AI diff"),
|
|
145
|
+
CATCommand("ai.generateTests", "CAT AI: Generate Unit Tests", "AI", description="Generate unit tests for selected code"),
|
|
146
|
+
]
|
|
147
|
+
for cmd in defaults:
|
|
148
|
+
self.register(cmd)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# Global singleton registry
|
|
152
|
+
_GLOBAL_REGISTRY: Optional[CommandRegistry] = None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_command_registry() -> CommandRegistry:
|
|
156
|
+
"""Return the global CommandRegistry singleton."""
|
|
157
|
+
global _GLOBAL_REGISTRY
|
|
158
|
+
if _GLOBAL_REGISTRY is None:
|
|
159
|
+
_GLOBAL_REGISTRY = CommandRegistry()
|
|
160
|
+
return _GLOBAL_REGISTRY
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""CAT Code Editor — Shortcut & Keybinding Manager (calc_terminal/editor/shortcuts.py).
|
|
2
|
+
|
|
3
|
+
Centralized keybinding registration, normalization, conflict detection, and resolution.
|
|
4
|
+
Enforces VS Code keyboard compatibility with customizable bindings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from dataclasses import asdict, dataclass
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Keybinding:
|
|
17
|
+
"""Represents a key combination mapped to a command ID."""
|
|
18
|
+
command_id: str
|
|
19
|
+
key: str
|
|
20
|
+
when: Optional[str] = None # e.g., "editor", "preview", "global"
|
|
21
|
+
priority: int = 0
|
|
22
|
+
enabled: bool = True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_key(key: str) -> str:
|
|
26
|
+
"""Normalize key combinations into canonical lowercase format: [ctrl+][alt+][shift+]key."""
|
|
27
|
+
if not key:
|
|
28
|
+
return ""
|
|
29
|
+
parts = [p.strip().lower() for p in key.replace("-", "+").split("+")]
|
|
30
|
+
modifiers = set()
|
|
31
|
+
base_key = ""
|
|
32
|
+
for p in parts:
|
|
33
|
+
if p in ("ctrl", "control"):
|
|
34
|
+
modifiers.add("ctrl")
|
|
35
|
+
elif p in ("alt", "option"):
|
|
36
|
+
modifiers.add("alt")
|
|
37
|
+
elif p in ("shift",):
|
|
38
|
+
modifiers.add("shift")
|
|
39
|
+
elif p:
|
|
40
|
+
base_key = p
|
|
41
|
+
ordered = []
|
|
42
|
+
if "ctrl" in modifiers:
|
|
43
|
+
ordered.append("ctrl")
|
|
44
|
+
if "alt" in modifiers:
|
|
45
|
+
ordered.append("alt")
|
|
46
|
+
if "shift" in modifiers:
|
|
47
|
+
ordered.append("shift")
|
|
48
|
+
if base_key:
|
|
49
|
+
ordered.append(base_key)
|
|
50
|
+
return "+".join(ordered)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ShortcutManager:
|
|
54
|
+
"""Manages all keyboard shortcuts across CAT IDE."""
|
|
55
|
+
|
|
56
|
+
CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".cat_keybindings.json")
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self._bindings: List[Keybinding] = []
|
|
60
|
+
self._register_default_bindings()
|
|
61
|
+
self.load_user_bindings()
|
|
62
|
+
|
|
63
|
+
def _register_default_bindings(self) -> None:
|
|
64
|
+
"""Register default VS Code-compatible keybindings."""
|
|
65
|
+
defaults = [
|
|
66
|
+
# File Operations
|
|
67
|
+
Keybinding("editor.save", "ctrl+s", when="editor", priority=10),
|
|
68
|
+
Keybinding("editor.save", "ctrl+s", when="global", priority=5),
|
|
69
|
+
Keybinding("editor.saveAs", "ctrl+shift+s", when="editor", priority=10),
|
|
70
|
+
Keybinding("editor.closeTab", "ctrl+w", when="editor", priority=10),
|
|
71
|
+
Keybinding("editor.reopenClosedTab", "ctrl+shift+t", when="global", priority=5),
|
|
72
|
+
Keybinding("editor.quickOpen", "ctrl+p", when="global", priority=10),
|
|
73
|
+
Keybinding("editor.commandPalette", "ctrl+shift+p", when="global", priority=10),
|
|
74
|
+
|
|
75
|
+
# Search & Navigation
|
|
76
|
+
Keybinding("editor.find", "ctrl+f", when="editor", priority=10),
|
|
77
|
+
Keybinding("editor.replace", "ctrl+h", when="editor", priority=10),
|
|
78
|
+
Keybinding("workspace.search", "ctrl+shift+f", when="global", priority=5),
|
|
79
|
+
Keybinding("editor.goToDefinition", "f12", when="editor", priority=10),
|
|
80
|
+
Keybinding("editor.findReferences", "shift+f12", when="editor", priority=10),
|
|
81
|
+
Keybinding("editor.rename", "f2", when="editor", priority=10),
|
|
82
|
+
|
|
83
|
+
# Code Editing
|
|
84
|
+
Keybinding("editor.toggleComment", "ctrl+/", when="editor", priority=10),
|
|
85
|
+
Keybinding("editor.selectNextOccurrence", "ctrl+d", when="editor", priority=10),
|
|
86
|
+
Keybinding("editor.deleteLine", "ctrl+shift+k", when="editor", priority=10),
|
|
87
|
+
Keybinding("editor.moveLineUp", "alt+up", when="editor", priority=10),
|
|
88
|
+
Keybinding("editor.moveLineDown", "alt+down", when="editor", priority=10),
|
|
89
|
+
Keybinding("editor.copyLineUp", "shift+alt+up", when="editor", priority=10),
|
|
90
|
+
Keybinding("editor.copyLineDown", "shift+alt+down", when="editor", priority=10),
|
|
91
|
+
Keybinding("editor.insertLineBelow", "alt+enter", when="editor", priority=10),
|
|
92
|
+
Keybinding("editor.insertLineAbove", "alt+shift+enter", when="editor", priority=10),
|
|
93
|
+
Keybinding("editor.triggerCompletion", "ctrl+space", when="editor", priority=10),
|
|
94
|
+
|
|
95
|
+
# Live Preview
|
|
96
|
+
Keybinding("preview.open", "shift+enter", when="editor", priority=20),
|
|
97
|
+
Keybinding("preview.open", "ctrl+shift+enter", when="editor", priority=20),
|
|
98
|
+
Keybinding("preview.open", "cmd+shift+enter", when="editor", priority=20),
|
|
99
|
+
Keybinding("preview.open", "super+shift+enter", when="editor", priority=20),
|
|
100
|
+
Keybinding("preview.open", "ctrl+enter", when="editor", priority=20),
|
|
101
|
+
Keybinding("preview.open", "shift+enter", when="global", priority=5),
|
|
102
|
+
Keybinding("preview.open", "ctrl+shift+enter", when="global", priority=5),
|
|
103
|
+
Keybinding("preview.reload", "ctrl+r", when="preview", priority=10),
|
|
104
|
+
Keybinding("preview.toggleSplit", "ctrl+shift+s", when="preview", priority=10),
|
|
105
|
+
|
|
106
|
+
# Workbench & Shell
|
|
107
|
+
Keybinding("workbench.action.toggleSidebar", "ctrl+b", when="global", priority=10),
|
|
108
|
+
Keybinding("workbench.action.terminal.toggle", "ctrl+`", when="global", priority=10),
|
|
109
|
+
Keybinding("workbench.view.explorer", "ctrl+shift+e", when="global", priority=10),
|
|
110
|
+
Keybinding("workbench.view.scm", "ctrl+shift+g", when="global", priority=10),
|
|
111
|
+
Keybinding("workbench.action.closeOverlays", "escape", when="global", priority=10),
|
|
112
|
+
|
|
113
|
+
# Viewer
|
|
114
|
+
Keybinding("viewer.fullscreen", "f11", when="global", priority=10),
|
|
115
|
+
Keybinding("viewer.zoomIn", "ctrl+=", when="viewer", priority=10),
|
|
116
|
+
Keybinding("viewer.zoomOut", "ctrl+-", when="viewer", priority=10),
|
|
117
|
+
]
|
|
118
|
+
for kb in defaults:
|
|
119
|
+
self.register(kb)
|
|
120
|
+
|
|
121
|
+
def register(self, binding: Keybinding) -> None:
|
|
122
|
+
"""Register a keybinding with normalized key representation."""
|
|
123
|
+
binding.key = normalize_key(binding.key)
|
|
124
|
+
self._bindings.append(binding)
|
|
125
|
+
|
|
126
|
+
def unregister(self, command_id: str, key: Optional[str] = None) -> None:
|
|
127
|
+
"""Remove a keybinding by command ID and optionally key string."""
|
|
128
|
+
norm_key = normalize_key(key) if key else None
|
|
129
|
+
self._bindings = [
|
|
130
|
+
b for b in self._bindings
|
|
131
|
+
if not (b.command_id == command_id and (norm_key is None or b.key == norm_key))
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
def detect_conflicts(self, key: str, when: Optional[str] = None) -> List[Keybinding]:
|
|
135
|
+
"""Detect any existing enabled keybindings that share the same key and compatible context."""
|
|
136
|
+
norm_key = normalize_key(key)
|
|
137
|
+
conflicts = []
|
|
138
|
+
for b in self._bindings:
|
|
139
|
+
if not b.enabled:
|
|
140
|
+
continue
|
|
141
|
+
if b.key == norm_key:
|
|
142
|
+
if when is None or b.when is None or b.when == "global" or when == "global" or b.when == when:
|
|
143
|
+
conflicts.append(b)
|
|
144
|
+
return conflicts
|
|
145
|
+
|
|
146
|
+
def resolve(self, raw_key: str, context: Optional[str] = None) -> Optional[str]:
|
|
147
|
+
return self._resolve_impl(raw_key, context)
|
|
148
|
+
|
|
149
|
+
match = resolve
|
|
150
|
+
|
|
151
|
+
def _resolve_impl(self, raw_key: str, context: Optional[str] = None) -> Optional[str]:
|
|
152
|
+
"""Resolve a raw keyboard input event into the winning command ID."""
|
|
153
|
+
norm_key = normalize_key(raw_key)
|
|
154
|
+
matching = []
|
|
155
|
+
for b in self._bindings:
|
|
156
|
+
if not b.enabled or b.key != norm_key:
|
|
157
|
+
continue
|
|
158
|
+
# Context match: exact context wins, or global matches if no specific binding
|
|
159
|
+
if context and b.when == context:
|
|
160
|
+
matching.append((b.priority + 20, b.command_id))
|
|
161
|
+
elif b.when in (None, "global"):
|
|
162
|
+
matching.append((b.priority, b.command_id))
|
|
163
|
+
elif not context:
|
|
164
|
+
matching.append((b.priority, b.command_id))
|
|
165
|
+
if not matching:
|
|
166
|
+
return None
|
|
167
|
+
# Highest score wins
|
|
168
|
+
matching.sort(key=lambda item: item[0], reverse=True)
|
|
169
|
+
return matching[0][1]
|
|
170
|
+
|
|
171
|
+
def get_keybindings_for_command(self, command_id: str) -> List[str]:
|
|
172
|
+
"""Return all active key combination strings for a command ID."""
|
|
173
|
+
keys = []
|
|
174
|
+
for b in self._bindings:
|
|
175
|
+
if b.command_id == command_id and b.enabled and b.key not in keys:
|
|
176
|
+
keys.append(b.key)
|
|
177
|
+
return keys
|
|
178
|
+
|
|
179
|
+
def list_bindings(self) -> List[Keybinding]:
|
|
180
|
+
"""Return all registered keybindings."""
|
|
181
|
+
return list(self._bindings)
|
|
182
|
+
|
|
183
|
+
def load_user_bindings(self) -> None:
|
|
184
|
+
"""Load user-configured keybinding overrides from ~/.cat_keybindings.json."""
|
|
185
|
+
if not os.path.exists(self.CONFIG_PATH):
|
|
186
|
+
return
|
|
187
|
+
try:
|
|
188
|
+
with open(self.CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
189
|
+
items = json.load(f)
|
|
190
|
+
if isinstance(items, list):
|
|
191
|
+
for item in items:
|
|
192
|
+
cmd = item.get("command_id")
|
|
193
|
+
key = item.get("key")
|
|
194
|
+
if cmd and key:
|
|
195
|
+
# Higher priority for user customizations
|
|
196
|
+
self.register(Keybinding(
|
|
197
|
+
command_id=cmd,
|
|
198
|
+
key=key,
|
|
199
|
+
when=item.get("when"),
|
|
200
|
+
priority=item.get("priority", 50),
|
|
201
|
+
enabled=item.get("enabled", True),
|
|
202
|
+
))
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
def save_user_bindings(self) -> bool:
|
|
207
|
+
"""Persist user custom keybindings to disk."""
|
|
208
|
+
try:
|
|
209
|
+
user_bindings = [asdict(b) for b in self._bindings if b.priority >= 50]
|
|
210
|
+
with open(self.CONFIG_PATH, "w", encoding="utf-8") as f:
|
|
211
|
+
json.dump(user_bindings, f, indent=2)
|
|
212
|
+
return True
|
|
213
|
+
except Exception:
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Global singleton manager
|
|
218
|
+
_GLOBAL_SHORTCUT_MANAGER: Optional[ShortcutManager] = None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_shortcut_manager() -> ShortcutManager:
|
|
222
|
+
"""Return the global ShortcutManager singleton."""
|
|
223
|
+
global _GLOBAL_SHORTCUT_MANAGER
|
|
224
|
+
if _GLOBAL_SHORTCUT_MANAGER is None:
|
|
225
|
+
_GLOBAL_SHORTCUT_MANAGER = ShortcutManager()
|
|
226
|
+
return _GLOBAL_SHORTCUT_MANAGER
|
calc_terminal/engine.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import sys
|
|
3
|
+
import textwrap
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from . import theme
|
|
7
|
+
from .mathtext import compose, mathify
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def term_width(min_w=60, max_w=100, margin=1, fallback=78):
|
|
11
|
+
"""The real, current terminal width, clamped to a sane range.
|
|
12
|
+
|
|
13
|
+
Every box/panel in CCT is drawn at a fixed width — if that width is
|
|
14
|
+
wider than the actual terminal, the border characters wrap onto the
|
|
15
|
+
next line and the box looks broken (this was reported as a "border
|
|
16
|
+
problem": the right-hand `│` shows up in a different, wrong column
|
|
17
|
+
on every row, because every long line wraps at a different point).
|
|
18
|
+
Sizing to the real terminal at startup fixes that in the vast
|
|
19
|
+
majority of cases without needing a live-resize architecture.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
cols = shutil.get_terminal_size(fallback=(fallback, 24)).columns
|
|
23
|
+
except Exception:
|
|
24
|
+
cols = fallback
|
|
25
|
+
return max(min_w, min(max_w, cols - margin))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
WIDTH = term_width()
|
|
29
|
+
|
|
30
|
+
ENGINE_STEPS = [
|
|
31
|
+
"Initializing Chemistry Engine...",
|
|
32
|
+
"Loading Formula Library...",
|
|
33
|
+
"Searching Database...",
|
|
34
|
+
"Checking Units...",
|
|
35
|
+
"Generating Notebook...",
|
|
36
|
+
"Rendering Output...",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
DERIVE_STEPS = [
|
|
40
|
+
"Setting up the differential rate law...",
|
|
41
|
+
"Separating variables...",
|
|
42
|
+
"Integrating both sides...",
|
|
43
|
+
"Simplifying the result...",
|
|
44
|
+
"Typesetting derivation...",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
IMAGE_STEPS = [
|
|
48
|
+
"Reading Image...",
|
|
49
|
+
"Recognizing Formula...",
|
|
50
|
+
"Extracting Text...",
|
|
51
|
+
"Preparing Solution...",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
SPINNER = "\u280b\u2819\u2839\u2838\u283c\u2834\u2826\u2827\u2807\u280f"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_steps(steps, step_time=0.35):
|
|
58
|
+
"""Print a sequential fake-AI processing log with a spinner + checkmark."""
|
|
59
|
+
for step in steps:
|
|
60
|
+
frames = max(3, int(step_time / 0.05))
|
|
61
|
+
for i in range(frames):
|
|
62
|
+
frame = SPINNER[i % len(SPINNER)]
|
|
63
|
+
sys.stdout.write(f"\r {theme.cyan(frame)} {theme.dim(step)}" + " " * 10)
|
|
64
|
+
sys.stdout.flush()
|
|
65
|
+
time.sleep(0.05)
|
|
66
|
+
sys.stdout.write(f"\r {theme.green(chr(0x2713))} {theme.dim(step)}" + " " * 10 + "\n")
|
|
67
|
+
sys.stdout.flush()
|
|
68
|
+
print(f" {theme.green(chr(0x2713) + ' Completed', bold=True)}")
|
|
69
|
+
time.sleep(0.15)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _wrap(s, width=WIDTH - 10):
|
|
73
|
+
return textwrap.wrap(s, width=width) or [""]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------- reveal --
|
|
77
|
+
# Animation is on by default; app.py can flip this off (e.g. non-tty runs
|
|
78
|
+
# or a user "fast mode" setting) without touching call sites.
|
|
79
|
+
ANIMATE = True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def set_animation(on):
|
|
83
|
+
global ANIMATE
|
|
84
|
+
ANIMATE = bool(on)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _border(width, top=True, color=theme.CYAN):
|
|
88
|
+
ch = theme.TL + theme.H * (width - 2) + theme.TR if top else theme.BL + theme.H * (width - 2) + theme.BR
|
|
89
|
+
return theme.fg(ch, color)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _row(text_line, width, color=theme.CYAN):
|
|
93
|
+
inner = width - 2
|
|
94
|
+
pad = max(0, inner - theme.vlen(text_line) - 2)
|
|
95
|
+
return theme.fg(theme.V, color) + " " + text_line + " " * pad + theme.fg(theme.V, color)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _title_row(title, width, color):
|
|
99
|
+
inner = width - 2
|
|
100
|
+
tag = f" {title} "
|
|
101
|
+
left = 3
|
|
102
|
+
right = max(0, inner - left - theme.vlen(tag))
|
|
103
|
+
return theme.fg(theme.TL + theme.H * left, color) + theme.fg(tag, color, bold=True) + theme.fg(theme.H * right + theme.TR, color)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def animate_panel(lines, title=None, color=theme.CYAN, width=WIDTH, stage_pause=0.10, line_pause=0.014):
|
|
107
|
+
"""Reveal a panel progressively: border draws in, then each line
|
|
108
|
+
fades in top-to-bottom, with a slightly longer pause on section
|
|
109
|
+
headers (lines containing the bullet marker) so every response
|
|
110
|
+
feels like it's being worked out live rather than dumped instantly.
|
|
111
|
+
Falls back to an instant static panel when ANIMATE is off."""
|
|
112
|
+
if not ANIMATE:
|
|
113
|
+
print(theme.panel(lines, title=title, color=color, width=width))
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
print(_title_row(title, width, color) if title else _border(width, top=True, color=color))
|
|
117
|
+
for line in lines:
|
|
118
|
+
print(_row(line, width, color))
|
|
119
|
+
time.sleep(stage_pause if "\u25cf" in line else line_pause)
|
|
120
|
+
print(_border(width, top=False, color=color))
|
|
121
|
+
except Exception:
|
|
122
|
+
# any terminal-width edge case -> never break the actual output
|
|
123
|
+
print(theme.panel(lines, title=title, color=color, width=width))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def render_notebook(nb):
|
|
127
|
+
"""Print a full step-by-step notebook panel for a generated numerical,
|
|
128
|
+
revealed one stage at a time (animated)."""
|
|
129
|
+
lines = []
|
|
130
|
+
lines.append(theme.purple(nb["topic"].upper(), bold=True))
|
|
131
|
+
for w in _wrap(nb["question"]):
|
|
132
|
+
lines.append(theme.text(w, bold=True))
|
|
133
|
+
lines.append("")
|
|
134
|
+
|
|
135
|
+
def stage(label, body_lines, final=False):
|
|
136
|
+
color = theme.GREEN if final else theme.CYAN
|
|
137
|
+
lines.append(theme.fg("\u25cf " + label, color, bold=True))
|
|
138
|
+
for bl in body_lines:
|
|
139
|
+
lines.append(" " + bl)
|
|
140
|
+
lines.append("")
|
|
141
|
+
|
|
142
|
+
stage("GIVEN DATA", [theme.dim("\u2022 ") + theme.text(g) for g in nb["given"]])
|
|
143
|
+
stage("FIND", [theme.text(nb["find"])])
|
|
144
|
+
stage("FORMULA", [theme.text(l) for l in compose(nb["formula"]).split("\n")])
|
|
145
|
+
stage("SUBSTITUTION", [theme.text(l) for l in compose(nb["substitution"]).split("\n")])
|
|
146
|
+
stage("CALCULATION", [theme.dim(f"{i+1}. ") + theme.text(mathify(c)) for i, c in enumerate(nb["calculation"])])
|
|
147
|
+
|
|
148
|
+
unit_lines = []
|
|
149
|
+
for w in _wrap(mathify(nb["unit"])):
|
|
150
|
+
unit_lines.append(theme.dim(w))
|
|
151
|
+
stage("UNITS", unit_lines)
|
|
152
|
+
|
|
153
|
+
ver_lines = []
|
|
154
|
+
for w in _wrap(mathify(nb["verification"])):
|
|
155
|
+
ver_lines.append(theme.dim(w))
|
|
156
|
+
stage("VERIFICATION", ver_lines)
|
|
157
|
+
|
|
158
|
+
stage("FINAL ANSWER", [theme.green(nb["final_answer"], bold=True)], final=True)
|
|
159
|
+
|
|
160
|
+
animate_panel(lines, title="notebook", color=theme.CYAN, width=WIDTH)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
import re as _re
|
|
164
|
+
|
|
165
|
+
# A header line is either a short markdown-ish "## SOME HEADER" line, or a
|
|
166
|
+
# short, mostly-uppercase line (with or without a trailing colon) — the
|
|
167
|
+
# style CCT Agent's system prompt asks the model to write in (GIVEN DATA,
|
|
168
|
+
# FORMULA, FINAL ANSWER...). Kept permissive since it's parsing free-form
|
|
169
|
+
# LLM text, not a fixed schema.
|
|
170
|
+
_AGENT_HEADER_RE = _re.compile(
|
|
171
|
+
r"^\s*(?:#{1,4}\s*|\*\*)?([A-Z][A-Z0-9 /&()\-]{2,40}?)(?:\*\*)?\s*:?\s*$"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _is_agent_header(line):
|
|
176
|
+
s = line.strip()
|
|
177
|
+
if not s or len(s) > 48:
|
|
178
|
+
return None
|
|
179
|
+
m = _AGENT_HEADER_RE.match(s)
|
|
180
|
+
if not m:
|
|
181
|
+
return None
|
|
182
|
+
label = m.group(1).strip()
|
|
183
|
+
letters = [c for c in label if c.isalpha()]
|
|
184
|
+
if len(letters) < 3:
|
|
185
|
+
return None
|
|
186
|
+
# Require it to actually look shouted (mostly uppercase), not just any
|
|
187
|
+
# short capitalized sentence fragment.
|
|
188
|
+
upper = sum(1 for c in letters if c.isupper())
|
|
189
|
+
if upper / len(letters) < 0.8:
|
|
190
|
+
return None
|
|
191
|
+
return label
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def render_agent_answer(text, tag="CAT Agent", color=theme.PURPLE):
|
|
195
|
+
"""Render CAT Agent's free-form "final" text as a notebook-style
|
|
196
|
+
panel — detecting the ALL-CAPS section headers (GIVEN DATA, FORMULA,
|
|
197
|
+
FINAL ANSWER, OVERVIEW, EXPLANATION...) the agent's system prompt asks
|
|
198
|
+
for, and falling back to a single plain ANSWER section for any text
|
|
199
|
+
that doesn't use them (e.g. a short aside or an older-style reply), so
|
|
200
|
+
/agent always reads like a worked notebook page rather than a wall of
|
|
201
|
+
unstructured prose."""
|
|
202
|
+
if not text or not text.strip():
|
|
203
|
+
return
|
|
204
|
+
raw_lines = text.strip().split("\n")
|
|
205
|
+
sections = [] # [(header, [body_lines])]
|
|
206
|
+
current_body = []
|
|
207
|
+
for line in raw_lines:
|
|
208
|
+
header = _is_agent_header(line)
|
|
209
|
+
if header:
|
|
210
|
+
sections.append([header, []])
|
|
211
|
+
current_body = sections[-1][1]
|
|
212
|
+
else:
|
|
213
|
+
if not sections:
|
|
214
|
+
sections.append(["ANSWER", []])
|
|
215
|
+
current_body = sections[-1][1]
|
|
216
|
+
if line.strip():
|
|
217
|
+
current_body.append(line.strip())
|
|
218
|
+
|
|
219
|
+
lines = [theme.badge(tag, theme.BG_AI, fgcolor=(225, 210, 255)), ""]
|
|
220
|
+
for header, body in sections:
|
|
221
|
+
is_final = "FINAL" in header
|
|
222
|
+
hcolor = theme.GREEN if is_final else theme.CYAN
|
|
223
|
+
lines.append(theme.fg("\u25cf " + header, hcolor, bold=True))
|
|
224
|
+
if not body:
|
|
225
|
+
body = [""]
|
|
226
|
+
for b in body:
|
|
227
|
+
for w in _wrap(b):
|
|
228
|
+
lines.append(" " + (theme.green(w, bold=True) if is_final else theme.text(w)))
|
|
229
|
+
lines.append("")
|
|
230
|
+
|
|
231
|
+
animate_panel(lines, title="agent notebook", color=color, width=WIDTH, stage_pause=0.10)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def render_derivation(dv):
|
|
235
|
+
"""Print a full step-by-step DERIVATION panel (real calculus, not a
|
|
236
|
+
numeric example) for the given derivation dict from derivations.py,
|
|
237
|
+
revealed step-by-step so the logic visibly builds instead of
|
|
238
|
+
appearing all at once."""
|
|
239
|
+
lines = []
|
|
240
|
+
lines.append(theme.purple(dv["topic"].upper(), bold=True))
|
|
241
|
+
for w in _wrap(dv["goal"]):
|
|
242
|
+
lines.append(theme.text(w, bold=True))
|
|
243
|
+
lines.append("")
|
|
244
|
+
|
|
245
|
+
for i, (label, parts) in enumerate(dv["steps"], 1):
|
|
246
|
+
lines.append(theme.fg(f"\u25cf Step {i} \u2014 {label}", theme.CYAN, bold=True))
|
|
247
|
+
for l in compose(parts).split("\n"):
|
|
248
|
+
lines.append(" " + theme.text(l))
|
|
249
|
+
lines.append("")
|
|
250
|
+
|
|
251
|
+
lines.append(theme.fg("\u25cf RESULT", theme.GREEN, bold=True))
|
|
252
|
+
lines.append(" " + theme.green(dv["result"], bold=True))
|
|
253
|
+
lines.append("")
|
|
254
|
+
|
|
255
|
+
lines.append(theme.fg("\u25cf WHAT IT MEANS", theme.ORANGE, bold=True))
|
|
256
|
+
for w in _wrap(dv["note"]):
|
|
257
|
+
lines.append(" " + theme.dim(w))
|
|
258
|
+
|
|
259
|
+
animate_panel(lines, title="derivation", color=theme.PURPLE, width=WIDTH, stage_pause=0.16)
|