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,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Browser Launcher - creates CAT's own browser window.
|
|
3
|
+
|
|
4
|
+
Entry point for CAT's embedded browser subsystem.
|
|
5
|
+
Creates a QWebEngineView-based browser window (same process).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from calc_terminal.host.launcher import launch_cat_host, can_launch_host
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import pathlib
|
|
16
|
+
|
|
17
|
+
def _has_module(name: str) -> bool:
|
|
18
|
+
try:
|
|
19
|
+
__import__(name)
|
|
20
|
+
return True
|
|
21
|
+
except ImportError:
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def can_launch_host() -> bool:
|
|
26
|
+
return _has_module("PySide6") or _has_module("PyQt6")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_host_status() -> dict:
|
|
30
|
+
engines = []
|
|
31
|
+
if _has_module("PySide6"):
|
|
32
|
+
engines.append("PySide6 (Qt WebEngine)")
|
|
33
|
+
if _has_module("PyQt6"):
|
|
34
|
+
engines.append("PyQt6 (Qt WebEngine)")
|
|
35
|
+
return {
|
|
36
|
+
"available": bool(engines),
|
|
37
|
+
"engines": engines,
|
|
38
|
+
"preferred": engines[0] if engines else None,
|
|
39
|
+
"reason": "ready" if engines else "no browser engine - install: pip install PySide6",
|
|
40
|
+
"webview2_runtime": _webview2_runtime_version(),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _webview2_runtime_version() -> str:
|
|
45
|
+
if sys.platform != "win32":
|
|
46
|
+
return ""
|
|
47
|
+
try:
|
|
48
|
+
import winreg
|
|
49
|
+
for path in [
|
|
50
|
+
r"SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
|
|
51
|
+
r"SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
|
|
52
|
+
]:
|
|
53
|
+
try:
|
|
54
|
+
k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path)
|
|
55
|
+
pv, _ = winreg.QueryValueEx(k, "pv")
|
|
56
|
+
winreg.CloseKey(k)
|
|
57
|
+
return str(pv)
|
|
58
|
+
except FileNotFoundError:
|
|
59
|
+
continue
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
return ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_PENDING_VERIFICATION_PATH = pathlib.Path.home() / ".cat_pending_verification.json"
|
|
66
|
+
|
|
67
|
+
def _store_pending_verification(url: str):
|
|
68
|
+
try:
|
|
69
|
+
import json, time
|
|
70
|
+
payload = json.dumps({"url": url, "ts": time.time()})
|
|
71
|
+
tmp_p = pathlib.Path.home() / ".cat_pending_verification.tmp"
|
|
72
|
+
tmp_p.write_text(payload, encoding="utf-8")
|
|
73
|
+
tmp_p.replace(_PENDING_VERIFICATION_PATH)
|
|
74
|
+
except Exception:
|
|
75
|
+
try:
|
|
76
|
+
_PENDING_VERIFICATION_PATH.write_text(
|
|
77
|
+
json.dumps({"url": url, "ts": time.time()}), encoding="utf-8"
|
|
78
|
+
)
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _ensure_qt_app():
|
|
84
|
+
"""Create or get existing QApplication. Returns (app, created_new)."""
|
|
85
|
+
# Fix Qt WebEngine cache permission errors
|
|
86
|
+
import tempfile
|
|
87
|
+
_cache_path = os.path.join(tempfile.gettempdir(), "cat_qt_cache")
|
|
88
|
+
os.makedirs(_cache_path, exist_ok=True)
|
|
89
|
+
os.environ["QTWEBENGINE_CACHE_PATH"] = _cache_path
|
|
90
|
+
# Performance: keep GPU enabled for smooth rendering, add safe flags
|
|
91
|
+
os.environ.setdefault("QTWEBENGINE_CHROMIUM_FLAGS",
|
|
92
|
+
"--disable-dev-shm-usage --enable-gpu-rasterization "
|
|
93
|
+
"--ignore-gpu-blocklist --enable-zero-copy "
|
|
94
|
+
"--disable-background-networking --disable-default-apps")
|
|
95
|
+
os.environ.setdefault("QTWEBENGINE_DISABLE_SANDBOX", "1")
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
from PySide6.QtWidgets import QApplication
|
|
99
|
+
except ImportError:
|
|
100
|
+
from PyQt6.QtWidgets import QApplication # type: ignore
|
|
101
|
+
app = QApplication.instance()
|
|
102
|
+
if app is None:
|
|
103
|
+
app = QApplication(sys.argv)
|
|
104
|
+
return app, True
|
|
105
|
+
return app, False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _is_pid_alive(pid: int) -> bool:
|
|
109
|
+
if pid <= 0:
|
|
110
|
+
return False
|
|
111
|
+
if sys.platform == "win32":
|
|
112
|
+
try:
|
|
113
|
+
import ctypes, ctypes.wintypes
|
|
114
|
+
h = ctypes.windll.kernel32.OpenProcess(0x1000, False, pid)
|
|
115
|
+
if h:
|
|
116
|
+
code = ctypes.wintypes.DWORD()
|
|
117
|
+
ctypes.windll.kernel32.GetExitCodeProcess(h, ctypes.byref(code))
|
|
118
|
+
ctypes.windll.kernel32.CloseHandle(h)
|
|
119
|
+
return code.value == 259
|
|
120
|
+
return False
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
else:
|
|
124
|
+
try:
|
|
125
|
+
os.kill(pid, 0)
|
|
126
|
+
return True
|
|
127
|
+
except OSError:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def launch_cat_host(
|
|
132
|
+
start_browser_url: str | None = None,
|
|
133
|
+
start_mode: str = "terminal",
|
|
134
|
+
repl=None,
|
|
135
|
+
block: bool = True,
|
|
136
|
+
) -> int:
|
|
137
|
+
"""Launch CAT's own browser window.
|
|
138
|
+
|
|
139
|
+
block=True: creates app + window + runs event loop (BLOCKS until closed).
|
|
140
|
+
block=False: navigates existing window to URL (no new app, returns fast).
|
|
141
|
+
"""
|
|
142
|
+
if start_browser_url in ("fatty", "--fatty", "cat web"):
|
|
143
|
+
start_browser_url = "http://localhost:8765/"
|
|
144
|
+
start_mode = "browser"
|
|
145
|
+
|
|
146
|
+
if start_browser_url and "8765" in start_browser_url:
|
|
147
|
+
try:
|
|
148
|
+
from ..web.server import ensure_fatty_server
|
|
149
|
+
ensure_fatty_server(timeout=5.0, auto_start=True)
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
if not block:
|
|
154
|
+
url = start_browser_url or "about:home"
|
|
155
|
+
_store_pending_verification(url)
|
|
156
|
+
# 1. Check if running in current process
|
|
157
|
+
try:
|
|
158
|
+
try:
|
|
159
|
+
from PySide6.QtWidgets import QApplication as _QApp
|
|
160
|
+
except ImportError:
|
|
161
|
+
from PyQt6.QtWidgets import QApplication as _QApp # type: ignore
|
|
162
|
+
app = _QApp.instance()
|
|
163
|
+
if app is not None:
|
|
164
|
+
for w in app.topLevelWidgets():
|
|
165
|
+
try:
|
|
166
|
+
if w.__class__.__name__ == "CATDesktopWindow" and hasattr(w, "browser"):
|
|
167
|
+
w.navigate(url)
|
|
168
|
+
if hasattr(w, "_bring_to_front"):
|
|
169
|
+
w._bring_to_front()
|
|
170
|
+
else:
|
|
171
|
+
w.show()
|
|
172
|
+
w.raise_()
|
|
173
|
+
w.activateWindow()
|
|
174
|
+
return 0
|
|
175
|
+
except Exception:
|
|
176
|
+
continue
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
# 2. Check if an external CAT Browser process is actively running (heartbeat within 3.5s)
|
|
181
|
+
try:
|
|
182
|
+
import time, json
|
|
183
|
+
hb_path = pathlib.Path.home() / ".cat_browser_heartbeat"
|
|
184
|
+
if hb_path.exists():
|
|
185
|
+
is_running = False
|
|
186
|
+
hwnd = 0
|
|
187
|
+
try:
|
|
188
|
+
raw = hb_path.read_text(encoding="utf-8").strip()
|
|
189
|
+
if raw:
|
|
190
|
+
if raw.startswith("{"):
|
|
191
|
+
data = json.loads(raw)
|
|
192
|
+
hb_ts = float(data.get("ts", 0))
|
|
193
|
+
pid = int(data.get("pid", 0))
|
|
194
|
+
hwnd = int(data.get("hwnd", 0))
|
|
195
|
+
else:
|
|
196
|
+
hb_ts = float(raw)
|
|
197
|
+
pid = 0
|
|
198
|
+
if (time.time() - hb_ts) < 3.5:
|
|
199
|
+
if pid > 0:
|
|
200
|
+
is_running = _is_pid_alive(pid)
|
|
201
|
+
else:
|
|
202
|
+
is_running = True
|
|
203
|
+
except Exception:
|
|
204
|
+
is_running = False
|
|
205
|
+
|
|
206
|
+
if is_running:
|
|
207
|
+
# Active window will pick up _store_pending_verification within 250ms
|
|
208
|
+
# On Windows, immediately restore & bring it to foreground over maximized terminal!
|
|
209
|
+
if sys.platform == "win32":
|
|
210
|
+
try:
|
|
211
|
+
import ctypes
|
|
212
|
+
if hwnd <= 0 and pid > 0:
|
|
213
|
+
def _enum_cb(h, extra):
|
|
214
|
+
lp = ctypes.c_ulong()
|
|
215
|
+
ctypes.windll.user32.GetWindowThreadProcessId(h, ctypes.byref(lp))
|
|
216
|
+
if lp.value == pid and ctypes.windll.user32.IsWindowVisible(h):
|
|
217
|
+
extra.append(h)
|
|
218
|
+
return False
|
|
219
|
+
return True
|
|
220
|
+
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_int, ctypes.py_object)
|
|
221
|
+
found = []
|
|
222
|
+
ctypes.windll.user32.EnumWindows(WNDENUMPROC(_enum_cb), found)
|
|
223
|
+
if found:
|
|
224
|
+
hwnd = found[0]
|
|
225
|
+
|
|
226
|
+
if hwnd > 0:
|
|
227
|
+
user32 = ctypes.windll.user32
|
|
228
|
+
kernel32 = ctypes.windll.kernel32
|
|
229
|
+
user32.ShowWindow(hwnd, 9) # SW_RESTORE
|
|
230
|
+
fg_hwnd = user32.GetForegroundWindow()
|
|
231
|
+
fg_thread = user32.GetWindowThreadProcessId(fg_hwnd, None) if fg_hwnd else 0
|
|
232
|
+
cur_thread = kernel32.GetCurrentThreadId()
|
|
233
|
+
attached = False
|
|
234
|
+
if fg_thread and fg_thread != cur_thread:
|
|
235
|
+
attached = bool(user32.AttachThreadInput(cur_thread, fg_thread, True))
|
|
236
|
+
swp_flags = 0x0001 | 0x0002 | 0x0040 # SWP_NOSIZE | SWP_NOMOVE | SWP_SHOWWINDOW
|
|
237
|
+
user32.SetWindowPos(hwnd, -1, 0, 0, 0, 0, swp_flags) # HWND_TOPMOST
|
|
238
|
+
user32.SetWindowPos(hwnd, -2, 0, 0, 0, 0, swp_flags) # HWND_NOTOPMOST
|
|
239
|
+
user32.BringWindowToTop(hwnd)
|
|
240
|
+
user32.SetForegroundWindow(hwnd)
|
|
241
|
+
if attached:
|
|
242
|
+
user32.AttachThreadInput(cur_thread, fg_thread, False)
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
return 0
|
|
246
|
+
else:
|
|
247
|
+
# Stale heartbeat file from dead/crashed process -> clean it up
|
|
248
|
+
hb_path.unlink(missing_ok=True)
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
# 3. No active CAT Browser window found -> spawn in background
|
|
253
|
+
has_qt = _has_module("PySide6") or _has_module("PyQt6")
|
|
254
|
+
if has_qt:
|
|
255
|
+
import subprocess
|
|
256
|
+
exe = sys.executable
|
|
257
|
+
if sys.platform == "win32":
|
|
258
|
+
pw = exe.replace("python.exe", "pythonw.exe")
|
|
259
|
+
if os.path.exists(pw):
|
|
260
|
+
exe = pw
|
|
261
|
+
# Source-checkout support only: a pip-installed CAT already has
|
|
262
|
+
# `calc_terminal` importable, so never force PYTHONPATH/cwd to a
|
|
263
|
+
# repo path that does not exist on the user's machine.
|
|
264
|
+
candidate_root = pathlib.Path(__file__).resolve().parent.parent.parent
|
|
265
|
+
is_source_checkout = (candidate_root / "calc_terminal").is_dir() and (
|
|
266
|
+
candidate_root / "main.py"
|
|
267
|
+
).exists()
|
|
268
|
+
env = os.environ.copy()
|
|
269
|
+
popen_kwargs: dict = {}
|
|
270
|
+
if is_source_checkout:
|
|
271
|
+
ws_root = str(candidate_root)
|
|
272
|
+
ppath = env.get("PYTHONPATH", "")
|
|
273
|
+
env["PYTHONPATH"] = ws_root + (os.pathsep + ppath if ppath else "")
|
|
274
|
+
popen_kwargs["cwd"] = ws_root
|
|
275
|
+
cmd = [exe, "-m", "calc_terminal.host.launcher", "--url", url, "--mode", "browser"]
|
|
276
|
+
creationflags = 0
|
|
277
|
+
if sys.platform == "win32":
|
|
278
|
+
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
279
|
+
try:
|
|
280
|
+
subprocess.Popen(
|
|
281
|
+
cmd,
|
|
282
|
+
env=env,
|
|
283
|
+
creationflags=creationflags,
|
|
284
|
+
close_fds=(sys.platform != "win32"),
|
|
285
|
+
stdout=subprocess.DEVNULL,
|
|
286
|
+
stderr=subprocess.DEVNULL,
|
|
287
|
+
**popen_kwargs,
|
|
288
|
+
)
|
|
289
|
+
return 0
|
|
290
|
+
except Exception as e:
|
|
291
|
+
print(f"[CAT Browser] failed to spawn background browser: {e}", file=sys.stderr)
|
|
292
|
+
return 0
|
|
293
|
+
|
|
294
|
+
has_qt = _has_module("PySide6") or _has_module("PyQt6")
|
|
295
|
+
if has_qt:
|
|
296
|
+
return _launch_with_qt(start_browser_url, start_mode, repl)
|
|
297
|
+
|
|
298
|
+
print("\n[CAT Browser] No browser engine found.", file=sys.stderr)
|
|
299
|
+
print("Install PySide6 for real HTML/CSS/JS rendering:", file=sys.stderr)
|
|
300
|
+
print(" pip install PySide6", file=sys.stderr)
|
|
301
|
+
return 2
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _launch_with_qt(start_browser_url, start_mode, repl) -> int:
|
|
305
|
+
"""Create the browser window AND run the event loop. BLOCKS until closed."""
|
|
306
|
+
try:
|
|
307
|
+
os.environ.setdefault(
|
|
308
|
+
"QTWEBENGINE_CHROMIUM_FLAGS",
|
|
309
|
+
"--disable-dev-shm-usage --enable-gpu-rasterization "
|
|
310
|
+
"--ignore-gpu-blocklist --enable-zero-copy "
|
|
311
|
+
"--disable-background-networking --disable-default-apps"
|
|
312
|
+
)
|
|
313
|
+
app, created = _ensure_qt_app()
|
|
314
|
+
|
|
315
|
+
from .desktop import CATDesktopWindow
|
|
316
|
+
initial_url = start_browser_url if (start_mode == "browser" or start_browser_url) else None
|
|
317
|
+
w = CATDesktopWindow(repl=repl, start_browser_url=initial_url)
|
|
318
|
+
w.show()
|
|
319
|
+
try:
|
|
320
|
+
if hasattr(w, "_bring_to_front"):
|
|
321
|
+
w._bring_to_front()
|
|
322
|
+
else:
|
|
323
|
+
w.raise_()
|
|
324
|
+
w.activateWindow()
|
|
325
|
+
except Exception:
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
if created:
|
|
329
|
+
return app.exec() # type: ignore
|
|
330
|
+
return 0
|
|
331
|
+
except Exception as e:
|
|
332
|
+
import traceback
|
|
333
|
+
print(f"[CAT Browser] launch failed: {e}", file=sys.stderr)
|
|
334
|
+
traceback.print_exc()
|
|
335
|
+
return 1
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def create_browser_window(start_browser_url=None, start_mode="browser", repl=None):
|
|
339
|
+
"""Create the browser window WITHOUT starting the event loop.
|
|
340
|
+
|
|
341
|
+
Returns (app, window, created_new).
|
|
342
|
+
Caller MUST call app.exec() after setting up other Qt objects.
|
|
343
|
+
"""
|
|
344
|
+
os.environ.setdefault(
|
|
345
|
+
"QTWEBENGINE_CHROMIUM_FLAGS",
|
|
346
|
+
"--disable-dev-shm-usage --enable-gpu-rasterization "
|
|
347
|
+
"--ignore-gpu-blocklist --enable-zero-copy "
|
|
348
|
+
"--disable-background-networking --disable-default-apps"
|
|
349
|
+
)
|
|
350
|
+
app, created = _ensure_qt_app()
|
|
351
|
+
|
|
352
|
+
from .desktop import CATDesktopWindow
|
|
353
|
+
initial_url = start_browser_url if (start_mode == "browser" or start_browser_url) else None
|
|
354
|
+
w = CATDesktopWindow(repl=repl, start_browser_url=initial_url)
|
|
355
|
+
w.show()
|
|
356
|
+
try:
|
|
357
|
+
w.raise_()
|
|
358
|
+
w.activateWindow()
|
|
359
|
+
except Exception:
|
|
360
|
+
pass
|
|
361
|
+
return app, w, created
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def main():
|
|
365
|
+
import argparse
|
|
366
|
+
p = argparse.ArgumentParser(description="CAT Browser - embedded Chromium")
|
|
367
|
+
p.add_argument("url_pos", nargs="?", default=None, help="URL to open (positional)")
|
|
368
|
+
p.add_argument("--url", default=None, help="URL to open")
|
|
369
|
+
p.add_argument("--mode", choices=["terminal", "browser"], default="browser")
|
|
370
|
+
p.add_argument("--no-block", action="store_true")
|
|
371
|
+
p.add_argument("--status", action="store_true")
|
|
372
|
+
p.add_argument("--fatty", action="store_true", help="Open Fatty CAT Web interface")
|
|
373
|
+
args = p.parse_args()
|
|
374
|
+
|
|
375
|
+
if args.status:
|
|
376
|
+
import json
|
|
377
|
+
print(json.dumps(get_host_status(), indent=2))
|
|
378
|
+
return 0
|
|
379
|
+
|
|
380
|
+
start_url = args.url or args.url_pos
|
|
381
|
+
if args.fatty:
|
|
382
|
+
start_url = "http://localhost:8765/"
|
|
383
|
+
args.mode = "browser"
|
|
384
|
+
elif start_url and "localhost" in start_url and args.mode == "terminal":
|
|
385
|
+
args.mode = "browser"
|
|
386
|
+
|
|
387
|
+
sys.exit(launch_cat_host(
|
|
388
|
+
start_browser_url=start_url,
|
|
389
|
+
start_mode=args.mode,
|
|
390
|
+
block=not args.no_block,
|
|
391
|
+
))
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
if __name__ == "__main__":
|
|
395
|
+
main()
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Host — Terminal Embed
|
|
3
|
+
|
|
4
|
+
Provides the terminal pane inside the graphical CAT Host window.
|
|
5
|
+
|
|
6
|
+
The host window is a real OS window (Qt/QWebEngine on Windows). This module
|
|
7
|
+
gives it a terminal surface that preserves existing CAT CLI behaviour:
|
|
8
|
+
|
|
9
|
+
PowerShell
|
|
10
|
+
↓
|
|
11
|
+
cat (launcher detects Windows, launches CAT Host)
|
|
12
|
+
↓
|
|
13
|
+
CAT Host OS Window
|
|
14
|
+
├── [Terminal] ← this widget (CAT REPL preserved)
|
|
15
|
+
├── [Browser] ← real QWebEngineView (WebView2/Chromium)
|
|
16
|
+
├── [IDE]
|
|
17
|
+
└── ...
|
|
18
|
+
|
|
19
|
+
Design decisions:
|
|
20
|
+
* The widget is a plain Qt widget (no external PTY, no xterm.js dependency)
|
|
21
|
+
so it works with only PySide6 installed.
|
|
22
|
+
* It hosts the existing calc_terminal.app.App REPL logic — the same
|
|
23
|
+
handle()/handle_question() dispatch the Textual and fallback CLI use.
|
|
24
|
+
Input from the GUI line edit is fed to App.handle(); output (print())
|
|
25
|
+
is captured and appended to the read-only log view.
|
|
26
|
+
* ANSI escape sequences from calc_terminal.theme are stripped for the
|
|
27
|
+
QPlainTextEdit (Qt handles rich text separately). The behaviour and
|
|
28
|
+
command set are 100% identical to the classic terminal.
|
|
29
|
+
* If the REPL raises, the widget stays alive — the error is shown inline
|
|
30
|
+
and the prompt returns (same contract as the fallback terminal).
|
|
31
|
+
|
|
32
|
+
Fomoji: the REPL instance passed in is the same one cli.bootstrap()
|
|
33
|
+
created, so `cat --auth` state is shared.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import io
|
|
39
|
+
import re
|
|
40
|
+
import sys
|
|
41
|
+
import contextlib
|
|
42
|
+
import threading
|
|
43
|
+
import traceback
|
|
44
|
+
|
|
45
|
+
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]|\x1b\].*?\x07|\x1b\(B")
|
|
46
|
+
|
|
47
|
+
def _strip_ansi(s: str) -> str:
|
|
48
|
+
return _ANSI_RE.sub("", s)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Fallback when Qt not installed — import-time safe so the host package
|
|
53
|
+
# can be imported on headless CI.
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
try:
|
|
56
|
+
from PySide6.QtWidgets import (
|
|
57
|
+
QWidget, QVBoxLayout, QHBoxLayout, QPlainTextEdit,
|
|
58
|
+
QLineEdit, QLabel, QPushButton
|
|
59
|
+
)
|
|
60
|
+
from PySide6.QtCore import Qt, Signal, Slot
|
|
61
|
+
from PySide6.QtGui import QFont, QTextCursor
|
|
62
|
+
_QT_AVAILABLE = True
|
|
63
|
+
except ImportError:
|
|
64
|
+
try:
|
|
65
|
+
from PyQt6.QtWidgets import (
|
|
66
|
+
QWidget, QVBoxLayout, QHBoxLayout, QPlainTextEdit,
|
|
67
|
+
QLineEdit, QLabel, QPushButton
|
|
68
|
+
)
|
|
69
|
+
from PyQt6.QtCore import Qt, pyqtSignal as Signal, pyqtSlot as Slot
|
|
70
|
+
from PyQt6.QtGui import QFont, QTextCursor
|
|
71
|
+
_QT_AVAILABLE = True
|
|
72
|
+
except ImportError:
|
|
73
|
+
_QT_AVAILABLE = False
|
|
74
|
+
QWidget = object # type: ignore
|
|
75
|
+
Signal = lambda *a, **k: None # type: ignore
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if _QT_AVAILABLE:
|
|
79
|
+
|
|
80
|
+
class CATTerminalWidget(QWidget):
|
|
81
|
+
"""
|
|
82
|
+
Graphical terminal pane for CAT Host.
|
|
83
|
+
|
|
84
|
+
Hosts calc_terminal.app.App (the REPL/product logic) and exposes
|
|
85
|
+
it through a Qt input line + scrollback. The host window owns the
|
|
86
|
+
layout; this widget owns only the terminal chrome.
|
|
87
|
+
|
|
88
|
+
Usage:
|
|
89
|
+
repl = App() # from calc_terminal.app
|
|
90
|
+
term = CATTerminalWidget(repl)
|
|
91
|
+
stacked.addWidget(term)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# Emitted on every command so the host can mirror to status bar
|
|
95
|
+
commandEntered = Signal(str)
|
|
96
|
+
|
|
97
|
+
def __init__(self, repl=None, parent=None):
|
|
98
|
+
super().__init__(parent)
|
|
99
|
+
self.repl = repl
|
|
100
|
+
self._history: list[str] = []
|
|
101
|
+
self._history_idx: int = -1
|
|
102
|
+
self._setup_ui()
|
|
103
|
+
self._print_boot()
|
|
104
|
+
|
|
105
|
+
def _setup_ui(self):
|
|
106
|
+
layout = QVBoxLayout(self)
|
|
107
|
+
layout.setContentsMargins(6, 6, 6, 6)
|
|
108
|
+
layout.setSpacing(6)
|
|
109
|
+
|
|
110
|
+
# Header — shows we are the real CAT terminal, not a plain shell
|
|
111
|
+
header = QLabel("CAT TERMINAL — type /help · /browser · /agent · Ctrl+Shift+B → Browser")
|
|
112
|
+
header.setStyleSheet("color: #878caf; font-size: 11px; padding: 2px 4px;")
|
|
113
|
+
layout.addWidget(header)
|
|
114
|
+
|
|
115
|
+
# Scrollback — read-only, selectable
|
|
116
|
+
self.log = QPlainTextEdit()
|
|
117
|
+
self.log.setReadOnly(True)
|
|
118
|
+
self.log.setLineWrapMode(QPlainTextEdit.LineWrapMode.WidgetWidth)
|
|
119
|
+
font = QFont("Consolas" if sys.platform == "win32" else "Monospace", 10)
|
|
120
|
+
font.setStyleHint(QFont.StyleHint.Monospace)
|
|
121
|
+
self.log.setFont(font)
|
|
122
|
+
# Dark terminal palette matching CAT themes
|
|
123
|
+
self.log.setStyleSheet(
|
|
124
|
+
"QPlainTextEdit { background: #1a1b26; color: #e8ebfa; "
|
|
125
|
+
"border: 1px solid #2a2e48; border-radius: 6px; padding: 6px; "
|
|
126
|
+
"selection-background-color: #82aaff; }"
|
|
127
|
+
)
|
|
128
|
+
layout.addWidget(self.log, 1)
|
|
129
|
+
|
|
130
|
+
# Input row — glyph + line edit + send
|
|
131
|
+
row = QHBoxLayout()
|
|
132
|
+
row.setSpacing(6)
|
|
133
|
+
glyph = QLabel("❯")
|
|
134
|
+
glyph.setStyleSheet("color: #82aaff; font-weight: bold; font-size: 14px; padding-left: 4px;")
|
|
135
|
+
glyph.setFixedWidth(22)
|
|
136
|
+
row.addWidget(glyph)
|
|
137
|
+
|
|
138
|
+
self.input = QLineEdit()
|
|
139
|
+
self.input.setPlaceholderText("Type a command or question… (/help for list)")
|
|
140
|
+
self.input.setStyleSheet(
|
|
141
|
+
"QLineEdit { background: #1e2030; color: #e8ebfa; "
|
|
142
|
+
"border: 1px solid #2a2e48; border-radius: 999px; padding: 6px 12px; "
|
|
143
|
+
"selection-background-color: #82aaff; }"
|
|
144
|
+
"QLineEdit:focus { border: 1px solid #82aaff; }"
|
|
145
|
+
)
|
|
146
|
+
self.input.returnPressed.connect(self._on_submit)
|
|
147
|
+
row.addWidget(self.input, 1)
|
|
148
|
+
|
|
149
|
+
send = QPushButton("↵")
|
|
150
|
+
send.setFixedSize(32, 28)
|
|
151
|
+
send.setToolTip("Send (Enter)")
|
|
152
|
+
send.setStyleSheet(
|
|
153
|
+
"QPushButton { background: #82aaff; color: #1a1b26; border: none; "
|
|
154
|
+
"border-radius: 14px; font-weight: bold; }"
|
|
155
|
+
"QPushButton:hover { background: #9ab8ff; }"
|
|
156
|
+
)
|
|
157
|
+
send.clicked.connect(self._on_submit)
|
|
158
|
+
row.addWidget(send)
|
|
159
|
+
|
|
160
|
+
layout.addLayout(row)
|
|
161
|
+
self.input.setFocus()
|
|
162
|
+
|
|
163
|
+
# --------------------------------------------------------------- I/O --
|
|
164
|
+
|
|
165
|
+
def _append(self, text: str):
|
|
166
|
+
"""Append text to scrollback, stripping ANSI for Qt."""
|
|
167
|
+
if not text:
|
|
168
|
+
return
|
|
169
|
+
clean = _strip_ansi(text)
|
|
170
|
+
# QPlainTextEdit.appendPlainText adds a newline; for multi-line
|
|
171
|
+
# captures we insert as single block to preserve formatting.
|
|
172
|
+
self.log.appendPlainText(clean.rstrip("\n"))
|
|
173
|
+
# Autoscroll
|
|
174
|
+
cursor = self.log.textCursor()
|
|
175
|
+
cursor.movePosition(QTextCursor.MoveOperation.End)
|
|
176
|
+
self.log.setTextCursor(cursor)
|
|
177
|
+
|
|
178
|
+
def _print_boot(self):
|
|
179
|
+
if self.repl is None:
|
|
180
|
+
self._append("CAT terminal ready — no REPL attached (UI preview mode).")
|
|
181
|
+
self._append("Run `cat` from PowerShell to attach the real session.")
|
|
182
|
+
return
|
|
183
|
+
# Capture the classic boot/home output (same as fallback CLI)
|
|
184
|
+
buf = io.StringIO()
|
|
185
|
+
try:
|
|
186
|
+
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
|
|
187
|
+
# theme.enable_windows_ansi + load_saved_theme are already
|
|
188
|
+
# done by cli.bootstrap(); just print home.
|
|
189
|
+
try:
|
|
190
|
+
self.repl.print_home()
|
|
191
|
+
except Exception:
|
|
192
|
+
# Fallback if print_home fails early
|
|
193
|
+
print(f"CAT v{getattr(self.repl, 'VERSION', '?')} — ready", file=buf)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
buf.write(f"[boot warning: {e}]\n")
|
|
196
|
+
out = buf.getvalue()
|
|
197
|
+
if out.strip():
|
|
198
|
+
self._append(out)
|
|
199
|
+
else:
|
|
200
|
+
self._append(f"CAT v{getattr(self.repl, 'VERSION', '0.7.9')} — ready. Type /help")
|
|
201
|
+
|
|
202
|
+
def _on_submit(self):
|
|
203
|
+
raw = self.input.text()
|
|
204
|
+
if raw is None:
|
|
205
|
+
return
|
|
206
|
+
raw = raw.strip()
|
|
207
|
+
if not raw:
|
|
208
|
+
return
|
|
209
|
+
# History
|
|
210
|
+
self._history.append(raw)
|
|
211
|
+
del self._history[:-200]
|
|
212
|
+
self._history_idx = len(self._history)
|
|
213
|
+
self.input.clear()
|
|
214
|
+
self._append(f"\n❯ {raw}")
|
|
215
|
+
self.commandEntered.emit(raw)
|
|
216
|
+
|
|
217
|
+
if self.repl is None:
|
|
218
|
+
self._append("(no REPL — echo only)")
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# Special: clear
|
|
222
|
+
if raw.lower() in ("/clear", "/home", "clear", "cls"):
|
|
223
|
+
self.log.clear()
|
|
224
|
+
self._print_boot()
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# Dispatch to the real REPL, capturing its print() output
|
|
228
|
+
buf = io.StringIO()
|
|
229
|
+
try:
|
|
230
|
+
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
|
|
231
|
+
self.repl.handle(raw)
|
|
232
|
+
except SystemExit:
|
|
233
|
+
# /exit /quit — don't kill host window, just note it
|
|
234
|
+
self._append("\n[Use the window close button or Ctrl+Q to quit CAT Host]")
|
|
235
|
+
# Still show any buffered output
|
|
236
|
+
except Exception as e:
|
|
237
|
+
buf.write(f"\n[error: {type(e).__name__}: {e}]\n")
|
|
238
|
+
traceback.print_exc(file=buf)
|
|
239
|
+
out = buf.getvalue()
|
|
240
|
+
if out:
|
|
241
|
+
self._append(out)
|
|
242
|
+
|
|
243
|
+
# --------------------------------------------------------- public API --
|
|
244
|
+
|
|
245
|
+
def focus_input(self):
|
|
246
|
+
self.input.setFocus()
|
|
247
|
+
self.input.selectAll()
|
|
248
|
+
|
|
249
|
+
def execute(self, raw: str):
|
|
250
|
+
"""Programmatic entry — used by host for /browser shortcuts."""
|
|
251
|
+
self.input.setText(raw)
|
|
252
|
+
self._on_submit()
|
|
253
|
+
|
|
254
|
+
def keyPressEvent(self, event):
|
|
255
|
+
# History navigation with Up/Down when input is focused
|
|
256
|
+
try:
|
|
257
|
+
if event.key() == Qt.Key.Key_Up:
|
|
258
|
+
if self.input.hasFocus() and self._history:
|
|
259
|
+
self._history_idx = max(0, self._history_idx - 1)
|
|
260
|
+
self.input.setText(self._history[self._history_idx])
|
|
261
|
+
return
|
|
262
|
+
elif event.key() == Qt.Key.Key_Down:
|
|
263
|
+
if self.input.hasFocus() and self._history:
|
|
264
|
+
if self._history_idx >= len(self._history) - 1:
|
|
265
|
+
self._history_idx = len(self._history)
|
|
266
|
+
self.input.clear()
|
|
267
|
+
else:
|
|
268
|
+
self._history_idx += 1
|
|
269
|
+
self.input.setText(self._history[self._history_idx])
|
|
270
|
+
return
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
super().keyPressEvent(event)
|
|
274
|
+
|
|
275
|
+
else:
|
|
276
|
+
# Stub so `from .terminal import CATTerminalWidget` never crashes
|
|
277
|
+
class CATTerminalWidget: # type: ignore
|
|
278
|
+
def __init__(self, *a, **k):
|
|
279
|
+
raise ImportError("Qt not installed — CAT Host requires PySide6 (pip install PySide6)")
|