@@ -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
@@ -264,6 +268,7 @@ export class StreamingMessageAggregator {
264268 */
265269 private cleanupStreamState ( messageId : string ) : void {
266270 this . activeStreams . delete ( messageId ) ;
271+ this . connectingStreams . delete ( messageId ) ;
267272 // Clear todos when stream ends - they're stream-scoped state
268273 // On reload, todos will be reconstructed from completed tool_write calls in history
269274 this . currentTodos = [ ] ;
@@ -372,6 +377,9 @@ export class StreamingMessageAggregator {
372377 this . pendingStreamStartTime = time ;
373378 }
374379
380+ hasConnectingStreams ( ) : boolean {
381+ return this . connectingStreams . size > 0 ;
382+ }
375383 getActiveStreams ( ) : StreamingContext [ ] {
376384 return Array . from ( this . activeStreams . values ( ) ) ;
377385 }
@@ -399,6 +407,11 @@ export class StreamingMessageAggregator {
399407 return context . model ;
400408 }
401409
410+ // If we're connecting (stream-pending), return that model
411+ for ( const context of this . connectingStreams . values ( ) ) {
412+ return context . model ;
413+ }
414+
402415 // Otherwise, return the model from the most recent assistant message
403416 const messages = this . getAllMessages ( ) ;
404417 for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
@@ -418,6 +431,7 @@ export class StreamingMessageAggregator {
418431 clear ( ) : void {
419432 this . messages . clear ( ) ;
420433 this . activeStreams . clear ( ) ;
434+ this . connectingStreams . clear ( ) ;
421435 this . invalidateCache ( ) ;
422436 }
423437
@@ -440,9 +454,30 @@ export class StreamingMessageAggregator {
440454 }
441455
442456 // Unified event handlers that encapsulate all complex logic
457+ handleStreamPending ( data : StreamPendingEvent ) : void {
458+ // Clear pending stream start timestamp - backend has accepted the request.
459+ this . setPendingStreamStartTime ( null ) ;
460+
461+ this . connectingStreams . set ( data . messageId , { startTime : Date . now ( ) , model : data . model } ) ;
462+
463+ // Create a placeholder assistant message (kept invisible until parts arrive)
464+ // so that out-of-order deltas (if they ever occur) have somewhere to attach.
465+ if ( ! this . messages . has ( data . messageId ) ) {
466+ const connectingMessage = createMuxMessage ( data . messageId , "assistant" , "" , {
467+ historySequence : data . historySequence ,
468+ timestamp : Date . now ( ) ,
469+ model : data . model ,
470+ } ) ;
471+ this . messages . set ( data . messageId , connectingMessage ) ;
472+ }
473+
474+ this . invalidateCache ( ) ;
475+ }
476+
443477 handleStreamStart ( data : StreamStartEvent ) : void {
444- // Clear pending stream start timestamp - stream has started
478+ // Clear pending/connecting state - stream has started.
445479 this . setPendingStreamStartTime ( null ) ;
480+ this . connectingStreams . delete ( data . messageId ) ;
446481
447482 // NOTE: We do NOT clear agentStatus or currentTodos here.
448483 // They are cleared when a new user message arrives (see handleMessage),
@@ -577,10 +612,10 @@ export class StreamingMessageAggregator {
577612 }
578613
579614 handleStreamError ( data : StreamErrorMessage ) : void {
580- // Direct lookup by messageId
581- const activeStream = this . activeStreams . get ( data . messageId ) ;
615+ const isTrackedStream =
616+ this . activeStreams . has ( data . messageId ) || this . connectingStreams . has ( data . messageId ) ;
582617
583- if ( activeStream ) {
618+ if ( isTrackedStream ) {
584619 // Mark the message with error metadata
585620 const message = this . messages . get ( data . messageId ) ;
586621 if ( message ?. metadata ) {
@@ -589,32 +624,33 @@ export class StreamingMessageAggregator {
589624 message . metadata . errorType = data . errorType ;
590625 }
591626
592- // Clean up stream-scoped state (active stream tracking, TODOs)
627+ // Clean up stream-scoped state (active/connecting tracking, TODOs)
593628 this . cleanupStreamState ( data . messageId ) ;
594629 this . invalidateCache ( ) ;
595- } else {
596- // Pre-stream error (e.g., API key not configured before streaming starts)
597- // Create a synthetic error message since there's no active stream to attach to
598- // Get the highest historySequence from existing messages so this appears at the end
599- const maxSequence = Math . max (
600- 0 ,
601- ...Array . from ( this . messages . values ( ) ) . map ( ( m ) => m . metadata ?. historySequence ?? 0 )
602- ) ;
603- const errorMessage : MuxMessage = {
604- id : data . messageId ,
605- role : "assistant" ,
606- parts : [ ] ,
607- metadata : {
608- partial : true ,
609- error : data . error ,
610- errorType : data . errorType ,
611- timestamp : Date . now ( ) ,
612- historySequence : maxSequence + 1 ,
613- } ,
614- } ;
615- this . messages . set ( data . messageId , errorMessage ) ;
616- this . invalidateCache ( ) ;
630+ return ;
617631 }
632+
633+ // Pre-stream error (e.g., API key not configured before streaming starts)
634+ // Create a synthetic error message since there's no tracked stream to attach to.
635+ // Get the highest historySequence from existing messages so this appears at the end.
636+ const maxSequence = Math . max (
637+ 0 ,
638+ ...Array . from ( this . messages . values ( ) ) . map ( ( m ) => m . metadata ?. historySequence ?? 0 )
639+ ) ;
640+ const errorMessage : MuxMessage = {
641+ id : data . messageId ,
642+ role : "assistant" ,
643+ parts : [ ] ,
644+ metadata : {
645+ partial : true ,
646+ error : data . error ,
647+ errorType : data . errorType ,
648+ timestamp : Date . now ( ) ,
649+ historySequence : maxSequence + 1 ,
650+ } ,
651+ } ;
652+ this . messages . set ( data . messageId , errorMessage ) ;
653+ this . invalidateCache ( ) ;
618654 }
619655
620656 handleToolCallStart ( data : ToolCallStartEvent ) : void {
0 commit comments