---
issueRef: "#83"
version: 5
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
updatedAt: 1784567952845
---
# Cadrage technique — contrainte pour UX

## Constats dans le code existant

La fermeture de la fenêtre principale est déjà interceptée côté Rust/Tauri dans `crates/app-tauri/src/lib.rs` via `window.on_window_event(...)` et `tauri::WindowEvent::CloseRequested`.

Le handler actuel ne bloque pas la fermeture : il exécute directement le teardown applicatif au moment du `CloseRequested` :

- snapshot des fenêtres ouvertes (`SnapshotOpenWindowsInput`) ;
- snapshot des agents en cours (`SnapshotRunningAgentsInput`) pour persister `agent_was_running` avant destruction des PTY ;
- kill de tous les handles PTY vivants ;
- arrêt des serveurs de modèles locaux ;
- arrêt du serveur embedded ;
- fermeture des fenêtres webview secondaires.

Ce flux correspond aux livraisons liées à la fermeture globale de l'application (#39) et aux fenêtres détachées (#50). Les fenêtres détachées ont aussi un handler `CloseRequested`, mais il sert seulement à émettre le lifecycle `closed` pour le panneau concerné ; il ne doit pas porter la confirmation globale.

Point important pour UX/dev : une interception uniquement côté React avec `onCloseRequested` serait fragile tant que le handler Rust actuel continue à faire le teardown immédiatement. Même si le frontend appelle `preventDefault`, le handler Rust peut déjà avoir snapshot/kill les sessions. Il faut donc déplacer/garder la décision de fermeture dans le flux backend, ou au minimum rendre le handler Rust conscient du guard.

## Interception de fermeture recommandée

Le point d'application robuste est le handler Rust de la fenêtre `main` :

1. Sur `WindowEvent::CloseRequested { api, .. }`, calculer si un travail détectable est en cours.
2. Si aucun travail n'est en cours, laisser passer le chemin actuel de shutdown.
3. Si du travail est en cours et qu'aucune confirmation n'a déjà été donnée, appeler `api.prevent_close()` / équivalent Tauri v2, puis demander au frontend d'afficher la popup UX.
4. Si l'utilisateur confirme, relancer une fermeture programmatique via une commande backend dédiée, avec un flag `confirmed_exit`/`exit_guard_bypassed` pour éviter une boucle de confirmation.
5. Le shutdown réel doit réutiliser exactement l'ordre actuel : snapshot fenêtres, snapshot agents, kill PTY, stop model servers, stop embedded server, fermeture des fenêtres secondaires.

Implémentation conseillée : extraire le teardown actuellement inline dans `lib.rs` vers une fonction/service interne réutilisable, par exemple `shutdown_app_after_confirm(app_handle)`. La commande appelée après confirmation doit soit :

- positionner le bypass puis appeler `main.close()` pour repasser par le handler et exécuter le teardown partagé ;
- soit exécuter explicitement le teardown partagé puis quitter l'application.

À éviter : appeler `destroy()` côté frontend ou contourner le handler Rust, car cela risquerait de sauter le snapshot `agent_was_running` et le nettoyage PTY/serveurs.

## Définition technique de « travail en cours »

Définition fiable recommandée pour déclencher la confirmation :

- au moins un agent a `busy.state == "busy"` dans le workstate ;
- ou au moins une tâche de fond non terminale existe : `Queued`, `Running` ou `Waiting` dans `BackgroundTaskState`.

Signaux disponibles et niveau de fiabilité :

- `GetProjectWorkState` / commande Tauri `get_project_work_state(project_id)` : source déjà agrégée par projet. Elle expose les agents du manifeste, leur `live`, leur `busy`, leurs tickets, leurs tâches de fond et les conversations résumées.
- `AgentBusyState` (`crates/domain/src/input.rs`) : signal le plus fiable pour un tour d'agent réellement en vol. `Busy { ticket, since_ms }` signifie qu'un tour a démarré et n'a pas encore rendu la main.
- `BackgroundTaskStore` / `BackgroundTaskState` (`crates/domain/src/background_task.rs`) : fiable pour les travaux asynchrones. Les états non terminaux sont `Queued`, `Running`, `Waiting`. Les états `Completed`, `Failed`, `Cancelled`, `Expired` sont terminaux et ne doivent pas compter comme « travail en cours » pour éviter les faux positifs. Les complétions non livrées peuvent mériter une notification UX, mais elles ne sont plus un travail actif.
- `LiveAgentReadModel` / champ `live` du workstate : indique une session agent vivante, pas nécessairement active. Une session vivante peut être idle ; ne pas l'utiliser seule comme déclencheur, sauf arbitrage produit explicite « prévenir dès qu'une session agent serait arrêtée ».
- PTY actifs (`PtyBridge.active_sessions()` / registre `terminal_sessions`) : utile pour le teardown et le snapshot, mais mauvais critère UX seul. Un terminal shell ouvert peut être idle ; compter tous les PTY provoquerait beaucoup de confirmations inutiles.
- Sessions structurées/chat (`ChatBridge.active_sessions()` ou sessions structurées ouvertes) : une session ouverte seule ne prouve pas un travail actif. À inclure seulement si un état backend indique un tour structuré en cours. Si ce signal n'est pas encore centralisé, il doit être ajouté au même snapshot backend plutôt que déduit depuis l'existence de la session.

Donc, pour la première livraison, le critère technique le plus défendable est :

`has_work_in_progress = any(agent.busy.is_busy()) || any(background_task.state in {Queued, Running, Waiting})`

Option produit à arbitrer avec UX/PO : faut-il aussi prévenir lorsqu'il existe des agents/PTY simplement vivants mais idle, car ils seront arrêtés à la fermeture ? Techniquement possible, mais cela change le sens de la popup de « travail en cours » vers « sessions ouvertes qui vont être interrompues » et augmente les faux positifs.

## Frontend pur ou aller-retour backend ?

Ce n'est pas un changement purement frontend.

La popup et son wording relèvent bien de l'UX/frontend, mais la décision fiable doit faire un aller-retour backend, idéalement depuis le handler de fermeture Rust lui-même, pour trois raisons :

1. Le backend possède la vérité complète sur tous les projets ouverts via `AppState::open_project_ids()`. Le frontend ne voit pas forcément un état frais pour tous les onglets/projets, notamment si seul le projet actif rafraîchit son `useProjectWorkState`.
2. Les tâches de fond et l'état `busy` sont des états applicatifs/backend. Lire un cache frontend peut rater une tâche démarrée hors vue active ou compter un état obsolète.
3. Le handler Rust actuel exécute déjà le teardown au `CloseRequested`. Il doit donc être modifié pour empêcher la fermeture avant teardown lorsque la confirmation est requise.

Surface backend proposée :

- ajouter un use case/commande de lecture `get_app_exit_work_guard_state` ou équivalent, qui agrège tous les `ProjectWorkState` des `open_project_ids()` et retourne un résumé minimal pour la popup : `hasWorkInProgress`, nombre d'agents busy, nombre de tâches de fond actives, éventuellement noms/projets pour affichage si UX le souhaite ;
- utiliser cette même logique dans le handler `CloseRequested` de `main` avant d'appeler le teardown ;
- exposer une commande `confirm_app_exit` / `request_app_exit_after_confirmation` qui bypass le guard et exécute le shutdown existant.

Frontière de lot :

- Backend/Tauri nécessaire : guard `CloseRequested`, agrégation fiable du work in progress, bypass confirmé, factorisation du teardown existant.
- Frontend/UX : rendu de la popup, libellés, hiérarchie d'information, boutons, éventuel détail des agents/tâches concernés.
- Aucun impact backend métier profond attendu : on réutilise `GetProjectWorkState`, `AgentBusyState`, `BackgroundTaskStore`, `open_project_ids()` et le shutdown existant. Pas d'impact DB/schema prévu, sauf si l'on choisit de persister une préférence utilisateur du type « ne plus demander » (hors demande actuelle).

## Critères d'acceptation techniques proposés

- Fermer la fenêtre principale sans travail actif garde le comportement actuel : snapshot, kill PTY, arrêt serveurs, fermeture globale.
- Fermer la fenêtre principale avec au moins un agent `Busy` empêche la fermeture et déclenche la demande de confirmation avant tout kill PTY.
- Fermer la fenêtre principale avec au moins une tâche de fond `Queued`/`Running`/`Waiting` empêche la fermeture et déclenche la confirmation.
- Annuler la popup laisse l'application et les sessions intactes : aucun PTY tué, aucun snapshot de fermeture forcé, serveurs toujours actifs.
- Confirmer exécute le shutdown existant dans le même ordre qu'aujourd'hui.
- Les fenêtres détachées ne déclenchent pas la confirmation globale ; seule la fenêtre `main` porte ce guard.

## Conception UX — confirmation de fermeture avec travail en cours

### Décision produit

Afficher une popup modale uniquement lorsque la fermeture de la fenêtre principale d'IdeA interrompt un travail actif détecté par le guard technique : agents en tour `busy` et/ou tâches de fond non terminales (`Queued`, `Running`, `Waiting`). Ne pas déclencher la popup pour une session agent simplement vivante mais idle dans la première livraison : le libellé demandé parle de « travail en cours », et compter les sessions ouvertes créerait trop de confirmations inutiles.

Il ne doit pas y avoir d'option `Ne plus avertir`. Cette confirmation protège contre une perte ou interruption volontairement coûteuse ; la rendre désactivable localement affaiblit la sécurité produit et crée une préférence à maintenir. Le seul bypass est l'action explicite `Quitter quand même` pour cette tentative de fermeture.

### Rôle de la popup

La popup doit communiquer trois choses, dans cet ordre :

1. IdeA a détecté du travail actif.
2. Quitter maintenant interrompra ce travail.
3. L'utilisateur peut annuler pour revenir à l'application, ou confirmer une fermeture volontaire.

La popup ne doit pas essayer de prédire si le travail est récupérable. Elle ne promet pas de sauvegarde, de reprise ou de livraison du résultat après fermeture. Elle indique seulement l'effet immédiat : les agents et tâches en cours seront interrompus pendant la fermeture.

### Libellé exact

Titre : `Du travail est encore en cours`

Corps, version avec un seul élément actif :

`1 travail actif sera interrompu si vous quittez IdeA maintenant.`

Corps, version plurielle :

`{count} travaux actifs seront interrompus si vous quittez IdeA maintenant.`

Phrase secondaire :

`Annulez la fermeture pour laisser les agents et les tâches se terminer.`

Si l'agrégat distingue les types, préférer une phrase plus informative :

- `1 agent travaille encore.`
- `{agentCount} agents travaillent encore.`
- `1 tâche de fond est encore active.`
- `{taskCount} tâches de fond sont encore actives.`

Exemple combiné :

`2 agents travaillent encore et 1 tâche de fond est encore active. Ces travaux seront interrompus si vous quittez IdeA maintenant.`

Actions :

- Action principale, non destructive : `Annuler`
- Action destructive secondaire : `Quitter quand même`

Ordre visuel recommandé : `Annuler` à gauche ou en premier, `Quitter quand même` à droite ou en dernier avec style danger. Le focus initial va sur `Annuler`.

### Détail affiché

Afficher un résumé court visible immédiatement :

- `{agentCount} agent(s) en cours`
- `{backgroundTaskCount} tâche(s) de fond active(s)`

Afficher ensuite une liste compacte des éléments concernés, limitée à 5 lignes maximum pour garder la popup lisible. Si plus de 5 éléments sont actifs, afficher les 5 premiers puis `+ {remainingCount} autre(s)`.

Format des lignes :

- Agent busy : `Agent {agentName} — {projectName}` ; si un ticket est connu, ajouter ` — #{ticketNumber}`.
- Tâche de fond : `{taskLabel} — {projectName}` ; si aucun libellé humain n'est disponible, utiliser `Tâche de fond {shortTaskId}`.

Exemples :

```text
Du travail est encore en cours

2 agents travaillent encore et 1 tâche de fond est encore active. Ces travaux seront interrompus si vous quittez IdeA maintenant.

Agents
• DevFrontend — IdeA — #82
• QA — IdeA

Tâches de fond
• npm test — IdeA

[Annuler] [Quitter quand même]
```

Si les noms ne sont pas disponibles côté backend au moment du guard, afficher seulement les compteurs. Ne pas bloquer la feature UX sur l'affichage détaillé, mais le contrat backend recommandé est de fournir au moins `projectName`, `agentName` et un label de tâche quand disponibles.

### États et interactions

- Ouverture : la popup apparaît après tentative de fermeture, avant tout teardown.
- `Annuler` : ferme la popup, ne ferme pas l'application, ne tue aucune session, ne modifie aucun état métier.
- `Échap` : équivalent à `Annuler`.
- Clic hors popup : désactivé ou équivalent à `Annuler`, selon le composant modal existant ; préférence UX : ne pas fermer par clic extérieur pour éviter une décision ambiguë.
- `Quitter quand même` : lance le flux backend confirmé. Pendant l'appel, désactiver les deux boutons et afficher l'état `Fermeture…` sur le bouton danger.
- Échec de la fermeture confirmée : rester dans la popup et afficher un message d'erreur inline : `IdeA n'a pas pu quitter correctement. Réessayez ou consultez les logs.` Le bouton `Quitter quand même` redevient disponible.
- Si le travail se termine pendant que la popup est ouverte, ne pas fermer automatiquement la popup. Actualiser le résumé si l'événement arrive facilement ; sinon garder l'état initial. L'utilisateur peut annuler puis fermer à nouveau sans confirmation.

### Hiérarchie visuelle

La popup est une alerte de confirmation destructive, pas un panneau de diagnostic :

- largeur cible : 440 à 520 px ;
- titre en `text-content`, taille modale standard ;
- résumé en texte normal, pas en rouge ;
- action `Quitter quand même` en variante danger ;
- liste des éléments dans un bloc compact `bg-raised` ou équivalent, sans tableau ;
- éviter les grands paragraphes et les détails techniques (`busy.state`, `Queued`, ids longs).

### Accessibilité

- Utiliser un vrai dialogue modal avec `role="alertdialog"` ou le composant modal existant configuré comme confirmation destructive.
- `aria-labelledby` pointe sur le titre `Du travail est encore en cours`.
- `aria-describedby` pointe sur le résumé de l'impact.
- Focus initial sur `Annuler`.
- Tabulation piégée dans la popup tant qu'elle est ouverte.
- `Entrée` active le bouton focusé, pas automatiquement `Quitter quand même`.
- `Échap` annule.
- Les compteurs et détails ne doivent pas dépendre uniquement de la couleur.

### Critères d'acceptation UX/frontend

- La popup n'apparaît que pour la fermeture de la fenêtre principale avec travail actif détecté.
- Le titre exact est `Du travail est encore en cours`.
- La popup indique le nombre total de travaux actifs et, quand disponible, le détail agents/tâches limité à 5 lignes.
- Les actions exactes sont `Annuler` et `Quitter quand même`.
- `Annuler` est l'action par défaut/focus initial et laisse l'application intacte.
- `Quitter quand même` est visuellement destructive et déclenche le flux backend confirmé.
- Aucune option `Ne plus avertir` n'est proposée.
- Les libellés visibles sont en français conformément à la décision UX #78.