From 7b5a89ac0214f53bfcc16305d6c7e6b56d25fd13 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 20 Jul 2026 10:04:20 +0000 Subject: [PATCH 1/4] fix(site): keep mobile model picker effort row and last model visible --- site/src/index.css | 16 ++++++++++++---- .../ChatElements/ModelSelector.stories.tsx | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/site/src/index.css b/site/src/index.css index fd2607b30c3c3..69e4ed9aa39b8 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 lets the list shrink below its content so it scrolls + inside the capped column instead of overflowing the clipped edge. */ + .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 99933bb0d395d..52401ff9fc01c 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx @@ -450,3 +450,19 @@ export const EffortRowClampedToMax: Story = { }); }, }; + +// Mobile dropdown with a reasoning-effort-capable model selected. The +// pinned effort row must stay visible inside the capped dropdown height +// above the composer. Visual regression coverage only. +export const MobileEffortRow: Story = { + args: { + options: [...openAIModels, effortModel], + value: "openai/gpt-5", + reasoningEffort: "medium", + onReasoningEffortChange: fn(), + enableMobileFullWidthDropdown: true, + }, + parameters: { + chromatic: { viewports: [320] }, + }, +}; From f9cb2787485246961c66bf98c7fb390273855aa3 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 20 Jul 2026 15:00:54 +0000 Subject: [PATCH 2/4] test(site): cover mobile model picker effort row against clipping --- site/src/index.css | 4 +- .../ChatElements/ModelSelector.stories.tsx | 51 ++++++++++++++++--- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/site/src/index.css b/site/src/index.css index 69e4ed9aa39b8..ef882c1a9ea48 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -86,8 +86,8 @@ display: flex !important; flex-direction: column !important; } - /* min-height: 0 lets the list shrink below its content so it scrolls - inside the capped column instead of overflowing the clipped edge. */ + /* 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; diff --git a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx index 52401ff9fc01c..62238489ed89c 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"; @@ -451,18 +451,57 @@ export const EffortRowClampedToMax: Story = { }, }; -// Mobile dropdown with a reasoning-effort-capable model selected. The -// pinned effort row must stay visible inside the capped dropdown height -// above the composer. Visual regression coverage only. +// 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: [...openAIModels, effortModel], + 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: { - chromatic: { viewports: [320] }, + chromatic: { viewports: [390] }, + }, + decorators: [ + (Story) => { + useEffect(() => { + 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 }) => { + await userEvent.click(within(canvasElement).getByRole("combobox")); + const body = within(document.body); + const listbox = await body.findByRole("listbox"); + const slider = await body.findByRole("slider"); + + await waitFor(() => { + // The list scrolls because the models overflow the cap. + expect(listbox.scrollHeight).toBeGreaterThan(listbox.clientHeight); + // The pinned effort row is not pushed past the clipped edge. + expect(slider.getBoundingClientRect().bottom).toBeLessThanOrEqual( + listbox + .closest("[data-radix-popper-content-wrapper]") + ?.getBoundingClientRect().bottom ?? 0, + ); + }); }, }; From f1945a3a06c5d15f4211acefffe5ae45ad9c1a9c Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 20 Jul 2026 17:41:46 +0000 Subject: [PATCH 3/4] test(site): pin mobile viewport for model picker story --- .../ChatElements/ModelSelector.stories.tsx | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx index 62238489ed89c..18003c7f78819 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx @@ -470,11 +470,16 @@ export const MobileEffortRow: Story = { 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" }, chromatic: { viewports: [390] }, }, 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", @@ -488,20 +493,9 @@ export const MobileEffortRow: Story = { }, ], 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")); - const body = within(document.body); - const listbox = await body.findByRole("listbox"); - const slider = await body.findByRole("slider"); - - await waitFor(() => { - // The list scrolls because the models overflow the cap. - expect(listbox.scrollHeight).toBeGreaterThan(listbox.clientHeight); - // The pinned effort row is not pushed past the clipped edge. - expect(slider.getBoundingClientRect().bottom).toBeLessThanOrEqual( - listbox - .closest("[data-radix-popper-content-wrapper]") - ?.getBoundingClientRect().bottom ?? 0, - ); - }); + await within(document.body).findByRole("listbox"); }, }; From aee03e9786acc8ae136e128b132418059a66ceb0 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 21 Jul 2026 10:34:16 +0000 Subject: [PATCH 4/4] test(site): capture mobile model picker at mobile width in visual gate --- .../components/ChatElements/ModelSelector.stories.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx index 18003c7f78819..1a59413fb42b6 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/ModelSelector.stories.tsx @@ -473,7 +473,9 @@ export const MobileEffortRow: Story = { // The interaction runner defaults to a desktop width where the // mobile dropdown CSS never applies, so pin a mobile viewport. viewport: { defaultViewport: "mobile1" }, - chromatic: { viewports: [390] }, + // 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) => {