neoagent 2.3.1-beta.4 → 2.3.1-beta.41

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 (290) hide show
  1. package/.env.example +45 -0
  2. package/docs/capabilities.md +2 -2
  3. package/docs/configuration.md +12 -5
  4. package/docs/hardware.md +1 -1
  5. package/docs/integrations.md +2 -3
  6. package/flutter_app/.metadata +42 -0
  7. package/flutter_app/README.md +21 -0
  8. package/flutter_app/analysis_options.yaml +32 -0
  9. package/flutter_app/android/app/build.gradle.kts +109 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  30. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  31. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  32. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  33. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  35. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  36. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  37. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  38. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  39. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  43. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  44. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  45. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  46. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  48. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  49. package/flutter_app/android/build.gradle.kts +24 -0
  50. package/flutter_app/android/ci-release.keystore +0 -0
  51. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  52. package/flutter_app/android/gradle.properties +3 -0
  53. package/flutter_app/android/key.properties +4 -0
  54. package/flutter_app/android/settings.gradle.kts +26 -0
  55. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  56. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  57. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  58. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  59. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  60. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  62. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  63. package/flutter_app/lib/features/location/location_service.dart +119 -0
  64. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  65. package/flutter_app/lib/main.dart +99 -0
  66. package/flutter_app/lib/main_account_settings.dart +1250 -0
  67. package/flutter_app/lib/main_admin.dart +886 -0
  68. package/flutter_app/lib/main_app_shell.dart +1682 -0
  69. package/flutter_app/lib/main_chat.dart +3352 -0
  70. package/flutter_app/lib/main_controller.dart +6781 -0
  71. package/flutter_app/lib/main_devices.dart +2301 -0
  72. package/flutter_app/lib/main_integrations.dart +1129 -0
  73. package/flutter_app/lib/main_launcher.dart +959 -0
  74. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  75. package/flutter_app/lib/main_models.dart +3546 -0
  76. package/flutter_app/lib/main_navigation.dart +193 -0
  77. package/flutter_app/lib/main_operations.dart +4851 -0
  78. package/flutter_app/lib/main_recordings.dart +870 -0
  79. package/flutter_app/lib/main_runtime.dart +806 -0
  80. package/flutter_app/lib/main_settings.dart +2024 -0
  81. package/flutter_app/lib/main_shared.dart +2861 -0
  82. package/flutter_app/lib/main_theme.dart +204 -0
  83. package/flutter_app/lib/main_voice_assistant.dart +957 -0
  84. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  85. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  86. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  87. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  88. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  89. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  90. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  91. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  92. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  93. package/flutter_app/lib/src/backend_client.dart +1833 -0
  94. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  95. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  96. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  97. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  98. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  99. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  100. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  101. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  102. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  103. package/flutter_app/lib/src/health_bridge.dart +136 -0
  104. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  105. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  106. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  107. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  108. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  109. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  110. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  111. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  112. package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
  113. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  114. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  115. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  116. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  117. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  118. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  119. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  120. package/flutter_app/lib/src/theme/palette.dart +81 -0
  121. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  122. package/flutter_app/linux/CMakeLists.txt +128 -0
  123. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  124. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
  125. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  126. package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
  127. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  128. package/flutter_app/linux/runner/main.cc +6 -0
  129. package/flutter_app/linux/runner/my_application.cc +144 -0
  130. package/flutter_app/linux/runner/my_application.h +18 -0
  131. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  132. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  133. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  134. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
  135. package/flutter_app/macos/Podfile +42 -0
  136. package/flutter_app/macos/Podfile.lock +87 -0
  137. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  138. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  139. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  140. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  141. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  142. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  143. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  144. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  145. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  146. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  147. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  148. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  149. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  150. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  151. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  152. package/flutter_app/macos/Runner/Info.plist +36 -0
  153. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  154. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  155. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  156. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  157. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  158. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  159. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  160. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  161. package/flutter_app/patch_strings.py +12 -0
  162. package/flutter_app/pubspec.lock +1088 -0
  163. package/flutter_app/pubspec.yaml +53 -0
  164. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  165. package/flutter_app/test/recording_payloads_test.dart +53 -0
  166. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  167. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  168. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  169. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  170. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  171. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  172. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  173. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  174. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  175. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  176. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  177. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  178. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  179. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  180. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  181. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  182. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  183. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  184. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  185. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  186. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  187. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  188. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  189. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  190. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  191. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  192. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  193. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  194. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  195. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  196. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  197. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  198. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  199. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  200. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  201. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  202. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  203. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  204. package/flutter_app/web/favicon.png +0 -0
  205. package/flutter_app/web/favicon.svg +12 -0
  206. package/flutter_app/web/icons/Icon-192.png +0 -0
  207. package/flutter_app/web/icons/Icon-512.png +0 -0
  208. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  209. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  210. package/flutter_app/web/index.html +39 -0
  211. package/flutter_app/web/manifest.json +35 -0
  212. package/flutter_app/windows/CMakeLists.txt +108 -0
  213. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  214. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
  215. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  216. package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
  217. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  218. package/flutter_app/windows/runner/Runner.rc +121 -0
  219. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  220. package/flutter_app/windows/runner/flutter_window.h +37 -0
  221. package/flutter_app/windows/runner/main.cpp +53 -0
  222. package/flutter_app/windows/runner/resource.h +16 -0
  223. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  224. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  225. package/flutter_app/windows/runner/utils.cpp +65 -0
  226. package/flutter_app/windows/runner/utils.h +19 -0
  227. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  228. package/flutter_app/windows/runner/win32_window.h +102 -0
  229. package/lib/install_helpers.js +31 -0
  230. package/lib/manager.js +227 -6
  231. package/package.json +3 -1
  232. package/server/db/database.js +110 -0
  233. package/server/http/middleware.js +55 -2
  234. package/server/http/routes.js +1 -0
  235. package/server/index.js +3 -0
  236. package/server/public/.last_build_id +1 -1
  237. package/server/public/assets/NOTICES +1 -1
  238. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  239. package/server/public/canvaskit/wimp.wasm +0 -0
  240. package/server/public/flutter_bootstrap.js +2 -2
  241. package/server/public/main.dart.js +74324 -73132
  242. package/server/routes/integrations.js +108 -1
  243. package/server/routes/memory.js +11 -2
  244. package/server/{http/routes → routes}/screenHistory.js +2 -2
  245. package/server/routes/settings.js +75 -2
  246. package/server/{http/routes → routes}/triggers.js +2 -2
  247. package/server/routes/wearable.js +67 -0
  248. package/server/services/ai/models.js +30 -0
  249. package/server/services/ai/providers/githubCopilot.js +97 -0
  250. package/server/services/ai/providers/openai.js +2 -1
  251. package/server/services/ai/providers/openaiCodex.js +31 -0
  252. package/server/services/ai/settings.js +20 -0
  253. package/server/services/ai/toolSelector.js +14 -1
  254. package/server/services/ai/tools.js +77 -4
  255. package/server/services/desktop/screenRecorder.js +65 -9
  256. package/server/services/integrations/env.js +5 -0
  257. package/server/services/integrations/figma/provider.js +1 -0
  258. package/server/services/integrations/github/common.js +106 -0
  259. package/server/services/integrations/github/provider.js +499 -0
  260. package/server/services/integrations/github/repos.js +1124 -0
  261. package/server/services/integrations/google/provider.js +1 -0
  262. package/server/services/integrations/home_assistant/provider.js +325 -26
  263. package/server/services/integrations/manager.js +88 -12
  264. package/server/services/integrations/microsoft/provider.js +1 -0
  265. package/server/services/integrations/oauth_provider.js +25 -8
  266. package/server/services/integrations/provider_config_store.js +85 -0
  267. package/server/services/integrations/registry.js +4 -0
  268. package/server/services/integrations/spotify/provider.js +1 -0
  269. package/server/services/integrations/trello/provider.js +842 -0
  270. package/server/services/manager.js +46 -1
  271. package/server/services/mcp/client.js +120 -23
  272. package/server/services/memory/manager.js +39 -2
  273. package/server/services/messaging/access_policy.js +10 -0
  274. package/server/services/messaging/manager.js +49 -0
  275. package/server/services/messaging/meshtastic.js +260 -0
  276. package/server/services/messaging/meshtastic_env.js +100 -0
  277. package/server/services/messaging/meshtastic_protocol.js +476 -0
  278. package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
  279. package/server/services/tasks/runtime.js +1 -1
  280. package/server/services/voice/openaiClient.js +4 -1
  281. package/server/services/voice/openaiSpeech.js +6 -1
  282. package/server/services/voice/providers.js +52 -12
  283. package/server/services/voice/runtimeManager.js +136 -19
  284. package/server/services/voice/turnRunner.js +29 -9
  285. package/server/services/wearable/firmware_manifest.js +370 -0
  286. package/server/services/wearable/gateway.js +350 -0
  287. package/server/services/wearable/protocol.js +45 -0
  288. package/server/services/wearable/service.js +244 -0
  289. package/server/utils/local_secrets.js +56 -0
  290. package/server/utils/logger.js +37 -9
@@ -0,0 +1,806 @@
1
+ part of 'main.dart';
2
+
3
+ class NeoAgentApp extends StatefulWidget {
4
+ const NeoAgentApp({super.key, this.mode = NeoAgentAppMode.standard});
5
+
6
+ final NeoAgentAppMode mode;
7
+
8
+ @override
9
+ State<NeoAgentApp> createState() => _NeoAgentAppState();
10
+ }
11
+
12
+ class _NeoAgentAppState extends State<NeoAgentApp>
13
+ with WindowListener, TrayListener {
14
+ late final NeoAgentController _controller;
15
+ final AppLaunchBridge _appLaunchBridge = AppLaunchBridge();
16
+ StreamSubscription<String>? _appLaunchSubscription;
17
+ StreamSubscription<String>? _widgetOpenSubscription;
18
+ GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
19
+ String? _navigatorScopeSignature;
20
+ Menu? _trayMenu;
21
+ HotKey? _assistantHotKey;
22
+ Timer? _assistantHotKeyHoldTimer;
23
+ bool _desktopShellInitialized = false;
24
+ bool _handlingDesktopClose = false;
25
+ bool _desktopToolbarWindowMode = false;
26
+ bool _desktopAssistantPopupWindowMode = false;
27
+ bool _syncingDesktopPresentation = false;
28
+ bool _assistantHotKeyPressed = false;
29
+ bool _assistantHotKeyHandledAsHold = false;
30
+ bool _assistantPttActive = false;
31
+ bool _desktopAssistantBlockedHintVisible = false;
32
+ bool _desktopAssistantReturnToHidden = false;
33
+ bool _desktopToolbarReturnToHidden = false;
34
+ Rect? _desktopNormalWindowBounds;
35
+
36
+ static const Size _desktopToolbarWindowSize = Size(840, 128);
37
+ static const Size _desktopAssistantPopupWindowSize = Size(460, 112);
38
+ static const Duration _desktopAssistantHoldThreshold = Duration(
39
+ milliseconds: 220,
40
+ );
41
+ static const Duration _desktopAssistantBlockedHintDuration = Duration(
42
+ milliseconds: 1400,
43
+ );
44
+
45
+ @override
46
+ void initState() {
47
+ super.initState();
48
+ final backendClient = BackendClient();
49
+ _controller = NeoAgentController(
50
+ appMode: widget.mode,
51
+ backendClient: backendClient,
52
+ healthBridge: HealthBridge(),
53
+ widgetBridge: WidgetBridge(),
54
+ recordingBridge: createRecordingBridge(),
55
+ )..bootstrap();
56
+ _controller.addListener(_handleControllerChanged);
57
+ _appLaunchSubscription = _appLaunchBridge.launchRequests.listen(
58
+ _handleAppLaunchRequest,
59
+ );
60
+ _widgetOpenSubscription = _controller.widgetOpenRequests.listen(
61
+ _controller.openWidgetSurface,
62
+ );
63
+ if (_supportsDesktopShell) {
64
+ unawaited(_initializeDesktopShell());
65
+ }
66
+ }
67
+
68
+ @override
69
+ void dispose() {
70
+ _appLaunchSubscription?.cancel();
71
+ _widgetOpenSubscription?.cancel();
72
+ _controller.removeListener(_handleControllerChanged);
73
+ if (_supportsDesktopShell) {
74
+ trayManager.removeListener(this);
75
+ windowManager.removeListener(this);
76
+ _assistantHotKeyHoldTimer?.cancel();
77
+ if (_assistantHotKey != null) {
78
+ unawaited(hotKeyManager.unregister(_assistantHotKey!));
79
+ }
80
+ unawaited(trayManager.destroy());
81
+ }
82
+ _controller.dispose();
83
+ super.dispose();
84
+ }
85
+
86
+ void _handleControllerChanged() {
87
+ if (!_supportsDesktopShell) {
88
+ return;
89
+ }
90
+ unawaited(_syncDesktopShell());
91
+ }
92
+
93
+ void _handleAppLaunchRequest(String action) {
94
+ if (action == AppLaunchBridge.voiceAssistantAction) {
95
+ _controller.openVoiceAssistantSurface();
96
+ }
97
+ }
98
+
99
+ Future<void> _initializeDesktopShell() async {
100
+ if (_desktopShellInitialized) {
101
+ return;
102
+ }
103
+ var windowListenerAdded = false;
104
+ var trayListenerAdded = false;
105
+ try {
106
+ windowManager.addListener(this);
107
+ windowListenerAdded = true;
108
+ trayManager.addListener(this);
109
+ trayListenerAdded = true;
110
+ await windowManager.setPreventClose(true);
111
+ await windowManager.setTitle('NeoAgent');
112
+ if (defaultTargetPlatform == TargetPlatform.windows) {
113
+ await windowManager.setIcon(_desktopWindowIconAsset);
114
+ }
115
+ await trayManager.setIcon(
116
+ _desktopTrayIconAsset,
117
+ isTemplate: defaultTargetPlatform == TargetPlatform.macOS,
118
+ );
119
+ await trayManager.setToolTip('NeoAgent');
120
+ await _syncTrayMenu();
121
+ await _syncAssistantHotkey();
122
+ _desktopShellInitialized = true;
123
+ } catch (error, stackTrace) {
124
+ _desktopShellInitialized = false;
125
+ if (trayListenerAdded) {
126
+ trayManager.removeListener(this);
127
+ }
128
+ if (windowListenerAdded) {
129
+ windowManager.removeListener(this);
130
+ }
131
+ AppDiagnostics.log(
132
+ 'desktop.shell',
133
+ 'initialize.failed',
134
+ error: error,
135
+ stackTrace: stackTrace,
136
+ );
137
+ }
138
+ }
139
+
140
+ Future<void> _syncDesktopShell() async {
141
+ if (!_desktopShellInitialized) {
142
+ return;
143
+ }
144
+ await _syncTrayMenu();
145
+ await _syncAssistantHotkey();
146
+ await _syncDesktopPresentation();
147
+ }
148
+
149
+ Future<void> _syncDesktopPresentation() async {
150
+ if (_syncingDesktopPresentation) {
151
+ return;
152
+ }
153
+ _syncingDesktopPresentation = true;
154
+ try {
155
+ final runtime = _controller.recordingRuntime;
156
+ if (_desktopAssistantPopupWindowMode) {
157
+ return;
158
+ }
159
+ final isWindowVisible = await windowManager.isVisible();
160
+ if (_desktopToolbarWindowMode &&
161
+ (!runtime.active || !runtime.floatingToolbarVisible)) {
162
+ await _restoreMainWindowPresentation(
163
+ hideAfterRestore: _desktopToolbarReturnToHidden,
164
+ focusWindow: false,
165
+ );
166
+ _desktopToolbarReturnToHidden = false;
167
+ return;
168
+ }
169
+ if (!_desktopToolbarWindowMode &&
170
+ runtime.active &&
171
+ runtime.supportsFloatingToolbar &&
172
+ runtime.floatingToolbarVisible &&
173
+ (_controller.desktopFloatingToolbarPopupRequested ||
174
+ !isWindowVisible)) {
175
+ await _showDetachedToolbarWindow(
176
+ focusWindow:
177
+ _controller.desktopFloatingToolbarPopupRequested ||
178
+ isWindowVisible,
179
+ );
180
+ _controller.acknowledgeDesktopFloatingToolbarPopupRequest();
181
+ }
182
+ } finally {
183
+ _syncingDesktopPresentation = false;
184
+ }
185
+ }
186
+
187
+ Future<void> _syncTrayMenu() async {
188
+ final runtime = _controller.recordingRuntime;
189
+ final isRecordingActive = runtime.active;
190
+ final pauseLabel = runtime.paused ? 'Resume' : 'Pause';
191
+ final toolbarLabel = runtime.floatingToolbarVisible
192
+ ? 'Hide floating bar'
193
+ : 'Show floating bar';
194
+ _trayMenu = Menu(
195
+ items: <MenuItem>[
196
+ MenuItem(key: 'open', label: 'Open'),
197
+ MenuItem(
198
+ key: 'start_recording',
199
+ label: 'Start recording',
200
+ disabled: isRecordingActive || !_controller.canStartDesktopRecording,
201
+ ),
202
+ MenuItem(
203
+ key: 'pause_resume_recording',
204
+ label: pauseLabel,
205
+ disabled: !isRecordingActive,
206
+ ),
207
+ MenuItem(
208
+ key: 'stop_recording',
209
+ label: 'Stop',
210
+ disabled: !isRecordingActive,
211
+ ),
212
+ MenuItem.separator(),
213
+ MenuItem(
214
+ key: 'toggle_toolbar',
215
+ label: toolbarLabel,
216
+ disabled:
217
+ !_controller.recordingRuntime.supportsFloatingToolbar ||
218
+ !isRecordingActive,
219
+ ),
220
+ MenuItem(key: 'open_voice_assistant', label: 'Open voice assistant'),
221
+ MenuItem.separator(),
222
+ MenuItem(key: 'quit', label: 'Quit'),
223
+ ],
224
+ );
225
+ await trayManager.setContextMenu(_trayMenu!);
226
+ }
227
+
228
+ Future<void> _syncAssistantHotkey() async {
229
+ final shouldRegister =
230
+ _controller.desktopAssistantHotkeyEnabled &&
231
+ _controller.recordingRuntime.supportsGlobalHotkeys;
232
+ if (!shouldRegister) {
233
+ if (_assistantHotKey != null) {
234
+ await hotKeyManager.unregister(_assistantHotKey!);
235
+ _assistantHotKey = null;
236
+ }
237
+ return;
238
+ }
239
+
240
+ final hotKey = HotKey(
241
+ key: LogicalKeyboardKey.space,
242
+ modifiers: const <HotKeyModifier>[
243
+ HotKeyModifier.control,
244
+ HotKeyModifier.shift,
245
+ ],
246
+ scope: HotKeyScope.system,
247
+ );
248
+ if (_assistantHotKey != null && _hotKeysMatch(_assistantHotKey!, hotKey)) {
249
+ return;
250
+ }
251
+ if (_assistantHotKey != null) {
252
+ await hotKeyManager.unregister(_assistantHotKey!);
253
+ }
254
+ await hotKeyManager.register(
255
+ hotKey,
256
+ keyDownHandler: _handleAssistantHotKeyDown,
257
+ keyUpHandler: _handleAssistantHotKeyUp,
258
+ );
259
+ _assistantHotKey = hotKey;
260
+ }
261
+
262
+ Future<void> _handleAssistantHotKeyDown(HotKey hotKey) async {
263
+ if (_assistantHotKeyPressed) {
264
+ return;
265
+ }
266
+ _assistantHotKeyPressed = true;
267
+ _assistantHotKeyHandledAsHold = false;
268
+ _assistantPttActive = false;
269
+ _desktopAssistantBlockedHintVisible = false;
270
+ _assistantHotKeyHoldTimer?.cancel();
271
+ _assistantHotKeyHoldTimer = Timer(
272
+ _desktopAssistantHoldThreshold,
273
+ () => unawaited(_activateAssistantPushToTalkMode()),
274
+ );
275
+ }
276
+
277
+ Future<void> _handleAssistantHotKeyUp(HotKey hotKey) async {
278
+ _assistantHotKeyPressed = false;
279
+ _assistantHotKeyHoldTimer?.cancel();
280
+ if (_assistantHotKeyHandledAsHold) {
281
+ _assistantHotKeyHandledAsHold = false;
282
+ _desktopAssistantBlockedHintVisible = false;
283
+ if (_assistantPttActive ||
284
+ _controller.isLiveVoiceCaptureStarting ||
285
+ _controller.isLiveVoiceCaptureActive) {
286
+ _assistantPttActive = false;
287
+ try {
288
+ await _controller.stopLiveVoiceCapture();
289
+ } catch (_) {}
290
+ }
291
+ await _hideAssistantPopupWindow();
292
+ return;
293
+ }
294
+ if (_desktopAssistantPopupWindowMode) {
295
+ await _hideAssistantPopupWindow();
296
+ return;
297
+ }
298
+ await _showAssistantPopupWindow();
299
+ }
300
+
301
+ Future<void> _activateAssistantPushToTalkMode() async {
302
+ if (!_assistantHotKeyPressed) {
303
+ return;
304
+ }
305
+ _desktopAssistantBlockedHintVisible = false;
306
+ _assistantHotKeyHandledAsHold = true;
307
+ if (_controller.recordingRuntime.active) {
308
+ await _showAssistantBlockedHint();
309
+ return;
310
+ }
311
+ try {
312
+ await _showAssistantPopupWindow();
313
+ await _controller.startLiveVoiceCapture();
314
+ _assistantPttActive = true;
315
+ } catch (error, stackTrace) {
316
+ _assistantPttActive = false;
317
+ AppDiagnostics.log(
318
+ 'desktop.assistant',
319
+ 'ptt.start_failed',
320
+ error: error,
321
+ stackTrace: stackTrace,
322
+ );
323
+ await _hideAssistantPopupWindow();
324
+ }
325
+ }
326
+
327
+ Future<void> _showAssistantBlockedHint() async {
328
+ _desktopAssistantBlockedHintVisible = true;
329
+ await _showAssistantPopupWindow();
330
+ if (mounted) {
331
+ setState(() {});
332
+ }
333
+ await Future<void>.delayed(_desktopAssistantBlockedHintDuration);
334
+ if (_desktopAssistantBlockedHintVisible) {
335
+ _desktopAssistantBlockedHintVisible = false;
336
+ await _hideAssistantPopupWindow();
337
+ }
338
+ }
339
+
340
+ bool _hotKeysMatch(HotKey first, HotKey second) {
341
+ final firstModifiers = Set<HotKeyModifier>.from(
342
+ first.modifiers ?? const <HotKeyModifier>[],
343
+ );
344
+ final secondModifiers = Set<HotKeyModifier>.from(
345
+ second.modifiers ?? const <HotKeyModifier>[],
346
+ );
347
+ return first.scope == second.scope &&
348
+ first.key == second.key &&
349
+ firstModifiers.length == secondModifiers.length &&
350
+ firstModifiers.containsAll(secondModifiers);
351
+ }
352
+
353
+ Future<void> _openMainWindow() async {
354
+ if (_desktopToolbarWindowMode || _desktopAssistantPopupWindowMode) {
355
+ await _restoreMainWindowPresentation();
356
+ }
357
+ _controller.acknowledgeDesktopFloatingToolbarPopupRequest();
358
+ _desktopToolbarReturnToHidden = false;
359
+ await windowManager.show();
360
+ await windowManager.focus();
361
+ }
362
+
363
+ Future<void> _hideMainWindow() async {
364
+ final runtime = _controller.recordingRuntime;
365
+ if (_desktopAssistantPopupWindowMode) {
366
+ await _restoreMainWindowPresentation(
367
+ hideAfterRestore: true,
368
+ focusWindow: false,
369
+ );
370
+ return;
371
+ }
372
+ if (runtime.active &&
373
+ runtime.supportsFloatingToolbar &&
374
+ runtime.floatingToolbarVisible) {
375
+ await _showDetachedToolbarWindow(focusWindow: false);
376
+ return;
377
+ }
378
+ if (_desktopToolbarWindowMode) {
379
+ await _restoreMainWindowPresentation(
380
+ hideAfterRestore: true,
381
+ focusWindow: false,
382
+ );
383
+ return;
384
+ }
385
+ await windowManager.hide();
386
+ }
387
+
388
+ Future<void> _showDetachedToolbarWindow({required bool focusWindow}) async {
389
+ final runtime = _controller.recordingRuntime;
390
+ if (!runtime.active || !runtime.supportsFloatingToolbar) {
391
+ return;
392
+ }
393
+ final isVisible = await windowManager.isVisible();
394
+ _desktopToolbarReturnToHidden =
395
+ !isVisible &&
396
+ !_desktopToolbarWindowMode &&
397
+ !_desktopAssistantPopupWindowMode;
398
+ if (!_desktopToolbarWindowMode && !_desktopAssistantPopupWindowMode) {
399
+ _desktopNormalWindowBounds = await windowManager.getBounds();
400
+ }
401
+ if (mounted &&
402
+ (!_desktopToolbarWindowMode || _desktopAssistantPopupWindowMode)) {
403
+ setState(() {
404
+ _desktopToolbarWindowMode = true;
405
+ _desktopAssistantPopupWindowMode = false;
406
+ });
407
+ }
408
+ await windowManager.setTitle('NeoAgent');
409
+ await windowManager.setBackgroundColor(Colors.transparent);
410
+ await windowManager.setTitleBarStyle(
411
+ TitleBarStyle.hidden,
412
+ windowButtonVisibility: false,
413
+ );
414
+ if (!kIsWeb && defaultTargetPlatform == TargetPlatform.macOS) {
415
+ await windowManager.setHasShadow(false);
416
+ }
417
+ await windowManager.setResizable(false);
418
+ await windowManager.setAlwaysOnTop(true);
419
+ await windowManager.setSkipTaskbar(true);
420
+ await windowManager.setSize(_desktopToolbarWindowSize);
421
+ await windowManager.center();
422
+ await windowManager.show(inactive: !focusWindow);
423
+ if (focusWindow) {
424
+ await windowManager.focus();
425
+ }
426
+ }
427
+
428
+ Future<void> _restoreMainWindowPresentation({
429
+ bool hideAfterRestore = false,
430
+ bool focusWindow = true,
431
+ }) async {
432
+ if (mounted &&
433
+ (_desktopToolbarWindowMode || _desktopAssistantPopupWindowMode)) {
434
+ setState(() {
435
+ _desktopToolbarWindowMode = false;
436
+ _desktopAssistantPopupWindowMode = false;
437
+ });
438
+ }
439
+ await windowManager.setAlwaysOnTop(false);
440
+ await windowManager.setResizable(true);
441
+ await windowManager.setTitleBarStyle(TitleBarStyle.normal);
442
+ if (!kIsWeb && defaultTargetPlatform == TargetPlatform.macOS) {
443
+ await windowManager.setHasShadow(true);
444
+ }
445
+ await windowManager.setSkipTaskbar(false);
446
+ await windowManager.setTitle('NeoAgent');
447
+ final restoreBounds = _desktopNormalWindowBounds;
448
+ if (restoreBounds != null) {
449
+ await windowManager.setBounds(restoreBounds);
450
+ } else {
451
+ await windowManager.setSize(const Size(1280, 720));
452
+ await windowManager.center();
453
+ }
454
+ if (hideAfterRestore) {
455
+ await windowManager.hide();
456
+ return;
457
+ }
458
+ await windowManager.show();
459
+ if (focusWindow) {
460
+ await windowManager.focus();
461
+ }
462
+ }
463
+
464
+ Future<void> _showAssistantPopupWindow() async {
465
+ final isVisible = await windowManager.isVisible();
466
+ _desktopAssistantReturnToHidden = !isVisible;
467
+ if (!_desktopToolbarWindowMode && !_desktopAssistantPopupWindowMode) {
468
+ _desktopNormalWindowBounds = await windowManager.getBounds();
469
+ }
470
+ if (mounted &&
471
+ (!_desktopAssistantPopupWindowMode || _desktopToolbarWindowMode)) {
472
+ setState(() {
473
+ _desktopToolbarWindowMode = false;
474
+ _desktopAssistantPopupWindowMode = true;
475
+ });
476
+ }
477
+ await windowManager.setTitle('NeoAgent Assistant');
478
+ await windowManager.setBackgroundColor(Colors.transparent);
479
+ await windowManager.setTitleBarStyle(
480
+ TitleBarStyle.hidden,
481
+ windowButtonVisibility: false,
482
+ );
483
+ if (!kIsWeb && defaultTargetPlatform == TargetPlatform.macOS) {
484
+ await windowManager.setHasShadow(false);
485
+ }
486
+ await windowManager.setResizable(false);
487
+ await windowManager.setAlwaysOnTop(true);
488
+ await windowManager.setSkipTaskbar(true);
489
+ await windowManager.setSize(_desktopAssistantPopupWindowSize);
490
+ await windowManager.setAlignment(const Alignment(0, 0.92));
491
+ await windowManager.show(inactive: false);
492
+ await windowManager.focus();
493
+ }
494
+
495
+ Future<void> _hideAssistantPopupWindow() async {
496
+ if (!_desktopAssistantPopupWindowMode) {
497
+ return;
498
+ }
499
+ _desktopAssistantBlockedHintVisible = false;
500
+ final shouldHideWindow = _desktopAssistantReturnToHidden;
501
+ _desktopAssistantReturnToHidden = false;
502
+ await _restoreMainWindowPresentation(
503
+ hideAfterRestore: shouldHideWindow,
504
+ focusWindow: !shouldHideWindow,
505
+ );
506
+ }
507
+
508
+ Future<void> _cancelAssistantPopupFromUi() async {
509
+ _assistantHotKeyPressed = false;
510
+ _assistantHotKeyHandledAsHold = false;
511
+ _assistantPttActive = false;
512
+ _assistantHotKeyHoldTimer?.cancel();
513
+ if (_controller.isLiveVoiceCaptureActive ||
514
+ _controller.isLiveVoiceCaptureStarting) {
515
+ try {
516
+ await _controller.stopLiveVoiceCapture();
517
+ } catch (_) {}
518
+ }
519
+ await _hideAssistantPopupWindow();
520
+ }
521
+
522
+ Future<void> _toggleAssistantPopupCaptureFromUi() async {
523
+ if (_desktopAssistantBlockedHintVisible) {
524
+ return;
525
+ }
526
+ try {
527
+ _assistantPttActive = !_controller.isLiveVoiceCaptureEngaged;
528
+ await _controller.toggleLiveVoiceCapture();
529
+ } catch (error, stackTrace) {
530
+ _assistantPttActive = false;
531
+ AppDiagnostics.log(
532
+ 'desktop.assistant',
533
+ 'popup.start_failed',
534
+ error: error,
535
+ stackTrace: stackTrace,
536
+ );
537
+ } finally {
538
+ if (!_controller.isLiveVoiceCaptureActive &&
539
+ !_controller.isLiveVoiceCaptureStarting) {
540
+ _assistantPttActive = false;
541
+ }
542
+ }
543
+ }
544
+
545
+ @override
546
+ Widget build(BuildContext context) {
547
+ return AnimatedBuilder(
548
+ animation: _controller,
549
+ builder: (context, _) {
550
+ final rootStateSignature =
551
+ 'boot:${_controller.isBooting}'
552
+ '|backend:${_controller.requiresBackendUrlSetup}'
553
+ '|auth:${_controller.isAuthenticated}'
554
+ '|refresh:${_controller.isRefreshing}'
555
+ '|section:${_controller.selectedSection.name}'
556
+ '|toolbarMode:$_desktopToolbarWindowMode'
557
+ '|assistantPopupMode:$_desktopAssistantPopupWindowMode'
558
+ '|assistantPttActive:${_controller.isLiveVoiceCaptureActive}'
559
+ '|assistantPttStarting:${_controller.isLiveVoiceCaptureStarting}'
560
+ '|assistantBlockedHint:$_desktopAssistantBlockedHintVisible';
561
+ if (_navigatorScopeSignature != rootStateSignature) {
562
+ _navigatorScopeSignature = rootStateSignature;
563
+ _navigatorKey = GlobalKey<NavigatorState>();
564
+ }
565
+ return MaterialApp(
566
+ key: ValueKey<String>(rootStateSignature),
567
+ navigatorKey: _navigatorKey,
568
+ title: widget.mode == NeoAgentAppMode.launcher
569
+ ? 'NeoAgent Launcher'
570
+ : 'NeoAgent',
571
+ debugShowCheckedModeBanner: false,
572
+ theme: _buildNeoAgentTheme(_lightPalette, Brightness.light),
573
+ darkTheme: _buildNeoAgentTheme(_darkPalette, Brightness.dark),
574
+ themeMode: ThemeMode.system,
575
+ builder: (context, child) {
576
+ return Stack(
577
+ children: <Widget>[
578
+ if (child != null) child,
579
+ if (!_desktopToolbarWindowMode &&
580
+ !_desktopAssistantPopupWindowMode &&
581
+ _controller.showOfflineBanner)
582
+ Positioned(
583
+ top: 0,
584
+ left: 0,
585
+ right: 0,
586
+ child: SafeArea(
587
+ bottom: false,
588
+ child: Padding(
589
+ padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
590
+ child: Center(
591
+ child: ConstrainedBox(
592
+ constraints: const BoxConstraints(maxWidth: 980),
593
+ child: _GlobalNetworkBanner(
594
+ controller: _controller,
595
+ ),
596
+ ),
597
+ ),
598
+ ),
599
+ ),
600
+ ),
601
+ if (_supportsDesktopShell &&
602
+ !_desktopToolbarWindowMode &&
603
+ !_desktopAssistantPopupWindowMode)
604
+ _DesktopFloatingToolbar(controller: _controller),
605
+ ],
606
+ );
607
+ },
608
+ home: _desktopAssistantPopupWindowMode
609
+ ? _DesktopAssistantPopupShell(
610
+ controller: _controller,
611
+ blockedHintVisible: _desktopAssistantBlockedHintVisible,
612
+ onPrimaryAction: _toggleAssistantPopupCaptureFromUi,
613
+ onCancel: _cancelAssistantPopupFromUi,
614
+ )
615
+ : (_desktopToolbarWindowMode
616
+ ? _DetachedDesktopFloatingToolbarShell(
617
+ controller: _controller,
618
+ onOpenMainWindow: _openMainWindow,
619
+ )
620
+ : NeoAgentRoot(controller: _controller)),
621
+ );
622
+ },
623
+ );
624
+ }
625
+
626
+ @override
627
+ void onTrayIconMouseDown() {
628
+ trayManager.popUpContextMenu();
629
+ }
630
+
631
+ @override
632
+ void onTrayMenuItemClick(MenuItem menuItem) {
633
+ final key = menuItem.key;
634
+ if (key == null) {
635
+ return;
636
+ }
637
+ switch (key) {
638
+ case 'open':
639
+ unawaited(_openMainWindow());
640
+ break;
641
+ case 'start_recording':
642
+ if (_controller.canStartDesktopRecording) {
643
+ unawaited(_controller.startDesktopRecording());
644
+ }
645
+ break;
646
+ case 'pause_resume_recording':
647
+ if (_controller.recordingRuntime.paused) {
648
+ unawaited(_controller.resumeDesktopRecording());
649
+ } else {
650
+ unawaited(_controller.pauseDesktopRecording());
651
+ }
652
+ break;
653
+ case 'stop_recording':
654
+ unawaited(_controller.stopRecording());
655
+ break;
656
+ case 'toggle_toolbar':
657
+ if (_controller.recordingRuntime.floatingToolbarVisible) {
658
+ unawaited(_controller.hideDesktopFloatingToolbar());
659
+ } else {
660
+ unawaited(_controller.showDesktopFloatingToolbar());
661
+ }
662
+ break;
663
+ case 'open_voice_assistant':
664
+ unawaited(_openMainWindow());
665
+ _controller.setSelectedSection(AppSection.voiceAssistant);
666
+ break;
667
+ case 'quit':
668
+ unawaited(_quitDesktopShell());
669
+ break;
670
+ }
671
+ }
672
+
673
+ @override
674
+ void onWindowClose() {
675
+ if (!_supportsDesktopShell || _handlingDesktopClose) {
676
+ return;
677
+ }
678
+ _handlingDesktopClose = true;
679
+ unawaited(_handleDesktopWindowClose());
680
+ }
681
+
682
+ Future<void> _handleDesktopWindowClose() async {
683
+ try {
684
+ final navigatorContext = _navigatorKey.currentContext;
685
+ if (navigatorContext == null) {
686
+ await _quitDesktopShell();
687
+ return;
688
+ }
689
+
690
+ final shouldPrompt = _controller.desktopAskOnClose;
691
+ if (!shouldPrompt) {
692
+ if (_controller.desktopKeepRunningOnClose) {
693
+ await _hideMainWindow();
694
+ } else {
695
+ await _quitDesktopShell();
696
+ }
697
+ return;
698
+ }
699
+
700
+ final decision = await showDialog<_DesktopCloseDecision>(
701
+ context: navigatorContext,
702
+ builder: (context) {
703
+ var rememberChoice = false;
704
+ return StatefulBuilder(
705
+ builder: (context, setDialogState) {
706
+ return AlertDialog(
707
+ backgroundColor: _bgCard,
708
+ title: Text('Keep NeoAgent running?'),
709
+ content: SizedBox(
710
+ width: 440,
711
+ child: Column(
712
+ mainAxisSize: MainAxisSize.min,
713
+ crossAxisAlignment: CrossAxisAlignment.start,
714
+ children: <Widget>[
715
+ Text(
716
+ 'Closing the window can either keep NeoAgent running in the background with tray access, or fully quit the desktop runtime.',
717
+ style: TextStyle(color: _textSecondary, height: 1.45),
718
+ ),
719
+ const SizedBox(height: 16),
720
+ CheckboxListTile(
721
+ contentPadding: EdgeInsets.zero,
722
+ value: rememberChoice,
723
+ onChanged: (value) {
724
+ setDialogState(() {
725
+ rememberChoice = value == true;
726
+ });
727
+ },
728
+ title: Text('Remember this choice'),
729
+ controlAffinity: ListTileControlAffinity.leading,
730
+ ),
731
+ ],
732
+ ),
733
+ ),
734
+ actions: <Widget>[
735
+ TextButton(
736
+ onPressed: () => Navigator.of(context).pop(
737
+ _DesktopCloseDecision(
738
+ keepRunning: false,
739
+ rememberChoice: rememberChoice,
740
+ ),
741
+ ),
742
+ child: Text('Quit'),
743
+ ),
744
+ FilledButton(
745
+ onPressed: () => Navigator.of(context).pop(
746
+ _DesktopCloseDecision(
747
+ keepRunning: true,
748
+ rememberChoice: rememberChoice,
749
+ ),
750
+ ),
751
+ child: Text('Keep running'),
752
+ ),
753
+ ],
754
+ );
755
+ },
756
+ );
757
+ },
758
+ );
759
+
760
+ if (decision == null) {
761
+ return;
762
+ }
763
+ if (decision.rememberChoice) {
764
+ await _controller.setDesktopClosePreference(
765
+ askOnClose: false,
766
+ keepRunningOnClose: decision.keepRunning,
767
+ );
768
+ }
769
+ if (decision.keepRunning) {
770
+ await _hideMainWindow();
771
+ } else {
772
+ await _quitDesktopShell();
773
+ }
774
+ } finally {
775
+ _handlingDesktopClose = false;
776
+ }
777
+ }
778
+
779
+ Future<void> _quitDesktopShell() async {
780
+ await windowManager.setPreventClose(false);
781
+ await windowManager.destroy();
782
+ }
783
+ }
784
+
785
+ class NeoAgentRoot extends StatelessWidget {
786
+ const NeoAgentRoot({super.key, required this.controller});
787
+
788
+ final NeoAgentController controller;
789
+
790
+ @override
791
+ Widget build(BuildContext context) {
792
+ if (controller.isBooting) {
793
+ return const SplashView();
794
+ }
795
+ if (controller.requiresBackendUrlSetup) {
796
+ return BackendSetupView(controller: controller);
797
+ }
798
+ if (!controller.isAuthenticated) {
799
+ return AuthView(controller: controller);
800
+ }
801
+ if (controller.isLauncherMode) {
802
+ return LauncherHomeView(controller: controller);
803
+ }
804
+ return HomeView(controller: controller);
805
+ }
806
+ }