@@ -14,6 +14,7 @@ import {
1414 serializeComposerFileLink ,
1515 type ComposerTrigger ,
1616} from "@t3tools/shared/composerTrigger" ;
17+ import * as Haptics from "expo-haptics" ;
1718import type { ReactNode } from "react" ;
1819import { memo , useCallback , useEffect , useMemo , useRef , useState , type RefObject } from "react" ;
1920import {
@@ -51,7 +52,7 @@ import {
5152 ComposerToolbarScroller ,
5253 ComposerToolbarTrigger ,
5354} from "../../components/ComposerToolbarTrigger" ;
54- import { ControlPill } from "../../components/ControlPill" ;
55+ import { ControlPill , ControlPillMenu } from "../../components/ControlPill" ;
5556import { ProviderIcon } from "../../components/ProviderIcon" ;
5657import type {
5758 DraftComposerAttachment ,
@@ -60,9 +61,18 @@ import type {
6061import { buildModelOptions , groupByProvider } from "../../lib/modelOptions" ;
6162import { useScaledTextRole } from "../settings/appearance/useScaledTextRole" ;
6263import type { RemoteClientConnectionState } from "../../lib/connection" ;
63- import { resolveProviderOptionDescriptors } from "../../lib/providerOptions" ;
64+ import {
65+ insertRankedSearchResult ,
66+ normalizeSearchQuery ,
67+ scoreQueryMatch ,
68+ } from "@t3tools/shared/searchRanking" ;
69+ import {
70+ applyProviderOptionSelection ,
71+ resolveProviderOptionDescriptors ,
72+ } from "../../lib/providerOptions" ;
6473import { useComposerPathSearch } from "../../state/use-composer-path-search" ;
6574import { ComposerCommandPopover , type ComposerCommandItem } from "./ComposerCommandPopover" ;
75+ import { buildThreadSettingsMenu } from "./thread-settings-menu" ;
6676import { ThreadSettingsSheet , threadSettingsSummaryLabel } from "./ThreadSettingsSheet" ;
6777import { useThreadSettingsSheetPresentation } from "./use-thread-settings-sheet-presentation" ;
6878
@@ -113,6 +123,8 @@ export interface ThreadComposerProps {
113123 readonly onUpdateInteractionMode : ( interactionMode : ProviderInteractionMode ) => void ;
114124 readonly onReconnectEnvironment : ( ) => void ;
115125 readonly onExpandedChange ?: ( expanded : boolean ) => void ;
126+ /** Fires on editor focus/blur; hosts use it to vet stale keyboard state. */
127+ readonly onEditorFocusChange ?: ( focused : boolean ) => void ;
116128}
117129
118130/**
@@ -308,14 +320,16 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
308320 }
309321 } , [ inputRef ] ) ;
310322
323+ const onEditorFocusChange = props . onEditorFocusChange ;
311324 const handleFocus = useCallback ( ( ) => {
312325 setIsFocused ( true ) ;
313- } , [ ] ) ;
326+ onEditorFocusChange ?.( true ) ;
327+ } , [ onEditorFocusChange ] ) ;
314328
315329 const handleBlur = useCallback ( ( ) => {
316330 setIsFocused ( false ) ;
317- onExpandedChange ?.( false ) ;
318- } , [ onExpandedChange ] ) ;
331+ onEditorFocusChange ?.( false ) ;
332+ } , [ onEditorFocusChange ] ) ;
319333 const showStopAction = props . canStopThread ;
320334
321335 const sendLabel =
@@ -562,6 +576,61 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
562576 interactionMode : currentInteractionMode ,
563577 } ) ;
564578
579+ // iOS gets a native menu on the trigger pill: the everyday adjustments
580+ // apply without resigning the keyboard, while "All Settings…" (and the
581+ // Android trigger) still route through the sheet, which must dismiss it.
582+ const settingsMenu = useMemo (
583+ ( ) =>
584+ Platform . OS === "ios"
585+ ? buildThreadSettingsMenu ( {
586+ providerGroups : threadProviderGroups ,
587+ selectedModel : currentModelSelection ,
588+ optionDescriptors : providerOptionDescriptors ,
589+ runtimeMode : currentRuntimeMode ,
590+ } )
591+ : null ,
592+ [ threadProviderGroups , currentModelSelection , providerOptionDescriptors , currentRuntimeMode ] ,
593+ ) ;
594+
595+ const onUpdateModelSelection = props . onUpdateModelSelection ;
596+ const onUpdateRuntimeMode = props . onUpdateRuntimeMode ;
597+ const handleSettingsMenuAction = useCallback (
598+ ( eventId : string ) => {
599+ const event = settingsMenu ?. events . get ( eventId ) ;
600+ if ( ! event ) {
601+ return ;
602+ }
603+ switch ( event . type ) {
604+ case "select-model" :
605+ void Haptics . selectionAsync ( ) ;
606+ onUpdateModelSelection ( event . option . selection ) ;
607+ return ;
608+ case "set-option" : {
609+ const options = applyProviderOptionSelection ( providerOptionDescriptors , {
610+ id : event . optionId ,
611+ value : event . value ,
612+ } ) ;
613+ if ( options ) {
614+ void Haptics . selectionAsync ( ) ;
615+ onUpdateModelSelection ( { ...currentModelSelection , options } ) ;
616+ }
617+ return ;
618+ }
619+ case "set-runtime" :
620+ void Haptics . selectionAsync ( ) ;
621+ onUpdateRuntimeMode ( event . mode ) ;
622+ return ;
623+ }
624+ } ,
625+ [
626+ currentModelSelection ,
627+ onUpdateModelSelection ,
628+ onUpdateRuntimeMode ,
629+ providerOptionDescriptors ,
630+ settingsMenu ,
631+ ] ,
632+ ) ;
633+
565634 return (
566635 < Animated . View
567636 className = "px-4"
@@ -732,15 +801,31 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
732801 onPress = { ( ) => void props . onPickDraftImages ( ) }
733802 showChevron = { false }
734803 />
735- < ComposerToolbarTrigger
736- accessibilityLabel = "Thread settings"
737- iconNode = {
738- < ProviderIcon provider = { currentModelOption ?. providerDriver } size = { 16 } />
739- }
740- label = { settingsSummaryLabel }
741- maxWidth = { 320 }
742- onPress = { settingsSheetPresentation . open }
743- />
804+ { settingsMenu ? (
805+ < ControlPillMenu
806+ actions = { settingsMenu . actions }
807+ onPressAction = { ( { nativeEvent } ) => handleSettingsMenuAction ( nativeEvent . event ) }
808+ >
809+ < ComposerToolbarTrigger
810+ accessibilityLabel = "Thread settings"
811+ iconNode = {
812+ < ProviderIcon provider = { currentModelOption ?. providerDriver } size = { 16 } />
813+ }
814+ label = { settingsSummaryLabel }
815+ maxWidth = { 320 }
816+ />
817+ </ ControlPillMenu >
818+ ) : (
819+ < ComposerToolbarTrigger
820+ accessibilityLabel = "Thread settings"
821+ iconNode = {
822+ < ProviderIcon provider = { currentModelOption ?. providerDriver } size = { 16 } />
823+ }
824+ label = { settingsSummaryLabel }
825+ maxWidth = { 320 }
826+ onPress = { settingsSheetPresentation . open }
827+ />
828+ ) }
744829 { showStopAction ? (
745830 < ComposerToolbarButton
746831 accessibilityLabel = "Stop"
0 commit comments