From a7a7b04a05ec51a2a134bbd32b1d76f1a3cf48f7 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 8 Jun 2026 10:45:48 +0200 Subject: [PATCH] =?UTF-8?q?fold=20sub-agent=20prompt=20into=20the=20Agent?= =?UTF-8?q?=20card;=20relabel=20you=20=E2=86=92=20agent=20prompt=20(T-263)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A sidechain sub-agent prompt was rendered with the blue "you" label, falsely implying the user typed it. Now: - transcript_reader parses parentUuid (was dropped) onto every ConversationItem. - conversation_view resolves each sidechain prompt to its spawning Agent/Task card via parentUuid (nearest-preceding Agent as fallback), folds the prompt into that card as a collapsed "prompt" segment, and suppresses the standalone block. Layered order when expanded: call → prompt → result (note E). - A sidechain UserMessage never gets the "you" treatment: folded into its card, or — when orphaned — rendered as a muted standalone "agent prompt". Tests: parentUuid parse, fold + suppression, parallel-agent attachment (would fail under a nearest-preceding heuristic), orphan relabel, and a golden for the Agent card's call → prompt → result layering. Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 1 + .pql/changelog/tickets/2026-06.sql | 27 +++++ CHANGELOG.md | 5 + lib/builtin/claude/src/conversation_view.dart | 111 ++++++++++++++---- lib/builtin/claude/src/transcript_reader.dart | 31 +++-- .../claude/conversation_view_test.dart | 48 ++++++++ .../claude/transcript_reader_test.dart | 17 +++ .../conversation_card_goldens_test.dart | 19 +++ .../linux/conversation_card_merged.png | Bin 28289 -> 43870 bytes 9 files changed, 227 insertions(+), 32 deletions(-) diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 08d969ac..240c41a6 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -814,3 +814,4 @@ Once this lands, loose UI/UX work + bugs can move from the T-276 tracker-epic to Relevant: lib/builtin/tickets/, lib/src/pql/ (pql wrapper), and pql''s ''ticket label'' / ''ticket list'' label fields.', NULL, '2026-06-08 08:34:24', '2026-06-08 08:34:24', '2026-06-08 08:34:24', NULL, '499355d06a5e7618a1c32e63a6cd1bcd', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-277', 'parent_id', NULL, 'T-276', NULL, '2026-06-08 08:34:47', '2026-06-08 08:34:47', '2026-06-08 08:34:47', NULL, '67d78cbf1e92166d60e45b3f51ed440e', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-263', 'status', 'in_progress', 'done', NULL, '2026-06-08 08:45:23', '2026-06-08 08:45:23', '2026-06-08 08:45:23', NULL, '7e18bb85a36603663881ebd55d77d308', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 1f7956ed..193379e9 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -1846,3 +1846,30 @@ Scope: Once this lands, loose UI/UX work + bugs can move from the T-276 tracker-epic to a label (e.g. ''ui-tweak''); file that migration as a follow-up — do NOT auto-close T-276 (it''s marked never-close). Relevant: lib/builtin/tickets/, lib/src/pql/ (pql wrapper), and pql''s ''ticket label'' / ''ticket list'' label fields.', 'backlog', 'medium', NULL, NULL, 'D-3', '2026-06-08 08:32:30', '2026-06-08 08:34:47', NULL, 'da5dd0bb879b9d088e009c735700c935', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-263', 'story', 'T-267', 'Fold sub-agent prompt into the Agent card; relabel from you to agent prompt', 'When Claude launches a sub-agent (the Agent/Task tool), the sub-agent conversation runs as a SIDECHAIN. The prompt Claude wrote for that sub-agent comes through the transcript as a UserMessage with isSidechain=true but injected=false. The conversation view does not look at isSidechain, so it renders that prompt with the blue "you" label exactly like real user input — FALSELY implying the user sent it. It sits as a standalone block below the Agent tool-use card. This is misleading: the user did not write that prompt. + +Confirmed facts from the code: every transcript envelope carries BOTH isSidechain AND parentUuid (transcript_reader.dart ~L547 parses isSidechain but never USES it; parentUuid is dropped entirely). The Agent/Task tool-use is rendered by _toolUse in lib/builtin/claude/src/conversation_view.dart (only AskUserQuestion is special-cased today). The "you" vs "context" label is chosen in conversation_view.dart (~L200): UserMessage with injected=true → "context" (muted); else → "you" (blue, tokens.globalFocus). isSidechain is currently never read by any rendering/grouping logic. + +Design (confirmed with user): +1. Never label a sidechain prompt "you". A UserMessage with isSidechain=true is an AGENT PROMPT, not user input — label it "agent prompt" with muted / de-emphasized styling (same precedent as injected "context" messages, D-78). Never the blue "you" treatment. +2. Fold the prompt INTO the Agent tool-use card above it, COLLAPSED BY DEFAULT. The Agent card becomes the container that owns its prompt: expand the card to reveal the prompt; collapsed (default) it is hidden. The standalone prompt block is SUPPRESSED from the list — same fold-a-standalone-item-into-its-card pattern as T-262. +3. Prompt folds into the CALL, not the results. Only the prompt folds into the collapsed Agent call card. The sub-agent work (its thinking / tool calls / results) stays VISIBLE as the results chain after the call — not buried. Shape: [collapsed Agent call, owning the prompt] then [the run results], with no misleading "you" block anywhere. Agent prompts are valuable enough to keep with the call rather than fold into the subsequent results chain. +4. Link the prompt to the RIGHT Agent card via parentUuid. Parse parentUuid (currently dropped) so each sidechain prompt attaches to its spawning Agent tool-use. This matters when several agents run in parallel in one turn — a positional "nearest preceding Agent" heuristic would misattach; the parent link is the robust route. Heuristic only as a fallback when the link cannot be resolved. + +Touch points: +- transcript_reader.dart: capture parentUuid from the envelope and expose it on ConversationItem (it is present in the JSONL, just not parsed). +- conversation_view.dart: relabel sidechain UserMessage to "agent prompt" (stop the "you" path); fold it into the Agent card body; special-case the Agent/Task tool name (today only AskUserQuestion is special-cased); suppress the standalone block. +- activity_cluster.dart: ensure the suppressed prompt is not double-counted in grouping; leave the sidechain results in the chain. + +Edge cases: +- Parallel agents in one turn → each prompt attaches to its own card (the reason for parentUuid). +- Orphan prompt (no resolvable parent) → still render as "agent prompt", muted/collapsed, never "you". +- Nested / background sub-agents. + +Tests: unit/widget coverage for sidechain-prompt relabel (never "you"), folding into the Agent card (collapsed hides, expand reveals), suppression of the standalone block, parentUuid attachment with parallel agents, and the orphan fallback. Add a golden for the Agent card with a folded prompt. + +Refs: D-78 (interaction zone / display-only conversation widgets; injected/context muting precedent). Related: T-262 (fold-standalone-item-into-its-card pattern). + +IMPLEMENTATION NOTE (E — Agent-card layered ordering, from streamlining analysis): + +With this ticket (fold prompt) and T-262 (merge success result) both landing, the Agent/Task card ends up owning multiple segments: the tool INPUT, the folded PROMPT, and — via T-262 — the sub-agents final returned RESULT (the Task ToolResultMessage). Define a deliberate layered order when expanded (e.g. call/input → prompt → returned result) with clear sub-labels/dividers so the Agent card stays readable and does not become a kitchen sink. Coordinate with T-262 (result merge) and T-264 (nesting the whole run): the nested run region vs the returned-result segment must not duplicate the sub-agent output.', 'done', 'medium', NULL, NULL, 'D-78', '2026-06-07 08:34:02', '2026-06-08 08:45:23', NULL, '561ac6a3308d08c0da27bcb23eb21cc0', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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); diff --git a/CHANGELOG.md b/CHANGELOG.md index e1f01af9..b7569f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -144,6 +144,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Changed +- A **sub-agent prompt** is no longer mislabelled as your input: a sidechain + prompt now reads as a muted **`agent prompt`** (never the blue `you`) and folds + into its **Agent/Task card**, collapsed by default. The prompt attaches to the + right card via `parentUuid`, so parallel agents in one turn stay correctly + paired. The sub-agent's work stays visible after the call. (T-263) - A successful tool call now renders as **one merged card** instead of a separate call + result pair: a green check sits at the header's right edge and the output folds in as a colorized code block (Read → file grammar, Bash → diff --git a/lib/builtin/claude/src/conversation_view.dart b/lib/builtin/claude/src/conversation_view.dart index 21648b7a..f785dfc5 100644 --- a/lib/builtin/claude/src/conversation_view.dart +++ b/lib/builtin/claude/src/conversation_view.dart @@ -93,7 +93,7 @@ class _ConversationViewState extends State { /// (its tool-use *and* result echo are noise — the prompt + the logged answer /// cover it), and any permission-prompted tool-use (keep its result — that's /// the useful answer). - List _visibleItems(List items) { + List _visibleItems(List items, Set foldedPromptUuids) { final hidden = widget.hiddenToolUseIds; final auqIds = { for (final it in items) @@ -114,6 +114,9 @@ class _ConversationViewState extends State { bool drop(ConversationItem it) { if (it is AssistantToolUse) return toolUseDropped(it); + // T-263: a sidechain agent prompt that folded into its Agent card is + // suppressed here so it doesn't also render as a standalone block. + if (it is UserMessage) return foldedPromptUuids.contains(it.uuid); if (it is ToolResultMessage) { if (auqIds.contains(it.toolUseId)) return true; // AUQ result echo — noise // T-262: a successful result whose paired tool-use is going to render @@ -134,6 +137,40 @@ class _ConversationViewState extends State { ]; } + /// Resolves which sidechain prompts fold into which Agent/Task card (T-263). + /// + /// A sidechain prompt's owner is the Agent tool-use its `parentUuid` branches + /// off — robust when several agents run in parallel in one turn. Falls back to + /// the nearest preceding Agent tool-use when the link can't be resolved. + /// Returns the prompt uuids to suppress and the prompts grouped by the owning + /// tool-use id (so the card can fold them). + ({Set foldedPromptUuids, Map> promptsByToolUseId}) _agentPromptFold(List items) { + final agentByMsgUuid = { + for (final it in items) + if (it is AssistantToolUse && _isAgentTool(it.name)) it.uuid: it, + }; + final folded = {}; + final byToolUseId = >{}; + AssistantToolUse? lastAgent; + for (final it in items) { + if (it is AssistantToolUse && _isAgentTool(it.name)) { + lastAgent = it; + continue; + } + // Only a text user message authored inside a sidechain is an agent + // prompt; harness-injected messages and tool results are not. + if (it is UserMessage && it.isSidechain && !it.injected) { + final viaParent = it.parentUuid != null ? agentByMsgUuid[it.parentUuid] : null; + final owner = viaParent ?? lastAgent; + if (owner != null) { + folded.add(it.uuid); + (byToolUseId[owner.toolUseId] ??= []).add(it); + } + } + } + return (foldedPromptUuids: folded, promptsByToolUseId: byToolUseId); + } + void _onChanged() { if (!mounted) return; setState(() {}); @@ -148,7 +185,11 @@ class _ConversationViewState extends State { @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - final items = _visibleItems(widget.controller.items); + final allItems = widget.controller.items; + // T-263: resolve sidechain prompts → owning Agent card before culling, so + // the standalone prompt block is suppressed and folded into its card. + final promptFold = _agentPromptFold(allItems); + final items = _visibleItems(allItems, promptFold.foldedPromptUuids); if (items.isEmpty) { return ColoredBox( @@ -161,7 +202,7 @@ class _ConversationViewState extends State { // fold a successful result in. Built from the full item list (not the // visible one — the success result is suppressed from `items`). final resultByToolUseId = { - for (final it in widget.controller.items) + for (final it in allItems) if (it is ToolResultMessage) it.toolUseId: it, }; @@ -183,6 +224,7 @@ class _ConversationViewState extends State { toolUseOutcomes: widget.toolUseOutcomes, toolUseById: widget.controller.toolUseById, resultByToolUseId: resultByToolUseId, + promptsByToolUseId: promptFold.promptsByToolUseId, ), FoldedCluster(:final items) => _ActivityCard( items: items, @@ -190,6 +232,7 @@ class _ConversationViewState extends State { toolUseOutcomes: widget.toolUseOutcomes, toolUseById: widget.controller.toolUseById, resultByToolUseId: resultByToolUseId, + promptsByToolUseId: promptFold.promptsByToolUseId, ), }; }, @@ -206,6 +249,10 @@ class _ConversationViewState extends State { /// used for the "claude" message card's stripe + label. const claudeAccent = Color(0xFFD97757); +/// The tool names that launch a sub-agent (sidechain). Claude Code emits +/// `Task`; the Agent SDK surface uses `Agent` — accept both (T-263). +bool _isAgentTool(String name) => name == 'Task' || name == 'Agent'; + /// One conversation item, rendered by kind. class _ConversationTurn extends StatelessWidget { const _ConversationTurn({ @@ -214,6 +261,7 @@ class _ConversationTurn extends StatelessWidget { this.toolUseOutcomes = const {}, this.toolUseById = const {}, this.resultByToolUseId = const {}, + this.promptsByToolUseId = const >{}, }); final ConversationItem item; @@ -227,30 +275,35 @@ class _ConversationTurn extends StatelessWidget { /// successful result into one merged card (T-262). final Map resultByToolUseId; + /// Index from an Agent/Task toolUseId → the sidechain prompt(s) it owns, so + /// the Agent card can fold its prompt in (T-263). + final Map> promptsByToolUseId; + @override Widget build(BuildContext context) { final i = item; return switch (i) { - UserMessage() => i.injected - // Harness-injected (skill load / command expansion / system - // reminder) — not typed by the user, so de-emphasise: a muted, - // collapsed "context" card rather than the "you" accent (D-78). - ? ConversationCard( - variant: ConversationCardVariant.bare, - accent: tokens.globalTextMuted, - label: 'context', - copyText: i.text, - collapsible: true, - collapsedByDefault: true, - collapsedSummary: _firstLine(i.text), - body: ClideText(i.text, muted: true, fontSize: clideFontMeta), - ) - : ConversationCard( - accent: tokens.globalFocus, - label: 'you', - copyText: i.text, - body: ClideMarkdown(i.text), - ), + // Harness-injected (skill load / command expansion / system reminder) and + // sidechain agent prompts are both NOT typed by the user, so de-emphasise: + // a muted, collapsed card rather than the blue "you" accent (D-78). A + // sidechain prompt here is an orphan one (its Agent card couldn't be + // resolved) — folded prompts are suppressed upstream (T-263). + UserMessage() when i.injected || i.isSidechain => ConversationCard( + variant: ConversationCardVariant.bare, + accent: tokens.globalTextMuted, + label: i.isSidechain ? 'agent prompt' : 'context', + copyText: i.text, + collapsible: true, + collapsedByDefault: true, + collapsedSummary: _firstLine(i.text), + body: ClideText(i.text, muted: true, fontSize: clideFontMeta), + ), + UserMessage() => ConversationCard( + accent: tokens.globalFocus, + label: 'you', + copyText: i.text, + body: ClideMarkdown(i.text), + ), AssistantTextMessage() => ConversationCard( accent: claudeAccent, label: 'claude', @@ -356,8 +409,13 @@ class _ConversationTurn extends StatelessWidget { final result = resultByToolUseId[t.toolUseId]; final succeeded = result != null && !result.isError; final status = result == null ? ConversationCardStatus.none : (result.isError ? ConversationCardStatus.error : ConversationCardStatus.success); - final segments = - succeeded ? [CardSegment(label: 'result', child: ClideCodeBlock(source: result.content, language: _resultLanguage(t)))] : const []; + // T-263: an Agent/Task card folds its sub-agent prompt(s) in. Layered order + // when expanded (note E): call input (body) → prompt → returned result. + final segments = [ + for (final p in promptsByToolUseId[t.toolUseId] ?? const []) + CardSegment(label: 'prompt', child: ClideText(p.text, muted: true, fontSize: clideFontMeta)), + if (succeeded) CardSegment(label: 'result', child: ClideCodeBlock(source: result.content, language: _resultLanguage(t))), + ]; // A resolved permission-prompted call: collapsed, green if approved / red // if denied — a quiet record of what was permitted (D-78). It still folds @@ -495,6 +553,7 @@ class _ActivityCard extends StatefulWidget { required this.toolUseOutcomes, required this.toolUseById, required this.resultByToolUseId, + required this.promptsByToolUseId, }); final List items; @@ -502,6 +561,7 @@ class _ActivityCard extends StatefulWidget { final Map toolUseOutcomes; final Map toolUseById; final Map resultByToolUseId; + final Map> promptsByToolUseId; @override State<_ActivityCard> createState() => _ActivityCardState(); @@ -570,6 +630,7 @@ class _ActivityCardState extends State<_ActivityCard> { toolUseOutcomes: widget.toolUseOutcomes, toolUseById: widget.toolUseById, resultByToolUseId: widget.resultByToolUseId, + promptsByToolUseId: widget.promptsByToolUseId, ), ], ), diff --git a/lib/builtin/claude/src/transcript_reader.dart b/lib/builtin/claude/src/transcript_reader.dart index 49149f68..90c3b467 100644 --- a/lib/builtin/claude/src/transcript_reader.dart +++ b/lib/builtin/claude/src/transcript_reader.dart @@ -39,11 +39,17 @@ import 'dart:isolate'; /// Discriminated union of conversation items the reader can emit. sealed class ConversationItem { - const ConversationItem({required this.uuid, required this.timestamp, required this.isSidechain}); + const ConversationItem({required this.uuid, required this.timestamp, required this.isSidechain, this.parentUuid}); final String uuid; final DateTime timestamp; final bool isSidechain; + + /// The uuid of this record's predecessor in its chain (from the transcript + /// envelope's `parentUuid`), or null when absent/empty. A sidechain prompt + /// 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). + final String? parentUuid; } /// A user-typed message (plain text, possibly multi-part). @@ -52,6 +58,7 @@ final class UserMessage extends ConversationItem { required super.uuid, required super.timestamp, required super.isSidechain, + super.parentUuid, required this.text, this.injected = false, }); @@ -74,6 +81,7 @@ final class ToolResultMessage extends ConversationItem { required super.uuid, required super.timestamp, required super.isSidechain, + super.parentUuid, required this.toolUseId, required this.content, required this.isError, @@ -93,6 +101,7 @@ final class AssistantTextMessage extends ConversationItem { required super.uuid, required super.timestamp, required super.isSidechain, + super.parentUuid, required this.text, }); @@ -108,6 +117,7 @@ final class AssistantThinkingMessage extends ConversationItem { required super.uuid, required super.timestamp, required super.isSidechain, + super.parentUuid, required this.thinking, }); @@ -123,6 +133,7 @@ final class AssistantToolUse extends ConversationItem { required super.uuid, required super.timestamp, required super.isSidechain, + super.parentUuid, required this.toolUseId, required this.name, required this.input, @@ -545,6 +556,8 @@ void _parseLineInto(String line, List out, List warnin final uuid = envelope['uuid'] as String? ?? ''; final isSidechain = envelope['isSidechain'] as bool? ?? false; + final rawParent = envelope['parentUuid'] as String?; + final parentUuid = (rawParent != null && rawParent.isNotEmpty) ? rawParent : null; DateTime timestamp; try { @@ -555,9 +568,9 @@ void _parseLineInto(String line, List out, List warnin switch (type) { case 'user': - _parseUserInto(envelope, uuid, timestamp, isSidechain, out); + _parseUserInto(envelope, uuid, timestamp, isSidechain, parentUuid, out); case 'assistant': - _parseAssistantInto(envelope, uuid, timestamp, isSidechain, out); + _parseAssistantInto(envelope, uuid, timestamp, isSidechain, parentUuid, out); _extractAssistantStatus(envelope, status); default: break; // unknown type — degrade gracefully @@ -582,6 +595,7 @@ void _parseUserInto( String uuid, DateTime timestamp, bool isSidechain, + String? parentUuid, List out, ) { final message = envelope['message'] as Map?; @@ -595,7 +609,7 @@ void _parseUserInto( if (content is String) { if (content.isNotEmpty) { - out.add(UserMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, text: content, injected: injected)); + out.add(UserMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, parentUuid: parentUuid, text: content, injected: injected)); } return; } @@ -614,6 +628,7 @@ void _parseUserInto( uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, + parentUuid: parentUuid, toolUseId: item['tool_use_id'] as String? ?? '', content: rawContent is String ? rawContent : jsonEncode(rawContent), isError: item['is_error'] as bool? ?? false, @@ -623,7 +638,7 @@ void _parseUserInto( } } if (textParts.isNotEmpty) { - out.add(UserMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, text: textParts.join('\n'), injected: injected)); + out.add(UserMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, parentUuid: parentUuid, text: textParts.join('\n'), injected: injected)); } } @@ -632,6 +647,7 @@ void _parseAssistantInto( String uuid, DateTime timestamp, bool isSidechain, + String? parentUuid, List out, ) { final message = envelope['message'] as Map?; @@ -645,12 +661,12 @@ void _parseAssistantInto( case 'text': final text = item['text'] as String? ?? ''; if (text.isNotEmpty) { - out.add(AssistantTextMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, text: text)); + out.add(AssistantTextMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, parentUuid: parentUuid, text: text)); } case 'thinking': final thinking = item['thinking'] as String? ?? ''; if (thinking.isNotEmpty) { - out.add(AssistantThinkingMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, thinking: thinking)); + out.add(AssistantThinkingMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, parentUuid: parentUuid, thinking: thinking)); } case 'tool_use': final rawInput = item['input']; @@ -658,6 +674,7 @@ void _parseAssistantInto( uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, + parentUuid: parentUuid, toolUseId: item['id'] as String? ?? '', name: item['name'] as String? ?? '', input: rawInput is Map ? rawInput.cast() : {}, diff --git a/test/builtin/claude/conversation_view_test.dart b/test/builtin/claude/conversation_view_test.dart index 2833617d..48265170 100644 --- a/test/builtin/claude/conversation_view_test.dart +++ b/test/builtin/claude/conversation_view_test.dart @@ -282,6 +282,54 @@ void main() { expect(find.text('thanks'), findsOneWidget); }); + testWidgets('a sidechain prompt folds into its Agent card; never labelled "you" (T-263)', (tester) async { + await pumpWith(tester, [ + AssistantToolUse( + uuid: 'agt-msg', timestamp: _t, isSidechain: false, toolUseId: 'task1', name: 'Task', input: const {'description': 'explore the codebase'}), + UserMessage(uuid: 'p1', timestamp: _t, isSidechain: true, text: 'find all the widgets'), + ]); + // Never the blue "you", and no standalone block (folded → suppressed). + expect(find.text('you'), findsNothing); + expect(find.text('agent prompt'), findsNothing); // not a standalone card here + expect(find.text('Task'), findsOneWidget); + // Collapsed by default: the prompt is hidden. + expect(find.text('find all the widgets'), findsNothing); + + // Expand the Agent card → a "prompt" segment reveals the folded prompt. + await tester.tap(find.bySemanticsLabel('Expand')); + await tester.pumpAndSettle(); + expect(find.text('prompt'), findsOneWidget); // segment sub-label + expect(find.text('find all the widgets'), findsOneWidget); + }); + + 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. + await pumpWith(tester, [ + UserMessage(uuid: 'orphan', timestamp: _t, isSidechain: true, text: 'orphaned agent instructions'), + ]); + expect(find.text('you'), findsNothing); + expect(find.text('agent prompt'), findsOneWidget); + }); + + testWidgets('parallel agents: each prompt attaches to its own card via parentUuid (T-263)', (tester) async { + // Document order scrambles the prompts so a nearest-preceding heuristic + // would misattach BOTH to agent B; parentUuid must route them correctly. + await pumpWith(tester, [ + AssistantToolUse(uuid: 'mA', timestamp: _t, isSidechain: false, toolUseId: 'tA', name: 'Task', input: const {'description': 'agent A'}), + AssistantToolUse(uuid: 'mB', timestamp: _t, isSidechain: false, toolUseId: 'tB', name: 'Task', input: const {'description': 'agent B'}), + UserMessage(uuid: 'pB', timestamp: _t, isSidechain: true, parentUuid: 'mB', text: 'PROMPT FOR B'), + UserMessage(uuid: 'pA', timestamp: _t, isSidechain: true, parentUuid: 'mA', text: 'PROMPT FOR A'), + ]); + // Two collapsed Agent cards; the first Expand caret belongs to card A. + expect(find.text('you'), findsNothing); + await tester.tap(find.bySemanticsLabel('Expand').first); + await tester.pumpAndSettle(); + // Only card A is expanded → its prompt (A) shows; B's stays folded away. + // Nearest-preceding would have put A's prompt under B, revealing nothing. + expect(find.text('PROMPT FOR A'), findsOneWidget); + expect(find.text('PROMPT FOR B'), findsNothing); + }); + testWidgets('a permission-prompted tool-use is hidden but its result is kept', (tester) async { await pumpWith( tester, diff --git a/test/builtin/claude/transcript_reader_test.dart b/test/builtin/claude/transcript_reader_test.dart index e65223f4..378081a2 100644 --- a/test/builtin/claude/transcript_reader_test.dart +++ b/test/builtin/claude/transcript_reader_test.dart @@ -361,6 +361,23 @@ void main() { final items = parseAll([raw]); expect(items.first.isSidechain, isTrue); }); + + test('parentUuid is parsed; empty parentUuid normalises to null (T-263)', () { + final withParent = envelope( + type: 'user', + uuid: 'u8', + parentUuid: 'msg-A', + message: {'role': 'user', 'content': 'a sidechain prompt'}, + ); + final withoutParent = envelope( + type: 'user', + uuid: 'u9', + message: {'role': 'user', 'content': 'top-level'}, // parentUuid defaults to '' + ); + final items = parseAll([withParent, withoutParent]); + expect(items[0].parentUuid, 'msg-A'); + expect(items[1].parentUuid, isNull); // '' → null + }); }); // ------------------------------------------------------------------------- diff --git a/test/goldens/conversation_card_goldens_test.dart b/test/goldens/conversation_card_goldens_test.dart index 4a44d54a..e8e53539 100644 --- a/test/goldens/conversation_card_goldens_test.dart +++ b/test/goldens/conversation_card_goldens_test.dart @@ -51,6 +51,25 @@ void main() { ), ), ), + GoldenTestScenario( + name: 'agent / expanded (call → prompt → result layering, T-263)', + child: _wrap( + f, + const ConversationCard( + variant: ConversationCardVariant.bordered, + accent: Color(0xFF4C9AFF), + label: 'Task', + status: ConversationCardStatus.success, + collapsible: true, + collapsedByDefault: false, + body: Text('{ "description": "explore the codebase" }', textDirection: TextDirection.ltr), + extraSegments: [ + CardSegment(label: 'prompt', child: Text('find all the widgets and summarise', textDirection: TextDirection.ltr)), + CardSegment(label: 'result', child: Text('found 42 widgets', textDirection: TextDirection.ltr)), + ], + ), + ), + ), GoldenTestScenario( name: 'error / header mark (call card stays separate from the red result card)', child: _wrap( diff --git a/test/goldens/goldens/linux/conversation_card_merged.png b/test/goldens/goldens/linux/conversation_card_merged.png index a78ae04dd89910e65b89ff753180f138bdda139a..498485a4670221e9d9b2fe64ef89003fcc2829a8 100644 GIT binary patch delta 27519 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