neoagent 3.0.1-beta.8 → 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 (100) 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.css +78 -0
  42. package/server/admin/admin.js +436 -10
  43. package/server/admin/billing.js +165 -42
  44. package/server/admin/index.html +80 -5
  45. package/server/admin/users.js +15 -15
  46. package/server/db/database.js +125 -20
  47. package/server/http/routes.js +1 -0
  48. package/server/http/static.js +4 -2
  49. package/server/public/.last_build_id +1 -1
  50. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  51. package/server/public/canvaskit/wimp.js.symbols +8475 -8467
  52. package/server/public/canvaskit/wimp.wasm +0 -0
  53. package/server/public/flutter_bootstrap.js +2 -2
  54. package/server/public/main.dart.js +91077 -87037
  55. package/server/routes/account.js +53 -0
  56. package/server/routes/admin.js +345 -64
  57. package/server/routes/agents.js +203 -21
  58. package/server/routes/billing.js +5 -0
  59. package/server/routes/browser.js +8 -1
  60. package/server/routes/recordings.js +96 -6
  61. package/server/routes/screenHistory.js +140 -2
  62. package/server/routes/timeline.js +43 -0
  63. package/server/services/account/erasure.js +263 -0
  64. package/server/services/ai/hooks.js +4 -1
  65. package/server/services/ai/loop/agent_engine_core.js +8 -1
  66. package/server/services/ai/loop/blank_recovery.js +36 -0
  67. package/server/services/ai/loop/conversation_loop.js +166 -33
  68. package/server/services/ai/messagingFallback.js +22 -0
  69. package/server/services/ai/rate_limits.js +28 -5
  70. package/server/services/ai/systemPrompt.js +6 -5
  71. package/server/services/ai/taskAnalysis.js +2 -0
  72. package/server/services/ai/toolEvidence.js +15 -0
  73. package/server/services/ai/toolResult.js +40 -0
  74. package/server/services/ai/tools.js +163 -4
  75. package/server/services/android/controller.js +6 -2
  76. package/server/services/billing/plans.js +2 -1
  77. package/server/services/browser/anti_detection.js +192 -0
  78. package/server/services/browser/controller.js +180 -54
  79. package/server/services/desktop/auth.js +3 -0
  80. package/server/services/desktop/registry.js +50 -2
  81. package/server/services/integrations/google/calendar.js +22 -14
  82. package/server/services/manager.js +12 -42
  83. package/server/services/memory/ingestion_chunking.js +268 -0
  84. package/server/services/messaging/telnyx.js +9 -8
  85. package/server/services/recordings/manager.js +60 -27
  86. package/server/services/runtime/backends/local-vm.js +40 -22
  87. package/server/services/runtime/docker-vm-manager.js +157 -266
  88. package/server/services/runtime/guest_bootstrap.js +17 -5
  89. package/server/services/runtime/guest_image.js +188 -0
  90. package/server/services/runtime/manager.js +0 -1
  91. package/server/services/runtime/validation.js +3 -8
  92. package/server/services/social_video/service.js +60 -10
  93. package/server/services/tasks/runtime.js +234 -9
  94. package/server/services/tasks/schedule_utils.js +5 -5
  95. package/server/services/tasks/task_repository.js +13 -0
  96. package/server/services/timeline/service.js +558 -0
  97. package/server/services/wearable/gateway.js +1 -1
  98. package/server/services/websocket.js +21 -3
  99. package/server/services/desktop/screenRecorder.js +0 -292
  100. package/server/services/desktop/screen_recorder_support.js +0 -46
@@ -47,6 +47,8 @@ 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 =
51
+ 500; // separate list from _maxVisibleLogs (chat diagnostics)
50
52
 
51
53
  static const String _configuredBackendUrl = String.fromEnvironment(
52
54
  'NEOAGENT_BACKEND_URL',
@@ -66,6 +68,7 @@ class NeoAgentController extends ChangeNotifier {
66
68
  final Map<String, DateTime> _manualRunCooldowns = <String, DateTime>{};
67
69
  static const Duration _manualRunCooldownDuration = Duration(seconds: 10);
68
70
  static const Duration _homeWidgetSyncCooldown = Duration(seconds: 5);
71
+ static const int _chatHistoryPageSize = 20;
69
72
  DateTime? _lastHomeWidgetSyncAt;
70
73
  int _authCycle = 0;
71
74
  bool _isPollingQrLogin = false;
@@ -98,6 +101,8 @@ class NeoAgentController extends ChangeNotifier {
98
101
  bool isApprovingQrLogin = false;
99
102
  bool isCheckingAppUpdate = false;
100
103
  bool isOpeningAppUpdate = false;
104
+ bool isLoadingBilling = false;
105
+ bool showBillingSection = false;
101
106
  bool socketConnected = false;
102
107
  bool hasNetworkConnection = true;
103
108
  bool networkStatusKnown = false;
@@ -142,12 +147,18 @@ class NeoAgentController extends ChangeNotifier {
142
147
  List<RecordingSessionItem> recordingSessions = const <RecordingSessionItem>[];
143
148
 
144
149
  List<ChatEntry> chatMessages = const <ChatEntry>[];
150
+ bool chatHistoryHasMore = false;
151
+ bool isLoadingOlderChatHistory = false;
145
152
  List<AgentProfile> agentProfiles = const <AgentProfile>[];
146
153
  String? selectedAgentId;
147
154
  List<ModelMeta> supportedModels = const <ModelMeta>[];
148
155
  List<AiProviderMeta> aiProviders = const <AiProviderMeta>[];
149
156
  List<RunSummary> recentRuns = const <RunSummary>[];
157
+ List<TimelineEventItem> timelineItems = const <TimelineEventItem>[];
150
158
  TokenUsageSnapshot? tokenUsage;
159
+ Map<String, dynamic>? billingSubscription;
160
+ List<Map<String, dynamic>> billingPlans = const <Map<String, dynamic>>[];
161
+ List<Map<String, dynamic>> billingInvoices = const <Map<String, dynamic>>[];
151
162
  UpdateStatusSnapshot updateStatus = const UpdateStatusSnapshot();
152
163
  List<LogEntry> logs = const <LogEntry>[];
153
164
  Map<String, MessagingPlatformStatus> messagingStatuses =
@@ -158,6 +169,7 @@ class NeoAgentController extends ChangeNotifier {
158
169
  MessagingQrState? pendingMessagingQr;
159
170
  ToolApprovalRequest? pendingApproval;
160
171
  final List<BlockedSenderNotice> _blockedSenderQueue = <BlockedSenderNotice>[];
172
+ final Set<String> _ignoredChats = <String>{};
161
173
  List<SkillItem> skills = const <SkillItem>[];
162
174
  List<StoreSkillItem> storeSkills = const <StoreSkillItem>[];
163
175
  List<OfficialIntegrationItem> officialIntegrations =
@@ -199,6 +211,10 @@ class NeoAgentController extends ChangeNotifier {
199
211
  String? _pendingChatDraft;
200
212
  List<SharedChatAttachment> _pendingSharedChatAttachments =
201
213
  const <SharedChatAttachment>[];
214
+ String? _chatHistoryBeforeCreatedAt;
215
+ String? _chatHistoryBeforeSource;
216
+ String? _chatHistoryBeforeId;
217
+ String? _requestedRunFocusId;
202
218
 
203
219
  ActiveRunState? activeRun;
204
220
  List<ToolEventItem> toolEvents = const <ToolEventItem>[];
@@ -227,14 +243,23 @@ class NeoAgentController extends ChangeNotifier {
227
243
  bool _desktopKeepRunningOnClose = true;
228
244
  bool _desktopAutoShowFloatingToolbar = true;
229
245
  bool _desktopAssistantHotkeyEnabled = true;
246
+ bool isRefreshingTimeline = false;
247
+ Set<String> selectedTimelineSources = <String>{'screen', 'tasks', 'runs'};
230
248
 
231
249
  bool get desktopCompanionEnabled => _desktopCompanion.enabled;
250
+ bool get desktopPassiveHistoryEnabled =>
251
+ _desktopCompanion.passiveHistoryEnabled;
232
252
  bool get isLauncherMode => appMode == NeoAgentAppMode.launcher;
233
253
  bool get desktopCompanionConnected => _desktopCompanion.connected;
234
254
  bool get desktopCompanionConnecting => _desktopCompanion.connecting;
235
255
  bool get desktopCompanionPaused => _desktopCompanion.paused;
236
256
  String get desktopCompanionLabel => _desktopCompanion.label;
237
257
  String? get desktopCompanionErrorMessage => _desktopCompanion.errorMessage;
258
+ String? get desktopPassiveHistoryLastUploadedAt =>
259
+ _desktopCompanion.passiveHistoryLastUploadedAt;
260
+ String? get desktopPassiveHistoryLastError =>
261
+ _desktopCompanion.passiveHistoryLastError;
262
+ String? get requestedRunFocusId => _requestedRunFocusId;
238
263
  Map<String, Object?> get desktopCompanionStatus => _desktopCompanion.status;
239
264
 
240
265
  bool get hasLiveRun => isSendingMessage && activeRun != null;
@@ -494,6 +519,103 @@ class NeoAgentController extends ChangeNotifier {
494
519
  ];
495
520
  }
496
521
 
522
+ void _resetChatHistoryPagination() {
523
+ chatHistoryHasMore = false;
524
+ isLoadingOlderChatHistory = false;
525
+ _chatHistoryBeforeCreatedAt = null;
526
+ _chatHistoryBeforeSource = null;
527
+ _chatHistoryBeforeId = null;
528
+ }
529
+
530
+ void _applyChatHistoryCursor(Map<String, dynamic> history) {
531
+ chatHistoryHasMore = history['hasMore'] == true;
532
+ _chatHistoryBeforeCreatedAt = _optionalIdFrom(
533
+ history['nextBeforeCreatedAt'],
534
+ );
535
+ _chatHistoryBeforeSource = _optionalIdFrom(history['nextBeforeSource']);
536
+ _chatHistoryBeforeId = _optionalIdFrom(history['nextBeforeId']);
537
+ }
538
+
539
+ String _chatEntryKey(ChatEntry entry) {
540
+ final stableId = entry.id.trim();
541
+ if (stableId.isNotEmpty) {
542
+ return [
543
+ stableId,
544
+ entry.platform,
545
+ entry.role,
546
+ entry.createdAt.toIso8601String(),
547
+ ].join('|');
548
+ }
549
+ return [
550
+ entry.role,
551
+ entry.platform,
552
+ entry.createdAt.toIso8601String(),
553
+ entry.content,
554
+ ].join('|');
555
+ }
556
+
557
+ List<ChatEntry> _chatHistoryEntriesFromResponse(
558
+ Map<String, dynamic> history,
559
+ ) {
560
+ return _decodeModelList(
561
+ 'chat_history',
562
+ history['messages'],
563
+ ChatEntry.fromJson,
564
+ fallbackToMapValues: true,
565
+ );
566
+ }
567
+
568
+ Future<bool> loadOlderChatHistory() async {
569
+ if (!isAuthenticated ||
570
+ isLoadingOlderChatHistory ||
571
+ !chatHistoryHasMore ||
572
+ _chatHistoryBeforeCreatedAt == null ||
573
+ _chatHistoryBeforeSource == null ||
574
+ _chatHistoryBeforeId == null) {
575
+ return false;
576
+ }
577
+
578
+ final agentId = _scopedAgentId;
579
+ final beforeCreatedAt = _chatHistoryBeforeCreatedAt;
580
+ final beforeSource = _chatHistoryBeforeSource;
581
+ final beforeId = _chatHistoryBeforeId;
582
+ isLoadingOlderChatHistory = true;
583
+ notifyListeners();
584
+ try {
585
+ final history = await _backendClient.fetchChatHistory(
586
+ backendUrl,
587
+ agentId: agentId,
588
+ limit: _chatHistoryPageSize,
589
+ beforeCreatedAt: beforeCreatedAt,
590
+ beforeSource: beforeSource,
591
+ beforeId: beforeId,
592
+ );
593
+ if (agentId != _scopedAgentId) {
594
+ return false;
595
+ }
596
+ final olderMessages = _chatHistoryEntriesFromResponse(history);
597
+ _applyChatHistoryCursor(history);
598
+ if (olderMessages.isEmpty) {
599
+ return false;
600
+ }
601
+ final existingKeys = chatMessages.map(_chatEntryKey).toSet();
602
+ final prepended = olderMessages
603
+ .where((entry) => existingKeys.add(_chatEntryKey(entry)))
604
+ .toList(growable: false);
605
+ if (prepended.isEmpty) {
606
+ return false;
607
+ }
608
+ chatMessages = <ChatEntry>[...prepended, ...chatMessages];
609
+ return true;
610
+ } catch (error) {
611
+ errorMessage = _friendlyErrorMessage(error);
612
+ return false;
613
+ } finally {
614
+ isLoadingOlderChatHistory = false;
615
+ notifyListeners();
616
+ }
617
+ }
618
+
497
619
  String _settingString(String key, String fallback, {bool lowercase = false}) {
498
620
  final value = settings[key]?.toString().trim() ?? '';
499
621
  if (value.isEmpty) {
@@ -631,6 +753,8 @@ class NeoAgentController extends ChangeNotifier {
631
753
  BlockedSenderNotice? get pendingBlockedSenderNotice =>
632
754
  _blockedSenderQueue.isEmpty ? null : _blockedSenderQueue.first;
633
755
 
756
+ List<String> get ignoredChats => _ignoredChats.toList();
757
+
634
758
  void _handleRecordingBridgeChanged() {
635
759
  _logRecording('bridge.changed');
636
760
  notifyListeners();
@@ -716,6 +840,9 @@ class NeoAgentController extends ChangeNotifier {
716
840
 
717
841
  Future<void> bootstrap() async {
718
842
  _prefs = await SharedPreferences.getInstance();
843
+ _ignoredChats.addAll(
844
+ _prefs?.getStringList('messaging.ignored_chats') ?? <String>[],
845
+ );
719
846
  await _desktopCompanion.bootstrap(_prefs!);
720
847
  final configured = _configuredBackendUrl.trim();
721
848
  final savedBackendUrl = _prefs?.getString('backend_url')?.trim() ?? '';
@@ -1552,11 +1679,14 @@ class NeoAgentController extends ChangeNotifier {
1552
1679
  linkedAuthProviders = const <LinkedAuthProviderItem>[];
1553
1680
  settings = const <String, dynamic>{};
1554
1681
  chatMessages = const <ChatEntry>[];
1682
+ _resetChatHistoryPagination();
1555
1683
  agentProfiles = const <AgentProfile>[];
1556
1684
  selectedAgentId = null;
1557
1685
  supportedModels = const <ModelMeta>[];
1558
1686
  aiProviders = const <AiProviderMeta>[];
1559
1687
  recentRuns = const <RunSummary>[];
1688
+ timelineItems = const <TimelineEventItem>[];
1689
+ isRefreshingTimeline = false;
1560
1690
  tokenUsage = null;
1561
1691
  updateStatus = const UpdateStatusSnapshot();
1562
1692
  _serverLogs = const <LogEntry>[];
@@ -1680,12 +1810,34 @@ class NeoAgentController extends ChangeNotifier {
1680
1810
  if (section == AppSection.devices) {
1681
1811
  unawaited(refreshDevices());
1682
1812
  }
1813
+ if (section == AppSection.timeline) {
1814
+ unawaited(refreshTimeline());
1815
+ }
1683
1816
  if (section == AppSection.accountSettings) {
1684
1817
  unawaited(refreshAccountSettings());
1685
1818
  }
1819
+ if (section == AppSection.billing) {
1820
+ unawaited(refreshBilling());
1821
+ }
1686
1822
  notifyListeners();
1687
1823
  }
1688
1824
 
1825
+ Future<void> openRunDetails(String runId) async {
1826
+ final normalized = runId.trim();
1827
+ if (normalized.isEmpty) {
1828
+ return;
1829
+ }
1830
+ _requestedRunFocusId = normalized;
1831
+ setSelectedSection(AppSection.runs);
1832
+ await refreshRunsOnly();
1833
+ }
1834
+
1835
+ void clearRequestedRunFocus(String runId) {
1836
+ if (_requestedRunFocusId == runId) {
1837
+ _requestedRunFocusId = null;
1838
+ }
1839
+ }
1840
+
1689
1841
  void _ensureSelectedAgent() {
1690
1842
  if (agentProfiles.isEmpty) {
1691
1843
  selectedAgentId = null;
@@ -1711,6 +1863,7 @@ class NeoAgentController extends ChangeNotifier {
1711
1863
  }
1712
1864
  selectedAgentId = id;
1713
1865
  chatMessages = const <ChatEntry>[];
1866
+ _resetChatHistoryPagination();
1714
1867
  recentRuns = const <RunSummary>[];
1715
1868
  messagingStatuses = const <String, MessagingPlatformStatus>{};
1716
1869
  messagingMessages = const <MessagingMessage>[];
@@ -1914,14 +2067,20 @@ class NeoAgentController extends ChangeNotifier {
1914
2067
 
1915
2068
  Future<void> allowMessagingSuggestion(
1916
2069
  String platform,
1917
- QuickAllowSuggestion suggestion,
1918
- ) async {
2070
+ QuickAllowSuggestion suggestion, {
2071
+ String? chatId,
2072
+ }) async {
1919
2073
  try {
1920
2074
  final nextPolicy = _policyWithAddedRule(
1921
2075
  currentMessagingAccessPolicy(platform),
1922
2076
  suggestion,
1923
2077
  );
1924
2078
  await saveMessagingAccessPolicy(platform, nextPolicy);
2079
+ if (chatId != null) {
2080
+ _blockedSenderQueue.removeWhere(
2081
+ (notice) => notice.platform == platform && notice.chatId == chatId,
2082
+ );
2083
+ }
1925
2084
  errorMessage = null;
1926
2085
  notifyListeners();
1927
2086
  } catch (error) {
@@ -1930,6 +2089,30 @@ class NeoAgentController extends ChangeNotifier {
1930
2089
  }
1931
2090
  }
1932
2091
 
2092
+ Future<void> ignoreBlockedSender(BlockedSenderNotice notice) async {
2093
+ final key = '${notice.platform}:${notice.chatId ?? notice.sender ?? ''}';
2094
+ _ignoredChats.add(key);
2095
+ _blockedSenderQueue.removeWhere(
2096
+ (n) =>
2097
+ n.platform == notice.platform &&
2098
+ (n.chatId == notice.chatId || n.sender == notice.sender),
2099
+ );
2100
+ await _prefs?.setStringList(
2101
+ 'messaging.ignored_chats',
2102
+ _ignoredChats.toList(),
2103
+ );
2104
+ notifyListeners();
2105
+ }
2106
+
2107
+ Future<void> removeIgnoredChat(String key) async {
2108
+ _ignoredChats.remove(key);
2109
+ await _prefs?.setStringList(
2110
+ 'messaging.ignored_chats',
2111
+ _ignoredChats.toList(),
2112
+ );
2113
+ notifyListeners();
2114
+ }
2115
+
1933
2116
  void consumeBlockedSenderNotice(String id) {
1934
2117
  if (_blockedSenderQueue.isNotEmpty && _blockedSenderQueue.first.id == id) {
1935
2118
  _blockedSenderQueue.removeAt(0);
@@ -1940,6 +2123,9 @@ class NeoAgentController extends ChangeNotifier {
1940
2123
  }
1941
2124
 
1942
2125
  void _enqueueBlockedSenderNotice(BlockedSenderNotice notice) {
2126
+ final ignoreKey =
2127
+ '${notice.platform}:${notice.chatId ?? notice.sender ?? ''}';
2128
+ if (_ignoredChats.contains(ignoreKey)) return;
1943
2129
  final exists = _blockedSenderQueue.any((item) => item.id == notice.id);
1944
2130
  if (!exists) {
1945
2131
  _blockedSenderQueue.add(notice);
@@ -2009,8 +2195,12 @@ class NeoAgentController extends ChangeNotifier {
2009
2195
 
2010
2196
  final historyFuture = _softRefreshLoad<Map<String, dynamic>>(
2011
2197
  'chat_history',
2012
- _backendClient.fetchChatHistory(backendUrl, agentId: agentId),
2013
- const <String, dynamic>{'messages': <dynamic>[]},
2198
+ _backendClient.fetchChatHistory(
2199
+ backendUrl,
2200
+ agentId: agentId,
2201
+ limit: _chatHistoryPageSize,
2202
+ ),
2203
+ const <String, dynamic>{'messages': <dynamic>[], 'hasMore': false},
2014
2204
  );
2015
2205
  final modelsFuture = _softRefreshLoad<Map<String, dynamic>>(
2016
2206
  'supported_models',
@@ -2032,6 +2222,15 @@ class NeoAgentController extends ChangeNotifier {
2032
2222
  _backendClient.fetchRuns(backendUrl, agentId: agentId),
2033
2223
  const <String, dynamic>{'runs': <dynamic>[]},
2034
2224
  );
2225
+ final timelineFuture = _softRefreshLoad<Map<String, dynamic>>(
2226
+ 'timeline',
2227
+ _backendClient.fetchTimeline(
2228
+ backendUrl,
2229
+ sources: selectedTimelineSources,
2230
+ limit: 50,
2231
+ ),
2232
+ const <String, dynamic>{'items': <dynamic>[]},
2233
+ );
2035
2234
  final versionFuture = _softRefreshLoad<Map<String, dynamic>>(
2036
2235
  'version',
2037
2236
  _backendClient.fetchVersion(backendUrl),
@@ -2113,6 +2312,7 @@ class NeoAgentController extends ChangeNotifier {
2113
2312
  final recordingsFuture = _backendClient
2114
2313
  .fetchRecordingSessions(backendUrl)
2115
2314
  .catchError((_) => const <Map<String, dynamic>>[]);
2315
+ unawaited(checkBillingEnabled());
2116
2316
  final browserFuture = _backendClient
2117
2317
  .fetchBrowserStatus(backendUrl)
2118
2318
  .catchError((_) => const <String, dynamic>{});
@@ -2154,6 +2354,7 @@ class NeoAgentController extends ChangeNotifier {
2154
2354
  final providersResponse = await providersFuture;
2155
2355
  final settingsResponse = await settingsFuture;
2156
2356
  final runsResponse = await runsFuture;
2357
+ final timelineResponse = await timelineFuture;
2157
2358
  final versionResponse = await versionFuture;
2158
2359
  final tokenResponse = await tokenFuture;
2159
2360
  final rateLimitResponse = await rateLimitFuture;
@@ -2177,12 +2378,8 @@ class NeoAgentController extends ChangeNotifier {
2177
2378
  return;
2178
2379
  }
2179
2380
 
2180
- chatMessages = _decodeModelList(
2181
- 'chat_history',
2182
- history['messages'],
2183
- ChatEntry.fromJson,
2184
- fallbackToMapValues: true,
2185
- );
2381
+ chatMessages = _chatHistoryEntriesFromResponse(history);
2382
+ _applyChatHistoryCursor(history);
2186
2383
 
2187
2384
  supportedModels = _decodeModelList(
2188
2385
  'supported_models',
@@ -2205,6 +2402,12 @@ class NeoAgentController extends ChangeNotifier {
2205
2402
  RunSummary.fromJson,
2206
2403
  fallbackToMapValues: true,
2207
2404
  );
2405
+ timelineItems = _decodeModelList(
2406
+ 'timeline',
2407
+ timelineResponse['items'],
2408
+ TimelineEventItem.fromJson,
2409
+ fallbackToMapValues: true,
2410
+ );
2208
2411
  versionInfo = versionResponse;
2209
2412
  backendHealthStatus = healthResponse;
2210
2413
  tokenUsage = TokenUsageSnapshot.fromJson(tokenResponse);
@@ -2398,6 +2601,62 @@ class NeoAgentController extends ChangeNotifier {
2398
2601
  } catch (_) {}
2399
2602
  }
2400
2603
 
2604
+ Future<void> refreshTimeline({
2605
+ Set<String>? sources,
2606
+ bool notify = true,
2607
+ }) async {
2608
+ if (!isAuthenticated) {
2609
+ return;
2610
+ }
2611
+ if (sources != null) {
2612
+ selectedTimelineSources = sources
2613
+ .map((value) => value.trim().toLowerCase())
2614
+ .where((value) => value.isNotEmpty)
2615
+ .toSet();
2616
+ }
2617
+ isRefreshingTimeline = true;
2618
+ if (notify) {
2619
+ notifyListeners();
2620
+ }
2621
+ try {
2622
+ final response = await _backendClient.fetchTimeline(
2623
+ backendUrl,
2624
+ sources: selectedTimelineSources,
2625
+ limit: 50,
2626
+ );
2627
+ timelineItems = _decodeModelList(
2628
+ 'timeline',
2629
+ response['items'],
2630
+ TimelineEventItem.fromJson,
2631
+ fallbackToMapValues: true,
2632
+ );
2633
+ } catch (error) {
2634
+ errorMessage = _friendlyErrorMessage(error);
2635
+ } finally {
2636
+ isRefreshingTimeline = false;
2637
+ if (notify) {
2638
+ notifyListeners();
2639
+ }
2640
+ }
2641
+ }
2642
+
2643
+ Future<void> toggleTimelineSource(String sourceKind) async {
2644
+ final normalized = sourceKind.trim().toLowerCase();
2645
+ if (normalized.isEmpty) {
2646
+ return;
2647
+ }
2648
+ final next = <String>{...selectedTimelineSources};
2649
+ if (next.contains(normalized)) {
2650
+ if (next.length == 1) {
2651
+ return;
2652
+ }
2653
+ next.remove(normalized);
2654
+ } else {
2655
+ next.add(normalized);
2656
+ }
2657
+ await refreshTimeline(sources: next);
2658
+ }
2659
+
2401
2660
  Future<void> refreshMessaging() async {
2402
2661
  try {
2403
2662
  final statuses = await _backendClient.fetchMessagingStatus(
@@ -3732,6 +3991,19 @@ class NeoAgentController extends ChangeNotifier {
3732
3991
  }
3733
3992
  }
3734
3993
 
3994
+ Future<void> setDesktopPassiveHistoryEnabled(bool value) async {
3995
+ final prefs = _prefs;
3996
+ if (prefs == null) {
3997
+ return;
3998
+ }
3999
+ try {
4000
+ await _desktopCompanion.setPassiveHistoryEnabled(value, prefs);
4001
+ } catch (error) {
4002
+ errorMessage = _friendlyErrorMessage(error);
4003
+ notifyListeners();
4004
+ }
4005
+ }
4006
+
3735
4007
  Future<void> rotateDesktopCompanionIdentity() async {
3736
4008
  final prefs = _prefs;
3737
4009
  if (prefs == null) {
@@ -4525,12 +4797,17 @@ class NeoAgentController extends ChangeNotifier {
4525
4797
  _appendChatMessage(content, role: 'user', platform: platform);
4526
4798
  }
4527
4799
 
4800
+ List<ToolEventItem> _capToolEvents(List<ToolEventItem> events) {
4801
+ if (events.length <= _maxToolEvents) return events;
4802
+ return events.sublist(events.length - _maxToolEvents);
4803
+ }
4804
+
4528
4805
  void _appendToolNote(String summary, {String toolName = 'note'}) {
4529
4806
  final trimmed = summary.trim();
4530
4807
  if (trimmed.isEmpty) {
4531
4808
  return;
4532
4809
  }
4533
- toolEvents = <ToolEventItem>[
4810
+ toolEvents = _capToolEvents(<ToolEventItem>[
4534
4811
  ...toolEvents,
4535
4812
  ToolEventItem(
4536
4813
  id: 'note-${DateTime.now().microsecondsSinceEpoch}',
@@ -4539,7 +4816,7 @@ class NeoAgentController extends ChangeNotifier {
4539
4816
  status: 'completed',
4540
4817
  summary: trimmed,
4541
4818
  ),
4542
- ];
4819
+ ]);
4543
4820
  }
4544
4821
 
4545
4822
  Future<void> refreshUpdateStatus() async {
@@ -4816,6 +5093,96 @@ class NeoAgentController extends ChangeNotifier {
4816
5093
  } catch (_) {}
4817
5094
  }
4818
5095
 
5096
+ // ── Billing ──────────────────────────────────────────────────────────────
5097
+
5098
+ Future<void> checkBillingEnabled() async {
5099
+ try {
5100
+ final r = await _backendClient.getBillingPlans(backendUrl);
5101
+ final enabled = r['plans'] != null;
5102
+ if (showBillingSection != enabled) {
5103
+ showBillingSection = enabled;
5104
+ notifyListeners();
5105
+ }
5106
+ } catch (_) {
5107
+ if (showBillingSection) {
5108
+ showBillingSection = false;
5109
+ notifyListeners();
5110
+ }
5111
+ }
5112
+ }
5113
+
5114
+ Future<void> refreshBilling() async {
5115
+ if (!isAuthenticated || !showBillingSection) return;
5116
+ isLoadingBilling = true;
5117
+ notifyListeners();
5118
+ try {
5119
+ final results = await Future.wait(<Future<Map<String, dynamic>>>[
5120
+ _backendClient.getBillingInfo(backendUrl),
5121
+ _backendClient.getBillingPlans(backendUrl),
5122
+ _backendClient.getBillingInvoices(backendUrl),
5123
+ ]);
5124
+ billingSubscription = results[0]['subscription'] as Map<String, dynamic>?;
5125
+ billingPlans = _asDynList(
5126
+ results[1]['plans'],
5127
+ ).cast<Map<String, dynamic>>();
5128
+ billingInvoices = _asDynList(
5129
+ results[2]['invoices'],
5130
+ ).cast<Map<String, dynamic>>();
5131
+ } catch (_) {
5132
+ // retain previous data on error
5133
+ } finally {
5134
+ isLoadingBilling = false;
5135
+ notifyListeners();
5136
+ }
5137
+ }
5138
+
5139
+ Future<String?> createCheckoutSession(String planId) async {
5140
+ try {
5141
+ final serverUrl = backendUrl;
5142
+ final result = await _backendClient.createCheckoutSession(
5143
+ baseUrl: serverUrl,
5144
+ planId: planId,
5145
+ successUrl: '$serverUrl/',
5146
+ cancelUrl: '$serverUrl/',
5147
+ );
5148
+ return result['url'] as String?;
5149
+ } catch (e) {
5150
+ errorMessage = _friendlyErrorMessage(e);
5151
+ notifyListeners();
5152
+ return null;
5153
+ }
5154
+ }
5155
+
5156
+ Future<String?> createPortalSession() async {
5157
+ try {
5158
+ final serverUrl = backendUrl;
5159
+ final result = await _backendClient.createPortalSession(
5160
+ baseUrl: serverUrl,
5161
+ returnUrl: '$serverUrl/',
5162
+ );
5163
+ return result['url'] as String?;
5164
+ } catch (e) {
5165
+ errorMessage = _friendlyErrorMessage(e);
5166
+ notifyListeners();
5167
+ return null;
5168
+ }
5169
+ }
5170
+
5171
+ Future<bool> cancelBillingSubscription() async {
5172
+ try {
5173
+ await _backendClient.cancelBillingSubscription(backendUrl);
5174
+ await refreshBilling();
5175
+ return true;
5176
+ } catch (e) {
5177
+ errorMessage = _friendlyErrorMessage(e);
5178
+ notifyListeners();
5179
+ return false;
5180
+ }
5181
+ }
5182
+
5183
+ List<dynamic> _asDynList(dynamic val) =>
5184
+ val is List ? val : const <dynamic>[];
5185
+
4819
5186
  Future<bool> updateAccountEmail({
4820
5187
  required String email,
4821
5188
  required String currentPassword,
@@ -5709,6 +6076,7 @@ class NeoAgentController extends ChangeNotifier {
5709
6076
  required String triggerType,
5710
6077
  required Map<String, dynamic> triggerConfig,
5711
6078
  required String prompt,
6079
+ Map<String, dynamic>? taskConfig,
5712
6080
  String? model,
5713
6081
  bool enabled = true,
5714
6082
  String? agentId,
@@ -5720,6 +6088,7 @@ class NeoAgentController extends ChangeNotifier {
5720
6088
  triggerType: triggerType,
5721
6089
  triggerConfig: triggerConfig,
5722
6090
  prompt: prompt,
6091
+ taskConfig: taskConfig,
5723
6092
  model: model,
5724
6093
  enabled: enabled,
5725
6094
  agentId: agentId ?? _scopedAgentId,
@@ -6764,6 +7133,9 @@ class NeoAgentController extends ChangeNotifier {
6764
7133
  }
6765
7134
  unawaited(_refreshRecordingSessionById(sessionId));
6766
7135
  });
7136
+ socket.on('timeline:updated', (dynamic _) {
7137
+ unawaited(refreshTimeline());
7138
+ });
6767
7139
  socket.on('voice:session_ready', (dynamic data) {
6768
7140
  final payload = _jsonMap(data);
6769
7141
  voiceAssistantLiveState = voiceAssistantLiveState.copyWith(
@@ -6982,7 +7354,7 @@ class NeoAgentController extends ChangeNotifier {
6982
7354
  'verification: ${payload['verification_need']?.toString() ?? 'none'}',
6983
7355
  'freshness: ${payload['freshness_risk']?.toString() ?? 'none'}',
6984
7356
  ].join(' | ');
6985
- toolEvents = <ToolEventItem>[
7357
+ toolEvents = _capToolEvents(<ToolEventItem>[
6986
7358
  ...toolEvents,
6987
7359
  ToolEventItem(
6988
7360
  id: 'analysis-${DateTime.now().microsecondsSinceEpoch}',
@@ -6991,7 +7363,7 @@ class NeoAgentController extends ChangeNotifier {
6991
7363
  status: 'completed',
6992
7364
  summary: summary,
6993
7365
  ),
6994
- ];
7366
+ ]);
6995
7367
  if (activeRun?.runId == runId) {
6996
7368
  activeRun = activeRun!.copyWith(phase: 'Analyzing');
6997
7369
  }
@@ -7016,7 +7388,7 @@ class NeoAgentController extends ChangeNotifier {
7016
7388
  .where((item) => item.trim().isNotEmpty)
7017
7389
  .take(4)
7018
7390
  .join(' | ');
7019
- toolEvents = <ToolEventItem>[
7391
+ toolEvents = _capToolEvents(<ToolEventItem>[
7020
7392
  ...toolEvents,
7021
7393
  ToolEventItem(
7022
7394
  id: 'plan-${DateTime.now().microsecondsSinceEpoch}',
@@ -7025,7 +7397,7 @@ class NeoAgentController extends ChangeNotifier {
7025
7397
  status: 'completed',
7026
7398
  summary: steps.ifEmpty('Execution plan created.'),
7027
7399
  ),
7028
- ];
7400
+ ]);
7029
7401
  if (activeRun?.runId == runId) {
7030
7402
  activeRun = activeRun!.copyWith(phase: 'Planning');
7031
7403
  }
@@ -7065,10 +7437,10 @@ class NeoAgentController extends ChangeNotifier {
7065
7437
  status: 'running',
7066
7438
  summary: _summarizeToolArgs(payload['toolArgs']),
7067
7439
  );
7068
- toolEvents = <ToolEventItem>[
7440
+ toolEvents = _capToolEvents(<ToolEventItem>[
7069
7441
  ...toolEvents.where((event) => event.id != item.id),
7070
7442
  item,
7071
- ];
7443
+ ]);
7072
7444
  if (activeRun?.runId == runId) {
7073
7445
  activeRun = activeRun!.copyWith(phase: 'Running tool');
7074
7446
  }
@@ -7083,7 +7455,7 @@ class NeoAgentController extends ChangeNotifier {
7083
7455
  if (_backgroundRunIds.contains(runId)) {
7084
7456
  return;
7085
7457
  }
7086
- toolEvents = <ToolEventItem>[
7458
+ toolEvents = _capToolEvents(<ToolEventItem>[
7087
7459
  ...toolEvents,
7088
7460
  ToolEventItem(
7089
7461
  id: 'verification-${DateTime.now().microsecondsSinceEpoch}',
@@ -7098,7 +7470,7 @@ class NeoAgentController extends ChangeNotifier {
7098
7470
  ) ??
7099
7471
  'Verification completed.',
7100
7472
  ),
7101
- ];
7473
+ ]);
7102
7474
  if (activeRun?.runId == runId) {
7103
7475
  activeRun = activeRun!.copyWith(phase: 'Verifying');
7104
7476
  }
@@ -7138,7 +7510,7 @@ class NeoAgentController extends ChangeNotifier {
7138
7510
  'Subagent update.',
7139
7511
  ),
7140
7512
  );
7141
- toolEvents = nextEvents;
7513
+ toolEvents = _capToolEvents(nextEvents);
7142
7514
  notifyListeners();
7143
7515
  });
7144
7516
  socket.on('run:tool_end', (dynamic data) {
@@ -7171,7 +7543,7 @@ class NeoAgentController extends ChangeNotifier {
7171
7543
  if (!replaced) {
7172
7544
  next.add(updated);
7173
7545
  }
7174
- toolEvents = next;
7546
+ toolEvents = _capToolEvents(next);
7175
7547
  final toolName = payload['toolName']?.toString() ?? '';
7176
7548
  final screenshotPath =
7177
7549
  payload['screenshotPath']?.toString() ??
@@ -7220,7 +7592,7 @@ class NeoAgentController extends ChangeNotifier {
7220
7592
  if (_backgroundRunIds.contains(runId)) {
7221
7593
  return;
7222
7594
  }
7223
- toolEvents = <ToolEventItem>[
7595
+ toolEvents = _capToolEvents(<ToolEventItem>[
7224
7596
  ...toolEvents,
7225
7597
  ToolEventItem(
7226
7598
  id: 'steer-queued-${DateTime.now().microsecondsSinceEpoch}',
@@ -7230,7 +7602,7 @@ class NeoAgentController extends ChangeNotifier {
7230
7602
  summary:
7231
7603
  'Queued as steering for the current run: ${payload['content']?.toString() ?? ''}',
7232
7604
  ),
7233
- ];
7605
+ ]);
7234
7606
  if (activeRun?.runId == runId || activeRun?.runId == 'pending') {
7235
7607
  activeRun = activeRun!.copyWith(
7236
7608
  pendingSteeringCount: _asInt(payload['pendingCount']),
@@ -7247,7 +7619,7 @@ class NeoAgentController extends ChangeNotifier {
7247
7619
  if (_backgroundRunIds.contains(runId)) {
7248
7620
  return;
7249
7621
  }
7250
- toolEvents = <ToolEventItem>[
7622
+ toolEvents = _capToolEvents(<ToolEventItem>[
7251
7623
  ...toolEvents,
7252
7624
  ToolEventItem(
7253
7625
  id: 'steer-applied-${DateTime.now().microsecondsSinceEpoch}',
@@ -7258,7 +7630,7 @@ class NeoAgentController extends ChangeNotifier {
7258
7630
  ? 'Applied the latest steering update to the current run.'
7259
7631
  : 'Applied ${_asInt(payload['count'])} queued steering updates to the current run.',
7260
7632
  ),
7261
- ];
7633
+ ]);
7262
7634
  if (activeRun?.runId == runId || activeRun?.runId == 'pending') {
7263
7635
  activeRun = activeRun!.copyWith(
7264
7636
  pendingSteeringCount: _asInt(payload['pendingCount']),