neoagent 3.0.1-beta.2 → 3.0.1-beta.20

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 (83) hide show
  1. package/.env.example +19 -0
  2. package/README.md +16 -0
  3. package/docs/benchmarking.md +102 -0
  4. package/docs/billing.md +224 -0
  5. package/docs/configuration.md +22 -0
  6. package/docs/index.md +1 -0
  7. package/flutter_app/lib/main.dart +4 -1
  8. package/flutter_app/lib/main_app_shell.dart +316 -0
  9. package/flutter_app/lib/main_billing.dart +1465 -0
  10. package/flutter_app/lib/main_chat.dart +1568 -231
  11. package/flutter_app/lib/main_controller.dart +263 -26
  12. package/flutter_app/lib/main_devices.dart +1 -1
  13. package/flutter_app/lib/main_install.dart +1147 -0
  14. package/flutter_app/lib/main_navigation.dart +12 -0
  15. package/flutter_app/lib/main_operations.dart +134 -9
  16. package/flutter_app/lib/main_settings.dart +148 -52
  17. package/flutter_app/lib/main_shared.dart +1 -0
  18. package/flutter_app/lib/src/backend_client.dart +69 -1
  19. package/landing/index.html +2 -2
  20. package/lib/manager.js +156 -0
  21. package/lib/schema_migrations.js +84 -0
  22. package/package.json +10 -4
  23. package/server/admin/access.js +12 -7
  24. package/server/admin/admin.css +78 -0
  25. package/server/admin/admin.js +511 -23
  26. package/server/admin/billing.js +415 -0
  27. package/server/admin/index.html +120 -13
  28. package/server/admin/users.js +15 -15
  29. package/server/db/database.js +13 -21
  30. package/server/db/sessions_db.js +8 -0
  31. package/server/http/middleware.js +4 -4
  32. package/server/http/routes.js +9 -0
  33. package/server/http/static.js +4 -2
  34. package/server/middleware/requireBilling.js +12 -0
  35. package/server/public/.last_build_id +1 -1
  36. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  37. package/server/public/flutter_bootstrap.js +1 -1
  38. package/server/public/main.dart.js +89120 -85431
  39. package/server/routes/admin.js +524 -29
  40. package/server/routes/agents.js +203 -21
  41. package/server/routes/billing.js +127 -0
  42. package/server/routes/billing_webhook.js +43 -0
  43. package/server/routes/memory.js +1 -4
  44. package/server/routes/settings.js +1 -2
  45. package/server/routes/skills.js +1 -1
  46. package/server/routes/triggers.js +2 -2
  47. package/server/services/account/sessions.js +1 -9
  48. package/server/services/ai/loop/agent_engine_core.js +42 -0
  49. package/server/services/ai/loop/blank_recovery.js +36 -0
  50. package/server/services/ai/loop/completion_judge.js +42 -0
  51. package/server/services/ai/loop/conversation_loop.js +248 -34
  52. package/server/services/ai/loop/progress_monitor.js +6 -6
  53. package/server/services/ai/loopPolicy.js +37 -44
  54. package/server/services/ai/messagingFallback.js +25 -1
  55. package/server/services/ai/models.js +18 -0
  56. package/server/services/ai/preModelCompaction.js +25 -5
  57. package/server/services/ai/rate_limits.js +28 -4
  58. package/server/services/ai/systemPrompt.js +23 -5
  59. package/server/services/ai/taskAnalysis.js +2 -0
  60. package/server/services/ai/tools.js +231 -20
  61. package/server/services/billing/billing_email.js +106 -0
  62. package/server/services/billing/config.js +17 -0
  63. package/server/services/billing/plans.js +121 -0
  64. package/server/services/billing/stripe_client.js +21 -0
  65. package/server/services/billing/subscriptions.js +462 -0
  66. package/server/services/billing/trial_guard.js +111 -0
  67. package/server/services/browser/contentExtractor.js +629 -0
  68. package/server/services/browser/controller.js +4 -8
  69. package/server/services/desktop/gateway.js +7 -4
  70. package/server/services/integrations/google/calendar.js +30 -2
  71. package/server/services/integrations/secrets.js +7 -4
  72. package/server/services/manager.js +11 -0
  73. package/server/services/messaging/automation.js +1 -1
  74. package/server/services/messaging/telnyx.js +12 -11
  75. package/server/services/messaging/whatsapp.js +1 -1
  76. package/server/services/recordings/manager.js +13 -7
  77. package/server/services/runtime/backends/local-vm.js +40 -22
  78. package/server/services/tasks/runtime.js +103 -15
  79. package/server/services/tasks/schedule_utils.js +5 -5
  80. package/server/services/voice/runtimeManager.js +19 -10
  81. package/server/services/wearable/gateway.js +8 -5
  82. package/server/services/websocket.js +26 -8
  83. package/server/services/workspace/manager.js +37 -3
@@ -47,6 +47,7 @@ class NeoAgentController extends ChangeNotifier {
47
47
  StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
48
48
  bool _connectivityPluginAvailable = true;
49
49
  static const int _maxVisibleLogs = 400;
50
+ static const int _maxToolEvents = 500; // separate list from _maxVisibleLogs (chat diagnostics)
50
51
 
51
52
  static const String _configuredBackendUrl = String.fromEnvironment(
52
53
  'NEOAGENT_BACKEND_URL',
@@ -66,6 +67,7 @@ class NeoAgentController extends ChangeNotifier {
66
67
  final Map<String, DateTime> _manualRunCooldowns = <String, DateTime>{};
67
68
  static const Duration _manualRunCooldownDuration = Duration(seconds: 10);
68
69
  static const Duration _homeWidgetSyncCooldown = Duration(seconds: 5);
70
+ static const int _chatHistoryPageSize = 20;
69
71
  DateTime? _lastHomeWidgetSyncAt;
70
72
  int _authCycle = 0;
71
73
  bool _isPollingQrLogin = false;
@@ -98,6 +100,8 @@ class NeoAgentController extends ChangeNotifier {
98
100
  bool isApprovingQrLogin = false;
99
101
  bool isCheckingAppUpdate = false;
100
102
  bool isOpeningAppUpdate = false;
103
+ bool isLoadingBilling = false;
104
+ bool showBillingSection = false;
101
105
  bool socketConnected = false;
102
106
  bool hasNetworkConnection = true;
103
107
  bool networkStatusKnown = false;
@@ -142,12 +146,17 @@ class NeoAgentController extends ChangeNotifier {
142
146
  List<RecordingSessionItem> recordingSessions = const <RecordingSessionItem>[];
143
147
 
144
148
  List<ChatEntry> chatMessages = const <ChatEntry>[];
149
+ bool chatHistoryHasMore = false;
150
+ bool isLoadingOlderChatHistory = false;
145
151
  List<AgentProfile> agentProfiles = const <AgentProfile>[];
146
152
  String? selectedAgentId;
147
153
  List<ModelMeta> supportedModels = const <ModelMeta>[];
148
154
  List<AiProviderMeta> aiProviders = const <AiProviderMeta>[];
149
155
  List<RunSummary> recentRuns = const <RunSummary>[];
150
156
  TokenUsageSnapshot? tokenUsage;
157
+ Map<String, dynamic>? billingSubscription;
158
+ List<Map<String, dynamic>> billingPlans = const <Map<String, dynamic>>[];
159
+ List<Map<String, dynamic>> billingInvoices = const <Map<String, dynamic>>[];
151
160
  UpdateStatusSnapshot updateStatus = const UpdateStatusSnapshot();
152
161
  List<LogEntry> logs = const <LogEntry>[];
153
162
  Map<String, MessagingPlatformStatus> messagingStatuses =
@@ -158,6 +167,7 @@ class NeoAgentController extends ChangeNotifier {
158
167
  MessagingQrState? pendingMessagingQr;
159
168
  ToolApprovalRequest? pendingApproval;
160
169
  final List<BlockedSenderNotice> _blockedSenderQueue = <BlockedSenderNotice>[];
170
+ final Set<String> _ignoredChats = <String>{};
161
171
  List<SkillItem> skills = const <SkillItem>[];
162
172
  List<StoreSkillItem> storeSkills = const <StoreSkillItem>[];
163
173
  List<OfficialIntegrationItem> officialIntegrations =
@@ -199,6 +209,9 @@ class NeoAgentController extends ChangeNotifier {
199
209
  String? _pendingChatDraft;
200
210
  List<SharedChatAttachment> _pendingSharedChatAttachments =
201
211
  const <SharedChatAttachment>[];
212
+ String? _chatHistoryBeforeCreatedAt;
213
+ String? _chatHistoryBeforeSource;
214
+ String? _chatHistoryBeforeId;
202
215
 
203
216
  ActiveRunState? activeRun;
204
217
  List<ToolEventItem> toolEvents = const <ToolEventItem>[];
@@ -494,6 +507,103 @@ class NeoAgentController extends ChangeNotifier {
494
507
  ];
495
508
  }
496
509
 
510
+ void _resetChatHistoryPagination() {
511
+ chatHistoryHasMore = false;
512
+ isLoadingOlderChatHistory = false;
513
+ _chatHistoryBeforeCreatedAt = null;
514
+ _chatHistoryBeforeSource = null;
515
+ _chatHistoryBeforeId = null;
516
+ }
517
+
518
+ void _applyChatHistoryCursor(Map<String, dynamic> history) {
519
+ chatHistoryHasMore = history['hasMore'] == true;
520
+ _chatHistoryBeforeCreatedAt = _optionalIdFrom(
521
+ history['nextBeforeCreatedAt'],
522
+ );
523
+ _chatHistoryBeforeSource = _optionalIdFrom(history['nextBeforeSource']);
524
+ _chatHistoryBeforeId = _optionalIdFrom(history['nextBeforeId']);
525
+ }
526
+
527
+ String _chatEntryKey(ChatEntry entry) {
528
+ final stableId = entry.id.trim();
529
+ if (stableId.isNotEmpty) {
530
+ return [
531
+ stableId,
532
+ entry.platform,
533
+ entry.role,
534
+ entry.createdAt.toIso8601String(),
535
+ ].join('|');
536
+ }
537
+ return [
538
+ entry.role,
539
+ entry.platform,
540
+ entry.createdAt.toIso8601String(),
541
+ entry.content,
542
+ ].join('|');
543
+ }
544
+
545
+ List<ChatEntry> _chatHistoryEntriesFromResponse(
546
+ Map<String, dynamic> history,
547
+ ) {
548
+ return _decodeModelList(
549
+ 'chat_history',
550
+ history['messages'],
551
+ ChatEntry.fromJson,
552
+ fallbackToMapValues: true,
553
+ );
554
+ }
555
+
556
+ Future<bool> loadOlderChatHistory() async {
557
+ if (!isAuthenticated ||
558
+ isLoadingOlderChatHistory ||
559
+ !chatHistoryHasMore ||
560
+ _chatHistoryBeforeCreatedAt == null ||
561
+ _chatHistoryBeforeSource == null ||
562
+ _chatHistoryBeforeId == null) {
563
+ return false;
564
+ }
565
+
566
+ final agentId = _scopedAgentId;
567
+ final beforeCreatedAt = _chatHistoryBeforeCreatedAt;
568
+ final beforeSource = _chatHistoryBeforeSource;
569
+ final beforeId = _chatHistoryBeforeId;
570
+ isLoadingOlderChatHistory = true;
571
+ notifyListeners();
572
+ try {
573
+ final history = await _backendClient.fetchChatHistory(
574
+ backendUrl,
575
+ agentId: agentId,
576
+ limit: _chatHistoryPageSize,
577
+ beforeCreatedAt: beforeCreatedAt,
578
+ beforeSource: beforeSource,
579
+ beforeId: beforeId,
580
+ );
581
+ if (agentId != _scopedAgentId) {
582
+ return false;
583
+ }
584
+ final olderMessages = _chatHistoryEntriesFromResponse(history);
585
+ _applyChatHistoryCursor(history);
586
+ if (olderMessages.isEmpty) {
587
+ return false;
588
+ }
589
+ final existingKeys = chatMessages.map(_chatEntryKey).toSet();
590
+ final prepended = olderMessages
591
+ .where((entry) => existingKeys.add(_chatEntryKey(entry)))
592
+ .toList(growable: false);
593
+ if (prepended.isEmpty) {
594
+ return false;
595
+ }
596
+ chatMessages = <ChatEntry>[...prepended, ...chatMessages];
597
+ return true;
598
+ } catch (error) {
599
+ errorMessage = _friendlyErrorMessage(error);
600
+ return false;
601
+ } finally {
602
+ isLoadingOlderChatHistory = false;
603
+ notifyListeners();
604
+ }
605
+ }
606
+
497
607
  String _settingString(String key, String fallback, {bool lowercase = false}) {
498
608
  final value = settings[key]?.toString().trim() ?? '';
499
609
  if (value.isEmpty) {
@@ -631,6 +741,8 @@ class NeoAgentController extends ChangeNotifier {
631
741
  BlockedSenderNotice? get pendingBlockedSenderNotice =>
632
742
  _blockedSenderQueue.isEmpty ? null : _blockedSenderQueue.first;
633
743
 
744
+ List<String> get ignoredChats => _ignoredChats.toList();
745
+
634
746
  void _handleRecordingBridgeChanged() {
635
747
  _logRecording('bridge.changed');
636
748
  notifyListeners();
@@ -716,6 +828,7 @@ class NeoAgentController extends ChangeNotifier {
716
828
 
717
829
  Future<void> bootstrap() async {
718
830
  _prefs = await SharedPreferences.getInstance();
831
+ _ignoredChats.addAll(_prefs?.getStringList('messaging.ignored_chats') ?? <String>[]);
719
832
  await _desktopCompanion.bootstrap(_prefs!);
720
833
  final configured = _configuredBackendUrl.trim();
721
834
  final savedBackendUrl = _prefs?.getString('backend_url')?.trim() ?? '';
@@ -1552,6 +1665,7 @@ class NeoAgentController extends ChangeNotifier {
1552
1665
  linkedAuthProviders = const <LinkedAuthProviderItem>[];
1553
1666
  settings = const <String, dynamic>{};
1554
1667
  chatMessages = const <ChatEntry>[];
1668
+ _resetChatHistoryPagination();
1555
1669
  agentProfiles = const <AgentProfile>[];
1556
1670
  selectedAgentId = null;
1557
1671
  supportedModels = const <ModelMeta>[];
@@ -1683,6 +1797,9 @@ class NeoAgentController extends ChangeNotifier {
1683
1797
  if (section == AppSection.accountSettings) {
1684
1798
  unawaited(refreshAccountSettings());
1685
1799
  }
1800
+ if (section == AppSection.billing) {
1801
+ unawaited(refreshBilling());
1802
+ }
1686
1803
  notifyListeners();
1687
1804
  }
1688
1805
 
@@ -1711,6 +1828,7 @@ class NeoAgentController extends ChangeNotifier {
1711
1828
  }
1712
1829
  selectedAgentId = id;
1713
1830
  chatMessages = const <ChatEntry>[];
1831
+ _resetChatHistoryPagination();
1714
1832
  recentRuns = const <RunSummary>[];
1715
1833
  messagingStatuses = const <String, MessagingPlatformStatus>{};
1716
1834
  messagingMessages = const <MessagingMessage>[];
@@ -1914,14 +2032,20 @@ class NeoAgentController extends ChangeNotifier {
1914
2032
 
1915
2033
  Future<void> allowMessagingSuggestion(
1916
2034
  String platform,
1917
- QuickAllowSuggestion suggestion,
1918
- ) async {
2035
+ QuickAllowSuggestion suggestion, {
2036
+ String? chatId,
2037
+ }) async {
1919
2038
  try {
1920
2039
  final nextPolicy = _policyWithAddedRule(
1921
2040
  currentMessagingAccessPolicy(platform),
1922
2041
  suggestion,
1923
2042
  );
1924
2043
  await saveMessagingAccessPolicy(platform, nextPolicy);
2044
+ if (chatId != null) {
2045
+ _blockedSenderQueue.removeWhere(
2046
+ (notice) => notice.platform == platform && notice.chatId == chatId,
2047
+ );
2048
+ }
1925
2049
  errorMessage = null;
1926
2050
  notifyListeners();
1927
2051
  } catch (error) {
@@ -1930,6 +2054,22 @@ class NeoAgentController extends ChangeNotifier {
1930
2054
  }
1931
2055
  }
1932
2056
 
2057
+ Future<void> ignoreBlockedSender(BlockedSenderNotice notice) async {
2058
+ final key = '${notice.platform}:${notice.chatId ?? notice.sender ?? ''}';
2059
+ _ignoredChats.add(key);
2060
+ _blockedSenderQueue.removeWhere(
2061
+ (n) => n.platform == notice.platform && (n.chatId == notice.chatId || n.sender == notice.sender),
2062
+ );
2063
+ await _prefs?.setStringList('messaging.ignored_chats', _ignoredChats.toList());
2064
+ notifyListeners();
2065
+ }
2066
+
2067
+ Future<void> removeIgnoredChat(String key) async {
2068
+ _ignoredChats.remove(key);
2069
+ await _prefs?.setStringList('messaging.ignored_chats', _ignoredChats.toList());
2070
+ notifyListeners();
2071
+ }
2072
+
1933
2073
  void consumeBlockedSenderNotice(String id) {
1934
2074
  if (_blockedSenderQueue.isNotEmpty && _blockedSenderQueue.first.id == id) {
1935
2075
  _blockedSenderQueue.removeAt(0);
@@ -1940,6 +2080,8 @@ class NeoAgentController extends ChangeNotifier {
1940
2080
  }
1941
2081
 
1942
2082
  void _enqueueBlockedSenderNotice(BlockedSenderNotice notice) {
2083
+ final ignoreKey = '${notice.platform}:${notice.chatId ?? notice.sender ?? ''}';
2084
+ if (_ignoredChats.contains(ignoreKey)) return;
1943
2085
  final exists = _blockedSenderQueue.any((item) => item.id == notice.id);
1944
2086
  if (!exists) {
1945
2087
  _blockedSenderQueue.add(notice);
@@ -2009,8 +2151,12 @@ class NeoAgentController extends ChangeNotifier {
2009
2151
 
2010
2152
  final historyFuture = _softRefreshLoad<Map<String, dynamic>>(
2011
2153
  'chat_history',
2012
- _backendClient.fetchChatHistory(backendUrl, agentId: agentId),
2013
- const <String, dynamic>{'messages': <dynamic>[]},
2154
+ _backendClient.fetchChatHistory(
2155
+ backendUrl,
2156
+ agentId: agentId,
2157
+ limit: _chatHistoryPageSize,
2158
+ ),
2159
+ const <String, dynamic>{'messages': <dynamic>[], 'hasMore': false},
2014
2160
  );
2015
2161
  final modelsFuture = _softRefreshLoad<Map<String, dynamic>>(
2016
2162
  'supported_models',
@@ -2113,6 +2259,7 @@ class NeoAgentController extends ChangeNotifier {
2113
2259
  final recordingsFuture = _backendClient
2114
2260
  .fetchRecordingSessions(backendUrl)
2115
2261
  .catchError((_) => const <Map<String, dynamic>>[]);
2262
+ unawaited(checkBillingEnabled());
2116
2263
  final browserFuture = _backendClient
2117
2264
  .fetchBrowserStatus(backendUrl)
2118
2265
  .catchError((_) => const <String, dynamic>{});
@@ -2177,12 +2324,8 @@ class NeoAgentController extends ChangeNotifier {
2177
2324
  return;
2178
2325
  }
2179
2326
 
2180
- chatMessages = _decodeModelList(
2181
- 'chat_history',
2182
- history['messages'],
2183
- ChatEntry.fromJson,
2184
- fallbackToMapValues: true,
2185
- );
2327
+ chatMessages = _chatHistoryEntriesFromResponse(history);
2328
+ _applyChatHistoryCursor(history);
2186
2329
 
2187
2330
  supportedModels = _decodeModelList(
2188
2331
  'supported_models',
@@ -4525,12 +4668,17 @@ class NeoAgentController extends ChangeNotifier {
4525
4668
  _appendChatMessage(content, role: 'user', platform: platform);
4526
4669
  }
4527
4670
 
4671
+ List<ToolEventItem> _capToolEvents(List<ToolEventItem> events) {
4672
+ if (events.length <= _maxToolEvents) return events;
4673
+ return events.sublist(events.length - _maxToolEvents);
4674
+ }
4675
+
4528
4676
  void _appendToolNote(String summary, {String toolName = 'note'}) {
4529
4677
  final trimmed = summary.trim();
4530
4678
  if (trimmed.isEmpty) {
4531
4679
  return;
4532
4680
  }
4533
- toolEvents = <ToolEventItem>[
4681
+ toolEvents = _capToolEvents(<ToolEventItem>[
4534
4682
  ...toolEvents,
4535
4683
  ToolEventItem(
4536
4684
  id: 'note-${DateTime.now().microsecondsSinceEpoch}',
@@ -4539,7 +4687,7 @@ class NeoAgentController extends ChangeNotifier {
4539
4687
  status: 'completed',
4540
4688
  summary: trimmed,
4541
4689
  ),
4542
- ];
4690
+ ]);
4543
4691
  }
4544
4692
 
4545
4693
  Future<void> refreshUpdateStatus() async {
@@ -4816,6 +4964,95 @@ class NeoAgentController extends ChangeNotifier {
4816
4964
  } catch (_) {}
4817
4965
  }
4818
4966
 
4967
+ // ── Billing ──────────────────────────────────────────────────────────────
4968
+
4969
+ Future<void> checkBillingEnabled() async {
4970
+ try {
4971
+ final r = await _backendClient.getBillingPlans(backendUrl);
4972
+ final enabled = r['plans'] != null;
4973
+ if (showBillingSection != enabled) {
4974
+ showBillingSection = enabled;
4975
+ notifyListeners();
4976
+ }
4977
+ } catch (_) {
4978
+ if (showBillingSection) {
4979
+ showBillingSection = false;
4980
+ notifyListeners();
4981
+ }
4982
+ }
4983
+ }
4984
+
4985
+ Future<void> refreshBilling() async {
4986
+ if (!isAuthenticated || !showBillingSection) return;
4987
+ isLoadingBilling = true;
4988
+ notifyListeners();
4989
+ try {
4990
+ final results = await Future.wait(<Future<Map<String, dynamic>>>[
4991
+ _backendClient.getBillingInfo(backendUrl),
4992
+ _backendClient.getBillingPlans(backendUrl),
4993
+ _backendClient.getBillingInvoices(backendUrl),
4994
+ ]);
4995
+ billingSubscription =
4996
+ results[0]['subscription'] as Map<String, dynamic>?;
4997
+ billingPlans = _asDynList(results[1]['plans'])
4998
+ .cast<Map<String, dynamic>>();
4999
+ billingInvoices = _asDynList(results[2]['invoices'])
5000
+ .cast<Map<String, dynamic>>();
5001
+ } catch (_) {
5002
+ // retain previous data on error
5003
+ } finally {
5004
+ isLoadingBilling = false;
5005
+ notifyListeners();
5006
+ }
5007
+ }
5008
+
5009
+ Future<String?> createCheckoutSession(String planId) async {
5010
+ try {
5011
+ final serverUrl = backendUrl;
5012
+ final result = await _backendClient.createCheckoutSession(
5013
+ baseUrl: serverUrl,
5014
+ planId: planId,
5015
+ successUrl: '$serverUrl/',
5016
+ cancelUrl: '$serverUrl/',
5017
+ );
5018
+ return result['url'] as String?;
5019
+ } catch (e) {
5020
+ errorMessage = _friendlyErrorMessage(e);
5021
+ notifyListeners();
5022
+ return null;
5023
+ }
5024
+ }
5025
+
5026
+ Future<String?> createPortalSession() async {
5027
+ try {
5028
+ final serverUrl = backendUrl;
5029
+ final result = await _backendClient.createPortalSession(
5030
+ baseUrl: serverUrl,
5031
+ returnUrl: '$serverUrl/',
5032
+ );
5033
+ return result['url'] as String?;
5034
+ } catch (e) {
5035
+ errorMessage = _friendlyErrorMessage(e);
5036
+ notifyListeners();
5037
+ return null;
5038
+ }
5039
+ }
5040
+
5041
+ Future<bool> cancelBillingSubscription() async {
5042
+ try {
5043
+ await _backendClient.cancelBillingSubscription(backendUrl);
5044
+ await refreshBilling();
5045
+ return true;
5046
+ } catch (e) {
5047
+ errorMessage = _friendlyErrorMessage(e);
5048
+ notifyListeners();
5049
+ return false;
5050
+ }
5051
+ }
5052
+
5053
+ List<dynamic> _asDynList(dynamic val) =>
5054
+ val is List ? val : const <dynamic>[];
5055
+
4819
5056
  Future<bool> updateAccountEmail({
4820
5057
  required String email,
4821
5058
  required String currentPassword,
@@ -6982,7 +7219,7 @@ class NeoAgentController extends ChangeNotifier {
6982
7219
  'verification: ${payload['verification_need']?.toString() ?? 'none'}',
6983
7220
  'freshness: ${payload['freshness_risk']?.toString() ?? 'none'}',
6984
7221
  ].join(' | ');
6985
- toolEvents = <ToolEventItem>[
7222
+ toolEvents = _capToolEvents(<ToolEventItem>[
6986
7223
  ...toolEvents,
6987
7224
  ToolEventItem(
6988
7225
  id: 'analysis-${DateTime.now().microsecondsSinceEpoch}',
@@ -6991,7 +7228,7 @@ class NeoAgentController extends ChangeNotifier {
6991
7228
  status: 'completed',
6992
7229
  summary: summary,
6993
7230
  ),
6994
- ];
7231
+ ]);
6995
7232
  if (activeRun?.runId == runId) {
6996
7233
  activeRun = activeRun!.copyWith(phase: 'Analyzing');
6997
7234
  }
@@ -7016,7 +7253,7 @@ class NeoAgentController extends ChangeNotifier {
7016
7253
  .where((item) => item.trim().isNotEmpty)
7017
7254
  .take(4)
7018
7255
  .join(' | ');
7019
- toolEvents = <ToolEventItem>[
7256
+ toolEvents = _capToolEvents(<ToolEventItem>[
7020
7257
  ...toolEvents,
7021
7258
  ToolEventItem(
7022
7259
  id: 'plan-${DateTime.now().microsecondsSinceEpoch}',
@@ -7025,7 +7262,7 @@ class NeoAgentController extends ChangeNotifier {
7025
7262
  status: 'completed',
7026
7263
  summary: steps.ifEmpty('Execution plan created.'),
7027
7264
  ),
7028
- ];
7265
+ ]);
7029
7266
  if (activeRun?.runId == runId) {
7030
7267
  activeRun = activeRun!.copyWith(phase: 'Planning');
7031
7268
  }
@@ -7065,10 +7302,10 @@ class NeoAgentController extends ChangeNotifier {
7065
7302
  status: 'running',
7066
7303
  summary: _summarizeToolArgs(payload['toolArgs']),
7067
7304
  );
7068
- toolEvents = <ToolEventItem>[
7305
+ toolEvents = _capToolEvents(<ToolEventItem>[
7069
7306
  ...toolEvents.where((event) => event.id != item.id),
7070
7307
  item,
7071
- ];
7308
+ ]);
7072
7309
  if (activeRun?.runId == runId) {
7073
7310
  activeRun = activeRun!.copyWith(phase: 'Running tool');
7074
7311
  }
@@ -7083,7 +7320,7 @@ class NeoAgentController extends ChangeNotifier {
7083
7320
  if (_backgroundRunIds.contains(runId)) {
7084
7321
  return;
7085
7322
  }
7086
- toolEvents = <ToolEventItem>[
7323
+ toolEvents = _capToolEvents(<ToolEventItem>[
7087
7324
  ...toolEvents,
7088
7325
  ToolEventItem(
7089
7326
  id: 'verification-${DateTime.now().microsecondsSinceEpoch}',
@@ -7098,7 +7335,7 @@ class NeoAgentController extends ChangeNotifier {
7098
7335
  ) ??
7099
7336
  'Verification completed.',
7100
7337
  ),
7101
- ];
7338
+ ]);
7102
7339
  if (activeRun?.runId == runId) {
7103
7340
  activeRun = activeRun!.copyWith(phase: 'Verifying');
7104
7341
  }
@@ -7138,7 +7375,7 @@ class NeoAgentController extends ChangeNotifier {
7138
7375
  'Subagent update.',
7139
7376
  ),
7140
7377
  );
7141
- toolEvents = nextEvents;
7378
+ toolEvents = _capToolEvents(nextEvents);
7142
7379
  notifyListeners();
7143
7380
  });
7144
7381
  socket.on('run:tool_end', (dynamic data) {
@@ -7171,7 +7408,7 @@ class NeoAgentController extends ChangeNotifier {
7171
7408
  if (!replaced) {
7172
7409
  next.add(updated);
7173
7410
  }
7174
- toolEvents = next;
7411
+ toolEvents = _capToolEvents(next);
7175
7412
  final toolName = payload['toolName']?.toString() ?? '';
7176
7413
  final screenshotPath =
7177
7414
  payload['screenshotPath']?.toString() ??
@@ -7220,7 +7457,7 @@ class NeoAgentController extends ChangeNotifier {
7220
7457
  if (_backgroundRunIds.contains(runId)) {
7221
7458
  return;
7222
7459
  }
7223
- toolEvents = <ToolEventItem>[
7460
+ toolEvents = _capToolEvents(<ToolEventItem>[
7224
7461
  ...toolEvents,
7225
7462
  ToolEventItem(
7226
7463
  id: 'steer-queued-${DateTime.now().microsecondsSinceEpoch}',
@@ -7230,7 +7467,7 @@ class NeoAgentController extends ChangeNotifier {
7230
7467
  summary:
7231
7468
  'Queued as steering for the current run: ${payload['content']?.toString() ?? ''}',
7232
7469
  ),
7233
- ];
7470
+ ]);
7234
7471
  if (activeRun?.runId == runId || activeRun?.runId == 'pending') {
7235
7472
  activeRun = activeRun!.copyWith(
7236
7473
  pendingSteeringCount: _asInt(payload['pendingCount']),
@@ -7247,7 +7484,7 @@ class NeoAgentController extends ChangeNotifier {
7247
7484
  if (_backgroundRunIds.contains(runId)) {
7248
7485
  return;
7249
7486
  }
7250
- toolEvents = <ToolEventItem>[
7487
+ toolEvents = _capToolEvents(<ToolEventItem>[
7251
7488
  ...toolEvents,
7252
7489
  ToolEventItem(
7253
7490
  id: 'steer-applied-${DateTime.now().microsecondsSinceEpoch}',
@@ -7258,7 +7495,7 @@ class NeoAgentController extends ChangeNotifier {
7258
7495
  ? 'Applied the latest steering update to the current run.'
7259
7496
  : 'Applied ${_asInt(payload['count'])} queued steering updates to the current run.',
7260
7497
  ),
7261
- ];
7498
+ ]);
7262
7499
  if (activeRun?.runId == runId || activeRun?.runId == 'pending') {
7263
7500
  activeRun = activeRun!.copyWith(
7264
7501
  pendingSteeringCount: _asInt(payload['pendingCount']),
@@ -950,7 +950,7 @@ class _DeviceSurfaceHeader extends StatelessWidget {
950
950
  ? 'Live'
951
951
  : 'Offline'))
952
952
  : surface == _DeviceSurface.files
953
- ? 'Isolated'
953
+ ? ''
954
954
  : (androidOnline
955
955
  ? 'Live'
956
956
  : androidStarting