import { useState } from 'react' import { ActivityIndicator, Keyboard, Pressable, StyleSheet, Text, View } from 'react-native' import { ChevronLeft, X } from 'lucide-react-native' import { BottomDrawer } from '../components/BottomDrawer' import { colors, radii, spacing, typography } from '../theme/mobile-theme' import type { SessionOptionDescriptor, SessionOptionValue } from '../../../src/shared/native-chat-session-options' import { mobileModelPillLabel, mobileOptionsPillLabel, mobileSessionOptionSummaryValue, mobileSessionOptionDisabledReason } from './mobile-native-chat-session-option-labels' import { DescriptorRows, Pill, SessionOptionCaption, SessionOptionSummaryRow } from './MobileNativeChatSessionOptionRows' import { sortNativeChatSessionOptions } from '../../../src/shared/native-chat-session-option-snapshot' import type { MobileNativeChatSessionOptionsController } from './use-mobile-native-chat-session-options' export type MobileNativeChatSessionOptionPickersProps = { controller: MobileNativeChatSessionOptionsController /** Pickers lock while the agent works — a mid-turn `/model` interleaves with * the agent's own output (desktop parity). */ isWorking: boolean /** A composer send owns the TUI input line until it settles. The host spaces a * send's body and its Enter ~500ms apart, so an apply dispatched inside that * window would be submitted as part of the user's prompt. The composer blocks * the reverse direction on `pendingId`; this is the same guard mirrored. */ sendInFlight?: boolean } /** Combined model/session-option trigger and its mobile bottom drawer. */ export function MobileNativeChatSessionOptionPickers({ controller, isWorking, sendInFlight = false }: MobileNativeChatSessionOptionPickersProps): React.JSX.Element | null { const [openDescriptorId, setOpenDescriptorId] = useState(null) const { snapshot, pendingId } = controller const model = snapshot.find((descriptor) => descriptor.category === 'model') const options = sortNativeChatSessionOptions(snapshot) if (!model) { return null } const disabled = isWorking || pendingId !== null || sendInFlight const activeDescriptor = snapshot.find((descriptor) => descriptor.id === openDescriptorId) const modelView = activeDescriptor?.id === model.id const modelLabel = mobileModelPillLabel(model) const optionsLabel = options.length > 0 ? mobileOptionsPillLabel(options) : null const pillLabel = optionsLabel ? `${modelLabel} ${optionsLabel}` : modelLabel const reason = mobileSessionOptionDisabledReason(activeDescriptor?.disabledReason) const closePicker = (): void => setOpenDescriptorId(null) const openPicker = (): void => { Keyboard.dismiss() setOpenDescriptorId(model.id) } const applyOption = (descriptor: SessionOptionDescriptor, value: SessionOptionValue): void => { // Re-picking the tracked value is a no-op — never re-dispatch it. if ( descriptor.valueSource !== 'unknown' && descriptor.kind.type === 'select' && descriptor.kind.currentValue === value ) { closePicker() return } void controller.setOption(descriptor.id, value).then((applied) => { if (applied) { closePicker() } }) } const invokeAction = (descriptor: SessionOptionDescriptor): void => { void controller.invokeAction(descriptor.id).then((invoked) => { if (invoked) { closePicker() } }) } return ( {activeDescriptor ? ( [styles.sheetNav, pressed && styles.pressed]} onPress={modelView ? closePicker : () => setOpenDescriptorId(model.id)} hitSlop={8} > {modelView ? ( ) : ( )} {modelView ? 'Select model' : `Select ${activeDescriptor.label.toLowerCase()}`} {pendingId !== null ? ( ) : null} {activeDescriptor.valueSource === 'dispatched' ? ( Sent to the agent — not confirmed ) : null} {reason ? {reason} : null} applyOption(activeDescriptor, value)} onInvokeAction={() => invokeAction(activeDescriptor)} /> {modelView && options.length > 0 ? ( {options.map((descriptor, index) => ( setOpenDescriptorId(descriptor.id)} /> ))} ) : null} ) : null} ) } const styles = StyleSheet.create({ sheet: { paddingBottom: spacing.xs }, sheetHeader: { flexDirection: 'row', alignItems: 'center', paddingBottom: spacing.lg }, sheetTitle: { flex: 1, color: colors.textPrimary, fontSize: typography.titleSize, fontWeight: '700', textAlign: 'center' }, sheetNav: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.bgRaised, borderWidth: StyleSheet.hairlineWidth, borderColor: colors.borderSubtle }, sheetHeaderSide: { width: 36, height: 36, alignItems: 'center', justifyContent: 'center' }, choiceGroup: { overflow: 'hidden', borderRadius: radii.card, borderWidth: StyleSheet.hairlineWidth, borderColor: colors.borderSubtle, backgroundColor: colors.bgRaised }, optionGroup: { overflow: 'hidden', marginTop: spacing.md, borderRadius: radii.card, borderWidth: StyleSheet.hairlineWidth, borderColor: colors.borderSubtle, backgroundColor: colors.bgRaised }, pressed: { opacity: 0.7 } })