diff --git a/.gitignore b/.gitignore index 37ac48d..95ed442 100644 --- a/.gitignore +++ b/.gitignore @@ -48,7 +48,6 @@ frontend/coverage/ # Ticket store IdeA: l'utilisateur veut conserver l'état local des tickets en # dehors des branches Git. À ignorer, sinon les changements de branche peuvent # écraser/supprimer cet état local. -.ideai/tickets/ # Volatile agent live-state snapshot ("who is doing what right now", lot LS2): # rebuilt at runtime, keyed last-writer-wins — not versioned (unlike .ideai/memory/). .ideai/live-state.json @@ -73,7 +72,5 @@ Thumbs.db .ideai/conversations/ .ideai/agents.json .ideai/background-tasks/ - -# IdeA local runtime state kept outside Git .ideai/tickets/ .ideai/mcp-tool-permissions.json diff --git a/.ideai/tickets/1/carnet.md b/.ideai/tickets/1/carnet.md new file mode 100644 index 0000000..3bd5f30 --- /dev/null +++ b/.ideai/tickets/1/carnet.md @@ -0,0 +1,29 @@ +--- +issueRef: "#1" +version: 11 +updatedBy: {"kind":"user"} +updatedAt: 1783175265022 +--- +- Cadrage: background-tasks-first-class-design. Arbitrage 5 écarts: b8-arbitration-outcomes. +- FAIT B8 runner PTY + boucle sink fermée + refactor point-2. QA vert. Commit backend 8cac147. +- FAIT UI cancel/retry câblés (cancel_background_task/retry_background_task). Commit frontend c179f93. +- FAIT déclencheur in-app: surface MCP idea_run_in_background (BE-1+BE-2), commit f08dae6. Ferme l'écart de périmètre « aucun producteur ». +- Dette tracée: #2 (PtyPort::wait + tee live), #3 (persistance sûre invocation/retry-after-reboot + énumération store), #5 (contrat DTO work-state). + +## MAJ 2026-07-04 — part autonome de #1 clôturée (Main) +- AppImage rebuild frais confirmé (build 2026-07-03 23:56 > dernier source 23:53), inclut T1+T3, NO_STRIP=true. +- VALIDATION AUTOMATISÉE VERTE : cargo build --workspace OK ; cargo test -p application / -p app-tauri / -p infrastructure = 0 failed ; frontend npm run build OK + vitest 459/460. L'unique rouge = flake de timing src/features/permissions/permissions.test.tsx (passe en isolation 2/2, non touché par #1, pas une régression). +- COMMITS Git sur feature/background-tasks-first-class : eb9cc16 (code T1+T3, 21 fichiers) + ebd992e (19 notes mémoire + MEMORY.md). AUCUN merge vers develop. Runtime .ideai/{background-tasks,proposals,tickets}/ volontairement non commités. + +## ⚠️ MAJ 2026-07-04 — IMPACT TOPOLOGIE (constat Git, cadrage #4) +- `feature/background-tasks-first-class` (#1) est DÉRIVÉE du `develop` ANORMAL/rembobiné (merge-base #1↔main = 1fc7869, pré-canonique ; a9653bc tip develop est ancêtre de #1). Donc #1 = baseline PRÉ-CANONIQUE (spawn_turn=0, run_turn batch, pas d'AgentTurnEvent). +- La vraie ligne d'intégration est `main` (canonique, release ~01/07), PAS le develop rembobiné (18 commits en retard sur main). ⇒ la cible de merge « → develop » prévue au point 3 ci-dessous est INVALIDÉE. +- #1 modifie massivement les fichiers divergés côté canonique : orchestrator/service.rs +425, domain/events.rs +184, lifecycle.rs +83, session/codex.rs. ⇒ portage sur canonique = CONFLITS NON TRIVIAUX attendus, pas un simple changement de cible. +- PLAN Git (Phase C, à faire quand on reprendra #1) : branche fraîche `feature/background-tasks-first-class-canonical` depuis main + cherry-pick séquentiel des 17 commits (ancienne branche gardée comme filet), conflits sémantiques service/events escaladés à DevBackend/Architect, puis QA avant tout merge. +- Décision de reconstruction de `develop` sur `main` : DESTRUCTIVE (rewrite branche partagée) → en attente validation utilisateur. + +## RESTE — DÉPENDANT UTILISATEUR (Main ne peut pas seul) +1. Re-QA LIVE T3 : relancer l'AppImage fraîche puis vérifier onglet Work → Cancel/Retry (T3-a..f du cadrage workstate-background-tasks-projection-fix). QA non pilotable depuis sandbox → besoin app lancée. +2. T2 — reconcile après reboot (fin AVANT wake), protocole MANUEL, coordination reboot utilisateur. +3. [CIBLE À REVOIR] merge de #1 : ne plus viser le develop rembobiné. Porter #1 sur canonique (Phase C ci-dessus) PUIS merger vers develop-reconstruit/main. Dépend de la décision de reconstruction de develop. +Main travaille sur #4 (annonces inter-agent) en attendant la dispo utilisateur pour 1-2-3. \ No newline at end of file diff --git a/.ideai/tickets/1/issue.md b/.ideai/tickets/1/issue.md new file mode 100644 index 0000000..bba7c6e --- /dev/null +++ b/.ideai/tickets/1/issue.md @@ -0,0 +1,24 @@ +--- +id: "ec19c041-7a44-456d-994d-f0dcb7c52f39" +number: 1 +title: "Tâches de fond — finaliser B8 (runner PTY) et clôturer le chantier" +status: "closed" +priority: "medium" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1783026129089 +updatedAt: 1783175265022 +version: 11 +--- +Description : + +Le chantier « tâches de fond de 1re classe » est livré et QA-vert jusqu'à B7 inclus (mailbox bornée, rendez-vous inter-agent comme tâche durable, reconcile au boot, réveil du propriétaire, UI F1–F4). Commité sur feature/background-tasks-first-class (e05edc6 backend, 5d88c95 frontend), non mergé. Reste à fermer les points suivants avant de considérer le chantier terminé. + +Reste à faire : + +- [ ] B8 — runner de commandes couplé PTY : créer un BackgroundTask{kind: Command} au spawn d'une commande longue (côté pty.rs / LocalProcessSpawner) et pousser exit/stdout/stderr dans le completion sink. C'est ce qui active le flux run_in_background complet : une commande qui finit après la fin du tour de l'agent réveille automatiquement son propriétaire avec le résultat. Aujourd'hui le sink est prêt mais aucun runner concret ne s'y abonne. +- [ ] UI cancel / retry des tâches de fond : les boutons existent mais sont désactivés faute de commande Tauri. Exposer cancel/retry (dépend de B8) et brancher l'UI. +- [ ] Validation live end-to-end de la feature dans l'app réelle : lancer un build/commande longue en run_in_background, laisser le tour se terminer, vérifier que le propriétaire est bien re-réveillé avec le résultat (et idem après redémarrage d'IdeA entre la fin et le wake — le reconcile doit retrouver la complétion). +- [ ] Merge feature/background-tasks-first-class → develop (sur validation) : résorbe aussi les 4 tests de protocole MCP périmés qui sont encore rouges sur develop (le fix d'alignement vit sur cette branche). \ No newline at end of file diff --git a/.ideai/tickets/10/carnet.md b/.ideai/tickets/10/carnet.md new file mode 100644 index 0000000..d082d71 --- /dev/null +++ b/.ideai/tickets/10/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#10" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783241883970 +--- diff --git a/.ideai/tickets/10/issue.md b/.ideai/tickets/10/issue.md new file mode 100644 index 0000000..fdd6ebb --- /dev/null +++ b/.ideai/tickets/10/issue.md @@ -0,0 +1,15 @@ +--- +id: "37bfbb89-a8a5-475f-91bf-ecfd60ee4203" +number: 10 +title: "Ticket par sprint" +status: "closed" +priority: "medium" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783183841014 +updatedAt: 1783241883970 +version: 5 +--- +J'aimerais pouvoir regrouper mes tickets en sprints. C'est a dire en catégories qui auraient elles meme un ordre d'execution (sprint 1, sprint 2 etc) \ No newline at end of file diff --git a/.ideai/tickets/100/carnet.md b/.ideai/tickets/100/carnet.md new file mode 100644 index 0000000..067457f --- /dev/null +++ b/.ideai/tickets/100/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#100" +version: 3 +updatedBy: {"kind":"user"} +updatedAt: 1784993984569 +--- diff --git a/.ideai/tickets/100/issue.md b/.ideai/tickets/100/issue.md new file mode 100644 index 0000000..0da3257 --- /dev/null +++ b/.ideai/tickets/100/issue.md @@ -0,0 +1,16 @@ +--- +id: "4709958c-5082-44fd-a1fd-d6bad85f9361" +number: 100 +title: "[Bug] Problème sur le scroll des agents OpenCode" +status: "closed" +priority: "high" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784992615586 +updatedAt: 1785083912470 +version: 4 +--- +Quand un agent est un agent opencode, je ne peux aps scroll très haut dans sa cellule. diff --git a/.ideai/tickets/101/carnet.md b/.ideai/tickets/101/carnet.md new file mode 100644 index 0000000..d2c4536 --- /dev/null +++ b/.ideai/tickets/101/carnet.md @@ -0,0 +1,96 @@ +--- +issueRef: "#101" +version: 7 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1785011116365 +--- +# Carnet #101 — défaut d'isolation multi-projet (cause racine figée) + +> Diagnostic Architect (2026-07-25). Source de vérité du fix. Mémoire durable : +> `ticket101-cross-talk-multi-project-rootcause`. + +## Cause racine +Stores disque IdeA **correctement scoppés par projet** (mémoire, contexte, handoff, live-state lus +depuis `input.project.root` ; agents persistés dans `.ideai/agents.json` par root projet, **sans +re-mint des UUID à l'ouverture**). Mais plusieurs **registres mémoire runtime restent globaux, +indexés par `AgentId` seul** → deux projets ouverts (surtout si l'un est une copie) peuvent porter +les **mêmes `AgentId`** et le runtime les confond. + +### 2 manifestations, même défaut +- **A — contamination du contexte d'inférence** : conversation/session d'un projet réutilisée pour + l'autre → contexte injecté à l'agent contaminé (preuve : pseudo-branche `wear-os-watch-sync` + créée par l'agent Git avec un nom de l'autre projet). +- **B — notification de tâche backend perdue / agent s'arrête** (sujet original) : complétion + livrable/bloquée/réveillant la mauvaise session. + +## Points précis (registres par `AgentId` seul à requalifier) +- ConversationRegistry global + `ConversationId` dérivé des UUID agents seuls : + `crates/domain/src/conversation.rs:71`, `crates/infrastructure/src/conversation/mod.rs:41`, + orchestrateur `crates/application/src/orchestrator/service.rs:2192` (et `:2293`, `:2368`). +- Sessions PTY/structured par `agent_id` seul : + `crates/application/src/terminal/registry.rs:182,437`. +- Verrous ask / busy-state / liveness / délégations différées : + `crates/application/src/orchestrator/service.rs:418`, + `crates/infrastructure/src/input/mod.rs:47`. +- Inbox/mailbox par `AgentId` seul : `crates/domain/src/inbox.rs:68,156`, + `crates/infrastructure/src/input/mod.rs:1052`, `crates/infrastructure/src/mailbox/mod.rs:44`. +- Wake par `AgentId` seul : `crates/application/src/orchestrator/wake.rs:87`, + provider `crates/backend/src/lib.rs:687`. + +### Pour B — distinction runtime vs modèle +La chaîne sink→`project_id`→wake est **correcte** jusqu'au bridge +(`crates/infrastructure/src/background_task/sink.rs:23,142` ; `crates/backend/src/lib.rs:2228` +recharge le projet par `project_id` puis `wake_agent`). Le défaut réapparaît juste après +(inbox/mailbox/busy/session par AgentId seul). **Runtime IdeA suspecté en premier.** GLM 5.2 n'est +l'hypothèse principale que si les logs prouvent enqueue+wake+`BackgroundTaskCompletionDelivered` +sur le bon projet sans reprise. Traces : `lib.rs:2238`, `wake.rs:93`. + +## Décision utilisateur +**Lancer le fix structurel maintenant (lots 1-2).** La collision d'UUID et les logs B se +vérifieront en parallèle. + +## Périmètre DevBackend (lots 1-2 de ce fix) +- **Lot 1** : introduire une clé runtime scellée `RuntimeAgentKey { project_id, agent_id }` ; + interdire toute map app-wide indexée par `AgentId` seul. +- **Lot 2** : propager la clé dans `AgentInbox`, `InputMediator`, `AgentMailbox`, + busy/liveness/deferred, wake, session lookup, ask-locks. **Correctif structurel commun à A et B.** + +Lots suivants (3-6, hors premier passage) : qualifier ConversationId/registry par projet (lot 3) ; +requalifier `TerminalSessions`/`StructuredSessions` par `(project_id, agent_id)` + +`AppWakeSessionProvider` refuse l'autre projet (lot 4) ; audit `session_limit`/tables reprise (lot 5) ; +télémétrie `project_id` sur logs diag wake/routage (lot 6). + +## Invariants +- Sources de contexte sur disque restent root-scopées (ne pas régresser). +- Templates/profils globaux restent globaux produit (pas de vecteur de session/conversation partagée). +- Un projet non-actif doit continuer à recevoir ses wakes et complétions. +- Pas de régression OpenCode/Codex/Claude (correctif transverse runtime, pas moteur-spécifique). +- Aucun nouveau DTO frontend pour la correction minimale. + +## QA — critère de vérité +Test d'intégration **non-cross-talk** : 2 projets ouverts avec **volontairement les mêmes `AgentId`** : +- délégation projet A ne réutilise jamais session/conversation vivante de projet B ; +- complétion `(projet A, agent X)` n'entre ni dans l'inbox ni dans la session de `(projet B, agent X)` ; +- wake d'un projet non-actif fonctionne ; +- collision d'ids qui échouait avant → verte sur PTY, structured et background wake. + +## Clôture 2026-07-25 +- Correctif livré sur `feature/ticket101-multi-project-isolation` puis mergé localement dans `develop`. +- Commit feature : `6e98fd8 fix(runtime): isolate agent state by project (#101)`. +- Merge local : `merge: integrate ticket 101 multi-project isolation`. +- QA ciblée verte : + - `cargo test -p application --test agent_wake --test orchestrator_service --test structured_registry_d1 --test structured_launch_d3 --test session_limit_service --test session_limit_t4 --test workstate --test workstate_actions` + - `cargo test -p infrastructure --test agent_inbox --test mcp_server` + - `cargo test -p app-tauri --test session_limit_wiring` + - `cargo test -p application` + - `cargo test -p app-tauri --tests` +- Réserve connue : `cargo test -p infrastructure` complet reste rouge dans le sandbox QA sur tests `openai_compat` à cause du bind local interdit (`Operation not permitted`), sans signal de régression #101. + +## Hors périmètre +- #91 (popup de notification) : purement front, indépendant. +- Remint systématique des AgentId à l'ouverture : non retenu (la clé runtime rend la collision + inoffensive même sans remint). + +## Lié +- #91 (relatesTo). Mémoires : `background-tasks-first-class-design`, `b8-command-runner-pty-framing`, + `mcp-bridge-and-delegation-runtime-notes` (règle rebuild AppImage). \ No newline at end of file diff --git a/.ideai/tickets/101/issue.md b/.ideai/tickets/101/issue.md new file mode 100644 index 0000000..15e64e6 --- /dev/null +++ b/.ideai/tickets/101/issue.md @@ -0,0 +1,16 @@ +--- +id: "05f9f05b-97d4-4ae2-9fd9-220dd71f7231" +number: 101 +title: "[Bug] Soucis de retour de notification sur les taches backend" +status: "closed" +priority: "critical" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784993230013 +updatedAt: 1785011116365 +version: 7 +--- +Lorsque un agent Opencode lance une tache backend, il s'arrete de travailler et je ne suis pas sur q'uil y ai un jour un retour de notification. Je ne sais aps si le soucis provient de OpenCode ou s'il provient du model (GLM 5.2 ici) \ No newline at end of file diff --git a/.ideai/tickets/102/carnet.md b/.ideai/tickets/102/carnet.md new file mode 100644 index 0000000..45a35f8 --- /dev/null +++ b/.ideai/tickets/102/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#102" +version: 4 +updatedBy: {"kind":"user"} +updatedAt: 1784993980505 +--- diff --git a/.ideai/tickets/102/issue.md b/.ideai/tickets/102/issue.md new file mode 100644 index 0000000..a13e871 --- /dev/null +++ b/.ideai/tickets/102/issue.md @@ -0,0 +1,16 @@ +--- +id: "e91fd358-da94-4382-aa70-6a3fe5a63840" +number: 102 +title: "[Bug] Devoir resize les cellule pour afficher la TUI d'un agent" +status: "closed" +priority: "medium" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784993319700 +updatedAt: 1785083912470 +version: 5 +--- +J'ai toujours un soucis qui fait que quand je switch de projet IdeA ou de layout ou que j'ajoute des cellules ou autres, je suis obligé de resize un coup la cellule pour que son constenu s'affiche correctement diff --git a/.ideai/tickets/103/carnet.md b/.ideai/tickets/103/carnet.md new file mode 100644 index 0000000..c74429c --- /dev/null +++ b/.ideai/tickets/103/carnet.md @@ -0,0 +1,99 @@ +--- +issueRef: "#103" +version: 4 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1785013507979 +--- +# Carnet #103 — permission réseau exposée dans IdeA + +## Problème +Certaines commandes lancées par les agents s'exécutent dans un environnement où le réseau est restreint, sans visibilité ni action claire dans IdeA. Exemple live : rebuild AppImage OK jusqu'au bundling, puis `appimagetool` échoue à télécharger le runtime GitHub car la session agent est `network restricted` + `approval policy: never`. + +## Décision UX +Mémoire : `ticket103-network-permission-ux-surface`. + +- Surface principale : `Permissions > Système`. +- Miroirs de lecture : badge compact dans `Agents`, bannière/erreur contextualisée dans `Terminal`. +- États visibles : `Réseau autorisé`, `Réseau interdit`, `Demande d'autorisation`, `Verrouillé par le runtime`. +- Distinction obligatoire : politique voulue, état effectif, verrou runtime. +- Si le runtime externe ne permet pas l'élévation dans la session active : contrôle read-only + explication explicite. + +## Cadrage architecture +Ne pas étendre le modèle existant `ProjectPermissions` fichier/bash : le réseau n'est ni une capability filesystem ni une règle Landlock. Ajouter un modèle/read-model séparé de permissions système. + +### Domaine / DTO V1 +- `NetworkPolicy = "allow" | "deny" | "ask"`. +- `SystemPermissionSet { network?: NetworkPolicy }`. +- `ProjectSystemPermissions { version, projectDefault?: SystemPermissionSet, agents?: [{ agentId, permissions: SystemPermissionSet }] }`. +- `ResolvedAgentSystemPermissions` : + - `wanted: NetworkPolicy | null` + - `effective: NetworkPolicy` + - `runtimeLock: { state: "none" | "locked", source?: string, reason?: string }` + - `control: { mode: "editable" | "readOnly", reason?: string }` + +### Ports / use cases +- `SystemPermissionStore`. +- `GetProjectSystemPermissions`. +- `UpdateProjectSystemPermissions`. +- `UpdateAgentSystemPermissions`. +- `ResolveAgentSystemPermissions`. +- `RuntimePermissionProbe` : expose ce que le runtime hôte/fournisseur autorise réellement et s'il verrouille le réseau. + +### API/commands attendus +- `get_project_system_permissions(projectId) -> ProjectSystemPermissionsDto` +- `update_project_system_permissions({ projectId, permissions }) -> ProjectSystemPermissionsDto` +- `update_agent_system_permissions({ projectId, agentId, permissions }) -> ProjectSystemPermissionsDto` +- `resolve_agent_system_permissions({ projectId, agentId }) -> ResolvedAgentSystemPermissionsDto` + +### Limite produit V1 +Implémentable maintenant : persister la politique voulue, afficher `wanted/effective/runtimeLock`, rendre le contrôle read-only si runtime verrouillé/non inspectable, badges agents, bannière terminal. + +Non promis en V1 : changer effectivement la permission réseau d'une session fournisseur déjà lancée ou élever un runtime externe `network restricted` / `approval never`. IdeA doit l'expliquer plutôt que simuler une élévation. + +## Découpage +### DevBackend +1. Ajouter domaine `system permissions` séparé de LP1 permissions fichier/bash. +2. Ajouter store + use cases + DTO + commands Tauri/HTTP. +3. Ajouter `RuntimePermissionProbe` read-only au composition root. +4. Ajouter read-model `resolve_agent_system_permissions`. +5. Optionnel si simple : mapper des échecs réseau vers un code stable `NETWORK_LOCKED` ou `NETWORK_UNAVAILABLE`. + +### DevFrontend +1. Étendre types domaine, ports, adapters Tauri/HTTP/mock. +2. Ajouter la sous-section `Réseau` dans `Permissions > Système`. +3. Ajouter badge compact dans `Agents`. +4. Ajouter bannière/erreur terminal contextualisée pour état verrouillé/échec réseau. + +### QA +- Projet sans config : état cohérent, pas de faux `allow`. +- Save project default puis override agent : relecture identique. +- Resolve distingue `wanted`, `effective`, `runtimeLock`. +- Probe `locked` : contrôle read-only + message visible. +- Non-régression `Permissions > Système` existant fichier/bash. +- Aucun moteur ne reçoit de faux flag réseau au spawn. +- Badge agents et bannière terminal reflètent l'état effectif. + +## Validation 2026-07-25 +QA verte sur le périmètre #103. + +Commandes exécutées par QA : +- `cargo test -p domain system_permissions` +- `cargo test -p application --test system_permission_usecases` +- `cargo test -p infrastructure --test system_permission_store` +- `cargo test -p app-tauri --test dto_system_permissions` +- `cargo test -p web-server allowlisted` +- `npm run typecheck` +- `npx vitest run src/features/permissions/permissions.test.tsx src/features/agents/agents.test.tsx src/features/terminals/TerminalView.test.tsx` +- `npx vitest run src/features/permissions/permissions.test.tsx` + +Résultats : backend ciblé vert, frontend typecheck vert, tests frontend ciblés 49 passed. + +## État Git +Implémentation validée dans le working tree courant, mais commit/merge non réalisé par Main : +- l'agent Git headless renvoie un pseudo-appel outil au lieu d'agir ; +- Main ne peut pas écrire `.git/index.lock` depuis cette session (`Read-only file system`). + +Le commit local reste à faire manuellement ou via un agent Git fonctionnel. + +## Critère de clôture +Tests pertinents verts, commit/merge local par Git, puis rebuild AppImage. \ No newline at end of file diff --git a/.ideai/tickets/103/issue.md b/.ideai/tickets/103/issue.md new file mode 100644 index 0000000..8232181 --- /dev/null +++ b/.ideai/tickets/103/issue.md @@ -0,0 +1,39 @@ +--- +id: "3d9021da-26c3-439d-9463-8d206bd06f1b" +number: 103 +title: "Exposer et piloter la permission réseau des agents/commandes dans IdeA" +status: "qa" +priority: "high" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1785011668081 +updatedAt: 1785013507979 +version: 4 +--- +## Problème + +Aujourd'hui certaines commandes lancées par les agents s'exécutent dans un environnement où le réseau est restreint, sans que l'utilisateur puisse le voir ni l'autoriser depuis IdeA. Exemple réel : rebuild AppImage OK jusqu'au bundling, puis `appimagetool` échoue à télécharger le runtime AppImage depuis GitHub (`Failed to download runtime: server returned status code 0`) parce que la session agent a `network restricted` et `approval policy: never`. + +## Besoin utilisateur + +Depuis IdeA, l'utilisateur doit pouvoir comprendre et piloter cette permission réseau : +- voir qu'un agent/profil/session est en mode réseau interdit ou autorisé ; +- configurer la politique réseau attendue pour les agents/commandes ; +- éviter les échecs opaques de commandes qui ont légitimement besoin d'Internet (build, install, téléchargement runtime, docs, dépendances) ; +- conserver un comportement sûr par défaut et explicite. + +## Attendu produit + +Définir puis implémenter une surface IdeA pour exposer cette permission. La solution doit respecter le modèle de permissions/sandbox existant et clarifier la limite éventuelle : si le sandbox fournisseur impose `network restricted` sans possibilité d'élévation runtime, IdeA doit l'expliquer plutôt que faire croire que le réseau est activable. + +## Critères d'acceptation + +- Une surface UI ou configuration explicite permet de voir la politique réseau applicable aux agents/commandes. +- L'utilisateur dispose d'une action ou d'un réglage clair quand IdeA peut piloter cette permission. +- Si la permission est imposée par le runtime externe et non modifiable, l'UI l'indique clairement. +- Les agents/commandes ne gagnent pas l'accès réseau silencieusement. +- Tests pertinents verts. +- AppImage reconstruite après livraison. \ No newline at end of file diff --git a/.ideai/tickets/107/carnet.md b/.ideai/tickets/107/carnet.md new file mode 100644 index 0000000..47158a8 --- /dev/null +++ b/.ideai/tickets/107/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#107" +version: 3 +updatedBy: {"kind":"user"} +updatedAt: 1785136945441 +--- diff --git a/.ideai/tickets/107/issue.md b/.ideai/tickets/107/issue.md new file mode 100644 index 0000000..ab84b6e --- /dev/null +++ b/.ideai/tickets/107/issue.md @@ -0,0 +1,17 @@ +--- +id: "6a79006b-0201-4176-ae54-39a05cc3baa6" +number: 107 +title: "[Bug] croisement entre les projet des retours des agents" +status: "open" +priority: "critical" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1785136727824 +updatedAt: 1785136945441 +version: 3 +--- +Il y a un soucis très important que j'ai constatés. J'ai actuellement 2 projets ouverts: IdeA et GameTime. Les deux projets travaillaient en même temps et j'ai vu GameTime qui semblait récupérer une requete du projet IdeA. Pour plus de précision, sur mes deux projets, j'ai un agent Git qui utilise OpenCode et un modelle local llamacpp, et j'ia eu l'impression qu'ils ont tous les deux appelé leur agent Git mais GameTime à recus la réponse de IdeA car la réponse parlait d'une branche du projet IdeA. +Je ne suis pas totalement sur de ce que j'avance, la seule chose dont je suis sur, c'est que GameTime s'est vu adressé une réponse qui était déstinée au projet IdeA. \ No newline at end of file diff --git a/.ideai/tickets/11/carnet.md b/.ideai/tickets/11/carnet.md new file mode 100644 index 0000000..bf377a8 --- /dev/null +++ b/.ideai/tickets/11/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#11" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783278726067 +--- diff --git a/.ideai/tickets/11/issue.md b/.ideai/tickets/11/issue.md new file mode 100644 index 0000000..0763a0f --- /dev/null +++ b/.ideai/tickets/11/issue.md @@ -0,0 +1,15 @@ +--- +id: "dad53cb9-a818-4475-be12-fd3ba6c59638" +number: 11 +title: "Interface de création de sprint" +status: "closed" +priority: "medium" +links: [{"target":"#10","kind":"dependsOn"}] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783183911244 +updatedAt: 1783278726067 +version: 6 +--- +Pour créer des sprints, j'aimerais une inteface de création dans laquelle je pourrais facilement ajouter/enlever des tickets de mes différents sprint \ No newline at end of file diff --git a/.ideai/tickets/12/carnet.md b/.ideai/tickets/12/carnet.md new file mode 100644 index 0000000..8f9e2c1 --- /dev/null +++ b/.ideai/tickets/12/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#12" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783312900907 +--- diff --git a/.ideai/tickets/12/issue.md b/.ideai/tickets/12/issue.md new file mode 100644 index 0000000..f4c1bfb --- /dev/null +++ b/.ideai/tickets/12/issue.md @@ -0,0 +1,15 @@ +--- +id: "fb8a73f9-c871-4513-aa09-b35fbbef637a" +number: 12 +title: "Checkbox pour les filters des tickets" +status: "closed" +priority: "low" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783184054842 +updatedAt: 1783312900907 +version: 5 +--- +Pour les filtres des tickets, j'aimerais qu'on ai une checkbox plutot que la selection d'un seul filtre, de façon a pouvoir faire des combinaisons de plusieurs priorité et de plusieurs status par exemple \ No newline at end of file diff --git a/.ideai/tickets/13/carnet.md b/.ideai/tickets/13/carnet.md new file mode 100644 index 0000000..3044a7d --- /dev/null +++ b/.ideai/tickets/13/carnet.md @@ -0,0 +1,54 @@ +--- +issueRef: "#13" +version: 19 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784193634690 +--- + +# Ticket #13 — Server/client mode (carnet de chantier) + +Ticket gated : review agent-utilisateur faite 2026-07-15. Base develop. Aucune action sortante. + +## Arbitrages produit validés (utilisateur) +- Remote perso, 1 SEUL user. Pas de multi-tenant. CLI natif via WS PTY, xterm.js inchangé, PTY serveur. SSH écarté. +- Desktop Tauri ET client/serveur sur cœur backend commun. Exposition Internet ⇒ TLS/reverse proxy obligatoire. +- Packaging `idea --serve`, artefact unique. Auth pairing code + cookie session, jamais dans l'URL. +- Multi-fenêtres OS HORS V1 web. B6→B8 + servir dist, validation live groupée à la fin. + +## Contrat de transport figé (Architect) +- `POST /api/invoke {command,args}` (allowlist read-only). Cookie session `HttpOnly,Secure,SameSite=Strict` au pairing (`POST /api/pair`). `POST /api/logout` = révocation (B8). Cookie invalide→401 ; mauvais code→403 ; Origin refusée→403. +- Frames WS JSON base64, ack unifié `terminal.attached`, output APRÈS l'ack. structured→UNSUPPORTED. +- Flags : `--listen`, `--public-origin`, `--allow-remote`, `--trust-reverse-proxy`, `--app-data-dir`, `--web-root` (B8). + +## Plan de lots — TOUS LES LOTS DEV LIVRÉS ✅ +B0→B8 + F1→F6 livrés/verts/committés. + correctifs live écarts 1/2. Reste : finir la validation live groupée (utilisateur) puis merge develop + clôture. + +## Livrés VERT +- develop (e457152) : B0 5505acc · B1 955db79 · B2 c8fef2a · F1 e0cdb4a · B3 4ed0b16 · B4 fa353f6 · F2 e500e31. +- feature/ticket13-pty-websocket : B5 917be99 · F3 7487902 · B6 6391d1c · F4 5254f16 · B7 1bc5217 · F5 dd1d083 · B8 d538808 · F6 6117177 · **fix live écarts 1+2 c9ce3d7**. +- B8 : sert dist/ same-origin, serve_static durci, `/api/logout`, logs sécurité, doc remote. 57/57. +- F6 : logout, 401→pairing, bannière reconnexion, serveur indispo. build+vitest 758/758. + +## ⚙️ Validation live 1er passage (utilisateur) — 3 écarts remontés +1. **Erreur `__TAURI_INTERNALS__ undefined` + projets vides** = MA commande de test était incomplète (build sans `VITE_TRANSPORT=http` → build desktop dans le navigateur ; et `--app-data-dir` non pointé sur le dossier desktop). PAS un bug de code. +2. **Défaut app-data-dir désaligné** (RÉSOLU c9ce3d7) : `default_app_data_dir()` retournait `~/.local/share/IdeA` alors que le desktop = identifier tauri `app.idea.ide` (`~/.local/share/app.idea.ide`). Corrigé : précédence `--app-data-dir` > `IDEA_APP_DATA_DIR` > `$XDG_DATA_HOME/app.idea.ide` > `$HOME/.local/share/app.idea.ide` + log démarrage `idea --serve: app data dir = `. Doc corrigée (npm + VITE_TRANSPORT=http + avertissement double-writer). Garde-fou lock inter-process app-data-dir = ticket court à créer AVANT d'ouvrir des écritures web. +3. **Bouton Browse (créer/ajouter projet) inopérant en web** = `pickFolder()`→`UNSUPPORTED_ON_WEB` (dialogue natif OS absent du navigateur). PAS régression : report documenté F1. Sélection d'un projet EXISTANT couverte par list_projects/open_project (OK une fois écart 2 corrigé). Créer un NOUVEAU projet en web = nouveau lot (folder browser serveur sandboxé + create_project write) → **ticket #64 créé** (dependsOn #13), cadré par Architect, hors #13. + +## ⚠️ RESTE : re-passage validation live (utilisateur, HORS SANDBOX) +Commande corrigée : +1. `cd frontend && VITE_TRANSPORT=http npx vite build` (npm, jamais pnpm — voir mémoire `frontend-uses-npm-not-pnpm`). +2. Fermer l'app desktop, puis `cargo run --release -p app-tauri -- --serve --web-root frontend/dist --app-data-dir ~/.local/share/app.idea.ide` (pairing code + app data dir sur stderr). +3. Navigateur `http://127.0.0.1:17373` → écran pairing → code. +4. Vérifier : liste projets (existants visibles), ouvrir projet, terminal CLI (frappe→serveur, reload→réattache/scrollback), cellule agent, surfaces live (workstate/background Cancel-Retry/inbox), reconnexion, logout→pairing. + +## Réserves / écarts V1 (acceptés) +- Replay V1 = repaint scrollback complet. Multi-onglets « dernier gagne » silencieux. F4 write-portal NON câblé web. Browse créer-projet web → #64. Lock double-writer app-data-dir → ticket court à créer. + +## Branches +`feature/ticket13-pty-websocket` (courante). À merger dans develop après validation live OK. + +## Prochaine étape +Re-passage validation live (commande corrigée ci-dessus). Si OK → Git merge dans develop → clôture #13. #64 (Browse web) à planifier après. Aucune action sortante sans validation utilisateur. + +## Dette hors chantier +Warnings clippy pré-existants crates/domain (fileguard/profile/sprint). diff --git a/.ideai/tickets/13/issue.md b/.ideai/tickets/13/issue.md new file mode 100644 index 0000000..9ad1d00 --- /dev/null +++ b/.ideai/tickets/13/issue.md @@ -0,0 +1,16 @@ +--- +id: "036783fa-9856-4643-8c28-653b93ac36e1" +number: 13 +title: "Server/client mode" +status: "closed" +priority: "low" +sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783184188272 +updatedAt: 1784193634690 +version: 19 +--- +J'aimerais pouvoir utiliser IdeA sous forme de client/server. C'est a dire que IdeA aurait son backend sur une machine et son interface qui serait un frontend web. Les agents etc seraient tous côté server. C'est a dire que la cli de Claude serait celle côté serveur, l'interface client ne serait que l'affichage frontend, une UI. Je pense que tout est faisable, il faudrait cependant parler du côté CLI. Est ce qu'il serait possible de garde rle CLI natif comme il est actuellement, de l'afficher côté client tout en gardant l'installation claude code, codex etc côté serveur ? Peut etre qu'en ssh ça passerait ? Ce ticket ne pourra pas être fait en autonomie par un agent sans une review agent-utilisateur avant. \ No newline at end of file diff --git a/.ideai/tickets/14/carnet.md b/.ideai/tickets/14/carnet.md new file mode 100644 index 0000000..9b6900c --- /dev/null +++ b/.ideai/tickets/14/carnet.md @@ -0,0 +1,20 @@ +--- +issueRef: "#14" +version: 8 +updatedBy: {"kind":"user"} +updatedAt: 1784449099987 +--- +## Production #14 — livrée dans develop (validation e2e live restante) + +Périmètre figé avec l'utilisateur : adapter HTTP OpenAI-compatible **purement additif** (zéro régression Claude/Codex), parité **tool-calling + MCP** complète, canal HTTP natif robuste. + +### Cycle +- Cadrage Architect : slug mémoire `ticket14-local-lan-openai-adapter-scoping` (asymétrie CLI-vs-serveur HTTP, port ToolInvoker in-process, reprise sans id provider). +- Backend (DevBackend) : variante `StructuredAdapter::OpenAiCompatible`, VO `HttpChatConfig`, `openai_compat.rs`, port `ToolInvoker` déléguant à la même OrchestratorService que le MCP. GO QA (domain 467 / application 530 / infrastructure 523 / app-tauri 247, 0 échec). Défaut réel corrigé : timeout post-contact mappé Io. +- Frontend (DevFrontend) : types DTO miroir, wizard/settings (endpoint/model/apiKeyEnv=nom de var/timeouts, validation miroir backend), profil de référence Ollama éditable, erreur endpoint rendue via bannière role="alert". GO QA (tsc 0 + vitest 59 fichiers / 566 tests, 0 échec). Défaut réel corrigé : erreur endpoint invisible dans le DOM. + +### Git +- Backend `aab4bca`, frontend `d89380c`, merge `--no-ff` `3c6cd04` → develop. Branche feature supprimée. **Local uniquement, rien poussé.** + +### Reste à faire (hors code) +- Validation e2e **live contre un vrai endpoint Ollama/LAN** : lancement cellule, conversation headless, reprise, délégation inter-agents, vivacité/timeout, endpoint indisponible/modèle absent (critère d'acceptation n°7). Non exercée en tests automatisés — c'est la raison du statut QA plutôt que closed. diff --git a/.ideai/tickets/14/issue.md b/.ideai/tickets/14/issue.md new file mode 100644 index 0000000..9575abd --- /dev/null +++ b/.ideai/tickets/14/issue.md @@ -0,0 +1,75 @@ +--- +id: "0c1f1991-b6db-47b7-bbef-2478bc7874ab" +number: 14 +title: "Integration de profils IA locaux/LAN comme profils IdeA canoniques" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1783184495846 +updatedAt: 1784449099987 +version: 8 +--- +Objectif : permettre à IdeA d'utiliser des modèles hébergés localement ou sur le réseau local, par exemple Qwen via Ollama ou un runtime Docker exposant une API, avec le même niveau d'intégration produit que les profils Claude Code et OpenAI Codex CLI. + +La finalité n'est pas un simple profil custom lancé dans un terminal brut. Ces profils doivent devenir de vrais profils IA IdeA : sélectionnables, lançables depuis les cellules, pilotés par la conversation headless canonique, compatibles avec les mécanismes de conversation/reprise, et utilisables par l'orchestration de la même manière que Claude/Codex. + +## Cible produit + +Ajouter une famille de profil structuré pour serveurs de modèles locaux ou LAN, idéalement basée sur une API HTTP compatible OpenAI/Ollama : + +- endpoint configurable (`localhost`, machine LAN, container Docker, etc.) ; +- modèle configurable (`qwen...`, autre modèle local) ; +- authentification optionnelle via nom de variable d'environnement, jamais clé en clair ; +- profil visible et sélectionnable dans IdeA comme Claude/Codex ; +- cellule IdeA utilisable de façon canonique : conversation headless, historique, reprise, délégation, affichage des réponses et état de vivacité. + +## Contraintes importantes + +- Ne pas considérer le mode PTY/TUI comme suffisant pour ce ticket. +- Éviter une dépendance obligatoire à `ollama`, `docker` ou une commande shell quand une API HTTP suffit. +- Étudier explicitement les impacts commandes/headless : + - adapter HTTP natif ; + - wrapper CLI éventuel ; + - Docker/LAN ; + - sandbox et permissions ; + - absence ou présence de tool-calling/MCP côté modèle local. +- Le résultat doit s'intégrer dans le modèle existant `StructuredAdapter` / `AgentSessionFactory`, pas contourner le runtime IA d'IdeA. + +## Travail attendu + +1. Ajouter un adapter structuré pour modèle local/LAN, par exemple `StructuredAdapter::OpenAiCompatible` ou `LocalChat`. +2. Étendre le modèle de profil pour porter la configuration nécessaire : endpoint, model, apiKeyEnv optionnel, timeouts/liveness si nécessaire. +3. Implémenter une session headless `AgentSession` : + - `send(prompt)` ; + - émission de `ReplyEvent` ; + - gestion propre des erreurs réseau/modèle ; + - conversation id ou stratégie de reprise compatible avec le modèle IdeA. +4. Brancher l'adapter dans `StructuredSessionFactory`. +5. Rendre ces profils sélectionnables dans le wizard/settings au même titre que Claude/Codex. +6. Ajouter un profil de référence local, par exemple “Ollama / OpenAI-compatible local model”, éditable. +7. Vérifier l'intégration avec : + - lancement depuis une cellule ; + - conversation headless ; + - reprise ; + - changement de profil ; + - délégation inter-agents quand applicable ; + - état de vivacité/timeout ; + - erreurs endpoint indisponible/modèle absent. + +## Critères d'acceptation + +- Je peux configurer un profil Qwen hébergé via Ollama ou endpoint LAN. +- Je peux créer/lancer un agent IdeA avec ce profil comme avec Claude/Codex. +- La cellule utilise le chemin headless structuré, pas un terminal brut PTY. +- Les conversations apparaissent et se comportent comme les conversations Claude/Codex. +- La reprise ne mélange pas l'id logique IdeA avec un éventuel id provider. +- Un endpoint indisponible produit une erreur propre dans l'UI, sans bloquer IdeA. +- Les dépendances à `ollama`, Docker ou une CLI externe sont documentées et non imposées si le mode HTTP suffit. +- Tests backend couvrant factory, session adapter, erreurs HTTP et sérialisation du profil. +- Tests frontend couvrant configuration/sélection du nouveau profil. + +Review agent-user requise avant démarrage d'implémentation pour figer le périmètre exact : HTTP natif uniquement, support d'un wrapper CLI, et niveau attendu de tool-calling/MCP pour les modèles locaux. diff --git a/.ideai/tickets/15/carnet.md b/.ideai/tickets/15/carnet.md new file mode 100644 index 0000000..58b55bd --- /dev/null +++ b/.ideai/tickets/15/carnet.md @@ -0,0 +1,15 @@ +--- +issueRef: "#15" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784093861343 +--- +## Cadrage Architect — DIFFÉRÉ (2026-07-13) + +#15 (cas DÉLÉGUÉ : A→B via idea_ask_agent, B limité → parquer le waiter de A et re-livrer au reset) est **distinct de #30** (cas direct) et **différé hors sprint**. + +**Motif (Architect)** : le faire maintenant en in-memory serait une rustine fragile — réintroduit un blocage long côté A et ne tient pas les scénarios reboot/crash/redémarrage IdeA déjà identifiés. + +**Prérequis bloquant** : un modèle fiable de *délégation durable runtime-agent identity* (stockage waiter, corrélation requester/target/request, re-livraison idempotente, reprise après reset, comportement si A meurt / B redémarre / IdeA redémarre, expiration/annulation). → design `durable-delegation-runtime-agent-identity-design`. + +**Statut** : reste ouvert, priorité low/stretch conservée. Dépendance baseline #7 maintenue + à rattacher au redesign délégation durable. Ne pas mélanger avec #30. \ No newline at end of file diff --git a/.ideai/tickets/15/issue.md b/.ideai/tickets/15/issue.md new file mode 100644 index 0000000..5178c2c --- /dev/null +++ b/.ideai/tickets/15/issue.md @@ -0,0 +1,28 @@ +--- +id: "5de121f3-1cd1-4a73-bcab-b0a43a7c257f" +number: 15 +title: "[Bloqué par #7 — design durable] Limites de session — re-livraison auto parquée au reset (stretch B5)" +status: "open" +priority: "low" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [{"target":"#7","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783208599385 +updatedAt: 1784093861343 +version: 5 +--- +Extrait du cadrage Architect du ticket #7 (baseline livrée : propagation inter-agent B1→B3 + F1/F2). Stretch non retenu dans #7. + +⛔ STATUT (2026-07-15) : BLOQUÉ / PARQUÉ. Dépend de #7 qui est encore en QA (non stabilisé). Architect (triage sprint « Gestion des bugs ») : « le waiter A→B n'est pas parqué/re-livré, l'ask retourne encore une erreur Process ; à traiter APRÈS stabilisation QA de #7/LS7 ». De plus c'est un item de « délégation durable » (in-memory fragile aux redémarrages) qui relève d'un design dédié, pas d'une rustine. Non lançable tant que #7 n'est pas vert et que le design durable-delegation n'est pas cadré. Sorti de la production active du sprint. + +--- Cadrage conservé --- + +Objectif : quand A délègue à B via idea_ask_agent et que B atteint sa limite, au lieu de demander à A de ré-interroger après le reset, PARQUER le waiter de A par (requester,target) ; au execute_resume de B (reprise auto au reset), RE-LIVRER la tâche d'origine et router la complétion vers le waiter de A s'il est encore vivant (sinon drop, contrainte in-memory). + +État actuel (triage) : le rendez-vous délégué détecte RateLimited et arme une reprise de la cible (crates/application/src/orchestrator/service.rs:1854), execute_resume relance l'agent (crates/application/src/agent/session_limit.rs:226) ; mais le waiter A→B n'est pas parqué/re-livré. + +Coût/risque (Architect) : rapproche du modèle « délégation durable » tout en restant in-memory → fragile aux redémarrages/interruptions de A, ré-introduit un blocage long côté A. À rattacher au design durable-delegation-runtime-agent-identity-design plutôt que traité en rustine in-memory. + +Dépend de : #7 (baseline inter-agent B1→B3). Voir mémoire ticket7-session-limit-interagent-cadrage. \ No newline at end of file diff --git a/.ideai/tickets/16/carnet.md b/.ideai/tickets/16/carnet.md new file mode 100644 index 0000000..5b83c91 --- /dev/null +++ b/.ideai/tickets/16/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#16" +version: 7 +updatedBy: {"kind":"user"} +updatedAt: 1783405334263 +--- diff --git a/.ideai/tickets/16/issue.md b/.ideai/tickets/16/issue.md new file mode 100644 index 0000000..aec8128 --- /dev/null +++ b/.ideai/tickets/16/issue.md @@ -0,0 +1,17 @@ +--- +id: "0a3585f0-9774-4109-a4e3-746dc5b2a4b2" +number: 16 +title: "[UI] réorganisation des menus" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1783329576091 +updatedAt: 1783405334263 +version: 7 +--- +J'aimerais réorganiser les mesnus de façon à être un peu plus proche de l'interface d'un IDE plus classique. J'aiemrais plutot avoir des menu déroulant en haut de la fenetre que d'avoir cette barre latérale +Les menus déroulant pourraient afficher des fenetres flottantes, ce qui laisserait plus de place pour l'affichage. \ No newline at end of file diff --git a/.ideai/tickets/17/carnet.md b/.ideai/tickets/17/carnet.md new file mode 100644 index 0000000..f29b6b3 --- /dev/null +++ b/.ideai/tickets/17/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#17" +version: 10 +updatedBy: {"kind":"user"} +updatedAt: 1783405334977 +--- diff --git a/.ideai/tickets/17/issue.md b/.ideai/tickets/17/issue.md new file mode 100644 index 0000000..c73dd75 --- /dev/null +++ b/.ideai/tickets/17/issue.md @@ -0,0 +1,16 @@ +--- +id: "66aaef66-c70a-4153-81e9-a8f323686092" +number: 17 +title: "[UI] fenetre de gestion de tickets" +status: "closed" +priority: "medium" +sprint: null +links: [{"target":"#16","kind":"dependsOn"},{"target":"#18","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1783329807682 +updatedAt: 1783405334977 +version: 10 +--- +J'aimerais une fenetre dédiée à la gestion des tickets. Autant pour le listing que pour la création/edition des tickets. Dans un même temps, j'aimerais qu'on améliore la gestion des tickets lié lorsque l'on édite les tickets en utilisant la popup du ticket #18. J'aiemrais aussi que l'agent permettant l'écriture des tickets soit proposé de façon plus évidante. Je pense qu'un bouton en bas à droite de la fenetre d'édition ouverte serait bien. \ No newline at end of file diff --git a/.ideai/tickets/18/carnet.md b/.ideai/tickets/18/carnet.md new file mode 100644 index 0000000..5566e9f --- /dev/null +++ b/.ideai/tickets/18/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#18" +version: 9 +updatedBy: {"kind":"user"} +updatedAt: 1783405335941 +--- diff --git a/.ideai/tickets/18/issue.md b/.ideai/tickets/18/issue.md new file mode 100644 index 0000000..bd9215c --- /dev/null +++ b/.ideai/tickets/18/issue.md @@ -0,0 +1,16 @@ +--- +id: "5c17d6d3-c537-45b1-a17e-9c4edee963d1" +number: 18 +title: "[UI] Créer une popup de selection de ticket" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1783330038271 +updatedAt: 1783405335941 +version: 9 +--- +Pour différentes feature comme la création de sprint ou même le link d'un ticket à d'autres, il est necessaire de selectionner un ticket. Pour ça j'aimerais un popup qui puisse être utilisée dans ces différentes features pour la selection d'un ticket. Il faudrait une barre de recherche ainsi que la possibilité d'appliquer des filtres sur l'avancée ainsi que la priorité des tickets, comme pour le listing principal des tickets. \ No newline at end of file diff --git a/.ideai/tickets/19/carnet.md b/.ideai/tickets/19/carnet.md new file mode 100644 index 0000000..2a860ea --- /dev/null +++ b/.ideai/tickets/19/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#19" +version: 8 +updatedBy: {"kind":"user"} +updatedAt: 1783405337651 +--- diff --git a/.ideai/tickets/19/issue.md b/.ideai/tickets/19/issue.md new file mode 100644 index 0000000..182dbcb --- /dev/null +++ b/.ideai/tickets/19/issue.md @@ -0,0 +1,16 @@ +--- +id: "b1c84dd2-cef7-4e48-a501-1a30eedb02f2" +number: 19 +title: "[UI] implémentation de la popup de selection de ticket dans la création d'un sprint" +status: "closed" +priority: "low" +sprint: null +links: [{"target":"#18","kind":"dependsOn"}] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1783330352910 +updatedAt: 1783405337651 +version: 8 +--- +Lors de l'edition d'un sprint, j'aimerais que pour selectionner un ticket, ça soit la popup de selection de ticket qui soit utilisée \ No newline at end of file diff --git a/.ideai/tickets/2/carnet.md b/.ideai/tickets/2/carnet.md new file mode 100644 index 0000000..d9dc2e5 --- /dev/null +++ b/.ideai/tickets/2/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#2" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784065385002 +--- diff --git a/.ideai/tickets/2/issue.md b/.ideai/tickets/2/issue.md new file mode 100644 index 0000000..3030546 --- /dev/null +++ b/.ideai/tickets/2/issue.md @@ -0,0 +1,39 @@ +--- +id: "0a492d45-e195-4df7-a2ad-65649372abf0" +number: 2 +title: "Tâches de fond — dette A : découpler fin de process et flux output PTY (PtyPort::wait/try_wait)" +status: "closed" +priority: "low" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783082860742 +updatedAt: 1784065385002 +version: 6 +--- +Ticket #2 — Tâches de fond : découpler fin de process et flux output PTY. + +REQUALIFIÉ (2026-07-14) : le hub broadcast PTY existe désormais (crates/infrastructure/src/pty/mod.rs), donc l'ancienne contrainte « pas de broadcast multi-consommateur » est OBSOLÈTE. Le volet « tee live UI » est sorti en sous-ticket B/F séparé (voir lien blocks). La dette restante ici est purement backend. + +Constat : `CommandBackgroundRunner` détecte encore la fin d'une tâche en drainant `PtyPort::subscribe_output` jusqu'à EOF (crates/infrastructure/src/background_task/runner.rs:187), ce qui couple la lifecycle du process à la consommation de sortie et empêche un tee sans casser la détection. + +Attendu : +- Ajouter au port figé `PtyPort` (crates/domain/src/ports.rs:952) deux opérations explicites : + - `async fn wait(&self, handle: &PtyHandle) -> Result` (attend la fin naturelle + status ; documenter l'idempotence : premier wait consomme, suivants retournent le status mémorisé). + - `fn try_wait(&self, handle: &PtyHandle) -> Result, PtyError>` (non bloquant : Ok(None) si vivant, Ok(Some(status)) si terminé). +- `kill` reste « forcer l'arrêt puis retourner status » ; si déjà terminé, retourne le status mémorisé. NotFound si handle inconnu/purgé. L'exit status est mémorisé dans le registre live tant que la session existe (cohérence wait/try_wait/kill). +- Implémenter dans `PortablePtyAdapter` (état d'exit partagé, attendre le child sans dépendre du reader EOF, éviter double wait/kill) et dans tous les fakes de test. +- Migrer le runner : remplacer l'attente EOF (runner.rs:187) par une attente sur `pty.wait(&handle)`, concurrencée avec cancel/deadline. La sortie de completion reste prise depuis le scrollback borné ; le runner n'a plus besoin de subscribe_output pour savoir si le process est fini. + +Impact contractuel : port figé modifié → changement source-breaking intra-workspace (tous les adapters/fakes ajoutent wait/try_wait), mais PAS de breaking IPC/front, pas de migration de données. + +Lots : B1 wait/try_wait au port + fakes ; B2 impl PortablePtyAdapter ; B3 migrer CommandBackgroundRunner vers wait ; B4 tests. Taille M. + +QA (point de vérité) : +- Fake PtyPort dont l'output n'est jamais drainé (ou consommé par 2 subscribers) mais `wait` résout → le runner complète quand même. +- Fake avec subscriber UI actif + runner → completion via `wait`, pas via EOF. +- Deadline : si `wait` ne résout pas avant deadline → runner retourne Expired et tue/cleanup correctement. +- Cancel : cancel gagne même si l'output continue. +- Portable PTY réel : commande courte (`echo hi`) → completion exit 0 sans dépendre d'un drain output. \ No newline at end of file diff --git a/.ideai/tickets/20/carnet.md b/.ideai/tickets/20/carnet.md new file mode 100644 index 0000000..9fd7e4c --- /dev/null +++ b/.ideai/tickets/20/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#20" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784049665491 +--- diff --git a/.ideai/tickets/20/issue.md b/.ideai/tickets/20/issue.md new file mode 100644 index 0000000..d272556 --- /dev/null +++ b/.ideai/tickets/20/issue.md @@ -0,0 +1,26 @@ +--- +id: "1be19ee5-fd03-42ea-8df6-da18704807a9" +number: 20 +title: "[Backend] Dette ticket_list — matching #ref dans text + curseur opaque/stable" +status: "closed" +priority: "low" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [{"target":"#18","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783331172226 +updatedAt: 1784049665491 +version: 5 +--- +Dette backend identifiée pendant le cadrage du sprint UI rework (gate G4), non bloquante pour la popup #18 qui démarre sur le contrat actuel. + +Deux écarts sur la commande `ticket_list` (handler `crates/app-tauri/src/tickets.rs:681`, filtre store `crates/infrastructure/src/issues.rs`) : + +1. Recherche `text` : appliquée serveur sur titre (issues.rs:322) + description/carnet (issues.rs:326/465) mais PAS sur le `#ref`/numéro de ticket. Étendre le matching `text` à `issue_ref` pour permettre la recherche par numéro dans la popup de sélection. + +2. `cursor` : actuellement un offset numérique parsé en usize avec fallback silencieux à 0 si invalide (tickets.rs:1231), non opaque et non stable face à des mutations entre pages. Contractualiser un curseur opaque + stable (ordre déterministe préservé). + +Garde G3-bis déjà en place côté frontend : `TicketPicker`/`useTicketSearch` traitent `cursor` comme un token opaque (jamais construit/incrémenté côté client), donc la migration vers un curseur opaque backend n'exigera AUCUN changement frontend — cette dette est non-breaking. + +statuses[]/priorities[] sont conformes (OR intra-facette, AND inter-facette, testés) — rien à faire de ce côté. \ No newline at end of file diff --git a/.ideai/tickets/21/carnet.md b/.ideai/tickets/21/carnet.md new file mode 100644 index 0000000..1b1d285 --- /dev/null +++ b/.ideai/tickets/21/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#21" +version: 7 +updatedBy: {"kind":"user"} +updatedAt: 1783437364397 +--- diff --git a/.ideai/tickets/21/issue.md b/.ideai/tickets/21/issue.md new file mode 100644 index 0000000..682030c --- /dev/null +++ b/.ideai/tickets/21/issue.md @@ -0,0 +1,16 @@ +--- +id: "4ffe9f90-2264-4861-b7da-f5535b0a622f" +number: 21 +title: "[UI] pouvoir trier les ticket par..." +status: "closed" +priority: "low" +sprint: null +links: [{"target":"#16","kind":"dependsOn"},{"target":"#18","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1783331825658 +updatedAt: 1783437364397 +version: 7 +--- +J'aiemrais qu'on ajoute une option "trier par..." dans la popup de selection des tickets et dans la liste principale des tickets. On pourrait trier par numéro de ticket, priorité, status, ou titre du ticket. Avec chaque fois croissant/decroissant. \ No newline at end of file diff --git a/.ideai/tickets/22/carnet.md b/.ideai/tickets/22/carnet.md new file mode 100644 index 0000000..366a1b8 --- /dev/null +++ b/.ideai/tickets/22/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#22" +version: 5 +updatedBy: {"kind":"user"} +updatedAt: 1783437364871 +--- diff --git a/.ideai/tickets/22/issue.md b/.ideai/tickets/22/issue.md new file mode 100644 index 0000000..c553ca9 --- /dev/null +++ b/.ideai/tickets/22/issue.md @@ -0,0 +1,16 @@ +--- +id: "2b90fd78-8fce-449d-ab61-e5aee5ede08c" +number: 22 +title: "[UI] Anchor de views" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1783405028818 +updatedAt: 1783437364871 +version: 5 +--- +J'aimerais qu'à la manière d'un IDE classique, il soit possible d'ancrer des views sur le côté droite ou gauche et de proposer un format vertical. 9a peut être utilise par exemple pour git, les work, les agents, les skills etc. J'ai en tête les IDE comme Visual COde ou la suite Jetbrains par exemple. \ No newline at end of file diff --git a/.ideai/tickets/23/carnet.md b/.ideai/tickets/23/carnet.md new file mode 100644 index 0000000..60aef9c --- /dev/null +++ b/.ideai/tickets/23/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#23" +version: 6 +updatedBy: {"kind":"user"} +updatedAt: 1783437365276 +--- diff --git a/.ideai/tickets/23/issue.md b/.ideai/tickets/23/issue.md new file mode 100644 index 0000000..efda7cc --- /dev/null +++ b/.ideai/tickets/23/issue.md @@ -0,0 +1,16 @@ +--- +id: "ed815933-ed48-466d-b265-3724f3ea943b" +number: 23 +title: "[UI] rendre les fenêtres View séparée de IdeA" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1783405188644 +updatedAt: 1783437365276 +version: 6 +--- +J'aimerais que les fenêtre qu'on affiche via le menu View soient des fenêtre à part entières, qu'on puisse les déplacer sur les différents écrans, les mettre fullscreen etc, que ça soit des enetres "systeme" normales \ No newline at end of file diff --git a/.ideai/tickets/25/carnet.md b/.ideai/tickets/25/carnet.md new file mode 100644 index 0000000..949b0c9 --- /dev/null +++ b/.ideai/tickets/25/carnet.md @@ -0,0 +1,25 @@ +--- +issueRef: "#25" +version: 11 +updatedBy: {"kind":"user"} +updatedAt: 1783491435654 +--- +## Root cause +`OpenTicketAssistant::execute` fabriquait un plan OS-sandbox bespoke via `SandboxPlan::project_read_only` (grant RO sur project_root, posture Deny). Sous Landlock, un grant RO fait *handle* la classe read/exec (`AccessFs::from_read(V1)` inclut Execute). Le binaire CLI (codex/claude) vit hors du project root ⇒ `execve` refusé ⇒ EACCES (os error 13) au spawn sandboxé. Indépendant de la CLI. Les agents workspace n'ont pas le bug (plan no-op sous posture Allow). + +## Correctif (backend-pur, zéro port/DTO) +Décision Main : option fallback `None`. La surface assistant de ticket passe désormais `None` en 5e arg de `factory.start` (aligné sur les agents workspace). Garde-fous restants : policy MCP `idea_ticket_read/update*` + advisory LP3. Helper `ticket_assistant_sandbox` et preset `SandboxPlan::project_read_only` supprimés (plus aucun appelant). + +Fichiers : crates/application/src/ticket_assistant.rs (+tests), crates/domain/src/sandbox.rs. + +## Validation +- `cargo test --workspace` VERT (app-tauri 63, application 80, infrastructure 254, domain OK). +- Test ciblé : `factory.start` reçoit `SessionPlan::None` + `sandbox.is_none()`. +- Tests Landlock infra verts sur la machine. +- RÉSERVE : repro live Codex+Claude non exécutée — nécessite rebuild + swap de l'AppImage active + relance IdeA (interrompt la session). + +## Git +Commit `961a2ca` → merge `--no-ff` `cb20fab` dans develop. Local only, pas de push. + +## Reste à faire +Repro live après rebuild AppImage pour clôture définitive. Dette différée : write-fence OS uniforme sur toutes les surfaces (cf. cadrage Architect) si l'on veut une clôture write-OS. \ No newline at end of file diff --git a/.ideai/tickets/25/issue.md b/.ideai/tickets/25/issue.md new file mode 100644 index 0000000..740e65a --- /dev/null +++ b/.ideai/tickets/25/issue.md @@ -0,0 +1,16 @@ +--- +id: "6123b180-b36b-4807-ba31-46d60d3ef022" +number: 25 +title: "[Bug] Codex/Claude erreur lors de l'utilisation dans l'edition d'un ticket" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1783406391625 +updatedAt: 1783491435654 +version: 11 +--- +J'ai voulu créer un ticket via Codex dasn l'interface d'edition d'un ticket, mais j'ai eu cette erreur: process error: agent session start failed: codex: Permission non accordée (os error 13). J'aéi séléctionné que je voulais utiliser codex et l'erreur est apparue quand j'ai essayé de lui donné mon premier message. Je me rend compte que j'ai la même erreur avec Claude \ No newline at end of file diff --git a/.ideai/tickets/26/carnet.md b/.ideai/tickets/26/carnet.md new file mode 100644 index 0000000..49e03fa --- /dev/null +++ b/.ideai/tickets/26/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#26" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783861758866 +--- diff --git a/.ideai/tickets/26/issue.md b/.ideai/tickets/26/issue.md new file mode 100644 index 0000000..ff1feb0 --- /dev/null +++ b/.ideai/tickets/26/issue.md @@ -0,0 +1,16 @@ +--- +id: "2201b898-9c45-4057-9014-5284897507ad" +number: 26 +title: "[UI] refonte totale de la gestion des menus via agent UX" +status: "closed" +priority: "medium" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783437525356 +updatedAt: 1783861758866 +version: 6 +--- +J'aimerais une refonte totale de l'UX et de l'UI d'IdeA pour tout ce qui touche aux menus. Je n'aime pas la disposition actuelle. Il est important de garder l'aspect fenêtre flottantes, le fait de pouvoir anchor les fenêtres sur le côté d'IdeA, mais je n'aime pas le fait qu'il y ai deux listes déroulantes View et Window. Onne comprends pas vraiemnt la différence. De plus, l'affichage de l'onglet du projet avec le nom "Projet : IdeA" sur lequel cliquer pour changer de projet je n'aime pas. Je pense peut être plutot ajouter un + à côté de l'onglet du projet pour pouvoir ouvrir plusieurs projet en parallèle ? (A l''approbation de UX). Par contre je ne veux pas qu'on touche aux cellules des agents, elles sont très bien comme ça, on se contente des menus et des fenêtres. \ No newline at end of file diff --git a/.ideai/tickets/27/carnet.md b/.ideai/tickets/27/carnet.md new file mode 100644 index 0000000..29bab02 --- /dev/null +++ b/.ideai/tickets/27/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#27" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783926649550 +--- diff --git a/.ideai/tickets/27/issue.md b/.ideai/tickets/27/issue.md new file mode 100644 index 0000000..08c0534 --- /dev/null +++ b/.ideai/tickets/27/issue.md @@ -0,0 +1,16 @@ +--- +id: "d7459ae0-31ae-49b2-bd93-2419397b0343" +number: 27 +title: "[Bug] L'outil assistance IA n'a pas les commandes MCP" +status: "closed" +priority: "high" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783439818722 +updatedAt: 1783926649550 +version: 6 +--- +Il semblerait que l'outil assistance IA des tickets n'ait pas connaissance des tools MCP pour l'edition des tickets. J'ai essayé de demandé à l'assistant de modifié le ticket pour que ça aille dans le sens de la conversation que j'ai eu avec lui et il m'a dabord donné un .md. Quand je lui ai demandé de modifier lui même le ticket, il est allé dans le fichier directement pour apporter des modifications au lieu d'utiliser le tool MCP d'IdeA \ No newline at end of file diff --git a/.ideai/tickets/28/carnet.md b/.ideai/tickets/28/carnet.md new file mode 100644 index 0000000..be663a6 --- /dev/null +++ b/.ideai/tickets/28/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#28" +version: 3 +updatedBy: {"kind":"user"} +updatedAt: 1783491458775 +--- diff --git a/.ideai/tickets/28/issue.md b/.ideai/tickets/28/issue.md new file mode 100644 index 0000000..0160d05 --- /dev/null +++ b/.ideai/tickets/28/issue.md @@ -0,0 +1,33 @@ +--- +id: "9d8ec8cf-312a-4f90-8199-e37b183ba11e" +number: 28 +title: "First-run wizard : bouton « Save and continue » figé (busy bloqué par l'auto-détection)" +status: "closed" +priority: "high" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1783458330440 +updatedAt: 1783491458775 +version: 3 +--- +## Symptôme (confirmé live, build 0.3.0 @2026-07-07 22:46, = develop 3c6cd04) +Au premier lancement, le wizard affiche les profils de référence (Claude/Codex/Ollama), l'utilisateur remplit le profil Ollama et coche la case, mais **« Save and continue » ET « Detect installed CLIs » restent grisés** → impossible de finir le first-run. Confirmé par l'utilisateur : les deux boutons sont grisés. + +## Diagnostic (source + JS embarqué vérifiés) +- Le JS embarqué contient exactement `disabled: n.busy` sur le bouton (`FirstRunWizard.tsx:103`) — **aucune** condition de validation. Donc bouton grisé ⇔ `busy` reste `true`. +- `useFirstRun.reload()` : `setBusy(true)` → affiche les lignes (d'où formulaire visible/éditable) → `await detectProfiles(refs)` (auto-détection à l'ouverture) → `finally { setBusy(false) }`. Le `finally` ne s'exécute que **si la détection se termine**. Si la promesse `detectProfiles` ne se résout jamais, `busy` reste figé → les deux boutons restent grisés. + +## Causes racines suspectées (2 défauts #14 qui se combinent) +1. **Profil Ollama sans commande de détection** : `detect: None` → `detection_spec` retombe sur `"{command} --version"` = `openai-compatible --version` (`runtime/mod.rs:64`), binaire inexistant → ligne « ✗ not found », et surtout probe inutile. +2. **`block_on` sur runtime tokio imbriqué** dans le chemin de détection : `DetectProfiles::execute` (`usecases.rs:79`) appelle `runtime.detect()` synchrone → `futures_block_on` construit un runtime current-thread et `block_on` (`runtime/mod.rs:231`) **à l'intérieur** de la commande async Tauri `detect_profiles` (`commands.rs:752`). Pattern qui peut paniquer (« Cannot start a runtime from within a runtime ») / rester pendu → la promesse ne se résout jamais. Se déclenche probablement sur une machine **sans Claude/Codex installés**. + +## Attendu du fix +- `busy` doit **toujours** revenir à `false` même si la détection panique/pend (le `finally` ne doit pas dépendre du résultat de la détection ; détection best-effort, non bloquante, éventuellement bornée par timeout). +- Donner une commande/stratégie de détection correcte au profil OpenAI-compatible (ou ne pas le sonder comme un CLI — c'est un endpoint HTTP, pas un binaire). +- Corriger le `block_on` imbriqué dans le runtime de détection (ne pas bloquer dans un contexte async Tauri). + +## Validation QA +Reproduire sur une machine **sans** Claude/Codex installés + Ollama lancé ; vérifier que le wizard reste utilisable (boutons cliquables) et que le profil Ollama se persiste et démarre. \ No newline at end of file diff --git a/.ideai/tickets/29/carnet.md b/.ideai/tickets/29/carnet.md new file mode 100644 index 0000000..8b7b673 --- /dev/null +++ b/.ideai/tickets/29/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#29" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783921595742 +--- diff --git a/.ideai/tickets/29/issue.md b/.ideai/tickets/29/issue.md new file mode 100644 index 0000000..3fceec1 --- /dev/null +++ b/.ideai/tickets/29/issue.md @@ -0,0 +1,16 @@ +--- +id: "51f19ca1-6b67-491b-b750-735c48a89d4b" +number: 29 +title: "[UI] Garder les filtres des tickets entre deux redemarrage" +status: "closed" +priority: "low" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783491008871 +updatedAt: 1783921595742 +version: 5 +--- +J'aimerais garder les filtres des tickets en mémoire entre deux redemarrage ou ouverture de la fenetre ou d ela popup de choix de tickets. \ No newline at end of file diff --git a/.ideai/tickets/3/carnet.md b/.ideai/tickets/3/carnet.md new file mode 100644 index 0000000..118d663 --- /dev/null +++ b/.ideai/tickets/3/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#3" +version: 7 +updatedBy: {"kind":"user"} +updatedAt: 1784377533777 +--- diff --git a/.ideai/tickets/3/issue.md b/.ideai/tickets/3/issue.md new file mode 100644 index 0000000..c5661e4 --- /dev/null +++ b/.ideai/tickets/3/issue.md @@ -0,0 +1,28 @@ +--- +id: "fedd343e-f9c1-467b-bab4-455f51907de7" +number: 3 +title: "[Différé — design à mûrir] Tâches de fond — dette B : retry durable après reboot" +status: "closed" +priority: "low" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1783082860758 +updatedAt: 1784377533777 +version: 7 +--- +Ticket #3 — Tâches de fond : retry après redémarrage sans fuite de secrets dans le projet. + +⛔ DÉCISION UTILISATEUR (2026-07-15) : DIFFÉRÉ. On ne persiste PAS de secrets en clair machine-local pour l'instant. À reprendre plus tard avec un vrai design sécurisé (keyring OS multiplateforme — Secret Service / Keychain / Credential Manager — ou modèle d'invocation déclaratif avec secret-refs re-résolus au retry depuis profil/env). Le retry-after-reboot reste indisponible en attendant. Ticket sorti de la production active du sprint « Gestion des bugs ». + +--- Cadrage conservé pour la reprise --- + +REQUALIFIÉ (2026-07-14) : +- Volet « énumération terminale/projet du BackgroundTaskStore » : RÉSOLU par l'implémentation actuelle (store durable par projet sous /.ideai/background-tasks, listes open par agent + completions non livrées, routage par projet). RETIRÉ du périmètre. +- Reste uniquement le retry après redémarrage. L'invocation d'origine (SpawnSpec) est conservée dans un registre in-memory session-scoped (BackgroundCommandArchive, crates/application/src/background/mod.rs:203) volontairement non persistée dans .ideai car elle peut contenir des secrets. Après reboot, une tâche persistée ne peut donc pas être relancée. + +Option A (écartée pour l'instant par décision utilisateur) : store machine-local hors projet (app-data OS/Tauri), SpawnSpec complet par (project_id, task_id), permissions 0700/0600 best-effort, JSON projet sans secret, retry échouant explicitement si invocation absente. Simple (M) mais persiste des secrets en clair local → refusée à ce stade. + +Piste retenue pour la reprise : keyring OS multiplateforme OU secret-refs déclaratifs (chantier plus large que « low », à cadrer comme design dédié le moment venu). \ No newline at end of file diff --git a/.ideai/tickets/30/carnet.md b/.ideai/tickets/30/carnet.md new file mode 100644 index 0000000..55d3558 --- /dev/null +++ b/.ideai/tickets/30/carnet.md @@ -0,0 +1,25 @@ +--- +issueRef: "#30" +version: 8 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783941648176 +--- +## Cadrage Architect (2026-07-13) — cas DIRECT, actionnable + +Débloqué : #27 fermé. #30 ≠ #15 (#15 = cas délégué, différé). + +**Périmètre** : le chemin direct de session agent (agent directement adressé, ex. Main) ne remonte pas la limite vers `SessionLimitService`. Fix = accrocher le détecteur sur le chemin direct, avant résolution finale du tour. + +**Frontière hexagonale** : infra détecte (adapter provider/profil déclaratif extrait la limite du flux structuré/regex → `ReplyEvent::RateLimited { resets_at_ms? }`) ; application arme la reprise (`SessionLimitService::on_rate_limited(agent_id,node_id,conversation_id,resets_at_ms)`) ; app-tauri relaie ; React affiche/annule. Aucune logique provider dans domaine/application. + +**Contrat d'événements** : +- `DomainEvent::AgentRateLimited { agent_id, resets_at_ms }` +- `DomainEvent::AgentResumeScheduled { agent_id, fire_at_ms }` si reprise armée +- reprise auto au reset, annulable via `cancel_resume` (contrat existant conservé) +- si heure de reset non récupérable : fallback niveau humain déjà cadré (état suspect + saisie d'heure, pas de silence) + +**Point clé** : identité runtime — résoudre l'agent courant + `node_id` vivant + `conversation_id` via le registre/session meta existant (pas de convention UI). Sans `conversation_id`, ne PAS prétendre la reprise armée. + +**Découpage** : B1 tap `ReplyEvent::RateLimited → on_rate_limited` dans le runner direct ; B2 résolution identité/session (agent_id/node_id/conversation_id) pour l'agent adressé ; B3 tests application/app-tauri (flux direct rate-limit avec resets_at_ms → AgentRateLimited puis AgentResumeScheduled) ; F1 régression badge existant (pas de nouveau contrat) ; F2 QA réelle (limite provoquée/mockée sur Main → heure affichée, annulation, reprise planifiée). + +**Sous-lot adjacent** (ferme #7/F2, mémoire `ticket7-f2-delegated-limit-no-agentratelimited`) : la branche rendez-vous délégué publie seulement `DelegationRateLimited` et doit aussi armer `on_rate_limited(target,…)`. Traité dans la même branche mais en commit séparé. \ No newline at end of file diff --git a/.ideai/tickets/30/issue.md b/.ideai/tickets/30/issue.md new file mode 100644 index 0000000..bee62a5 --- /dev/null +++ b/.ideai/tickets/30/issue.md @@ -0,0 +1,16 @@ +--- +id: "f2c4ed9c-ba54-42c1-90aa-0345d4ff4c6b" +number: 30 +title: "[Bug] Le handle de limite session n'est pas fonctionnel quand c'est Main qui limite session" +status: "closed" +priority: "medium" +sprint: "d8f3f37b-87ca-4509-9116-45a99bc711df" +links: [{"target":"#27","kind":"blockedBy"}] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783491181648 +updatedAt: 1783941648176 +version: 8 +--- +Lorsque l'agent qui atteint sa limite est celui à qui ont parle, la limite de session n'est pas récupérée par IdeA et n'est pas gérée \ No newline at end of file diff --git a/.ideai/tickets/31/carnet.md b/.ideai/tickets/31/carnet.md new file mode 100644 index 0000000..59d2de4 --- /dev/null +++ b/.ideai/tickets/31/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#31" +version: 4 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784064442657 +--- diff --git a/.ideai/tickets/31/issue.md b/.ideai/tickets/31/issue.md new file mode 100644 index 0000000..9914003 --- /dev/null +++ b/.ideai/tickets/31/issue.md @@ -0,0 +1,31 @@ +--- +id: "83d320f1-766b-4744-b518-8e39a2518e9c" +number: 31 +title: "Détection de profils séquentielle : N × 800 ms au pire, résultat potentiellement partiel" +status: "closed" +priority: "low" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [{"target":"#28","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783491197541 +updatedAt: 1784064442657 +version: 4 +--- +## Origine +Relevé par Git en revue du diff du ticket #28 (merge 710fa8f), hors périmètre du fix. + +## Constat +Depuis #28, `DetectProfiles::execute` séquence les `await` candidat par candidat, chaque sonde bornée par `tokio::time::timeout(800ms)`. N profils indisponibles coûtent donc **N × 800 ms** au pire. + +Côté frontend, `DETECT_TIMEOUT_MS = 3 s` relâche le flag `detecting`. À partir de ~4 candidats muets, le tour de détection dépasse ce délai : l'indicateur « Detecting… » disparaît avant que les résultats n'arrivent. + +## Impact +Cosmétique. L'UI **ne gèle plus** (c'est bien l'objet de #28, corrigé) ; le risque résiduel est un affichage de disponibilité qui arrive après la retombée de l'indicateur. + +## Piste de fix +Paralléliser les sondes avec `futures::future::join_all` dans `DetectProfiles::execute` (`crates/application/src/agent/usecases.rs`) : le coût au pire retombe à ~800 ms quel que soit N, largement sous le timeout UI. + +## Validation +Test `#[tokio::test(flavor = "multi_thread")]` sur `DetectProfiles::execute` avec N candidats muets, asserting que la durée totale reste bornée par ~1 × timeout et non N ×. \ No newline at end of file diff --git a/.ideai/tickets/32/carnet.md b/.ideai/tickets/32/carnet.md new file mode 100644 index 0000000..966cee4 --- /dev/null +++ b/.ideai/tickets/32/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#32" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783723378815 +--- diff --git a/.ideai/tickets/32/issue.md b/.ideai/tickets/32/issue.md new file mode 100644 index 0000000..203de56 --- /dev/null +++ b/.ideai/tickets/32/issue.md @@ -0,0 +1,23 @@ +--- +id: "9291e7ad-a300-4f15-83b1-93f56f6e1b79" +number: 32 +title: "[Bug] Erreur CLI Ollama profile" +status: "closed" +priority: "medium" +sprint: null +links: [{"target":"#14","kind":"relatesTo"}] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783491571863 +updatedAt: 1783723378815 +version: 6 +--- +J'ai créé un profile IA Ollama et j'ai ajouté un agent avec ce profile (agent TestOllama), mais quand + j'essaie d'afficher sa cli j'ai un bandeau rouge qui dit "Échec du lancement de l'agent : process error: pty spawn + failed: Unable to spawn openai-compatible because: + No viable candidates found in PATH "/tmp/.mount_IdeA_0mlgJnH/usr/bin/:/tmp/.mount_IdeA_0mlgJnH/usr/sbin/:/tmp/.mount_ + IdeA_0mlgJnH/usr/games/:/tmp/.mount_IdeA_0mlgJnH/bin/:/tmp/.mount_IdeA_0mlgJnH/sbin/:/home/anthony/.local/bin:/run/us + er/1000/fnm_multishells/4610_1783490075749/bin:/home/anthony/.local/share/fnm:/home/anthony/go/bin:/home/anthony/.car + go/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl: + /usr/bin/core_perl:/home/anthony/.local/share/JetBrains/Toolbox/scripts" \ No newline at end of file diff --git a/.ideai/tickets/33/carnet.md b/.ideai/tickets/33/carnet.md new file mode 100644 index 0000000..edbe6fd --- /dev/null +++ b/.ideai/tickets/33/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#33" +version: 4 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784048928225 +--- diff --git a/.ideai/tickets/33/issue.md b/.ideai/tickets/33/issue.md new file mode 100644 index 0000000..04eaf94 --- /dev/null +++ b/.ideai/tickets/33/issue.md @@ -0,0 +1,39 @@ +--- +id: "c3e1fbcc-cedc-48a5-a00e-b22ea8dc90de" +number: 33 +title: "[Dette] Fallthrough silencieux du routage structuré (lifecycle.rs:1705)" +status: "closed" +priority: "high" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [{"target":"#32","kind":"relatesTo"},{"target":"#14","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783492190391 +updatedAt: 1784048928225 +version: 4 +--- +Cause racine structurelle identifiée par Architect lors du cadrage de #32. + +`LaunchAgent::execute` (crates/application/src/agent/lifecycle.rs:1705) route vers la session structurée avec : + +```rust +if let (Some(factory), Some(structured), true) = ( + self.session_factory.as_ref(), self.structured.as_ref(), + profile.structured_adapter.is_some(), +) +``` + +Ce `if let` **confond deux situations distinctes** : +- « profil structuré, mais factory non câblée » (cas de l'UI : `state.rs:1466` construit le `LaunchAgent` sans `.with_structured(...)`) ; +- « profil non structuré » (vrai profil PTY historique). + +Dans les deux cas on tombe silencieusement, sans erreur ni branche explicite, sur `lifecycle.rs:1746` `self.pty.spawn(spec)`. + +C'est ce fallthrough qui a produit le bug #32 : un profil `openAiCompatible` (aucun binaire) atteignait `CommandBuilder::new("openai-compatible")`. Claude et Codex tombent dans le **même** fallthrough ; ça ne passe inaperçu que parce qu'un binaire `claude`/`codex` existe dans le PATH. + +Le fix de #32 neutralise le cas transport-backed, mais **le piège reste armé** pour le prochain adapter. + +**Attendu** : remplacer le `if let` par un `match` explicite qui distingue les deux cas, et décider du comportement quand un profil structuré rencontre une factory non câblée (erreur explicite plutôt que dégradation muette vers le PTY). + +Hors périmètre de #32 (déposé comme bug). Cadrage Architect requis avant implémentation. \ No newline at end of file diff --git a/.ideai/tickets/34/carnet.md b/.ideai/tickets/34/carnet.md new file mode 100644 index 0000000..e9d7328 --- /dev/null +++ b/.ideai/tickets/34/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#34" +version: 4 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784050010085 +--- diff --git a/.ideai/tickets/34/issue.md b/.ideai/tickets/34/issue.md new file mode 100644 index 0000000..1195850 --- /dev/null +++ b/.ideai/tickets/34/issue.md @@ -0,0 +1,29 @@ +--- +id: "8dca00bf-ec0f-4da1-a231-46b27c711b78" +number: 34 +title: "[Risque] ChatBridge.scrollback non borné (croissance mémoire)" +status: "closed" +priority: "medium" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [{"target":"#32","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783492562056 +updatedAt: 1784050010085 +version: 4 +--- +Identifié par Architect lors du cadrage de #32 (variante B). + +`ChatBridge` retient le scrollback d'une session chat pour le rejouer au remount (`reattach_agent_chat`). L'enregistrement se fait par un `push` nu, **sans aucun cap** : + +- `app-tauri/src/chat.rs:161` — `push` sans borne. +- `app-tauri/src/chat.rs:47` — la doc prétend « Bounded only by the conversation length (a turn count) », ce qui est un euphémisme : chaque `TextDelta` (quelques octets) est retenu à vie. + +**Dormant jusqu'ici** : aucune session structurée vivante ne passait par ce chemin. **Devient actif avec #32 variante B** (cellule chat pour les adapters transport-backed) : une conversation Ollama longue fait croître le `Vec` sans borne. + +Distinct de la rétention/rotation du transcript disque (mémoire `conversation-log-ls6-rotation-and-paginated-read`), qui porte sur `log.jsonl`, pas sur ce buffer mémoire. + +**Attendu** : borner le scrollback (cap en nombre d'événements ou en octets, avec troncature par la tête), corriger la doc `chat.rs:47`, et décider si le remount doit signaler une troncature à l'UI. + +Hors périmètre #32. Cadrage Architect requis. \ No newline at end of file diff --git a/.ideai/tickets/35/carnet.md b/.ideai/tickets/35/carnet.md new file mode 100644 index 0000000..5c394c5 --- /dev/null +++ b/.ideai/tickets/35/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#35" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783777898736 +--- diff --git a/.ideai/tickets/35/issue.md b/.ideai/tickets/35/issue.md new file mode 100644 index 0000000..b9db7e2 --- /dev/null +++ b/.ideai/tickets/35/issue.md @@ -0,0 +1,16 @@ +--- +id: "77ef65a5-bbb8-44d2-a610-27bf15c54a0a" +number: 35 +title: "Avoir llamacpp intégré" +status: "closed" +priority: "high" +sprint: "883534aa-7fc1-4d83-a9c0-17cac4a4eea5" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783756863540 +updatedAt: 1783777898736 +version: 6 +--- +Nous avons llamacpp qui marche super avec OpencCode à présent. J'aimerais maintenant que llamacpp soit lancé automatiquement via IdeA avec le bon modele à la bonne url et bon port si celui-ci n'est pas encore lancé \ No newline at end of file diff --git a/.ideai/tickets/36/carnet.md b/.ideai/tickets/36/carnet.md new file mode 100644 index 0000000..bc4eb8e --- /dev/null +++ b/.ideai/tickets/36/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#36" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783777899737 +--- diff --git a/.ideai/tickets/36/issue.md b/.ideai/tickets/36/issue.md new file mode 100644 index 0000000..eca3606 --- /dev/null +++ b/.ideai/tickets/36/issue.md @@ -0,0 +1,16 @@ +--- +id: "3bb071b9-08eb-4e1b-b4b4-dccee8114a7e" +number: 36 +title: "[UI] Pouvoir déclarer plusieurs profils AI de modeles locaux" +status: "closed" +priority: "medium" +sprint: "883534aa-7fc1-4d83-a9c0-17cac4a4eea5" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783757232381 +updatedAt: 1783777899737 +version: 6 +--- +Actuellement chaque type de profils ne peux déclarer qu'un seul profile AI. J'aimerais que le profile OpenCode puisse permetre d'en décalrer plusieurs différents. \ No newline at end of file diff --git a/.ideai/tickets/37/carnet.md b/.ideai/tickets/37/carnet.md new file mode 100644 index 0000000..ed3e018 --- /dev/null +++ b/.ideai/tickets/37/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#37" +version: 7 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783878976369 +--- diff --git a/.ideai/tickets/37/issue.md b/.ideai/tickets/37/issue.md new file mode 100644 index 0000000..a88b571 --- /dev/null +++ b/.ideai/tickets/37/issue.md @@ -0,0 +1,16 @@ +--- +id: "a2b6212a-8725-4ace-b8a4-c3a41e2c2988" +number: 37 +title: "[UI] garder les tickets dans les sprints, mais ajouter l'affichage du status et la possibiliter de filtrer les tickets" +status: "closed" +priority: "low" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783757421147 +updatedAt: 1783878976369 +version: 7 +--- +Actuelleemnt dans les sprints les tickets sont retirés une fois fait. J'aimerais que les tickets restent dans le sprints mais qu'on ajoute l'affichage du status de chaque ticket. J'aimerais aussi qu'on ai la possibilité de trier les tickets via leur status, leur priorité etc comme sur la liste des tickets \ No newline at end of file diff --git a/.ideai/tickets/38/carnet.md b/.ideai/tickets/38/carnet.md new file mode 100644 index 0000000..a8a6ef2 --- /dev/null +++ b/.ideai/tickets/38/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#38" +version: 7 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783879443381 +--- diff --git a/.ideai/tickets/38/issue.md b/.ideai/tickets/38/issue.md new file mode 100644 index 0000000..49f8bd6 --- /dev/null +++ b/.ideai/tickets/38/issue.md @@ -0,0 +1,16 @@ +--- +id: "7e89bfc9-dc56-4656-a154-690f02e69756" +number: 38 +title: "[UI] Pouvoir ajouter un ticket à un sprint pendant sa création" +status: "closed" +priority: "low" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783757548614 +updatedAt: 1783879443381 +version: 7 +--- +J'aimerais que dans la création de ticket on puisse lui attribuer un sprint via une popup comme celle du choix de tickets \ No newline at end of file diff --git a/.ideai/tickets/39/carnet.md b/.ideai/tickets/39/carnet.md new file mode 100644 index 0000000..9ce884f --- /dev/null +++ b/.ideai/tickets/39/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#39" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783921595316 +--- diff --git a/.ideai/tickets/39/issue.md b/.ideai/tickets/39/issue.md new file mode 100644 index 0000000..d7cd824 --- /dev/null +++ b/.ideai/tickets/39/issue.md @@ -0,0 +1,16 @@ +--- +id: "02ff2d0c-4312-47c3-b84c-834029723fc4" +number: 39 +title: "[UI] Fermer toutes les fernetres lors de la fermeture de la fenetre principale principale" +status: "closed" +priority: "low" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783757707896 +updatedAt: 1783921595316 +version: 5 +--- +Quand on ferme la fenetre principale d'IdeA, j'aimerais que toutes les fenêtres IdeA se ferment aussi. \ No newline at end of file diff --git a/.ideai/tickets/4/carnet.md b/.ideai/tickets/4/carnet.md new file mode 100644 index 0000000..8710bcc --- /dev/null +++ b/.ideai/tickets/4/carnet.md @@ -0,0 +1,28 @@ +--- +issueRef: "#4" +version: 9 +updatedBy: {"kind":"user"} +updatedAt: 1783437430509 +--- +## 🔄 REDÉMARRAGE DE ZÉRO (2026-07-04) — décision utilisateur + +TOUT le travail #4 précédent (sur base canonique/main) est ABANDONNÉ comme ligne de livraison. Raison : l'utilisateur a réaligné main = develop = feature/background-tasks-first-class (ebd992e, base PRÉ-canonique de #1). Le moteur de conversation « canonique » (spawn_turn/AgentTurnEvent/agentBusyChanged) sur lequel reposait l'ancien #4 n'est PLUS sur main/develop. On refait #4 de zéro sur la base develop. + +BRANCHE DE TRAVAIL : `feature/agent-live-announcements` (créée depuis develop ebd992e). + +BRANCHES SUPPRIMÉES (obsolètes) : feature/announcements-canonical, feature/inter-agent-announcements. +FILETS DE RÉCUPÉRATION (si on veut repiquer du code) : +- backup/announcements-work-20260704 (691b2ce) = ancien #4 complet (backend B0-B3 + frontend F1/F2/F3 recâblé busy + tests). +- backup/inter-agent-announcements-71d307d (71d307d) = backend annonces d'origine. +- backup/main-canonical-20260704 (3cff2b1) = moteur canonique + releases. + +OBJECTIF PRODUIT (inchangé, reconfirmé utilisateur) : affichage live sur la cellule d'un agent de ce qu'il raconte quand un AUTRE agent le sollicite via idea_ask_agent. Overlay sur la cellule CIBLE (« un agent est en train de lui parler » + annonces défilantes), monté quand la cible est busy, retiré à l'idle. Preview côté DEMANDEUR près de la liste d'agents, filtré par ticket + requester==self. + +⚠️ ATTENTION base develop : moteur « batch » (run_turn), PAS le canonique. À cadrer par Architect : quels signaux live develop émet-il déjà (annonces intermédiaires ? busy/idle par agent ? l'ancien #4 utilisait agentBusyChanged + read-model agents[].busy — existent-ils sur develop ?), et que faut-il (ré)implémenter côté backend pour que les annonces soient LIVE (pas flushées en fin de tour). Réutiliser les composants frontend depuis backup/announcements-work-20260704 si le contrat de signaux le permet. + +## RESTE À FAIRE (cycle complet sur base develop) +1. [EN COURS] Architect : cadrage from-scratch sur base develop (signaux live disponibles + à créer, contrat Announcement/Final, plan B/F, réutilisabilité du code sauvegardé). +2. Git : branche de travail = feature/agent-live-announcements (FAIT). +3. DevBackend : mécanisme annonces/Final + signaux live sur moteur develop. +4. DevFrontend : F1 store + F2 preview demandeur + F3 overlay cible. +5. QA : reproduire le déclenchement inter-agent + constater overlay/preview live + fermeture à l'idle. \ No newline at end of file diff --git a/.ideai/tickets/4/issue.md b/.ideai/tickets/4/issue.md new file mode 100644 index 0000000..3e03988 --- /dev/null +++ b/.ideai/tickets/4/issue.md @@ -0,0 +1,35 @@ +--- +id: "547f6a47-9c86-4c16-9018-f050af75a6aa" +number: 4 +title: "Annonces inter-agent (UI live) — reprise et finalisation : overlay cellule cible + preview demandeur (F1-F3)" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1783083604350 +updatedAt: 1783437430509 +version: 9 +--- +Objectif produit (demande utilisateur, design figé le 2026-07-02) : +Quand un agent en contacte un autre via idea_ask_agent, l'agent CONTACTÉ doit afficher, par-dessus sa cellule, un overlay « un agent est en train de lui parler » avec ses réflexions/annonces en cours qui défilent ; l'agent DEMANDEUR affiche un petit preview des annonces près de la drop-list d'agents, filtré par ticket. L'overlay est piloté par le live-state (monté sur Working, retiré au Final/idle, persiste tant qu'≥1 ticket actif sur la cible). Design et cadrage complets en mémoire projet : inter-agent-announcements-feature-and-codex-final-bug (+ inter-agent-live-context-shared-per-agent). + +ÉTAT ACTUEL (vérifié 2026-07-03) : +- Backend B0-B3 DÉJÀ implémenté sur la branche feature/inter-agent-announcements, commit 71d307d : + - ReplyEvent::Announcement (non terminal) vs Final (terminal, résout le ticket). + - DomainEvent::AgentAnnouncement{project_id, requester, target, ticket_id, text, at_ms}. + - Relay Tauri (crates/app-tauri/src/events.rs), DTO. + - Fix du Final Codex : dernier agent_message avant turn.completed = Final (avant : le préambule était renvoyé au demandeur au lieu de la conclusion). 18 fichiers backend + tests. +- FRONTEND F1/F2/F3 : JAMAIS écrit. Aucune référence « announcement » sous frontend/src. C'est la pièce visuelle centrale de la demande (preview demandeur + overlay cible) — absente. +- La branche feature/inter-agent-announcements est 15 commits EN RETARD sur develop, jamais mergée. Une seconde branche feature/inter-agent-announcements-v2 ne contient PAS le code d'annonces (repartie sur d'autres fixes codex/orchestrator). + +RESTE À FAIRE : +1. Architect : cadrer la REPRISE — décider du rebase de 71d307d sur develop actuel, arbitrer les conflits avec le nouveau modèle de conversation (le fix Final Codex et le modèle de fil ont évolué depuis ; cf. inter-agent-live-context-shared-per-agent : log canonique PAR AGENT + vues par paire). Confirmer que le contrat Announcement/Final tient encore. +2. Git : décider de la topologie (rebase vs re-cherry-pick du backend sur une branche fraîche depuis develop). +3. DevBackend : réintégrer/adapter B0-B3 sur develop actuel si des conflits d'API le nécessitent. +4. DevFrontend : écrire F1 (store/gateway annonces, index par ticket/target, borné ~20-50), F2 (preview cellule demandeur près de la drop-list, filtré par ticket_id), F3 (overlay cellule cible : texte haut + annonces défilantes au centre, monté sur live-state Working, retiré au Final/idle). +5. QA e2e : reproduire le bug initial (Main→Architect→QA avec préambule+outil+conclusion, vérifier que le demandeur ne reçoit QUE la conclusion), puis constater le preview demandeur + l'overlay cible pendant le travail, et la fermeture au Final. + +Chantier DISTINCT du ticket #1 (tâches de fond). Non prioritaire tant que #1 n'est pas mergé, sauf décision contraire. \ No newline at end of file diff --git a/.ideai/tickets/40/carnet.md b/.ideai/tickets/40/carnet.md new file mode 100644 index 0000000..6604ca9 --- /dev/null +++ b/.ideai/tickets/40/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#40" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783922451268 +--- diff --git a/.ideai/tickets/40/issue.md b/.ideai/tickets/40/issue.md new file mode 100644 index 0000000..22efb0e --- /dev/null +++ b/.ideai/tickets/40/issue.md @@ -0,0 +1,16 @@ +--- +id: "6b04c8d1-177c-422a-9955-7fee2e8afdeb" +number: 40 +title: "[UI] réouvrir les fenetres au lancement d'IdeA" +status: "closed" +priority: "low" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [{"target":"#39","kind":"dependsOn"}] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783757766786 +updatedAt: 1783922451268 +version: 6 +--- +Au lancement d'IdeA, j'aimerais que les fenêtres fermées lors de la précédentes fermeture de la fenêtre principale d'IdeA soient automatiqument réouvertes dans le même status, position, écran que quand elles ont été fermée \ No newline at end of file diff --git a/.ideai/tickets/41/carnet.md b/.ideai/tickets/41/carnet.md new file mode 100644 index 0000000..e376c02 --- /dev/null +++ b/.ideai/tickets/41/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#41" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783878363957 +--- diff --git a/.ideai/tickets/41/issue.md b/.ideai/tickets/41/issue.md new file mode 100644 index 0000000..02d8b68 --- /dev/null +++ b/.ideai/tickets/41/issue.md @@ -0,0 +1,16 @@ +--- +id: "7d026901-333a-46a6-87f6-7384bf742310" +number: 41 +title: "[UI] Selection multiple lors de la selection de tickets pour les sprints" +status: "closed" +priority: "low" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783757895085 +updatedAt: 1783878363957 +version: 6 +--- +Lorsque je selectionne les tickets pour mes sprints, j'iamerais que la selection me permette d'en choisir plusieurs à la fois pour ne pas avoir à réouvrir chaque fois la fenetre. \ No newline at end of file diff --git a/.ideai/tickets/42/carnet.md b/.ideai/tickets/42/carnet.md new file mode 100644 index 0000000..ba664c2 --- /dev/null +++ b/.ideai/tickets/42/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#42" +version: 2 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783877612067 +--- diff --git a/.ideai/tickets/42/issue.md b/.ideai/tickets/42/issue.md new file mode 100644 index 0000000..2858164 --- /dev/null +++ b/.ideai/tickets/42/issue.md @@ -0,0 +1,16 @@ +--- +id: "239aff64-a131-47eb-9a95-cc56c8c369c6" +number: 42 +title: "[UI] Clarifier les contrôles d'en-tête de panneau (DockControls) — icônes libellées + tooltips alignés sur le menu Panneaux" +status: "closed" +priority: "low" +sprint: null +links: [{"target":"#26","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783862180616 +updatedAt: 1783877612067 +version: 2 +--- +Refinement UX suite à #26. Remplacer les glyphes peu explicites des en-têtes de panneaux docké/flottant (◧ ◨ ⧉ ⤢ ✕) par des boutons-icônes libellés + tooltips (title natif), alignés strictement sur les libellés du menu Panneaux. Spec UX figé : IconButton existant (@/shared, pas de lib externe, pas de Tooltip custom), ordre [⇤ Ancré à gauche][⇥ Ancré à droite][□ Flottant][↗ Fenêtre détachée][× Fermé] ; title + aria-label "" ; placement courant = actif (aria-pressed=true, bg-raised text-content, ring-1 ring-border-strong) et NON désactivé ; "Fenêtre détachée" masqué pour projects et désactivé sans projet actif ; conteneur flex shrink-0 items-center gap-1. Périmètre strict : DockControls dans ProjectsView.tsx uniquement, aucun changement LayoutGrid/LeafView/cellules agents/write-portal/F3/DockRegion/FloatingWindow/ViewWindow. \ No newline at end of file diff --git a/.ideai/tickets/43/carnet.md b/.ideai/tickets/43/carnet.md new file mode 100644 index 0000000..3c51fa8 --- /dev/null +++ b/.ideai/tickets/43/carnet.md @@ -0,0 +1,349 @@ +--- +issueRef: "#43" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784707900405 +--- +# Ticket #43 — Cadrage consolidé v2 du système de plugins + +> Mise à jour Architect du 2026-07-21 après retour QA F4. Ce carnet v2 annule les ambiguïtés du cadrage initial, en particulier sur le schéma de layout custom plugin. + +## 0. Décisions discovery conservées + +- Plugins **full-trust** v1 : pas de sandbox UI. +- Bundle **JS/ESM pré-compilé**, importé dynamiquement ; IdeA ne compile pas de TS/TSX. +- Installation **globale** dans le dossier données utilisateur de l'application, pas dans les projets. +- Distribution v1 locale : dossier ou archive locale ; packaging compatible archive partageable future type `.vsix`. +- Contributions v1 : menus top-level, items de menus existants, layouts custom React arbitraires, serveurs MCP externes. +- MCP plugin = process serveur MCP externe déclaré par manifeste et supervisé via le pont MCP existant. +- Propreté retrait : après uninstall + redémarrage, aucune contribution, aucun process, aucune entrée fantôme, aucun état plugin-specific. + +## 1. Store global et cycle de vie + +Store global : + +```text +{app_data_dir}/plugins/ + registry.json + installed/ + / + idea-plugin.json + dist/index.js + assets/... + servers/... +``` + +États v1 persistés : + +```text +enabled +disabled +pending-enable +pending-disable +pending-uninstall +invalid +``` + +Règles : + +- Installer depuis archive ou dossier local copie toujours un snapshot dans `installed//`. +- `registry.json` porte seulement l'état global nécessaire ; une désinstallation réussie supprime l'entrée registry. +- Disable/uninstall en session masque les contributions UI et arrête MCP immédiatement si possible, mais le code ESM déjà importé n'est purgé strictement qu'au redémarrage. +- Au boot, le frontend reconstruit une registry plugin vide puis charge uniquement les plugins `enabled` et non `pending-uninstall`. + +## 2. Manifeste v1 + +Fichier obligatoire : `idea-plugin.json`. + +```json +{ + "ideaPluginManifestVersion": 1, + "id": "dev.acme.gitgraph", + "displayName": "Git Graph", + "publisher": "Acme DevTools", + "version": "1.2.3", + "engines": { "idea": ">=0.1.0 <1.0.0" }, + "main": "dist/index.js", + "icon": "assets/icon.svg", + "trustLevel": "full", + "capabilities": ["ui", "mcp"], + "contributes": { + "menus": [], + "menuItems": [], + "layouts": [], + "mcpServers": [] + }, + "archive": { + "files": ["idea-plugin.json", "dist/**", "assets/**", "servers/**"] + } +} +``` + +Validation : + +- `trustLevel` vaut uniquement `full` en v1. +- `main`, `icon`, assets et commandes MCP relatives ne doivent contenir ni chemin absolu ni `..`. +- `id` stable, unique, reverse-DNS recommandé. +- `version` SemVer. +- `engines.idea` incompatible => plugin `invalid`, jamais chargé. + +## 3. Contrat définitif des layouts custom plugin + +### 3.1 Décision ferme + +`CustomPluginLayout` est une **vraie variante top-level de `LayoutNode`**, pas un champ optionnel de `LeafCell`. + +Motif : un layout plugin occupe une zone de l'arbre au même niveau conceptuel qu'une leaf terminal, un split ou une grid. Le mettre dans `LeafCell` mélangerait deux responsabilités incompatibles : terminal/session/agent d'un côté, composant React arbitraire et état opaque plugin de l'autre. + +### 3.2 Schéma JSON canonique + +Le layout tree garde le format serde existant `#[serde(tag = "type", content = "node")]`. + +Union canonique : + +```ts +export type LayoutNode = + | { type: "leaf"; node: LeafCell } + | { type: "split"; node: SplitContainer } + | { type: "grid"; node: GridContainer } + | { type: "customPluginLayout"; node: CustomPluginLayoutCell }; +``` + +Payload canonique : + +```ts +export interface CustomPluginLayoutCell { + id: string; + pluginId: string; + layoutType: string; + state: unknown; +} +``` + +Exemple complet : + +```json +{ + "root": { + "type": "customPluginLayout", + "node": { + "id": "018f0c5a-2b4b-70d4-a7c2-300000000001", + "pluginId": "dev.acme.gitgraph", + "layoutType": "dev.acme.gitgraph.layout", + "state": { + "branchFilter": "main" + } + } + } +} +``` + +Dans un split : + +```json +{ + "type": "split", + "node": { + "id": "split-1", + "direction": "row", + "children": [ + { + "weight": 1, + "node": { + "type": "leaf", + "node": { "id": "terminal-1" } + } + }, + { + "weight": 1, + "node": { + "type": "customPluginLayout", + "node": { + "id": "plugin-cell-1", + "pluginId": "dev.acme.gitgraph", + "layoutType": "dev.acme.gitgraph.layout", + "state": {} + } + } + } + ] + } +} +``` + +Dans une grid, `GridCell.node` peut pareillement être `{ type: "customPluginLayout", node: ... }`. + +### 3.3 Ce qui est explicitement interdit + +Cette forme n'est **pas** contractuelle et ne doit plus être produite ni consommée comme modèle canonique : + +```json +{ + "type": "leaf", + "node": { + "id": "leaf-1", + "pluginLayout": { + "pluginId": "dev.acme.gitgraph", + "layoutType": "dev.acme.gitgraph.layout", + "state": {} + } + } +} +``` + +`LeafCell.pluginLayout` est une divergence frontend issue de l'ambiguïté initiale. Elle doit être supprimée du modèle domaine TS ou limitée à une migration locale temporaire de tests/mocks ; elle ne fait pas partie de l'IPC ni de la persistance. + +### 3.4 Disponibilité et fallback + +La disponibilité n'est pas stockée dans le layout. Elle est dérivée côté UI à partir du plugin registry/runtime catalog : + +```ts +export type PluginLayoutAvailability = + | "available" + | "plugin-disabled" + | "plugin-missing" + | "incompatible"; +``` + +Rendu : + +- `available` : rendre le composant React enregistré pour `layoutType`. +- `plugin-disabled`, `plugin-missing`, `incompatible` : rendre le fallback non destructif `Layout indisponible`. +- Le fallback ne transforme pas automatiquement le nœud et ne supprime jamais `state`. + +### 3.5 Ajustements requis + +Verdict convergence F4 : **frontend à ajuster, backend à conserver**. + +Backend : + +- L'implémentation actuelle `LayoutNode::CustomPluginLayout(CustomPluginLayoutCell)` sérialisée `type: "customPluginLayout"` est le contrat canonique. +- À vérifier seulement : roundtrip serde et DTO Tauri exposent bien `pluginId`, `layoutType`, `state` en camelCase. + +Frontend : + +- Ajouter la variante `{ type: "customPluginLayout"; node: CustomPluginLayoutCell }` à `LayoutNode`. +- Retirer `pluginLayout?: CustomPluginLayoutCell` de `LeafCell` comme contrat domaine. +- Adapter `LayoutGrid`/renderer récursif pour router `type === "customPluginLayout"` vers `PluginLayoutCellView`. +- Adapter `layoutAvailability`, fallback et tests pour recevoir le payload depuis `node.node` de la variante top-level. +- Ajouter un test de parsing/rendu avec un layout JSON produit par Rust contenant `type: "customPluginLayout"`. + +## 4. Contribution points menus et `when` + +Menus v1 : + +```ts +type MenuTargetId = "panels" | "settings" | `plugin:${string}`; +``` + +Items v1 : + +```ts +interface PluginMenuItemContribution { + id: string; + targetMenuId: MenuTargetId; + label: string; + command: string; + order?: number; + icon?: string; + when?: string; +} +``` + +`when` v1 utilise le mini-langage booléen `&&`, `||`, `!`, parenthèses. + +Variables : + +```ts +type WhenVariable = + | "projectOpen" + | "gitRepository" + | "agentSelected" + | "terminalFocused" + | "layoutCellFocused"; +``` + +Arbitrage QA sur les variables actuellement figées à `false` : + +- `projectOpen` : obligatoire v1, doit refléter l'état réel. +- `gitRepository` : **bloquant avant clôture #43/F3**. Le manifeste exemple et le cas GitGraph dépendent de `projectOpen && gitRepository`; le laisser à `false` rend des contributions valides inatteignables. +- `agentSelected`, `terminalFocused`, `layoutCellFocused` : dette acceptable v1 si elles restent explicitement documentées comme **best-effort non câblé** et donc `false` jusqu'à un lot focus/selection dédié. Ce n'est pas bloquant pour F4 ni pour la clean-removal QA, sauf si un plugin de validation les utilise dans son manifeste. + +Ajustement recommandé : DevFrontend câble au minimum `gitRepository` depuis l'état projet/git déjà disponible, ou retire temporairement `gitRepository` des manifests/tests de validation. La préférence architecture est de le câbler, car il est déjà annoncé comme variable v1. + +## 5. Contribution MCP et `${appDataDir}` + +Manifest MCP : + +```ts +interface PluginMcpServerContribution { + id: string; + displayName: string; + command: string; + args?: string[]; + env?: Record; + cwd?: "${pluginRoot}" | "${appDataDir}" | string; + transport: "stdio"; + autoStart?: boolean; +} +``` + +Variables de substitution contractuelles dans `command`, `args`, `env` et `cwd` : + +- `${pluginRoot}` : obligatoire v1. +- `${appDataDir}` : **obligatoire v1 si la spec continue de l'accepter**. +- `${projectRoot}` : interdit v1. + +Arbitrage QA sur `${appDataDir}` non implémenté : + +- Pas bloquant pour F4 layout. +- **Bloquant pour clôture B4/#43** si le manifeste continue de documenter `${appDataDir}` comme disponible. Un contrat annoncé mais non substitué crée des specs MCP fausses. +- Deux sorties acceptables, par ordre de préférence : + 1. DevBackend implémente la substitution `${appDataDir}` dans les specs MCP plugin et ajoute tests args/env/cwd. + 2. Si le coût est refusé pour v1, retirer `${appDataDir}` du contrat manifeste et du validator, puis documenter explicitement `${pluginRoot}` comme seule variable v1. + +Décision architecture par défaut : garder `${appDataDir}` et le faire implémenter par DevBackend, car le store global est déjà résolu côté backend et le coût est borné. + +## 6. Lots et responsabilités actualisés + +### F4 — convergence layout custom plugin + +Owner : DevFrontend. + +Livrables : + +- Union TS `LayoutNode` alignée sur Rust avec `customPluginLayout` top-level. +- Renderer/fallback branchés sur cette variante. +- Suppression du contrat `LeafCell.pluginLayout`. +- Test frontend à partir d'un JSON Rust réel. + +Backend F4 : pas de refonte attendue ; seulement vérifier/maintenir les tests serde existants. + +### F3 — `when.gitRepository` + +Owner : DevFrontend. + +Livrable : `gitRepository` ne doit plus être figé à `false` pour un projet Git réel avant clôture #43/F3. + +### B4 — `${appDataDir}` MCP + +Owner : DevBackend. + +Livrable : substitution `${appDataDir}` dans `command`, `args`, `env`, `cwd` des specs MCP plugin, ou retrait explicite du contrat si arbitré à la baisse. Par défaut : implémenter. + +### QA — non-régression à ajouter + +- Layout JSON backend `customPluginLayout` top-level rendu côté frontend. +- Fallback atteint si plugin missing/disabled avec nœud top-level. +- Aucun fallback basé sur `leaf.node.pluginLayout` ne compte comme validation du contrat. +- MCP spec avec `${pluginRoot}` et `${appDataDir}`. +- Menu item `when: "projectOpen && gitRepository"` actif dans un projet Git. + +## 7. Non-objectifs maintenus v1 + +- Pas de sandbox UI. +- Pas de hot-unload mémoire garanti dans la même session. +- Pas de marketplace distant. +- Pas de compilation TS/TSX. +- Pas de `${projectRoot}` pour serveurs MCP plugin globaux. +- Pas de garantie v1 sur `agentSelected`, `terminalFocused`, `layoutCellFocused` tant qu'un lot focus/selection n'a pas été cadré. \ No newline at end of file diff --git a/.ideai/tickets/43/issue.md b/.ideai/tickets/43/issue.md new file mode 100644 index 0000000..f91f8c8 --- /dev/null +++ b/.ideai/tickets/43/issue.md @@ -0,0 +1,16 @@ +--- +id: "63e80352-8ce1-4116-9ec3-b4cb3fc5c077" +number: 43 +title: "Systeme de plugins" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783879315858 +updatedAt: 1784707900405 +version: 6 +--- +Système de plugins/extensions pour IdeA (à la VSCode/Visual Studio) : permettre d'ajouter des menus (dropdown façon Panneaux/Paramètres), des items dans des menus existants, des types de layout custom (à la GitGraph), et des tools MCP. \ No newline at end of file diff --git a/.ideai/tickets/44/carnet.md b/.ideai/tickets/44/carnet.md new file mode 100644 index 0000000..6dd4f0f --- /dev/null +++ b/.ideai/tickets/44/carnet.md @@ -0,0 +1,25 @@ +--- +issueRef: "#44" +version: 8 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783976073932 +--- +## Résolution (2026-07-13) + +Le first-run wizard tourne désormais en deux **modes explicites** passés en argument (jamais inférés de `isFirstRun`) : +- `firstRun` : catalogue de référence seul, tout désélectionné, availability inconnue (détection auto pré-coche les CLI installés). +- `edit` (rouvert via *Settings ▸ Configure profiles*, `forceOpen`) : les profils déjà configurés reviennent **pré-sélectionnés avec leur vraie config**, en tête ; les profils de référence dont l'`id` est déjà configuré sont **dédupliqués par `profile.id` uniquement** (plusieurs OpenCode locaux partagent command/adapter mais diffèrent par endpoint/model). La détection auto ne touche PAS la sélection initiale (`preselect:false`). + +Logique pure isolée dans `wizardEntries.ts` (`buildWizardEntries`) pour tester les invariants unitairement. + +### Fichiers +- `frontend/src/features/first-run/wizardEntries.ts` (+ `.test.ts`) — nouveaux +- `useFirstRun.ts` — `mode` param, charge `listProfiles()` en edit via `Promise.all` +- `FirstRunWizard.tsx` — `mode = forceOpen ? "edit" : "firstRun"` +- `FirstRunWizard.test.tsx` — étendu + +### QA — vert +`npx vitest run wizardEntries.test.ts FirstRunWizard.test.tsx` → 37 passed ; `npx tsc --noEmit` → exit 0. + +### Git +Commit `8cdc44d` sur `feature/ticket44-firstrun-keep-profile-info` (base develop), mergé `--no-ff` vers `develop` (`c67ec4f`). Aucune action sortante. \ No newline at end of file diff --git a/.ideai/tickets/44/issue.md b/.ideai/tickets/44/issue.md new file mode 100644 index 0000000..6d24712 --- /dev/null +++ b/.ideai/tickets/44/issue.md @@ -0,0 +1,16 @@ +--- +id: "d7c17017-f6fb-4e55-8119-8fbc42641f48" +number: 44 +title: "Garder les infos du profils courant sur l'affichage d'edition des profiles" +status: "closed" +priority: "medium" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783879634794 +updatedAt: 1783976073932 +version: 8 +--- +Si je vais dans l'edition des profiles, le pré-remplissage des profiles ne correspond pas aux profiles existants. C'est a dire que par exemple pour le profiles OpenCode, j'aimerais que si j'en ai déjà un de créé, celui (ou ceux d'affichés) correspondent à celui ou ceux qui existent déjà. \ No newline at end of file diff --git a/.ideai/tickets/45/carnet.md b/.ideai/tickets/45/carnet.md new file mode 100644 index 0000000..2126ba8 --- /dev/null +++ b/.ideai/tickets/45/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#45" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783925599109 +--- diff --git a/.ideai/tickets/45/issue.md b/.ideai/tickets/45/issue.md new file mode 100644 index 0000000..4098766 --- /dev/null +++ b/.ideai/tickets/45/issue.md @@ -0,0 +1,16 @@ +--- +id: "b4e30445-b972-400a-879f-193c9c7aa10e" +number: 45 +title: "[Bug] Erreur OpenCode au lancement IdeA" +status: "closed" +priority: "medium" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783920311014 +updatedAt: 1783925599109 +version: 6 +--- +Quand je lance IdeA avec un layout qui contient opencode, j'ai un beandeau d'erreur dans la cellule opencode qui dit : Échec du lancement de l'agent : model server error (port_occupied): port occupied: 8080. Par contre si je relance une cellule une fois IdeA lancé, ça fonctionne et la cellule s'affiche correctement. \ No newline at end of file diff --git a/.ideai/tickets/46/carnet.md b/.ideai/tickets/46/carnet.md new file mode 100644 index 0000000..61c93ad --- /dev/null +++ b/.ideai/tickets/46/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#46" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783939380889 +--- diff --git a/.ideai/tickets/46/issue.md b/.ideai/tickets/46/issue.md new file mode 100644 index 0000000..010e469 --- /dev/null +++ b/.ideai/tickets/46/issue.md @@ -0,0 +1,16 @@ +--- +id: "1fe837f0-bc97-4b8e-945e-940fdd3c0249" +number: 46 +title: "[Bug] Plus de boutons pour ajouter un projet en onglet" +status: "closed" +priority: "low" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783923416403 +updatedAt: 1783939380889 +version: 6 +--- +Il manque un bouton + dans la barre des onglets des projets pour pouvoir ajouter un projet en onglet \ No newline at end of file diff --git a/.ideai/tickets/47/carnet.md b/.ideai/tickets/47/carnet.md new file mode 100644 index 0000000..a472dfb --- /dev/null +++ b/.ideai/tickets/47/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#47" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783939089770 +--- diff --git a/.ideai/tickets/47/issue.md b/.ideai/tickets/47/issue.md new file mode 100644 index 0000000..90be9bb --- /dev/null +++ b/.ideai/tickets/47/issue.md @@ -0,0 +1,16 @@ +--- +id: "0591d2d3-b03e-41bc-8c6b-349fc4ddf1a5" +number: 47 +title: "[Bug] Réouverture de fenêtres non lié au projet" +status: "closed" +priority: "medium" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783923465694 +updatedAt: 1783939089770 +version: 6 +--- +Si je suis sur un projet A, et que j'ouvre par exemple le panneau des agents. Si je ferme IdeA et que je réouvre IdeA, comme convenui j'ai ma fenêtre des agents qui s'affiche. Mais il m'affiche directement les agents de mon projet A, alors que je n'ai encore ouvert aucun projet. J'aimerais alors qu'il ne m'affiche rien avec juste un message qui me demande d'ouvrir un projet pour qu'il puisse afficher ce qu'il faut dedans et que ça soit toujours lié au projet en focus \ No newline at end of file diff --git a/.ideai/tickets/48/carnet.md b/.ideai/tickets/48/carnet.md new file mode 100644 index 0000000..f7b32fe --- /dev/null +++ b/.ideai/tickets/48/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#48" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783924961311 +--- diff --git a/.ideai/tickets/48/issue.md b/.ideai/tickets/48/issue.md new file mode 100644 index 0000000..052085c --- /dev/null +++ b/.ideai/tickets/48/issue.md @@ -0,0 +1,16 @@ +--- +id: "0e449e11-2af9-47ca-83da-03568d9af100" +number: 48 +title: "[Bug] Bandeau d'affichage d'erreur au dessus des boutons de gestion de cellule" +status: "closed" +priority: "medium" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783923674564 +updatedAt: 1783924961311 +version: 6 +--- +Les bandeaux d'affichage d'erreur de cellule passent par dessus les boutons de changement d'agent, de split de cellule ou de fermeture de cellule ce qui rend la gestion de la cellule impossible s'il y a une erreur. Il faudrait que les boutons soient par dessus le bandeau d'erreur pour être accessibles \ No newline at end of file diff --git a/.ideai/tickets/49/carnet.md b/.ideai/tickets/49/carnet.md new file mode 100644 index 0000000..f70a75d --- /dev/null +++ b/.ideai/tickets/49/carnet.md @@ -0,0 +1,20 @@ +--- +issueRef: "#49" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783940643502 +--- +## Nettoyage mémoire — fait (Main, 2026-07-13) + +Store passé de ~83 notes à **52 notes**, index `MEMORY.md` remis en parité stricte (52 entrées = 52 fichiers, 0 lien cassé, 0 orphelin). + +**Supprimé (31, récupérable via git)** — notes transitoires / supersédées : +- 20 `checkpoint-*` (snapshots de work-state de sprints livrés). +- Campagne de tests MCP/backstop clôturée (programme live-state fermé, cf. `live-state-persistence-program-closed-ls8`) : `mcp-functional-test-plan-2026-06-24`, `mcp-functional-tests-t1-t9-green-live-2026-06-24`, `mcp-t7-backstop-...`, `mcp-t10a-...`, `mcp-t10b-...`, `mcp-e2e-findings-...`, `backstop-no-reply-live-test-failed-...`, `backstop-fires-on-intra-task-turn-rootcause`, `reconcile-live-state-implemented-feature-branch`, `resume-after-appimage-rebuild-mcp-e2e`. +- `f35-launch-status-done-config-crud-blocked` (supersédé par `f35-config-crud-delivered`). + +**Conservé** : toutes les notes de design/contrat/référence durables + gotchas (build, focus-trap, sandbox, ports/contrats). + +**Restauré à l'index** (fichiers présents mais index dérivé/désync) : `develop-realigned-to-cli-ui-baseline-2026-07-02`, `feature-agent-skill-awareness-design`, `inter-agent-announcements-feature-and-codex-final-bug`, `inter-agent-live-context-shared-per-agent`, `ticket4-announcements-frontend-f1f2f3`. + +Commit du diff `.ideai/memory/` à confier à l'agent Git. \ No newline at end of file diff --git a/.ideai/tickets/49/issue.md b/.ideai/tickets/49/issue.md new file mode 100644 index 0000000..19c4861 --- /dev/null +++ b/.ideai/tickets/49/issue.md @@ -0,0 +1,16 @@ +--- +id: "a6552142-1c48-49b0-85a7-56b6ebaaacff" +number: 49 +title: "[Misc] clean les memory qui ne sont plus valable ou qui n'ont plus de raisons d'être" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783935372750 +updatedAt: 1783940643502 +version: 5 +--- +Il y a beaucoup de memories dans la liste Memory. J'iamerais qu'un nettoyage soit fait au cas ou certaines ne soient plus d'actualité. \ No newline at end of file diff --git a/.ideai/tickets/5/carnet.md b/.ideai/tickets/5/carnet.md new file mode 100644 index 0000000..0af8e95 --- /dev/null +++ b/.ideai/tickets/5/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#5" +version: 4 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783328740886 +--- diff --git a/.ideai/tickets/5/issue.md b/.ideai/tickets/5/issue.md new file mode 100644 index 0000000..d335f73 --- /dev/null +++ b/.ideai/tickets/5/issue.md @@ -0,0 +1,22 @@ +--- +id: "bd0f52c4-480f-4cf8-9c4c-05d05fb9e102" +number: 5 +title: "Tâches de fond — dette de contrat DTO work-state : summary non affiché + owner/project inutiles + tri" +status: "closed" +priority: "low" +links: [{"target":"#1","kind":"relatesTo"}] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783115680555 +updatedAt: 1783328740886 +version: 4 +--- +Dette de contrat mineure détectée lors du fix T3 du ticket #1 (projection background_tasks dans le read-model work-state, branche feature/background-tasks-first-class). Non bloquant : Cancel/Retry fonctionnent en live. + +Points à traiter : + +- [ ] `summary` : le DTO backend `AgentBackgroundTaskStateDto` émet un `summary` (résumé/erreur/raison du résultat terminal) que le type frontend `BackgroundCompletion` ne porte pas → info perdue, non affichée dans le panneau. Décider si on l'affiche (utile pour comprendre un échec/annulation d'un coup d'œil) et brancher. +- [ ] `ownerAgentId`/`projectId` absents du payload par-agent : inoffensif (les tâches sont imbriquées sous leur agent, fallback ownerAgentId = agentId), mais le chemin de merge top-level dans `normalizeProjectWorkState` reste du code défensif inutilisé pour ce contrat. À nettoyer ou documenter comme volontaire. + +Contexte : mémoire projet `workstate-background-tasks-projection-fix` (décision d'archi) et `tickets-t3-frontend-validation-verdict` (verdict FE + écarts). Le point tri sur finishedAtMs a déjà été corrigé (bascule sur updatedAtMs) et n'est PAS dans cette dette. \ No newline at end of file diff --git a/.ideai/tickets/50/carnet.md b/.ideai/tickets/50/carnet.md new file mode 100644 index 0000000..598224b --- /dev/null +++ b/.ideai/tickets/50/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#50" +version: 7 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783959345112 +--- diff --git a/.ideai/tickets/50/issue.md b/.ideai/tickets/50/issue.md new file mode 100644 index 0000000..6e580af --- /dev/null +++ b/.ideai/tickets/50/issue.md @@ -0,0 +1,16 @@ +--- +id: "9975f15a-2140-4c0b-8c15-2672a13d8af4" +number: 50 +title: "[UI] Erreur d'affichage sur les fenetre ouvertes a l'ouverture d'IdeA" +status: "closed" +priority: "low" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783940995092 +updatedAt: 1783959345112 +version: 7 +--- +Si une fenetre s'ouvre avec l'ouverture d'IdeA, dans les menus, la fenetre est affichées comme non ouverte, la pastille bleu est sur "Fermé" \ No newline at end of file diff --git a/.ideai/tickets/51/carnet.md b/.ideai/tickets/51/carnet.md new file mode 100644 index 0000000..f8dd171 --- /dev/null +++ b/.ideai/tickets/51/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#51" +version: 1 +updatedBy: {"kind":"user"} +updatedAt: 1783941336721 +--- diff --git a/.ideai/tickets/51/issue.md b/.ideai/tickets/51/issue.md new file mode 100644 index 0000000..0e8426f --- /dev/null +++ b/.ideai/tickets/51/issue.md @@ -0,0 +1,15 @@ +--- +id: "6b74084c-0323-4f87-83f0-d9f944c364dc" +number: 51 +title: "Clean des session headless" +status: "open" +priority: "medium" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1783941336721 +updatedAt: 1783941336721 +version: 1 +--- diff --git a/.ideai/tickets/52/carnet.md b/.ideai/tickets/52/carnet.md new file mode 100644 index 0000000..396b59c --- /dev/null +++ b/.ideai/tickets/52/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#52" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783958689416 +--- diff --git a/.ideai/tickets/52/issue.md b/.ideai/tickets/52/issue.md new file mode 100644 index 0000000..d85ddce --- /dev/null +++ b/.ideai/tickets/52/issue.md @@ -0,0 +1,16 @@ +--- +id: "fe674f4b-bc3e-4fb3-8849-edd8f9450a4c" +number: 52 +title: "[UI] mettre le bouton plus pour ajouter un projet juste a droite des onglet déjà ouvert comme pour les layouts" +status: "closed" +priority: "low" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783942134638 +updatedAt: 1783958689416 +version: 6 +--- +J'iamerais que le + qui est actuellement tout a droite de la fenetre pour ajouter un onglet projet, soit juste à droite des onglets déjà ouvert comme pour les layouts \ No newline at end of file diff --git a/.ideai/tickets/54/carnet.md b/.ideai/tickets/54/carnet.md new file mode 100644 index 0000000..6400ef3 --- /dev/null +++ b/.ideai/tickets/54/carnet.md @@ -0,0 +1,27 @@ +--- +issueRef: "#54" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784018300921 +--- +## Cadrage Architect (2026-07-13) + +**Seam actuel du timeout** : `EnsureLocalModelServer` dans `crates/application/src/model_server.rs` — boucle de readiness HTTP `ReadinessPolicy` par défaut `20 × 250ms` (~5s), puis `Failed{code:"timeout"}` + kill du process (model_server.rs:336/359). Démarrage déclenché depuis `lifecycle.rs:1665` (`ensure_local_model_server_for_opencode`). Adapter concret `LocalManagedProcess` (`crates/infrastructure/src/model_server/mod.rs`) : spawn sans capture stdout/stderr ; `LlamaCppRuntime` construit `-hf ` ou `--model ` (mod.rs:101). Le probe HTTP `/v1/models` ne distingue que prêt/pas-joignable — pas de source « téléchargement » fiable aujourd'hui. + +**Frontend** : event `modelServerStatusChanged` existe déjà (`domain/index.ts`), replié par `serverId` dans `useAgents.ts:313`, affiché surtout dans AgentsPanel — pas en overlay cellule. Overlay à brancher dans `LeafView`/`LayoutGrid.tsx:883` (même niveau que les overlays plein-cellule existants), en corrélant `agentId → profileId → opencode.localModelServerId → statut serveur`. + +**Contrat DTO (réutilise le stream existant, pas de nouveau)** — étendre `ModelServerLifecycleStatus` : +``` +Downloading { downloaded_bytes: Option, total_bytes: Option, percent: Option, source: Option } +Ready { reused: bool } +Failed { code, message } +``` +Miroir TS : `{ state: "downloading"; downloadedBytes?; totalBytes?; percent?; source? } | ...` + +### Découpage +- **B1 (backend MVP)** : étendre le DTO status ; pour source HF auto-start, ne PAS traiter 5s de process vivant comme erreur → publier `Downloading`/`Preparing`, continuer à sonder tant que le process vit, échouer seulement si exit/probe terminal/port collision/deadline longue configurable. +- **F1 (frontend MVP)** : étendre `ModelServerStatus` TS ; overlay plein-cellule dans LeafView pour cellules dont l'agent référence le serveur en `downloading/starting/probing` (« Téléchargement du modèle… » / « Chargement du serveur… »). +- **B2 (stretch)** : progression fine réelle — court terme PTY + parse progress llama.cpp ; long terme port `ModelArtifactDownloader` (pré-DL HF avec callbacks, puis spawn `--model `). +- **F2 (stretch)** : barre/%, bytes, source. + +**Ordre** : B1 → F1 → QA (MVP). Stretch B2/F2 séparable et décidé après. diff --git a/.ideai/tickets/54/issue.md b/.ideai/tickets/54/issue.md new file mode 100644 index 0000000..0f74ec0 --- /dev/null +++ b/.ideai/tickets/54/issue.md @@ -0,0 +1,16 @@ +--- +id: "18641885-1b38-43c1-a81b-42f4c407690d" +number: 54 +title: "[UI] Ajouter le handle du téléchargement des modeles lors du démarage llamacpp" +status: "closed" +priority: "medium" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783965036142 +updatedAt: 1784018300921 +version: 6 +--- +Lorsqu'un serveur llamacpp se démarre, dans le cas ou le modele se télécharge, il faudrait que le téléchargement soit affiché au lieu d'afficher un timeout, avec en idéal l'affichage de la préogression du téléchargement en supperposition de la (ou les cellules) qui cherche à afficher le modèle \ No newline at end of file diff --git a/.ideai/tickets/55/carnet.md b/.ideai/tickets/55/carnet.md new file mode 100644 index 0000000..de09b72 --- /dev/null +++ b/.ideai/tickets/55/carnet.md @@ -0,0 +1,28 @@ +--- +issueRef: "#55" +version: 12 +updatedBy: {"kind":"user"} +updatedAt: 1784649948611 +--- +## Réouverture (2026-07-15) — le fix 141c13d ne suffit pas + +Le fix #55 (commit 141c13d) a supprimé le timeout **prématuré** (~5 s) en distinguant process vivant-en-warmup / mort, avec `ReadinessPolicy::warmup_deadline = 120 s`. Mais l'utilisateur reproduit toujours « model server error (timeout): readiness timed out » **au lancement d'IdeA**, et ça finit par marcher après plusieurs essais. + +### Cause racine +`crates/application/src/model_server.rs` → `wait_for_started_server` (~l.437) : tant que process `Running` + endpoint `Unreachable`, poll jusqu'à `deadline = now + warmup_deadline` (120 s), puis `stop_started_server` + `ModelServerError::Timeout`. + +- **1er lancement à froid** : page cache OS froid, GGUF multi-Go, chargement RAM/VRAM, I/O disque en concurrence avec le reste du boot IdeA ⇒ warmup > 120 s ⇒ timeout + kill. +- **Essais suivants** : fichier en page cache chaud ⇒ chargement < 120 s ⇒ succès. Explique le « au bout de plusieurs essais ça finit ». + +### Fix Lot 1 backend (livré, commit f7cae4c sur feature/ticket55-model-warmup-deadline) +Cadré par Architect (mémoire `architecture-local-model-readiness-timeout`), approche A configurable : +- Défaut `ReadinessPolicy::warmup_deadline` : 120 s → **600 s** (`crates/application/src/model_server.rs`). +- Nouveau `LocalModelServerConfig::warmup_deadline_secs: Option`, rétrocompatible, validation stricte `[30, 1800]` (`crates/domain/src/model_server.rs`). +- Policy effective dérivée **par config serveur** (deadline par-serveur), `probe`/`backoff` gardés séparés. DTO IPC `warmupDeadlineSecs` (`app-tauri/dto.rs`). +- Contrat readiness inchangé : Exited⇒échec rapide ; Running+HTTP OK⇒prêt ; Running+HTTP KO⇒continuer jusqu'à deadline ; Running au-delà⇒stop+Timeout. + +### QA — VERT (ports mockés) +`cargo test -p domain` (252), `-p application` (81 + 22 model_server ciblés), `-p app-tauri --test dto_model_servers` (6), `-p infrastructure model_server` (2), build OK. Tests clés : slow_local_warmup...succeeds_without_stop, warmup_deadline_reached_returns_timeout_and_stops, process_exit_during_warmup_fails_fast, effective_policy_uses_configured_deadline, default_is_ten_minutes, hors-bornes⇒INVALID. + +### RESTE À FAIRE — vérification manuelle utilisateur (bloquant merge) +Les tests prouvent le **mécanisme**, PAS que 600 s guérit le vrai cold-start `llama-server` (bind réel hors sandbox). Git a committé mais **retient le merge sur develop** jusqu'à confirmation « cold-start OK » de l'utilisateur (vrai lancement à froid, après reboot / cache purgé). develop non divergé (ce5aa28) ⇒ merge --no-ff trivial au feu vert. \ No newline at end of file diff --git a/.ideai/tickets/55/issue.md b/.ideai/tickets/55/issue.md new file mode 100644 index 0000000..07dce12 --- /dev/null +++ b/.ideai/tickets/55/issue.md @@ -0,0 +1,17 @@ +--- +id: "9ab1eb21-d6dd-435a-b088-377f0ec7e04f" +number: 55 +title: "[Bug] Error on loading local model" +status: "closed" +priority: "high" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1784045935720 +updatedAt: 1784649948611 +version: 12 +--- +Quand jke cherche a lancer un modele local sur une cellule, il commence apr charger le serveur, ce qui est bon mais au bout de quelques seconde, le chargement du serveur disparait et j'ai cette erreur qui s'affiche en bandeau rouge: Échec du lancement de l'agent : model server error (timeout): readiness timed out. +Si j'insiste assez en changeant d'agent et en remettant l'agent, au bourt d'un moment ça fini par fonctionner \ No newline at end of file diff --git a/.ideai/tickets/56/carnet.md b/.ideai/tickets/56/carnet.md new file mode 100644 index 0000000..2e016fe --- /dev/null +++ b/.ideai/tickets/56/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#56" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784048212123 +--- diff --git a/.ideai/tickets/56/issue.md b/.ideai/tickets/56/issue.md new file mode 100644 index 0000000..b7511a2 --- /dev/null +++ b/.ideai/tickets/56/issue.md @@ -0,0 +1,16 @@ +--- +id: "12943bca-4c29-46ec-8cad-936c30a95d5e" +number: 56 +title: "[Bug] selection desactivée d'agent non affichés" +status: "closed" +priority: "low" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784046090493 +updatedAt: 1784048212123 +version: 6 +--- +Si j'affiche un agent dans une cellule, que dans cette même cellule je change d'agent et que dans une autre cellule j'essaie d'afficher le premier agent, la selection d el'agent est désactivée comme si cet agent était déjà affiché dans une cellule alors que ce n'est pas le cas \ No newline at end of file diff --git a/.ideai/tickets/58/carnet.md b/.ideai/tickets/58/carnet.md new file mode 100644 index 0000000..7e0fa2e --- /dev/null +++ b/.ideai/tickets/58/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#58" +version: 1 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784064521992 +--- diff --git a/.ideai/tickets/58/issue.md b/.ideai/tickets/58/issue.md new file mode 100644 index 0000000..110d1b0 --- /dev/null +++ b/.ideai/tickets/58/issue.md @@ -0,0 +1,26 @@ +--- +id: "495699aa-d5d1-409f-8830-cdfaba7a4be3" +number: 58 +title: "[B/F] Rendu live des tâches de fond — subscriber UI + canal IPC attachable au task output" +status: "open" +priority: "low" +sprint: null +links: [{"target":"#2","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784064521992 +updatedAt: 1784064521992 +version: 1 +--- +Sorti du ticket #2 lors de sa requalification (2026-07-14). + +Le hub broadcast PTY (crates/infrastructure/src/pty/mod.rs) rend désormais possible un tee de la sortie d'une tâche de fond vers l'UI sans voler le flux au runner. Une fois #2 livré (découplage fin-de-process via PtyPort::wait, l'ajout d'un subscriber UI ne casse plus la détection de fin), brancher le rendu live. + +Attendu : +- Backend : créer un subscriber `pty.subscribe_output(&handle)` pour la tâche de fond ; canal dédié par task vers le frontend (idéalement Tauri `Channel`, comme les terminaux) ; commande type `attach_background_task_output(taskId, channel)` (ou intégration au panneau workstate). +- Frontend : composant/UX affichant le flux live, gérant attach/detach, repaint via `scrollback` au (ré)attachement, état terminal. + +QA : deux subscribers reçoivent les mêmes chunks ; le runner complète même pendant qu'un subscriber UI reste attaché ; reattach UI récupère le scrollback puis reçoit les nouveaux chunks. + +Dépend de #2 (PtyPort::wait/try_wait + découplage runner). \ No newline at end of file diff --git a/.ideai/tickets/6/carnet.md b/.ideai/tickets/6/carnet.md new file mode 100644 index 0000000..dcb10e0 --- /dev/null +++ b/.ideai/tickets/6/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#6" +version: 15 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783312054217 +--- diff --git a/.ideai/tickets/6/issue.md b/.ideai/tickets/6/issue.md new file mode 100644 index 0000000..e340c45 --- /dev/null +++ b/.ideai/tickets/6/issue.md @@ -0,0 +1,15 @@ +--- +id: "b9ec818e-918e-489a-983c-04c62757bf53" +number: 6 +title: "Suppression d'un ticket" +status: "closed" +priority: "low" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783166803287 +updatedAt: 1783312054217 +version: 15 +--- +Un ticket doit pouvoir être supprimé \ No newline at end of file diff --git a/.ideai/tickets/60/carnet.md b/.ideai/tickets/60/carnet.md new file mode 100644 index 0000000..3603a54 --- /dev/null +++ b/.ideai/tickets/60/carnet.md @@ -0,0 +1,21 @@ +--- +issueRef: "#60" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784360139966 +--- +## Réconciliation (2026-07-18) — déjà livré et mergé, statut désynchronisé + +Le fix Lot A (backend + frontend) a en réalité été implémenté et mergé sur `develop` **avant** cette relance de sprint : commit `2f7f111` (`fix(ticket-assistant): rendre visible l'échec « aucune réponse » de l'assistant de ticket (#60)`), fusionné via `ebad5ff` (`Merge feature/ticket60-ticket-assistant-no-reply into develop (#60)`). `ebad5ff` est ancêtre de `develop` HEAD actuel (`17d6baf`) — plusieurs sprints livrés depuis sans régression signalée. + +Contenu du fix (conforme au périmètre cadré) : +- domain/ports : variant `ReplyEvent::Error { message }`. +- infra/session : parse `reasoning_content` en fallback quand `content` est vide. +- app-tauri : mapping `ReplyEvent::Error → ReplyChunk::Error`, Final vide converti en Error visible. +- frontend : `ReplyChunk` type `error`, rendu dans `useTicketAssistant` (+ test dédié), `busy` ne retombe plus prématurément. + +Reconfirmé le 2026-07-18 par DevFrontend sur l'arbre de travail courant : `npx vitest run src/features/tickets/useTicketAssistant.test.tsx` → 3/3 vert, y compris le cas error-chunk. + +Le ticket était resté `inProgress` avec un carnet vide par désynchronisation d'état (le commit d'origine portait la note « #60 reste inProgress » avant la vérification hors-sandbox qui a permis le merge ensuite). Clôture sur constat de fait, pas de nouveau travail nécessaire. + +Branche orpheline `feature/ticket60-assistant-mute-reply` créée par erreur lors de cette relance (rien à committer, identique à `develop`) — à supprimer par Git, non utilisée. \ No newline at end of file diff --git a/.ideai/tickets/60/issue.md b/.ideai/tickets/60/issue.md new file mode 100644 index 0000000..447531c --- /dev/null +++ b/.ideai/tickets/60/issue.md @@ -0,0 +1,35 @@ +--- +id: "623b1719-c71d-448e-a0ef-3c7d5823b8a3" +number: 60 +title: "[Bug] Assistant IA d'édition de ticket : aucune réponse dans la conversation (stream sans Final non signalé)" +status: "closed" +priority: "high" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [{"target":"#25","kind":"relatesTo"},{"target":"#27","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784094260499 +updatedAt: 1784360139966 +version: 5 +--- +Rapporté par l'utilisateur (2026-07-15) : dans l'édition d'un ticket, le bouton « Assistant IA » ouvre une conversation, mais l'envoi d'un message ne produit JAMAIS de réponse visible — la conversation reste muette. PROFIL UTILISÉ : modèle local OpenAI-compatible (llamacpp). + +CAUSE (Architect) : +1. CONFIRMÉ code — silence sur stream sans Final : agent_send draine le flux et détache le canal SANS rien émettre si aucun ReplyChunk::Final n'est produit (commands.rs:1661-1691). Le frontend ne termine que sur `final` (useTicketAssistant.ts:138-144). + finally setBusy(false) prématuré (useTicketAssistant.ts:147-151). +2. PROBABLE runtime (local OpenAI-compatible) — Final VIDE : le chemin HTTP émet bien un Final quand le serveur renvoie choices[0].message.content ou SSE delta.content (openai_compat.rs:348-397/517-538/286-292), mais llama.cpp peut renvoyer un flux sans delta.content utile (reasoning_content / tool_calls / contenu vide) → ReplyEvent::Final { content: "" } → le frontend remplace le tour par une chaîne vide (useTicketAssistant.ts:138-140) = « muet ». + +PÉRIMÈTRE DE CE TICKET (#60) — Lot A robustesse + gestion Final vide, B+F, S/M : +- Backend : étendre ReplyChunk avec Error { message } (crates/app-tauri/src/dto.rs). agent_send : si le stream finit sans Final → émettre ReplyChunk::Error avant detach_if. Traiter Final { content: "" } comme terminal visible (Error « réponse vide du modèle » ou fallback explicite). Si l'adapter dispose d'un reasoning_content non vide, envisager de le surfacer plutôt que de le jeter (à confirmer selon payload). +- Frontend : ReplyChunk TS + useTicketAssistant gère `error`, termine le pending, affiche le message, busy tombe sur final OU error, suppression du finally setBusy(false) prématuré. + +Effet attendu : l'assistant ne reste JAMAIS muet — soit une réponse, soit une erreur/vide explicite visible. + +QA : +- Backend agent_send : fake AgentSession renvoie Heartbeat puis EOF sans Final → le channel reçoit ReplyChunk::Error. +- Backend OpenAI-compatible : fake HTTP renvoie SSE [DONE] sans delta.content → terminal visible (erreur/non vide), jamais un tour muet. +- Frontend : sendTicketChat reçoit `error` → tour non pending, busy=false, message visible. + +HORS PÉRIMÈTRE (→ ticket de suivi) : identité requester explicite (port AgentSessionFactory::start, factory.rs:153-157 dérive le requester du cwd → devient le n° de ticket au lieu de ticket-assistant::) + branchement de ToolPolicyRegistry sur l'invoker OpenAI-compatible (openai_tools.rs:92-145) + éventuel local_model_server_id sur HttpChatConfig. Concerne un port figé + la sécurité des tool calls. + +Relations : #25, #27 (mêmes surfaces, déjà fermées). \ No newline at end of file diff --git a/.ideai/tickets/61/carnet.md b/.ideai/tickets/61/carnet.md new file mode 100644 index 0000000..e9e523c --- /dev/null +++ b/.ideai/tickets/61/carnet.md @@ -0,0 +1,19 @@ +--- +issueRef: "#61" +version: 11 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784661823324 +--- +Un premier fix (commit 445ecaf, "fix(frontend): refit différé des cellules terminal après mutation de layout (#61)", mergé via a69c5db) a couvert le cas desktop LayoutGrid/TerminalView (split/merge déclenchant un refit explicite via requestAnimationFrame). + +## Réouverture (2026-07-21) + +L'utilisateur signale que le problème persiste : +- Ouverture d'une nouvelle cellule → la CLI déjà affichée ne se redimensionne pas correctement tant qu'un resize manuel n'est pas déclenché. +- Reproduit aussi sur la version **Web** : sur PC, un redimensionnement manuel de la fenêtre du navigateur corrige le scaling. Sur mobile, impossible de redimensionner la fenêtre → le bug reste bloquant, pas de contournement possible. + +Hypothèse de travail : le fix précédent (445ecaf) est probablement scopé à `LayoutGrid`/`useLayout` (desktop, split/merge d'un arbre de layout). Le workspace web n'utilise pas ce chemin — il a sa propre surface (`WebAgentCell`/`LiveProjectPanel`, cf. déviation notée par DevFrontend sur le ticket #90) qui ouvre/remplace des cellules terminal autrement. Le signal de refit explicite ajouté pour split/merge ne se déclenche donc probablement jamais côté web, et `TerminalView` retombe uniquement sur son `ResizeObserver` local — insuffisant selon le timing de montage/redimensionnement de la surface web. + +À vérifier par Architect/DevFrontend : le chemin exact d'ouverture/fermeture de cellule côté web (WebAgentCell, WebWorkspace, WebMenuSheet le cas échéant après #90), et si le mécanisme de refit du fix 445ecaf peut être réutilisé/étendu à ce chemin plutôt que dupliqué. + +Périmètre : frontend pur, desktop non régressé, web (PC et mobile) doit refit sans action manuelle de l'utilisateur. \ No newline at end of file diff --git a/.ideai/tickets/61/issue.md b/.ideai/tickets/61/issue.md new file mode 100644 index 0000000..c28ca5c --- /dev/null +++ b/.ideai/tickets/61/issue.md @@ -0,0 +1,16 @@ +--- +id: "b0568a4e-4804-480c-ada4-bc9d6b7b40c6" +number: 61 +title: "[UI] rafraichissement des cellule" +status: "closed" +priority: "low" +sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784095163730 +updatedAt: 1784661823324 +version: 11 +--- +Lorsque j'ajoute une nouvelle cellule ou que j'en enlève une, je suis obligé de redimentionner un coup la fenêtre ou les cellule pour rafraichir le scaling des cli \ No newline at end of file diff --git a/.ideai/tickets/62/carnet.md b/.ideai/tickets/62/carnet.md new file mode 100644 index 0000000..4785955 --- /dev/null +++ b/.ideai/tickets/62/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#62" +version: 2 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784406936659 +--- diff --git a/.ideai/tickets/62/issue.md b/.ideai/tickets/62/issue.md new file mode 100644 index 0000000..76c6904 --- /dev/null +++ b/.ideai/tickets/62/issue.md @@ -0,0 +1,31 @@ +--- +id: "dd897d74-4e88-4d34-a6fb-d7e7f330094f" +number: 62 +title: "[Sécurité/Cohérence] Identité requester explicite pour les sessions structurées + policy des tools OpenAI-compatible" +status: "closed" +priority: "medium" +sprint: null +links: [{"target":"#60","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784095187734 +updatedAt: 1784406936659 +version: 2 +--- +Sorti de #60 (assistant IA de ticket muet) — volet distinct, touchant un port figé et la sécurité des tool calls. + +CONSTAT (Architect, cadrage #60) : +1. Le requester d'une session structurée est dérivé implicitement du nom du cwd/run dir (crates/infrastructure/src/session/factory.rs:153-157). Pour l'assistant de ticket, le requester devient donc le NUMÉRO DU TICKET, alors que la policy MCP est posée sur ticket-assistant:: (crates/application/src/ticket_assistant.rs:100-119). Mismatch → incohérence d'attribution et policy potentiellement non appliquée. +2. L'invoker de tools OpenAI-compatible (AppOpenAiToolInvoker, openai_tools.rs:92-145) dispatch directement SANS consulter ToolPolicyRegistry, alors que le serveur MCP stdio applique bien la policy (mcp/server.rs:349-355, :470-512). Les tool calls d'un profil OpenAI-compatible ne sont donc pas soumis à la policy. + +ATTENDU : +- Faire évoluer le port AgentSessionFactory::start pour recevoir une identité requester explicite (Option<&str> ou petit SessionIdentity) ; fallback sur cwd.file_name() seulement si absente. Impact : port figé → propagation aux impls/fakes. +- OpenTicketAssistant passe requester = ticket-assistant::. +- LaunchAgent passe l'agent id comme requester pour les cellules normales (au lieu de dépendre du nom du run dir). +- Brancher ToolPolicyRegistry sur l'invoker OpenAI-compatible (parité avec le serveur MCP stdio). +- Décider pour le modèle local managé : étendre HttpChatConfig avec local_model_server_id (appeler EnsureLocalModelServer avant OpenAiCompatibleSession::new) OU documenter que OpenAI-compatible = endpoint externe. + +Cadrage Architect requis (port figé + sécurité). QA : un assistant ticket OpenAI-compatible qui appelle idea_ticket_update reçoit __ideaRequester = ticket-assistant:: et la policy ticket est appliquée ; un tool refusé par la policy l'est aussi sur le chemin OpenAI-compatible. + +Dépend de / relié à #60. \ No newline at end of file diff --git a/.ideai/tickets/63/carnet.md b/.ideai/tickets/63/carnet.md new file mode 100644 index 0000000..eea29b6 --- /dev/null +++ b/.ideai/tickets/63/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#63" +version: 1 +updatedBy: {"kind":"user"} +updatedAt: 1784098438671 +--- diff --git a/.ideai/tickets/63/issue.md b/.ideai/tickets/63/issue.md new file mode 100644 index 0000000..e0b9508 --- /dev/null +++ b/.ideai/tickets/63/issue.md @@ -0,0 +1,15 @@ +--- +id: "b5753734-1474-48d5-af6c-4a41b78005dc" +number: 63 +title: "Systeme de test de l'UI" +status: "open" +priority: "medium" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1784098438671 +updatedAt: 1784098438671 +version: 1 +--- diff --git a/.ideai/tickets/64/carnet.md b/.ideai/tickets/64/carnet.md new file mode 100644 index 0000000..a0fb77e --- /dev/null +++ b/.ideai/tickets/64/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#64" +version: 6 +updatedBy: {"kind":"user"} +updatedAt: 1784718418070 +--- diff --git a/.ideai/tickets/64/issue.md b/.ideai/tickets/64/issue.md new file mode 100644 index 0000000..3f1a6ab --- /dev/null +++ b/.ideai/tickets/64/issue.md @@ -0,0 +1,31 @@ +--- +id: "c8f1c5b3-7674-4c6c-bba5-bb2c5faf201b" +number: 64 +title: "Web : créer/ajouter un projet depuis le navigateur (folder browser serveur)" +status: "closed" +priority: "medium" +sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" +links: [{"target":"#13","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1784183727404 +updatedAt: 1784718418070 +version: 6 +--- +Issu de la validation live de #13 (mode client/serveur web). En web, le bouton « Browse » (choisir un dossier de projet) est inopérant : `pickFolder()` renvoie `UNSUPPORTED_ON_WEB` car il s'appuie sur le dialogue natif OS de Tauri, absent du navigateur. Sélectionner un projet EXISTANT est déjà couvert par la liste (list_projects/open_project). Il manque CRÉER/AJOUTER un projet = choisir un dossier côté SERVEUR. + +Cadrage Architect (post-#13) : nouvelle surface backend exposant le filesystem serveur au navigateur, à sandboxer. + +Backend : +- Commande read `list_server_dir` (racine parcourable explicite via `--server-browse-root PATH` / `IDEA_SERVER_BROWSE_ROOT` ; défaut prudent : aucun browsing ou $HOME en loopback ; jamais `/` implicite). +- `create_project` sur l'allowlist WRITE, avec sa propre validation de racine (ne fait pas confiance au folder browser). +- Invariants sécurité (mêmes gardes que serve_static) : canonicalisation browse_root + cible, refus si hors racine, refus `..`/segments vides/backslash/`%2e`/`%2f`/`%5c`, dossiers seulement, pas de symlink escape, dotdirs masqués (`hidden:true`), réponse bornée + tri stable. + +DTO : +- ListServerDirRequest { path?: string } → ListServerDirResponse { root, current, parent|null, entries: [{name,path,kind:'directory',hidden}], canCreateProjectHere } +- CreateProjectRequest { name, root(absolu serveur validé) } + +Découpage : B1 list_server_dir + tests traversal/symlink/bounds ; B2 allowlist write create_project + validation racine ; F1 nouveau port UI `ServerFolderGateway` (ne pas surcharger `SystemGateway.pickFolder()`) ; F2 composant folder browser web (desktop garde le picker natif) ; F3 tests adapter HTTP + vue projet. + +Dépend de #13 (mode web) et du garde-fou lock inter-process app-data-dir (à créer) avant d'activer les écritures web. \ No newline at end of file diff --git a/.ideai/tickets/65/carnet.md b/.ideai/tickets/65/carnet.md new file mode 100644 index 0000000..afb79ab --- /dev/null +++ b/.ideai/tickets/65/carnet.md @@ -0,0 +1,46 @@ +--- +issueRef: "#65" +version: 7 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784210420516 +--- +# Ticket #65 — Serveur headless `idea-serve` (carnet de chantier) + +Statut : **QA — en attente de validation live utilisateur (non-régression desktop)**. +Branche : `feature/ticket65-idea-serve-headless` (base `develop` @ `506d589`). Local, aucune action sortante. + +## Arbitrages Architect (tranchés) +- **DTO/events → propriétaire canonique = crate `backend`.** PAS de crate `presentation-dto`/`backend-api` : `backend` est déjà le composition root transport-neutre et la façade des use cases pour adapters entrants. `web-server` ET `app-tauri` le consomment ⇒ contrat canonique partagé entre deux driving adapters. +- **Frontière** : les DTO dans `backend` ne tirent NI Tauri, NI HTTP/WS/cookies, NI Axum/Hyper, NI UI. `domain`/`application` ne dépendent PAS des DTO. Vérifié QA. +- **Dédup events = dans L1, pas en dette** (sinon deux sources de vérité au moment où #65 stabilise le contrat). Dette acceptable uniquement pour le Tauri-spécifique (relay `app_handle.emit`, wiring runtime). + +## Structure cible (livrée) +- `crates/backend/src/dto.rs` + `crates/backend/src/events.rs` ; `lib.rs` expose `pub mod dto; pub mod events;` +- `app-tauri/src/dto.rs` = shim `pub use backend::dto::*;` +- `app-tauri/src/events.rs` = relay Tauri uniquement, DTO importés de `backend::events`. +- `web-server` consomme `backend::{dto, events}`. + +## Livré — L1/L2/L3 + run_embedded +Commits : `82e8e77` (wip de sûreté, ne fige rien) · `ddbea7b` (dédup DTO events). +`web_server::run_embedded(...)` présent : `ServerConfig`, `EmbeddedServerHandle`, `url()`, `pairing_code()`, `state()`, `stop()`, shutdown via `oneshot`. **C'est le point d'extension qui prépare #68** (le desktop consommera `web-server` comme lib, pas de fork). + +## Vérif QA (exécution réelle) — VERT +- `cargo build -p web-server --bin idea-serve` + `-p app-tauri --bin app-tauri` : OK. +- `cargo test -p backend` 41 passed · `-p web-server` 55 passed · `-p app-tauri` 35 passed. +- **Invariant clé** : `ldd target/debug/idea-serve | rg -i 'webkit|javascriptcore|gtk|gdk|soup|tauri|wry'` → **aucune sortie**. Comparatif `app-tauri` tire bien libwebkit2gtk/libgtk/libsoup/libjavascriptcore. +- Contrat HTTP/WS inchangé (55 tests web-server : pair/invoke/logout/ws/static/allowlist/B8). `git diff --name-only 506d589...HEAD -- frontend` → **vide**. + +## Faux mystère élucidé (à ne pas refaire paniquer) +`cargo test -p app-tauri` est passé de **45 → 35** : les tests ne sont PAS perdus, ils ont MIGRÉ. Écart réel depuis develop = **68 tests déplacés** (`src/lib.rs` : 104 → 36) : 55 `server::tests::*` → `web-server`, 10 `events::tests::*` → `backend::events`, 3 `dto::ls6_pagination_tests::*` → `backend::dto`. Vérifié par `cargo test -p app-tauri -- --list` comparé sur `506d589`. + +## Faux négatif connu (arbitré NON bloquant) +`cargo test --workspace` : 10 échecs `infrastructure::session::openai_compat::*` + `factory_routes_openai_compatible_to_http_session` sur `bind: Os { code: 1, kind: PermissionDenied }` = la sandbox interdit l'écoute réseau. **Confirmé préexistant sur `506d589`** (même liste). Validation retenue par crates ciblés. + +## Réserve QA restante +Aucun test dédié à `run_embedded().stop()` (un test de bind réel serait soumis à la même restriction sandbox). À couvrir dans #68, qui consommera ce handle. + +## Incident de topologie (résolu — leçon) +DevBackend a d'abord travaillé sur la branche de #69 sans committer. Git a rapatrié le diff Rust (stash `-u` limité à `Cargo.lock Cargo.toml crates/`), intégrité vérifiée par empreinte SHA-256 avant/après (`c1aecea8…c499d3b` / `701bc7ee…4d6ff016`), rien perdu. **Leçon : vérifier la branche courante AVANT de commencer.** + +## Prochaine étape +Validation live utilisateur : (1) le desktop ne perd rien, (2) `idea-serve` sert le client web. Si OK → Git merge dans develop (Git refuse de merger sans QA réelle sur les DEUX binaires ; point sensible = workspace `Cargo.toml` members). Débloque ensuite #68 (via `run_embedded`) et #66 (Docker). diff --git a/.ideai/tickets/65/issue.md b/.ideai/tickets/65/issue.md new file mode 100644 index 0000000..21f86c2 --- /dev/null +++ b/.ideai/tickets/65/issue.md @@ -0,0 +1,27 @@ +--- +id: "f9f7067b-92b7-4737-ad99-4487ad8c8b13" +number: 65 +title: "Serveur headless : extraire idea-serve du binaire Tauri (cœur partagé)" +status: "closed" +priority: "high" +sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" +links: [{"target":"#13","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784187583229 +updatedAt: 1784210420516 +version: 7 +--- +Objectif : produire un binaire serveur headless (`idea-serve`) SANS dépendance Tauri/WebKit, bâti sur le même cœur backend que le desktop (pas de fork). Prérequis de l'image Docker serveur/client. Cadré par Architect (suite #13). + +Point pivot : `server.rs` vit aujourd'hui dans `crates/app-tauri` et réutilise indirectement la présentation Tauri via `crate::state::AppState`, `crate::dto::*`, `crate::events::DomainEventDto`, `crate::pty::PtyChunk`, `crate::mcp_endpoint::*`, `ResumeContext`. Le protocole HTTP/WS lui est déjà autonome. + +Plan de lots (Architect) : +- **L1 — Extraire les DTO transport-neutres** dans un module/crate partagé (ex. `crates/presentation-dto` ou `backend-api`) : ErrorDto, Health*Dto, ProjectDto/ProjectListDto, ProjectWorkStateDto, BackgroundTaskDto, TerminalSessionDto, LaunchAgentRequestDto, OpenTerminalRequestDto, parseurs d'IDs, DomainEventDto. Adapter app-tauri::commands ET server.rs pour les consommer. JSON INCHANGÉ. +- **L2 — Extraire le serveur HTTP/WS** dans `crates/web-server` (lib) sans dépendance Tauri : déplacer server.rs, remplacer `AppState` par `BackendCore`, garder strictement routes + allowlist + sécurité B8 (POST /api/pair|invoke|logout, /api/ws, static same-origin). Aucune modif frontend. +- **L3 — Créer le bin `idea-serve`** (crate bin) : parse CLI/env → config → `web_server::run(config)`. Flags identiques à `idea --serve` (--listen/--app-data-dir/--web-root/--allow-remote/--public-origin/--trust-reverse-proxy). Défaut local = même app-data-dir que desktop (identifier app.idea.ide). Validation : le build/`ldd` ne tire PAS WebKitGTK. Garder éventuellement `app-tauri --serve` en compat dev, mais qui appelle `web-server`. + +Invariants : desktop AppImage ne perd RIEN, aucun import Tauri dans le bin headless, même cœur/use cases/stores, contrat HTTP/WS inchangé. Non-régression desktop validée avant tout merge. + +Dépend de #13. \ No newline at end of file diff --git a/.ideai/tickets/66/carnet.md b/.ideai/tickets/66/carnet.md new file mode 100644 index 0000000..a9e8413 --- /dev/null +++ b/.ideai/tickets/66/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#66" +version: 2 +updatedBy: {"kind":"user"} +updatedAt: 1784193460805 +--- diff --git a/.ideai/tickets/66/issue.md b/.ideai/tickets/66/issue.md new file mode 100644 index 0000000..ce6e6c3 --- /dev/null +++ b/.ideai/tickets/66/issue.md @@ -0,0 +1,27 @@ +--- +id: "f805cd9b-8ecd-401a-8f03-665a27fe73cb" +number: 66 +title: "Image Docker serveur/client IdeA" +status: "open" +priority: "medium" +sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" +links: [{"target":"#65","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1784187597004 +updatedAt: 1784193460805 +version: 2 +--- +Objectif : livrer une image Docker exécutant le mode serveur/client d'IdeA, bâtie sur le binaire headless `idea-serve` (#65), pas sur le binaire Tauri. Cadré par Architect (suite #13). + +Plan de lots (Architect) : +- **L4 — Build web transport HTTP** : produire les assets client Vite en `VITE_TRANSPORT=http` (npm, jamais pnpm), vérifier qu'aucun import Tauri ne fuit dans ce mode, packager dans `/usr/share/idea/web`. +- **L5 — Docker runtime** : Dockerfile multi-stage (build Rust headless + build frontend + runtime minimal Debian/Ubuntu selon deps PTY/process). Défauts : `IDEA_APP_DATA_DIR=/data`, `--listen 0.0.0.0:17373`, `--web-root /usr/share/idea/web`. Volumes `/data` (app-data : projects/profiles/templates/tasks/logs) et `/workspace` (projets manipulés par les agents). Entrypoint `idea-serve`. Healthcheck HTTP local. Conteneur reste HTTP interne ; reverse proxy TLS externe obligatoire en prod distante (`--allow-remote`/`--public-origin https`/`--trust-reverse-proxy`, doc B8). Pas de TLS applicatif V1. +- **L6 — Agents CLI en conteneur** : décider image minimale (profils détectés au runtime, exécutables attendus dans PATH) vs image `idea-server-agents` (CLIs redistribuables installées si licence OK). Seed profils compatibles conteneur. Documenter env (OPENAI_API_KEY, ANTHROPIC_API_KEY, vars opencode) et montages de credentials. Tester au moins un agent bout en bout. Hors périmètre : installer automatiquement des CLIs propriétaires sans validation licence. + +Lock app-data-dir : peu critique en Docker mono-conteneur mono-writer ; multi-conteneurs sur le même /data explicitement hors support sans lock distribué. + +Hors périmètre V1 : Kubernetes, multi-tenant, auth externe, TLS intégré. + +Dépend de #65 (binaire headless). \ No newline at end of file diff --git a/.ideai/tickets/67/carnet.md b/.ideai/tickets/67/carnet.md new file mode 100644 index 0000000..42d8577 --- /dev/null +++ b/.ideai/tickets/67/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#67" +version: 2 +updatedBy: {"kind":"user"} +updatedAt: 1784193467784 +--- diff --git a/.ideai/tickets/67/issue.md b/.ideai/tickets/67/issue.md new file mode 100644 index 0000000..b914898 --- /dev/null +++ b/.ideai/tickets/67/issue.md @@ -0,0 +1,22 @@ +--- +id: "e3d16c70-01c5-448e-9b5e-37ba2ccbbb58" +number: 67 +title: "Lock inter-process de l'app-data-dir (desktop ↔ idea --serve)" +status: "open" +priority: "low" +sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" +links: [{"target":"#13","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1784187618710 +updatedAt: 1784193467784 +version: 2 +--- +Issu de la validation live #13. `idea --serve` et l'app desktop peuvent écrire le même app-data-dir (`~/.local/share/app.idea.ide`) simultanément — pas de lock inter-process → risque de corruption d'état (projects.json/profiles.json/tasks). Aujourd'hui contourné par une simple consigne de doc (« ne pas lancer les deux en même temps »). + +À faire : garde-fou lock inter-process partagé desktop/serveur sur l'app-data-dir (le desktop DOIT le prendre aussi, un lock côté serveur seul ne suffit pas). À poser AVANT d'ouvrir des commandes write web au-delà de l'existant (cancel/retry background) — notamment avant `create_project` (#64). + +Peu critique en Docker mono-conteneur mono-writer (cf. #66) ; surtout pertinent pour l'usage AppImage `--serve` local sur la même machine que le desktop. Cadrage lock détaillé à confirmer avec Architect au démarrage. + +Dépend de #13. \ No newline at end of file diff --git a/.ideai/tickets/68/carnet.md b/.ideai/tickets/68/carnet.md new file mode 100644 index 0000000..6170a99 --- /dev/null +++ b/.ideai/tickets/68/carnet.md @@ -0,0 +1,63 @@ +--- +issueRef: "#68" +version: 8 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784277313706 +--- +# Ticket #68 — Serveur embarqué activable depuis le desktop : carnet de chantier + +Statut : **livré et mergé dans `develop` (`f965f64`)** le 2026-07-16, en exécution autonome de nuit. +**Reste à faire : la validation live utilisateur.** Rien n'a été cliqué dans l'app réelle — voir la réserve plus bas. + +## Livraison en deux temps + +**B1 — seam shared-core** (mergé plus tôt, `b7d38f1` + `328c941`). `web_server::run_embedded_with_core(config, Arc)`. Sans lui, le desktop aurait construit une **seconde composition root dans le même processus** — deux event bus, deux registres live, deux jeux de stores — et le conflit d'écriture app-data-dir serait réapparu à l'intérieur d'un seul process. + +**B2/F1/F2 — le reste** (`49e1d5a` backend, `7efa634` frontend, merge `b30c9c7`). +- `crates/app-tauri/src/embedded_server.rs` : controller de cycle de vie sur `AppState`, commandes `embedded_server_start/stop/status` et `get/save/preview_server_exposure_settings`, `FsServerExposureSettingsStore`. +- `frontend/src/features/settings/` : `SettingsView`, `DeploymentSettings`, `useDeployment` ; `adapters/desktopServer.ts` ; port `DesktopServerGateway`. +- `ProjectsView` : `showSettings: boolean` → navigation interne `AI Profiles` / `Deployment`. Le libellé alternant « Close AI Profiles » a disparu. + +## Décisions structurantes appliquées + +- **Pas de `PanelId: "settings"`.** Architect s'est corrigé en cours de route : Settings existe déjà dans `ProjectsView` comme surface principale, hors modèle `viewPlacement`. C'est de la configuration globale persistée, pas une vue projet dockable/détachable. +- **Persistance dans `/deployment/server-exposure.json`**, pas `localStorage`/`uiPreferences` : falsifier cette config peut exposer le serveur, c'est de la config de sécurité. Écriture atomique, `0600` posé **avant** le `rename`. +- **Le pairing code n'est jamais persisté** — secret runtime, affiché seulement si `running`. +- **Le frontend n'invente jamais une IP** : `candidateLanAddresses` et `upstreamUrl` viennent du backend, d'où l'existence de `preview`. +- `EmbedderSettings` / `ModelServersPanel` **non rapatriés** : refonte latérale hors sprint. La liste de sections est le seam où ils s'inséreront. + +## ⚠️ Le brief de Main était FAUX — intercepté par DevFrontend + +Main décrivait le mode `remoteProxyOtherMachine` avec deux champs (origine publique + IP du proxy). **`validate_settings` en exige un troisième : `lanBindAddress`**, et rejette loopback/unspecified. Construit selon le brief, le lot aurait été **vert et cassé** : chaque `save` et chaque `start` en mode 3 auraient échoué, sans qu'aucun test ne le voie. + +Corrigé par un select alimenté par `candidateLanAddresses` — la règle « le frontend n'invente jamais une IP » tient. Leçon consignée dans `7efa634`. + +## Vérification réelle (Main puis Git, tous deux hors sandbox) + +``` +npm run typecheck → propre +npx vitest run → 87 files / 789 passed +cargo test -p app-tauri → 43 passed, 1 ignored +cargo test -p web-server → 66 passed +``` +Re-vérifié sur `develop` après merge : 24 suites Rust, 0 échec. + +**Invariants vérifiés par exécution, pas par lecture de rapport :** `run_embedded_with_core` est le seul appelant (aucun `run_embedded` nu dans `app-tauri`) ; aucune fuite d'`EmbeddedServer*`/`ServerExposure*` dans `domain` ni `application` ; ordre du store `write(tmp)` → `chmod 0600` → `rename` ; `stop()` appelé à la sortie d'app (`lib.rs:153`) ; `features/web` n'importe pas la surface Settings (épinglé par test) et le transport web rend `UNSUPPORTED_ON_WEB`. + +**Piège d'environnement :** `app-tauri` et `web-server` ont un sandbox qui bloque `TcpListener::bind` (EPERM). Le test `end_to_end_over_real_loopback` est `#[ignore]` **préexistant** — lancé hors sandbox avec `--ignored`, il passe. Garde d'environnement confirmée par exécution, pas faux vert. + +## RÉSERVE — aucune validation live + +Tout est vert, **rien n'a été cliqué**. Le panneau n'a jamais été ouvert dans l'app réelle : ni le démarrage/arrêt, ni l'affichage du pairing code, ni les trois modes, ni la persistance entre deux lancements. jsdom ne prouve pas le rendu. Une AppImage a été construite pour que l'utilisateur teste — c'est le point de vérité qui manque. + +## Dette identifiée, non traitée (ne bloque rien) + +1. **`preview` valide avant de prévisualiser** (`embedded_server.rs:211`) : un brouillon mode 3 ne peut pas être prévisualisé tant qu'il n'a pas le `lanBindAddress` que la liste des candidats doit justement fournir. Contourné côté UI par une sonde `localOnly` toujours valide. Défaut d'ordonnancement backend. +2. **Code mort** : le warning `missingTrustedProxy` (`embedded_server.rs:452`) est inatteignable — `validate_settings` rejette les `trustedProxies` vides en mode 3 avant que `preview_settings` ne puisse le produire. +3. **Aucun événement de statut** : le backend n'émet rien, `onStatusChanged` est du polling **dans l'adapter Tauri**. La forme du port est prête pour un vrai événement, sans toucher l'UI. +4. **Fenêtre umask sur le temporaire** du store : le `0600` arrive après la création. Aucun secret dans ce fichier (modes, ports, IP), risque faible. Un `OpenOptions::mode(0o600)` à la création fermerait le sujet. +5. **Piège latent hors #68** : les stubs `unsupported.ts` préexistants (`WebRemoteGateway`, `WebWindowGateway`) *throwent de façon synchrone* depuis des méthodes qui rendent des Promise — `.then(ok, err)` ne les attraperait pas. Sans effet aujourd'hui (les appelants `await`), mais mérite un nettoyage. + +## Contexte : pourquoi #68 est passé avant #71 et #73 + +Arbitrage **utilisateur**, contre la recommandation d'Architect. Ce dernier proposait #72 → #73 → #71 → #68, ce qui reléguait en dernier la fonctionnalité demandée le matin même. L'utilisateur a tranché : #72 ajusté, puis #68. #71 (diagnostics) et #73 (TLS intégré) suivent. diff --git a/.ideai/tickets/68/issue.md b/.ideai/tickets/68/issue.md new file mode 100644 index 0000000..6b45f1d --- /dev/null +++ b/.ideai/tickets/68/issue.md @@ -0,0 +1,16 @@ +--- +id: "13821b24-f566-402e-bc10-02ac6e8a5f7c" +number: 68 +title: "Ajouter dans l'app desktop, la possibilité d'activer le serveur" +status: "closed" +priority: "medium" +sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" +links: [{"target":"#65","kind":"dependsOn"}] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784193318419 +updatedAt: 1784277313706 +version: 8 +--- +J'iamerais que depuis l'app desktop, on puisse quand même embarquer le serveur. Comme ça un utilisateur pourrait continuer son travail en cours en remote \ No newline at end of file diff --git a/.ideai/tickets/69/carnet.md b/.ideai/tickets/69/carnet.md new file mode 100644 index 0000000..60a0f8c --- /dev/null +++ b/.ideai/tickets/69/carnet.md @@ -0,0 +1,56 @@ +--- +issueRef: "#69" +version: 12 +updatedBy: {"kind":"user"} +updatedAt: 1784449061397 +--- +# Ticket #69 — Adaptibilité client téléphone (carnet de chantier) + +Statut : **validation live utilisateur PARTIELLE effectuée — mergé dans `develop` avec réserve explicite sur le lot 3**. +Branche : `feature/ticket69-mobile-responsive-client`, **rebasée sur `develop` @ `fe0e53e`** (base d'origine `506d589`). Commits après rebase : `48b8853` (shell), `62ecefe` (terminal + tests), `f0f2f3b` (fix typecheck) — anciennement `e22ea5b`/`8f15ad6`/`6ed0087`. Arbre `frontend/` identique bit pour bit avant/après rebase (`git diff 6ed0087 HEAD -- frontend` vide) ; historique d'origine conservé sur `backup/ticket69-pre-rebase-6ed0087`. Frontend uniquement — vérifié. + +## Cadrage Architect +Ticket **frontend-pur** : aucun changement DTO, use case, ni modèle `LayoutTree`. Contrats HTTP/WS et layout/terminal inchangés. Mais PAS du « CSS pur ». + +## ⚠️ Le cadrage initial reposait sur un modèle FAUX (constat majeur) +Le lot 2 prévoyait « remplacer les docks gauche/droite/floating par une pile verticale/drawer/bottom sheet ». **Il n'y a rien à remplacer** : le client web n'a JAMAIS eu de docks, de fenêtres flottantes ni de `LayoutTree`. `main.tsx` route le mode `http` vers `WebApp`, surface autonome livrée par #13 : pairing **ou** `WebWorkspace`, déjà une colonne verticale unique. Les seuls imports `@/features/*` de tout `features/web/` sont `terminals` et `workstate` — le shell desktop n'est pas atteignable depuis le web. +⇒ **La question « bottom sheet vs drawer » ne se pose pas.** Frontière épinglée par un test (`WebMobile.test.tsx`) + mémoire `web-client-is-single-column-no-desktop-shell`, pour que le cadrage ne reparte pas du même modèle faux. + +## Le vrai blocage était le terminal, pas le layout +Le clavier virtuel n'a ni Esc, ni Tab, ni Ctrl, ni flèches : on pouvait taper un prompt à un agent CLI mais pas l'interrompre, compléter un chemin, sortir d'un éditeur ou rappeler l'historique. + +## Livré +- **Lot 1 (shell)** : `100dvh` (le `height:100%` se résolvait sur le *large* viewport ⇒ la barre d'URL rétractable masquait le bas), `viewport-fit=cover` + safe-area insets, padding responsive, `inputMode="numeric"` sur le code d'appairage. Zoom laissé libre (accessibilité). +- **Lot 2 (réinterprété)** : `AgentLiveRow` + rangée de tâche de fond (statut/kind/Cancel/Retry ne tenaient pas à 360px) s'empilent sous `sm`, ligne unique dès `sm`. +- **Lot 3 (terminal)** : `TerminalView` expose `onReady(api)` **optionnel** (desktop inchangé) ; `api.send` passe par `term.input()` — même chemin qu'une frappe réelle, donc relais PTY + suspension du write-portal s'appliquent à l'identique (écrire sur le handle aurait court-circuité le portal). `TerminalKeyBar` : Esc/Tab/Ctrl-C/Ctrl-D/flèches en chips 44px ; **chaque tap annule le déplacement de focus** puis refocalise xterm (sinon le clavier virtuel se referme à chaque touche). Cellule `h-64` → `h-[60dvh]`. + +## Vérif QA (exécution réelle) — VERT +- `npm run typecheck` exit 0 · `npx vitest run` → 85 files / **774 passed** · `VITE_TRANSPORT=http npx vite build` OK. +- **CSS mobile confirmé DANS le bundle** (les valeurs arbitraires Tailwind avec `env()` no-opent en silence) : `100dvh`, `60dvh`, les 4 `safe-area-inset-*`, `viewport-fit=cover`, `.h-\[60dvh\]{height:60dvh}`. +- Non-régression desktop : `LayoutGrid` ne passe pas `onReady` ; chemin desktop inchangé. +- Rendu réel Chrome headless 360×780 : pairing tient sans débordement horizontal. +- **Ré-exécutée par Git sur la base rebasée `fe0e53e` avant merge** (les chiffres du carnet ne valaient que sur `506d589`) : `npm run typecheck` exit 0 · `npx vitest run` → 85 files / **774 passed**, 0 échec · `VITE_TRANSPORT=http npx vite build` exit 0. Identique — le rebase n'a rien cassé. + +## Réserve ASSUMÉE — trou de couverture réel +**jsdom n'évalue PAS les media queries** : les tests sont structurels, pas visuels. Ils ne prouvent NI le rendu aux breakpoints, NI l'effet réel de `dvh`, NI les safe areas, NI le clavier virtuel/focus sur navigateur mobile. QA : « acceptable pour merger avec réserve explicite », pas rouge. **C'est exactement ce que la validation utilisateur doit couvrir.** + +## Validation live utilisateur — PÉRIMÈTRE EXACT (2026-07-16) + +Effectuée par l'utilisateur sur un **vrai téléphone**, contre le serveur `idea-serve` exposé via le reverse proxy `https://idea.anthonybouteiller.ovh`. + +**Couvert — verdict utilisateur : OK.** « Le terminal s'ouvre correctement pour le téléphone. » Atteindre le terminal implique d'avoir traversé l'appairage **puis** le workspace : cela valide en réel le shell (lot 1) et la mise en page à taille de téléphone (lot 2), y compris le `100dvh` et le `60dvh` de la cellule terminal, que jsdom ne pouvait pas prouver. + +**NON couvert — angle mort qui subsiste.** Le **lot 3 n'a pas été exercé** : la `TerminalKeyBar` (Esc/Tab/Ctrl-C/Ctrl-D/flèches) n'a pas été utilisée, et le comportement de focus au tap — l'invariant « chaque tap annule le déplacement de focus puis refocalise xterm », sans lequel le clavier virtuel se referme à chaque touche — n'a **pas** été observé sur un navigateur mobile réel. Or c'est le cœur du ticket : le carnet identifie le terminal, pas le layout, comme le vrai blocage. + +Le test qui fermerait ce trou tient en 30 s : lancer une commande longue, taper **Ctrl-C** dans la barre de touches, vérifier (a) que la commande s'interrompt et (b) que le clavier virtuel reste ouvert. + +**Arbitrage Main : merge autorisé avec cette réserve écrite, pas effacée.** Le risque résiduel est borné (surface frontend-pure, desktop non atteint, non-régression prouvée) et ne justifie pas de bloquer le sprint. Mais tant que le test Ctrl-C n'est pas fait, **le lot 3 est livré et non prouvé en conditions réelles** — à ne pas présenter comme validé. + +**Report de la réserve dans l'historique git (Git, 2026-07-16)** : la réserve est recopiée dans le corps du commit de merge de `develop`. Un carnet peut être réécrit ; un message de commit mergé, non. Le fait « lot 3 non prouvé en réel au moment du merge » est donc daté et infalsifiable, indépendamment de ce fichier. + +## Écart de périmètre — Playwright : TRANCHÉ, reporté vers #63 +Lot 4 demandait « Playwright ou équivalent ». DevFrontend a livré l'équivalent (vitest/jsdom) et REFUSÉ d'installer Playwright unilatéralement (binaires navigateur + surface CI = décision de dépendance qui remonte à Architect/Main) — refus fondé. QA : non bloquant pour ce lot. +**Décision Main (2026-07-16) : ne bloque pas #69 et ne donne PAS lieu à un ticket neuf.** La décision de dépendance appartient à **#63 « Système de test de l'UI »**, déjà ouvert, où elle sera tranchée avec la stratégie de test UI dans son ensemble plutôt qu'au coin d'un sprint serveur/client. Rationnel conservé : Playwright rend un vrai navigateur (media queries, `dvh`, safe areas évalués pour de vrai) et aurait attrapé automatiquement les deux bugs de classe « CSS silencieusement absent du bundle » corrigés ici ; il **n'émule** cependant qu'un téléphone et ne remplace pas la validation manuelle du clavier virtuel — sa valeur est d'empêcher la régression silencieuse entre deux validations manuelles. + +## Prochaine étape +Merge fait par Git (rebase + `--no-ff` dans `develop`, sans conflit). **Le test Ctrl-C sur téléphone réel reste ouvert** : s'il échoue, il ouvre un bug, il ne réverte pas le ticket. Le passage du ticket à un statut terminal appartient à Main : tant que le lot 3 n'est pas prouvé en réel, « mergé » ≠ « validé ». diff --git a/.ideai/tickets/69/issue.md b/.ideai/tickets/69/issue.md new file mode 100644 index 0000000..e434628 --- /dev/null +++ b/.ideai/tickets/69/issue.md @@ -0,0 +1,16 @@ +--- +id: "7f5a40cf-837f-46f7-8c5f-653c80975396" +number: 69 +title: "Adaptibilité client téléphone" +status: "closed" +priority: "medium" +sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1784193391258 +updatedAt: 1784449061397 +version: 12 +--- +J'aimerais un format vertical qui rende l'app client compatible téléphone \ No newline at end of file diff --git a/.ideai/tickets/7/carnet.md b/.ideai/tickets/7/carnet.md new file mode 100644 index 0000000..11e6162 --- /dev/null +++ b/.ideai/tickets/7/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#7" +version: 8 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784718581033 +--- diff --git a/.ideai/tickets/7/issue.md b/.ideai/tickets/7/issue.md new file mode 100644 index 0000000..6379641 --- /dev/null +++ b/.ideai/tickets/7/issue.md @@ -0,0 +1,16 @@ +--- +id: "9f6979fb-a68f-4660-9d57-cf69ea1daf85" +number: 7 +title: "Gestion des session limite" +status: "closed" +priority: "high" +sprint: "d8f3f37b-87ca-4509-9116-45a99bc711df" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783167495082 +updatedAt: 1784718581033 +version: 8 +--- +J'aimerais que IdeA puisse gérer les session limite des profile IA. Lorsqu'un profile IA atteint une session limite, j'aimerais que IdeA le sache, qu'il récupère l'heure et la date du reset et qu'il puisse automatiquement reprendre le travail une fois le reset fait. Lorsqu'une reprise automatique est prévue, il faut que l'utilisateur puisse cancel la reprise. Il est important de noter que les agent s'appelant entre eux avec des profile IA différents, il faut prendre en compte qu'un agent A qui appelle un agent B, peut voir l'agent B atteindre la session limite. IdeA devra savoir gérer ça correctement. \ No newline at end of file diff --git a/.ideai/tickets/70/carnet.md b/.ideai/tickets/70/carnet.md new file mode 100644 index 0000000..4b0137e --- /dev/null +++ b/.ideai/tickets/70/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#70" +version: 4 +updatedBy: {"kind":"user"} +updatedAt: 1784194049447 +--- diff --git a/.ideai/tickets/70/issue.md b/.ideai/tickets/70/issue.md new file mode 100644 index 0000000..0587f78 --- /dev/null +++ b/.ideai/tickets/70/issue.md @@ -0,0 +1,16 @@ +--- +id: "e73ed25f-8e22-48a5-84ea-521212ea80b5" +number: 70 +title: "Pouvoir gérer les models AI locaux téléchargés via les serveur llamacpp" +status: "closed" +priority: "low" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784194016529 +updatedAt: 1785083912470 +version: 5 +--- +Avec les serveurs de models locaux llamacpp, on télécharge des modeles, j'aimerais aussi pouvoir les supprimer diff --git a/.ideai/tickets/71/carnet.md b/.ideai/tickets/71/carnet.md new file mode 100644 index 0000000..0318e68 --- /dev/null +++ b/.ideai/tickets/71/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#71" +version: 1 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784210443885 +--- diff --git a/.ideai/tickets/71/issue.md b/.ideai/tickets/71/issue.md new file mode 100644 index 0000000..619b635 --- /dev/null +++ b/.ideai/tickets/71/issue.md @@ -0,0 +1,39 @@ +--- +id: "4d78a0e9-b54c-4db6-92e5-1461c17d7c84" +number: 71 +title: "Diagnostic d'accessibilité du serveur : dire pourquoi ça ne marche pas, pas seulement que ça tourne" +status: "open" +priority: "medium" +sprint: null +links: [{"target":"#68","kind":"relatesTo"},{"target":"#65","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784210443885 +updatedAt: 1784210443885 +version: 1 +--- +Issu d'une session de debug live réelle (2026-07-16) où un serveur `idea-serve` CORRECTEMENT configuré est resté injoignable, sans que rien dans le produit n'indique pourquoi. Le serveur tournait, se croyait bon, affichait « listening on … ». Deux boucles de debug successives ont été nécessaires, dont aucune n'était diagnosticable depuis le produit. + +**Problème de fond : l'échec est invisible.** Un serveur peut être parfaitement configuré et totalement inatteignable ; aujourd'hui la seule façon de le savoir est de sortir du produit (curl, ss, ufw, dig). + +Les trois échecs réels rencontrés, tous silencieux : + +1. **Bind loopback + reverse proxy sur une autre machine** → le proxy ouvre une connexion vers *sa propre* loopback, la requête n'arrive jamais. Aucun message, juste un timeout côté navigateur. Aggravé par `docs/server-client-mode-remote.md`, dont l'exemple du cas distant utilise `--listen 127.0.0.1:17373` en supposant sans le dire que le proxy est co-localisé (correction doc traitée à part). +2. **Pare-feu hôte (ufw actif, port non ouvert)** → dissymétrie caractéristique : joignable depuis la machine locale, timeout depuis toute autre machine. Rien ne le signale. +3. **Origine non conforme → 403 muet** → `--public-origin` compare en ÉGALITÉ STRICTE. Ouvrir l'UI par l'IP (`http://192.168.1.75:17373`) au lieu du nom de domaine donne un 403 sur toute l'API, sans explication dans l'UI. C'est le piège n°1 pour un nouvel utilisateur. + +**Périmètre proposé (à cadrer par Architect).** + +**Lot 1 — Surfacer les origines rejetées. Meilleur rapport valeur/coût : l'événement EXISTE déjà.** `SecurityLogEvent::OriginRejected { origin, route }` dans `crates/web-server/src/lib.rs` est aujourd'hui simplement `eprintln!` sur stderr — donc invisible en desktop, et invisible en Docker sans aller lire les logs. L'exposer (DTO + surface UI) transforme le mystère en diagnostic d'une ligne : « un client s'est connecté depuis `http://192.168.1.75:17373`, or l'origine autorisée est `https://idea.anthonybouteiller.ovh` ». Attention : c'est un log de sécurité, borner la rétention et ne pas en faire un canal de fuite d'information. + +**Lot 2 — Dire quand le serveur est JOIGNABLE, pas seulement qu'il tourne.** Le serveur ne peut pas se tester depuis l'extérieur, mais il peut détecter ce qui rend l'échec certain : +- pare-feu hôte actif dont le port d'écoute n'est pas autorisé (ufw/firewalld) ; +- `public_origin` dont le DNS ne résout vers aucune interface locale ⇒ indice fort que le proxy est ailleurs, donc qu'un bind loopback ne peut pas marcher ; +- afficher l'upstream exact à coller dans la config du proxy (`http://192.168.1.75:17373`) plutôt que de le laisser deviner. + +Ces détections sont des heuristiques : elles doivent INFORMER, jamais bloquer le démarrage. + +**Hors périmètre :** le choix du mode par intention (« proxy sur cette machine » vs « sur une autre machine ») appartient à F2 de #68 — c'est le panneau desktop, ça ne change aucun contrat backend. + +**Attention frontière :** les lots ci-dessus doivent servir AUSSI le mode headless/Docker (`idea-serve` sans desktop), pas seulement le panneau desktop. Le diagnostic ne doit pas naître prisonnier de l'UI Tauri. \ No newline at end of file diff --git a/.ideai/tickets/72/carnet.md b/.ideai/tickets/72/carnet.md new file mode 100644 index 0000000..b522600 --- /dev/null +++ b/.ideai/tickets/72/carnet.md @@ -0,0 +1,55 @@ +--- +issueRef: "#72" +version: 3 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784236365445 +--- +# Ticket #72 — Confiance reverse proxy : carnet de chantier + +Statut : **livré et mergé dans `develop` (`7fbaa8e`)** le 2026-07-16. +Commits : `75d1ce8` (code), `42804fc` (doc), `ae01297` (merge `--no-ff`). Branche supprimée. + +## Ce qui a été livré + +- `trusted_proxies` dans `ServerConfig` + flag CLI `--trusted-proxy` (répétable, IP ou CIDR). +- Refus **au démarrage** d'un bind non-loopback distant sans proxy autorisé. +- Guard runtime sur les **trois** surfaces — statique, `/api/*`, WebSocket : le `peer_addr` est vérifié **avant** toute lecture de header. +- `X-Forwarded-Proto: https` obligatoire en mode proxy. +- `X-Forwarded-For` n'autorise **jamais** rien — journalisé uniquement. +- **B0** : réconciliation du port effectif dans `run_server` (le chemin CLI standalone souffrait du même bug que l'embedded : `origin_allowed` comparait à `http://127.0.0.1:0`). +- Diagnostics : `UntrustedProxyPeer`, `ForwardedProtoRejected`, `ForwardedHostMismatch`. + +## Ajustement en cours de route — le hard reject `X-Forwarded-Host` a été RETIRÉ + +Cadré initialement comme un refus, il est devenu un **warning de diagnostic**. Raison (Architect) : redondant avec la vérification d'`Origin` en égalité stricte déjà appliquée à toutes les routes `/api/*`, et il **cassait la configuration par défaut de nginx**, qui n'envoie pas cet en-tête. Le vrai contrôle d'accès reste pairing/session + pair proxy autorisé. Gain trop faible pour la friction créée. + +Le repli de lecture accepte `X-Forwarded-Host`, `Forwarded host=` **ou** `Host`. + +## Vérification réelle (Main, hors sandbox) + +`cargo test -p web-server` → **66 passed / 0 failed**. Re-vérifié sur `develop` après merge : 66 · 41 (backend) · 35 (app-tauri), 0 échec. + +**Comportement exercé sur un serveur réellement lancé**, pas seulement en test : +- proxy autorisé + `X-Forwarded-Proto: https` + **host absent** (= nginx par défaut) → **passe** le guard, `forwardedHostMismatch` en diagnostic. +- host **différent** → passe également, warning seulement. +- `X-Forwarded-Proto` absent → **403**, message : « Configure the reverse proxy to send it; for nginx add: proxy_set_header X-Forwarded-Proto $scheme; ». + +## ⚠️ Piège d'environnement — un faux vert s'est produit ici, deux fois + +**DevBackend et QA sont tous deux bloqués par EPERM sur `TcpListener::bind`.** Un `cargo test -p web-server` sandboxé rend un vert qui ne prouve rien : sur #68 B1, un « 57 passed » sandboxé a masqué un **vrai bug produit** (port éphémère vs `origin_allowed`). Tout vert sur ce crate doit être produit **hors sandbox**. C'est consigné dans les messages de commit, pas seulement ici. + +## Revue de sécurité (Git, au-delà du vert) + +Guard câblé sur les trois surfaces, aucune route qui y échappe. Le chemin de production propage le vrai pair (`handle_tcp_connection` → `Some(peer_addr.ip())`). Le repli `peer_ip.unwrap_or(config.listen.ip())` n'est atteignable que depuis des appelants `#[cfg(test)]`. CIDR correct, y compris `prefix == 0` (qui aurait débordé au shift sans traitement à part). + +## Changement de comportement ASSUMÉ + +Le durcissement **casse volontairement les configurations existantes** : un bind non-loopback distant sans `--trusted-proxy` est désormais refusé au démarrage, avec un message qui dit quoi faire. Consigné dans l'historique git. + +## Origine du ticket + +Trouvé par **Git**, pas par une revue d'architecture : c'est lui qui a établi que `--trust-reverse-proxy` n'avait aucun effet runtime — il n'était lu que pour exiger sa propre présence. La trilogie déclarait une topologie sécurisée sans jamais la garantir. + +## Point ouvert, remonté par Git — à arbitrer par l'utilisateur + +**#73 (TLS intégré) rendrait une partie de #72 obsolète.** Si `idea-serve` termine lui-même TLS, la cérémonie proxy — `--trusted-proxy`, le guard, ces diagnostics — ne concerne plus que les déploiements qui gardent un proxy devant. Ce n'est pas du travail perdu (#72 corrige un vrai trou aujourd'hui, et B0 était un bug réel), mais l'ordre des deux mérite un regard. diff --git a/.ideai/tickets/72/issue.md b/.ideai/tickets/72/issue.md new file mode 100644 index 0000000..d9b4a41 --- /dev/null +++ b/.ideai/tickets/72/issue.md @@ -0,0 +1,53 @@ +--- +id: "0fba0a7f-8f04-460e-b81a-93790e10b066" +number: 72 +title: "Sécurité : donner un effet réel à la confiance reverse proxy (--trust-reverse-proxy est un drapeau creux)" +status: "closed" +priority: "high" +sprint: null +links: [{"target":"#68","kind":"blocks"},{"target":"#71","kind":"relatesTo"},{"target":"#65","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784218596269 +updatedAt: 1784236365445 +version: 3 +--- +**Vulnérabilité de conception existante, dans le mode CLI d'aujourd'hui — indépendante de #68.** Trouvée par Git pendant la mise en place de #68, confirmée et cadrée par Architect. + +**Le constat.** `--trust-reverse-proxy` n'a **aucun effet à l'exécution**. Vérifié dans `crates/web-server/src/lib.rs` : le champ n'est lu qu'en ligne 192, dans `ServerConfig::validate()`, **pour exiger sa propre présence** quand `allow_remote` est vrai. Le serveur ne parse jamais `X-Forwarded-*`. C'est un drapeau d'intention pur. + +**Le problème.** `validate()` valide une configuration, pas la réalité : rien ne vérifie qu'un proxy est réellement devant le serveur. **Binder `0.0.0.0` avec les trois drapeaux, sans aucun proxy, satisfait `validate()` et sert du HTTP en clair au monde entier.** La trilogie `allow_remote` + origine HTTPS + `trust_reverse_proxy` DÉCLARE une topologie sécurisée, elle ne la GARANTIT pas. + +Verdict Architect : « la trilogie actuelle n'est pas un invariant de sécurité, c'est seulement une déclaration d'intention ». Aggravé par #68 : en CLI, taper trois drapeaux est un acte délibéré d'opérateur ; dans un panneau desktop, cocher un bouton ne l'est pas. Le drapeau donnerait un faux sentiment de protection en un clic. + +**Invariant runtime corrigé (Architect).** +- `allow_remote=true` signifie : mode public accepté UNIQUEMENT derrière un proxy HTTPS déclaré. +- Les headers `X-Forwarded-*` ne sont fiables QUE si le `peer_addr` réseau est un proxy autorisé — vérifier le pair AVANT de leur faire confiance. +- `X-Forwarded-Proto: https` exigé en mode remote. +- `X-Forwarded-Host` (ou équivalent) doit correspondre à `public_origin`. +- **`X-Forwarded-For` ne doit JAMAIS servir à autoriser une requête** — spoofable, log uniquement. +- Proxy sur cette machine : bind loopback, peer forcément loopback, `trust_reverse_proxy` reste acceptable. +- Proxy sur une autre machine : bind LAN autorisé SEULEMENT avec un proxy explicite (`--trusted-proxy 192.168.1.10` ou CIDR). Toute requête dont le `peer_addr` est hors allowlist est refusée, même porteuse de `X-Forwarded-Proto: https`. + +Conséquence produit : le mode desktop « proxy autre machine » **ne peut pas être un clic magique**. Il doit demander l'IP/CIDR du proxy autorisé — ce n'est pas une « listen address » mais un contrôle de sécurité compréhensible : « quelle machine a le droit de parler au serveur IdeA ? ». + +**Lots (Architect).** +- **B1 config** : `trusted_proxies: Vec` dans `ServerConfig`, flag CLI `--trusted-proxy`, DTO desktop équivalent. +- **B2 runtime guard** : en tête du handling HTTP/WS, vérifier `peer_addr`, `X-Forwarded-Proto=https`, host public conforme. +- **B3 diagnostics** : événements `untrustedProxyPeer`, `forwardedProtoRejected`, `forwardedHostRejected` (à croiser avec #71). +- **B4 docs** : exemples séparés proxy local vs proxy autre machine. + +**Points de vérité QA.** +- `0.0.0.0 + allow_remote + public_origin + trust_reverse_proxy` sans trusted proxy → REFUSÉ. +- peer non autorisé vers bind LAN → rejeté. +- peer autorisé mais `X-Forwarded-Proto=http` → rejeté. +- peer autorisé + https + host conforme → passe. +- origine API non conforme → toujours 403. +- aucun test ne fait confiance à `X-Forwarded-For`. + +**Compat / migration.** Garder `--trust-reverse-proxy` pour la CLI, mais le déclasser en option de mode : il ne suffit plus pour un bind non-loopback. Chemin de migration à choisir (refus au démarrage vs refus runtime). + +⚠️ **Impact utilisateur réel connu** : l'installation actuelle de l'utilisateur (bind `192.168.1.75:17373` + trilogie, proxy sur une autre machine, exposé via `https://idea.anthonybouteiller.ovh`) sera **refusée** par la validation corrigée tant qu'elle ne déclare pas `--trusted-proxy`. Le chemin de migration doit être explicite et le message d'erreur doit dire quoi faire, pas seulement refuser. + +**Ordre imposé par Architect** : #68 core (B1 shared-core) peut continuer, il n'est pas exposé utilisateur. Mais **#72 doit être mergé dans `develop` AVANT `feature/ticket68-embedded-server-panel`** — pas de panneau remote avant ce durcissement. \ No newline at end of file diff --git a/.ideai/tickets/73/carnet.md b/.ideai/tickets/73/carnet.md new file mode 100644 index 0000000..f2aae01 --- /dev/null +++ b/.ideai/tickets/73/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#73" +version: 1 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784223044517 +--- diff --git a/.ideai/tickets/73/issue.md b/.ideai/tickets/73/issue.md new file mode 100644 index 0000000..c93bb13 --- /dev/null +++ b/.ideai/tickets/73/issue.md @@ -0,0 +1,46 @@ +--- +id: "a43a07d0-573f-41e9-9b7b-2f88beb4e660" +number: 73 +title: "TLS intégré à idea-serve : supprimer la cause racine de la cérémonie reverse proxy" +status: "open" +priority: "high" +sprint: null +links: [{"target":"#72","kind":"relatesTo"},{"target":"#66","kind":"relatesTo"},{"target":"#68","kind":"relatesTo"},{"target":"#71","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784223044517 +updatedAt: 1784223044517 +version: 1 +--- +**Décision utilisateur (2026-07-16) : IdeA gagne le TLS intégré.** Contrainte posée : prendre la technologie la plus légère. + +**La cause racine, établie par une session de debug réelle.** Un utilisateur a mis une matinée à exposer `idea-serve` derrière son nginx. Cinq obstacles, découverts un par un en tapant dans le mur : bind loopback injoignable par un proxy distant (timeout muet), ufw (timeout muet), `--trusted-proxy` obligatoire (#72), `X-Forwarded-Proto` absent (403), `X-Forwarded-Host` absent (403). + +**Ces cinq obstacles ont UNE cause : le serveur ne sait pas chiffrer.** Ne pouvant pas terminer le TLS, il doit *croire quelqu'un d'autre sur parole* quand on lui dit que la connexion publique était en HTTPS. `--trusted-proxy` sert à savoir qui a le droit de lui mentir ; `X-Forwarded-Proto` est le mensonge lui-même. Toute la machinerie de #72 n'existe **que** parce qu'il manque cette capacité. #66 avait acté l'exclusion noir sur blanc (« Pas de TLS applicatif V1 », « Hors périmètre V1 : TLS intégré ») — c'est cette ligne qui a produit les cinq obstacles. + +**Ce que le TLS intégré change.** L'utilisateur ouvre le 443 de son routeur, donne son domaine, terminé : pas de `--trusted-proxy`, pas de `X-Forwarded-*`, pas de proxy du tout. Un reverse proxy devant reste possible, mais devient un **choix**, plus une obligation structurelle. Une bonne partie de #72 se simplifie mécaniquement — sans disparaître : le mode « derrière proxy » reste supporté et garde ses contrôles. + +**Constat technique qui répond à la contrainte de légèreté (vérifié dans `Cargo.lock`) :** +- `rustls` 0.23, `tokio-rustls`, `ring`, `webpki-roots`, `hyper-rustls` sont **DÉJÀ** dans l'arbre de dépendances. +- **Aucun** `openssl`, `openssl-sys` ni `native-tls`. +- Le coût marginal d'un TLS serveur est donc quasi nul : la brique est déjà vendue avec le projet. Pas de nouvelle chaîne de dépendances, pas de dépendance système (contrairement à OpenSSL). + +**Pistes ACME à arbitrer par Architect :** +- `rustls-acme` — bâti sur rustls/tokio, fournit le resolver de certificats et le renouvellement automatique. Plus intégré, moins de code à écrire. +- `instant-acme` — plus bas niveau, pure Rust, minimal ; le challenge et le stockage restent à notre charge. +- **Argument fort pour le challenge TLS-ALPN-01 : il ne nécessite QUE le port 443.** Pas de port 80, pas de web root séparé pour HTTP-01. Cohérent avec « l'utilisateur ouvre un port ». + +**Limites à assumer et à documenter (ne pas vendre du rêve) :** +- ACME exige un **domaine public** et un port joignable depuis l'extérieur. Couvre l'exposition Internet, **pas** le LAN pur sans domaine. +- LAN pur ⇒ certificat auto-signé ⇒ avertissement navigateur. À traiter comme un mode distinct, pas à masquer. +- IdeA devient **responsable des certificats** : stockage, permissions, renouvellement, gestion des rate limits Let's Encrypt (utiliser l'environnement de staging en test, sous peine de blocage). +- Ne jamais mettre de certificat/clé dans un dossier synchronisé ou versionné. + +**Interactions à cadrer :** +- **#72** : le mode « TLS direct » ne doit exiger ni `--trusted-proxy` ni `X-Forwarded-*` — le serveur SAIT que la connexion est HTTPS puisqu'il la termine. Le mode « derrière proxy » garde ses contrôles. +- **#66 (Docker)** : son défaut `--listen 0.0.0.0:17373` + distant est **déjà invalide** depuis #72 (refusé au démarrage sans `--trusted-proxy`). Le TLS intégré rouvre la question de l'image : proxy embarqué ou TLS applicatif ? +- **#68** : les modes d'exposition du panneau desktop gagnent un mode « TLS direct », et le champ « IP du proxy autorisé » disparaît dans ce mode. +- **#71** : les diagnostics doivent couvrir les échecs ACME (challenge, DNS, port 443 injoignable), qui seront la nouvelle classe d'échec silencieux. + +**Exigence produit transverse, issue du même incident :** l'échec ne doit jamais être muet. Un certificat qui ne s'obtient pas doit le dire, avec la raison et l'action. C'est exactement le défaut qu'on corrige partout ailleurs dans ce sprint (#71). \ No newline at end of file diff --git a/.ideai/tickets/74/carnet.md b/.ideai/tickets/74/carnet.md new file mode 100644 index 0000000..f66e578 --- /dev/null +++ b/.ideai/tickets/74/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#74" +version: 3 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784277308704 +--- diff --git a/.ideai/tickets/74/issue.md b/.ideai/tickets/74/issue.md new file mode 100644 index 0000000..ad2f84c --- /dev/null +++ b/.ideai/tickets/74/issue.md @@ -0,0 +1,67 @@ +--- +id: "249bb0de-118f-4d6f-b0c4-1d4e2aadd83f" +number: 74 +title: "Le serveur embarqué sert le bundle desktop (Tauri) au navigateur — __TAURI_INTERNALS__ undefined" +status: "closed" +priority: "high" +sprint: null +links: [{"target":"#68","kind":"relatesTo"},{"target":"#66","kind":"relatesTo"},{"target":"#13","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784271086298 +updatedAt: 1784277308704 +version: 3 +--- +## Symptôme (constaté live, exposition derrière reverse proxy) + +UI chargée dans le navigateur via `https://idea.anthonybouteiller.ovh` (nginx → 192.168.1.75:17373, serveur embarqué desktop, mode `remoteProxyOtherMachine`) : + +- `Error: can't access property "invoke", window.__TAURI_INTERNALS__ is undefined` (x2) +- Aucun projet affiché. + +## Cause racine (confirmée) + +`crates/app-tauri/tauri.conf.json` utilise **un seul** `frontend/dist` pour deux besoins incompatibles : + +- `build.frontendDist: "../../frontend/dist"` + `beforeBuildCommand: "npm --prefix ../frontend run build"` → `vite build` **sans** `VITE_TRANSPORT` → bundle **desktop** (transport `tauri`). +- `bundle.resources: { "../../frontend/dist": "web" }` → ce **même** bundle desktop est packagé en ressource `web/`. + +Or `resolve_web_root()` (`crates/app-tauri/src/embedded_server.rs:498-517`) sert `resource_dir.join("web")`. Le serveur embarqué sert donc le bundle desktop au navigateur. + +Le transport est figé **au build** par Vite : `resolveTransport()` (`frontend/src/app/di.tsx:39-46`) lit `import.meta.env.VITE_TRANSPORT`, constant-folded à la compilation. + +**Preuve** dans `frontend/dist/assets/index-nl-2Eu6U.js` (build du 2026-07-17 08:00) : + +```js +function Cb(){return"tauri"} // resolveTransport() figé sur tauri +``` + +Les deux jeux d'adapters sont bien présents dans le bundle, mais le sélecteur ne pointera jamais sur `createHttpWsGateways()`. + +## Attendu + +L'AppImage doit packager en ressource `web/` un bundle construit avec `VITE_TRANSPORT=http`, distinct du `frontendDist` desktop. Un seul `dist` ne peut pas servir les deux transports. + +## Contexte + +- `docs/server-client-mode-remote.md` documente déjà que le build web **doit** être produit en `VITE_TRANSPORT=http`, sinon exactement ce symptôme. +- Le carnet de #13 avait classé ce symptôme en « erreur de commande de test, pas un bug de code » — vrai à l'époque de `idea --serve --web-root`, mais le packaging AppImage de #66/#68 réintroduit le problème par défaut. +- #66 L4 anticipait le besoin (« produire les assets client Vite en `VITE_TRANSPORT=http` … packager dans `/usr/share/idea/web` ») ; la conf actuelle ne le fait pas. +- npm, jamais pnpm (mémoire `frontend-uses-npm-not-pnpm`). + +## Workaround live + +Build séparé + `IDEA_WEB_ROOT` (premier candidat de `resolve_web_root`, court-circuite la ressource packagée) : + +```bash +cd frontend && VITE_TRANSPORT=http npx vite build --outDir dist-web --emptyOutDir +IDEA_WEB_ROOT=…/frontend/dist-web ./IdeA.AppImage +``` + +## DoD + +- Build AppImage produit une ressource `web/` en transport http, `frontendDist` desktop inchangé. +- Vérif automatisable : le bundle servi ne doit pas résoudre le transport sur `tauri`. +- Validation live navigateur derrière reverse proxy : UI + projets listés, aucune erreur `__TAURI_INTERNALS__`. +- Rebuild AppImage livrable pour test utilisateur. \ No newline at end of file diff --git a/.ideai/tickets/75/carnet.md b/.ideai/tickets/75/carnet.md new file mode 100644 index 0000000..a5414cf --- /dev/null +++ b/.ideai/tickets/75/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#75" +version: 3 +updatedBy: {"kind":"user"} +updatedAt: 1784449082160 +--- diff --git a/.ideai/tickets/75/issue.md b/.ideai/tickets/75/issue.md new file mode 100644 index 0000000..c868389 --- /dev/null +++ b/.ideai/tickets/75/issue.md @@ -0,0 +1,48 @@ +--- +id: "d54a1f20-03e6-4925-982b-923a37331749" +number: 75 +title: "Écran d'appairage web : clavier numérique sur mobile alors que le code contient des lettres" +status: "closed" +priority: "medium" +sprint: null +links: [{"target":"#69","kind":"relatesTo"},{"target":"#74","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1784277353320 +updatedAt: 1784449082160 +version: 3 +--- +## Symptôme (constaté live, téléphone) + +Sur l'écran d'appairage web (`PairingScreen`), le clavier virtuel qui s'ouvre est un pavé **numérique uniquement**, alors que le code d'appairage à saisir contient des **lettres**. Impossible de saisir le code sans changer de clavier à la main. + +## Cause racine (confirmée) + +Le code généré côté serveur est **hexadécimal, 8 caractères, majuscules** (`0-9A-F`) : + +```rust +// crates/web-server/src/lib.rs:2220 +fn new_pairing_code() -> String { + Uuid::new_v4().simple().to_string().chars().take(8) + .collect::().to_ascii_uppercase() +} +``` + +Or `frontend/src/features/web/PairingScreen.tsx:79` force `inputMode="numeric"` (introduit par #69 en supposant un code purement chiffré), et le placeholder `p. ex. 4821-93` (ligne 74) décrit un format qui n'existe pas. + +## Attendu + +- Le clavier mobile permet de saisir lettres **et** chiffres. +- Le placeholder reflète le format réel (hex 8 caractères majuscules). +- La saisie reste sans autocorrection ni capitalisation surprise ; le code étant en majuscules, la casse ne doit pas être un piège pour l'utilisateur. + +## Périmètre + +Frontend pur. Le format du code côté serveur n'est pas remis en cause par ce ticket. + +## DoD + +- Saisie possible au clavier mobile standard, lettres incluses. +- Placeholder cohérent avec `new_pairing_code()`. +- Test de non-régression sur le champ. \ No newline at end of file diff --git a/.ideai/tickets/76/carnet.md b/.ideai/tickets/76/carnet.md new file mode 100644 index 0000000..797b9d6 --- /dev/null +++ b/.ideai/tickets/76/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#76" +version: 2 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784287679986 +--- diff --git a/.ideai/tickets/76/issue.md b/.ideai/tickets/76/issue.md new file mode 100644 index 0000000..7f00e3c --- /dev/null +++ b/.ideai/tickets/76/issue.md @@ -0,0 +1,40 @@ +--- +id: "c91f547f-96f0-4c9c-9fde-dbe7d2772dbc" +number: 76 +title: "Appairage : la comparaison du code est sensible à la casse côté serveur" +status: "closed" +priority: "low" +sprint: null +links: [{"target":"#75","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784277540034 +updatedAt: 1784287679986 +version: 2 +--- +## Constat + +`crates/web-server/src/lib.rs:1612` compare le code d'appairage reçu au code attendu par égalité stricte : + +```rust +if request.code != state.pairing_code() { … } +``` + +Or `new_pairing_code()` (ligne 2220) génère le code en majuscules. Un client qui envoie `3f7a9c21` au lieu de `3F7A9C21` est donc rejeté. + +## Pourquoi c'est une dette et pas un bug bloquant + +Le fix #75 rend l'écran web tolérant en normalisant la saisie en majuscules avant `POST /api/pair`. La tolérance vit donc **côté client** : tout autre client de l'API (curl, un futur client mobile natif, un test manuel) échouera encore sur une saisie minuscule, avec un message d'erreur qui n'explique pas pourquoi. + +## Attendu + +La comparaison serveur normalise la casse (et les espaces) avant égalité. La normalisation côté UI de #75 devient alors redondante mais inoffensive — pas besoin de la retirer. + +## Vigilance + +La comparaison doit rester en temps constant si elle l'est aujourd'hui : normaliser ne doit pas introduire de court-circuit exploitable en timing. À vérifier lors de l'implémentation. + +## Périmètre + +Backend pur, `crates/web-server`. \ No newline at end of file diff --git a/.ideai/tickets/77/carnet.md b/.ideai/tickets/77/carnet.md new file mode 100644 index 0000000..6366e1d --- /dev/null +++ b/.ideai/tickets/77/carnet.md @@ -0,0 +1,153 @@ +--- +issueRef: "#77" +version: 5 +updatedBy: {"kind":"user"} +updatedAt: 1784449075576 +--- +# Carnet #77 — cadrage UX + Architecture (2026-07-17) + +Le design produit est dans la description du ticket. Ce carnet porte le **cadrage**, validé UX puis Architect. UX est passée avant Architect (la forme conditionnait les DTO). + +--- + +## Arbitrages Main + +**Affichage du code — groupement visuel uniquement.** UX proposait `AB12-CD34`. **Refusé.** Le code réel n'a pas de séparateur, et la normalisation frontend de #75 supprime les espaces mais **pas les tirets** : l'utilisateur recopierait le tiret et l'appairage échouerait. Groupement en deux blocs de 4 par espacement typographique/CSS, valeur copiée = `AB12CD34`. + +**#76 est absorbé dans ce lot** (Architect, confirmé Main) : normalisation serveur (espaces, tirets, puis uppercase) avant comparaison. La sécurité ne doit pas dépendre de la normalisation frontend. + +**`already_used` n'est pas exposé** (tranché par Architect, périmètre sécurité). L'API répond `invalid_or_expired` pour code absent, expiré, déjà consommé, remplacé ou incorrect — pas d'oracle pour un attaquant. Les logs internes peuvent distinguer ; le DTO public jamais. + +**TTL du code : 10 minutes** (UX, confirmé Architect). + +--- + +## Spec UX — surface « Appareils » + +Écran unique, mobile-first, monté dans **l'UI web ET l'app desktop**. Navigation : `Paramètres → Appareils`. Même écran, même vocabulaire, mêmes actions des deux côtés. + +**Liste** — par ligne : nom (niveau principal), badge `Cet appareil` pour la session courante, dernière activité (`Actif à l'instant`, `Aujourd'hui à 14:32`, `Hier`, puis date courte), date d'appairage en secondaire discret. **Jamais** de User-Agent brut, jamais d'IP. + +**Appairer** — action primaire en haut. Panneau : code grand et lisible, bouton `Copier`, `Saisissez ce code sur le nouvel appareil.`, `Expire dans 10 min`. À expiration : `Ce code a expiré.` + bouton `Générer un nouveau code`, sans alarmisme. Après succès : `Nouvel appareil appairé`, liste rafraîchie, highlight temporaire 2-3 s. + +**Nom d'appareil** — saisi sur le **nouvel appareil** au moment de l'appairage, prérempli par dérivation lisible (`iPhone`, `Chrome sur Windows`), obligatoire, 1-40 caractères, renommable ensuite via le menu `⋯`. + +**Révocation** — menu `⋯` → `Révoquer`. Confirmation : `Révoquer cet appareil ?` / `Il devra être appairé à nouveau pour accéder à IdeA.` Si c'est l'appareil courant : `Vous serez déconnecté immédiatement.` puis coupure et redirection vers l'écran d'appairage. `Révoquer tous les appareils` en bas, confirmation forte, n'exige pas de lire la liste, inclut l'appareil courant. + +**Erreurs** — `Code invalide ou expiré.` (incorrect/expiré/utilisé, message unique) ; `Trop de tentatives. Réessayez dans quelques minutes avec un nouveau code.` + +--- + +## Cadrage Architecture + +### Persistance — sous-domaine d'accès mono-utilisateur + +- **domain** : `PairedDevice`, `DeviceId`, `SessionTokenHash`, `DeviceName`, `PairingCode`. +- **application** : `PairDevice`, `ListDevices`, `RenameDevice`, `RevokeDevice`, `RevokeAllDevices`, `AuthenticateSession`, `TouchDevice`. +- **port** : `DeviceSessionStore` — **adapter** : `FsDeviceSessionStore`. + +Store dans `{app_data_dir}/security/devices.json`, **pas dans `.ideai/` projet** : ce sont des accès à l'instance, pas des données du repo. + +```json +{ "version": 1, "devices": [ { "deviceId": "uuid", "name": "…", "pairedAt": "…", "lastSeenAt": "…", "sessionTokenHash": "sha256:…" } ] } +``` + +**Token** : 32 octets CSPRNG. Hash disque `SHA-256("idea-session-v1\0" || token_bytes)`, hex préfixé par l'algo, **comparaison constant-time**. + +**Pas d'Argon2id** — décision Architect, et elle est juste : ce n'est pas un mot de passe faible mais un secret aléatoire 256-bit. Un KDF lent n'ajoute rien contre la préimage et coûte du CPU serveur à chaque requête. Le point dur reste : jamais de token en clair sur disque. + +**DTO UI** : `{ deviceId, name, pairedAt, lastSeenAt, isCurrentDevice }`. Pas d'IP, pas de User-Agent. + +### Code éphémère + +**Ne va pas dans le store persistant** — reste en mémoire process, mais sort du `pairing_code: String` statique de `ServerState`. État : `{ code_hash, expires_at, generation_id, used }`. Consommation atomique `consume(code, now) -> Valid | InvalidOrExpired`. Générer invalide toujours le précédent. Sans `--new-code`, **aucun code n'existe au boot**. + +Desktop Tauri : commande appelant le **même use case via le composition root, pas via HTTP**. + +À supprimer : `EmbeddedServerHandle.pairing_code` / `EmbeddedServerStatusDto.pairing_code` comme source permanente. + +### Révocation → WebSockets (durcissement n°4) + +Le domaine publie `DeviceRevoked { device_id }` / `AllDevicesRevoked`. Le web-server (adapter transport) tient un `ActiveConnectionRegistry: device_id -> Vec`. `AuthenticateSession` retourne `AuthenticatedDevice { device_id, token_hash }` — **pas un booléen** — pour que `run_ws_connection` s'enregistre sous le bon `device_id`. + +Séquence : le use case supprime du store → publie l'événement → l'adapter observe → ferme les WS concernés via un canal `shutdown` que la boucle sélectionne en parallèle de `read_ws_frame`, puis unregister `ws_pty_bridge` comme aujourd'hui. `OutputBridge` reste un outil de flux PTY, **jamais le mécanisme d'autorisation**. + +### Rate-limit + +Port `PairAttemptLimiter` dans l'application, appelé par `PairDevice` **avant** validation du code. Adapter prod `InMemoryPairAttemptLimiter`, adapter test à horloge fixe. Clé `RateLimitKey { origin, route }` — **pas de headers HTTP dans le port**, la résolution proxy reste dans l'adapter. Repères : 5 échecs/min par origine, 30 échecs/min global. + +### Cookie + +`HttpOnly`, `SameSite=Strict`, `Secure` selon config existante, `Path=/`, `Max-Age≈34560000` (400 j). **Renouvellement glissant** sur toute requête authentifiée réussie. Seule la révocation invalide. `lastSeenAt` **throttlé** (≤ 1 écriture / 5 min / appareil). + +### `--new-code` + +Bool dans `ServerArgs`. Après `ServerState` créé et listener bindé : génération + impression **uniquement dans ce cas**. Ne touche pas le store, ne crée pas d'appareil, ne change aucune config. + +--- + +## Lots + +| Lot | Contenu | Dépend de | +|---|---|---| +| **B1** | `DeviceSessionStore`, hash tokens, `PairDevice {code, name}`, cookie Max-Age + renouvellement, normalisation serveur (#76) | — (**bloquant**) | +| **B2** | Code éphémère, `POST /api/pairing-code`, `--new-code`, suppression impression au boot, TTL/usage unique/invalidation, `invalid_or_expired` | B1 | +| **B3** | Endpoints list/rename/revoke/revoke-all/logout, event `DeviceRevoked`, registre WS par `device_id`, fermeture immédiate + cleanup `OutputBridge` | B1 | +| **B4** | Port `PairAttemptLimiter`, adapter mémoire + tests horloge, réponse `rate_limited` | parallèle à B2 | +| **F1** | `POST /api/pair {code, name}`, normalisation frontend espaces **et tirets**, erreurs | contrat B1 | +| **F2** | Écran Appareils partagé web/desktop | DTO figés | + +Écart UX ↔ Architecture : aucun sur la forme des DTO. + +--- + +## État d'avancement (2026-07-17) + +- **B1 — vert, vérifié par Main** (le rapport de DevBackend a été perdu par un timeout de rendez-vous ; les tests ont été rejoués indépendamment). `cargo test -p domain -p application -p infrastructure -p web-server` intégralement vert. Couverture réelle constatée : store (round-trip, fichier absent, JSON corrompu), `session_token_hash_is_prefixed_sha256_and_verifies_constant_time`, `authenticated_invoke_renews_session_cookie_max_age`, `pairing_code_normalization_removes_spaces_hyphens_and_uppercases`, `touch_device_is_throttled_to_five_minutes`, validation du nom. +- **F1 + F2 — verts sur mock**, 91 fichiers / 841 tests. `tsc --noEmit` à 0. Garde-fous `no-direct-invoke` et `desktop-only` verts. `test:bundle-transport` confirme `dist` = tauri, `dist-web` = http. +- **B2 + B4 — en cours.** +- **B3 — à lancer.** + +--- + +## Contrats figés par Main après F1/F2 + +F1/F2 ont été livrés avant B3. DevFrontend a dû déduire des contrats que le cadrage ne nommait pas ; **je les fige tels quels**, ils sont cohérents avec les routes déjà nommées par Architect. **B3 s'y conforme** — si divergence, c'est le backend qui s'aligne, pas le frontend. + +### Routes REST + +| Route | Usage | +|---|---| +| `GET /api/devices` | liste | +| `POST /api/devices/{id}/rename` | renommage | +| `POST /api/devices/{id}/revoke` | révocation d'un appareil | +| `POST /api/devices/revoke-all` | révocation totale | +| `POST /api/pairing-code` | génération d'un code | +| `POST /api/pair` | `{code, name}` | + +Raisonnement retenu (DevFrontend, validé Main) : la surface d'authentification ne peut pas vivre sur le RPC générique `/api/invoke`, qui est lui-même auth-gated. Elle vit donc sur des routes dédiées, comme `/api/pair` et `/api/logout` aujourd'hui. + +### Commandes Tauri + +`list_devices`, `create_pairing_code`, `rename_device`, `revoke_device`, `revoke_all_devices`. + +### Notification d'appairage — polling, pas d'event + +UX demande « nouvel appareil appairé → liste rafraîchie + highlight ». **Décision Main : pas d'event `DevicePaired` côté B3.** L'UI poll `listDevices()` toutes les 3 s **uniquement pendant que le panneau de code est ouvert** (≤ 10 min, jamais en régime permanent), pour un acte rare. Un event domaine routé jusqu'au WS live serait plus propre en théorie, mais ajoute une surface et du code client pour un gain invisible. Option notée, non retenue. + +### Choix de sécurité frontend à préserver + +**Le message d'erreur du serveur n'est jamais réaffiché** : l'UI mappe sur le code (`invalid_or_expired`, `rate_limited`) vers les libellés figés. Si un backend écrivait un jour « code déjà utilisé » dans `message`, un pont qui forwarde réintroduirait l'oracle que le ticket interdit. Le mapping rend la fuite structurellement impossible plutôt que d'en faire une affaire de discipline. **Un test verrouille ce point — ne pas le contourner.** + +### Écart B1 → corrigé en B2 + +`new_session_token()` concaténait deux UUID v4 au lieu d'un CSPRNG (aucune crate `rand` dans `web-server`). Pas une faille — UUID v4 tire de `getrandom`, 244 bits effectifs — mais écart au cadrage sur un chemin de sécurité, et construction que le prochain lecteur devrait re-vérifier pour se rassurer. Correction demandée dans B2. + +### Hors périmètre, consigné ailleurs + +- Langue mélangée des sections Settings desktop (« Appareils » vs « AI Profiles », « Deployment ») → **#78**, décision UX de fond (règle de langue de l'UI, éventuel i18n). +- `ConfirmDialog` laissée locale à la feature plutôt qu'ajoutée au design system : décision UX/Architect, pas un effet de bord de ce ticket. + +### Dette de topologie à traiter par Git + +**B1, F1 et F2 ont été implémentés directement sur `develop`**, sans branche de feature — erreur de cadrage de Main, qui a lancé les devs sans passer par Git. Le travail est sain et non commité. **Git doit ranger ça avant tout commit**, sachant que `develop` porte déjà 37 commits non poussés. diff --git a/.ideai/tickets/77/issue.md b/.ideai/tickets/77/issue.md new file mode 100644 index 0000000..25582cb --- /dev/null +++ b/.ideai/tickets/77/issue.md @@ -0,0 +1,77 @@ +--- +id: "eecf77ee-dfcb-436c-aa5f-0c7033c7bdfa" +number: 77 +title: "Appairage : appareils enregistrés persistants, révocables, et code éphémère à usage unique" +status: "closed" +priority: "high" +sprint: null +links: [{"target":"#76","kind":"relatesTo"},{"target":"#75","kind":"relatesTo"},{"target":"#68","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1784279214815 +updatedAt: 1784449075576 +version: 5 +--- +## Besoin utilisateur + +Deux constats live (téléphone, instance exposée derrière reverse proxy) : + +1. Le code d'appairage est redemandé **à chaque ouverture de la page**. Impraticable. +2. Le code étant affiché sur la machine, un appairage est impossible quand l'utilisateur n'est pas chez lui. + +## Cause racine du #1 — trois couches, confirmées + +- **Le cookie de session n'a ni `Max-Age` ni `Expires`** (`crates/web-server/src/lib.rs:1629`). C'est un cookie de session au sens navigateur : détruit à la fermeture. Sur mobile, purge agressive en arrière-plan → réappairage quasi systématique. **C'est la cause directe.** +- **Les sessions vivent en mémoire** (`sessions: Mutex>`, ligne 422). Un redémarrage désappaire tous les appareils, même avec un cookie persistant. +- **Le code d'appairage est régénéré à chaque démarrage** (`new_pairing_code()` appelé dans `with_core`, ligne 437). Le code noté hier ne vaut plus rien. + +## Design validé (discussion utilisateur ↔ Main, 2026-07-17) + +### Appareils + +- Un **appareil** appairé est persistant, nommé, listable, révocable. Session valide **indéfiniment jusqu'à révocation**. +- Le **serveur est la source de vérité** de la durée de vie. Le cookie n'est qu'un porteur, renouvelé à chaque visite (le plafond navigateur réel est ~400 jours côté Chrome — « indéfini » se tient côté serveur, pas côté cookie). +- Vocabulaire figé : **« appareils »**, jamais « utilisateurs ». Ce design est **mono-utilisateur assumé** : tous les appareils ont les pleins pouvoirs, pas de comptes, pas de mots de passe, pas de permissions différenciées. Le jour où plusieurs personnes seront nécessaires, ce sera un autre chantier — le vocabulaire ne doit pas avoir menti entre-temps. + +### Code d'appairage + +- **Éphémère, à usage unique, généré à la demande.** TTL court (5-10 min à arbitrer). Générer un nouveau code invalide le précédent. +- **Suppression du code statique au démarrage**, y compris l'`eprintln!("IdeA pairing code: …")` (ligne 619) : un secret permanent qui part dans stderr/journald/toute capture de sortie. Après ce lot, aucun secret au repos — un code n'existe que quand il est demandé. +- Génération : depuis l'**app desktop** (serveur embarqué, accès direct à l'état) et depuis l'**UI web déjà appairée**. +- **Pas de commande CLI de génération, pas de socket de contrôle, pas de store partagé entre processus.** Décision explicite : supprime l'IPC, la concurrence d'écriture CLI↔serveur et la classe de bugs associée. +- **Flag `--new-code`** au lancement du serveur headless : affiche un code au démarrage. C'est le **bootstrap du premier lancement** et la **trappe de secours** quand plus aucune UI n'est accessible. Le flag ne doit **rien rendre persistant** (laissé par mégarde dans une unit systemd, il ne doit pas transformer chaque redémarrage en distribution de code). +- Le code réapparaît dans stderr avec `--new-code` : risque résiduel accepté, car TTL court + usage unique rendent sans valeur un log qui fuite plus tard. + +### Condition de validité du design + +**L'écran de gestion des appareils doit vivre dans l'UI web, pas seulement dans l'app desktop.** Sinon, sur une installation headless, générer un code impose un redémarrage — donc couper PTY, agents en cours et WebSockets — à chaque nouvel appareil. Avec l'écran dans l'UI web, la boucle se ferme : `--new-code` donne le premier appareil, tout le reste se gère depuis cet appareil, et `--new-code` redevient une trappe de secours. + +### Accès distant — trou assumé + +Le design ne couvre **pas** l'appairage d'un appareil neuf à distance : générer un code suppose une UI appairée ou un accès à la machine. Le filet est **SSH** (se connecter, relancer avec `--new-code`). Assumé consciemment, acceptable tant que les appareils persistent réellement — le cas devient rare. **Passkey/WebAuthn gardée en réserve, hors périmètre de ce lot.** + +## Durcissements non négociables + +1. **TTL sur le code** — pas seulement l'usage unique. Un code généré et jamais consommé qui reste valide pour toujours recrée le problème d'aujourd'hui. +2. **Limite de tentatives sur `POST /api/pair`** — 8 caractères hex = 4,3e9 combinaisons, brute-forçable en quelques semaines à débit soutenu contre un code permanent. TTL **et** rate-limit, pas l'un ou l'autre. +3. **Tokens de session hachés sur disque, jamais en clair.** Le store `.ideai/` part dans les backups et les synchros ; en clair il donne un accès shell. À traiter comme un fichier de mots de passe. +4. **La révocation doit couper les WebSockets déjà ouverts.** Un WS établi ne revalide jamais le cookie : révoquer un appareil qui a un PTY ouvert le laisserait piloter la machine. Une révocation qui ne coupe pas la connexion vivante n'est pas une révocation. + +## Surface de gestion + +Lister les appareils (nom, date d'appairage, dernière activité), les révoquer un par un, et **révoquer tout** — pour le jour où un téléphone est perdu et où lister avant d'agir n'est pas souhaitable. + +## Points d'entrée code + +- `crates/web-server/src/lib.rs` : `ServerState` (418-482), `create_session`/`has_session`/`revoke_session` (469-484), `eprintln!` du code (619), comparaison du code (1612), pose du cookie (1629-1640), `logout_response` (1643), `new_pairing_code` (2220), `new_session_token` (2230). +- `crates/app-tauri/src/embedded_server.rs` : exposition `pairing_code` (97, 334). +- `frontend/src/features/web/PairingScreen.tsx`, `frontend/src/adapters/http/webSession.ts`. + +## DoD + +- Un appareil appairé le reste après fermeture du navigateur **et** après redémarrage du serveur. +- Aucun code d'appairage n'existe au repos ; un code demandé expire et ne sert qu'une fois. +- Révocation effective immédiatement, WebSockets vivants inclus. +- Écran de gestion accessible depuis l'UI web **et** l'app desktop. +- Validation live : téléphone appairé une fois, toujours connecté le lendemain après redémarrage de l'AppImage. \ No newline at end of file diff --git a/.ideai/tickets/78/carnet.md b/.ideai/tickets/78/carnet.md new file mode 100644 index 0000000..6a0f599 --- /dev/null +++ b/.ideai/tickets/78/carnet.md @@ -0,0 +1,70 @@ +--- +issueRef: "#78" +version: 3 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784379475877 +--- +## Décision UX — langue de l'UI IdeA + +Règle tranchée : l'UI humaine d'IdeA est en français par défaut, de façon uniforme sur toute une surface. Une même surface ne mélange pas des libellés français et anglais pour des éléments de navigation, titres, actions, états ou textes d'aide. + +Exceptions admises : noms propres de produits/protocoles (`IdeA`, `OpenAI`, `OpenCode`, `Claude Code`, `HTTPS`, `LAN`, `URL`, `IP/CIDR`, `API`, `CLI`, etc.), chemins techniques, commandes, variables, valeurs de configuration et termes backend affichés comme données. Ces éléments restent dans leur forme technique si la traduction réduit la reconnaissance ou la précision. + +Conséquence : pas d'i18n à introduire pour ce ticket. #78 est un correctif frontend pur d'alignement rédactionnel de la surface Settings desktop vers le français. L'i18n multi-langue serait un chantier produit séparé, non requis ici. + +## Libellés à renommer pour #78 + +Périmètre minimal attendu : la surface desktop `Settings` et ses sections actuellement visibles. + +- `Settings` → `Paramètres` quand le mot est affiché à l'utilisateur dans le menu, la surface ou les renvois internes. +- `Close Settings` → `Fermer les paramètres`. +- `AI Profiles` → `Profils IA` dans la navigation de sections et comme titre de panneau. +- `Configure profiles` → `Configurer les profils`. +- `No profiles configured.` → `Aucun profil configuré.`. +- `Delete` → `Supprimer` pour les actions de suppression de profil. +- `delete {name}` → `supprimer {name}` pour le libellé accessible correspondant. +- `Deployment` → `Déploiement` dans la navigation et comme titre de panneau. +- `deployment settings` → `paramètres de déploiement` pour les libellés accessibles. +- `Loading…` → `Chargement…`. +- `Server` → `Serveur`. +- `Start` → `Démarrer`. +- `Stop` → `Arrêter`. +- `Stopped` → `Arrêté`. +- `Starting…` → `Démarrage…`. +- `Running` → `En cours d'exécution`. +- `Stopping…` → `Arrêt…`. +- `Failed` → `Échec`. +- `Exposure` → `Exposition réseau` ou `Accès réseau` ; préférence UX : `Accès réseau`, plus compréhensible pour l'utilisateur. +- `exposure mode` → `mode d'accès réseau`. +- `This computer only` → `Cet ordinateur uniquement`. +- `For using IdeA on this desktop only. Remote devices cannot connect.` → `Pour utiliser IdeA uniquement sur cet ordinateur. Les appareils distants ne peuvent pas se connecter.` +- `Remote access, proxy on this computer` → `Accès distant, proxy sur cet ordinateur`. +- `Use this when your HTTPS proxy runs on the same machine as IdeA Desktop.` → `À utiliser quand le proxy HTTPS tourne sur la même machine qu'IdeA Desktop.` +- `Remote access, proxy on another machine` → `Accès distant, proxy sur une autre machine`. +- `Use this when the HTTPS proxy runs on another machine. IdeA will only accept traffic from that proxy.` → `À utiliser quand le proxy HTTPS tourne sur une autre machine. IdeA n'acceptera que le trafic provenant de ce proxy.` +- `Public origin` → `Origine publique`. +- `Example: https://idea.example.com` → `Exemple : https://idea.example.com`. +- `LAN address to bind` → `Adresse LAN d'écoute`. +- `The address on this machine that the proxy will connect to.` → `L'adresse de cette machine à laquelle le proxy se connectera.` +- `Select an address…` → `Sélectionner une adresse…`. +- `Authorized proxy IP/CIDR` → `IP/CIDR du proxy autorisé`. +- `This is not where IdeA listens. It is the machine allowed to contact IdeA.` → `Ce n'est pas l'adresse d'écoute d'IdeA. C'est la machine autorisée à contacter IdeA.` +- `If your proxy is not on this computer, choose this mode. Otherwise the proxy may time out without an IdeA error.` → `Si votre proxy n'est pas sur cet ordinateur, choisissez ce mode. Sinon, le proxy peut expirer sans erreur IdeA.` +- `Proxy setup` → `Configuration du proxy`. +- `Point your HTTPS reverse proxy at this upstream.` → `Faites pointer votre proxy inverse HTTPS vers cet upstream.` +- `copy upstream url` → `copier l'URL upstream`. +- `Copy` → `Copier`. +- `Copied` → `Copié`. +- `Complete the settings above to get the upstream URL.` → `Complétez les paramètres ci-dessus pour obtenir l'URL upstream.` +- `Pairing` → `Appairage`. +- `Pairing is managed in Settings → Appareils, where you can generate a code and revoke devices.` → `L'appairage est géré dans Paramètres → Appareils, où vous pouvez générer un code et révoquer des appareils.` + +Libellés déjà conformes : `Appareils`, `Appairer`, `Code d'appairage`, `Chargement des appareils…`, `Aucun appareil appairé.`, `Révoquer...` restent en français. + +## Critères d'acceptation UX + +- La navigation des paramètres affiche `Profils IA`, `Déploiement`, `Appareils` sans mélange anglais/français. +- Les titres, boutons, états et textes d'aide visibles dans ces sections sont en français, hors exceptions techniques listées plus haut. +- Les libellés accessibles suivent la même langue que le libellé visible correspondant. +- Aucun mécanisme i18n n'est introduit pour #78 ; il s'agit uniquement de remplacer les chaînes frontend existantes. +- Les termes `URL`, `LAN`, `IP/CIDR`, `HTTPS` et `upstream` peuvent rester tels quels car ils désignent des objets techniques reconnus par la cible utilisateur. \ No newline at end of file diff --git a/.ideai/tickets/78/issue.md b/.ideai/tickets/78/issue.md new file mode 100644 index 0000000..b83ff7c --- /dev/null +++ b/.ideai/tickets/78/issue.md @@ -0,0 +1,34 @@ +--- +id: "8d8bc65a-50f6-4fab-bb02-486b6145205f" +number: 78 +title: "Settings desktop : sections en langues mélangées (« Appareils » à côté de « AI Profiles », « Deployment »)" +status: "closed" +priority: "low" +sprint: null +links: [{"target":"#77","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784284746767 +updatedAt: 1784379475877 +version: 3 +--- +## Constat + +Relevé par DevFrontend pendant l'implémentation de #77 (F2), hors périmètre de ce ticket. + +L'écran Settings du desktop mélange les langues dans sa liste de sections : la nouvelle section **« Appareils »** (vocabulaire figé par UX pour #77, non négociable) voisine avec **« AI Profiles »** et **« Deployment »**. + +Le problème n'est pas la section ajoutée par #77 — c'est que la surface Settings n'a pas de règle de langue établie. Le français de « Appareils » est correct et cohérent avec le reste de l'UI produit (`Appairer cet appareil`, `Code d'appairage`) ; ce sont les sections préexistantes qui sont en anglais. + +## Pourquoi c'est un ticket UX et pas un fix de libellé + +Trancher demande une décision de fond qui dépasse un renommage : quelle est la langue de l'UI d'IdeA, et est-elle uniforme ou dépend-elle de la surface ? La réponse engage toutes les surfaces existantes, pas seulement Settings, et conditionne un éventuel besoin d'i18n. + +## Attendu + +UX tranche la règle de langue de l'UI, puis on aligne les sections de Settings dessus. + +## Périmètre + +Frontend pur si la décision est « tout en français » ou « tout en anglais ». Devient un chantier distinct si la réponse est « il faut de l'i18n ». \ No newline at end of file diff --git a/.ideai/tickets/79/carnet.md b/.ideai/tickets/79/carnet.md new file mode 100644 index 0000000..c799c7a --- /dev/null +++ b/.ideai/tickets/79/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#79" +version: 2 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784379476483 +--- diff --git a/.ideai/tickets/79/issue.md b/.ideai/tickets/79/issue.md new file mode 100644 index 0000000..4456d88 --- /dev/null +++ b/.ideai/tickets/79/issue.md @@ -0,0 +1,47 @@ +--- +id: "05a70dc5-fd28-4c58-8ba1-be6058ae3cfc" +number: 79 +title: "Test flaky : PermissionsPanel « saves project defaults » échoue par intermittence" +status: "closed" +priority: "low" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784287450082 +updatedAt: 1784379476483 +version: 2 +--- +## Constat + +Relevé par DevFrontend pendant #77, **sans rapport avec ce ticket**. + +`frontend/src/features/permissions/permissions.test.tsx` → `PermissionsPanel > saves project defaults` échoue par intermittence sur une exécution complète de la suite : + +``` + FAIL src/features/permissions/permissions.test.tsx > PermissionsPanel > saves project defaults + Test Files 1 failed | 90 passed (91) + Tests 1 failed | 847 passed (848) +``` + +## Pourquoi ce n'est pas une régression de #77 + +Vérifié au moment du constat : + +- `git diff HEAD -- src/features/permissions` est **vide** — le lot #77 n'a pas touché cette surface. +- Le test **passe seul**. +- Le passage complet suivant est **vert** (848/848), sans modification entre les deux. +- Durée du test : ~1371 ms — sensible au timing. + +## Pourquoi ça mérite un ticket quand même + +Un test qui passe deux fois sur trois est pire qu'un test rouge : il apprend à l'équipe à relancer plutôt qu'à lire. Il finira par mordre en CI, où l'on ne relance pas toujours, et il érode la confiance dans une suite dont ce chantier a montré qu'elle est le principal garde-fou. + +## Attendu + +Identifier la source du non-déterminisme (timers, attente implicite, état partagé entre tests) et rendre le test déterministe. **Ne pas le neutraliser ni augmenter un timeout pour le faire taire** : si le comportement testé est réellement dépendant du timing, c'est le comportement qu'il faut regarder. + +## Périmètre + +Frontend, tests. \ No newline at end of file diff --git a/.ideai/tickets/8/carnet.md b/.ideai/tickets/8/carnet.md new file mode 100644 index 0000000..3a5bc66 --- /dev/null +++ b/.ideai/tickets/8/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#8" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783310831918 +--- diff --git a/.ideai/tickets/8/issue.md b/.ideai/tickets/8/issue.md new file mode 100644 index 0000000..152c340 --- /dev/null +++ b/.ideai/tickets/8/issue.md @@ -0,0 +1,15 @@ +--- +id: "80a751de-3c20-485e-b810-148862a71d45" +number: 8 +title: "Création de ticket via agent IA" +status: "closed" +priority: "medium" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783167694310 +updatedAt: 1783310831918 +version: 6 +--- +j'aimerais que l'écriture des tickets puisse se faire assisté par un agent IA. Dans l'edition du ticket, j'aimerais qu'on ai la possibilité d'ouvrir une ocnversation. Dans cette conversation on pourrait choisir l'un des profile IA d'IdeA. Ici, la fenetre de chat serait entièrement fournit par l'UI d'IdeA (donc pas une CLI). La session serait propre au ticket, l'agent n'aurait aucun droit d'écriture de code ou d'edition de fichier, il ne pourrait que lire les fichiers du dossier et editer le ticket actuel. Le but serait vraiment pour l'utilisateur de pouvoir discuter de sa feature avec l'agent et que l'agent en tire ce qui est necessaire. Je pense que le mieux serait que IdeA fournisse le context de cet agent, stockée chez IdeA, et qu'a l'ouverture du chat, un agent temporaire soit invoqué avec ce contexte et que la conversation se fasse via les commandes headless. \ No newline at end of file diff --git a/.ideai/tickets/80/carnet.md b/.ideai/tickets/80/carnet.md new file mode 100644 index 0000000..42e9d46 --- /dev/null +++ b/.ideai/tickets/80/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#80" +version: 1 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784287697560 +--- diff --git a/.ideai/tickets/80/issue.md b/.ideai/tickets/80/issue.md new file mode 100644 index 0000000..bb4bcec --- /dev/null +++ b/.ideai/tickets/80/issue.md @@ -0,0 +1,56 @@ +--- +id: "f36ada98-0ca1-41e6-94c1-24eb81731fee" +number: 80 +title: "L'environnement de QA ne peut pas ouvrir de socket — il ne peut pas valider les features réseau" +status: "open" +priority: "medium" +sprint: null +links: [{"target":"#77","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784287697560 +updatedAt: 1784287697560 +version: 1 +--- +## Constat + +Relevé pendant #77, confirmé indépendamment par Main et par Git. + +L'agent QA exécute ses tests dans un environnement dont le sandbox **interdit les binds réseau** : + +```text +failed to bind 127.0.0.1:0: Operation not permitted (os error 1) +bind: Os { code: 1, kind: PermissionDenied, message: "Operation not permitted" } +Failed building the Runtime: Os { code: 24, kind: Uncategorized, message: "Too many open files" } +``` + +Sur #77, QA a rapporté : + +- `cargo test -p web-server` → 73 passed / **6 failed** (2 binds + 4 tests WebSocket en cascade), +- `cargo test -p domain -p application -p infrastructure -p web-server` → 275 passed / **10 failed** (tous `infrastructure::session::openai_compat`). + +**Les mêmes suites passent intégralement hors de ce sandbox** : 79/79 sur `web-server`, exit 0 sur la commande combinée (vérifié par Main, puis rejoué par Git dans son propre environnement). DevBackend observe le même artefact de son côté. + +## Pourquoi c'est un problème et pas une curiosité + +Le cœur de #77 était précisément **la révocation de WebSockets vivants** — un durcissement de sécurité non négociable. QA ne pouvait structurellement pas le valider de bout en bout : l'environnement qui doit prouver que la feature marche est celui qui ne peut pas l'exécuter. + +Le coût réel se paie deux fois : + +1. **Faux rouges** : QA a bloqué son verdict sur des échecs qui n'existaient pas, et il a fallu du temps à Main pour établir que c'était l'environnement. C'était le bon réflexe de sa part — le problème n'est pas QA, c'est son bac à sable. +2. **Faux verts potentiels**, plus grave : un test réseau qui ne s'exécute jamais chez QA ne peut pas y échouer non plus. La prochaine feature réseau rejouera la scène, et rien ne garantit qu'on la relira aussi attentivement. + +## Attendu + +Que l'environnement de QA puisse ouvrir des sockets sur la loopback, ou à défaut que la limitation soit **explicite et connue** (QA sait ce qu'il ne peut pas valider, et le dit dans son verdict au lieu de le rapporter en échec). + +À regarder aussi : le `Too many open files` (`code: 24`), qui suggère une limite de descripteurs de fichiers trop basse en plus de la restriction réseau. + +## Note + +Voir la mémoire `permissions-sandbox-system-state` pour l'état du système de permissions/sandbox et le risque résiduel déjà documenté. + +## Périmètre + +Infrastructure d'agents / configuration de sandbox. Pas de code applicatif. \ No newline at end of file diff --git a/.ideai/tickets/81/carnet.md b/.ideai/tickets/81/carnet.md new file mode 100644 index 0000000..cc48892 --- /dev/null +++ b/.ideai/tickets/81/carnet.md @@ -0,0 +1,223 @@ +--- +issueRef: "#81" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784565918809 +--- +# Cadrage — ticket #81 + +## Objectif + +Permettre aux agents IdeA d'administrer les templates d'agents via MCP, sans surface humaine nouvelle : créer, éditer et supprimer des templates globaux IdeA depuis des tools `idea_*`. + +UX n'est pas concerné pour ce ticket : il n'y a pas d'écran, de workflow humain ni de libellés UI à concevoir. La seule surface est le catalogue MCP exposé aux agents. + +## État réel du système templates + +Surfaces inspectées : + +- Domaine : `crates/domain/src/template.rs` +- Port : `TemplateStore` dans `crates/domain/src/ports.rs` +- Use cases : `crates/application/src/template/usecases.rs` +- Store : `crates/infrastructure/src/store/template.rs` +- Commands Tauri : `crates/app-tauri/src/commands.rs` +- DTO : `crates/backend/src/dto.rs` / `crates/app-tauri/src/dto.rs` +- Frontend existant : `frontend/src/features/templates/*`, `frontend/src/adapters/template.ts` +- MCP + permissions #82 : `crates/infrastructure/src/orchestrator/mcp/tools.rs`, `server.rs`, `backend/src/openai_tools.rs`, `app-tauri/src/openai_tools.rs` + +Templates existants : + +```text +AgentTemplate { + id: TemplateId, + name: String, + content_md: MarkdownDoc, + version: TemplateVersion, + default_profile_id: ProfileId, +} +``` + +Invariants existants : + +- `name` non vide ; +- `version` démarre à `1` ; +- `version` est bumpée par `AgentTemplate::with_updated_content`, donc par changement de contenu Markdown ; +- `default_profile_id` sert aux futurs agents créés depuis le template. + +Stockage existant : global app-data, pas projet : + +```text +/templates/ +├── index.json +└── md/.md +``` + +`index.json` porte les métadonnées (`id`, `name`, `version`, `contentHash`, `defaultProfileId`) ; le Markdown vit dans `md/.md`. Le store est déjà derrière `TemplateStore`, donc Tauri-agnostique côté infra. + +## Synchronisation template → agents + +Les agents créés depuis un template copient le `content_md` dans leur propre contexte `.md` projet et gardent dans le manifeste : + +- `template_id` +- `synchronized` +- `synced_template_version` + +`UpdateTemplate` actuel met à jour le template global, bump la version et publie `DomainEvent::TemplateUpdated`. Il ne modifie pas directement les agents existants. + +`DetectAgentDrift` compare `template.version > synced_template_version` pour les agents `synchronized == true` et publie `AgentDriftDetected`. + +`SyncAgentWithTemplate` est l'opération explicite qui remplace le `.md` de l'agent synchronisé par le contenu courant du template et met à jour `synced_template_version`. + +Conclusion : si un agent édite un template via MCP, les agents synchronisés qui l'utilisent ne sont pas impactés immédiatement. Ils deviennent en drift jusqu'à appel explicite de sync. Au lancement suivant, ils relisent leur `.md` agent existant, pas automatiquement le template global. Ce comportement doit rester tel quel pour #81, sauf arbitrage produit séparé. + +## Tools MCP à ajouter + +Ajouter une surface templates explicite ; `idea_skill_read`, `idea_context_read` et les tools existants ne couvrent pas les templates. Les templates ne sont ni des skills ni des contextes agent. + +Proposition de catalogue : + +### Lecture, autorisée par défaut (#82) + +- `idea_template_list` + - Rôle : lister les templates globaux disponibles. + - Payload conseillé : templates complets ou résumés. Pour l'ergonomie agent, retour complet acceptable au premier lot (`id`, `name`, `contentMd`, `version`, `defaultProfileId`) car `ListTemplates` renvoie déjà les entités complètes. + +- `idea_template_read` + - Rôle : lire un template par id. + - Input : `{ "templateId": "..." }` + - Retour : même DTO qu'un template Tauri. + - Nécessite un thin use case `ReadTemplate` ou un provider qui appelle `TemplateStore::get` via un use case applicatif dédié. + +### Écriture/action, refusée par défaut (#82) + +- `idea_template_create` + - Input : `{ name, content, defaultProfileId }` + - Réutilise `CreateTemplate`. + - Retour : template créé. + +- `idea_template_update` + - Input minimal aligné existant : `{ templateId, content }`. + - Réutilise `UpdateTemplate` actuel, qui met à jour le contenu et bump la version. + - Extension recommandée si on veut couvrir pleinement "éditer" : accepter aussi `name?` et `defaultProfileId?`, avec bump de version seulement si `content` change. Cela nécessite d'étendre le domaine/use case car aujourd'hui l'UI elle-même ne persiste pas le changement de nom/profil en mode edit. + - Option de sûreté à arbitrer : ajouter `expectedVersion` pour éviter les écrasements concurrents entre agents. Le store/use case Tauri actuel n'a pas d'optimistic concurrency sur templates, donc ce serait une extension de contrat, pas une simple exposition MCP. + +- `idea_template_delete` + - Input : `{ templateId }` + - Réutilise `DeleteTemplate`. + - Effet existant : supprime de l'index global ; le fichier Markdown orphelin peut rester sur disque car le port FS n'a pas de delete. Les agents créés depuis ce template gardent leur `.md`; drift detection ignore le template absent. + +## Intégration obligatoire avec #82 + +#82 a introduit la classification canonique dans `crates/infrastructure/src/orchestrator/mcp/tools.rs` : + +- `READ_ONLY_TOOLS` +- `WRITE_ACTION_TOOLS` +- `tool_access` +- test garde-fou `catalogue_tools_have_explicit_read_or_write_access` + +Tout tool ajouté au catalogue doit être classé immédiatement, sinon le test doit échouer. + +Classification #81 attendue : + +```text +READ_ONLY_TOOLS += [ + "idea_template_list", + "idea_template_read", +] + +WRITE_ACTION_TOOLS += [ + "idea_template_create", + "idea_template_update", + "idea_template_delete", +] +``` + +Comportement policy attendu : + +- agent sans override #82 : peut lire/lister les templates, ne peut pas créer/éditer/supprimer ; +- agent avec override incluant un ou plusieurs tools `idea_template_*` d'écriture : peut seulement appeler ceux explicitement autorisés ; +- le refus doit arriver avant effet applicatif, sur le serveur MCP stdio et sur l'invoker OpenAI-compatible. + +Ce comportement est cohérent avec la demande #82 : par défaut seuls les tools de lecture sont autorisés. Pas d'arbitrage produit nécessaire sauf si l'utilisateur veut que certains agents aient une permission template-write préconfigurée par défaut. + +## Point d'implémentation recommandé + +Ne pas ajouter ces opérations au `OrchestratorCommand` sauf nécessité. Les templates sont une famille CRUD applicative, comme les tickets, pas un protocole d'orchestration inter-agent. Le pattern le plus local est donc de créer un provider MCP dédié, parallèle à `TicketToolProvider` : + +- `crates/infrastructure/src/orchestrator/mcp/templates.rs` + - `TemplateToolProvider` + - `TemplateToolError` + - `is_template_tool(name)` + - `catalogue()` des tools templates + +- `crates/infrastructure/src/orchestrator/mcp/tools.rs` + - étendre `catalogue()` avec `templates::catalogue()` ; + - ajouter `is_template_tool` ; + - ajouter les 5 tools dans les classifications #82 ; + - si besoin, faire retourner `tool_returns_reply` pour les read/create/update/list/delete selon convention (delete peut retourner un ACK JSON). + +- `crates/infrastructure/src/orchestrator/mcp/server.rs` + - ajouter `template_tools: Option>` ; + - dans `tools_call`, après enforcement #82 et avant `map_tool_call`, router `is_template_tool` vers le provider, comme les tickets ; + - garder l'enforcement durable/éphémère avant provider. + +- Composition root backend/app-tauri + - créer `LateBoundTemplateToolProvider` si besoin pour casser les cycles comme `LateBoundTicketToolProvider` ; + - binder un `AppTemplateToolProvider` construit avec `create_template`, `list_templates`, `update_template`, `delete_template` et le nouveau `read_template` si ajouté ; + - injecter ce provider dans `McpServer::new(...).with_template_tools(...)` ; + - injecter aussi dans `AppOpenAiToolInvoker` pour parité OpenAI-compatible. + +Alternative possible : ajouter des variants `OrchestratorCommand::Template*` et mapper les tools via `map_tool_call`. Je ne la recommande pas en premier choix : cela gonfle l'orchestrateur avec un CRUD global qui n'est pas une coordination agent-agent, alors que le précédent ticket système a déjà accepté le pattern provider pour les tickets. + +## Lots proposés + +### Lot B1 — Catalogue MCP + classification #82 + provider squelette + +- Ajouter `templates.rs` côté MCP infra. +- Ajouter les 5 tool defs. +- Classer immédiatement `idea_template_list/read` en lecture et `create/update/delete` en écriture/action. +- Tests : garde-fou catalogue/classification vert ; `tools/list` expose les templates selon policy #82. + +### Lot B2 — Use cases/DTO/provider templates + +- Ajouter `ReadTemplate` use case fin si on garde `idea_template_read`. +- Implémenter `AppTemplateToolProvider` avec les use cases existants. +- Mapper erreurs : `notFound`, `invalid`, `store`, `internal`. +- Tests provider : create/list/read/update/delete sur fakes, version bump sur update contenu. + +### Lot B3 — Enforcement policy + OpenAI-compatible parity + +- Vérifier MCP stdio : un agent default read-only peut `idea_template_list/read`, mais `idea_template_create/update/delete` est refusé avant provider. +- Vérifier override #82 : autoriser seulement `idea_template_update` n'autorise pas create/delete. +- Répliquer le dispatch provider dans `AppOpenAiToolInvoker`, avec même policy durable avant effet. + +### Lot B4 — Édition complète optionnelle + +À faire seulement si le produit veut que "éditer" couvre autre chose que le contenu Markdown : + +- étendre `UpdateTemplateInput` avec `name?`, `defaultProfileId?`, `content?` ; +- préserver l'invariant : bump version uniquement quand `content_md` change ; +- décider si changement de `defaultProfileId` doit avoir un effet sur agents existants (recommandation : non, seulement futurs `CreateAgentFromTemplate`) ; +- ajouter éventuellement `expectedVersion` pour update/delete, avec erreur de conflit. + +## Frontières + +Frontend/UX : hors périmètre. Aucune surface humaine nouvelle. + +Domaine templates : réutiliser `AgentTemplate`, `TemplateVersion`, `TemplateStore`. Extension domaine seulement si B4 est retenu. + +Projet `.ideai/agents.json` : hors périmètre pour CRUD templates. Les agents créés depuis template et leur sync existante restent inchangés. + +Synchronisation automatique : hors périmètre. #81 ne doit pas auto-écraser les `.md` des agents synchronisés après update template ; laisser `DetectAgentDrift`/`SyncAgentWithTemplate` piloter cela. + +Permissions #82 : dans le périmètre obligatoire. Aucun tool template ne doit entrer dans le catalogue sans classification explicite et sans enforcement identique MCP stdio/OpenAI-compatible. + +## Critères d'acceptation + +- Les agents voient les tools template dans le catalogue MCP selon leur policy #82. +- Les tools lecture template sont accessibles à un agent sans override. +- Les tools create/update/delete sont refusés à un agent sans override, avant mutation. +- Une policy agent qui autorise un write template permet uniquement ce write. +- `idea_template_update` bump la version quand le contenu change et publie `TemplateUpdated` via le use case existant. +- Les agents synchronisés au template passent en drift, mais ne sont pas modifiés tant qu'un sync explicite n'est pas demandé. +- Les chemins MCP stdio et OpenAI-compatible ont la même sémantique et les mêmes refus. \ No newline at end of file diff --git a/.ideai/tickets/81/issue.md b/.ideai/tickets/81/issue.md new file mode 100644 index 0000000..e688bf7 --- /dev/null +++ b/.ideai/tickets/81/issue.md @@ -0,0 +1,16 @@ +--- +id: "e9c936c5-4540-42ab-a482-c64e6f9b9f8c" +number: 81 +title: "MCP d'edition de templates" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784296848609 +updatedAt: 1784565918809 +version: 5 +--- +J'aimerais que les agents aient la possibilité de créer, supprimer et editer des templates d'agent IdeA \ No newline at end of file diff --git a/.ideai/tickets/82/carnet.md b/.ideai/tickets/82/carnet.md new file mode 100644 index 0000000..9705e4c --- /dev/null +++ b/.ideai/tickets/82/carnet.md @@ -0,0 +1,383 @@ +--- +issueRef: "#82" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784410760329 +--- +# Cadrage — ticket #82 + +## Objectif produit + +Donner à chaque agent IdeA une policy explicite d'utilisation des tools MCP IdeA. Par défaut, un agent ne doit pouvoir appeler que les tools de lecture. L'utilisateur doit ensuite pouvoir accorder ou retirer des permissions agent par agent. La surface d'édition est laissée à UX/frontend et n'est pas dans le lot backend de base. + +## État réel du code + +Surfaces inspectées : + +- `crates/infrastructure/src/orchestrator/mcp/tools.rs` : catalogue MCP actuel, 25 tools exposés. +- `crates/infrastructure/src/orchestrator/mcp/server.rs` : `tools/call` applique déjà une policy optionnelle avant dispatch (`tool_policies.get(requester)` puis `enforce_tool_policy`). +- `crates/domain/src/agent_tool_policy.rs` : modèle existant `AgentToolPolicy { allow, bound_issue, deny_others }`. +- `crates/infrastructure/src/orchestrator/mcp/policy.rs` : `ToolPolicyRegistry` in-memory par requester. +- `crates/application/src/ticket_assistant.rs` : l'assistant de ticket pose une allowlist éphémère bornée à un ticket. +- `crates/backend/src/openai_tools.rs` : l'invoker OpenAI-compatible expose le même catalogue et dispatch via `OrchestratorService`, mais ne consulte pas la policy avant appel. +- `crates/domain/src/permission.rs` + `crates/infrastructure/src/store/permission.rs` : système permissions/sandbox existant, persisté dans `.ideai/permissions.json`, orienté fichiers/commandes/sandbox/projections CLI. + +Conclusion : il existe déjà un point d'application côté MCP stdio, mais pas de policy durable par agent et pas de default-deny pour les tools d'écriture. Le modèle existant est réutilisable comme base conceptuelle, mais il est actuellement trop spécialisé pour les assistants de ticket et stocké en mémoire. + +## Classification lecture / écriture des tools MCP IdeA + +Lecture autorisée par défaut : + +- `idea_list_agents` +- `idea_context_read` +- `idea_memory_read` +- `idea_skill_read` +- `idea_workstate_read` +- `idea_ticket_read` +- `idea_ticket_list` +- `idea_ticket_read_carnet` +- `idea_sprint_list` + +Écriture / action / exécution à refuser par défaut : + +- `idea_ask_agent` : délégation active, peut lancer/réattacher une cible et produire des effets indirects. +- `idea_run_in_background` : exécute une commande et crée une tâche. +- `idea_launch_agent` : lance/attache une session agent. +- `idea_stop_agent` : tue une session. +- `idea_update_context` : écrit le contexte d'un agent. +- `idea_context_propose` : écrit/propose du contexte ; avec `target` agent, c'est une écriture directe du `.md` agent. +- `idea_memory_write` : écrit la mémoire projet. +- `idea_workstate_set` : écrit la ligne live-state du requester. +- `idea_create_skill` : crée un skill. +- `idea_ticket_create` +- `idea_ticket_update` +- `idea_ticket_update_status` +- `idea_ticket_update_priority` +- `idea_ticket_update_carnet` +- `idea_ticket_link` +- `idea_ticket_unlink` + +Règle de maintenance : la classification doit vivre à côté du catalogue MCP, pas dans l'UI, pour que tout nouveau tool doive choisir explicitement `read` ou `write/action`. + +## Cause racine / besoin architectural + +Aujourd'hui, l'absence de policy pour un requester signifie "autorisé" sur le serveur MCP, parce que l'enforcement ne s'exécute que si `registry.get(requester)` retourne une policy. C'était acceptable pour l'ancien cas ticket-assistant, où la policy éphémère était posée avant ouverture. Ce n'est pas acceptable pour des agents IdeA généraux : l'absence de configuration doit se résoudre en policy par défaut lecture seule. + +Le besoin n'est pas couvert par Landlock/sandbox : Landlock protège le système de fichiers et l'exécution de commandes au niveau OS/PTY/structured process. Les tools MCP IdeA sont des capabilities applicatives internes (`memory`, `context`, `tickets`, `delegation`, `workstate`, etc.) qui doivent être refusées avant dispatch applicatif. Un sandbox peut empêcher certains effets externes, mais ne sait pas qu'un appel `idea_memory_write` ou `idea_ask_agent` est interdit. + +## Modèle de données proposé + +Créer une policy MCP durable par projet, sparse par agent, distincte du modèle `PermissionSet` fichiers/commandes. + +Option recommandée : nouveau document `.ideai/mcp-tool-permissions.json` plutôt qu'étendre `.ideai/permissions.json`. + +Raison : `.ideai/permissions.json` a un sens précis et déjà chargé : permissions de fichiers/commandes + projection CLI + compilation Landlock. Mélanger les tools MCP avec ces capabilities risquerait de rendre confus un modèle qui n'a pas le même point d'application ni la même sémantique de sandbox. + +Schéma conceptuel : + +```text +ProjectMcpToolPermissions { + version: 1, + projectDefault: McpToolPolicy? // absent => default lecture seule canonique + agents: Vec +} + +AgentMcpToolPolicyOverride { + agentId: AgentId, + policy: McpToolPolicy +} + +McpToolPolicy { + allowedTools: Vec, // noms exacts du catalogue MCP + deniedTools: Vec?, // optionnel si UX veut exprimer un retrait explicite + mode: AllowListed | ReadOnlyPlus // à arbitrer avec UX, mais backend peut commencer simple +} +``` + +Simplification backend acceptable pour un premier lot : stocker directement `allowedTools` par agent, et résoudre ainsi : + +1. si override agent existe, il remplace le défaut projet ; +2. sinon si défaut projet existe, l'utiliser ; +3. sinon utiliser `READ_ONLY_TOOLS` canonique ; +4. refuser tout tool absent de l'allowlist effective. + +Le modèle doit valider les noms contre le catalogue connu ou au minimum rejeter les chaînes vides/dupliquées. Les tools inconnus ne doivent pas devenir des permissions latentes silencieuses. + +## Point d'application de la policy + +Point principal : `McpServer::tools_call`, avant tout dispatch, exactement à l'endroit où l'enforcement éphémère existe déjà aujourd'hui. + +À faire : + +- remplacer/compléter `registry.get(requester)` par une résolution effective : requester handshake -> `AgentId` -> policy durable projet -> fallback lecture seule ; +- si le requester est vide/legacy `"mcp"`, appliquer aussi le fallback lecture seule, ou refuser les écritures fail-closed ; +- refuser via erreur MCP lisible (`isError`/JsonRpcError cohérent) avant `TicketToolProvider` et avant `OrchestratorService::dispatch` ; +- publier éventuellement `OrchestratorRequestProcessed { ok: false }` pour garder une trace UI/diagnostic des refus, sans exécuter le tool. + +Point secondaire obligatoire pour éviter le contournement : `AppOpenAiToolInvoker` doit appliquer la même résolution avant `map_tool_call`/`dispatch`. C'est le chevauchement direct avec #62 : #62 demande déjà la parité OpenAI-compatible + identité requester explicite. #82 dépend fonctionnellement de ce point ; sinon un agent utilisant un profil OpenAI-compatible pourrait contourner la policy MCP stdio. + +## Relation avec #62 et #60 + +#60 a fermé le bug de conversation muette et a sorti #62 comme dette sécurité adjacente. + +#62 reste pertinent et doit être traité avant ou dans le premier lot de #82 : + +- passer une identité requester explicite aux sessions structurées ; +- `LaunchAgent` doit utiliser l'agent id comme requester, pas une dérivation fragile du run dir ; +- l'assistant de ticket doit garder `ticket-assistant::` ; +- l'invoker OpenAI-compatible doit consulter la même policy que le serveur MCP stdio. + +#82 généralise ensuite la policy à tous les agents déclarés, avec un défaut lecture seule durable. Les policies éphémères du ticket-assistant peuvent rester comme cas spécial plus restrictif/borné à un ticket, mais elles ne doivent pas masquer la policy globale agent si un assistant normal est lancé. + +## Frontières avec le système permissions/sandbox existant + +À ne pas faire dans #82 : + +- ne pas modifier la compilation Landlock ; +- ne pas ajouter de `Capability::McpTool` dans le modèle fichiers/commandes sans arbitrage Architecture ; +- ne pas projeter cette policy dans les settings Claude/Codex ; +- ne pas compter sur les prompts natifs des CLIs pour autoriser/refuser les tools IdeA. + +Le système existant reste responsable de ce que le process agent peut faire au niveau OS. #82 est une policy applicative IdeA, appliquée côté serveur/bridge avant use case. + +## Découpage recommandé + +### Lot B1 — Domaine + catalogue + store durable + +- Ajouter un modèle pur `McpToolPermissionPolicy` / `ProjectMcpToolPermissions` avec fallback lecture seule. +- Déplacer la classification read/write dans une source backend canonique proche du catalogue MCP. +- Ajouter un port `McpToolPermissionStore` et un store FS sous `.ideai/mcp-tool-permissions.json`. +- Tests domaine : fallback lecture seule, override agent, refus tool inconnu, nouveau catalogue sans classification explicite détecté par test. + +### Lot B2 — Enforcement MCP stdio + +- Injecter le resolver/store dans `McpServer` ou dans un service de policy appelé par `tools_call`. +- Appliquer la policy avant ticket provider et avant orchestrator dispatch. +- Remplacer le comportement "pas de policy => tout passe" par "pas de policy => lecture seule" pour les agents généraux. +- Garder la policy ticket-assistant bornée au ticket comme restriction éphémère additionnelle ou cas de requester dédié. +- Tests : agent sans override peut `idea_memory_read`/`idea_ticket_list`, mais pas `idea_memory_write`, `idea_ask_agent`, `idea_ticket_update_carnet`, `idea_run_in_background`. + +### Lot B3 — Parité OpenAI-compatible / dépendance #62 + +- Faire passer l'identité requester explicite jusqu'à tous les appels tools structurés. +- Brancher la même policy resolver dans `AppOpenAiToolInvoker`. +- Tests : un profil OpenAI-compatible refusé sur `idea_memory_write` l'est de la même manière que via MCP stdio ; un tool lecture passe. + +### Lot B4 — API backend pour future UI + +- Ajouter des use cases read/update de permissions MCP par agent/projet. +- Ajouter DTO/commands Tauri ou endpoints web selon la surface existante. +- Ne pas concevoir l'UI ici ; seulement exposer un contrat stable à UX/DevFrontend. + +### Lot UX/F — séparé + +- UX décide la surface de modification agent par agent. +- Frontend consomme les APIs B4. + +## Rétrocompatibilité + +Agents déjà déclarés : aucun champ à ajouter dans `agents.json`. En absence de document `.ideai/mcp-tool-permissions.json` ou d'override agent, ils deviennent lecture seule pour les tools MCP IdeA. C'est un changement volontaire demandé par l'utilisateur. + +Attention migration : des workflows existants qui s'appuient sur `idea_ask_agent`, `idea_memory_write`, `idea_context_propose`, `idea_workstate_set` ou `idea_ticket_update_carnet` devront être explicitement autorisés agent par agent après livraison. Pour limiter la casse pendant le développement, prévoir un message de refus clair indiquant le tool refusé et l'agent/requester concerné. + +## Critères d'acceptation backend + +- Un agent sans override ne peut appeler que les tools listés en lecture. +- Les tools d'écriture/action sont refusés avant effet applicatif. +- Une allowlist agent permet explicitement un tool d'écriture choisi. +- Un retrait/absence d'allowlist retire effectivement le droit au prochain appel, sans relancer l'application si possible. +- Le serveur MCP stdio et l'invoker OpenAI-compatible appliquent la même décision. +- Les permissions filesystem/commandes et le sandbox Landlock restent inchangés. + +## Conception UX/F — surface permissions MCP par agent + +### Décision de placement + +La modification des permissions MCP IdeA vit dans le panneau projet `Permissions`, pas dans `Settings` et pas uniquement dans la fiche d'un agent. + +Raison UX : ce réglage est une matrice de capacités applicatives par agent dans le projet courant. Il doit être consultable et comparable au même endroit que les permissions/sandbox existantes, sans polluer les paramètres globaux desktop (`Settings`) ni cacher un droit critique dans une fiche agent isolée. La fiche/liste d'agent peut afficher un raccourci ou un badge, mais l'édition canonique reste `Permissions`. + +Le panneau `Permissions` devient une surface à deux onglets internes : + +- `Système` : permissions fichiers/commandes/sandbox existantes. +- `Tools MCP IdeA` : nouveau réglage #82. + +La colonne de gauche reste le sélecteur de cible : `Défaut projet`, puis les agents. Le panneau de droite change selon l'onglet sélectionné. + +### Layout attendu + +```text +Permissions +[ Système ] [ Tools MCP IdeA ] [Actualiser] + +┌──────────────────────────────┬──────────────────────────────────────────────┐ +│ Défaut projet │ Tools MCP IdeA — DevFrontend │ +│ Lecture seule │ Hérite du défaut projet │ +│ │ [Utiliser le défaut projet v] │ +│ Agents │ │ +│ Main Hérité │ Résumé effectif │ +│ Architect Override │ 9 lecture autorisés · 2 écriture autorisés │ +│ DevFrontend Override │ │ +│ QA Hérité │ Accord rapide │ +│ Git Hérité │ [ ] Déléguer à un agent │ +│ │ [x] Modifier les tickets │ +│ │ [ ] Écrire la mémoire │ +│ │ │ +│ │ Détail des tools │ +│ │ ▾ Lecture, autorisés par défaut (9) │ +│ │ ✓ idea_ticket_read │ +│ │ ✓ idea_context_read │ +│ │ ▾ Écriture et actions (16) │ +│ │ Tickets │ +│ │ [x] idea_ticket_update_carnet │ +│ │ [ ] idea_ticket_update_status │ +│ │ Agents │ +│ │ [ ] idea_ask_agent │ +│ │ [ ] idea_launch_agent │ +│ │ [Réinitialiser l'override] [Enregistrer] │ +└──────────────────────────────┴──────────────────────────────────────────────┘ +``` + +Sur desktop large : deux colonnes comme le panneau permissions actuel, avec la liste des cibles à gauche et l'éditeur à droite. Sur largeur contrainte : la cible sélectionnée reste au-dessus de l'éditeur, puis les groupes de tools s'empilent ; les actions restent en bas du panneau, non flottantes. + +### Modèle mental affiché + +L'utilisateur ne manipule pas une liste brute de 25 cases. Il voit trois niveaux : + +1. Cible : `Défaut projet` ou un agent précis. +2. Mode : `Utiliser le défaut projet` ou `Override personnalisé`. +3. Capabilités groupées : lecture, tickets, agents, contexte, mémoire, workstate, skills, exécution. + +Pour un agent sans override, l'éditeur est en lecture de l'état hérité jusqu'à ce que l'utilisateur choisisse `Créer un override`. Les contrôles hérités sont visibles mais atténués, avec la mention `Hérité du défaut projet`. Cela permet de comprendre l'état effectif avant de modifier. + +Pour un agent avec override, le badge de la colonne gauche affiche `Override`. Le panneau de droite affiche `Override personnalisé` et un bouton `Réinitialiser l'override` qui remet l'agent sur le défaut projet. + +### Présentation du catalogue + +Les tools sont groupés par domaine fonctionnel, à partir des métadonnées du catalogue retourné par `get_mcp_tool_permissions` si disponibles côté backend, sinon par mapping frontend local strictement présentationnel. La classification `read`/`write` reste backend-canonique. + +Groupes UX recommandés : + +- `Lecture projet` : `idea_list_agents`, `idea_context_read`, `idea_memory_read`, `idea_skill_read`, `idea_workstate_read`. +- `Lecture tickets` : `idea_ticket_read`, `idea_ticket_list`, `idea_ticket_read_carnet`, `idea_sprint_list`. +- `Délégation agents` : `idea_ask_agent`, `idea_launch_agent`, `idea_stop_agent`. +- `Contexte et mémoire` : `idea_update_context`, `idea_context_propose`, `idea_memory_write`. +- `Tickets` : `idea_ticket_create`, `idea_ticket_update`, `idea_ticket_update_status`, `idea_ticket_update_priority`, `idea_ticket_update_carnet`, `idea_ticket_link`, `idea_ticket_unlink`. +- `Travail et exécution` : `idea_run_in_background`, `idea_workstate_set`. +- `Skills` : `idea_create_skill`. + +Chaque groupe affiche un compteur : `3/7 autorisés`, et peut être replié/déplié. Les groupes lecture sont ouverts par défaut dans `Défaut projet`, mais repliés par défaut dans l'édition agent pour réduire le bruit. Les groupes écriture/action sont ouverts par défaut, car ce sont les décisions à risque. + +Chaque ligne de tool contient : + +- le nom exact monospace (`idea_ticket_update_carnet`) ; +- un libellé humain court (`Modifier le carnet d'un ticket`) ; +- un badge `Lecture` ou `Écriture` ; +- un état `Autorisé`, `Refusé`, ou `Hérité` ; +- une case à cocher uniquement quand la cible est éditable. + +Les checkboxes sont réservées aux tools individuels. Les groupes utilisent un bouton discret `Tout autoriser dans ce groupe` / `Tout retirer dans ce groupe`, jamais une checkbox tri-state ambiguë. + +### Défaut projet vs overrides agent + +Le `Défaut projet` est le point de départ appliqué à tous les agents sans override. Son état initial est `Lecture seule` : tous les tools classifiés lecture sont autorisés, tous les tools écriture/action sont refusés. + +Pour un agent, afficher explicitement : + +- `Hérite du défaut projet` si aucun override n'existe. +- `Override personnalisé` si une allowlist agent existe. +- `Diffère du défaut : +2 écriture, -1 lecture` quand l'API permet de comparer l'allowlist effective au défaut. + +Dans les lignes de tool agent : + +- un tool hérité autorisé affiche une coche grisée + `Hérité` ; +- un tool ajouté par override affiche une coche active + badge `Ajouté` ; +- un tool retiré par override affiche une case vide + badge `Retiré` si le backend expose une notion de retrait par remplacement complet ; sinon afficher simplement l'état effectif `Refusé`. + +Important : comme l'API `update_agent_mcp_tool_permissions` accepte une allowlist complète de noms de tools, l'UI doit traiter l'override agent comme un remplacement de l'état effectif, pas comme une série de patches implicites. Au moment où l'utilisateur crée un override depuis l'état hérité, la draft est préremplie avec l'allowlist effective courante. + +### Parcours principal — accorder un tool d'écriture + +1. L'utilisateur ouvre le panneau `Permissions` depuis la barre de panneaux projet. +2. Il sélectionne l'onglet `Tools MCP IdeA`. +3. Il clique l'agent cible dans la colonne gauche, par exemple `DevFrontend`. +4. Si l'agent hérite du défaut, il clique `Créer un override`. La liste devient éditable et reprend l'état effectif actuel. +5. Il ouvre le groupe concerné, par exemple `Tickets`. +6. Il coche `idea_ticket_update_carnet — Modifier le carnet d'un ticket`. +7. Le résumé en haut passe à `9 lecture autorisés · 1 écriture autorisé` et une barre d'actions affiche `Modifications non enregistrées`. +8. Il clique `Enregistrer`. +9. Après succès, le badge de l'agent passe à `Override` et la ligne du tool affiche `Ajouté`. + +### Parcours principal — retirer un tool d'écriture + +1. L'utilisateur sélectionne un agent avec badge `Override`. +2. Il ouvre le groupe contenant le tool autorisé. +3. Il décoche le tool d'écriture. +4. Le résumé et le compteur du groupe se mettent à jour immédiatement dans la draft. +5. Il clique `Enregistrer`. +6. Si l'override devient identique au défaut projet, proposer après sauvegarde de le nettoyer avec une action secondaire `Supprimer l'override inutile`. Ne pas le faire automatiquement sans retour visuel. + +### Défaut projet + +Le défaut projet est éditable dans le même onglet, mais avec une friction légère pour les tools d'écriture/action : quand l'utilisateur active un tool d'écriture au niveau défaut projet, afficher une confirmation inline avant sauvegarde : + +`Ce tool sera autorisé pour tous les agents sans override. Confirmer cette modification ?` + +Cette confirmation ne bloque pas l'édition agent par agent, car le cas utilisateur principal est d'accorder des tools d'écriture spécifiques à un agent donné. + +### États et feedback + +- Chargement : skeleton compact dans la colonne cible et dans les groupes, pas de spinner plein écran. +- Erreur de chargement : message inline en haut du panneau avec bouton `Réessayer`. +- Erreur de sauvegarde : conserver la draft locale, afficher l'erreur au-dessus des actions, garder `Enregistrer` disponible. +- Aucune agent : état vide dans la colonne gauche `Aucun agent dans ce projet.` ; le défaut projet reste éditable. +- Tool inconnu dans une allowlist existante : afficher dans un groupe `Tools inconnus` avec badge `Inconnu`, désactivé par défaut, et demander à DevFrontend de ne pas permettre de ré-enregistrer silencieusement une permission inconnue comme si elle était valide. Si le backend rejette les inconnus, afficher l'erreur telle quelle. +- Modifications non enregistrées : actions `Annuler` et `Enregistrer` visibles dans l'éditeur ; changement de cible avec draft modifiée demande confirmation. +- Sauvegarde réussie : feedback discret `Permissions enregistrées` pendant environ 2 secondes. + +### Accessibilité + +- Les onglets `Système` / `Tools MCP IdeA` utilisent `role="tablist"`, `role="tab"`, `aria-selected` et navigation clavier gauche/droite. +- Chaque groupe repliable expose un bouton avec `aria-expanded` et un nom incluant le compteur, par exemple `Tickets, 1 sur 7 autorisé`. +- Chaque checkbox a un label complet incluant le libellé humain et le nom du tool, par exemple `Modifier le carnet d'un ticket, idea_ticket_update_carnet`. +- Les badges couleur (`Lecture`, `Écriture`, `Hérité`, `Override`) ne doivent jamais être le seul signal : le texte doit porter l'information. +- Cibles tactiles et souris : 32 px minimum pour les lignes compactes, 40 px pour les actions principales. +- Focus visible sur onglets, lignes de cible, boutons de groupe, checkboxes et actions. + +### Ton et libellés + +Conformément à la règle UX #78, les libellés humains sont en français. Les noms exacts des tools restent en anglais/monospace car ce sont des identifiants techniques. + +Libellés principaux : + +- `Permissions` +- `Système` +- `Tools MCP IdeA` +- `Défaut projet` +- `Lecture seule` +- `Hérite du défaut projet` +- `Override personnalisé` +- `Créer un override` +- `Réinitialiser l'override` +- `Modifications non enregistrées` +- `Annuler` +- `Enregistrer` +- `Autorisé` +- `Refusé` +- `Hérité` +- `Ajouté` +- `Retiré` + +### Critères d'acceptation UX/frontend + +- Les permissions MCP sont éditables depuis le panneau projet `Permissions`, onglet `Tools MCP IdeA`. +- L'utilisateur peut sélectionner `Défaut projet` ou un agent dans une colonne/listing de cibles. +- Un agent sans override affiche clairement qu'il hérite du défaut projet, et ses contrôles ne deviennent éditables qu'après `Créer un override`. +- Un agent avec override est identifiable dans la liste par un badge textuel `Override`. +- Les ~25 tools ne sont pas affichés comme une liste plate : ils sont groupés par domaine, avec compteurs et sections repliables. +- Les tools lecture et écriture/action sont distingués par badges textuels et par hiérarchie visuelle. +- Le parcours d'autorisation d'un tool d'écriture agent par agent prend au maximum : ouvrir `Permissions`, onglet `Tools MCP IdeA`, choisir l'agent, créer/éditer l'override, cocher le tool, enregistrer. +- Le retrait d'un tool d'écriture existant est symétrique : décocher puis enregistrer. +- Les changements non sauvegardés sont visibles et protégés lors d'un changement de cible. +- L'UI consomme `get_mcp_tool_permissions`, `update_project_mcp_tool_permissions`, `update_agent_mcp_tool_permissions` sans hardcoder la classification lecture/écriture comme source de vérité métier. +- Aucun changement de backend ou d'i18n n'est requis pour ce lot frontend. \ No newline at end of file diff --git a/.ideai/tickets/82/issue.md b/.ideai/tickets/82/issue.md new file mode 100644 index 0000000..019a349 --- /dev/null +++ b/.ideai/tickets/82/issue.md @@ -0,0 +1,16 @@ +--- +id: "4904e380-b9a8-49d4-a06f-032024909be4" +number: 82 +title: "Ajouter des permissions d'utilisations des tools MCP d'IdeA par agent" +status: "closed" +priority: "critical" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784296923284 +updatedAt: 1784410760329 +version: 6 +--- +J'aimerais avoir la possibilité de modifier les permissions d'utilisation des outils MCP IdeA de mes agents IdeA. J'aimerais que par défaut seuls les outils MCP de lectures soient autorisés, puis j'aimerais avoir un moyen (a faire determiner par l'agent UX), de modifier ces permissions agent par agent, et autoriser ou enlever des permissions sur les tools MCP proposés par IdeA \ No newline at end of file diff --git a/.ideai/tickets/83/carnet.md b/.ideai/tickets/83/carnet.md new file mode 100644 index 0000000..476b35c --- /dev/null +++ b/.ideai/tickets/83/carnet.md @@ -0,0 +1,222 @@ +--- +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. \ No newline at end of file diff --git a/.ideai/tickets/83/issue.md b/.ideai/tickets/83/issue.md new file mode 100644 index 0000000..6f09831 --- /dev/null +++ b/.ideai/tickets/83/issue.md @@ -0,0 +1,16 @@ +--- +id: "18a92510-3af4-4aef-84e8-e23cd7422118" +number: 83 +title: "Popup pour vérifier la volonté de quitter l'app" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784360573747 +updatedAt: 1784567952845 +version: 5 +--- +Dans le cas ou un travail est en cours, j'aimerais qu'une popup qui demande la confirmation qu'on veut quitter IdeA malgré le travail en cours, apparaisse \ No newline at end of file diff --git a/.ideai/tickets/84/carnet.md b/.ideai/tickets/84/carnet.md new file mode 100644 index 0000000..703b2c2 --- /dev/null +++ b/.ideai/tickets/84/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#84" +version: 1 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784377833773 +--- diff --git a/.ideai/tickets/84/issue.md b/.ideai/tickets/84/issue.md new file mode 100644 index 0000000..ab9f99d --- /dev/null +++ b/.ideai/tickets/84/issue.md @@ -0,0 +1,20 @@ +--- +id: "beab1811-2fd7-4563-ac39-6a354fbd1feb" +number: 84 +title: "[Bug] Reprise auto après limite de session ne se déclenche pas pour l'orchestrator (Main, profil Claude)" +status: "open" +priority: "high" +sprint: null +links: [{"target":"#7","kind":"relatesTo"},{"target":"#15","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784377833773 +updatedAt: 1784377833773 +version: 1 +--- +Rapporté par l'utilisateur (2026-07-18) sur l'AppImage courante (buildée depuis develop, contient #7 baseline + #30 fix chemin direct Main) : quand l'orchestrator (Main) atteint une limite de session sur un profil Claude, la reprise automatique au reset n'a PAS lieu en pratique, alors que la feature #7 (mergée d7041c5, 2026-06-17) et le fix #30 dédié précisément au cas "Main qui limite session" (commit 9430c65, 2026-07-13, "brancher le handle de limite sur le chemin direct") sont censés couvrir ce cas. + +Écart constaté : design/tests verts ≠ comportement live observé par l'utilisateur. À investiguer : soit régression depuis 9430c65, soit un chemin non couvert par les tests d'intégration existants (session_limit_wiring.rs), soit une limite du "EN MEMOIRE uniquement" (mémoire session-limit-handling-design : le réveil auto ne joue que tant qu'IdeA reste ouvert — si l'utilisateur a fermé/rouvert IdeA entre la limite et le reset, c'est le comportement attendu, pas un bug). + +Première question à trancher par Architect : est-ce une vraie régression du chemin direct Main, ou un cas hors-couverture connu (IdeA fermé/rouvert pendant la fenêtre de reset) ? Nécessite de faire préciser à l'utilisateur les conditions exactes de repro (IdeA resté ouvert ou non pendant l'attente du reset). \ No newline at end of file diff --git a/.ideai/tickets/85/carnet.md b/.ideai/tickets/85/carnet.md new file mode 100644 index 0000000..e4aad97 --- /dev/null +++ b/.ideai/tickets/85/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#85" +version: 1 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784379436323 +--- diff --git a/.ideai/tickets/85/issue.md b/.ideai/tickets/85/issue.md new file mode 100644 index 0000000..3e4dd56 --- /dev/null +++ b/.ideai/tickets/85/issue.md @@ -0,0 +1,27 @@ +--- +id: "34b23044-4b82-4e7e-b999-cca7cbef7861" +number: 85 +title: "Test flaky : setCellAgent « changing the agent kills the previous PTY (Bug #3) » échoue en exécution shuffled" +status: "open" +priority: "low" +sprint: null +links: [{"target":"#79","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784379436323 +updatedAt: 1784379436323 +version: 1 +--- +Découvert en marge de #79 (DevFrontend puis confirmé par QA sur `npx vitest run --sequence.shuffle`, ~2 échecs sur 3 passages) : `src/features/layout/setCellAgent.test.tsx` → « changing the agent kills the previous PTY (Bug #3) ». + +``` +FAIL src/features/layout/setCellAgent.test.tsx +changing the agent kills the previous PTY (Bug #3) +AssertionError: expected "closeTerminal" to be called with arguments: [ 'old-session' ] +Number of calls: 0 +``` + +Vert isolément (`npx vitest run src/features/layout/setCellAgent.test.tsx` → 9/9) et vert en suite complète non-shuffled (850/850). Rouge uniquement en ordre shuffled — dépendance d'ordre/état partagé entre fichiers de test, pas un bug du code applicatif a priori (à confirmer). Sans rapport avec #78/#79, ne pas mélanger. + +Attendu : identifier la source de la dépendance d'ordre (état partagé, mock non réinitialisé, timer) et rendre le test déterministe, comme pour #79 — ne pas neutraliser ni élargir un timeout pour faire taire. \ No newline at end of file diff --git a/.ideai/tickets/86/carnet.md b/.ideai/tickets/86/carnet.md new file mode 100644 index 0000000..c40cdeb --- /dev/null +++ b/.ideai/tickets/86/carnet.md @@ -0,0 +1,336 @@ +--- +issueRef: "#86" +version: 7 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784612705899 +--- +# Conception UX — tickets et sprints dans le client web + +## Intention + +La version web doit permettre de consulter et modifier les tickets/sprints sans reproduire le shell desktop. Le client web existant est une surface autonome en colonne verticale unique : après appairage, l'utilisateur choisit un projet, puis voit l'état live et les cellules agents. #86 doit ajouter une surface de pilotage tickets/sprints cohérente avec ce modèle mobile/web, pas importer les docks, fenêtres flottantes et overlays desktop. + +Décision UX : réutiliser les contrats métier et les hooks transport-neutres autant que possible (`useTickets`, `useTicketDetail`, recherche, filtres, événements live), mais créer une présentation web dédiée. Ce n'est pas une version appauvrie en capacités, c'est une adaptation de navigation et de densité. + +La cible prioritaire web/mobile est l'intervention rapide : lire, trier, changer statut/priorité, corriger titre/description/carnet, créer un ticket simple, assigner/désassigner, déplacer dans un sprint, créer/renommer/supprimer un sprint. Les opérations complexes restent disponibles mais rangées derrière des panneaux dédiés : liens entre tickets, carnet long, gestion complète des sprints. + +## Placement dans le client web + +Dans `WebWorkspace`, après ouverture d'un projet, ajouter une navigation de projet compacte au-dessus des surfaces projet : + +```text +Projet: IdeA [Rafraîchir] +[ Live ] [ Tickets ] [ Sprints ] +``` + +- `Live` : surface actuelle `État live`, inchangée dans son rôle. +- `Tickets` : nouvelle liste et édition des tickets. +- `Sprints` : gestion des sprints. + +Sur mobile, ces onglets sont des boutons segmentés scrollables horizontalement si nécessaire. Sur desktop web large, ils restent dans la même colonne contrainte, pas de dock ni layout grid. + +Ne pas placer les tickets sous la liste des agents : ce sont deux surfaces de projet au même niveau. L'utilisateur web doit pouvoir passer de l'état live au backlog sans chercher dans une carte agent. + +## Vue Tickets — layout + +La vue `Tickets` est une pile verticale : barre d'actions, recherche/filtres compacts, liste groupée par sprint, puis écran de détail lorsqu'un ticket est ouvert. + +```text +Tickets [+ Ticket] +[Recherche…] +[Statut ▼] [Priorité ▼] [Assigné ▼] [Tri ▼] + +Sprint courant (4) +#86 medium open Ajouter tickets/sprints au web +#83 medium open Confirmation fermeture + +Sans sprint (2) +#78 low open Langue uniforme Settings + +[Charger plus] +``` + +Sur petit écran, un ticket s'affiche comme une ligne compacte à deux étages : + +```text +#86 Ajouter tickets/sprints au web +open · medium · Sprint courant · Main +``` + +La ligne entière ouvre le détail. Les badges doivent rester textuels et lisibles ; ne pas empiler 5 pastilles colorées si elles provoquent un retour ligne incohérent à 360 px. + +## Actions prioritaires tickets + +Priorité haute sur web : + +- consulter la liste ; +- rechercher et filtrer ; +- ouvrir un ticket ; +- changer statut et priorité ; +- éditer titre, description et carnet ; +- créer un ticket simple ; +- supprimer un ticket avec confirmation ; +- assigner/désassigner des agents ; +- changer le sprint d'un ticket. + +Priorité secondaire, mais à conserver si les endpoints existent : + +- gérer les liens entre tickets ; +- utiliser le `TicketPicker` adapté mobile pour lier/ajouter des tickets à un sprint ; +- copier le contexte de délégation. + +Hors périmètre recommandé pour la première livraison web : assistant IA de ticket. Le desktop peut garder cette affordance avancée ; sur mobile/web, elle ajoute du coût UI et des permissions sans être nécessaire à la demande utilisateur. + +## Création de ticket + +Le bouton `+ Ticket` ouvre un panneau plein écran mobile ou une section expansée desktop web, pas un petit formulaire inline permanent. Le formulaire est court : + +- `Titre` obligatoire ; +- `Priorité` ; +- `Sprint` avec picker ; +- `Description` optionnelle, textarea repliable ou sous le champ titre ; +- actions `Annuler` et `Créer`. + +Après création : ouvrir automatiquement le détail du ticket créé pour permettre de compléter carnet, assignations ou liens. Si l'affectation sprint échoue après création, garder le ticket créé et afficher l'erreur sans perdre le résultat. + +Libellés : `Nouveau ticket`, `Titre`, `Description`, `Priorité`, `Sprint`, `Sans sprint`, `Créer`. + +## Détail ticket sur petit écran + +Sur web/mobile, le détail ticket remplace temporairement la liste dans la colonne, au lieu d'utiliser la fenêtre flottante desktop. Navigation : bouton retour en haut. + +```text +[← Tickets] #86 [⋯] +Ajouter tickets/sprints au web +open · medium · Sprint courant + +[Résumé] +Titre +Description +[Enregistrer] + +[Statut et priorité] +Statut [open ▼] +Priorité [medium ▼] +Sprint [Sprint courant ▼] + +[Carnet] +textarea Markdown +[Enregistrer le carnet] + +[Agents assignés] +Main DevFrontend [+] + +[Liens] +relatesTo #83 [+ Lier] +``` + +Les sections du détail sont accordéons ou blocs empilés. Ouverts par défaut : `Résumé`, `Statut et priorité`, `Carnet`. Repliés par défaut : `Agents assignés`, `Liens`, `Zone dangereuse`. + +Le bouton de suppression vit dans une section `Zone dangereuse` ou dans un menu `⋯`, jamais dans la première ligne d'actions. Confirmation obligatoire : + +Titre : `Supprimer ce ticket ?` +Corps : `Le ticket {ref} sera supprimé définitivement. Cette action ne supprime pas les sprints ni les agents assignés.` +Actions : `Annuler` / `Supprimer` + +Gestion des changements non enregistrés : si l'utilisateur revient à la liste avec des modifications locales non sauvegardées, afficher la confirmation existante adaptée en français : `Des modifications ne sont pas enregistrées.` avec `Continuer l'édition`, `Ignorer`, `Enregistrer et quitter` si techniquement disponible. + +## Édition rapide + +Pour les champs à faible risque (`statut`, `priorité`, `sprint`), la modification peut être enregistrée immédiatement au changement de select, avec spinner discret sur la ligne/section. Pour les champs texte (`titre`, `description`, `carnet`), garder une action explicite `Enregistrer` afin d'éviter les sauvegardes involontaires sur mobile. + +En cas de conflit de version : afficher un message inline en haut du détail : `Ce ticket a été modifié ailleurs et rechargé. Réappliquez votre modification.` Ne pas écraser silencieusement le brouillon local. + +## Vue Sprints — layout + +La vue `Sprints` est une surface dédiée, pas un overlay plein écran au-dessus de la liste comme sur desktop. Elle est accessible depuis l'onglet projet `Sprints` et depuis le bouton `Gérer les sprints` dans la vue tickets si ce raccourci est conservé. + +```text +Sprints [+ Sprint] + +#1 Sprint courant [⋯] +4 tickets +[Voir tickets] [Ajouter tickets] + +#2 Backlog client web [⋯] +7 tickets +[Voir tickets] [Ajouter tickets] +``` + +Créer un sprint : bouton `+ Sprint`, champ `Nom du sprint`, action `Créer`. Si le nom est vide, soit désactiver `Créer`, soit reprendre le comportement desktop de nom automatique `Sprint N`; préférence UX web : désactiver tant qu'un nom n'est pas saisi, car le mobile bénéficie d'une décision explicite. + +Renommer : action dans menu `⋯`, ouvre une ligne d'édition inline dans la carte sprint ou un panneau bas sur petit écran. Actions `Annuler` / `Enregistrer`. + +Réordonner : boutons `Monter` / `Descendre` dans le menu ou dans une section `Ordre`. Pas de drag-and-drop requis sur mobile. + +Supprimer : confirmation obligatoire : + +Titre : `Supprimer le sprint « {name} » ?` +Corps : `Les tickets de ce sprint seront conservés et passeront en « Sans sprint ».` +Actions : `Annuler` / `Supprimer` + +Ajouter des tickets à un sprint : ouvrir un picker mobile de tickets, avec recherche et multi-sélection. Action finale `Ajouter {count} ticket(s)`. Les tickets déjà dans le sprint sont exclus ou affichés cochés et désactivés ; ne pas laisser l'utilisateur croire qu'ils seront ajoutés une seconde fois. + +Retirer un ticket d'un sprint : depuis la carte sprint, dans la liste compacte des tickets, action `Retirer du sprint` accessible via menu ligne. Cela ne supprime pas le ticket. + +## Pickers et popups sur mobile/web + +Les pickers desktop (`TicketPicker`, `SprintPicker`) ne doivent pas apparaître comme de petites modales centrées sur téléphone. Adapter leur chrome : + +- mobile : panneau plein écran ou bottom sheet haute, avec header fixe, recherche en haut, liste scrollable, actions en bas ; +- desktop web large : modal centrée acceptable, mais largeur limitée et focus trap ; +- toujours garder `Échap` / retour / bouton `Fermer` selon plateforme. + +Le même contenu métier peut être réutilisé : recherche, filtres, sélection simple ou multiple. Seul le contenant responsive change. + +## Cohérence avec le web existant + +Respecter les patrons #69 : + +- une seule colonne verticale dans `WebWorkspace` ; +- pas de `LayoutGrid`, docks, fenêtres flottantes desktop ou tabs projet desktop ; +- padding avec safe-area ; +- contrôles qui wrap proprement à 360 px ; +- hauteurs basées sur `dvh` pour les panneaux longs ; +- reconnect banner existante conservée au-dessus des surfaces. + +Les surfaces tickets/sprints doivent continuer à se resynchroniser sur les événements domaine via le transport web live, comme `État live` le fait déjà. En cas de reconnexion, refetch complet du snapshot/listes plutôt qu'application de deltas manqués. + +## Langue et libellés + +Conformément à la règle UX #78, les libellés humains sont en français. Les valeurs métier techniques peuvent rester celles du domaine si elles sont déjà exposées comme enums (`open`, `closed`, `medium`) seulement si leur traduction demanderait un chantier transversal ; préférence UX : afficher `Ouvert`, `Fermé`, `Faible`, `Moyenne`, `Haute`, `Critique` dans l'UI. + +Libellés principaux : + +- `Tickets` +- `Sprints` +- `Nouveau ticket` +- `Créer un ticket` +- `Gérer les sprints` +- `Nouveau sprint` +- `Créer un sprint` +- `Modifier` +- `Enregistrer` +- `Annuler` +- `Supprimer` +- `Sans sprint` +- `Charger plus` +- `Aucun ticket.` +- `Aucun sprint.` +- `Tickets liés` +- `Agents assignés` +- `Carnet` +- `Zone dangereuse` + +## États et erreurs + +- Chargement liste tickets : `Chargement des tickets…` avec spinner compact. +- Liste vide sans filtre : `Aucun ticket dans ce projet.` + bouton `Créer un ticket`. +- Liste vide avec filtres : `Aucun ticket ne correspond aux filtres.` + `Réinitialiser les filtres`. +- Chargement sprints : `Chargement des sprints…`. +- Aucun sprint : `Aucun sprint.` + bouton `Créer un sprint`. +- Erreur réseau/session : message inline en haut de la surface, avec `Réessayer`. +- Déconnexion live : conserver la bannière existante `Connexion perdue — reconnexion en cours…` ; les formulaires peuvent rester éditables, mais sauvegarder doit afficher l'erreur réelle si le transport est indisponible. +- Suppression réussie : retour à la liste, ticket/sprint retiré après événement ou refresh. + +## Accessibilité + +- Les onglets `Live`, `Tickets`, `Sprints` utilisent `role="tablist"`, `role="tab"`, `aria-selected` et navigation clavier gauche/droite. +- Chaque ticket de liste est un bouton ou lien avec nom accessible complet : `{ref}, {title}, statut {status}, priorité {priority}`. +- Les formulaires ont labels visibles ou labels accessibles explicites ; ne dépendre d'aucun placeholder comme seul label. +- Les confirmations de suppression utilisent `role="alertdialog"`, focus initial sur `Annuler`, action destructive textuelle. +- Les menus `⋯` ont un label explicite : `Actions du ticket {ref}` ou `Actions du sprint {name}`. +- Les zones scrollables longues conservent le focus et ne piègent pas la navigation clavier hors modal. + +## Critères d'acceptation UX/frontend + +- Après ouverture d'un projet web, l'utilisateur peut basculer entre `Live`, `Tickets` et `Sprints` sans shell desktop. +- La vue web reste une colonne verticale responsive et ne monte pas le layout grid/docks/floating windows desktop. +- L'utilisateur peut lister, filtrer, créer, ouvrir, modifier et supprimer des tickets depuis le web. +- L'utilisateur peut créer, renommer, réordonner, supprimer des sprints et ajouter/retirer des tickets d'un sprint depuis le web. +- L'édition ticket sur mobile se fait dans une vue détail pleine colonne avec retour explicite, pas dans une petite popup desktop. +- Les suppressions ticket/sprint demandent confirmation et précisent ce qui est supprimé ou conservé. +- Les pickers ticket/sprint sont adaptés mobile : plein écran ou bottom sheet, recherche en haut, actions claires en bas. +- Les changements texte nécessitent `Enregistrer`; les changements statut/priorité/sprint peuvent être immédiats avec feedback de sauvegarde. +- Les libellés visibles sont en français. +- L'assistant IA de ticket n'est pas requis pour la première livraison web de #86. + +--- + +# Cadrage technique — contrat web-server tickets/sprints + +## Résultat de l'audit + +Le socle applicatif tickets/sprints existe déjà dans `BackendCore` : création, lecture, liste, update, suppression, carnet, liens, assignation agent, création/liste/rename/reorder/suppression sprint, assignation/désassignation ticket→sprint. Le problème #86 n'est donc pas un manque de use case application ni de store : c'est un écart de driving adapter web. + +Côté desktop, `crates/app-tauri/src/lib.rs` enregistre les commandes UI implémentées dans `crates/app-tauri/src/tickets.rs`. Côté web, `crates/web-server/src/lib.rs` expose seulement quelques commandes dans le dispatcher `POST /api/invoke` (`health`, `list_projects`, `open_project`, `get_project_work_state`, tâches de fond). Aucune commande `ticket_*` ou `sprint_*` n'y est reconnue aujourd'hui, donc le `HttpTicketGateway` web tombe en `UNKNOWN_COMMAND`. + +Le frontend web est déjà prêt côté transport : `frontend/src/adapters/http/streamGateways.ts` contient `HttpTicketGateway`, câblé dans `frontend/src/adapters/http/index.ts`, et il appelle les mêmes noms de commandes que le desktop via `/api/invoke`. Le blocage principal est donc backend/contrat, pas layout/UI. + +## Commandes web-server à ajouter + +Rester sur le style RPC existant : ajouter des branches à `POST /api/invoke`, pas créer une nouvelle API REST `/api/tickets`. + +Tickets : + +- `ticket_create` — `{ request: { projectId, title, description?, priority?, status?, assignedAgentIds?, links? } }` → `TicketDto`. +- `ticket_read` — `{ request: { projectId, ref, includeCarnet? } }` → `TicketDto`. +- `ticket_list` — `{ request: { projectId, statuses?, priorities?, assignedAgentId?, sprintId?, text?, sort?, limit?, cursor? } }` → `TicketListDto`. +- `ticket_update` — `{ request: { projectId, ref, title?, description?, status?, priority?, assignedAgentIds?, expectedVersion } }` → `TicketDto`. Cette commande couvre l'édition statut/priorité côté UI ; les commandes séparées `idea_ticket_update_status` / `idea_ticket_update_priority` sont des tools MCP agent, pas des commandes UI Tauri. +- `ticket_delete` — `{ request: { projectId, ref } }` → vide/null. +- `ticket_read_carnet` — `{ request: { projectId, ref } }` → `TicketCarnetDto`. +- `ticket_update_carnet` — `{ request: { projectId, ref, carnet, expectedVersion } }` → `TicketDto`. +- `ticket_link` — `{ request: { projectId, ref, targetRef, kind, expectedVersion } }` → `TicketDto`. +- `ticket_unlink` — `{ request: { projectId, ref, targetRef, kind?, expectedVersion } }` → `TicketDto`. +- `ticket_assign` — `{ request: { projectId, ref, agentId, assigned, expectedVersion } }` → `TicketDto`. +- `ticket_assign_sprint` — `{ request: { projectId, ref, sprintId, expectedVersion } }` → `TicketDto`. +- `ticket_unassign_sprint` — `{ request: { projectId, ref, expectedVersion } }` → `TicketDto`. + +Sprints : + +- `sprint_create` — `{ request: { projectId, name, status? } }` → `SprintDto`. +- `sprint_list` — `{ request: { projectId } }` → `SprintListDto { items }`. +- `sprint_rename` — `{ request: { projectId, sprintId, name, expectedVersion } }` → `SprintDto`. +- `sprint_reorder` — `{ request: { projectId, orderedIds } }` → `SprintListDto { items }`. +- `sprint_delete` — `{ request: { projectId, sprintId } }` → vide/null ; le comportement existant conserve les tickets et les repasse en `Sans sprint`. + +Commandes assistant ticket proches mais hors minimum #86 : `open_ticket_chat`, `close_ticket_chat`, puis le flux `sendTicketChat`/`chat.*`. Recommandation : ne pas les inclure dans le lot initial, car la demande vise l'édition tickets/sprints et le flux chat web est une surface live distincte. + +## Factorisation nécessaire + +Les DTO publics tickets/sprints (`TicketDto`, `TicketSummaryDto`, `TicketListDto`, `TicketCarnetDto`, `SprintDto`, `SprintListDto`, request DTOs, pagination/tri/parse helpers) vivent aujourd'hui dans `crates/app-tauri/src/tickets.rs`. `web-server` ne doit pas dépendre de `app-tauri`. + +Option recommandée : déplacer le contrat pur tickets/sprints vers `backend` (`backend::dto` ou `backend::tickets`) puis faire consommer ce module par les deux driving adapters : + +- `app-tauri` garde uniquement les wrappers `#[tauri::command]` ; +- `web-server` ajoute des helpers `invoke_ticket_*` / `invoke_sprint_*` ; +- les tests de pagination/tri/conflit actuellement proches du module Tauri migrent avec la logique pure. + +Option minimale possible mais moins saine : dupliquer DTO/conversions dans `web-server`. À éviter, car cela crée deux contrats wire desktop/web à maintenir. + +## Auth et sécurité web (#77) + +Les nouvelles commandes doivent rester derrière le gate existant de `POST /api/invoke` : pairing device, cookie de session HttpOnly, contrôle d'origine/CORS, révocation device et logs sécurité. Ne pas exposer de mutation ticket/sprint en GET ni hors allowlist. + +Point à arbitrer produit/sécurité : un device web appairé pourra modifier les fichiers projet `.ideai/tickets` et `.ideai/sprints`. C'est cohérent avec la demande, mais il n'existe pas actuellement de permission fine par device lecture seule/écriture. Si cette granularité est souhaitée, c'est un chantier séparé de policy device, pas un prérequis technique au #86. + +## Événements live + +Les domain events `issue*` et `sprint*` existent déjà dans `backend::events::DomainEventDto`, et le web-server relaie les événements domaine sur websocket `event.domain` en excluant seulement `PtyOutput`. Après exposition des commandes, les vues web peuvent réutiliser les helpers frontend `isTicketEvent` / `isSprintEvent` et refaire un refetch, sans nouveau canal événementiel. + +## Tests backend attendus + +- Non authentifié : `ticket_list` et une mutation ticket/sprint retournent `UNAUTHORIZED`. +- Authentifié : `ticket_list` et `sprint_list` retournent le même shape que Tauri. +- Cycle ticket : create → read → update titre/statut/priorité → carnet → link/unlink → delete. +- Cycle sprint : create → list → rename → reorder → assign ticket → unassign ticket → delete. +- Conflit `expectedVersion` propagé en `ErrorDto` cohérent avec le desktop. +- Les commandes hors allowlist restent `UNKNOWN_COMMAND`. + +## Découpe proposée + +Lot 1 — backend/contrat web : factorisation DTO, ajout des 17 commandes `ticket_*`/`sprint_*` dans `/api/invoke`, tests auth/lecture/mutation/conflit. + +Lot 2 — surface web UX/frontend : implémenter les vues décrites ci-dessus sur le `HttpTicketGateway` existant, gérer responsive, confirmations et conflits. + +Lot 3 optionnel — assistant ticket web : exposer et finaliser le flux chat seulement si la surface assistant est explicitement demandée. diff --git a/.ideai/tickets/86/issue.md b/.ideai/tickets/86/issue.md new file mode 100644 index 0000000..b9baeab --- /dev/null +++ b/.ideai/tickets/86/issue.md @@ -0,0 +1,16 @@ +--- +id: "fd0fa9e2-cdf3-4807-919a-f562c9b21026" +number: 86 +title: "[UI] ajouter l'affichage et l'dition des tickets et des sprints à la version web" +status: "closed" +priority: "medium" +sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784449151968 +updatedAt: 1784612705899 +version: 7 +--- +J'iamerais que la version web me permette aussi d'editer les tickets, d'en ajouter et d'en supprimer, ainsi que d'editer, ajouter ou supprimer des sprints diff --git a/.ideai/tickets/87/carnet.md b/.ideai/tickets/87/carnet.md new file mode 100644 index 0000000..eee4150 --- /dev/null +++ b/.ideai/tickets/87/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#87" +version: 7 +updatedBy: {"kind":"user"} +updatedAt: 1784650196560 +--- diff --git a/.ideai/tickets/87/issue.md b/.ideai/tickets/87/issue.md new file mode 100644 index 0000000..9232917 --- /dev/null +++ b/.ideai/tickets/87/issue.md @@ -0,0 +1,16 @@ +--- +id: "0fabb0dd-9a83-4bc1-bbad-d96c794faed5" +number: 87 +title: "[UI] sur l'affichage Web, la liste des background task polluent l'affchage" +status: "closed" +priority: "high" +sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1784451329374 +updatedAt: 1784650196560 +version: 7 +--- +Sur l'affichage Web, la liste qui affiche toutes les background task polluent l'affichage. Il faudrait que la liste soit par défaut repliée de façon a n'avoir dans un premier temps que la liste des agents \ No newline at end of file diff --git a/.ideai/tickets/89/carnet.md b/.ideai/tickets/89/carnet.md new file mode 100644 index 0000000..b502d85 --- /dev/null +++ b/.ideai/tickets/89/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#89" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784661583231 +--- diff --git a/.ideai/tickets/89/issue.md b/.ideai/tickets/89/issue.md new file mode 100644 index 0000000..b08aa75 --- /dev/null +++ b/.ideai/tickets/89/issue.md @@ -0,0 +1,16 @@ +--- +id: "1f3e913f-0896-43be-818a-614e75d1fe3f" +number: 89 +title: "Pouvoir lancer le serveur au lancement IdeA" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784649907432 +updatedAt: 1784661583231 +version: 5 +--- +J'aiemrais avoir une option pour pouvoir lancer le serveur web IdeA au lancement d'IdeA \ No newline at end of file diff --git a/.ideai/tickets/9/carnet.md b/.ideai/tickets/9/carnet.md new file mode 100644 index 0000000..e66a0dd --- /dev/null +++ b/.ideai/tickets/9/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#9" +version: 6 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1783210849647 +--- diff --git a/.ideai/tickets/9/issue.md b/.ideai/tickets/9/issue.md new file mode 100644 index 0000000..1c145dd --- /dev/null +++ b/.ideai/tickets/9/issue.md @@ -0,0 +1,15 @@ +--- +id: "6ab62c34-3704-4d1c-b57c-ec42ae25ca55" +number: 9 +title: "Ne pas revert la description d'un ticket lorsque l'on change la priorité" +status: "closed" +priority: "medium" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1783168333077 +updatedAt: 1783210849647 +version: 6 +--- +Lors de l'edition d'un ticket, si l'on tape la description et que sans cliquer sur save change on change la priorité, la description ou le titre qui était en train d'être édité est revert. Il faut que pendant l'edition les modifications soient persistantes. Pour s'assurer que l'utilistateur n'oublie pas de sauvegarder, j'aimerais que si l'on clique sur "close" sans avoir sauvegardé, uen popup nous en informe pour nous demander si on veut sauvegarder avant de fermer \ No newline at end of file diff --git a/.ideai/tickets/90/carnet.md b/.ideai/tickets/90/carnet.md new file mode 100644 index 0000000..9b2f7e4 --- /dev/null +++ b/.ideai/tickets/90/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#90" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784661583085 +--- diff --git a/.ideai/tickets/90/issue.md b/.ideai/tickets/90/issue.md new file mode 100644 index 0000000..749b457 --- /dev/null +++ b/.ideai/tickets/90/issue.md @@ -0,0 +1,16 @@ +--- +id: "828e32cc-83af-4a59-99b2-d0dd3e496c5f" +number: 90 +title: "Ajouter les menus manquantes à la version Web d'IdeA" +status: "closed" +priority: "high" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784649988741 +updatedAt: 1784661583085 +version: 5 +--- +J'iamerais que les menus qui ne sont pas encore disponibles dans la version Web le deviennent (J'aimerais que tous els menus du menus "Panneaux" soient disponibles dans la version web, ainsi que le setup des profils AI du menu Settings) \ No newline at end of file diff --git a/.ideai/tickets/91/carnet.md b/.ideai/tickets/91/carnet.md new file mode 100644 index 0000000..1d4d9a4 --- /dev/null +++ b/.ideai/tickets/91/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#91" +version: 4 +updatedBy: {"kind":"user"} +updatedAt: 1784994105390 +--- diff --git a/.ideai/tickets/91/issue.md b/.ideai/tickets/91/issue.md new file mode 100644 index 0000000..3d88ef8 --- /dev/null +++ b/.ideai/tickets/91/issue.md @@ -0,0 +1,16 @@ +--- +id: "2d54c254-d2e8-44b3-ac12-3bf20f818d23" +number: 91 +title: "Sur la notification de fin de tache backend, mettre plutot les deux agents en conversation" +status: "open" +priority: "medium" +sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1784652125081 +updatedAt: 1784994105390 +version: 4 +--- +Sur la notification de fin de tache backend, mettre plutot les deux agents en conveersation et qui a lancé l'appel (par exemple Main->DevBackend) pour que ça soit un peu plus explicite \ No newline at end of file diff --git a/.ideai/tickets/92/carnet.md b/.ideai/tickets/92/carnet.md new file mode 100644 index 0000000..9ddc8d0 --- /dev/null +++ b/.ideai/tickets/92/carnet.md @@ -0,0 +1,25 @@ +--- +issueRef: "#92" +version: 9 +updatedBy: {"kind":"user"} +updatedAt: 1784993956910 +--- +## Résumé de livraison + +Backend et frontend livrés sur `feature/ticket92-opencode-provider-cloud`, mergée dans `develop` (merge `12c7d10`). + +**Backend** (commit `23a3c27`) : +- Domain : `OpenCodeProviderConfig { provider_id, model, api_key_ref: SecretRef }` sur `AgentProfile.opencode_provider`, invariant `opencode_backend_is_consistent()` (llamacpp local XOR provider cloud, jamais les deux). +- Port `SecretStore` + adaptateur `FsSecretStore` (AES-256-GCM via `ring`, clé locale `secret.key` chmod 0600) — la clé API cloud n'est jamais en clair dans `profiles.json`. +- Catalogue de providers **statique** (`application/agent/provider_catalogue.rs`) exposé via la commande Tauri `list_opencode_providers`. +- Use case `SaveOpenCodeProviderProfile` (scelle la clé API dans le `SecretStore`, mint/réutilise un `SecretRef`) exposé via `save_opencode_provider_profile`. Pas de patch partiel : la clé littérale est obligatoire à chaque appel, y compris en édition. +- `DeleteProfile` purge le secret associé si le profil supprimé porte `opencode_provider`. +- Résolution de la clé au lancement dans les deux générateurs de `opencode.json` (`infrastructure/assistant/mod.rs`, `application/agent/lifecycle.rs`) ; échec dur si secret absent. +- QA vert : `cargo build --workspace` + `cargo test --workspace -- --test-threads=1` (tests de couverture SaveOpenCodeProviderProfile/DeleteProfile ajoutés suite à un premier retour QA). + +**Frontend** (commit `c181b43`) : +- First-run wizard : segmented control Local/Cloud exclusif dans le formulaire de profil OpenCode, sous-formulaire cloud (provider → modèle en cascade, clé API masquée jamais pré-remplie), validation inline, bannière d'erreur de sauvegarde. +- En édition d'un profil cloud existant : champ clé toujours vide, bouton de sauvegarde désactivé tant que la clé n'est pas ressaisie (contrainte backend, pas de patch partiel). +- QA vert : `npm run build` + `vitest run` (952 tests, y compris un test ajouté pour le parcours d'édition suite à un retour QA). + +**Dette hors périmètre signalée par Architect, non traitée ici** : duplication de deux fonctions `opencode_config_json`/`opencode_provider_config_json` entre `infrastructure/assistant/mod.rs` et `application/agent/lifecycle.rs` — à trancher (laquelle est la vraie) dans un ticket séparé si besoin. \ No newline at end of file diff --git a/.ideai/tickets/92/issue.md b/.ideai/tickets/92/issue.md new file mode 100644 index 0000000..70e40c8 --- /dev/null +++ b/.ideai/tickets/92/issue.md @@ -0,0 +1,16 @@ +--- +id: "9ba8d2f4-49fa-4536-a38e-891f1df3e3b7" +number: 92 +title: "Configurer OpenCode avec un provider Opencode" +status: "closed" +priority: "high" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1784729443275 +updatedAt: 1784993956910 +version: 9 +--- +On peut actuellement utiliser un profile AI qui utilise OpenCode pour llamacpp. J'aimerais que l'on puisse configurer des profil AI OpenCode avec llamacpp comme actuellement, mais aussi en selectionnant un provider listé dans la commande /connect de OpenCode. Je pense que le mieux serait que lors de la création d'un profile AI OpenCode, il nous soit demandé si on souhaite utiliser LlamaCpp ou un provider OpenCode. Dans le cas d'un provider llamaCpp, on utilise la même chose qu'actuellement, dans l'autre cas on nous demande quel provider et il faudrait être capable de récupérer la liste fournie par OpenCode. Une fois le provider selectionné, il faudra que l'utilisateur puisse entrer une clé API car les providers opencodes en demadnent toujours un. Pour la partie utilisation ensuite d'opencode dans les agent, je pense que cette page peut aider: https://opencode.ai/docs/fr/cli/. On y trouve entre autre la commande pour se connecter avec opencode auth login. Le but est que la partie configuration se fasse dans les profil AI comme jusqu'à présent, et que l'utilisateur puisse directement attribuer un agent à son profile AI et le lancer sans configurer d'autre choses. OpenCode marche déjà correctement avec llamacpp, j'insiste sur le fait qu'on ne fait qu'ajouter la possibilité de configurer un provider autre que llamacpp \ No newline at end of file diff --git a/.ideai/tickets/93/carnet.md b/.ideai/tickets/93/carnet.md new file mode 100644 index 0000000..dbf3fee --- /dev/null +++ b/.ideai/tickets/93/carnet.md @@ -0,0 +1,46 @@ +--- +issueRef: "#93" +version: 3 +updatedBy: {"kind":"user"} +updatedAt: 1784934179780 +--- +# Contexte + +Test manuel demandé par l'utilisateur pour valider la conversation inter-agent : Main a appelé `idea_ask_agent(target="Context", task="...")` avec une tâche de simple confirmation d'identité/rôle. + +## Observation + +Les deux appels (identiques) ont renvoyé, à la place d'une réponse finale en langage naturel, exactement ce fragment JSON brut : + +```json +{ + "name": "idea_idea_context_read", + "arguments": {} +} +``` + +- Le nom d'outil est corrompu : préfixe `idea_` dupliqué (`idea_idea_context_read` au lieu de `idea_context_read`). +- Le fragment a la forme d'un appel d'outil (tool_use), pas d'un texte de réponse — semble être une tentative de Context de lire son contexte projet via `idea_context_read`, jamais exécutée, puis remontée telle quelle comme si c'était le Final capturé. +- Reproduit à l'identique sur 2 tentatives consécutives → pas un aléa de génération, plutôt un bug de câblage/capture. + +## Détails agent Context + +- id: `5d07e4a7-8676-4a71-aea1-5b64caefa944` +- contextPath: `agents/context.md` +- profileId: `a7037cbe-6d04-48fb-ba74-f353c93fc70a` (profil différent de celui des autres agents du manifeste, qui partagent tous `664cc20c-47b8-53ad-9351-dce3c09c0de4` — Context est le seul sur ce profil) +- origin: scratch + +## Pistes d'investigation pour Architect / DevBackend + +1. Le profil `a7037cbe-...` (LLM local léger dédié à Context, cf. mémoire `agent-context-memory-and-profile-handoff`) semble ne pas capturer correctement le "Final" du tour — le pont MCP/runtime renverrait le premier tool_use brut au lieu d'attendre la réponse texte finale. +2. Vérifier le mapping des noms d'outils exposés à ce profil : la duplication `idea_idea_*` suggère un préfixage appliqué deux fois (une fois côté définition d'outil MCP, une fois côté wrapper du profil/pont). +3. Comparer avec le comportement des autres profils (`664cc20c-...`) qui fonctionnent correctement en inter-agent, pour isoler ce qui diffère structurellement dans le pont pour un profil local/structured. +4. Voir mémoire projet `mcp-bridge-and-delegation-runtime-notes` pour les pièges déjà connus du pont MCP/délégation — possible recoupement. + +## Repro + +Depuis Main : +``` +idea_ask_agent(target="Context", task="") +``` +→ renvoie le fragment JSON ci-dessus au lieu d'une réponse. diff --git a/.ideai/tickets/93/issue.md b/.ideai/tickets/93/issue.md new file mode 100644 index 0000000..a495da2 --- /dev/null +++ b/.ideai/tickets/93/issue.md @@ -0,0 +1,27 @@ +--- +id: "40f56f7d-b0ec-4409-bf6b-c010e3003b76" +number: 93 +title: "Agent Context : réponse finale non capturée, fragment d'appel d'outil brut renvoyé via idea_ask_agent" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1784817692650 +updatedAt: 1784934179780 +version: 3 +--- +Lors d'un test de la conversation inter-agent (idea_ask_agent ciblant Context), l'appel renvoie systématiquement un fragment JSON brut au lieu d'une réponse finale exploitable : + +```json +{ + "name": "idea_idea_context_read", + "arguments": {} +} +``` + +Le nom d'outil est corrompu (préfixe "idea_" dupliqué : "idea_idea_context_read" au lieu de "idea_context_read"), et ce fragment ressemble à une tentative d'appel d'outil non exécutée, renvoyée telle quelle comme si c'était la réponse finale du modèle. + +Comportement reproduit deux fois de suite à l'identique, donc pas un aléa ponctuel. \ No newline at end of file diff --git a/.ideai/tickets/94/carnet.md b/.ideai/tickets/94/carnet.md new file mode 100644 index 0000000..e9a0650 --- /dev/null +++ b/.ideai/tickets/94/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#94" +version: 2 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784823278030 +--- diff --git a/.ideai/tickets/94/issue.md b/.ideai/tickets/94/issue.md new file mode 100644 index 0000000..8acd0be --- /dev/null +++ b/.ideai/tickets/94/issue.md @@ -0,0 +1,48 @@ +--- +id: "33a844e0-6a06-46c7-b800-d3497a7f45f4" +number: 94 +title: "Permissions OpenCode ignorées : opencode.json code en dur bash/edit=ask au lieu de projeter EffectivePermissions" +status: "closed" +priority: "high" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784821271815 +updatedAt: 1784823278030 +version: 2 +--- +## Bug + +Les permissions configurées dans IdeA pour un agent sous profil OpenCode ne sont PAS appliquées dans le `opencode.json` généré. Le bloc `permission` est codé en dur à `{"bash":"ask","edit":"ask"}` quel que soit le réglage IdeA. + +## Repro + +Config IdeA (agent OpenCode) : Read=Allow, Write=Deny, Delete=Allow, bash=Allow. +`opencode.json` généré (run dir) : `"permission":{"bash":"ask","edit":"ask"}`. +Attendu : `bash` reflète la posture bash (Allow→"allow"), `edit` reflète la posture Write (Deny→"deny"). + +Claude et Codex appliquent correctement les permissions ; seul OpenCode est touché. + +## Cause racine + +Le pattern de projection des permissions (`PermissionProjector` + `ProjectorKey::{Claude,Codex}` dans `crates/infrastructure/src/permission/`) est bypassé pour OpenCode. Les générateurs `opencode.json` codent `permission` en dur à 4 endroits : + +- `crates/application/src/agent/lifecycle.rs:2728-2734` (`opencode_config_json`, variante llamacpp) +- `crates/application/src/agent/lifecycle.rs:2814-2820` (`opencode_provider_config_json`, variante cloud) +- `crates/infrastructure/src/assistant/mod.rs:406-412` (`opencode_config_json`, doublon) +- `crates/infrastructure/src/assistant/mod.rs:494-500` (`opencode_provider_config_json`, doublon) + +Les call sites (`lifecycle.rs:2361-2378` branche `OpenCodeConfig`, et `assistant/mod.rs:247-256`) ne passent pas les `EffectivePermissions` résolues aux générateurs. + +## Mapping attendu (à confirmer par Architect) + +OpenCode `permission` = map tool→"allow"|"ask"|"deny". IdéA → OpenCode : +- `bash` ← posture bash effective +- `edit` ← posture Write effective +- Read/Delete ne sont pas exprimables dans opencode (pas de clé read/delete) — déjà enforcees par le sandbox Landlock (mémoire `permissions-sandbox-system-state`). + +## Périmètre + +Backend Rust pur. Validation possible par tests unitaires sur les générateurs (`opencode_provider_config_json` est déjà testé à lifecycle.rs:4413 et assistant/mod.rs:639) sans rebuild AppImage. Validation live = rebuild AppImage + relance IdeA. \ No newline at end of file diff --git a/.ideai/tickets/95/carnet.md b/.ideai/tickets/95/carnet.md new file mode 100644 index 0000000..4705421 --- /dev/null +++ b/.ideai/tickets/95/carnet.md @@ -0,0 +1,36 @@ +--- +issueRef: "#95" +version: 6 +updatedBy: {"kind":"user"} +updatedAt: 1784934174326 +--- +## Cause racine RÉELLE (2026-07-24, après investigation code) + +### Ce qui se passe + +Le `"timeout": 15000` (15 secondes) **hardcoded** dans le bloc `mcp.idea` de l'`opencode.json` généré par IdeA. OpenCode applique ce timeout **par appel `tools/call`** à ses serveurs MCP. `idea_ask_agent` bloque pendant que l'agent cible travaille (potentiellement des minutes) → OpenCode tue la requête à 15s avec l'erreur `-32001: Request timed out`. + +**Pourquoi Claude/Codex ne sont pas affectés** : leur config MCP (`.mcp.json` / `config.toml`) n'a **pas** de champ `timeout`. Ce champ est spécifique à la config OpenCode. + +### Emplacements du hardcoded 15000 (4 occurrences) + +1. `application/src/agent/lifecycle.rs:2728` — `opencode_config_json()` +2. `application/src/agent/lifecycle.rs:2811` — `opencode_provider_config_json()` +3. `infrastructure/src/assistant/mod.rs:410` — `opencode_config_json()` (duplicate) +4. `infrastructure/src/assistant/mod.rs:495` — `opencode_provider_config_json()` (duplicate) + +### Fix appliqué (feature/ticket95-opencode-mcp-timeout) + +- Constante `DEFAULT_OPENCODE_MCP_TIMEOUT_MS` = 4h (14_400_000 ms), alignée sur `DEFAULT_RENDEZVOUS_CEILING` — le client MCP ne expire jamais avant le watchdog serveur. +- Fonction `resolve_opencode_mcp_timeout_ms()` — overridable via `IDEA_OPENCODE_MCP_TIMEOUT_MS`, fallback sûr sur 0/parse-error. +- Les 4 occurrences remplacées par `resolve_opencode_mcp_timeout_ms()`. +- Exporté depuis `application::agent` pour réutilisation par `infrastructure`. +- **Claude/Codex non touchés** : aucune modification de leur config MCP. + +### Tests +- Application : 115 passed, 0 failed +- Infrastructure : 313 passed, 0 failed +- Compilation : OK, aucun nouveau warning + +### Note déploiement +Le `opencode.json` est régénéré à chaque lancement d'agent par le code lifecycle. Nécessite rebuild AppImage pour que le binaire qui tourne génère la nouvelle config. \ No newline at end of file diff --git a/.ideai/tickets/95/issue.md b/.ideai/tickets/95/issue.md new file mode 100644 index 0000000..b948e79 --- /dev/null +++ b/.ideai/tickets/95/issue.md @@ -0,0 +1,54 @@ +--- +id: "611077a6-f703-44b9-935b-64f8301085bb" +number: 95 +title: "Rendez-vous idea_ask_agent : timeout MCP OpenCode trop court (15s), le demandeur OpenCode ne reçoit jamais la réponse" +status: "closed" +priority: "high" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1784822537740 +updatedAt: 1784934174326 +version: 6 +--- +## Bug + +Une délégation `idea_ask_agent` depuis un agent sous profil **OpenCode** (le **demandeur**) échoue systématiquement en `timeout` (-32001 « Request timed out »), peu importe la cible (Claude, Codex, ou OpenCode). Les agents cibles terminent bien leurs tâches (visible dans le workstate = done), mais leurs réponses ne sont jamais livrées au demandeur OpenCode. + +Claude et Codex ne sont **pas** affectés, qu'ils soient demandeur ou cible. + +## Cause racine (vérifiée dans les sources) + +Le `"timeout": 15000` (15 secondes) **hardcoded** dans le bloc `mcp.idea` de l'`opencode.json` généré par IdeA. OpenCode applique ce timeout **par appel `tools/call`** à ses serveurs MCP. `idea_ask_agent` bloque pendant que l'agent cible travaille (potentiellement des minutes) → OpenCode tue la requête à 15s. + +**Pourquoi Claude/Codex ne sont pas affectés** : leur config MCP (`.mcp.json` / `config.toml`) n'a **pas** de champ `timeout`. Ce champ est spécifique à la config OpenCode (`opencode.json` → `mcp.idea.timeout`). + +### Emplacements (4 occurrences de `15000`) + +1. `crates/application/src/agent/lifecycle.rs` — `opencode_config_json()` +2. `crates/application/src/agent/lifecycle.rs` — `opencode_provider_config_json()` +3. `crates/infrastructure/src/assistant/mod.rs` — `opencode_config_json()` (duplicate) +4. `crates/infrastructure/src/assistant/mod.rs` — `opencode_provider_config_json()` (duplicate) + +## Ce qui n'est PAS la cause + +- Ce n'est pas un problème de sonde de vivacité du rendez-vous (la description originale pointait la sonde Claude-only côté cible — mauvaise piste). +- Ce n'est pas un problème de permissions, de sandbox, ou de taille de payload. +- Ce n'est pas lié au modèle : tout profil `structuredAdapter: "openCode"` est touché en tant que demandeur. + +## Fix appliqué (feature/ticket95-opencode-mcp-timeout) + +- Constante `DEFAULT_OPENCODE_MCP_TIMEOUT_MS` = 4h (14_400_000 ms), alignée sur `DEFAULT_RENDEZVOUS_CEILING`. +- Fonction `resolve_opencode_mcp_timeout_ms()` — overridable via `IDEA_OPENCODE_MCP_TIMEOUT_MS`, fallback sûr. +- Les 4 occurrences remplacées. +- **Claude/Codex non touchés**. + +## Tests + +Application : 115 passed. Infrastructure : 313 passed. 0 failed. + +## Déploiement + +Nécessite rebuild AppImage (l'`opencode.json` est régénéré à chaque lancement d'agent). \ No newline at end of file diff --git a/.ideai/tickets/96/carnet.md b/.ideai/tickets/96/carnet.md new file mode 100644 index 0000000..057471b --- /dev/null +++ b/.ideai/tickets/96/carnet.md @@ -0,0 +1,18 @@ +--- +issueRef: "#96" +version: 3 +updatedBy: {"kind":"user"} +updatedAt: 1784994119004 +--- +## Décision (2026-07-24) : différée — documentée, non codée + +Sur instruction utilisateur (#96 mis en pause pour se concentrer sur #95) : **on documente le constat et la décision de fond reste ouverte pour plus tard**. Aucun code ajouté. + +### Constat confirmé dans les sources +`crates/infrastructure/src/assistant/mod.rs::TicketAssistantEnvironmentPreparer::materialise_mcp` passe `eff: None` à `opencode_config_json` / `opencode_provider_config_json` (lignes ~253 et ~262). Le commentaire local (l. 246-251) le documente déjà : aucun `PermissionStore`/`EffectivePermissions` n'est résolu sur ce chemin, **pour aucun adaptateur**. Conséquence : les assistants de ticket tournent toujours avec le comportement **natif** d'OpenCode (prompting à chaque action), indépendamment des permissions IdeA configurées. + +### Pourquoi c'est différé (la vraie question produit) +Les assistants de ticket sont lancés depuis un **profil** (pas depuis un **agent** avec une politique par-agent). Il n'y a donc **pas de source naturelle** d'`EffectivePermissions` sur ce chemin. Câbler nécessite d'abord de trancher : les permissions viennent-elles du profil ? d'un fallback projet ? d'une posture dédiée aux assistants ? Tant que ce choix produit n'est pas posé, `eff: None` (= prompting natif) reste le défaut **sûr** — ce n'est pas une régression, c'est le comportement historique préservé. + +### Suivi +Non bloquant, priorité basse. À reprendre quand un besoin produit le justifie : définir la source d'autorités pour un assistant de ticket, puis la résoudre ici (comme pour les agents normaux dans `application::agent::lifecycle`). \ No newline at end of file diff --git a/.ideai/tickets/96/issue.md b/.ideai/tickets/96/issue.md new file mode 100644 index 0000000..d7834cb --- /dev/null +++ b/.ideai/tickets/96/issue.md @@ -0,0 +1,18 @@ +--- +id: "d5993d21-307b-4fc8-93f9-2bfbf44223d8" +number: 96 +title: "Les assistants de ticket (tous adaptateurs) n'ont aucune résolution EffectivePermissions — permission toujours native/absente" +status: "open" +priority: "low" +sprint: "883534aa-7fc1-4d83-a9c0-17cac4a4eea5" +links: [] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1784823281794 +updatedAt: 1784994119004 +version: 3 +--- +Trouvé en implémentant #94 (projection des permissions dans opencode.json). Le call site des assistants de ticket dans `crates/infrastructure/src/assistant/mod.rs` n'a **aucun** `PermissionStore`/`EffectivePermissions` câblé, pour aucun adaptateur (Claude, Codex, OpenCode) — vérifié par grep sur la composition root `crates/backend/src/lib.rs`. Le fix #94 y passe donc `eff: None` (préserve le comportement natif existant, pas de régression), mais ce n'est pas un vrai fix de fond : les permissions IdeA configurées pour un agent n'ont jamais été appliquées aux assistants de ticket, quel que soit l'adaptateur. + +À trancher par Architect : soit câbler la résolution `EffectivePermissions` pour ce call site (comme pour les agents normaux), soit documenter que c'est un choix produit intentionnel (permanent) et fermer sans y toucher. Non bloquant, priorité basse. \ No newline at end of file diff --git a/.ideai/tickets/97/carnet.md b/.ideai/tickets/97/carnet.md new file mode 100644 index 0000000..0048a07 --- /dev/null +++ b/.ideai/tickets/97/carnet.md @@ -0,0 +1,28 @@ +--- +issueRef: "#97" +version: 3 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784915626417 +--- +## Décision Architect (cadrage validé) + +Bug 100% backend, deux défauts composés : +1. `SaveOpenCodeProviderProfile::execute` (`usecases.rs:364-367`) pose `opencode_provider` sans `opencode = None`. +2. Invariant `opencode_backend_is_consistent` (`profile.rs:1220`) existe mais jamais appelé (garde morte). +Lecture priorise `opencode` → cloud écrasé (`lifecycle.rs:2367`, `assistant/mod.rs:252`). + +### Périmètre du lot (un seul, parallélisable) +- **DevBackend** : + - Domaine : `with_opencode`/`with_opencode_provider` (`profile.rs:1132,1140`) imposent l'exclusion mutuelle (chacun efface l'autre). + - Infrastructure : `FsProfileStore::save` rejette tout profil incohérent → `AppError::Invalid` (active enfin le prédicat). + - Application : `SaveOpenCodeProviderProfile` reconstruit via builder (`with_opencode_provider`) ; idem pour `SaveProfile`/`ConfigureProfiles`. + - **Migration requise** : à la lecture (ou passe dédiée), quand `opencode` ET `opencodeProvider` présents → dropper `opencode` stale (l'intention est cloud). Sinon ne corrige que les nouveaux profils. +- **DevFrontend** : strip de la config inactive au save selon le mode courant (requis pour SaveProfile/ConfigureProfiles qui ne portent pas d'intention explicite ; backend reste autorité via la garde). +- **QA** : tests unitaires domaine (builders exclusifs) + garde store + intégration création profil cloud + scénario migration profils corrompus. + +### Décisions de frontière +- DTO `SaveOpenCodeProviderProfileRequestDto` **inchangé** (whole-profile) ; output = profil normalisé, autorité pour le frontend. +- Priorité lecture `opencode` d'abord **gardée** (irrelevant post-exclusion), commenter comme fallback défensif. +- Duplication de la résolution (lifecycle.rs:2367 + assistant/mod.rs:252) = dette hexagonale préexistante, **hors périmètre de ce lot** (suivre, ne pas refactoriser ici). + +Voir mémoire projet `ticket97-opencode-provider-mutual-exclusion`. \ No newline at end of file diff --git a/.ideai/tickets/97/issue.md b/.ideai/tickets/97/issue.md new file mode 100644 index 0000000..2d71a9e --- /dev/null +++ b/.ideai/tickets/97/issue.md @@ -0,0 +1,26 @@ +--- +id: "fd7057dc-0177-41e1-811c-701c802e81d3" +number: 97 +title: "Duplication de provider lors de la création de profil AI - llamacpp ET provider cloud inclus simultanément" +status: "closed" +priority: "high" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784899606438 +updatedAt: 1784915626417 +version: 3 +--- +Lors de la création d'un profil AI dans le premier-run wizard, le système génère un profiles.json qui contient à la fois la section `opencode` (llamacpp) et `opencodeProvider` (cloud), quel que soit le choix de l'utilisateur. + +**Comportement attendu :** +- Si l'utilisateur choisit "llamacpp" : seule la section `opencode` doit être présente +- Si l'utilisateur choisit "provider cloud" : seule la section `opencodeProvider` doit être présente + +**Comportement actuel :** +Les deux sections sont présentes simultanément, ce qui fait que llamacpp est priorisé même quand l'utilisateur veut utiliser un provider cloud (ex: ZAI/GLM). + +**Cas de test :** +Création du profil GLM5.2 avec provider ZAI Code → profiles.json contient `opencodeProvider` correct MAIS contient aussi une section `opencode` vide ou inutile, ce qui force le fallback sur llamacpp. \ No newline at end of file diff --git a/.ideai/tickets/98/carnet.md b/.ideai/tickets/98/carnet.md new file mode 100644 index 0000000..1632246 --- /dev/null +++ b/.ideai/tickets/98/carnet.md @@ -0,0 +1,87 @@ +--- +issueRef: "#98" +version: 5 +updatedBy: {"kind":"user"} +updatedAt: 1784983186248 +--- +# Carnet #98 — Contrat figé (approche **B2**) + +> Cadrage ARCHITECT figé et gelé. **Source de vérité pour DevBackend et QA.** Ce carnet remplace la phase d'arbitrage ; les options A/B1/C sont closes (voir §2 pour le rejet motivé). + +--- + +## 1. Cause racine affinée + +Le modèle est perdu **au spawn**, pas à la sauvegarde (persistance vérifiée correcte bout en bout, `profiles.json` conserve bien `opencodeProvider.model`). Deux facteurs composés : + +1. **Pas de bloc `models`** pour les providers catalogue connus — `opencode_provider_config_json` (`lifecycle.rs:2774-2844` + jumeau `assistant/mod.rs:428-503`) n'émet `npm`/`baseURL`/`models` **que** pour le chemin `custom`. Pour un provider catalogue (zai), seul `provider..options.apiKey` est écrit. Test fige ce manque : `lifecycle.rs:4439-4440`. +2. **Cache models.dev isolé et vide** — IdeA passe `XDG_CACHE_HOME=/.opencode/cache` vide (`lifecycle.rs:2386,2405` ; `assistant/mod.rs:272,282`) + `HOME` isolé. Or `zai` n'est connu d'OpenCode **que** via ce cache (catalogue lu côté IdeA depuis le vrai `~/.cache/opencode/models.json`, `provider_catalogue.rs:79-84`). + +→ OpenCode ne connaît plus `zai`, ne résout pas `zai/` → **fallback silencieux `glm-5.2`** (fallback interne CLI OpenCode, absent du code IdeA). + +Asymétrie confirmée : local llama.cpp et cloud custom marchent car ils émettent un bloc `models` (`assistant/mod.rs:371-379`, `:457-460`). + +## 2. Approche retenue : **B2** (seed best-effort du cache hôte) + +Copier best-effort le cache models.dev hôte (`opencode_models_cache_path()`) vers `/opencode/models.json` dans le cache isolé, juste après le `create_dir_all` du cache dans `apply_mcp_config` (branche commune opencode/opencodeProvider), aux **DEUX** sites. + +**Rejet motivé des alternatives :** +- **A (émettre bloc `models` pour providers connus)** : rejeté car `provider_catalogue.rs` ne persiste que `name`+`models` et **ignore `npm`+`baseURL`**. Un bloc `models` seul laisse OpenCode incapable de *joindre* `zai` — il faudrait en plus étendre le domaine (`display_name`/`npm`/`base_url`) → scope bien plus large et risque régression. A n'est viable qu'en révision A-étendue, écartée pour ce lot. +- **B1 (= B sans factorisation, deux sites copiés)** : rejeté pour dette hexagonale — le writer `opencode_provider_config_json` est déjà dupliqué `lifecycle.rs` vs `assistant/mod.rs`. Dupliquer aussi le seed amplifierait la dette et rendrait toute divergence un bug silencieux. B2 impose une factorisation unique. +- **C (hybride : pointer `XDG_CACHE_HOME` vers le vrai cache)** : rejeté — casse l'invariant d'isolation du run (un profil pourrait lire/écrire le cache hôte ou un cache d'un autre run), et OpenCode pourrait y écrire (logs, refresh) → pollution mutuelle. + +**Pourquoi B2 :** donne à OpenCode sa connaissance registry complète (npm, baseURL, modèles) sans toucher au domaine ni casser l'isolation écriture — c'est de la donnée publique read-only copiée **dans** l'espace isolé. + +## 3. Contrat figé — ports / fichiers / invariants + +### 3.1 Fonctions à implémenter (factorisation) + +- **Rendre `opencode_models_cache_path()` publique** — source de vérité unique du chemin hôte, **ne pas re-dériver** le chemin dans le seed. +- **`seed_opencode_models_cache(fs, isolated_cache_dir)`** (fonction partagée) : lit le cache hôte via `opencode_models_cache_path()`, copie best-effort vers `/opencode/models.json`. Appelée aux **deux** sites après le `create_dir_all` du cache dans `apply_mcp_config` (branche commune `opencode`/`opencodeProvider`). +- **`seed_from_bytes(fs, dest, src_bytes)`** (partie pure testable) : extrait la logique d'écriture du fichier destination à partir de bytes source. C'est le seam de test unitaire. + +### 3.2 Sites d'appel (les DEUX) + +1. `crates/application/src/agent/lifecycle.rs` — après `create_dir_all` cache dans `apply_mcp_config`. +2. `crates/infrastructure/src/assistant/mod.rs` — après `create_dir_all` cache dans `apply_mcp_config`. + +### 3.3 Invariants (à respecter, à tester) + +- **Isolation préservée en écriture** : le seed ne fait que copier une donnée read-only **vers** le cache isolé. Jamais de `XDG_CACHE_HOME` pointé vers l'hôte. +- **Best-effort** : le seed **n'échoue jamais le launch**. Toute erreur (fichier hôte absent, IO) est tracée (warn/log) et ignorée — on retombe sur le comportement actuel (fallback glm-5.2), pas sur un crash. +- **Pas de régression** : profils local (llama.cpp), custom et built-ins (anthropic/openai/openrouter) doivent continuer à fonctionner byte-identique au rendu `opencode.json` actuel. +- **Exclusion mutuelle #97 non touchée** : le seed s'exécute sur la branche commune `opencode`/`opencodeProvider`, sans affecter la logique d'exclusion. +- **Cohérence picker↔spawn** : le catalogue vu côté UI (picker) et celui seedé au spawn proviennent du même fichier hôte → l'utilisateur ne peut pas picker un modèle qu'OpenCode ne connaîtra pas au spawn. + +### 3.4 Hors périmètre (explicitement exclu) + +- Remote SSH/WSL (le seed ne concerne que le spawn local). +- Déduplication du rendu lifecycle↔infra au-delà du seed (la dette du writer `opencode_provider_config_json` reste ouverte — autre lot). +- UI wizard (aucun changement surface). + +## 4. Périmètre QA — 2 couches + +### Couche 1 — Unitaire pure (obligatoire, rapide, déterministe) +- **`seed_from_bytes`** : assert écriture byte-identique du contenu source vers `dest`, gestion erreurs (fs en échec → pas de panic), idempotence. +- **Non-régression rendu `opencode.json`** : les tests existants `lifecycle.rs:4439-4440` et équivalents infra doivent rester **byte-identiques** pour local/custom/built-ins (le seed ne change pas le rendu config). Mettre à jour l'assert si et seulement si B2 modifie réellement le rendu — sinon la conserver telle quelle. + +### Couche 2 — Intégration gated (obligatoire avant fermeture du lot) +- **Spawn réel OpenCode** sur profil `zai` + modèle X choisi. +- **Assert** : OpenCode démarre sur le modèle X (pas de fallback `glm-5.2`). +- Vérifier dans les background-tasks/IO qu'aucune trace `glm-5.2` n'apparaît comme modèle actif. +- **Gate obligatoire à lever** : OpenCode **refresh/écrase-t-il le cache `models.json` au démarrage** ? → si **oui**, le seed est inutile (écrasé avant lecture) et **il faut escalader vers A-étendue** (persistir npm/baseURL/models côté domaine). Consigner le verdict dans le rapport QA. + +## 5. Risque résiduel — refresh models.dev par OpenCode + +**Risque ouvert, à trancher en QA couche 2.** Si OpenCode rafraîchit/écrase `/opencode/models.json` au démarrage (network call ou réécriture locale), le seed B2 est potentiellement **inopérant** : +- Meilleur cas : OpenCode lit le cache avant tout refresh → B2 fonctionne. +- Cas dégradé : OpenCode refresh en premier, écrase le seed, et comme le profil est isolé (pas d'accès réseau garanti / HOME isolé), le refresh peut échouer ou produire un cache incomplet → bug persiste. +- **Plan de contournement si échec** : escalader vers A-étendue (ajouter `npm`+`base_url`+`models` persistés côté `OpenCodeProviderConfig` + émettre le bloc complet au spawn). Ce plan est documenté mais **hors scope B2** — il ferait l'objet d'un lot suivant si QA le confirme nécessaire. + +--- + +## Contexte de mise à jour + +- Cadrage figé par Main sur validation Architect (approche B2). +- Version précédente (v1) : phase d'arbitrage A/B/C — clos. +- Prochaine étape cycle : Git (branche) → DevBackend (implémentation 2 sites + factorisation) → QA (2 couches, gate refresh). diff --git a/.ideai/tickets/98/issue.md b/.ideai/tickets/98/issue.md new file mode 100644 index 0000000..3b6decf --- /dev/null +++ b/.ideai/tickets/98/issue.md @@ -0,0 +1,38 @@ +--- +id: "9db40467-a826-4dac-a679-e7934cbdda81" +number: 98 +title: "Profil cloud provider catalogue (ex: zai) : modèle choisi écrasé par glm-5.2 au spawn" +status: "closed" +priority: "high" +sprint: null +links: [] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} +createdAt: 1784966899811 +updatedAt: 1784983186248 +version: 5 +--- +## Symptôme + +Dans le wizard de création de profil AI OpenCode (first-run), quand l'utilisateur choisit un cloud provider du catalogue (ex: **zai / ZAI Code**) puis sélectionne un modèle spécifique, **après sauvegarde l'agent tourne en `glm-5.2`** quel que soit le modèle choisi. + +## Périmètre + +Bug **backend** (couche application/infrastructure du spawn OpenCode). Suite logique et distincte du ticket #97 (qui réglait l'exclusion mutuelle `opencode` vs `opencodeProvider` — désormais fixée). + +## Cause racine (diagnostiquée, à confirmer par Architect) + +- La persistance est **correcte** : `profiles.json` conserve bien `opencodeProvider.model` = modèle choisi. `glm-5.2` est **absent de tout le code IdeA** ; c'est le **fallback interne de la CLI OpenCode** quand elle ne sait pas résoudre le modèle demandé. +- Le modèle est perdu **au spawn** : + 1. IdeA écrit `opencode.json` avec `model: "zai/"` mais **sans bloc `models`** pour les providers catalogue connus (`crates/application/src/agent/lifecycle.rs:2796-2821`, `crates/infrastructure/src/assistant/mod.rs:447-466`). Elle suppose OpenCode built-in. + 2. Or `zai` n'est connu d'OpenCode **que** via son cache models.dev, et IdeA **isole `XDG_CACHE_HOME` vide** au spawn (`lifecycle.rs:2386,2405` ; `assistant/mod.rs:272,282`). → OpenCode ne résout ni `zai` ni le modèle → fallback silencieux `glm-5.2`. +- Pourquoi ça marche en local (llama.cpp) et custom : ils émettent un bloc `models` (`assistant/mod.rs:371-379`, `:457-460`). + +## Exemples impactés + +Tout provider cloud **connu uniquement via le cache models.dev** (zai, et vraisemblablement tout provider non built-in dans OpenCode). Les providers hardcodés dans OpenCode (anthropic, openai, openrouter) fonctionnent malgré l'absence de bloc `models`. + +## Lié à + +- **#97** (relatesTo) : exclusion mutuelle provider — prérequis déjà livré. diff --git a/.ideai/tickets/99/carnet.md b/.ideai/tickets/99/carnet.md new file mode 100644 index 0000000..55c1de3 --- /dev/null +++ b/.ideai/tickets/99/carnet.md @@ -0,0 +1,128 @@ +--- +issueRef: "#99" +version: 3 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1785059758742 +--- +# Carnet #99 — Cadrage (prêt pour le cycle) + +> Cadrage établi par Main après investigation code + **vérification empirique des binaires +> installés** (`codex-cli 0.145.0`, `Claude Code 2.1.220`). Les faits CLI ci-dessous sont +> **capitalisés et vérifiés** ; ils conditionnent toute l'architecture. Architect doit valider +> les ports/VO et les questions ouvertes (§6) avant l'implémentation. + +--- + +## 1. État des lieux vérifié — comparaison des 3 moteurs + +| Moteur | Modèle contrôlé par IdeA ? | Mécanisme | Source | +|---|---|---|---| +| **OpenCode** | ✅ Oui | `model` écrit à la racine du `opencode.json` isolé (`OPENCODE_CONFIG`), lu par la session structured | `lifecycle.rs:2686-2689`, `2786-2788` | +| **Codex** | ❌ Non | aucun `--model`, `codex_config_toml` n'écrit pas `model` → défaut du binaire | `codex.rs:215-239`, `lifecycle.rs:2941-2955` | +| **Claude** | ❌ Non | aucun `--model`, `claude_settings_seed` n'écrit pas `model` → défaut du binaire | `claude.rs:273-284`, `infrastructure/permission/claude.rs:59` | + +PTY et headless sont **deux processus OS séparés** pour un même agent +(`allow_structured_alongside_pty: true`, `lifecycle.rs:1512-1517`) ; ils partagent le même +`CODEX_HOME`/cwd mais **aucun état modèle** n'est synchronisé entre eux côté IdeA. Le `/model` +de la TUI ne sort jamais du process TUI (pass-through brut, `terminal/usecases.rs:140`). + +## 2. Faits CLI vérifiés (capitalisation) — la clé qui débloque tout + +### Codex 0.145.0 — `model` est une clé config.toml **documentée** +- L'exemple littéral du `--help` est `-c model="o3"`. `config.toml` est chargé depuis + `$CODEX_HOME/config.toml`. +- **Honorée par `codex` (TUI) ET `codex exec` (headless)** — même mécanisme de base + (`--ignore-user-config` confirme qu'il est chargé par défaut). +- Bonus : `codex exec` accepte aussi `-m, --model ` (seconde porte d'injection). +- **→ Écrire `model = "..."` dans le `$CODEX_HOME/config.toml` isolé fixe le défaut pour les + DEUX canaux.** + +### Claude 2.1.220 — `model` est gérable via settings + flag +- `--model ` fonctionne en interactif **et** `-p/--print` (headless). +- `--settings ` + la hiérarchie `./.claude/settings.local.json` (project-local) + supportent une clé `model`. +- **→ Écrire `"model": "..."` dans le `.claude/settings.local.json` du run dir fixe le défaut + pour les DEUX canaux** (cwd = run dir, lu par PTY et headless). + +### Conclusion d'architecture +Les trois moteurs convergent vers **le même pattern** : écrire le modèle du profil dans le +**fichier de config isolé du run dir**. Le fichier partagé = point de vérité unique pour +headless + TUI au lancement. L'exigence produit « modèle par défaut = modèle headless = modèle +exposé TUI au lancement » est satisfaite **par construction**, sans sync à coder. + +## 3. Points d'intégration IdeA précis (avec chemins) + +| Moteur | Renderer à modifier | Fichier produit | Ownership | Isolation | +|---|---|---|---|---| +| **Codex** | `codex_config_toml` (`application/agent/lifecycle.rs:2941-2955`) | `$CODEX_HOME/config.toml` | `MergeToml` (préservé aux régénérations) | `CODEX_HOME={runDir}/.codex` déjà isolé (`lifecycle.rs:2361`) | +| **Claude** | `claude_settings_seed` (`infrastructure/src/permission/claude.rs:59`) | `{runDir}/.claude/settings.local.json` | `Replace` (régénéré du profil à chaque lancement) | cwd=run dir ; pas d'iso home mais **project-local override user** → le modèle IdeA gagne | + +Champ existant à consommer ou déprécier : **`AgentProfile.model: Option`** +(`domain/src/profile.rs:1008`) — actuellement code mort. Référence OpenCode à répliquer : +`OpenCodeProviderConfig { provider_id, model, api_key_ref }` (`domain/src/profile.rs:372-391`) ++ `SaveOpenCodeProviderProfile` (`application/agent/usecases.rs`). + +## 4. Architecture proposée — découpage en lots + +- **A. Domaine** — Nouveaux VO miroir d'`OpenCodeProviderConfig` : + `CodexProviderConfig { provider, model, api_key_ref }` et `ClaudeProviderConfig { provider, + model, api_key_ref }`. Rendre les backends mutuellement exclusifs par moteur (cf. + `opencode_backend_is_consistent`, `domain/src/profile.rs:363-364,1224`). **Décision + Architect** : nouveau VO vs réutiliser `AgentProfile.model` (voir §6). +- **B. Renderers de config** — `codex_config_toml` écrit `model` (+ `model_provider` si requis + par la sémantique codex) ; `claude_settings_seed` écrit la clé `model`. +- **C. Use cases / persistance** — `SaveCodexProviderProfile` / `SaveClaudeProviderProfile` + miroirs de `SaveOpenCodeProviderProfile`, même SecretRef pour la clé scellée. +- **D. UI / wizard** — duplication de profils Codex/Claude comme OpenCode (catalogue providers, + sélection modèle, clé scellée). Réutiliser `provider_catalogue` + picker existants. +- **E. QA** — assert headless + TUI utilisent le modèle du profil (voir §5). + +## 5. Invariants (à respecter, à tester) + +- **Source de vérité unique** : le modèle vient du profil AI, écrit dans le fichier de config + isolé du run dir, lu par headless **et** TUI au lancement. +- **Isolation préservée** : Codex via `CODEX_HOME` (rien ne change) ; Claude via project-local + override (rien ne change). Pas d'accès au home global pour le modèle. +- **`/model` en cours de session TUI ne corrompt pas le headless** (acceptable : ne concerne que + le process PTY en cours ; le prochain lancement réapplique le défaut du profil). +- **Non-régression OpenCode** : rendu `opencode.json` byte-identique (rien ne touche ce chemin). +- **Non-régression permissions** : `codex_config_toml` et `claude_settings_seed` continuent + d'écrire `mcp`/`sandbox`/`trust`/`approval` comme aujourd'hui (le `model` s'ajoute). + +## 6. Questions ouvertes pour Architect (à trancher avant DevBackend) + +1. **VO** : créer `CodexProviderConfig`/`ClaudeProviderConfig` (homogène à OpenCode) **ou** + consommer le champ existant `AgentProfile.model` (code mort) ? Recommandation Main : nouveaux + VO pour rester isomorphe à OpenCode (provider + clé scellée), déprécier `AgentProfile.model`. +2. **Codex `model_provider`** : la config codex distingue `model` et `model_provider`. Faut-il + exposer les deux au profil, ou `model` seul suffit (provider implicite) ? +3. **Claude** : clé `model` dans `settings.local.json` suffit, ou faut-il gérer aussi la clé + API Anthropic du profil (SecretRef) pour que le modèle soit réellement joignable ? +4. **Catalogue providers** : quels providers exposer pour Codex (lié OpenAI) et Claude + (Anthropic) ? Réutiliser `provider_catalogue.rs` ou catalogue dédié par moteur ? +5. **Sémantique TUI** : confirmer qu'aucune CLI n'écrase `config.toml`/`settings` au démarrage + (risque équivalent au « gate refresh » du ticket #98 côté OpenCode). + +## 7. Périmètre QA — 2 couches (cf. méthodologie #98) + +- **Couche 1 (unitaire pure)** : le rendu `codex_config_toml` contient la clé `model` + attendue ; `claude_settings_seed` contient `"model"` attendue ; non-régression + byte-identique du reste (mcp/sandbox/trust/approval) ; exclusion mutuelle des backends. +- **Couche 2 (intégration gated)** : spawn réel `codex exec` et `claude -p` sur un profil avec + modèle X → assert le modèle actif est X (pas le défaut binaire). Vérifier côté TUI aussi + (modèle exposé au lancement). **Gate §6.5** à lever. + +## 8. Hors périmètre (exclu de ce ticket) + +- Override **runtime** du modèle à l'appel (`idea_ask_agent` porterait un modèle) — autre lot. +- Providers distants SSH/WSL (ne concerne que le spawn local). +- Détail de la `provider_catalogue` au-delà de la réutilisation (fast-follow). + +--- + +## Contexte de mise à jour + +- Cadrage Main après vérification empirique CLI (codex 0.145.0, claude 2.1.220). +- Lié à #98 (relatesTo) — même famille « modèle au spawn ». +- Prochaine étape cycle : **Architect** (valide ports/VO + tranche §6) → **Git** (branche) → + **DevBackend** (lots A-C) + **DevFrontend** (lot D) → **QA** (2 couches) → **Git** (commit). diff --git a/.ideai/tickets/99/issue.md b/.ideai/tickets/99/issue.md new file mode 100644 index 0000000..018a47d --- /dev/null +++ b/.ideai/tickets/99/issue.md @@ -0,0 +1,53 @@ +--- +id: "45733f3f-5ef5-4f33-96e7-25afddcd1ce6" +number: 99 +title: "Modèle par agent contrôlable pour Codex & Claude (headless + TUI) — égaler le pattern OpenCode" +status: "qa" +priority: "high" +sprint: null +links: [{"target":"#98","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784987973464 +updatedAt: 1785059758742 +version: 3 +--- +## Problème + +Aujourd'hui IdeA **ne contrôle pas le modèle** pour les moteurs **Codex** et **Claude** : +le modèle utilisé par le headless inter-agent (`codex exec` / `claude -p`) **et** par la TUI +interactive est le **défaut du binaire CLI installé**, indépendamment du profil AI de l'agent. + +- Aucun flag `--model` n'est passé au spawn (`codex.rs:215-239`, `claude.rs:273-284`). +- `codex_config_toml` (`lifecycle.rs:2941-2955`) **n'écrit jamais de clé `model`**. +- `AgentProfile.model` (`profile.rs:1008`) existe mais est du **code mort** (jamais consommé). +- Le `/model` tapé dans la TUI ne se propage **pas** au headless : ce sont deux processus OS + séparés (PTY vs `codex exec`/`claude -p`) avec des threads distincts. Le `CODEX_HOME`/cwd est + partagé mais rien n'écrit le modèle sur disque côté IdeA. + +**Contraste** : pour **OpenCode**, le modèle du profil (`OpenCodeConfig.model` / +`OpenCodeProviderConfig.model`) **est** celui utilisé par le headless, via le `opencode.json` +isolé pointé par `OPENCODE_CONFIG` (`lifecycle.rs:2686-2689`). Un profil IdeA = un +(provider, modèle, clé scellée). + +## Objectif produit + +Qu'il existe un **modèle par défaut par agent**, défini dans le **profil AI**, qui soit : + +1. utilisé par le **headless inter-agent** (`idea_ask_agent`) ; +2. **exposé par la TUI au lancement** (valeur initiale du modèle dans la CLI) ; + +pour les **trois** moteurs — OpenCode (déjà OK), Codex et Claude (à égaler). + +## Périmètre + +- **Backend** : nouveau VO domaine miroir d'`OpenCodeProviderConfig`, renderers de config + écrivant le modèle, use cases de persistance. +- **UI** : duplication / création de profils Codex et Claude comme OpenCode (catalogue de + providers, sélection du modèle, clé scellée via SecretRef). + +## Lié à + +- **#98** (relatesTo) : même famille « contrôle du modèle au spawn » (côté OpenCode, résolveur + zai/glm-5.2). Indépendant mais cohérent. diff --git a/.ideai/tickets/counter.json b/.ideai/tickets/counter.json new file mode 100644 index 0000000..5c590ed --- /dev/null +++ b/.ideai/tickets/counter.json @@ -0,0 +1,3 @@ +{ + "nextNumber": 107 +} \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json new file mode 100644 index 0000000..be21241 --- /dev/null +++ b/.ideai/tickets/index.json @@ -0,0 +1,1081 @@ +{ + "version": 1, + "issues": [ + { + "issueRef": "#1", + "path": "1", + "title": "Tâches de fond — finaliser B8 (runner PTY) et clôturer le chantier", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783175265022 + }, + { + "issueRef": "#2", + "path": "2", + "title": "Tâches de fond — dette A : découpler fin de process et flux output PTY (PtyPort::wait/try_wait)", + "status": "closed", + "priority": "low", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784065385002 + }, + { + "issueRef": "#3", + "path": "3", + "title": "[Différé — design à mûrir] Tâches de fond — dette B : retry durable après reboot", + "status": "closed", + "priority": "low", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784377533777 + }, + { + "issueRef": "#4", + "path": "4", + "title": "Annonces inter-agent (UI live) — reprise et finalisation : overlay cellule cible + preview demandeur (F1-F3)", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783437430509 + }, + { + "issueRef": "#5", + "path": "5", + "title": "Tâches de fond — dette de contrat DTO work-state : summary non affiché + owner/project inutiles + tri", + "status": "closed", + "priority": "low", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783328740886 + }, + { + "issueRef": "#6", + "path": "6", + "title": "Suppression d'un ticket", + "status": "closed", + "priority": "low", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783312054217 + }, + { + "issueRef": "#7", + "path": "7", + "title": "Gestion des session limite", + "status": "closed", + "priority": "high", + "sprint": "d8f3f37b-87ca-4509-9116-45a99bc711df", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784718581033 + }, + { + "issueRef": "#8", + "path": "8", + "title": "Création de ticket via agent IA", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783310831918 + }, + { + "issueRef": "#9", + "path": "9", + "title": "Ne pas revert la description d'un ticket lorsque l'on change la priorité", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783210849647 + }, + { + "issueRef": "#10", + "path": "10", + "title": "Ticket par sprint", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783241883970 + }, + { + "issueRef": "#11", + "path": "11", + "title": "Interface de création de sprint", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783278726067 + }, + { + "issueRef": "#12", + "path": "12", + "title": "Checkbox pour les filters des tickets", + "status": "closed", + "priority": "low", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783312900907 + }, + { + "issueRef": "#13", + "path": "13", + "title": "Server/client mode", + "status": "closed", + "priority": "low", + "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784193634690 + }, + { + "issueRef": "#14", + "path": "14", + "title": "Integration de profils IA locaux/LAN comme profils IdeA canoniques", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784449099987 + }, + { + "issueRef": "#15", + "path": "15", + "title": "[Bloqué par #7 — design durable] Limites de session — re-livraison auto parquée au reset (stretch B5)", + "status": "open", + "priority": "low", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [], + "updatedAt": 1784093861343 + }, + { + "issueRef": "#16", + "path": "16", + "title": "[UI] réorganisation des menus", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783405334263 + }, + { + "issueRef": "#17", + "path": "17", + "title": "[UI] fenetre de gestion de tickets", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1783405334977 + }, + { + "issueRef": "#18", + "path": "18", + "title": "[UI] Créer une popup de selection de ticket", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783405335941 + }, + { + "issueRef": "#19", + "path": "19", + "title": "[UI] implémentation de la popup de selection de ticket dans la création d'un sprint", + "status": "closed", + "priority": "low", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783405337651 + }, + { + "issueRef": "#20", + "path": "20", + "title": "[Backend] Dette ticket_list — matching #ref dans text + curseur opaque/stable", + "status": "closed", + "priority": "low", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [], + "updatedAt": 1784049665491 + }, + { + "issueRef": "#21", + "path": "21", + "title": "[UI] pouvoir trier les ticket par...", + "status": "closed", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1783437364397 + }, + { + "issueRef": "#22", + "path": "22", + "title": "[UI] Anchor de views", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1783437364871 + }, + { + "issueRef": "#23", + "path": "23", + "title": "[UI] rendre les fenêtres View séparée de IdeA", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1783437365276 + }, + { + "issueRef": "#25", + "path": "25", + "title": "[Bug] Codex/Claude erreur lors de l'utilisation dans l'edition d'un ticket", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783491435654 + }, + { + "issueRef": "#26", + "path": "26", + "title": "[UI] refonte totale de la gestion des menus via agent UX", + "status": "closed", + "priority": "medium", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [], + "updatedAt": 1783861758866 + }, + { + "issueRef": "#27", + "path": "27", + "title": "[Bug] L'outil assistance IA n'a pas les commandes MCP", + "status": "closed", + "priority": "high", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783926649550 + }, + { + "issueRef": "#28", + "path": "28", + "title": "First-run wizard : bouton « Save and continue » figé (busy bloqué par l'auto-détection)", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1783491458775 + }, + { + "issueRef": "#29", + "path": "29", + "title": "[UI] Garder les filtres des tickets entre deux redemarrage", + "status": "closed", + "priority": "low", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [], + "updatedAt": 1783921595742 + }, + { + "issueRef": "#30", + "path": "30", + "title": "[Bug] Le handle de limite session n'est pas fonctionnel quand c'est Main qui limite session", + "status": "closed", + "priority": "medium", + "sprint": "d8f3f37b-87ca-4509-9116-45a99bc711df", + "assignedAgentIds": [], + "updatedAt": 1783941648176 + }, + { + "issueRef": "#31", + "path": "31", + "title": "Détection de profils séquentielle : N × 800 ms au pire, résultat potentiellement partiel", + "status": "closed", + "priority": "low", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [], + "updatedAt": 1784064442657 + }, + { + "issueRef": "#32", + "path": "32", + "title": "[Bug] Erreur CLI Ollama profile", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783723378815 + }, + { + "issueRef": "#33", + "path": "33", + "title": "[Dette] Fallthrough silencieux du routage structuré (lifecycle.rs:1705)", + "status": "closed", + "priority": "high", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [], + "updatedAt": 1784048928225 + }, + { + "issueRef": "#34", + "path": "34", + "title": "[Risque] ChatBridge.scrollback non borné (croissance mémoire)", + "status": "closed", + "priority": "medium", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [], + "updatedAt": 1784050010085 + }, + { + "issueRef": "#35", + "path": "35", + "title": "Avoir llamacpp intégré", + "status": "closed", + "priority": "high", + "sprint": "883534aa-7fc1-4d83-a9c0-17cac4a4eea5", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783777898736 + }, + { + "issueRef": "#36", + "path": "36", + "title": "[UI] Pouvoir déclarer plusieurs profils AI de modeles locaux", + "status": "closed", + "priority": "medium", + "sprint": "883534aa-7fc1-4d83-a9c0-17cac4a4eea5", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783777899737 + }, + { + "issueRef": "#37", + "path": "37", + "title": "[UI] garder les tickets dans les sprints, mais ajouter l'affichage du status et la possibiliter de filtrer les tickets", + "status": "closed", + "priority": "low", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783878976369 + }, + { + "issueRef": "#38", + "path": "38", + "title": "[UI] Pouvoir ajouter un ticket à un sprint pendant sa création", + "status": "closed", + "priority": "low", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [], + "updatedAt": 1783879443381 + }, + { + "issueRef": "#39", + "path": "39", + "title": "[UI] Fermer toutes les fernetres lors de la fermeture de la fenetre principale principale", + "status": "closed", + "priority": "low", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [], + "updatedAt": 1783921595316 + }, + { + "issueRef": "#40", + "path": "40", + "title": "[UI] réouvrir les fenetres au lancement d'IdeA", + "status": "closed", + "priority": "low", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783922451268 + }, + { + "issueRef": "#41", + "path": "41", + "title": "[UI] Selection multiple lors de la selection de tickets pour les sprints", + "status": "closed", + "priority": "low", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783878363957 + }, + { + "issueRef": "#42", + "path": "42", + "title": "[UI] Clarifier les contrôles d'en-tête de panneau (DockControls) — icônes libellées + tooltips alignés sur le menu Panneaux", + "status": "closed", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1783877612067 + }, + { + "issueRef": "#43", + "path": "43", + "title": "Systeme de plugins", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784707900405 + }, + { + "issueRef": "#44", + "path": "44", + "title": "Garder les infos du profils courant sur l'affichage d'edition des profiles", + "status": "closed", + "priority": "medium", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783976073932 + }, + { + "issueRef": "#45", + "path": "45", + "title": "[Bug] Erreur OpenCode au lancement IdeA", + "status": "closed", + "priority": "medium", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783925599109 + }, + { + "issueRef": "#46", + "path": "46", + "title": "[Bug] Plus de boutons pour ajouter un projet en onglet", + "status": "closed", + "priority": "low", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783939380889 + }, + { + "issueRef": "#47", + "path": "47", + "title": "[Bug] Réouverture de fenêtres non lié au projet", + "status": "closed", + "priority": "medium", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783939089770 + }, + { + "issueRef": "#48", + "path": "48", + "title": "[Bug] Bandeau d'affichage d'erreur au dessus des boutons de gestion de cellule", + "status": "closed", + "priority": "medium", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783924961311 + }, + { + "issueRef": "#49", + "path": "49", + "title": "[Misc] clean les memory qui ne sont plus valable ou qui n'ont plus de raisons d'être", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783940643502 + }, + { + "issueRef": "#50", + "path": "50", + "title": "[UI] Erreur d'affichage sur les fenetre ouvertes a l'ouverture d'IdeA", + "status": "closed", + "priority": "low", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783959345112 + }, + { + "issueRef": "#51", + "path": "51", + "title": "Clean des session headless", + "status": "open", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1783941336721 + }, + { + "issueRef": "#52", + "path": "52", + "title": "[UI] mettre le bouton plus pour ajouter un projet juste a droite des onglet déjà ouvert comme pour les layouts", + "status": "closed", + "priority": "low", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1783958689416 + }, + { + "issueRef": "#54", + "path": "54", + "title": "[UI] Ajouter le handle du téléchargement des modeles lors du démarage llamacpp", + "status": "closed", + "priority": "medium", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [], + "updatedAt": 1784018300921 + }, + { + "issueRef": "#55", + "path": "55", + "title": "[Bug] Error on loading local model", + "status": "closed", + "priority": "high", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784649948611 + }, + { + "issueRef": "#56", + "path": "56", + "title": "[Bug] selection desactivée d'agent non affichés", + "status": "closed", + "priority": "low", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784048212123 + }, + { + "issueRef": "#58", + "path": "58", + "title": "[B/F] Rendu live des tâches de fond — subscriber UI + canal IPC attachable au task output", + "status": "open", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784064521992 + }, + { + "issueRef": "#60", + "path": "60", + "title": "[Bug] Assistant IA d'édition de ticket : aucune réponse dans la conversation (stream sans Final non signalé)", + "status": "closed", + "priority": "high", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [], + "updatedAt": 1784360139966 + }, + { + "issueRef": "#61", + "path": "61", + "title": "[UI] rafraichissement des cellule", + "status": "closed", + "priority": "low", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784661823324 + }, + { + "issueRef": "#62", + "path": "62", + "title": "[Sécurité/Cohérence] Identité requester explicite pour les sessions structurées + policy des tools OpenAI-compatible", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784406936659 + }, + { + "issueRef": "#63", + "path": "63", + "title": "Systeme de test de l'UI", + "status": "open", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784098438671 + }, + { + "issueRef": "#64", + "path": "64", + "title": "Web : créer/ajouter un projet depuis le navigateur (folder browser serveur)", + "status": "closed", + "priority": "medium", + "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", + "assignedAgentIds": [], + "updatedAt": 1784718418070 + }, + { + "issueRef": "#65", + "path": "65", + "title": "Serveur headless : extraire idea-serve du binaire Tauri (cœur partagé)", + "status": "closed", + "priority": "high", + "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", + "assignedAgentIds": [], + "updatedAt": 1784210420516 + }, + { + "issueRef": "#66", + "path": "66", + "title": "Image Docker serveur/client IdeA", + "status": "open", + "priority": "medium", + "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", + "assignedAgentIds": [], + "updatedAt": 1784193460805 + }, + { + "issueRef": "#67", + "path": "67", + "title": "Lock inter-process de l'app-data-dir (desktop ↔ idea --serve)", + "status": "open", + "priority": "low", + "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", + "assignedAgentIds": [], + "updatedAt": 1784193467784 + }, + { + "issueRef": "#68", + "path": "68", + "title": "Ajouter dans l'app desktop, la possibilité d'activer le serveur", + "status": "closed", + "priority": "medium", + "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784277313706 + }, + { + "issueRef": "#69", + "path": "69", + "title": "Adaptibilité client téléphone", + "status": "closed", + "priority": "medium", + "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784449061397 + }, + { + "issueRef": "#70", + "path": "70", + "title": "Pouvoir gérer les models AI locaux téléchargés via les serveur llamacpp", + "status": "closed", + "priority": "low", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1785083912470 + }, + { + "issueRef": "#71", + "path": "71", + "title": "Diagnostic d'accessibilité du serveur : dire pourquoi ça ne marche pas, pas seulement que ça tourne", + "status": "open", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784210443885 + }, + { + "issueRef": "#72", + "path": "72", + "title": "Sécurité : donner un effet réel à la confiance reverse proxy (--trust-reverse-proxy est un drapeau creux)", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784236365445 + }, + { + "issueRef": "#73", + "path": "73", + "title": "TLS intégré à idea-serve : supprimer la cause racine de la cérémonie reverse proxy", + "status": "open", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784223044517 + }, + { + "issueRef": "#74", + "path": "74", + "title": "Le serveur embarqué sert le bundle desktop (Tauri) au navigateur — __TAURI_INTERNALS__ undefined", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784277308704 + }, + { + "issueRef": "#75", + "path": "75", + "title": "Écran d'appairage web : clavier numérique sur mobile alors que le code contient des lettres", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784449082160 + }, + { + "issueRef": "#76", + "path": "76", + "title": "Appairage : la comparaison du code est sensible à la casse côté serveur", + "status": "closed", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784287679986 + }, + { + "issueRef": "#77", + "path": "77", + "title": "Appairage : appareils enregistrés persistants, révocables, et code éphémère à usage unique", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784449075576 + }, + { + "issueRef": "#78", + "path": "78", + "title": "Settings desktop : sections en langues mélangées (« Appareils » à côté de « AI Profiles », « Deployment »)", + "status": "closed", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784379475877 + }, + { + "issueRef": "#79", + "path": "79", + "title": "Test flaky : PermissionsPanel « saves project defaults » échoue par intermittence", + "status": "closed", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784379476483 + }, + { + "issueRef": "#80", + "path": "80", + "title": "L'environnement de QA ne peut pas ouvrir de socket — il ne peut pas valider les features réseau", + "status": "open", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784287697560 + }, + { + "issueRef": "#81", + "path": "81", + "title": "MCP d'edition de templates", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784565918809 + }, + { + "issueRef": "#82", + "path": "82", + "title": "Ajouter des permissions d'utilisations des tools MCP d'IdeA par agent", + "status": "closed", + "priority": "critical", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784410760329 + }, + { + "issueRef": "#83", + "path": "83", + "title": "Popup pour vérifier la volonté de quitter l'app", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784567952845 + }, + { + "issueRef": "#84", + "path": "84", + "title": "[Bug] Reprise auto après limite de session ne se déclenche pas pour l'orchestrator (Main, profil Claude)", + "status": "open", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784377833773 + }, + { + "issueRef": "#85", + "path": "85", + "title": "Test flaky : setCellAgent « changing the agent kills the previous PTY (Bug #3) » échoue en exécution shuffled", + "status": "open", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784379436323 + }, + { + "issueRef": "#86", + "path": "86", + "title": "[UI] ajouter l'affichage et l'dition des tickets et des sprints à la version web", + "status": "closed", + "priority": "medium", + "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784612705899 + }, + { + "issueRef": "#87", + "path": "87", + "title": "[UI] sur l'affichage Web, la liste des background task polluent l'affchage", + "status": "closed", + "priority": "high", + "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784650196560 + }, + { + "issueRef": "#89", + "path": "89", + "title": "Pouvoir lancer le serveur au lancement IdeA", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784661583231 + }, + { + "issueRef": "#90", + "path": "90", + "title": "Ajouter les menus manquantes à la version Web d'IdeA", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784661583085 + }, + { + "issueRef": "#91", + "path": "91", + "title": "Sur la notification de fin de tache backend, mettre plutot les deux agents en conversation", + "status": "open", + "priority": "medium", + "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1784994105390 + }, + { + "issueRef": "#92", + "path": "92", + "title": "Configurer OpenCode avec un provider Opencode", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784993956910 + }, + { + "issueRef": "#93", + "path": "93", + "title": "Agent Context : réponse finale non capturée, fragment d'appel d'outil brut renvoyé via idea_ask_agent", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784934179780 + }, + { + "issueRef": "#94", + "path": "94", + "title": "Permissions OpenCode ignorées : opencode.json code en dur bash/edit=ask au lieu de projeter EffectivePermissions", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784823278030 + }, + { + "issueRef": "#95", + "path": "95", + "title": "Rendez-vous idea_ask_agent : timeout MCP OpenCode trop court (15s), le demandeur OpenCode ne reçoit jamais la réponse", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784934174326 + }, + { + "issueRef": "#96", + "path": "96", + "title": "Les assistants de ticket (tous adaptateurs) n'ont aucune résolution EffectivePermissions — permission toujours native/absente", + "status": "open", + "priority": "low", + "sprint": "883534aa-7fc1-4d83-a9c0-17cac4a4eea5", + "assignedAgentIds": [], + "updatedAt": 1784994119004 + }, + { + "issueRef": "#97", + "path": "97", + "title": "Duplication de provider lors de la création de profil AI - llamacpp ET provider cloud inclus simultanément", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784915626417 + }, + { + "issueRef": "#98", + "path": "98", + "title": "Profil cloud provider catalogue (ex: zai) : modèle choisi écrasé par glm-5.2 au spawn", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784983186248 + }, + { + "issueRef": "#99", + "path": "99", + "title": "Modèle par agent contrôlable pour Codex & Claude (headless + TUI) — égaler le pattern OpenCode", + "status": "qa", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1785059758742 + }, + { + "issueRef": "#100", + "path": "100", + "title": "[Bug] Problème sur le scroll des agents OpenCode", + "status": "closed", + "priority": "high", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [], + "updatedAt": 1785083912470 + }, + { + "issueRef": "#101", + "path": "101", + "title": "[Bug] Soucis de retour de notification sur les taches backend", + "status": "closed", + "priority": "critical", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1785011116365 + }, + { + "issueRef": "#102", + "path": "102", + "title": "[Bug] Devoir resize les cellule pour afficher la TUI d'un agent", + "status": "closed", + "priority": "medium", + "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1785083912470 + }, + { + "issueRef": "#103", + "path": "103", + "title": "Exposer et piloter la permission réseau des agents/commandes dans IdeA", + "status": "qa", + "priority": "high", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "updatedAt": 1785013507979 + } + ] +}