From 9905854fcaa35a4851c30295fc46182911419c19 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 7 Jun 2026 11:08:51 +0200 Subject: [PATCH] shuffle the running-verb order per turn (T-255) Each RunningIndicator shuffles a copy of the verb list on creation, so a turn doesn't always start with the same word. A `shuffle` flag (default true) is turned off in tests for deterministic assertions. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/builtin/claude/src/running_indicator.dart | 14 +++++++++++--- test/builtin/claude/running_indicator_test.dart | 10 +++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/builtin/claude/src/running_indicator.dart b/lib/builtin/claude/src/running_indicator.dart index 100db116..0c133a65 100644 --- a/lib/builtin/claude/src/running_indicator.dart +++ b/lib/builtin/claude/src/running_indicator.dart @@ -41,13 +41,21 @@ const List runningVerbs = [ const int _secondsPerWord = 4; class RunningIndicator extends StatefulWidget { - const RunningIndicator({super.key}); + const RunningIndicator({super.key, this.shuffle = true}); + + /// Randomize the rotation order per turn so you don't always see the same + /// sequence. Off in tests for deterministic assertions. + final bool shuffle; @override State createState() => _RunningIndicatorState(); } class _RunningIndicatorState extends State with SingleTickerProviderStateMixin { + /// The verbs for this turn — a shuffled copy in production, the canonical + /// order in tests. Same length, so the period is unchanged. + late final List _verbs = widget.shuffle ? (List.of(runningVerbs)..shuffle()) : runningVerbs; + // One full pass over every verb; value 0→1 maps linearly to elapsed seconds. static final int _periodSeconds = runningVerbs.length * _secondsPerWord; @@ -81,13 +89,13 @@ class _RunningIndicatorState extends State with SingleTickerPr label: 'Claude is running', child: ExcludeSemantics( child: reduced - ? ClideText('${runningVerbs.first}…', muted: true, fontSize: clideFontMeta) + ? ClideText('${_verbs.first}…', muted: true, fontSize: clideFontMeta) : AnimatedBuilder( animation: _c, builder: (ctx, _) { final elapsed = _c.value * _periodSeconds; final dots = '.' * (elapsed.floor() % 4); - final word = runningVerbs[(elapsed ~/ _secondsPerWord) % runningVerbs.length]; + final word = _verbs[(elapsed ~/ _secondsPerWord) % _verbs.length]; return ClideText('$word$dots', muted: true, fontSize: clideFontMeta); }, ), diff --git a/test/builtin/claude/running_indicator_test.dart b/test/builtin/claude/running_indicator_test.dart index ee05b27c..76885595 100644 --- a/test/builtin/claude/running_indicator_test.dart +++ b/test/builtin/claude/running_indicator_test.dart @@ -23,7 +23,7 @@ void main() { Widget wrap({bool reducedMotion = false}) => MediaQuery( data: MediaQueryData(disableAnimations: reducedMotion), - child: const RunningIndicator(), + child: const RunningIndicator(shuffle: false), ); testWidgets('animates the ellipsis and rotates the verb', (tester) async { @@ -51,4 +51,12 @@ void main() { await tester.pump(const Duration(seconds: 6)); expect(_text(tester), 'Pondering…'); // unchanged — no animation running }); + + testWidgets('shuffle:true renders a verb from the list', (tester) async { + await tester.pumpWidget(harness(f, const MediaQuery(data: MediaQueryData(), child: RunningIndicator(shuffle: true)))); + await tester.pump(); + final word = _text(tester)!.replaceAll('.', ''); + expect(runningVerbs.contains(word), isTrue); + await tester.pumpWidget(harness(f, const SizedBox())); // dispose the animation + }); }