Skip to content
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/ComposerModelEffort.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ export function ComposerEffortColumn(props: { traits: TraitsSelectionInput | nul
</div>
{/* pt-2.5 puts the first level on the same line as the first model. */}
<div
className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto overscroll-contain px-2 pt-2.5 pb-1.5"
className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto px-2 pt-2.5 pb-1.5"
// Capture, so Up and Down on an option row move rows instead of opening its menu.
onKeyDownCapture={moveBetweenRows}
>
Expand Down
26 changes: 21 additions & 5 deletions apps/web/src/components/chat/ModelPickerContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -256,11 +257,13 @@ function ModelPickerList({
itemKeys,
extraData,
renderItem,
fitRows,
}: {
listRef: RefObject<LegendListRef | null>;
itemKeys: ReadonlyArray<string>;
extraData: unknown;
renderItem: (props: { item: string; index: number }) => ReactNode;
fitRows: boolean | undefined;
}) {
const [showTopScrollFade, setShowTopScrollFade] = useState(false);
const [showBottomScrollFade, setShowBottomScrollFade] = useState(false);
Expand All @@ -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
}
>
<ComboboxListVirtualized>
<LegendList<string>
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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") => {
Expand Down Expand Up @@ -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
Expand All @@ -833,6 +846,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
>
{showSidebar && (
<ModelPickerSidebar
docked={props.docked ?? false}
selectedInstanceId={selectedInstanceId}
onSelectInstance={handleSelectInstance}
onFocusSearch={focusSearchInput}
Expand Down Expand Up @@ -893,7 +907,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
>
<div
className={cn(
"flex min-h-0 flex-1 flex-col overflow-hidden bg-muted/40",
"flex min-h-0 flex-1 flex-col overflow-hidden",
!props.docked && "bg-muted/40",
showSidebar && "border-l border-border/70",
)}
>
Expand Down Expand Up @@ -986,6 +1001,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
/>

<ModelPickerList
fitRows={props.docked}
listRef={modelListRef}
itemKeys={filteredItemKeys}
extraData={modelListExtraData}
Expand Down
10 changes: 8 additions & 2 deletions apps/web/src/components/chat/ModelPickerSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
onFocusSearch: () => void;
instanceEntries: ReadonlyArray<ProviderInstanceEntry>;
showFavorites?: boolean;
docked?: boolean;
disabledInstanceIds?: ReadonlySet<ProviderInstanceId>;
selectableUnavailableInstanceIds?: ReadonlySet<ProviderInstanceId>;
getDisabledInstanceTooltip?: (entry: ProviderInstanceEntry) => string;
Expand Down Expand Up @@ -69,7 +70,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {

return (
<Toolbar.Root
className="w-11 shrink-0 overflow-hidden bg-muted/30"
className={cn("w-11 shrink-0 overflow-hidden", !props.docked && "bg-muted/30")}
data-model-picker-sidebar="true"
aria-label="Providers"
orientation="vertical"
Expand All @@ -82,7 +83,12 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
}
}}
>
<div className="h-full overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<div
className={cn(
"h-full overflow-y-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
!props.docked && "overscroll-contain",
)}
>
<div ref={sidebarContentRef} className="relative flex min-h-full flex-col gap-1 p-1">
{selectedIndicatorTop !== null ? (
<div
Expand Down
43 changes: 25 additions & 18 deletions apps/web/src/components/chat/ProviderModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
useRef,
useState,
} from "react";
import { AnimatedHeight } from "../AnimatedHeight";
import { Badge } from "../ui/badge";
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
Expand Down Expand Up @@ -308,7 +309,6 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
? {
docked,
onFocusEffort: () => void focusEffort("any"),
// Inside the fixed height, so switching to or from a ChatGPT plan never resizes it.
...(props.selectedModels === undefined
? { listFooter: <ChatGptSharingControl provider={activeEntry?.snapshot ?? null} /> }
: {}),
Expand Down Expand Up @@ -457,23 +457,30 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
{props.effortColumn ? (
<DockedPickerContext value={{ modelLabel: triggerTitle }}>
<div className="flex max-h-(--available-height) flex-col">
{/* One fixed height, so a model with more options never resizes the popup. Short
windows shrink it and keep the footer. */}
<div className="flex h-86.5 min-h-0 max-sm:h-auto max-sm:flex-col">
{picker}
<div
data-model-picker-effort
className="flex min-h-0 w-48 flex-col sm:shrink-0 border-l border-border/70 bg-muted/40 max-sm:h-64 max-sm:w-full max-sm:border-t max-sm:border-l-0"
onKeyDown={(event) => {
if (event.key !== "ArrowLeft" || event.altKey || event.ctrlKey || event.metaKey)
return;
event.preventDefault();
event.stopPropagation();
searchInputRef.current?.focus();
}}
>
{props.effortColumn}
</div>
<div className="min-h-0 overflow-y-auto overscroll-y-contain bg-muted/40 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<AnimatedHeight>
<div className="flex max-h-86.5 max-sm:max-h-none max-sm:flex-col">
{picker}
<div
data-model-picker-effort
className="flex min-h-0 w-48 flex-col sm:shrink-0 border-l border-border/70 max-sm:max-h-64 max-sm:w-full max-sm:border-t max-sm:border-l-0"
onKeyDown={(event) => {
if (
event.key !== "ArrowLeft" ||
event.altKey ||
event.ctrlKey ||
event.metaKey
)
return;
event.preventDefault();
event.stopPropagation();
searchInputRef.current?.focus();
}}
>
{props.effortColumn}
</div>
</div>
</AnimatedHeight>
</div>
{props.footer}
</div>
Expand Down
Loading