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
@@ -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
 
@@ -431,6 +475,8 @@ class DesktopCompanionManager extends ChangeNotifier {
431
475
  void dispose() {
432
476
  _reconnectTimer?.cancel();
433
477
  _reconnectTimer = null;
478
+ _connectionWatchdogTimer?.cancel();
479
+ _connectionWatchdogTimer = null;
434
480
  _stopStreaming();
435
481
  _connecting = false;
436
482
  _connected = false;
@@ -447,12 +493,28 @@ class DesktopCompanionManager extends ChangeNotifier {
447
493
 
448
494
  void _scheduleReconnect() {
449
495
  if (!_enabled || !_authenticated || _sessionCookie.isEmpty) return;
496
+ _ensureConnectionWatchdog();
450
497
  _reconnectTimer?.cancel();
451
498
  _reconnectTimer = Timer(const Duration(seconds: 5), () {
452
499
  unawaited(_ensureConnected());
453
500
  });
454
501
  }
455
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
+
456
518
  Future<void> _sendEvent(String event, Map<String, Object?> payload) async {
457
519
  final socket = _socket;
458
520
  if (socket == null || !_connected) return;
@@ -478,6 +540,7 @@ class DesktopCompanionManager extends ChangeNotifier {
478
540
  }
479
541
  final interval = Duration(milliseconds: max(1, (1000 / fps).floor()));
480
542
  _frameSeq = 0;
543
+ _currentStreamQuality = quality;
481
544
  _streamTimer = Timer.periodic(interval, (_) {
482
545
  unawaited(_captureAndSendBinaryFrame(quality, generation));
483
546
  });
@@ -498,7 +561,11 @@ class DesktopCompanionManager extends ChangeNotifier {
498
561
  return <String, Object?>{'success': true};
499
562
  }
500
563
 
501
- Future<void> _captureAndSendBinaryFrame(int quality, int generation) async {
564
+ Future<void> _captureAndSendBinaryFrame(
565
+ int quality,
566
+ int generation, {
567
+ bool forced = false,
568
+ }) async {
502
569
  final socket = _socket;
503
570
  if (socket == null ||
504
571
  !_connected ||
@@ -506,6 +573,9 @@ class DesktopCompanionManager extends ChangeNotifier {
506
573
  generation != _streamGeneration) {
507
574
  return;
508
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;
509
579
  _streamCaptureInFlight = true;
510
580
  try {
511
581
  final snapshot = await _actions.captureSnapshot(
@@ -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
+ }
@@ -15,6 +15,9 @@ class StreamRenderer extends StatefulWidget {
15
15
  this.onSwipe,
16
16
  this.onType,
17
17
  this.onHover,
18
+ this.onFirstFrame,
19
+ this.onFrameTimeout,
20
+ this.firstFrameTimeout = const Duration(seconds: 8),
18
21
  this.fit = BoxFit.contain,
19
22
  this.alignment = Alignment.center,
20
23
  });
@@ -27,6 +30,9 @@ class StreamRenderer extends StatefulWidget {
27
30
  final void Function(double x1, double y1, double x2, double y2)? onSwipe;
28
31
  final void Function(String text)? onType;
29
32
  final void Function(double x, double y)? onHover;
33
+ final VoidCallback? onFirstFrame;
34
+ final VoidCallback? onFrameTimeout;
35
+ final Duration firstFrameTimeout;
30
36
  final BoxFit fit;
31
37
  final Alignment alignment;
32
38
 
@@ -43,6 +49,8 @@ class _StreamRendererState extends State<StreamRenderer> {
43
49
  Offset? _dragEnd;
44
50
 
45
51
  Timer? _hoverThrottleTimer;
52
+ Timer? _firstFrameTimer;
53
+ bool _firstFrameTimedOut = false;
46
54
  Offset? _pendingHoverOffset;
47
55
  DateTime _lastHoverTime = DateTime.fromMillisecondsSinceEpoch(0);
48
56
 
@@ -54,6 +62,7 @@ class _StreamRendererState extends State<StreamRenderer> {
54
62
  'deviceId': widget.deviceId,
55
63
  'platform': widget.platform,
56
64
  });
65
+ _startFirstFrameTimer();
57
66
  }
58
67
 
59
68
  @override
@@ -65,7 +74,9 @@ class _StreamRendererState extends State<StreamRenderer> {
65
74
  return;
66
75
  }
67
76
  _frameSize = null;
77
+ _frame = null;
68
78
  _detachImageListener();
79
+ _firstFrameTimer?.cancel();
69
80
  oldWidget.socket.off('stream:frame', _onFrame);
70
81
  oldWidget.socket.emit('stream:unsubscribe', <String, Object?>{
71
82
  'deviceId': oldWidget.deviceId,
@@ -76,6 +87,24 @@ class _StreamRendererState extends State<StreamRenderer> {
76
87
  'deviceId': widget.deviceId,
77
88
  'platform': widget.platform,
78
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
+ });
79
108
  }
80
109
 
81
110
  void _onFrame(dynamic data) {
@@ -102,6 +131,14 @@ class _StreamRendererState extends State<StreamRenderer> {
102
131
  _ => null,
103
132
  };
104
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
+ }
105
142
  if (_frameSize == null) {
106
143
  _resolveFrameSize(frame);
107
144
  }
@@ -146,7 +183,8 @@ class _StreamRendererState extends State<StreamRenderer> {
146
183
  return MouseRegion(
147
184
  onHover: widget.onHover == null
148
185
  ? null
149
- : (event) => _handleHoverEvent(event.localPosition, constraints.biggest),
186
+ : (event) =>
187
+ _handleHoverEvent(event.localPosition, constraints.biggest),
150
188
  child: GestureDetector(
151
189
  behavior: HitTestBehavior.opaque,
152
190
  onTapDown: widget.onTap == null
@@ -197,7 +235,7 @@ class _StreamRendererState extends State<StreamRenderer> {
197
235
  final now = DateTime.now();
198
236
  final elapsed = now.difference(_lastHoverTime);
199
237
  const throttleDuration = Duration(milliseconds: 70);
200
-
238
+
201
239
  if (elapsed >= throttleDuration) {
202
240
  _sendPendingHover(boxSize);
203
241
  } else {
@@ -213,7 +251,7 @@ class _StreamRendererState extends State<StreamRenderer> {
213
251
  if (offset == null) return;
214
252
  _pendingHoverOffset = null;
215
253
  _lastHoverTime = DateTime.now();
216
-
254
+
217
255
  final point = _mapToRemote(offset, boxSize);
218
256
  if (point != null) {
219
257
  widget.onHover?.call(point.dx, point.dy);
@@ -275,6 +313,7 @@ class _StreamRendererState extends State<StreamRenderer> {
275
313
  @override
276
314
  void dispose() {
277
315
  _hoverThrottleTimer?.cancel();
316
+ _firstFrameTimer?.cancel();
278
317
  widget.socket.emit('stream:unsubscribe', <String, Object?>{
279
318
  'deviceId': widget.deviceId,
280
319
  'platform': widget.platform,
@@ -1,5 +1,19 @@
1
1
  import 'package:flutter/widgets.dart';
2
2
 
3
+ /// Resolves the active [NeoAgentPalette] for the given [brightness].
4
+ NeoAgentPalette paletteFor(Brightness brightness) =>
5
+ brightness == Brightness.light ? lightPalette : darkPalette;
6
+
7
+ /// Resolves the active [NeoAgentPalette] from the ambient theme brightness.
8
+ NeoAgentPalette paletteOf(BuildContext context) =>
9
+ paletteFor(MediaQuery.platformBrightnessOf(context));
10
+
11
+ /// Canonical color tokens for the NeoAgent "Control Surface" design system.
12
+ ///
13
+ /// Visual language: warm-organic "agent OS" — olive/sage greens paired with
14
+ /// a gold accent, a paper-light day theme and a deep-olive night theme.
15
+ /// Every screen reads its colors from these tokens (via the theme getters in
16
+ /// `main_theme.dart`), so adjusting a value here re-skins the whole app.
3
17
  class NeoAgentPalette {
4
18
  const NeoAgentPalette({
5
19
  required this.bgPrimary,
@@ -21,61 +35,89 @@ class NeoAgentPalette {
21
35
  required this.info,
22
36
  });
23
37
 
38
+ /// Page background ("paper" by day, "deep olive" by night).
24
39
  final Color bgPrimary;
40
+
41
+ /// Slightly recessed background used for rails and inset wells.
25
42
  final Color bgSecondary;
43
+
44
+ /// Tertiary surface for nested wells, track fills and pressed states.
26
45
  final Color bgTertiary;
46
+
47
+ /// Raised surface for cards, sheets and bubbles.
27
48
  final Color bgCard;
49
+
50
+ /// Primary ink — headings and body text.
28
51
  final Color textPrimary;
52
+
53
+ /// Secondary ink — supporting copy and inactive labels.
29
54
  final Color textSecondary;
55
+
56
+ /// Muted ink — captions, placeholders and disabled glyphs.
30
57
  final Color textMuted;
58
+
59
+ /// Gold accent — primary actions, active nav, focus rings.
31
60
  final Color accent;
61
+
62
+ /// Readable gold ("gold-ink") for eyebrows, links and accent text on paper.
32
63
  final Color accentHover;
64
+
65
+ /// Sage green companion accent — calls, traces and secondary affordances.
33
66
  final Color accentAlt;
67
+
68
+ /// Translucent gold wash for soft fills and selection backgrounds.
34
69
  final Color accentMuted;
70
+
71
+ /// Hairline divider color.
35
72
  final Color border;
73
+
74
+ /// Stronger hairline for inputs and interactive outlines.
36
75
  final Color borderLight;
76
+
37
77
  final Color success;
38
78
  final Color warning;
39
79
  final Color danger;
40
80
  final Color info;
41
81
  }
42
82
 
83
+ /// Deep-olive night theme.
43
84
  const NeoAgentPalette darkPalette = NeoAgentPalette(
44
- bgPrimary: Color(0xFF071015),
45
- bgSecondary: Color(0xFF0E171E),
46
- bgTertiary: Color(0xFF15232B),
47
- bgCard: Color(0xFF16212A),
48
- textPrimary: Color(0xFFF5EFE4),
49
- textSecondary: Color(0xFFD2D7DC),
50
- textMuted: Color(0xFF95A0A8),
51
- accent: Color(0xFFC7A36A),
52
- accentHover: Color(0xFFE4C58D),
53
- accentAlt: Color(0xFF5FA897),
54
- accentMuted: Color(0x24C7A36A),
55
- border: Color(0x30576875),
56
- borderLight: Color(0x4A7B8D99),
57
- success: Color(0xFF37B67E),
58
- warning: Color(0xFFD49A43),
59
- danger: Color(0xFFE26E61),
60
- info: Color(0xFF74A8D9),
85
+ bgPrimary: Color(0xFF0E1511),
86
+ bgSecondary: Color(0xFF0A0F0C),
87
+ bgTertiary: Color(0xFF252D28),
88
+ bgCard: Color(0xFF171F1A),
89
+ textPrimary: Color(0xFFECEFE5),
90
+ textSecondary: Color(0xFFAEB7A6),
91
+ textMuted: Color(0xFF7E8877),
92
+ accent: Color(0xFFE1B052),
93
+ accentHover: Color(0xFFEAC272),
94
+ accentAlt: Color(0xFF84BA87),
95
+ accentMuted: Color(0x29E1B052),
96
+ border: Color(0x1AE0F0E0),
97
+ borderLight: Color(0x2BE0F0E0),
98
+ success: Color(0xFF74C07C),
99
+ warning: Color(0xFFD9A24B),
100
+ danger: Color(0xFFDE8A78),
101
+ info: Color(0xFF6FB0A4),
61
102
  );
62
103
 
104
+ /// Paper-light day theme.
63
105
  const NeoAgentPalette lightPalette = NeoAgentPalette(
64
- bgPrimary: Color(0xFFF3F0EB),
65
- bgSecondary: Color(0xFFEAE4DA),
66
- bgTertiary: Color(0xFFDED6CA),
67
- bgCard: Color(0xFFFFFEFC),
68
- textPrimary: Color(0xFF16181D),
69
- textSecondary: Color(0xFF444A55),
70
- textMuted: Color(0xFF727987),
71
- accent: Color(0xFF8F6D3E),
72
- accentHover: Color(0xFFAF8750),
73
- accentAlt: Color(0xFF2F7D6E),
74
- accentMuted: Color(0x1F8F6D3E),
75
- border: Color(0x223F4652),
76
- borderLight: Color(0x3847505D),
77
- success: Color(0xFF1F8C58),
78
- warning: Color(0xFFB87322),
79
- danger: Color(0xFFD25B4D),
80
- info: Color(0xFF2F7AA8),
106
+ bgPrimary: Color(0xFFF4F1E8),
107
+ bgSecondary: Color(0xFFEDE9DC),
108
+ bgTertiary: Color(0xFFF1EDE1),
109
+ bgCard: Color(0xFFFDFCF8),
110
+ textPrimary: Color(0xFF1C2117),
111
+ textSecondary: Color(0xFF49503F),
112
+ textMuted: Color(0xFF7E8470),
113
+ accent: Color(0xFFB07D2B),
114
+ accentHover: Color(0xFF8A5F1C),
115
+ accentAlt: Color(0xFF5E6B4C),
116
+ accentMuted: Color(0x24B07D2B),
117
+ border: Color(0x171C2117),
118
+ borderLight: Color(0x291C2117),
119
+ success: Color(0xFF527C4F),
120
+ warning: Color(0xFF9A7B33),
121
+ danger: Color(0xFFAE473C),
122
+ info: Color(0xFF2F7D6E),
81
123
  );
@@ -418,10 +418,10 @@ packages:
418
418
  dependency: "direct main"
419
419
  description:
420
420
  name: google_fonts
421
- sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
421
+ sha256: "4e9391085e524954a51e3625b7c9c7e9851dc3f376603208bb45c24b9a66255d"
422
422
  url: "https://pub.dev"
423
423
  source: hosted
424
- version: "6.3.3"
424
+ version: "8.1.0"
425
425
  hotkey_manager:
426
426
  dependency: "direct main"
427
427
  description:
@@ -12,7 +12,7 @@ dependencies:
12
12
  cupertino_icons: ^1.0.8
13
13
  flutter_markdown: ^0.7.4+1
14
14
  file_picker: ^8.0.7
15
- google_fonts: ^6.3.1
15
+ google_fonts: ^8.1.0
16
16
  image: ^4.5.4
17
17
  http: ^1.5.0
18
18
  mixpanel_flutter: ^2.6.1
@@ -55,5 +55,11 @@ flutter:
55
55
  assets:
56
56
  - web/icons/Icon-192.png
57
57
  - assets/branding/app_icon_256.png
58
+ - assets/branding/app_icon_512.png
59
+ - assets/branding/app_icon_1024.png
60
+ - assets/branding/app_icon_light_256.png
61
+ - assets/branding/app_icon_light_512.png
62
+ - assets/branding/app_icon_light_1024.png
58
63
  - assets/branding/tray_icon_template.png
64
+ - assets/branding/tray_icon_light_template.png
59
65
  - assets/branding/onboarding_intro.mp4
@@ -0,0 +1,43 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 100 100">
2
+ <defs>
3
+ <linearGradient id="tile" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" stop-color="#705331"></stop>
5
+ <stop offset="100%" stop-color="#22564c"></stop>
6
+ </linearGradient>
7
+ <radialGradient id="shine" cx="26%" cy="14%" r="72%">
8
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.22"></stop>
9
+ <stop offset="100%" stop-color="#ffffff" stop-opacity="0"></stop>
10
+ </radialGradient>
11
+ <radialGradient id="core" cx="38%" cy="32%" r="75%">
12
+ <stop offset="0%" stop-color="#fffdf6"></stop>
13
+ <stop offset="48%" stop-color="#eee3cc"></stop>
14
+ <stop offset="100%" stop-color="#b5a888"></stop>
15
+ </radialGradient>
16
+ <radialGradient id="node" cx="36%" cy="30%" r="80%">
17
+ <stop offset="0%" stop-color="#f8dca8"></stop>
18
+ <stop offset="52%" stop-color="#d3a85f"></stop>
19
+ <stop offset="100%" stop-color="#9b6f2f"></stop>
20
+ </radialGradient>
21
+ <linearGradient id="tube" x1="0%" y1="0%" x2="0%" y2="100%">
22
+ <stop offset="0%" stop-color="#fffaf0"></stop>
23
+ <stop offset="52%" stop-color="#efe7d6"></stop>
24
+ <stop offset="100%" stop-color="#cabf9f"></stop>
25
+ </linearGradient>
26
+ <filter id="sh" x="-30%" y="-30%" width="160%" height="160%">
27
+ <feDropShadow dx="0" dy="1.1" stdDeviation="1.1" flood-color="#000000" flood-opacity="0.45"></feDropShadow>
28
+ </filter>
29
+ <clipPath id="tc"><rect x="0" y="0" width="100" height="100" rx="22.5"></rect></clipPath>
30
+ </defs>
31
+ <g clip-path="url(#tc)">
32
+ <rect x="0" y="0" width="100" height="100" fill="url(#tile)"></rect>
33
+ <rect x="0" y="0" width="100" height="100" fill="url(#shine)"></rect>
34
+ <g fill="none" stroke="url(#tube)" stroke-linecap="round" filter="url(#sh)">
35
+ <circle cx="50" cy="50" r="17" stroke-width="5" stroke-dasharray="78 29" transform="rotate(-35 50 50)" stroke-opacity="0.92"></circle>
36
+ <circle cx="50" cy="50" r="30" stroke-width="5" stroke-dasharray="150 39" transform="rotate(70 50 50)" stroke-opacity="0.8"></circle>
37
+ </g>
38
+ <circle cx="50" cy="50" r="7" fill="url(#core)"></circle>
39
+ <circle cx="50" cy="20" r="5.6" fill="url(#node)"></circle>
40
+ <circle cx="48" cy="18.4" r="1.7" fill="#fff7e6" fill-opacity="0.8"></circle>
41
+ </g>
42
+ <rect x="0.6" y="0.6" width="98.8" height="98.8" rx="22" fill="none" stroke="rgba(255,255,255,0.16)" stroke-width="1.2"></rect>
43
+ </svg>