1818 */
1919
2020import { EcaRemoteApi } from './api' ;
21- import { chatToRestoreEvents } from './chat-restore' ;
21+ import { chatToPendingQuestions , chatToRestoreEvents } from './chat-restore' ;
2222import { messageCache } from './message-cache' ;
2323import { handleOutbound , type OutboundContext } from './outbound-handler' ;
2424import { SSEClient , type SSEEvent } from './sse' ;
@@ -91,6 +91,9 @@ export class WebBridge {
9191 */
9292 private loadedChatIds = new Set < string > ( ) ;
9393
94+ /** Questions received during a lazy history load must wait for chat creation. */
95+ private loadingChatIds = new Set < string > ( ) ;
96+
9497 /**
9598 * True after `disconnect()` has been called. Checked after every async
9699 * boundary in `connect()` so that orphaned bridges (e.g. from React
@@ -109,8 +112,8 @@ export class WebBridge {
109112 private restoring = false ;
110113
111114 /**
112- * SSE events queued while ` restoring` is true . These are replayed
113- * in order after restore completes via `flushRestoreQueue()`.
115+ * SSE events queued while restoring or lazily loading a chat . These are replayed
116+ * in order after loading completes via `flushRestoreQueue()`.
114117 */
115118 private restoreQueue : SSEEvent [ ] = [ ] ;
116119
@@ -683,6 +686,9 @@ export class WebBridge {
683686 // ---------------------------------------------------------------------------
684687
685688 private handleSessionConnected ( event : SSEEvent ) : void {
689+ // History cached before this connection cannot tell us which questions are
690+ // still pending. Invalidate all chats for this host, including lazy loads.
691+ messageCache . invalidateAll ( this . host ) ;
686692 try {
687693 const data : SSESessionConnectedPayload = JSON . parse ( event . data ) ;
688694 this . sessionState = {
@@ -714,6 +720,7 @@ export class WebBridge {
714720 'chat:cleared' ,
715721 'chat:deleted' ,
716722 'chat:opened' ,
723+ 'chat:ask-question' ,
717724 ] ) ;
718725
719726 private handleSSEEvent ( event : SSEEvent ) : void {
@@ -828,7 +835,12 @@ export class WebBridge {
828835 }
829836
830837 case 'chat:ask-question' :
831- this . dispatch ( 'chat/askQuestion' , data ) ;
838+ messageCache . invalidate ( this . host , data . chatId ) ;
839+ if ( this . loadingChatIds . has ( data . chatId ) ) {
840+ this . restoreQueue . push ( event ) ;
841+ } else {
842+ this . dispatch ( 'chat/askQuestion' , data ) ;
843+ }
832844 break ;
833845
834846 case 'jobs:updated' :
@@ -968,6 +980,7 @@ export class WebBridge {
968980 if ( ! candidate ?. id ) continue ;
969981 this . currentChatId = candidate . id ;
970982 const result = await this . loadChatMessages ( candidate . id ) ;
983+ if ( this . disposed ) return ;
971984 if ( result === true ) {
972985 // Clear stale preferred if we had to fall back to a different chat.
973986 if ( preferred && candidate . id !== preferred . id ) {
@@ -1020,7 +1033,7 @@ export class WebBridge {
10201033 * to the webview. Skips if the chat was already loaded.
10211034 *
10221035 * Performance: checks the in-memory message cache first to avoid a REST
1023- * round-trip (e.g. on tab switch or reconnection) . Falls back to REST
1036+ * round-trip within a connection . Falls back to REST
10241037 * on cache miss, and populates the cache on success.
10251038 *
10261039 * Called automatically on initial connect (for the most recent chat)
@@ -1031,8 +1044,10 @@ export class WebBridge {
10311044 * `'error'` if loading failed due to a transient issue (timeout, network, 500).
10321045 */
10331046 async loadChatMessages ( chatId : string ) : Promise < boolean | 'not_found' | 'error' > {
1047+ if ( this . disposed ) return 'error' ;
10341048 if ( this . loadedChatIds . has ( chatId ) ) return true ;
10351049
1050+ this . loadingChatIds . add ( chatId ) ;
10361051 try {
10371052 console . log ( `[Bridge] Loading messages for chat ${ chatId } ` ) ;
10381053
@@ -1045,6 +1060,7 @@ export class WebBridge {
10451060 } else {
10461061 // Cache miss — fetch from server with retries
10471062 const result = await this . fetchChatWithRetry ( chatId ) ;
1063+ if ( this . disposed ) return 'error' ;
10481064 if ( result === WebBridge . CHAT_NOT_FOUND ) return 'not_found' ;
10491065 if ( result === WebBridge . CHAT_FETCH_ERROR ) return 'error' ;
10501066 chat = result ;
@@ -1081,11 +1097,20 @@ export class WebBridge {
10811097 // would cause N separate Immer drafts and N React renders).
10821098 this . dispatch ( 'chat/batchContentReceived' , events ) ;
10831099
1100+ // The question reducer ignores unknown chats, so restore interactive
1101+ // state only after the content batch has created its destination.
1102+ for ( const question of chatToPendingQuestions ( chat ) ) {
1103+ this . dispatch ( 'chat/askQuestion' , question ) ;
1104+ }
1105+
10841106 console . log ( `[Bridge] Restored ${ chat ?. messages ?. length ?? 0 } message(s) for chat ${ chatId } ` ) ;
10851107 return true ;
10861108 } catch ( err ) {
10871109 console . error ( `[Bridge] Failed to load messages for chat ${ chatId } :` , err ) ;
10881110 return 'error' ;
1111+ } finally {
1112+ this . loadingChatIds . delete ( chatId ) ;
1113+ this . flushRestoreQueue ( ) ;
10891114 }
10901115 }
10911116
@@ -1132,11 +1157,12 @@ export class WebBridge {
11321157 }
11331158
11341159 /**
1135- * Replay SSE events that were queued while ` restoring` was true .
1136- * Called in the `finally` block of `dispatchInitialState()` and
1137- * `syncAfterReconnect()` after `restoring` is set back to false .
1160+ * Replay SSE events that were queued while restoring or lazily loading a chat .
1161+ * Called in the `finally` blocks of `dispatchInitialState()`,
1162+ * `syncAfterReconnect()`, and `loadChatMessages()` .
11381163 */
11391164 private flushRestoreQueue ( ) : void {
1165+ if ( this . disposed || this . restoring ) return ;
11401166 if ( this . restoreQueue . length === 0 ) return ;
11411167
11421168 const queued = this . restoreQueue ;
0 commit comments