@@ -23,6 +23,14 @@ function subagentToolCall(
2323 }
2424}
2525
26+ function mainText ( content : string ) : ContentBlock {
27+ return { type : 'text' , content, timestamp : 1 }
28+ }
29+
30+ function mainToolCall ( id : string , name : string ) : ContentBlock {
31+ return { type : 'tool_call' , toolCall : { id, name, status : 'success' } , timestamp : 1 }
32+ }
33+
2634describe ( 'parseBlocks span-identity tree' , ( ) => {
2735 it ( 'nests a deploy subagent inside the workflow subagent that spawned it' , ( ) => {
2836 const blocks : ContentBlock [ ] = [
@@ -101,6 +109,61 @@ describe('parseBlocks span-identity tree', () => {
101109 expect ( segments [ 0 ] . items . some ( ( item ) => item . type === 'tool' ) ) . toBe ( true )
102110 } )
103111
112+ it ( 'interleaves mothership tools with main text instead of clustering them at the top' , ( ) => {
113+ const blocks : ContentBlock [ ] = [
114+ mainText ( 'Let me search.' ) ,
115+ mainToolCall ( 't1' , 'grep' ) ,
116+ subagentStart ( 'research' , 'S1' , 'main' ) ,
117+ { type : 'subagent_text' , content : 'looking' , spanId : 'S1' , timestamp : 2 } ,
118+ { type : 'subagent_end' , spanId : 'S1' , parentSpanId : 'main' , timestamp : 3 } ,
119+ mainText ( 'Found it, now finding files.' ) ,
120+ mainToolCall ( 't2' , 'glob' ) ,
121+ ]
122+
123+ const segments = parseBlocks ( blocks )
124+
125+ // Order is preserved chronologically: the second mothership tool stays below
126+ // the research subagent and the trailing text rather than jumping back up
127+ // into the first group.
128+ const shape = segments . map ( ( s ) => ( s . type === 'agent_group' ? s . agentName : s . type ) )
129+ expect ( shape ) . toEqual ( [ 'text' , 'mothership' , 'research' , 'text' , 'mothership' ] )
130+
131+ // The two mothership tools land in two distinct groups, one each.
132+ const mothershipGroups = segments . filter (
133+ ( s ) => s . type === 'agent_group' && s . agentName === 'mothership'
134+ )
135+ expect ( mothershipGroups ) . toHaveLength ( 2 )
136+ const [ first , second ] = mothershipGroups
137+ if ( first . type !== 'agent_group' || second . type !== 'agent_group' ) {
138+ throw new Error ( 'expected mothership groups' )
139+ }
140+ expect ( first . items ) . toHaveLength ( 1 )
141+ expect ( second . items ) . toHaveLength ( 1 )
142+ expect ( first . items [ 0 ] . type === 'tool' && first . items [ 0 ] . data . toolName ) . toBe ( 'grep' )
143+ expect ( second . items [ 0 ] . type === 'tool' && second . items [ 0 ] . data . toolName ) . toBe ( 'glob' )
144+ } )
145+
146+ it ( 'absorbs the dispatch tool of a nested file subagent from its parent span group' , ( ) => {
147+ const blocks : ContentBlock [ ] = [
148+ subagentStart ( 'workflow' , 'S1' , 'main' ) ,
149+ subagentToolCall ( 't1' , 'workspace_file' , 'S1' , 'workflow' ) ,
150+ { type : 'subagent' , content : 'file' , spanId : 'S2' , parentSpanId : 'S1' , timestamp : 2 } ,
151+ { type : 'subagent_text' , content : 'writing' , spanId : 'S2' , timestamp : 3 } ,
152+ ]
153+
154+ const segments = parseBlocks ( blocks )
155+ expect ( segments ) . toHaveLength ( 1 )
156+ const workflow = segments [ 0 ]
157+ if ( workflow . type !== 'agent_group' ) throw new Error ( 'expected workflow group' )
158+
159+ // The workspace_file dispatch tool is absorbed (not shown as a sibling tool);
160+ // only the nested file subagent remains under workflow.
161+ expect ( workflow . items . some ( ( item ) => item . type === 'tool' ) ) . toBe ( false )
162+ const nested = workflow . items . find ( ( item ) => item . type === 'agent_group' )
163+ if ( ! nested || nested . type !== 'agent_group' ) throw new Error ( 'expected nested file group' )
164+ expect ( nested . group . agentName ) . toBe ( 'file' )
165+ } )
166+
104167 it ( 'falls back to legacy flat grouping when blocks have no span identity' , ( ) => {
105168 const blocks : ContentBlock [ ] = [
106169 { type : 'subagent' , content : 'workflow' , parentToolCallId : 'tc-1' , timestamp : 1 } ,
0 commit comments