fix(chat): Such-Scroll springt nicht mehr endlos (Retry-Limit + Skip)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1041,11 +1041,20 @@ const ChatScreen: React.FC = () => {
|
|||||||
for (const cmid of Array.from(ackTimers.current.keys())) {
|
for (const cmid of Array.from(ackTimers.current.keys())) {
|
||||||
clearAckTimer(cmid);
|
clearAckTimer(cmid);
|
||||||
}
|
}
|
||||||
setMessages(prev => prev.map(m =>
|
// Reference-stable: wenn keine Bubble zu aendern ist, geben wir
|
||||||
m.sender === 'user' && m.deliveryStatus === 'sending'
|
// prev unveraendert zurueck. Sonst triggert .map() ein neues
|
||||||
? { ...m, deliveryStatus: 'sent' }
|
// Array + Re-Render, was waehrend einer aktiven Such-Scroll-
|
||||||
: m
|
// 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-
|
// In den Gedanken-Stream einfuegen. Dedup gegen identische Folge-
|
||||||
// Events (z.B. zwei mal 'thinking' direkt hintereinander). Tool-
|
// 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
|
// ein neuer Search-Hit kommt, damit alte Retries nicht den neuen
|
||||||
// Scroll-Versuch durcheinanderbringen ("permanent springen"-Bug).
|
// Scroll-Versuch durcheinanderbringen ("permanent springen"-Bug).
|
||||||
const pendingScrollRetry = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const pendingScrollRetry = useRef<ReturnType<typeof setTimeout> | 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<number>(0);
|
||||||
|
const MAX_SCROLL_RETRIES = 3;
|
||||||
const clearPendingScrollRetry = () => {
|
const clearPendingScrollRetry = () => {
|
||||||
if (pendingScrollRetry.current) {
|
if (pendingScrollRetry.current) {
|
||||||
clearTimeout(pendingScrollRetry.current);
|
clearTimeout(pendingScrollRetry.current);
|
||||||
pendingScrollRetry.current = null;
|
pendingScrollRetry.current = null;
|
||||||
}
|
}
|
||||||
|
scrollRetryCount.current = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bei Search-Index-Wechsel zur entsprechenden Bubble scrollen.
|
// Bei Search-Index-Wechsel zur entsprechenden Bubble scrollen.
|
||||||
@@ -2152,13 +2168,24 @@ const ChatScreen: React.FC = () => {
|
|||||||
scrollEventThrottle={120}
|
scrollEventThrottle={120}
|
||||||
onScrollToIndexFailed={(info) => {
|
onScrollToIndexFailed={(info) => {
|
||||||
// FlatList kennt das Item-Layout noch nicht. Wir scrollen grob in
|
// FlatList kennt das Item-Layout noch nicht. Wir scrollen grob in
|
||||||
// die Naehe (Average-Item-Hoehe-Schaetzung) und versuchen EINMAL
|
// die Naehe (Average-Item-Hoehe-Schaetzung) und versuchen bis zu
|
||||||
// nach 300ms praezise nachzusetzen. Mehr Retries → Endlos-Cascade
|
// MAX_SCROLL_RETRIES mal praezise nachzusetzen. Danach geben wir
|
||||||
// (jeder failed Retry triggert wieder den Handler → 3, 9, 27 ...
|
// auf — User sieht die Bubble in der ungefaehren Naehe und kann
|
||||||
// Scrolls in der Pipeline = der "permanent springen"-Bug).
|
// 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;
|
const offset = info.averageItemLength * info.index;
|
||||||
try { flatListRef.current?.scrollToOffset({ offset, animated: false }); } catch {}
|
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 = setTimeout(() => {
|
||||||
pendingScrollRetry.current = null;
|
pendingScrollRetry.current = null;
|
||||||
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 }); } catch {}
|
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 }); } catch {}
|
||||||
|
|||||||
Reference in New Issue
Block a user