From 9a8f8641f779765bf09453258943b81e0e5d047b Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 7 Jun 2026 19:41:28 +0200 Subject: [PATCH] merge successful tool call + result into one card (T-262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A successful tool call now renders as a single merged card instead of a separate call card + result card: - ConversationCard gains a trailing header status slot (success check / error cross, semantics-labelled) and optional labelled extra body segments (P1 + P2 — reused by T-263's Agent card). - conversation_view builds a reverse toolUseId→result lookup, folds a successful result into its call card as a colorized ClideCodeBlock (Read → file grammar, Bash → shell, else text), and suppresses the standalone success result in _visibleItems. Errors stay a separate prominent red card, now with a matching red header mark (note C). - Suppression happens before grouping, so a merged success result is no longer a separate activity-cluster step (note D). Tests: card status-mark + segment coverage, merged-fold + language inference + in-flight/orphan/error cases, cluster non-double-count, and a golden for the merged card (collapsed + expanded + error). Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 1 + .pql/changelog/tickets/2026-06.sql | 34 +++++++ CHANGELOG.md | 5 + lib/builtin/claude/src/conversation_card.dart | 78 ++++++++++++++- lib/builtin/claude/src/conversation_view.dart | 81 ++++++++++++++-- .../claude/conversation_card_test.dart | 62 ++++++++++++ .../claude/conversation_view_test.dart | 89 ++++++++++++++++-- .../conversation_card_goldens_test.dart | 78 +++++++++++++++ .../linux/conversation_card_merged.png | Bin 0 -> 28289 bytes 9 files changed, 412 insertions(+), 16 deletions(-) create mode 100644 test/goldens/conversation_card_goldens_test.dart create mode 100644 test/goldens/goldens/linux/conversation_card_merged.png diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 82188f4d..c31bab58 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -784,3 +784,4 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, 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-267', 'status', 'backlog', 'ready', NULL, '2026-06-07 16:25:28', '2026-06-07 16:25:28', '2026-06-07 16:25:28', NULL, 'f89b4bd8614ca3f405f7f888e789d56a', 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-267', 'status', 'ready', 'in_progress', NULL, '2026-06-07 17:22:06', '2026-06-07 17:22:06', '2026-06-07 17:22:06', NULL, 'bd4730367ab0af0d33d145a56748b2ab', 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-262', 'status', 'backlog', 'in_progress', NULL, '2026-06-07 17:23:39', '2026-06-07 17:23:39', '2026-06-07 17:23:39', NULL, 'dd562ebdd3cb176d4d58bdf07190924a', 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-262', 'status', 'in_progress', 'done', NULL, '2026-06-07 17:41:11', '2026-06-07 17:41:11', '2026-06-07 17:41:11', NULL, '2e581770ff1d438497379beaa530b9c6', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index d8e4ae2f..1cf73f0d 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -1703,3 +1703,37 @@ C — Error-card symmetry decision: this ticket merges only SUCCESS into the too D — Activity-cluster coupling: activity_cluster.groupConversation currently folds a tool CALL and its RESULT as TWO separate foldable items into a cluster (activity_cluster.dart ~L100-109, classifying each by toolUseId). Once this ticket makes call+result a single self-contained card, the grouping pass must treat the tool call as ONE unit (its result is part of the card, no longer a separate foldable item) or the result will double-render (once folded into the card, once as a cluster item). Update _isFoldable / the pairing logic accordingly and add a test that a merged tool card is not double-counted.', 'in_progress', 'medium', NULL, NULL, 'D-78', '2026-06-07 08:10:42', '2026-06-07 17:23:39', NULL, '04fae1762c477c0e883f426080c90b53', 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-273', 'task', NULL, 'turn indicator font color → Claude accent orange', 'Change the animated running/turn indicator''s text color to the Claude accent orange (the brand accent), instead of its current color. Lives in lib/builtin/claude/src/running_indicator.dart (the rotating-verb turn indicator, T-255). Use the appropriate accent token from SurfaceTokens (ui-design skill: confirm which token maps to Claude accent orange) — no hardcoded hex. Update/adjust any golden or widget test that asserts the indicator color.', 'backlog', 'low', NULL, NULL, NULL, '2026-06-07 17:39:10', '2026-06-07 17:39:10', NULL, '2f09f004ad10014a7e881fe28d9e2f15', 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-262', 'story', 'T-267', 'Merge tool-call + success result into one card with header status check', 'Today a Claude tool call renders as TWO stacked cards in the conversation log: the tool-use card (e.g. "Write " with a collapsible call body) and a separate success/result card ("Write · result / File created successfully..."). Collapse the successful pair into ONE card; failure keeps the current two-card interaction. + +Current code: lib/builtin/claude/src/conversation_view.dart — _toolUse (~L294) renders AssistantToolUse, _toolResult (~L329) renders ToolResultMessage; the two ConversationItems are linked by toolUseId (ToolResultMessage.isError marks failure). ConversationCard header is built in lib/builtin/claude/src/conversation_card.dart _header (~L200). Builds on T-168 (per-tool body rendering). + +Desired behavior (success): +- One card per successful tool call — the tool-use card. The standalone success result card is suppressed (folded in), not rendered separately. +- A success-green check mark sits at the RIGHT END of the card header row (reuse tokens.statusSuccess + a check glyph: PhosphorIcons.check or CheckIcon). No new theme tokens. +- Collapsed by default: header shows the existing summary + the check. Expanding the card reveals the CALL segment (the input — path / diff / command, as today) and, BELOW it, the RESULT segment (the swallowed output). This applies to ALL tools, not just Write/Edit: for Bash/Read/Grep/LS the real output relocates into the expandable body so nothing is lost; for Write/Edit the trivial confirmation folds in the same way. +- Render the result segment as an inline ClideCodeBlock (lib/widgets/src/clide_code_block.dart) WITH editor syntax colorization. ClideCodeBlock already colorizes via TreeSitterService when handed a language — so the work is inferring/passing the right language per tool: Read → from the file extension/path (language_map.grammarForPath), Bash → bash, Grep/LS/others → text fallback. Where a path is available, prefer it so highlighting matches the editor. + +Desired behavior (failure) — UNCHANGED: +- Leave the current interaction as-is: the tool-use card plus the prominent red "· error" result card (borderColor statusError, expanded-by-default). Optionally show a red status mark on the tool-use header for symmetry, but do NOT fold the error into one card. + +What to build: +1. ConversationCard: a trailing header status slot (e.g. status: success|error|none) rendered in _header between the Spacer/summary and the hover action buttons. +2. ConversationCard: support a second body segment so an expanded card can show CALL then RESULT with a clear visual separator / sub-label (e.g. a divider or a muted "result" label) so the user can tell the call from its output. +3. conversation_view: a result-by-toolUseId lookup (reverse of the existing toolUseById) so a tool-use card knows its outcome; on a successful pair, stamp the check + fold the result body and SUPPRESS the standalone success ToolResultMessage from the rendered list. Errors render both cards as today. +4. Per-tool result language inference for the folded code block. + +Edge cases to handle: +- In-flight tool-use with no result yet: render the call card as today (no check, no folded result); status/result appear once the result arrives. +- Orphan result with no paired tool-use: keep rendering it standalone. +- Permission-resolved tool-use cards (conversation_view.dart ~L297, already green/red bordered + collapsed): reconcile so the merged-card + check treatment is consistent and not duplicated with the existing border-outcome styling. +- Result items may not be strictly adjacent to their tool-use in the list; suppression must be keyed by toolUseId, not list position. + +Tests: unit/widget coverage for the merged success card (collapsed shows check; expanded shows call + colorized result), language inference per tool, the unchanged error path, and the in-flight/orphan cases. Add a golden for the merged success card. + +Refs: D-78 (interaction zone / display-only conversation widgets). Builds on T-168 (per-tool tool-use/result body rendering). + +IMPLEMENTATION NOTES (from streamlining analysis): + +C — Error-card symmetry decision: this ticket merges only SUCCESS into the tool card; the error path intentionally stays a separate prominent red "· error" card (conversation_view.dart ~L337) for failure visibility. Decide explicitly: keep that asymmetry (success folds, failure stays two-card) OR also stamp a red status mark on the tool-use header for visual symmetry while still keeping the separate red card. Default leaning: keep the separate red card; optionally add the red header mark. + +D — Activity-cluster coupling: activity_cluster.groupConversation currently folds a tool CALL and its RESULT as TWO separate foldable items into a cluster (activity_cluster.dart ~L100-109, classifying each by toolUseId). Once this ticket makes call+result a single self-contained card, the grouping pass must treat the tool call as ONE unit (its result is part of the card, no longer a separate foldable item) or the result will double-render (once folded into the card, once as a cluster item). Update _isFoldable / the pairing logic accordingly and add a test that a merged tool card is not double-counted.', 'done', 'medium', NULL, NULL, 'D-78', '2026-06-07 08:10:42', '2026-06-07 17:41:11', NULL, '536c793d50d899e4d403b5a2a246f6a5', 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 161ddc62..e1f01af9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -144,6 +144,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Changed +- 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 → + shell) revealed on expand. Failures keep their prominent red card, now with a + matching header mark. (T-262) - The in-flight turn indicator now feels alive: instead of a static gray `running…`, it shows a rotating curated status verb (`Pondering…`, `Conjuring…`, …) with an animated ellipsis. Respects reduced-motion (static diff --git a/lib/builtin/claude/src/conversation_card.dart b/lib/builtin/claude/src/conversation_card.dart index 45b32e84..f6869cd2 100644 --- a/lib/builtin/claude/src/conversation_card.dart +++ b/lib/builtin/claude/src/conversation_card.dart @@ -31,6 +31,21 @@ class MessageAction { enum ConversationCardVariant { stripe, bordered, bare } +/// A trailing status mark shown at the right end of the header (T-262): a +/// green check for a succeeded tool call, a red cross for a failure. [none] +/// renders no mark (the default for non-tool cards). +enum ConversationCardStatus { none, success, error } + +/// An extra labelled body segment shown below the primary [ConversationCard.body] +/// when the card is expanded (T-262). Lets one card lay out CALL → RESULT (or, +/// for the Agent card, CALL → PROMPT → RESULT — T-263) with a muted sub-label + +/// divider between segments so the reader can tell the parts apart. +class CardSegment { + const CardSegment({required this.label, required this.child}); + final String label; + final Widget child; +} + class ConversationCard extends StatefulWidget { const ConversationCard({ super.key, @@ -44,6 +59,8 @@ class ConversationCard extends StatefulWidget { this.collapsedByDefault = false, this.collapsedSummary, this.borderColor, + this.status = ConversationCardStatus.none, + this.extraSegments = const [], }); final ConversationCardVariant variant; @@ -51,6 +68,14 @@ class ConversationCard extends StatefulWidget { final String label; final Widget body; + /// Trailing header status mark (T-262) — a success check or error cross at + /// the right end of the header. [ConversationCardStatus.none] shows nothing. + final ConversationCardStatus status; + + /// Extra labelled segments rendered below [body] when expanded (T-262/T-263); + /// each gets a muted sub-label + divider so CALL/PROMPT/RESULT read apart. + final List extraSegments; + /// Raw text the copy action yields; no copy button when null. final String? copyText; @@ -149,7 +174,14 @@ class _ConversationCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ _header(tokens), - if (!_collapsed) ...[const SizedBox(height: 4), widget.body], + if (!_collapsed) ...[ + const SizedBox(height: 4), + widget.body, + for (final seg in widget.extraSegments) ...[ + _segmentLabel(tokens, seg.label), + seg.child, + ], + ], ], ); return Padding( @@ -215,6 +247,9 @@ class _ConversationCardState extends State { ), ] else const Spacer(), + // Trailing status mark (T-262): success check / error cross, sitting + // between the summary/spacer and the action bar. + if (widget.status != ConversationCardStatus.none) _statusMark(tokens), // Actions are always in the tree (keyboard/AT always reachable). // Opacity reveals them on hover or keyboard focus; opacity-0 keeps // them layout-present but visually hidden so they don't distract. @@ -253,6 +288,47 @@ class _ConversationCardState extends State { ); } + /// The trailing success/error mark (T-262). Carries a Semantics label so the + /// outcome is announced, not just colour-coded. + Widget _statusMark(SurfaceTokens tokens) { + final ClideIconPainter icon; + final Color color; + final String label; + switch (widget.status) { + case ConversationCardStatus.success: + icon = const CheckIcon(); + color = tokens.statusSuccess; + label = 'succeeded'; + case ConversationCardStatus.error: + icon = const CloseIcon(); + color = tokens.statusError; + label = 'failed'; + case ConversationCardStatus.none: + return const SizedBox.shrink(); + } + return Semantics( + label: label, + container: true, + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: ClideIcon(icon, size: 12, color: color), + ), + ); + } + + /// A muted sub-label + hairline divider introducing an [CardSegment] below + /// the primary body (T-262), so CALL/PROMPT/RESULT read as distinct parts. + Widget _segmentLabel(SurfaceTokens tokens, String label) => Padding( + padding: const EdgeInsets.only(top: 8, bottom: 4), + child: Row( + children: [ + ClideText(label, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + const SizedBox(width: 8), + Expanded(child: Container(height: 1, color: tokens.panelBorder)), + ], + ), + ); + List _actions(SurfaceTokens tokens) { final items = <_ActionItem>[]; if (widget.copyText != null) items.add(_ActionItem('copy', _copy)); diff --git a/lib/builtin/claude/src/conversation_view.dart b/lib/builtin/claude/src/conversation_view.dart index c7c5ee12..21648b7a 100644 --- a/lib/builtin/claude/src/conversation_view.dart +++ b/lib/builtin/claude/src/conversation_view.dart @@ -18,6 +18,7 @@ import 'package:clide/builtin/claude/src/conversation_controller.dart'; import 'package:clide/builtin/claude/src/prompt_card.dart'; import 'package:clide/builtin/claude/src/transcript_reader.dart'; import 'package:clide/kernel/src/facade.dart'; +import 'package:clide/kernel/src/syntax/language_map.dart'; import 'package:clide/kernel/src/theme/controller.dart'; import 'package:clide/kernel/src/theme/tokens.dart'; import 'package:clide/widgets/widgets.dart'; @@ -99,14 +100,31 @@ class _ConversationViewState extends State { if (it is AssistantToolUse && it.name == 'AskUserQuestion') it.toolUseId, }; final outcomes = widget.toolUseOutcomes; + final toolUseById = { + for (final it in items) + if (it is AssistantToolUse) it.toolUseId: it, + }; + + // A tool-use is dropped (not rendered as a card) when it's an + // AskUserQuestion or a still-pending permission prompt. + bool toolUseDropped(AssistantToolUse t) { + if (t.name == 'AskUserQuestion') return true; + return hidden.contains(t.toolUseId) && !outcomes.containsKey(t.toolUseId); + } + bool drop(ConversationItem it) { - if (it is AssistantToolUse) { - if (it.name == 'AskUserQuestion') return true; - // Permission-prompted: hide only while pending; once resolved it shows - // collapsed with a green/red border. - return hidden.contains(it.toolUseId) && !outcomes.containsKey(it.toolUseId); + if (it is AssistantToolUse) return toolUseDropped(it); + 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 + // folds INTO that card — suppress the standalone result here so it + // doesn't double-render. Errors stay standalone (prominent red card); + // orphan results (no paired tool-use) stay standalone too. + if (it.isError) return false; + final tu = toolUseById[it.toolUseId]; + if (tu == null) return false; + return !toolUseDropped(tu); } - if (it is ToolResultMessage) return auqIds.contains(it.toolUseId); // AUQ result only; keep permission results return false; } @@ -139,6 +157,14 @@ class _ConversationViewState extends State { ); } + // Reverse pairing (T-262): toolUseId → its result, so a tool-use card can + // 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) + if (it is ToolResultMessage) it.toolUseId: it, + }; + // Fold runs of meta items into collapsible activity cards (T-230); sticky // items (user/prose/surfaced errors) render first-class as before. final groups = groupConversation(items, widget.foldLevel); @@ -156,12 +182,14 @@ class _ConversationViewState extends State { tokens: tokens, toolUseOutcomes: widget.toolUseOutcomes, toolUseById: widget.controller.toolUseById, + resultByToolUseId: resultByToolUseId, ), FoldedCluster(:final items) => _ActivityCard( items: items, tokens: tokens, toolUseOutcomes: widget.toolUseOutcomes, toolUseById: widget.controller.toolUseById, + resultByToolUseId: resultByToolUseId, ), }; }, @@ -185,6 +213,7 @@ class _ConversationTurn extends StatelessWidget { required this.tokens, this.toolUseOutcomes = const {}, this.toolUseById = const {}, + this.resultByToolUseId = const {}, }); final ConversationItem item; @@ -194,6 +223,10 @@ class _ConversationTurn extends StatelessWidget { /// Index from toolUseId → AssistantToolUse, for result-card pairing (T-168). final Map toolUseById; + /// Index from toolUseId → its result, so a tool-use card can fold a + /// successful result into one merged card (T-262). + final Map resultByToolUseId; + @override Widget build(BuildContext context) { final i = item; @@ -315,8 +348,20 @@ class _ConversationTurn extends StatelessWidget { ); Widget _toolUse(AssistantToolUse t) { + // T-262: fold the paired result into this card. A successful result becomes + // a "result" segment below the call + a green header check; a failed result + // stamps a red header cross but stays a separate prominent error card (the + // result is not suppressed — see _visibleItems). No result yet (in-flight) → + // no mark, no segment. + 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 []; + // A resolved permission-prompted call: collapsed, green if approved / red - // if denied — a quiet record of what was permitted (D-78). + // if denied — a quiet record of what was permitted (D-78). It still folds + // its result + outcome check like any other merged card (T-262). final outcome = toolUseOutcomes[t.toolUseId]; if (outcome != null) { final color = outcome ? tokens.statusSuccess : tokens.statusError; @@ -329,7 +374,9 @@ class _ConversationTurn extends StatelessWidget { collapsible: true, collapsedByDefault: true, collapsedSummary: _toolUseSummary(t), + status: status, body: toolInputBody(tokens, t.name, t.input), + extraSegments: segments, ); } // Per-tool body rendering (T-168): Bash → command block, Edit/Write → diff, @@ -345,10 +392,27 @@ class _ConversationTurn extends StatelessWidget { collapsible: true, collapsedByDefault: true, collapsedSummary: summary, + status: status, body: body, + extraSegments: segments, ); } + /// Per-tool language for the folded result code block (T-262): Read shows the + /// file's content, so colorize by the file's grammar; Bash output is shell; + /// everything else (Grep/LS/Write/Edit confirmations/…) falls back to plain. + String _resultLanguage(AssistantToolUse t) { + switch (t.name) { + case 'Read': + final path = (t.input['file_path'] ?? t.input['path']) as String?; + return (path != null && path.isNotEmpty ? grammarForPath(path) : null) ?? 'text'; + case 'Bash': + return 'bash'; + default: + return 'text'; + } + } + Widget _toolResult(ToolResultMessage t) { final paired = toolUseById[t.toolUseId]; final accent = t.isError ? tokens.statusError : tokens.globalTextMuted; @@ -430,12 +494,14 @@ class _ActivityCard extends StatefulWidget { required this.tokens, required this.toolUseOutcomes, required this.toolUseById, + required this.resultByToolUseId, }); final List items; final SurfaceTokens tokens; final Map toolUseOutcomes; final Map toolUseById; + final Map resultByToolUseId; @override State<_ActivityCard> createState() => _ActivityCardState(); @@ -503,6 +569,7 @@ class _ActivityCardState extends State<_ActivityCard> { tokens: tokens, toolUseOutcomes: widget.toolUseOutcomes, toolUseById: widget.toolUseById, + resultByToolUseId: widget.resultByToolUseId, ), ], ), diff --git a/test/builtin/claude/conversation_card_test.dart b/test/builtin/claude/conversation_card_test.dart index 7597cd5b..31e4a9e4 100644 --- a/test/builtin/claude/conversation_card_test.dart +++ b/test/builtin/claude/conversation_card_test.dart @@ -283,6 +283,68 @@ void main() { expect(find.text('result'), findsOneWidget); }); + testWidgets('status: success renders a "succeeded" semantics mark in the header (T-262)', (tester) async { + final handle = tester.ensureSemantics(); + await tester.pumpWidget(harness( + f, + const ConversationCard( + variant: ConversationCardVariant.bordered, + accent: Color(0xFFFFFFFF), + label: 'Bash', + status: ConversationCardStatus.success, + body: Text('body', textDirection: TextDirection.ltr), + ), + )); + await tester.pump(); + expect(find.bySemanticsLabel('succeeded'), findsOneWidget); + expect(find.bySemanticsLabel('failed'), findsNothing); + handle.dispose(); + }); + + testWidgets('status: error renders a "failed" semantics mark; none renders nothing (T-262)', (tester) async { + final handle = tester.ensureSemantics(); + await tester.pumpWidget(harness( + f, + const ConversationCard( + variant: ConversationCardVariant.bordered, + accent: Color(0xFFFFFFFF), + label: 'Bash', + status: ConversationCardStatus.error, + body: Text('body', textDirection: TextDirection.ltr), + ), + )); + await tester.pump(); + expect(find.bySemanticsLabel('failed'), findsOneWidget); + expect(find.bySemanticsLabel('succeeded'), findsNothing); + handle.dispose(); + }); + + testWidgets('extraSegments render below the body with their sub-label when expanded (T-262)', (tester) async { + await tester.pumpWidget(harness( + f, + const ConversationCard( + variant: ConversationCardVariant.bordered, + accent: Color(0xFFFFFFFF), + label: 'Read', + collapsible: true, + collapsedByDefault: true, + body: Text('the call', textDirection: TextDirection.ltr), + extraSegments: [CardSegment(label: 'result', child: Text('the output', textDirection: TextDirection.ltr))], + ), + )); + await tester.pump(); + // Collapsed: neither the body nor the segment shows. + expect(find.text('the call'), findsNothing); + expect(find.text('result'), findsNothing); + expect(find.text('the output'), findsNothing); + + await tester.tap(find.bySemanticsLabel('Expand')); + await tester.pump(); + expect(find.text('the call'), findsOneWidget); // primary body + expect(find.text('result'), findsOneWidget); // segment sub-label + expect(find.text('the output'), findsOneWidget); // segment child + }); + testWidgets('bare variant renders content without frame decoration', (tester) async { await tester.pumpWidget(harness( f, diff --git a/test/builtin/claude/conversation_view_test.dart b/test/builtin/claude/conversation_view_test.dart index ead7d8a2..2833617d 100644 --- a/test/builtin/claude/conversation_view_test.dart +++ b/test/builtin/claude/conversation_view_test.dart @@ -177,7 +177,9 @@ void main() { tester, [ _tool('Bash', const {'command': 'echo hi'}), - _result('hi'), + // A second, distinct in-flight tool call (T-262 folds a success + // result into its call card, so two *calls* are what make 2 steps). + AssistantToolUse(uuid: 'tu2', timestamp: _t, isSidechain: false, toolUseId: 'x2', name: 'Read', input: const {'file_path': '/a'}), ], foldLevel: FoldLevel.tools); // Collapsed by default: one card with a step count, not the raw rows. @@ -189,6 +191,21 @@ void main() { expect(find.bySemanticsLabel('Activity, 2 steps, expanded'), findsOneWidget); }); + testWidgets('a folded success result is not a separate step — merged into its call (T-262 note D)', (tester) async { + await pumpWith( + tester, + [ + _tool('Bash', const {'command': 'echo hi'}), + _result('hi there'), + ], + foldLevel: FoldLevel.tools); + // The call + its success result is ONE unit now: 1 step, not 2. + expect(find.text('1 step'), findsOneWidget); + expect(find.text('2 steps'), findsNothing); + // The result is not double-rendered: no standalone result card. + expect(find.text('Bash · result'), findsNothing); + }); + testWidgets('an image card renders with the "image" label, the file, and its caption (T-249)', (tester) async { await pumpWith(tester, [_image('/no/such/file.png', caption: 'before the fix')]); expect(find.text('image'), findsOneWidget); @@ -247,8 +264,9 @@ void main() { expect(find.text('claude'), findsOneWidget); expect(find.text('thinking'), findsOneWidget); expect(find.text('Bash'), findsOneWidget); - // Result labels now include the paired tool name (T-168). - expect(find.text('Bash · result'), findsOneWidget); + // T-262: a successful result folds into the tool card (no standalone + // "Bash · result"); a failed result keeps its prominent standalone card. + expect(find.text('Bash · result'), findsNothing); expect(find.text('Bash · error'), findsOneWidget); }); @@ -289,8 +307,12 @@ void main() { hiddenToolUseIds: {'x1'}, toolUseOutcomes: {'x1': true}, // approved ); + final handle = tester.ensureSemantics(); expect(find.text('Write'), findsOneWidget); // shown (resolved) - expect(find.byType(ClideIcon), findsOneWidget); // collapsed caret + expect(find.bySemanticsLabel('Expand'), findsOneWidget); // collapsed caret + // T-262: the resolved card also folds its result + a success check. + expect(find.bySemanticsLabel('succeeded'), findsOneWidget); + handle.dispose(); }); testWidgets('an injected user message renders as a muted "context" card, not "you"', (tester) async { @@ -323,12 +345,63 @@ void main() { expect(find.text('/foo/bar.dart'), findsOneWidget); }); - testWidgets('result label includes paired tool name (T-168)', (tester) async { + testWidgets('a successful result folds into the tool-use card with a check (T-262)', (tester) async { + final handle = tester.ensureSemantics(); await pumpWith(tester, [ - _tool('Read', {'file_path': '/x'}), - _result('file content'), + _tool('Read', {'file_path': '/foo/bar.dart'}), + _result('final answer = 42;'), ]); - expect(find.text('Read · result'), findsOneWidget); + // No standalone success result card — it's folded into the Read card. + expect(find.text('Read · result'), findsNothing); + // The merged card shows a success check. + expect(find.bySemanticsLabel('succeeded'), findsOneWidget); + // Collapsed by default: neither the call body nor the result is shown yet. + expect(find.text('final answer = 42;'), findsNothing); + + // Expand: the call segment, a "result" sub-label, and the folded result + // as a colorized code block (Read → grammar from the .dart path). + await tester.tap(find.bySemanticsLabel('Expand')); + await tester.pumpAndSettle(); + expect(find.text('result'), findsOneWidget); // segment sub-label + final blocks = tester.widgetList(find.byType(ClideCodeBlock)).toList(); + expect(blocks.any((b) => b.language == 'dart' && b.source == 'final answer = 42;'), isTrue); + handle.dispose(); + }); + + testWidgets('Bash result folds as a bash code block; in-flight call has no check (T-262)', (tester) async { + final handle = tester.ensureSemantics(); + // In-flight: a call with no result yet → no check, no folded result. + await pumpWith(tester, [ + _tool('Bash', {'command': 'ls'}) + ]); + expect(find.bySemanticsLabel('succeeded'), findsNothing); + expect(find.bySemanticsLabel('failed'), findsNothing); + handle.dispose(); + }); + + testWidgets('Bash success result infers the bash language for the folded block (T-262)', (tester) async { + await pumpWith(tester, [ + _tool('Bash', {'command': 'echo hi'}), + _result('hi\nthere'), + ]); + await tester.tap(find.bySemanticsLabel('Expand')); + await tester.pumpAndSettle(); + final blocks = tester.widgetList(find.byType(ClideCodeBlock)).toList(); + expect(blocks.any((b) => b.language == 'bash' && b.source == 'hi\nthere'), isTrue); + }); + + testWidgets('a failed result is NOT folded: standalone error card + red header mark (T-262)', (tester) async { + final handle = tester.ensureSemantics(); + await pumpWith(tester, [ + _tool('Bash', {'command': 'boom'}), + _result('permission denied', isError: true), + ]); + // The error stays a separate prominent card… + expect(find.text('Bash · error'), findsOneWidget); + expect(find.text('permission denied'), findsOneWidget); + // …and the call card carries a red failure mark for symmetry (note C). + expect(find.bySemanticsLabel('failed'), findsOneWidget); + handle.dispose(); }); testWidgets('error result label includes paired tool name (T-168)', (tester) async { diff --git a/test/goldens/conversation_card_goldens_test.dart b/test/goldens/conversation_card_goldens_test.dart new file mode 100644 index 00000000..4a44d54a --- /dev/null +++ b/test/goldens/conversation_card_goldens_test.dart @@ -0,0 +1,78 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:clide/builtin/claude/src/conversation_card.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../helpers/kernel_fixture.dart'; +import '../helpers/widget_harness.dart'; + +void main() { + late KernelFixture f; + + setUp(() async => f = await KernelFixture.create()); + tearDown(() async => f.dispose()); + + goldenTest( + 'ConversationCard merged tool card (T-262): status mark + folded result segment', + fileName: 'conversation_card_merged', + builder: () => GoldenTestGroup( + columns: 1, + children: [ + GoldenTestScenario( + name: 'success / collapsed (check + summary)', + child: _wrap( + f, + const ConversationCard( + variant: ConversationCardVariant.bordered, + accent: Color(0xFF4C9AFF), + label: 'Read', + status: ConversationCardStatus.success, + collapsible: true, + collapsedByDefault: true, + collapsedSummary: '/lib/main.dart', + body: Text('/lib/main.dart', textDirection: TextDirection.ltr), + extraSegments: [CardSegment(label: 'result', child: Text('void main() {}', textDirection: TextDirection.ltr))], + ), + ), + ), + GoldenTestScenario( + name: 'success / expanded (call → result segment)', + child: _wrap( + f, + const ConversationCard( + variant: ConversationCardVariant.bordered, + accent: Color(0xFF4C9AFF), + label: 'Read', + status: ConversationCardStatus.success, + collapsible: true, + collapsedByDefault: false, + body: Text('/lib/main.dart', textDirection: TextDirection.ltr), + extraSegments: [CardSegment(label: 'result', child: Text('void main() {}', textDirection: TextDirection.ltr))], + ), + ), + ), + GoldenTestScenario( + name: 'error / header mark (call card stays separate from the red result card)', + child: _wrap( + f, + const ConversationCard( + variant: ConversationCardVariant.bordered, + accent: Color(0xFF4C9AFF), + label: 'Bash', + status: ConversationCardStatus.error, + collapsible: true, + collapsedByDefault: true, + collapsedSummary: 'cat nonexistent', + body: Text('cat nonexistent', textDirection: TextDirection.ltr), + ), + ), + ), + ], + ), + ); +} + +Widget _wrap(KernelFixture f, Widget child) => SizedBox( + width: 360, + child: harness(f, child), + ); diff --git a/test/goldens/goldens/linux/conversation_card_merged.png b/test/goldens/goldens/linux/conversation_card_merged.png new file mode 100644 index 0000000000000000000000000000000000000000..a78ae04dd89910e65b89ff753180f138bdda139a GIT binary patch literal 28289 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 literal 0 HcmV?d00001