import { type ProviderInstanceId, type ProviderDriverKind, type ResolvedKeybindingsConfig, } from "@t3tools/contracts"; import { resolveSelectableModel } from "@t3tools/shared/model"; import { LegendList, type LegendListRef } from "@legendapp/list/react"; import { memo, useMemo, useState, useCallback, useEffect, useLayoutEffect, useRef } from "react"; import { SearchIcon } from "lucide-react"; import { ModelListRow } from "./ModelListRow"; import { ModelPickerSidebar } from "./ModelPickerSidebar"; import { isModelPickerNewModel } from "./modelPickerModelHighlights"; import { buildModelPickerSearchText, scoreModelPickerSearch } from "./modelPickerSearch"; import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxListVirtualized } from "../ui/combobox"; import { ModelEsque } from "./providerIconUtils"; import { modelPickerJumpCommandForIndex, modelPickerJumpIndexFromCommand, resolveShortcutCommand, shortcutLabelForCommand, } from "../../keybindings"; import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"; import { cn } from "~/lib/utils"; import { TooltipProvider } from "../ui/tooltip"; import { isProviderInstancePickerReady, isProviderInstancePickerVisible, type ProviderInstanceEntry, } from "../../providerInstances"; import { providerModelKey, sortProviderModelItems } from "../../modelOrdering"; type ModelPickerItem = { slug: string; name: string; shortName?: string; subProvider?: string; instanceId: ProviderInstanceId; driverKind: ProviderDriverKind; instanceDisplayName: string; instanceAccentColor?: string | undefined; continuationGroupKey?: string | undefined; }; const EMPTY_MODEL_JUMP_LABELS = new Map(); // Split a `${instanceId}:${slug}` combobox key back into its pieces. Slugs // can contain colons (e.g. some vendor model ids), so we only split on the // first colon — anything after that is the slug. function splitInstanceModelKey(key: string): { instanceId: ProviderInstanceId; slug: string } { const colonIndex = key.indexOf(":"); if (colonIndex === -1) { return { instanceId: key as ProviderInstanceId, slug: "" }; } return { instanceId: key.slice(0, colonIndex) as ProviderInstanceId, slug: key.slice(colonIndex + 1), }; } export const ModelPickerContent = memo(function ModelPickerContent(props: { /** The instance currently selected in the composer (combobox "value"). */ activeInstanceId: ProviderInstanceId; model: string; /** * When set, the picker is locked to the given driver kind — typically * because the user is editing a previously-sent message and can't change * which driver served the turn. Multiple instances of the same kind * remain selectable (e.g. locked to `codex` still lets the user switch * between the default Codex and a custom Codex Personal). */ lockedProvider: ProviderDriverKind | null; lockedContinuationGroupKey?: string | null; /** * All configured provider instances in display order. Used to render * the sidebar (one button per instance) and to resolve display names * for the locked-mode header. */ instanceEntries: ReadonlyArray; keybindings?: ResolvedKeybindingsConfig; /** * Model options per instance. Keyed by `ProviderInstanceId` so the * default Codex instance and any custom Codex instances each have their * own list (custom instances typically start with the same built-in * model set but are free to diverge via customModels). */ modelOptionsByInstance: ReadonlyMap>; terminalOpen: boolean; onRequestClose?: () => void; getModelDisabledReason?: (instanceId: ProviderInstanceId, model: string) => string | null; onInstanceModelChange: (instanceId: ProviderInstanceId, model: string) => void; }) { const { keybindings: providedKeybindings, modelOptionsByInstance, instanceEntries, getModelDisabledReason, onInstanceModelChange, } = props; const [searchQuery, setSearchQuery] = useState(""); const [showTopScrollFade, setShowTopScrollFade] = useState(false); const [showBottomScrollFade, setShowBottomScrollFade] = useState(false); const searchInputRef = useRef(null); const modelListRef = useRef(null); const highlightedModelKeyRef = useRef(null); const favorites = useClientSettings((s) => s.favorites ?? []); const [selectedInstanceId, setSelectedInstanceId] = useState( () => { if (props.lockedProvider !== null) { // When locked, prime the sidebar to the currently-active instance // so jumping into the picker keeps the focused instance visible. return props.activeInstanceId; } return favorites.length > 0 ? "favorites" : props.activeInstanceId; }, ); const keybindings = useMemo( () => providedKeybindings ?? [], [providedKeybindings], ); const updateSettings = useUpdateClientSettings(); const focusSearchInput = useCallback(() => { searchInputRef.current?.focus({ preventScroll: true }); }, []); const handleSelectInstance = useCallback( (instanceId: ProviderInstanceId | "favorites") => { setSelectedInstanceId(instanceId); window.requestAnimationFrame(() => { focusSearchInput(); }); }, [focusSearchInput], ); useLayoutEffect(() => { focusSearchInput(); const frame = window.requestAnimationFrame(() => { focusSearchInput(); }); const timeout = window.setTimeout(() => { focusSearchInput(); }, 0); return () => { window.cancelAnimationFrame(frame); window.clearTimeout(timeout); }; }, [focusSearchInput]); // Create a Set for efficient lookup. Favorites are keyed by // `${instanceId}:${slug}`; the storage schema widened from ProviderDriverKind // to ProviderInstanceId so pre-migration favorites keyed by driver slugs // (e.g. `"codex:gpt-5"`) still resolve — the default instance id equals // the driver slug. const favoritesSet = useMemo(() => { return new Set(favorites.map((fav) => providerModelKey(fav.provider, fav.model))); }, [favorites]); /** * Lookup table keyed by `instanceId`. Used for display name + driver * kind enrichment and for `ready`/enabled filtering before flattening * models into the search list. */ const entryByInstanceId = useMemo( () => new Map(instanceEntries.map((entry) => [entry.instanceId, entry])), [instanceEntries], ); const matchesLockedProvider = useCallback( (entry: Pick): boolean => { if (props.lockedProvider === null) return true; if (entry.driverKind !== props.lockedProvider) return false; if (!props.lockedContinuationGroupKey) return true; return entry.continuationGroupKey === props.lockedContinuationGroupKey; }, [props.lockedContinuationGroupKey, props.lockedProvider], ); const readyInstanceSet = useMemo(() => { const ready = new Set(); for (const entry of instanceEntries) { if (isProviderInstancePickerReady(entry)) { ready.add(entry.instanceId); } } return ready; }, [instanceEntries]); // Flatten models into a searchable array. One pass over the // instance-keyed map; each model carries its instance id + driver kind // so the list row can render the right icon and display name without // another lookup. const flatModels = useMemo(() => { const out: ModelPickerItem[] = []; for (const [instanceId, models] of modelOptionsByInstance) { const entry = entryByInstanceId.get(instanceId); if (!entry) { // Instance disappeared between renders (configuration change). Skip // its models — stale options shouldn't appear in the picker. continue; } if (!readyInstanceSet.has(instanceId)) { continue; } for (const model of models) { out.push({ slug: model.slug, name: model.name, ...(model.shortName ? { shortName: model.shortName } : {}), ...(model.subProvider ? { subProvider: model.subProvider } : {}), instanceId, driverKind: entry.driverKind, instanceDisplayName: entry.displayName, ...(entry.accentColor ? { instanceAccentColor: entry.accentColor } : {}), ...(entry.continuationGroupKey ? { continuationGroupKey: entry.continuationGroupKey } : {}), }); } } return out; }, [modelOptionsByInstance, entryByInstanceId, readyInstanceSet]); const isLocked = props.lockedProvider !== null; const isSearching = searchQuery.trim().length > 0; const lockedDisabledInstanceIds = useMemo(() => { if (!isLocked) { return undefined; } const disabled = new Set(); for (const entry of instanceEntries) { if (!matchesLockedProvider(entry)) { disabled.add(entry.instanceId); } } return disabled; }, [instanceEntries, isLocked, matchesLockedProvider]); const sidebarInstanceEntries = useMemo(() => { const enabledEntries = instanceEntries.filter(isProviderInstancePickerVisible); if (!isLocked) { return enabledEntries; } const available: ProviderInstanceEntry[] = []; const disabled: ProviderInstanceEntry[] = []; for (const entry of enabledEntries) { if (matchesLockedProvider(entry)) { available.push(entry); } else { disabled.push(entry); } } return [...available, ...disabled]; }, [instanceEntries, isLocked, matchesLockedProvider]); const showSidebar = !isSearching && sidebarInstanceEntries.length > 0; const instanceOrder = useMemo( () => instanceEntries.map((entry) => entry.instanceId), [instanceEntries], ); // Filter models based on search query and selected instance const filteredModels = useMemo(() => { let result = flatModels; // Apply tokenized fuzzy search across the combined provider/model search fields. if (searchQuery.trim()) { const rankedMatches = result .map((model) => ({ model, score: scoreModelPickerSearch( { name: model.name, ...(model.shortName ? { shortName: model.shortName } : {}), ...(model.subProvider ? { subProvider: model.subProvider } : {}), driverKind: model.driverKind, providerDisplayName: model.instanceDisplayName, isFavorite: favoritesSet.has(providerModelKey(model.instanceId, model.slug)), }, searchQuery, ), isFavorite: favoritesSet.has(providerModelKey(model.instanceId, model.slug)), tieBreaker: buildModelPickerSearchText({ name: model.name, ...(model.shortName ? { shortName: model.shortName } : {}), ...(model.subProvider ? { subProvider: model.subProvider } : {}), driverKind: model.driverKind, providerDisplayName: model.instanceDisplayName, }), })) .filter( ( rankedModel, ): rankedModel is { model: ModelPickerItem; score: number; isFavorite: boolean; tieBreaker: string; } => rankedModel.score !== null, ); // When searching, we only respect locked provider (by driver kind), // ignoring sidebar selection so account-scoped searches can find a // model before the user chooses a specific instance rail item. if (props.lockedProvider !== null) { const lockedProviderMatches: Array<(typeof rankedMatches)[number]> = []; for (const rankedModel of rankedMatches) { if (matchesLockedProvider(rankedModel.model)) { lockedProviderMatches.push(rankedModel); } } return lockedProviderMatches .toSorted((a, b) => { const scoreDelta = a.score - b.score; if (scoreDelta !== 0) { return scoreDelta; } if (a.isFavorite !== b.isFavorite) { return a.isFavorite ? -1 : 1; } return a.tieBreaker.localeCompare(b.tieBreaker); }) .map((rankedModel) => rankedModel.model); } return rankedMatches .toSorted((a, b) => { const scoreDelta = a.score - b.score; if (scoreDelta !== 0) { return scoreDelta; } if (a.isFavorite !== b.isFavorite) { return a.isFavorite ? -1 : 1; } return a.tieBreaker.localeCompare(b.tieBreaker); }) .map((rankedModel) => rankedModel.model); } if (props.lockedProvider !== null) { result = result.filter((m) => matchesLockedProvider(m)); if (selectedInstanceId === "favorites") { result = result.filter((m) => favoritesSet.has(providerModelKey(m.instanceId, m.slug))); } else { result = result.filter((m) => m.instanceId === selectedInstanceId); } } else if (selectedInstanceId === "favorites") { result = result.filter((m) => favoritesSet.has(providerModelKey(m.instanceId, m.slug))); } else { result = result.filter((m) => m.instanceId === selectedInstanceId); } return sortProviderModelItems(result, { favoriteModelKeys: favoritesSet, groupFavorites: selectedInstanceId !== "favorites", instanceOrder: selectedInstanceId === "favorites" ? instanceOrder : [], }); }, [ favoritesSet, flatModels, instanceOrder, matchesLockedProvider, props.lockedProvider, searchQuery, selectedInstanceId, ]); const handleModelSelect = useCallback( (modelSlug: string, instanceId: ProviderInstanceId) => { if (getModelDisabledReason?.(instanceId, modelSlug)) { return; } const options = modelOptionsByInstance.get(instanceId); if (!options) { return; } const entry = entryByInstanceId.get(instanceId); if (!entry) { return; } // `resolveSelectableModel` uses the driver kind for normalization // (slug casing etc.). Custom instances share their driver's // normalization rules, so pass the driver kind here. const resolvedModel = resolveSelectableModel(entry.driverKind, modelSlug, options); if (resolvedModel) { onInstanceModelChange(instanceId, resolvedModel); } }, [entryByInstanceId, getModelDisabledReason, modelOptionsByInstance, onInstanceModelChange], ); const toggleFavorite = useCallback( (instanceId: ProviderInstanceId, model: string) => { const newFavorites = [...favorites]; const index = newFavorites.findIndex((f) => f.provider === instanceId && f.model === model); if (index >= 0) { newFavorites.splice(index, 1); } else { newFavorites.push({ provider: instanceId, model }); } updateSettings({ favorites: newFavorites }); }, [favorites, updateSettings], ); const modelJumpCommandByKey = useMemo(() => { const mapping = new Map< string, NonNullable> >(); let selectableModelIndex = 0; for (const model of filteredModels) { if (getModelDisabledReason?.(model.instanceId, model.slug)) { continue; } const jumpCommand = modelPickerJumpCommandForIndex(selectableModelIndex); if (!jumpCommand) { return mapping; } mapping.set(`${model.instanceId}:${model.slug}`, jumpCommand); selectableModelIndex += 1; } return mapping; }, [filteredModels, getModelDisabledReason]); const modelJumpModelKeys = useMemo( () => [...modelJumpCommandByKey.keys()], [modelJumpCommandByKey], ); const allModelKeys = useMemo( (): string[] => flatModels.map((model) => `${model.instanceId}:${model.slug}`), [flatModels], ); const filteredModelKeys = useMemo( (): string[] => filteredModels.map((model) => `${model.instanceId}:${model.slug}`), [filteredModels], ); const filteredModelByKey = useMemo( (): ReadonlyMap => new Map(filteredModels.map((model) => [`${model.instanceId}:${model.slug}`, model] as const)), [filteredModels], ); const updateModelListScrollFades = useCallback(() => { const scrollElement = modelListRef.current?.getScrollableNode(); if (!(scrollElement instanceof HTMLElement)) { return; } const maxScrollOffset = Math.max(0, scrollElement.scrollHeight - scrollElement.clientHeight); setShowTopScrollFade(scrollElement.scrollTop > 1); setShowBottomScrollFade(maxScrollOffset - scrollElement.scrollTop > 1); }, []); const modelJumpShortcutContext = useMemo( () => ({ terminalFocus: false, terminalOpen: props.terminalOpen, modelPickerOpen: true, }) as const, [props.terminalOpen], ); const modelJumpLabelByKey = useMemo((): ReadonlyMap => { if (modelJumpCommandByKey.size === 0) { return EMPTY_MODEL_JUMP_LABELS; } const shortcutLabelOptions = { platform: navigator.platform, context: modelJumpShortcutContext, }; const mapping = new Map(); for (const [modelKey, command] of modelJumpCommandByKey) { const label = shortcutLabelForCommand(keybindings, command, shortcutLabelOptions); if (label) { mapping.set(modelKey, label); } } return mapping.size > 0 ? mapping : EMPTY_MODEL_JUMP_LABELS; }, [keybindings, modelJumpCommandByKey, modelJumpShortcutContext]); useEffect(() => { const onWindowKeyDown = (event: globalThis.KeyboardEvent) => { if (event.defaultPrevented || event.repeat) { return; } const command = resolveShortcutCommand(event, keybindings, { platform: navigator.platform, context: modelJumpShortcutContext, }); const jumpIndex = modelPickerJumpIndexFromCommand(command ?? ""); if (jumpIndex === null) { return; } const targetModelKey = modelJumpModelKeys[jumpIndex]; if (!targetModelKey) { return; } const { instanceId, slug } = splitInstanceModelKey(targetModelKey); event.preventDefault(); event.stopPropagation(); handleModelSelect(slug, instanceId); }; window.addEventListener("keydown", onWindowKeyDown, true); return () => { window.removeEventListener("keydown", onWindowKeyDown, true); }; }, [handleModelSelect, keybindings, modelJumpModelKeys, modelJumpShortcutContext]); useLayoutEffect(() => { setShowTopScrollFade(false); setShowBottomScrollFade(filteredModelKeys.length > 5); let nestedFrame = 0; const frame = window.requestAnimationFrame(() => { updateModelListScrollFades(); nestedFrame = window.requestAnimationFrame(updateModelListScrollFades); }); return () => { window.cancelAnimationFrame(frame); window.cancelAnimationFrame(nestedFrame); }; }, [filteredModelKeys, updateModelListScrollFades]); return (
{/* Sidebar */} {showSidebar && ( `${entry.displayName} is unavailable in this thread. Start a new thread to switch providers.`, } : {})} /> )} {/* Main content area */} { highlightedModelKeyRef.current = typeof modelKey === "string" ? modelKey : null; if (eventDetails.reason === "keyboard" && eventDetails.index >= 0) { void modelListRef.current?.scrollIndexIntoView?.({ index: eventDetails.index, animated: false, }); } }} onValueChange={(modelKey) => { if (typeof modelKey !== "string") { return; } const { instanceId, slug } = splitInstanceModelKey(modelKey); handleModelSelect(slug, instanceId); }} >
{/* Search bar */}
} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); props.onRequestClose?.(); return; } if (e.key === "Enter" && highlightedModelKeyRef.current) { ( e as typeof e & { preventBaseUIHandler?: () => void } ).preventBaseUIHandler?.(); e.preventDefault(); e.stopPropagation(); const { instanceId, slug } = splitInstanceModelKey( highlightedModelKeyRef.current, ); handleModelSelect(slug, instanceId); return; } e.stopPropagation(); }} onMouseDown={(e) => e.stopPropagation()} onTouchStart={(e) => e.stopPropagation()} size="sm" unstyled />
{/* Model list */}
ref={modelListRef} data={filteredModelKeys} extraData={favoritesSet} keyExtractor={(modelKey) => modelKey} renderItem={({ item: modelKey, index }) => { const model = filteredModelByKey.get(modelKey); if (!model) { return null; } const disabledReason = getModelDisabledReason?.(model.instanceId, model.slug) ?? null; return ( toggleFavorite(model.instanceId, model.slug)} /> ); }} estimatedItemSize={60} drawDistance={480} recycleItems onLayout={updateModelListScrollFades} onScroll={updateModelListScrollFades} className={cn( "scrollbar-gutter-both h-full overflow-x-hidden overscroll-y-contain py-1.5 [--fade-size:1.5rem]", showTopScrollFade && "mask-t-from-[calc(100%-var(--fade-size))]", showBottomScrollFade && "mask-b-from-[calc(100%-var(--fade-size))]", )} />
No models found
); });