authorgravatar for saphid@gmail.comAlex Southwell <saphid@gmail.com> 2026-10-04 11:59:24+11:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-03 21:59:24-03:00
loga6aebbe2e06e6f2fb247a291272dcfbedb8ed63f
tree5143c0dc2cd2b90b087a1426ffeaa8b2a22052f8
parent8e399944393ee48054c0ca4aece0b6c9befd0998
signature Signed by PGP key B5690EEEBB952194

fix(web): size the model picker to its content (#15152)

Co-authored-by: saphid <saphid@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

1 files changed, 26 insertions(+), 4 deletions(-)

apps/web/src/components/chat/ModelPickerContent.tsx+26-4
...@@ -144,6 +144,7 @@ export function adjacentModelPickerProvider(input: {...@@ -144,6 +144,7 @@ export function adjacentModelPickerProvider(input: {
144}144}
145145
146const EMPTY_MODEL_JUMP_LABELS = new Map<string, string>();146const EMPTY_MODEL_JUMP_LABELS = new Map<string, string>();
147const MODEL_LIST_ESTIMATED_ITEM_SIZE = 52;
147148
148function ModelListSeparator() {149function ModelListSeparator() {
149 return <div className="h-0.5" />;150 return <div className="h-0.5" />;
...@@ -197,6 +198,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -197,6 +198,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
197 const [showBottomScrollFade, setShowBottomScrollFade] = useState(false);198 const [showBottomScrollFade, setShowBottomScrollFade] = useState(false);
198 const searchInputRef = useRef<HTMLInputElement>(null);199 const searchInputRef = useRef<HTMLInputElement>(null);
199 const modelListRef = useRef<LegendListRef | null>(null);200 const modelListRef = useRef<LegendListRef | null>(null);
201 const pickerContentRef = useRef<HTMLDivElement>(null);
200 const highlightedModelKeyRef = useRef<string | null>(null);202 const highlightedModelKeyRef = useRef<string | null>(null);
201 const favorites = useClientSettings((s) => s.favorites ?? []);203 const favorites = useClientSettings((s) => s.favorites ?? []);
202 const activeEntry = props.instanceEntries.find(204 const activeEntry = props.instanceEntries.find(
...@@ -699,6 +701,17 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -699,6 +701,17 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
699 ),701 ),
700 [visibleModels],702 [visibleModels],
701 );703 );
704 const [modelListContentSize, setModelListContentSize] = useState(
705 () => filteredItemKeys.length * MODEL_LIST_ESTIMATED_ITEM_SIZE,
706 );
707 const [searchHeight, setSearchHeight] = useState(0);
708 useLayoutEffect(
709 () => modelListRef.current?.getState().listen("totalSize", setModelListContentSize),
710 [],
711 );
712 // Fit the list to its rows plus the combobox list `py-1` and LegendList `py-1.5`.
713 const modelListHeight =
714 filteredItemKeys.length === 0 ? 0 : `calc(${modelListContentSize}px + var(--spacing) * 5)`;
702 const updateModelListScrollFades = useCallback(() => {715 const updateModelListScrollFades = useCallback(() => {
703 const scrollElement = modelListRef.current?.getScrollableNode();716 const scrollElement = modelListRef.current?.getScrollableNode();
704 if (!(scrollElement instanceof HTMLElement)) {717 if (!(scrollElement instanceof HTMLElement)) {
...@@ -815,7 +828,10 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -815,7 +828,10 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
815 return (828 return (
816 <TooltipProvider delay={0}>829 <TooltipProvider delay={0}>
817 <div830 <div
818 className="relative flex h-screen max-h-86.5 w-screen max-w-90 flex-row overflow-hidden"831 ref={pickerContentRef}
832 className="relative flex max-h-86.5 w-screen max-w-90 flex-row overflow-hidden"
833 // Hold the height from when the search started; results scroll instead of resizing.
834 style={isSearching ? { height: searchHeight } : undefined}
819 data-model-picker-content="true"835 data-model-picker-content="true"
820 >836 >
821 {/* Sidebar */}837 {/* Sidebar */}
...@@ -890,7 +906,10 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -890,7 +906,10 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
890 ref={searchInputRef}906 ref={searchInputRef}
891 placeholder="Search models..."907 placeholder="Search models..."
892 value={searchQuery}908 value={searchQuery}
893 onChange={(e) => setSearchQuery(e.target.value)}909 onChange={(e) => {
910 if (!isSearching) setSearchHeight(pickerContentRef.current?.offsetHeight ?? 0);
911 setSearchQuery(e.target.value);
912 }}
894 onKeyDown={(e) => {913 onKeyDown={(e) => {
895 if (914 if (
896 showSidebar &&915 showSidebar &&
...@@ -944,7 +963,10 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -944,7 +963,10 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
944 />963 />
945964
946 {/* Model list */}965 {/* Model list */}
947 <div className="relative min-h-0 flex-1 overflow-hidden pr-px">966 <div
967 className="relative min-h-0 overflow-hidden pr-px"
968 style={{ height: modelListHeight }}
969 >
948 <ComboboxListVirtualized>970 <ComboboxListVirtualized>
949 <LegendList<string>971 <LegendList<string>
950 ref={modelListRef}972 ref={modelListRef}
...@@ -1013,7 +1035,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -1013,7 +1035,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
1013 />1035 />
1014 );1036 );
1015 }}1037 }}
1016 estimatedItemSize={52}1038 estimatedItemSize={MODEL_LIST_ESTIMATED_ITEM_SIZE}
1017 drawDistance={480}1039 drawDistance={480}
1018 recycleItems1040 recycleItems
1019 contentContainerClassName="pl-2 pr-px"1041 contentContainerClassName="pl-2 pr-px"