From ae3c55a13d60f4e82f796679341c91ac5ecacaac Mon Sep 17 00:00:00 2001 From: Blomios Date: Tue, 11 Aug 2026 10:28:04 +0200 Subject: [PATCH] =?UTF-8?q?feat(cli):=20repense=20la=20timeline=20de=20con?= =?UTF-8?q?versation=20(tools=20MCP=20centr=C3=A9s,=20progress=20=C3=A0=20?= =?UTF-8?q?droite,=20tours=20compacts)=20=E2=80=94=20#174=20(QA=20verte)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ToolProgressCard: tools/MCP/délégations centrés, extrait succinct de la demande inter-agent - ProgressBubble: progress non-outil alignés à droite, clamp 4 lignes (fourLineClamp) - TurnMarker: marqueurs turn.started/turn.completed compacts et centrés - Délégations explicites (« Demande à », « Appel inter-agent ») + libellés FR (Terminé) --- .../agents/CustomAgentChatView.test.tsx | 39 ++++- .../features/agents/CustomAgentChatView.tsx | 161 +++++++++++++++--- 2 files changed, 173 insertions(+), 27 deletions(-) diff --git a/frontend/src/features/agents/CustomAgentChatView.test.tsx b/frontend/src/features/agents/CustomAgentChatView.test.tsx index 113b1753..6c9e46a3 100644 --- a/frontend/src/features/agents/CustomAgentChatView.test.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.test.tsx @@ -252,6 +252,17 @@ describe("CustomAgentChatView", () => { scrollback: [], })), sendAgentChat: vi.fn(async (_sessionId: string, _prompt: string, onChunk) => { + onChunk({ + kind: "progress", + progress: { + source: "providerNative", + kind: "turn", + stage: "started", + label: "turn.started", + provider: "codex", + nativeEvent: "turn.started", + }, + }); onChunk({ kind: "progress", progress: { @@ -285,6 +296,17 @@ describe("CustomAgentChatView", () => { toolName: "idea_ask_agent", }, }); + onChunk({ + kind: "progress", + progress: { + source: "providerNative", + kind: "turn", + stage: "completed", + label: "turn.completed", + provider: "codex", + nativeEvent: "turn.completed", + }, + }); onChunk({ kind: "final", content: "done" }); }), cancelAgentChat: vi.fn(async () => {}), @@ -326,13 +348,19 @@ describe("CustomAgentChatView", () => { fireEvent.click(screen.getByRole("button", { name: "Envoyer" })); await screen.findByText("Progress"); + expect(screen.getAllByTestId("chat-turn-marker")).toHaveLength(2); + expect(screen.getByText("turn.started")).toBeTruthy(); + expect(screen.getByText("turn.completed")).toBeTruthy(); + expect(screen.getByText("Progress").closest("[data-testid='chat-progress-status']")).toBeTruthy(); expect(screen.getByText("Analyse")).toBeTruthy(); expect(screen.getByText("Lecture du contexte disponible.")).toBeTruthy(); 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("Délégation")).toBeTruthy(); - expect(screen.getByText("target=QA task=Valider le rendu progress")).toBeTruthy(); - await screen.findByText("done"); + expect(screen.getByText("Demande à QA")).toBeTruthy(); + expect(screen.getByText("Demande à QA").closest("[data-testid='chat-tool-progress']")).toBeTruthy(); + expect(screen.getByText("Valider le rendu progress")).toBeTruthy(); + expect((await screen.findAllByText("done")).length).toBeGreaterThan(0); }); it("renders available pre-MCP progress before the first MCP card", async () => { @@ -529,8 +557,9 @@ describe("CustomAgentChatView", () => { }); await screen.findByText("done"); - expect(screen.getAllByText("Délégation")).toHaveLength(2); - expect(screen.getByText("target=QA task=validate")).toBeTruthy(); + 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.queryByText("Tool")).toBeNull(); await waitFor(() => diff --git a/frontend/src/features/agents/CustomAgentChatView.tsx b/frontend/src/features/agents/CustomAgentChatView.tsx index 5b8cb14f..44765212 100644 --- a/frontend/src/features/agents/CustomAgentChatView.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.tsx @@ -204,10 +204,6 @@ function isDelegationProgress(progress: ReplyProgress): boolean { return name === "idea_ask_agent" || name === "idea_ask_agents"; } -function isToolLikeProgress(progress: ReplyProgress): boolean { - return progress.kind === "tool" || progress.kind === "mcp" || isDelegationProgress(progress); -} - function progressEntityKey(progress: ReplyProgress): string | null { const name = normalizedProgressToolName(progress); if (!name) return null; @@ -217,7 +213,7 @@ function progressEntityKey(progress: ReplyProgress): string | null { } function progressStageBucket(progress: ReplyProgress): string { - if (!isToolLikeProgress(progress)) return progress.stage; + if (!isCenteredToolProgress(progress)) return progress.stage; if (progress.stage === "started" || progress.stage === "delta") return "started"; if (progress.stage === "completed") return "completed"; return "info"; @@ -255,6 +251,7 @@ function completeRunningProgress(turns: ChatTurn[]): ChatTurn[] { return turns.map((turn) => { if ( turn.role !== "progress" || + turn.progress.kind === "turn" || (turn.progress.stage !== "started" && turn.progress.stage !== "delta") ) { return turn; @@ -1316,9 +1313,9 @@ function ChatBubble({ turn }: { turn: ChatTurn }) { } if (turn.role === "final") { return ( -
-
-
Task Complete
+
+
+
Terminé

{turn.text}

@@ -1337,7 +1334,7 @@ function ChatBubble({ turn }: { turn: ChatTurn }) { const user = turn.role === "user"; return ( -
+
140 ? `${text.slice(0, 137)}...` : text; +function isCenteredToolProgress(progress: ReplyProgress): boolean { + return ( + progress.kind === "tool" || + progress.kind === "mcp" || + isDelegationProgress(progress) + ); +} + +function isTimelineProgress(progress: ReplyProgress): boolean { + return progress.kind === "turn"; +} + +function compactToolName(progress: ReplyProgress): string { + return progress.toolName ?? progress.label; +} + +function truncateText(text: string, max = 140): string { + const flat = text.replace(/\s+/g, " ").trim(); + return flat.length > max ? `${flat.slice(0, max - 3)}...` : flat; +} + +function parseKeyValue(text: string, key: string): string | null { + const match = new RegExp( + `(?:^|\\s)${key}=([^\\n]+?)(?=\\s\\w+=|$)`, + "i", + ).exec(text); + return match?.[1]?.trim().replace(/^["']|["']$/g, "") || null; +} + +function delegationSummary(progress: ReplyProgress): { + target: string | null; + excerpt: string | null; +} { + const text = progress.text ?? ""; + const target = parseKeyValue(text, "target"); + const task = parseKeyValue(text, "task"); + const fallback = text && !target && !task ? text : null; + return { + target, + excerpt: task + ? truncateText(task, 92) + : fallback + ? truncateText(fallback, 92) + : null, + }; +} + +const fourLineClamp = { + display: "-webkit-box", + WebkitBoxOrient: "vertical", + WebkitLineClamp: 4, +} as const; + +function TurnMarker({ progress }: { progress: ReplyProgress }) { + const stage = progressStageLabel(progress.stage); + const label = progress.label.trim() || "Tour"; + return ( +
+
+ + + {stage} + + {label} + +
+
+ ); +} + +function ToolProgressCard({ + progress, + settled, +}: { + progress: ReplyProgress; + settled: boolean; +}) { + const running = !settled && (progress.stage === "started" || progress.stage === "delta"); + const stage = progressStageLabel(progress.stage); + const detail = + progress.text && !isDelegationProgress(progress) ? truncateText(progress.text) : null; + const delegation = isDelegationProgress(progress) ? delegationSummary(progress) : null; + const title = delegation + ? delegation.target + ? `Demande à ${delegation.target}` + : "Appel inter-agent" + : progressTitle(progress); + return ( +
+
+
+ {running && } + {title} + {!delegation && ( + {compactToolName(progress)} + )} + · {stage} +
+ {delegation?.excerpt && ( +

+ {delegation.excerpt} +

+ )} + {delegation && ( +

+ {compactToolName(progress)} +

+ )} + {detail && ( +

+ {detail} +

+ )} +
+
+ ); } function ProgressBubble({ @@ -1416,21 +1532,20 @@ function ProgressBubble({ progress: ReplyProgress; settled?: boolean; }) { + if (isTimelineProgress(progress)) return ; + if (isCenteredToolProgress(progress)) { + return ; + } const title = progressTitle(progress); const stage = progressStageLabel(progress.stage); const source = progressSourceLabel(progress); - const detail = progress.text ? truncateProgressText(progress.text) : null; + const detail = progress.text ? truncateText(progress.text, 260) : null; const running = !settled && (progress.stage === "started" || progress.stage === "delta"); return ( -
+
@@ -1440,15 +1555,17 @@ function ProgressBubble({ {progress.label} · {stage}
-
+
{source} - {progress.toolName && tool: {progress.toolName}} {progress.nativeEvent && ( event: {progress.nativeEvent} )}
{detail && ( -

+

{detail}

)}