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 @@
|
|
|
1
|
+
"""CAT Live Preview Infrastructure (dev server, preview manager, live reload, diagnostics)."""
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""CAT Live Preview — Dev Server Manager (calc_terminal/preview/dev_server.py).
|
|
2
|
+
|
|
3
|
+
Owns development server lifecycle:
|
|
4
|
+
- Detects project configuration (Static HTML, Vite, Next.js, CRA, Node).
|
|
5
|
+
- Manages available port detection and avoids collisions.
|
|
6
|
+
- Starts, stops, restarts real local HTTP server binding 127.0.0.1 strictly.
|
|
7
|
+
- Prevents duplicate server processes for the same workspace.
|
|
8
|
+
- Verifies server readiness before declaring live state.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import socket
|
|
16
|
+
import subprocess
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ServerState:
|
|
24
|
+
STOPPED = "stopped"
|
|
25
|
+
STARTING = "starting"
|
|
26
|
+
RUNNING = "running"
|
|
27
|
+
ERROR = "error"
|
|
28
|
+
RESTARTING = "restarting"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ProjectInfo:
|
|
33
|
+
kind: str # "static", "vite", "next", "cra", "node"
|
|
34
|
+
label: str
|
|
35
|
+
root: str
|
|
36
|
+
entry_file: Optional[str] = None
|
|
37
|
+
start_command: Optional[List[str]] = None
|
|
38
|
+
is_framework: bool = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def detect_project_type(root: str) -> ProjectInfo:
|
|
42
|
+
"""Analyze workspace root to determine project type and start strategy."""
|
|
43
|
+
abs_root = os.path.abspath(os.path.expanduser(root or ""))
|
|
44
|
+
if not os.path.isdir(abs_root):
|
|
45
|
+
return ProjectInfo(kind="static", label="Static Web", root=abs_root)
|
|
46
|
+
|
|
47
|
+
pkg_path = os.path.join(abs_root, "package.json")
|
|
48
|
+
if os.path.isfile(pkg_path):
|
|
49
|
+
try:
|
|
50
|
+
with open(pkg_path, "r", encoding="utf-8", errors="replace") as f:
|
|
51
|
+
pkg = json.load(f)
|
|
52
|
+
scripts = pkg.get("scripts", {})
|
|
53
|
+
deps = {}
|
|
54
|
+
deps.update(pkg.get("dependencies", {}))
|
|
55
|
+
deps.update(pkg.get("devDependencies", {}))
|
|
56
|
+
|
|
57
|
+
# Vite detection
|
|
58
|
+
for cfg in ("vite.config.js", "vite.config.ts", "vite.config.mjs", "vite.config.cjs"):
|
|
59
|
+
if os.path.isfile(os.path.join(abs_root, cfg)):
|
|
60
|
+
cmd = ["npm", "run", "dev"] if "dev" in scripts else ["npx", "vite"]
|
|
61
|
+
return ProjectInfo(kind="vite", label="Vite", root=abs_root, start_command=cmd, is_framework=True)
|
|
62
|
+
if "vite" in deps and "dev" in scripts:
|
|
63
|
+
return ProjectInfo(kind="vite", label="Vite", root=abs_root, start_command=["npm", "run", "dev"], is_framework=True)
|
|
64
|
+
|
|
65
|
+
# Next.js detection
|
|
66
|
+
for cfg in ("next.config.js", "next.config.ts", "next.config.mjs", "next.config.cjs"):
|
|
67
|
+
if os.path.isfile(os.path.join(abs_root, cfg)):
|
|
68
|
+
cmd = ["npm", "run", "dev"] if "dev" in scripts else ["npx", "next", "dev"]
|
|
69
|
+
return ProjectInfo(kind="next", label="Next.js", root=abs_root, start_command=cmd, is_framework=True)
|
|
70
|
+
if "next" in deps and "dev" in scripts:
|
|
71
|
+
return ProjectInfo(kind="next", label="Next.js", root=abs_root, start_command=["npm", "run", "dev"], is_framework=True)
|
|
72
|
+
|
|
73
|
+
# Create React App detection
|
|
74
|
+
if "react-scripts" in deps:
|
|
75
|
+
cmd = ["npm", "start"]
|
|
76
|
+
return ProjectInfo(kind="cra", label="Create React App", root=abs_root, start_command=cmd, is_framework=True)
|
|
77
|
+
|
|
78
|
+
# Generic Node dev script
|
|
79
|
+
if "dev" in scripts:
|
|
80
|
+
return ProjectInfo(kind="node", label="Node Dev", root=abs_root, start_command=["npm", "run", "dev"], is_framework=True)
|
|
81
|
+
if "start" in scripts:
|
|
82
|
+
return ProjectInfo(kind="node", label="Node Start", root=abs_root, start_command=["npm", "start"], is_framework=True)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Static project fallback
|
|
87
|
+
index_file = None
|
|
88
|
+
for cand in ("index.html", "index.htm", "main.html", "app.html"):
|
|
89
|
+
p = os.path.join(abs_root, cand)
|
|
90
|
+
if os.path.isfile(p):
|
|
91
|
+
index_file = p
|
|
92
|
+
break
|
|
93
|
+
return ProjectInfo(kind="static", label="Static Web", root=abs_root, entry_file=index_file)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_port_available(port: int, host: str = "127.0.0.1") -> bool:
|
|
97
|
+
"""Check if a port is free to bind on localhost."""
|
|
98
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
99
|
+
try:
|
|
100
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
101
|
+
s.bind((host, port))
|
|
102
|
+
return True
|
|
103
|
+
except OSError:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def find_available_port(preferred: int = 5173, fallback_ports: Optional[List[int]] = None) -> int:
|
|
108
|
+
"""Find an available port starting with preferred, then fallbacks, then dynamic bind."""
|
|
109
|
+
if is_port_available(preferred):
|
|
110
|
+
return preferred
|
|
111
|
+
fallbacks = fallback_ports or [5174, 5175, 5176, 3000, 3001, 8080, 8000]
|
|
112
|
+
for p in fallbacks:
|
|
113
|
+
if is_port_available(p):
|
|
114
|
+
return p
|
|
115
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
116
|
+
s.bind(("127.0.0.1", 0))
|
|
117
|
+
return s.getsockname()[1]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class DevServerManager:
|
|
121
|
+
"""Manages local dev server instances with zero duplicate processes and clean lifecycle."""
|
|
122
|
+
|
|
123
|
+
_active_servers: Dict[str, DevServerManager] = {}
|
|
124
|
+
_lock = threading.RLock()
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def get_or_create(cls, root: str) -> DevServerManager:
|
|
128
|
+
"""Get running server for workspace root or create a new manager."""
|
|
129
|
+
norm_root = os.path.abspath(os.path.expanduser(root or ""))
|
|
130
|
+
with cls._lock:
|
|
131
|
+
if norm_root in cls._active_servers:
|
|
132
|
+
return cls._active_servers[norm_root]
|
|
133
|
+
mgr = cls(norm_root)
|
|
134
|
+
cls._active_servers[norm_root] = mgr
|
|
135
|
+
return mgr
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def cleanup_all(cls) -> None:
|
|
139
|
+
"""Terminate and clean up all running development servers across CAT."""
|
|
140
|
+
with cls._lock:
|
|
141
|
+
for mgr in list(cls._active_servers.values()):
|
|
142
|
+
try:
|
|
143
|
+
mgr.stop()
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
cls._active_servers.clear()
|
|
147
|
+
|
|
148
|
+
def __init__(self, root: str) -> None:
|
|
149
|
+
self.root = os.path.abspath(os.path.expanduser(root or ""))
|
|
150
|
+
self.project_info = detect_project_type(self.root)
|
|
151
|
+
self.state = ServerState.STOPPED
|
|
152
|
+
self.port: Optional[int] = None
|
|
153
|
+
self.url: Optional[str] = None
|
|
154
|
+
self.last_error: str = ""
|
|
155
|
+
self._server_instance: Any = None # LiveServer instance or subprocess.Popen
|
|
156
|
+
self._proc: Optional[subprocess.Popen] = None
|
|
157
|
+
self._logs: List[str] = []
|
|
158
|
+
self._on_state_change: Optional[Callable[[str, str], None]] = None
|
|
159
|
+
|
|
160
|
+
def set_state_listener(self, cb: Callable[[str, str], None]) -> None:
|
|
161
|
+
self._on_state_change = cb
|
|
162
|
+
|
|
163
|
+
def _notify(self, state: str, error: str = "") -> None:
|
|
164
|
+
self.state = state
|
|
165
|
+
if error:
|
|
166
|
+
self.last_error = error
|
|
167
|
+
if self._on_state_change:
|
|
168
|
+
try:
|
|
169
|
+
self._on_state_change(state, error)
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
def is_running(self) -> bool:
|
|
174
|
+
if self.state != ServerState.RUNNING:
|
|
175
|
+
return False
|
|
176
|
+
if self._proc is not None:
|
|
177
|
+
return self._proc.poll() is None
|
|
178
|
+
if self._server_instance is not None:
|
|
179
|
+
return getattr(self._server_instance, "running", False)
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
def start(self, preferred_port: Optional[int] = None) -> Tuple[bool, str]:
|
|
183
|
+
"""Start the local development server for this project."""
|
|
184
|
+
if self.is_running():
|
|
185
|
+
return True, self.url or f"http://127.0.0.1:{self.port}/"
|
|
186
|
+
|
|
187
|
+
self._notify(ServerState.STARTING)
|
|
188
|
+
self.project_info = detect_project_type(self.root)
|
|
189
|
+
|
|
190
|
+
if not self.project_info.is_framework:
|
|
191
|
+
return self._start_static(preferred_port)
|
|
192
|
+
else:
|
|
193
|
+
return self._start_framework(preferred_port)
|
|
194
|
+
|
|
195
|
+
def _start_static(self, preferred_port: Optional[int] = None) -> Tuple[bool, str]:
|
|
196
|
+
"""Start CAT's internal LiveServer with hot-reload."""
|
|
197
|
+
try:
|
|
198
|
+
from ..browser.server import LiveServer
|
|
199
|
+
self.port = find_available_port(preferred_port or 5173)
|
|
200
|
+
# Create LiveServer bound to root
|
|
201
|
+
srv = LiveServer(self.root)
|
|
202
|
+
srv.port = self.port
|
|
203
|
+
started = srv.start()
|
|
204
|
+
if not started:
|
|
205
|
+
self._notify(ServerState.ERROR, f"Port {self.port} could not be bound.")
|
|
206
|
+
return False, self.last_error
|
|
207
|
+
|
|
208
|
+
self._server_instance = srv
|
|
209
|
+
self.port = srv.port
|
|
210
|
+
self.url = f"http://127.0.0.1:{self.port}/"
|
|
211
|
+
self._notify(ServerState.RUNNING)
|
|
212
|
+
self._logs.append(f"Static dev server started at {self.url}")
|
|
213
|
+
return True, self.url
|
|
214
|
+
except Exception as e:
|
|
215
|
+
err = f"Failed to start static server: {e}"
|
|
216
|
+
self._notify(ServerState.ERROR, err)
|
|
217
|
+
return False, err
|
|
218
|
+
|
|
219
|
+
def _start_framework(self, preferred_port: Optional[int] = None) -> Tuple[bool, str]:
|
|
220
|
+
"""Launch project's own dev server (e.g. Vite, Next.js)."""
|
|
221
|
+
cmd = self.project_info.start_command
|
|
222
|
+
if not cmd:
|
|
223
|
+
return self._start_static(preferred_port)
|
|
224
|
+
|
|
225
|
+
self.port = find_available_port(preferred_port or 5173)
|
|
226
|
+
try:
|
|
227
|
+
# Launch dev process safely
|
|
228
|
+
env = os.environ.copy()
|
|
229
|
+
env["PORT"] = str(self.port)
|
|
230
|
+
self._proc = subprocess.Popen(
|
|
231
|
+
cmd,
|
|
232
|
+
cwd=self.root,
|
|
233
|
+
env=env,
|
|
234
|
+
stdout=subprocess.PIPE,
|
|
235
|
+
stderr=subprocess.STDOUT,
|
|
236
|
+
text=True,
|
|
237
|
+
bufsize=1,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Monitor startup in background thread
|
|
241
|
+
def _reader():
|
|
242
|
+
while self._proc and self._proc.poll() is None:
|
|
243
|
+
line = self._proc.stdout.readline()
|
|
244
|
+
if line:
|
|
245
|
+
self._logs.append(line.rstrip())
|
|
246
|
+
if len(self._logs) > 500:
|
|
247
|
+
del self._logs[:-500]
|
|
248
|
+
|
|
249
|
+
t = threading.Thread(target=_reader, daemon=True)
|
|
250
|
+
t.start()
|
|
251
|
+
|
|
252
|
+
# Wait briefly for process to initialize or report url
|
|
253
|
+
start_time = time.monotonic()
|
|
254
|
+
while time.monotonic() - start_time < 5.0:
|
|
255
|
+
if self._proc.poll() is not None:
|
|
256
|
+
err = "\n".join(self._logs[-10:]) or "Process exited prematurely."
|
|
257
|
+
self._notify(ServerState.ERROR, err)
|
|
258
|
+
return False, err
|
|
259
|
+
if not is_port_available(self.port):
|
|
260
|
+
# Port is now bound by dev server!
|
|
261
|
+
break
|
|
262
|
+
time.sleep(0.2)
|
|
263
|
+
|
|
264
|
+
self.url = f"http://127.0.0.1:{self.port}/"
|
|
265
|
+
self._notify(ServerState.RUNNING)
|
|
266
|
+
return True, self.url
|
|
267
|
+
except Exception as e:
|
|
268
|
+
err = f"Failed to spawn dev command {cmd}: {e}"
|
|
269
|
+
self._notify(ServerState.ERROR, err)
|
|
270
|
+
return False, err
|
|
271
|
+
|
|
272
|
+
def stop(self) -> None:
|
|
273
|
+
"""Cleanly stop the running development server."""
|
|
274
|
+
if self._server_instance is not None:
|
|
275
|
+
try:
|
|
276
|
+
self._server_instance.shutdown()
|
|
277
|
+
except Exception:
|
|
278
|
+
pass
|
|
279
|
+
self._server_instance = None
|
|
280
|
+
|
|
281
|
+
if self._proc is not None:
|
|
282
|
+
try:
|
|
283
|
+
self._proc.terminate()
|
|
284
|
+
self._proc.wait(timeout=2.0)
|
|
285
|
+
except Exception:
|
|
286
|
+
try:
|
|
287
|
+
self._proc.kill()
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
self._proc = None
|
|
291
|
+
|
|
292
|
+
self._notify(ServerState.STOPPED)
|
|
293
|
+
|
|
294
|
+
def restart(self) -> Tuple[bool, str]:
|
|
295
|
+
"""Restart the running server."""
|
|
296
|
+
self._notify(ServerState.RESTARTING)
|
|
297
|
+
self.stop()
|
|
298
|
+
time.sleep(0.3)
|
|
299
|
+
return self.start(self.port)
|
|
300
|
+
|
|
301
|
+
def get_logs(self, limit: int = 50) -> List[str]:
|
|
302
|
+
"""Return latest captured stdout/stderr lines."""
|
|
303
|
+
return list(self._logs[-limit:])
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""CAT Live Preview — Diagnostics & Error Mapping (calc_terminal/preview/diagnostics.py).
|
|
2
|
+
|
|
3
|
+
Unified diagnostic model and error navigation:
|
|
4
|
+
- Normalizes errors from browser runtime, dev server, build, and editor.
|
|
5
|
+
- Extracts file, line, and column coordinates from error messages.
|
|
6
|
+
- Supports jumping from Problems/Console errors directly into the source code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import List, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SourceLocation:
|
|
20
|
+
"""Exact source code target of a diagnostic error."""
|
|
21
|
+
file: str
|
|
22
|
+
line: Optional[int] = None
|
|
23
|
+
column: Optional[int] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Diagnostic:
|
|
28
|
+
"""Normalized diagnostic entry across browser, server, build, and editor."""
|
|
29
|
+
id: str
|
|
30
|
+
severity: str # "info", "warning", "error"
|
|
31
|
+
message: str
|
|
32
|
+
file: Optional[str] = None
|
|
33
|
+
line: Optional[int] = None
|
|
34
|
+
column: Optional[int] = None
|
|
35
|
+
source: str = "editor" # "browser", "server", "build", "editor"
|
|
36
|
+
timestamp: float = 0.0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Regex patterns to detect file:line:col in runtime and build errors
|
|
40
|
+
# e.g., main.js:42:17, ./src/App.tsx (12:5), File "app.py", line 123
|
|
41
|
+
_LOCATION_PATTERNS = [
|
|
42
|
+
re.compile(r'([a-zA-Z0-9_\-\.\/\\]+\.(?:js|jsx|ts|tsx|py|html|css)):(\d+):(\d+)'),
|
|
43
|
+
re.compile(r'([a-zA-Z0-9_\-\.\/\\]+\.(?:js|jsx|ts|tsx|py|html|css)):(\d+)'),
|
|
44
|
+
re.compile(r'File "([^"]+)", line (\d+)'),
|
|
45
|
+
re.compile(r'([a-zA-Z0-9_\-\.\/\\]+\.(?:js|jsx|ts|tsx|py|html|css))\s*\((\d+):(\d+)\)'),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_error_location(error_text: str, workspace_root: Optional[str] = None) -> Optional[SourceLocation]:
|
|
50
|
+
"""Extract source file, line, and column from an error string."""
|
|
51
|
+
if not error_text:
|
|
52
|
+
return None
|
|
53
|
+
for pattern in _LOCATION_PATTERNS:
|
|
54
|
+
match = pattern.search(error_text)
|
|
55
|
+
if match:
|
|
56
|
+
groups = match.groups()
|
|
57
|
+
raw_path = groups[0]
|
|
58
|
+
line = int(groups[1]) if len(groups) > 1 and groups[1] else None
|
|
59
|
+
col = int(groups[2]) if len(groups) > 2 and groups[2] else None
|
|
60
|
+
|
|
61
|
+
# Resolve relative paths against workspace root if provided
|
|
62
|
+
abs_path = raw_path
|
|
63
|
+
if workspace_root and not os.path.isabs(raw_path):
|
|
64
|
+
cand = os.path.join(workspace_root, raw_path)
|
|
65
|
+
if os.path.exists(cand):
|
|
66
|
+
abs_path = os.path.abspath(cand)
|
|
67
|
+
|
|
68
|
+
return SourceLocation(file=abs_path, line=line, column=col)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DiagnosticsManager:
|
|
73
|
+
"""Collects and organizes diagnostics from all runtime and build systems."""
|
|
74
|
+
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
self._diagnostics: List[Diagnostic] = []
|
|
77
|
+
|
|
78
|
+
def add(self, severity: str, message: str, file: Optional[str] = None,
|
|
79
|
+
line: Optional[int] = None, column: Optional[int] = None,
|
|
80
|
+
source: str = "editor") -> Diagnostic:
|
|
81
|
+
"""Add a new diagnostic item."""
|
|
82
|
+
# Auto-parse location if not explicitly provided
|
|
83
|
+
if not file or line is None:
|
|
84
|
+
loc = parse_error_location(message)
|
|
85
|
+
if loc:
|
|
86
|
+
file = file or loc.file
|
|
87
|
+
line = line if line is not None else loc.line
|
|
88
|
+
column = column if column is not None else loc.column
|
|
89
|
+
|
|
90
|
+
diag = Diagnostic(
|
|
91
|
+
id=uuid.uuid4().hex[:8],
|
|
92
|
+
severity=severity.lower(),
|
|
93
|
+
message=message.strip(),
|
|
94
|
+
file=file,
|
|
95
|
+
line=line,
|
|
96
|
+
column=column,
|
|
97
|
+
source=source,
|
|
98
|
+
timestamp=__import__("time").time(),
|
|
99
|
+
)
|
|
100
|
+
self._diagnostics.append(diag)
|
|
101
|
+
if len(self._diagnostics) > 200:
|
|
102
|
+
del self._diagnostics[:-200]
|
|
103
|
+
return diag
|
|
104
|
+
|
|
105
|
+
def list_diagnostics(self, severity: Optional[str] = None, source: Optional[str] = None) -> List[Diagnostic]:
|
|
106
|
+
"""Return matching diagnostics filtered by severity and/or source."""
|
|
107
|
+
results = list(self._diagnostics)
|
|
108
|
+
if severity:
|
|
109
|
+
results = [d for d in results if d.severity == severity.lower()]
|
|
110
|
+
if source:
|
|
111
|
+
results = [d for d in results if d.source == source.lower()]
|
|
112
|
+
return results
|
|
113
|
+
|
|
114
|
+
def clear(self, source: Optional[str] = None) -> None:
|
|
115
|
+
"""Clear diagnostics, optionally for a specific source."""
|
|
116
|
+
if source:
|
|
117
|
+
self._diagnostics = [d for d in self._diagnostics if d.source != source.lower()]
|
|
118
|
+
else:
|
|
119
|
+
self._diagnostics.clear()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Global diagnostics singleton
|
|
123
|
+
_GLOBAL_DIAGNOSTICS_MANAGER: Optional[DiagnosticsManager] = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_diagnostics_manager() -> DiagnosticsManager:
|
|
127
|
+
"""Return global DiagnosticsManager singleton."""
|
|
128
|
+
global _GLOBAL_DIAGNOSTICS_MANAGER
|
|
129
|
+
if _GLOBAL_DIAGNOSTICS_MANAGER is None:
|
|
130
|
+
_GLOBAL_DIAGNOSTICS_MANAGER = DiagnosticsManager()
|
|
131
|
+
return _GLOBAL_DIAGNOSTICS_MANAGER
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""CAT Live Preview — Live Reload Manager (calc_terminal/preview/live_reload.py).
|
|
2
|
+
|
|
3
|
+
Debounced change aggregation and hot-reload dispatcher for local web projects.
|
|
4
|
+
Avoids restarting dev servers on ordinary keystrokes / edits.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from typing import Callable, List, Optional, Set
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LiveReloadManager:
|
|
16
|
+
"""Aggregates file modification events and emits debounced reload requests."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, debounce_sec: float = 0.25, on_reload: Optional[Callable[[List[str], bool], None]] = None) -> None:
|
|
19
|
+
self.debounce_sec = debounce_sec
|
|
20
|
+
self.on_reload = on_reload # (changed_files, is_css_only) -> None
|
|
21
|
+
self._pending_files: Set[str] = set()
|
|
22
|
+
self._timer: Optional[threading.Timer] = None
|
|
23
|
+
self._lock = threading.Lock()
|
|
24
|
+
self._last_reload_time: float = 0.0
|
|
25
|
+
|
|
26
|
+
def notify_file_changed(self, file_path: str) -> None:
|
|
27
|
+
"""Register that a file in the workspace was saved or edited."""
|
|
28
|
+
abs_path = os.path.abspath(file_path)
|
|
29
|
+
with self._lock:
|
|
30
|
+
self._pending_files.add(abs_path)
|
|
31
|
+
if self._timer is not None:
|
|
32
|
+
self._timer.cancel()
|
|
33
|
+
self._timer = threading.Timer(self.debounce_sec, self._flush)
|
|
34
|
+
self._timer.daemon = True
|
|
35
|
+
self._timer.start()
|
|
36
|
+
|
|
37
|
+
def _flush(self) -> None:
|
|
38
|
+
"""Process pending changes and invoke reload callback."""
|
|
39
|
+
with self._lock:
|
|
40
|
+
if not self._pending_files:
|
|
41
|
+
return
|
|
42
|
+
files = list(self._pending_files)
|
|
43
|
+
self._pending_files.clear()
|
|
44
|
+
self._timer = None
|
|
45
|
+
|
|
46
|
+
# Ignore if reloaded too frequently (avoid reload loops)
|
|
47
|
+
now = time.monotonic()
|
|
48
|
+
if now - self._last_reload_time < 0.1:
|
|
49
|
+
return
|
|
50
|
+
self._last_reload_time = now
|
|
51
|
+
|
|
52
|
+
# Determine if CSS only
|
|
53
|
+
is_css_only = all(f.lower().endswith(".css") for f in files)
|
|
54
|
+
if self.on_reload:
|
|
55
|
+
try:
|
|
56
|
+
self.on_reload(files, is_css_only)
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
def cancel(self) -> None:
|
|
61
|
+
"""Cancel any pending debounce timer."""
|
|
62
|
+
with self._lock:
|
|
63
|
+
if self._timer is not None:
|
|
64
|
+
self._timer.cancel()
|
|
65
|
+
self._timer = None
|
|
66
|
+
self._pending_files.clear()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""CAT Live Preview — Preview Manager (calc_terminal/preview/manager.py).
|
|
2
|
+
|
|
3
|
+
High-level session coordinator according to Preview API contract:
|
|
4
|
+
- start(), stop(), restart(), reload(), get(), get_active().
|
|
5
|
+
- Coordinates between DevServerManager, LiveReloadManager, and UI.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Dict, Optional
|
|
14
|
+
|
|
15
|
+
from .dev_server import DevServerManager, ServerState
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class PreviewSession:
|
|
20
|
+
"""Represents an active preview session for a project."""
|
|
21
|
+
id: str
|
|
22
|
+
project_path: str
|
|
23
|
+
url: str
|
|
24
|
+
port: int
|
|
25
|
+
status: str
|
|
26
|
+
framework: Optional[str] = None
|
|
27
|
+
created_at: float = 0.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PreviewManager:
|
|
31
|
+
"""Manages active preview sessions across CAT IDE."""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._sessions: Dict[str, PreviewSession] = {}
|
|
35
|
+
self._path_to_session_id: Dict[str, str] = {}
|
|
36
|
+
|
|
37
|
+
def start(self, project_path: str, options: Optional[dict] = None) -> PreviewSession:
|
|
38
|
+
"""Start or reuse an existing preview session for a project path."""
|
|
39
|
+
norm_path = DevServerManager(project_path).root
|
|
40
|
+
existing = self.get_active(norm_path)
|
|
41
|
+
if existing and existing.status == ServerState.RUNNING:
|
|
42
|
+
return existing
|
|
43
|
+
|
|
44
|
+
dev_mgr = DevServerManager.get_or_create(norm_path)
|
|
45
|
+
ok, url = dev_mgr.start()
|
|
46
|
+
session_id = self._path_to_session_id.get(norm_path) or uuid.uuid4().hex
|
|
47
|
+
|
|
48
|
+
status = dev_mgr.state
|
|
49
|
+
port = dev_mgr.port or 5173
|
|
50
|
+
framework = dev_mgr.project_info.kind if dev_mgr.project_info.is_framework else None
|
|
51
|
+
|
|
52
|
+
session = PreviewSession(
|
|
53
|
+
id=session_id,
|
|
54
|
+
project_path=norm_path,
|
|
55
|
+
url=url if ok else "",
|
|
56
|
+
port=port,
|
|
57
|
+
status=status,
|
|
58
|
+
framework=framework,
|
|
59
|
+
created_at=time.time(),
|
|
60
|
+
)
|
|
61
|
+
self._sessions[session_id] = session
|
|
62
|
+
self._path_to_session_id[norm_path] = session_id
|
|
63
|
+
return session
|
|
64
|
+
|
|
65
|
+
def stop(self, session_id: str) -> None:
|
|
66
|
+
"""Stop a preview session."""
|
|
67
|
+
session = self.get(session_id)
|
|
68
|
+
if session:
|
|
69
|
+
dev_mgr = DevServerManager.get_or_create(session.project_path)
|
|
70
|
+
dev_mgr.stop()
|
|
71
|
+
session.status = ServerState.STOPPED
|
|
72
|
+
|
|
73
|
+
def restart(self, session_id: str) -> Optional[PreviewSession]:
|
|
74
|
+
"""Restart a preview session."""
|
|
75
|
+
session = self.get(session_id)
|
|
76
|
+
if not session:
|
|
77
|
+
return None
|
|
78
|
+
dev_mgr = DevServerManager.get_or_create(session.project_path)
|
|
79
|
+
ok, url = dev_mgr.restart()
|
|
80
|
+
session.status = dev_mgr.state
|
|
81
|
+
session.url = url
|
|
82
|
+
return session
|
|
83
|
+
|
|
84
|
+
def reload(self, session_id: str) -> bool:
|
|
85
|
+
"""Trigger reload of the active preview."""
|
|
86
|
+
session = self.get(session_id)
|
|
87
|
+
if not session:
|
|
88
|
+
return False
|
|
89
|
+
# Notify connected preview clients through LiveServer ws channel if static
|
|
90
|
+
dev_mgr = DevServerManager.get_or_create(session.project_path)
|
|
91
|
+
if dev_mgr._server_instance is not None and hasattr(dev_mgr._server_instance, "broadcast"):
|
|
92
|
+
try:
|
|
93
|
+
dev_mgr._server_instance.broadcast({"cmd": "reload"})
|
|
94
|
+
return True
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def get(self, session_id: str) -> Optional[PreviewSession]:
|
|
100
|
+
"""Look up a preview session by its ID."""
|
|
101
|
+
return self._sessions.get(session_id)
|
|
102
|
+
|
|
103
|
+
def get_active(self, project_path: str) -> Optional[PreviewSession]:
|
|
104
|
+
"""Return the active preview session for a project path if any exists."""
|
|
105
|
+
norm_path = DevServerManager(project_path).root
|
|
106
|
+
sid = self._path_to_session_id.get(norm_path)
|
|
107
|
+
if sid:
|
|
108
|
+
return self._sessions.get(sid)
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def cleanup_all(self) -> None:
|
|
112
|
+
"""Clean up all active preview sessions and dev servers."""
|
|
113
|
+
for sid in list(self._sessions.keys()):
|
|
114
|
+
self.stop(sid)
|
|
115
|
+
self._sessions.clear()
|
|
116
|
+
self._path_to_session_id.clear()
|
|
117
|
+
DevServerManager.cleanup_all()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Global preview manager singleton
|
|
121
|
+
_GLOBAL_PREVIEW_MANAGER: Optional[PreviewManager] = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_preview_manager() -> PreviewManager:
|
|
125
|
+
"""Return the global PreviewManager singleton."""
|
|
126
|
+
global _GLOBAL_PREVIEW_MANAGER
|
|
127
|
+
if _GLOBAL_PREVIEW_MANAGER is None:
|
|
128
|
+
_GLOBAL_PREVIEW_MANAGER = PreviewManager()
|
|
129
|
+
return _GLOBAL_PREVIEW_MANAGER
|