#!/usr/bin/env python3 """Bridge desktop-only tools to Hermes-desktop renderer events. The desktop ``tui_gateway`` installs an emitter via :func:`set_emitter`; elsewhere it stays ``None`` and tools report "desktop only". Routing keys off ``HERMES_UI_SESSION_ID`` so the event lands on the window that owns the turn (the sink is lock-guarded). """ import json from typing import Callable, Optional from gateway.session_context import get_session_env from tools.registry import tool_error # (sid, event, payload) sink, installed by the desktop gateway. _emit: Optional[Callable[[str, str, dict], None]] = None def set_emitter(fn: Optional[Callable[[str, str, dict], None]]) -> None: """Install (or clear) the renderer-event sink. Called by the desktop gateway.""" global _emit _emit = fn def available() -> bool: """True when running under the desktop app (an emitter is wired).""" return _emit is not None def user_enabled(setting: str, default: bool) -> bool: """Read one of the desktop's Appearance switches from ``display.``. The renderer mirrors these toggles onto the CONNECTED gateway's config, so this reads the user's real answer for local/SSH/URL/cloud gateways alike (an env var would only describe the process). ``check_fn``s use it to withdraw a tool from the schema when the user switched the feature off — Hermes should not be told about a surface it may not use. Unreadable config -> ``default`` so a shipped-on feature does not vanish on a transient read error. """ try: from hermes_cli.config import load_config_readonly display = load_config_readonly().get("display") except Exception: return default if not isinstance(display, dict) or setting not in display: return default return bool(display.get(setting)) def emit(event: str, payload: dict) -> bool: """Route ``event`` to the window that owns the current turn. Returns ``False`` when no emitter is wired (i.e. not the desktop app).""" fn = _emit if fn is None: return False fn(get_session_env("HERMES_UI_SESSION_ID", ""), event, payload) return True def emit_or_error(event: str, payload: dict, fail_prefix: str, desktop_only: str, result: dict) -> str: """Emit ``event``; return ``tool_error`` text on failure, else ``result`` as JSON. ``fail_prefix`` is prepended to the exception text; ``desktop_only`` is the error when no emitter is wired. Looked up as ``desktop_ui.emit`` so tests can patch it. """ try: ok = emit(event, payload) except Exception as exc: return tool_error(f"{fail_prefix}{exc}") if not ok: return tool_error(desktop_only) return json.dumps(result, ensure_ascii=False) def passthrough_json(raw) -> str: """Desktop answers with a JSON object; pass it through, else wrap the raw text.""" try: return json.dumps(json.loads(raw), ensure_ascii=False) except (TypeError, ValueError): return json.dumps({"text": str(raw)}, ensure_ascii=False)