nest the whole sub-agent run under its Agent card (T-264)

A sub-agent's sidechain run used to spill loose into the main chain,
indistinguishable from main-thread items. Now:

- _sidechainFold routes every sidechain item to its owning Agent/Task
  tool-use by walking the parentUuid chain up to the Agent message it
  branches off (nearest-preceding Agent as fallback) — correct even for
  parallel agents.
- The run (prose / thinking / tool cards) nests in an "agent run"
  ClideHolderCard UNDER the Agent card, suppressed from the top level. The
  prompt still folds into the call (T-263); a successful sidechain tool
  result folds into its own tool card inside the run, so it isn't a
  separate step.
- When a run is shown, the Agent card's returned-result segment is dropped
  (it duplicates the run's final output, note E) — but kept when no run
  was captured, so output is never lost.

Tests: run nesting, returned-result dedup, parallel-run attachment (would
fail under nearest-preceding), and folded-result-not-double-counted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 11:26:09 +02:00
co-authored by Claude Opus 4.8
parent 5253b7b759
commit db6ecbd5df
5 changed files with 223 additions and 47 deletions
@@ -330,6 +330,69 @@ void main() {
expect(find.text('PROMPT FOR B'), findsNothing);
});
testWidgets('the sub-agent run nests in a holder under its Agent card (T-264)', (tester) async {
await pumpWith(tester, [
AssistantToolUse(uuid: 'mA', timestamp: _t, isSidechain: false, toolUseId: 'tA', name: 'Task', input: const {'description': 'explore'}),
UserMessage(uuid: 'p', timestamp: _t, isSidechain: true, parentUuid: 'mA', text: 'go explore'),
// A sidechain tool call — part of the run, chained off the prompt.
AssistantToolUse(
uuid: 's1', timestamp: _t, isSidechain: true, parentUuid: 'p', toolUseId: 'sb', name: 'Bash', input: const {'command': 'grep widgets'}),
]);
expect(find.text('Task'), findsOneWidget);
// The run is a nested holder titled "agent run", collapsed by default —
// the run's Bash card is not loose in the main chain.
expect(find.bySemanticsLabel('agent run, 1 step, collapsed'), findsOneWidget);
expect(find.text('Bash'), findsNothing); // run card hidden while holder collapsed
await tester.tap(find.bySemanticsLabel('agent run, 1 step, collapsed'));
await tester.pumpAndSettle();
expect(find.text('Bash'), findsOneWidget); // the run's tool card, now nested + visible
});
testWidgets('the returned result is not duplicated when the run is shown (T-264 note E)', (tester) async {
await pumpWith(tester, [
AssistantToolUse(uuid: 'mA', timestamp: _t, isSidechain: false, toolUseId: 'tA', name: 'Task', input: const {'description': 'x'}),
UserMessage(uuid: 'p', timestamp: _t, isSidechain: true, parentUuid: 'mA', text: 'go'),
AssistantTextMessage(uuid: 's1', timestamp: _t, isSidechain: true, parentUuid: 'p', text: 'THE FINAL ANSWER'),
// The Task's returned result (main chain) — equals the run's final prose.
ToolResultMessage(uuid: 'tr', timestamp: _t, isSidechain: false, parentUuid: 'mA', toolUseId: 'tA', content: 'THE FINAL ANSWER', isError: false),
]);
// Expand the Agent card (its "result" segment would show here if kept)…
await tester.tap(find.bySemanticsLabel('Expand'));
await tester.pumpAndSettle();
// …and the nested run.
await tester.tap(find.bySemanticsLabel('agent run, 1 step, collapsed'));
await tester.pumpAndSettle();
// The answer appears once (in the run), not also as a folded result segment.
expect(find.textContaining('THE FINAL ANSWER'), findsOneWidget);
expect(find.text('result'), findsNothing); // no result sub-label on the Agent card
});
testWidgets('parallel agents: each run nests under its own card via parentUuid (T-264)', (tester) async {
await pumpWith(tester, [
AssistantToolUse(uuid: 'mA', timestamp: _t, isSidechain: false, toolUseId: 'tA', name: 'Task', input: const {'description': 'A'}),
AssistantToolUse(uuid: 'mB', timestamp: _t, isSidechain: false, toolUseId: 'tB', name: 'Task', input: const {'description': 'B'}),
AssistantToolUse(uuid: 'sA', timestamp: _t, isSidechain: true, parentUuid: 'mA', toolUseId: 'sbA', name: 'Bash', input: const {'command': 'CMD_A'}),
AssistantToolUse(uuid: 'sB', timestamp: _t, isSidechain: true, parentUuid: 'mB', toolUseId: 'sbB', name: 'Bash', input: const {'command': 'CMD_B'}),
]);
// Correct routing → two separate 1-step runs. A nearest-preceding heuristic
// would pool both under agent B as one 2-step run.
expect(find.bySemanticsLabel('agent run, 1 step, collapsed'), findsNWidgets(2));
expect(find.bySemanticsLabel('agent run, 2 steps, collapsed'), findsNothing);
});
testWidgets('a successful sidechain result folds into its run tool card, not a separate step (T-264)', (tester) async {
await pumpWith(tester, [
AssistantToolUse(uuid: 'mA', timestamp: _t, isSidechain: false, toolUseId: 'tA', name: 'Task', input: const {'description': 'x'}),
UserMessage(uuid: 'p', timestamp: _t, isSidechain: true, parentUuid: 'mA', text: 'go'),
AssistantToolUse(uuid: 's1', timestamp: _t, isSidechain: true, parentUuid: 'p', toolUseId: 'sb', name: 'Bash', input: const {'command': 'ls'}),
ToolResultMessage(uuid: 'sr', timestamp: _t, isSidechain: true, parentUuid: 's1', toolUseId: 'sb', content: 'file listing', isError: false),
]);
// The tool call + its success result is ONE step in the run, not two.
expect(find.bySemanticsLabel('agent run, 1 step, collapsed'), findsOneWidget);
expect(find.bySemanticsLabel('agent run, 2 steps, collapsed'), findsNothing);
});
testWidgets('a permission-prompted tool-use is hidden but its result is kept', (tester) async {
await pumpWith(
tester,