merge(frontend): fix #365 — repositionne le bouton Conversations de CustomAgentChatView hors des contrôles de choix d'agents (QA verte)

This commit is contained in:
2026-09-17 08:29:24 +02:00
4 changed files with 129 additions and 4 deletions

View File

@ -1,4 +1,6 @@
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { useState } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
@ -25,6 +27,11 @@ import {
customAgentChatQueueCount,
} from "./CustomAgentChatView";
const themeCss = readFileSync(
resolve(process.cwd(), "src/shared/styles/theme.css"),
"utf8",
);
const profile: AgentProfile = {
id: "structured",
name: "Structured Codex",
@ -199,6 +206,63 @@ describe("CustomAgentChatView", () => {
__resetProjectCliInteractionActivityForTests();
});
it.each([
{ width: 600, mode: "single-row" },
{ width: 360, mode: "second-row" },
{ width: 240, mode: "full-width" },
])("keeps its conversation access clear of cell controls at $width px ($mode)", ({ width, mode }) => {
const agent = {
reattachAgentChat: vi.fn(async (sessionId: string) => ({ sessionId, scrollback: [] })),
sendAgentChat: vi.fn(),
cancelAgentChat: vi.fn(),
closeAgentChat: vi.fn(),
};
render(
<div data-testid="representative-cell" style={{ width, height: 640 }}>
<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-1"
conversationId="conversation-1"
onSessionId={vi.fn()}
onConversationId={vi.fn()}
/>
</DIProvider>
</div>,
);
const header = screen.getByTestId("custom-agent-chat-header");
const identity = screen.getByTestId("custom-agent-chat-identity");
const conversations = screen.getByTestId("custom-agent-chat-conversations");
const button = screen.getByRole("button", { name: "Conversations" });
expect(screen.getByTestId("representative-cell").style.width).toBe(`${width}px`);
expect(header.className).toContain("custom-agent-chat-header");
expect(header.contains(identity)).toBe(true);
expect(header.contains(conversations)).toBe(true);
expect(conversations.contains(button)).toBe(true);
expect(screen.getByText("CLI personnalisée · Structured Codex")).toBeTruthy();
expect(themeCss).toContain("padding: 2.25rem 0.75rem 0.5rem");
if (mode === "single-row") {
expect(themeCss).toContain(".custom-agent-chat-conversations-menu {\n right: 0;");
} else {
expect(themeCss).toContain("@container (max-width: 419.99px)");
expect(themeCss).toContain("flex-direction: column");
expect(themeCss).toContain("left: 0");
}
if (mode === "full-width") {
expect(themeCss).toContain("@container (max-width: 279.99px)");
expect(themeCss).toContain(".custom-agent-chat-conversations-button,\n .custom-agent-chat-conversations-menu");
}
});
it("renders the project icon as a non-interactive custom-CLI watermark and updates it immediately", async () => {
const agent = {
reattachAgentChat: vi.fn(async (sessionId: string) => ({ sessionId, scrollback: [] })),

View File

@ -2966,18 +2966,26 @@ export function CustomAgentChatView({
<div
role="toolbar"
aria-label={t("custom.actions")}
className="relative z-20 flex shrink-0 items-center gap-2 border-b border-border px-3 py-2"
data-testid="custom-agent-chat-header"
className="custom-agent-chat-header relative z-20 shrink-0 border-b border-border"
>
<div className="min-w-0 flex-1 overflow-hidden">
<div
data-testid="custom-agent-chat-identity"
className="custom-agent-chat-identity min-w-0 overflow-hidden"
>
<div className="truncate text-sm font-medium">{agentName}</div>
<div className="truncate text-xs text-muted">
{tr("CLI personnalisée", "Custom CLI")} · {profile.name}
</div>
</div>
<div className="relative shrink-0">
<div
data-testid="custom-agent-chat-conversations"
className="custom-agent-chat-conversations relative shrink-0"
>
<Button
size="sm"
variant="ghost"
className="custom-agent-chat-conversations-button"
aria-expanded={conversationMenuOpen}
aria-haspopup="menu"
onClick={() => setConversationMenuOpen((open) => !open)}
@ -2988,7 +2996,8 @@ export function CustomAgentChatView({
<div
role="menu"
aria-label={tr("Conversations", "Conversations")}
className="absolute right-0 top-full z-50 mt-1 w-[min(22rem,calc(100vw-1rem))] overflow-hidden rounded-md border border-border bg-raised shadow-xl"
data-testid="custom-agent-chat-conversations-menu"
className="custom-agent-chat-conversations-menu absolute top-full z-50 mt-1 overflow-hidden rounded-md border border-border bg-raised shadow-xl"
>
{effectiveBusy && (
<p className="border-b border-border bg-warning/10 px-3 py-2 text-xs text-warning">

View File

@ -415,9 +415,12 @@ describe("LayoutGrid — ticket #48 cell control layering", () => {
const customToolbar = screen.getByRole("toolbar", {
name: "custom agent chat actions",
});
const conversations = screen.getByRole("button", { name: "Conversations" });
const mutate = vi.spyOn(setup.layout, "mutateLayout");
expect(customToolbar.className).toContain("z-20");
expect(customToolbar.className).toContain("custom-agent-chat-header");
expect(customToolbar.contains(conversations)).toBe(true);
expect(screen.getByLabelText(`Sélecteur d’agent ${setup.bId}`)).toBeTruthy();
expect(screen.getByRole("group", {
name: `Mode CLI de l’agent ${setup.bId}`,

View File

@ -107,6 +107,55 @@
container-type: size;
}
/* The LayoutGrid cell controls occupy the top strip as an absolute sibling.
Keep the custom-chat header below that strip, inside the chat's local
stacking context, so its conversation affordance can never sit underneath
the agent selector/split/close controls. */
.custom-agent-chat-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 2.25rem 0.75rem 0.5rem;
}
.custom-agent-chat-identity {
flex: 1 1 auto;
}
.custom-agent-chat-conversations-menu {
right: 0;
width: 22rem;
max-width: calc(100cqw - 1.5rem);
}
/* Medium split cells put the action on its own left-aligned row while
keeping the menu constrained to the cell rather than the viewport. */
@container (max-width: 419.99px) {
.custom-agent-chat-header {
flex-direction: column;
align-items: flex-start;
}
.custom-agent-chat-identity {
width: 100%;
}
.custom-agent-chat-conversations-menu {
right: auto;
left: 0;
}
}
/* Very narrow cells retain a usable, non-overlapping full-width target. */
@container (max-width: 279.99px) {
.custom-agent-chat-conversations,
.custom-agent-chat-conversations-button,
.custom-agent-chat-conversations-menu {
width: 100%;
max-width: 100%;
}
}
.project-cell-illustration-area {
container-type: size;
}