"""Preview bundle inspection, live session rendering, and popup helpers.""" from __future__ import annotations import functools import html import json import os import shutil import subprocess from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any, Dict, Iterable, List, Optional, Tuple def _read_json(path: Path) -> Dict[str, Any]: with open(path, "r", encoding="utf-8") as fh: return json.load(fh) def resolve_bundle_ref(bundle_ref: str) -> Tuple[Path, Path]: ref = Path(bundle_ref).expanduser().resolve() if ref.is_dir(): manifest = ref / "manifest.json" if not manifest.is_file(): raise FileNotFoundError(f"manifest.json not found in bundle directory: {ref}") return ref, manifest if ref.is_file(): if ref.name != "manifest.json": raise ValueError("Bundle ref must be a bundle directory or a manifest.json path") return ref.parent, ref raise FileNotFoundError(f"Bundle ref not found: {bundle_ref}") def resolve_session_ref(session_ref: str) -> Tuple[Path, Path]: ref = Path(session_ref).expanduser().resolve() if ref.is_dir(): session_path = ref / "session.json" if not session_path.is_file(): raise FileNotFoundError(f"session.json not found in live session directory: {ref}") return ref, session_path if ref.is_file(): if ref.name != "session.json": raise ValueError("Session ref must be a live session directory or a session.json path") return ref.parent, ref raise FileNotFoundError(f"Session ref not found: {session_ref}") def is_live_session_ref(preview_ref: str) -> bool: ref = Path(preview_ref).expanduser().resolve() if ref.is_dir(): return (ref / "session.json").is_file() return ref.is_file() and ref.name == "session.json" def load_bundle(bundle_ref: str) -> Tuple[Path, Dict[str, Any], Dict[str, Any]]: bundle_dir, manifest_path = resolve_bundle_ref(bundle_ref) manifest = _read_json(manifest_path) summary_rel = manifest.get("summary_path", "summary.json") summary_path = (bundle_dir / summary_rel).resolve() summary = _read_json(summary_path) if summary_path.is_file() else {} return bundle_dir, manifest, summary def load_session(session_ref: str) -> Tuple[Path, Dict[str, Any]]: session_dir, session_path = resolve_session_ref(session_ref) return session_dir, _read_json(session_path) def format_bytes(size: int) -> str: if size < 1024: return f"{size} B" if size < 1024 * 1024: return f"{size / 1024:.1f} KB" if size < 1024 * 1024 * 1024: return f"{size / (1024 * 1024):.1f} MB" return f"{size / (1024 * 1024 * 1024):.1f} GB" _TRAJECTORY_FILENAMES = ("trajectory.json", "timeline.json") _TRAJECTORY_CONTAINER_KEYS = {"trajectory", "timeline"} _TRAJECTORY_PATH_KEYS = { "trajectory_path", "timeline_path", "trajectory_file", "timeline_file", "trajectory_ref", "timeline_ref", } def _coalesce(*values: Any) -> Any: for value in values: if value is None: continue if isinstance(value, str) and not value.strip(): continue return value return None def _stringify_command(value: Any) -> Optional[str]: if value is None: return None if isinstance(value, str): value = value.strip() return value or None if isinstance(value, (list, tuple)): text = " ".join(str(part) for part in value if part is not None) return text.strip() or None if isinstance(value, dict): for key in ("display", "display_cmd", "command", "raw", "argv"): if key in value: return _stringify_command(value[key]) return json.dumps(value, ensure_ascii=False, sort_keys=True) return str(value) def _normalize_index(value: Any, fallback: int) -> int: if isinstance(value, bool): return fallback if isinstance(value, int): return value if isinstance(value, float): return int(value) if isinstance(value, str): stripped = value.strip() if stripped.isdigit(): return int(stripped) return fallback def _resolve_ref_path(base_dir: Path, path_ref: str) -> Path: ref = Path(path_ref).expanduser() if not ref.is_absolute(): ref = (base_dir / ref).resolve() else: ref = ref.resolve() return ref def _iter_trajectory_hints(node: Any, *, _seen: Optional[set[int]] = None) -> Iterable[Tuple[str, Any, str]]: if _seen is None: _seen = set() if not isinstance(node, (dict, list)): return node_id = id(node) if node_id in _seen: return _seen.add(node_id) if isinstance(node, list): for item in node: yield from _iter_trajectory_hints(item, _seen=_seen) return for key, value in node.items(): lower = str(key).lower() if lower in _TRAJECTORY_CONTAINER_KEYS: if isinstance(value, dict): yield ("object", value, lower) elif isinstance(value, str): yield ("path", value, lower) elif lower in _TRAJECTORY_PATH_KEYS and isinstance(value, str): yield ("path", value, lower) if isinstance(value, (dict, list)): yield from _iter_trajectory_hints(value, _seen=_seen) def _trajectory_candidate_refs(base_dir: Path, *payloads: Dict[str, Any]) -> List[str]: refs: List[str] = [] seen = set() for payload in payloads: if not isinstance(payload, dict): continue for kind, value, _label in _iter_trajectory_hints(payload): if kind != "path" or not isinstance(value, str): continue resolved = _resolve_ref_path(base_dir, value) if not resolved.is_file(): continue rel = os.path.relpath(resolved, base_dir) if rel not in seen: refs.append(rel) seen.add(rel) for filename in _TRAJECTORY_FILENAMES: if filename not in seen: refs.append(filename) seen.add(filename) return refs def _extract_bundle_payload(item: Dict[str, Any]) -> Dict[str, Any]: for key in ("copied_bundle", "bundle", "preview_bundle", "current_bundle", "published_bundle"): value = item.get(key) if isinstance(value, dict): return value return item def _normalize_timeline_row(item: Any, index: int) -> Dict[str, Any]: if not isinstance(item, dict): return { "order_index": index, "step_index": index, "step_id": f"step-{index:03d}", "step_label": str(item), "command": None, "command_started_at": None, "command_finished_at": None, "timeline_ready_at": None, "publish_reason": None, "bundle_id": None, "bundle_dir": None, "manifest_path": None, "summary_path": None, "status": None, "stage_label": None, "note": None, } bundle = _extract_bundle_payload(item) command = _stringify_command( _coalesce( item.get("command"), item.get("display_cmd"), item.get("display_command"), item.get("argv"), item.get("raw_command"), ) ) step_index = _normalize_index( _coalesce(item.get("step_index"), item.get("index"), item.get("sequence_index")), index, ) status = item.get("status") if status is None and "returncode" in item: status = "ok" if int(item.get("returncode", 1)) == 0 else "error" return { "order_index": index, "step_index": step_index, "step_id": str( _coalesce(item.get("step_id"), item.get("id"), item.get("command_id"), f"step-{step_index:03d}") ), "step_label": _coalesce( item.get("step_label"), item.get("label"), item.get("title"), item.get("name"), item.get("stage_title"), item.get("stage_label"), ), "command": command, "command_started_at": _coalesce( item.get("command_started_at"), item.get("started_at"), item.get("timeline_start_s"), item.get("start_s"), ), "command_finished_at": _coalesce( item.get("command_finished_at"), item.get("finished_at"), item.get("timeline_end_s"), item.get("end_s"), item.get("completed_at"), ), "timeline_ready_at": _coalesce( item.get("timeline_ready_s"), item.get("ready_at"), item.get("published_at"), item.get("created_at"), ), "publish_reason": _coalesce(item.get("publish_reason"), item.get("reason")), "bundle_id": _coalesce(item.get("bundle_id"), bundle.get("bundle_id")), "bundle_dir": _coalesce(item.get("bundle_dir"), bundle.get("bundle_dir")), "manifest_path": _coalesce(item.get("manifest_path"), bundle.get("manifest_path")), "summary_path": _coalesce(item.get("summary_path"), bundle.get("summary_path")), "status": status, "stage_label": _coalesce( item.get("stage_title"), item.get("stage_label"), item.get("stage_id"), item.get("stage"), ), "note": _coalesce( item.get("note"), item.get("stage_story"), item.get("story"), item.get("description"), ), } def _merge_timeline_rows(target: Dict[str, Any], source: Dict[str, Any]) -> None: for key in ( "step_label", "command", "command_started_at", "command_finished_at", "timeline_ready_at", "publish_reason", "bundle_id", "bundle_dir", "manifest_path", "summary_path", "status", "stage_label", "note", ): target[key] = _coalesce(target.get(key), source.get(key)) def _pick_trajectory_events(raw: Dict[str, Any]) -> List[Dict[str, Any]]: for key in ("preview_events", "events", "publishes", "entries", "history", "steps", "timeline"): value = raw.get(key) if isinstance(value, list) and value: return value return [] def _sort_timeline_rows(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def sort_key(row: Dict[str, Any]) -> Tuple[int, int, str]: step_index = _normalize_index(row.get("step_index"), row.get("order_index", 0)) order_index = _normalize_index(row.get("order_index"), step_index) finish = _coalesce(row.get("command_finished_at"), row.get("timeline_ready_at"), "") return step_index, order_index, str(finish) return sorted(rows, key=sort_key) def _normalize_trajectory(raw: Optional[Dict[str, Any]], *, fallback_history: Optional[List[Dict[str, Any]]] = None) -> Optional[Dict[str, Any]]: if not raw and not fallback_history: return None rows: List[Dict[str, Any]] = [] rows_by_id: Dict[str, Dict[str, Any]] = {} rows_by_index: Dict[int, Dict[str, Any]] = {} commands = raw.get("commands", []) if isinstance(raw, dict) else [] if isinstance(commands, list): for index, item in enumerate(commands): row = _normalize_timeline_row(item, index) rows.append(row) rows_by_id[row["step_id"]] = row rows_by_index[row["step_index"]] = row events = _pick_trajectory_events(raw) if isinstance(raw, dict) else [] if events: for index, item in enumerate(events): event_row = _normalize_timeline_row(item, index) existing = rows_by_id.get(event_row["step_id"]) or rows_by_index.get(event_row["step_index"]) if existing is None: rows.append(event_row) rows_by_id[event_row["step_id"]] = event_row rows_by_index[event_row["step_index"]] = event_row continue _merge_timeline_rows(existing, event_row) if not rows and fallback_history: for index, item in enumerate(fallback_history): rows.append(_normalize_timeline_row(item, index)) rows = _sort_timeline_rows(rows) if not rows: return None recent_command_row = next((row for row in reversed(rows) if row.get("command")), None) recent_publish_row = next( (row for row in reversed(rows) if row.get("publish_reason") or row.get("bundle_id")), None, ) step_count = _coalesce( raw.get("step_count") if isinstance(raw, dict) else None, len(commands) if isinstance(commands, list) and commands else None, len(rows), ) published_bundles = sum(1 for row in rows if row.get("bundle_id")) return { "protocol": raw.get("protocol") or raw.get("protocol_version") if isinstance(raw, dict) else None, "step_count": step_count, "current_step_id": _coalesce( raw.get("current_step_id") if isinstance(raw, dict) else None, recent_publish_row.get("step_id") if recent_publish_row else None, recent_command_row.get("step_id") if recent_command_row else None, ), "published_bundle_count": published_bundles, "recent_command": _coalesce( raw.get("latest_command") if isinstance(raw, dict) else None, recent_command_row.get("command") if recent_command_row else None, ), "recent_publish_reason": _coalesce( raw.get("latest_publish_reason") if isinstance(raw, dict) else None, recent_publish_row.get("publish_reason") if recent_publish_row else None, ), "recent_bundle_id": recent_publish_row.get("bundle_id") if recent_publish_row else None, "entries": rows, } def _load_trajectory(base_dir: Path, *payloads: Dict[str, Any]) -> Optional[Dict[str, Any]]: for ref in _trajectory_candidate_refs(base_dir, *payloads): candidate = _resolve_ref_path(base_dir, ref) if candidate.is_file(): raw = _read_json(candidate) normalized = _normalize_trajectory(raw) if normalized is not None: normalized["mode"] = "trajectory" normalized["source_path"] = str(candidate) normalized["source_label"] = os.path.relpath(candidate, base_dir) return normalized for payload in payloads: if not isinstance(payload, dict): continue for kind, value, label in _iter_trajectory_hints(payload): if kind != "object" or not isinstance(value, dict): continue normalized = _normalize_trajectory(value) if normalized is not None: normalized["mode"] = "trajectory" normalized["source_path"] = None normalized["source_label"] = label return normalized return None def _history_from_session(session: Dict[str, Any]) -> Optional[Dict[str, Any]]: history = session.get("history", []) if not isinstance(history, list) or not history: return None normalized = _normalize_trajectory({}, fallback_history=history) if normalized is None: return None source_state = session.get("source_state", {}) if isinstance(session.get("source_state"), dict) else {} normalized["mode"] = "legacy-history" normalized["source_path"] = None normalized["source_label"] = "session.history" normalized["current_step_id"] = _coalesce( session.get("current_step_id"), normalized.get("current_step_id"), ) normalized["recent_command"] = _coalesce( session.get("latest_command"), normalized.get("recent_command"), ) normalized["recent_publish_reason"] = _coalesce( session.get("latest_publish_reason"), normalized.get("recent_publish_reason"), source_state.get("last_publish_reason"), ) return normalized def _apply_session_trajectory_metadata(trajectory: Optional[Dict[str, Any]], session: Dict[str, Any]) -> Optional[Dict[str, Any]]: if trajectory is None: return None trajectory["protocol"] = _coalesce( trajectory.get("protocol"), session.get("trajectory_protocol_version"), ) trajectory["step_count"] = _coalesce( session.get("trajectory_step_count"), trajectory.get("step_count"), ) trajectory["current_step_id"] = _coalesce( session.get("current_step_id"), trajectory.get("current_step_id"), ) trajectory["recent_command"] = _coalesce( session.get("latest_command"), trajectory.get("recent_command"), ) trajectory["recent_publish_reason"] = _coalesce( session.get("latest_publish_reason"), trajectory.get("recent_publish_reason"), ) return trajectory def _render_trajectory_text_lines(title: str, trajectory: Optional[Dict[str, Any]], *, limit: int = 5) -> List[str]: if not trajectory: return [] lines = [ "", title, f" Source: {trajectory.get('source_label') or trajectory.get('mode') or 'unknown'}", f" Steps: {trajectory.get('step_count', 0)}", f" Published bundles: {trajectory.get('published_bundle_count', 0)}", ] if trajectory.get("current_step_id"): lines.append(f" Current step: {trajectory['current_step_id']}") if trajectory.get("recent_command"): lines.append(f" Recent command: {trajectory['recent_command']}") if trajectory.get("recent_publish_reason"): lines.append(f" Recent publish: {trajectory['recent_publish_reason']}") if trajectory.get("recent_bundle_id"): lines.append(f" Recent bundle: {trajectory['recent_bundle_id']}") entries = trajectory.get("entries", []) if entries: lines.append(" Timeline") for entry in entries[-limit:]: label = entry.get("step_label") or entry.get("stage_label") or entry.get("step_id") or "step" lines.append(f" - {label}") if entry.get("command"): lines.append(f" Command: {entry['command']}") if entry.get("publish_reason"): lines.append(f" Publish: {entry['publish_reason']}") if entry.get("bundle_id"): lines.append(f" Bundle: {entry['bundle_id']}") return lines def inspect_bundle(bundle_ref: str) -> Dict[str, Any]: bundle_dir, manifest, summary = load_bundle(bundle_ref) return { "bundle_dir": str(bundle_dir), "manifest": manifest, "summary": summary, "artifact_count": len(manifest.get("artifacts", [])), "trajectory": _load_trajectory(bundle_dir, manifest, summary), } def inspect_session(session_ref: str) -> Dict[str, Any]: session_dir, session = load_session(session_ref) current_bundle = None try: current_bundle = inspect_bundle(str(session_dir / session.get("current_link", "current"))) except (FileNotFoundError, ValueError): current_bundle = None trajectory = _load_trajectory(session_dir, session) if trajectory is None: trajectory = _history_from_session(session) trajectory = _apply_session_trajectory_metadata(trajectory, session) return { "session_dir": str(session_dir), "session": session, "current_bundle": current_bundle, "trajectory": trajectory, } def render_inspect_text(bundle_ref: str) -> str: payload = inspect_bundle(bundle_ref) bundle_dir = Path(payload["bundle_dir"]) manifest = payload["manifest"] summary = payload["summary"] lines = [ f"Bundle: {bundle_dir}", f"Protocol: {manifest.get('protocol_version', 'unknown')}", f"Software: {manifest.get('software', 'unknown')}", f"Recipe: {manifest.get('recipe', 'unknown')}", f"Kind: {manifest.get('bundle_kind', 'unknown')}", f"Status: {manifest.get('status', 'unknown')}", f"Created: {manifest.get('created_at', 'unknown')}", ] source = manifest.get("source", {}) if source: lines.append( "Source: " + ( source.get("project_path") or source.get("capture_path") or source.get("project_name") or "n/a" ) ) if source.get("project_fingerprint"): lines.append(f"Fingerprint: {source['project_fingerprint']}") elif source.get("capture_fingerprint"): lines.append(f"Fingerprint: {source['capture_fingerprint']}") if summary: lines.append("") lines.append("Summary") lines.append(f" Headline: {summary.get('headline', '(none)')}") facts = summary.get("facts", {}) for key, value in facts.items(): lines.append(f" {key}: {value}") for warning in summary.get("warnings", []): lines.append(f" Warning: {warning}") lines.append("") lines.append("Artifacts") for artifact in manifest.get("artifacts", []): desc = ( f" - {artifact.get('artifact_id', '?')} " f"[{artifact.get('role', '?')}] {artifact.get('label', '')}" ) desc += f" -> {artifact.get('path', '')}" if artifact.get("bytes") is not None: desc += f" ({format_bytes(int(artifact['bytes']))})" lines.append(desc) lines.extend(_render_trajectory_text_lines("Trajectory", payload.get("trajectory"))) return "\n".join(lines) + "\n" def render_session_text(session_ref: str) -> str: payload = inspect_session(session_ref) session_dir = Path(payload["session_dir"]) session = payload["session"] trajectory = payload.get("trajectory") lines = [ f"Live Session: {session_dir}", f"Protocol: {session.get('protocol_version', 'unknown')}", f"Software: {session.get('software', 'unknown')}", f"Recipe: {session.get('recipe', 'unknown')}", f"Status: {session.get('status', 'unknown')}", f"Updated: {session.get('updated_at', 'unknown')}", f"Current: {session.get('current_bundle_id', 'n/a')}", f"Project: {session.get('project_path') or session.get('project_name') or 'n/a'}", ] if session.get("watch_command"): lines.append(f"Watch: {session['watch_command']}") history_title = "History" if trajectory and trajectory.get("mode") == "legacy-history" else "Trajectory" lines.extend(_render_trajectory_text_lines(history_title, trajectory)) if trajectory is None: history = session.get("history", []) if history: lines.append("") lines.append("History") for item in history: lines.append( f" - {item.get('bundle_id', '?')} " f"[{item.get('status', 'unknown')}] {item.get('created_at', 'unknown')}" ) return "\n".join(lines) + "\n" def _artifact_href(output_dir: Path, bundle_dir: Path, artifact_path: str) -> str: target = (bundle_dir / artifact_path).resolve() return os.path.relpath(target, output_dir) def _render_artifact_card(output_dir: Path, bundle_dir: Path, artifact: Dict[str, Any]) -> str: role = html.escape(artifact.get("role", "artifact")) label = html.escape(artifact.get("label", artifact.get("artifact_id", "artifact"))) path_ref = _artifact_href(output_dir, bundle_dir, artifact.get("path", "")) media_type = artifact.get("media_type", "") size = artifact.get("bytes") meta = [] if artifact.get("width") and artifact.get("height"): meta.append(f"{artifact['width']}×{artifact['height']}") if artifact.get("duration_s") is not None: meta.append(f"{artifact['duration_s']}s") if size is not None: meta.append(format_bytes(int(size))) meta_line = " · ".join(meta) if media_type.startswith("image/"): body = f'{label}' elif media_type.startswith("video/"): body = ( f'" ) else: body = ( '
' f'{html.escape(artifact.get("path", ""))}' "
" ) badge = f'{role}' details = f'
{html.escape(meta_line)}
' if meta_line else "" return ( '
' f"{badge}" f'

{label}

' f"{details}" f"{body}" "
" ) def _render_trajectory_html_section(trajectory: Optional[Dict[str, Any]]) -> str: if not trajectory: return "" summary_cards = [ ("Source", trajectory.get("source_label") or trajectory.get("mode") or "unknown"), ("Steps", trajectory.get("step_count", 0)), ("Published", trajectory.get("published_bundle_count", 0)), ] if trajectory.get("recent_publish_reason"): summary_cards.append(("Recent publish", trajectory["recent_publish_reason"])) elif trajectory.get("recent_bundle_id"): summary_cards.append(("Recent bundle", trajectory["recent_bundle_id"])) cards_html = "".join( f'
{html.escape(str(label))}{html.escape(str(value))}
' for label, value in summary_cards ) items = trajectory.get("entries", [])[-6:] items_html = "".join( ( '
' f'{html.escape(str(item.get("step_label") or item.get("stage_label") or item.get("step_id") or "step"))}' + ( f'
{html.escape(str(item["command"]))}
' if item.get("command") else "" ) + ( f'
Publish reason: {html.escape(str(item["publish_reason"]))}
' if item.get("publish_reason") else "" ) + ( f'
Bundle: {html.escape(str(item["bundle_id"]))}
' if item.get("bundle_id") else "" ) + "
" ) for item in items ) empty_message = '
No step timeline entries yet.
' return ( '
' "

Trajectory

" f'
{cards_html}
' f'
{items_html or empty_message}
' "
" ) def render_html(bundle_ref: str, output_path: str) -> str: payload = inspect_bundle(bundle_ref) bundle_dir = Path(payload["bundle_dir"]) manifest = payload["manifest"] summary = payload["summary"] trajectory = payload.get("trajectory") output_file = Path(output_path).expanduser().resolve() output_file.parent.mkdir(parents=True, exist_ok=True) headline = html.escape( summary.get("headline", f"{manifest.get('software', 'Preview')} preview bundle") ) warnings = summary.get("warnings", []) facts = summary.get("facts", {}) fact_cards = "".join( f'
{html.escape(str(key))}{html.escape(str(value))}
' for key, value in facts.items() ) warning_html = "".join(f"
  • {html.escape(str(item))}
  • " for item in warnings) artifact_cards = "".join( _render_artifact_card(output_file.parent, bundle_dir, artifact) for artifact in manifest.get("artifacts", []) ) trajectory_html = _render_trajectory_html_section(trajectory) html_text = f""" {html.escape(manifest.get("software", "Preview"))} Preview Bundle
    CLI-Anything Preview Bundle

    {headline}

    Software: {html.escape(str(manifest.get("software", "unknown")))} · Recipe: {html.escape(str(manifest.get("recipe", "unknown")))} · Kind: {html.escape(str(manifest.get("bundle_kind", "capture")))}
    {fact_cards or '
    Artifacts' + str(len(manifest.get("artifacts", []))) + '
    '}
    {"
    Warnings
      " + warning_html + "
    " if warning_html else ""}
    Bundle ID{html.escape(str(manifest.get("bundle_id", "unknown")))}
    Status{html.escape(str(manifest.get("status", "unknown")))}
    Created{html.escape(str(manifest.get("created_at", "unknown")))}
    Source{html.escape(str(manifest.get("source", {}).get("project_path") or manifest.get("source", {}).get("capture_path") or "n/a"))}
    {trajectory_html}

    Artifacts

    {artifact_cards}
    """ with open(output_file, "w", encoding="utf-8") as fh: fh.write(html_text) return str(output_file) def render_live_html(session_ref: str, output_path: str, poll_ms: int = 1500) -> str: payload = inspect_session(session_ref) session_dir = Path(payload["session_dir"]) session = payload["session"] trajectory = payload.get("trajectory") output_file = Path(output_path).expanduser().resolve() output_file.parent.mkdir(parents=True, exist_ok=True) headline = html.escape( session.get("project_name") or session.get("project_path") or f"{session.get('software', 'Preview')} live preview" ) poll_ms = max(250, int(poll_ms)) trajectory_candidate_refs = _trajectory_candidate_refs(session_dir, session) html_text = f""" {html.escape(str(session.get("software", "Preview")))} Live Preview
    CLI-Anything Live Preview Session

    {headline}

    Watching
    Polling the latest preview bundle every {poll_ms} ms.

    Hero Frame

    Waiting for the first bundle
    Run {html.escape(session.get("publish_command", "shotcut preview live push"))} to publish or refresh the session.

    Gallery

    Sampled stills for fast visual checks

    Review Clip

    Low-res preview render
    No preview clip has been published yet.

    Session Notes

    Agent-native summary + refresh state

    History / Timeline

    Trajectory-aware command to bundle view
    """ with open(output_file, "w", encoding="utf-8") as fh: fh.write(html_text) return str(output_file) class _NoCacheHandler(SimpleHTTPRequestHandler): """Serve preview assets without cache so live sessions refresh correctly.""" def end_headers(self) -> None: self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") self.send_header("Pragma", "no-cache") self.send_header("Expires", "0") super().end_headers() def log_message(self, format: str, *args: Any) -> None: return def start_static_server(directory: str, host: str = "127.0.0.1", port: int = 0) -> Tuple[ThreadingHTTPServer, str]: root = Path(directory).expanduser().resolve() handler = functools.partial(_NoCacheHandler, directory=str(root)) server = ThreadingHTTPServer((host, int(port)), handler) base_url = f"http://{host}:{server.server_port}" return server, base_url def open_in_browser(target: str) -> Dict[str, Any]: candidates = [ ("chromium", ["chromium", f"--app={target}"]), ("google-chrome", ["google-chrome", f"--app={target}"]), ("google-chrome-stable", ["google-chrome-stable", f"--app={target}"]), ("microsoft-edge", ["microsoft-edge", f"--app={target}"]), ("firefox", ["firefox", "--new-window", target]), ("xdg-open", ["xdg-open", target]), ] for label, command in candidates: binary = shutil.which(command[0]) if not binary: continue full_command = [binary] + command[1:] process = subprocess.Popen( full_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, ) return { "launched": True, "browser": label, "pid": process.pid, "command": full_command, } return { "launched": False, "browser": None, "command": [], "reason": "No supported browser launcher found on PATH", }