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,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
calc_terminal/identity.py
|
|
3
|
+
==========================
|
|
4
|
+
Single source of truth for CCT's application identity: what CCT is, who
|
|
5
|
+
made it, and what it's actually built with.
|
|
6
|
+
|
|
7
|
+
Before this module existed, "who made you" style questions were answered
|
|
8
|
+
in three different, drifting places: the AI's default system prompt (no
|
|
9
|
+
creator info at all), the AGENT/AI system prompts in agent.py (also
|
|
10
|
+
nothing), and a hidden easter egg in easter_eggs.py (vibes, no name).
|
|
11
|
+
That's why the app used to give inconsistent or flatly wrong answers.
|
|
12
|
+
|
|
13
|
+
Every one of those places now reads from here instead of hardcoding its
|
|
14
|
+
own copy:
|
|
15
|
+
- aicore.py's default system prompts embed IDENTITY_BLOCK
|
|
16
|
+
- agent.py's AGENT_SYSTEM_PROMPT / AI_SYSTEM_PROMPT embed IDENTITY_BLOCK
|
|
17
|
+
- easter_eggs.py's "who made this" panel renders answer_creator()
|
|
18
|
+
- the Textual UI (ui/app.py) and the fallback CLI (app.py) both run a
|
|
19
|
+
deterministic fast-path intercept (is_identity_question / answer_for)
|
|
20
|
+
*before* ever reaching the AI, so the answer is 100% consistent no
|
|
21
|
+
matter which AI provider is active, whether it's offline, or how a
|
|
22
|
+
particular model happens to phrase things.
|
|
23
|
+
|
|
24
|
+
tech_stack_sentence() never invents technology — it only ever reports the
|
|
25
|
+
Python/Textual/engine core that's always true plus whatever AI provider
|
|
26
|
+
is actually configured right now (from aicore's own config), so it can't
|
|
27
|
+
drift out of sync with reality either.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
|
|
32
|
+
# --------------------------------------------------------------- facts --
|
|
33
|
+
APP_NAME = "Coding Agent Terminal"
|
|
34
|
+
SHORT_NAME = "CAT"
|
|
35
|
+
APP_TAGLINE = "a terminal-based AI-powered coding and scientific workspace"
|
|
36
|
+
APP_VERSION = "0.7.9.0"
|
|
37
|
+
|
|
38
|
+
CLI_TITLE = "CAT CLI"
|
|
39
|
+
CLI_EMOJI = "🐱"
|
|
40
|
+
FULL_CLI_TITLE = f"{CLI_EMOJI} {CLI_TITLE}"
|
|
41
|
+
|
|
42
|
+
CREATOR_NAME = "Kazi Zillani"
|
|
43
|
+
CREATOR_ROLE = "a student and independent developer"
|
|
44
|
+
DEVELOPMENT_NOTE = "designed and developed by a single developer"
|
|
45
|
+
|
|
46
|
+
# Technology that's always part of CAT regardless of which AI backend
|
|
47
|
+
# (if any) is currently configured.
|
|
48
|
+
_CORE_TECH = [
|
|
49
|
+
"Python",
|
|
50
|
+
"the Textual terminal UI framework",
|
|
51
|
+
"a custom chemistry calculation engine",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
_PROVIDER_LABELS = {
|
|
55
|
+
"openai": "OpenAI",
|
|
56
|
+
"anthropic": "Anthropic",
|
|
57
|
+
"gemini": "Google Gemini",
|
|
58
|
+
"openrouter": "OpenRouter",
|
|
59
|
+
"groq": "Groq",
|
|
60
|
+
"ollama": "a local Ollama model",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _provider_phrase(config):
|
|
65
|
+
"""Describe the currently configured AI backend truthfully, or return
|
|
66
|
+
None if nothing is configured — never guess a provider that isn't
|
|
67
|
+
actually active."""
|
|
68
|
+
if not config:
|
|
69
|
+
return None
|
|
70
|
+
provider = (config.get("provider") or "").strip().lower()
|
|
71
|
+
if not provider:
|
|
72
|
+
return None
|
|
73
|
+
model = (config.get("model") or "").strip()
|
|
74
|
+
label = _PROVIDER_LABELS.get(provider, provider)
|
|
75
|
+
if provider == "ollama":
|
|
76
|
+
return f"{label} (model: {model})" if model else label
|
|
77
|
+
return f"{label} (model: {model})" if model else label
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def tech_stack_parts(config=None):
|
|
81
|
+
"""List of technology strings actually in play right now. `config`
|
|
82
|
+
should be aicore.load_config()'s return value, if available."""
|
|
83
|
+
parts = list(_CORE_TECH)
|
|
84
|
+
provider_phrase = _provider_phrase(config)
|
|
85
|
+
if provider_phrase:
|
|
86
|
+
parts.append(provider_phrase)
|
|
87
|
+
return parts
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def tech_stack_sentence(config=None):
|
|
91
|
+
parts = tech_stack_parts(config)
|
|
92
|
+
if len(parts) == 1:
|
|
93
|
+
return parts[0]
|
|
94
|
+
return ", ".join(parts[:-1]) + ", and " + parts[-1]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------- system prompt --
|
|
98
|
+
# Folded into every AI system prompt so the model itself stays accurate
|
|
99
|
+
# even for identity phrasings the fast-path intercept below doesn't
|
|
100
|
+
# happen to catch.
|
|
101
|
+
IDENTITY_BLOCK = f"""[CAT IDENTITY \u2014 factual, do not deviate from this]
|
|
102
|
+
You are running inside {APP_NAME} ({SHORT_NAME}), {APP_TAGLINE}.
|
|
103
|
+
{SHORT_NAME} the application was created by {CREATOR_NAME}, {CREATOR_ROLE} \u2014 {DEVELOPMENT_NOTE}.
|
|
104
|
+
You (the AI answering right now) are a separate thing from {SHORT_NAME} itself: {SHORT_NAME} is the app/terminal the person is typing into; the AI is whichever model provider is currently configured. Never claim {CREATOR_NAME} built or trained the underlying AI model \u2014 only the {SHORT_NAME} application around it.
|
|
105
|
+
If asked who made/built/created/developed {SHORT_NAME}, or who owns this project: the answer is {CREATOR_NAME}, {CREATOR_ROLE}.
|
|
106
|
+
If asked what AI model or provider is answering: name the actual configured provider/model, never {CREATOR_NAME}.
|
|
107
|
+
Never invent capabilities, companies, or technologies that aren't actually part of {SHORT_NAME}."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------- deterministic Q&A --
|
|
111
|
+
# A fast-path for the most common phrasings, answered directly from this
|
|
112
|
+
# module with zero AI involved, so the answer can never depend on model
|
|
113
|
+
# mood, provider, or connectivity. Broader than an exact-match easter
|
|
114
|
+
# egg on purpose: "who built cat", "who owns this project", "who
|
|
115
|
+
# developed you" etc. should all land here, not just one exact string.
|
|
116
|
+
_CREATOR_PATTERNS = [
|
|
117
|
+
r"\bwho\b[^.?!]{0,40}\b(made|built|created|developed|designed|coded|wrote|owns)\b[^.?!]{0,25}\b(you|this|it|cat|cct|the app|the application|the project|coding agent terminal)\b",
|
|
118
|
+
r"\bwho(?:'s| is)\b[^.?!]{0,25}\b(creator|developer|maker|author|owner)\b",
|
|
119
|
+
r"\bwho\s+(?:made|built|created|developed|owns)\s+(?:cat|cct)\b",
|
|
120
|
+
]
|
|
121
|
+
_WHAT_ARE_YOU_PATTERNS = [
|
|
122
|
+
r"^\s*what\s+are\s+you\s*\??\s*$",
|
|
123
|
+
r"^\s*who\s+are\s+you\s*\??\s*$",
|
|
124
|
+
r"^\s*whoareyou\s*\??\s*$",
|
|
125
|
+
r"^\s*whatareyou\s*\??\s*$",
|
|
126
|
+
r"\bwhat\s+is\s+cat\b",
|
|
127
|
+
r"\bwhat\s+is\s+cct\b",
|
|
128
|
+
r"\bwhat\s+is\s+coding\s+agent\s+terminal\b",
|
|
129
|
+
# handle concatenated like Hi,howcanIhelpyou?User:whoareyou?
|
|
130
|
+
r"who\s*are\s*you",
|
|
131
|
+
r"what\s*are\s*you",
|
|
132
|
+
]
|
|
133
|
+
_MODEL_PATTERNS = [
|
|
134
|
+
r"\bwhat\s+(?:ai\s+)?model\b[^.?!]{0,25}\b(are you|is this|using|running|powered by)\b",
|
|
135
|
+
r"\bwhich\s+(?:ai\s+)?model\b",
|
|
136
|
+
r"\bwhat\s+llm\b",
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
_CREATOR_RE = re.compile("|".join(_CREATOR_PATTERNS), re.IGNORECASE)
|
|
140
|
+
_WHAT_ARE_YOU_RE = re.compile("|".join(_WHAT_ARE_YOU_PATTERNS), re.IGNORECASE)
|
|
141
|
+
_MODEL_RE = re.compile("|".join(_MODEL_PATTERNS), re.IGNORECASE)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def classify_identity_question(text):
|
|
145
|
+
"""Return 'model', 'creator', 'what', or None. Checked in this order
|
|
146
|
+
so 'what AI model are you using' (a model question) doesn't get
|
|
147
|
+
swallowed by the broader 'what are you' pattern."""
|
|
148
|
+
t = (text or "").strip()
|
|
149
|
+
if not t:
|
|
150
|
+
return None
|
|
151
|
+
if _MODEL_RE.search(t):
|
|
152
|
+
return "model"
|
|
153
|
+
if _CREATOR_RE.search(t):
|
|
154
|
+
return "creator"
|
|
155
|
+
if _WHAT_ARE_YOU_RE.search(t):
|
|
156
|
+
return "what"
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def is_identity_question(text):
|
|
161
|
+
return classify_identity_question(text) is not None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def answer_creator(config=None):
|
|
165
|
+
return (
|
|
166
|
+
f"{SHORT_NAME} ({APP_NAME}) was created by {CREATOR_NAME}, {CREATOR_ROLE} \u2014 "
|
|
167
|
+
f"{DEVELOPMENT_NOTE}.\n"
|
|
168
|
+
f"Under the hood it's built with {tech_stack_sentence(config)}."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def answer_what(config=None):
|
|
173
|
+
return (
|
|
174
|
+
f"{SHORT_NAME} is {APP_TAGLINE}. It's built with {tech_stack_sentence(config)}, "
|
|
175
|
+
f"created by {CREATOR_NAME}, {CREATOR_ROLE}."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def answer_model(config=None):
|
|
180
|
+
provider_phrase = _provider_phrase(config)
|
|
181
|
+
if not provider_phrase:
|
|
182
|
+
return (
|
|
183
|
+
f"No AI provider is configured for {SHORT_NAME} right now \u2014 run /model to set "
|
|
184
|
+
f"one up. That's separate from {SHORT_NAME} itself, which was created by "
|
|
185
|
+
f"{CREATOR_NAME}, {CREATOR_ROLE}."
|
|
186
|
+
)
|
|
187
|
+
return (
|
|
188
|
+
f"Right now {SHORT_NAME} is talking to you through {provider_phrase}. That's the "
|
|
189
|
+
f"underlying AI provider, not {SHORT_NAME} itself \u2014 the {SHORT_NAME} application "
|
|
190
|
+
f"around it was created by {CREATOR_NAME}."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def answer_for(text, config=None):
|
|
195
|
+
"""Return the canonical answer for an identity question, or None if
|
|
196
|
+
`text` isn't one."""
|
|
197
|
+
kind = classify_identity_question(text)
|
|
198
|
+
if kind == "creator":
|
|
199
|
+
return answer_creator(config)
|
|
200
|
+
if kind == "model":
|
|
201
|
+
return answer_model(config)
|
|
202
|
+
if kind == "what":
|
|
203
|
+
return answer_what(config)
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Re-export terminal identity functions for convenience
|
|
208
|
+
from .terminal_identity import (
|
|
209
|
+
set_terminal_title,
|
|
210
|
+
restore_terminal_title,
|
|
211
|
+
init_terminal_identity,
|
|
212
|
+
get_icon_path,
|
|
213
|
+
configure_windows_terminal_profile,
|
|
214
|
+
start_title_guard,
|
|
215
|
+
stop_title_guard,
|
|
216
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Input Architecture Module
|
|
3
|
+
Centralized input abstraction for touchscreens, 2-in-1s, mice, and keyboards.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .capabilities import (
|
|
7
|
+
DeviceType,
|
|
8
|
+
InputCapabilities,
|
|
9
|
+
detect_capabilities,
|
|
10
|
+
get_input_capabilities,
|
|
11
|
+
set_touch_mode,
|
|
12
|
+
toggle_touch_mode,
|
|
13
|
+
register_listener,
|
|
14
|
+
unregister_listener,
|
|
15
|
+
)
|
|
16
|
+
from .touch import (
|
|
17
|
+
TouchTapRecognizer,
|
|
18
|
+
TouchScrollHandler,
|
|
19
|
+
TAP_MAX_DISTANCE,
|
|
20
|
+
TAP_MAX_DURATION,
|
|
21
|
+
DOUBLE_TAP_MAX_DELAY,
|
|
22
|
+
LONG_PRESS_DURATION,
|
|
23
|
+
)
|
|
24
|
+
from .pointer import (
|
|
25
|
+
TouchHitZone,
|
|
26
|
+
RESIZER_TOUCH_PADDING,
|
|
27
|
+
)
|
|
28
|
+
from .focus import (
|
|
29
|
+
TouchFocusManager,
|
|
30
|
+
)
|
|
31
|
+
from .gestures import (
|
|
32
|
+
GestureBridge,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"DeviceType",
|
|
37
|
+
"InputCapabilities",
|
|
38
|
+
"detect_capabilities",
|
|
39
|
+
"get_input_capabilities",
|
|
40
|
+
"set_touch_mode",
|
|
41
|
+
"toggle_touch_mode",
|
|
42
|
+
"register_listener",
|
|
43
|
+
"unregister_listener",
|
|
44
|
+
"TouchTapRecognizer",
|
|
45
|
+
"TouchScrollHandler",
|
|
46
|
+
"TAP_MAX_DISTANCE",
|
|
47
|
+
"TAP_MAX_DURATION",
|
|
48
|
+
"DOUBLE_TAP_MAX_DELAY",
|
|
49
|
+
"LONG_PRESS_DURATION",
|
|
50
|
+
"TouchHitZone",
|
|
51
|
+
"RESIZER_TOUCH_PADDING",
|
|
52
|
+
"TouchFocusManager",
|
|
53
|
+
"GestureBridge",
|
|
54
|
+
]
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Input Architecture — capabilities.py
|
|
3
|
+
Runtime input device detection and capabilities abstraction.
|
|
4
|
+
|
|
5
|
+
Detects:
|
|
6
|
+
- Touchscreen presence and maximum concurrent touch points (Win32 SM_MAXIMUMTOUCHES, SM_DIGITIZER)
|
|
7
|
+
- Mouse availability (SM_MOUSEPRESENT)
|
|
8
|
+
- Convertible / 2-in-1 slate mode (SM_CONVERTIBLESLATEMODE)
|
|
9
|
+
- Tablet PC capabilities (SM_TABLETPC)
|
|
10
|
+
- Display scaling / DPI awareness
|
|
11
|
+
|
|
12
|
+
Classifies runtime environment into:
|
|
13
|
+
- Normal Desktop
|
|
14
|
+
- Touchscreen Laptop
|
|
15
|
+
- 2-in-1 / Convertible
|
|
16
|
+
- Tablet-style Windows Device
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import json
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from enum import Enum
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Optional, Callable, List
|
|
28
|
+
|
|
29
|
+
CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".cct_config.json")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DeviceType(str, Enum):
|
|
33
|
+
DESKTOP = "desktop"
|
|
34
|
+
TOUCHSCREEN_LAPTOP = "touchscreen_laptop"
|
|
35
|
+
CONVERTIBLE_2IN1 = "convertible_2in1"
|
|
36
|
+
TABLET_WINDOWS = "tablet_windows"
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def display_name(self) -> str:
|
|
40
|
+
if self == DeviceType.TOUCHSCREEN_LAPTOP:
|
|
41
|
+
return "Touchscreen Laptop"
|
|
42
|
+
if self == DeviceType.CONVERTIBLE_2IN1:
|
|
43
|
+
return "2-in-1 / Convertible"
|
|
44
|
+
if self == DeviceType.TABLET_WINDOWS:
|
|
45
|
+
return "Tablet-style Windows Device"
|
|
46
|
+
return "Normal Desktop"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class InputCapabilities:
|
|
51
|
+
"""Centralized input capability descriptor reusable across CAT UI."""
|
|
52
|
+
touchscreen_available: bool = False
|
|
53
|
+
mouse_available: bool = True
|
|
54
|
+
keyboard_available: bool = True
|
|
55
|
+
touch_primary: bool = False
|
|
56
|
+
touch_mode_enabled: bool = False
|
|
57
|
+
max_touch_points: int = 0
|
|
58
|
+
device_type: DeviceType = DeviceType.DESKTOP
|
|
59
|
+
slate_mode: bool = False
|
|
60
|
+
scaling_factor: float = 1.0
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict:
|
|
63
|
+
return {
|
|
64
|
+
"touchscreen_available": self.touchscreen_available,
|
|
65
|
+
"mouse_available": self.mouse_available,
|
|
66
|
+
"keyboard_available": self.keyboard_available,
|
|
67
|
+
"touch_primary": self.touch_primary,
|
|
68
|
+
"touch_mode_enabled": self.touch_mode_enabled,
|
|
69
|
+
"max_touch_points": self.max_touch_points,
|
|
70
|
+
"device_type": self.device_type.value,
|
|
71
|
+
"device_name": self.device_type.display_name,
|
|
72
|
+
"slate_mode": self.slate_mode,
|
|
73
|
+
"scaling_factor": self.scaling_factor,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Listeners for capability changes (e.g. tablet flip, touch mode toggle)
|
|
78
|
+
_LISTENERS: List[Callable[[InputCapabilities], None]] = []
|
|
79
|
+
_CURRENT_CAPABILITIES: Optional[InputCapabilities] = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _read_config_touch_mode() -> Optional[bool]:
|
|
83
|
+
"""Read user-configured touch mode preference from ~/.cct_config.json if set."""
|
|
84
|
+
try:
|
|
85
|
+
if os.path.exists(CONFIG_PATH):
|
|
86
|
+
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
87
|
+
data = json.load(f)
|
|
88
|
+
if isinstance(data, dict) and "touch_mode" in data:
|
|
89
|
+
val = data["touch_mode"]
|
|
90
|
+
if isinstance(val, bool):
|
|
91
|
+
return val
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _save_config_touch_mode(enabled: bool) -> None:
|
|
98
|
+
"""Save user touch mode toggle to ~/.cct_config.json."""
|
|
99
|
+
try:
|
|
100
|
+
data = {}
|
|
101
|
+
if os.path.exists(CONFIG_PATH):
|
|
102
|
+
try:
|
|
103
|
+
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
104
|
+
data = json.load(f)
|
|
105
|
+
except Exception:
|
|
106
|
+
data = {}
|
|
107
|
+
data["touch_mode"] = bool(enabled)
|
|
108
|
+
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
|
109
|
+
json.dump(data, f, indent=2)
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def detect_capabilities() -> InputCapabilities:
|
|
115
|
+
"""Perform real runtime capability detection on Windows and cross-platform."""
|
|
116
|
+
touchscreen_available = False
|
|
117
|
+
mouse_available = True
|
|
118
|
+
keyboard_available = True
|
|
119
|
+
max_touch_points = 0
|
|
120
|
+
slate_mode = False
|
|
121
|
+
is_tablet_pc = False
|
|
122
|
+
slate_mode_val = None
|
|
123
|
+
device_type = DeviceType.DESKTOP
|
|
124
|
+
scaling_factor = 1.0
|
|
125
|
+
|
|
126
|
+
# Win32 detection
|
|
127
|
+
if sys.platform == "win32":
|
|
128
|
+
try:
|
|
129
|
+
import ctypes
|
|
130
|
+
user32 = ctypes.windll.user32
|
|
131
|
+
|
|
132
|
+
# SM_DIGITIZER (94)
|
|
133
|
+
# 0x01 = NID_INTEGRATED_TOUCH, 0x02 = NID_EXTERNAL_TOUCH, 0x40 = NID_MULTI_INPUT, 0x80 = NID_READY
|
|
134
|
+
digitizer_val = user32.GetSystemMetrics(94)
|
|
135
|
+
# SM_MAXIMUMTOUCHES (95) - returns number of touches or 0
|
|
136
|
+
max_touches = user32.GetSystemMetrics(95)
|
|
137
|
+
max_touch_points = max(0, int(max_touches))
|
|
138
|
+
|
|
139
|
+
# SM_MOUSEPRESENT (19)
|
|
140
|
+
mouse_present = user32.GetSystemMetrics(19)
|
|
141
|
+
mouse_available = bool(mouse_present)
|
|
142
|
+
|
|
143
|
+
# SM_TABLETPC (86)
|
|
144
|
+
tablet_pc_val = user32.GetSystemMetrics(86)
|
|
145
|
+
is_tablet_pc = bool(tablet_pc_val)
|
|
146
|
+
|
|
147
|
+
# SM_CONVERTIBLESLATEMODE (0x2003 = 8195)
|
|
148
|
+
# 0 = laptop/clamshell mode, 1 = slate/tablet mode
|
|
149
|
+
slate_mode_val = user32.GetSystemMetrics(0x2003)
|
|
150
|
+
slate_mode = bool(slate_mode_val == 1)
|
|
151
|
+
|
|
152
|
+
# If max touches > 0 or digitizer has touch bits
|
|
153
|
+
if max_touch_points > 0 or (digitizer_val & 0x03):
|
|
154
|
+
touchscreen_available = True
|
|
155
|
+
|
|
156
|
+
# DPI detection
|
|
157
|
+
try:
|
|
158
|
+
dpi = user32.GetDpiForSystem()
|
|
159
|
+
if dpi > 0:
|
|
160
|
+
scaling_factor = round(dpi / 96.0, 2)
|
|
161
|
+
except (AttributeError, Exception):
|
|
162
|
+
scaling_factor = 1.0
|
|
163
|
+
|
|
164
|
+
except Exception:
|
|
165
|
+
# Fallback if ctypes or user32 fails
|
|
166
|
+
touchscreen_available = False
|
|
167
|
+
mouse_available = True
|
|
168
|
+
|
|
169
|
+
# Device Classification
|
|
170
|
+
if touchscreen_available:
|
|
171
|
+
if slate_mode or (is_tablet_pc and not mouse_available):
|
|
172
|
+
device_type = DeviceType.TABLET_WINDOWS
|
|
173
|
+
elif is_tablet_pc or slate_mode_val == 0:
|
|
174
|
+
# Device has 2-in-1 sensor or convertible capability
|
|
175
|
+
device_type = DeviceType.CONVERTIBLE_2IN1
|
|
176
|
+
else:
|
|
177
|
+
device_type = DeviceType.TOUCHSCREEN_LAPTOP
|
|
178
|
+
else:
|
|
179
|
+
device_type = DeviceType.DESKTOP
|
|
180
|
+
|
|
181
|
+
# Touch Primary determination
|
|
182
|
+
touch_primary = False
|
|
183
|
+
if device_type == DeviceType.TABLET_WINDOWS or slate_mode:
|
|
184
|
+
touch_primary = True
|
|
185
|
+
|
|
186
|
+
# Touch Mode Enabled determination:
|
|
187
|
+
# 1. Environment variable override: CCT_TOUCH_MODE=1 / 0
|
|
188
|
+
# 2. Config file preference: ~/.cct_config.json -> touch_mode
|
|
189
|
+
# 3. Default: True if touchscreen is detected, False otherwise
|
|
190
|
+
env_override = os.environ.get("CCT_TOUCH_MODE", "").strip().lower()
|
|
191
|
+
config_pref = _read_config_touch_mode()
|
|
192
|
+
|
|
193
|
+
if env_override in ("1", "true", "yes", "on"):
|
|
194
|
+
touch_mode_enabled = True
|
|
195
|
+
elif env_override in ("0", "false", "no", "off"):
|
|
196
|
+
touch_mode_enabled = False
|
|
197
|
+
elif config_pref is not None:
|
|
198
|
+
touch_mode_enabled = config_pref
|
|
199
|
+
else:
|
|
200
|
+
touch_mode_enabled = bool(touchscreen_available)
|
|
201
|
+
|
|
202
|
+
return InputCapabilities(
|
|
203
|
+
touchscreen_available=touchscreen_available,
|
|
204
|
+
mouse_available=mouse_available,
|
|
205
|
+
keyboard_available=keyboard_available,
|
|
206
|
+
touch_primary=touch_primary,
|
|
207
|
+
touch_mode_enabled=touch_mode_enabled,
|
|
208
|
+
max_touch_points=max_touch_points,
|
|
209
|
+
device_type=device_type,
|
|
210
|
+
slate_mode=slate_mode,
|
|
211
|
+
scaling_factor=scaling_factor,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_input_capabilities(force_refresh: bool = False) -> InputCapabilities:
|
|
216
|
+
"""Get or initialize the cached input capabilities."""
|
|
217
|
+
global _CURRENT_CAPABILITIES
|
|
218
|
+
if _CURRENT_CAPABILITIES is None or force_refresh:
|
|
219
|
+
_CURRENT_CAPABILITIES = detect_capabilities()
|
|
220
|
+
return _CURRENT_CAPABILITIES
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def set_touch_mode(enabled: bool, persist: bool = True) -> InputCapabilities:
|
|
224
|
+
"""Explicitly enable or disable touch-friendly mode."""
|
|
225
|
+
caps = get_input_capabilities()
|
|
226
|
+
caps.touch_mode_enabled = bool(enabled)
|
|
227
|
+
if persist:
|
|
228
|
+
_save_config_touch_mode(caps.touch_mode_enabled)
|
|
229
|
+
_notify_listeners(caps)
|
|
230
|
+
return caps
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def toggle_touch_mode(persist: bool = True) -> bool:
|
|
234
|
+
"""Toggle touch-friendly mode on or off."""
|
|
235
|
+
caps = get_input_capabilities()
|
|
236
|
+
new_state = not caps.touch_mode_enabled
|
|
237
|
+
set_touch_mode(new_state, persist=persist)
|
|
238
|
+
return new_state
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def register_listener(callback: Callable[[InputCapabilities], None]) -> None:
|
|
242
|
+
"""Register a listener invoked when input capabilities or touch mode changes."""
|
|
243
|
+
if callback not in _LISTENERS:
|
|
244
|
+
_LISTENERS.append(callback)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def unregister_listener(callback: Callable[[InputCapabilities], None]) -> None:
|
|
248
|
+
"""Unregister a capability listener."""
|
|
249
|
+
if callback in _LISTENERS:
|
|
250
|
+
_LISTENERS.remove(callback)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _notify_listeners(caps: InputCapabilities) -> None:
|
|
254
|
+
for cb in list(_LISTENERS):
|
|
255
|
+
try:
|
|
256
|
+
cb(caps)
|
|
257
|
+
except Exception:
|
|
258
|
+
pass
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Input Architecture — focus.py
|
|
3
|
+
Touch-friendly focus management and virtual/on-screen keyboard adaptation.
|
|
4
|
+
|
|
5
|
+
Prevents focus jumping, preserves text input focus when Windows touch keyboard appears,
|
|
6
|
+
and manages focus restoration on modal/popup dismissals.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
from textual.widget import Widget
|
|
13
|
+
from textual.widgets import Input, TextArea
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TouchFocusManager:
|
|
17
|
+
"""Manages focus state, input activation, and focus restoration for touchscreen interactions."""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self._last_focused: Optional[Widget] = None
|
|
21
|
+
self._previous_screen_focus: Optional[Widget] = None
|
|
22
|
+
|
|
23
|
+
def on_touch_tap_widget(self, target: Optional[Widget]) -> None:
|
|
24
|
+
"""Ensure tapped text inputs receive immediate and stable focus."""
|
|
25
|
+
if target is None:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
# Check if the target is an editable input or inside one
|
|
29
|
+
cur = target
|
|
30
|
+
input_widget = None
|
|
31
|
+
while cur is not None:
|
|
32
|
+
if isinstance(cur, (Input, TextArea)):
|
|
33
|
+
input_widget = cur
|
|
34
|
+
break
|
|
35
|
+
cur = getattr(cur, "parent", None)
|
|
36
|
+
|
|
37
|
+
if input_widget is not None and getattr(input_widget, "can_focus", True):
|
|
38
|
+
try:
|
|
39
|
+
if not input_widget.has_focus:
|
|
40
|
+
input_widget.focus()
|
|
41
|
+
self._last_focused = input_widget
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# Check if target is inside composer card (prompt glyph, prompt row, card margin)
|
|
47
|
+
cur = target
|
|
48
|
+
inside_composer = False
|
|
49
|
+
is_button = False
|
|
50
|
+
from textual.widgets import Button
|
|
51
|
+
while cur is not None:
|
|
52
|
+
if getattr(cur, "id", None) == "cct-composer":
|
|
53
|
+
inside_composer = True
|
|
54
|
+
if isinstance(cur, (Button,)):
|
|
55
|
+
is_button = True
|
|
56
|
+
cur = getattr(cur, "parent", None)
|
|
57
|
+
if inside_composer and not is_button:
|
|
58
|
+
try:
|
|
59
|
+
app = getattr(target, "app", None)
|
|
60
|
+
if app is not None:
|
|
61
|
+
composer_input = app.query_one("#cct-input", TextArea)
|
|
62
|
+
if composer_input is not None and not composer_input.has_focus:
|
|
63
|
+
composer_input.focus()
|
|
64
|
+
self._last_focused = composer_input
|
|
65
|
+
return
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
if getattr(target, "can_focus", False):
|
|
70
|
+
try:
|
|
71
|
+
self._last_focused = target
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
def save_focus(self, widget: Optional[Widget]) -> None:
|
|
76
|
+
"""Save active focus before opening a modal or popup."""
|
|
77
|
+
self._previous_screen_focus = widget or self._last_focused
|
|
78
|
+
|
|
79
|
+
def restore_focus(self) -> None:
|
|
80
|
+
"""Restore saved focus after modal or popup dismisses."""
|
|
81
|
+
if self._previous_screen_focus is not None:
|
|
82
|
+
try:
|
|
83
|
+
if self._previous_screen_focus.is_attached and getattr(self._previous_screen_focus, "can_focus", False):
|
|
84
|
+
self._previous_screen_focus.focus()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
self._previous_screen_focus = None
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Input Architecture — gestures.py
|
|
3
|
+
Touchscreen gesture integration with CAT Gestures Extension.
|
|
4
|
+
|
|
5
|
+
Connects touch interactions (tap, double-tap, long-press, swipe) to the
|
|
6
|
+
existing Gestures extension without creating duplicate backends.
|
|
7
|
+
Works safely even when the Gestures extension is disabled.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Optional, Dict, Any
|
|
13
|
+
from textual.widget import Widget
|
|
14
|
+
from textual.app import App
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GestureBridge:
|
|
18
|
+
"""Bridges touch gestures into the CAT Gestures subsystem."""
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def dispatch_touch_gesture(
|
|
22
|
+
cls,
|
|
23
|
+
trigger: str,
|
|
24
|
+
target: str,
|
|
25
|
+
app: Optional[App] = None,
|
|
26
|
+
context: Optional[Dict[str, Any]] = None,
|
|
27
|
+
) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Dispatch a gesture to calc_terminal.gestures.manager.
|
|
30
|
+
Returns True if an enabled action was triggered, False otherwise.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
from ..gestures.manager import handle_gesture
|
|
34
|
+
# Map double_tap to double_click for gesture registry compatibility
|
|
35
|
+
mapped_trigger = "double_click" if trigger == "double_tap" else trigger
|
|
36
|
+
return bool(handle_gesture(mapped_trigger, target, app=app, context=context or {}))
|
|
37
|
+
except Exception:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def resolve_target_surface(cls, widget: Optional[Widget]) -> str:
|
|
42
|
+
"""Map a Textual widget to a Gestures target surface (editor, chat, file, diff, preview, explorer)."""
|
|
43
|
+
if widget is None:
|
|
44
|
+
return "global"
|
|
45
|
+
|
|
46
|
+
cur = widget
|
|
47
|
+
while cur is not None:
|
|
48
|
+
wid = getattr(cur, "id", "") or ""
|
|
49
|
+
cls_name = cur.__class__.__name__
|
|
50
|
+
|
|
51
|
+
if wid == "cct-editor" or "EditorPane" in cls_name or "TextArea" in cls_name:
|
|
52
|
+
return "editor"
|
|
53
|
+
if wid == "cct-conversation" or "Conversation" in cls_name or "MessageRow" in cls_name:
|
|
54
|
+
return "chat"
|
|
55
|
+
if wid in ("cct-sidebar", "cct-sidebar-body") or "DirectoryTree" in cls_name or "Explorer" in cls_name:
|
|
56
|
+
return "file"
|
|
57
|
+
if wid == "cct-preview" or "Preview" in cls_name:
|
|
58
|
+
return "preview"
|
|
59
|
+
if "diff" in wid.lower() or "Diff" in cls_name:
|
|
60
|
+
return "diff"
|
|
61
|
+
|
|
62
|
+
cur = getattr(cur, "parent", None)
|
|
63
|
+
|
|
64
|
+
return "global"
|