fold sub-agent prompts via parent_tool_use_id (T-338)
In live stream-json sessions a sub-agent's spawning prompt is tagged with parent_tool_use_id, not the transcript JSONL's isSidechain + parentUuid. The parser ignored that field, so the prompt parsed as a main-thread user turn and rendered as a blue "you" card above the Activity Agent card instead of folding into it. Carry parent_tool_use_id onto ConversationItem; its presence now marks the item as a sidechain message. The sidechain fold resolves ownership directly by tool-use id (no transcript-only uuid chain to walk), so the prompt folds into its Agent card and the run nests under it as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3186,3 +3186,4 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
|
|||||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3KS499THAD899M0NWN7E3R', 'status', 'backlog', 'ready', NULL, '2026-06-10 13:53:55', '2026-06-10 13:53:55', '2026-06-10 13:53:55', NULL, '72773d8d91af176305bd6c1b7d5d65be', 2) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3KS499THAD899M0NWN7E3R', 'status', 'backlog', 'ready', NULL, '2026-06-10 13:53:55', '2026-06-10 13:53:55', '2026-06-10 13:53:55', NULL, '72773d8d91af176305bd6c1b7d5d65be', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JM0AXK1CTWD720RV1AVZ0', 'status', 'backlog', 'ready', NULL, '2026-06-10 13:54:02', '2026-06-10 13:54:02', '2026-06-10 13:54:02', NULL, '5cb488441033ac5a781d4b7d91dbda6f', 2) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JM0AXK1CTWD720RV1AVZ0', 'status', 'backlog', 'ready', NULL, '2026-06-10 13:54:02', '2026-06-10 13:54:02', '2026-06-10 13:54:02', NULL, '5cb488441033ac5a781d4b7d91dbda6f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F2TBJJV1F2KP7XR8', 'status', 'in_progress', 'done', NULL, '2026-06-10 13:55:30', '2026-06-10 13:55:30', '2026-06-10 13:55:30', NULL, 'f618ee8a3a33dabfa822bd0cebae4e03', 2) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F2TBJJV1F2KP7XR8', 'status', 'in_progress', 'done', NULL, '2026-06-10 13:55:30', '2026-06-10 13:55:30', '2026-06-10 13:55:30', NULL, 'f618ee8a3a33dabfa822bd0cebae4e03', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JAXDKZMS0805MMEYB6820', 'status', 'ready', 'done', NULL, '2026-06-10 14:03:10', '2026-06-10 14:03:10', '2026-06-10 14:03:10', NULL, '96365bc3ef7dbf505448be2dad16e4b6', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
|||||||
@@ -2582,3 +2582,12 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
|
|||||||
- Markdown rendering is clide-owned custom CustomPaint/widgets (not a package), so detection + hit-testing lands in the conversation/markdown render path.
|
- Markdown rendering is clide-owned custom CustomPaint/widgets (not a package), so detection + hit-testing lands in the conversation/markdown render path.
|
||||||
- Honors User/Claude parity (D-6): the click maps to the existing `clide editor open` verb.
|
- Honors User/Claude parity (D-6): the click maps to the existing `clide editor open` verb.
|
||||||
- Open questions to settle during design: how aggressive path detection should be (existence check vs. heuristic), handling of non-existent / external paths, and visual affordance (underline/hover) for a linkified ref.', 'done', 'medium', NULL, NULL, NULL, '2026-06-09 20:02:41', '2026-06-10 13:55:30', NULL, 'd4022c088022495e2a0bda625a45c636', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
- Open questions to settle during design: how aggressive path detection should be (existence check vs. heuristic), handling of non-existent / external paths, and visual affordance (underline/hover) for a linkified ref.', 'done', 'medium', NULL, NULL, NULL, '2026-06-09 20:02:41', '2026-06-10 13:55:30', NULL, 'd4022c088022495e2a0bda625a45c636', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JAXDKZMS0805MMEYB6820', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Agent prompt renders as a ''you'' card instead of folding into the Activity Agent card', 'When Claude spawns a sub-agent (Agent/Task tool, rendered as an ''Activity Agent'' card), the prompt handed to the sub-agent renders as a separate top-level UserMessage card labelled ''you'' (blue, globalFocus accent) above the card, instead of folding into the Activity Agent card as a muted ''agent prompt'' (the T-263 behaviour).
|
||||||
|
|
||||||
|
Repro: launch any sub-agent from the Claude pane; the task prompt appears as a ''you'' turn.
|
||||||
|
|
||||||
|
Root cause: the sidechain fold keys entirely off the transcript envelope. In transcript_reader.dart the prompt''s UserMessage carries isSidechain/parentUuid straight from envelope[''isSidechain''] and envelope[''parentUuid''] (transcript_reader.dart:558-560, parsed in _parseUserInto at :593-643). conversation_view.dart _sidechainFold() (:179-233) only claims an item when it.isSidechain is true AND resolveOwner() walks its parentUuid chain up to the Agent tool_use message (or the nearest preceding Agent as fallback). If the spawning prompt''s envelope arrives WITHOUT isSidechain==true (or without a parentUuid that roots at the Agent), the item is never added to ownedSidechainUuids, _visibleItems() (:116-160) never drops it, and _ConversationTurn.build() falls through past the ''when i.injected || i.isSidechain'' guard into the default UserMessage() case => label: ''you'' (the blue user card).
|
||||||
|
|
||||||
|
Need to confirm whether the prompt envelope is missing isSidechain (likely — the spawning prompt is the FIRST user turn of the sidechain and may be emitted by the harness without the flag, or with a parentUuid that points at the spawning turn rather than the Agent tool_use). Check stream_json_session.dart emission too.
|
||||||
|
|
||||||
|
Fix direction (TBD after confirming the envelope shape): either (a) ensure the prompt envelope is correctly flagged/parented, or (b) in _sidechainFold associate the first sidechain-adjacent user prompt with its Agent tool_use even when the flag/chain is incomplete. Related: T-263 (prompt folds into the call), T-264 (run nests under the card).', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 13:47:04', '2026-06-10 14:03:10', NULL, 'ee474dfe790eb6e72bd6a7381cd900e2', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Sub-agent prompts no longer render as a blue "you" card.** In live
|
||||||
|
(stream-json) sessions a sub-agent's spawning prompt is tagged with
|
||||||
|
`parent_tool_use_id` rather than the transcript's `isSidechain`/`parentUuid`,
|
||||||
|
so it slipped past the sidechain fold and showed as a top-level user turn. The
|
||||||
|
parser now treats `parent_tool_use_id` as a sidechain marker and routes the
|
||||||
|
item straight to its Agent card by tool-use id. (T-338)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Clickable file references in the Claude conversation.** Workspace file paths
|
- **Clickable file references in the Claude conversation.** Workspace file paths
|
||||||
|
|||||||
@@ -182,6 +182,13 @@ class _ConversationViewState extends State<ConversationView> {
|
|||||||
for (final it in items)
|
for (final it in items)
|
||||||
if (it is AssistantToolUse && _isAgentTool(it.name)) it.uuid: it,
|
if (it is AssistantToolUse && _isAgentTool(it.name)) it.uuid: it,
|
||||||
};
|
};
|
||||||
|
// Stream-json tags sidechain items with the spawning Agent's tool-use id
|
||||||
|
// directly (T-338), so map Agent cards by tool-use id for a direct lookup
|
||||||
|
// that doesn't depend on the (transcript-only) parentUuid chain.
|
||||||
|
final agentByToolUseId = <String, AssistantToolUse>{
|
||||||
|
for (final it in items)
|
||||||
|
if (it is AssistantToolUse && _isAgentTool(it.name)) it.toolUseId: it,
|
||||||
|
};
|
||||||
// Envelope-level chain info (consistent across items sharing a uuid).
|
// Envelope-level chain info (consistent across items sharing a uuid).
|
||||||
final parentByUuid = <String, String?>{};
|
final parentByUuid = <String, String?>{};
|
||||||
final sidechainByUuid = <String, bool>{};
|
final sidechainByUuid = <String, bool>{};
|
||||||
@@ -193,6 +200,13 @@ class _ConversationViewState extends State<ConversationView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AssistantToolUse? resolveOwner(ConversationItem item, AssistantToolUse? nearest) {
|
AssistantToolUse? resolveOwner(ConversationItem item, AssistantToolUse? nearest) {
|
||||||
|
// Direct route: stream-json hands us the spawning Agent's tool-use id on
|
||||||
|
// the item itself (T-338) — no chain to walk.
|
||||||
|
final byTool = item.parentToolUseId;
|
||||||
|
if (byTool != null) {
|
||||||
|
final agent = agentByToolUseId[byTool];
|
||||||
|
if (agent != null) return agent;
|
||||||
|
}
|
||||||
var cur = item.uuid;
|
var cur = item.uuid;
|
||||||
final seen = <String>{};
|
final seen = <String>{};
|
||||||
while (seen.add(cur)) {
|
while (seen.add(cur)) {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import 'dart:isolate';
|
|||||||
|
|
||||||
/// Discriminated union of conversation items the reader can emit.
|
/// Discriminated union of conversation items the reader can emit.
|
||||||
sealed class ConversationItem {
|
sealed class ConversationItem {
|
||||||
const ConversationItem({required this.uuid, required this.timestamp, required this.isSidechain, this.parentUuid});
|
const ConversationItem({required this.uuid, required this.timestamp, required this.isSidechain, this.parentUuid, this.parentToolUseId});
|
||||||
|
|
||||||
final String uuid;
|
final String uuid;
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
@@ -50,6 +50,14 @@ sealed class ConversationItem {
|
|||||||
/// branches off the assistant message that issued its spawning Agent/Task
|
/// branches off the assistant message that issued its spawning Agent/Task
|
||||||
/// tool-use, so this links the prompt to the right Agent card (T-263).
|
/// tool-use, so this links the prompt to the right Agent card (T-263).
|
||||||
final String? parentUuid;
|
final String? parentUuid;
|
||||||
|
|
||||||
|
/// The `parent_tool_use_id` from the stream-json wire (T-338): the tool-use
|
||||||
|
/// id of the Agent/Task call that spawned this sub-agent message. Stream-json
|
||||||
|
/// tags every sidechain item with it — the transcript JSONL instead uses
|
||||||
|
/// [isSidechain] + [parentUuid]. When present it routes the item straight to
|
||||||
|
/// its Agent card by tool-use id, no uuid-chain walk needed, and on its own
|
||||||
|
/// marks the item as a sidechain message.
|
||||||
|
final String? parentToolUseId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A user-typed message (plain text, possibly multi-part).
|
/// A user-typed message (plain text, possibly multi-part).
|
||||||
@@ -59,6 +67,7 @@ final class UserMessage extends ConversationItem {
|
|||||||
required super.timestamp,
|
required super.timestamp,
|
||||||
required super.isSidechain,
|
required super.isSidechain,
|
||||||
super.parentUuid,
|
super.parentUuid,
|
||||||
|
super.parentToolUseId,
|
||||||
required this.text,
|
required this.text,
|
||||||
this.injected = false,
|
this.injected = false,
|
||||||
});
|
});
|
||||||
@@ -82,6 +91,7 @@ final class ToolResultMessage extends ConversationItem {
|
|||||||
required super.timestamp,
|
required super.timestamp,
|
||||||
required super.isSidechain,
|
required super.isSidechain,
|
||||||
super.parentUuid,
|
super.parentUuid,
|
||||||
|
super.parentToolUseId,
|
||||||
required this.toolUseId,
|
required this.toolUseId,
|
||||||
required this.content,
|
required this.content,
|
||||||
required this.isError,
|
required this.isError,
|
||||||
@@ -102,6 +112,7 @@ final class AssistantTextMessage extends ConversationItem {
|
|||||||
required super.timestamp,
|
required super.timestamp,
|
||||||
required super.isSidechain,
|
required super.isSidechain,
|
||||||
super.parentUuid,
|
super.parentUuid,
|
||||||
|
super.parentToolUseId,
|
||||||
required this.text,
|
required this.text,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,6 +129,7 @@ final class AssistantThinkingMessage extends ConversationItem {
|
|||||||
required super.timestamp,
|
required super.timestamp,
|
||||||
required super.isSidechain,
|
required super.isSidechain,
|
||||||
super.parentUuid,
|
super.parentUuid,
|
||||||
|
super.parentToolUseId,
|
||||||
required this.thinking,
|
required this.thinking,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,6 +146,7 @@ final class AssistantToolUse extends ConversationItem {
|
|||||||
required super.timestamp,
|
required super.timestamp,
|
||||||
required super.isSidechain,
|
required super.isSidechain,
|
||||||
super.parentUuid,
|
super.parentUuid,
|
||||||
|
super.parentToolUseId,
|
||||||
required this.toolUseId,
|
required this.toolUseId,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.input,
|
required this.input,
|
||||||
@@ -555,9 +568,14 @@ void _parseLineInto(String line, List<ConversationItem> out, List<String> warnin
|
|||||||
if (_skipTypes.contains(type)) return;
|
if (_skipTypes.contains(type)) return;
|
||||||
|
|
||||||
final uuid = envelope['uuid'] as String? ?? '';
|
final uuid = envelope['uuid'] as String? ?? '';
|
||||||
final isSidechain = envelope['isSidechain'] as bool? ?? false;
|
|
||||||
final rawParent = envelope['parentUuid'] as String?;
|
final rawParent = envelope['parentUuid'] as String?;
|
||||||
final parentUuid = (rawParent != null && rawParent.isNotEmpty) ? rawParent : null;
|
final parentUuid = (rawParent != null && rawParent.isNotEmpty) ? rawParent : null;
|
||||||
|
// Stream-json tags sub-agent messages with `parent_tool_use_id` (the spawning
|
||||||
|
// Agent/Task tool-use), not the transcript's isSidechain/parentUuid (T-338).
|
||||||
|
// Treat its presence as a sidechain marker so the fold + de-emphasis kick in.
|
||||||
|
final rawParentTool = envelope['parent_tool_use_id'] as String?;
|
||||||
|
final parentToolUseId = (rawParentTool != null && rawParentTool.isNotEmpty) ? rawParentTool : null;
|
||||||
|
final isSidechain = (envelope['isSidechain'] as bool? ?? false) || parentToolUseId != null;
|
||||||
|
|
||||||
DateTime timestamp;
|
DateTime timestamp;
|
||||||
try {
|
try {
|
||||||
@@ -568,9 +586,9 @@ void _parseLineInto(String line, List<ConversationItem> out, List<String> warnin
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'user':
|
case 'user':
|
||||||
_parseUserInto(envelope, uuid, timestamp, isSidechain, parentUuid, out);
|
_parseUserInto(envelope, uuid, timestamp, isSidechain, parentUuid, parentToolUseId, out);
|
||||||
case 'assistant':
|
case 'assistant':
|
||||||
_parseAssistantInto(envelope, uuid, timestamp, isSidechain, parentUuid, out);
|
_parseAssistantInto(envelope, uuid, timestamp, isSidechain, parentUuid, parentToolUseId, out);
|
||||||
_extractAssistantStatus(envelope, status);
|
_extractAssistantStatus(envelope, status);
|
||||||
default:
|
default:
|
||||||
break; // unknown type — degrade gracefully
|
break; // unknown type — degrade gracefully
|
||||||
@@ -596,6 +614,7 @@ void _parseUserInto(
|
|||||||
DateTime timestamp,
|
DateTime timestamp,
|
||||||
bool isSidechain,
|
bool isSidechain,
|
||||||
String? parentUuid,
|
String? parentUuid,
|
||||||
|
String? parentToolUseId,
|
||||||
List<ConversationItem> out,
|
List<ConversationItem> out,
|
||||||
) {
|
) {
|
||||||
final message = envelope['message'] as Map?;
|
final message = envelope['message'] as Map?;
|
||||||
@@ -609,7 +628,14 @@ void _parseUserInto(
|
|||||||
|
|
||||||
if (content is String) {
|
if (content is String) {
|
||||||
if (content.isNotEmpty) {
|
if (content.isNotEmpty) {
|
||||||
out.add(UserMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, parentUuid: parentUuid, text: content, injected: injected));
|
out.add(UserMessage(
|
||||||
|
uuid: uuid,
|
||||||
|
timestamp: timestamp,
|
||||||
|
isSidechain: isSidechain,
|
||||||
|
parentUuid: parentUuid,
|
||||||
|
parentToolUseId: parentToolUseId,
|
||||||
|
text: content,
|
||||||
|
injected: injected));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -629,6 +655,7 @@ void _parseUserInto(
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
isSidechain: isSidechain,
|
isSidechain: isSidechain,
|
||||||
parentUuid: parentUuid,
|
parentUuid: parentUuid,
|
||||||
|
parentToolUseId: parentToolUseId,
|
||||||
toolUseId: item['tool_use_id'] as String? ?? '',
|
toolUseId: item['tool_use_id'] as String? ?? '',
|
||||||
content: rawContent is String ? rawContent : jsonEncode(rawContent),
|
content: rawContent is String ? rawContent : jsonEncode(rawContent),
|
||||||
isError: item['is_error'] as bool? ?? false,
|
isError: item['is_error'] as bool? ?? false,
|
||||||
@@ -648,6 +675,7 @@ void _parseAssistantInto(
|
|||||||
DateTime timestamp,
|
DateTime timestamp,
|
||||||
bool isSidechain,
|
bool isSidechain,
|
||||||
String? parentUuid,
|
String? parentUuid,
|
||||||
|
String? parentToolUseId,
|
||||||
List<ConversationItem> out,
|
List<ConversationItem> out,
|
||||||
) {
|
) {
|
||||||
final message = envelope['message'] as Map?;
|
final message = envelope['message'] as Map?;
|
||||||
@@ -661,12 +689,14 @@ void _parseAssistantInto(
|
|||||||
case 'text':
|
case 'text':
|
||||||
final text = item['text'] as String? ?? '';
|
final text = item['text'] as String? ?? '';
|
||||||
if (text.isNotEmpty) {
|
if (text.isNotEmpty) {
|
||||||
out.add(AssistantTextMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, parentUuid: parentUuid, text: text));
|
out.add(AssistantTextMessage(
|
||||||
|
uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, parentUuid: parentUuid, parentToolUseId: parentToolUseId, text: text));
|
||||||
}
|
}
|
||||||
case 'thinking':
|
case 'thinking':
|
||||||
final thinking = item['thinking'] as String? ?? '';
|
final thinking = item['thinking'] as String? ?? '';
|
||||||
if (thinking.isNotEmpty) {
|
if (thinking.isNotEmpty) {
|
||||||
out.add(AssistantThinkingMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, parentUuid: parentUuid, thinking: thinking));
|
out.add(AssistantThinkingMessage(
|
||||||
|
uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, parentUuid: parentUuid, parentToolUseId: parentToolUseId, thinking: thinking));
|
||||||
}
|
}
|
||||||
case 'tool_use':
|
case 'tool_use':
|
||||||
final rawInput = item['input'];
|
final rawInput = item['input'];
|
||||||
@@ -675,6 +705,7 @@ void _parseAssistantInto(
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
isSidechain: isSidechain,
|
isSidechain: isSidechain,
|
||||||
parentUuid: parentUuid,
|
parentUuid: parentUuid,
|
||||||
|
parentToolUseId: parentToolUseId,
|
||||||
toolUseId: item['id'] as String? ?? '',
|
toolUseId: item['id'] as String? ?? '',
|
||||||
name: item['name'] as String? ?? '',
|
name: item['name'] as String? ?? '',
|
||||||
input: rawInput is Map ? rawInput.cast<String, dynamic>() : <String, dynamic>{},
|
input: rawInput is Map ? rawInput.cast<String, dynamic>() : <String, dynamic>{},
|
||||||
|
|||||||
@@ -430,6 +430,24 @@ void main() {
|
|||||||
expect(find.text('find all the widgets'), findsOneWidget);
|
expect(find.text('find all the widgets'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('a stream-json prompt folds via parentToolUseId, not "you" (T-338)', (tester) async {
|
||||||
|
// The live stream-json wire flags sub-agent prompts with parent_tool_use_id
|
||||||
|
// (the Task tool-use id) and NO isSidechain/parentUuid — the prompt must
|
||||||
|
// still fold into the Agent card rather than render as a blue "you" turn.
|
||||||
|
await pumpWith(tester, [
|
||||||
|
AssistantToolUse(uuid: 'agt-msg', timestamp: _t, isSidechain: false, toolUseId: 'task1', name: 'Task', input: const {'description': 'explore'}),
|
||||||
|
UserMessage(uuid: 'sp', timestamp: _t, isSidechain: true, parentToolUseId: 'task1', text: 'find all the widgets'),
|
||||||
|
]);
|
||||||
|
expect(find.text('you'), findsNothing);
|
||||||
|
expect(find.text('agent prompt'), findsNothing);
|
||||||
|
expect(find.text('Task'), findsOneWidget);
|
||||||
|
expect(find.text('find all the widgets'), findsNothing); // folded, collapsed
|
||||||
|
|
||||||
|
await tester.tap(find.bySemanticsLabel('Task, 1 step, collapsed'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('find all the widgets'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('an orphan sidechain prompt renders as muted "agent prompt", never "you" (T-263)', (tester) async {
|
testWidgets('an orphan sidechain prompt renders as muted "agent prompt", never "you" (T-263)', (tester) async {
|
||||||
// No Agent tool-use to attach to → stays standalone, but relabelled.
|
// No Agent tool-use to attach to → stays standalone, but relabelled.
|
||||||
await pumpWith(tester, [
|
await pumpWith(tester, [
|
||||||
|
|||||||
@@ -378,6 +378,30 @@ void main() {
|
|||||||
expect(items[0].parentUuid, 'msg-A');
|
expect(items[0].parentUuid, 'msg-A');
|
||||||
expect(items[1].parentUuid, isNull); // '' → null
|
expect(items[1].parentUuid, isNull); // '' → null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('stream-json parent_tool_use_id marks a sidechain message (T-338)', () {
|
||||||
|
// The stream-json wire tags sub-agent messages with parent_tool_use_id and
|
||||||
|
// NO isSidechain flag — we must treat it as a sidechain item anyway.
|
||||||
|
final raw = envelope(
|
||||||
|
type: 'user',
|
||||||
|
uuid: 'sp',
|
||||||
|
message: {'role': 'user', 'content': 'go explore the codebase'},
|
||||||
|
)..['parent_tool_use_id'] = 'toolu_task1';
|
||||||
|
final items = parseAll([raw]);
|
||||||
|
expect(items.first.isSidechain, isTrue);
|
||||||
|
expect(items.first.parentToolUseId, 'toolu_task1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty parent_tool_use_id normalises to null and stays main-thread (T-338)', () {
|
||||||
|
final raw = envelope(
|
||||||
|
type: 'user',
|
||||||
|
uuid: 'mt',
|
||||||
|
message: {'role': 'user', 'content': 'a normal turn'},
|
||||||
|
)..['parent_tool_use_id'] = '';
|
||||||
|
final items = parseAll([raw]);
|
||||||
|
expect(items.first.isSidechain, isFalse);
|
||||||
|
expect(items.first.parentToolUseId, isNull);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user