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}