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.