neoagent 2.3.1-beta.6 → 2.3.1-beta.61

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 (312) hide show
  1. package/.env.example +45 -0
  2. package/docs/capabilities.md +2 -2
  3. package/docs/configuration.md +6 -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 +111 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +171 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +804 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/auto/NeoAgentCarAppService.kt +114 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnection.kt +118 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnectionService.kt +86 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  30. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  31. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  32. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  33. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  35. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  36. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  37. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  38. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  39. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  43. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  44. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  45. package/flutter_app/android/app/src/main/res/values/arrays.xml +8 -0
  46. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  48. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  49. package/flutter_app/android/app/src/main/res/xml/automotive_app_desc.xml +4 -0
  50. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  51. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  52. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  53. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  54. package/flutter_app/android/build.gradle.kts +24 -0
  55. package/flutter_app/android/ci-release.keystore +0 -0
  56. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  57. package/flutter_app/android/gradle.properties +3 -0
  58. package/flutter_app/android/key.properties +4 -0
  59. package/flutter_app/android/settings.gradle.kts +26 -0
  60. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  62. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  63. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  64. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  65. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  66. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  67. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  68. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  69. package/flutter_app/lib/features/location/location_service.dart +119 -0
  70. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  71. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +571 -0
  72. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +275 -0
  73. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +366 -0
  74. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +57 -0
  75. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +218 -0
  76. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +46 -0
  77. package/flutter_app/lib/main.dart +103 -0
  78. package/flutter_app/lib/main_account_settings.dart +1250 -0
  79. package/flutter_app/lib/main_admin.dart +886 -0
  80. package/flutter_app/lib/main_app_shell.dart +2134 -0
  81. package/flutter_app/lib/main_chat.dart +3316 -0
  82. package/flutter_app/lib/main_controller.dart +6836 -0
  83. package/flutter_app/lib/main_devices.dart +2396 -0
  84. package/flutter_app/lib/main_integrations.dart +909 -0
  85. package/flutter_app/lib/main_launcher.dart +959 -0
  86. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  87. package/flutter_app/lib/main_models.dart +3539 -0
  88. package/flutter_app/lib/main_navigation.dart +188 -0
  89. package/flutter_app/lib/main_operations.dart +4851 -0
  90. package/flutter_app/lib/main_recordings.dart +920 -0
  91. package/flutter_app/lib/main_runtime.dart +838 -0
  92. package/flutter_app/lib/main_settings.dart +2040 -0
  93. package/flutter_app/lib/main_shared.dart +3147 -0
  94. package/flutter_app/lib/main_theme.dart +257 -0
  95. package/flutter_app/lib/main_voice_assistant.dart +957 -0
  96. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  97. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  98. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  99. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  100. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  101. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  102. package/flutter_app/lib/src/android_auto_bridge.dart +59 -0
  103. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  104. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  105. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  106. package/flutter_app/lib/src/backend_client.dart +1837 -0
  107. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  108. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  109. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  110. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  111. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  112. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  113. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  114. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  115. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  116. package/flutter_app/lib/src/health_bridge.dart +136 -0
  117. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  118. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  119. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  120. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  121. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  122. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  123. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  124. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  125. package/flutter_app/lib/src/oauth_launcher_io.dart +95 -0
  126. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  127. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  128. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  129. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  130. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  131. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  132. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  133. package/flutter_app/lib/src/theme/palette.dart +81 -0
  134. package/flutter_app/lib/src/web_app_update_monitor.dart +17 -0
  135. package/flutter_app/lib/src/web_app_update_monitor_stub.dart +24 -0
  136. package/flutter_app/lib/src/web_app_update_monitor_web.dart +123 -0
  137. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  138. package/flutter_app/linux/CMakeLists.txt +128 -0
  139. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  140. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +47 -0
  141. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  142. package/flutter_app/linux/flutter/generated_plugins.cmake +32 -0
  143. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  144. package/flutter_app/linux/runner/main.cc +6 -0
  145. package/flutter_app/linux/runner/my_application.cc +144 -0
  146. package/flutter_app/linux/runner/my_application.h +18 -0
  147. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  148. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  149. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  150. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +44 -0
  151. package/flutter_app/macos/Podfile +42 -0
  152. package/flutter_app/macos/Podfile.lock +87 -0
  153. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  154. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  155. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  156. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  157. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  158. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  159. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  160. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  161. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  162. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  163. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  164. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  165. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  166. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  167. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  168. package/flutter_app/macos/Runner/Info.plist +36 -0
  169. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  170. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  171. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  172. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  173. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  174. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  175. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  176. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  177. package/flutter_app/patch_strings.py +12 -0
  178. package/flutter_app/pubspec.lock +1224 -0
  179. package/flutter_app/pubspec.yaml +57 -0
  180. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  181. package/flutter_app/test/recording_payloads_test.dart +53 -0
  182. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  183. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  184. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  185. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  186. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  187. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  188. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  189. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  190. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  191. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  192. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  193. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  194. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  195. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  196. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  197. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  198. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  199. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  200. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  201. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  202. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  203. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  204. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  205. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  206. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  207. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  208. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  209. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  210. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  211. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  212. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  213. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  214. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  215. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  216. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  217. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  218. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  219. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  220. package/flutter_app/web/favicon.png +0 -0
  221. package/flutter_app/web/favicon.svg +12 -0
  222. package/flutter_app/web/icons/Icon-192.png +0 -0
  223. package/flutter_app/web/icons/Icon-512.png +0 -0
  224. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  225. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  226. package/flutter_app/web/index.html +39 -0
  227. package/flutter_app/web/manifest.json +35 -0
  228. package/flutter_app/windows/CMakeLists.txt +108 -0
  229. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  230. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +50 -0
  231. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  232. package/flutter_app/windows/flutter/generated_plugins.cmake +36 -0
  233. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  234. package/flutter_app/windows/runner/Runner.rc +121 -0
  235. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  236. package/flutter_app/windows/runner/flutter_window.h +37 -0
  237. package/flutter_app/windows/runner/main.cpp +53 -0
  238. package/flutter_app/windows/runner/resource.h +16 -0
  239. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  240. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  241. package/flutter_app/windows/runner/utils.cpp +65 -0
  242. package/flutter_app/windows/runner/utils.h +19 -0
  243. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  244. package/flutter_app/windows/runner/win32_window.h +102 -0
  245. package/lib/install_helpers.js +31 -0
  246. package/lib/manager.js +201 -6
  247. package/package.json +5 -3
  248. package/server/db/database.js +125 -1
  249. package/server/http/middleware.js +55 -2
  250. package/server/http/routes.js +1 -0
  251. package/server/http/static.js +41 -1
  252. package/server/index.js +3 -0
  253. package/server/public/.last_build_id +1 -1
  254. package/server/public/assets/AssetManifest.bin +1 -1
  255. package/server/public/assets/AssetManifest.bin.json +1 -1
  256. package/server/public/assets/NOTICES +281 -1
  257. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  258. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  259. package/server/public/canvaskit/wimp.wasm +0 -0
  260. package/server/public/flutter_bootstrap.js +2 -2
  261. package/server/public/main.dart.js +86707 -82621
  262. package/server/routes/auth.js +26 -5
  263. package/server/routes/integrations.js +108 -1
  264. package/server/routes/memory.js +11 -2
  265. package/server/routes/settings.js +84 -2
  266. package/server/routes/wearable.js +67 -0
  267. package/server/services/ai/engine.js +3 -2
  268. package/server/services/ai/models.js +30 -0
  269. package/server/services/ai/providers/githubCopilot.js +97 -0
  270. package/server/services/ai/providers/openai.js +2 -1
  271. package/server/services/ai/providers/openaiCodex.js +31 -0
  272. package/server/services/ai/settings.js +20 -0
  273. package/server/services/ai/toolSelector.js +54 -1
  274. package/server/services/ai/tools.js +88 -5
  275. package/server/services/desktop/screenRecorder.js +65 -9
  276. package/server/services/integrations/env.js +5 -0
  277. package/server/services/integrations/figma/provider.js +1 -0
  278. package/server/services/integrations/github/common.js +106 -0
  279. package/server/services/integrations/github/provider.js +499 -0
  280. package/server/services/integrations/github/repos.js +1124 -0
  281. package/server/services/integrations/google/provider.js +1 -0
  282. package/server/services/integrations/manager.js +88 -12
  283. package/server/services/integrations/microsoft/provider.js +1 -0
  284. package/server/services/integrations/oauth_provider.js +25 -8
  285. package/server/services/integrations/provider_config_store.js +85 -0
  286. package/server/services/integrations/registry.js +4 -2
  287. package/server/services/integrations/spotify/provider.js +1 -0
  288. package/server/services/integrations/trello/provider.js +842 -0
  289. package/server/services/manager.js +46 -1
  290. package/server/services/mcp/client.js +120 -23
  291. package/server/services/memory/manager.js +39 -2
  292. package/server/services/messaging/access_policy.js +10 -0
  293. package/server/services/messaging/manager.js +49 -0
  294. package/server/services/messaging/meshtastic.js +277 -0
  295. package/server/services/messaging/meshtastic_env.js +40 -0
  296. package/server/services/messaging/meshtastic_protocol.js +530 -0
  297. package/server/services/messaging/meshtastic_protocol.test.js +33 -0
  298. package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
  299. package/server/services/tasks/runtime.js +1 -1
  300. package/server/services/voice/openaiClient.js +4 -1
  301. package/server/services/voice/openaiSpeech.js +6 -1
  302. package/server/services/voice/providers.js +144 -15
  303. package/server/services/voice/runtimeManager.js +156 -19
  304. package/server/services/voice/turnRunner.js +29 -9
  305. package/server/services/wearable/firmware_manifest.js +353 -0
  306. package/server/services/wearable/gateway.js +350 -0
  307. package/server/services/wearable/protocol.js +45 -0
  308. package/server/services/wearable/service.js +240 -0
  309. package/server/services/widgets/service.js +3 -0
  310. package/server/utils/local_secrets.js +56 -0
  311. package/server/utils/logger.js +37 -9
  312. package/server/services/integrations/home_assistant/provider.js +0 -350
@@ -0,0 +1,909 @@
1
+ part of 'main.dart';
2
+
3
+ class IntegrationsPanel extends StatelessWidget {
4
+ const IntegrationsPanel({super.key, required this.controller});
5
+
6
+ final NeoAgentController controller;
7
+
8
+ @override
9
+ Widget build(BuildContext context) {
10
+ return Padding(
11
+ padding: _pagePadding(context),
12
+ child: Column(
13
+ children: <Widget>[
14
+ _PageTitle(
15
+ title: 'Integrations',
16
+ subtitle:
17
+ 'Connect and manage official integrations separately from reusable skills.',
18
+ ),
19
+ const SizedBox(height: 12),
20
+ Expanded(child: OfficialIntegrationsTab(controller: controller)),
21
+ ],
22
+ ),
23
+ );
24
+ }
25
+ }
26
+
27
+ class OfficialIntegrationsTab extends StatelessWidget {
28
+ const OfficialIntegrationsTab({super.key, required this.controller});
29
+
30
+ final NeoAgentController controller;
31
+
32
+ @override
33
+ Widget build(BuildContext context) {
34
+ final visibleIntegrations = controller.officialIntegrations
35
+ .where(
36
+ (item) =>
37
+ item.env.configured ||
38
+ item.env.setupMode == 'user' ||
39
+ item.isConnected,
40
+ )
41
+ .toList();
42
+
43
+ if (visibleIntegrations.isEmpty) {
44
+ return Card(
45
+ child: Padding(
46
+ padding: EdgeInsets.all(24),
47
+ child: Center(
48
+ child: Text(
49
+ 'No official integrations are available yet.',
50
+ style: TextStyle(color: _textSecondary),
51
+ ),
52
+ ),
53
+ ),
54
+ );
55
+ }
56
+
57
+ final connectedIntegrations = visibleIntegrations
58
+ .where((item) => item.isConnected)
59
+ .toList();
60
+ final availableIntegrations = visibleIntegrations
61
+ .where(
62
+ (item) =>
63
+ !item.isConnected &&
64
+ (item.env.configured || item.env.setupMode == 'user'),
65
+ )
66
+ .toList();
67
+
68
+ return Card(
69
+ child: ListView(
70
+ padding: const EdgeInsets.all(16),
71
+ children: <Widget>[
72
+ if (connectedIntegrations.isNotEmpty) ...[
73
+ const _IntegrationSectionTitle(title: 'Connected'),
74
+ ...connectedIntegrations.asMap().entries.map(
75
+ (entry) => Padding(
76
+ padding: EdgeInsets.only(
77
+ bottom: entry.key < connectedIntegrations.length - 1 ? 12 : 0,
78
+ ),
79
+ child: _buildIntegrationCard(context, entry.value),
80
+ ),
81
+ ),
82
+ ],
83
+ if (connectedIntegrations.isNotEmpty &&
84
+ availableIntegrations.isNotEmpty)
85
+ const SizedBox(height: 24),
86
+ if (availableIntegrations.isNotEmpty) ...[
87
+ const _IntegrationSectionTitle(title: 'Available'),
88
+ ...availableIntegrations.asMap().entries.map(
89
+ (entry) => Padding(
90
+ padding: EdgeInsets.only(
91
+ bottom: entry.key < availableIntegrations.length - 1 ? 12 : 0,
92
+ ),
93
+ child: _buildIntegrationCard(context, entry.value),
94
+ ),
95
+ ),
96
+ ],
97
+ ],
98
+ ),
99
+ );
100
+ }
101
+
102
+ Widget _buildIntegrationCard(
103
+ BuildContext context,
104
+ OfficialIntegrationItem item,
105
+ ) {
106
+ return Container(
107
+ padding: const EdgeInsets.all(16),
108
+ decoration: BoxDecoration(
109
+ color: _bgSecondary,
110
+ borderRadius: BorderRadius.circular(16),
111
+ border: Border.all(color: item.isConnected ? _accentMuted : _border),
112
+ ),
113
+ child: Column(
114
+ crossAxisAlignment: CrossAxisAlignment.start,
115
+ children: <Widget>[
116
+ Row(
117
+ crossAxisAlignment: CrossAxisAlignment.start,
118
+ children: <Widget>[
119
+ _OfficialIntegrationIcon(item: item),
120
+ const SizedBox(width: 12),
121
+ Expanded(
122
+ child: Column(
123
+ crossAxisAlignment: CrossAxisAlignment.start,
124
+ children: <Widget>[
125
+ Row(
126
+ children: <Widget>[
127
+ Expanded(
128
+ child: Text(
129
+ item.label,
130
+ style: TextStyle(
131
+ fontSize: 18,
132
+ fontWeight: FontWeight.w800,
133
+ ),
134
+ ),
135
+ ),
136
+ _StatusPill(
137
+ label: item.statusLabel,
138
+ color: item.isConnected
139
+ ? _success
140
+ : item.hasExpiredAccounts
141
+ ? _warning
142
+ : item.env.configured
143
+ ? _info
144
+ : _warning,
145
+ ),
146
+ ],
147
+ ),
148
+ const SizedBox(height: 6),
149
+ Text(
150
+ item.description,
151
+ style: TextStyle(color: _textSecondary),
152
+ ),
153
+ const SizedBox(height: 10),
154
+ Wrap(
155
+ spacing: 8,
156
+ runSpacing: 8,
157
+ children: <Widget>[
158
+ _MetaPill(
159
+ label: '${item.connection.accountCount} accounts',
160
+ icon: Icons.alternate_email_rounded,
161
+ ),
162
+ _MetaPill(
163
+ label: '${item.connection.appCount} apps active',
164
+ icon: Icons.apps_rounded,
165
+ ),
166
+ _MetaPill(
167
+ label: '${item.availableToolCount} tools',
168
+ icon: Icons.build_outlined,
169
+ ),
170
+ ],
171
+ ),
172
+ const SizedBox(height: 10),
173
+ Text(
174
+ !item.env.configured
175
+ ? item.env.summary
176
+ : item.hasExpiredAccounts
177
+ ? 'One or more accounts expired. Reconnect the affected account to restore tool access.'
178
+ : !item.supportsMultipleAccounts && item.isConnected
179
+ ? 'This integration currently supports one connected account per agent. Re-open setup to replace it.'
180
+ : item.isConnected
181
+ ? 'Connect as many accounts as you want. Each app can use a different account.'
182
+ : ((item.connectPrompt ?? '').trim().isNotEmpty
183
+ ? item.connectPrompt!.trim()
184
+ : 'Connect app accounts individually so the AI can use the right account for each official integration.'),
185
+ style: TextStyle(color: _textSecondary),
186
+ ),
187
+ ],
188
+ ),
189
+ ),
190
+ ],
191
+ ),
192
+ const SizedBox(height: 16),
193
+ ...item.apps.map(
194
+ (app) => Padding(
195
+ padding: const EdgeInsets.only(bottom: 12),
196
+ child: _OfficialIntegrationAppCard(
197
+ controller: controller,
198
+ provider: item,
199
+ app: app,
200
+ ),
201
+ ),
202
+ ),
203
+ ],
204
+ ),
205
+ );
206
+ }
207
+ }
208
+
209
+ void _openOfficialIntegrationSetupDialog(
210
+ BuildContext context,
211
+ NeoAgentController controller,
212
+ String providerId,
213
+ ) {
214
+ switch (providerId) {
215
+ case 'trello':
216
+ _showTrelloSetupDialog(context, controller);
217
+ return;
218
+ }
219
+ }
220
+
221
+ Future<void> _showTrelloSetupDialog(
222
+ BuildContext context,
223
+ NeoAgentController controller,
224
+ ) async {
225
+ Map<String, dynamic> existing;
226
+ try {
227
+ existing = await controller.getOfficialIntegrationConfig('trello');
228
+ } catch (error) {
229
+ if (context.mounted) {
230
+ ScaffoldMessenger.of(context).showSnackBar(
231
+ SnackBar(content: Text(controller.errorMessage ?? error.toString())),
232
+ );
233
+ }
234
+ return;
235
+ }
236
+
237
+ final apiKeyConfigured = existing['apiKeyConfigured'] == true;
238
+ final savedApiKey = existing['apiKey']?.toString() ?? '';
239
+ final apiKeyManagedByServer = apiKeyConfigured && savedApiKey.trim().isEmpty;
240
+ final authorizeUrl = existing['authorizeUrl']?.toString() ?? '';
241
+ final accountCount = (existing['accountCount'] as num?)?.toInt() ?? 0;
242
+ final hasConnectedAccount =
243
+ existing['hasConnectedAccount'] == true || accountCount > 0;
244
+ var formError = '';
245
+ var connecting = false;
246
+
247
+ final apiKeyController = TextEditingController(text: savedApiKey);
248
+ final tokenInputController = TextEditingController();
249
+
250
+ await showDialog<void>(
251
+ context: context,
252
+ barrierDismissible: false,
253
+ builder: (dialogContext) {
254
+ return StatefulBuilder(
255
+ builder: (dialogContext, setState) {
256
+ return AlertDialog(
257
+ title: const Text('Trello Setup'),
258
+ content: SizedBox(
259
+ width: 520,
260
+ child: Column(
261
+ mainAxisSize: MainAxisSize.min,
262
+ crossAxisAlignment: CrossAxisAlignment.start,
263
+ children: <Widget>[
264
+ Text(
265
+ 'Save a Trello API key for this agent, then connect one Trello account securely. The account token is stored on the server and used only for this agent.',
266
+ style: TextStyle(color: _textSecondary),
267
+ ),
268
+ const SizedBox(height: 16),
269
+ _TrelloStatusItem(
270
+ label: 'API Key',
271
+ status: apiKeyConfigured ? 'Configured' : 'Not configured',
272
+ isConnected: apiKeyConfigured,
273
+ ),
274
+ const SizedBox(height: 12),
275
+ _TrelloStatusItem(
276
+ label: 'Connected Account',
277
+ status: hasConnectedAccount
278
+ ? '$accountCount ${accountCount == 1 ? 'connected account' : 'connected accounts'}'
279
+ : 'Not connected',
280
+ isConnected: hasConnectedAccount,
281
+ ),
282
+ if (apiKeyManagedByServer) ...<Widget>[
283
+ const SizedBox(height: 12),
284
+ Text(
285
+ 'This agent is using a server-managed Trello API key. You only need to authorize an account token below.',
286
+ style: TextStyle(color: _textSecondary),
287
+ ),
288
+ ] else ...<Widget>[
289
+ const SizedBox(height: 12),
290
+ TextField(
291
+ controller: apiKeyController,
292
+ onChanged: (_) => setState(() {}),
293
+ obscureText: true,
294
+ decoration: const InputDecoration(
295
+ labelText: 'Trello API Key',
296
+ border: OutlineInputBorder(),
297
+ ),
298
+ ),
299
+ ],
300
+ if (apiKeyConfigured ||
301
+ apiKeyController.text.trim().isNotEmpty ||
302
+ apiKeyManagedByServer) ...<Widget>[
303
+ const SizedBox(height: 12),
304
+ TextField(
305
+ controller: tokenInputController,
306
+ onChanged: (_) => setState(() {}),
307
+ obscureText: true,
308
+ decoration: InputDecoration(
309
+ labelText: hasConnectedAccount
310
+ ? 'Paste a replacement token'
311
+ : 'Paste your account token',
312
+ border: OutlineInputBorder(),
313
+ ),
314
+ ),
315
+ ],
316
+ if (formError.isNotEmpty) ...<Widget>[
317
+ const SizedBox(height: 12),
318
+ Container(
319
+ padding: const EdgeInsets.all(8),
320
+ decoration: BoxDecoration(
321
+ color: _danger.withOpacity(0.1),
322
+ borderRadius: BorderRadius.circular(4),
323
+ border: Border.all(color: _danger.withOpacity(0.3)),
324
+ ),
325
+ child: Text(
326
+ formError,
327
+ style: TextStyle(color: _danger, fontSize: 12),
328
+ ),
329
+ ),
330
+ ],
331
+ ],
332
+ ),
333
+ ),
334
+ actions: <Widget>[
335
+ if (apiKeyConfigured || savedApiKey.trim().isNotEmpty)
336
+ TextButton(
337
+ onPressed: connecting
338
+ ? null
339
+ : () async {
340
+ final shouldClear =
341
+ await showDialog<bool>(
342
+ context: dialogContext,
343
+ builder: (context) {
344
+ return AlertDialog(
345
+ title: const Text('Disconnect Trello?'),
346
+ content: const Text(
347
+ 'This removes the Trello setup and connected accounts for this agent.',
348
+ ),
349
+ actions: [
350
+ TextButton(
351
+ onPressed: () =>
352
+ Navigator.of(context).pop(false),
353
+ child: const Text('Cancel'),
354
+ ),
355
+ FilledButton(
356
+ onPressed: () =>
357
+ Navigator.of(context).pop(true),
358
+ child: const Text('Disconnect'),
359
+ ),
360
+ ],
361
+ );
362
+ },
363
+ ) ??
364
+ false;
365
+ if (!shouldClear) {
366
+ return;
367
+ }
368
+ setState(() {
369
+ formError = '';
370
+ connecting = true;
371
+ });
372
+ try {
373
+ await controller.clearOfficialIntegrationConfig(
374
+ 'trello',
375
+ );
376
+ if (dialogContext.mounted) {
377
+ Navigator.of(dialogContext).pop();
378
+ }
379
+ } catch (_) {
380
+ setState(() {
381
+ formError =
382
+ controller.errorMessage ??
383
+ 'Could not disconnect Trello.';
384
+ connecting = false;
385
+ });
386
+ }
387
+ },
388
+ child: const Text('Disconnect'),
389
+ ),
390
+ TextButton(
391
+ onPressed: connecting
392
+ ? null
393
+ : () => Navigator.of(dialogContext).pop(),
394
+ child: const Text('Close'),
395
+ ),
396
+ if (authorizeUrl.isNotEmpty ||
397
+ apiKeyManagedByServer ||
398
+ apiKeyController.text.trim().isNotEmpty)
399
+ FilledButton.icon(
400
+ onPressed: connecting
401
+ ? null
402
+ : () async {
403
+ setState(() {
404
+ formError = '';
405
+ connecting = true;
406
+ });
407
+ try {
408
+ final effectiveApiKey = apiKeyManagedByServer
409
+ ? ''
410
+ : apiKeyController.text.trim();
411
+ if (!apiKeyManagedByServer &&
412
+ effectiveApiKey.isEmpty) {
413
+ setState(() {
414
+ formError = 'Trello API Key is required.';
415
+ connecting = false;
416
+ });
417
+ return;
418
+ }
419
+ final url = authorizeUrl.isNotEmpty
420
+ ? authorizeUrl
421
+ : 'https://trello.com/1/authorize?expiration=never&scope=read,write,account&response_type=token&key=' +
422
+ Uri.encodeComponent(effectiveApiKey);
423
+ final result = await controller._oauthLauncher
424
+ .openExternal(url: url, label: 'Trello');
425
+ if (!result.launched) {
426
+ setState(() {
427
+ formError =
428
+ result.error ??
429
+ 'Could not open Trello in your browser.';
430
+ connecting = false;
431
+ });
432
+ } else {
433
+ setState(() {
434
+ connecting = false;
435
+ });
436
+ }
437
+ } catch (error) {
438
+ setState(() {
439
+ formError = error.toString();
440
+ connecting = false;
441
+ });
442
+ }
443
+ },
444
+ icon: const Icon(Icons.open_in_browser_rounded),
445
+ label: Text(connecting ? 'Opening...' : 'Open Trello'),
446
+ ),
447
+ FilledButton(
448
+ onPressed: connecting
449
+ ? null
450
+ : () async {
451
+ setState(() {
452
+ formError = '';
453
+ connecting = true;
454
+ });
455
+ try {
456
+ final apiKey = apiKeyController.text.trim();
457
+ final token = tokenInputController.text.trim();
458
+ if (!apiKeyManagedByServer && apiKey.isEmpty) {
459
+ setState(() {
460
+ formError = 'Trello API Key is required.';
461
+ connecting = false;
462
+ });
463
+ return;
464
+ }
465
+ if (token.isEmpty &&
466
+ apiKeyConfigured &&
467
+ !apiKeyManagedByServer) {
468
+ await controller.saveOfficialIntegrationConfig(
469
+ 'trello',
470
+ config: <String, dynamic>{'apiKey': apiKey},
471
+ );
472
+ } else if (token.isEmpty && !apiKeyManagedByServer) {
473
+ await controller.saveOfficialIntegrationConfig(
474
+ 'trello',
475
+ config: <String, dynamic>{'apiKey': apiKey},
476
+ );
477
+ } else {
478
+ await controller.saveOfficialIntegrationConfig(
479
+ 'trello',
480
+ config: <String, dynamic>{
481
+ if (!apiKeyManagedByServer) 'apiKey': apiKey,
482
+ 'token': token,
483
+ },
484
+ );
485
+ }
486
+ if (dialogContext.mounted) {
487
+ Navigator.of(dialogContext).pop();
488
+ }
489
+ } catch (_) {
490
+ setState(() {
491
+ formError =
492
+ controller.errorMessage ??
493
+ 'Could not save Trello setup.';
494
+ connecting = false;
495
+ });
496
+ }
497
+ },
498
+ child: Text(
499
+ connecting
500
+ ? 'Saving...'
501
+ : tokenInputController.text.trim().isNotEmpty
502
+ ? hasConnectedAccount
503
+ ? 'Replace Account'
504
+ : 'Connect Account'
505
+ : 'Save Setup',
506
+ ),
507
+ ),
508
+ ],
509
+ );
510
+ },
511
+ );
512
+ },
513
+ );
514
+
515
+ apiKeyController.dispose();
516
+ tokenInputController.dispose();
517
+ }
518
+
519
+ class _TrelloStatusItem extends StatelessWidget {
520
+ const _TrelloStatusItem({
521
+ required this.label,
522
+ required this.status,
523
+ required this.isConnected,
524
+ });
525
+
526
+ final String label;
527
+ final String status;
528
+ final bool isConnected;
529
+
530
+ @override
531
+ Widget build(BuildContext context) {
532
+ return Container(
533
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
534
+ decoration: BoxDecoration(
535
+ color: _bgSecondary,
536
+ borderRadius: BorderRadius.circular(8),
537
+ border: Border.all(
538
+ color: isConnected ? _success.withOpacity(0.3) : _border,
539
+ ),
540
+ ),
541
+ child: Row(
542
+ children: <Widget>[
543
+ Icon(
544
+ isConnected ? Icons.check_circle_outlined : Icons.circle_outlined,
545
+ size: 18,
546
+ color: isConnected ? _success : _textSecondary,
547
+ ),
548
+ const SizedBox(width: 8),
549
+ Expanded(
550
+ child: Column(
551
+ crossAxisAlignment: CrossAxisAlignment.start,
552
+ children: <Widget>[
553
+ Text(
554
+ label,
555
+ style: TextStyle(fontSize: 12, color: _textSecondary),
556
+ ),
557
+ Text(
558
+ status,
559
+ style: TextStyle(
560
+ fontSize: 13,
561
+ fontWeight: FontWeight.w500,
562
+ color: isConnected ? _success : _textPrimary,
563
+ ),
564
+ ),
565
+ ],
566
+ ),
567
+ ),
568
+ ],
569
+ ),
570
+ );
571
+ }
572
+ }
573
+
574
+ class _OfficialIntegrationAppCard extends StatelessWidget {
575
+ const _OfficialIntegrationAppCard({
576
+ required this.controller,
577
+ required this.provider,
578
+ required this.app,
579
+ });
580
+
581
+ final NeoAgentController controller;
582
+ final OfficialIntegrationItem provider;
583
+ final OfficialIntegrationAppItem app;
584
+
585
+ @override
586
+ Widget build(BuildContext context) {
587
+ final connectBusy = controller.isOfficialIntegrationBusy(
588
+ '${provider.id}:${app.id}:connect',
589
+ );
590
+
591
+ return Container(
592
+ padding: const EdgeInsets.all(14),
593
+ decoration: BoxDecoration(
594
+ color: _bgPrimary,
595
+ borderRadius: BorderRadius.circular(14),
596
+ border: Border.all(color: _border),
597
+ ),
598
+ child: Column(
599
+ crossAxisAlignment: CrossAxisAlignment.start,
600
+ children: <Widget>[
601
+ Column(
602
+ crossAxisAlignment: CrossAxisAlignment.start,
603
+ children: <Widget>[
604
+ Row(
605
+ crossAxisAlignment: CrossAxisAlignment.start,
606
+ children: <Widget>[
607
+ Expanded(
608
+ child: Column(
609
+ crossAxisAlignment: CrossAxisAlignment.start,
610
+ children: <Widget>[
611
+ Row(
612
+ children: <Widget>[
613
+ Expanded(
614
+ child: Text(
615
+ app.label,
616
+ style: TextStyle(
617
+ fontSize: 16,
618
+ fontWeight: FontWeight.w700,
619
+ ),
620
+ ),
621
+ ),
622
+ _StatusPill(
623
+ label: app.statusLabel,
624
+ color: app.isConnected
625
+ ? _success
626
+ : app.hasExpiredAccounts
627
+ ? _warning
628
+ : _textSecondary,
629
+ ),
630
+ ],
631
+ ),
632
+ if ((app.description ?? '')
633
+ .trim()
634
+ .isNotEmpty) ...<Widget>[
635
+ const SizedBox(height: 4),
636
+ Text(
637
+ app.description!,
638
+ style: TextStyle(color: _textSecondary),
639
+ ),
640
+ ],
641
+ const SizedBox(height: 8),
642
+ Wrap(
643
+ spacing: 8,
644
+ runSpacing: 8,
645
+ children: <Widget>[
646
+ _MetaPill(
647
+ label: '${app.accounts.length} accounts',
648
+ icon: Icons.account_circle_outlined,
649
+ ),
650
+ _MetaPill(
651
+ label: '${app.availableToolCount} tools',
652
+ icon: Icons.build_circle_outlined,
653
+ ),
654
+ ],
655
+ ),
656
+ ],
657
+ ),
658
+ ),
659
+ ],
660
+ ),
661
+ const SizedBox(height: 12),
662
+ Align(
663
+ alignment: Alignment.centerLeft,
664
+ child: _buildIntegrationActionButton(context, connectBusy),
665
+ ),
666
+ ],
667
+ ),
668
+ const SizedBox(height: 12),
669
+ if (app.accounts.isEmpty)
670
+ Text(
671
+ 'No accounts connected yet.',
672
+ style: TextStyle(color: _textSecondary),
673
+ )
674
+ else
675
+ Column(
676
+ children: app.accounts.map((account) {
677
+ final disconnectBusy = controller.isOfficialIntegrationBusy(
678
+ '${provider.id}:${account.id}:disconnect',
679
+ );
680
+ final accessBusy = controller.isOfficialIntegrationBusy(
681
+ '${provider.id}:${account.id}:access_mode',
682
+ );
683
+ return Container(
684
+ margin: const EdgeInsets.only(bottom: 8),
685
+ padding: const EdgeInsets.all(12),
686
+ decoration: BoxDecoration(
687
+ color: _bgSecondary,
688
+ borderRadius: BorderRadius.circular(12),
689
+ border: Border.all(
690
+ color: account.connected ? _accentMuted : _border,
691
+ ),
692
+ ),
693
+ child: Column(
694
+ crossAxisAlignment: CrossAxisAlignment.start,
695
+ children: <Widget>[
696
+ Text(
697
+ account.accountEmail ?? 'Unknown account',
698
+ style: TextStyle(fontWeight: FontWeight.w700),
699
+ ),
700
+ const SizedBox(height: 4),
701
+ Text(
702
+ 'Connection #${account.id}',
703
+ style: TextStyle(color: _textSecondary),
704
+ ),
705
+ const SizedBox(height: 4),
706
+ Text(
707
+ 'Access: ${account.accessModeLabel}',
708
+ style: TextStyle(color: _textSecondary),
709
+ ),
710
+ const SizedBox(height: 12),
711
+ Wrap(
712
+ spacing: 8,
713
+ runSpacing: 8,
714
+ crossAxisAlignment: WrapCrossAlignment.center,
715
+ children: <Widget>[
716
+ PopupMenuButton<String>(
717
+ enabled: !accessBusy,
718
+ tooltip: 'Access mode',
719
+ onSelected: (value) {
720
+ if (value == account.accessMode) return;
721
+ controller.setOfficialIntegrationAccessMode(
722
+ provider.id,
723
+ connectionId: account.id,
724
+ accessMode: value,
725
+ );
726
+ },
727
+ itemBuilder: (context) =>
728
+ const <PopupMenuEntry<String>>[
729
+ PopupMenuItem<String>(
730
+ value: 'read_write',
731
+ child: Text('Read / Write'),
732
+ ),
733
+ PopupMenuItem<String>(
734
+ value: 'read_only',
735
+ child: Text('Read Only'),
736
+ ),
737
+ ],
738
+ child: Container(
739
+ padding: const EdgeInsets.symmetric(
740
+ horizontal: 10,
741
+ vertical: 8,
742
+ ),
743
+ decoration: BoxDecoration(
744
+ borderRadius: BorderRadius.circular(10),
745
+ border: Border.all(color: _border),
746
+ ),
747
+ child: Row(
748
+ mainAxisSize: MainAxisSize.min,
749
+ children: <Widget>[
750
+ Icon(
751
+ Icons.lock_open_rounded,
752
+ size: 16,
753
+ color: _textSecondary,
754
+ ),
755
+ const SizedBox(width: 6),
756
+ Text(
757
+ accessBusy
758
+ ? 'Saving...'
759
+ : account.accessModeLabel,
760
+ style: TextStyle(color: _textSecondary),
761
+ ),
762
+ ],
763
+ ),
764
+ ),
765
+ ),
766
+ _StatusPill(
767
+ label: account.statusLabel,
768
+ color: account.connected
769
+ ? _success
770
+ : account.isExpired
771
+ ? _warning
772
+ : _textSecondary,
773
+ ),
774
+ OutlinedButton.icon(
775
+ onPressed: disconnectBusy
776
+ ? null
777
+ : () =>
778
+ controller.disconnectOfficialIntegration(
779
+ provider.id,
780
+ connectionId: account.id,
781
+ ),
782
+ icon: Icon(Icons.link_off_rounded),
783
+ label: Text(
784
+ disconnectBusy ? 'Working...' : 'Disconnect',
785
+ ),
786
+ ),
787
+ ],
788
+ ),
789
+ ],
790
+ ),
791
+ );
792
+ }).toList(),
793
+ ),
794
+ ],
795
+ ),
796
+ );
797
+ }
798
+
799
+ Widget _buildIntegrationActionButton(BuildContext context, bool connectBusy) {
800
+ if (provider.connectionMethod == 'user_config') {
801
+ return FilledButton.icon(
802
+ onPressed: () => _openOfficialIntegrationSetupDialog(
803
+ context,
804
+ controller,
805
+ provider.id,
806
+ ),
807
+ icon: const Icon(Icons.settings_rounded),
808
+ label: Text(
809
+ provider.env.configured ? 'Manage Setup' : 'Complete Setup',
810
+ ),
811
+ );
812
+ }
813
+
814
+ if (!provider.env.configured) {
815
+ return provider.env.setupMode == 'user'
816
+ ? FilledButton.icon(
817
+ onPressed: () => _openOfficialIntegrationSetupDialog(
818
+ context,
819
+ controller,
820
+ provider.id,
821
+ ),
822
+ icon: const Icon(Icons.settings_rounded),
823
+ label: const Text('Configure'),
824
+ )
825
+ : OutlinedButton.icon(
826
+ onPressed: null,
827
+ icon: const Icon(Icons.settings_suggest_outlined),
828
+ label: const Text('Admin Setup Required'),
829
+ );
830
+ }
831
+
832
+ return FilledButton.icon(
833
+ onPressed: connectBusy
834
+ ? null
835
+ : () => controller.connectOfficialIntegration(
836
+ provider.id,
837
+ appId: app.id,
838
+ ),
839
+ icon: const Icon(Icons.link_rounded),
840
+ label: Text(
841
+ connectBusy
842
+ ? 'Connecting...'
843
+ : provider.supportsMultipleAccounts && app.isConnected
844
+ ? 'Add Account'
845
+ : 'Connect Account',
846
+ ),
847
+ );
848
+ }
849
+ }
850
+
851
+ class _OfficialIntegrationIcon extends StatelessWidget {
852
+ const _OfficialIntegrationIcon({required this.item});
853
+
854
+ final OfficialIntegrationItem item;
855
+
856
+ @override
857
+ Widget build(BuildContext context) {
858
+ final color = switch (item.icon) {
859
+ 'google' => const Color(0xFF4285F4),
860
+ 'trello' => const Color(0xFF0C66E4),
861
+ _ => _accent,
862
+ };
863
+ final label = switch (item.icon) {
864
+ 'google' => 'G',
865
+ 'trello' => 'T',
866
+ _ => item.label.isNotEmpty ? item.label[0] : '?',
867
+ };
868
+ return Container(
869
+ width: 44,
870
+ height: 44,
871
+ decoration: BoxDecoration(
872
+ color: color.withValues(alpha: 0.18),
873
+ borderRadius: BorderRadius.circular(14),
874
+ border: Border.all(color: color.withValues(alpha: 0.36)),
875
+ ),
876
+ alignment: Alignment.center,
877
+ child: Text(
878
+ label,
879
+ style: TextStyle(
880
+ color: color,
881
+ fontSize: 20,
882
+ fontWeight: FontWeight.w800,
883
+ ),
884
+ ),
885
+ );
886
+ }
887
+ }
888
+
889
+ class _IntegrationSectionTitle extends StatelessWidget {
890
+ const _IntegrationSectionTitle({required this.title});
891
+
892
+ final String title;
893
+
894
+ @override
895
+ Widget build(BuildContext context) {
896
+ return Padding(
897
+ padding: const EdgeInsets.only(top: 8, bottom: 12),
898
+ child: Text(
899
+ title,
900
+ style: TextStyle(
901
+ fontSize: 14,
902
+ fontWeight: FontWeight.w700,
903
+ color: _textSecondary,
904
+ letterSpacing: 0.5,
905
+ ),
906
+ ),
907
+ );
908
+ }
909
+ }