@@ -335,7 +335,7 @@ describe('parseBlocks span-identity tree', () => {
335335 expect ( segments [ 0 ] . items . some ( ( item ) => item . type === 'tool' ) ) . toBe ( true )
336336 } )
337337
338- it ( 'interleaves mothership tools with main text instead of clustering them at the top ' , ( ) => {
338+ it ( 'replaces earlier main activity across prose and subagents while preserving stream order ' , ( ) => {
339339 const blocks : ContentBlock [ ] = [
340340 mainText ( 'Let me search.' ) ,
341341 mainToolCall ( 't1' , 'grep' ) ,
@@ -348,25 +348,19 @@ describe('parseBlocks span-identity tree', () => {
348348
349349 const segments = parseBlocks ( blocks )
350350
351- // Order is preserved chronologically: the second mothership tool stays below
352- // the research subagent and the trailing text rather than jumping back up
353- // into the first group.
354351 const shape = segments . map ( ( s ) => ( s . type === 'agent_group' ? s . agentName : s . type ) )
355- expect ( shape ) . toEqual ( [ 'text' , 'mothership' , ' research', 'text' , 'mothership' ] )
352+ expect ( shape ) . toEqual ( [ 'text' , 'research' , 'text' , 'mothership' ] )
356353
357- // The two mothership tools land in two distinct groups, one each.
358354 const mothershipGroups = segments . filter (
359355 ( s ) => s . type === 'agent_group' && s . agentName === 'mothership'
360356 )
361- expect ( mothershipGroups ) . toHaveLength ( 2 )
362- const [ first , second ] = mothershipGroups
363- if ( first . type !== 'agent_group' || second . type !== 'agent_group' ) {
364- throw new Error ( 'expected mothership groups ' )
357+ expect ( mothershipGroups ) . toHaveLength ( 1 )
358+ const [ latest ] = mothershipGroups
359+ if ( latest . type !== 'agent_group' ) {
360+ throw new Error ( 'expected mothership group ' )
365361 }
366- expect ( first . items ) . toHaveLength ( 1 )
367- expect ( second . items ) . toHaveLength ( 1 )
368- expect ( first . items [ 0 ] . type === 'tool' && first . items [ 0 ] . data . toolName ) . toBe ( 'grep' )
369- expect ( second . items [ 0 ] . type === 'tool' && second . items [ 0 ] . data . toolName ) . toBe ( 'glob' )
362+ expect ( latest . items ) . toHaveLength ( 1 )
363+ expect ( latest . items [ 0 ] . type === 'tool' && latest . items [ 0 ] . data . id ) . toBe ( 't2' )
370364 } )
371365
372366 it ( 'absorbs the dispatch tool of a nested file subagent from its parent span group' , ( ) => {
@@ -721,7 +715,7 @@ describe('narration text seams', () => {
721715} )
722716
723717describe ( 'parseBlocks legacy — thinking between top-level tools' , ( ) => {
724- it ( 'keeps consecutive mothership tools in one group across intervening thinking' , ( ) => {
718+ it ( 'shows only the latest main tool across intervening thinking' , ( ) => {
725719 const blocks : ContentBlock [ ] = [
726720 { type : 'thinking' , content : 'planning the search' , timestamp : 1 } ,
727721 mainToolCall ( 't1' , 'grep' ) ,
@@ -734,18 +728,20 @@ describe('parseBlocks legacy — thinking between top-level tools', () => {
734728 expect ( groups ) . toHaveLength ( 1 )
735729 if ( groups [ 0 ] . type !== 'agent_group' ) throw new Error ( 'expected group' )
736730 expect ( groups [ 0 ] . agentName ) . toBe ( 'mothership' )
737- expect ( groups [ 0 ] . items ) . toHaveLength ( 3 )
731+ expect ( groups [ 0 ] . items ) . toHaveLength ( 1 )
732+ expect ( groups [ 0 ] . items [ 0 ] . type === 'tool' && groups [ 0 ] . items [ 0 ] . data . id ) . toBe ( 't3' )
738733 } )
739734
740- it ( 'still splits the mothership run on real main text ' , ( ) => {
735+ it ( 'replaces earlier main tools across prose without leaving an empty activity segment ' , ( ) => {
741736 const blocks : ContentBlock [ ] = [
742737 mainToolCall ( 't1' , 'grep' ) ,
743738 mainText ( 'Here is what I found so far.' ) ,
744739 mainToolCall ( 't2' , 'read' ) ,
745740 ]
746741 const segments = parseBlocks ( blocks )
747742 const groups = segments . filter ( ( s ) => s . type === 'agent_group' )
748- expect ( groups ) . toHaveLength ( 2 )
743+ expect ( groups ) . toHaveLength ( 1 )
744+ expect ( segments . map ( ( segment ) => segment . type ) ) . toEqual ( [ 'text' , 'agent_group' ] )
749745 } )
750746
751747 it ( 'does not let main thinking affect subagent lane grouping' , ( ) => {
@@ -787,10 +783,28 @@ describe('parseBlocks legacy — thinking between top-level tools', () => {
787783} )
788784
789785describe ( 'assistantMessageHasVisibleExecutingTool' , ( ) => {
786+ it . each ( [ undefined , 'main' ] ) ( 'ignores a replaced running tool with spanId=%s' , ( spanId ) => {
787+ const blocks : ContentBlock [ ] = [
788+ {
789+ type : 'tool_call' ,
790+ toolCall : { id : 'older' , name : 'grep' , status : 'executing' } ,
791+ spanId,
792+ timestamp : 1 ,
793+ } ,
794+ mainText ( 'Reading the result.' ) ,
795+ mainToolCall ( 'latest' , 'read' ) ,
796+ ]
797+ const segments = parseBlocks ( blocks )
798+ expect ( segments . map ( ( segment ) => segment . type ) ) . toEqual ( [ 'text' , 'agent_group' ] )
799+ expect ( assistantMessageHasVisibleExecutingTool ( segments ) ) . toBe ( false )
800+ } )
801+
790802 it ( 'does not treat an open subagent lane as an executing tool row' , ( ) => {
791- expect ( assistantMessageHasVisibleExecutingTool ( [ subagentStart ( 'workflow' , 'S1' , 'main' ) ] ) ) . toBe (
792- false
793- )
803+ expect (
804+ assistantMessageHasVisibleExecutingTool (
805+ parseBlocks ( [ subagentStart ( 'workflow' , 'S1' , 'main' ) ] )
806+ )
807+ ) . toBe ( false )
794808 } )
795809
796810 it ( 'keeps a visible executing tool as active work' , ( ) => {
@@ -803,15 +817,15 @@ describe('assistantMessageHasVisibleExecutingTool', () => {
803817 timestamp : 3 ,
804818 } ,
805819 ]
806- expect ( assistantMessageHasVisibleExecutingTool ( blocks ) ) . toBe ( true )
820+ expect ( assistantMessageHasVisibleExecutingTool ( parseBlocks ( blocks ) ) ) . toBe ( true )
807821 } )
808822
809823 it ( 'does not let open parallel lanes suppress the single turn-level indicator' , ( ) => {
810824 const blocks : ContentBlock [ ] = [
811825 subagentStart ( 'workflow' , 'S1' , 'main' ) ,
812826 subagentStart ( 'search' , 'S2' , 'main' ) ,
813827 ]
814- expect ( assistantMessageHasVisibleExecutingTool ( blocks ) ) . toBe ( false )
828+ expect ( assistantMessageHasVisibleExecutingTool ( parseBlocks ( blocks ) ) ) . toBe ( false )
815829 } )
816830
817831 it ( 'ignores the executing dispatch tool represented by its subagent lane' , ( ) => {
@@ -826,10 +840,68 @@ describe('assistantMessageHasVisibleExecutingTool', () => {
826840 parentToolCallId : 'dispatch-1' ,
827841 } ,
828842 ]
829- expect ( assistantMessageHasVisibleExecutingTool ( blocks ) ) . toBe ( false )
843+ expect ( assistantMessageHasVisibleExecutingTool ( parseBlocks ( blocks ) ) ) . toBe ( false )
830844 } )
831845} )
832846
847+ describe ( 'parseBlocks main activity controls' , ( ) => {
848+ it . each ( [ undefined , 'main' ] ) (
849+ 'retains blocking controls across prose and completion with spanId=%s' ,
850+ ( spanId ) => {
851+ const blocks : ContentBlock [ ] = [
852+ {
853+ type : 'tool_call' ,
854+ toolCall : { id : 'permission' , name : 'read' , status : 'awaiting_approval' } ,
855+ spanId,
856+ timestamp : 1 ,
857+ } ,
858+ mainText ( 'A permission decision is pending.' ) ,
859+ {
860+ type : 'tool_call' ,
861+ toolCall : {
862+ id : 'handoff' ,
863+ name : 'terminal' ,
864+ status : 'executing' ,
865+ params : { operation : 'handoff' } ,
866+ } ,
867+ timestamp : 2 ,
868+ } ,
869+ mainToolCall ( 'older' , 'grep' ) ,
870+ mainText ( 'Checking another source.' ) ,
871+ {
872+ type : 'tool_call' ,
873+ toolCall : { id : 'latest' , name : 'read' , status : 'executing' } ,
874+ timestamp : 3 ,
875+ } ,
876+ ]
877+
878+ const visibleTools = ( content : ContentBlock [ ] ) =>
879+ parseBlocks ( content ) . flatMap ( ( segment ) =>
880+ segment . type === 'agent_group'
881+ ? segment . items . flatMap ( ( item ) => ( item . type === 'tool' ? [ item . data ] : [ ] ) )
882+ : [ ]
883+ )
884+
885+ expect ( visibleTools ( blocks ) . map ( ( tool ) => tool . id ) ) . toEqual ( [
886+ 'permission' ,
887+ 'handoff' ,
888+ 'latest' ,
889+ ] )
890+ const completed = blocks . map ( ( block ) =>
891+ block . toolCall ?. id === 'latest'
892+ ? { ...block , toolCall : { ...block . toolCall , status : 'success' as const } }
893+ : block
894+ )
895+ expect ( visibleTools ( completed ) . map ( ( tool ) => tool . id ) ) . toEqual ( [
896+ 'permission' ,
897+ 'handoff' ,
898+ 'latest' ,
899+ ] )
900+ expect ( visibleTools ( completed ) . at ( - 1 ) ?. status ) . toBe ( 'success' )
901+ }
902+ )
903+ } )
904+
833905describe ( 'deriveThinkingLabel' , ( ) => {
834906 it ( 'maps the most recent block to an activity phrase' , ( ) => {
835907 expect ( deriveThinkingLabel ( [ ] ) ) . toBe ( 'Thinking…' )
0 commit comments