neoagent 2.4.1-beta.8 → 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 (248) 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 +45 -7
  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 -2
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +15 -2
  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 +3 -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 +338 -46
  52. package/flutter_app/lib/main_devices.dart +851 -122
  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 +764 -887
  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 +79 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
  70. package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
  71. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  72. package/flutter_app/lib/src/stream_renderer.dart +205 -35
  73. package/flutter_app/lib/src/theme/palette.dart +76 -34
  74. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  75. package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  81. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  82. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  83. package/flutter_app/pubspec.lock +2 -2
  84. package/flutter_app/pubspec.yaml +7 -1
  85. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  94. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  95. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  96. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  97. package/flutter_app/web/favicon.png +0 -0
  98. package/flutter_app/web/favicon.svg +40 -9
  99. package/flutter_app/web/favicon_light.svg +43 -0
  100. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-192.png +0 -0
  102. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-512.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  106. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  107. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  108. package/flutter_app/windows/runner/main.cpp +7 -1
  109. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  110. package/lib/manager.js +445 -83
  111. package/package.json +17 -3
  112. package/runtime/paths.js +3 -1
  113. package/server/admin/access.js +198 -0
  114. package/server/admin/admin.css +268 -0
  115. package/server/admin/admin.js +348 -0
  116. package/server/admin/analytics.js +128 -0
  117. package/server/admin/index.html +1015 -0
  118. package/server/admin/login.html +290 -0
  119. package/server/admin/logo.svg +43 -0
  120. package/server/admin/sql.js +134 -0
  121. package/server/admin/users.js +147 -0
  122. package/server/config/origins.js +3 -1
  123. package/server/db/database.js +92 -0
  124. package/server/db/ftsQuery.js +27 -0
  125. package/server/http/routes.js +1 -0
  126. package/server/http/static.js +23 -6
  127. package/server/index.js +1 -1
  128. package/server/middleware/adminAuth.js +48 -0
  129. package/server/middleware/auth.js +1 -40
  130. package/server/public/.last_build_id +1 -1
  131. package/server/public/app_icon.png +0 -0
  132. package/server/public/assets/AssetManifest.bin +1 -1
  133. package/server/public/assets/AssetManifest.bin.json +1 -1
  134. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  138. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  139. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  140. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  141. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  142. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  143. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  144. package/server/public/assets/web/icons/Icon-192.png +0 -0
  145. package/server/public/favicon.png +0 -0
  146. package/server/public/favicon.svg +40 -9
  147. package/server/public/favicon_light.svg +43 -0
  148. package/server/public/flutter_bootstrap.js +2 -2
  149. package/server/public/icons/Icon-192-light.png +0 -0
  150. package/server/public/icons/Icon-192.png +0 -0
  151. package/server/public/icons/Icon-512-light.png +0 -0
  152. package/server/public/icons/Icon-512.png +0 -0
  153. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-192.png +0 -0
  155. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  156. package/server/public/icons/Icon-maskable-512.png +0 -0
  157. package/server/public/main.dart.js +88944 -85720
  158. package/server/routes/admin.js +632 -0
  159. package/server/routes/agent_profiles.js +3 -0
  160. package/server/routes/agents.js +1 -1
  161. package/server/routes/auth.js +3 -1
  162. package/server/routes/browser.js +14 -0
  163. package/server/routes/browser_extension.js +21 -4
  164. package/server/routes/desktop.js +10 -0
  165. package/server/routes/mcp.js +29 -13
  166. package/server/routes/memory.js +23 -1
  167. package/server/routes/messaging.js +2 -0
  168. package/server/routes/screenHistory.js +14 -8
  169. package/server/routes/settings.js +14 -2
  170. package/server/routes/stream.js +12 -3
  171. package/server/routes/triggers.js +4 -4
  172. package/server/routes/voice_assistant.js +36 -1
  173. package/server/routes/workspace.js +86 -0
  174. package/server/services/account/admin_two_factor.js +132 -0
  175. package/server/services/account/password_policy.js +6 -1
  176. package/server/services/ai/completion.js +44 -0
  177. package/server/services/ai/engine.js +321 -500
  178. package/server/services/ai/imageAnalysis.js +9 -5
  179. package/server/services/ai/logFormat.js +46 -0
  180. package/server/services/ai/loopPolicy.js +11 -0
  181. package/server/services/ai/messagingFallback.js +228 -0
  182. package/server/services/ai/models.js +194 -55
  183. package/server/services/ai/providerRetry.js +169 -0
  184. package/server/services/ai/providers/anthropic.js +15 -4
  185. package/server/services/ai/providers/google.js +38 -11
  186. package/server/services/ai/providers/grok.js +19 -68
  187. package/server/services/ai/providers/grokOauth.js +141 -0
  188. package/server/services/ai/providers/nvidia.js +154 -0
  189. package/server/services/ai/providers/ollama.js +76 -39
  190. package/server/services/ai/providers/openai.js +20 -31
  191. package/server/services/ai/providers/openaiCodex.js +6 -2
  192. package/server/services/ai/providers/openaiCompatible.js +70 -0
  193. package/server/services/ai/providers/openrouter.js +162 -0
  194. package/server/services/ai/settings.js +30 -0
  195. package/server/services/ai/systemPrompt.js +51 -29
  196. package/server/services/ai/taskAnalysis.js +60 -1
  197. package/server/services/ai/toolEvidence.js +207 -0
  198. package/server/services/ai/toolSelector.js +8 -1
  199. package/server/services/ai/tools.js +80 -15
  200. package/server/services/android/controller.js +36 -1
  201. package/server/services/browser/controller.js +18 -0
  202. package/server/services/browser/extension/manifest.js +33 -0
  203. package/server/services/browser/extension/provider.js +12 -6
  204. package/server/services/browser/extension/registry.js +229 -18
  205. package/server/services/desktop/protocol.js +1 -0
  206. package/server/services/desktop/provider.js +4 -0
  207. package/server/services/desktop/registry.js +104 -1
  208. package/server/services/desktop/screenRecorder.js +208 -98
  209. package/server/services/desktop/screen_recorder_support.js +46 -0
  210. package/server/services/integrations/google/provider.js +13 -0
  211. package/server/services/integrations/home_assistant/constants.js +88 -0
  212. package/server/services/integrations/home_assistant/network.js +207 -0
  213. package/server/services/integrations/home_assistant/provider.js +249 -0
  214. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  215. package/server/services/integrations/home_assistant/tools.js +101 -0
  216. package/server/services/integrations/manager.js +5 -0
  217. package/server/services/integrations/registry.js +2 -0
  218. package/server/services/integrations/trello/provider.js +8 -2
  219. package/server/services/manager.js +51 -47
  220. package/server/services/mcp/client.js +208 -268
  221. package/server/services/mcp/client_support.js +172 -0
  222. package/server/services/mcp/recovery.js +116 -0
  223. package/server/services/mcp/tool_operations.js +123 -0
  224. package/server/services/memory/ingestion.js +185 -370
  225. package/server/services/memory/ingestion_coverage.js +129 -0
  226. package/server/services/memory/ingestion_documents.js +123 -0
  227. package/server/services/memory/ingestion_support.js +171 -0
  228. package/server/services/memory/intelligence.js +181 -0
  229. package/server/services/memory/manager.js +476 -32
  230. package/server/services/messaging/automation.js +71 -134
  231. package/server/services/messaging/formatting_guides.js +26 -1
  232. package/server/services/messaging/inbound_queue.js +111 -0
  233. package/server/services/messaging/manager.js +25 -8
  234. package/server/services/messaging/typing_keepalive.js +110 -0
  235. package/server/services/runtime/manager.js +7 -3
  236. package/server/services/runtime/settings.js +17 -0
  237. package/server/services/streaming/android-stream.js +296 -32
  238. package/server/services/streaming/stream-hub.js +1 -1
  239. package/server/services/tasks/integration_runtime.js +4 -1
  240. package/server/services/tasks/runtime.js +415 -56
  241. package/server/services/tasks/task_repository.js +48 -6
  242. package/server/services/websocket.js +8 -2
  243. package/server/services/widgets/service.js +17 -1
  244. package/server/services/workspace/manager.js +116 -0
  245. package/server/utils/logger.js +44 -6
  246. package/server/utils/security.js +3 -0
  247. package/server/utils/version.js +29 -19
  248. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -25,10 +25,19 @@ class DesktopCompanionManager extends ChangeNotifier {
25
25
  final DesktopCompanionActions _actions;
26
26
  WebSocket? _socket;
27
27
  Timer? _reconnectTimer;
28
+ Timer? _connectionWatchdogTimer;
28
29
  Timer? _streamTimer;
29
30
  bool _streamCaptureInFlight = false;
31
+ // Set true while a click / drag / scroll / typeText / pressKey command is
32
+ // being executed. _captureAndSendBinaryFrame respects this flag so it does
33
+ // not compete with the input command for the native bridge or the WebSocket
34
+ // send buffer, and a fresh frame is forced immediately after the action.
35
+ bool _inputCommandInFlight = false;
30
36
  int _frameSeq = 0;
31
37
  int _streamGeneration = 0;
38
+ // Tracks the current stream quality so the forced post-input capture can use
39
+ // the same setting without re-parsing the original startStream payload.
40
+ int _currentStreamQuality = 80;
32
41
 
33
42
  String _backendUrl = '';
34
43
  String _sessionCookie = '';
@@ -86,6 +95,7 @@ class DesktopCompanionManager extends ChangeNotifier {
86
95
  await disconnect();
87
96
  return;
88
97
  }
98
+ _ensureConnectionWatchdog();
89
99
  await _ensureConnected();
90
100
  }
91
101
 
@@ -130,6 +140,8 @@ class DesktopCompanionManager extends ChangeNotifier {
130
140
  Future<void> disconnect() async {
131
141
  _reconnectTimer?.cancel();
132
142
  _reconnectTimer = null;
143
+ _connectionWatchdogTimer?.cancel();
144
+ _connectionWatchdogTimer = null;
133
145
  _stopStreaming();
134
146
  _connecting = false;
135
147
  _connected = false;
@@ -216,6 +228,7 @@ class DesktopCompanionManager extends ChangeNotifier {
216
228
  uri.toString(),
217
229
  headers: <String, Object>{'Cookie': _sessionCookie},
218
230
  );
231
+ socket.pingInterval = const Duration(seconds: 25);
219
232
  _socket = socket;
220
233
  socket.listen(
221
234
  _handleMessage,
@@ -286,6 +299,19 @@ class DesktopCompanionManager extends ChangeNotifier {
286
299
  }
287
300
  }
288
301
 
302
+ // Commands that interact with the remote machine's input system. While one
303
+ // of these is executing we pause frame captures so the WebSocket send buffer
304
+ // is clear for the result message, and to avoid the native bridge being busy
305
+ // with a screenshot when the click/drag/etc. needs to run.
306
+ static const _inputCommands = <String>{
307
+ 'click',
308
+ 'mouseMove',
309
+ 'drag',
310
+ 'scroll',
311
+ 'typeText',
312
+ 'pressKey',
313
+ };
314
+
289
315
  Future<void> _handleCommand(Map<String, Object?> message) async {
290
316
  final id = message['id']?.toString() ?? '';
291
317
  final command = message['command']?.toString() ?? '';
@@ -294,6 +320,10 @@ class DesktopCompanionManager extends ChangeNotifier {
294
320
  (key, value) => MapEntry(key.toString(), value),
295
321
  )
296
322
  : const <String, Object?>{};
323
+
324
+ final isInput = _inputCommands.contains(command);
325
+ if (isInput) _inputCommandInFlight = true;
326
+
297
327
  try {
298
328
  final response = await _dispatchCommand(command, payload);
299
329
  _socket?.add(
@@ -304,6 +334,18 @@ class DesktopCompanionManager extends ChangeNotifier {
304
334
  'payload': response,
305
335
  }),
306
336
  );
337
+ // Immediately capture a fresh frame after an input action so the user
338
+ // sees the result of their interaction without waiting for the next
339
+ // timer tick.
340
+ if (isInput && _streamTimer != null && _connected) {
341
+ unawaited(
342
+ _captureAndSendBinaryFrame(
343
+ _currentStreamQuality,
344
+ _streamGeneration,
345
+ forced: true,
346
+ ),
347
+ );
348
+ }
307
349
  } catch (error) {
308
350
  _socket?.add(
309
351
  jsonEncode(<String, Object?>{
@@ -313,6 +355,8 @@ class DesktopCompanionManager extends ChangeNotifier {
313
355
  'error': '$error',
314
356
  }),
315
357
  );
358
+ } finally {
359
+ if (isInput) _inputCommandInFlight = false;
316
360
  }
317
361
  }
318
362
 
@@ -348,6 +392,12 @@ class DesktopCompanionManager extends ChangeNotifier {
348
392
  button: payload['button']?.toString() ?? 'left',
349
393
  displayId: _activeDisplayId,
350
394
  );
395
+ case 'mouseMove':
396
+ return _actions.mouseMove(
397
+ x: (payload['x'] as num?)?.round() ?? 0,
398
+ y: (payload['y'] as num?)?.round() ?? 0,
399
+ displayId: _activeDisplayId,
400
+ );
351
401
  case 'drag':
352
402
  return _actions.drag(
353
403
  x1: (payload['x1'] as num?)?.round() ?? 0,
@@ -425,6 +475,8 @@ class DesktopCompanionManager extends ChangeNotifier {
425
475
  void dispose() {
426
476
  _reconnectTimer?.cancel();
427
477
  _reconnectTimer = null;
478
+ _connectionWatchdogTimer?.cancel();
479
+ _connectionWatchdogTimer = null;
428
480
  _stopStreaming();
429
481
  _connecting = false;
430
482
  _connected = false;
@@ -441,12 +493,28 @@ class DesktopCompanionManager extends ChangeNotifier {
441
493
 
442
494
  void _scheduleReconnect() {
443
495
  if (!_enabled || !_authenticated || _sessionCookie.isEmpty) return;
496
+ _ensureConnectionWatchdog();
444
497
  _reconnectTimer?.cancel();
445
498
  _reconnectTimer = Timer(const Duration(seconds: 5), () {
446
499
  unawaited(_ensureConnected());
447
500
  });
448
501
  }
449
502
 
503
+ void _ensureConnectionWatchdog() {
504
+ if (!_enabled || !_authenticated || _sessionCookie.isEmpty) return;
505
+ if (_connectionWatchdogTimer != null) return;
506
+ _connectionWatchdogTimer = Timer.periodic(const Duration(seconds: 30), (_) {
507
+ if (!_enabled || !_authenticated || _sessionCookie.isEmpty) {
508
+ _connectionWatchdogTimer?.cancel();
509
+ _connectionWatchdogTimer = null;
510
+ return;
511
+ }
512
+ if (!_connected && !_connecting) {
513
+ unawaited(_ensureConnected());
514
+ }
515
+ });
516
+ }
517
+
450
518
  Future<void> _sendEvent(String event, Map<String, Object?> payload) async {
451
519
  final socket = _socket;
452
520
  if (socket == null || !_connected) return;
@@ -472,6 +540,7 @@ class DesktopCompanionManager extends ChangeNotifier {
472
540
  }
473
541
  final interval = Duration(milliseconds: max(1, (1000 / fps).floor()));
474
542
  _frameSeq = 0;
543
+ _currentStreamQuality = quality;
475
544
  _streamTimer = Timer.periodic(interval, (_) {
476
545
  unawaited(_captureAndSendBinaryFrame(quality, generation));
477
546
  });
@@ -492,7 +561,11 @@ class DesktopCompanionManager extends ChangeNotifier {
492
561
  return <String, Object?>{'success': true};
493
562
  }
494
563
 
495
- Future<void> _captureAndSendBinaryFrame(int quality, int generation) async {
564
+ Future<void> _captureAndSendBinaryFrame(
565
+ int quality,
566
+ int generation, {
567
+ bool forced = false,
568
+ }) async {
496
569
  final socket = _socket;
497
570
  if (socket == null ||
498
571
  !_connected ||
@@ -500,6 +573,9 @@ class DesktopCompanionManager extends ChangeNotifier {
500
573
  generation != _streamGeneration) {
501
574
  return;
502
575
  }
576
+ // If an input command is actively running, skip this frame unless we were
577
+ // explicitly forced (i.e. this IS the post-input refresh capture).
578
+ if (!forced && _inputCommandInFlight) return;
503
579
  _streamCaptureInFlight = true;
504
580
  try {
505
581
  final snapshot = await _actions.captureSnapshot(
@@ -44,6 +44,19 @@ class DesktopNativeBridge {
44
44
  });
45
45
  }
46
46
 
47
+ Future<void> mouseMove({
48
+ required int x,
49
+ required int y,
50
+ String? displayId,
51
+ }) {
52
+ return _channel.invokeMethod<void>('mouseMove', <String, Object?>{
53
+ 'x': x,
54
+ 'y': y,
55
+ if (displayId != null && displayId.trim().isNotEmpty)
56
+ 'displayId': displayId.trim(),
57
+ });
58
+ }
59
+
47
60
  Future<void> drag({
48
61
  required int x1,
49
62
  required int y1,
@@ -0,0 +1,84 @@
1
+ class PasswordStrengthEvaluation {
2
+ const PasswordStrengthEvaluation({
3
+ required this.score,
4
+ required this.containsUserInfo,
5
+ required this.obviousPattern,
6
+ required this.hasMinimumLength,
7
+ });
8
+
9
+ final int score;
10
+ final bool containsUserInfo;
11
+ final bool obviousPattern;
12
+ final bool hasMinimumLength;
13
+ }
14
+
15
+ bool hasSequentialPattern(String input) {
16
+ if (input.length < 4) return false;
17
+
18
+ // check for ascending/descending sequences of length >= 4
19
+ for (int i = 0; i <= input.length - 4; i++) {
20
+ bool asc = true;
21
+ bool desc = true;
22
+ for (int j = 0; j < 3; j++) {
23
+ if (input.codeUnitAt(i + j + 1) != input.codeUnitAt(i + j) + 1) asc = false;
24
+ if (input.codeUnitAt(i + j + 1) != input.codeUnitAt(i + j) - 1) desc = false;
25
+ }
26
+ if (asc || desc) return true;
27
+ }
28
+ return false;
29
+ }
30
+
31
+ PasswordStrengthEvaluation evaluatePasswordStrength({
32
+ required String password,
33
+ String username = '',
34
+ String email = '',
35
+ }) {
36
+ final value = password.trim();
37
+ if (value.isEmpty) {
38
+ return const PasswordStrengthEvaluation(
39
+ score: 0,
40
+ containsUserInfo: false,
41
+ obviousPattern: false,
42
+ hasMinimumLength: false,
43
+ );
44
+ }
45
+
46
+ final lower = RegExp(r'[a-z]').hasMatch(value);
47
+ final upper = RegExp(r'[A-Z]').hasMatch(value);
48
+ final digits = RegExp(r'[0-9]').hasMatch(value);
49
+ final symbols = RegExp(r'[^A-Za-z0-9]').hasMatch(value);
50
+ final variety = <bool>[lower, upper, digits, symbols]
51
+ .where((item) => item)
52
+ .length;
53
+ final normalized = value.toLowerCase();
54
+ final userHints = <String>{
55
+ username.trim().toLowerCase(),
56
+ email.trim().toLowerCase(),
57
+ email.trim().toLowerCase().split('@').first,
58
+ }.where((item) => item.length >= 3);
59
+ final containsUserInfo = userHints.any(normalized.contains);
60
+ final obviousPattern =
61
+ RegExp(r'(.)\1\1').hasMatch(value) ||
62
+ normalized.contains('password') ||
63
+ normalized.contains('123456') ||
64
+ normalized.contains('qwerty') ||
65
+ normalized.contains('letmein') ||
66
+ normalized.contains('welcome') ||
67
+ normalized.contains('admin') ||
68
+ normalized.contains('neoagent') ||
69
+ hasSequentialPattern(normalized);
70
+
71
+ var score = 0;
72
+ if (value.length >= 8) score += 1;
73
+ if (value.length >= 12) score += 1;
74
+ if (variety >= 3) score += 1;
75
+ if (variety == 4 || value.length >= 16) score += 1;
76
+ if (containsUserInfo || obviousPattern) score -= 1;
77
+
78
+ return PasswordStrengthEvaluation(
79
+ score: score.clamp(0, 4),
80
+ containsUserInfo: containsUserInfo,
81
+ obviousPattern: obviousPattern,
82
+ hasMinimumLength: value.length >= 8,
83
+ );
84
+ }
@@ -1,3 +1,4 @@
1
+ import 'dart:async';
1
2
  import 'dart:typed_data';
2
3
 
3
4
  import 'package:flutter/material.dart';
@@ -9,19 +10,31 @@ class StreamRenderer extends StatefulWidget {
9
10
  required this.socket,
10
11
  required this.deviceId,
11
12
  required this.platform,
12
- required this.remoteResolution,
13
+ this.remoteResolution,
13
14
  this.onTap,
15
+ this.onSwipe,
14
16
  this.onType,
17
+ this.onHover,
18
+ this.onFirstFrame,
19
+ this.onFrameTimeout,
20
+ this.firstFrameTimeout = const Duration(seconds: 8),
15
21
  this.fit = BoxFit.contain,
22
+ this.alignment = Alignment.center,
16
23
  });
17
24
 
18
25
  final io.Socket socket;
19
26
  final String deviceId;
20
27
  final String platform;
21
- final Size remoteResolution;
28
+ final Size? remoteResolution;
22
29
  final void Function(double x, double y)? onTap;
30
+ final void Function(double x1, double y1, double x2, double y2)? onSwipe;
23
31
  final void Function(String text)? onType;
32
+ final void Function(double x, double y)? onHover;
33
+ final VoidCallback? onFirstFrame;
34
+ final VoidCallback? onFrameTimeout;
35
+ final Duration firstFrameTimeout;
24
36
  final BoxFit fit;
37
+ final Alignment alignment;
25
38
 
26
39
  @override
27
40
  State<StreamRenderer> createState() => _StreamRendererState();
@@ -29,6 +42,17 @@ class StreamRenderer extends StatefulWidget {
29
42
 
30
43
  class _StreamRendererState extends State<StreamRenderer> {
31
44
  Uint8List? _frame;
45
+ Size? _frameSize;
46
+ ImageStream? _imageStream;
47
+ ImageStreamListener? _imageListener;
48
+ Offset? _dragStart;
49
+ Offset? _dragEnd;
50
+
51
+ Timer? _hoverThrottleTimer;
52
+ Timer? _firstFrameTimer;
53
+ bool _firstFrameTimedOut = false;
54
+ Offset? _pendingHoverOffset;
55
+ DateTime _lastHoverTime = DateTime.fromMillisecondsSinceEpoch(0);
32
56
 
33
57
  @override
34
58
  void initState() {
@@ -38,6 +62,7 @@ class _StreamRendererState extends State<StreamRenderer> {
38
62
  'deviceId': widget.deviceId,
39
63
  'platform': widget.platform,
40
64
  });
65
+ _startFirstFrameTimer();
41
66
  }
42
67
 
43
68
  @override
@@ -48,6 +73,10 @@ class _StreamRendererState extends State<StreamRenderer> {
48
73
  oldWidget.platform == widget.platform) {
49
74
  return;
50
75
  }
76
+ _frameSize = null;
77
+ _frame = null;
78
+ _detachImageListener();
79
+ _firstFrameTimer?.cancel();
51
80
  oldWidget.socket.off('stream:frame', _onFrame);
52
81
  oldWidget.socket.emit('stream:unsubscribe', <String, Object?>{
53
82
  'deviceId': oldWidget.deviceId,
@@ -58,6 +87,24 @@ class _StreamRendererState extends State<StreamRenderer> {
58
87
  'deviceId': widget.deviceId,
59
88
  'platform': widget.platform,
60
89
  });
90
+ _startFirstFrameTimer();
91
+ }
92
+
93
+ void _startFirstFrameTimer() {
94
+ _firstFrameTimer?.cancel();
95
+ _firstFrameTimedOut = false;
96
+ if (widget.onFrameTimeout == null ||
97
+ widget.firstFrameTimeout <= Duration.zero ||
98
+ _frame != null) {
99
+ return;
100
+ }
101
+ _firstFrameTimer = Timer(widget.firstFrameTimeout, () {
102
+ if (!mounted || _frame != null) {
103
+ return;
104
+ }
105
+ _firstFrameTimedOut = true;
106
+ widget.onFrameTimeout?.call();
107
+ });
61
108
  }
62
109
 
63
110
  void _onFrame(dynamic data) {
@@ -84,9 +131,47 @@ class _StreamRendererState extends State<StreamRenderer> {
84
131
  _ => null,
85
132
  };
86
133
  if (frame == null || frame.isEmpty || !mounted) return;
134
+ final hadFrame = _frame != null;
135
+ if (!hadFrame) {
136
+ _firstFrameTimer?.cancel();
137
+ _firstFrameTimer = null;
138
+ if (!_firstFrameTimedOut) {
139
+ widget.onFirstFrame?.call();
140
+ }
141
+ }
142
+ if (_frameSize == null) {
143
+ _resolveFrameSize(frame);
144
+ }
87
145
  setState(() => _frame = frame);
88
146
  }
89
147
 
148
+ void _resolveFrameSize(Uint8List frame) {
149
+ _detachImageListener();
150
+ final provider = MemoryImage(frame);
151
+ final stream = provider.resolve(const ImageConfiguration());
152
+ final listener = ImageStreamListener((image, _) {
153
+ if (!mounted || _frameSize != null) return;
154
+ setState(() {
155
+ _frameSize = Size(
156
+ image.image.width.toDouble(),
157
+ image.image.height.toDouble(),
158
+ );
159
+ });
160
+ _detachImageListener();
161
+ });
162
+ _imageStream = stream;
163
+ _imageListener = listener;
164
+ stream.addListener(listener);
165
+ }
166
+
167
+ void _detachImageListener() {
168
+ if (_imageStream != null && _imageListener != null) {
169
+ _imageStream!.removeListener(_imageListener!);
170
+ }
171
+ _imageStream = null;
172
+ _imageListener = null;
173
+ }
174
+
90
175
  @override
91
176
  Widget build(BuildContext context) {
92
177
  final frame = _frame;
@@ -95,61 +180,146 @@ class _StreamRendererState extends State<StreamRenderer> {
95
180
  }
96
181
  return LayoutBuilder(
97
182
  builder: (context, constraints) {
98
- return GestureDetector(
99
- behavior: HitTestBehavior.opaque,
100
- onTapDown: widget.onTap == null
183
+ return MouseRegion(
184
+ onHover: widget.onHover == null
101
185
  ? null
102
- : (details) => _handleTap(details, constraints.biggest),
103
- child: Image.memory(
104
- frame,
105
- gaplessPlayback: true,
106
- fit: widget.fit,
107
- width: constraints.maxWidth,
108
- height: constraints.maxHeight,
186
+ : (event) =>
187
+ _handleHoverEvent(event.localPosition, constraints.biggest),
188
+ child: GestureDetector(
189
+ behavior: HitTestBehavior.opaque,
190
+ onTapDown: widget.onTap == null
191
+ ? null
192
+ : (details) => _handleTap(details, constraints.biggest),
193
+ onPanStart: widget.onSwipe == null
194
+ ? null
195
+ : (details) {
196
+ _dragStart = details.localPosition;
197
+ _dragEnd = details.localPosition;
198
+ },
199
+ onPanUpdate: widget.onSwipe == null
200
+ ? null
201
+ : (details) {
202
+ _dragEnd = details.localPosition;
203
+ },
204
+ onPanEnd: widget.onSwipe == null
205
+ ? null
206
+ : (_) {
207
+ final start = _dragStart;
208
+ final end = _dragEnd;
209
+ _dragStart = null;
210
+ _dragEnd = null;
211
+ if (start == null ||
212
+ end == null ||
213
+ (start - end).distance < 12) {
214
+ return;
215
+ }
216
+ _handleSwipe(start, end, constraints.biggest);
217
+ },
218
+ child: Image.memory(
219
+ frame,
220
+ gaplessPlayback: true,
221
+ fit: widget.fit,
222
+ alignment: widget.alignment,
223
+ width: constraints.maxWidth,
224
+ height: constraints.maxHeight,
225
+ ),
109
226
  ),
110
227
  );
111
228
  },
112
229
  );
113
230
  }
114
231
 
232
+ void _handleHoverEvent(Offset localPosition, Size boxSize) {
233
+ if (widget.onHover == null) return;
234
+ _pendingHoverOffset = localPosition;
235
+ final now = DateTime.now();
236
+ final elapsed = now.difference(_lastHoverTime);
237
+ const throttleDuration = Duration(milliseconds: 70);
238
+
239
+ if (elapsed >= throttleDuration) {
240
+ _sendPendingHover(boxSize);
241
+ } else {
242
+ _hoverThrottleTimer ??= Timer(throttleDuration - elapsed, () {
243
+ _hoverThrottleTimer = null;
244
+ _sendPendingHover(boxSize);
245
+ });
246
+ }
247
+ }
248
+
249
+ void _sendPendingHover(Size boxSize) {
250
+ final offset = _pendingHoverOffset;
251
+ if (offset == null) return;
252
+ _pendingHoverOffset = null;
253
+ _lastHoverTime = DateTime.now();
254
+
255
+ final point = _mapToRemote(offset, boxSize);
256
+ if (point != null) {
257
+ widget.onHover?.call(point.dx, point.dy);
258
+ }
259
+ }
260
+
115
261
  void _handleTap(TapDownDetails details, Size boxSize) {
116
- final remote = widget.remoteResolution;
117
- if (remote.width <= 0 || remote.height <= 0 || boxSize.width <= 0 || boxSize.height <= 0) {
262
+ final point = _mapToRemote(details.localPosition, boxSize);
263
+ if (point == null) {
118
264
  return;
119
265
  }
120
- final imageAspect = remote.width / remote.height;
121
- final boxAspect = boxSize.width / boxSize.height;
122
- double renderWidth;
123
- double renderHeight;
124
- double offsetX = 0;
125
- double offsetY = 0;
126
- if (boxAspect > imageAspect) {
127
- renderHeight = boxSize.height;
128
- renderWidth = renderHeight * imageAspect;
129
- offsetX = (boxSize.width - renderWidth) / 2;
130
- } else {
131
- renderWidth = boxSize.width;
132
- renderHeight = renderWidth / imageAspect;
133
- offsetY = (boxSize.height - renderHeight) / 2;
134
- }
135
- final localX = details.localPosition.dx - offsetX;
136
- final localY = details.localPosition.dy - offsetY;
137
- if (localX < 0 || localY < 0 || localX > renderWidth || localY > renderHeight) {
266
+ widget.onTap?.call(point.dx, point.dy);
267
+ }
268
+
269
+ void _handleSwipe(Offset start, Offset end, Size boxSize) {
270
+ final mappedStart = _mapToRemote(start, boxSize);
271
+ final mappedEnd = _mapToRemote(end, boxSize);
272
+ if (mappedStart == null || mappedEnd == null) {
138
273
  return;
139
274
  }
140
- widget.onTap?.call(
141
- localX * remote.width / renderWidth,
142
- localY * remote.height / renderHeight,
275
+ widget.onSwipe?.call(
276
+ mappedStart.dx,
277
+ mappedStart.dy,
278
+ mappedEnd.dx,
279
+ mappedEnd.dy,
280
+ );
281
+ }
282
+
283
+ Offset? _mapToRemote(Offset localPosition, Size boxSize) {
284
+ final remote = widget.remoteResolution ?? _frameSize;
285
+ if (remote == null ||
286
+ remote.width <= 0 ||
287
+ remote.height <= 0 ||
288
+ boxSize.width <= 0 ||
289
+ boxSize.height <= 0) {
290
+ return null;
291
+ }
292
+ final remoteSize = Size(remote.width, remote.height);
293
+ final fitted = applyBoxFit(widget.fit, remoteSize, boxSize);
294
+ final sourceRect = widget.alignment.inscribe(
295
+ fitted.source,
296
+ Offset.zero & remoteSize,
297
+ );
298
+ final destRect = widget.alignment.inscribe(
299
+ fitted.destination,
300
+ Offset.zero & boxSize,
301
+ );
302
+ if (!destRect.contains(localPosition)) {
303
+ return null;
304
+ }
305
+ final localX = localPosition.dx - destRect.left;
306
+ final localY = localPosition.dy - destRect.top;
307
+ return Offset(
308
+ sourceRect.left + localX * fitted.source.width / destRect.width,
309
+ sourceRect.top + localY * fitted.source.height / destRect.height,
143
310
  );
144
311
  }
145
312
 
146
313
  @override
147
314
  void dispose() {
315
+ _hoverThrottleTimer?.cancel();
316
+ _firstFrameTimer?.cancel();
148
317
  widget.socket.emit('stream:unsubscribe', <String, Object?>{
149
318
  'deviceId': widget.deviceId,
150
319
  'platform': widget.platform,
151
320
  });
152
321
  widget.socket.off('stream:frame', _onFrame);
322
+ _detachImageListener();
153
323
  super.dispose();
154
324
  }
155
325
  }