# UX Fix — bulle pause Léa (1 affichage, scroll, boutons fonctionnels) **Date** : 2026-05-08 (matin, J démo GHT Sud 95) **Branche** : `feature/qw-suite-mai` **Périmètre** : `agent_v0/agent_v1/` uniquement (client Windows). Pas de modif serveur, pas de modif VWB. ## 1. Constat utilisateur Verbatim Dom (chat avant démo) : > "Pour le thinker, il y en a 3 maintenant ! un en haut à droite de l'écran, l'autre dans le chat (qui n'a pas de scroll donc message tronqué) puis dans le vwb. Je précise également que les boutons annulé sur toutes les questions ne fonctionnent pas !" Quatre bugs UX simultanés au déclenchement d'un `pause_for_human` : 1. **Trois affichages parallèles** — toast Tkinter `paused_toast` en haut à droite, bulle dans la fenêtre chat Tkinter, popup PauseDialog dans le frontend VWB React. 2. **Message tronqué dans la bulle chat** — pas de scroll interne, donc un `pause_message` long (>200 chars, fréquent côté serveur) déborde de la fenêtre. 3. **Bouton "Annuler" inopérant** — clic sans effet visuel. 4. **Bouton "Continuer" sans feedback** — pas de plainte explicite mais aucune confirmation visuelle après clic. ## 2. Diagnostic technique ### Bug 1 — 3 toasts paused_toast en parallèle `grep -rn "show_paused_toast" agent_v0/agent_v1/` a remonté **trois call sites** qui se déclenchent en cascade pour la même pause : | # | Fichier | Ligne | Contexte | |---|---|---|---| | (a) | `core/executor.py` | 1831 | Plan B polling — `replay_paused=True` détecté | | (b) | `ui/chat_window.py` | 860 | `_add_paused_bubble` appelait aussi le toast en complément | | (c) | `ui/notifications.py` | 156 | `notify_message(BLOCAGE)` déclenché en parallèle par `executor` (mécanisme legacy) | Chacun des trois chemins force le toast topmost ⇒ Dom voit 3 popups + 1 bulle = 4 éléments UI. Confirmé dans `agent_debug.log` Windows : ``` 2026-05-08 10:24:37,750 [paused_toast] INFO: paused_toast scheduled on existing Tk root 2026-05-08 10:26:45,586 [paused_toast] INFO: paused_toast scheduled on existing Tk root 2026-05-08 10:28:46,217 [paused_toast] INFO: paused_toast scheduled on existing Tk root ``` Plus la PauseDialog du VWB côté Linux (non concernée par la démo Windows mais visible si on ouvre le navigateur). ### Bug 2 — bulle non scrollable La fenêtre chat dispose déjà d'un Canvas + Scrollbar global (`_build_messages_area`, ligne 464-507). Mais `_render_paused_bubble` n'appelait pas `_scroll_to_bottom()` après le `pack()`, donc une bulle insérée en bas de la zone restait potentiellement masquée. Et le contenu de la bulle (`reason`) était rendu via un `tk.Label` à `wraplength` fixe : pas de scroll interne pour les messages très longs (>300 chars). ### Bug 3 — bouton "Annuler" inopérant Lecture de `_on_paused_abort` (ligne 975) : ```python self._bus.abort_replay(replay_id) # émet lea:replay_abort if self._active_paused_bubble: self._active_paused_bubble["btn_resume"].config(state="disabled") self._active_paused_bubble["btn_abort"].config(state="disabled") ``` Le bus émet bien `lea:replay_abort`. Côté serveur (`agent_chat/app.py:1720`), le handler met `execution_status["running"] = False` et émet `lea:abort_acked`. Mais : - `_on_lea_event` ligne 768 ignore explicitement `lea:abort_acked` (silencieux côté UI). - Aucun `lea:resumed` n'est émis pour un abort (ce serait sémantiquement faux). - Donc `_close_active_paused_bubble` n'est jamais déclenché ⇒ la bulle reste affichée avec ses boutons disabled, sans aucun message de fermeture. Pour Dom, "rien ne se passe". ### Bug 4 — bouton "Continuer" sans feedback immédiat Même mécanisme : le clic émet `lea:replay_resume`, le serveur relaie en POST HTTP vers le streaming server. La fermeture de la bulle ne survient qu'à la réception ultérieure de `lea:resumed` (plusieurs secondes plus tard). Pas de feedback sur le clic lui-même. ## 3. Solution implémentée ### 3.1 Un seul affichage canonique : la bulle chat `core/executor.py` — Plan B simplifié : ```python # UX fix 8 mai 2026 : un seul affichage. La bulle ChatWindow EST l'affichage # canonique (force show + topmost + bell sonore). Plus de paused_toast en double. chat_window = getattr(self, "_chat_window_ref", None) if chat_window is not None: chat_window._add_paused_bubble(payload) else: # Fallback headless / tests : toast Tkinter custom from ..ui.paused_toast import show_paused_toast show_paused_toast(title=..., message=pause_msg[:300]) ``` `ui/chat_window.py:_add_paused_bubble` — suppression du `show_paused_toast` en complément, remplacé par un `root.bell()` natif Tkinter + `attributes("-topmost", True)` + `lift()` pour la mise au premier plan. `ui/notifications.py:notify_message` — suppression du `show_paused_toast` BLOCAGE (devenu redondant). Plyer reste actif comme notification système Windows discrète. ### 3.2 Scroll dans la bulle pour messages longs `_render_paused_bubble` — remplacement du `tk.Label` par un `tk.Text` read-only avec hauteur calculée dynamiquement (2-8 lignes selon longueur), et scrollbar interne au-delà de 280 caractères : ```python approx_lines = max(2, min(8, (len(reason_str) // 60) + 1)) reason_text = tk.Text(msg_frame, height=approx_lines, wrap=tk.WORD, ...) reason_text.insert("1.0", reason_str) reason_text.configure(state="disabled") if len(reason_str) > 280: reason_scroll = tk.Scrollbar(msg_frame, command=reason_text.yview, ...) reason_text.configure(yscrollcommand=reason_scroll.set) ``` Ajout d'un appel `self._scroll_to_bottom()` à la fin de `_render_paused_bubble` ET de `_render_action_bubble` pour que la bulle apparaisse toujours dans la zone visible. ### 3.3 Fermeture immédiate sur Annuler + feedback visuel `_on_paused_abort` : ```python emitted = self._bus.abort_replay(replay_id) if (self._bus and self._bus.connected) else False self._disable_paused_buttons() self._update_paused_feedback("✗ Annulé" if emitted else "✗ Annulé (bus indisponible)") self._close_active_paused_bubble(reason="abort_local") # NEW : fermeture locale immédiate ``` `_on_paused_resume` : même structure avec feedback `"→ Reprise demandée…"`. La bulle reste visible avec boutons disabled jusqu'à réception de `lea:resumed` qui déclenche `_close_active_paused_bubble("lea:resumed")`. Helpers ajoutés : `_disable_paused_buttons()` et `_update_paused_feedback(text)`. Un `feedback_label` (label vide) est intégré dans la bulle au render et mis à jour à chaque clic. ## 4. Test isolé Script ajouté : `agent_v0/agent_v1/tools/test_lea_pause_flow.py` (déployé `C:\rpa_vision\agent_v1\tools\`). Commande exacte sur le PC Windows : ```cmd cd C:\rpa_vision .venv\Scripts\python.exe -m agent_v1.tools.test_lea_pause_flow ``` Le script ouvre une ChatWindow, simule un `paused_need_help` avec un message de 350 chars (« Je n'arrive pas à trouver le champ Numéro de dossier patient... »), et garde la fenêtre ouverte 30s pour validation visuelle. Vérifications attendues : 1. **UN SEUL popup** (la bulle chat dans la fenêtre Léa, pas de toast Tkinter en plus). 2. Message long visible avec scroll interne si débordement. 3. Boutons Continuer / Annuler fonctionnels. 4. Clic Annuler ⇒ bulle fermée + feedback `✗ Annulé`. ## 5. Tests automatisés exécutés ```bash $ pytest tests/unit/test_lea_notifications.py 101 passed in 0.69s $ pytest tests/integration/test_chat_window_templates.py tests/integration/test_feedback_bus_client.py 35 passed ``` Aucune régression. Les tests existants vérifient `notify_message(BLOCAGE)` retourne True — le retour reste True via `notify(...)` (le toast en complément a juste été retiré). ## 6. Déploiement Windows | Fichier | MD5 Linux | MD5 Windows | Match | |---|---|---|---| | `agent_v1/ui/chat_window.py` | `50597f1f7531ab8e15fdc91e3a03e98a` | identique | OK | | `agent_v1/ui/notifications.py` | `8382ce3cbbc819af0e1a25fc708a0596` | identique | OK | | `agent_v1/core/executor.py` | `dfec3a9da28ef44019fd705404d670a5` | identique | OK | | `agent_v1/tools/test_lea_pause_flow.py` | `edd66b613430d10e1fce8c50f478c90c` | identique | OK | Cache `__pycache__` purgé sur Windows : ```powershell Get-ChildItem -Recurse -Path C:\rpa_vision\agent_v1 -Include *.pyc | Remove-Item -Force Get-ChildItem -Recurse -Path C:\rpa_vision\agent_v1 -Include __pycache__ -Directory | Remove-Item -Recurse -Force ``` **Action restante avant démo** : redémarrer l'agent Léa V1 sur le PC Windows (le client doit recharger les modules). Procédure standard : tray icon Léa → Quitter, puis `Win+R` → `C:\rpa_vision\start_lea.cmd` (ou équivalent dans la doc reference_windows_pc.md). ## 7. Synthèse fichiers modifiés - `agent_v0/agent_v1/ui/chat_window.py` : `_add_paused_bubble`, `_render_paused_bubble`, `_on_paused_resume`, `_on_paused_abort` + helpers `_disable_paused_buttons`, `_update_paused_feedback`. Auto-scroll ajouté à `_render_action_bubble` aussi. - `agent_v0/agent_v1/ui/notifications.py` : `notify_message` — suppression du déclenchement `show_paused_toast` BLOCAGE. - `agent_v0/agent_v1/core/executor.py` : Plan B polling — suppression du `show_paused_toast` direct, remplacé par fallback uniquement si `_chat_window_ref` est None. - `agent_v0/agent_v1/tools/test_lea_pause_flow.py` : nouveau script de smoke test. Aucun fichier serveur ni VWB modifié. Conforme `feedback_agent_frozen.md` (modif client validée par Dom ce matin).