@@ -356,9 +356,34 @@ function ChatContentInner({
356356 * re-fade the entire already-visible message.
357357 */
358358 const streamedThisSession = useRef ( false )
359+ const [ animationDrained , setAnimationDrained ] = useState ( false )
360+ const fadeCutoffRef = useRef ( false )
361+
362+ /**
363+ * The per-session latches above outlive the content when React reuses this
364+ * instance for a different logical message — parent rows key by turn
365+ * position and text segments by run ordinal (both deliberately stable across
366+ * the live→persisted id swap), so an ordinal shift or regeneration can hand
367+ * a settled instance brand-new content whose stale `animationDrained` would
368+ * silently render the new stream static. Reset the latches when the content
369+ * is REPLACED (not an append of the previous string) after the instance has
370+ * settled. A resumed turn only ever appends, so this never undoes the
371+ * one-way drain; mid-stream sanitize rewrites are excluded by the
372+ * `animationDrained` gate (the drain only fires after settle).
373+ */
374+ const prevDisplayContentRef = useRef ( displayContent )
375+ if ( prevDisplayContentRef . current !== displayContent ) {
376+ const replaced = ! displayContent . startsWith ( prevDisplayContentRef . current )
377+ prevDisplayContentRef . current = displayContent
378+ if ( replaced && animationDrained ) {
379+ streamedThisSession . current = false
380+ fadeCutoffRef . current = false
381+ setAnimationDrained ( false )
382+ }
383+ }
384+
359385 if ( isStreaming ) streamedThisSession . current = true
360386
361- const [ animationDrained , setAnimationDrained ] = useState ( false )
362387 useEffect ( ( ) => {
363388 if ( isRevealing || animationDrained || ! streamedThisSession . current ) return
364389 const timeout = setTimeout ( ( ) => setAnimationDrained ( true ) , ANIMATION_DRAIN_MS )
@@ -373,7 +398,6 @@ function ChatContentInner({
373398 * `animated` — a fresh animate plugin has no prev-content tracking and would
374399 * re-fade the entire visible segment.
375400 */
376- const fadeCutoffRef = useRef ( false )
377401 if ( streamedContent . length > FADE_MAX_REVEALED_CHARS ) fadeCutoffRef . current = true
378402 const fadeActive = streamingTree && ! fadeCutoffRef . current
379403
0 commit comments