diff --git a/src/renderer/src/components/native-chat/NativeChatSubagentRun.test.tsx b/src/renderer/src/components/native-chat/NativeChatSubagentRun.test.tsx
index c4aeb758b91..f2237c33cc5 100644
--- a/src/renderer/src/components/native-chat/NativeChatSubagentRun.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatSubagentRun.test.tsx
@@ -232,6 +232,43 @@ describe('NativeChatToolRun with a spawn group', () => {
expect(screen.queryByText('1 tool call')).toBeNull()
})
+ // Every settled turn sits here by default: the list passes
+ // `expandOverride={expandedTurnIds.has(turnKey)}` — false until the reader
+ // opens that turn — and `activeTurnIsWorking={false}`. The completed-turn
+ // guard above bailed before the roster branch, so the one row this feature
+ // exists to draw vanished the moment its turn finished, and the message row
+ // that kept itself alive for it rendered an empty ghost bubble.
+ it('keeps the roster visible on a completed turn whose activity is collapsed', () => {
+ render(
+
+ )
+
+ expect(screen.getByText('Ran 1 subagent')).toBeInTheDocument()
+ })
+
+ // The roster-only branch returns a `mt-3` wrapper whenever it has rows, so a
+ // group that draws nothing must not count as one — that wrapper would be the
+ // empty bubble with a margin that the message row refuses to emit.
+ it('draws nothing at all for a spawn group that carries no children', () => {
+ const { container } = render(
+
+ )
+
+ expect(container).toBeEmptyDOMElement()
+ })
+
it('renders the roster alongside the tool activity of its turn', () => {
render(
setOpen(expandOverride ?? expandSignal), [expandOverride, expandSignal])
- const subagentRows = subagentGroups.map((group) => (
-
- ))
+ // Childless groups are dropped so `subagentRows.length` stays an honest test of
+ // "something will draw": the roster-only branch below returns a margin-bearing
+ // wrapper on the strength of it, and a group with no children renders null.
+ const subagentRows = subagentGroups
+ .filter((group) => group.agents.length > 0)
+ .map((group) => (
+
+ ))
const callCount = countToolCalls(blocks) || blocks.length
const summary = summarizeToolRun(blocks)
const latestActiveCall = structuredActivityUi
@@ -241,6 +246,19 @@ export function NativeChatToolRun({
value0: callCount
})
+ // A roster with no tool calls beside it is the whole run: rendering the tool
+ // header too would announce "1 tool call" for activity that has none.
+ //
+ // Ordered BEFORE the completed-turn guard below on purpose. That guard hides
+ // TOOL activity behind the turn-status disclosure, and a roster row has none
+ // to hide: it is the compact summary this row exists to leave behind. Bailing
+ // there instead dropped it from every settled turn — the default state of the
+ // whole transcript — and left the caller, which counts a spawn group as
+ // renderable, drawing the empty bubble it explicitly guards against.
+ if (blocks.length === 0) {
+ return subagentRows.length > 0 ? {subagentRows}
: null
+ }
+
// Completed turn activity belongs behind the turn-status disclosure. Keeping
// the grouped row visible here made a failed child command look like the
// whole response was still running (or had failed) even while collapsed.
@@ -253,12 +271,6 @@ export function NativeChatToolRun({
return null
}
- // A roster with no tool calls beside it is the whole run: rendering the tool
- // header too would announce "1 tool call" for activity that has none.
- if (blocks.length === 0) {
- return subagentRows.length > 0 ? {subagentRows}
: null
- }
-
return (
// Extra top margin sets the tool run apart from the assistant prose above it
// so the turn's activity doesn't crowd the message text.