From a9346476ee3a884ab81d3e0e1df198f83c110439 Mon Sep 17 00:00:00 2001 From: Blomios Date: Thu, 6 Aug 2026 14:29:31 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(layout):=20bouton=20Cancel=20en=20z-ord?= =?UTF-8?q?er=20propre=20pendant=20un=20tour=20agent=20=E2=80=94=20#151=20?= =?UTF-8?q?(QA=20verte)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extrait le bouton Cancel du banner inline et le rend en position absolue (top:24, right:20) avec zIndex CELL_Z.turnActions. Le banner de statut est décalé (right:76 en état busy) et rétrogradé en CELL_Z.banner, afin que Cancel reste au premier plan et cliquable sans recouvrement en état busy. Co-Authored-By: Claude Opus 4.8 --- .../LayoutGrid.cellControlsLayering.test.tsx | 13 +++-- frontend/src/features/layout/LayoutGrid.tsx | 52 +++++++++++-------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/frontend/src/features/layout/LayoutGrid.cellControlsLayering.test.tsx b/frontend/src/features/layout/LayoutGrid.cellControlsLayering.test.tsx index 8797d5e..a871167 100644 --- a/frontend/src/features/layout/LayoutGrid.cellControlsLayering.test.tsx +++ b/frontend/src/features/layout/LayoutGrid.cellControlsLayering.test.tsx @@ -232,12 +232,19 @@ describe("LayoutGrid — ticket #48 cell control layering", () => { const cancel = await screen.findByRole("button", { name: `cancel current turn ${setup.bId}`, }); - const status = cancel.parentElement as HTMLElement; + const status = screen + .getAllByRole("status") + .find((element) => element.textContent === "Busy") as HTMLElement; const { controls } = controlsFor(setup.bId); + expect(status).toBeTruthy(); + expect(cancel.style.position).toBe("absolute"); expect(status.style.top).toBe("24px"); - expect(status.style.zIndex).toBe("6"); - expect(Number(status.style.zIndex)).toBeGreaterThan(Number(controls.style.zIndex)); + expect(status.style.zIndex).toBe("3"); + expect(cancel.style.top).toBe("24px"); + expect(cancel.style.right).toBe("20px"); + expect(cancel.style.zIndex).toBe("6"); + expect(Number(cancel.style.zIndex)).toBeGreaterThan(Number(controls.style.zIndex)); expect(screen.queryByRole("button", { name: `open cell conversation ${setup.bId}`, })).toBeNull(); diff --git a/frontend/src/features/layout/LayoutGrid.tsx b/frontend/src/features/layout/LayoutGrid.tsx index c04cf0e..0c3bd30 100644 --- a/frontend/src/features/layout/LayoutGrid.tsx +++ b/frontend/src/features/layout/LayoutGrid.tsx @@ -1172,6 +1172,32 @@ function LeafView({ )} + {agentId && busyByWorkState && ( + + )} {agentId && (busyByWorkState || inboxDepth > 0 || completedBackgroundTasks.length > 0) && (
)} - {busyByWorkState && ( - - )}
)} {/* Option 1 (Terminal + MCP): every cell — plain or agent — renders the From 57ced6801bc3a5a8cd1f5d9b17049a5d4b18a6ce Mon Sep 17 00:00:00 2001 From: Blomios Date: Thu, 6 Aug 2026 14:29:39 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(cli):=20arr=C3=AAt=20du=20spinner=20Pro?= =?UTF-8?q?gress=20=C3=A0=20l'=C3=A9tat=20terminal=20=E2=80=94=20#157=20(Q?= =?UTF-8?q?A=20verte)?= 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") });