merge(cli): lot CLI/TUI 2026-08-06 — #168 #169 #170 (QA verte) + #155 (contrat vert, e2e AppImage fichier non-image en attente)
This commit is contained in:
@ -1,8 +1,8 @@
|
||||
---
|
||||
issueRef: "#151"
|
||||
version: 11
|
||||
updatedBy: {"kind":"agent","agent_id":"1ff94b51-3e17-4a39-8543-7715d1ff0f80"}
|
||||
updatedAt: 1786036332616
|
||||
version: 12
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedAt: 1786036825333
|
||||
---
|
||||
## 2026-08-06 — Reopened from user validation on AppImage
|
||||
- User retested the custom CLI on the current AppImage build.
|
||||
|
||||
@ -9,9 +9,9 @@ links: []
|
||||
agentRefs: [{"agentId":"8f7da528-58df-4315-97e9-0562230ecc19","role":"assigned"}]
|
||||
attachments: []
|
||||
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedBy: {"kind":"agent","agent_id":"1ff94b51-3e17-4a39-8543-7715d1ff0f80"}
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
createdAt: 1785945386695
|
||||
updatedAt: 1786036332616
|
||||
version: 11
|
||||
updatedAt: 1786036825333
|
||||
version: 12
|
||||
---
|
||||
Régression toujours présente sur la custom CLI: pendant une conversation agent active, le bouton rouge Cancel en haut à droite de la cellule reste sous/derrière les autres contrôles de la toolbar au lieu de passer au premier plan et de rester cliquable. Attendu: hiérarchie visuelle stable et z-order correct en état busy, sans recouvrement du bouton Cancel.
|
||||
@ -1,11 +1,26 @@
|
||||
---
|
||||
issueRef: "#155"
|
||||
version: 6
|
||||
version: 12
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedAt: 1786034876664
|
||||
updatedAt: 1786039145667
|
||||
---
|
||||
## 2026-08-06 — Reopened from user validation on AppImage
|
||||
- User reports that clipboard paste in the custom CLI chat still does not work in practice.
|
||||
- Suspected historical scope was around #154/#155; this cycle treats #155 as the user-visible clipboard UX bug and keeps the #154 attachment foundation dependency in view.
|
||||
- Scope for this cycle: verify whether the regression is frontend paste interception, attachment import, backend routing, or AppImage/runtime mismatch; restore end-to-end paste of clipboard image/file into the custom chat composer.
|
||||
- Main reopened the ticket and assigned frontend/backend ownership before architecture arbitration.
|
||||
|
||||
## 2026-08-06 — DevFrontend
|
||||
- Frontend complété sur `fix/cli-tui-batch-2026-08-06`: le collage clipboard accepte maintenant les fichiers génériques en plus des images.
|
||||
- Les fichiers venant de `clipboardData.items/files` sont convertis en attachment `contentBase64` avec `filename` / `mime` / `sourceKind=clipboard` ; les images conservent une preview, les autres fichiers partent comme attachments sans preview.
|
||||
- Tests frontend verts : `cd frontend && npx vitest run src/features/agents/CustomAgentChatView.test.tsx src/features/layout/LayoutGrid.chat.test.tsx src/features/layout/singletonAgent.test.tsx` -> 46 passed ; `cd frontend && npm run typecheck` -> exit 0 ; `cd frontend && npx vitest run` -> 117 files, 1130 tests passed.
|
||||
|
||||
## 2026-08-06 — DevBackend
|
||||
- Vérification backend effectuée. Aucun changement backend requis pour le scope image/fichier depuis clipboard : le pipeline existe déjà côté DTO/Tauri/application/infrastructure.
|
||||
- `ChatAttachmentInputDto` accepte `path` ou `contentBase64`; `import_chat_attachments` et `agent_send` routent vers `ImportChatAttachments`; le store FS persiste chemins locaux et bytes clipboard dans `.ideai/attachments/agent-chat/<session>/`.
|
||||
- Tests backend verts : `cargo test -p application --test chat_attachments -- --nocapture` -> 3 passed ; `CARGO_HOME=/tmp/idea-cargo-home cargo test -p infrastructure --test chat_attachments -- --nocapture` -> 4 passed.
|
||||
|
||||
## 2026-08-06 — QA
|
||||
- QA a rejoué les validations frontend/backend ciblées avec succès.
|
||||
- Verdict: VERT AVEC RÉSERVE. Le contrat frontend/application/infrastructure du collage image/fichier est vert, mais cette session ne fournit pas de preuve runtime/AppImage réelle d'un collage OS de fichier non-image.
|
||||
- Décision QA explicite: si le ticket exige une preuve runtime/AppImage du collage clipboard fichier, il doit rester ouvert jusqu'à validation E2E sur l'AppImage.
|
||||
@ -1,8 +1,8 @@
|
||||
---
|
||||
id: "620bc08f-835c-483c-a9f1-59690ebf8ab5"
|
||||
number: 155
|
||||
title: "Custom chat: paste image from clipboard into composer"
|
||||
status: "open"
|
||||
title: "Custom chat: paste image/fichier depuis le clipboard dans le composer"
|
||||
status: "qa"
|
||||
priority: "medium"
|
||||
sprint: null
|
||||
links: [{"target":"#154","kind":"dependsOn"}]
|
||||
@ -11,7 +11,7 @@ attachments: []
|
||||
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
createdAt: 1785964740363
|
||||
updatedAt: 1786034876664
|
||||
version: 6
|
||||
updatedAt: 1786039145667
|
||||
version: 12
|
||||
---
|
||||
La custom CLI ne permet toujours pas de coller une image/fichier depuis le presse-papiers dans le chat. Après un test utilisateur du 2026-08-06, le collage attendu ne crée aucun attachment utilisable dans le composer. Il faut rétablir le support de collage clipboard dans le chat custom, sur la base du pipeline d'attachments, puis valider end-to-end sur l'AppImage réelle.
|
||||
Demande utilisateur requalifiée le 2026-08-06: la custom CLI ne permet toujours pas de coller un fichier dans la barre de chat, comme dans une TUI native Claude Code ou Codex. Le scope ne doit pas rester limité au seul collage d'image: il faut couvrir au minimum les fichiers/images exposés par le clipboard et les convertir en attachments utilisables dans le composer custom, via le pipeline d'attachments existant.
|
||||
@ -1,8 +1,8 @@
|
||||
---
|
||||
issueRef: "#167"
|
||||
version: 3
|
||||
updatedBy: {"kind":"agent","agent_id":"1ff94b51-3e17-4a39-8543-7715d1ff0f80"}
|
||||
updatedAt: 1786036332616
|
||||
version: 4
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedAt: 1786036825352
|
||||
---
|
||||
## 2026-08-06 — New regression ticket from user test
|
||||
- User validated that classic progress messages stream correctly in the custom CLI.
|
||||
|
||||
@ -9,9 +9,9 @@ links: []
|
||||
agentRefs: [{"agentId":"fe887179-933f-47d4-960f-c3b06827f86c","role":"assigned"},{"agentId":"8f7da528-58df-4315-97e9-0562230ecc19","role":"assigned"}]
|
||||
attachments: []
|
||||
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedBy: {"kind":"agent","agent_id":"1ff94b51-3e17-4a39-8543-7715d1ff0f80"}
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
createdAt: 1786034864702
|
||||
updatedAt: 1786036332616
|
||||
version: 3
|
||||
updatedAt: 1786036825352
|
||||
version: 4
|
||||
---
|
||||
Régression observée sur la custom CLI (test utilisateur du 2026-08-06): les messages de progress classiques apparaissent bien pendant le tour, mais les événements de fin de tour (`turn.started`/`turn.completed` côté provider) et les appels MCP/outils n'apparaissent qu'après le rendu du Final. Attendu: tout événement IdeA observable localement avant Final doit être projeté au fil de l'eau dans le bon ordre temporel, sans post-affichage trompeur après la réponse finale.
|
||||
20
.ideai/tickets/168/carnet.md
Normal file
20
.ideai/tickets/168/carnet.md
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
issueRef: "#168"
|
||||
version: 6
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedAt: 1786039145559
|
||||
---
|
||||
## 2026-08-06 — Ouverture depuis retour utilisateur
|
||||
- Le ticket #151 a été requalifié comme hors-sujet pour ce besoin précis: l'utilisateur ne demande pas un fix de z-index ou de profondeur du bouton Cancel.
|
||||
- Besoin exact: dans la custom CLI, il ne doit pas y avoir de bouton `Cancel` séparé pour interrompre une question en cours.
|
||||
- Attendu UX: le bouton `Send` devient `Stop` pendant le tour en cours, puis redevient `Send` après fin/annulation.
|
||||
- Périmètre pressenti: frontend pur (composer / barre d'action custom CLI), sans nouveau contrat backend si l'action stop existe déjà.
|
||||
|
||||
## 2026-08-06 — DevFrontend
|
||||
- Frontend livré sur `fix/cli-tui-batch-2026-08-06`: suppression du bouton `Cancel` séparé dans la custom CLI.
|
||||
- Le bouton principal d'envoi devient `Stop` pendant un tour en cours, appelle `cancelAgentChat(sessionId)`, puis redevient `Envoyer` une fois le tour terminé ou interrompu.
|
||||
- Tests frontend verts, dont `CustomAgentChatView.test.tsx` dans la campagne ciblée (`46 passed`) et la suite frontend complète (`117 files`, `1130 tests passed`).
|
||||
|
||||
## 2026-08-06 — QA
|
||||
- QA a rejoué la campagne ciblée et la suite frontend complète avec succès.
|
||||
- Verdict: VERT. Preuve principale: le bouton séparé Cancel n'est plus présent; le bouton principal passe bien de Envoyer à Stop pendant le tour puis revient à Envoyer. Couvert par `CustomAgentChatView.test.tsx`.
|
||||
17
.ideai/tickets/168/issue.md
Normal file
17
.ideai/tickets/168/issue.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
id: "b79839df-769c-4a2e-a4af-22a10cb6ce28"
|
||||
number: 168
|
||||
title: "Custom CLI: transformer le bouton Send en Stop pendant une question agent"
|
||||
status: "qa"
|
||||
priority: "medium"
|
||||
sprint: null
|
||||
links: [{"target":"#151","kind":"relatesTo"},{"target":"#147","kind":"relatesTo"}]
|
||||
agentRefs: [{"agentId":"8f7da528-58df-4315-97e9-0562230ecc19","role":"assigned"}]
|
||||
attachments: []
|
||||
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
createdAt: 1786037910947
|
||||
updatedAt: 1786039145559
|
||||
version: 6
|
||||
---
|
||||
Demande utilisateur du 2026-08-06: dans la custom CLI, il ne faut pas de bouton Cancel séparé dans la toolbar pendant une conversation. Le comportement attendu est qu'au moment où l'utilisateur envoie une question à un agent, le bouton d'envoi se transforme en bouton Stop, puis revienne en bouton Send quand le tour est terminé ou arrêté. Ce n'est pas un bug de profondeur/z-index du bouton Cancel historique (#151), mais un changement UX/comportemental du composer custom.
|
||||
25
.ideai/tickets/169/carnet.md
Normal file
25
.ideai/tickets/169/carnet.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
issueRef: "#169"
|
||||
version: 6
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedAt: 1786039145579
|
||||
---
|
||||
## 2026-08-06 — Ouverture depuis retour utilisateur
|
||||
- Repro utilisateur: si un agent est déjà occupé par une conversation inter-agent (ex: Main -> Architect), l'ouverture de la custom CLI sur cet agent échoue avec une variante de `already open elsewhere`.
|
||||
- Attendu produit: comme pour la TUI native, l'utilisateur doit disposer d'une session dédiée côté humain, distincte des sessions inter-agent/headless en cours.
|
||||
- Hypothèse de cadrage: le verrou d'occupation/surface mélange actuellement visibilité/surface et exclusivité de session, ou ne distingue pas assez la session user des autres sessions.
|
||||
- Périmètre pressenti: architecture + backend + frontend.
|
||||
|
||||
## 2026-08-06 — DevBackend
|
||||
- Fix appliqué dans `application::LaunchAgent`: une session structurée/headless déjà vivante ne bloque plus un lancement utilisateur PTY/TUI explicite du même agent.
|
||||
- La garde `StructuredSessions` ne s'applique désormais que lorsque le lancement vise la surface structurée; la garde d'occupation TUI reste portée par `TerminalSessions`.
|
||||
- Test ajouté et vert: `pty_launch_is_allowed_while_structured_session_is_live`.
|
||||
|
||||
## 2026-08-06 — DevFrontend
|
||||
- Frontend/mock aligné sur la séparation surface humaine vs session structurée: une session structured/headless préexistante ne bloque plus l'ouverture de la custom CLI utilisateur dans les tests frontend.
|
||||
- Le point de vérité restant à valider en QA est le backend réel via `launch_agent(cellKind=chat)`/surface user sur agent déjà occupé inter-agent.
|
||||
- Campagne frontend verte (`46 passed` ciblés, `117 files` / `1130 tests passed` globale).
|
||||
|
||||
## 2026-08-06 — QA
|
||||
- QA a rejoué les validations ciblées frontend/backend et confirme le comportement attendu.
|
||||
- Verdict: VERT. Preuve principale: le test backend `pty_launch_is_allowed_while_structured_session_is_live` est vert, et le cas frontend d'ouverture du custom CLI malgré une session structurée de fond est vert.
|
||||
17
.ideai/tickets/169/issue.md
Normal file
17
.ideai/tickets/169/issue.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
id: "2fed8980-10b8-411a-a93e-a3d2a06c4798"
|
||||
number: 169
|
||||
title: "Custom CLI: session user dédiée même si l'agent est déjà en conversation inter-agent"
|
||||
status: "qa"
|
||||
priority: "high"
|
||||
sprint: null
|
||||
links: [{"target":"#149","kind":"relatesTo"},{"target":"#147","kind":"relatesTo"}]
|
||||
agentRefs: [{"agentId":"b4730d7f-c54d-4736-8a04-c6203aa2fd49","role":"assigned"},{"agentId":"fe887179-933f-47d4-960f-c3b06827f86c","role":"assigned"},{"agentId":"8f7da528-58df-4315-97e9-0562230ecc19","role":"assigned"}]
|
||||
attachments: []
|
||||
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
createdAt: 1786037910965
|
||||
updatedAt: 1786039145579
|
||||
version: 6
|
||||
---
|
||||
Demande utilisateur du 2026-08-06: si Main parle à Architect, l'utilisateur ne peut pas lancer la custom CLI d'Architect car IdeA dit qu'elle est déjà ouverte ailleurs. Attendu: comme pour la TUI native, la surface custom CLI doit pouvoir ouvrir une session spéciale côté user, distincte des conversations inter-agent/headless en cours, sans collision de disponibilité 'already open elsewhere'.
|
||||
24
.ideai/tickets/170/carnet.md
Normal file
24
.ideai/tickets/170/carnet.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
issueRef: "#170"
|
||||
version: 6
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedAt: 1786039145598
|
||||
---
|
||||
## 2026-08-06 — Ouverture depuis retour utilisateur
|
||||
- Repro utilisateur: le basculement de la custom CLI vers la TUI native ne se fait pas proprement; l'app dit parfois que la TUI est déjà active ailleurs.
|
||||
- Attendu produit: aucun faux positif de disponibilité lors du hand-off entre surface custom CLI et TUI native; une seule surface humaine active à la fois si c'est la règle, mais avec cleanup/transition correcte.
|
||||
- Hypothèse de cadrage: hand-off incomplet ou registry de session/surface qui conserve un état occupé après le switch.
|
||||
- Périmètre pressenti: architecture + backend + frontend.
|
||||
|
||||
## 2026-08-06 — DevBackend
|
||||
- Côté backend, le même correctif que #169 couvre le hand-off custom CLI/chat -> TUI native: un lancement PTY explicite n'est plus refusé à cause d'une session structurée existante.
|
||||
- Non-régressions vertes: `structured_launch_new_in_other_cell_refuses_when_live_elsewhere` et `structured_relaunch_other_cell_with_conversation_id_rebinds`.
|
||||
|
||||
## 2026-08-06 — DevFrontend
|
||||
- Frontend livré: le passage custom CLI -> TUI native ferme d'abord la session custom par `closeAgentChat(sessionId)`, sans stop global par agent, puis bascule vers la TUI.
|
||||
- Test ajouté pour vérifier que `stopLiveAgent` n'est pas appelé sur ce chemin.
|
||||
- Campagne frontend verte (`46 passed` ciblés, `117 files` / `1130 tests passed` globale).
|
||||
|
||||
## 2026-08-06 — QA
|
||||
- QA a rejoué les validations ciblées frontend/backend et confirme le hand-off attendu.
|
||||
- Verdict: VERT. Preuve principale: le passage custom CLI -> TUI native ferme bien la session chat structurée par `sessionId` via `closeAgentChat(sessionId)` avant retour à la TUI, sans `stopLiveAgent`, et le backend ne refuse plus le lancement PTY explicite du fait d'une structured session vivante.
|
||||
17
.ideai/tickets/170/issue.md
Normal file
17
.ideai/tickets/170/issue.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
id: "ae7f4faf-ed44-4117-84fe-4c5250060397"
|
||||
number: 170
|
||||
title: "Custom CLI <-> TUI native: éviter les faux 'TUI déjà active ailleurs' lors du basculement"
|
||||
status: "qa"
|
||||
priority: "high"
|
||||
sprint: null
|
||||
links: [{"target":"#149","kind":"relatesTo"},{"target":"#147","kind":"relatesTo"}]
|
||||
agentRefs: [{"agentId":"b4730d7f-c54d-4736-8a04-c6203aa2fd49","role":"assigned"},{"agentId":"fe887179-933f-47d4-960f-c3b06827f86c","role":"assigned"},{"agentId":"8f7da528-58df-4315-97e9-0562230ecc19","role":"assigned"}]
|
||||
attachments: []
|
||||
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||
createdAt: 1786037910985
|
||||
updatedAt: 1786039145598
|
||||
version: 6
|
||||
---
|
||||
Demande utilisateur du 2026-08-06: le passage de la custom CLI à la TUI native ne se fait pas proprement; IdeA affiche parfois que la TUI est déjà active ailleurs. Attendu: un basculement cohérent entre surface custom CLI et TUI native, avec nettoyage/hand-off de session suffisant pour qu'aucune des deux surfaces ne bloque l'autre à tort.
|
||||
@ -1,3 +1,3 @@
|
||||
{
|
||||
"nextNumber": 168
|
||||
"nextNumber": 171
|
||||
}
|
||||
@ -1991,7 +1991,7 @@
|
||||
"kind": "agent",
|
||||
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
|
||||
},
|
||||
"updatedAt": 1786036332616
|
||||
"updatedAt": 1786036825333
|
||||
},
|
||||
{
|
||||
"issueRef": "#152",
|
||||
@ -2024,8 +2024,8 @@
|
||||
{
|
||||
"issueRef": "#155",
|
||||
"path": "155",
|
||||
"title": "Custom chat: paste image from clipboard into composer",
|
||||
"status": "open",
|
||||
"title": "Custom chat: paste image/fichier depuis le clipboard dans le composer",
|
||||
"status": "qa",
|
||||
"priority": "medium",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [
|
||||
@ -2036,7 +2036,7 @@
|
||||
"kind": "agent",
|
||||
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
|
||||
},
|
||||
"updatedAt": 1786034876664
|
||||
"updatedAt": 1786039145667
|
||||
},
|
||||
{
|
||||
"issueRef": "#156",
|
||||
@ -2224,7 +2224,59 @@
|
||||
"kind": "agent",
|
||||
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
|
||||
},
|
||||
"updatedAt": 1786036332616
|
||||
"updatedAt": 1786036825352
|
||||
},
|
||||
{
|
||||
"issueRef": "#168",
|
||||
"path": "168",
|
||||
"title": "Custom CLI: transformer le bouton Send en Stop pendant une question agent",
|
||||
"status": "qa",
|
||||
"priority": "medium",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [
|
||||
"8f7da528-58df-4315-97e9-0562230ecc19"
|
||||
],
|
||||
"createdBy": {
|
||||
"kind": "agent",
|
||||
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
|
||||
},
|
||||
"updatedAt": 1786039145559
|
||||
},
|
||||
{
|
||||
"issueRef": "#169",
|
||||
"path": "169",
|
||||
"title": "Custom CLI: session user dédiée même si l'agent est déjà en conversation inter-agent",
|
||||
"status": "qa",
|
||||
"priority": "high",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [
|
||||
"b4730d7f-c54d-4736-8a04-c6203aa2fd49",
|
||||
"fe887179-933f-47d4-960f-c3b06827f86c",
|
||||
"8f7da528-58df-4315-97e9-0562230ecc19"
|
||||
],
|
||||
"createdBy": {
|
||||
"kind": "agent",
|
||||
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
|
||||
},
|
||||
"updatedAt": 1786039145579
|
||||
},
|
||||
{
|
||||
"issueRef": "#170",
|
||||
"path": "170",
|
||||
"title": "Custom CLI <-> TUI native: éviter les faux 'TUI déjà active ailleurs' lors du basculement",
|
||||
"status": "qa",
|
||||
"priority": "high",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [
|
||||
"b4730d7f-c54d-4736-8a04-c6203aa2fd49",
|
||||
"fe887179-933f-47d4-960f-c3b06827f86c",
|
||||
"8f7da528-58df-4315-97e9-0562230ecc19"
|
||||
],
|
||||
"createdBy": {
|
||||
"kind": "agent",
|
||||
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
|
||||
},
|
||||
"updatedAt": 1786039145598
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1924,51 +1924,58 @@ impl LaunchAgent {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Garde structurée (§17.4) : même sémantique côté registre IA. R0a appliqué de
|
||||
// façon identique — rebind de la cellule-vue pour une réattache légitime,
|
||||
// idempotence sans node/conversation, refus d'un second lancement neuf ailleurs.
|
||||
if let Some(structured) = &self.structured {
|
||||
if let Some(existing) =
|
||||
structured.session_for_agent_in_project(input.project.id, &input.agent_id)
|
||||
{
|
||||
let host_node =
|
||||
structured.node_for_agent_in_project(input.project.id, &input.agent_id);
|
||||
let node_id = match reattach_decision(
|
||||
input.node_id,
|
||||
host_node,
|
||||
input.conversation_id.as_deref(),
|
||||
) {
|
||||
ReattachDecision::Rebind { node_id } => {
|
||||
let _ = structured.rebind_agent_node_in_project(
|
||||
input.project.id,
|
||||
&input.agent_id,
|
||||
node_id,
|
||||
);
|
||||
node_id
|
||||
}
|
||||
// Idempotent — garder le node hôte courant, sinon un node neuf.
|
||||
ReattachDecision::Idempotent => host_node.unwrap_or_else(NodeId::new_random),
|
||||
ReattachDecision::Refuse { node_id } => {
|
||||
return Err(AppError::AgentAlreadyRunning {
|
||||
// Garde structurée (§17.4) : même sémantique côté registre IA, mais seulement
|
||||
// pour un lancement qui vise lui-même la surface structurée. Un lancement
|
||||
// humain PTY/TUI explicite doit pouvoir coexister avec une conversation
|
||||
// inter-agent/headless déjà vivante pour le même agent : les canaux d'entrée
|
||||
// et de sortie sont distincts, et la garde PTY ci-dessus reste la source de
|
||||
// vérité de l'occupation TUI utilisateur.
|
||||
if wants_structured {
|
||||
if let Some(structured) = &self.structured {
|
||||
if let Some(existing) =
|
||||
structured.session_for_agent_in_project(input.project.id, &input.agent_id)
|
||||
{
|
||||
let host_node =
|
||||
structured.node_for_agent_in_project(input.project.id, &input.agent_id);
|
||||
let node_id = match reattach_decision(
|
||||
input.node_id,
|
||||
host_node,
|
||||
input.conversation_id.as_deref(),
|
||||
) {
|
||||
ReattachDecision::Rebind { node_id } => {
|
||||
let _ = structured.rebind_agent_node_in_project(
|
||||
input.project.id,
|
||||
&input.agent_id,
|
||||
node_id,
|
||||
);
|
||||
node_id
|
||||
}
|
||||
// Idempotent — garder le node hôte courant, sinon un node neuf.
|
||||
ReattachDecision::Idempotent => {
|
||||
host_node.unwrap_or_else(NodeId::new_random)
|
||||
}
|
||||
ReattachDecision::Refuse { node_id } => {
|
||||
return Err(AppError::AgentAlreadyRunning {
|
||||
agent_id: input.agent_id,
|
||||
node_id,
|
||||
});
|
||||
}
|
||||
};
|
||||
return Ok(LaunchAgentOutput {
|
||||
session: structured_snapshot(&existing, input.agent_id, node_id, size),
|
||||
assigned_conversation_id: None,
|
||||
// Rebind de vue : aucune (ré)assignation ⇒ rien de neuf à cacher.
|
||||
engine_session_id: None,
|
||||
structured: Some(StructuredSessionDescriptor {
|
||||
session_id: existing.id(),
|
||||
agent_id: input.agent_id,
|
||||
node_id,
|
||||
});
|
||||
}
|
||||
};
|
||||
return Ok(LaunchAgentOutput {
|
||||
session: structured_snapshot(&existing, input.agent_id, node_id, size),
|
||||
assigned_conversation_id: None,
|
||||
// Rebind de vue : aucune (ré)assignation ⇒ rien de neuf à cacher.
|
||||
engine_session_id: None,
|
||||
structured: Some(StructuredSessionDescriptor {
|
||||
session_id: existing.id(),
|
||||
agent_id: input.agent_id,
|
||||
node_id,
|
||||
conversation_id: existing.conversation_id(),
|
||||
}),
|
||||
// Réattache structurée (rebind de vue) : aucun profil re-résolu.
|
||||
profile: None,
|
||||
});
|
||||
conversation_id: existing.conversation_id(),
|
||||
}),
|
||||
// Réattache structurée (rebind de vue) : aucun profil re-résolu.
|
||||
profile: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -50,8 +50,8 @@ use domain::{MemoryIndexEntry, NodeId, PtySize, SessionId, SessionKind, SkillId}
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::{
|
||||
ChangeAgentProfile, ChangeAgentProfileInput, LaunchAgent, LaunchAgentInput, StructuredSessions,
|
||||
TerminalSessions,
|
||||
ChangeAgentProfile, ChangeAgentProfileInput, LaunchAgent, LaunchAgentInput,
|
||||
StructuredRoutingMode, StructuredSessions, TerminalSessions,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -811,6 +811,14 @@ struct LaunchFixture {
|
||||
|
||||
/// Wires a `LaunchAgent.with_structured(...)` for a given profile + factory.
|
||||
fn launch_fixture(profile: AgentProfile, factory: FakeFactory) -> LaunchFixture {
|
||||
launch_fixture_with_mode(profile, factory, StructuredRoutingMode::RequireStructured)
|
||||
}
|
||||
|
||||
fn launch_fixture_with_mode(
|
||||
profile: AgentProfile,
|
||||
factory: FakeFactory,
|
||||
mode: StructuredRoutingMode,
|
||||
) -> LaunchFixture {
|
||||
let agent = scratch_agent(aid(1), "Backend", "agents/backend.md", profile.id);
|
||||
let contexts = FakeContexts::with_agent(&agent, "# ctx body");
|
||||
let profiles = FakeProfiles::new(vec![profile]);
|
||||
@ -834,7 +842,8 @@ fn launch_fixture(profile: AgentProfile, factory: FakeFactory) -> LaunchFixture
|
||||
Arc::new(FakeRecall),
|
||||
None,
|
||||
)
|
||||
.with_structured(Arc::new(factory.clone()), Arc::clone(&structured));
|
||||
.with_structured(Arc::new(factory.clone()), Arc::clone(&structured))
|
||||
.with_structured_routing_mode(mode);
|
||||
LaunchFixture {
|
||||
launch: Arc::new(launch),
|
||||
agent,
|
||||
@ -1092,6 +1101,58 @@ async fn structured_relaunch_other_cell_with_conversation_id_rebinds() {
|
||||
assert_eq!(desc.node_id, target);
|
||||
}
|
||||
|
||||
/// #169/#170 — une session structurée vivante (chat/headless) ne doit pas bloquer
|
||||
/// un lancement PTY/TUI utilisateur explicite du même agent. Les deux surfaces ont
|
||||
/// des canaux distincts ; la garde singleton de la TUI reste portée par le registre
|
||||
/// PTY, pas par `StructuredSessions`.
|
||||
#[tokio::test]
|
||||
async fn pty_launch_is_allowed_while_structured_session_is_live() {
|
||||
let factory = FakeFactory::new(500, Some("engine-conv"));
|
||||
let f = launch_fixture_with_mode(
|
||||
structured_profile(pid(9)),
|
||||
factory,
|
||||
StructuredRoutingMode::HumanPtyFallback,
|
||||
);
|
||||
|
||||
let chat_node = nid(1);
|
||||
let mut chat = launch_input(f.agent.id);
|
||||
chat.node_id = Some(chat_node);
|
||||
chat.require_structured = true;
|
||||
f.launch.execute(chat).await.expect("structured launch");
|
||||
assert_eq!(f.factory.start_count(), 1);
|
||||
assert_eq!(
|
||||
f.structured
|
||||
.session_id_for_agent_in_project(project_id(), &f.agent.id),
|
||||
Some(sid(500))
|
||||
);
|
||||
|
||||
let tui_node = nid(2);
|
||||
let mut tui = launch_input(f.agent.id);
|
||||
tui.node_id = Some(tui_node);
|
||||
tui.require_structured = false;
|
||||
let out = f.launch.execute(tui).await.expect("pty launch");
|
||||
|
||||
assert_eq!(
|
||||
f.factory.start_count(),
|
||||
1,
|
||||
"the PTY launch must not re-enter the structured session factory"
|
||||
);
|
||||
assert_eq!(f.pty.spawn_count(), 1, "the user TUI gets its own PTY");
|
||||
assert_eq!(out.session.id, sid(777));
|
||||
assert!(out.structured.is_none(), "output describes the PTY surface");
|
||||
assert_eq!(
|
||||
f.sessions
|
||||
.session_for_agent_in_project(project_id(), &f.agent.id),
|
||||
Some(sid(777))
|
||||
);
|
||||
assert_eq!(
|
||||
f.structured
|
||||
.session_id_for_agent_in_project(project_id(), &f.agent.id),
|
||||
Some(sid(500)),
|
||||
"the structured conversation remains live and distinct"
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper: executes a launch that is expected to be refused, returning the error.
|
||||
async fn second_launch_err(f: &LaunchFixture, input: LaunchAgentInput) -> application::AppError {
|
||||
f.launch
|
||||
|
||||
@ -745,11 +745,18 @@ export class MockAgentGateway implements AgentGateway {
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
// Singleton invariant: refuse a launch when the agent is already live in a
|
||||
// *different* cell (mirrors the backend `AGENT_ALREADY_RUNNING`). The same
|
||||
// node is allowed (idempotent re-launch of the very same cell).
|
||||
// Singleton invariant for the human native TUI surface: refuse a launch when
|
||||
// the agent already has a PTY in a different cell. Structured/headless
|
||||
// sessions are separate surfaces and must not make the human TUI look
|
||||
// "already active elsewhere" (#169/#170).
|
||||
const liveNode = this.liveByAgent.get(agentId);
|
||||
if (liveNode !== undefined && options.nodeId && liveNode !== options.nodeId) {
|
||||
const liveKind = this.liveKindByAgent.get(agentId) ?? "pty";
|
||||
if (
|
||||
liveNode !== undefined &&
|
||||
liveKind === "pty" &&
|
||||
options.nodeId &&
|
||||
liveNode !== options.nodeId
|
||||
) {
|
||||
const err: GatewayError = {
|
||||
code: "AGENT_ALREADY_RUNNING",
|
||||
message: `agent ${agentId} is already running in cell ${liveNode}`,
|
||||
@ -814,7 +821,13 @@ export class MockAgentGateway implements AgentGateway {
|
||||
} as GatewayError;
|
||||
}
|
||||
const liveNode = this.liveByAgent.get(agentId);
|
||||
if (liveNode !== undefined && options.nodeId && liveNode !== options.nodeId) {
|
||||
const liveKind = this.liveKindByAgent.get(agentId) ?? "pty";
|
||||
if (
|
||||
liveNode !== undefined &&
|
||||
liveKind === "pty" &&
|
||||
options.nodeId &&
|
||||
liveNode !== options.nodeId
|
||||
) {
|
||||
throw {
|
||||
code: "AGENT_ALREADY_RUNNING",
|
||||
message: `agent ${agentId} is already running in cell ${liveNode}`,
|
||||
|
||||
@ -111,8 +111,9 @@ describe("CustomAgentChatView", () => {
|
||||
expect.any(Function),
|
||||
),
|
||||
);
|
||||
expect(screen.queryByRole("button", { name: "Cancel" })).toBeNull();
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Cancel" }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(agent.cancelAgentChat).toHaveBeenCalledWith("chat-session-1"),
|
||||
@ -1080,6 +1081,81 @@ describe("CustomAgentChatView", () => {
|
||||
expect(screen.getByText("Fichier: paste-image.png")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("accepts pasted generic files exposed only through clipboardData.files", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||
sessionId,
|
||||
scrollback: [],
|
||||
})),
|
||||
sendAgentChat: vi.fn(async () => {}),
|
||||
cancelAgentChat: vi.fn(async () => {}),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
render(
|
||||
<DIProvider
|
||||
gateways={{
|
||||
agent,
|
||||
system: { pickFile: vi.fn(async () => null) },
|
||||
} as unknown as Gateways}
|
||||
>
|
||||
<CustomAgentChatView
|
||||
projectId="project-1"
|
||||
agentId="agent-1"
|
||||
agentName="Worker"
|
||||
profile={profile}
|
||||
cwd="/repo"
|
||||
nodeId="node-1"
|
||||
sessionId="chat-session-1"
|
||||
conversationId="conversation-1"
|
||||
onSessionId={vi.fn()}
|
||||
onConversationId={vi.fn()}
|
||||
/>
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(agent.reattachAgentChat).toHaveBeenCalledWith(
|
||||
"chat-session-1",
|
||||
expect.any(Function),
|
||||
),
|
||||
);
|
||||
|
||||
const file = new File(["ignored"], "files-only.pdf", { type: "application/pdf" });
|
||||
Object.defineProperty(file, "arrayBuffer", {
|
||||
value: vi.fn(async () => new Uint8Array([4, 5, 6]).buffer),
|
||||
});
|
||||
fireEvent.paste(screen.getByLabelText(/message CLI custom/), {
|
||||
clipboardData: {
|
||||
items: [],
|
||||
files: [file],
|
||||
getData: () => "",
|
||||
},
|
||||
});
|
||||
|
||||
await screen.findByText("Fichier joint: files-only.pdf");
|
||||
expect(screen.queryByTestId("attachment-preview-files-only.pdf")).toBeNull();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
|
||||
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledTimes(1));
|
||||
expect(agent.sendAgentChat).toHaveBeenCalledWith(
|
||||
"chat-session-1",
|
||||
"",
|
||||
expect.any(Function),
|
||||
{
|
||||
attachments: [
|
||||
{
|
||||
filename: "files-only.pdf",
|
||||
contentBase64: "BAUG",
|
||||
mime: "application/pdf",
|
||||
sourceKind: "clipboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the chat shell bounded with a scrollable message area and fixed composer", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(() => new Promise<never>(() => {})),
|
||||
@ -1120,14 +1196,12 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
const scroll = screen.getByTestId("custom-agent-chat-scroll");
|
||||
const composer = screen.getByTestId("custom-agent-chat-composer");
|
||||
const cancel = await screen.findByRole("button", { name: "Cancel" });
|
||||
|
||||
expect(shell.className).toContain("h-full");
|
||||
expect(shell.className).toContain("min-h-0");
|
||||
expect(shell.className).toContain("overflow-hidden");
|
||||
expect(toolbar.className).toContain("overflow-hidden");
|
||||
expect(cancel.className).toContain("shrink-0");
|
||||
expect(cancel.className).toContain("z-30");
|
||||
expect(screen.queryByRole("button", { name: "Cancel" })).toBeNull();
|
||||
expect(scroll.className).toContain("flex-1");
|
||||
expect(scroll.className).toContain("basis-0");
|
||||
expect(scroll.className).toContain("overflow-y-auto");
|
||||
@ -1446,7 +1520,7 @@ describe("CustomAgentChatView", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Cancel" }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
|
||||
|
||||
expect(await screen.findByText("Session déjà fermée.")).toBeTruthy();
|
||||
expect(onSessionId).toHaveBeenCalledWith(null);
|
||||
|
||||
@ -246,11 +246,13 @@ function bytesToBase64(bytes: Uint8Array): string {
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
async function clipboardImageToAttachment(file: File): Promise<AttachmentDraft> {
|
||||
async function clipboardFileToAttachment(file: File): Promise<AttachmentDraft> {
|
||||
const mime = file.type || "application/octet-stream";
|
||||
const filename =
|
||||
file.name ||
|
||||
`clipboard-image-${Date.now()}.${fileExtension(mime)}`;
|
||||
(mime.startsWith("image/")
|
||||
? `clipboard-image-${Date.now()}.${fileExtension(mime)}`
|
||||
: `clipboard-file-${Date.now()}`);
|
||||
const contentBase64 = bytesToBase64(new Uint8Array(await file.arrayBuffer()));
|
||||
return {
|
||||
id: `clipboard-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
@ -267,11 +269,24 @@ async function clipboardImageToAttachment(file: File): Promise<AttachmentDraft>
|
||||
};
|
||||
}
|
||||
|
||||
function clipboardImageFiles(event: ClipboardEvent<HTMLTextAreaElement>): File[] {
|
||||
return Array.from(event.clipboardData.items)
|
||||
.filter((item) => item.kind === "file" && item.type.startsWith("image/"))
|
||||
function clipboardFiles(event: ClipboardEvent<HTMLTextAreaElement>): File[] {
|
||||
const clipboardItems = event.clipboardData.items
|
||||
? Array.from(event.clipboardData.items)
|
||||
: [];
|
||||
const clipboardFiles = event.clipboardData.files
|
||||
? Array.from(event.clipboardData.files)
|
||||
: [];
|
||||
const fromItems = clipboardItems
|
||||
.filter((item) => item.kind === "file")
|
||||
.map((item) => item.getAsFile())
|
||||
.filter((file): file is File => Boolean(file));
|
||||
const seen = new Set<string>();
|
||||
return [...fromItems, ...clipboardFiles].filter((file) => {
|
||||
const key = `${file.name}\u0000${file.type}\u0000${file.size}\u0000${file.lastModified}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function slashCommandPrefix(draft: string): string | null {
|
||||
@ -835,10 +850,10 @@ export function CustomAgentChatView({
|
||||
}
|
||||
}
|
||||
|
||||
async function pasteClipboardImages(
|
||||
async function pasteClipboardFiles(
|
||||
event: ClipboardEvent<HTMLTextAreaElement>,
|
||||
) {
|
||||
const files = clipboardImageFiles(event);
|
||||
const files = clipboardFiles(event);
|
||||
if (files.length === 0) return;
|
||||
|
||||
event.preventDefault();
|
||||
@ -847,7 +862,7 @@ export function CustomAgentChatView({
|
||||
const selectionEnd = event.currentTarget.selectionEnd;
|
||||
try {
|
||||
const nextAttachments = await Promise.all(
|
||||
files.map(clipboardImageToAttachment),
|
||||
files.map(clipboardFileToAttachment),
|
||||
);
|
||||
setAttachments((prev) => [...prev, ...nextAttachments]);
|
||||
if (pastedText) {
|
||||
@ -962,18 +977,6 @@ export function CustomAgentChatView({
|
||||
CLI custom · {profile.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center justify-end gap-2">
|
||||
{(opening || busy) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
className="relative z-30 shrink-0 whitespace-nowrap shadow-sm"
|
||||
onClick={() => void cancel()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!supported && (
|
||||
@ -1192,7 +1195,7 @@ export function CustomAgentChatView({
|
||||
disabled={!supported || opening || busy}
|
||||
placeholder="Message à l'agent…"
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onPaste={(e) => void pasteClipboardImages(e)}
|
||||
onPaste={(e) => void pasteClipboardFiles(e)}
|
||||
onKeyDown={(e) => {
|
||||
if (slashMenuOpen && slashCommands.length > 0) {
|
||||
if (e.key === "ArrowDown") {
|
||||
@ -1236,12 +1239,12 @@ export function CustomAgentChatView({
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={busy ? "danger" : undefined}
|
||||
className="shrink-0 whitespace-nowrap"
|
||||
disabled={!canSend}
|
||||
loading={busy}
|
||||
onClick={() => void send()}
|
||||
disabled={busy ? !supported || !currentSession : !canSend}
|
||||
onClick={() => (busy ? void cancel() : void send())}
|
||||
>
|
||||
Envoyer
|
||||
{busy ? "Stop" : "Envoyer"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -179,6 +179,100 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
expect(screen.getAllByText(/mock-attachment.txt/).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("opens the custom CLI even when the agent already has a structured background session (#169)", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
const agent = new MockAgentGateway();
|
||||
const profileGateway = new MockProfileGateway();
|
||||
const terminal = new MockTerminalGateway();
|
||||
const system = new MockSystemGateway();
|
||||
await profileGateway.configureProfiles([structuredProfile]);
|
||||
const created = await agent.createAgent("p1", {
|
||||
name: "Worker",
|
||||
profileId: structuredProfile.id,
|
||||
});
|
||||
const tree = await layout.loadLayout("p1");
|
||||
const leafId = leaves(tree)[0].id;
|
||||
await layout.mutateLayout("p1", {
|
||||
type: "setCellAgent",
|
||||
target: leafId,
|
||||
agent: created.id,
|
||||
});
|
||||
window.localStorage.setItem(`idea.agent-cell-mode.p1.${leafId}`, "custom");
|
||||
await agent.launchAgentChat("p1", created.id, {
|
||||
cwd: "/home/me/proj",
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
nodeId: "structured-background",
|
||||
});
|
||||
const launchChat = vi.spyOn(agent, "launchAgentChat");
|
||||
|
||||
renderGrid({
|
||||
layout,
|
||||
agent,
|
||||
profile: profileGateway,
|
||||
terminal,
|
||||
system,
|
||||
} as unknown as Gateways);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("custom-agent-chat-view")).toBeTruthy(),
|
||||
);
|
||||
await waitFor(() => expect(launchChat).toHaveBeenCalled());
|
||||
expect(launchChat.mock.calls.at(-1)?.[2]).toMatchObject({ nodeId: leafId });
|
||||
expect(screen.queryByText(/déjà actif dans une autre cellule/)).toBeNull();
|
||||
});
|
||||
|
||||
it("closes the structured chat session by id before switching back to native TUI (#170)", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
const agent = new MockAgentGateway();
|
||||
const profileGateway = new MockProfileGateway();
|
||||
const terminal = new MockTerminalGateway();
|
||||
const system = new MockSystemGateway();
|
||||
await profileGateway.configureProfiles([structuredProfile]);
|
||||
const created = await agent.createAgent("p1", {
|
||||
name: "Worker",
|
||||
profileId: structuredProfile.id,
|
||||
});
|
||||
const tree = await layout.loadLayout("p1");
|
||||
const leafId = leaves(tree)[0].id;
|
||||
await layout.mutateLayout("p1", {
|
||||
type: "setCellAgent",
|
||||
target: leafId,
|
||||
agent: created.id,
|
||||
});
|
||||
window.localStorage.setItem(`idea.agent-cell-mode.p1.${leafId}`, "custom");
|
||||
const closeChat = vi.spyOn(agent, "closeAgentChat");
|
||||
const stopLive = vi.spyOn(agent, "stopLiveAgent");
|
||||
|
||||
renderGrid({
|
||||
layout,
|
||||
agent,
|
||||
profile: profileGateway,
|
||||
terminal,
|
||||
system,
|
||||
} as unknown as Gateways);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("custom-agent-chat-view")).toBeTruthy(),
|
||||
);
|
||||
await waitFor(async () => {
|
||||
const updated = await layout.loadLayout("p1");
|
||||
expect(leaves(updated)[0].session).toMatch(/^mock-agent-chat-/);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("TUI native"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole("alertdialog", { name: "Confirmer le changement de CLI" })).toBeTruthy(),
|
||||
);
|
||||
fireEvent.click(screen.getByText("Arrêter et relancer"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(closeChat).toHaveBeenCalledWith(expect.stringMatching(/^mock-agent-chat-/)),
|
||||
);
|
||||
expect(stopLive).not.toHaveBeenCalled();
|
||||
await waitFor(() => expect(screen.getByTestId("terminal-view")).toBeTruthy());
|
||||
});
|
||||
|
||||
it("requires confirmation before switching a live native TUI session", async () => {
|
||||
renderGrid(await seeded(structuredProfile));
|
||||
|
||||
|
||||
@ -712,16 +712,12 @@ function LeafView({
|
||||
|
||||
async function stopCurrentSessionForSwitch(): Promise<void> {
|
||||
if (!session) return;
|
||||
if (agentId && agentGateway?.stopLiveAgent) {
|
||||
if (cellMode === "custom" && agentGateway?.closeAgentChat) {
|
||||
await agentGateway.closeAgentChat(session);
|
||||
} else if (agentId && agentGateway?.stopLiveAgent) {
|
||||
await agentGateway.stopLiveAgent(projectId, agentId).catch(async () => {
|
||||
if (cellMode === "custom" && agentGateway.closeAgentChat) {
|
||||
await agentGateway.closeAgentChat(session);
|
||||
return;
|
||||
}
|
||||
await terminal?.closeTerminal(session);
|
||||
});
|
||||
} else if (cellMode === "custom" && agentGateway?.closeAgentChat) {
|
||||
await agentGateway.closeAgentChat(session);
|
||||
} else {
|
||||
await terminal?.closeTerminal(session);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user