@@ -211,19 +211,22 @@ export function SessionChatScreen({ route, navigation }: any) {
211211 const insets = useSafeAreaInsets ( )
212212 const { workspaceName, sessionId : initialSessionId , agentType = 'claude-code' , isNew } = route . params
213213
214- const [ allMessages , setAllMessages ] = useState < ChatMessage [ ] > ( [ ] )
215- const [ displayCount , setDisplayCount ] = useState ( MESSAGES_PER_PAGE )
214+ const [ messages , setMessages ] = useState < ChatMessage [ ] > ( [ ] )
216215 const [ input , setInput ] = useState ( '' )
217216 const [ isStreaming , setIsStreaming ] = useState ( false )
218217 const [ connected , setConnected ] = useState ( false )
219218 const [ currentSessionId , setCurrentSessionId ] = useState < string | null > ( initialSessionId || null )
220219 const [ initialScrollDone , setInitialScrollDone ] = useState ( false )
221220 const [ streamingParts , setStreamingParts ] = useState < MessagePart [ ] > ( [ ] )
222221 const [ keyboardVisible , setKeyboardVisible ] = useState ( false )
222+ const [ hasMoreMessages , setHasMoreMessages ] = useState ( false )
223+ const [ messageOffset , setMessageOffset ] = useState ( 0 )
224+ const [ isLoadingMore , setIsLoadingMore ] = useState ( false )
223225 const wsRef = useRef < WebSocket | null > ( null )
224226 const flatListRef = useRef < FlatList > ( null )
225227 const streamingPartsRef = useRef < MessagePart [ ] > ( [ ] )
226228 const messageIdCounter = useRef ( 0 )
229+ const hasLoadedInitial = useRef ( false )
227230
228231 useEffect ( ( ) => {
229232 const showSub = Keyboard . addListener ( 'keyboardWillShow' , ( ) => setKeyboardVisible ( true ) )
@@ -234,65 +237,94 @@ export function SessionChatScreen({ route, navigation }: any) {
234237 }
235238 } , [ ] )
236239
237- const { data : sessionData , isLoading : sessionLoading } = useQuery ( {
238- queryKey : [ 'session' , workspaceName , initialSessionId ] ,
239- queryFn : ( ) => api . getSession ( workspaceName , initialSessionId , agentType ) ,
240- enabled : ! ! initialSessionId && ! isNew ,
241- } )
242-
243240 const generateId = useCallback ( ( ) => {
244241 messageIdCounter . current += 1
245242 return `msg-${ messageIdCounter . current } `
246243 } , [ ] )
247244
248- useEffect ( ( ) => {
249- if ( sessionData ?. messages ) {
250- const converted : ChatMessage [ ] = [ ]
251- let currentParts : MessagePart [ ] = [ ]
252-
253- const flushParts = ( ) => {
254- if ( currentParts . length > 0 ) {
255- const textContent = currentParts
256- . filter ( p => p . type === 'text' )
257- . map ( p => p . content )
258- . join ( '' )
259- converted . push ( {
260- role : 'assistant' ,
261- content : textContent || '' ,
262- id : generateId ( ) ,
263- parts : [ ...currentParts ] ,
264- } )
265- currentParts = [ ]
266- }
245+ const parseMessages = useCallback ( ( rawMessages : any [ ] ) : ChatMessage [ ] => {
246+ const converted : ChatMessage [ ] = [ ]
247+ let currentParts : MessagePart [ ] = [ ]
248+
249+ const flushParts = ( ) => {
250+ if ( currentParts . length > 0 ) {
251+ const textContent = currentParts
252+ . filter ( p => p . type === 'text' )
253+ . map ( p => p . content )
254+ . join ( '' )
255+ converted . push ( {
256+ role : 'assistant' ,
257+ content : textContent || '' ,
258+ id : generateId ( ) ,
259+ parts : [ ...currentParts ] ,
260+ } )
261+ currentParts = [ ]
267262 }
263+ }
268264
269- for ( const m of sessionData . messages ) {
270- if ( m . type === 'user' && m . content ) {
271- flushParts ( )
272- converted . push ( { role : 'user' , content : m . content , id : generateId ( ) } )
273- } else if ( m . type === 'assistant' && m . content ) {
274- currentParts . push ( { type : 'text' , content : m . content } )
275- } else if ( m . type === 'tool_use' ) {
276- currentParts . push ( {
277- type : 'tool_use' ,
278- content : m . toolInput || '' ,
279- toolName : m . toolName ,
280- toolId : m . toolId ,
281- } )
282- } else if ( m . type === 'tool_result' ) {
283- currentParts . push ( {
284- type : 'tool_result' ,
285- content : m . content || '' ,
286- toolId : m . toolId ,
287- } )
288- }
265+ for ( const m of rawMessages ) {
266+ if ( m . type === 'user' && m . content ) {
267+ flushParts ( )
268+ converted . push ( { role : 'user' , content : m . content , id : generateId ( ) } )
269+ } else if ( m . type === 'assistant' && m . content ) {
270+ currentParts . push ( { type : 'text' , content : m . content } )
271+ } else if ( m . type === 'tool_use' ) {
272+ currentParts . push ( {
273+ type : 'tool_use' ,
274+ content : m . toolInput || '' ,
275+ toolName : m . toolName ,
276+ toolId : m . toolId ,
277+ } )
278+ } else if ( m . type === 'tool_result' ) {
279+ currentParts . push ( {
280+ type : 'tool_result' ,
281+ content : m . content || '' ,
282+ toolId : m . toolId ,
283+ } )
289284 }
290- flushParts ( )
285+ }
286+ flushParts ( )
287+
288+ return converted
289+ } , [ generateId ] )
290+
291+ const { data : sessionData , isLoading : sessionLoading } = useQuery ( {
292+ queryKey : [ 'session' , workspaceName , initialSessionId , 'initial' ] ,
293+ queryFn : ( ) => api . getSession ( workspaceName , initialSessionId , agentType , MESSAGES_PER_PAGE , 0 ) ,
294+ enabled : ! ! initialSessionId && ! isNew ,
295+ } )
291296
292- setAllMessages ( converted )
293- setInitialScrollDone ( false )
297+ useEffect ( ( ) => {
298+ if ( sessionData ?. messages && ! hasLoadedInitial . current ) {
299+ hasLoadedInitial . current = true
300+ const converted = parseMessages ( sessionData . messages )
301+ setMessages ( converted )
302+ setHasMoreMessages ( sessionData . hasMore || false )
303+ setMessageOffset ( sessionData . messages . length )
304+ setTimeout ( ( ) => {
305+ flatListRef . current ?. scrollToEnd ( { animated : false } )
306+ } , 150 )
294307 }
295- } , [ sessionData , generateId ] )
308+ } , [ sessionData , parseMessages ] )
309+
310+ const loadMoreMessages = useCallback ( async ( ) => {
311+ if ( ! hasMoreMessages || isLoadingMore || ! initialSessionId ) return
312+
313+ setIsLoadingMore ( true )
314+ try {
315+ const moreData = await api . getSession ( workspaceName , initialSessionId , agentType , MESSAGES_PER_PAGE , messageOffset )
316+ if ( moreData ?. messages ) {
317+ const olderMessages = parseMessages ( moreData . messages )
318+ setMessages ( prev => [ ...olderMessages , ...prev ] )
319+ setHasMoreMessages ( moreData . hasMore || false )
320+ setMessageOffset ( prev => prev + moreData . messages . length )
321+ }
322+ } catch ( err ) {
323+ console . error ( 'Failed to load more messages:' , err )
324+ } finally {
325+ setIsLoadingMore ( false )
326+ }
327+ } , [ hasMoreMessages , isLoadingMore , initialSessionId , workspaceName , agentType , messageOffset , parseMessages ] )
296328
297329 const connect = useCallback ( ( ) => {
298330 const url = getChatUrl ( workspaceName , agentType as AgentType )
@@ -373,7 +405,7 @@ export function SessionChatScreen({ route, navigation }: any) {
373405 . filter ( p => p . type === 'text' )
374406 . map ( p => p . content )
375407 . join ( '' )
376- setAllMessages ( ( prev ) => [ ...prev , {
408+ setMessages ( ( prev ) => [ ...prev , {
377409 role : 'assistant' ,
378410 content : textContent || '' ,
379411 id : `msg-done-${ Date . now ( ) } ` ,
@@ -387,7 +419,7 @@ export function SessionChatScreen({ route, navigation }: any) {
387419 }
388420
389421 if ( msg . type === 'error' ) {
390- setAllMessages ( ( prev ) => [ ...prev , { role : 'system' , content : `Error: ${ msg . content || msg . message } ` , id : `msg-err-${ Date . now ( ) } ` } ] )
422+ setMessages ( ( prev ) => [ ...prev , { role : 'system' , content : `Error: ${ msg . content || msg . message } ` , id : `msg-err-${ Date . now ( ) } ` } ] )
391423 setIsStreaming ( false )
392424 return
393425 }
@@ -405,7 +437,7 @@ export function SessionChatScreen({ route, navigation }: any) {
405437 ws . onerror = ( ) => {
406438 setConnected ( false )
407439 setIsStreaming ( false )
408- setAllMessages ( ( prev ) => [ ...prev , { role : 'system' , content : 'Connection error' , id : `msg-conn-err-${ Date . now ( ) } ` } ] )
440+ setMessages ( ( prev ) => [ ...prev , { role : 'system' , content : 'Connection error' , id : `msg-conn-err-${ Date . now ( ) } ` } ] )
409441 }
410442
411443 return ( ) => ws . close ( )
@@ -416,35 +448,18 @@ export function SessionChatScreen({ route, navigation }: any) {
416448 return cleanup
417449 } , [ connect ] )
418450
419- useEffect ( ( ) => {
420- if ( allMessages . length > 0 && ! initialScrollDone ) {
421- setTimeout ( ( ) => {
422- flatListRef . current ?. scrollToEnd ( { animated : false } )
423- setInitialScrollDone ( true )
424- } , 100 )
425- }
426- } , [ allMessages , initialScrollDone ] )
427-
428- const displayedMessages = useMemo ( ( ) => {
429- if ( allMessages . length <= displayCount ) {
430- return allMessages
431- }
432- return allMessages . slice ( - displayCount )
433- } , [ allMessages , displayCount ] )
434-
435- const hasMoreMessages = allMessages . length > displayCount
436-
437- const loadMoreMessages = useCallback ( ( ) => {
438- if ( hasMoreMessages ) {
439- setDisplayCount ( ( prev ) => prev + MESSAGES_PER_PAGE )
451+ const handleScroll = useCallback ( ( event : any ) => {
452+ const { contentOffset } = event . nativeEvent
453+ if ( contentOffset . y < 100 && hasMoreMessages && ! isLoadingMore ) {
454+ loadMoreMessages ( )
440455 }
441- } , [ hasMoreMessages ] )
456+ } , [ hasMoreMessages , isLoadingMore , loadMoreMessages ] )
442457
443458 const sendMessage = ( ) => {
444459 if ( ! input . trim ( ) || ! wsRef . current || wsRef . current . readyState !== WebSocket . OPEN ) return
445460
446461 const msg = input . trim ( )
447- setAllMessages ( ( prev ) => [ ...prev , { role : 'user' , content : msg , id : `msg-user-${ Date . now ( ) } ` } ] )
462+ setMessages ( ( prev ) => [ ...prev , { role : 'user' , content : msg , id : `msg-user-${ Date . now ( ) } ` } ] )
448463 setInput ( '' )
449464 setIsStreaming ( true )
450465 streamingPartsRef . current = [ ]
@@ -504,15 +519,17 @@ export function SessionChatScreen({ route, navigation }: any) {
504519
505520 < FlatList
506521 ref = { flatListRef }
507- data = { displayedMessages }
522+ data = { messages }
508523 keyExtractor = { ( item ) => item . id }
509524 renderItem = { ( { item } ) => < MessageBubble message = { item } /> }
510525 contentContainerStyle = { styles . messageList }
526+ onScroll = { handleScroll }
527+ scrollEventThrottle = { 100 }
511528 ListHeaderComponent = {
512- hasMoreMessages ? (
513- < TouchableOpacity style = { styles . loadMoreBtn } onPress = { loadMoreMessages } >
514- < Text style = { styles . loadMoreText } > Load older messages </ Text >
515- </ TouchableOpacity >
529+ isLoadingMore ? (
530+ < View style = { styles . loadingMore } >
531+ < ActivityIndicator size = "small" color = "#0a84ff" / >
532+ </ View >
516533 ) : null
517534 }
518535 ListFooterComponent = {
@@ -528,6 +545,7 @@ export function SessionChatScreen({ route, navigation }: any) {
528545 ) : null
529546 }
530547 onScrollToIndexFailed = { ( ) => { } }
548+ maintainVisibleContentPosition = { { minIndexForVisible : 0 } }
531549 />
532550
533551 < View style = { [ styles . inputContainer , { paddingBottom : keyboardVisible ? 8 : insets . bottom + 8 } ] } >
@@ -740,15 +758,9 @@ const styles = StyleSheet.create({
740758 dot3 : {
741759 opacity : 0.8 ,
742760 } ,
743- loadMoreBtn : {
761+ loadingMore : {
744762 alignItems : 'center' ,
745- paddingVertical : 12 ,
746- marginBottom : 8 ,
747- } ,
748- loadMoreText : {
749- fontSize : 14 ,
750- color : '#0a84ff' ,
751- fontWeight : '500' ,
763+ paddingVertical : 16 ,
752764 } ,
753765 emptyChat : {
754766 flex : 1 ,
0 commit comments