merge(frontend): la conversation survit au redemarrage agent et a l'aller-retour projet — #291 (QA verte reconfirmee par Git: ProjectsView.ls7 scenario "rehydrates the same conversation", ConversationViewer + useConversationThread 19/19, npm run build, git diff --check; branche fix/291-keep-conversation-after-restart-project-roundtrip commit 2c72205 + chore(tickets) 503f6e5; restants: rebuild AppImage + validation live restart puis aller-retour projet; reserves hors perimetre preexistantes: 3 echecs historiques ProjectsView.ls7 + 1 WebDesktopWorkspace (libelle Project context); scripts/ hors perimetre; aucun push)
This commit is contained in:
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#290"
|
issueRef: "#290"
|
||||||
version: 11
|
version: 12
|
||||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||||
updatedAt: 1788937781234
|
updatedAt: 1788938529924
|
||||||
---
|
---
|
||||||
# Ticket #290 - Redemarrage agent finalise le tour et bloque le chat
|
# Ticket #290 - Redemarrage agent finalise le tour et bloque le chat
|
||||||
|
|
||||||
@ -170,3 +170,19 @@ Couverture verifiee:
|
|||||||
|
|
||||||
Reserve:
|
Reserve:
|
||||||
- Validation runtime AppImage/live non executee dans cette passe QA; les tests automatises prouvent le contrat backend/transport, pas une relance manuelle de l'AppImage en cours d'utilisation.
|
- Validation runtime AppImage/live non executee dans cette passe QA; les tests automatises prouvent le contrat backend/transport, pas une relance manuelle de l'AppImage en cours d'utilisation.
|
||||||
|
|
||||||
|
## Git regression partielle
|
||||||
|
- Commit fix: `18c1b01` sur `fix/290-self-restart-resume-objective`.
|
||||||
|
- Merge local no-ff vers `develop`: `162de27`.
|
||||||
|
- Aucun push.
|
||||||
|
- Branche finale active: `develop`.
|
||||||
|
- Worktree propre sauf `scripts/restart-idea-over-ssh.sh` non suivi hors perimetre recurrent.
|
||||||
|
- Git a reconfirme avant commit: `cargo +stable check --workspace`, tests cibles, fmt et diff-check verts.
|
||||||
|
|
||||||
|
## Build AppImage regression partielle
|
||||||
|
- Skill `build-appimage` applique le 2026-09-09.
|
||||||
|
- `npm --prefix frontend run build` depuis project root -> vert.
|
||||||
|
- `CARGO_HOME=/tmp/idea-cargo-home APPIMAGE_EXTRACT_AND_RUN=1 NO_STRIP=1 ../../frontend/node_modules/.bin/tauri build --bundles appimage` depuis `crates/app-tauri` -> vert.
|
||||||
|
- Le `beforeBuildCommand` a aussi produit `frontend/dist` et `frontend/dist-web`.
|
||||||
|
- Artefact produit: `/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage`.
|
||||||
|
- Validation live restante: remplacer l'AppImage installee et relancer IdeA, puis refaire le test utilisateur exact: demander a un agent de redemarrer et de confirmer apres relance; le premier tour post-restart doit reprendre cet objectif exact.
|
||||||
@ -11,7 +11,7 @@ attachments: []
|
|||||||
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||||
createdAt: 1788934173642
|
createdAt: 1788934173642
|
||||||
updatedAt: 1788937781234
|
updatedAt: 1788938529924
|
||||||
version: 11
|
version: 12
|
||||||
---
|
---
|
||||||
Bug rapporte par l'utilisateur le 2026-09-09: apres une demande de redemarrage, l'agent lance le redemarrage puis envoie un message; ce message est considere comme Final, le tour se termine, Stop repond "Request already stopped", et le chat devient bloque en ecriture. Attachement de reproduction: /home/anthony/Documents/Projects/IdeA/.ideai/attachments/agent-chat/7d2abba7-ed93-405f-9fe5-857c8455e436/9a881769-d8e8-410d-85f1-7a3ec2afdde0-clipboard-image-1788934010151.png. Objectif: corriger le flux pour que le redemarrage aboutisse vraiment, que l'utilisateur recupere un chat utilisable, et que le message de succes ne soit pas capture comme final prematurant le rendez-vous.
|
Bug rapporte par l'utilisateur le 2026-09-09: apres une demande de redemarrage, l'agent lance le redemarrage puis envoie un message; ce message est considere comme Final, le tour se termine, Stop repond "Request already stopped", et le chat devient bloque en ecriture. Attachement de reproduction: /home/anthony/Documents/Projects/IdeA/.ideai/attachments/agent-chat/7d2abba7-ed93-405f-9fe5-857c8455e436/9a881769-d8e8-410d-85f1-7a3ec2afdde0-clipboard-image-1788934010151.png. Objectif: corriger le flux pour que le redemarrage aboutisse vraiment, que l'utilisateur recupere un chat utilisable, et que le message de succes ne soit pas capture comme final prematurant le rendez-vous.
|
||||||
180
.ideai/tickets/291/carnet.md
Normal file
180
.ideai/tickets/291/carnet.md
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#291"
|
||||||
|
version: 5
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||||
|
updatedAt: 1788940207349
|
||||||
|
---
|
||||||
|
# Carnet #291
|
||||||
|
|
||||||
|
## Signalement utilisateur
|
||||||
|
Le redémarrage agent est fonctionnel et l'affichage initial après redémarrage est correct. Régression restante: après un changement de projet puis retour sur le projet courant, la conversation s'efface si un redémarrage de l'agent a eu lieu.
|
||||||
|
|
||||||
|
## Attendu produit
|
||||||
|
La conversation doit rester visible et rechargée comme si aucun redémarrage agent n'avait eu lieu.
|
||||||
|
|
||||||
|
## Reproduction cible
|
||||||
|
1. Projet A ouvert, conversation agent visible.
|
||||||
|
2. Redémarrer l'agent.
|
||||||
|
3. Vérifier que la conversation reste visible.
|
||||||
|
4. Basculer vers un autre projet.
|
||||||
|
5. Revenir au projet A.
|
||||||
|
6. Vérifier que la conversation est toujours présente.
|
||||||
|
|
||||||
|
## Cadrage UX
|
||||||
|
- Le transcript persisté est source de vérité après changement de projet, redémarrage app ou réhydratation de surface.
|
||||||
|
- Le live buffer ne complète que la session active; un buffer vide ne doit jamais écraser un transcript persisté non vide.
|
||||||
|
- Pendant la réhydratation, ne pas afficher prématurément `Aucun message`; garder l'ancien contenu si disponible ou afficher un chargement transitoire.
|
||||||
|
- En erreur réelle de lecture: afficher `Impossible de recharger la conversation. Réessayer` sans remplacer un contenu ancien disponible.
|
||||||
|
|
||||||
|
## Cadrage architecture
|
||||||
|
- Frontière probable: frontend project switching / reconnect et reconstruction du viewer, pas persistance backend en première intention.
|
||||||
|
- Invariant: `conversationId` stable à travers restart agent, changement de projet, reconnect/refresh; contenu lu via `Gateways.conversation.readPage` / `read_conversation_page`.
|
||||||
|
- Le live-state peut porter un pointeur mais ne doit pas devenir source de vérité du transcript.
|
||||||
|
- Inspecter notamment `ProjectsView.tsx`, `WebWorkspace.tsx`, `ConversationViewer.tsx`, `useConversationThread.ts`, adapters conversation, source de `conversationId` dans ports/workstate/layout.
|
||||||
|
- Backend à vérifier seulement si perte de projection `conversationId` après restart/open_project.
|
||||||
|
|
||||||
|
## Critères QA
|
||||||
|
- Scénario restart agent + changement projet + retour projet: messages pré et post restart visibles, ordre conservé, sans duplication.
|
||||||
|
- Test automatisé frontend attendu: `conversationId` stable, switch projet puis retour, `ConversationGateway.readPage` relancé avec le même id.
|
||||||
|
- Test backend de projection workstate seulement si l'inspection révèle une perte côté backend.
|
||||||
|
|
||||||
|
## Orchestration
|
||||||
|
- Ticket créé par Main le 2026-09-09.
|
||||||
|
- UX et Architect consultés.
|
||||||
|
- Prochaine étape: Git pour branche locale, puis DevFrontend en première intention.
|
||||||
|
|
||||||
|
## Livraison DevFrontend — 2026-09-09
|
||||||
|
|
||||||
|
### Diagnostic
|
||||||
|
- `ProjectsView` conservait bien `{ projectId, conversationId }`, mais rendait le viewer du projet source même lorsqu'un autre projet devenait actif. Le composant ne se démontait donc pas pendant l'aller-retour et `useConversationThread` ne relançait pas `ConversationGateway.readPage` au retour, puisque ses identifiants n'avaient jamais changé.
|
||||||
|
- `WebWorkspace` avait l'écart symétrique: il supprimait entièrement le pointeur du viewer à chaque changement de projet, empêchant toute réhydratation au retour.
|
||||||
|
- Les adapters Tauri et HTTP transmettent déjà correctement le même `projectId` / `conversationId` à `read_conversation_page`; aucune preuve de perte backend du pointeur n'a été trouvée.
|
||||||
|
|
||||||
|
### Correctif
|
||||||
|
- Le viewer conservé n'est rendu que si son `projectId` correspond au projet actif. Le projet intermédiaire affiche sa propre surface; le retour au projet source remonte le viewer et relance la lecture persistée avec le même `conversationId`.
|
||||||
|
- Même règle appliquée au shell web; son pointeur n'est plus effacé au switch.
|
||||||
|
- `useConversationThread` ne vide plus préventivement un buffer existant avant la relecture. Le premier montage affiche toujours `Chargement du fil…`; un contenu déjà disponible reste visible pendant une relecture ou en cas d'échec.
|
||||||
|
- L'erreur de lecture affiche `Impossible de recharger la conversation.` et l'action `Réessayer`, sans retirer les tours déjà visibles.
|
||||||
|
|
||||||
|
### Fichiers modifiés
|
||||||
|
- `frontend/src/features/projects/ProjectsView.tsx`
|
||||||
|
- `frontend/src/features/web/WebWorkspace.tsx`
|
||||||
|
- `frontend/src/features/conversations/useConversationThread.ts`
|
||||||
|
- `frontend/src/features/conversations/ConversationViewer.tsx`
|
||||||
|
- `frontend/src/features/projects/ProjectsView.ls7.test.tsx`
|
||||||
|
- `frontend/src/features/conversations/ConversationViewer.test.tsx`
|
||||||
|
|
||||||
|
### Preuves exécutées
|
||||||
|
- `npm --prefix frontend test -- --run src/features/projects/ProjectsView.ls7.test.tsx -t "rehydrates the same conversation"` — OK, 1 test passé (8 ignorés par le filtre). Le test simule le restart, A→B→A, vérifie le contenu et deux appels `readPage` strictement identiques sur `{ projectId, conversationId }`.
|
||||||
|
- `npm --prefix frontend test -- --run src/features/conversations/ConversationViewer.test.tsx src/features/conversations/useConversationThread.test.tsx` — OK, 2 fichiers / 19 tests passés.
|
||||||
|
- `npm --prefix frontend run build` — OK (`tsc --noEmit` puis build Vite, 515 modules).
|
||||||
|
- `git diff --check` — OK.
|
||||||
|
|
||||||
|
### Gaps observés hors lot
|
||||||
|
- Le fichier complet `ProjectsView.ls7.test.tsx` conserve 4 scénarios historiques KO car leur helper ne parvient plus à ouvrir le panneau Work et ne trouve pas `open conversation …`; le nouveau scénario #291 passe isolément et ouvre le viewer via l'événement de complétion persistant.
|
||||||
|
- `WebDesktopWorkspace.test.tsx` a 1 test historique KO sur un libellé attendu en anglais (`Project context`) alors que l'UI expose désormais `Contexte projet`; 2 autres tests passent. Aucun de ces échecs ne touche le chemin #291.
|
||||||
|
- Aucun gap backend détecté.
|
||||||
|
|
||||||
|
## Validation QA indépendante — 2026-09-09
|
||||||
|
|
||||||
|
### Verdict
|
||||||
|
VERT pour le périmètre #291 sur `fix/291-keep-conversation-after-restart-project-roundtrip`.
|
||||||
|
|
||||||
|
### Inspection
|
||||||
|
- Diff vérifié sur `ProjectsView.tsx`: le viewer conserve son pointeur global, mais n'est rendu que lorsque `viewerConversation.projectId === active.id`; le switch vers un autre projet démonte donc la surface et le retour remonte le viewer avec la même clé `{projectId, conversationId}`.
|
||||||
|
- Diff vérifié sur `WebWorkspace.tsx`: même règle côté web, sans remise à `null` du pointeur au changement de projet.
|
||||||
|
- Diff vérifié sur `useConversationThread.ts`: suppression du `setBuffer([])` au changement/rechargement; le buffer existant n'est plus vidé pendant une relecture.
|
||||||
|
- Diff vérifié sur `ConversationViewer.tsx`: l'erreur de lecture expose un état de retry sans remplacer explicitement un contenu déjà présent.
|
||||||
|
- Test #291 ajouté dans `ProjectsView.ls7.test.tsx`: simule restart agent, switch alpha -> beta -> alpha, vérifie le contenu et deux appels `readPage` identiques sur `{ projectId, conversationId }`.
|
||||||
|
|
||||||
|
### Commandes exécutées
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short --branch
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
## fix/291-keep-conversation-after-restart-project-roundtrip
|
||||||
|
M .ideai/tickets/290/carnet.md
|
||||||
|
M .ideai/tickets/290/issue.md
|
||||||
|
M .ideai/tickets/counter.json
|
||||||
|
M .ideai/tickets/index.json
|
||||||
|
M frontend/src/features/conversations/ConversationViewer.test.tsx
|
||||||
|
M frontend/src/features/conversations/ConversationViewer.tsx
|
||||||
|
M frontend/src/features/conversations/useConversationThread.ts
|
||||||
|
M frontend/src/features/projects/ProjectsView.ls7.test.tsx
|
||||||
|
M frontend/src/features/projects/ProjectsView.tsx
|
||||||
|
M frontend/src/features/web/WebWorkspace.tsx
|
||||||
|
?? .ideai/tickets/291/
|
||||||
|
?? scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm --prefix frontend test -- --run src/features/projects/ProjectsView.ls7.test.tsx -t "rehydrates the same conversation"
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
Test Files 1 passed (1)
|
||||||
|
Tests 1 passed | 8 skipped (9)
|
||||||
|
Duration 2.38s
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm --prefix frontend test -- --run src/features/conversations/ConversationViewer.test.tsx src/features/conversations/useConversationThread.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
Test Files 2 passed (2)
|
||||||
|
Tests 19 passed (19)
|
||||||
|
Duration 1.34s
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm --prefix frontend run build
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
> idea-frontend@0.3.0 build
|
||||||
|
> tsc --noEmit && vite build
|
||||||
|
✓ 515 modules transformed.
|
||||||
|
✓ built in 2.60s
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
OK (aucune sortie)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test additionnel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm --prefix frontend test -- --run src/features/projects/ProjectsView.ls7.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
Test Files 1 failed (1)
|
||||||
|
Tests 3 failed | 6 passed (9)
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests rouges observés dans ce fichier complet:
|
||||||
|
- `opens the viewer on a conversation click and returns to the grid on back`
|
||||||
|
- `keeps the viewer open when the active layout changes`
|
||||||
|
- `does not close an open viewer when a background-task toast has no conversation`
|
||||||
|
|
||||||
|
Ces trois échecs cherchent encore `open conversation ...` via l'ancien helper/panneau Work et correspondent aux rouges historiques déjà mentionnés hors lot. Le nouveau scénario #291 `rehydrates the same conversation after an agent restart and a project roundtrip` passe dans ce run complet.
|
||||||
|
|
||||||
|
### Risques résiduels
|
||||||
|
- Validation effectuée par tests frontend et build local, pas par AppImage/runtime desktop réel.
|
||||||
|
- Le worktree contient des modifications non liées à #291 (`.ideai/tickets/290/*`, index/counter tickets, `scripts/`) que QA n'a pas modifiées ni qualifiées.
|
||||||
|
|
||||||
|
## Git — 2026-09-09
|
||||||
|
- Branche: `fix/291-keep-conversation-after-restart-project-roundtrip` créée depuis `develop`.
|
||||||
|
- Commit fix: `2c72205` (6 fichiers frontend uniquement).
|
||||||
|
- QA reconfirmée par Git avant commit: ConversationViewer + useConversationThread 19/19, scénario #291 `rehydrates the same conversation` isolé vert, `git diff --check` OK.
|
||||||
|
- Store de tickets synchronisé dans un `chore(tickets)` séparé (inclut carnet #290 v12 sections Git + build AppImage du merge `162de27`).
|
||||||
|
- Merge local no-ff vers `develop`, aucun push.
|
||||||
|
- Restants: rebuild AppImage + validation live du scénario exact (restart agent puis aller-retour projet).
|
||||||
|
- `scripts/` non suivi maintenu hors périmètre.
|
||||||
25
.ideai/tickets/291/issue.md
Normal file
25
.ideai/tickets/291/issue.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
id: "729aaeb8-0601-4ad0-aed4-4ca625856c62"
|
||||||
|
number: 291
|
||||||
|
title: "Conserver la conversation après redémarrage agent et aller-retour projet"
|
||||||
|
status: "qa"
|
||||||
|
priority: "high"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: []
|
||||||
|
attachments: []
|
||||||
|
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||||
|
createdAt: 1788939576761
|
||||||
|
updatedAt: 1788940207349
|
||||||
|
version: 5
|
||||||
|
---
|
||||||
|
Bug utilisateur: le redémarrage de l'agent fonctionne et l'affichage initial est correct, mais si l'utilisateur change de projet puis revient sur le projet courant après un redémarrage, la conversation affichée s'efface. Attendu: la conversation reste en place comme s'il n'y avait jamais eu de redémarrage de l'agent.
|
||||||
|
|
||||||
|
Scénario de reproduction attendu:
|
||||||
|
1. Ouvrir un projet avec une conversation agent visible.
|
||||||
|
2. Redémarrer l'agent.
|
||||||
|
3. Constater que la conversation reste affichée immédiatement après le redémarrage.
|
||||||
|
4. Changer de projet.
|
||||||
|
5. Revenir sur le projet initial.
|
||||||
|
6. La conversation doit toujours être présente et rechargée depuis la source persistée, sans écran vide lié au redémarrage.
|
||||||
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"nextNumber": 291
|
"nextNumber": 292
|
||||||
}
|
}
|
||||||
@ -3978,7 +3978,21 @@
|
|||||||
"kind": "agent",
|
"kind": "agent",
|
||||||
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
|
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
|
||||||
},
|
},
|
||||||
"updatedAt": 1788937781234
|
"updatedAt": 1788938529924
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issueRef": "#291",
|
||||||
|
"path": "291",
|
||||||
|
"title": "Conserver la conversation après redémarrage agent et aller-retour projet",
|
||||||
|
"status": "qa",
|
||||||
|
"priority": "high",
|
||||||
|
"sprint": null,
|
||||||
|
"assignedAgentIds": [],
|
||||||
|
"createdBy": {
|
||||||
|
"kind": "agent",
|
||||||
|
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
|
||||||
|
},
|
||||||
|
"updatedAt": 1788940207349
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -4,7 +4,7 @@
|
|||||||
* states, scroll-up "load older" trigger, and the back button (`onClose`).
|
* states, scroll-up "load older" trigger, and the back button (`onClose`).
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent, within } from "@testing-library/react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { DIProvider } from "@/app/di";
|
import { DIProvider } from "@/app/di";
|
||||||
@ -256,7 +256,12 @@ describe("ConversationViewer", () => {
|
|||||||
};
|
};
|
||||||
renderViewer({ conversation: failing });
|
renderViewer({ conversation: failing });
|
||||||
const alert = await screen.findByRole("alert");
|
const alert = await screen.findByRole("alert");
|
||||||
expect(alert.textContent).toContain("lecture impossible");
|
expect(alert.textContent).toContain(
|
||||||
|
"Impossible de recharger la conversation.",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
within(alert).getByRole("button", { name: "Réessayer" }),
|
||||||
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("offers a 'load older' control when more turns exist and triggers loadOlder", async () => {
|
it("offers a 'load older' control when more turns exist and triggers loadOlder", async () => {
|
||||||
|
|||||||
@ -288,12 +288,20 @@ export function ConversationViewer({
|
|||||||
|
|
||||||
{/* ── Error ── */}
|
{/* ── Error ── */}
|
||||||
{vm.error && (
|
{vm.error && (
|
||||||
<p
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
className="mx-4 mt-2 shrink-0 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger"
|
className="mx-4 mt-2 flex shrink-0 items-center justify-between gap-3 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger"
|
||||||
>
|
>
|
||||||
{vm.error}
|
<span>Impossible de recharger la conversation.</span>
|
||||||
</p>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => void vm.refreshTail()}
|
||||||
|
loading={vm.busy}
|
||||||
|
>
|
||||||
|
Réessayer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Thread ── */}
|
{/* ── Thread ── */}
|
||||||
|
|||||||
@ -151,10 +151,9 @@ export function useConversationThread(
|
|||||||
|
|
||||||
// (Re)open whenever the target conversation changes.
|
// (Re)open whenever the target conversation changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBuffer([]);
|
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
void open();
|
void open();
|
||||||
}, [open, setBuffer]);
|
}, [open]);
|
||||||
|
|
||||||
return { turns, busy, error, hasMore, loadOlder, refreshTail };
|
return { turns, busy, error, hasMore, loadOlder, refreshTail };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* LS7 — `ProjectsView` integration: clicking a conversation in the Work panel swaps
|
* LS7 — `ProjectsView` integration: clicking a conversation in the Work panel swaps
|
||||||
* the terminal grid for the read-only `ConversationViewer`; the back button returns
|
* the terminal grid for the read-only `ConversationViewer`; the back button returns
|
||||||
* to the grid; switching project preserves the viewer; and with no conversation
|
* to the grid; switching away hides the viewer and returning rehydrates it from the
|
||||||
* open the terminal grid is shown unchanged (non-regression).
|
* persisted transcript; and with no conversation open the terminal grid is shown
|
||||||
|
* unchanged (non-regression).
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
|
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
|
||||||
@ -186,15 +187,17 @@ describe("ProjectsView — LS7 conversation viewer integration", () => {
|
|||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the viewer attached to its source project when the active project changes", async () => {
|
it("rehydrates the same conversation after an agent restart and a project roundtrip", async () => {
|
||||||
const project = new MockProjectGateway();
|
const project = new MockProjectGateway();
|
||||||
const alpha = await project.createProject("alpha", "/p/a");
|
const alpha = await project.createProject("alpha", "/p/a");
|
||||||
await project.createProject("beta", "/p/b");
|
await project.createProject("beta", "/p/b");
|
||||||
const reads: string[] = [];
|
const system = new MockSystemGateway();
|
||||||
|
const reads: Array<{ projectId: string; conversationId: string }> = [];
|
||||||
renderView(project, {
|
renderView(project, {
|
||||||
|
system,
|
||||||
conversation: {
|
conversation: {
|
||||||
readPage: async (projectId) => {
|
readPage: async (projectId, conversationId) => {
|
||||||
reads.push(projectId);
|
reads.push({ projectId, conversationId });
|
||||||
return {
|
return {
|
||||||
turns: [
|
turns: [
|
||||||
{
|
{
|
||||||
@ -225,24 +228,57 @@ describe("ProjectsView — LS7 conversation viewer integration", () => {
|
|||||||
}
|
}
|
||||||
await waitFor(() => expect(screen.getAllByRole("tab")).toHaveLength(2));
|
await waitFor(() => expect(screen.getAllByRole("tab")).toHaveLength(2));
|
||||||
|
|
||||||
// Make alpha the active tab, then open its conversation viewer.
|
// Make alpha the active tab, then open its persisted conversation from a
|
||||||
|
// completion event carrying the stable conversation pointer.
|
||||||
fireEvent.click(screen.getByRole("tab", { name: "alpha" }));
|
fireEvent.click(screen.getByRole("tab", { name: "alpha" }));
|
||||||
openPanel("Work state");
|
system.emit({
|
||||||
|
type: "backgroundTaskChanged",
|
||||||
|
projectId: alpha.id,
|
||||||
|
agentId: "agent-1",
|
||||||
|
taskId: "task-rehydrate-291",
|
||||||
|
state: "completed",
|
||||||
|
conversationId: CONV_ID,
|
||||||
|
});
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
await screen.findByRole("button", { name: `open conversation ${CONV_ID}` }),
|
await screen.findByRole("button", { name: /Background task completed/i }),
|
||||||
);
|
);
|
||||||
await screen.findByRole("button", { name: "← Retour aux terminaux" });
|
await screen.findByRole("button", { name: "← Retour aux terminaux" });
|
||||||
|
await screen.findByText(`contenu du fil ${alpha.id}`);
|
||||||
|
|
||||||
// Switch the active project (top tab) ⇒ the viewer remains visible and
|
// Simulate the successful self-restart lifecycle. It must not replace the
|
||||||
// keeps reading the conversation from the project where it was opened.
|
// persisted conversation pointer with a fresh live buffer.
|
||||||
|
system.emit({
|
||||||
|
type: "agentRestartRequested",
|
||||||
|
agentId: "agent-1",
|
||||||
|
restartId: "restart-291",
|
||||||
|
reason: null,
|
||||||
|
});
|
||||||
|
system.emit({
|
||||||
|
type: "agentRestartCompleted",
|
||||||
|
agentId: "agent-1",
|
||||||
|
restartId: "restart-291",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switching away shows beta's own surface instead of leaking alpha's viewer.
|
||||||
fireEvent.click(screen.getByRole("tab", { name: "beta" }));
|
fireEvent.click(screen.getByRole("tab", { name: "beta" }));
|
||||||
|
expect(await screen.findByTestId("layout-grid")).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("button", { name: "← Retour aux terminaux" }),
|
||||||
|
).toBeNull();
|
||||||
|
|
||||||
|
// Returning remounts the viewer and reloads the persisted transcript with
|
||||||
|
// exactly the same project/conversation identity.
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "alpha" }));
|
||||||
expect(
|
expect(
|
||||||
await screen.findByRole("button", { name: "← Retour aux terminaux" }),
|
await screen.findByRole("button", { name: "← Retour aux terminaux" }),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(await screen.findByText(`contenu du fil ${alpha.id}`)).toBeTruthy();
|
expect(await screen.findByText(`contenu du fil ${alpha.id}`)).toBeTruthy();
|
||||||
expect(screen.queryByTestId("layout-grid")).toBeNull();
|
expect(screen.queryByTestId("layout-grid")).toBeNull();
|
||||||
expect(reads.every((projectId) => projectId === alpha.id)).toBe(true);
|
await waitFor(() => expect(reads).toHaveLength(2));
|
||||||
|
expect(reads).toEqual([
|
||||||
|
{ projectId: alpha.id, conversationId: CONV_ID },
|
||||||
|
{ projectId: alpha.id, conversationId: CONV_ID },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the viewer open when the active layout changes", async () => {
|
it("keeps the viewer open when the active layout changes", async () => {
|
||||||
|
|||||||
@ -247,6 +247,8 @@ export function ProjectsView() {
|
|||||||
const [taskToasts, setTaskToasts] = useState<BackgroundTaskToast[]>([]);
|
const [taskToasts, setTaskToasts] = useState<BackgroundTaskToast[]>([]);
|
||||||
|
|
||||||
const active = vm.openTabs.find((t) => t.id === vm.activeTabId) ?? null;
|
const active = vm.openTabs.find((t) => t.id === vm.activeTabId) ?? null;
|
||||||
|
const activeViewerConversation =
|
||||||
|
viewerConversation?.projectId === active?.id ? viewerConversation : null;
|
||||||
const projectNameById = useMemo(() => {
|
const projectNameById = useMemo(() => {
|
||||||
const names = new Map<string, string>();
|
const names = new Map<string, string>();
|
||||||
for (const project of vm.projects) names.set(project.id, project.name);
|
for (const project of vm.projects) names.set(project.id, project.name);
|
||||||
@ -924,11 +926,11 @@ export function ProjectsView() {
|
|||||||
projectId={active.id}
|
projectId={active.id}
|
||||||
onActiveLayoutChange={setActiveLayout}
|
onActiveLayoutChange={setActiveLayout}
|
||||||
/>
|
/>
|
||||||
{viewerConversation ? (
|
{activeViewerConversation ? (
|
||||||
<ConversationViewer
|
<ConversationViewer
|
||||||
key={`${viewerConversation.projectId}-${viewerConversation.conversationId}`}
|
key={`${activeViewerConversation.projectId}-${activeViewerConversation.conversationId}`}
|
||||||
projectId={viewerConversation.projectId}
|
projectId={activeViewerConversation.projectId}
|
||||||
conversationId={viewerConversation.conversationId}
|
conversationId={activeViewerConversation.conversationId}
|
||||||
onClose={() => setViewerConversation(null)}
|
onClose={() => setViewerConversation(null)}
|
||||||
onOpenConversation={openConversation}
|
onOpenConversation={openConversation}
|
||||||
/>
|
/>
|
||||||
@ -953,14 +955,6 @@ export function ProjectsView() {
|
|||||||
</AnnouncementsProvider>
|
</AnnouncementsProvider>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : viewerConversation ? (
|
|
||||||
<ConversationViewer
|
|
||||||
key={`${viewerConversation.projectId}-${viewerConversation.conversationId}`}
|
|
||||||
projectId={viewerConversation.projectId}
|
|
||||||
conversationId={viewerConversation.conversationId}
|
|
||||||
onClose={() => setViewerConversation(null)}
|
|
||||||
onOpenConversation={openConversation}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 justify-center overflow-y-auto p-6">
|
<div className="flex flex-1 justify-center overflow-y-auto p-6">
|
||||||
<div className="w-full max-w-xl">
|
<div className="w-full max-w-xl">
|
||||||
|
|||||||
@ -279,9 +279,13 @@ function WebDesktopWorkspace({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveLayout(null);
|
setActiveLayout(null);
|
||||||
setViewerConversation(null);
|
|
||||||
}, [openId]);
|
}, [openId]);
|
||||||
|
|
||||||
|
const activeViewerConversation =
|
||||||
|
viewerConversation?.projectId === openProject?.id
|
||||||
|
? viewerConversation
|
||||||
|
: null;
|
||||||
|
|
||||||
function setPlacement(panel: WebDesktopPanelId, placement: Exclude<ViewPlacement, "detached">) {
|
function setPlacement(panel: WebDesktopPanelId, placement: Exclude<ViewPlacement, "detached">) {
|
||||||
setPlacements((prev) => {
|
setPlacements((prev) => {
|
||||||
const next: WebDesktopPlacements = { ...prev };
|
const next: WebDesktopPlacements = { ...prev };
|
||||||
@ -535,11 +539,11 @@ function WebDesktopWorkspace({
|
|||||||
projectId={openProject.id}
|
projectId={openProject.id}
|
||||||
onActiveLayoutChange={setActiveLayout}
|
onActiveLayoutChange={setActiveLayout}
|
||||||
/>
|
/>
|
||||||
{viewerConversation ? (
|
{activeViewerConversation ? (
|
||||||
<ConversationViewer
|
<ConversationViewer
|
||||||
key={`${viewerConversation.projectId}-${viewerConversation.conversationId}`}
|
key={`${activeViewerConversation.projectId}-${activeViewerConversation.conversationId}`}
|
||||||
projectId={viewerConversation.projectId}
|
projectId={activeViewerConversation.projectId}
|
||||||
conversationId={viewerConversation.conversationId}
|
conversationId={activeViewerConversation.conversationId}
|
||||||
onClose={() => setViewerConversation(null)}
|
onClose={() => setViewerConversation(null)}
|
||||||
onOpenConversation={openConversation}
|
onOpenConversation={openConversation}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user