From cf36eca4e50208683088342b6e60d8702d56f3f7 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 10 Jun 2026 10:57:01 +0200 Subject: [PATCH] migrate group cards onto ClideCollapserCard; retire ClideHolderCard (T-305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The activity run, edit run, and sub-agent run cards now render through the shared ClideCollapserCard primitive instead of ClideHolderCard. The collapsed ticker now leads with the card label, the count sits in a fixed-width slot, and the status tick is pinned to the right edge. ClideHolderCard (T-266) is fully superseded — removed along with its test and golden; the deeper-control-passthrough coverage moved to the ClideCollapserCard widget test. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 6 + lib/builtin/claude/src/conversation_view.dart | 26 +-- lib/builtin/claude/src/holder_card.dart | 207 ------------------ test/builtin/claude/holder_card_test.dart | 138 ------------ test/goldens/goldens/linux/holder_card.png | Bin 19616 -> 0 bytes test/goldens/holder_card_goldens_test.dart | 66 ------ .../src/clide_collapser_card_test.dart | 17 ++ 7 files changed, 36 insertions(+), 424 deletions(-) delete mode 100644 lib/builtin/claude/src/holder_card.dart delete mode 100644 test/builtin/claude/holder_card_test.dart delete mode 100644 test/goldens/goldens/linux/holder_card.png delete mode 100644 test/goldens/holder_card_goldens_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 473fbf18..e4975753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -194,6 +194,12 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Changed +- The conversation stream's **collapsible cards** (activity runs, edit runs, + sub-agent runs) now share one `ClideCollapserCard` primitive with consistent + chrome: the collapsed ticker leads with the card's label, the step/edit count + sits in a fixed-width slot, and the status tick (spinner / check / cross) is + pinned hard against the card's right edge while the chevron hugs the left. A + `color` drives the border + label tint per card type. (T-305) - The Claude composer's **slash typeahead**, the team-chat **@-mention** list, and the status-bar **theme switcher** now ride the shared `ClideAnchoredOverlay` + `ClideMenu` popover primitive, alongside the menu diff --git a/lib/builtin/claude/src/conversation_view.dart b/lib/builtin/claude/src/conversation_view.dart index fc320c74..25ba8120 100644 --- a/lib/builtin/claude/src/conversation_view.dart +++ b/lib/builtin/claude/src/conversation_view.dart @@ -16,7 +16,6 @@ import 'dart:io'; import 'package:clide/builtin/claude/src/activity_cluster.dart'; import 'package:clide/builtin/claude/src/conversation_card.dart'; import 'package:clide/builtin/claude/src/conversation_controller.dart'; -import 'package:clide/builtin/claude/src/holder_card.dart'; import 'package:clide/builtin/claude/src/image_thumbnail.dart'; import 'package:clide/builtin/claude/src/prompt_card.dart'; import 'package:clide/builtin/claude/src/transcript_reader.dart'; @@ -603,10 +602,10 @@ class _ConversationTurn extends StatelessWidget { card, Padding( padding: const EdgeInsets.only(left: 12), - child: ClideHolderCard( - title: 'agent run', + child: ClideCollapserCard( + label: 'agent run', collapsedSummary: _summarizeActivity(runItems.last), - stepLabel: runItems.length == 1 ? '1 step' : '${runItems.length} steps', + counter: runItems.length == 1 ? '1 step' : '${runItems.length} steps', children: [ for (final r in runItems) _ConversationTurn( @@ -711,11 +710,11 @@ class _ConversationTurn extends StatelessWidget { } /// A folded run of meta items rendered as one collapsible activity card -/// (T-230), now through the shared [ClideHolderCard] container (T-266). +/// (T-230), now through the shared [ClideCollapserCard] container (T-305). /// Collapsed (default): a one-line live ticker of the latest step + a step /// count — re-grouped on every rebuild, so the ticker updates in place as the -/// run grows. Expanded: every folded step, wrapped in the holder frame whose -/// background toggles collapse. Stateless — the holder owns the expand state. +/// run grows. Expanded: every folded step, wrapped in the collapser frame whose +/// background toggles collapse. Stateless — the collapser owns the expand state. class _ActivityCard extends StatelessWidget { const _ActivityCard({ super.key, @@ -739,9 +738,10 @@ class _ActivityCard extends StatelessWidget { @override Widget build(BuildContext context) { final count = items.length; - return ClideHolderCard( + return ClideCollapserCard( + label: 'Activity', collapsedSummary: _summarizeActivity(items.last), - stepLabel: count == 1 ? '1 step' : '$count steps', + counter: count == 1 ? '1 step' : '$count steps', status: _runStatus(items, resultByToolUseId), children: [ for (final item in items) @@ -774,7 +774,7 @@ ClideRunStatus? _runStatus(List items, Map children; - - /// Title shown in the expanded header (default `Activity`). - final String title; - - final bool initiallyExpanded; - - /// Optional aggregate run status (spinner / check / cross) shown at the head - /// of the ticker + header — the run's live state (T-296). Null shows nothing. - final ClideRunStatus? status; - - @override - State createState() => _ClideHolderCardState(); -} - -class _ClideHolderCardState extends State { - late bool _expanded = widget.initiallyExpanded; - final FocusNode _controlFocus = FocusNode(debugLabel: 'holder-control'); - - @override - void dispose() { - _controlFocus.dispose(); - super.dispose(); - } - - void _toggle() => setState(() => _expanded = !_expanded); - - @override - Widget build(BuildContext context) { - final tokens = ClideTheme.of(context).surface; - return Semantics( - button: true, - expanded: _expanded, - label: '${widget.title}, ${widget.stepLabel}, ${_expanded ? 'expanded' : 'collapsed'}', - excludeSemantics: true, - child: Padding( - // Match ConversationCard's inter-card margin (bottom 14, no top) so a - // folded activity / agent-run card sits in the same rhythm as the prose - // cards around it — not crammed 3px below the next one (T-282). - padding: const EdgeInsets.only(bottom: 14), - child: _expanded ? _expandedFrame(tokens) : _tickerRow(tokens), - ), - ); - } - - /// Collapsed: the ticker row IS the toggle, focusable for keyboard/AT. - Widget _tickerRow(SurfaceTokens tokens) => ClideTappable( - focusNode: _controlFocus, - onTap: _toggle, - tooltip: 'Expand', - builder: (context, hovered, focused) => Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: (hovered || focused) ? tokens.listItemHoverBackground : tokens.listItemBackground, - border: Border.all(color: tokens.panelBorder), - borderRadius: BorderRadius.circular(4), - ), - child: Row( - children: [ - ClideIcon(const ChevronRightIcon(), size: 12, color: tokens.globalTextMuted), - const SizedBox(width: 8), - Expanded( - child: ClideText( - widget.collapsedSummary, - fontSize: clideFontCaption, - fontFamily: clideMonoFamily, - color: tokens.globalTextMuted, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 8), - if (widget.status != null) ...[ - ClideStatusIndicator(status: widget.status!, size: 12), - const SizedBox(width: 8), - ], - ClideText(widget.stepLabel, fontSize: clideFontCaption, color: tokens.globalTextMuted), - ], - ), - ), - ); - - /// Expanded: a framed container wrapping the sub-cards. The frame BACKGROUND - /// is a gesture target behind the children that only fires for hits the - /// children don't consume. - Widget _expandedFrame(SurfaceTokens tokens) => DecoratedBox( - decoration: BoxDecoration( - border: Border.all(color: tokens.panelBorder), - borderRadius: BorderRadius.circular(4), - ), - child: Stack( - children: [ - // Background toggle: behind the children, not a whole-card overlay, - // so child taps are never intercepted. Excluded from focus traversal - // — the header caret is the single keyboard stop. - Positioned.fill( - child: ExcludeFocus( - child: ClideTappable( - onTap: _toggle, - tooltip: 'Collapse', - builder: (_, __, ___) => const SizedBox.expand(), - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _headerRow(tokens), - Padding( - padding: const EdgeInsets.fromLTRB(10, 0, 10, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - for (final child in widget.children) - // Each child opaquely consumes its full bounds so a body - // tap interacts with the card (or does nothing), never - // the holder background. Deeper controls (caret/copy) - // still win; only taps are absorbed, so selection drags - // pass through to the SelectionArea. - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () {}, - child: child, - ), - ], - ), - ), - ], - ), - ], - ), - ); - - /// The explicit, focusable collapse control. A background tap is not - /// keyboard/AT reachable on its own, so this keeps the control on the Tab - /// path and Enter/Space-activatable (D-78). - Widget _headerRow(SurfaceTokens tokens) => ClideTappable( - focusNode: _controlFocus, - onTap: _toggle, - tooltip: 'Collapse', - builder: (context, hovered, focused) => Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: (hovered || focused) ? tokens.listItemHoverBackground : tokens.listItemBackground, - border: Border(bottom: BorderSide(color: tokens.panelBorder)), - ), - child: Row( - children: [ - ClideIcon(const ChevronDownIcon(), size: 12, color: tokens.globalTextMuted), - const SizedBox(width: 8), - Expanded( - child: ClideText(widget.title, fontSize: clideFontCaption, fontFamily: clideMonoFamily, color: tokens.globalTextMuted), - ), - const SizedBox(width: 8), - if (widget.status != null) ...[ - ClideStatusIndicator(status: widget.status!, size: 12), - const SizedBox(width: 8), - ], - ClideText(widget.stepLabel, fontSize: clideFontCaption, color: tokens.globalTextMuted), - ], - ), - ), - ); -} diff --git a/test/builtin/claude/holder_card_test.dart b/test/builtin/claude/holder_card_test.dart deleted file mode 100644 index 0f3aa11c..00000000 --- a/test/builtin/claude/holder_card_test.dart +++ /dev/null @@ -1,138 +0,0 @@ -/// Widget tests for the shared [ClideHolderCard] container primitive (T-266): -/// collapsed ticker, background-as-toggle, child taps not hijacked, the copy -/// control still works, and the keyboard/AT collapse path. -library; - -import 'package:clide/builtin/claude/src/conversation_card.dart'; -import 'package:clide/builtin/claude/src/holder_card.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.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(); - TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (_) async => null); - }); - tearDown(() { - TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null); - f.dispose(); - }); - - // A tight, positioned tree so the holder's Row/Expanded get a bounded width - // and a deterministic on-screen rect for coordinate hit-testing. - Future pump(WidgetTester tester, {List? children, String summary = 'latest step', bool expanded = false}) async { - await tester.pumpWidget(harness( - f, - Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 400, - child: ClideHolderCard( - collapsedSummary: summary, - stepLabel: '2 steps', - initiallyExpanded: expanded, - children: children ?? - const [ - ConversationCard( - variant: ConversationCardVariant.bordered, - accent: Color(0xFFFFFFFF), - label: 'step', - body: Text('child body', textDirection: TextDirection.ltr), - ), - ], - ), - ), - ), - )); - await tester.pump(); - } - - testWidgets('collapsed shows the ticker summary + step count; tap expands', (tester) async { - await pump(tester); - expect(find.text('latest step'), findsOneWidget); - expect(find.text('2 steps'), findsOneWidget); - expect(find.bySemanticsLabel('Activity, 2 steps, collapsed'), findsOneWidget); - expect(find.text('child body'), findsNothing); // children hidden while collapsed - - await tester.tap(find.bySemanticsLabel('Activity, 2 steps, collapsed')); - await tester.pump(); - expect(find.bySemanticsLabel('Activity, 2 steps, expanded'), findsOneWidget); - expect(find.text('child body'), findsOneWidget); - }); - - testWidgets('a tap on the holder BACKGROUND (gutter) collapses it', (tester) async { - await pump(tester, expanded: true); - expect(find.text('child body'), findsOneWidget); // expanded - - // The left gutter (x = left+4) is holder background — children start at - // left+10 — and below the header, so this hits the background toggle. - final r = tester.getRect(find.byType(ClideHolderCard)); - await tester.tapAt(Offset(r.left + 4, r.center.dy)); - await tester.pump(); - expect(find.text('child body'), findsNothing); // collapsed - expect(find.bySemanticsLabel('Activity, 2 steps, collapsed'), findsOneWidget); - }); - - testWidgets('a tap on a child sub-card does NOT toggle the holder', (tester) async { - await pump(tester, expanded: true); - expect(find.text('child body'), findsOneWidget); - - // Tapping the child's body must be absorbed by the child, not bubble to the - // holder background toggle. - await tester.tap(find.text('child body')); - await tester.pump(); - expect(find.text('child body'), findsOneWidget); // still expanded - expect(find.bySemanticsLabel('Activity, 2 steps, expanded'), findsOneWidget); - }); - - testWidgets('a child copy button still copies — not swallowed by the holder', (tester) async { - await pump(tester, expanded: true, children: const [ - ConversationCard( - variant: ConversationCardVariant.bordered, - accent: Color(0xFFFFFFFF), - label: 'step', - copyText: 'copied from a held card', - body: Text('child body', textDirection: TextDirection.ltr), - ), - ]); - - // Hover the child to reveal its copy action, then tap it. - final g = await tester.createGesture(kind: PointerDeviceKind.mouse); - await g.addPointer(location: Offset.zero); - addTearDown(() => g.removePointer()); - await g.moveTo(tester.getCenter(find.byType(ConversationCard))); - await tester.pump(); - - await tester.tap(find.text('copy')); - await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); - expect(f.services.clipboard.readAs(), 'copied from a held card'); - // And the holder stayed expanded (the copy tap wasn't a background toggle). - expect(find.bySemanticsLabel('Activity, 2 steps, expanded'), findsOneWidget); - - // The hover starts a ClideTooltip show-delay (a Future.delayed that exit - // can't cancel). Move off and advance past the delay so it fires and leaves - // no pending timer at teardown. - await g.moveTo(const Offset(-100, -100)); - await tester.pump(const Duration(milliseconds: 600)); - }); - - testWidgets('the explicit control is keyboard-focusable and toggles on Activate (a11y)', (tester) async { - await pump(tester); // collapsed; the ticker row is the control - final focusWidget = tester.widget( - find.ancestor(of: find.text('latest step'), matching: find.byType(Focus)).first, - ); - focusWidget.focusNode!.requestFocus(); - await tester.pump(); - expect(focusWidget.focusNode!.hasFocus, isTrue); - - Actions.invoke(focusWidget.focusNode!.context!, const ActivateIntent()); - await tester.pump(); - expect(find.bySemanticsLabel('Activity, 2 steps, expanded'), findsOneWidget); - }); -} diff --git a/test/goldens/goldens/linux/holder_card.png b/test/goldens/goldens/linux/holder_card.png deleted file mode 100644 index 155dbb87bed4cab5759bba46e91113b151beec88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19616 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
E$AJnkr`{=h2K*ch3GZ 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 NK)_-AA f = await KernelFixture.create()); - tearDown(() async => f.dispose()); - - goldenTest( - 'ClideHolderCard (T-266): collapsed ticker + expanded container of sub-cards', - fileName: 'holder_card', - builder: () => GoldenTestGroup( - columns: 1, - children: [ - GoldenTestScenario( - name: 'collapsed (ticker + step count)', - child: _wrap( - f, - const ClideHolderCard( - collapsedSummary: 'Bash ls -la', - stepLabel: '3 steps', - children: [], - ), - ), - ), - GoldenTestScenario( - name: 'expanded (framed container wrapping sub-cards)', - child: _wrap( - f, - const ClideHolderCard( - collapsedSummary: 'Bash ls -la', - stepLabel: '3 steps', - initiallyExpanded: true, - children: [ - ConversationCard( - variant: ConversationCardVariant.bordered, - accent: Color(0xFF4C9AFF), - label: 'Bash', - body: Text('ls -la', textDirection: TextDirection.ltr), - ), - ConversationCard( - variant: ConversationCardVariant.bordered, - accent: Color(0xFF4C9AFF), - label: 'Read', - body: Text('/lib/main.dart', textDirection: TextDirection.ltr), - ), - ], - ), - ), - ), - ], - ), - ); -} - -Widget _wrap(KernelFixture f, Widget child) => SizedBox( - width: 360, - child: harness(f, child), - ); diff --git a/test/widgets/src/clide_collapser_card_test.dart b/test/widgets/src/clide_collapser_card_test.dart index 48a3df19..cd62cf80 100644 --- a/test/widgets/src/clide_collapser_card_test.dart +++ b/test/widgets/src/clide_collapser_card_test.dart @@ -96,6 +96,23 @@ void main() { expect(find.bySemanticsLabel('Edits, 3 edits, expanded'), findsOneWidget); }); + testWidgets('a deeper interactive control inside an item still fires (not swallowed)', (tester) async { + var tapped = false; + await pump(tester, expanded: true, children: [ + ClideTappable( + onTap: () => tapped = true, + builder: (_, __, ___) => const Padding( + padding: EdgeInsets.all(8), + child: Text('press me', textDirection: TextDirection.ltr), + ), + ), + ]); + await tester.tap(find.text('press me')); + await tester.pump(); + expect(tapped, isTrue); // the item's own control won the hit + expect(find.bySemanticsLabel('Edits, 3 edits, expanded'), findsOneWidget); // collapser unchanged + }); + testWidgets('the ticker is keyboard-focusable and toggles on Activate (a11y)', (tester) async { await pump(tester); final focusWidget = tester.widget(