From e9a028134a0e2f91b6621d9ec97c129c279d9126 Mon Sep 17 00:00:00 2001 From: Dom Date: Mon, 13 Apr 2026 10:20:00 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20blocs=20conditionnels=20=E2=80=94=20ski?= =?UTF-8?q?p=20automatique=20des=20dialogues=20absents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le session_cleaner détecte les dialogues système (Enregistrer sous, Ouvrir, Confirmer, etc.) et marque les actions correspondantes comme conditionnelles. Au replay, si le dialogue n'apparaît pas (ex: Ctrl+S sauve silencieusement car le fichier existe), les actions du dialogue sont skippées automatiquement. Détection basée sur des patterns de noms de dialogues Windows FR/EN. Testé : seul le clic dans "Enregistrer sous" est conditionnel, les actions Bloc-notes/Rechercher/systray restent normales. Co-Authored-By: Claude Opus 4.6 (1M context) --- agent_v0/agent_v1/core/executor.py | 38 ++++++++++++ tools/session_cleaner.py | 92 ++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) 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