mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
fix(native-chat): stop the subagent roster vanishing from every settled turn
`NativeChatToolRun` bailed out for a completed turn whose activity disclosure
is collapsed before it reached the branch that draws a roster-only run. That
guard exists to push TOOL activity behind the turn-status disclosure, and it
fires on exactly the shape a spawn group has: a roster message carries no tool
blocks, so `selectActiveToolCall` returns null and `isSettled` is true, while
the list passes `expandOverride={expandedTurnIds.has(turnKey)}` — false until
the reader opens that turn — and `activeTurnIsWorking={false}`.
That is the default state of every finished turn in the transcript, so the one
compact row this feature exists to leave behind ("Ran 3 subagents") disappeared
the moment its turn ended. Worse, `MessageRow` counts a spawn group as
renderable specifically so the row survives, then rendered a wrapper around a
component that returned null — the empty ghost bubble its own guard is written
to prevent.
Order the roster branch before the disclosure guard. A roster has no tool
activity to hide, and the guard's reasoning ("a failed child command looked
like the whole response was still running") does not reach it. Runs that do
carry tool blocks still fall through to the guard unchanged, and in practice a
roster never shares a message with them: it is its own `role: 'system'` journal
row and `isToolOnlyMessage` is false for it, so `foldToolMessages` never merges
tool blocks into it.
Also drop childless groups when building the rows, so `subagentRows.length`
stays an honest test of "something will draw" — the roster-only branch returns
a margin-bearing wrapper on the strength of it, and a group with no children
renders null.
Both tests fail with their fix reverted; the existing NativeChatToolRun suite
still passes, so the completed-turn disclosure behaviour is unchanged.
This commit is contained in:
@@ -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(
|
||||
<NativeChatToolRun
|
||||
blocks={[]}
|
||||
subagentGroups={[group([{ id: 'a', label: 'read', state: 'completed' }])]}
|
||||
expandSignal={false}
|
||||
expandOverride={false}
|
||||
activeTurnIsWorking={false}
|
||||
/>
|
||||
)
|
||||
|
||||
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(
|
||||
<NativeChatToolRun
|
||||
blocks={[]}
|
||||
subagentGroups={[group([])]}
|
||||
expandSignal={false}
|
||||
expandOverride={false}
|
||||
activeTurnIsWorking={false}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('renders the roster alongside the tool activity of its turn', () => {
|
||||
render(
|
||||
<NativeChatToolRun
|
||||
|
||||
@@ -210,13 +210,18 @@ export function NativeChatToolRun({
|
||||
// Re-sync when the global toolbar toggle flips.
|
||||
useEffect(() => setOpen(expandOverride ?? expandSignal), [expandOverride, expandSignal])
|
||||
|
||||
const subagentRows = subagentGroups.map((group) => (
|
||||
<NativeChatSubagentRun
|
||||
key={group.groupId}
|
||||
block={group}
|
||||
activeTurnIsWorking={activeTurnIsWorking}
|
||||
/>
|
||||
))
|
||||
// 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) => (
|
||||
<NativeChatSubagentRun
|
||||
key={group.groupId}
|
||||
block={group}
|
||||
activeTurnIsWorking={activeTurnIsWorking}
|
||||
/>
|
||||
))
|
||||
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 ? <div className="mt-3">{subagentRows}</div> : 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 ? <div className="mt-3">{subagentRows}</div> : 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.
|
||||
|
||||
Reference in New Issue
Block a user