From 635944299e4a40e1b9344466eb8f35ebcb5d7bc3 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 15 May 2026 11:58:54 +0200 Subject: [PATCH] fix(chat): Such-Scroll springt nicht mehr endlos (Retry-Limit + Skip) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: Suche zeigt Treffer, springt aber permanent zwischen Bubbles hin und her in Endlosschleife. Zwei Ursachen, beide angeschlossen: 1) agent_activity-Handler rief setMessages mit prev.map() — auch wenn keine sending-Bubble da war. Das erzeugte trotzdem ein neues Array bei jedem Tool-Event (5-10x pro Brain-Call). invertedMessages neu → FlatList-Layouts invalidiert mitten in einer aktiven Scroll-Sequenz. Fix: prev.some() vor map() — wenn nichts zu aendern ist, prev unveraendert returnen (reference-stable, kein Re-Render). 2) onScrollToIndexFailed retried unbegrenzt. Jeder failed Retry rief den Handler erneut auf → neuer setTimeout → neuer Versuch → fail → loop. Vorher waren cascading 3 Retries, dann auf 1 reduziert um den 3-9-27-Cascade zu fixen, aber EIN ungebremster Retry-Schluss pro fail bleibt eine Endlos-Schleife wenn Layouts nie stabil werden. Fix: harter Counter (MAX_SCROLL_RETRIES = 3). Counter wird bei jedem neuen Search-Hit via clearPendingScrollRetry resettet. Co-Authored-By: Claude Opus 4.7 (1M context) --- android/src/screens/ChatScreen.tsx | 47 +++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index 9a7b3cf..8533e94 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -1041,11 +1041,20 @@ const ChatScreen: React.FC = () => { for (const cmid of Array.from(ackTimers.current.keys())) { clearAckTimer(cmid); } - setMessages(prev => prev.map(m => - m.sender === 'user' && m.deliveryStatus === 'sending' - ? { ...m, deliveryStatus: 'sent' } - : m - )); + // Reference-stable: wenn keine Bubble zu aendern ist, geben wir + // prev unveraendert zurueck. Sonst triggert .map() ein neues + // Array + Re-Render, was waehrend einer aktiven Such-Scroll- + // Sequenz die FlatList-Layouts invalidiert → permanenter + // onScrollToIndexFailed-Loop. + setMessages(prev => { + const needs = prev.some(m => m.sender === 'user' && m.deliveryStatus === 'sending'); + if (!needs) return prev; + return prev.map(m => + m.sender === 'user' && m.deliveryStatus === 'sending' + ? { ...m, deliveryStatus: 'sent' } + : m, + ); + }); } // In den Gedanken-Stream einfuegen. Dedup gegen identische Folge- // Events (z.B. zwei mal 'thinking' direkt hintereinander). Tool- @@ -1394,11 +1403,18 @@ const ChatScreen: React.FC = () => { // ein neuer Search-Hit kommt, damit alte Retries nicht den neuen // Scroll-Versuch durcheinanderbringen ("permanent springen"-Bug). const pendingScrollRetry = useRef | null>(null); + // Zaehler fuer fehlgeschlagene Scroll-Retries. Hartes Limit gegen + // Endlos-Loops wenn das Item-Layout aus irgendwelchen Gruenden nie + // verfuegbar wird (z.B. weil setMessages mitten in der Sequenz die + // FlatList re-rendert). + const scrollRetryCount = useRef(0); + const MAX_SCROLL_RETRIES = 3; const clearPendingScrollRetry = () => { if (pendingScrollRetry.current) { clearTimeout(pendingScrollRetry.current); pendingScrollRetry.current = null; } + scrollRetryCount.current = 0; }; // Bei Search-Index-Wechsel zur entsprechenden Bubble scrollen. @@ -2152,13 +2168,24 @@ const ChatScreen: React.FC = () => { scrollEventThrottle={120} onScrollToIndexFailed={(info) => { // FlatList kennt das Item-Layout noch nicht. Wir scrollen grob in - // die Naehe (Average-Item-Hoehe-Schaetzung) und versuchen EINMAL - // nach 300ms praezise nachzusetzen. Mehr Retries → Endlos-Cascade - // (jeder failed Retry triggert wieder den Handler → 3, 9, 27 ... - // Scrolls in der Pipeline = der "permanent springen"-Bug). + // die Naehe (Average-Item-Hoehe-Schaetzung) und versuchen bis zu + // MAX_SCROLL_RETRIES mal praezise nachzusetzen. Danach geben wir + // auf — User sieht die Bubble in der ungefaehren Naehe und kann + // selber finetunen. Frueher: jeder failed Retry triggerte einen + // neuen Retry ohne Limit → "permanent springen"-Bug, vor allem + // wenn waehrenddessen setMessages die Layouts invalidierte. const offset = info.averageItemLength * info.index; try { flatListRef.current?.scrollToOffset({ offset, animated: false }); } catch {} - clearPendingScrollRetry(); + if (pendingScrollRetry.current) { + clearTimeout(pendingScrollRetry.current); + pendingScrollRetry.current = null; + } + if (scrollRetryCount.current >= MAX_SCROLL_RETRIES) { + // Aufgeben — Item ist offenbar nicht stabil renderbar + scrollRetryCount.current = 0; + return; + } + scrollRetryCount.current += 1; pendingScrollRetry.current = setTimeout(() => { pendingScrollRetry.current = null; try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 }); } catch {}