diff --git a/frontend/src/features/agents/CustomAgentChatView.test.tsx b/frontend/src/features/agents/CustomAgentChatView.test.tsx index 6ee04ff..903638a 100644 --- a/frontend/src/features/agents/CustomAgentChatView.test.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { useState } from "react"; import { describe, expect, it, vi } from "vitest"; import { DIProvider } from "@/app/di"; @@ -151,6 +152,63 @@ describe("CustomAgentChatView", () => { expect(screen.queryByText(/structured session gone/)).toBeNull(); }); + it("does not reopen when the parent echoes a launched session id", async () => { + const agent = { + launchAgentChat: vi.fn(async () => ({ + sessionId: "chat-session-echo", + assignedConversationId: "conversation-2", + })), + reattachAgentChat: vi.fn(async (sessionId: string) => ({ + sessionId, + scrollback: [], + })), + sendAgentChat: vi.fn(() => new Promise(() => {})), + cancelAgentChat: vi.fn(async () => {}), + closeAgentChat: vi.fn(async () => {}), + }; + const onSessionId = vi.fn(); + + function EchoingParent() { + const [echoedSessionId, setEchoedSessionId] = useState(null); + return ( + null) }, + } as unknown as Gateways} + > + { + onSessionId(nextSessionId); + setEchoedSessionId(nextSessionId); + }} + onConversationId={vi.fn()} + /> + + ); + } + + render(); + + await waitFor(() => + expect(onSessionId).toHaveBeenCalledWith("chat-session-echo"), + ); + await waitFor(() => expect(agent.launchAgentChat).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(agent.reattachAgentChat).toHaveBeenCalledTimes(1)); + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(agent.launchAgentChat).toHaveBeenCalledTimes(1); + expect(agent.reattachAgentChat).toHaveBeenCalledTimes(1); + }); + it("recovers and retries the prompt when send reports NOT_FOUND", async () => { const agent = { launchAgentChat: vi.fn(async () => ({ diff --git a/frontend/src/features/agents/CustomAgentChatView.tsx b/frontend/src/features/agents/CustomAgentChatView.tsx index 82dd1fc..9f262da 100644 --- a/frontend/src/features/agents/CustomAgentChatView.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.tsx @@ -131,6 +131,7 @@ export function CustomAgentChatView({ const { agent, system } = useGateways(); const [turns, setTurns] = useState([]); const [currentSession, setCurrentSession] = useState(sessionId); + const [externalSessionId, setExternalSessionId] = useState(sessionId); const [draft, setDraft] = useState(""); const [attachment, setAttachment] = useState(null); const [opening, setOpening] = useState(false); @@ -141,6 +142,7 @@ export function CustomAgentChatView({ sessionRef.current = currentSession; const mountedRef = useRef(false); const openOrAttachCountRef = useRef(0); + const selfEmittedSessionIdRef = useRef(undefined); const onSessionIdRef = useRef(onSessionId); onSessionIdRef.current = onSessionId; const onConversationIdRef = useRef(onConversationId); @@ -167,6 +169,11 @@ export function CustomAgentChatView({ if (chunk.kind === "final" || chunk.kind === "error") setBusy(false); }, []); + const publishSessionId = useCallback((nextSessionId: string | null) => { + selfEmittedSessionIdRef.current = nextSessionId; + onSessionIdRef.current(nextSessionId); + }, []); + const reattachStructuredSession = useCallback( async (sid: string, options: { applyScrollback?: boolean } = {}) => { if (!agent.reattachAgentChat) throw new Error("Structured reattach unavailable"); @@ -249,7 +256,7 @@ export function CustomAgentChatView({ }); if (!mountedRef.current) return launched.sessionId; setCurrentSession(launched.sessionId); - onSessionIdRef.current(launched.sessionId); + publishSessionId(launched.sessionId); if (launched.assignedConversationId) { onConversationIdRef.current(launched.assignedConversationId); } @@ -268,7 +275,7 @@ export function CustomAgentChatView({ retryAttachNotFound = false; if (mountedRef.current) { setCurrentSession(null); - onSessionIdRef.current(null); + publishSessionId(null); } continue; } @@ -289,6 +296,7 @@ export function CustomAgentChatView({ nodeId, conversationId, reattachStructuredSession, + publishSessionId, ], ); @@ -297,6 +305,18 @@ export function CustomAgentChatView({ if (el) el.scrollTop = el.scrollHeight; }, [turns]); + useEffect(() => { + if (selfEmittedSessionIdRef.current === sessionId) { + console.debug("[ticket149] openOrAttach:self-session-echo:skip", { + timestamp: new Date().toISOString(), + sessionId, + }); + selfEmittedSessionIdRef.current = undefined; + return; + } + setExternalSessionId(sessionId); + }, [sessionId]); + useEffect(() => { if (!supported) return; let cancelled = false; @@ -307,19 +327,19 @@ export function CustomAgentChatView({ console.debug("[ticket149] openOrAttach:start", { timestamp: new Date().toISOString(), execution, - sessionId, + sessionId: externalSessionId, }); setOpening(true); setError(null); try { - if (sessionId) { + if (externalSessionId) { try { - await reattachStructuredSession(sessionId, { applyScrollback: true }); + await reattachStructuredSession(externalSessionId, { applyScrollback: true }); if (cancelled) return; console.debug("[ticket149] openOrAttach:reattach-existing:success", { timestamp: new Date().toISOString(), execution, - sessionId, + sessionId: externalSessionId, }); return; } catch (e) { @@ -329,12 +349,12 @@ export function CustomAgentChatView({ if (!isNotFound(e)) throw e; if (!cancelled) { setCurrentSession(null); - onSessionIdRef.current(null); + publishSessionId(null); } console.debug("[ticket149] openOrAttach:reattach-existing:not-found", { timestamp: new Date().toISOString(), execution, - sessionId, + sessionId: externalSessionId, error: instrumentationError(e), }); } @@ -347,13 +367,13 @@ export function CustomAgentChatView({ console.debug("[ticket149] openOrAttach:recover:success", { timestamp: new Date().toISOString(), execution, - receivedSessionId: sessionId, + receivedSessionId: externalSessionId, }); } catch (e) { console.error("[ticket149] openOrAttach:error", { timestamp: new Date().toISOString(), execution, - sessionId, + sessionId: externalSessionId, error: instrumentationError(e), }); if (!cancelled) setError(describe(e)); @@ -368,9 +388,10 @@ export function CustomAgentChatView({ }; }, [ supported, - sessionId, + externalSessionId, reattachStructuredSession, recoverStructuredSession, + publishSessionId, ]); const canSend = useMemo( @@ -408,7 +429,7 @@ export function CustomAgentChatView({ if (isNotFound(e)) { try { setCurrentSession(null); - onSessionIdRef.current(null); + publishSessionId(null); const recoveredSession = await recoverStructuredSession({ applyScrollback: false, retryAttachNotFound: true, @@ -440,7 +461,7 @@ export function CustomAgentChatView({ } catch (e) { if (isNotFound(e)) { setCurrentSession(null); - onSessionIdRef.current(null); + publishSessionId(null); setTurns((prev) => [...prev, { role: "tool", label: "Session déjà fermée." }]); return; }