feat(cli): aligne les Final à droite, tools MCP « qui parle à qui », icône animée seulement pendant l'exécution (started→done), supprime le message de fin de tool redondant — #176 (QA verte)
This commit is contained in:
@ -242,6 +242,9 @@ describe("CustomAgentChatView", () => {
|
|||||||
|
|
||||||
await screen.findByText("done");
|
await screen.findByText("done");
|
||||||
expect(screen.getAllByText("hello agent")).toHaveLength(1);
|
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 () => {
|
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")).toBeTruthy();
|
||||||
expect(screen.getByText("MCP").closest("[data-testid='chat-tool-progress']")).toBeTruthy();
|
expect(screen.getByText("MCP").closest("[data-testid='chat-tool-progress']")).toBeTruthy();
|
||||||
expect(screen.getByText("idea_ticket_read")).toBeTruthy();
|
expect(screen.getByText("idea_ticket_read")).toBeTruthy();
|
||||||
expect(screen.getByText("Demande à QA")).toBeTruthy();
|
expect(screen.getByText("Worker ← QA")).toBeTruthy();
|
||||||
expect(screen.getByText("Demande à QA").closest("[data-testid='chat-tool-progress']")).toBeTruthy();
|
expect(screen.getByText("Worker ← QA").closest("[data-testid='chat-tool-progress']")).toBeTruthy();
|
||||||
expect(screen.getByText("Valider le rendu progress")).toBeTruthy();
|
expect(screen.getByText("Valider le rendu progress")).toBeTruthy();
|
||||||
expect((await screen.findAllByText("done")).length).toBeGreaterThan(0);
|
expect((await screen.findAllByText("done")).length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@ -547,6 +550,13 @@ describe("CustomAgentChatView", () => {
|
|||||||
act(() => {
|
act(() => {
|
||||||
emitChunk?.(progress("providerNative", "started"));
|
emitChunk?.(progress("providerNative", "started"));
|
||||||
emitChunk?.(progress("ideaLocal", "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("providerNative", "completed"));
|
||||||
emitChunk?.(progress("ideaLocal", "completed"));
|
emitChunk?.(progress("ideaLocal", "completed"));
|
||||||
emitChunk?.(progress("providerNative", "started"));
|
emitChunk?.(progress("providerNative", "started"));
|
||||||
@ -557,11 +567,12 @@ describe("CustomAgentChatView", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await screen.findByText("done");
|
await screen.findByText("done");
|
||||||
expect(screen.getByText("Demande à QA")).toBeTruthy();
|
expect(screen.getAllByText("Worker ← QA")).toHaveLength(2);
|
||||||
expect(screen.getByText("validate")).toBeTruthy();
|
expect(screen.getAllByText("validate")).toHaveLength(2);
|
||||||
expect(screen.getByText("Appel inter-agent")).toBeTruthy();
|
expect(screen.queryByText("Appel inter-agent")).toBeNull();
|
||||||
expect(screen.getByText("reply delivered")).toBeTruthy();
|
expect(screen.queryByText("reply delivered")).toBeNull();
|
||||||
expect(screen.queryByText("Tool")).toBeNull();
|
expect(screen.queryByText("Tool")).toBeNull();
|
||||||
|
expect(screen.queryByText("started")).toBeNull();
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.queryByRole("status", { name: "Loading" })).toBeNull(),
|
expect(screen.queryByRole("status", { name: "Loading" })).toBeNull(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -226,6 +226,32 @@ function progressDisplayKey(progress: ReplyProgress): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function appendProgress(turns: ChatTurn[], progress: ReplyProgress): ChatTurn[] {
|
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);
|
const key = progressDisplayKey(progress);
|
||||||
if (
|
if (
|
||||||
turns.some(
|
turns.some(
|
||||||
@ -237,6 +263,18 @@ function appendProgress(turns: ChatTurn[], progress: ReplyProgress): ChatTurn[]
|
|||||||
return [...turns, { role: "progress", progress }];
|
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[] {
|
function completeRunningProgress(turns: ChatTurn[]): ChatTurn[] {
|
||||||
const completedEntities = new Set(
|
const completedEntities = new Set(
|
||||||
turns
|
turns
|
||||||
@ -258,7 +296,14 @@ function completeRunningProgress(turns: ChatTurn[]): ChatTurn[] {
|
|||||||
}
|
}
|
||||||
const entity = progressEntityKey(turn.progress);
|
const entity = progressEntityKey(turn.progress);
|
||||||
if (entity && completedEntities.has(entity)) {
|
if (entity && completedEntities.has(entity)) {
|
||||||
return { ...turn, settled: true };
|
return {
|
||||||
|
...turn,
|
||||||
|
settled: true,
|
||||||
|
progress: {
|
||||||
|
...turn.progress,
|
||||||
|
stage: "completed",
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...turn,
|
...turn,
|
||||||
@ -1146,7 +1191,9 @@ export function CustomAgentChatView({
|
|||||||
Envoyez un message pour démarrer la conversation structurée.
|
Envoyez un message pour démarrer la conversation structurée.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
turns.map((turn, index) => <ChatBubble key={index} turn={turn} />)
|
turns.map((turn, index) => (
|
||||||
|
<ChatBubble key={index} turn={turn} requesterName={agentName} />
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1298,7 +1345,13 @@ export function CustomAgentChatView({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatBubble({ turn }: { turn: ChatTurn }) {
|
function ChatBubble({
|
||||||
|
turn,
|
||||||
|
requesterName,
|
||||||
|
}: {
|
||||||
|
turn: ChatTurn;
|
||||||
|
requesterName: string;
|
||||||
|
}) {
|
||||||
if (turn.role === "tool") {
|
if (turn.role === "tool") {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 justify-center">
|
<div className="flex min-w-0 justify-center">
|
||||||
@ -1309,11 +1362,17 @@ function ChatBubble({ turn }: { turn: ChatTurn }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (turn.role === "progress") {
|
if (turn.role === "progress") {
|
||||||
return <ProgressBubble progress={turn.progress} settled={turn.settled} />;
|
return (
|
||||||
|
<ProgressBubble
|
||||||
|
progress={turn.progress}
|
||||||
|
requesterName={requesterName}
|
||||||
|
settled={turn.settled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (turn.role === "final") {
|
if (turn.role === "final") {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 justify-start">
|
<div className="flex min-w-0 justify-end">
|
||||||
<div className="min-w-0 max-w-[82%] overflow-hidden rounded-md border border-success/50 bg-success/10 px-3 py-2 text-sm text-content">
|
<div className="min-w-0 max-w-[82%] overflow-hidden rounded-md border border-success/50 bg-success/10 px-3 py-2 text-sm text-content">
|
||||||
<div className="mb-1 text-xs font-semibold text-success">Terminé</div>
|
<div className="mb-1 text-xs font-semibold text-success">Terminé</div>
|
||||||
<p className="whitespace-pre-wrap break-words">{turn.text}</p>
|
<p className="whitespace-pre-wrap break-words">{turn.text}</p>
|
||||||
@ -1432,14 +1491,17 @@ function parseKeyValue(text: string, key: string): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function delegationSummary(progress: ReplyProgress): {
|
function delegationSummary(progress: ReplyProgress): {
|
||||||
|
requester: string | null;
|
||||||
target: string | null;
|
target: string | null;
|
||||||
excerpt: string | null;
|
excerpt: string | null;
|
||||||
} {
|
} {
|
||||||
const text = progress.text ?? "";
|
const text = progress.text ?? "";
|
||||||
|
const requester = parseKeyValue(text, "requester");
|
||||||
const target = parseKeyValue(text, "target");
|
const target = parseKeyValue(text, "target");
|
||||||
const task = parseKeyValue(text, "task");
|
const task = parseKeyValue(text, "task");
|
||||||
const fallback = text && !target && !task ? text : null;
|
const fallback = text && !target && !task ? text : null;
|
||||||
return {
|
return {
|
||||||
|
requester,
|
||||||
target,
|
target,
|
||||||
excerpt: task
|
excerpt: task
|
||||||
? truncateText(task, 92)
|
? truncateText(task, 92)
|
||||||
@ -1474,9 +1536,11 @@ function TurnMarker({ progress }: { progress: ReplyProgress }) {
|
|||||||
|
|
||||||
function ToolProgressCard({
|
function ToolProgressCard({
|
||||||
progress,
|
progress,
|
||||||
|
requesterName,
|
||||||
settled,
|
settled,
|
||||||
}: {
|
}: {
|
||||||
progress: ReplyProgress;
|
progress: ReplyProgress;
|
||||||
|
requesterName: string;
|
||||||
settled: boolean;
|
settled: boolean;
|
||||||
}) {
|
}) {
|
||||||
const running = !settled && (progress.stage === "started" || progress.stage === "delta");
|
const running = !settled && (progress.stage === "started" || progress.stage === "delta");
|
||||||
@ -1486,7 +1550,9 @@ function ToolProgressCard({
|
|||||||
const delegation = isDelegationProgress(progress) ? delegationSummary(progress) : null;
|
const delegation = isDelegationProgress(progress) ? delegationSummary(progress) : null;
|
||||||
const title = delegation
|
const title = delegation
|
||||||
? delegation.target
|
? delegation.target
|
||||||
? `Demande à ${delegation.target}`
|
? `${delegation.requester ?? requesterName} ${
|
||||||
|
progress.stage === "completed" ? "←" : "→"
|
||||||
|
} ${delegation.target}`
|
||||||
: "Appel inter-agent"
|
: "Appel inter-agent"
|
||||||
: progressTitle(progress);
|
: progressTitle(progress);
|
||||||
return (
|
return (
|
||||||
@ -1527,14 +1593,22 @@ function ToolProgressCard({
|
|||||||
|
|
||||||
function ProgressBubble({
|
function ProgressBubble({
|
||||||
progress,
|
progress,
|
||||||
|
requesterName,
|
||||||
settled = false,
|
settled = false,
|
||||||
}: {
|
}: {
|
||||||
progress: ReplyProgress;
|
progress: ReplyProgress;
|
||||||
|
requesterName: string;
|
||||||
settled?: boolean;
|
settled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (isTimelineProgress(progress)) return <TurnMarker progress={progress} />;
|
if (isTimelineProgress(progress)) return <TurnMarker progress={progress} />;
|
||||||
if (isCenteredToolProgress(progress)) {
|
if (isCenteredToolProgress(progress)) {
|
||||||
return <ToolProgressCard progress={progress} settled={settled} />;
|
return (
|
||||||
|
<ToolProgressCard
|
||||||
|
progress={progress}
|
||||||
|
requesterName={requesterName}
|
||||||
|
settled={settled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const title = progressTitle(progress);
|
const title = progressTitle(progress);
|
||||||
const stage = progressStageLabel(progress.stage);
|
const stage = progressStageLabel(progress.stage);
|
||||||
|
|||||||
Reference in New Issue
Block a user