From 2c722059374f8178dd328f9dadb3af4ed735533c Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 9 Sep 2026 09:54:52 +0200 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20la=20conversation=20survit=20a?= =?UTF-8?q?u=20redemarrage=20agent=20et=20a=20l'aller-retour=20projet=20?= =?UTF-8?q?=E2=80=94=20#291=20(ProjectsView/WebWorkspace=20conservent=20le?= =?UTF-8?q?=20pointeur=20viewerConversation=20mais=20ne=20rendent=20le=20v?= =?UTF-8?q?iewer=20que=20si=20sa=20conversation=20appartient=20au=20projet?= =?UTF-8?q?=20actif:=20le=20projet=20intermediaire=20affiche=20sa=20propre?= =?UTF-8?q?=20surface=20sans=20fuite,=20le=20retour=20au=20projet=20source?= =?UTF-8?q?=20remonte=20la=20vue=20avec=20la=20meme=20cle=20project/conver?= =?UTF-8?q?sation=20et=20relit=20le=20transcript=20persiste;=20useConversa?= =?UTF-8?q?tionThread=20ne=20vide=20plus=20le=20buffer=20au=20changement?= =?UTF-8?q?=20de=20conversation=20cible,=20un=20contenu=20deja=20visible?= =?UTF-8?q?=20reste=20affiche=20pendant=20relecture=20ou=20echec;=20l'erre?= =?UTF-8?q?ur=20de=20lecture=20devient=20"Impossible=20de=20recharger=20la?= =?UTF-8?q?=20conversation."=20avec=20bouton=20Reessayer=20branchant=20ref?= =?UTF-8?q?reshTail=20sans=20retirer=20les=20tours=20visibles;=20QA=20vert?= =?UTF-8?q?e=20reconfirmee=20par=20Git:=20ProjectsView.ls7=20scenario=20"r?= =?UTF-8?q?ehydrates=20the=20same=20conversation"=20(restart=20agent,=20sw?= =?UTF-8?q?itch=20alpha->beta->alpha,=20deux=20readPage=20identiques=20sur?= =?UTF-8?q?=20{projectId,=20conversationId}),=20ConversationViewer=20+=20u?= =?UTF-8?q?seConversationThread=2019/19,=20npm=20run=20build,=20git=20diff?= =?UTF-8?q?=20--check;=20reserves=20hors=20perimetre=20preexistantes:=203?= =?UTF-8?q?=20echecs=20historiques=20ProjectsView.ls7=20(helper=20panneau?= =?UTF-8?q?=20Work/open=20conversation)=20+=201=20echec=20WebDesktopWorksp?= =?UTF-8?q?ace=20(libelle=20anglais=20Project=20context);=20validation=20r?= =?UTF-8?q?untime=20AppImage/live=20restante;=20aucun=20push)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../conversations/ConversationViewer.test.tsx | 9 ++- .../conversations/ConversationViewer.tsx | 16 +++-- .../conversations/useConversationThread.ts | 3 +- .../projects/ProjectsView.ls7.test.tsx | 60 +++++++++++++++---- .../src/features/projects/ProjectsView.tsx | 18 ++---- frontend/src/features/web/WebWorkspace.tsx | 14 +++-- 6 files changed, 83 insertions(+), 37 deletions(-) diff --git a/frontend/src/features/conversations/ConversationViewer.test.tsx b/frontend/src/features/conversations/ConversationViewer.test.tsx index ef387de3..2f66b353 100644 --- a/frontend/src/features/conversations/ConversationViewer.test.tsx +++ b/frontend/src/features/conversations/ConversationViewer.test.tsx @@ -4,7 +4,7 @@ * states, scroll-up "load older" trigger, and the back button (`onClose`). */ 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 { DIProvider } from "@/app/di"; @@ -256,7 +256,12 @@ describe("ConversationViewer", () => { }; renderViewer({ conversation: failing }); 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 () => { diff --git a/frontend/src/features/conversations/ConversationViewer.tsx b/frontend/src/features/conversations/ConversationViewer.tsx index ffd54f01..278863ff 100644 --- a/frontend/src/features/conversations/ConversationViewer.tsx +++ b/frontend/src/features/conversations/ConversationViewer.tsx @@ -288,12 +288,20 @@ export function ConversationViewer({ {/* ── Error ── */} {vm.error && ( -

- {vm.error} -

+ Impossible de recharger la conversation. + + )} {/* ── Thread ── */} diff --git a/frontend/src/features/conversations/useConversationThread.ts b/frontend/src/features/conversations/useConversationThread.ts index d68cbebe..1e1e1bc4 100644 --- a/frontend/src/features/conversations/useConversationThread.ts +++ b/frontend/src/features/conversations/useConversationThread.ts @@ -151,10 +151,9 @@ export function useConversationThread( // (Re)open whenever the target conversation changes. useEffect(() => { - setBuffer([]); setHasMore(false); void open(); - }, [open, setBuffer]); + }, [open]); return { turns, busy, error, hasMore, loadOlder, refreshTail }; } diff --git a/frontend/src/features/projects/ProjectsView.ls7.test.tsx b/frontend/src/features/projects/ProjectsView.ls7.test.tsx index fe112205..9aa1d99b 100644 --- a/frontend/src/features/projects/ProjectsView.ls7.test.tsx +++ b/frontend/src/features/projects/ProjectsView.ls7.test.tsx @@ -1,8 +1,9 @@ /** * LS7 — `ProjectsView` integration: clicking a conversation in the Work panel swaps * the terminal grid for the read-only `ConversationViewer`; the back button returns - * to the grid; switching project preserves the viewer; and with no conversation - * open the terminal grid is shown unchanged (non-regression). + * to the grid; switching away hides the viewer and returning rehydrates it from the + * persisted transcript; and with no conversation open the terminal grid is shown + * unchanged (non-regression). */ import { describe, it, expect } from "vitest"; import { render, screen, fireEvent, waitFor, within } from "@testing-library/react"; @@ -186,15 +187,17 @@ describe("ProjectsView — LS7 conversation viewer integration", () => { ).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 alpha = await project.createProject("alpha", "/p/a"); await project.createProject("beta", "/p/b"); - const reads: string[] = []; + const system = new MockSystemGateway(); + const reads: Array<{ projectId: string; conversationId: string }> = []; renderView(project, { + system, conversation: { - readPage: async (projectId) => { - reads.push(projectId); + readPage: async (projectId, conversationId) => { + reads.push({ projectId, conversationId }); return { turns: [ { @@ -225,24 +228,57 @@ describe("ProjectsView — LS7 conversation viewer integration", () => { } 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" })); - openPanel("Work state"); + system.emit({ + type: "backgroundTaskChanged", + projectId: alpha.id, + agentId: "agent-1", + taskId: "task-rehydrate-291", + state: "completed", + conversationId: CONV_ID, + }); 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.findByText(`contenu du fil ${alpha.id}`); - // Switch the active project (top tab) ⇒ the viewer remains visible and - // keeps reading the conversation from the project where it was opened. + // Simulate the successful self-restart lifecycle. It must not replace the + // 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" })); + 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( await screen.findByRole("button", { name: "← Retour aux terminaux" }), ).toBeTruthy(); expect(await screen.findByText(`contenu du fil ${alpha.id}`)).toBeTruthy(); 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 () => { diff --git a/frontend/src/features/projects/ProjectsView.tsx b/frontend/src/features/projects/ProjectsView.tsx index 58abf9c8..a43c4476 100644 --- a/frontend/src/features/projects/ProjectsView.tsx +++ b/frontend/src/features/projects/ProjectsView.tsx @@ -247,6 +247,8 @@ export function ProjectsView() { const [taskToasts, setTaskToasts] = useState([]); const active = vm.openTabs.find((t) => t.id === vm.activeTabId) ?? null; + const activeViewerConversation = + viewerConversation?.projectId === active?.id ? viewerConversation : null; const projectNameById = useMemo(() => { const names = new Map(); for (const project of vm.projects) names.set(project.id, project.name); @@ -924,11 +926,11 @@ export function ProjectsView() { projectId={active.id} onActiveLayoutChange={setActiveLayout} /> - {viewerConversation ? ( + {activeViewerConversation ? ( setViewerConversation(null)} onOpenConversation={openConversation} /> @@ -953,14 +955,6 @@ export function ProjectsView() { )} - ) : viewerConversation ? ( - setViewerConversation(null)} - onOpenConversation={openConversation} - /> ) : (
diff --git a/frontend/src/features/web/WebWorkspace.tsx b/frontend/src/features/web/WebWorkspace.tsx index d4b16587..581416d7 100644 --- a/frontend/src/features/web/WebWorkspace.tsx +++ b/frontend/src/features/web/WebWorkspace.tsx @@ -279,9 +279,13 @@ function WebDesktopWorkspace({ useEffect(() => { setActiveLayout(null); - setViewerConversation(null); }, [openId]); + const activeViewerConversation = + viewerConversation?.projectId === openProject?.id + ? viewerConversation + : null; + function setPlacement(panel: WebDesktopPanelId, placement: Exclude) { setPlacements((prev) => { const next: WebDesktopPlacements = { ...prev }; @@ -535,11 +539,11 @@ function WebDesktopWorkspace({ projectId={openProject.id} onActiveLayoutChange={setActiveLayout} /> - {viewerConversation ? ( + {activeViewerConversation ? ( setViewerConversation(null)} onOpenConversation={openConversation} />