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,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT UI — thinking.py: the "Advanced AI Response Animation System" (spec
|
|
3
|
+
item #25). Pure logic + formatting helpers only — no Textual dependency,
|
|
4
|
+
no widgets. composer.py's live status bar and conversation.py's
|
|
5
|
+
completion-summary line are the only two things that render what this
|
|
6
|
+
module computes; this file just decides *what* to show (stage label,
|
|
7
|
+
color, spinner frame, elapsed-time text) and never *how* to paint it.
|
|
8
|
+
|
|
9
|
+
Kept dependency-free on purpose: stage detection and timer formatting
|
|
10
|
+
are simple enough to unit test directly, and staying importable outside
|
|
11
|
+
Textual means the fallback CLI could reuse the same stage vocabulary
|
|
12
|
+
later without dragging in ui/ at all.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
print("This is a library file and is not meant to be run directly.")
|
|
17
|
+
import sys
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------- spinner styles
|
|
23
|
+
# Animation Style 1 / 2 / 3 from the spec.
|
|
24
|
+
DOT_FRAMES = ["\u25cf\u25cb\u25cb\u25cb\u25cb", "\u25cf\u25cf\u25cb\u25cb\u25cb",
|
|
25
|
+
"\u25cf\u25cf\u25cf\u25cb\u25cb", "\u25cf\u25cf\u25cf\u25cf\u25cb",
|
|
26
|
+
"\u25cf\u25cf\u25cf\u25cf\u25cf"]
|
|
27
|
+
BRAILLE_FRAMES = ["\u280b", "\u2819", "\u2839", "\u2838", "\u283c", "\u2834",
|
|
28
|
+
"\u2826", "\u2827", "\u2807", "\u280f"]
|
|
29
|
+
BLOCK_FRAMES = ["\u25a1\u25a1\u25a1\u25a1\u25a1\u25a1\u25a1\u25a1",
|
|
30
|
+
"\u25a0\u25a0\u25a1\u25a1\u25a1\u25a1\u25a1\u25a1",
|
|
31
|
+
"\u25a0\u25a0\u25a0\u25a0\u25a1\u25a1\u25a1\u25a1",
|
|
32
|
+
"\u25a0\u25a0\u25a0\u25a0\u25a0\u25a0\u25a1\u25a1",
|
|
33
|
+
"\u25a0\u25a0\u25a0\u25a0\u25a0\u25a0\u25a0\u25a0"]
|
|
34
|
+
|
|
35
|
+
STYLES = {"dots": DOT_FRAMES, "braille": BRAILLE_FRAMES, "blocks": BLOCK_FRAMES}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def spinner_frame(style, tick):
|
|
39
|
+
"""Loops continuously, per spec ('Loop until completion')."""
|
|
40
|
+
frames = STYLES.get(style, BRAILLE_FRAMES)
|
|
41
|
+
return frames[tick % len(frames)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --------------------------------------------------------------- stages --
|
|
45
|
+
# stage_key -> (display label, COLOR ROLE, spinner style).
|
|
46
|
+
#
|
|
47
|
+
# v0.7.9.0 LIGHT-MODE FIX: the second tuple slot used to be a hardcoded
|
|
48
|
+
# Tokyo-Night hex (#7aa2f7, #9ece6a, #e0af68...). Those pastels are
|
|
49
|
+
# unreadable on the light theme's white surfaces — several fall below
|
|
50
|
+
# 2:1 contrast. Colors are now semantic ROLES resolved through
|
|
51
|
+
# theme.role_hex() at CALL time (see stage_info), so dark keeps its
|
|
52
|
+
# exact Tokyo Night values and light automatically gets its darker,
|
|
53
|
+
# readable palette. `style` is just which of the three animation styles
|
|
54
|
+
# best fits that stage's feel.
|
|
55
|
+
STAGES = {
|
|
56
|
+
"preparing": ("Preparing", "blue", "braille"),
|
|
57
|
+
"thinking": ("Thinking", "blue", "braille"),
|
|
58
|
+
"web": ("Searching Web", "cyan", "dots"),
|
|
59
|
+
"knowledge": ("Searching Knowledge", "cyan", "dots"),
|
|
60
|
+
"notebook": ("Searching Notebook", "cyan", "dots"),
|
|
61
|
+
"formula_db": ("Searching Formula Database", "cyan", "dots"),
|
|
62
|
+
"quantum_lib": ("Searching Quantum Library", "cyan", "dots"),
|
|
63
|
+
"analyzing": ("Analyzing Question", "blue", "braille"),
|
|
64
|
+
"calc": ("Running Calculation", "amber", "blocks"),
|
|
65
|
+
"simulation": ("Running Simulation", "purple", "blocks"),
|
|
66
|
+
"sim3d": ("Rendering 3D Simulation", "purple", "blocks"),
|
|
67
|
+
"graph": ("Rendering Graph", "purple", "blocks"),
|
|
68
|
+
"html": ("Generating HTML", "magenta", "braille"),
|
|
69
|
+
"code": ("Executing Code", "magenta", "braille"),
|
|
70
|
+
"building": ("Building Response", "blue", "braille"),
|
|
71
|
+
"formatting": ("Formatting Result", "blue", "dots"),
|
|
72
|
+
"finalizing": ("Finalizing", "blue", "dots"),
|
|
73
|
+
"debugging": ("Debugging", "red", "braille"),
|
|
74
|
+
"installing": ("Installing Package", "amber", "blocks"),
|
|
75
|
+
"done": ("Completed", "green", "dots"),
|
|
76
|
+
"error": ("Error", "red", "dots"),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Role -> theme.color-role used by theme.role_hex(). "magenta" maps to
|
|
80
|
+
# the purple family in both palettes.
|
|
81
|
+
_ROLE_MAP = {
|
|
82
|
+
"blue": "blue", "cyan": "cyan", "purple": "purple",
|
|
83
|
+
"magenta": "purple", "amber": "amber", "green": "green",
|
|
84
|
+
"red": "red", "gray": "gray",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
DEFAULT_STAGE = "thinking"
|
|
88
|
+
|
|
89
|
+
# Elapsed-time fallback used once no keyword signal fires — keeps the
|
|
90
|
+
# indicator visibly alive on a plain, keyword-free question instead of
|
|
91
|
+
# freezing on one word the whole time it's waiting on the model.
|
|
92
|
+
_PRE_TOKEN_PROGRESSION = ["preparing", "thinking", "analyzing"]
|
|
93
|
+
_POST_TOKEN_PROGRESSION = ["building", "formatting", "finalizing"]
|
|
94
|
+
|
|
95
|
+
# "Smart Status Detection": prompt keywords -> stage. Order matters —
|
|
96
|
+
# first match wins, most specific first.
|
|
97
|
+
_KEYWORD_STAGES = [
|
|
98
|
+
(("/research", "deep research"), "web"),
|
|
99
|
+
(("/websearch", "web search", "search the web", "search online"), "web"),
|
|
100
|
+
(("/install", "/packages", "install ", "pip install", "npm install", "setup "), "installing"),
|
|
101
|
+
(("/debug", "debug this", "fix this bug", "error message", "traceback", "why is this failing"), "debugging"),
|
|
102
|
+
(("/pipeline", "build me a", "build us an", "build a full"), "building"),
|
|
103
|
+
(("/orbitals", "orbital", "quantum library", "electron cloud"), "quantum_lib"),
|
|
104
|
+
(("/atomsim", "/sim3d", "3d simulation", "atomic simulation"), "sim3d"),
|
|
105
|
+
(("/react", "reaction sim", "kinetics ode", "equilibrium ode", "simulate"), "simulation"),
|
|
106
|
+
(("/graph", "plot ", "plot a", "graph of", "draw a graph"), "graph"),
|
|
107
|
+
(("/codepad", "```", "write code", "generate code", "run python"), "code"),
|
|
108
|
+
(("render html", "generate html", "html report"), "html"),
|
|
109
|
+
(("/formulas", "formula database", "formula library"), "formula_db"),
|
|
110
|
+
(("/history", "notebook", "past numericals"), "notebook"),
|
|
111
|
+
(("/solve", "calculate", "compute", "solve for", "/calculator"), "calc"),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def detect_stage(prompt, elapsed, chunk_count=0):
|
|
116
|
+
"""Best-effort stage for a moment in time. `prompt` drives an early,
|
|
117
|
+
strong keyword match (what's about to happen is real signal); once
|
|
118
|
+
tokens are already streaming (`chunk_count > 0`) with nothing more
|
|
119
|
+
specific detected, elapsed time alone walks the generic
|
|
120
|
+
building -> formatting -> finalizing tail so the status bar keeps
|
|
121
|
+
moving instead of sitting still for the rest of the response."""
|
|
122
|
+
text = (prompt or "").lower()
|
|
123
|
+
for keywords, stage in _KEYWORD_STAGES:
|
|
124
|
+
if any(k in text for k in keywords):
|
|
125
|
+
return stage
|
|
126
|
+
|
|
127
|
+
progression = _POST_TOKEN_PROGRESSION if chunk_count else _PRE_TOKEN_PROGRESSION
|
|
128
|
+
idx = min(int(elapsed // 1.4), len(progression) - 1)
|
|
129
|
+
return progression[idx]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def stage_info(stage_key):
|
|
133
|
+
"""(label, hex_color, spinner_style) for a stage key, defaulting to
|
|
134
|
+
'thinking' for anything unrecognized so callers never need a guard.
|
|
135
|
+
|
|
136
|
+
The hex is resolved from the CURRENT theme at call time (dark =
|
|
137
|
+
Tokyo Night values as before; light = its darker, readable
|
|
138
|
+
palette), so a live /theme switch recolors every spinner/status on
|
|
139
|
+
the next tick without any extra wiring."""
|
|
140
|
+
label, role, style = STAGES.get(stage_key, STAGES[DEFAULT_STAGE])
|
|
141
|
+
try:
|
|
142
|
+
from .. import theme
|
|
143
|
+
color = theme.role_hex(_ROLE_MAP.get(role, "blue"))
|
|
144
|
+
except Exception:
|
|
145
|
+
# Dependency-free fallbacks matching the DARK theme (the
|
|
146
|
+
# classic terminal may import this module before theme setup).
|
|
147
|
+
_DARK_FALLBACK = {"blue": "#7aa2f7", "cyan": "#7dcfff",
|
|
148
|
+
"purple": "#bb9af7", "magenta": "#bb9af7",
|
|
149
|
+
"amber": "#e0af68", "green": "#9ece6a",
|
|
150
|
+
"red": "#f7768e"}
|
|
151
|
+
color = _DARK_FALLBACK.get(role, "#7aa2f7")
|
|
152
|
+
return label, color, style
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def format_timer(elapsed):
|
|
156
|
+
"""0.1 s / 1.2 s / 5.8 s under a minute; mm:ss beyond, matching the
|
|
157
|
+
spec's live-timer examples exactly for the common case."""
|
|
158
|
+
if elapsed < 0:
|
|
159
|
+
elapsed = 0.0
|
|
160
|
+
if elapsed < 60:
|
|
161
|
+
return f"{elapsed:.1f} s"
|
|
162
|
+
minutes, seconds = divmod(int(elapsed), 60)
|
|
163
|
+
return f"{minutes}:{seconds:02d}"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------- idle chemistry icons
|
|
167
|
+
IDLE_ICONS = ["\u269b", "\U0001f9ea", "\U0001f9ec", "\u2601"] # ⚛ 🧪 🧬 ☁
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def idle_icon(tick):
|
|
171
|
+
return IDLE_ICONS[tick % len(IDLE_ICONS)]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# --------------------------------------------------------- command usage --
|
|
175
|
+
_SLASH_CMD_RE = re.compile(r"(?<!\S)/[a-zA-Z][a-zA-Z\-]*")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def commands_used(*texts):
|
|
179
|
+
"""Slash commands mentioned across the given texts (prompt +
|
|
180
|
+
response), de-duplicated and sorted, for the completion summary's
|
|
181
|
+
'Commands Used' line."""
|
|
182
|
+
found = set()
|
|
183
|
+
for text in texts:
|
|
184
|
+
if not text:
|
|
185
|
+
continue
|
|
186
|
+
found.update(m.group(0) for m in _SLASH_CMD_RE.finditer(text))
|
|
187
|
+
return sorted(found)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ------------------------------------------------------- completion block
|
|
191
|
+
def completion_summary_lines(*, provider=None, model=None, duration=None,
|
|
192
|
+
tokens=None, commands=None, extras=None):
|
|
193
|
+
"""Rich-markup lines for the 'Message Completion' block the spec
|
|
194
|
+
describes (✓ Response Complete / Provider / Model / Time / Tokens /
|
|
195
|
+
Commands Used / feature flags). Returns a list of markup strings —
|
|
196
|
+
rendering them is the caller's job (conversation.py turns each into
|
|
197
|
+
a Text.from_markup line under the finished bubble).
|
|
198
|
+
|
|
199
|
+
v0.7.9.0: the ✓ color resolves from the CURRENT theme (was a
|
|
200
|
+
hardcoded dark-palette green that washed out in light mode)."""
|
|
201
|
+
try:
|
|
202
|
+
from .. import theme
|
|
203
|
+
ok_hex = theme.role_hex("green")
|
|
204
|
+
except Exception:
|
|
205
|
+
ok_hex = "#9ece6a"
|
|
206
|
+
lines = [f"[{ok_hex}]\u2713 Response Complete[/]"]
|
|
207
|
+
|
|
208
|
+
meta = []
|
|
209
|
+
if provider:
|
|
210
|
+
meta.append(f"Provider: {provider}")
|
|
211
|
+
if model:
|
|
212
|
+
meta.append(f"Model: {model}")
|
|
213
|
+
if duration is not None:
|
|
214
|
+
meta.append(f"Time: {format_timer(duration)}")
|
|
215
|
+
if tokens:
|
|
216
|
+
meta.append(f"Tokens: {tokens}")
|
|
217
|
+
if meta:
|
|
218
|
+
lines.append("[dim]" + " \u00b7 ".join(meta) + "[/dim]")
|
|
219
|
+
|
|
220
|
+
if commands:
|
|
221
|
+
lines.append("[dim]Commands used: " + ", ".join(commands) + "[/dim]")
|
|
222
|
+
|
|
223
|
+
if extras:
|
|
224
|
+
lines.append("[dim]" + " \u00b7 ".join(extras) + "[/dim]")
|
|
225
|
+
|
|
226
|
+
return lines
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT UI — timeline_panel.py: Session Timeline panel (v0.7.2 roadmap).
|
|
3
|
+
Real Screen push (same family as context_menu.py/header.py's
|
|
4
|
+
PermissionModeMenu — this package's standing "no floating popup
|
|
5
|
+
windows" rule), showing calc_terminal/timeline.py's event log with a
|
|
6
|
+
live search box and per-topic filter chips. "Jump to event" opens the
|
|
7
|
+
related file in the Editor if it still exists on disk; "Restore
|
|
8
|
+
previous state" is the roadmap's own named FUTURE feature and isn't
|
|
9
|
+
implemented here.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
print("This is a library file and is not meant to be run directly.")
|
|
14
|
+
import sys
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
from .. import timeline
|
|
20
|
+
from .events import FileOpenRequested
|
|
21
|
+
|
|
22
|
+
TEXTUAL_AVAILABLE = True
|
|
23
|
+
try:
|
|
24
|
+
from textual.containers import Vertical, Horizontal, VerticalScroll
|
|
25
|
+
from textual.screen import Screen
|
|
26
|
+
from textual.widgets import Static, Input, Button
|
|
27
|
+
except Exception:
|
|
28
|
+
TEXTUAL_AVAILABLE = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if TEXTUAL_AVAILABLE:
|
|
32
|
+
|
|
33
|
+
class _TimelineRow(Static):
|
|
34
|
+
def __init__(self, event):
|
|
35
|
+
super().__init__(event.line(), classes="cct-timeline-row")
|
|
36
|
+
self._event = event
|
|
37
|
+
|
|
38
|
+
def on_click(self, ev):
|
|
39
|
+
if self._event.related_file and os.path.isfile(self._event.related_file):
|
|
40
|
+
self.post_message(FileOpenRequested(self._event.related_file))
|
|
41
|
+
|
|
42
|
+
class TimelinePanel(Screen):
|
|
43
|
+
"""Full event log. `_filter_topic`, when set, narrows to one
|
|
44
|
+
eventbus topic; the search box narrows further by substring
|
|
45
|
+
(timeline.search) on top of that. Both apply together, list
|
|
46
|
+
re-rendered via _render_rows() rather than a full recompose."""
|
|
47
|
+
|
|
48
|
+
CSS = """
|
|
49
|
+
TimelinePanel { align: center middle; background: $app-background 60%; }
|
|
50
|
+
#cct-timeline-box {
|
|
51
|
+
width: 84%; height: 80%; background: $surface;
|
|
52
|
+
border: round $border; padding: 1 2;
|
|
53
|
+
}
|
|
54
|
+
#cct-timeline-header { height: 1; }
|
|
55
|
+
#cct-timeline-search { margin-bottom: 1; }
|
|
56
|
+
#cct-timeline-list { height: 1fr; }
|
|
57
|
+
.cct-timeline-row { height: 1; color: $text; padding: 0 1; }
|
|
58
|
+
.cct-timeline-row:hover { color: $accent; }
|
|
59
|
+
#cct-timeline-empty { color: $text-faint; padding: 1; }
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
super().__init__()
|
|
64
|
+
self._query = ""
|
|
65
|
+
|
|
66
|
+
def compose(self):
|
|
67
|
+
with Vertical(id="cct-timeline-box"):
|
|
68
|
+
with Horizontal(id="cct-timeline-header"):
|
|
69
|
+
yield Static("[b]Session Timeline[/b]")
|
|
70
|
+
yield Button("Close", id="cct-timeline-close")
|
|
71
|
+
yield Input(placeholder="Search timeline\u2026", id="cct-timeline-search")
|
|
72
|
+
yield VerticalScroll(id="cct-timeline-list")
|
|
73
|
+
|
|
74
|
+
def on_mount(self):
|
|
75
|
+
self._render_rows()
|
|
76
|
+
|
|
77
|
+
def _render_rows(self):
|
|
78
|
+
box = self.query_one("#cct-timeline-list", VerticalScroll)
|
|
79
|
+
for child in list(box.children):
|
|
80
|
+
child.remove()
|
|
81
|
+
evs = timeline.search(self._query)
|
|
82
|
+
if not evs:
|
|
83
|
+
box.mount(Static("No matching activity yet.", id="cct-timeline-empty"))
|
|
84
|
+
return
|
|
85
|
+
for ev in evs[:200]:
|
|
86
|
+
box.mount(_TimelineRow(ev))
|
|
87
|
+
|
|
88
|
+
def on_input_changed(self, event):
|
|
89
|
+
if event.input.id == "cct-timeline-search":
|
|
90
|
+
self._query = event.value
|
|
91
|
+
self._render_rows()
|
|
92
|
+
|
|
93
|
+
def on_button_pressed(self, event):
|
|
94
|
+
if event.button.id == "cct-timeline-close":
|
|
95
|
+
self.dismiss(None)
|
|
96
|
+
|
|
97
|
+
def on_key(self, event):
|
|
98
|
+
if event.key == "escape":
|
|
99
|
+
self.dismiss(None)
|
|
100
|
+
|
|
101
|
+
else:
|
|
102
|
+
TimelinePanel = None
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT UI — todo_panel.py: AI Todo Manager panel (v0.7.2 roadmap). Real
|
|
3
|
+
Screen push over calc_terminal/todos.py's per-workspace JSON store —
|
|
4
|
+
add, complete/reopen, delete, search, and filter by status, plus a
|
|
5
|
+
live progress bar. Priority is settable at creation (cycles low/
|
|
6
|
+
normal/high via a click on the priority badge); due dates and full
|
|
7
|
+
in-place text editing are accepted from todos.update_todo() but this
|
|
8
|
+
pass's UI only exposes add/toggle/delete/priority-cycle/search/filter
|
|
9
|
+
— see the module docstring in todos.py for what's genuinely
|
|
10
|
+
implemented vs. deferred.
|
|
11
|
+
|
|
12
|
+
The roadmap's own mockup nests this under a "Main Menu" (Workspace /
|
|
13
|
+
Recent Projects / AI Todo / Settings / Extensions / Documentation)
|
|
14
|
+
that doesn't exist anywhere in this codebase yet — header.py's
|
|
15
|
+
hamburger glyph is a plain sidebar-visibility toggle, and Settings/
|
|
16
|
+
Extensions/Documentation aren't panels here either. Building a whole
|
|
17
|
+
new main-menu hierarchy just to host this one panel would be scope
|
|
18
|
+
creep well beyond "add a Todo Manager", so this is exposed the same
|
|
19
|
+
way TimelinePanel is: a dedicated `/todo` command, following this
|
|
20
|
+
codebase's existing pattern for every other panel (/permissions,
|
|
21
|
+
/workspace, /model, etc.) rather than a menu system that has no other
|
|
22
|
+
reason to exist yet.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
print("This is a library file and is not meant to be run directly.")
|
|
27
|
+
import sys
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
from .. import todos
|
|
31
|
+
|
|
32
|
+
TEXTUAL_AVAILABLE = True
|
|
33
|
+
try:
|
|
34
|
+
from textual.containers import Vertical, Horizontal, VerticalScroll
|
|
35
|
+
from textual.screen import Screen
|
|
36
|
+
from textual.widgets import Static, Input, Button, ProgressBar
|
|
37
|
+
except Exception:
|
|
38
|
+
TEXTUAL_AVAILABLE = False
|
|
39
|
+
|
|
40
|
+
_PRIORITY_GLYPH = {"low": "\u25bd", "normal": "\u25cb", "high": "\u25b3"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if TEXTUAL_AVAILABLE:
|
|
44
|
+
|
|
45
|
+
class _TodoRow(Horizontal):
|
|
46
|
+
def __init__(self, todo):
|
|
47
|
+
super().__init__(classes="cct-todo-row")
|
|
48
|
+
self.todo = todo
|
|
49
|
+
|
|
50
|
+
def compose(self):
|
|
51
|
+
box = "\u2611" if self.todo["status"] == "done" else "\u2610"
|
|
52
|
+
style = "strike dim" if self.todo["status"] == "done" else ""
|
|
53
|
+
source = " [dim](AI)[/]" if self.todo.get("source") == "ai" else ""
|
|
54
|
+
yield Static(box, classes="cct-todo-check", id=f"cct-todo-check-{self.todo['id']}")
|
|
55
|
+
yield Static(_PRIORITY_GLYPH.get(self.todo["priority"], "\u25cb"),
|
|
56
|
+
classes="cct-todo-priority", id=f"cct-todo-pri-{self.todo['id']}")
|
|
57
|
+
label = f"[{style}]{self.todo['text']}[/]" if style else self.todo["text"]
|
|
58
|
+
yield Static(label + source, classes="cct-todo-text")
|
|
59
|
+
yield Button("\u2715", classes="cct-todo-delete", id=f"cct-todo-del-{self.todo['id']}")
|
|
60
|
+
|
|
61
|
+
class TodoPanel(Screen):
|
|
62
|
+
"""AI Todo Manager. `workspace` scopes the list to the currently
|
|
63
|
+
open project (or None for the global/no-folder list) — see
|
|
64
|
+
todos.py's module docstring on why todos are per-workspace."""
|
|
65
|
+
|
|
66
|
+
CSS = """
|
|
67
|
+
TodoPanel { align: center middle; background: $app-background 60%; }
|
|
68
|
+
#cct-todo-box {
|
|
69
|
+
width: 84%; height: 80%; background: $surface;
|
|
70
|
+
border: round $border; padding: 1 2;
|
|
71
|
+
}
|
|
72
|
+
#cct-todo-header { height: 1; }
|
|
73
|
+
#cct-todo-progress-row { height: 1; padding: 0 0 1 0; }
|
|
74
|
+
#cct-todo-add-row { height: 3; padding-bottom: 1; }
|
|
75
|
+
#cct-todo-add-row Input { width: 1fr; }
|
|
76
|
+
#cct-todo-filters { height: 1; padding-bottom: 1; }
|
|
77
|
+
#cct-todo-filters Button { margin-right: 1; min-width: 10; }
|
|
78
|
+
#cct-todo-search { margin-bottom: 1; }
|
|
79
|
+
#cct-todo-list { height: 1fr; }
|
|
80
|
+
.cct-todo-row { height: 1; padding: 0 1; }
|
|
81
|
+
.cct-todo-row:hover { color: $accent; }
|
|
82
|
+
.cct-todo-check { width: 3; }
|
|
83
|
+
.cct-todo-priority { width: 3; color: $warning; }
|
|
84
|
+
.cct-todo-text { width: 1fr; }
|
|
85
|
+
.cct-todo-delete { min-width: 3; height: 1; border: none; color: $error; }
|
|
86
|
+
#cct-todo-empty { color: $text-faint; padding: 1; }
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, workspace=None):
|
|
90
|
+
super().__init__()
|
|
91
|
+
self._workspace = workspace
|
|
92
|
+
self._query = ""
|
|
93
|
+
self._filter = "all" # all | pending | done
|
|
94
|
+
|
|
95
|
+
def compose(self):
|
|
96
|
+
with Vertical(id="cct-todo-box"):
|
|
97
|
+
with Horizontal(id="cct-todo-header"):
|
|
98
|
+
yield Static("[b]AI Todo[/b]")
|
|
99
|
+
yield Button("Close", id="cct-todo-close")
|
|
100
|
+
with Horizontal(id="cct-todo-progress-row"):
|
|
101
|
+
yield Static("", id="cct-todo-progress-label")
|
|
102
|
+
yield ProgressBar(id="cct-todo-progress", show_eta=False)
|
|
103
|
+
with Horizontal(id="cct-todo-add-row"):
|
|
104
|
+
yield Input(placeholder="Add a task and press Enter\u2026", id="cct-todo-add")
|
|
105
|
+
with Horizontal(id="cct-todo-filters"):
|
|
106
|
+
yield Button("All", id="cct-todo-filter-all", variant="primary")
|
|
107
|
+
yield Button("Pending", id="cct-todo-filter-pending")
|
|
108
|
+
yield Button("Done", id="cct-todo-filter-done")
|
|
109
|
+
yield Input(placeholder="Search\u2026", id="cct-todo-search")
|
|
110
|
+
yield VerticalScroll(id="cct-todo-list")
|
|
111
|
+
|
|
112
|
+
def on_mount(self):
|
|
113
|
+
self._render()
|
|
114
|
+
|
|
115
|
+
def _render(self):
|
|
116
|
+
frac, done, total = todos.progress(self._workspace)
|
|
117
|
+
self.query_one("#cct-todo-progress-label", Static).update(
|
|
118
|
+
f"{done}/{total} complete" if total else "No tasks yet")
|
|
119
|
+
try:
|
|
120
|
+
self.query_one("#cct-todo-progress", ProgressBar).update(total=1.0, progress=frac)
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
items = todos.list_todos(self._workspace)
|
|
124
|
+
if self._filter == "pending":
|
|
125
|
+
items = [t for t in items if t["status"] != "done"]
|
|
126
|
+
elif self._filter == "done":
|
|
127
|
+
items = [t for t in items if t["status"] == "done"]
|
|
128
|
+
if self._query:
|
|
129
|
+
q = self._query.lower()
|
|
130
|
+
items = [t for t in items if q in t["text"].lower()]
|
|
131
|
+
box = self.query_one("#cct-todo-list", VerticalScroll)
|
|
132
|
+
for child in list(box.children):
|
|
133
|
+
child.remove()
|
|
134
|
+
if not items:
|
|
135
|
+
box.mount(Static("Nothing here.", id="cct-todo-empty"))
|
|
136
|
+
return
|
|
137
|
+
for t in items:
|
|
138
|
+
box.mount(_TodoRow(t))
|
|
139
|
+
|
|
140
|
+
def on_input_submitted(self, event):
|
|
141
|
+
if event.input.id == "cct-todo-add":
|
|
142
|
+
todos.add_todo(event.value, workspace=self._workspace, source="manual")
|
|
143
|
+
event.input.value = ""
|
|
144
|
+
self._render()
|
|
145
|
+
|
|
146
|
+
def on_input_changed(self, event):
|
|
147
|
+
if event.input.id == "cct-todo-search":
|
|
148
|
+
self._query = event.value
|
|
149
|
+
self._render()
|
|
150
|
+
|
|
151
|
+
def on_button_pressed(self, event):
|
|
152
|
+
bid = event.button.id or ""
|
|
153
|
+
if bid == "cct-todo-close":
|
|
154
|
+
self.dismiss(None)
|
|
155
|
+
return
|
|
156
|
+
if bid.startswith("cct-todo-filter-"):
|
|
157
|
+
self._filter = bid.rsplit("-", 1)[1]
|
|
158
|
+
for b in self.query("#cct-todo-filters Button"):
|
|
159
|
+
b.variant = "primary" if b.id == bid else "default"
|
|
160
|
+
self._render()
|
|
161
|
+
return
|
|
162
|
+
if bid.startswith("cct-todo-del-"):
|
|
163
|
+
todo_id = int(bid.rsplit("-", 1)[1])
|
|
164
|
+
todos.delete_todo(todo_id, workspace=self._workspace)
|
|
165
|
+
self._render()
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
def on_click(self, event):
|
|
169
|
+
widget = getattr(event, "widget", None)
|
|
170
|
+
wid = getattr(widget, "id", "") or ""
|
|
171
|
+
if wid.startswith("cct-todo-check-"):
|
|
172
|
+
todo_id = int(wid.rsplit("-", 1)[1])
|
|
173
|
+
current = next((t for t in todos.list_todos(self._workspace) if t["id"] == todo_id), None)
|
|
174
|
+
if current:
|
|
175
|
+
new_status = "pending" if current["status"] == "done" else "done"
|
|
176
|
+
todos.set_status(todo_id, new_status, workspace=self._workspace)
|
|
177
|
+
self._render()
|
|
178
|
+
elif wid.startswith("cct-todo-pri-"):
|
|
179
|
+
todo_id = int(wid.rsplit("-", 1)[1])
|
|
180
|
+
current = next((t for t in todos.list_todos(self._workspace) if t["id"] == todo_id), None)
|
|
181
|
+
if current:
|
|
182
|
+
order = ("low", "normal", "high")
|
|
183
|
+
nxt = order[(order.index(current["priority"]) + 1) % len(order)] \
|
|
184
|
+
if current["priority"] in order else "normal"
|
|
185
|
+
todos.update_todo(todo_id, workspace=self._workspace, priority=nxt)
|
|
186
|
+
self._render()
|
|
187
|
+
|
|
188
|
+
def on_key(self, event):
|
|
189
|
+
if event.key == "escape":
|
|
190
|
+
self.dismiss(None)
|
|
191
|
+
|
|
192
|
+
else:
|
|
193
|
+
TodoPanel = None
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT CLI — CATRootViewport: Shared Responsive Viewport Engine.
|
|
3
|
+
|
|
4
|
+
Provides unified terminal dimension measuring, responsive breakpoints,
|
|
5
|
+
and full-screen layout constraints (100% width/height) to eliminate
|
|
6
|
+
the top-left corner shrinkage glitch across Provider, User, and Settings screens.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional, Callable
|
|
10
|
+
import shutil
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from textual.screen import Screen
|
|
14
|
+
from textual.containers import Container
|
|
15
|
+
from textual.reactive import reactive
|
|
16
|
+
from textual.events import Resize
|
|
17
|
+
TEXTUAL_OK = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
Screen = object # type: ignore
|
|
20
|
+
Container = object # type: ignore
|
|
21
|
+
TEXTUAL_OK = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Breakpoint definitions
|
|
25
|
+
BP_LARGE = 100
|
|
26
|
+
BP_MEDIUM = 80
|
|
27
|
+
BP_SMALL = 60
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_terminal_size_class(width: Optional[int] = None) -> str:
|
|
31
|
+
"""Return responsive size class ('large', 'medium', 'small', 'very_small')
|
|
32
|
+
based on current terminal or specified width."""
|
|
33
|
+
if width is None:
|
|
34
|
+
try:
|
|
35
|
+
cols, _ = shutil.get_terminal_size((80, 24))
|
|
36
|
+
width = cols
|
|
37
|
+
except Exception:
|
|
38
|
+
width = 80
|
|
39
|
+
|
|
40
|
+
if width >= BP_LARGE:
|
|
41
|
+
return "large"
|
|
42
|
+
elif width >= BP_MEDIUM:
|
|
43
|
+
return "medium"
|
|
44
|
+
elif width >= BP_SMALL:
|
|
45
|
+
return "small"
|
|
46
|
+
return "very_small"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if TEXTUAL_OK:
|
|
50
|
+
class CATRootViewport(Container):
|
|
51
|
+
"""Root container that guarantees 100% width and height and adapts
|
|
52
|
+
|
|
53
|
+
its child layout to the terminal viewport dimensions.
|
|
54
|
+
"""
|
|
55
|
+
DEFAULT_CSS = """
|
|
56
|
+
CATRootViewport {
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: 100%;
|
|
59
|
+
min-width: 100%;
|
|
60
|
+
min-height: 100%;
|
|
61
|
+
layout: vertical;
|
|
62
|
+
padding: 0;
|
|
63
|
+
margin: 0;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
}
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
size_class = reactive("large")
|
|
69
|
+
|
|
70
|
+
def __init__(self, *children, on_size_change: Optional[Callable[[str], None]] = None, **kwargs):
|
|
71
|
+
super().__init__(*children, **kwargs)
|
|
72
|
+
self._on_size_change = on_size_change
|
|
73
|
+
|
|
74
|
+
def on_mount(self) -> None:
|
|
75
|
+
self._update_size_class()
|
|
76
|
+
|
|
77
|
+
def on_resize(self, event: Resize) -> None:
|
|
78
|
+
self._update_size_class(event.size.width)
|
|
79
|
+
|
|
80
|
+
def _update_size_class(self, width: Optional[int] = None) -> None:
|
|
81
|
+
w = width if width is not None else (self.size.width or 80)
|
|
82
|
+
new_cls = get_terminal_size_class(w)
|
|
83
|
+
if new_cls != self.size_class:
|
|
84
|
+
self.size_class = new_cls
|
|
85
|
+
# Update CSS classes for styling hooks (.viewport-large, etc.)
|
|
86
|
+
for cls_name in ("viewport-large", "viewport-medium", "viewport-small", "viewport-very-small"):
|
|
87
|
+
self.remove_class(cls_name)
|
|
88
|
+
self.add_class(f"viewport-{new_cls.replace('_', '-')}")
|
|
89
|
+
if self._on_size_change:
|
|
90
|
+
try:
|
|
91
|
+
self._on_size_change(new_cls)
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CATViewportScreenMixin:
|
|
97
|
+
"""Mixin for Textual Screen subclasses ensuring full terminal viewport
|
|
98
|
+
|
|
99
|
+
inheritance, proper resize dispatch, and lifecycle cleanup.
|
|
100
|
+
"""
|
|
101
|
+
DEFAULT_CSS = """
|
|
102
|
+
Screen {
|
|
103
|
+
width: 100%;
|
|
104
|
+
height: 100%;
|
|
105
|
+
min-width: 100%;
|
|
106
|
+
min-height: 100%;
|
|
107
|
+
padding: 0;
|
|
108
|
+
margin: 0;
|
|
109
|
+
}
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def get_current_size_class(self) -> str:
|
|
113
|
+
w = getattr(self.size, "width", 0) or getattr(getattr(self, "app", None), "size", None)
|
|
114
|
+
if hasattr(w, "width"):
|
|
115
|
+
width_val = w.width
|
|
116
|
+
elif isinstance(w, int) and w > 0:
|
|
117
|
+
width_val = w
|
|
118
|
+
else:
|
|
119
|
+
cols, _ = shutil.get_terminal_size((80, 24))
|
|
120
|
+
width_val = cols
|
|
121
|
+
return get_terminal_size_class(width_val)
|
|
122
|
+
|
|
123
|
+
def on_resize(self, event: Resize) -> None:
|
|
124
|
+
size_class = get_terminal_size_class(event.size.width)
|
|
125
|
+
if hasattr(self, "on_viewport_resized"):
|
|
126
|
+
try:
|
|
127
|
+
self.on_viewport_resized(size_class, event.size.width, event.size.height)
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
else:
|
|
132
|
+
class CATRootViewport: # type: ignore
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
class CATViewportScreenMixin: # type: ignore
|
|
136
|
+
pass
|