@@ -80,38 +80,55 @@ const StaticMessages = memo(
8080StaticMessages . displayName = 'StaticMessages' ;
8181
8282// ---------------------------------------------------------------------------
83- // Dynamic messages — re-renders as streaming progresses
83+ // Dynamic messages — re-renders as streaming progresses while chat is visible
8484// ---------------------------------------------------------------------------
85- function DynamicMessages ( {
86- messages,
87- pendingMessages,
88- getChildMessages,
89- onOpenChildSession,
90- } : {
85+ type DynamicMessagesProps = {
86+ active : boolean ;
9187 messages : StoredMessage [ ] ;
9288 pendingMessages : ReadonlyMap < string , MessageDeliveryState > ;
9389 getChildMessages ?: ( sessionId : string ) => StoredMessage [ ] ;
9490 onOpenChildSession ?: OpenChildSession ;
95- } ) {
96- return (
97- < >
98- { messages . map ( msg => {
99- const streaming = isMessageStreaming ( msg ) ;
100- return (
101- < MessageErrorBoundary key = { msg . info . id } >
102- < MessageBubble
103- message = { msg }
104- isStreaming = { streaming }
105- deliveryState = { pendingMessages . get ( msg . info . id ) }
106- getChildMessages = { getChildMessages }
107- onOpenChildSession = { onOpenChildSession }
108- />
109- </ MessageErrorBoundary >
110- ) ;
111- } ) }
112- </ >
113- ) ;
114- }
91+ } ;
92+
93+ const DynamicMessages = memo (
94+ function DynamicMessages ( {
95+ messages,
96+ pendingMessages,
97+ getChildMessages,
98+ onOpenChildSession,
99+ } : DynamicMessagesProps ) {
100+ return (
101+ < >
102+ { messages . map ( msg => {
103+ const streaming = isMessageStreaming ( msg ) ;
104+ return (
105+ < MessageErrorBoundary key = { msg . info . id } >
106+ < MessageBubble
107+ message = { msg }
108+ isStreaming = { streaming }
109+ deliveryState = { pendingMessages . get ( msg . info . id ) }
110+ getChildMessages = { getChildMessages }
111+ onOpenChildSession = { onOpenChildSession }
112+ />
113+ </ MessageErrorBoundary >
114+ ) ;
115+ } ) }
116+ </ >
117+ ) ;
118+ } ,
119+ ( previous , next ) => {
120+ if ( ! previous . active && ! next . active ) return true ;
121+
122+ return (
123+ previous . active === next . active &&
124+ previous . messages === next . messages &&
125+ previous . pendingMessages === next . pendingMessages &&
126+ previous . getChildMessages === next . getChildMessages &&
127+ previous . onOpenChildSession === next . onOpenChildSession
128+ ) ;
129+ }
130+ ) ;
131+ DynamicMessages . displayName = 'DynamicMessages' ;
115132
116133// ---------------------------------------------------------------------------
117134// CloudChatPage
@@ -141,7 +158,7 @@ function TerminalPaneSlot({
141158 ) ;
142159
143160 return (
144- < div className = { active ? 'h-full min-h-0' : 'hidden' } aria-hidden = { ! active } >
161+ < div className = { active ? 'h-full min-h-0' : 'hidden' } >
145162 { sessionId && (
146163 < CloudAgentTerminalPane
147164 cloudAgentSessionId = { sessionId }
@@ -209,6 +226,7 @@ export default function CloudChatPage({ organizationId }: CloudChatPageProps) {
209226 const [ terminalStatuses , setTerminalStatuses ] = useState <
210227 Record < string , TerminalStatusSummary | undefined >
211228 > ( { } ) ;
229+ const chatTabActive = workspaceTabs . activeTabId === CHAT_TAB_ID ;
212230
213231 useEffect ( ( ) => {
214232 setWorkspaceTabs ( resetWorkspaceTabs ) ;
@@ -247,11 +265,23 @@ export default function CloudChatPage({ organizationId }: CloudChatPageProps) {
247265 const lastScrollTopRef = useRef ( 0 ) ;
248266
249267 const autoScrollFrameRef = useRef ( 0 ) ;
268+ const followUpAutoScrollFrameRef = useRef ( 0 ) ;
250269 const delayedAutoScrollRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
251270
271+ const cancelScheduledAutoScroll = useCallback ( ( ) => {
272+ cancelAnimationFrame ( autoScrollFrameRef . current ) ;
273+ cancelAnimationFrame ( followUpAutoScrollFrameRef . current ) ;
274+ autoScrollFrameRef . current = 0 ;
275+ followUpAutoScrollFrameRef . current = 0 ;
276+ if ( delayedAutoScrollRef . current !== null ) {
277+ clearTimeout ( delayedAutoScrollRef . current ) ;
278+ delayedAutoScrollRef . current = null ;
279+ }
280+ } , [ ] ) ;
281+
252282 const scrollToBottomNow = useCallback ( ( ) => {
253283 const el = scrollContainerRef . current ;
254- if ( ! el ) return ;
284+ if ( ! el || el . hidden ) return ;
255285
256286 const scrollRun = autoScrollRunRef . current + 1 ;
257287 autoScrollRunRef . current = scrollRun ;
@@ -268,38 +298,41 @@ export default function CloudChatPage({ organizationId }: CloudChatPageProps) {
268298 } , [ ] ) ;
269299
270300 const scheduleScrollToBottom = useCallback ( ( ) => {
271- cancelAnimationFrame ( autoScrollFrameRef . current ) ;
272- if ( delayedAutoScrollRef . current !== null ) {
273- clearTimeout ( delayedAutoScrollRef . current ) ;
274- delayedAutoScrollRef . current = null ;
275- }
301+ cancelScheduledAutoScroll ( ) ;
276302
277303 autoScrollFrameRef . current = requestAnimationFrame ( ( ) => {
304+ autoScrollFrameRef . current = 0 ;
278305 scrollToBottomNow ( ) ;
279- requestAnimationFrame ( scrollToBottomNow ) ;
306+ followUpAutoScrollFrameRef . current = requestAnimationFrame ( ( ) => {
307+ followUpAutoScrollFrameRef . current = 0 ;
308+ scrollToBottomNow ( ) ;
309+ } ) ;
280310 delayedAutoScrollRef . current = setTimeout ( ( ) => {
281311 delayedAutoScrollRef . current = null ;
282312 scrollToBottomNow ( ) ;
283313 } , 100 ) ;
284314 } ) ;
285- } , [ scrollToBottomNow ] ) ;
315+ } , [ cancelScheduledAutoScroll , scrollToBottomNow ] ) ;
316+
317+ useEffect ( ( ) => cancelScheduledAutoScroll , [ cancelScheduledAutoScroll ] ) ;
286318
287319 useEffect ( ( ) => {
288- return ( ) => {
289- cancelAnimationFrame ( autoScrollFrameRef . current ) ;
290- if ( delayedAutoScrollRef . current !== null ) {
291- clearTimeout ( delayedAutoScrollRef . current ) ;
292- }
293- } ;
294- } , [ ] ) ;
320+ if ( ! chatTabActive ) cancelScheduledAutoScroll ( ) ;
321+ } , [ cancelScheduledAutoScroll , chatTabActive ] ) ;
295322
296323 useEffect ( ( ) => {
297- if ( ! chatUI . shouldAutoScroll ) return ;
324+ if ( ! chatTabActive || ! chatUI . shouldAutoScroll ) return ;
298325 scheduleScrollToBottom ( ) ;
299- } , [ staticMessages , dynamicMessages , chatUI . shouldAutoScroll , scheduleScrollToBottom ] ) ;
326+ } , [
327+ staticMessages ,
328+ dynamicMessages ,
329+ chatTabActive ,
330+ chatUI . shouldAutoScroll ,
331+ scheduleScrollToBottom ,
332+ ] ) ;
300333
301334 useEffect ( ( ) => {
302- if ( ! chatUI . shouldAutoScroll ) return ;
335+ if ( ! chatTabActive || ! chatUI . shouldAutoScroll ) return ;
303336 if ( typeof ResizeObserver === 'undefined' ) return ;
304337
305338 const content = messagesContentRef . current ;
@@ -310,7 +343,7 @@ export default function CloudChatPage({ organizationId }: CloudChatPageProps) {
310343 } ) ;
311344 observer . observe ( content ) ;
312345 return ( ) => observer . disconnect ( ) ;
313- } , [ chatUI . shouldAutoScroll , scheduleScrollToBottom ] ) ;
346+ } , [ chatTabActive , chatUI . shouldAutoScroll , scheduleScrollToBottom ] ) ;
314347
315348 useEffect ( ( ) => {
316349 if ( ! sessionIdFromParams ) return ;
@@ -670,54 +703,56 @@ export default function CloudChatPage({ organizationId }: CloudChatPageProps) {
670703 className = "flex min-h-0 flex-1 flex-col"
671704 >
672705 < div className = "relative min-h-0 flex-1" >
673- { workspaceTabs . activeTabId === CHAT_TAB_ID && (
674- < >
675- < div
676- ref = { scrollContainerRef }
677- className = { `absolute inset-0 overflow-y-auto px-[max(1rem,calc(50%_-_27rem))] pb-2 pt-4 transition-opacity duration-150 ${ showLoadingIndicator ? 'pointer-events-none opacity-40' : 'opacity-100' } ` }
678- onScroll = { handleScroll }
679- >
680- < div ref = { messagesContentRef } >
681- < StaticMessages
682- messages = { staticMessages }
683- pendingMessages = { pendingMessages }
684- getChildMessages = { getChildMessages }
685- onOpenChildSession = { handleOpenTopLevelChildSession }
686- />
687- < DynamicMessages
688- messages = { dynamicMessages }
689- pendingMessages = { pendingMessages }
690- getChildMessages = { getChildMessages }
691- onOpenChildSession = { handleOpenTopLevelChildSession }
692- />
693-
706+ < >
707+ < div
708+ ref = { scrollContainerRef }
709+ hidden = { ! chatTabActive }
710+ className = { `absolute inset-0 overflow-y-auto px-[max(1rem,calc(50%_-_27rem))] pb-2 pt-4 transition-opacity duration-150 ${ showLoadingIndicator ? 'pointer-events-none opacity-40' : 'opacity-100' } ` }
711+ onScroll = { handleScroll }
712+ >
713+ < div ref = { messagesContentRef } >
714+ < StaticMessages
715+ messages = { staticMessages }
716+ pendingMessages = { pendingMessages }
717+ getChildMessages = { getChildMessages }
718+ onOpenChildSession = { handleOpenTopLevelChildSession }
719+ />
720+ < DynamicMessages
721+ active = { chatTabActive }
722+ messages = { dynamicMessages }
723+ pendingMessages = { pendingMessages }
724+ getChildMessages = { getChildMessages }
725+ onOpenChildSession = { handleOpenTopLevelChildSession }
726+ />
727+
728+ { chatTabActive && (
694729 < WorkingIndicator
695730 messages = { dynamicMessages }
696731 isStreaming = { isStreaming }
697732 />
698- { statusIndicator && (
699- < SessionStatusIndicator indicator = { statusIndicator } />
700- ) }
733+ ) }
734+ { statusIndicator && (
735+ < SessionStatusIndicator indicator = { statusIndicator } />
736+ ) }
701737
702- < div ref = { messagesEndRef } />
703- </ div >
738+ < div ref = { messagesEndRef } />
704739 </ div >
740+ </ div >
705741
706- { showScrollButton && (
707- < button
708- type = "button"
709- onClick = { scrollToBottom }
710- className = "border-border bg-background absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full border p-2 shadow-md"
711- >
712- < ArrowDown className = "h-4 w-4" />
713- </ button >
714- ) }
715- </ >
716- ) }
742+ { chatTabActive && showScrollButton && (
743+ < button
744+ type = "button"
745+ onClick = { scrollToBottom }
746+ className = "border-border bg-background absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full border p-2 shadow-md"
747+ >
748+ < ArrowDown className = "h-4 w-4" />
749+ </ button >
750+ ) }
751+ </ >
717752
718753 < div
719754 className = {
720- workspaceTabs . activeTabId === CHAT_TAB_ID
755+ chatTabActive
721756 ? 'hidden'
722757 : 'h-full min-h-0 px-[max(1rem,calc(50%_-_27rem))] py-2'
723758 }
@@ -726,7 +761,7 @@ export default function CloudChatPage({ organizationId }: CloudChatPageProps) {
726761 </ div >
727762 </ div >
728763
729- { workspaceTabs . activeTabId === CHAT_TAB_ID && (
764+ { chatTabActive && (
730765 < >
731766 { isReadOnly ? (
732767 ! isLoading && sessionIdFromParams && fetchedSessionData ? (
0 commit comments