From b8e801364aec9dcf5d4a9b102d507f5ab9c1f419 Mon Sep 17 00:00:00 2001 From: JeroenBolhuis Date: Sun, 1 Nov 2026 23:23:20 +0100 Subject: [PATCH 1/5] fix(web): prevent docked model picker height regression - Fit the docked model list to its rows and anchor height changes at the bottom - Animate the picker as its content changes while respecting available height --- apps/web/src/components/AnimatedHeight.tsx | 8 +++- .../components/chat/ModelPickerContent.tsx | 18 ++++++-- .../components/chat/ProviderModelPicker.tsx | 45 ++++++++++--------- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/apps/web/src/components/AnimatedHeight.tsx b/apps/web/src/components/AnimatedHeight.tsx index a098851233a72..6558e22d17d1f 100644 --- a/apps/web/src/components/AnimatedHeight.tsx +++ b/apps/web/src/components/AnimatedHeight.tsx @@ -1,14 +1,17 @@ import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react"; import { observeResize } from "../lib/observeResize"; +import { cn } from "../lib/utils"; const HEIGHT_TRANSITION_FALLBACK_MS = 250; export function AnimatedHeight({ children, holdHeight = false, + anchor = "top", }: { readonly children: ReactNode; readonly holdHeight?: boolean; + readonly anchor?: "top" | "bottom"; }) { const contentRef = useRef(null); const [heightState, setHeightState] = useState<{ @@ -83,7 +86,10 @@ export function AnimatedHeight({ return (
; itemKeys: ReadonlyArray; extraData: unknown; renderItem: (props: { item: string; index: number }) => ReactNode; + fitRows: boolean | undefined; }) { const [showTopScrollFade, setShowTopScrollFade] = useState(false); const [showBottomScrollFade, setShowBottomScrollFade] = useState(false); @@ -301,7 +304,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 + } > @@ -366,7 +377,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 { @@ -840,9 +851,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 @@ -1009,6 +1020,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { /> void focusEffort("any"), - // Inside the fixed height, so switching to or from a ChatGPT plan never resizes it. ...(props.selectedModels === undefined ? { listFooter: } : {}), @@ -422,27 +422,32 @@ 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}
- {props.footer} -
+ ) : ( picker From 6d644e6598db198646213d251782239073db3dd9 Mon Sep 17 00:00:00 2001 From: JeroenBolhuis Date: Mon, 2 Nov 2026 00:14:41 +0100 Subject: [PATCH 2/5] fix(web): keep the docked model picker footer fixed while it resizes Co-authored-by: Cursor --- apps/web/src/components/AnimatedHeight.tsx | 8 +-- .../components/chat/ProviderModelPicker.tsx | 50 ++++++++++--------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/apps/web/src/components/AnimatedHeight.tsx b/apps/web/src/components/AnimatedHeight.tsx index 6558e22d17d1f..a098851233a72 100644 --- a/apps/web/src/components/AnimatedHeight.tsx +++ b/apps/web/src/components/AnimatedHeight.tsx @@ -1,17 +1,14 @@ import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react"; import { observeResize } from "../lib/observeResize"; -import { cn } from "../lib/utils"; const HEIGHT_TRANSITION_FALLBACK_MS = 250; export function AnimatedHeight({ children, holdHeight = false, - anchor = "top", }: { readonly children: ReactNode; readonly holdHeight?: boolean; - readonly anchor?: "top" | "bottom"; }) { const contentRef = useRef(null); const [heightState, setHeightState] = useState<{ @@ -86,10 +83,7 @@ export function AnimatedHeight({ return (
{props.effortColumn ? ( - -
-
- {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} +
- + {props.footer} +
) : ( picker From 166ace05b2da37323017994d01bebaf55ba5241e Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 3 Nov 2026 01:00:42 +0000 Subject: [PATCH 3/5] fix(web): chain docked model list scrolling to popup --- apps/web/src/components/chat/ModelPickerContent.tsx | 3 ++- apps/web/src/components/chat/ProviderModelPicker.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index fddc2a2ea19e9..4b368b2a3eb04 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -329,7 +329,8 @@ function ModelPickerList({ onLayout={updateScrollFades} onScroll={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, diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx index 4842c5a94deb7..89d164eec0aec 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.tsx @@ -423,7 +423,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { {props.effortColumn ? (
-
+
{picker} From 432a9024a7414ffdba2354e30e7f471caf9980ce Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 3 Nov 2026 01:05:13 +0000 Subject: [PATCH 4/5] fix(web): chain effort scrolling back to model search --- apps/web/src/components/chat/ComposerModelEffort.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/ComposerModelEffort.tsx b/apps/web/src/components/chat/ComposerModelEffort.tsx index 3a863936101b2..7c998df64d972 100644 --- a/apps/web/src/components/chat/ComposerModelEffort.tsx +++ b/apps/web/src/components/chat/ComposerModelEffort.tsx @@ -513,7 +513,7 @@ export function ComposerEffortColumn(props: { traits: TraitsSelectionInput | nul
{/* pt-2.5 puts the first level on the same line as the first model. */}
From a2d741625bad3cbbf0e34a8675c385ee30ae52d3 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 3 Nov 2026 01:09:59 +0000 Subject: [PATCH 5/5] fix(web): chain docked provider rail scrolling to popup --- apps/web/src/components/chat/ModelPickerContent.tsx | 1 + apps/web/src/components/chat/ModelPickerSidebar.tsx | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 4b368b2a3eb04..405a6de94afe1 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -868,6 +868,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { > {showSidebar && ( void; instanceEntries: ReadonlyArray; showFavorites?: boolean; + docked?: boolean; disabledInstanceIds?: ReadonlySet; selectableUnavailableInstanceIds?: ReadonlySet; getDisabledInstanceTooltip?: (entry: ProviderInstanceEntry) => string; @@ -82,7 +83,12 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { } }} > -
+
{selectedIndicatorTop !== null ? (