neoagent 3.0.1-beta.9 → 3.1.0

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 (97) hide show
  1. package/.env.example +2 -10
  2. package/README.md +12 -3
  3. package/docs/automation.md +37 -0
  4. package/docs/benchmarking.md +102 -0
  5. package/docs/billing.md +34 -8
  6. package/docs/configuration.md +11 -2
  7. package/docs/getting-started.md +10 -8
  8. package/docs/operations.md +1 -1
  9. package/flutter_app/lib/main.dart +3 -0
  10. package/flutter_app/lib/main_account_settings.dart +138 -0
  11. package/flutter_app/lib/main_app_shell.dart +38 -13
  12. package/flutter_app/lib/main_billing.dart +1465 -0
  13. package/flutter_app/lib/main_chat.dart +1612 -214
  14. package/flutter_app/lib/main_controller.dart +398 -26
  15. package/flutter_app/lib/main_devices.dart +293 -207
  16. package/flutter_app/lib/main_models.dart +142 -0
  17. package/flutter_app/lib/main_navigation.dart +19 -1
  18. package/flutter_app/lib/main_operations.dart +288 -9
  19. package/flutter_app/lib/main_settings.dart +510 -300
  20. package/flutter_app/lib/main_shared.dart +2 -0
  21. package/flutter_app/lib/main_timeline.dart +1378 -0
  22. package/flutter_app/lib/src/backend_client.dart +149 -19
  23. package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
  24. package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
  25. package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
  26. package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
  27. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
  28. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
  29. package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
  30. package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
  31. package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
  32. package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
  33. package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
  34. package/flutter_app/lib/src/recording_payloads.dart +9 -0
  35. package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
  36. package/flutter_app/windows/runner/flutter_window.cpp +75 -0
  37. package/landing/index.html +3 -3
  38. package/lib/manager.js +184 -66
  39. package/package.json +4 -1
  40. package/server/admin/access.js +12 -7
  41. package/server/admin/admin.js +436 -10
  42. package/server/admin/billing.js +158 -35
  43. package/server/admin/index.html +72 -2
  44. package/server/admin/users.js +15 -15
  45. package/server/db/database.js +125 -20
  46. package/server/http/routes.js +1 -0
  47. package/server/http/static.js +4 -2
  48. package/server/public/.last_build_id +1 -1
  49. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  50. package/server/public/canvaskit/wimp.js.symbols +8475 -8467
  51. package/server/public/canvaskit/wimp.wasm +0 -0
  52. package/server/public/flutter_bootstrap.js +2 -2
  53. package/server/public/main.dart.js +91077 -87037
  54. package/server/routes/account.js +53 -0
  55. package/server/routes/admin.js +345 -64
  56. package/server/routes/agents.js +203 -21
  57. package/server/routes/billing.js +5 -0
  58. package/server/routes/browser.js +8 -1
  59. package/server/routes/recordings.js +96 -6
  60. package/server/routes/screenHistory.js +140 -2
  61. package/server/routes/timeline.js +43 -0
  62. package/server/services/account/erasure.js +263 -0
  63. package/server/services/ai/hooks.js +4 -1
  64. package/server/services/ai/loop/agent_engine_core.js +8 -1
  65. package/server/services/ai/loop/blank_recovery.js +36 -0
  66. package/server/services/ai/loop/conversation_loop.js +166 -33
  67. package/server/services/ai/messagingFallback.js +22 -0
  68. package/server/services/ai/rate_limits.js +28 -5
  69. package/server/services/ai/systemPrompt.js +6 -5
  70. package/server/services/ai/taskAnalysis.js +2 -0
  71. package/server/services/ai/toolEvidence.js +15 -0
  72. package/server/services/ai/toolResult.js +40 -0
  73. package/server/services/ai/tools.js +163 -4
  74. package/server/services/android/controller.js +6 -2
  75. package/server/services/billing/plans.js +2 -1
  76. package/server/services/browser/anti_detection.js +192 -0
  77. package/server/services/browser/controller.js +180 -54
  78. package/server/services/desktop/auth.js +3 -0
  79. package/server/services/desktop/registry.js +50 -2
  80. package/server/services/integrations/google/calendar.js +22 -14
  81. package/server/services/manager.js +12 -42
  82. package/server/services/memory/ingestion_chunking.js +268 -0
  83. package/server/services/messaging/telnyx.js +9 -8
  84. package/server/services/recordings/manager.js +60 -27
  85. package/server/services/runtime/docker-vm-manager.js +157 -266
  86. package/server/services/runtime/guest_bootstrap.js +17 -5
  87. package/server/services/runtime/guest_image.js +188 -0
  88. package/server/services/runtime/manager.js +0 -1
  89. package/server/services/runtime/validation.js +3 -8
  90. package/server/services/social_video/service.js +60 -10
  91. package/server/services/tasks/runtime.js +234 -9
  92. package/server/services/tasks/task_repository.js +13 -0
  93. package/server/services/timeline/service.js +558 -0
  94. package/server/services/wearable/gateway.js +1 -1
  95. package/server/services/websocket.js +21 -3
  96. package/server/services/desktop/screenRecorder.js +0 -292
  97. package/server/services/desktop/screen_recorder_support.js +0 -46
@@ -350,18 +350,22 @@ class _DevicesPanelState extends State<DevicesPanel> {
350
350
 
351
351
  void _handleHover(Offset point) {
352
352
  if (_isBrowser) {
353
- unawaited(widget.controller.hoverBrowserPointRuntime(
354
- x: point.dx.round(),
355
- y: point.dy.round(),
356
- ));
353
+ unawaited(
354
+ widget.controller.hoverBrowserPointRuntime(
355
+ x: point.dx.round(),
356
+ y: point.dy.round(),
357
+ ),
358
+ );
357
359
  } else if (_isDesktop) {
358
360
  if (_desktopRequiresSelection) {
359
361
  return;
360
362
  }
361
- unawaited(widget.controller.hoverDesktopRuntime(
362
- x: point.dx.round(),
363
- y: point.dy.round(),
364
- ));
363
+ unawaited(
364
+ widget.controller.hoverDesktopRuntime(
365
+ x: point.dx.round(),
366
+ y: point.dy.round(),
367
+ ),
368
+ );
365
369
  }
366
370
  }
367
371
 
@@ -551,34 +555,95 @@ class _DevicesPanelState extends State<DevicesPanel> {
551
555
  onSelect: _selectSurface,
552
556
  ),
553
557
  ] else ...<Widget>[
554
- if (_isBrowser && prefersExtension) ...<Widget>[
555
- const SizedBox(height: 14),
556
- if (_browserExtensionDevices.isNotEmpty) ...<Widget>[
558
+ if (_isBrowser && prefersExtension) ...<Widget>[
559
+ const SizedBox(height: 14),
560
+ if (_browserExtensionDevices.isNotEmpty) ...<Widget>[
561
+ DropdownButtonFormField<String>(
562
+ isExpanded: true,
563
+ initialValue: selectedBrowserExtension?['tokenId']
564
+ ?.toString(),
565
+ decoration: const InputDecoration(
566
+ labelText: 'Chrome extension device',
567
+ prefixIcon: Icon(Icons.extension_outlined),
568
+ ),
569
+ hint: const Text('Select a paired extension'),
570
+ items: _browserExtensionDevices.map((device) {
571
+ final tokenId =
572
+ device['tokenId']?.toString() ?? '';
573
+ final label =
574
+ device['name']
575
+ ?.toString()
576
+ .trim()
577
+ .isNotEmpty ==
578
+ true
579
+ ? device['name'].toString()
580
+ : tokenId;
581
+ final state =
582
+ device['online'] == true ||
583
+ device['connected'] == true
584
+ ? 'online'
585
+ : 'offline';
586
+ return DropdownMenuItem<String>(
587
+ value: tokenId,
588
+ child: Text(
589
+ '$label · $state',
590
+ maxLines: 1,
591
+ overflow: TextOverflow.ellipsis,
592
+ softWrap: false,
593
+ ),
594
+ );
595
+ }).toList(),
596
+ onChanged: _isCurrentSurfaceBusy
597
+ ? null
598
+ : (value) {
599
+ if (value == null || value.isEmpty) {
600
+ return;
601
+ }
602
+ unawaited(
603
+ controller.selectBrowserExtensionRuntime(
604
+ value,
605
+ ),
606
+ );
607
+ },
608
+ ),
609
+ const SizedBox(height: 10),
610
+ ],
611
+ _ExtensionStatusBar(
612
+ connected: extensionConnected,
613
+ onDownload: controller.downloadBrowserExtension,
614
+ onRefresh: controller.refreshBrowserExtensionStatus,
615
+ ),
616
+ ],
617
+ if (_isDesktop) ...<Widget>[
618
+ const SizedBox(height: 14),
557
619
  DropdownButtonFormField<String>(
558
620
  isExpanded: true,
559
- initialValue: selectedBrowserExtension?['tokenId']
621
+ initialValue: selectedDesktopDevice?['deviceId']
560
622
  ?.toString(),
561
623
  decoration: const InputDecoration(
562
- labelText: 'Chrome extension device',
563
- prefixIcon: Icon(Icons.extension_outlined),
624
+ labelText: 'Desktop device',
625
+ prefixIcon: Icon(Icons.computer_outlined),
564
626
  ),
565
- hint: const Text('Select a paired extension'),
566
- items: _browserExtensionDevices.map((device) {
567
- final tokenId = device['tokenId']?.toString() ?? '';
627
+ hint: const Text('Select a companion desktop'),
628
+ items: controller.desktopDevices.map((device) {
629
+ final deviceId =
630
+ device['deviceId']?.toString() ?? '';
568
631
  final label =
569
- device['name']?.toString().trim().isNotEmpty ==
632
+ device['label']?.toString().trim().isNotEmpty ==
570
633
  true
571
- ? device['name'].toString()
572
- : tokenId;
573
- final state =
574
- device['online'] == true ||
575
- device['connected'] == true
576
- ? 'online'
634
+ ? device['label'].toString()
635
+ : (device['hostname']?.toString() ?? deviceId);
636
+ final os =
637
+ device['platform']?.toString() ?? 'desktop';
638
+ final state = device['online'] == true
639
+ ? (device['paused'] == true
640
+ ? 'paused'
641
+ : 'online')
577
642
  : 'offline';
578
643
  return DropdownMenuItem<String>(
579
- value: tokenId,
644
+ value: deviceId,
580
645
  child: Text(
581
- '$label · $state',
646
+ '$label · $os · $state',
582
647
  maxLines: 1,
583
648
  overflow: TextOverflow.ellipsis,
584
649
  softWrap: false,
@@ -592,193 +657,216 @@ class _DevicesPanelState extends State<DevicesPanel> {
592
657
  return;
593
658
  }
594
659
  unawaited(
595
- controller.selectBrowserExtensionRuntime(
660
+ controller.selectDesktopDeviceRuntime(
596
661
  value,
597
662
  ),
598
663
  );
599
664
  },
600
665
  ),
601
666
  const SizedBox(height: 10),
602
- ],
603
- _ExtensionStatusBar(
604
- connected: extensionConnected,
605
- onDownload: controller.downloadBrowserExtension,
606
- onRefresh: controller.refreshBrowserExtensionStatus,
607
- ),
608
- ],
609
- if (_isDesktop) ...<Widget>[
610
- const SizedBox(height: 14),
611
- DropdownButtonFormField<String>(
612
- isExpanded: true,
613
- initialValue: selectedDesktopDevice?['deviceId']
614
- ?.toString(),
615
- decoration: const InputDecoration(
616
- labelText: 'Desktop device',
617
- prefixIcon: Icon(Icons.computer_outlined),
667
+ Wrap(
668
+ spacing: 10,
669
+ runSpacing: 10,
670
+ children: <Widget>[
671
+ _DotStatus(
672
+ label: desktopDeviceOnline
673
+ ? 'Companion live'
674
+ : controller.desktopCompanionConnected
675
+ ? 'Companion live'
676
+ : controller.desktopCompanionConnecting
677
+ ? 'Connecting'
678
+ : 'Companion idle',
679
+ color: desktopDeviceOnline
680
+ ? _success
681
+ : controller.desktopCompanionConnected
682
+ ? _success
683
+ : controller.desktopCompanionConnecting
684
+ ? _accent
685
+ : _warning,
686
+ ),
687
+ if (selectedDesktopDevice != null)
688
+ _DotStatus(
689
+ label: selectedDesktopDevice['paused'] == true
690
+ ? 'Paused'
691
+ : (selectedDesktopDevice['online'] == true
692
+ ? 'Ready'
693
+ : 'Offline'),
694
+ color: selectedDesktopDevice['paused'] == true
695
+ ? _warning
696
+ : (selectedDesktopDevice['online'] == true
697
+ ? _success
698
+ : _textMuted),
699
+ ),
700
+ if (selectedDesktopDevice != null)
701
+ _DotStatus(
702
+ label:
703
+ selectedDesktopDevice['passiveHistoryEnabled'] ==
704
+ true
705
+ ? 'History on'
706
+ : 'History off',
707
+ color:
708
+ selectedDesktopDevice['passiveHistoryEnabled'] ==
709
+ true
710
+ ? _accent
711
+ : _textMuted,
712
+ ),
713
+ ],
618
714
  ),
619
- hint: const Text('Select a companion desktop'),
620
- items: controller.desktopDevices.map((device) {
621
- final deviceId = device['deviceId']?.toString() ?? '';
622
- final label =
623
- device['label']?.toString().trim().isNotEmpty ==
624
- true
625
- ? device['label'].toString()
626
- : (device['hostname']?.toString() ?? deviceId);
627
- final os =
628
- device['platform']?.toString() ?? 'desktop';
629
- final state = device['online'] == true
630
- ? (device['paused'] == true ? 'paused' : 'online')
631
- : 'offline';
632
- return DropdownMenuItem<String>(
633
- value: deviceId,
634
- child: Text(
635
- '$label · $os · $state',
636
- maxLines: 1,
637
- overflow: TextOverflow.ellipsis,
638
- softWrap: false,
715
+ if (selectedDesktopDevice != null &&
716
+ (selectedDesktopDevice['passiveHistoryLastUploadedAt']
717
+ ?.toString()
718
+ .trim()
719
+ .isNotEmpty ==
720
+ true ||
721
+ selectedDesktopDevice['passiveHistoryLastError']
722
+ ?.toString()
723
+ .trim()
724
+ .isNotEmpty ==
725
+ true)) ...<Widget>[
726
+ const SizedBox(height: 10),
727
+ Container(
728
+ width: double.infinity,
729
+ padding: const EdgeInsets.all(12),
730
+ decoration: BoxDecoration(
731
+ color: _bgSecondary.withValues(alpha: 0.7),
732
+ borderRadius: BorderRadius.circular(14),
733
+ border: Border.all(color: _borderLight),
639
734
  ),
640
- );
641
- }).toList(),
642
- onChanged: _isCurrentSurfaceBusy
643
- ? null
644
- : (value) {
645
- if (value == null || value.isEmpty) {
646
- return;
647
- }
648
- unawaited(
649
- controller.selectDesktopDeviceRuntime(value),
650
- );
651
- },
652
- ),
653
- const SizedBox(height: 10),
654
- Wrap(
655
- spacing: 10,
656
- runSpacing: 10,
657
- children: <Widget>[
658
- _DotStatus(
659
- label: desktopDeviceOnline
660
- ? 'Companion live'
661
- : controller.desktopCompanionConnected
662
- ? 'Companion live'
663
- : controller.desktopCompanionConnecting
664
- ? 'Connecting'
665
- : 'Companion idle',
666
- color: desktopDeviceOnline
667
- ? _success
668
- : controller.desktopCompanionConnected
669
- ? _success
670
- : controller.desktopCompanionConnecting
671
- ? _accent
672
- : _warning,
673
- ),
674
- if (selectedDesktopDevice != null)
675
- _DotStatus(
676
- label: selectedDesktopDevice['paused'] == true
677
- ? 'Paused'
678
- : (selectedDesktopDevice['online'] == true
679
- ? 'Ready'
680
- : 'Offline'),
681
- color: selectedDesktopDevice['paused'] == true
682
- ? _warning
683
- : (selectedDesktopDevice['online'] == true
684
- ? _success
685
- : _textMuted),
735
+ child: Column(
736
+ crossAxisAlignment: CrossAxisAlignment.start,
737
+ children: <Widget>[
738
+ if (selectedDesktopDevice['passiveHistoryLastUploadedAt']
739
+ ?.toString()
740
+ .trim()
741
+ .isNotEmpty ==
742
+ true)
743
+ Text(
744
+ 'Last history upload: ${selectedDesktopDevice['passiveHistoryLastUploadedAt']}',
745
+ style: TextStyle(color: _textSecondary),
746
+ ),
747
+ if (selectedDesktopDevice['passiveHistoryLastError']
748
+ ?.toString()
749
+ .trim()
750
+ .isNotEmpty ==
751
+ true)
752
+ Padding(
753
+ padding: const EdgeInsets.only(top: 6),
754
+ child: Text(
755
+ 'Last history error: ${selectedDesktopDevice['passiveHistoryLastError']}',
756
+ style: TextStyle(color: _danger),
757
+ ),
758
+ ),
759
+ ],
686
760
  ),
761
+ ),
687
762
  ],
763
+ ],
764
+ const SizedBox(height: 16),
765
+ _DeviceLaunchBar(
766
+ surface: _surface,
767
+ controller: _isBrowser
768
+ ? _browserUrlController
769
+ : (_isDesktop
770
+ ? _desktopLaunchController
771
+ : _androidLaunchController),
772
+ active: _isBrowser
773
+ ? browserStatus['launched'] == true
774
+ : (_isDesktop
775
+ ? _desktopOnline
776
+ : _androidOnline || _androidStarting),
777
+ starting:
778
+ !_isBrowser && !_isDesktop && _androidStarting,
779
+ busy: _isCurrentSurfaceBusy,
780
+ onSubmit: _openPrimary,
781
+ onSleep: _sleepPrimary,
688
782
  ),
689
- ],
690
- const SizedBox(height: 16),
691
- _DeviceLaunchBar(
692
- surface: _surface,
693
- controller: _isBrowser
694
- ? _browserUrlController
695
- : (_isDesktop
696
- ? _desktopLaunchController
697
- : _androidLaunchController),
698
- active: _isBrowser
699
- ? browserStatus['launched'] == true
700
- : (_isDesktop
701
- ? _desktopOnline
702
- : _androidOnline || _androidStarting),
703
- starting: !_isBrowser && !_isDesktop && _androidStarting,
704
- busy: _isCurrentSurfaceBusy,
705
- onSubmit: _openPrimary,
706
- onSleep: _sleepPrimary,
707
- ),
708
- const SizedBox(height: 18),
709
- _InteractiveSurfacePreview(
710
- surface: _surface,
711
- controller: controller,
712
- screenshotPath: _activeScreenshotPath,
713
- streamPlatform: _isBrowser && browserStatus['launched'] == true
714
- ? 'browser'
715
- : (_isDesktop && _desktopOnline
716
- ? 'desktop'
717
- : (!_isBrowser && !_isDesktop && _androidOnline
718
- ? 'android'
719
- : null)),
720
- streamDeviceId: _isBrowser && browserStatus['launched'] == true
721
- ? 'browser'
722
- : (_isDesktop && _desktopOnline
723
- ? (widget.controller.selectedDesktopDeviceId ??
724
- (_onlineDesktopDevices.isNotEmpty ? _onlineDesktopDevices.first['deviceId']?.toString() : null))
725
- : (!_isBrowser && !_isDesktop && _androidOnline
726
- ? _androidDeviceId
727
- : null)),
728
- busy: _isCurrentSurfaceBusy,
729
- wakingUp: !_isBrowser && !_isDesktop && _androidStarting,
730
- enabled: _isBrowser || _isDesktop || _androidOnline,
731
- connectRequired: _isBrowser
732
- ? _extensionPreferredButOffline
733
- : _desktopRequiresSelection,
734
- onTapPoint: _handleTap,
735
- onSwipe: _handleSwipe,
736
- onHover: _handleHover,
737
- onWakeRequested: _openPrimary,
738
- ),
739
- if (!_isBrowser && !_isDesktop) ...<Widget>[
740
- const SizedBox(height: 12),
741
- _AndroidNavDock(
783
+ const SizedBox(height: 18),
784
+ _InteractiveSurfacePreview(
785
+ surface: _surface,
786
+ controller: controller,
787
+ screenshotPath: _activeScreenshotPath,
788
+ streamPlatform:
789
+ _isBrowser && browserStatus['launched'] == true
790
+ ? 'browser'
791
+ : (_isDesktop && _desktopOnline
792
+ ? 'desktop'
793
+ : (!_isBrowser &&
794
+ !_isDesktop &&
795
+ _androidOnline
796
+ ? 'android'
797
+ : null)),
798
+ streamDeviceId:
799
+ _isBrowser && browserStatus['launched'] == true
800
+ ? 'browser'
801
+ : (_isDesktop && _desktopOnline
802
+ ? (widget
803
+ .controller
804
+ .selectedDesktopDeviceId ??
805
+ (_onlineDesktopDevices.isNotEmpty
806
+ ? _onlineDesktopDevices
807
+ .first['deviceId']
808
+ ?.toString()
809
+ : null))
810
+ : (!_isBrowser &&
811
+ !_isDesktop &&
812
+ _androidOnline
813
+ ? _androidDeviceId
814
+ : null)),
742
815
  busy: _isCurrentSurfaceBusy,
743
- androidOnline: _androidOnline,
744
- onAction: _runQuickAction,
816
+ wakingUp:
817
+ !_isBrowser && !_isDesktop && _androidStarting,
818
+ enabled: _isBrowser || _isDesktop || _androidOnline,
819
+ connectRequired: _isBrowser
820
+ ? _extensionPreferredButOffline
821
+ : _desktopRequiresSelection,
822
+ onTapPoint: _handleTap,
823
+ onSwipe: _handleSwipe,
824
+ onHover: _handleHover,
825
+ onWakeRequested: _openPrimary,
745
826
  ),
746
- if (kIsWeb) ...<Widget>[
827
+ if (!_isBrowser && !_isDesktop) ...<Widget>[
747
828
  const SizedBox(height: 12),
748
- _AndroidActionsBox(
749
- enabled: _androidOnline,
829
+ _AndroidNavDock(
750
830
  busy: _isCurrentSurfaceBusy,
751
- onInstall: ({required filename, required bytes}) {
752
- return controller.installAndroidApkRuntime(
753
- filename: filename,
754
- bytes: bytes,
755
- );
756
- },
831
+ androidOnline: _androidOnline,
832
+ onAction: _runQuickAction,
833
+ ),
834
+ if (kIsWeb) ...<Widget>[
835
+ const SizedBox(height: 12),
836
+ _AndroidActionsBox(
837
+ enabled: _androidOnline,
838
+ busy: _isCurrentSurfaceBusy,
839
+ onInstall: ({required filename, required bytes}) {
840
+ return controller.installAndroidApkRuntime(
841
+ filename: filename,
842
+ bytes: bytes,
843
+ );
844
+ },
845
+ ),
846
+ ],
847
+ ],
848
+ const SizedBox(height: 18),
849
+ _DeviceTypeDock(
850
+ controller: _textEntryController,
851
+ busy: _isCurrentSurfaceBusy,
852
+ surface: _surface,
853
+ onSubmit: _sendText,
854
+ ),
855
+ if (_isBrowser || _isDesktop) ...<Widget>[
856
+ const SizedBox(height: 14),
857
+ _DeviceQuickActions(
858
+ surface: _surface,
859
+ androidOnline: _androidOnline,
860
+ busy: _isCurrentSurfaceBusy,
861
+ onAction: _runQuickAction,
757
862
  ),
758
863
  ],
759
- ],
760
- const SizedBox(height: 18),
761
- _DeviceTypeDock(
762
- controller: _textEntryController,
763
- busy: _isCurrentSurfaceBusy,
764
- surface: _surface,
765
- onSubmit: _sendText,
766
- ),
767
- if (_isBrowser || _isDesktop) ...<Widget>[
768
864
  const SizedBox(height: 14),
769
- _DeviceQuickActions(
865
+ _SurfaceSwitcher(
770
866
  surface: _surface,
771
- androidOnline: _androidOnline,
772
- busy: _isCurrentSurfaceBusy,
773
- onAction: _runQuickAction,
867
+ onSelect: _selectSurface,
774
868
  ),
775
869
  ],
776
- const SizedBox(height: 14),
777
- _SurfaceSwitcher(
778
- surface: _surface,
779
- onSelect: _selectSurface,
780
- ),
781
- ],
782
870
  ],
783
871
  ),
784
872
  ),
@@ -950,7 +1038,7 @@ class _DeviceSurfaceHeader extends StatelessWidget {
950
1038
  ? 'Live'
951
1039
  : 'Offline'))
952
1040
  : surface == _DeviceSurface.files
953
- ? 'Isolated'
1041
+ ? ''
954
1042
  : (androidOnline
955
1043
  ? 'Live'
956
1044
  : androidStarting
@@ -1336,7 +1424,11 @@ class _MutedBadge extends StatelessWidget {
1336
1424
  color: Colors.black54,
1337
1425
  borderRadius: BorderRadius.circular(8),
1338
1426
  ),
1339
- child: const Icon(Icons.volume_off_rounded, size: 11, color: Colors.white),
1427
+ child: const Icon(
1428
+ Icons.volume_off_rounded,
1429
+ size: 11,
1430
+ color: Colors.white,
1431
+ ),
1340
1432
  );
1341
1433
  }
1342
1434
  }
@@ -1738,10 +1830,7 @@ class _InteractiveSurfacePreviewState
1738
1830
  final surfaceHeightCap = widget.surface == _DeviceSurface.android
1739
1831
  ? 640.0
1740
1832
  : 560.0;
1741
- final previewMaxHeight = math.min(
1742
- surfaceHeightCap,
1743
- viewportHeight * 0.48,
1744
- );
1833
+ final previewMaxHeight = math.min(surfaceHeightCap, viewportHeight * 0.48);
1745
1834
  final aspectRatio = switch (widget.surface) {
1746
1835
  _DeviceSurface.browser => 16 / 10,
1747
1836
  _DeviceSurface.android => 10 / 16,
@@ -1818,10 +1907,7 @@ class _InteractiveSurfacePreviewState
1818
1907
  const Positioned(
1819
1908
  top: 8,
1820
1909
  right: 8,
1821
- child: Opacity(
1822
- opacity: 0.45,
1823
- child: _MutedBadge(),
1824
- ),
1910
+ child: Opacity(opacity: 0.45, child: _MutedBadge()),
1825
1911
  ),
1826
1912
  Positioned(
1827
1913
  left: 12,
@@ -2389,10 +2475,8 @@ class _WorkspaceExplorerState extends State<_WorkspaceExplorer> {
2389
2475
  : ListView.separated(
2390
2476
  shrinkWrap: true,
2391
2477
  itemCount: entries.length,
2392
- separatorBuilder: (_, __) => Divider(
2393
- height: 1,
2394
- color: _borderLight,
2395
- ),
2478
+ separatorBuilder: (_, __) =>
2479
+ Divider(height: 1, color: _borderLight),
2396
2480
  itemBuilder: (context, index) {
2397
2481
  final entry = entries[index];
2398
2482
  final type = entry['type']?.toString() ?? 'file';
@@ -2495,7 +2579,9 @@ class _WorkspaceExplorerState extends State<_WorkspaceExplorer> {
2495
2579
  }
2496
2580
 
2497
2581
  String _formatWorkspaceBytes(Object? value) {
2498
- final bytes = value is num ? value.toDouble() : double.tryParse('$value') ?? 0;
2582
+ final bytes = value is num
2583
+ ? value.toDouble()
2584
+ : double.tryParse('$value') ?? 0;
2499
2585
  if (bytes >= 1024 * 1024) {
2500
2586
  return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
2501
2587
  }