group consecutive same-file edits into one collapsed card (T-296)

A run of 2+ consecutive edits to the same file now folds into one ClideHolderCard
labelled '# edits' (coalesceEditRuns, run after groupConversation) instead of a
stack of cards; a different file or an interleaving step splits the run. Every
edit stays reachable on expand.

The holder gained an optional aggregate status. New owned primitives: ClideSpinner
(the logo mark, monochrome, 3D Y-axis rotation, reduced-motion-aware) and
ClideStatusIndicator (running→spinner / success→check / error→cross, with an
AnimatedSwitcher seam for a richer transition later — kept self-contained, not
built on ConversationCard's mark). The activity card shares the same indicator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 18:34:27 +02:00
co-authored by Claude Opus 4.8
parent 6efb3b5d2b
commit 407ed25ab0
10 changed files with 447 additions and 6 deletions
@@ -0,0 +1,43 @@
/// A self-contained run-status glyph: spinner while running, check on success,
/// cross on failure (T-296).
///
/// Deliberately NOT built on ConversationCard's success/error mark — it owns its
/// own states and rendering so the spinner→check / spinner→cross transition can
/// grow richer (a morph/cross-fade) without being constrained by that card. A
/// light [AnimatedSwitcher] cross-fade between states is wired now; the keyed
/// children leave the seam for a fuller transition later.
library;
import 'package:clide/kernel/src/theme/controller.dart';
import 'package:clide/widgets/src/clide_icon.dart';
import 'package:clide/widgets/src/clide_spinner.dart';
import 'package:clide/widgets/src/icons/check.dart';
import 'package:clide/widgets/src/icons/x.dart';
import 'package:flutter/widgets.dart';
enum ClideRunStatus { running, success, error }
class ClideStatusIndicator extends StatelessWidget {
const ClideStatusIndicator({super.key, required this.status, this.size = 14});
final ClideRunStatus status;
final double size;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
final (Widget glyph, String label) = switch (status) {
ClideRunStatus.running => (ClideSpinner(size: size, color: tokens.globalTextMuted, key: const ValueKey('running')), 'running'),
ClideRunStatus.success => (ClideIcon(const CheckIcon(), size: size, color: tokens.statusSuccess, key: const ValueKey('success')), 'succeeded'),
ClideRunStatus.error => (ClideIcon(const CloseIcon(), size: size, color: tokens.statusError, key: const ValueKey('error')), 'failed'),
};
return Semantics(
label: label,
container: true,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: glyph,
),
);
}
}