@@ -111,15 +111,13 @@ export class StreamingMessageAggregator {
111111
112112 this . activeStreams . set ( context . streamingId , context ) ;
113113
114- // Create initial streaming message
114+ // Create initial streaming message with empty parts (deltas will append)
115115 const streamingMessage = createCmuxMessage ( data . messageId , "assistant" , "" , {
116116 historySequence : data . historySequence ,
117117 streamingId : context . streamingId ,
118118 timestamp : Date . now ( ) ,
119119 model : data . model ,
120120 } ) ;
121- // Start with empty text part (streaming status inferred from activeStreams)
122- streamingMessage . parts [ 0 ] = { type : "text" , text : "" } ;
123121
124122 this . messages . set ( data . messageId , streamingMessage ) ;
125123 this . incrementDisplayVersion ( ) ;
@@ -129,21 +127,11 @@ export class StreamingMessageAggregator {
129127 const message = this . messages . get ( data . messageId ) ;
130128 if ( ! message ) return ;
131129
132- // Check if last part is text (consistent with text handling in streamManager)
133- const lastPart = message . parts [ message . parts . length - 1 ] ;
134- if ( lastPart ?. type === "text" ) {
135- // Update existing text part
136- message . parts [ message . parts . length - 1 ] = {
137- type : "text" ,
138- text : lastPart . text + data . delta ,
139- } ;
140- } else {
141- // Push new text part
142- message . parts . push ( {
143- type : "text" ,
144- text : data . delta ,
145- } ) ;
146- }
130+ // Append each delta as a new part (merging happens at display time)
131+ message . parts . push ( {
132+ type : "text" ,
133+ text : data . delta ,
134+ } ) ;
147135 this . incrementDisplayVersion ( ) ;
148136 }
149137
@@ -279,21 +267,11 @@ export class StreamingMessageAggregator {
279267 const message = this . messages . get ( data . messageId ) ;
280268 if ( ! message ) return ;
281269
282- // Check if last part is reasoning (consistent with text handling)
283- const lastPart = message . parts [ message . parts . length - 1 ] ;
284- if ( lastPart ?. type === "reasoning" ) {
285- // Update existing reasoning part
286- message . parts [ message . parts . length - 1 ] = {
287- type : "reasoning" ,
288- text : lastPart . text + data . delta ,
289- } ;
290- } else {
291- // Push new reasoning part
292- message . parts . push ( {
293- type : "reasoning" ,
294- text : data . delta ,
295- } ) ;
296- }
270+ // Append each delta as a new part (merging happens at display time)
271+ message . parts . push ( {
272+ type : "reasoning" ,
273+ text : data . delta ,
274+ } ) ;
297275 this . incrementDisplayVersion ( ) ;
298276 }
299277
@@ -376,11 +354,37 @@ export class StreamingMessageAggregator {
376354 // Check if this message has an active stream (for inferring streaming status)
377355 const hasActiveStream = this . getActiveStreams ( ) . some ( ( s ) => s . messageId === message . id ) ;
378356
357+ // Merge adjacent parts of same type (text with text, reasoning with reasoning)
358+ // This is where all merging happens - streaming just appends raw deltas
359+ const mergedParts : typeof message . parts = [ ] ;
360+ for ( let i = 0 ; i < message . parts . length ; i ++ ) {
361+ const part = message . parts [ i ] ;
362+ const lastMerged = mergedParts [ mergedParts . length - 1 ] ;
363+
364+ // Try to merge with last part if same type
365+ if ( lastMerged ?. type === "text" && part . type === "text" ) {
366+ // Merge text parts
367+ mergedParts [ mergedParts . length - 1 ] = {
368+ type : "text" ,
369+ text : lastMerged . text + part . text ,
370+ } ;
371+ } else if ( lastMerged ?. type === "reasoning" && part . type === "reasoning" ) {
372+ // Merge reasoning parts
373+ mergedParts [ mergedParts . length - 1 ] = {
374+ type : "reasoning" ,
375+ text : lastMerged . text + part . text ,
376+ } ;
377+ } else {
378+ // Different type or tool part - add new part
379+ mergedParts . push ( part ) ;
380+ }
381+ }
382+
379383 // Find the last part that will produce a DisplayedMessage
380384 // (reasoning, text parts with content, OR tool parts)
381385 let lastPartIndex = - 1 ;
382- for ( let i = message . parts . length - 1 ; i >= 0 ; i -- ) {
383- const part = message . parts [ i ] ;
386+ for ( let i = mergedParts . length - 1 ; i >= 0 ; i -- ) {
387+ const part = mergedParts [ i ] ;
384388 if (
385389 part . type === "reasoning" ||
386390 ( part . type === "text" && part . text ) ||
@@ -391,7 +395,7 @@ export class StreamingMessageAggregator {
391395 }
392396 }
393397
394- message . parts . forEach ( ( part , partIndex ) => {
398+ mergedParts . forEach ( ( part , partIndex ) => {
395399 const isLastPart = partIndex === lastPartIndex ;
396400 // Part is streaming if: active stream exists AND this is the last part
397401 const isStreaming = hasActiveStream && isLastPart ;
0 commit comments