diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 2fd9883ad..73ca57f03 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -2987,6 +2987,29 @@ tr.insights-row-clickable:hover { color: var(--muted-foreground); } +.create-agent-panel { + min-width: 0; +} + +.create-agent-panel [data-slot="dialog-body"] { + min-width: 0; + overflow-x: hidden; + padding-inline: 0.5rem 0.75rem; + scrollbar-gutter: stable; +} + +.create-agent-panel [data-slot="dialog-body"] > *, +.create-agent-panel .create-agent-advanced-body > * { + min-width: 0; +} + +.create-agent-panel [data-slot="dialog-body"] input, +.create-agent-panel [data-slot="dialog-body"] select, +.create-agent-panel [data-slot="dialog-body"] textarea { + min-width: 0; + max-width: 100%; +} + /* The optional "what should this agent do?" field: visually quieter than the required Name field above it — a smaller label, no bold-required asterisk, since a name alone is the supported happy path. */ diff --git a/apps/web/src/pages/create-agent-panel.tsx b/apps/web/src/pages/create-agent-panel.tsx index b058eee92..2d3734974 100644 --- a/apps/web/src/pages/create-agent-panel.tsx +++ b/apps/web/src/pages/create-agent-panel.tsx @@ -398,7 +398,7 @@ export function CreateAgentPanel({ return ( - + New agent diff --git a/apps/web/test/create-agent-panel.test.tsx b/apps/web/test/create-agent-panel.test.tsx index fbee17094..5f0ad27a8 100644 --- a/apps/web/test/create-agent-panel.test.tsx +++ b/apps/web/test/create-agent-panel.test.tsx @@ -163,6 +163,15 @@ async function settle() { } describe("CreateAgentPanel happy path", () => { + test("scopes drawer overflow handling to the New Agent panel", async () => { + await mount(); + expect( + document + .querySelector('[data-slot="dialog-content"]') + ?.classList.contains("create-agent-panel"), + ).toBe(true); + }); + test("Advanced is collapsed by default", async () => { await mount(); const details = document.querySelector( diff --git a/scripts/e2e/browser/walkthrough.ts b/scripts/e2e/browser/walkthrough.ts index 105ad3842..98c1dae57 100644 --- a/scripts/e2e/browser/walkthrough.ts +++ b/scripts/e2e/browser/walkthrough.ts @@ -284,6 +284,158 @@ async function countMatching(page: Page, selector: string): Promise { ); } +async function createAgentDrawerLayout( + page: Page, + viewportName: string, +): Promise<{ + readonly viewportName: string; + readonly bodyClientWidth: number; + readonly bodyScrollWidth: number; + readonly panelClientWidth: number; + readonly panelScrollWidth: number; + readonly minimumLeftClearance: number; + readonly maximumRightOverflow: number; + readonly activeControl: string; +}> { + return page.evaluate((name: string) => { + const panel = document.querySelector( + '[data-slot="dialog-content"].create-agent-panel', + ); + if (panel === null) { + throw new Error("New Agent drawer was not rendered"); + } + const body = panel.querySelector('[data-slot="dialog-body"]'); + if (body === null) { + throw new Error("New Agent drawer body was not rendered"); + } + + const bodyRect = body.getBoundingClientRect(); + const bodyContentLeft = bodyRect.left + body.clientLeft; + const bodyContentRight = bodyContentLeft + body.clientWidth; + const controls = Array.from( + body.querySelectorAll("input, select, textarea, button"), + ); + if (controls.length === 0) { + throw new Error("New Agent drawer rendered no controls"); + } + + const minimumLeftClearance = Math.min( + ...controls.map( + (control) => control.getBoundingClientRect().left - bodyContentLeft, + ), + ); + const maximumRightOverflow = Math.max( + ...controls.map( + (control) => control.getBoundingClientRect().right - bodyContentRight, + ), + ); + + return { + viewportName: name, + bodyClientWidth: body.clientWidth, + bodyScrollWidth: body.scrollWidth, + panelClientWidth: panel.clientWidth, + panelScrollWidth: panel.scrollWidth, + minimumLeftClearance, + maximumRightOverflow, + activeControl: + document.activeElement instanceof HTMLElement + ? document.activeElement.id + : "", + }; + }, viewportName); +} + +async function openCreateAgentDrawer( + page: Page, + webBaseUrl: string, +): Promise { + await page.goto(`${webBaseUrl}/agents`, { waitUntil: "domcontentloaded" }); + await page.waitForSelector('button[aria-label="Create an agent"]', { + timeout: 15_000, + }); + await clickStable(page, 'button[aria-label="Create an agent"]'); + await page.waitForSelector( + '[data-slot="dialog-content"].create-agent-panel', + { timeout: 15_000 }, + ); + await page.waitForSelector("#create-agent-name"); + await page.type( + "#create-agent-name", + "Research Buddy with a deliberately long name", + ); + await page.type( + "#create-agent-purpose", + "A deliberately long description that wraps across multiple lines so the drawer has to contain the full form without clipping its focus ring or letting the scrollbar cover the field.", + ); + await page.focus("#create-agent-name"); + await clickStable(page, ".create-agent-advanced > summary"); + await page.waitForSelector("#create-agent-advanced-handle"); + await page + .waitForFunction( + () => { + const model = document.querySelector( + "#create-agent-advanced-model", + ); + return ( + (model !== null && model.options.length > 0) || + document.querySelector('[role="status"]') !== null + ); + }, + { timeout: 5_000 }, + ) + .catch(() => undefined); + const modelValues = await page.$$eval( + "#create-agent-advanced-model option", + (options) => + options + .map((option) => option.value) + .filter((value) => value.trim() !== ""), + ); + const modelSelectorAvailable = modelValues.length > 0; + if (modelSelectorAvailable) { + const modelValue = modelValues[0]; + if (modelValue === undefined) { + throw new Error("Model selector reported no usable options"); + } + await page.select("#create-agent-advanced-model", modelValue); + await page.focus("#create-agent-advanced-model"); + } else { + await page.focus("#create-agent-advanced-handle"); + } + await page.evaluate(() => { + const body = document.querySelector( + '[data-slot="dialog-body"]', + ); + if (body === null) throw new Error("New Agent drawer body was not found"); + body.scrollTop = body.scrollHeight; + }); + await page.waitForFunction( + () => + document.querySelector('[data-slot="dialog-body"]') + ?.scrollTop !== 0, + { timeout: 5_000 }, + ); + return modelSelectorAvailable; +} + +async function closeCreateAgentDrawer(page: Page): Promise { + await page.evaluate(() => { + const panel = document.querySelector( + '[data-slot="dialog-content"].create-agent-panel', + ); + const cancel = Array.from(panel?.querySelectorAll("button") ?? []).find( + (button) => button.textContent?.trim() === "Cancel", + ); + if (cancel === undefined) throw new Error("Cancel button was not found"); + cancel.click(); + }); + await page.waitForSelector( + '[data-slot="dialog-content"].create-agent-panel', + { hidden: true, timeout: 5_000 }, + ); +} + // --- the walkthrough ----------------------------------------------------- /** The picker card that mints a plain empty channel (`workbench-templates.ts`). */ @@ -643,6 +795,63 @@ async function run(): Promise { }, ); + await step( + () => page, + "04b-create-agent-drawer-layout", + async () => { + const previousViewport = page.viewport() ?? { + width: 1440, + height: 900, + }; + const cases = [ + { name: "desktop", width: 1280, height: 720 }, + { name: "short", width: 1280, height: 480 }, + ] as const; + const measurements: Awaited< + ReturnType + >[] = []; + let modelSelectorSeen = false; + + try { + for (const viewport of cases) { + await page.setViewport(viewport); + const modelSelectorAvailable = await openCreateAgentDrawer( + page, + webBaseUrl, + ); + modelSelectorSeen ||= modelSelectorAvailable; + const measurement = await createAgentDrawerLayout( + page, + viewport.name, + ); + measurements.push(measurement); + if ( + measurement.bodyScrollWidth !== measurement.bodyClientWidth || + measurement.panelScrollWidth !== measurement.panelClientWidth || + measurement.minimumLeftClearance < 4 || + measurement.maximumRightOverflow > 1 || + measurement.activeControl !== + (modelSelectorAvailable + ? "create-agent-advanced-model" + : "create-agent-advanced-handle") + ) { + throw new Error( + `New Agent drawer escaped its bounds at ${viewport.name}: ${JSON.stringify(measurement)}`, + ); + } + await closeCreateAgentDrawer(page); + } + } finally { + await page.setViewport(previousViewport); + } + + return { + status: "pass", + detail: `name, description, Advanced, ${modelSelectorSeen ? "model selector, " : "model catalog unavailable, "}focus, and scroll stayed contained: ${JSON.stringify(measurements)}`, + }; + }, + ); + await step( () => page, "04c-workbench-sidebar",