diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 73fc2c94b1e7a..efeacbd4b3cd5 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -163,6 +163,11 @@ function adjacentModelPickerProvider(input: { const EMPTY_MODEL_JUMP_LABELS = new Map(); const MODEL_LIST_ESTIMATED_ITEM_SIZE = 52; +// Rendered heights in `--spacing` units. Chrome is the search box and list padding, plus a 1px border. +const ONE_LINE_ROW_UNITS = 8; +const TWO_LINE_ROW_UNITS = { classic: 11.25, redesign: 12 }; +const ROW_SEPARATOR_UNITS = 0.5; +const LIST_CHROME_UNITS = 15.5; function ModelListSeparator() { return
; @@ -259,10 +264,6 @@ function ModelPickerList({ }) { const [showTopScrollFade, setShowTopScrollFade] = useState(false); const [showBottomScrollFade, setShowBottomScrollFade] = useState(false); - const [contentSize, setContentSize] = useState( - () => itemKeys.length * MODEL_LIST_ESTIMATED_ITEM_SIZE, - ); - useLayoutEffect(() => listRef.current?.getState().listen("totalSize", setContentSize), [listRef]); const updateScrollFades = () => { const scrollElement = listRef.current?.getScrollableNode(); if (!(scrollElement instanceof HTMLElement)) { @@ -297,10 +298,10 @@ function ModelPickerList({ return (
0 ? "flex-1" : "h-0", + )} > @@ -578,6 +579,33 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { // Inside one provider's tab the provider line repeats the selected rail icon on every row. const redesign = useClientSettings((s) => s.redesignEnabled); const rowsMixProviders = !redesign || isSearching || selectedInstanceId === "favorites"; + // Fit the tallest tab so switching tabs never resizes the popup and moves the rail under the cursor. + const tallestTabUnits = (() => { + const unitsByTab = new Map(); + const addRow = (tab: ProviderInstanceId | "favorites", twoLine: boolean) => { + const rowUnits = twoLine + ? TWO_LINE_ROW_UNITS[redesign ? "redesign" : "classic"] + : ONE_LINE_ROW_UNITS; + const previous = unitsByTab.get(tab); + unitsByTab.set( + tab, + previous === undefined ? rowUnits : previous + ROW_SEPARATOR_UNITS + rowUnits, + ); + }; + const tabsWithLegacyRow = new Set(); + for (const model of flatModels) { + if (!matchesLockedProvider(model)) continue; + const isFavorite = favoritesSet.has(providerModelKey(model.instanceId, model.slug)); + if (isFavorite) addRow("favorites", true); + if (model.isLegacy && !isFavorite) { + tabsWithLegacyRow.add(model.instanceId); + } else { + addRow(model.instanceId, !redesign || Boolean(model.subProvider)); + } + } + for (const instanceId of tabsWithLegacyRow) addRow(instanceId, true); + return Math.max(0, ...unitsByTab.values()); + })(); const lockedDisabledInstanceIds = (() => { if (!isLocked) { return undefined; @@ -799,7 +827,13 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
{showSidebar && (