import { useCallback, useMemo, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Keyboard, ScrollView, Text, View, type PressableStateCallbackType } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { Bot } from "lucide-react-native";
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
import type { AgentProfilePicker } from "@/agent-profiles";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import { getProviderIcon } from "@/components/provider-icons";
import { ModelBrowser, useModelBrowser } from "@/components/model-browser";
import { AgentControlTrigger } from "@/composer/agent-controls/control";
import { ComposerToolbarGlyph } from "@/composer/agent-controls/glyph";
import {
resolveModelBrowserScrolling,
resolveModelSheetOpening,
} from "@/composer/agent-controls/model-sheet-flow";
import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection";
import { useIsCompactFormFactor } from "@/constants/layout";
import { isWeb } from "@/constants/platform";
const SNAP_POINTS = ["80%", "90%"];
const MODEL_LIST_TOP_INSET = 4;
const MODEL_ROW_STRIDE = 44;
const MODEL_VIEWPORT_VISIBLE_ROWS = 4.5;
const FIXED_MODEL_VIEWPORT_HEIGHT =
MODEL_LIST_TOP_INSET + MODEL_ROW_STRIDE * MODEL_VIEWPORT_VISIBLE_ROWS;
interface CompactModelSheetProps {
providers: ProviderSelectorProvider[];
selectedProvider: string;
selectedModel: string;
thinkingLabel: string | null;
onSelect: (provider: string, modelId: string) => void;
isLoading: boolean;
profiles?: AgentProfilePicker | null;
onApplyProfile?: (profileId: string) => void;
onEditProfiles?: () => void;
onOpen?: () => void;
onClose?: () => void;
onRetryProvider?: (provider: AgentProvider) => void;
isRetryingProvider?: boolean;
disabled?: boolean;
serverId?: string | null;
glyphSize: number;
canSwitchProvider: boolean;
children: ReactNode;
}
function shortModelLabel(label: string): string {
const separatorIndex = label.lastIndexOf("/");
return separatorIndex === -1 ? label : label.slice(separatorIndex + 1);
}
export function CompactModelSheet({
providers,
selectedProvider,
selectedModel,
thinkingLabel,
onSelect,
isLoading,
profiles = null,
onApplyProfile,
onEditProfiles,
onOpen,
onClose,
onRetryProvider,
isRetryingProvider = false,
disabled = false,
serverId = null,
glyphSize,
canSwitchProvider,
children,
}: CompactModelSheetProps) {
const { t } = useTranslation();
const usesBottomSheet = useIsCompactFormFactor();
const modelBrowserScrolling = resolveModelBrowserScrolling(usesBottomSheet);
const [isOpen, setIsOpen] = useState(false);
const [isModelBrowserOpen, setIsModelBrowserOpen] = useState(false);
const availableProviders = useMemo(() => {
if (canSwitchProvider) return providers;
const fixedProvider =
providers.find((entry) => entry.id === selectedProvider) ?? providers[0] ?? null;
return fixedProvider ? [fixedProvider] : [];
}, [canSwitchProvider, providers, selectedProvider]);
const rootBrowser = useModelBrowser({
providers: availableProviders,
selectedProvider,
selectedModel,
isLoading,
autoFocusSearch: isWeb && !usesBottomSheet,
profiles,
serverId,
});
const modelBrowser = useModelBrowser({
providers: availableProviders,
selectedProvider,
selectedModel,
isLoading,
autoFocusSearch: isWeb && !usesBottomSheet,
serverId,
});
const ProviderIcon =
selectedProvider.trim().length > 0 ? getProviderIcon(selectedProvider) : null;
const ModelIcon = ProviderIcon ?? Bot;
const rootHeader = useMemo(
() => ({
...rootBrowser.header,
title: t("modelSelector.selectModel"),
search:
rootBrowser.header.search && !canSwitchProvider
? {
...rootBrowser.header.search,
placeholder: t("modelSelector.searchPlaceholder"),
}
: rootBrowser.header.search,
}),
[canSwitchProvider, rootBrowser.header, t],
);
const open = useCallback(() => {
Keyboard.dismiss();
rootBrowser.showAll();
setIsOpen(true);
onOpen?.();
}, [onOpen, rootBrowser]);
const close = useCallback(() => {
setIsModelBrowserOpen(false);
setIsOpen(false);
rootBrowser.reset();
modelBrowser.reset();
onClose?.();
}, [modelBrowser, onClose, rootBrowser]);
const handleSearchSelect = useCallback(
(provider: string, modelId: string) => {
onSelect(provider, modelId);
Keyboard.dismiss();
rootBrowser.showAll();
},
[onSelect, rootBrowser],
);
const openModelBrowser = useCallback(() => {
Keyboard.dismiss();
const destination = resolveModelSheetOpening({
canSwitchProvider,
providers: availableProviders,
selectedProvider,
});
if (destination.kind === "all") {
modelBrowser.showAll();
} else {
modelBrowser.drillDown(destination.providerId, destination.providerLabel);
}
setIsModelBrowserOpen(true);
}, [availableProviders, canSwitchProvider, modelBrowser, selectedProvider]);
const closeModelBrowser = useCallback(() => {
setIsModelBrowserOpen(false);
modelBrowser.reset();
}, [modelBrowser]);
const handleBrowserSelect = useCallback(
(provider: string, modelId: string) => {
onSelect(provider, modelId);
closeModelBrowser();
},
[closeModelBrowser, onSelect],
);
const handleDesktopSelect = useCallback(
(provider: string, modelId: string) => {
onSelect(provider, modelId);
close();
},
[close, onSelect],
);
const handleApplyProfile = useCallback(
(profileId: string) => {
onApplyProfile?.(profileId);
close();
},
[close, onApplyProfile],
);
const handleEditProfiles = useCallback(() => {
close();
onEditProfiles?.();
}, [close, onEditProfiles]);
const toggle = useCallback(() => {
if (isOpen) {
close();
return;
}
open();
}, [close, isOpen, open]);
const triggerStyle = useCallback(
({ hovered, pressed }: PressableStateCallbackType) => [
styles.trigger,
hovered && styles.triggerHovered,
(pressed || isOpen) && styles.triggerPressed,
disabled && styles.triggerDisabled,
],
[disabled, isOpen],
);
const mobileRootContent = useMemo(
() => (
{children}
),
[ModelIcon, children, disabled, openModelBrowser, rootBrowser.selectedModelLabel, t],
);
return (
<>
{ProviderIcon ? (
) : null}
{shortModelLabel(rootBrowser.triggerLabel)}
{thinkingLabel ? (
{thinkingLabel}
) : null}
{!usesBottomSheet ? (
<>
{children}
>
) : null}
{usesBottomSheet ? (
) : null}
>
);
}
const styles = StyleSheet.create((theme) => ({
trigger: {
height: 28,
minWidth: 0,
flexShrink: 1,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius["2xl"],
backgroundColor: "transparent",
},
triggerHovered: {
backgroundColor: theme.colors.surface2,
},
triggerPressed: {
backgroundColor: theme.colors.surface0,
},
triggerDisabled: {
opacity: 0.5,
},
triggerText: {
minWidth: 0,
flexShrink: 1,
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal,
},
triggerLabels: {
minWidth: 0,
flexShrink: 1,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
},
triggerThinking: {
flexShrink: 0,
color: theme.colors.foregroundExtraMuted,
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal,
},
providerIcon: {
color: theme.colors.foregroundMuted,
},
sheetBody: {
paddingHorizontal: 0,
paddingTop: 0,
paddingBottom: 0,
gap: 0,
},
modelViewport: {
overflow: "hidden",
backgroundColor: theme.colors.surfaceSidebar,
},
flexibleModelViewport: {
flex: 1,
minHeight: 0,
},
fixedModelViewport: {
height: FIXED_MODEL_VIEWPORT_HEIGHT,
minHeight: FIXED_MODEL_VIEWPORT_HEIGHT,
},
modelViewportDivider: {
height: 1,
flexShrink: 0,
backgroundColor: theme.colors.border,
},
controlsScroll: {
flex: 1,
minHeight: 0,
},
mobileRootContent: {
backgroundColor: theme.colors.surfaceSidebar,
borderTopWidth: 1,
borderTopColor: theme.colors.border,
},
controlsContent: {
paddingHorizontal: theme.spacing[2],
paddingTop: theme.spacing[3],
paddingBottom: theme.spacing[3],
gap: theme.spacing[1],
},
}));