# agent_v0/server_v1/monitor_router.py """MonitorRouter — résolution de l'écran cible pour le replay (QW1). Stratégie en cascade : 1. action.monitor_index (hérité de la session source) → cible cet écran 2. session.last_focused_monitor (focus actif vu en dernier heartbeat) → fallback 3. composite (offset 0, 0) → backward compat Émet sur le bus lea:* l'event monitor_routed avec la source de la décision. """ from dataclasses import dataclass from typing import Any, Dict, List, Optional @dataclass class MonitorTarget: """Représente l'écran cible résolu pour une action de replay.""" idx: int offset_x: int offset_y: int w: int h: int source: str # "action" | "focus" | "composite_fallback" _COMPOSITE_FALLBACK = MonitorTarget( idx=-1, offset_x=0, offset_y=0, w=0, h=0, source="composite_fallback", ) def _find_monitor(geometry: List[Dict[str, Any]], idx: int) -> Optional[Dict[str, Any]]: """Retourne le monitor d'index donné, ou None si absent.""" for m in geometry: if m.get("idx") == idx: return m return None def _to_target(monitor: Dict[str, Any], source: str) -> MonitorTarget: return MonitorTarget( idx=int(monitor["idx"]), offset_x=int(monitor.get("x", 0)), offset_y=int(monitor.get("y", 0)), w=int(monitor.get("w", 0)), h=int(monitor.get("h", 0)), source=source, ) def resolve_target_monitor( action: Dict[str, Any], session_state: Dict[str, Any], ) -> MonitorTarget: """Résout l'écran cible d'une action de replay. Args: action: Dict de l'action (peut contenir `monitor_index`). session_state: État de la session (doit contenir `monitors_geometry` et `last_focused_monitor`). Returns: MonitorTarget avec l'offset à appliquer aux coordonnées de grounding. """ geometry: List[Dict[str, Any]] = session_state.get("monitors_geometry") or [] # 1. Cible explicite via action explicit_idx = action.get("monitor_index") if explicit_idx is not None and geometry: m = _find_monitor(geometry, int(explicit_idx)) if m is not None: return _to_target(m, source="action") # Index invalide → on tombe sur le fallback focus # 2. Fallback focus actif focused_idx = session_state.get("last_focused_monitor") if focused_idx is not None and geometry: m = _find_monitor(geometry, int(focused_idx)) if m is not None: return _to_target(m, source="focus") # 3. Fallback composite (backward compat — comportement actuel mss.monitors[0]) return _COMPOSITE_FALLBACK