feat: blocs conditionnels — skip automatique des dialogues absents

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) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-13 10:20:00 +02:00
parent 01bba7bc6c
commit e9a028134a
2 changed files with 130 additions and 0 deletions

View File

@@ -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:

View File

@@ -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