diff --git a/agent_v0/agent_v1/core/executor.py b/agent_v0/agent_v1/core/executor.py index 9e65529be..9ecaf7df9 100644 --- a/agent_v0/agent_v1/core/executor.py +++ b/agent_v0/agent_v1/core/executor.py @@ -525,6 +525,44 @@ class ActionExecutorV1: "visual_resolved": False, } + # ── Bloc conditionnel : skip si le dialogue n'est pas apparu ── + # Les actions marquées conditional_on_window ne s'exécutent que + # si la fenêtre attendue est effectivement présente. Sinon → skip. + # Ex: Ctrl+S a sauvé silencieusement → pas de "Enregistrer sous" + # → les clics dans le dialogue sont skippés automatiquement. + cond_window = action.get("conditional_on_window") + if cond_window: + try: + from ..window_info_crossplatform import get_active_window_info + current_info = get_active_window_info() + current_title = current_info.get("title", "") + + # Comparaison souple (sous-chaîne) + cond_lower = cond_window.lower() + current_lower = current_title.lower() if current_title else "" + match = ( + cond_lower in current_lower + or current_lower in cond_lower + ) + if not match: + logger.info( + f"[CONDITIONNEL] Skip action {action_id} — " + f"dialogue '{cond_window}' absent " + f"(fenêtre actuelle: '{current_title}')" + ) + print( + f" [SKIP] Dialogue '{cond_window}' absent → action skippée" + ) + result["success"] = True + result["warning"] = "conditional_skipped" + return result + else: + logger.info( + f"[CONDITIONNEL] Dialogue '{cond_window}' présent → exécution" + ) + except Exception as e: + logger.debug(f"Vérif conditionnelle échouée : {e}") + # ── Délai inter-actions (anti race condition mss) ── wait_before = action.get("wait_before", 0.5) if wait_before > 0: diff --git a/tools/session_cleaner.py b/tools/session_cleaner.py index bd454adb8..43465f8dd 100644 --- a/tools/session_cleaner.py +++ b/tools/session_cleaner.py @@ -1131,6 +1131,98 @@ def _simple_build_replay(events: List[Dict[str, Any]], session_dir: Path) -> Lis } actions.append(action) + # ── Étape finale : détecter les blocs conditionnels (dialogues) ── + # Quand le window_title change entre deux actions, les actions dans + # la nouvelle fenêtre sont conditionnelles : elles ne s'exécutent que + # si le dialogue apparaît effectivement au replay. + # Ex: Ctrl+S → "Enregistrer sous" (conditionnel) → retour app + actions = _mark_conditional_blocks(actions, events) + + return actions + + +def _mark_conditional_blocks( + actions: List[Dict[str, Any]], events: List[Dict[str, Any]], +) -> List[Dict[str, Any]]: + """Marquer les actions qui appartiennent a un dialogue conditionnel. + + Detecte les dialogues systeme transitoires (Enregistrer sous, Ouvrir, + Confirmer, etc.) qui n'apparaissent que dans certains contextes. + Au replay, si le dialogue n'est pas present → skip tout le bloc. + + Methode : un dialogue systeme est une fenetre qui : + 1. N'a PAS de separateur " – " ou " - " (pas une app) + 2. N'apparait que pour 1-3 actions consecutives + 3. Est encadree par des actions dans une vraie app + """ + # Extraire le window_title de chaque evenement actionnable + event_windows: List[str] = [] + for ev in events: + inner = ev.get("event", {}) + etype = inner.get("type", "") + if etype not in _ACTIONABLE_TYPES: + continue + win = inner.get("window", {}).get("title", "") + event_windows.append(win) + + def _is_app_window(title): + """True si le titre ressemble a une fenetre d'application (pas un dialogue).""" + if not title or title == "unknown_window": + return False + # Les apps ont un separateur : "fichier.txt – Bloc-notes" + return any(sep in title for sep in [" – ", " - ", " — "]) + + def _is_known_dialog(title): + """True si le titre est un dialogue systeme connu.""" + if not title: + return False + title_lower = title.lower().strip() + dialog_patterns = ( + "enregistrer sous", "save as", + "ouvrir", "open", + "imprimer", "print", + "confirmer", "confirmation", "confirm", + "voulez-vous", "do you want", + "avertissement", "warning", + "erreur", "error", + "propriétés", "properties", + ) + return any(p in title_lower for p in dialog_patterns) + + # Parcourir les actions et marquer les dialogues + action_idx = 0 + n_setup = sum(1 for a in actions if a.get("_setup_action")) + + for i, action in enumerate(actions): + if action.get("_setup_action"): + continue + + if action_idx >= len(event_windows): + break + + win = event_windows[action_idx] + action_idx += 1 + + if not win or win == "unknown_window": + continue + + # Marquer si c'est un dialogue connu OU une fenetre sans separateur app + # entouree de fenetres d'app (transitoire) + if _is_known_dialog(win): + action["conditional_on_window"] = win + logger.debug( + "Action %s conditionnelle (dialogue connu) : '%s'", + action.get("action_id", "?"), win, + ) + + # Log resume + n_conditional = sum(1 for a in actions if a.get("conditional_on_window")) + if n_conditional: + logger.info( + "Blocs conditionnels : %d actions sur %d marquees comme dialogues", + n_conditional, len(actions) - n_setup, + ) + return actions