neoagent 2.4.1-beta.9 → 2.4.1

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 (234) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +36 -4
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -3
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +13 -0
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +2 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +197 -42
  52. package/flutter_app/lib/main_devices.dart +436 -4
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +673 -871
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +46 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
  70. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  71. package/flutter_app/lib/src/stream_renderer.dart +42 -3
  72. package/flutter_app/lib/src/theme/palette.dart +76 -34
  73. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  74. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  75. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  81. package/flutter_app/pubspec.lock +2 -2
  82. package/flutter_app/pubspec.yaml +7 -1
  83. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  84. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  85. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  94. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  95. package/flutter_app/web/favicon.png +0 -0
  96. package/flutter_app/web/favicon.svg +40 -9
  97. package/flutter_app/web/favicon_light.svg +43 -0
  98. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  99. package/flutter_app/web/icons/Icon-192.png +0 -0
  100. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-512.png +0 -0
  102. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  106. package/flutter_app/windows/runner/main.cpp +7 -1
  107. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  108. package/lib/manager.js +445 -83
  109. package/package.json +17 -3
  110. package/runtime/paths.js +3 -1
  111. package/server/admin/access.js +198 -0
  112. package/server/admin/admin.css +268 -0
  113. package/server/admin/admin.js +348 -0
  114. package/server/admin/analytics.js +128 -0
  115. package/server/admin/index.html +1015 -0
  116. package/server/admin/login.html +290 -0
  117. package/server/admin/logo.svg +43 -0
  118. package/server/admin/sql.js +134 -0
  119. package/server/admin/users.js +147 -0
  120. package/server/config/origins.js +3 -1
  121. package/server/db/database.js +92 -0
  122. package/server/db/ftsQuery.js +27 -0
  123. package/server/http/routes.js +1 -0
  124. package/server/http/static.js +23 -6
  125. package/server/index.js +1 -1
  126. package/server/middleware/adminAuth.js +48 -0
  127. package/server/middleware/auth.js +1 -40
  128. package/server/public/.last_build_id +1 -1
  129. package/server/public/app_icon.png +0 -0
  130. package/server/public/assets/AssetManifest.bin +1 -1
  131. package/server/public/assets/AssetManifest.bin.json +1 -1
  132. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  133. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  134. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  138. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  139. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  140. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  141. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  142. package/server/public/assets/web/icons/Icon-192.png +0 -0
  143. package/server/public/favicon.png +0 -0
  144. package/server/public/favicon.svg +40 -9
  145. package/server/public/favicon_light.svg +43 -0
  146. package/server/public/flutter_bootstrap.js +2 -2
  147. package/server/public/icons/Icon-192-light.png +0 -0
  148. package/server/public/icons/Icon-192.png +0 -0
  149. package/server/public/icons/Icon-512-light.png +0 -0
  150. package/server/public/icons/Icon-512.png +0 -0
  151. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  152. package/server/public/icons/Icon-maskable-192.png +0 -0
  153. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-512.png +0 -0
  155. package/server/public/main.dart.js +88537 -85936
  156. package/server/routes/admin.js +632 -0
  157. package/server/routes/agent_profiles.js +3 -0
  158. package/server/routes/agents.js +1 -1
  159. package/server/routes/auth.js +3 -1
  160. package/server/routes/mcp.js +29 -13
  161. package/server/routes/memory.js +23 -1
  162. package/server/routes/messaging.js +2 -0
  163. package/server/routes/screenHistory.js +14 -8
  164. package/server/routes/settings.js +10 -2
  165. package/server/routes/stream.js +1 -1
  166. package/server/routes/triggers.js +4 -4
  167. package/server/routes/voice_assistant.js +36 -1
  168. package/server/routes/workspace.js +86 -0
  169. package/server/services/account/admin_two_factor.js +132 -0
  170. package/server/services/account/password_policy.js +6 -1
  171. package/server/services/ai/completion.js +44 -0
  172. package/server/services/ai/engine.js +321 -500
  173. package/server/services/ai/imageAnalysis.js +9 -5
  174. package/server/services/ai/logFormat.js +46 -0
  175. package/server/services/ai/loopPolicy.js +11 -0
  176. package/server/services/ai/messagingFallback.js +228 -0
  177. package/server/services/ai/models.js +194 -55
  178. package/server/services/ai/providerRetry.js +169 -0
  179. package/server/services/ai/providers/anthropic.js +15 -4
  180. package/server/services/ai/providers/google.js +38 -11
  181. package/server/services/ai/providers/grok.js +19 -68
  182. package/server/services/ai/providers/grokOauth.js +141 -0
  183. package/server/services/ai/providers/nvidia.js +154 -0
  184. package/server/services/ai/providers/ollama.js +76 -39
  185. package/server/services/ai/providers/openai.js +20 -31
  186. package/server/services/ai/providers/openaiCodex.js +6 -2
  187. package/server/services/ai/providers/openaiCompatible.js +70 -0
  188. package/server/services/ai/providers/openrouter.js +162 -0
  189. package/server/services/ai/settings.js +30 -0
  190. package/server/services/ai/systemPrompt.js +51 -29
  191. package/server/services/ai/taskAnalysis.js +60 -1
  192. package/server/services/ai/toolEvidence.js +207 -0
  193. package/server/services/ai/toolSelector.js +8 -1
  194. package/server/services/ai/tools.js +78 -13
  195. package/server/services/browser/extension/registry.js +94 -9
  196. package/server/services/desktop/registry.js +104 -1
  197. package/server/services/desktop/screenRecorder.js +208 -98
  198. package/server/services/desktop/screen_recorder_support.js +46 -0
  199. package/server/services/integrations/google/provider.js +13 -0
  200. package/server/services/integrations/home_assistant/constants.js +88 -0
  201. package/server/services/integrations/home_assistant/network.js +207 -0
  202. package/server/services/integrations/home_assistant/provider.js +249 -0
  203. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  204. package/server/services/integrations/home_assistant/tools.js +101 -0
  205. package/server/services/integrations/manager.js +5 -0
  206. package/server/services/integrations/registry.js +2 -0
  207. package/server/services/integrations/trello/provider.js +8 -2
  208. package/server/services/manager.js +51 -53
  209. package/server/services/mcp/client.js +208 -268
  210. package/server/services/mcp/client_support.js +172 -0
  211. package/server/services/mcp/recovery.js +116 -0
  212. package/server/services/mcp/tool_operations.js +123 -0
  213. package/server/services/memory/ingestion.js +185 -370
  214. package/server/services/memory/ingestion_coverage.js +129 -0
  215. package/server/services/memory/ingestion_documents.js +123 -0
  216. package/server/services/memory/ingestion_support.js +171 -0
  217. package/server/services/memory/intelligence.js +181 -0
  218. package/server/services/memory/manager.js +476 -32
  219. package/server/services/messaging/automation.js +71 -134
  220. package/server/services/messaging/formatting_guides.js +26 -1
  221. package/server/services/messaging/inbound_queue.js +111 -0
  222. package/server/services/messaging/manager.js +25 -8
  223. package/server/services/messaging/typing_keepalive.js +110 -0
  224. package/server/services/streaming/android-stream.js +293 -40
  225. package/server/services/tasks/integration_runtime.js +4 -1
  226. package/server/services/tasks/runtime.js +415 -56
  227. package/server/services/tasks/task_repository.js +48 -6
  228. package/server/services/websocket.js +8 -2
  229. package/server/services/widgets/service.js +17 -1
  230. package/server/services/workspace/manager.js +116 -0
  231. package/server/utils/logger.js +44 -6
  232. package/server/utils/security.js +3 -0
  233. package/server/utils/version.js +29 -19
  234. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -63,6 +63,7 @@ class NeoAgentController extends ChangeNotifier {
63
63
  final Set<String> _backgroundRunIds = <String>{};
64
64
  final Set<String> _voiceRunIds = <String>{};
65
65
  final Set<String> _busyOfficialIntegrationKeys = <String>{};
66
+ final Set<String> _busyMessagingPlatformKeys = <String>{};
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,6 +71,7 @@ class NeoAgentController extends ChangeNotifier {
70
71
  int _authCycle = 0;
71
72
  bool _isPollingQrLogin = false;
72
73
  bool _socketHasConnectedOnce = false;
74
+ bool _onboardingManuallyReopened = false;
73
75
  List<LogEntry> _serverLogs = const <LogEntry>[];
74
76
  List<LogEntry> _clientLogs = const <LogEntry>[];
75
77
 
@@ -91,6 +93,8 @@ class NeoAgentController extends ChangeNotifier {
91
93
  bool isSavingReleaseChannel = false;
92
94
  bool isSyncingHealth = false;
93
95
  bool isRunningDeviceAction = false;
96
+ bool isLoadingWorkspaceFiles = false;
97
+ bool isSavingWorkspaceFile = false;
94
98
  bool isPreparingQrLogin = false;
95
99
  bool isApprovingQrLogin = false;
96
100
  bool isCheckingAppUpdate = false;
@@ -188,6 +192,10 @@ class NeoAgentController extends ChangeNotifier {
188
192
  String? androidLastResult;
189
193
  String? desktopLastResult;
190
194
  String? androidUiDumpPath;
195
+ String workspaceCurrentPath = '';
196
+ String? workspaceSelectedFilePath;
197
+ String workspaceEditorContent = '';
198
+ List<Map<String, dynamic>> workspaceEntries = const <Map<String, dynamic>>[];
191
199
  final Map<String, RunDetailSnapshot> _runDetailsCache =
192
200
  <String, RunDetailSnapshot>{};
193
201
  String? _selectedWidgetId;
@@ -236,6 +244,8 @@ class NeoAgentController extends ChangeNotifier {
236
244
 
237
245
  bool isOfficialIntegrationBusy(String key) =>
238
246
  _busyOfficialIntegrationKeys.contains(key);
247
+ bool isMessagingPlatformBusy(String platform, String action) =>
248
+ _busyMessagingPlatformKeys.contains('$platform:$action');
239
249
 
240
250
  String get chatComposerHint => hasLiveRun
241
251
  ? 'Send a steering update or next-up note for the current run...'
@@ -448,6 +458,7 @@ class NeoAgentController extends ChangeNotifier {
448
458
  required String role,
449
459
  required String platform,
450
460
  bool transient = false,
461
+ Map<String, dynamic> metadata = const <String, dynamic>{},
451
462
  }) {
452
463
  final trimmed = content.trim();
453
464
  if (trimmed.isEmpty) {
@@ -458,7 +469,8 @@ class NeoAgentController extends ChangeNotifier {
458
469
  if (previous != null &&
459
470
  previous.role == role &&
460
471
  previous.platform == platform &&
461
- previous.content.trim() == trimmed) {
472
+ previous.content.trim() == trimmed &&
473
+ metadata.isEmpty) {
462
474
  return;
463
475
  }
464
476
 
@@ -471,6 +483,7 @@ class NeoAgentController extends ChangeNotifier {
471
483
  platform: platform,
472
484
  createdAt: DateTime.now(),
473
485
  transient: transient,
486
+ metadata: metadata,
474
487
  ),
475
488
  ];
476
489
  }
@@ -812,6 +825,7 @@ class NeoAgentController extends ChangeNotifier {
812
825
  status['user'] as Map<String, dynamic>,
813
826
  );
814
827
  isAuthenticated = true;
828
+ _syncOnboardingFromAccount();
815
829
  }
816
830
  if (isAuthenticated) {
817
831
  unawaited(refresh());
@@ -1350,9 +1364,7 @@ class NeoAgentController extends ChangeNotifier {
1350
1364
  pendingTwoFactorUsername = '';
1351
1365
  password = '';
1352
1366
 
1353
- final bool backendCompletedOnboarding =
1354
- user?['hasCompletedOnboarding'] == true;
1355
- showOnboarding = isRegistration || !backendCompletedOnboarding;
1367
+ _syncOnboardingFromAccount();
1356
1368
 
1357
1369
  _clearQrLoginChallenge();
1358
1370
  await _persistCredentials();
@@ -1596,6 +1608,7 @@ class NeoAgentController extends ChangeNotifier {
1596
1608
  }
1597
1609
 
1598
1610
  Future<void> dismissOnboarding() async {
1611
+ _onboardingManuallyReopened = false;
1599
1612
  showOnboarding = false;
1600
1613
  notifyListeners();
1601
1614
  try {
@@ -1612,10 +1625,23 @@ class NeoAgentController extends ChangeNotifier {
1612
1625
  }
1613
1626
 
1614
1627
  void reopenOnboarding() {
1628
+ _onboardingManuallyReopened = true;
1615
1629
  showOnboarding = true;
1616
1630
  notifyListeners();
1617
1631
  }
1618
1632
 
1633
+ void _syncOnboardingFromAccount() {
1634
+ final hasCompletedOnboarding = user?['hasCompletedOnboarding'] == true;
1635
+ if (hasCompletedOnboarding) {
1636
+ if (!_onboardingManuallyReopened) {
1637
+ showOnboarding = false;
1638
+ }
1639
+ return;
1640
+ }
1641
+ _onboardingManuallyReopened = false;
1642
+ showOnboarding = true;
1643
+ }
1644
+
1619
1645
  void _clearAuthenticatedState() {
1620
1646
  _disconnectSocket();
1621
1647
  _updatePollTimer?.cancel();
@@ -1623,6 +1649,8 @@ class NeoAgentController extends ChangeNotifier {
1623
1649
  _clearQrLoginChallenge();
1624
1650
  isAuthenticated = false;
1625
1651
  isRefreshing = false;
1652
+ _onboardingManuallyReopened = false;
1653
+ _busyMessagingPlatformKeys.clear();
1626
1654
  isAwaitingTwoFactor = false;
1627
1655
  pendingTwoFactorUsername = '';
1628
1656
  errorMessage = null;
@@ -2080,6 +2108,7 @@ class NeoAgentController extends ChangeNotifier {
2080
2108
  user = Map<String, dynamic>.from(
2081
2109
  authStatus['user'] as Map<String, dynamic>,
2082
2110
  );
2111
+ _syncOnboardingFromAccount();
2083
2112
 
2084
2113
  final profilesResponse = await _backendClient.fetchAgentProfiles(
2085
2114
  backendUrl,
@@ -2478,31 +2507,6 @@ class NeoAgentController extends ChangeNotifier {
2478
2507
  );
2479
2508
  notifyListeners();
2480
2509
  } catch (_) {}
2481
-
2482
- Future<String> fetchMemoryTransferPrompt() async {
2483
- final response = await _backendClient.fetchMemoryTransferPrompt(
2484
- backendUrl,
2485
- agentId: _scopedAgentId,
2486
- );
2487
- return response['prompt']?.toString() ?? '';
2488
- }
2489
-
2490
- Future<MemoryTransferImportResult> importMemoryTransfer(
2491
- String text, {
2492
- bool applyBehaviorNotes = true,
2493
- bool applyCoreMemory = true,
2494
- }) async {
2495
- final response = await _backendClient.importMemoryTransfer(
2496
- backendUrl,
2497
- text: text,
2498
- applyBehaviorNotes: applyBehaviorNotes,
2499
- applyCoreMemory: applyCoreMemory,
2500
- agentId: _scopedAgentId,
2501
- );
2502
- final result = MemoryTransferImportResult.fromJson(response);
2503
- await refreshMemory();
2504
- return result;
2505
- }
2506
2510
  }
2507
2511
 
2508
2512
  Future<void> refreshMessaging() async {
@@ -3493,6 +3497,102 @@ class NeoAgentController extends ChangeNotifier {
3493
3497
  );
3494
3498
  }
3495
3499
 
3500
+ String workspaceDownloadUrl(String path) {
3501
+ return '${_socketOrigin()}/${_backendClient.workspaceDownloadPath(path).replaceFirst(RegExp(r'^/'), '')}';
3502
+ }
3503
+
3504
+ Future<void> refreshWorkspaceFiles({String? path}) async {
3505
+ if (!isAuthenticated || isLoadingWorkspaceFiles) {
3506
+ return;
3507
+ }
3508
+ isLoadingWorkspaceFiles = true;
3509
+ errorMessage = null;
3510
+ notifyListeners();
3511
+ try {
3512
+ final response = await _backendClient.fetchWorkspaceDirectory(
3513
+ backendUrl,
3514
+ path: path ?? workspaceCurrentPath,
3515
+ );
3516
+ workspaceCurrentPath = response['path']?.toString() ?? '';
3517
+ workspaceEntries = _jsonMapList(
3518
+ response['entries'],
3519
+ fallbackToMapValues: true,
3520
+ );
3521
+ } catch (error) {
3522
+ errorMessage = _friendlyErrorMessage(error);
3523
+ } finally {
3524
+ isLoadingWorkspaceFiles = false;
3525
+ notifyListeners();
3526
+ }
3527
+ }
3528
+
3529
+ Future<void> openWorkspaceDirectory(String path) async {
3530
+ workspaceSelectedFilePath = null;
3531
+ workspaceEditorContent = '';
3532
+ await refreshWorkspaceFiles(path: path);
3533
+ }
3534
+
3535
+ Future<void> openWorkspaceFile(String path) async {
3536
+ if (isLoadingWorkspaceFiles) {
3537
+ return;
3538
+ }
3539
+ isLoadingWorkspaceFiles = true;
3540
+ errorMessage = null;
3541
+ notifyListeners();
3542
+ try {
3543
+ final response = await _backendClient.fetchWorkspaceFile(
3544
+ backendUrl,
3545
+ path: path,
3546
+ );
3547
+ workspaceSelectedFilePath = response['path']?.toString() ?? path;
3548
+ workspaceEditorContent = response['content']?.toString() ?? '';
3549
+ } catch (error) {
3550
+ errorMessage = _friendlyErrorMessage(error);
3551
+ } finally {
3552
+ isLoadingWorkspaceFiles = false;
3553
+ notifyListeners();
3554
+ }
3555
+ }
3556
+
3557
+ Future<void> saveWorkspaceFile(String content) async {
3558
+ final path = workspaceSelectedFilePath?.trim() ?? '';
3559
+ if (path.isEmpty || isSavingWorkspaceFile) {
3560
+ return;
3561
+ }
3562
+ isSavingWorkspaceFile = true;
3563
+ errorMessage = null;
3564
+ notifyListeners();
3565
+ try {
3566
+ await _backendClient.saveWorkspaceFile(
3567
+ backendUrl,
3568
+ path: path,
3569
+ content: content,
3570
+ );
3571
+ workspaceEditorContent = content;
3572
+ await refreshWorkspaceFiles(path: workspaceCurrentPath);
3573
+ } catch (error) {
3574
+ errorMessage = _friendlyErrorMessage(error);
3575
+ } finally {
3576
+ isSavingWorkspaceFile = false;
3577
+ notifyListeners();
3578
+ }
3579
+ }
3580
+
3581
+ Future<void> downloadWorkspaceFile(String path) async {
3582
+ final normalized = path.trim();
3583
+ if (normalized.isEmpty) {
3584
+ return;
3585
+ }
3586
+ final result = await _oauthLauncher.openExternal(
3587
+ url: workspaceDownloadUrl(normalized),
3588
+ label: 'neoagent_workspace_file_download',
3589
+ );
3590
+ if (!result.launched) {
3591
+ errorMessage = result.error ?? 'Could not open workspace file download.';
3592
+ notifyListeners();
3593
+ }
3594
+ }
3595
+
3496
3596
  Uri resolveRuntimeAsset(String path) {
3497
3597
  final separator = path.contains('?') ? '&' : '?';
3498
3598
  return _backendClient.resolveAssetUri(
@@ -4536,12 +4636,14 @@ class NeoAgentController extends ChangeNotifier {
4536
4636
  String content, {
4537
4637
  required String platform,
4538
4638
  bool transient = false,
4639
+ Map<String, dynamic> metadata = const <String, dynamic>{},
4539
4640
  }) {
4540
4641
  _appendChatMessage(
4541
4642
  content,
4542
4643
  role: 'assistant',
4543
4644
  platform: platform,
4544
4645
  transient: transient,
4646
+ metadata: metadata,
4545
4647
  );
4546
4648
  }
4547
4649
 
@@ -4602,6 +4704,18 @@ class NeoAgentController extends ChangeNotifier {
4602
4704
  }
4603
4705
  }
4604
4706
 
4707
+ Future<String> transcribeDictationAudio({
4708
+ required String audioBase64,
4709
+ String mimeType = 'audio/pcm;rate=16000;channels=1',
4710
+ }) async {
4711
+ final result = await _backendClient.transcribeAudio(
4712
+ backendUrl,
4713
+ audioBase64: audioBase64,
4714
+ mimeType: mimeType,
4715
+ );
4716
+ return result['transcript']?.toString() ?? '';
4717
+ }
4718
+
4605
4719
  Future<void> sendMessage(
4606
4720
  String task, {
4607
4721
  List<SharedChatAttachment> sharedAttachments =
@@ -5512,12 +5626,28 @@ class NeoAgentController extends ChangeNotifier {
5512
5626
  }
5513
5627
 
5514
5628
  Future<void> disconnectMessagingPlatform(String platform) async {
5515
- await _backendClient.disconnectMessagingPlatform(
5516
- backendUrl,
5517
- platform: platform,
5518
- agentId: _scopedAgentId,
5519
- );
5520
- await refreshMessaging();
5629
+ final busyKey = '$platform:disconnect';
5630
+ if (_busyMessagingPlatformKeys.contains(busyKey)) {
5631
+ return;
5632
+ }
5633
+
5634
+ _busyMessagingPlatformKeys.add(busyKey);
5635
+ errorMessage = null;
5636
+ notifyListeners();
5637
+
5638
+ try {
5639
+ await _backendClient.disconnectMessagingPlatform(
5640
+ backendUrl,
5641
+ platform: platform,
5642
+ agentId: _scopedAgentId,
5643
+ );
5644
+ await refreshMessaging();
5645
+ } catch (error) {
5646
+ errorMessage = _friendlyErrorMessage(error);
5647
+ } finally {
5648
+ _busyMessagingPlatformKeys.remove(busyKey);
5649
+ notifyListeners();
5650
+ }
5521
5651
  }
5522
5652
 
5523
5653
  Future<void> logoutMessagingPlatform(String platform) async {
@@ -5987,18 +6117,36 @@ class NeoAgentController extends ChangeNotifier {
5987
6117
  }
5988
6118
 
5989
6119
  Future<void> startMcpServer(int id) async {
5990
- await _backendClient.startMcpServer(backendUrl, id);
5991
- await refreshMcp();
6120
+ try {
6121
+ await _backendClient.startMcpServer(backendUrl, id);
6122
+ await refreshMcp();
6123
+ } catch (error) {
6124
+ errorMessage = _friendlyErrorMessage(error);
6125
+ await refreshMcp();
6126
+ notifyListeners();
6127
+ }
5992
6128
  }
5993
6129
 
5994
6130
  Future<void> stopMcpServer(int id) async {
5995
- await _backendClient.stopMcpServer(backendUrl, id);
5996
- await refreshMcp();
6131
+ try {
6132
+ await _backendClient.stopMcpServer(backendUrl, id);
6133
+ await refreshMcp();
6134
+ } catch (error) {
6135
+ errorMessage = _friendlyErrorMessage(error);
6136
+ await refreshMcp();
6137
+ notifyListeners();
6138
+ }
5997
6139
  }
5998
6140
 
5999
6141
  Future<void> deleteMcpServer(int id) async {
6000
- await _backendClient.deleteMcpServer(backendUrl, id);
6001
- await refreshMcp();
6142
+ try {
6143
+ await _backendClient.deleteMcpServer(backendUrl, id);
6144
+ await refreshMcp();
6145
+ } catch (error) {
6146
+ errorMessage = _friendlyErrorMessage(error);
6147
+ await refreshMcp();
6148
+ notifyListeners();
6149
+ }
6002
6150
  }
6003
6151
 
6004
6152
  Future<void> requestHealthPermissions() async {
@@ -7275,7 +7423,14 @@ class NeoAgentController extends ChangeNotifier {
7275
7423
  }
7276
7424
  final content = payload['content']?.toString().trim() ?? '';
7277
7425
  if (content.isNotEmpty) {
7278
- _appendAssistantChatMessage(content, platform: 'web');
7426
+ final schema = payload['schema'];
7427
+ _appendAssistantChatMessage(
7428
+ content,
7429
+ platform: 'web',
7430
+ metadata: schema != null
7431
+ ? <String, dynamic>{'schema': schema}
7432
+ : const <String, dynamic>{},
7433
+ );
7279
7434
  }
7280
7435
  streamingAssistant = '';
7281
7436
  isSendingMessage = false;