@@ -187,6 +187,13 @@ export interface SendMessageOptions {
187187 assistantSearchLevel ?: AssistantSearchLevel
188188}
189189
190+ interface FinalizeOptions {
191+ error ?: boolean
192+ targetChatId ?: string
193+ /** A lost transport must remain recoverable while the server is still running. */
194+ streamTerminal ?: boolean
195+ }
196+
190197/**
191198 * `true` when the send owns the transcript (rendered, or handed to reconnect),
192199 * `false` when the caller should restore the queue entry, and the object form
@@ -883,9 +890,7 @@ export function useChat(
883890 const resolveDetachedChatForStreamRef = useRef <
884891 ( streamId : string , signal ?: AbortSignal ) => Promise < DetachedChatResolution >
885892 > ( async ( ) => ( { terminal : false } ) )
886- const finalizeRef = useRef < ( options ?: { error ?: boolean ; targetChatId ?: string } ) => void > (
887- ( ) => { }
888- )
893+ const finalizeRef = useRef < ( options ?: FinalizeOptions ) => void > ( ( ) => { } )
889894 const recoveringQueuedSendHandoffRef = useRef < ActiveQueuedSendHandoffRecovery | null > ( null )
890895 const recoverActiveStreamRef = useRef <
891896 ( reason : 'pageshow' | 'visible' | 'online' | 'exhausted_recheck' ) => Promise < void >
@@ -1238,21 +1243,34 @@ export function useChat(
12381243 options ?. requestMode ?? chatHistory ?. mode ?? ( organizationId ? 'assistant' : 'agent' )
12391244 const pendingTurn =
12401245 chatHistory ?. id === ( initialChatId ?? chatIdRef . current ) ? activeTurnRef . current : null
1246+ const liveContent = streamingContentRef . current
1247+ const liveBlocks = streamingBlocksRef . current
12411248 const messages = useMemo ( ( ) => {
12421249 const source = chatHistory ?. messages . map ( toDisplayMessage ) ?? [ ...pendingMessages ]
1243- /** A resource-history read can lag admission; keep this chat's own optimistic user visible. */
1244- if ( pendingTurn && ! source . some ( ( message ) => message . id === pendingTurn . userMessageId ) ) {
1245- const assistantIndex = source . findIndex (
1246- ( message ) => message . id === pendingTurn . assistantMessageId
1247- )
1248- source . splice (
1249- assistantIndex < 0 ? source . length : assistantIndex ,
1250- 0 ,
1251- pendingTurn . optimisticUserMessage
1252- )
1250+ /** History reads can lag the live turn; retain both its owner and current response. */
1251+ if ( pendingTurn ) {
1252+ let ownerIndex = source . findIndex ( ( message ) => message . id === pendingTurn . userMessageId )
1253+ if ( ownerIndex < 0 ) {
1254+ const assistantIndex = source . findIndex (
1255+ ( message ) => message . id === pendingTurn . assistantMessageId
1256+ )
1257+ ownerIndex = assistantIndex < 0 ? source . length : assistantIndex
1258+ source . splice ( ownerIndex , 0 , pendingTurn . optimisticUserMessage )
1259+ }
1260+ const assistant = source [ ownerIndex + 1 ]
1261+ const liveAssistant = {
1262+ ...pendingTurn . optimisticAssistantMessage ,
1263+ content : liveContent ,
1264+ contentBlocks : liveBlocks ,
1265+ }
1266+ if ( assistant ?. id === pendingTurn . assistantMessageId ) {
1267+ source [ ownerIndex + 1 ] = { ...assistant , ...liveAssistant }
1268+ } else if ( assistant ?. role !== 'assistant' ) {
1269+ source . splice ( ownerIndex + 1 , 0 , liveAssistant )
1270+ }
12531271 }
12541272 return source . map ( ( m ) => restoreRevealedSimKeysForMessage ( m , revealedSimKeysRef . current ) )
1255- } , [ chatHistory , pendingMessages , pendingTurn ] )
1273+ } , [ chatHistory , pendingMessages , pendingTurn , liveContent , liveBlocks ] )
12561274 const addResource = useCallback (
12571275 ( resourceUpdate : MothershipResourceUpdate ) : boolean => {
12581276 // The single fan-in for tab creation, so the invariant lives here.
@@ -2006,7 +2024,11 @@ export function useChat(
20062024 }
20072025 if ( ! succeeded && streamGenRef . current === gen ) {
20082026 try {
2009- finalizeRef . current ( { error : true , targetChatId : chatHistory . id } )
2027+ finalizeRef . current ( {
2028+ error : true ,
2029+ targetChatId : chatHistory . id ,
2030+ streamTerminal : false ,
2031+ } )
20102032 } catch {
20112033 setTransportIdle ( )
20122034 abortControllerRef . current = null
@@ -2941,7 +2963,7 @@ export function useChat(
29412963 shouldContinue : isSameRecoverySubject ,
29422964 } )
29432965 if ( ! succeeded && streamGenRef . current === recoveryGen && isSameRecoverySubject ( ) ) {
2944- finalizeRef . current ( { error : true , targetChatId : chatId } )
2966+ finalizeRef . current ( { error : true , targetChatId : chatId , streamTerminal : false } )
29452967 }
29462968 }
29472969 } ) ( )
@@ -3170,7 +3192,7 @@ export function useChat(
31703192 )
31713193
31723194 const finalize = useCallback (
3173- ( options ?: { error ?: boolean ; targetChatId ?: string } ) => {
3195+ ( options ?: FinalizeOptions ) => {
31743196 const isError = ! ! options ?. error
31753197 if ( isError ) {
31763198 const blocks = streamingBlocksRef . current
@@ -3219,8 +3241,10 @@ export function useChat(
32193241 if ( completedActivityTracker ?. generation === streamGenRef . current ) {
32203242 clearResourceActivity ( completedActivityTracker , true )
32213243 }
3222- locallyTerminalStreamIdRef . current =
3223- streamIdRef . current ?? activeTurnRef . current ?. userMessageId ?? undefined
3244+ if ( options ?. streamTerminal !== false ) {
3245+ locallyTerminalStreamIdRef . current =
3246+ streamIdRef . current ?? activeTurnRef . current ?. userMessageId ?? undefined
3247+ }
32243248 clearActiveTurn ( )
32253249 setTransportIdle ( )
32263250 abortControllerRef . current = null
@@ -3804,6 +3828,7 @@ export function useChat(
38043828 if ( gen !== undefined && streamGenRef . current === gen ) {
38053829 finalize ( {
38063830 error : true ,
3831+ streamTerminal : false ,
38073832 ...( streamTargetChatId ? { targetChatId : streamTargetChatId } : { } ) ,
38083833 } )
38093834 }
0 commit comments