Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/create-agent-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ export function CreateAgentPanel({

return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent side="right">
<DialogContent side="right" className="create-agent-panel">
<DialogHeader>
<DialogTitle>New agent</DialogTitle>
<DialogDescription>
Expand Down
9 changes: 9 additions & 0 deletions apps/web/test/create-agent-panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
209 changes: 209 additions & 0 deletions scripts/e2e/browser/walkthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,158 @@ async function countMatching(page: Page, selector: string): Promise<number> {
);
}

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<HTMLElement>(
'[data-slot="dialog-content"].create-agent-panel',
);
if (panel === null) {
throw new Error("New Agent drawer was not rendered");
}
const body = panel.querySelector<HTMLElement>('[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<HTMLElement>("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<boolean> {
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<HTMLSelectElement>(
"#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<HTMLElement>(
'[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<HTMLElement>('[data-slot="dialog-body"]')
?.scrollTop !== 0,
{ timeout: 5_000 },
);
return modelSelectorAvailable;
}

async function closeCreateAgentDrawer(page: Page): Promise<void> {
await page.evaluate(() => {
const panel = document.querySelector<HTMLElement>(
'[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`). */
Expand Down Expand Up @@ -643,6 +795,63 @@ async function run(): Promise<void> {
},
);

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<typeof createAgentDrawerLayout>
>[] = [];
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",
Expand Down
Loading