From 0dad1d7a613e6dc6f4d8bdf630dd3e594ab3b96e Mon Sep 17 00:00:00 2001 From: yz <43207690+yzim@users.noreply.github.com> Date: Sun, 30 Aug 2026 05:10:47 +0800 Subject: [PATCH] fix(app): use popout scrolling on iPad desktop (#3992) Fix native wide-layout model selectors mounting bottom-sheet scrolling outside a bottom sheet. Keep the scrolling policy with the model browser and cover native compact, native wide, and web surfaces. --- .../src/components/combined-model-selector.tsx | 6 +++++- .../app/src/components/model-browser-view.test.ts | 15 +++++++++++++++ packages/app/src/components/model-browser-view.ts | 10 ++++++++++ .../agent-controls/model-sheet-flow.test.ts | 12 +----------- .../composer/agent-controls/model-sheet-flow.ts | 4 ---- .../src/composer/agent-controls/model-sheet.tsx | 13 +++++++------ 6 files changed, 38 insertions(+), 22 deletions(-) diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx index 5639c604aa..dda77246c0 100644 --- a/packages/app/src/components/combined-model-selector.tsx +++ b/packages/app/src/components/combined-model-selector.tsx @@ -8,6 +8,8 @@ import { ComboboxTrigger } from "@/components/ui/combobox-trigger"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { Combobox, type ComboboxOption, type ComboboxProps } from "@/components/ui/combobox"; import { ModelBrowser, ModelProviderGlyph, useModelBrowser } from "@/components/model-browser"; +import { resolveModelBrowserScrolling } from "@/components/model-browser-view"; +import { useIsCompactFormFactor } from "@/constants/layout"; import { isNative, isWeb } from "@/constants/platform"; import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; import { ICON_SIZE, type Theme } from "@/styles/theme"; @@ -87,6 +89,8 @@ export function CombinedModelSelector({ toolbar, }: CombinedModelSelectorProps) { const { t } = useTranslation(); + const isCompact = useIsCompactFormFactor(); + const modelBrowserScrolling = resolveModelBrowserScrolling({ isNative, isCompact }); const anchorRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [isContentReady, setIsContentReady] = useState(isWeb); @@ -198,7 +202,7 @@ export function CombinedModelSelector({ onEditProfile={onEditProfile ? handleEditProfile : undefined} onRetryProvider={onRetryProvider} isRetryingProvider={isRetryingProvider} - scrolling={isWeb ? "independent" : "sheet"} + scrolling={modelBrowserScrolling} /> ) : ( diff --git a/packages/app/src/components/model-browser-view.test.ts b/packages/app/src/components/model-browser-view.test.ts index b227daead5..028eed8044 100644 --- a/packages/app/src/components/model-browser-view.test.ts +++ b/packages/app/src/components/model-browser-view.test.ts @@ -7,6 +7,7 @@ import { resolveInitialModelBrowserView, resolveModelBrowserAllView, groupProfilesByProviderModel, + resolveModelBrowserScrolling, } from "./model-browser-view"; function provider( @@ -37,6 +38,20 @@ function modelRow( }; } +describe("model browser scrolling", () => { + it("participates in native compact bottom-sheet scrolling", () => { + expect(resolveModelBrowserScrolling({ isNative: true, isCompact: true })).toBe("sheet"); + }); + + it.each([ + { platform: "native wide", isNative: true, isCompact: false }, + { platform: "compact web", isNative: false, isCompact: true }, + { platform: "wide web", isNative: false, isCompact: false }, + ])("owns scrolling on $platform surfaces", ({ isNative, isCompact }) => { + expect(resolveModelBrowserScrolling({ isNative, isCompact })).toBe("independent"); + }); +}); + describe("model browser initial view", () => { const codex = provider("codex", "Codex"); const pi = provider("pi", "Pi"); diff --git a/packages/app/src/components/model-browser-view.ts b/packages/app/src/components/model-browser-view.ts index 464aa69eff..f5c92afa31 100644 --- a/packages/app/src/components/model-browser-view.ts +++ b/packages/app/src/components/model-browser-view.ts @@ -9,6 +9,16 @@ export type ModelBrowserView = | { kind: "all" } | { kind: "provider"; providerId: string; providerLabel: string }; +export function resolveModelBrowserScrolling({ + isNative, + isCompact, +}: { + isNative: boolean; + isCompact: boolean; +}): "sheet" | "independent" { + return isNative && isCompact ? "sheet" : "independent"; +} + /** A profile's model reference; used to match profiles back to model rows. */ export interface ModelProfileRef { provider: string; diff --git a/packages/app/src/composer/agent-controls/model-sheet-flow.test.ts b/packages/app/src/composer/agent-controls/model-sheet-flow.test.ts index 1bf71521a7..a3f41b7910 100644 --- a/packages/app/src/composer/agent-controls/model-sheet-flow.test.ts +++ b/packages/app/src/composer/agent-controls/model-sheet-flow.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; -import { resolveModelBrowserScrolling, resolveModelSheetOpening } from "./model-sheet-flow"; +import { resolveModelSheetOpening } from "./model-sheet-flow"; function provider(id: string, label: string): ProviderSelectorProvider { return { id, label, modelSelection: { kind: "models", rows: [] } }; @@ -40,13 +40,3 @@ describe("model sheet opening", () => { ).toEqual({ kind: "provider", providerId: "codex", providerLabel: "Codex" }); }); }); - -describe("model sheet gesture ownership", () => { - it("uses the sheet-aware model browser inside compact bottom sheets", () => { - expect(resolveModelBrowserScrolling(true)).toBe("sheet"); - }); - - it("keeps independent scrolling in the desktop split viewport", () => { - expect(resolveModelBrowserScrolling(false)).toBe("independent"); - }); -}); diff --git a/packages/app/src/composer/agent-controls/model-sheet-flow.ts b/packages/app/src/composer/agent-controls/model-sheet-flow.ts index 4158f90756..192689e78d 100644 --- a/packages/app/src/composer/agent-controls/model-sheet-flow.ts +++ b/packages/app/src/composer/agent-controls/model-sheet-flow.ts @@ -1,10 +1,6 @@ import type { ModelBrowserView } from "@/components/model-browser-view"; import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; -export function resolveModelBrowserScrolling(usesBottomSheet: boolean): "sheet" | "independent" { - return usesBottomSheet ? "sheet" : "independent"; -} - export function resolveModelSheetOpening({ canSwitchProvider, providers, diff --git a/packages/app/src/composer/agent-controls/model-sheet.tsx b/packages/app/src/composer/agent-controls/model-sheet.tsx index 3e11e9040a..bd55e72c55 100644 --- a/packages/app/src/composer/agent-controls/model-sheet.tsx +++ b/packages/app/src/composer/agent-controls/model-sheet.tsx @@ -9,15 +9,13 @@ import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; import { ComboboxTrigger } from "@/components/ui/combobox-trigger"; import { getProviderIcon } from "@/components/provider-icons"; import { ModelBrowser, useModelBrowser } from "@/components/model-browser"; +import { resolveModelBrowserScrolling } from "@/components/model-browser-view"; import { AgentControlTrigger } from "@/composer/agent-controls/control"; import { ComposerToolbarGlyph } from "@/composer/agent-controls/glyph"; -import { - resolveModelBrowserScrolling, - resolveModelSheetOpening, -} from "@/composer/agent-controls/model-sheet-flow"; +import { resolveModelSheetOpening } from "@/composer/agent-controls/model-sheet-flow"; import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; import { useIsCompactFormFactor } from "@/constants/layout"; -import { isWeb } from "@/constants/platform"; +import { isNative, isWeb } from "@/constants/platform"; const SNAP_POINTS = ["80%", "90%"]; const MODEL_LIST_TOP_INSET = 4; @@ -102,7 +100,10 @@ export function CompactModelSheet({ }: CompactModelSheetProps) { const { t } = useTranslation(); const usesBottomSheet = useIsCompactFormFactor(); - const modelBrowserScrolling = resolveModelBrowserScrolling(usesBottomSheet); + const modelBrowserScrolling = resolveModelBrowserScrolling({ + isNative, + isCompact: usesBottomSheet, + }); const [isOpen, setIsOpen] = useState(false); const [isModelBrowserOpen, setIsModelBrowserOpen] = useState(false); const availableProviders = useMemo(() => {