feat(web): messages progress et usages MCP dépliables/repliables, repliés par défaut — #233

This commit is contained in:
2026-08-21 16:09:25 +02:00
parent a1d25d7d9e
commit e20182fa5e
2 changed files with 298 additions and 17 deletions

View File

@ -82,6 +82,21 @@ function expectedChatTime(ms: number): string {
return `${hour}:${minute}`;
}
function setMeasuredTextOverflow(
element: Element,
{ scrollHeight, clientHeight }: { scrollHeight: number; clientHeight: number },
) {
Object.defineProperty(element, "scrollHeight", {
value: scrollHeight,
configurable: true,
});
Object.defineProperty(element, "clientHeight", {
value: clientHeight,
configurable: true,
});
fireEvent(window, new Event("resize"));
}
describe("CustomAgentChatView", () => {
beforeEach(() => {
vi.clearAllMocks();
@ -1493,6 +1508,127 @@ describe("CustomAgentChatView", () => {
expect(screen.getByText("Tool").parentElement?.textContent).toContain("En cours");
});
it("expands only truncated MCP progress details and preserves expansion across updates", 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<void>((resolve) => {
emitChunk = (chunk: unknown) => {
onChunk(chunk);
if ((chunk as { kind?: string }).kind === "final") resolve();
};
}),
),
cancelAgentChat: vi.fn(async () => {}),
closeAgentChat: vi.fn(async () => {}),
};
render(
<DIProvider
gateways={{
agent,
system: { pickFile: vi.fn(async () => null) },
} as unknown as Gateways}
>
<CustomAgentChatView
projectId="project-1"
agentId="agent-1"
agentName="Worker"
profile={profile}
cwd="/repo"
nodeId="node-1"
sessionId="chat-session-1"
conversationId="conversation-1"
onSessionId={vi.fn()}
onConversationId={vi.fn()}
/>
</DIProvider>,
);
await waitFor(() =>
expect(agent.reattachAgentChat).toHaveBeenCalledWith(
"chat-session-1",
expect.any(Function),
),
);
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
target: { value: "inspect mcp payloads" },
});
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
await waitFor(() => expect(emitChunk).not.toBeNull());
act(() => {
emitChunk?.({
kind: "progress" as const,
progress: {
progressId: "mcp-large",
source: "ideaLocal" as const,
kind: "mcp" as const,
stage: "started" as const,
label: "idea_ticket_read",
toolName: "idea_ticket_read",
text: "line 1\nline 2\nline 3\nline 4\nline 5",
},
});
emitChunk?.({
kind: "progress" as const,
progress: {
progressId: "mcp-small",
source: "ideaLocal" as const,
kind: "mcp" as const,
stage: "started" as const,
label: "idea_memory_read",
toolName: "idea_memory_read",
text: "short payload",
},
});
});
const details = await screen.findAllByTestId("chat-progress-detail");
expect(details).toHaveLength(2);
setMeasuredTextOverflow(details[0], { scrollHeight: 120, clientHeight: 48 });
setMeasuredTextOverflow(details[1], { scrollHeight: 24, clientHeight: 24 });
const expand = await screen.findByRole("button", { name: "Afficher plus" });
expect(expand.getAttribute("aria-expanded")).toBe("false");
expect(expand.getAttribute("aria-controls")).toBe(details[0].id);
expect(screen.queryAllByRole("button", { name: "Afficher plus" })).toHaveLength(1);
fireEvent.click(expand);
expect(
screen.getByRole("button", { name: "Afficher moins" }).getAttribute("aria-expanded"),
).toBe("true");
expect(screen.queryByRole("button", { name: "Afficher plus" })).toBeNull();
act(() => {
emitChunk?.({
kind: "progress" as const,
progress: {
progressId: "mcp-large",
source: "ideaLocal" as const,
kind: "mcp" as const,
stage: "delta" as const,
label: "idea_ticket_read",
toolName: "idea_ticket_read",
text: "updated line 1\nupdated line 2\nupdated line 3\nupdated line 4\nupdated line 5",
},
});
});
expect(screen.getByText(/updated line 5/)).toBeTruthy();
expect(
screen.getByRole("button", { name: "Afficher moins" }).getAttribute("aria-expanded"),
).toBe("true");
expect(screen.queryByRole("button", { name: "Afficher plus" })).toBeNull();
});
it("renders progress immediately when the chunk callback fires", async () => {
let emitChunk: ((chunk: unknown) => void) | null = null;
const agent = {