from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from pathlib import Path from typing import Any from card_store import ( delete_cards_for_chat, ensure_card_store_dirs, sync_templates_context_file, ) from inbox_service import InboxService from nanobot_api_client import send_nanobot_api_request from rtc_manager import RtcSessionManager from session_store import ensure_session_store_dirs from supertonic_gateway import SuperTonicGateway from tool_job_service import ToolJobService from ui_event_bus import UiEventBus from workbench_store import ensure_workbench_store_dirs def normalize_event_chat_id(normalize_chat_id, raw_chat_id: Any) -> str | None: chat_id = normalize_chat_id(str(raw_chat_id or "")) return chat_id or None @dataclass(slots=True) class WebAppRuntime: gateway: SuperTonicGateway event_bus: UiEventBus inbox_service: InboxService tool_job_service: ToolJobService rtc_manager: RtcSessionManager | None list_sessions: Callable[[], list[dict[str, Any]]] normalize_chat_id: Callable[[str], str] def open_inbox_items(self, *, limit: int = 4) -> list[dict[str, Any]]: return self.inbox_service.open_items(limit=limit) async def publish_sessions_changed(self) -> None: await self.event_bus.publish( { "type": "sessions.changed", "sessions": self.list_sessions(), }, broadcast=True, ) async def publish_cards_changed(self, chat_id: Any = None) -> None: target_chat_id = normalize_event_chat_id(self.normalize_chat_id, chat_id) await self.event_bus.publish( {"type": "cards.changed", "chat_id": target_chat_id}, chat_id=target_chat_id, broadcast=target_chat_id is None, ) async def publish_workbench_changed(self, chat_id: Any = None) -> None: target_chat_id = normalize_event_chat_id(self.normalize_chat_id, chat_id) await self.event_bus.publish( {"type": "workbench.changed", "chat_id": target_chat_id}, chat_id=target_chat_id, broadcast=target_chat_id is None, ) async def publish_inbox_changed(self) -> None: await self.event_bus.publish( { "type": "inbox.changed", "items": self.open_inbox_items(), }, broadcast=True, ) async def shutdown(self) -> None: await self.tool_job_service.shutdown() if self.rtc_manager is not None: await self.rtc_manager.shutdown() await self.gateway.shutdown() @staticmethod def delete_cards_for_chat(chat_id: str) -> int: return delete_cards_for_chat(chat_id) def create_web_runtime( *, repo_dir: Path, inbox_dir: Path, tool_job_timeout_seconds: float, tool_job_retention_seconds: float, list_sessions, normalize_chat_id, ) -> WebAppRuntime: ensure_card_store_dirs() ensure_session_store_dirs() ensure_workbench_store_dirs() inbox_dir.mkdir(parents=True, exist_ok=True) sync_templates_context_file() gateway = SuperTonicGateway() event_bus = UiEventBus() inbox_service = InboxService(repo_dir=repo_dir, inbox_dir=inbox_dir) tool_job_service = ToolJobService( send_request=lambda method, params: send_nanobot_api_request( method, params, timeout_seconds=tool_job_timeout_seconds, ), timeout_seconds=tool_job_timeout_seconds, retention_seconds=tool_job_retention_seconds, ) runtime = WebAppRuntime( gateway=gateway, event_bus=event_bus, inbox_service=inbox_service, tool_job_service=tool_job_service, rtc_manager=None, list_sessions=list_sessions, normalize_chat_id=normalize_chat_id, ) runtime.rtc_manager = RtcSessionManager( gateway=gateway, publish_cards_changed=runtime.publish_cards_changed, publish_workbench_changed=runtime.publish_workbench_changed, publish_sessions_changed=runtime.publish_sessions_changed, ) return runtime