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.
This commit is contained in:
yz
2026-08-29 23:10:47 +02:00
committed by GitHub
parent 63587a9c03
commit 0dad1d7a61
6 changed files with 38 additions and 22 deletions
@@ -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<View>(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}
/>
) : (
<View style={styles.sheetLoadingState}>
@@ -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");
@@ -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;
@@ -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");
});
});
@@ -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,
@@ -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(() => {