@@ -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 ,
@@ -135,6 +136,9 @@ function mergeAdjacentParts(parts: MuxMessage["parts"]): MuxMessage["parts"] {
135136}
136137
137138export class StreamingMessageAggregator {
139+ // Streams that have been registered/started in the backend but haven't emitted stream-start yet.
140+ // This is the "connecting" phase: abort should work, but no deltas have started.
141+ private connectingStreams = new Map < string , { startTime : number ; model : string } > ( ) ;
138142 private messages = new Map < string , MuxMessage > ( ) ;
139143 private activeStreams = new Map < string , StreamingContext > ( ) ;
140144
@@ -344,6 +348,7 @@ export class StreamingMessageAggregator {
344348 */
345349 private cleanupStreamState ( messageId : string ) : void {
346350 this . activeStreams . delete ( messageId ) ;
351+ this . connectingStreams . delete ( messageId ) ;
347352 // Clear todos when stream ends - they're stream-scoped state
348353 // On reload, todos will be reconstructed from completed tool_write calls in history
349354 this . currentTodos = [ ] ;
@@ -461,6 +466,9 @@ export class StreamingMessageAggregator {
461466 this . pendingStreamStartTime = time ;
462467 }
463468
469+ hasConnectingStreams ( ) : boolean {
470+ return this . connectingStreams . size > 0 ;
471+ }
464472 getActiveStreams ( ) : StreamingContext [ ] {
465473 return Array . from ( this . activeStreams . values ( ) ) ;
466474 }
@@ -488,6 +496,11 @@ export class StreamingMessageAggregator {
488496 return context . model ;
489497 }
490498
499+ // If we're connecting (stream-pending), return that model
500+ for ( const context of this . connectingStreams . values ( ) ) {
501+ return context . model ;
502+ }
503+
491504 // Otherwise, return the model from the most recent assistant message
492505 const messages = this . getAllMessages ( ) ;
493506 for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
@@ -507,6 +520,7 @@ export class StreamingMessageAggregator {
507520 clear ( ) : void {
508521 this . messages . clear ( ) ;
509522 this . activeStreams . clear ( ) ;
523+ this . connectingStreams . clear ( ) ;
510524 this . invalidateCache ( ) ;
511525 }
512526
@@ -529,9 +543,30 @@ export class StreamingMessageAggregator {
529543 }
530544
531545 // Unified event handlers that encapsulate all complex logic
546+ handleStreamPending ( data : StreamPendingEvent ) : void {
547+ // Clear pending stream start timestamp - backend has accepted the request.
548+ this . setPendingStreamStartTime ( null ) ;
549+
550+ this . connectingStreams . set ( data . messageId , { startTime : Date . now ( ) , model : data . model } ) ;
551+
552+ // Create a placeholder assistant message (kept invisible until parts arrive)
553+ // so that out-of-order deltas (if they ever occur) have somewhere to attach.
554+ if ( ! this . messages . has ( data . messageId ) ) {
555+ const connectingMessage = createMuxMessage ( data . messageId , "assistant" , "" , {
556+ historySequence : data . historySequence ,
557+ timestamp : Date . now ( ) ,
558+ model : data . model ,
559+ } ) ;
560+ this . messages . set ( data . messageId , connectingMessage ) ;
561+ }
562+
563+ this . invalidateCache ( ) ;
564+ }
565+
532566 handleStreamStart ( data : StreamStartEvent ) : void {
533- // Clear pending stream start timestamp - stream has started
567+ // Clear pending/connecting state - stream has started.
534568 this . setPendingStreamStartTime ( null ) ;
569+ this . connectingStreams . delete ( data . messageId ) ;
535570
536571 // NOTE: We do NOT clear agentStatus or currentTodos here.
537572 // They are cleared when a new user message arrives (see handleMessage),
@@ -673,10 +708,10 @@ export class StreamingMessageAggregator {
673708 }
674709
675710 handleStreamError ( data : StreamErrorMessage ) : void {
676- // Direct lookup by messageId
677- const activeStream = this . activeStreams . get ( data . messageId ) ;
711+ const isTrackedStream =
712+ this . activeStreams . has ( data . messageId ) || this . connectingStreams . has ( data . messageId ) ;
678713
679- if ( activeStream ) {
714+ if ( isTrackedStream ) {
680715 // Mark the message with error metadata
681716 const message = this . messages . get ( data . messageId ) ;
682717 if ( message ?. metadata ) {
@@ -688,32 +723,33 @@ export class StreamingMessageAggregator {
688723 this . compactMessageParts ( message ) ;
689724 }
690725
691- // Clean up stream-scoped state (active stream tracking, TODOs)
726+ // Clean up stream-scoped state (active/connecting tracking, TODOs)
692727 this . cleanupStreamState ( data . messageId ) ;
693728 this . invalidateCache ( ) ;
694- } else {
695- // Pre-stream error (e.g., API key not configured before streaming starts)
696- // Create a synthetic error message since there's no active stream to attach to
697- // Get the highest historySequence from existing messages so this appears at the end
698- const maxSequence = Math . max (
699- 0 ,
700- ...Array . from ( this . messages . values ( ) ) . map ( ( m ) => m . metadata ?. historySequence ?? 0 )
701- ) ;
702- const errorMessage : MuxMessage = {
703- id : data . messageId ,
704- role : "assistant" ,
705- parts : [ ] ,
706- metadata : {
707- partial : true ,
708- error : data . error ,
709- errorType : data . errorType ,
710- timestamp : Date . now ( ) ,
711- historySequence : maxSequence + 1 ,
712- } ,
713- } ;
714- this . messages . set ( data . messageId , errorMessage ) ;
715- this . invalidateCache ( ) ;
729+ return ;
716730 }
731+
732+ // Pre-stream error (e.g., API key not configured before streaming starts)
733+ // Create a synthetic error message since there's no tracked stream to attach to.
734+ // Get the highest historySequence from existing messages so this appears at the end.
735+ const maxSequence = Math . max (
736+ 0 ,
737+ ...Array . from ( this . messages . values ( ) ) . map ( ( m ) => m . metadata ?. historySequence ?? 0 )
738+ ) ;
739+ const errorMessage : MuxMessage = {
740+ id : data . messageId ,
741+ role : "assistant" ,
742+ parts : [ ] ,
743+ metadata : {
744+ partial : true ,
745+ error : data . error ,
746+ errorType : data . errorType ,
747+ timestamp : Date . now ( ) ,
748+ historySequence : maxSequence + 1 ,
749+ } ,
750+ } ;
751+ this . messages . set ( data . messageId , errorMessage ) ;
752+ this . invalidateCache ( ) ;
717753 }
718754
719755 handleToolCallStart ( data : ToolCallStartEvent ) : void {
0 commit comments