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 + }); }