@@ -10,10 +10,17 @@ import {
1010 Platform ,
1111 ActivityIndicator ,
1212 Keyboard ,
13+ ActionSheetIOS ,
1314} from 'react-native'
1415import { useSafeAreaInsets } from 'react-native-safe-area-context'
1516import { useQuery } from '@tanstack/react-query'
16- import { api , AgentType , getChatUrl , HOST_WORKSPACE_NAME } from '../lib/api'
17+ import { api , AgentType , getChatUrl , HOST_WORKSPACE_NAME , ModelInfo } from '../lib/api'
18+
19+ const FALLBACK_CLAUDE_MODELS : ModelInfo [ ] = [
20+ { id : 'sonnet' , name : 'Sonnet' } ,
21+ { id : 'opus' , name : 'Opus' } ,
22+ { id : 'haiku' , name : 'Haiku' } ,
23+ ]
1724
1825interface MessagePart {
1926 type : 'text' | 'tool_use' | 'tool_result'
@@ -222,11 +229,41 @@ export function SessionChatScreen({ route, navigation }: any) {
222229 const [ hasMoreMessages , setHasMoreMessages ] = useState ( false )
223230 const [ messageOffset , setMessageOffset ] = useState ( 0 )
224231 const [ isLoadingMore , setIsLoadingMore ] = useState ( false )
232+ const [ selectedModel , setSelectedModel ] = useState < string | undefined > ( undefined )
225233 const wsRef = useRef < WebSocket | null > ( null )
226234 const flatListRef = useRef < FlatList > ( null )
227235 const streamingPartsRef = useRef < MessagePart [ ] > ( [ ] )
228236 const messageIdCounter = useRef ( 0 )
229237 const hasLoadedInitial = useRef ( false )
238+ const modelInitialized = useRef ( false )
239+
240+ const fetchAgentType = agentType === 'opencode' ? 'opencode' : 'claude-code'
241+
242+ const { data : modelsData } = useQuery ( {
243+ queryKey : [ 'models' , fetchAgentType ] ,
244+ queryFn : ( ) => api . listModels ( fetchAgentType , workspaceName ) ,
245+ } )
246+
247+ const { data : agentsConfig } = useQuery ( {
248+ queryKey : [ 'agents' ] ,
249+ queryFn : api . getAgents ,
250+ } )
251+
252+ const availableModels = useMemo ( ( ) => {
253+ if ( modelsData ?. models ?. length ) return modelsData . models
254+ if ( fetchAgentType === 'claude-code' ) return FALLBACK_CLAUDE_MODELS
255+ return [ ]
256+ } , [ modelsData , fetchAgentType ] )
257+
258+ useEffect ( ( ) => {
259+ if ( availableModels . length > 0 && ! modelInitialized . current ) {
260+ modelInitialized . current = true
261+ const configModel = fetchAgentType === 'opencode'
262+ ? agentsConfig ?. opencode ?. model
263+ : agentsConfig ?. claude_code ?. model
264+ setSelectedModel ( configModel || availableModels [ 0 ] . id )
265+ }
266+ } , [ availableModels , agentsConfig , fetchAgentType ] )
230267
231268 useEffect ( ( ) => {
232269 const showSub = Keyboard . addListener ( 'keyboardWillShow' , ( ) => setKeyboardVisible ( true ) )
@@ -470,11 +507,17 @@ export function SessionChatScreen({ route, navigation }: any) {
470507
471508 setTimeout ( ( ) => flatListRef . current ?. scrollToEnd ( { animated : true } ) , 100 )
472509
473- wsRef . current . send ( JSON . stringify ( {
510+ const payload : Record < string , unknown > = {
474511 type : 'message' ,
475512 content : msg ,
476513 sessionId : currentSessionId ,
477- } ) )
514+ }
515+
516+ if ( selectedModel ) {
517+ payload . model = selectedModel
518+ }
519+
520+ wsRef . current . send ( JSON . stringify ( payload ) )
478521 }
479522
480523 const interrupt = ( ) => {
@@ -484,6 +527,40 @@ export function SessionChatScreen({ route, navigation }: any) {
484527 }
485528 }
486529
530+ const showModelPicker = ( ) => {
531+ if ( availableModels . length === 0 ) return
532+ if ( isStreaming ) return
533+ if ( agentType === 'opencode' && currentSessionId ) return
534+
535+ const options = [ ...availableModels . map ( m => m . name ) , 'Cancel' ]
536+ ActionSheetIOS . showActionSheetWithOptions (
537+ {
538+ options,
539+ cancelButtonIndex : options . length - 1 ,
540+ title : 'Select Model' ,
541+ } ,
542+ ( buttonIndex ) => {
543+ if ( buttonIndex < availableModels . length ) {
544+ const newModel = availableModels [ buttonIndex ] . id
545+ if ( newModel !== selectedModel ) {
546+ setSelectedModel ( newModel )
547+ if ( agentType !== 'opencode' && currentSessionId ) {
548+ setCurrentSessionId ( null )
549+ setMessages ( prev => [ ...prev , {
550+ role : 'system' ,
551+ content : `Switching to model: ${ availableModels [ buttonIndex ] . name } ` ,
552+ id : `msg-model-${ Date . now ( ) } ` ,
553+ } ] )
554+ }
555+ }
556+ }
557+ }
558+ )
559+ }
560+
561+ const selectedModelName = availableModels . find ( m => m . id === selectedModel ) ?. name || 'Model'
562+ const canChangeModel = ! isStreaming && ! ( agentType === 'opencode' && currentSessionId )
563+
487564 const agentLabels : Record < AgentType , string > = {
488565 'claude-code' : 'Claude Code' ,
489566 opencode : 'OpenCode' ,
@@ -517,7 +594,18 @@ export function SessionChatScreen({ route, navigation }: any) {
517594 </ View >
518595 < Text style = { styles . headerSubtitle } > { agentLabels [ agentType as AgentType ] } </ Text >
519596 </ View >
520- < View style = { styles . placeholder } />
597+ { availableModels . length > 0 && (
598+ < TouchableOpacity
599+ style = { [ styles . modelBtn , ! canChangeModel && styles . modelBtnDisabled ] }
600+ onPress = { showModelPicker }
601+ disabled = { ! canChangeModel }
602+ >
603+ < Text style = { [ styles . modelBtnText , ! canChangeModel && styles . modelBtnTextDisabled ] } >
604+ { selectedModelName }
605+ </ Text >
606+ </ TouchableOpacity >
607+ ) }
608+ { availableModels . length === 0 && < View style = { styles . placeholder } /> }
521609 </ View >
522610
523611 < FlatList
@@ -639,6 +727,23 @@ const styles = StyleSheet.create({
639727 placeholder : {
640728 width : 44 ,
641729 } ,
730+ modelBtn : {
731+ backgroundColor : '#1c1c1e' ,
732+ paddingHorizontal : 10 ,
733+ paddingVertical : 6 ,
734+ borderRadius : 8 ,
735+ } ,
736+ modelBtnDisabled : {
737+ opacity : 0.5 ,
738+ } ,
739+ modelBtnText : {
740+ fontSize : 13 ,
741+ color : '#0a84ff' ,
742+ fontWeight : '500' ,
743+ } ,
744+ modelBtnTextDisabled : {
745+ color : '#8e8e93' ,
746+ } ,
642747 messageList : {
643748 padding : 16 ,
644749 flexGrow : 1 ,
0 commit comments