import type { EnvironmentId, ModelSelection, ProviderInstanceId, ProviderOptionDescriptor, ProviderOptionSelection, RuntimeMode, } from "@t3tools/contracts"; import type { LegendListRenderItemProps } from "@legendapp/list/react-native"; import { AnimatedLegendList } from "@legendapp/list/reanimated"; import { HeaderHeightContext } from "@react-navigation/elements"; import { getProviderOptionCurrentLabel, getProviderOptionCurrentValue, getProviderOptionDescriptors, } from "@t3tools/shared/model"; import { useNavigation, useRoute, type RouteProp } from "@react-navigation/native"; import { createNativeStackNavigator, type NativeStackNavigationProp, } from "@react-navigation/native-stack"; import * as Haptics from "expo-haptics"; import { createContext, use, useCallback, useEffect, useMemo, useState, type ReactNode, } from "react"; import { Alert, Platform, Pressable, ScrollView, TextInput, View } from "react-native"; import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { SymbolView } from "../../components/AppSymbol"; import { AppText as Text } from "../../components/AppText"; import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; import { ProviderIcon } from "../../components/ProviderIcon"; import { ThemedSwitch } from "../../components/ThemedSwitch"; import { cn } from "../../lib/cn"; import type { ModelOption, ProviderGroup } from "../../lib/modelOptions"; import { applyProviderOptionSelection } from "../../lib/providerOptions"; import { resolveProviderOptionDescriptors } from "../../lib/providerOptions"; import { useUniwindTheme } from "../../lib/useUniwindTheme"; import { NativeHeaderToolbar, NativeStackScreenOptions, nativeHeaderScrollEdgeEffects, } from "../../native/StackHeader"; import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; import { serverEnvironment } from "../../state/server"; import { useAtomCommand } from "../../state/use-atom-command"; import { useNewTaskFlow } from "./new-task-flow-provider"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { createProviderCatalogRefreshRunner, providerCatalogRefreshError, } from "./provider-catalog-refresh"; import { createNativeMailSearchToolbarItem, NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET, NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, } from "../layout/native-mail-search-toolbar"; import { RUNTIME_MODE_CHOICES, selectableChoices } from "./thread-settings-options"; import { canCommitPendingModel, modelMatchesCatalogQuery, pendingModelAfterPress, providerSectionIsCollapsed, } from "./thread-settings-sheet-state"; /** * Everyday harnesses start expanded; every other provider (OpenRouter catalogs * and friends) starts folded so a 300-model catalog cannot bury the list. All * provider headers remain user-collapsible. */ const PRIMARY_PROVIDER_DRIVERS: ReadonlySet = new Set([ "claudeAgent", "codex", "antigravity", ]); /** * Keep measured row changes stable, but let catalog mutations use the list's * native bounds so a filtered catalog that underflows returns to the top. */ const THREAD_SETTINGS_MAINTAIN_VISIBLE_CONTENT_POSITION = { data: false, size: true, } as const; const THREAD_SETTINGS_CATALOG_LAYOUT_TRANSITION = LinearTransition.duration(180); const THREAD_SETTINGS_CATALOG_ENTER_TRANSITION = FadeIn.duration(140); const THREAD_SETTINGS_CATALOG_EXIT_TRANSITION = FadeOut.duration(120); const THREAD_SETTINGS_OPTIONS_LAYOUT_TRANSITION = LinearTransition.duration(180); const THREAD_SETTINGS_OPTION_ENTER_TRANSITION = FadeIn.duration(140); const THREAD_SETTINGS_OPTION_EXIT_TRANSITION = FadeOut.duration(100); const THREAD_SETTINGS_HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects( Platform.OS, Platform.Version, ); function ModelRow(props: { readonly option: ModelOption; readonly selected: boolean; readonly onPress: () => void; readonly isFirst: boolean; readonly isLast: boolean; }) { const { materialYouStyleLayoutActive } = useAppearancePreferences(); const selectedMaterialRow = materialYouStyleLayoutActive && props.selected; return ( {props.option.label} {props.option.isDefault ? ( Default ) : null} {props.option.isLegacy ? ( Legacy ) : null} {props.option.isUnavailable ? ( Unavailable ) : null} {props.option.subtitle ? ( {props.option.subtitle} ) : null} {props.selected ? ( ) : null} ); } /** Provider catalog header with its harness logo and disclosure state. */ function ProviderHeader(props: { readonly driver: string | undefined; readonly label: string; readonly collapsible: boolean; readonly collapsed: boolean; readonly modelCount: number; readonly onToggle: () => void; }) { const content = ( <> {props.label} {props.collapsible ? ( <> {props.collapsed ? ( {props.modelCount} ) : null} ) : null} ); if (props.collapsible) { return ( {content} ); } return ( {content} ); } /** Compact row that opens a single-choice submenu panel. */ function DisclosureRow(props: { readonly label: string; readonly value: string | undefined; readonly onPress: () => void; readonly isLast?: boolean; }) { return ( {props.label} {props.value ? ( {props.value} ) : null} ); } /** Single option inside a submenu panel. */ function ChoiceRow(props: { readonly label: string; readonly description?: string; readonly selected: boolean; readonly onPress: () => void; readonly isLast: boolean; }) { return ( {props.label} {props.description ? ( {props.description} ) : null} {props.selected ? ( ) : null} ); } function SwitchRow(props: { readonly label: string; readonly value: boolean; readonly onValueChange: (value: boolean) => void; readonly isLast?: boolean; }) { return ( {props.label} ); } type ThreadSettingsSubmenuPage = | { readonly kind: "descriptor"; readonly id: string } | { readonly kind: "runtime" }; type ThreadSettingsSessionProps = { readonly environmentId: EnvironmentId | null; readonly providerInstanceId?: ProviderInstanceId; readonly providerGroups: ReadonlyArray; readonly selectedModel: ModelSelection | null; readonly onSelectModel: (option: ModelOption) => void; readonly optionDescriptors: ReadonlyArray; readonly onUpdateOptionSelections: (selections: ReadonlyArray) => void; readonly runtimeMode: RuntimeMode; readonly onUpdateRuntimeMode: (mode: RuntimeMode) => void; }; export type ExistingThreadSettingsRouteSession = ThreadSettingsSessionProps & { readonly ownerId: string; }; type ExistingThreadSettingsRouteContextValue = { readonly session: ExistingThreadSettingsRouteSession | null; readonly present: (session: ExistingThreadSettingsRouteSession) => void; readonly clear: (ownerId: string) => void; }; const ExistingThreadSettingsRouteContext = createContext(null); /** Bridges the active thread's settings state into the root native sheet route. */ export function ExistingThreadSettingsRouteProvider(props: { readonly children: ReactNode }) { const [session, setSession] = useState(null); const present = useCallback((nextSession: ExistingThreadSettingsRouteSession) => { setSession(nextSession); }, []); const clear = useCallback((ownerId: string) => { setSession((current) => (current?.ownerId === ownerId ? null : current)); }, []); const value = useMemo(() => ({ session, present, clear }), [clear, present, session]); return ( {props.children} ); } export function useExistingThreadSettingsRoutePresentation() { const value = use(ExistingThreadSettingsRouteContext); if (!value) { throw new Error( "useExistingThreadSettingsRoutePresentation must be used inside ExistingThreadSettingsRouteProvider.", ); } return value; } type ThreadSettingsSessionValue = { readonly environmentId: EnvironmentId | null; readonly providerInstanceId?: ProviderInstanceId; readonly providerGroups: ReadonlyArray; readonly runtimeMode: RuntimeMode; readonly onUpdateRuntimeMode: (mode: RuntimeMode) => void; readonly displayedDescriptors: ReadonlyArray; readonly providerExpansionOverrides: ReadonlySet; readonly hasLegacyModels: boolean; readonly pendingModel: ModelOption | null; readonly providerFilter: string | null; readonly searchQuery: string; readonly showLegacy: boolean; readonly applyOptionChange: (id: string, value: string | boolean) => void; readonly commitPendingModel: () => boolean; readonly isApplied: (option: ModelOption) => boolean; readonly isDisplayed: (option: ModelOption) => boolean; readonly pressModel: (option: ModelOption) => void; readonly setProviderFilter: (providerKey: string | null) => void; readonly setSearchQuery: (query: string) => void; readonly setShowLegacy: (showLegacy: boolean) => void; readonly toggleProvider: (providerKey: string) => void; }; const ThreadSettingsSessionContext = createContext(null); /** Owns the staged model and option state for one picker presentation. */ function ThreadSettingsSessionProvider( props: ThreadSettingsSessionProps & { readonly children: ReactNode }, ) { const [showLegacyToggle, setShowLegacyToggle] = useState(false); const [providerFilter, setProviderFilter] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [providerExpansionOverrides, setProviderExpansionOverrides] = useState>( () => new Set(), ); const [pendingModel, setPendingModel] = useState(null); const isApplied = useCallback( (option: ModelOption) => option.selection.instanceId === props.selectedModel?.instanceId && option.selection.model === props.selectedModel.model, [props.selectedModel], ); // The list highlights the staged pick; Save turns it into the applied one. const isDisplayed = useCallback( (option: ModelOption) => (pendingModel ? option.key === pendingModel.key : isApplied(option)), [isApplied, pendingModel], ); // While a model is staged, the settings rows describe and edit the staged // model's options (kept on its pending selection); Save applies model and // options together. Otherwise they edit the applied selection directly. const displayedDescriptors = useMemo( () => pendingModel ? pendingModel.capabilities ? getProviderOptionDescriptors({ caps: pendingModel.capabilities, selections: pendingModel.selection.options, }) : [] : props.optionDescriptors, [pendingModel, props.optionDescriptors], ); const hasLegacyModels = useMemo( () => props.providerGroups.some((group) => group.models.some((model) => model.isLegacy)), [props.providerGroups], ); const commitPendingModel = useCallback(() => { if (pendingModel) { if (!canCommitPendingModel(pendingModel, props.providerGroups)) { Alert.alert( "Model unavailable", "Set up this provider on web or desktop, or select another model.", ); return false; } void Haptics.selectionAsync(); props.onSelectModel(pendingModel); } return true; }, [pendingModel, props.onSelectModel, props.providerGroups]); const applyOptionChange = useCallback( (id: string, value: string | boolean) => { const next = applyProviderOptionSelection(displayedDescriptors, { id, value }); if (!next) { return; } if (pendingModel) { setPendingModel({ ...pendingModel, selection: { ...pendingModel.selection, options: next }, }); } else { props.onUpdateOptionSelections(next); } }, [displayedDescriptors, pendingModel, props.onUpdateOptionSelections], ); const toggleProvider = useCallback((providerKey: string) => { setProviderExpansionOverrides((current) => { const next = new Set(current); if (!next.delete(providerKey)) { next.add(providerKey); } return next; }); }, []); const pressModel = useCallback( (option: ModelOption) => { void Haptics.selectionAsync(); setPendingModel((current) => pendingModelAfterPress({ current, pressed: option, pressedIsApplied: isApplied(option), }), ); }, [isApplied], ); const value = useMemo( () => ({ environmentId: props.environmentId, providerInstanceId: props.providerInstanceId, providerGroups: props.providerGroups, runtimeMode: props.runtimeMode, onUpdateRuntimeMode: props.onUpdateRuntimeMode, displayedDescriptors, providerExpansionOverrides, hasLegacyModels, pendingModel, providerFilter, searchQuery, showLegacy: showLegacyToggle, applyOptionChange, commitPendingModel, isApplied, isDisplayed, pressModel, setProviderFilter, setSearchQuery, setShowLegacy: setShowLegacyToggle, toggleProvider, }), [ applyOptionChange, commitPendingModel, displayedDescriptors, providerExpansionOverrides, hasLegacyModels, isApplied, isDisplayed, props.environmentId, props.providerInstanceId, pendingModel, pressModel, providerFilter, props.onUpdateRuntimeMode, props.providerGroups, props.runtimeMode, searchQuery, showLegacyToggle, toggleProvider, ], ); return ( {props.children} ); } function useThreadSettingsSession() { const value = use(ThreadSettingsSessionContext); if (!value) { throw new Error("useThreadSettingsSession must be used inside ThreadSettingsSessionProvider."); } return value; } type ThreadSettingsProviderCatalog = { readonly key: string; readonly driver: string | undefined; readonly label: string; readonly collapsible: boolean; readonly collapsed: boolean; readonly modelCount: number; readonly models: ReadonlyArray; }; type ThreadSettingsCatalogItem = | { readonly kind: "provider"; readonly key: string; readonly provider: ThreadSettingsProviderCatalog; } | { readonly kind: "model"; readonly key: string; readonly option: ModelOption; readonly isFirst: boolean; readonly isLast: boolean; } | { readonly kind: "empty"; readonly key: "empty"; } | { readonly kind: "options"; readonly key: "options"; }; function ThreadSettingsModelListRow(props: { readonly option: ModelOption; readonly isFirst: boolean; readonly isLast: boolean; }) { const session = useThreadSettingsSession(); const onPress = useCallback( () => session.pressModel(props.option), [props.option, session.pressModel], ); return ( ); } function ThreadSettingsProviderListHeader(props: { readonly provider: ThreadSettingsProviderCatalog; }) { const session = useThreadSettingsSession(); const onToggle = useCallback( () => session.toggleProvider(props.provider.key), [props.provider.key, session.toggleProvider], ); return ( ); } function useThreadSettingsCatalogItems( session: ThreadSettingsSessionValue, ): ReadonlyArray { return useMemo( () => session.providerGroups.flatMap((group) => { if (session.providerFilter !== null && group.providerKey !== session.providerFilter) { return []; } const driver = group.models[0]?.providerDriver ?? group.providerKey; const catalogModels = session.showLegacy ? group.models : group.models.filter((model) => !model.isLegacy || session.isDisplayed(model)); const visibleModels = catalogModels.filter((model) => modelMatchesCatalogQuery({ model, providerLabel: group.providerLabel, query: session.searchQuery, }), ); if (visibleModels.length === 0) { return []; } const isPrimary = driver !== undefined && PRIMARY_PROVIDER_DRIVERS.has(driver); // Staging a model must not change disclosure state. The applied model // stays stable for the lifetime of this picker (Save closes it), so it // is safe to use as the initial selected-provider default. const containsAppliedSelection = group.models.some(session.isApplied); const isNarrowed = session.providerFilter !== null || session.searchQuery.trim().length > 0; const collapsible = !isNarrowed; const collapsed = providerSectionIsCollapsed({ defaultExpanded: isPrimary || containsAppliedSelection, hasExpansionOverride: session.providerExpansionOverrides.has(group.providerKey), isNarrowed, }); const provider: ThreadSettingsProviderCatalog = { key: group.providerKey, driver, label: group.providerLabel, collapsible, collapsed, modelCount: visibleModels.length, models: collapsed ? [] : visibleModels, }; return [ { kind: "provider" as const, key: `provider:${group.providerKey}`, provider, }, ...provider.models.map((option, index) => ({ kind: "model" as const, key: `model:${option.key}`, option, isFirst: index === 0, isLast: index === provider.models.length - 1, })), ]; }), [ session.isApplied, session.isDisplayed, session.providerExpansionOverrides, session.providerFilter, session.providerGroups, session.searchQuery, session.showLegacy, ], ); } function ThreadSettingsOptionsItem(props: { readonly animationsReady: boolean; readonly onOpenSubmenu: (submenu: ThreadSettingsSubmenuPage) => void; }) { const insets = useSafeAreaInsets(); const session = useThreadSettingsSession(); const bottomToolbarInset = Platform.OS === "ios" && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED ? NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET : 0; return ( Options {session.displayedDescriptors.map((descriptor) => { if (descriptor.type === "select") { return ( props.onOpenSubmenu({ kind: "descriptor", id: descriptor.id })} /> ); } return ( session.applyOptionChange(descriptor.id, value)} /> ); })} choice.mode === session.runtimeMode)?.label } onPress={() => props.onOpenSubmenu({ kind: "runtime" })} /> {Platform.OS !== "ios" && session.hasLegacyModels ? ( <> Catalog ) : null} ); } /** One native scroll owner for the model catalog and its related settings. */ function ThreadSettingsMainContent(props: { readonly onOpenSubmenu: (submenu: ThreadSettingsSubmenuPage) => void; }) { const session = useThreadSettingsSession(); const catalogItems = useThreadSettingsCatalogItems(session); const [animationsReady, setAnimationsReady] = useState(false); const nativeHeaderHeight = use(HeaderHeightContext) ?? 0; const hasActiveCatalogFilter = session.providerFilter !== null || session.searchQuery.trim().length > 0; const usesTransparentNativeHeader = Platform.OS === "ios" && NATIVE_LIQUID_GLASS_SUPPORTED; const listItems = useMemo>( () => [ ...(catalogItems.length === 0 ? ([{ kind: "empty", key: "empty" }] as const) : catalogItems), { kind: "options", key: "options" }, ], [catalogItems], ); const renderCatalogItem = useCallback( (itemProps: LegendListRenderItemProps) => { const item = itemProps.item; let content: ReactNode; if (item.kind === "provider") { content = ; } else if (item.kind === "model") { content = ( ); } else if (item.kind === "empty") { content = ( {hasActiveCatalogFilter ? "No matching models" : "No available models"} ); } else { content = ( ); } return ( {content} ); }, [animationsReady, hasActiveCatalogFilter, props.onOpenSubmenu], ); return ( item.kind} itemLayoutAnimation={THREAD_SETTINGS_CATALOG_LAYOUT_TRANSITION} keyExtractor={(item) => item.key} keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" maintainVisibleContentPosition={THREAD_SETTINGS_MAINTAIN_VISIBLE_CONTENT_POSITION} ListHeaderComponent={ <> {usesTransparentNativeHeader ? : null} {Platform.OS === "android" ? ( ) : null} } recycleItems onLoad={() => setAnimationsReady(true)} renderItem={renderCatalogItem} showsVerticalScrollIndicator={false} /> ); } /** Compact choice page pushed by the picker navigator. */ function ThreadSettingsChoiceContent(props: { readonly submenu: ThreadSettingsSubmenuPage; readonly onSelected: () => void; }) { const insets = useSafeAreaInsets(); const session = useThreadSettingsSession(); const descriptorId = props.submenu.kind === "descriptor" ? props.submenu.id : null; const activeDescriptor = descriptorId !== null ? session.displayedDescriptors.find( (descriptor) => descriptor.type === "select" && descriptor.id === descriptorId, ) : undefined; const submenuContent = props.submenu.kind === "runtime" ? { rows: RUNTIME_MODE_CHOICES.map((choice) => ({ id: choice.mode, label: choice.label, description: choice.description, selected: choice.mode === session.runtimeMode, onPress: () => { void Haptics.selectionAsync(); session.onUpdateRuntimeMode(choice.mode); props.onSelected(); }, })), } : activeDescriptor?.type === "select" ? { rows: selectableChoices(activeDescriptor).map((choice) => ({ id: choice.id, label: choice.label, description: undefined, selected: choice.id === getProviderOptionCurrentValue(activeDescriptor), onPress: () => { void Haptics.selectionAsync(); session.applyOptionChange(activeDescriptor.id, choice.id); props.onSelected(); }, })), } : null; if (!submenuContent) { return ; } return ( {submenuContent.rows.map((row, index) => ( ))} ); } type ThreadSettingsPickerStackParams = { ThreadSettingsModels: undefined; ThreadSettingsChoice: ThreadSettingsSubmenuPage & { readonly title: string }; }; type ThreadSettingsPickerPresentation = { readonly onClose: () => void; }; const ThreadSettingsPickerStack = createNativeStackNavigator(); const ThreadSettingsPickerPresentationContext = createContext(null); function useThreadSettingsPickerPresentation() { const value = use(ThreadSettingsPickerPresentationContext); if (!value) { throw new Error( "useThreadSettingsPickerPresentation must be used inside ThreadSettingsPickerNavigator.", ); } return value; } function ThreadSettingsModelsScreen() { const session = useThreadSettingsSession(); const presentation = useThreadSettingsPickerPresentation(); const navigation = useNavigation>(); const usesNativeMailSearchToolbar = Platform.OS === "ios" && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; const hasCustomCatalogFilter = session.providerFilter !== null || session.showLegacy; const refreshProvidersCommand = useAtomCommand(serverEnvironment.refreshProviders, { reportFailure: false, }); const refreshProviderCatalog = useMemo( () => createProviderCatalogRefreshRunner(refreshProvidersCommand), [refreshProvidersCommand], ); const [isRefreshingProviders, setIsRefreshingProviders] = useState(false); const refreshProviders = useCallback(() => { if (!session.environmentId || isRefreshingProviders) return; setIsRefreshingProviders(true); void refreshProviderCatalog(session.environmentId).then((result) => { setIsRefreshingProviders(false); const error = providerCatalogRefreshError(result); if (error) Alert.alert("Could not refresh models", error); }); }, [isRefreshingProviders, refreshProviderCatalog, session.environmentId]); const commitAndClose = useCallback(() => { if (!session.commitPendingModel()) return; presentation.onClose(); }, [presentation, session]); const filterMenu = useMemo( () => ({ title: "Model filters", items: [ { type: "submenu" as const, title: "Provider", items: [ { type: "action" as const, title: "All providers", state: session.providerFilter === null ? ("on" as const) : ("off" as const), onPress: () => session.setProviderFilter(null), }, ...session.providerGroups.map((group) => ({ type: "action" as const, title: group.providerLabel, state: session.providerFilter === group.providerKey ? ("on" as const) : ("off" as const), onPress: () => session.setProviderFilter(group.providerKey), })), ], }, ...(session.hasLegacyModels ? [ { type: "action" as const, title: "Show legacy models", state: session.showLegacy ? ("on" as const) : ("off" as const), onPress: () => session.setShowLegacy(!session.showLegacy), }, ] : []), ], }), [session], ); return ( <> {Platform.OS === "android" ? ( ) : null} group.providerKey), session.showLegacy, ]} options={{ unstable_headerToolbarItems: usesNativeMailSearchToolbar ? () => [ createNativeMailSearchToolbarItem({ filterButtonId: "thread-settings-model-filter", filterMenu, filterSystemImageName: hasCustomCatalogFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease", onSearchTextChange: session.setSearchQuery, placeholder: "Find a model", searchTextChangeId: "thread-settings-model-search-text", showsSearchDismissButton: true, }), ] : undefined, headerShown: Platform.OS !== "android", headerSearchBarOptions: Platform.OS === "ios" && !usesNativeMailSearchToolbar ? { autoCapitalize: "none", hideNavigationBar: false, obscureBackground: false, onCancelButtonPress: () => session.setSearchQuery(""), onChangeText: (event) => session.setSearchQuery(event.nativeEvent.text), placeholder: "Find a model", } : undefined, }} /> { const title = submenu.kind === "runtime" ? "Runtime" : (session.displayedDescriptors.find( (descriptor) => descriptor.type === "select" && descriptor.id === submenu.id, )?.label ?? "Option"); navigation.navigate("ThreadSettingsChoice", { ...submenu, title }); }} /> {Platform.OS === "ios" && !usesNativeMailSearchToolbar ? ( Provider session.setProviderFilter(null)} > All providers {session.providerGroups.map((group) => ( session.setProviderFilter(group.providerKey)} > {group.providerLabel} ))} {session.hasLegacyModels ? ( session.setShowLegacy(!session.showLegacy)} > Show legacy models ) : null} ) : null} ); } function ThreadSettingsChoiceScreen() { const navigation = useNavigation>(); const route = useRoute>(); return ( <> {Platform.OS === "android" ? ( navigation.goBack()} /> ) : null} navigation.goBack()} /> ); } function ThreadSettingsPickerNavigator(props: ThreadSettingsPickerPresentation) { const theme = useUniwindTheme(); const solidSheetBackground = theme["--color-sheet-solid"]; const foreground = theme["--color-foreground"]; const presentation = useMemo( () => ({ onClose: props.onClose, }), [props.onClose], ); return ( ({ title: route.params.title })} /> ); } /** Existing-thread model picker hosted by the root RNS form-sheet route. */ export function ExistingThreadSettingsRouteScreen() { const navigation = useNavigation>>(); const presentation = useExistingThreadSettingsRoutePresentation(); const session = presentation.session; useEffect(() => { if (session) { return; } navigation.goBack(); }, [navigation, session]); if (!session) { return ; } const { ownerId: _ownerId, ...settings } = session; return ( navigation.goBack()} /> ); } /** * Native stack hosted by the New Task navigator's form-sheet route. Keeping * the sheet presentation in RNS gives UIKit ownership of nested dismissal, * while Reasoning and Runtime remain regular pushes inside this navigator. */ export function NewTaskThreadSettingsRouteScreen() { const flow = useNewTaskFlow(); const navigation = useNavigation>>(); const optionDescriptors = useMemo( () => resolveProviderOptionDescriptors({ capabilities: flow.selectedModelOption?.capabilities, selections: flow.selectedModel?.options, }), [flow.selectedModel?.options, flow.selectedModelOption?.capabilities], ); return ( flow.setSelectedModelKey(option.key, option.selection.options)} optionDescriptors={optionDescriptors} onUpdateOptionSelections={flow.setSelectedModelOptions} runtimeMode={flow.runtimeMode} onUpdateRuntimeMode={flow.setRuntimeMode} > navigation.goBack()} /> ); }