From bc368ba2fdfd359b7130951d5430e04158f30d90 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sun, 16 Aug 2026 21:47:33 -0700 Subject: [PATCH] [eric] streaming: an open chat scrolled off screen downshifts to the 1Hz buffer and flushes the instant it returns (ENG-329) --- .../src/app/pages/AgentChat/AgentChat.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index 6325461e..a1651382 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -452,6 +452,31 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose }; }, [id, isDraft, dispatch]); + // An OPEN chat whose card sits off screen still streamed at frame rate for nobody; downshift its + // socket to the 1Hz background buffer while invisible, and flush synchronously the moment it + // scrolls back in (the buffer's reopen contract), so a visible transcript is never behind. + useEffect(() => { + const el = scrollContainerRef.current; + if (!el || typeof IntersectionObserver === 'undefined') return undefined; + let hideTimer: ReturnType | null = null; + const io = new IntersectionObserver((entries) => { + const visible = entries.some((e) => e.isIntersecting); + if (visible) { + if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } + wsRef.current?.setBackgrounded(false); + } else if (!hideTimer) { + // 2s of hysteresis so a pan that clips the card for a beat never churns the mode. + hideTimer = setTimeout(() => { hideTimer = null; wsRef.current?.setBackgrounded(true); }, 2000); + } + }, { threshold: 0 }); + io.observe(el); + return () => { + if (hideTimer) clearTimeout(hideTimer); + io.disconnect(); + wsRef.current?.setBackgrounded(false); + }; + }, [id]); + useEffect(() => { if (initialContextApplied.current || !initialContextPaths?.length) return; const timer = setTimeout(() => {