fix(frontend): repositionne le bouton Conversations de CustomAgentChatView hors des contrôles de choix d'agents — header custom-agent-chat-header repoussé sous la bande des contrôles de cellule (padding haut 2.25rem) dans le contexte d'empilement local du chat, menu Conversations borné à la cellule (droite 22rem, max-width 100cqw), repli en colonne aligné à gauche sous 419.99px puis cible pleine largeur sous 279.99px via container queries, testids custom-agent-chat-header/identity/conversations/menu, tests de non-chevauchement à 600/360/240px et garde-fous dans LayoutGrid.cellControlsLayering — #365 (QA verte: npm test ciblé CustomAgentChatView + LayoutGrid.cellControlsLayering 2 fichiers 106 tests OK, Firefox headless layout check 600/360/240 PASS, npm run build OK, git diff --check OK; npm audit: 9 vulnérabilités préexistantes non liées)
This commit is contained in:
@ -1,4 +1,6 @@
|
|||||||
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
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 { useState } from "react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
@ -25,6 +27,11 @@ import {
|
|||||||
customAgentChatQueueCount,
|
customAgentChatQueueCount,
|
||||||
} from "./CustomAgentChatView";
|
} from "./CustomAgentChatView";
|
||||||
|
|
||||||
|
const themeCss = readFileSync(
|
||||||
|
resolve(process.cwd(), "src/shared/styles/theme.css"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
const profile: AgentProfile = {
|
const profile: AgentProfile = {
|
||||||
id: "structured",
|
id: "structured",
|
||||||
name: "Structured Codex",
|
name: "Structured Codex",
|
||||||
@ -199,6 +206,63 @@ describe("CustomAgentChatView", () => {
|
|||||||
__resetProjectCliInteractionActivityForTests();
|
__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 () => {
|
it("renders the project icon as a non-interactive custom-CLI watermark and updates it immediately", async () => {
|
||||||
const agent = {
|
const agent = {
|
||||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({ sessionId, scrollback: [] })),
|
reattachAgentChat: vi.fn(async (sessionId: string) => ({ sessionId, scrollback: [] })),
|
||||||
|
|||||||
@ -2966,18 +2966,26 @@ export function CustomAgentChatView({
|
|||||||
<div
|
<div
|
||||||
role="toolbar"
|
role="toolbar"
|
||||||
aria-label={t("custom.actions")}
|
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-sm font-medium">{agentName}</div>
|
||||||
<div className="truncate text-xs text-muted">
|
<div className="truncate text-xs text-muted">
|
||||||
{tr("CLI personnalisée", "Custom CLI")} · {profile.name}
|
{tr("CLI personnalisée", "Custom CLI")} · {profile.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative shrink-0">
|
<div
|
||||||
|
data-testid="custom-agent-chat-conversations"
|
||||||
|
className="custom-agent-chat-conversations relative shrink-0"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
className="custom-agent-chat-conversations-button"
|
||||||
aria-expanded={conversationMenuOpen}
|
aria-expanded={conversationMenuOpen}
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
onClick={() => setConversationMenuOpen((open) => !open)}
|
onClick={() => setConversationMenuOpen((open) => !open)}
|
||||||
@ -2988,7 +2996,8 @@ export function CustomAgentChatView({
|
|||||||
<div
|
<div
|
||||||
role="menu"
|
role="menu"
|
||||||
aria-label={tr("Conversations", "Conversations")}
|
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 && (
|
{effectiveBusy && (
|
||||||
<p className="border-b border-border bg-warning/10 px-3 py-2 text-xs text-warning">
|
<p className="border-b border-border bg-warning/10 px-3 py-2 text-xs text-warning">
|
||||||
|
|||||||
@ -415,9 +415,12 @@ describe("LayoutGrid — ticket #48 cell control layering", () => {
|
|||||||
const customToolbar = screen.getByRole("toolbar", {
|
const customToolbar = screen.getByRole("toolbar", {
|
||||||
name: "custom agent chat actions",
|
name: "custom agent chat actions",
|
||||||
});
|
});
|
||||||
|
const conversations = screen.getByRole("button", { name: "Conversations" });
|
||||||
const mutate = vi.spyOn(setup.layout, "mutateLayout");
|
const mutate = vi.spyOn(setup.layout, "mutateLayout");
|
||||||
|
|
||||||
expect(customToolbar.className).toContain("z-20");
|
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.getByLabelText(`Sélecteur d’agent ${setup.bId}`)).toBeTruthy();
|
||||||
expect(screen.getByRole("group", {
|
expect(screen.getByRole("group", {
|
||||||
name: `Mode CLI de l’agent ${setup.bId}`,
|
name: `Mode CLI de l’agent ${setup.bId}`,
|
||||||
|
|||||||
@ -107,6 +107,55 @@
|
|||||||
container-type: size;
|
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 {
|
.project-cell-illustration-area {
|
||||||
container-type: size;
|
container-type: size;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user