Skip to content

Commit dfdb362

Browse files
fix(mothership): reset animation latches when a reused ChatContent gets replaced content
1 parent 61c9e19 commit dfdb362

1 file changed

Lines changed: 26 additions & 2 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)