@@ -468,19 +468,25 @@ const UNMOUNT_ABORT_REASON = 'unmount:client_cleanup'
468468
469469/**
470470 * The lifetime a browser tool started from one stream observes. Replacing the reader (a window
471- * returning to view, a history reconnect) leaves an in-flight action running; an unmount ends it
472- * as a closed chat view, which still reports its outcome; a Stop ends it silently.
471+ * returning to view, a history reconnect) leaves an in-flight action running. The chat view
472+ * closing ends it as a closed view, which still reports its outcome, even when a later reader
473+ * replaced the one that started it. A Stop ends it silently.
473474 */
474- function browserToolLifetime ( streamSignal : AbortSignal | undefined ) : AbortSignal | undefined {
475- if ( ! streamSignal ) return undefined
475+ function browserToolLifetime (
476+ streamSignal : AbortSignal | undefined ,
477+ chatViewSignal : AbortSignal | undefined
478+ ) : AbortSignal {
476479 const lifetime = new AbortController ( )
477- const follow = ( ) => {
478- const reason : unknown = streamSignal . reason
480+ const followStream = ( ) => {
481+ const reason : unknown = streamSignal ? .reason
479482 if ( READER_REPLACEMENT_ABORT_REASONS . has ( reason ) ) return
480483 lifetime . abort ( reason === UNMOUNT_ABORT_REASON ? CHAT_VIEW_CLOSED_ABORT_REASON : reason )
481484 }
482- if ( streamSignal . aborted ) follow ( )
483- else streamSignal . addEventListener ( 'abort' , follow , { once : true } )
485+ const followChatView = ( ) => lifetime . abort ( CHAT_VIEW_CLOSED_ABORT_REASON )
486+ if ( streamSignal ?. aborted ) followStream ( )
487+ else streamSignal ?. addEventListener ( 'abort' , followStream , { once : true } )
488+ if ( chatViewSignal ?. aborted ) followChatView ( )
489+ else chatViewSignal ?. addEventListener ( 'abort' , followChatView , { once : true } )
484490 return lifetime . signal
485491}
486492
@@ -953,6 +959,8 @@ export function useChat(
953959 const reconnectExhaustedRecheckTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null )
954960
955961 const abortControllerRef = useRef < AbortController | null > ( null )
962+ /** Ends when this chat view unmounts, whichever stream reader started a tool. */
963+ const chatViewLifetimeRef = useRef < AbortController | null > ( null )
956964 const detachedChatResolutionControllersRef = useRef < Set < AbortController > | null > ( null )
957965 const detachedChatResolutionControllers = ( detachedChatResolutionControllersRef . current ??=
958966 new Set ( ) )
@@ -2183,7 +2191,10 @@ export function useChat(
21832191 shouldContinue ?: ( ) => boolean
21842192 }
21852193 ) => {
2186- const browserToolSignal = browserToolLifetime ( abortControllerRef . current ?. signal )
2194+ const browserToolSignal = browserToolLifetime (
2195+ abortControllerRef . current ?. signal ,
2196+ chatViewLifetimeRef . current ?. signal
2197+ )
21872198 const activityTracker = getResourceActivityTracker (
21882199 expectedGen ?? streamGenRef . current ,
21892200 options ?. targetChatId
@@ -4997,6 +5008,12 @@ export function useChat(
49975008 remoteActiveStreamId ,
49985009 ] )
49995010
5011+ useEffect ( ( ) => {
5012+ const chatViewLifetime = new AbortController ( )
5013+ chatViewLifetimeRef . current = chatViewLifetime
5014+ return ( ) => chatViewLifetime . abort ( CHAT_VIEW_CLOSED_ABORT_REASON )
5015+ } , [ ] )
5016+
50005017 useEffect ( ( ) => {
50015018 return ( ) => {
50025019 cancelActiveStreamRecovery ( )
0 commit comments