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

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