@@ -7,6 +7,7 @@ import type {
77} from "@/common/types/message" ;
88import { createMuxMessage } from "@/common/types/message" ;
99import type {
10+ StreamPendingEvent ,
1011 StreamStartEvent ,
1112 StreamDeltaEvent ,
1213 UsageDeltaEvent ,
@@ -75,6 +76,9 @@ function hasFailureResult(result: unknown): boolean {
7576}
7677
7778export class StreamingMessageAggregator {
79+ // Streams that have been registered/started in the backend but haven't emitted stream-start yet.
80+ // This is the "connecting" phase: abort should work, but no deltas have started.
81+ private connectingStreams = new Map < string , { startTime : number ; model : string } > ( ) ;
7882 private messages = new Map < string , MuxMessage > ( ) ;
7983 private activeStreams = new Map < string , StreamingContext > ( ) ;
8084
@@ -283,6 +287,7 @@ export class StreamingMessageAggregator {
283287 */
284288 private cleanupStreamState ( messageId : string ) : void {
285289 this . activeStreams . delete ( messageId ) ;
290+ this . connectingStreams . delete ( messageId ) ;
286291 // Clear todos when stream ends - they're stream-scoped state
287292 // On reload, todos will be reconstructed from completed tool_write calls in history
288293 this . currentTodos = [ ] ;
@@ -391,6 +396,9 @@ export class StreamingMessageAggregator {
391396 this . pendingStreamStartTime = time ;
392397 }
393398
399+ hasConnectingStreams ( ) : boolean {
400+ return this . connectingStreams . size > 0 ;
401+ }
394402 getActiveStreams ( ) : StreamingContext [ ] {
395403 return Array . from ( this . activeStreams . values ( ) ) ;
396404 }
@@ -418,6 +426,11 @@ export class StreamingMessageAggregator {
418426 return context . model ;
419427 }
420428
429+ // If we're connecting (stream-pending), return that model
430+ for ( const context of this . connectingStreams . values ( ) ) {
431+ return context . model ;
432+ }
433+
421434 // Otherwise, return the model from the most recent assistant message
422435 const messages = this . getAllMessages ( ) ;
423436 for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
@@ -437,6 +450,7 @@ export class StreamingMessageAggregator {
437450 clear ( ) : void {
438451 this . messages . clear ( ) ;
439452 this . activeStreams . clear ( ) ;
453+ this . connectingStreams . clear ( ) ;
440454 this . invalidateCache ( ) ;
441455 }
442456
@@ -459,9 +473,30 @@ export class StreamingMessageAggregator {
459473 }
460474
461475 // Unified event handlers that encapsulate all complex logic
476+ handleStreamPending ( data : StreamPendingEvent ) : void {
477+ // Clear pending stream start timestamp - backend has accepted the request.
478+ this . setPendingStreamStartTime ( null ) ;
479+
480+ this . connectingStreams . set ( data . messageId , { startTime : Date . now ( ) , model : data . model } ) ;
481+
482+ // Create a placeholder assistant message (kept invisible until parts arrive)
483+ // so that out-of-order deltas (if they ever occur) have somewhere to attach.
484+ if ( ! this . messages . has ( data . messageId ) ) {
485+ const connectingMessage = createMuxMessage ( data . messageId , "assistant" , "" , {
486+ historySequence : data . historySequence ,
487+ timestamp : Date . now ( ) ,
488+ model : data . model ,
489+ } ) ;
490+ this . messages . set ( data . messageId , connectingMessage ) ;
491+ }
492+
493+ this . invalidateCache ( ) ;
494+ }
495+
462496 handleStreamStart ( data : StreamStartEvent ) : void {
463- // Clear pending stream start timestamp - stream has started
497+ // Clear pending/connecting state - stream has started.
464498 this . setPendingStreamStartTime ( null ) ;
499+ this . connectingStreams . delete ( data . messageId ) ;
465500
466501 // NOTE: We do NOT clear agentStatus or currentTodos here.
467502 // They are cleared when a new user message arrives (see handleMessage),
@@ -596,10 +631,10 @@ export class StreamingMessageAggregator {
596631 }
597632
598633 handleStreamError ( data : StreamErrorMessage ) : void {
599- // Direct lookup by messageId
600- const activeStream = this . activeStreams . get ( data . messageId ) ;
634+ const isTrackedStream =
635+ this . activeStreams . has ( data . messageId ) || this . connectingStreams . has ( data . messageId ) ;
601636
602- if ( activeStream ) {
637+ if ( isTrackedStream ) {
603638 // Mark the message with error metadata
604639 const message = this . messages . get ( data . messageId ) ;
605640 if ( message ?. metadata ) {
@@ -608,32 +643,33 @@ export class StreamingMessageAggregator {
608643 message . metadata . errorType = data . errorType ;
609644 }
610645
611- // Clean up stream-scoped state (active stream tracking, TODOs)
646+ // Clean up stream-scoped state (active/connecting tracking, TODOs)
612647 this . cleanupStreamState ( data . messageId ) ;
613648 this . invalidateCache ( ) ;
614- } else {
615- // Pre-stream error (e.g., API key not configured before streaming starts)
616- // Create a synthetic error message since there's no active stream to attach to
617- // Get the highest historySequence from existing messages so this appears at the end
618- const maxSequence = Math . max (
619- 0 ,
620- ...Array . from ( this . messages . values ( ) ) . map ( ( m ) => m . metadata ?. historySequence ?? 0 )
621- ) ;
622- const errorMessage : MuxMessage = {
623- id : data . messageId ,
624- role : "assistant" ,
625- parts : [ ] ,
626- metadata : {
627- partial : true ,
628- error : data . error ,
629- errorType : data . errorType ,
630- timestamp : Date . now ( ) ,
631- historySequence : maxSequence + 1 ,
632- } ,
633- } ;
634- this . messages . set ( data . messageId , errorMessage ) ;
635- this . invalidateCache ( ) ;
649+ return ;
636650 }
651+
652+ // Pre-stream error (e.g., API key not configured before streaming starts)
653+ // Create a synthetic error message since there's no tracked stream to attach to.
654+ // Get the highest historySequence from existing messages so this appears at the end.
655+ const maxSequence = Math . max (
656+ 0 ,
657+ ...Array . from ( this . messages . values ( ) ) . map ( ( m ) => m . metadata ?. historySequence ?? 0 )
658+ ) ;
659+ const errorMessage : MuxMessage = {
660+ id : data . messageId ,
661+ role : "assistant" ,
662+ parts : [ ] ,
663+ metadata : {
664+ partial : true ,
665+ error : data . error ,
666+ errorType : data . errorType ,
667+ timestamp : Date . now ( ) ,
668+ historySequence : maxSequence + 1 ,
669+ } ,
670+ } ;
671+ this . messages . set ( data . messageId , errorMessage ) ;
672+ this . invalidateCache ( ) ;
637673 }
638674
639675 handleToolCallStart ( data : ToolCallStartEvent ) : void {
0 commit comments