# agent_v1/ui/shared_state.py """ Etat partage entre le systray et le chat Lea. Thread-safe. Point central de verite pour l'etat de l'agent : - Enregistrement en cours (oui/non, nom de la tache) - Replay en cours - Compteur d'actions Les deux composants UI (SmartTrayV1 et ChatWindow) lisent et ecrivent dans cet objet. Chaque changement notifie tous les listeners enregistres. """ from __future__ import annotations import logging import threading from typing import Any, Callable, List, Optional logger = logging.getLogger(__name__) class AgentState: """Etat partage entre le systray et le chat Lea. Thread-safe.""" def __init__(self) -> None: self._lock = threading.Lock() # Etat d'enregistrement self._recording = False self._recording_name = "" self._actions_count = 0 # Etat de replay self._replay_active = False # Callbacks de demarrage/arret de session (relies au moteur agent) self._on_start: Optional[Callable[[str], None]] = None self._on_stop: Optional[Callable[[], None]] = None # Listeners notifies a chaque changement d'etat self._listeners: List[Callable[["AgentState"], None]] = [] # ------------------------------------------------------------------ # Proprietes en lecture seule (thread-safe) # ------------------------------------------------------------------ @property def is_recording(self) -> bool: with self._lock: return self._recording @property def recording_name(self) -> str: with self._lock: return self._recording_name @property def actions_count(self) -> int: with self._lock: return self._actions_count @property def is_replay_active(self) -> bool: with self._lock: return self._replay_active # ------------------------------------------------------------------ # Mutations (thread-safe, notifient les listeners) # ------------------------------------------------------------------ def start_recording(self, name: str) -> None: """Demarre un enregistrement (appele depuis systray OU chat). Appelle le callback on_start si defini, puis notifie les listeners. """ with self._lock: if self._recording: logger.warning("Enregistrement deja en cours, ignore") return self._recording = True self._recording_name = name self._actions_count = 0 on_start = self._on_start logger.info("Enregistrement demarre : %s", name) # Appeler le callback moteur (hors du lock pour eviter deadlock) if on_start is not None: try: on_start(name) except Exception as e: logger.error("Erreur demarrage session : %s", e) # Annuler l'enregistrement si le moteur echoue with self._lock: self._recording = False self._recording_name = "" self._notify_listeners() raise self._notify_listeners() def stop_recording(self) -> None: """Arrete l'enregistrement (appele depuis systray OU chat). Appelle le callback on_stop si defini, puis notifie les listeners. """ with self._lock: if not self._recording: logger.debug("Pas d'enregistrement en cours, ignore") return self._recording = False name = self._recording_name count = self._actions_count on_stop = self._on_stop logger.info("Enregistrement arrete : %s (%d actions)", name, count) # Appeler le callback moteur if on_stop is not None: try: on_stop() except Exception as e: logger.error("Erreur arret session : %s", e) self._notify_listeners() def update_actions_count(self, count: int) -> None: """Met a jour le compteur d'actions (appele par le moteur agent).""" with self._lock: self._actions_count = count self._notify_listeners() def set_replay_active(self, active: bool) -> None: """Active ou desactive le mode replay.""" with self._lock: if self._replay_active == active: return self._replay_active = active logger.info("Replay %s", "actif" if active else "termine") self._notify_listeners() # ------------------------------------------------------------------ # Enregistrement des callbacks et listeners # ------------------------------------------------------------------ def set_on_start(self, callback: Callable[[str], None]) -> None: """Definit le callback appele quand un enregistrement demarre. Ce callback est le pont vers le moteur agent (AgentV1.start_session). """ with self._lock: self._on_start = callback def set_on_stop(self, callback: Callable[[], None]) -> None: """Definit le callback appele quand un enregistrement s'arrete. Ce callback est le pont vers le moteur agent (AgentV1.stop_session). """ with self._lock: self._on_stop = callback def on_change(self, callback: Callable[["AgentState"], None]) -> None: """Enregistre un listener notifie a chaque changement d'etat. Les listeners sont appeles dans un thread separe pour ne pas bloquer l'appelant. """ with self._lock: self._listeners.append(callback) # ------------------------------------------------------------------ # Notification interne # ------------------------------------------------------------------ def _notify_listeners(self) -> None: """Notifie tous les listeners enregistres du changement d'etat.""" with self._lock: listeners = list(self._listeners) for listener in listeners: try: # Appel dans un thread pour ne pas bloquer threading.Thread( target=listener, args=(self,), daemon=True, ).start() except Exception as e: logger.error("Erreur notification listener : %s", e)