diff --git a/apps/web/src/components/chat/ComposerModelEffort.tsx b/apps/web/src/components/chat/ComposerModelEffort.tsx index f56997ff2a6a1..f9409e7abf179 100644 --- a/apps/web/src/components/chat/ComposerModelEffort.tsx +++ b/apps/web/src/components/chat/ComposerModelEffort.tsx @@ -512,7 +512,7 @@ export function ComposerEffortColumn(props: { traits: TraitsSelectionInput | nul {/* pt-2.5 puts the first level on the same line as the first model. */}
diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 211bbd59dbd98..6a088dcd07e3a 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -167,6 +167,7 @@ const MODEL_LIST_ESTIMATED_ITEM_SIZE = 32; // Rendered heights in `--spacing` units. Chrome is the search box and list padding, plus a 1px border. const ROW_UNITS = 8; const ROW_SEPARATOR_UNITS = 0.5; +const LIST_PADDING_UNITS = 5; const LIST_CHROME_UNITS = 15.5; function ModelListSeparator() { @@ -256,11 +257,13 @@ function ModelPickerList({ itemKeys, extraData, renderItem, + fitRows, }: { listRef: RefObject; itemKeys: ReadonlyArray; extraData: unknown; renderItem: (props: { item: string; index: number }) => ReactNode; + fitRows: boolean | undefined; }) { const [showTopScrollFade, setShowTopScrollFade] = useState(false); const [showBottomScrollFade, setShowBottomScrollFade] = useState(false); @@ -277,7 +280,15 @@ function ModelPickerList({ className={cn( "relative min-h-0 overflow-hidden pr-px", itemKeys.length > 0 ? "flex-1" : "h-0", + fitRows && "[--row-units:9] sm:[--row-units:8]", )} + style={ + fitRows && itemKeys.length > 0 + ? { + flexBasis: `calc(var(--spacing) * (${itemKeys.length} * var(--row-units) + ${(itemKeys.length - 1) * ROW_SEPARATOR_UNITS} + ${LIST_PADDING_UNITS}))`, + } + : undefined + } > @@ -295,7 +306,8 @@ function ModelPickerList({ onScroll={updateScrollFades} onReady={updateScrollFades} className={cn( - "scrollbar-gutter-stable h-full overflow-x-hidden overscroll-y-contain py-1.5 [&::-webkit-scrollbar-track]:my-2", + "scrollbar-gutter-stable h-full overflow-x-hidden py-1.5 [&::-webkit-scrollbar-track]:my-2", + !fitRows && "overscroll-y-contain", getVirtualizedScrollFadeClassName({ top: showTopScrollFade, bottom: showBottomScrollFade, @@ -343,7 +355,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { docked?: boolean; /** Right at the end of the search text moves into the popup's effort column. */ onFocusEffort?: () => void; - /** Closes the list column, inside the popup's fixed height. */ + /** Closes the list column. */ listFooter?: ReactNode; }) { const { @@ -436,6 +448,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { const focusSearchInput = () => { searchInputRef.current?.focus({ preventScroll: true }); + searchInputRef.current?.scrollIntoView({ block: "nearest", inline: "nearest" }); }; const handleSelectInstance = (instanceId: ProviderInstanceId | "favorites") => { @@ -817,9 +830,9 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { ref={pickerContentRef} className={cn( "relative flex w-screen max-w-90 flex-row overflow-hidden", - props.docked ? "max-sm:h-56 max-sm:w-full max-sm:max-w-none" : "max-h-86.5", + props.docked ? "max-sm:max-h-56 max-sm:w-full max-sm:max-w-none" : "max-h-86.5", )} - // Docked, the popup sets the height; a minimum here would overflow short windows. + // Docked, the list sets the height; a minimum here would overflow short windows. style={ props.docked ? undefined @@ -833,6 +846,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { > {showSidebar && (
@@ -986,6 +1001,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { /> void; instanceEntries: ReadonlyArray; showFavorites?: boolean; + docked?: boolean; disabledInstanceIds?: ReadonlySet; selectableUnavailableInstanceIds?: ReadonlySet; getDisabledInstanceTooltip?: (entry: ProviderInstanceEntry) => string; @@ -69,7 +70,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { return ( -
+
{selectedIndicatorTop !== null ? (
void focusEffort("any"), - // Inside the fixed height, so switching to or from a ChatGPT plan never resizes it. ...(props.selectedModels === undefined ? { listFooter: } : {}), @@ -457,23 +457,30 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { {props.effortColumn ? (
- {/* One fixed height, so a model with more options never resizes the popup. Short - windows shrink it and keep the footer. */} -
- {picker} -
{ - if (event.key !== "ArrowLeft" || event.altKey || event.ctrlKey || event.metaKey) - return; - event.preventDefault(); - event.stopPropagation(); - searchInputRef.current?.focus(); - }} - > - {props.effortColumn} -
+
+ +
+ {picker} +
{ + if ( + event.key !== "ArrowLeft" || + event.altKey || + event.ctrlKey || + event.metaKey + ) + return; + event.preventDefault(); + event.stopPropagation(); + searchInputRef.current?.focus(); + }} + > + {props.effortColumn} +
+
+
{props.footer}