From 84fd3547a918e0b87c7ba02bd3bf8d45cc708544 Mon Sep 17 00:00:00 2001
From: Blomios
Date: Tue, 11 Aug 2026 11:24:44 +0200
Subject: [PATCH] =?UTF-8?q?feat(cli):=20aligne=20les=20Final=20=C3=A0=20dr?=
=?UTF-8?q?oite,=20tools=20MCP=20=C2=AB=20qui=20parle=20=C3=A0=20qui=20?=
=?UTF-8?q?=C2=BB,=20ic=C3=B4ne=20anim=C3=A9e=20seulement=20pendant=20l'ex?=
=?UTF-8?q?=C3=A9cution=20(started=E2=86=92done),=20supprime=20le=20messag?=
=?UTF-8?q?e=20de=20fin=20de=20tool=20redondant=20=E2=80=94=20#176=20(QA?=
=?UTF-8?q?=20verte)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../agents/CustomAgentChatView.test.tsx | 23 +++--
.../features/agents/CustomAgentChatView.tsx | 88 +++++++++++++++++--
2 files changed, 98 insertions(+), 13 deletions(-)
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);