import { createContext, useCallback, useContext, useMemo, useReducer, useState } from "react"; import { useTranslation } from "react-i18next"; import { FlatList, Platform, Pressable, ScrollView, Text, View, type AccessibilityActionEvent, type GestureResponderEvent, type PressableStateCallbackType, type StyleProp, type ViewStyle, } from "react-native"; import { BottomSheetFlatList } from "@gorhom/bottom-sheet"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { AlertTriangle, Check, ChevronRight, Pencil, Plus, Search, Settings, } from "lucide-react-native"; import type { AgentProvider } from "@getpaseo/protocol/agent-types"; import { AgentProfileGlyph, type AgentProfilePicker, type AgentProfilePickerRow as AgentProfilePickerRowModel, } from "@/agent-profiles"; import type { SheetHeader } from "@/components/adaptive-modal-sheet"; import { Button } from "@/components/ui/button"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { getProviderIcon } from "@/components/provider-icons"; import { useIsCompactFormFactor } from "@/constants/layout"; import { isNative, isWeb } from "@/constants/platform"; import { buildProviderQualifiedDescription, buildSelectedTriggerLabel, filterAndRankModelRows, getAllProviderModelRows, getProviderModelRows, resolveSelectedModelLabel, type ProviderSelectionModelRow, type ProviderSelectorProvider, } from "@/provider-selection/provider-selection"; import { useProviderSettingsStore } from "@/stores/provider-settings-store"; import { useCurrentOverlayLayer } from "@/lib/overlay-root"; import { ICON_SIZE, type Theme } from "@/styles/theme"; import { resolveInitialModelBrowserView, resolveModelBrowserAllView, type ModelBrowserView, } from "@/components/model-browser-view"; const DESKTOP_PROVIDER_VIEW_MIN_HEIGHT = 220; const DESKTOP_PROVIDER_VIEW_MAX_HEIGHT = 400; const DESKTOP_PROVIDER_VIEW_BASE_HEIGHT = 80; const DESKTOP_MODEL_ROW_HEIGHT = 40; const ThemedAlertTriangle = withUnistyles(AlertTriangle); const ThemedCheck = withUnistyles(Check); const ThemedChevronRight = withUnistyles(ChevronRight); const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); const ThemedPencil = withUnistyles(Pencil); const ThemedPlus = withUnistyles(Plus); const ThemedSearch = withUnistyles(Search); const ThemedSettings = withUnistyles(Settings); function ProviderSettingsAction({ accessibilityLabel, provider, serverId, }: { accessibilityLabel: string; provider: string; serverId: string | null; }) { const overlayParentLayer = useCurrentOverlayLayer(); const handlePress = useCallback(() => { if (!serverId) return; useProviderSettingsStore.getState().open({ serverId, provider, overlayParentLayer }); }, [overlayParentLayer, provider, serverId]); return ( ); } function AgentProfilesEditAction({ onPress }: { onPress: () => void }) { const { t } = useTranslation(); return ( {t("modelSelector.editProfilesLabel")} ); } const IndependentScrollGestureContext = createContext | null>( null, ); const foregroundMutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted, }); const foregroundExtraMutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundExtraMuted, }); const headerSettingsMapping = (disabled: boolean) => (theme: Theme) => ({ color: disabled ? theme.colors.border : theme.colors.foregroundMuted, }); interface ModelBrowserInput { providers: ProviderSelectorProvider[]; selectedProvider: string; selectedModel: string; isLoading: boolean; autoFocusSearch?: boolean; /** Pinned above the provider list on the root view. `null` hides the section. */ profiles?: AgentProfilePicker | null; serverId?: string | null; } export interface ModelBrowserState { providers: ProviderSelectorProvider[]; selectedProvider: string; selectedModel: string; profiles: AgentProfilePicker | null; view: ModelBrowserView; searchQuery: string; isSearchFocused: boolean; header: SheetHeader; selectedModelLabel: string; triggerLabel: string; desktopFixedHeight: number | undefined; isProviderView: boolean; prepareToOpen: () => void; showAll: () => void; reset: () => void; drillDown: (providerId: string, providerLabel: string) => void; } interface ModelBrowserProps { state: ModelBrowserState; onSelect: (provider: string, modelId: string) => void; /** Applying a profile resolves the pick and dismisses, exactly like a model row. */ onApplyProfile?: (profileId: string) => void; onEditProfiles?: () => void; onRetryProvider?: (provider: AgentProvider) => void; isRetryingProvider?: boolean; scrolling?: "sheet" | "independent"; /** Empty focused search shows all model rows instead of the browse root. */ searchAllOnFocus?: boolean; /** Replaces provider rows only while the all-provider search is empty. */ rootBrowseContent?: React.ReactNode; } interface ModelBrowserContentProps extends Omit { view: ModelBrowserView; providers: ProviderSelectorProvider[]; selectedProvider: string; selectedModel: string; searchQuery: string; isSearchFocused: boolean; profiles: AgentProfilePicker | null; onDrillDown: (providerId: string, providerLabel: string) => void; scrolling: "sheet" | "independent"; searchAllOnFocus: boolean; rootBrowseContent?: React.ReactNode; } type ProviderGlyphTone = "muted" | "foreground"; export function ModelProviderGlyph({ provider, size, tone = "muted", }: { provider: string; size: number; tone?: ProviderGlyphTone; }) { const Icon = getProviderIcon(provider); const color = tone === "foreground" ? styles.providerIconForeground.color : styles.providerIconMuted.color; return ; } function HeaderSettingsIcon({ disabled }: { disabled: boolean }) { const uniProps = useMemo(() => headerSettingsMapping(disabled), [disabled]); return ; } function iconButtonStyle({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) { return [ styles.rowIconButton, Boolean(hovered) && styles.rowIconButtonHovered, pressed && styles.rowIconButtonPressed, ]; } function countModelsInView(view: ModelBrowserView, providers: ProviderSelectorProvider[]): number { if (view.kind === "all") { return getAllProviderModelRows(providers).length; } const provider = providers.find((entry) => entry.id === view.providerId); return provider?.modelSelection.kind === "models" ? getProviderModelRows(provider).length : 0; } function resolveDesktopFixedHeight( view: ModelBrowserView, providers: ProviderSelectorProvider[], ): number { const modelCount = countModelsInView(view, providers); return Math.min( Math.max( DESKTOP_PROVIDER_VIEW_MIN_HEIGHT, DESKTOP_PROVIDER_VIEW_BASE_HEIGHT + modelCount * DESKTOP_MODEL_ROW_HEIGHT, ), DESKTOP_PROVIDER_VIEW_MAX_HEIGHT, ); } export function useModelBrowser({ providers, selectedProvider, selectedModel, isLoading, autoFocusSearch = isWeb, profiles = null, serverId = null, }: ModelBrowserInput): ModelBrowserState { const { t } = useTranslation(); const [view, setView] = useState({ kind: "all" }); const [searchQuery, setSearchQuery] = useState(""); const [isSearchFocused, setIsSearchFocused] = useState(false); const [searchResetKey, bumpSearchResetKey] = useReducer((key: number) => key + 1, 0); const hasProfiles = (profiles?.rows.length ?? 0) > 0; const initialView = useMemo( () => resolveInitialModelBrowserView({ providers, selectedProvider, selectedModel, hasProfiles, }), [hasProfiles, providers, selectedModel, selectedProvider], ); const prepareToOpen = useCallback(() => { setView(initialView); }, [initialView]); const reset = useCallback(() => { setSearchQuery(""); setIsSearchFocused(false); bumpSearchResetKey(); }, []); const showAll = useCallback(() => { setView({ kind: "all" }); reset(); }, [reset]); const drillDown = useCallback( (providerId: string, providerLabel: string) => { setView({ kind: "provider", providerId, providerLabel }); reset(); }, [reset], ); const handleSearchQueryChange = useCallback((value: string) => { setSearchQuery(value); }, []); const singleProviderView = providers.length === 1; const header = useMemo(() => { if (view.kind === "all") { return { title: t("modelSelector.title"), search: { onChange: handleSearchQueryChange, onFocus: () => setIsSearchFocused(true), onBlur: () => setIsSearchFocused(false), resetKey: `all:${searchResetKey}`, placeholder: t("modelSelector.searchAllPlaceholder"), autoFocus: autoFocusSearch, testID: "model-search-all-input", }, }; } return { title: view.providerLabel, leading: ( ), back: singleProviderView ? undefined : { onPress: showAll }, actions: ( ), search: { onChange: handleSearchQueryChange, onFocus: () => setIsSearchFocused(true), onBlur: () => setIsSearchFocused(false), resetKey: `${view.providerId}:${searchResetKey}`, placeholder: t("modelSelector.searchPlaceholder"), autoFocus: autoFocusSearch, testID: "model-search-input", }, }; }, [ autoFocusSearch, handleSearchQueryChange, searchResetKey, serverId, singleProviderView, showAll, t, view, ]); const selectedModelLabel = useMemo( () => resolveSelectedModelLabel({ providers, selectedProvider, selectedModel, isLoading, }), [isLoading, providers, selectedModel, selectedProvider], ); const triggerLabel = useMemo(() => { const isPlaceholder = selectedModelLabel === t("modelSelector.loading") || selectedModelLabel === t("modelSelector.selectModel"); return isPlaceholder ? selectedModelLabel : buildSelectedTriggerLabel(selectedModelLabel); }, [selectedModelLabel, t]); const desktopFixedHeight = useMemo( () => resolveDesktopFixedHeight(view, providers), [providers, view], ); return { providers, selectedProvider, selectedModel, profiles, view, searchQuery, isSearchFocused, header, selectedModelLabel, triggerLabel, desktopFixedHeight, isProviderView: view.kind === "provider", prepareToOpen, showAll, reset, drillDown, }; } function normalizeSearchQuery(value: string): string { return value.trim().toLowerCase(); } interface ModelBrowserPressableProps { children: React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode); style?: | StyleProp | ((state: PressableStateCallbackType & { hovered?: boolean }) => StyleProp); onPress: () => void; hitSlop?: number; accessibilityLabel?: string; /** Only rows that can express selection pass this; the rest stay unannotated. */ accessibilitySelected?: boolean; testID?: string; } function ModelBrowserPressable({ children, style, onPress, hitSlop, accessibilityLabel, accessibilitySelected, testID, }: ModelBrowserPressableProps) { const independentScrollGesture = useContext(IndependentScrollGestureContext); const [pressed, setPressed] = useState(false); // Android's scroll handler must keep the pointer stream until release so a // fling survives leaving the short viewport. A simultaneous Tap keeps rows // interactive, while maxDistance makes a real scroll fail instead of select. const tapGesture = useMemo(() => { const gesture = Gesture.Tap() .maxDistance(8) .shouldCancelWhenOutside(true) .runOnJS(true) .onBegin(() => setPressed(true)) .onEnd((_event, success) => { if (success) onPress(); }) .onFinalize(() => setPressed(false)); if (hitSlop !== undefined) gesture.hitSlop(hitSlop); if (independentScrollGesture) { gesture.simultaneousWithExternalGesture(independentScrollGesture); } return gesture; }, [hitSlop, independentScrollGesture, onPress]); const handlePress = useCallback( (event: GestureResponderEvent) => { event.stopPropagation(); onPress(); }, [onPress], ); const handleAccessibilityAction = useCallback( (event: AccessibilityActionEvent) => { if (event.nativeEvent.actionName === "activate") onPress(); }, [onPress], ); const accessibilityState = useMemo( () => (accessibilitySelected === undefined ? undefined : { selected: accessibilitySelected }), [accessibilitySelected], ); if (!independentScrollGesture) { return ( {children} ); } const state = { pressed }; const resolvedStyle = typeof style === "function" ? style(state) : style; const resolvedChildren = typeof children === "function" ? children(state) : children; return ( {resolvedChildren} ); } type ModelBrowserRowTone = "default" | "elevated" | "drillDown"; function ModelBrowserRow({ label, description, leadingSlot, trailingSlot, selected = false, selectionIndicator = false, tone = "default", labelMuted = false, spacing = "model", onPress, testID, }: { label: string; description?: string; leadingSlot: React.ReactNode; trailingSlot?: React.ReactNode; selected?: boolean; selectionIndicator?: boolean; tone?: ModelBrowserRowTone; /** For rows that offer an action rather than name a thing you can pick. */ labelMuted?: boolean; spacing?: "model" | "provider"; onPress: () => void; testID?: string; }) { const pressableStyle = useCallback( ({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.browserRow, spacing === "model" && styles.browserModelRow, Boolean(hovered) && (tone === "elevated" ? styles.browserRowHoveredElevated : styles.browserRowHovered), pressed && (tone === "default" ? styles.browserRowPressed : styles.browserRowPressedElevated), ], [spacing, tone], ); const contentStyle = useMemo( () => [styles.browserRowText, description && styles.browserRowTextInline], [description], ); const hasTrailing = selected || trailingSlot; return ( {leadingSlot} {label} {description ? ( {description} ) : null} {hasTrailing ? ( {selectionIndicator ? ( {selected ? ( ) : null} ) : null} {trailingSlot} ) : null} ); } function ModelRow({ row, isSelected, showProviderLabel = false, onPress, }: { row: ProviderSelectionModelRow; isSelected: boolean; showProviderLabel?: boolean; onPress: () => void; }) { const leadingSlot = useMemo( () => , [row.provider], ); const description = showProviderLabel ? buildProviderQualifiedDescription(row) : row.description; return ( ); } function SelectableModelRow({ row, isSelected, showProviderLabel, onSelect, }: { row: ProviderSelectionModelRow; isSelected: boolean; showProviderLabel?: boolean; onSelect: (provider: string, modelId: string) => void; }) { const handlePress = useCallback(() => { onSelect(row.provider, row.modelId); }, [onSelect, row.modelId, row.provider]); return ( ); } function AgentProfilePickerRowView({ row, onApply, }: { row: AgentProfilePickerRowModel; onApply: (profileId: string) => void; }) { const handlePress = useCallback(() => onApply(row.id), [onApply, row.id]); const leadingSlot = useMemo( () => , [row.color, row.icon], ); return ( ); } /** * Pinned above the provider list. Rows are actions, not selections: applying a * profile writes its values into the composer and nothing stays bound to it, so * there is no checkmark and no active row to show. */ function AgentProfilesPickerSection({ rows, onApplyProfile, onEditProfiles, }: { rows: AgentProfilePickerRowModel[]; onApplyProfile?: (profileId: string) => void; onEditProfiles?: () => void; }) { const { t } = useTranslation(); const handleApply = useCallback( (profileId: string) => onApplyProfile?.(profileId), [onApplyProfile], ); return ( {t("modelSelector.profiles")} {onEditProfiles ? ( ) : null} {rows.map((row) => ( ))} ); } function CreateAgentProfileRow({ onPress }: { onPress: () => void }) { const { t } = useTranslation(); const leadingSlot = useMemo( () => ( ), [], ); return ( ); } function AgentProfilesPickerContent({ rows, onApplyProfile, onEditProfiles, }: { rows: AgentProfilePickerRowModel[]; onApplyProfile?: (profileId: string) => void; onEditProfiles?: () => void; }) { if (rows.length === 0) { return onEditProfiles ? : null; } return ( ); } function GroupProviderButton({ provider, onDrillDown, }: { provider: ProviderSelectorProvider; onDrillDown: (providerId: string, providerLabel: string) => void; }) { const { t } = useTranslation(); const selection = provider.modelSelection; const handlePress = useCallback(() => { onDrillDown(provider.id, provider.label); }, [onDrillDown, provider.id, provider.label]); const stateNode = useMemo(() => { if (selection.kind === "models") { const count = selection.rows.length; return ( {t(count === 1 ? "modelSelector.modelCount" : "modelSelector.modelCountPlural", { count, })} ); } if (selection.kind === "loading") { return ( {t("modelSelector.loadingShort")} ); } return ( {t("modelSelector.error")} ); }, [selection, t]); const leadingSlot = useMemo( () => , [provider.id], ); const trailingSlot = useMemo( () => ( {stateNode} ), [stateNode], ); return ( ); } function GroupedProviderRows({ providers, onDrillDown, }: { providers: ProviderSelectorProvider[]; onDrillDown: (providerId: string, providerLabel: string) => void; }) { return ( {providers.map((provider, index) => ( {index > 0 ? : null} ))} ); } function IndependentScrollBoundary({ children }: { children: React.ReactElement }) { // Prevent the parent sheet from cancelling Android's native scroll when the // finger crosses this viewport; receiving ACTION_UP is what preserves fling. const nativeScrollGesture = useMemo( () => Gesture.Native() .shouldActivateOnStart(true) .shouldCancelWhenOutside(false) .disallowInterruption(true), [], ); if (Platform.OS !== "android") { return children; } return ( {children} ); } function IndependentModelList({ rows, renderItem, header, }: { rows: ProviderSelectionModelRow[]; renderItem: ({ item }: { item: ProviderSelectionModelRow }) => React.ReactElement; header?: React.ReactElement; }) { return ( ); } function getModelRowKey(row: ProviderSelectionModelRow): string { return row.favoriteKey; } function IndependentProviderList({ children }: { children: React.ReactNode }) { return ( {children} ); } function ModelRowList({ rows, selectedProvider, selectedModel, onSelect, showProviderLabel = false, header, scrolling, }: { rows: ProviderSelectionModelRow[]; selectedProvider: string; selectedModel: string; onSelect: (provider: string, modelId: string) => void; showProviderLabel?: boolean; header?: React.ReactElement; scrolling: "sheet" | "independent"; }) { const isCompact = useIsCompactFormFactor(); const renderItem = useCallback( ({ item }: { item: ProviderSelectionModelRow }) => ( ), [onSelect, selectedModel, selectedProvider, showProviderLabel], ); const keyExtractor = useCallback((row: ProviderSelectionModelRow) => row.favoriteKey, []); if (scrolling === "independent") { return ; } if (isCompact && isNative) { return ( ); } return ( {header} {rows.map((row) => ( {renderItem({ item: row })} ))} ); } function ProviderErrorEmptyState({ providerId, message, onRetryProvider, isRetryingProvider, }: { providerId: string; message: string; onRetryProvider?: (provider: AgentProvider) => void; isRetryingProvider: boolean; }) { const { t } = useTranslation(); const handleRetry = useCallback(() => { onRetryProvider?.(providerId); }, [onRetryProvider, providerId]); return ( {message} {onRetryProvider ? ( ) : null} ); } function ModelSearchEmptyState() { const { t } = useTranslation(); return ( {t("modelSelector.noMatches")} ); } function ProviderModelBrowserContent({ view, provider, profiles, selectedProvider, selectedModel, normalizedQuery, onSelect, onApplyProfile, onEditProfiles, onRetryProvider, isRetryingProvider, scrolling, }: { view: Extract; provider: ProviderSelectorProvider | null; profiles: AgentProfilePicker | null; selectedProvider: string; selectedModel: string; normalizedQuery: string; onSelect: (provider: string, modelId: string) => void; onApplyProfile?: (profileId: string) => void; onEditProfiles?: () => void; onRetryProvider?: (provider: AgentProvider) => void; isRetryingProvider: boolean; scrolling: "sheet" | "independent"; }) { const { t } = useTranslation(); const visibleRows = useMemo( () => (provider ? filterAndRankModelRows(getProviderModelRows(provider), normalizedQuery) : []), [normalizedQuery, provider], ); const providerProfileRows = useMemo( () => profiles?.rows.filter((row) => row.provider === view.providerId) ?? [], [profiles, view.providerId], ); const profileHeader = useMemo( () => normalizedQuery.length === 0 && profiles ? ( ) : undefined, [normalizedQuery, onApplyProfile, onEditProfiles, profiles, providerProfileRows], ); if (!provider) return ; const selection = provider.modelSelection; if (selection.kind === "loading") { return ( {t("modelSelector.loadingShort")} ); } if (selection.kind === "error") { return ( ); } if (visibleRows.length === 0) { return profileHeader ?? ; } return ( ); } function ModelBrowserContent({ view, providers, selectedProvider, selectedModel, searchQuery, isSearchFocused, profiles, onSelect, onApplyProfile, onEditProfiles, onDrillDown, onRetryProvider, isRetryingProvider = false, scrolling, searchAllOnFocus, rootBrowseContent, }: ModelBrowserContentProps) { const { t } = useTranslation(); const normalizedQuery = useMemo(() => normalizeSearchQuery(searchQuery), [searchQuery]); const selectedViewProvider = useMemo( () => view.kind === "provider" ? (providers.find((provider) => provider.id === view.providerId) ?? null) : null, [providers, view], ); const allView = useMemo( () => resolveModelBrowserAllView({ providers, normalizedQuery, isSearchFocused: searchAllOnFocus && isSearchFocused, }), [isSearchFocused, normalizedQuery, providers, searchAllOnFocus], ); const hasResults = profiles !== null || providers.length > 0 || rootBrowseContent != null; if (view.kind === "provider") { return ( ); } if (allView.kind === "noSearchMatches") { return ( {t("modelSelector.noMatchesForQuery", { query: searchQuery.trim() })} ); } if (allView.kind === "searchResults") { return ( ); } const allProvidersContent = ( {profiles ? ( ) : null} {rootBrowseContent ?? (providers.length > 0 ? ( {profiles ? ( {t("modelSelector.providers")} ) : null} ) : null)} {!hasResults ? : null} ); return scrolling === "independent" ? ( {allProvidersContent} ) : ( allProvidersContent ); } export function ModelBrowser({ state, onSelect, onApplyProfile, onEditProfiles, onRetryProvider, isRetryingProvider = false, scrolling = "sheet", searchAllOnFocus = false, rootBrowseContent, }: ModelBrowserProps) { return ( ); } const styles = StyleSheet.create((theme) => ({ headerActionRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], }, profilesContainer: { backgroundColor: theme.colors.surface1, borderBottomWidth: 1, borderBottomColor: theme.colors.border, }, separator: { height: 1, backgroundColor: theme.colors.border, }, sectionHeading: { position: "relative", flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: isWeb ? theme.spacing[3] : theme.spacing[6], paddingTop: theme.spacing[2], paddingBottom: theme.spacing[1], }, sectionHeadingAction: { position: "absolute", top: theme.spacing[1], right: isWeb ? theme.spacing[3] : theme.spacing[6], }, sectionHeadingText: { flex: 1, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, color: theme.colors.foregroundMuted, }, browserRow: { flexDirection: "row", paddingVertical: theme.spacing[2], minHeight: 36, }, browserModelRow: isWeb ? {} : { marginBottom: theme.spacing[1] }, browserRowHovered: { backgroundColor: theme.colors.surface1, }, browserRowHoveredElevated: { backgroundColor: theme.colors.surface2, }, browserRowPressed: { backgroundColor: theme.colors.surface1, }, browserRowPressedElevated: { backgroundColor: theme.colors.surface2, }, browserRowContent: { flex: 1, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: isWeb ? theme.spacing[3] : theme.spacing[6], }, browserRowLeading: { width: 16, alignItems: "center", justifyContent: "center", }, browserRowText: { flex: 1, flexShrink: 1, }, browserRowTextInline: { flexDirection: "row", alignItems: "baseline", gap: theme.spacing[2], }, browserRowLabel: { fontSize: theme.fontSize.base, color: theme.colors.foreground, flexShrink: 0, }, browserRowLabelMuted: { fontSize: theme.fontSize.base, color: theme.colors.foregroundMuted, flexShrink: 0, }, browserRowDescription: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, flexShrink: 1, }, browserRowTrailing: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], marginLeft: "auto", }, browserRowSelection: { width: 16, alignItems: "center", justifyContent: "center", }, drillDownTrailing: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], }, drillDownCount: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, }, rowStateInline: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], flexShrink: 1, minWidth: 0, }, rowIconButton: { width: 24, height: 24, borderRadius: theme.borderRadius.full, alignItems: "center", justifyContent: "center", }, rowSpinner: { transform: [{ scale: 0.7 }], }, rowIconButtonHovered: { backgroundColor: theme.colors.surface2, }, rowIconButtonPressed: { backgroundColor: theme.colors.surface1, }, emptyState: { paddingVertical: theme.spacing[4], alignItems: "center", gap: theme.spacing[2], }, emptyStateText: { fontSize: theme.fontSize.base, color: theme.colors.foregroundMuted, }, tooltipText: { fontSize: theme.fontSize.sm, color: theme.colors.foreground, }, virtualizedModelList: { flex: 1, }, virtualizedModelListContent: { paddingTop: theme.spacing[1], paddingBottom: theme.spacing[8], }, virtualizedProviderListContent: { paddingTop: 0, }, providerIconMuted: { color: theme.colors.foregroundMuted, }, providerIconForeground: { color: theme.colors.foreground, }, }));