diff --git a/frontend/src/features/agents/CustomAgentChatView.test.tsx b/frontend/src/features/agents/CustomAgentChatView.test.tsx
index f9d45687..18e15003 100644
--- a/frontend/src/features/agents/CustomAgentChatView.test.tsx
+++ b/frontend/src/features/agents/CustomAgentChatView.test.tsx
@@ -522,6 +522,85 @@ describe("CustomAgentChatView", () => {
expect(screen.getByLabelText(/message CLI custom/).hasAttribute("disabled")).toBe(true);
});
+ it("restores visible conversation from conversationId cache when layout returns before sessionId", async () => {
+ let scrollback: ReplyChunk[] = [
+ { kind: "userPrompt" as const, text: "Initial prompt" },
+ { kind: "final" as const, content: "Initial reply" },
+ ];
+ const secondReattach = deferred<{ sessionId: string; scrollback: ReplyChunk[] }>();
+ const agent = {
+ launchAgentChat: vi.fn(),
+ reattachAgentChat: vi
+ .fn()
+ .mockImplementationOnce(async (sessionId: string) => ({
+ sessionId,
+ scrollback,
+ }))
+ .mockImplementationOnce(() => secondReattach.promise),
+ detachAgentChat: vi.fn(async () => {}),
+ sendAgentChat: vi.fn(async () => {}),
+ cancelAgentChat: vi.fn(async () => {}),
+ closeAgentChat: vi.fn(async () => {}),
+ };
+ const props = {
+ projectId: "project-1",
+ agentId: "agent-1",
+ agentName: "Worker",
+ profile,
+ cwd: "/repo",
+ nodeId: "node-1",
+ sessionId: "chat-session-1" as string | null,
+ conversationId: "conversation-1",
+ onSessionId: vi.fn(),
+ onConversationId: vi.fn(),
+ };
+
+ const first = render(
+ null) },
+ } as unknown as Gateways}
+ >
+
+ ,
+ );
+
+ expect(await screen.findByText("Initial reply")).toBeTruthy();
+ first.unmount();
+ await waitFor(() => expect(agent.detachAgentChat).toHaveBeenCalledWith("chat-session-1"));
+
+ scrollback = [
+ { kind: "userPrompt" as const, text: "Initial prompt" },
+ { kind: "final" as const, content: "Initial reply" },
+ { kind: "userPrompt" as const, text: "Question during graph view" },
+ { kind: "final" as const, content: "Answer restored from replay" },
+ ];
+
+ render(
+ null) },
+ } as unknown as Gateways}
+ >
+
+ ,
+ );
+
+ expect(screen.getByText("Initial reply")).toBeTruthy();
+ expect(agent.launchAgentChat).not.toHaveBeenCalled();
+ expect(screen.getByRole("button", { name: "Envoyer" }).hasAttribute("disabled")).toBe(true);
+
+ act(() => {
+ secondReattach.resolve({ sessionId: "chat-session-1", scrollback });
+ });
+
+ expect(await screen.findByText("Answer restored from replay")).toBeTruthy();
+ expect(screen.getByText("Question during graph view")).toBeTruthy();
+ expect(agent.launchAgentChat).not.toHaveBeenCalled();
+ });
+
it("drops a stale no-response chunk when replay contains a later real answer (#190)", async () => {
const agent = {
launchAgentChat: vi.fn(),
diff --git a/frontend/src/features/agents/CustomAgentChatView.tsx b/frontend/src/features/agents/CustomAgentChatView.tsx
index 113f9ada..6439ac46 100644
--- a/frontend/src/features/agents/CustomAgentChatView.tsx
+++ b/frontend/src/features/agents/CustomAgentChatView.tsx
@@ -67,15 +67,18 @@ interface CachedChatView {
turns: ChatTurn[];
progressIndex: Map;
chunkCount: number;
+ sessionId?: string;
}
const chatSessionViewCache = new Map();
+const chatConversationViewCache = new Map();
const EMPTY_MODEL_RESPONSE_MESSAGE = "Le modèle n'a renvoyé aucune réponse.";
const COMPOSER_RESIZE_STEP = 12;
const FALLBACK_COMPOSER_HEIGHT = 64;
export function __clearCustomAgentChatViewCacheForTests() {
chatSessionViewCache.clear();
+ chatConversationViewCache.clear();
}
interface AttachmentDraft {
@@ -310,6 +313,14 @@ function emptyChatViewState(): ChatViewState {
return { turns: [], progressIndex: new Map() };
}
+function conversationCacheKey(
+ projectId: string,
+ agentId: string,
+ conversationId: string | null,
+): string | null {
+ return conversationId ? `${projectId}::${agentId}::${conversationId}` : null;
+}
+
function appendProgress(state: ChatViewState, progress: ReplyProgress): ChatViewState {
const key = progressUpsertKey(progress);
const existingIndex = state.turns.findIndex(
@@ -711,10 +722,27 @@ export function CustomAgentChatView({
}: CustomAgentChatViewProps) {
const { agent, profile: profileGateway, system } = useGateways();
const pluginRuntime = usePluginRuntime();
- const [turns, setTurns] = useState(
- () => (sessionId ? (chatSessionViewCache.get(sessionId)?.turns ?? []) : []),
+ const initialConversationCacheKey = conversationCacheKey(
+ projectId,
+ agentId,
+ conversationId,
+ );
+ const [turns, setTurns] = useState(
+ () =>
+ sessionId
+ ? (chatSessionViewCache.get(sessionId)?.turns ?? [])
+ : initialConversationCacheKey
+ ? (chatConversationViewCache.get(initialConversationCacheKey)?.turns ?? [])
+ : [],
+ );
+ const [progressIndex, setProgressIndex] = useState