diff --git a/frontend/src/features/agents/CustomAgentChatView.test.tsx b/frontend/src/features/agents/CustomAgentChatView.test.tsx index 6c9e46a3..8b9ba2ee 100644 --- a/frontend/src/features/agents/CustomAgentChatView.test.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.test.tsx @@ -242,6 +242,9 @@ describe("CustomAgentChatView", () => { await screen.findByText("done"); expect(screen.getAllByText("hello agent")).toHaveLength(1); + expect(screen.getByText("Terminé").parentElement?.parentElement?.className).toContain( + "justify-end", + ); }); it("renders canonical progress chunks as live agent, MCP, and delegation activity", async () => { @@ -357,8 +360,8 @@ describe("CustomAgentChatView", () => { expect(screen.getByText("MCP")).toBeTruthy(); expect(screen.getByText("MCP").closest("[data-testid='chat-tool-progress']")).toBeTruthy(); expect(screen.getByText("idea_ticket_read")).toBeTruthy(); - expect(screen.getByText("Demande à QA")).toBeTruthy(); - expect(screen.getByText("Demande à QA").closest("[data-testid='chat-tool-progress']")).toBeTruthy(); + expect(screen.getByText("Worker ← QA")).toBeTruthy(); + expect(screen.getByText("Worker ← QA").closest("[data-testid='chat-tool-progress']")).toBeTruthy(); expect(screen.getByText("Valider le rendu progress")).toBeTruthy(); expect((await screen.findAllByText("done")).length).toBeGreaterThan(0); }); @@ -547,6 +550,13 @@ describe("CustomAgentChatView", () => { act(() => { emitChunk?.(progress("providerNative", "started")); emitChunk?.(progress("ideaLocal", "started")); + }); + + expect(screen.getByText("Worker → QA")).toBeTruthy(); + expect(screen.getByText("Worker → QA").parentElement?.textContent).toContain("started"); + expect(screen.getAllByRole("status", { name: "Loading" }).length).toBeGreaterThan(0); + + act(() => { emitChunk?.(progress("providerNative", "completed")); emitChunk?.(progress("ideaLocal", "completed")); emitChunk?.(progress("providerNative", "started")); @@ -557,11 +567,12 @@ describe("CustomAgentChatView", () => { }); await screen.findByText("done"); - expect(screen.getByText("Demande à QA")).toBeTruthy(); - expect(screen.getByText("validate")).toBeTruthy(); - expect(screen.getByText("Appel inter-agent")).toBeTruthy(); - expect(screen.getByText("reply delivered")).toBeTruthy(); + expect(screen.getAllByText("Worker ← QA")).toHaveLength(2); + expect(screen.getAllByText("validate")).toHaveLength(2); + expect(screen.queryByText("Appel inter-agent")).toBeNull(); + expect(screen.queryByText("reply delivered")).toBeNull(); expect(screen.queryByText("Tool")).toBeNull(); + expect(screen.queryByText("started")).toBeNull(); await waitFor(() => expect(screen.queryByRole("status", { name: "Loading" })).toBeNull(), ); diff --git a/frontend/src/features/agents/CustomAgentChatView.tsx b/frontend/src/features/agents/CustomAgentChatView.tsx index 44765212..797abc13 100644 --- a/frontend/src/features/agents/CustomAgentChatView.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.tsx @@ -226,6 +226,32 @@ function progressDisplayKey(progress: ReplyProgress): string { } function appendProgress(turns: ChatTurn[], progress: ReplyProgress): ChatTurn[] { + const entity = progressEntityKey(progress); + if (entity && progress.stage === "completed") { + let runningIndex = -1; + for (let index = turns.length - 1; index >= 0; index -= 1) { + const turn = turns[index]; + if ( + turn.role === "progress" && + progressEntityKey(turn.progress) === entity && + (turn.progress.stage === "started" || turn.progress.stage === "delta") + ) { + runningIndex = index; + break; + } + } + if (runningIndex >= 0) { + const next = [...turns]; + const current = next[runningIndex]; + if (current.role === "progress") { + next[runningIndex] = { + role: "progress", + progress: mergeCompletedProgress(current.progress, progress), + }; + } + return next; + } + } const key = progressDisplayKey(progress); if ( turns.some( @@ -237,6 +263,18 @@ function appendProgress(turns: ChatTurn[], progress: ReplyProgress): ChatTurn[] return [...turns, { role: "progress", progress }]; } +function mergeCompletedProgress( + running: ReplyProgress, + completed: ReplyProgress, +): ReplyProgress { + if (!isDelegationProgress(running) || !completed.text) return completed; + if (delegationSummary(completed).target) return completed; + return { + ...completed, + text: running.text, + }; +} + function completeRunningProgress(turns: ChatTurn[]): ChatTurn[] { const completedEntities = new Set( turns @@ -258,7 +296,14 @@ function completeRunningProgress(turns: ChatTurn[]): ChatTurn[] { } const entity = progressEntityKey(turn.progress); if (entity && completedEntities.has(entity)) { - return { ...turn, settled: true }; + return { + ...turn, + settled: true, + progress: { + ...turn.progress, + stage: "completed", + }, + }; } return { ...turn, @@ -1146,7 +1191,9 @@ export function CustomAgentChatView({ Envoyez un message pour démarrer la conversation structurée.

) : ( - turns.map((turn, index) => ) + turns.map((turn, index) => ( + + )) )} @@ -1298,7 +1345,13 @@ export function CustomAgentChatView({ ); } -function ChatBubble({ turn }: { turn: ChatTurn }) { +function ChatBubble({ + turn, + requesterName, +}: { + turn: ChatTurn; + requesterName: string; +}) { if (turn.role === "tool") { return (
@@ -1309,11 +1362,17 @@ function ChatBubble({ turn }: { turn: ChatTurn }) { ); } if (turn.role === "progress") { - return ; + return ( + + ); } if (turn.role === "final") { return ( -
+
Terminé

{turn.text}

@@ -1432,14 +1491,17 @@ function parseKeyValue(text: string, key: string): string | null { } function delegationSummary(progress: ReplyProgress): { + requester: string | null; target: string | null; excerpt: string | null; } { const text = progress.text ?? ""; + const requester = parseKeyValue(text, "requester"); const target = parseKeyValue(text, "target"); const task = parseKeyValue(text, "task"); const fallback = text && !target && !task ? text : null; return { + requester, target, excerpt: task ? truncateText(task, 92) @@ -1474,9 +1536,11 @@ function TurnMarker({ progress }: { progress: ReplyProgress }) { function ToolProgressCard({ progress, + requesterName, settled, }: { progress: ReplyProgress; + requesterName: string; settled: boolean; }) { const running = !settled && (progress.stage === "started" || progress.stage === "delta"); @@ -1486,7 +1550,9 @@ function ToolProgressCard({ const delegation = isDelegationProgress(progress) ? delegationSummary(progress) : null; const title = delegation ? delegation.target - ? `Demande à ${delegation.target}` + ? `${delegation.requester ?? requesterName} ${ + progress.stage === "completed" ? "←" : "→" + } ${delegation.target}` : "Appel inter-agent" : progressTitle(progress); return ( @@ -1527,14 +1593,22 @@ function ToolProgressCard({ function ProgressBubble({ progress, + requesterName, settled = false, }: { progress: ReplyProgress; + requesterName: string; settled?: boolean; }) { if (isTimelineProgress(progress)) return ; if (isCenteredToolProgress(progress)) { - return ; + return ( + + ); } const title = progressTitle(progress); const stage = progressStageLabel(progress.stage);