neoagent 2.4.1-beta.30 → 2.4.1-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  2. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  3. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +78 -56
  4. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  5. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +18 -17
  6. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +2 -1
  7. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  8. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +15 -11
  9. package/flutter_app/lib/main_account_settings.dart +1 -1
  10. package/flutter_app/lib/main_admin.dart +1 -1
  11. package/flutter_app/lib/main_app_shell.dart +190 -191
  12. package/flutter_app/lib/main_chat.dart +381 -228
  13. package/flutter_app/lib/main_devices.dart +1 -1
  14. package/flutter_app/lib/main_launcher.dart +1 -1
  15. package/flutter_app/lib/main_operations.dart +3 -3
  16. package/flutter_app/lib/main_shared.dart +336 -340
  17. package/flutter_app/lib/main_spacing.dart +4 -4
  18. package/flutter_app/lib/main_theme.dart +20 -14
  19. package/flutter_app/lib/src/theme/palette.dart +76 -34
  20. package/flutter_app/pubspec.lock +2 -2
  21. package/flutter_app/pubspec.yaml +1 -1
  22. package/package.json +1 -1
  23. package/server/public/.last_build_id +1 -1
  24. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  25. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  26. package/server/public/flutter_bootstrap.js +1 -1
  27. package/server/public/main.dart.js +60738 -60521
  28. package/server/services/ai/tools.js +51 -7
  29. package/server/services/messaging/formatting_guides.js +26 -1
  30. package/server/services/messaging/manager.js +5 -2
  31. package/server/services/tasks/runtime.js +182 -7
  32. package/server/services/tasks/task_repository.js +13 -0
@@ -357,268 +357,422 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
357
357
  final messages = controller.visibleChatMessages;
358
358
  _maybeFollowChatContent(messages, controller);
359
359
 
360
+ final threadChildren = <Widget>[
361
+ if (controller.errorMessage != null) ...<Widget>[
362
+ _InlineError(message: controller.errorMessage!),
363
+ const SizedBox(height: 16),
364
+ ],
365
+ if (controller.activeRun != null || controller.toolEvents.isNotEmpty)
366
+ Padding(
367
+ padding: const EdgeInsets.only(bottom: 16),
368
+ child: _RunStatusPanel(
369
+ run: controller.activeRun,
370
+ tools: controller.toolEvents,
371
+ ),
372
+ ),
373
+ if (messages.isEmpty)
374
+ Padding(
375
+ padding: const EdgeInsets.only(top: 64),
376
+ child: Center(
377
+ child: _EmptyState(
378
+ title: 'How can I help?',
379
+ subtitle:
380
+ 'Runs, tools, memory, scheduling, skills, and MCP are all available here.',
381
+ ),
382
+ ),
383
+ )
384
+ else
385
+ ...messages.map(
386
+ (entry) => Padding(
387
+ padding: const EdgeInsets.only(bottom: 18),
388
+ child: _ChatBubble(
389
+ entry: entry,
390
+ onLoadRunDetail: controller.fetchRunDetail,
391
+ ),
392
+ ),
393
+ ),
394
+ ];
395
+
396
+ Future<void> sendComposerMessage() async {
397
+ final task = _composerController.text;
398
+ if ((task.trim().isEmpty && _pendingSharedAttachments.isEmpty) ||
399
+ _isSendingChatMessage) {
400
+ return;
401
+ }
402
+ setState(() {
403
+ _isSendingChatMessage = true;
404
+ });
405
+ _composerController.clear();
406
+ final outgoingAttachments = _pendingSharedAttachments;
407
+ _clearSharedPayload();
408
+ try {
409
+ await controller.sendMessage(
410
+ task,
411
+ sharedAttachments: outgoingAttachments,
412
+ );
413
+ } finally {
414
+ if (mounted) {
415
+ setState(() {
416
+ _isSendingChatMessage = false;
417
+ });
418
+ }
419
+ }
420
+ }
421
+
422
+ final sidePadding = _chatSidePadding(context);
423
+
360
424
  return Column(
361
425
  children: <Widget>[
426
+ _ChatTopBar(controller: controller),
362
427
  Expanded(
363
428
  child: SelectionArea(
364
429
  child: ListView(
365
430
  controller: _scrollController,
366
- padding: _pagePadding(context),
431
+ padding: EdgeInsets.fromLTRB(sidePadding, 30, sidePadding, 18),
367
432
  children: <Widget>[
368
- _PageTitle(
369
- title: 'Chat',
370
- subtitle: 'Live agent chat with tool and stream status.',
371
- trailing: Wrap(
372
- spacing: 10,
373
- runSpacing: 10,
374
- crossAxisAlignment: WrapCrossAlignment.center,
375
- children: <Widget>[
376
- FilledButton.icon(
377
- onPressed: () => controller.setSelectedSection(
378
- AppSection.voiceAssistant,
379
- ),
380
- icon: Icon(Icons.call),
381
- label: Text('Call'),
382
- ),
383
- _MetaPill(
384
- label: controller.modelIndicator,
385
- icon: Icons.memory_outlined,
386
- ),
387
- _MetaPill(
388
- label: 'Agent: ${controller.activeAgentLabel}',
389
- icon: Icons.smart_toy_outlined,
390
- ),
391
- ],
392
- ),
393
- ),
394
- if (controller.errorMessage != null) ...<Widget>[
395
- _InlineError(message: controller.errorMessage!),
396
- const SizedBox(height: 16),
397
- ],
398
- if (controller.activeRun != null ||
399
- controller.toolEvents.isNotEmpty)
400
- Padding(
401
- padding: const EdgeInsets.only(bottom: 16),
402
- child: _RunStatusPanel(
403
- run: controller.activeRun,
404
- tools: controller.toolEvents,
405
- ),
406
- ),
407
- if (messages.isEmpty)
408
- Padding(
409
- padding: EdgeInsets.only(top: 64),
410
- child: Center(
411
- child: _EmptyState(
412
- title: 'How can I help?',
413
- subtitle:
414
- 'Runs, tools, memory, scheduling, skills, and MCP are all available here.',
415
- ),
416
- ),
417
- )
418
- else
419
- ...messages.map(
420
- (entry) => Padding(
421
- padding: const EdgeInsets.only(bottom: 18),
422
- child: _ChatBubble(
423
- entry: entry,
424
- onLoadRunDetail: controller.fetchRunDetail,
425
- ),
433
+ Center(
434
+ child: ConstrainedBox(
435
+ constraints: const BoxConstraints(maxWidth: 860),
436
+ child: Column(
437
+ crossAxisAlignment: CrossAxisAlignment.stretch,
438
+ children: threadChildren,
426
439
  ),
427
440
  ),
441
+ ),
428
442
  ],
429
443
  ),
430
444
  ),
431
445
  ),
432
446
  Container(
433
- padding: const EdgeInsets.fromLTRB(20, 14, 20, 20),
447
+ padding: EdgeInsets.fromLTRB(sidePadding, 12, sidePadding, 16),
434
448
  decoration: BoxDecoration(
435
- color: _bgPrimary,
449
+ color: _bgPrimary.withValues(alpha: 0.88),
436
450
  border: Border(top: BorderSide(color: _border)),
437
451
  ),
438
- child: Column(
439
- children: <Widget>[
440
- if (_pendingSharedAttachments.isNotEmpty)
441
- Padding(
442
- padding: const EdgeInsets.only(bottom: 10),
443
- child: _SharedAttachmentTray(
444
- attachments: _pendingSharedAttachments,
445
- onRemoveAt: (index) {
446
- setState(() {
447
- _pendingSharedAttachments = _pendingSharedAttachments
448
- .asMap()
449
- .entries
450
- .where((entry) => entry.key != index)
451
- .map((entry) => entry.value)
452
- .toList(growable: false);
453
- });
454
- if (_stickToBottom) {
455
- _scheduleScrollToBottom();
456
- }
457
- if (_pendingSharedAttachments.isEmpty) {
458
- _clearSharedPayload();
459
- }
460
- },
461
- onClear: () {
462
- _clearSharedPayload();
463
- },
464
- ),
465
- ),
466
- Container(
467
- padding: const EdgeInsets.fromLTRB(16, 4, 4, 4),
468
- decoration: BoxDecoration(
469
- color: _bgTertiary,
470
- borderRadius: BorderRadius.circular(14),
471
- border: Border.all(color: _border),
472
- ),
473
- child: Row(
474
- crossAxisAlignment: CrossAxisAlignment.end,
475
- children: <Widget>[
476
- Expanded(
477
- child: TextField(
478
- controller: _composerController,
479
- minLines: 1,
480
- maxLines: 6,
481
- keyboardType: TextInputType.multiline,
482
- textInputAction: TextInputAction.newline,
483
- decoration: InputDecoration(
484
- hintText: controller.chatComposerHint,
485
- isDense: true,
486
- filled: false,
487
- border: InputBorder.none,
488
- enabledBorder: InputBorder.none,
489
- focusedBorder: InputBorder.none,
490
- ),
452
+ child: Center(
453
+ child: ConstrainedBox(
454
+ constraints: const BoxConstraints(maxWidth: 860),
455
+ child: Column(
456
+ children: <Widget>[
457
+ if (_pendingSharedAttachments.isNotEmpty)
458
+ Padding(
459
+ padding: const EdgeInsets.only(bottom: 10),
460
+ child: _SharedAttachmentTray(
461
+ attachments: _pendingSharedAttachments,
462
+ onRemoveAt: (index) {
463
+ setState(() {
464
+ _pendingSharedAttachments =
465
+ _pendingSharedAttachments
466
+ .asMap()
467
+ .entries
468
+ .where((entry) => entry.key != index)
469
+ .map((entry) => entry.value)
470
+ .toList(growable: false);
471
+ });
472
+ if (_stickToBottom) {
473
+ _scheduleScrollToBottom();
474
+ }
475
+ if (_pendingSharedAttachments.isEmpty) {
476
+ _clearSharedPayload();
477
+ }
478
+ },
479
+ onClear: _clearSharedPayload,
491
480
  ),
492
481
  ),
493
- const SizedBox(width: 8),
494
- IconButton(
495
- tooltip: 'Attach files',
496
- onPressed: _attachFiles,
497
- icon: const Icon(Icons.attach_file_rounded),
498
- color: _textSecondary,
482
+ Container(
483
+ padding: const EdgeInsets.fromLTRB(8, 8, 8, 8),
484
+ decoration: BoxDecoration(
485
+ color: _bgCard,
486
+ borderRadius: BorderRadius.circular(21),
487
+ border: Border.all(color: _borderLight),
488
+ boxShadow: <BoxShadow>[
489
+ BoxShadow(
490
+ color: Colors.black.withValues(alpha: 0.08),
491
+ blurRadius: 18,
492
+ offset: const Offset(0, 6),
493
+ ),
494
+ ],
499
495
  ),
500
- const SizedBox(width: 2),
501
- _isTranscribing
502
- ? const SizedBox(
503
- width: 40,
504
- height: 40,
505
- child: Padding(
506
- padding: EdgeInsets.all(10),
507
- child: CircularProgressIndicator(strokeWidth: 2),
508
- ),
509
- )
510
- : IconButton(
511
- tooltip: _isDictating
512
- ? 'Stop & transcribe'
513
- : 'Dictate',
514
- onPressed: _isDictating
515
- ? _stopAndTranscribe
516
- : _startDictation,
517
- icon: Icon(
518
- _isDictating
519
- ? Icons.stop_circle_outlined
520
- : Icons.mic_none_rounded,
496
+ child: Row(
497
+ crossAxisAlignment: CrossAxisAlignment.end,
498
+ children: <Widget>[
499
+ _ChatComposerIconButton(
500
+ tooltip: 'Attach files',
501
+ icon: Icons.attach_file_rounded,
502
+ onPressed: _attachFiles,
503
+ ),
504
+ const SizedBox(width: 8),
505
+ Expanded(
506
+ child: TextField(
507
+ controller: _composerController,
508
+ minLines: 1,
509
+ maxLines: 6,
510
+ keyboardType: TextInputType.multiline,
511
+ textInputAction: TextInputAction.newline,
512
+ decoration: InputDecoration(
513
+ hintText: controller.chatComposerHint,
514
+ isDense: true,
515
+ filled: false,
516
+ border: InputBorder.none,
517
+ enabledBorder: InputBorder.none,
518
+ focusedBorder: InputBorder.none,
519
+ contentPadding: const EdgeInsets.symmetric(
520
+ vertical: 10,
521
+ ),
521
522
  ),
522
- color: _isDictating
523
- ? Theme.of(context).colorScheme.error
524
- : _textSecondary,
525
523
  ),
526
- const SizedBox(width: 2),
527
- FilledButton(
528
- onPressed: () => controller.setSelectedSection(
529
- AppSection.voiceAssistant,
530
- ),
531
- style: FilledButton.styleFrom(
532
- minimumSize: const Size(46, 42),
533
- padding: const EdgeInsets.symmetric(horizontal: 12),
534
- backgroundColor: _success,
535
- shape: RoundedRectangleBorder(
536
- borderRadius: BorderRadius.circular(10),
537
524
  ),
538
- ),
539
- child: Icon(Icons.call_rounded, color: Colors.white),
540
- ),
541
- const SizedBox(width: 8),
542
- Tooltip(
543
- message: 'Send (⌘↩)',
544
- child: FilledButton(
545
- onPressed: _isSendingChatMessage
546
- ? null
547
- : () async {
548
- final task = _composerController.text;
549
- if ((task.trim().isEmpty &&
550
- _pendingSharedAttachments.isEmpty) ||
551
- _isSendingChatMessage) {
552
- return;
553
- }
554
- setState(() {
555
- _isSendingChatMessage = true;
556
- });
557
- _composerController.clear();
558
- final outgoingAttachments =
559
- _pendingSharedAttachments;
560
- _clearSharedPayload();
561
- try {
562
- await controller.sendMessage(
563
- task,
564
- sharedAttachments: outgoingAttachments,
565
- );
566
- } finally {
567
- if (mounted) {
568
- setState(() {
569
- _isSendingChatMessage = false;
570
- });
571
- }
572
- }
573
- },
574
- style: FilledButton.styleFrom(
575
- minimumSize: const Size(46, 42),
576
- padding: const EdgeInsets.symmetric(horizontal: 12),
577
- backgroundColor: _accent,
578
- shape: RoundedRectangleBorder(
579
- borderRadius: BorderRadius.circular(10),
525
+ const SizedBox(width: 8),
526
+ _isTranscribing
527
+ ? const SizedBox(
528
+ width: 40,
529
+ height: 40,
530
+ child: Padding(
531
+ padding: EdgeInsets.all(10),
532
+ child: CircularProgressIndicator(
533
+ strokeWidth: 2,
534
+ ),
535
+ ),
536
+ )
537
+ : _ChatComposerIconButton(
538
+ tooltip: _isDictating
539
+ ? 'Stop & transcribe'
540
+ : 'Dictate',
541
+ icon: _isDictating
542
+ ? Icons.stop_circle_outlined
543
+ : Icons.mic_none_rounded,
544
+ color: _isDictating
545
+ ? Theme.of(context).colorScheme.error
546
+ : null,
547
+ onPressed: _isDictating
548
+ ? _stopAndTranscribe
549
+ : _startDictation,
550
+ ),
551
+ const SizedBox(width: 6),
552
+ _ChatComposerIconButton(
553
+ tooltip: 'Call agent',
554
+ icon: Icons.call_rounded,
555
+ color: Colors.white,
556
+ backgroundColor: _success,
557
+ onPressed: () => controller.setSelectedSection(
558
+ AppSection.voiceAssistant,
580
559
  ),
581
560
  ),
582
- child: Icon(
583
- controller.hasLiveRun
561
+ const SizedBox(width: 6),
562
+ _ChatComposerIconButton(
563
+ tooltip: 'Send',
564
+ icon: controller.hasLiveRun
584
565
  ? Icons.alt_route_rounded
585
566
  : Icons.north_east_rounded,
586
567
  color: Colors.white,
568
+ backgroundColor: _accent,
569
+ onPressed: _isSendingChatMessage
570
+ ? null
571
+ : sendComposerMessage,
572
+ ),
573
+ ],
574
+ ),
575
+ ),
576
+ const SizedBox(height: 10),
577
+ Row(
578
+ children: <Widget>[
579
+ Expanded(
580
+ child: Row(
581
+ children: <Widget>[
582
+ Container(
583
+ width: 6,
584
+ height: 6,
585
+ decoration: BoxDecoration(
586
+ shape: BoxShape.circle,
587
+ color: controller.hasLiveRun
588
+ ? _success
589
+ : _textMuted,
590
+ ),
591
+ ),
592
+ const SizedBox(width: 7),
593
+ Expanded(
594
+ child: Text(
595
+ controller.chatStatusLabel,
596
+ maxLines: 1,
597
+ overflow: TextOverflow.ellipsis,
598
+ style: GoogleFonts.geistMono(
599
+ fontSize: 11.5,
600
+ color: _textMuted,
601
+ ),
602
+ ),
603
+ ),
604
+ ],
587
605
  ),
588
606
  ),
607
+ const SizedBox(width: 12),
608
+ Flexible(
609
+ child: Container(
610
+ padding: const EdgeInsets.symmetric(
611
+ horizontal: 9,
612
+ vertical: 4,
613
+ ),
614
+ decoration: BoxDecoration(
615
+ color: _bgCard,
616
+ borderRadius: BorderRadius.circular(8),
617
+ border: Border.all(color: _border),
618
+ ),
619
+ child: Row(
620
+ mainAxisSize: MainAxisSize.min,
621
+ children: <Widget>[
622
+ Flexible(
623
+ child: Text(
624
+ controller.hasLiveRun
625
+ ? 'Steering mode'
626
+ : controller.modelIndicator,
627
+ maxLines: 1,
628
+ overflow: TextOverflow.ellipsis,
629
+ style: GoogleFonts.geistMono(
630
+ fontSize: 11.5,
631
+ color: _textSecondary,
632
+ ),
633
+ ),
634
+ ),
635
+ const SizedBox(width: 5),
636
+ Icon(
637
+ Icons.keyboard_arrow_down_rounded,
638
+ size: 13,
639
+ color: _textMuted,
640
+ ),
641
+ ],
642
+ ),
643
+ ),
644
+ ),
645
+ ],
646
+ ),
647
+ ],
648
+ ),
649
+ ),
650
+ ),
651
+ ),
652
+ ],
653
+ );
654
+ }
655
+ }
656
+
657
+ double _chatSidePadding(BuildContext context) {
658
+ final width = MediaQuery.sizeOf(context).width;
659
+ if (width >= 1280) return 40;
660
+ if (width >= 900) return 30;
661
+ return 20;
662
+ }
663
+
664
+ class _ChatTopBar extends StatelessWidget {
665
+ const _ChatTopBar({required this.controller});
666
+
667
+ final NeoAgentController controller;
668
+
669
+ @override
670
+ Widget build(BuildContext context) {
671
+ return Container(
672
+ height: 58,
673
+ padding: EdgeInsets.symmetric(horizontal: _chatSidePadding(context)),
674
+ decoration: BoxDecoration(
675
+ color: _bgPrimary.withValues(alpha: 0.72),
676
+ border: Border(bottom: BorderSide(color: _border)),
677
+ ),
678
+ child: Row(
679
+ children: <Widget>[
680
+ Text(
681
+ 'Chat',
682
+ style: GoogleFonts.geist(
683
+ fontSize: 15,
684
+ fontWeight: FontWeight.w600,
685
+ color: _textPrimary,
686
+ ),
687
+ ),
688
+ const SizedBox(width: 12),
689
+ Expanded(
690
+ child: Text(
691
+ '/ ${controller.activeAgentLabel} / ${controller.modelIndicator}',
692
+ maxLines: 1,
693
+ overflow: TextOverflow.ellipsis,
694
+ style: GoogleFonts.geistMono(fontSize: 11, color: _textMuted),
695
+ ),
696
+ ),
697
+ const SizedBox(width: 12),
698
+ Row(
699
+ mainAxisSize: MainAxisSize.min,
700
+ children: <Widget>[
701
+ Container(
702
+ width: 7,
703
+ height: 7,
704
+ decoration: BoxDecoration(
705
+ shape: BoxShape.circle,
706
+ color: controller.hasLiveRun ? _success : _accentAlt,
707
+ boxShadow: <BoxShadow>[
708
+ BoxShadow(
709
+ color: (controller.hasLiveRun ? _success : _accentAlt)
710
+ .withValues(alpha: 0.18),
711
+ blurRadius: 0,
712
+ spreadRadius: 3,
589
713
  ),
590
714
  ],
591
715
  ),
592
716
  ),
593
- const SizedBox(height: 8),
594
- Row(
595
- children: <Widget>[
596
- Expanded(
597
- child: Text(
598
- controller.chatStatusLabel,
599
- maxLines: 1,
600
- overflow: TextOverflow.ellipsis,
601
- style: TextStyle(fontSize: 12, color: _textSecondary),
602
- ),
603
- ),
604
- const SizedBox(width: 12),
605
- Flexible(
606
- child: Text(
607
- controller.hasLiveRun
608
- ? 'Steering mode'
609
- : controller.modelIndicator,
610
- maxLines: 1,
611
- overflow: TextOverflow.ellipsis,
612
- textAlign: TextAlign.right,
613
- style: TextStyle(fontSize: 12, color: _textSecondary),
614
- ),
615
- ),
616
- ],
717
+ const SizedBox(width: 7),
718
+ Text(
719
+ controller.hasLiveRun ? 'live' : 'idle',
720
+ style: GoogleFonts.geistMono(
721
+ fontSize: 12,
722
+ color: _textSecondary,
723
+ ),
617
724
  ),
618
725
  ],
619
726
  ),
727
+ ],
728
+ ),
729
+ );
730
+ }
731
+ }
732
+
733
+ class _ChatComposerIconButton extends StatelessWidget {
734
+ const _ChatComposerIconButton({
735
+ required this.tooltip,
736
+ required this.icon,
737
+ required this.onPressed,
738
+ this.color,
739
+ this.backgroundColor,
740
+ });
741
+
742
+ final String tooltip;
743
+ final IconData icon;
744
+ final VoidCallback? onPressed;
745
+ final Color? color;
746
+ final Color? backgroundColor;
747
+
748
+ @override
749
+ Widget build(BuildContext context) {
750
+ final foreground = color ?? _textSecondary;
751
+ return Tooltip(
752
+ message: tooltip,
753
+ child: Material(
754
+ color: Colors.transparent,
755
+ borderRadius: BorderRadius.circular(11),
756
+ child: InkWell(
757
+ borderRadius: BorderRadius.circular(11),
758
+ onTap: onPressed,
759
+ child: Container(
760
+ width: 40,
761
+ height: 40,
762
+ decoration: BoxDecoration(
763
+ color: onPressed == null
764
+ ? _bgTertiary.withValues(alpha: 0.52)
765
+ : backgroundColor ?? Colors.transparent,
766
+ borderRadius: BorderRadius.circular(11),
767
+ ),
768
+ child: Icon(
769
+ icon,
770
+ size: 19,
771
+ color: onPressed == null ? _textMuted : foreground,
772
+ ),
773
+ ),
620
774
  ),
621
- ],
775
+ ),
622
776
  );
623
777
  }
624
778
  }
@@ -3403,7 +3557,7 @@ class _RunResponseCard extends StatelessWidget {
3403
3557
  height: 1.6,
3404
3558
  ),
3405
3559
  code: Theme.of(context).textTheme.bodyMedium?.copyWith(
3406
- fontFamily: GoogleFonts.jetBrainsMono().fontFamily,
3560
+ fontFamily: GoogleFonts.geistMono().fontFamily,
3407
3561
  backgroundColor: _bgSecondary,
3408
3562
  color: _textPrimary,
3409
3563
  ),
@@ -3486,8 +3640,7 @@ class _DeliverableSummaryCard extends StatelessWidget {
3486
3640
  style: TextStyle(
3487
3641
  color: _textSecondary,
3488
3642
  fontSize: 12,
3489
- fontFamily:
3490
- GoogleFonts.jetBrainsMono().fontFamily,
3643
+ fontFamily: GoogleFonts.geistMono().fontFamily,
3491
3644
  ),
3492
3645
  ),
3493
3646
  ],
@@ -3798,7 +3951,7 @@ class _RunDetailBlock extends StatelessWidget {
3798
3951
  fontSize: 12.5,
3799
3952
  color: _textPrimary,
3800
3953
  fontFamily: monospace
3801
- ? GoogleFonts.jetBrainsMono().fontFamily
3954
+ ? GoogleFonts.geistMono().fontFamily
3802
3955
  : null,
3803
3956
  ),
3804
3957
  ),