From 7d69229cd76447b92ee66f472f760994d00817ae Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 14:14:56 +0100
Subject: [PATCH] fix: reliable scroll-to-bottom, keyboard-aware project picker

---
 components/SessionDrawer.tsx    |   25 +++++++++++++++++--------
 components/chat/MessageList.tsx |   21 ++++++++++++++-------
 2 files changed, 31 insertions(+), 15 deletions(-)

diff --git a/components/SessionDrawer.tsx b/components/SessionDrawer.tsx
index d7a1998..be58cef 100644
--- a/components/SessionDrawer.tsx
+++ b/components/SessionDrawer.tsx
@@ -9,9 +9,7 @@
   Animated,
   Dimensions,
   Keyboard,
-  KeyboardAvoidingView,
   LayoutAnimation,
-  Platform,
   Pressable,
   ScrollView,
   StyleSheet,
@@ -276,6 +274,18 @@
   const slideAnim = useRef(new Animated.Value(-DRAWER_WIDTH)).current;
   const fadeAnim = useRef(new Animated.Value(0)).current;
   const [rendered, setRendered] = useState(false);
+  const [keyboardHeight, setKeyboardHeight] = useState(0);
+  const pickerScrollRef = useRef<ScrollView>(null);
+
+  useEffect(() => {
+    const showSub = Keyboard.addListener("keyboardWillShow", (e) => {
+      setKeyboardHeight(e.endCoordinates.height);
+    });
+    const hideSub = Keyboard.addListener("keyboardWillHide", () => {
+      setKeyboardHeight(0);
+    });
+    return () => { showSub.remove(); hideSub.remove(); };
+  }, []);
 
   // Local ordering: merge server sessions while preserving user's drag order
   const [orderedSessions, setOrderedSessions] = useState<WsSession[]>([]);
@@ -473,11 +483,7 @@
             elevation: 20,
           }}
         >
-          <KeyboardAvoidingView
-            style={{ flex: 1 }}
-            behavior={Platform.OS === "ios" ? "padding" : undefined}
-          >
-          <GestureHandlerRootView style={{ flex: 1 }}>
+          <GestureHandlerRootView style={{ flex: 1, paddingBottom: keyboardHeight }}>
             {/* Header */}
             <View
               style={{
@@ -564,6 +570,7 @@
 
               {showProjectPicker && (
                 <ScrollView
+                  ref={pickerScrollRef}
                   style={{
                     marginHorizontal: 12,
                     borderRadius: 12,
@@ -630,6 +637,9 @@
                       autoCapitalize="none"
                       autoCorrect={false}
                       returnKeyType="go"
+                      onFocus={() => {
+                        setTimeout(() => pickerScrollRef.current?.scrollToEnd({ animated: true }), 100);
+                      }}
                       onSubmitEditing={() => {
                         if (customPath.trim()) launchSession({ path: customPath.trim() });
                       }}
@@ -679,7 +689,6 @@
               </Text>
             </View>
           </GestureHandlerRootView>
-          </KeyboardAvoidingView>
         </Animated.View>
       </View>
     </View>
diff --git a/components/chat/MessageList.tsx b/components/chat/MessageList.tsx
index 5ff7aea..c3eb13e 100644
--- a/components/chat/MessageList.tsx
+++ b/components/chat/MessageList.tsx
@@ -18,17 +18,17 @@
   // Track the last message's content so transcript reflections trigger a scroll
   const lastContent = messages.length > 0 ? messages[messages.length - 1].content : "";
 
+  // Flag: when true, every content size change triggers a scroll to bottom.
+  // Used for bulk loads (restart, session switch) where FlatList renders lazily.
+  const bulkScrollRef = useRef(false);
+
   useEffect(() => {
     if (messages.length > 0) {
       const delta = Math.abs(messages.length - prevLengthRef.current);
       if (delta > 1) {
-        // Bulk load (restart, session switch) — FlatList renders lazily,
-        // so fire multiple scroll attempts to catch late renders.
-        for (const delay of [100, 300, 700]) {
-          setTimeout(() => {
-            listRef.current?.scrollToEnd({ animated: false });
-          }, delay);
-        }
+        // Bulk load — let onContentSizeChange handle scrolling
+        bulkScrollRef.current = true;
+        setTimeout(() => { bulkScrollRef.current = false; }, 3000);
       } else {
         // Single new message — smooth scroll
         setTimeout(() => {
@@ -38,6 +38,12 @@
     }
     prevLengthRef.current = messages.length;
   }, [messages.length, isTyping, lastContent]);
+
+  const handleContentSizeChange = useCallback(() => {
+    if (bulkScrollRef.current) {
+      listRef.current?.scrollToEnd({ animated: false });
+    }
+  }, []);
 
   // Play from a voice message and auto-chain all consecutive assistant voice messages after it
   const handlePlayVoice = useCallback(async (messageId: string) => {
@@ -77,6 +83,7 @@
           onPlayVoice={handlePlayVoice}
         />
       )}
+      onContentSizeChange={handleContentSizeChange}
       contentContainerStyle={{ paddingVertical: 12 }}
       showsVerticalScrollIndicator={false}
       ListFooterComponent={

--
Gitblit v1.3.1