From 57ced6801bc3a5a8cd1f5d9b17049a5d4b18a6ce Mon Sep 17 00:00:00 2001 From: Blomios Date: Thu, 6 Aug 2026 14:29:39 +0200 Subject: [PATCH] =?UTF-8?q?fix(cli):=20arr=C3=AAt=20du=20spinner=20Progres?= =?UTF-8?q?s=20=C3=A0=20l'=C3=A9tat=20terminal=20=E2=80=94=20#157=20(QA=20?= =?UTF-8?q?verte)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute completeRunningProgress() qui passe les stages started/delta à completed lors des événements final/error. La ligne Progress conserve son contenu statique utile mais n'affiche plus l'animation de chargement une fois le tour terminé. Co-Authored-By: Claude Opus 4.8 --- .../agents/CustomAgentChatView.test.tsx | 86 ++++++++++++++++++- .../features/agents/CustomAgentChatView.tsx | 22 ++++- 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/agents/CustomAgentChatView.test.tsx b/frontend/src/features/agents/CustomAgentChatView.test.tsx index da1e9a0..f820f17 100644 --- a/frontend/src/features/agents/CustomAgentChatView.test.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { useState } from "react"; import { describe, expect, it, vi } from "vitest"; @@ -296,6 +296,90 @@ describe("CustomAgentChatView", () => { await screen.findByText("done"); }); + it("stops animating running progress rows once the turn reaches a final chunk", async () => { + let emitChunk: ((chunk: unknown) => void) | null = null; + const agent = { + launchAgentChat: vi.fn(), + reattachAgentChat: vi.fn(async (sessionId: string) => ({ + sessionId, + scrollback: [], + })), + sendAgentChat: vi.fn( + (_sessionId: string, _prompt: string, onChunk: (chunk: unknown) => void) => + new Promise((resolve) => { + emitChunk = (chunk: unknown) => { + onChunk(chunk); + if ((chunk as { kind?: string }).kind === "final") resolve(); + }; + }), + ), + cancelAgentChat: vi.fn(async () => {}), + closeAgentChat: vi.fn(async () => {}), + }; + + render( + null) }, + } as unknown as Gateways} + > + + , + ); + + await waitFor(() => + expect(agent.reattachAgentChat).toHaveBeenCalledWith( + "chat-session-1", + expect.any(Function), + ), + ); + + fireEvent.change(screen.getByLabelText(/message CLI custom/), { + target: { value: "ship progress UI" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Envoyer" })); + await waitFor(() => expect(emitChunk).not.toBeNull()); + + act(() => { + emitChunk?.({ + kind: "progress", + progress: { + source: "providerNative", + kind: "message", + stage: "delta", + label: "Analyse", + text: "Lecture du contexte disponible.", + }, + }); + }); + + expect(screen.getAllByRole("status", { name: "Loading" }).length).toBeGreaterThan(0); + expect(screen.getByText("Progress").parentElement?.textContent).toContain("running"); + + act(() => { + emitChunk?.({ kind: "final", content: "done" }); + }); + + await screen.findByText("done"); + await waitFor(() => + expect(screen.queryByRole("status", { name: "Loading" })).toBeNull(), + ); + expect(screen.getByText("Lecture du contexte disponible.")).toBeTruthy(); + expect(screen.getByText("Progress").parentElement?.textContent).toContain("done"); + }); + it("pastes a clipboard image as a removable preview chip", async () => { const agent = { launchAgentChat: vi.fn(), diff --git a/frontend/src/features/agents/CustomAgentChatView.tsx b/frontend/src/features/agents/CustomAgentChatView.tsx index bd2d686..a6825d3 100644 --- a/frontend/src/features/agents/CustomAgentChatView.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.tsx @@ -191,6 +191,24 @@ function appendProgress(turns: ChatTurn[], progress: ReplyProgress): ChatTurn[] return [...turns, { role: "progress", progress }]; } +function completeRunningProgress(turns: ChatTurn[]): ChatTurn[] { + return turns.map((turn) => { + if ( + turn.role !== "progress" || + (turn.progress.stage !== "started" && turn.progress.stage !== "delta") + ) { + return turn; + } + return { + ...turn, + progress: { + ...turn.progress, + stage: "completed", + }, + }; + }); +} + function fileExtension(mime: string): string { if (mime === "image/png") return "png"; if (mime === "image/jpeg") return "jpg"; @@ -257,14 +275,14 @@ function foldChunk(turns: ChatTurn[], raw: unknown): ChatTurn[] { } case "final": { const content = String(raw.content ?? ""); - const next = [...turns]; + const next = completeRunningProgress(turns); const last = next[next.length - 1]; if (last?.role === "agent") next[next.length - 1] = { ...last, pending: false }; next.push({ role: "final", text: content }); return next; } case "error": { - const next = [...turns]; + const next = completeRunningProgress(turns); const last = next[next.length - 1]; if (last?.role === "agent") next[next.length - 1] = { ...last, pending: false }; next.push({ role: "error", text: String(raw.message ?? "Erreur agent") });