diff --git a/site/src/index.css b/site/src/index.css index fd2607b30c3..ef882c1a9ea 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -83,13 +83,21 @@ calc(100vh - var(--mobile-dropdown-above-composer-bottom, 9rem) - 1rem) ) !important; overflow: hidden !important; + display: flex !important; + flex-direction: column !important; + } + /* min-height: 0 on both flex children lets the list shrink below its + content and scroll inside the capped column instead of overflowing. */ + .mobile-full-width-dropdown-above-composer > [cmdk-root], + .mobile-full-width-dropdown-above-composer > [cmdk-root] > [cmdk-list] { + flex: 1 1 0% !important; + min-height: 0 !important; + } + .mobile-full-width-dropdown-above-composer > [cmdk-root] > [cmdk-list] { + max-height: none !important; } .mobile-full-width-dropdown-above-composer .mobile-full-width-dropdown-scroll-area { - max-height: calc( - var(--mobile-dropdown-above-composer-max-height, 18rem) - - 0.5rem - ) !important; overflow-y: auto !important; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; diff --git a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx index 99933bb0d39..1a59413fb42 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { expect, fn, screen, userEvent, waitFor, within } from "storybook/test"; import { ModelSelector, type ModelSelectorOption } from "./ModelSelector"; import { MockModelSelectorOption } from "./modelSelectorFixtures"; @@ -450,3 +450,54 @@ export const EffortRowClampedToMax: Story = { }); }, }; + +// The pinned effort row must stay inside the mobile dropdown's capped +// height above the composer while the model list scrolls. +export const MobileEffortRow: Story = { + args: { + options: [ + ...Array.from({ length: 30 }, (_, index) => ({ + ...MockModelSelectorOption, + id: `openai/model-${index}`, + model: `model-${index}`, + displayName: `Model ${index}`, + })), + effortModel, + ], + value: "openai/gpt-5", + reasoningEffort: "medium", + onReasoningEffortChange: fn(), + enableMobileFullWidthDropdown: true, + }, + parameters: { + // The interaction runner defaults to a desktop width where the + // mobile dropdown CSS never applies, so pin a mobile viewport. + viewport: { defaultViewport: "mobile1" }, + // Capture the visual snapshot at a mobile width so the pinned + // effort row and scrollable list render in the CI visual gate. + lostpixel: { breakpoints: [320] }, + }, + decorators: [ + (Story) => { + useEffect(() => { + // Tight enough that the model list plus the pinned effort row + // overflow the dropdown, forcing the layout under test. + const root = document.documentElement.style; + root.setProperty( + "--mobile-dropdown-above-composer-max-height", + "260px", + ); + return () => { + root.removeProperty("--mobile-dropdown-above-composer-max-height"); + }; + }, []); + return ; + }, + ], + play: async ({ canvasElement }) => { + // Open the picker so the snapshot captures the dropdown, the pinned + // effort row, and the scrollable list. + await userEvent.click(within(canvasElement).getByRole("combobox")); + await within(document.body).findByRole("listbox"); + }, +};