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,1129 @@
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 'home_assistant':
216
+ _showHomeAssistantSetupDialog(context, controller);
217
+ return;
218
+ case 'trello':
219
+ _showTrelloSetupDialog(context, controller);
220
+ return;
221
+ }
222
+ }
223
+
224
+ Future<void> _showHomeAssistantSetupDialog(
225
+ BuildContext context,
226
+ NeoAgentController controller,
227
+ ) async {
228
+ Map<String, dynamic> existing;
229
+ try {
230
+ existing = await controller.getOfficialIntegrationConfig('home_assistant');
231
+ } catch (error) {
232
+ if (context.mounted) {
233
+ ScaffoldMessenger.of(context).showSnackBar(
234
+ SnackBar(content: Text(controller.errorMessage ?? error.toString())),
235
+ );
236
+ }
237
+ return;
238
+ }
239
+
240
+ final baseUrlController = TextEditingController(
241
+ text: existing['baseUrl']?.toString() ?? '',
242
+ );
243
+ final clientIdController = TextEditingController(
244
+ text: existing['clientId']?.toString() ?? '',
245
+ );
246
+ final clientSecretController = TextEditingController();
247
+ final redirectUriController = TextEditingController(
248
+ text: existing['redirectUri']?.toString() ?? '',
249
+ );
250
+ final hasSavedSetup =
251
+ (existing['baseUrl']?.toString().trim().isNotEmpty ?? false) ||
252
+ (existing['clientId']?.toString().trim().isNotEmpty ?? false) ||
253
+ existing['hasClientSecret'] == true;
254
+ var formError = '';
255
+ var saving = false;
256
+
257
+ await showDialog<void>(
258
+ context: context,
259
+ builder: (dialogContext) {
260
+ return StatefulBuilder(
261
+ builder: (dialogContext, setState) {
262
+ return AlertDialog(
263
+ title: const Text('Home Assistant Setup'),
264
+ content: SizedBox(
265
+ width: 520,
266
+ child: Column(
267
+ mainAxisSize: MainAxisSize.min,
268
+ children: <Widget>[
269
+ TextField(
270
+ controller: baseUrlController,
271
+ decoration: const InputDecoration(
272
+ labelText: 'Base URL',
273
+ hintText: 'https://ha.example.com',
274
+ ),
275
+ ),
276
+ const SizedBox(height: 10),
277
+ TextField(
278
+ controller: clientIdController,
279
+ decoration: const InputDecoration(
280
+ labelText: 'OAuth Client ID',
281
+ ),
282
+ ),
283
+ const SizedBox(height: 10),
284
+ TextField(
285
+ controller: clientSecretController,
286
+ obscureText: true,
287
+ decoration: InputDecoration(
288
+ labelText: 'OAuth Client Secret',
289
+ hintText: existing['hasClientSecret'] == true
290
+ ? 'Saved secret exists. Enter to replace it.'
291
+ : null,
292
+ ),
293
+ ),
294
+ const SizedBox(height: 10),
295
+ TextField(
296
+ controller: redirectUriController,
297
+ decoration: const InputDecoration(
298
+ labelText: 'Redirect URI (optional)',
299
+ hintText: 'Leave blank to use the default callback URL',
300
+ ),
301
+ ),
302
+ if (formError.isNotEmpty) ...<Widget>[
303
+ const SizedBox(height: 10),
304
+ Align(
305
+ alignment: Alignment.centerLeft,
306
+ child: Text(formError, style: TextStyle(color: _danger)),
307
+ ),
308
+ ],
309
+ ],
310
+ ),
311
+ ),
312
+ actions: <Widget>[
313
+ if (hasSavedSetup)
314
+ TextButton(
315
+ onPressed: saving
316
+ ? null
317
+ : () async {
318
+ final shouldClear =
319
+ await showDialog<bool>(
320
+ context: dialogContext,
321
+ builder: (context) {
322
+ return AlertDialog(
323
+ title: const Text('Clear Setup?'),
324
+ content: const Text(
325
+ 'This removes your saved Home Assistant base URL and OAuth client credentials for this agent.',
326
+ ),
327
+ actions: [
328
+ TextButton(
329
+ onPressed: () =>
330
+ Navigator.of(context).pop(false),
331
+ child: const Text('Cancel'),
332
+ ),
333
+ FilledButton(
334
+ onPressed: () =>
335
+ Navigator.of(context).pop(true),
336
+ child: const Text('Clear Setup'),
337
+ ),
338
+ ],
339
+ );
340
+ },
341
+ ) ??
342
+ false;
343
+ if (!shouldClear) {
344
+ return;
345
+ }
346
+ setState(() {
347
+ formError = '';
348
+ saving = true;
349
+ });
350
+ try {
351
+ await controller.clearOfficialIntegrationConfig(
352
+ 'home_assistant',
353
+ );
354
+ if (dialogContext.mounted) {
355
+ Navigator.of(dialogContext).pop();
356
+ }
357
+ } catch (_) {
358
+ setState(() {
359
+ formError =
360
+ controller.errorMessage ??
361
+ 'Could not clear Home Assistant setup.';
362
+ saving = false;
363
+ });
364
+ }
365
+ },
366
+ child: const Text('Clear Setup'),
367
+ ),
368
+ TextButton(
369
+ onPressed: saving
370
+ ? null
371
+ : () => Navigator.of(dialogContext).pop(),
372
+ child: const Text('Cancel'),
373
+ ),
374
+ FilledButton(
375
+ onPressed: saving
376
+ ? null
377
+ : () async {
378
+ setState(() {
379
+ formError = '';
380
+ });
381
+ final baseUrl = baseUrlController.text.trim();
382
+ final clientId = clientIdController.text.trim();
383
+ final clientSecret = clientSecretController.text.trim();
384
+ final hasSavedSecret =
385
+ existing['hasClientSecret'] == true;
386
+ if (baseUrl.isEmpty || clientId.isEmpty) {
387
+ setState(() {
388
+ formError =
389
+ 'Base URL and OAuth Client ID are required.';
390
+ });
391
+ return;
392
+ }
393
+ if (clientSecret.isEmpty && !hasSavedSecret) {
394
+ setState(() {
395
+ formError = 'OAuth Client Secret is required.';
396
+ });
397
+ return;
398
+ }
399
+
400
+ setState(() {
401
+ saving = true;
402
+ });
403
+ try {
404
+ await controller.saveOfficialIntegrationConfig(
405
+ 'home_assistant',
406
+ config: <String, dynamic>{
407
+ 'baseUrl': baseUrl,
408
+ 'clientId': clientId,
409
+ if (clientSecret.isNotEmpty)
410
+ 'clientSecret': clientSecret,
411
+ 'redirectUri': redirectUriController.text.trim(),
412
+ },
413
+ );
414
+ if (dialogContext.mounted) {
415
+ Navigator.of(dialogContext).pop();
416
+ }
417
+ } catch (_) {
418
+ setState(() {
419
+ formError =
420
+ controller.errorMessage ??
421
+ 'Could not save Home Assistant setup.';
422
+ saving = false;
423
+ });
424
+ }
425
+ },
426
+ child: Text(saving ? 'Saving...' : 'Save Setup'),
427
+ ),
428
+ ],
429
+ );
430
+ },
431
+ );
432
+ },
433
+ );
434
+
435
+ baseUrlController.dispose();
436
+ clientIdController.dispose();
437
+ clientSecretController.dispose();
438
+ redirectUriController.dispose();
439
+ }
440
+
441
+ Future<void> _showTrelloSetupDialog(
442
+ BuildContext context,
443
+ NeoAgentController controller,
444
+ ) async {
445
+ Map<String, dynamic> existing;
446
+ try {
447
+ existing = await controller.getOfficialIntegrationConfig('trello');
448
+ } catch (error) {
449
+ if (context.mounted) {
450
+ ScaffoldMessenger.of(context).showSnackBar(
451
+ SnackBar(content: Text(controller.errorMessage ?? error.toString())),
452
+ );
453
+ }
454
+ return;
455
+ }
456
+
457
+ final apiKeyConfigured = existing['apiKeyConfigured'] == true;
458
+ final savedApiKey = existing['apiKey']?.toString() ?? '';
459
+ final apiKeyManagedByServer = apiKeyConfigured && savedApiKey.trim().isEmpty;
460
+ final authorizeUrl = existing['authorizeUrl']?.toString() ?? '';
461
+ final accountCount = (existing['accountCount'] as num?)?.toInt() ?? 0;
462
+ final hasConnectedAccount =
463
+ existing['hasConnectedAccount'] == true || accountCount > 0;
464
+ var formError = '';
465
+ var connecting = false;
466
+
467
+ final apiKeyController = TextEditingController(text: savedApiKey);
468
+ final tokenInputController = TextEditingController();
469
+
470
+ await showDialog<void>(
471
+ context: context,
472
+ barrierDismissible: false,
473
+ builder: (dialogContext) {
474
+ return StatefulBuilder(
475
+ builder: (dialogContext, setState) {
476
+ return AlertDialog(
477
+ title: const Text('Trello Setup'),
478
+ content: SizedBox(
479
+ width: 520,
480
+ child: Column(
481
+ mainAxisSize: MainAxisSize.min,
482
+ crossAxisAlignment: CrossAxisAlignment.start,
483
+ children: <Widget>[
484
+ Text(
485
+ '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.',
486
+ style: TextStyle(color: _textSecondary),
487
+ ),
488
+ const SizedBox(height: 16),
489
+ _TrelloStatusItem(
490
+ label: 'API Key',
491
+ status: apiKeyConfigured ? 'Configured' : 'Not configured',
492
+ isConnected: apiKeyConfigured,
493
+ ),
494
+ const SizedBox(height: 12),
495
+ _TrelloStatusItem(
496
+ label: 'Connected Account',
497
+ status: hasConnectedAccount
498
+ ? '$accountCount ${accountCount == 1 ? 'connected account' : 'connected accounts'}'
499
+ : 'Not connected',
500
+ isConnected: hasConnectedAccount,
501
+ ),
502
+ if (apiKeyManagedByServer) ...<Widget>[
503
+ const SizedBox(height: 12),
504
+ Text(
505
+ 'This agent is using a server-managed Trello API key. You only need to authorize an account token below.',
506
+ style: TextStyle(color: _textSecondary),
507
+ ),
508
+ ] else ...<Widget>[
509
+ const SizedBox(height: 12),
510
+ TextField(
511
+ controller: apiKeyController,
512
+ onChanged: (_) => setState(() {}),
513
+ obscureText: true,
514
+ decoration: const InputDecoration(
515
+ labelText: 'Trello API Key',
516
+ border: OutlineInputBorder(),
517
+ ),
518
+ ),
519
+ ],
520
+ if (apiKeyConfigured ||
521
+ apiKeyController.text.trim().isNotEmpty ||
522
+ apiKeyManagedByServer) ...<Widget>[
523
+ const SizedBox(height: 12),
524
+ TextField(
525
+ controller: tokenInputController,
526
+ onChanged: (_) => setState(() {}),
527
+ obscureText: true,
528
+ decoration: InputDecoration(
529
+ labelText: hasConnectedAccount
530
+ ? 'Paste a replacement token'
531
+ : 'Paste your account token',
532
+ border: OutlineInputBorder(),
533
+ ),
534
+ ),
535
+ ],
536
+ if (formError.isNotEmpty) ...<Widget>[
537
+ const SizedBox(height: 12),
538
+ Container(
539
+ padding: const EdgeInsets.all(8),
540
+ decoration: BoxDecoration(
541
+ color: _danger.withOpacity(0.1),
542
+ borderRadius: BorderRadius.circular(4),
543
+ border: Border.all(color: _danger.withOpacity(0.3)),
544
+ ),
545
+ child: Text(
546
+ formError,
547
+ style: TextStyle(color: _danger, fontSize: 12),
548
+ ),
549
+ ),
550
+ ],
551
+ ],
552
+ ),
553
+ ),
554
+ actions: <Widget>[
555
+ if (apiKeyConfigured || savedApiKey.trim().isNotEmpty)
556
+ TextButton(
557
+ onPressed: connecting
558
+ ? null
559
+ : () async {
560
+ final shouldClear =
561
+ await showDialog<bool>(
562
+ context: dialogContext,
563
+ builder: (context) {
564
+ return AlertDialog(
565
+ title: const Text('Disconnect Trello?'),
566
+ content: const Text(
567
+ 'This removes the Trello setup and connected accounts for this agent.',
568
+ ),
569
+ actions: [
570
+ TextButton(
571
+ onPressed: () =>
572
+ Navigator.of(context).pop(false),
573
+ child: const Text('Cancel'),
574
+ ),
575
+ FilledButton(
576
+ onPressed: () =>
577
+ Navigator.of(context).pop(true),
578
+ child: const Text('Disconnect'),
579
+ ),
580
+ ],
581
+ );
582
+ },
583
+ ) ??
584
+ false;
585
+ if (!shouldClear) {
586
+ return;
587
+ }
588
+ setState(() {
589
+ formError = '';
590
+ connecting = true;
591
+ });
592
+ try {
593
+ await controller.clearOfficialIntegrationConfig(
594
+ 'trello',
595
+ );
596
+ if (dialogContext.mounted) {
597
+ Navigator.of(dialogContext).pop();
598
+ }
599
+ } catch (_) {
600
+ setState(() {
601
+ formError =
602
+ controller.errorMessage ??
603
+ 'Could not disconnect Trello.';
604
+ connecting = false;
605
+ });
606
+ }
607
+ },
608
+ child: const Text('Disconnect'),
609
+ ),
610
+ TextButton(
611
+ onPressed: connecting
612
+ ? null
613
+ : () => Navigator.of(dialogContext).pop(),
614
+ child: const Text('Close'),
615
+ ),
616
+ if (authorizeUrl.isNotEmpty ||
617
+ apiKeyManagedByServer ||
618
+ apiKeyController.text.trim().isNotEmpty)
619
+ FilledButton.icon(
620
+ onPressed: connecting
621
+ ? null
622
+ : () async {
623
+ setState(() {
624
+ formError = '';
625
+ connecting = true;
626
+ });
627
+ try {
628
+ final effectiveApiKey = apiKeyManagedByServer
629
+ ? ''
630
+ : apiKeyController.text.trim();
631
+ if (!apiKeyManagedByServer &&
632
+ effectiveApiKey.isEmpty) {
633
+ setState(() {
634
+ formError = 'Trello API Key is required.';
635
+ connecting = false;
636
+ });
637
+ return;
638
+ }
639
+ final url = authorizeUrl.isNotEmpty
640
+ ? authorizeUrl
641
+ : 'https://trello.com/1/authorize?expiration=never&scope=read,write,account&response_type=token&key=' +
642
+ Uri.encodeComponent(effectiveApiKey);
643
+ final result = await controller._oauthLauncher
644
+ .openExternal(url: url, label: 'Trello');
645
+ if (!result.launched) {
646
+ setState(() {
647
+ formError =
648
+ result.error ??
649
+ 'Could not open Trello in your browser.';
650
+ connecting = false;
651
+ });
652
+ } else {
653
+ setState(() {
654
+ connecting = false;
655
+ });
656
+ }
657
+ } catch (error) {
658
+ setState(() {
659
+ formError = error.toString();
660
+ connecting = false;
661
+ });
662
+ }
663
+ },
664
+ icon: const Icon(Icons.open_in_browser_rounded),
665
+ label: Text(connecting ? 'Opening...' : 'Open Trello'),
666
+ ),
667
+ FilledButton(
668
+ onPressed: connecting
669
+ ? null
670
+ : () async {
671
+ setState(() {
672
+ formError = '';
673
+ connecting = true;
674
+ });
675
+ try {
676
+ final apiKey = apiKeyController.text.trim();
677
+ final token = tokenInputController.text.trim();
678
+ if (!apiKeyManagedByServer && apiKey.isEmpty) {
679
+ setState(() {
680
+ formError = 'Trello API Key is required.';
681
+ connecting = false;
682
+ });
683
+ return;
684
+ }
685
+ if (token.isEmpty &&
686
+ apiKeyConfigured &&
687
+ !apiKeyManagedByServer) {
688
+ await controller.saveOfficialIntegrationConfig(
689
+ 'trello',
690
+ config: <String, dynamic>{'apiKey': apiKey},
691
+ );
692
+ } else if (token.isEmpty && !apiKeyManagedByServer) {
693
+ await controller.saveOfficialIntegrationConfig(
694
+ 'trello',
695
+ config: <String, dynamic>{'apiKey': apiKey},
696
+ );
697
+ } else {
698
+ await controller.saveOfficialIntegrationConfig(
699
+ 'trello',
700
+ config: <String, dynamic>{
701
+ if (!apiKeyManagedByServer) 'apiKey': apiKey,
702
+ 'token': token,
703
+ },
704
+ );
705
+ }
706
+ if (dialogContext.mounted) {
707
+ Navigator.of(dialogContext).pop();
708
+ }
709
+ } catch (_) {
710
+ setState(() {
711
+ formError =
712
+ controller.errorMessage ??
713
+ 'Could not save Trello setup.';
714
+ connecting = false;
715
+ });
716
+ }
717
+ },
718
+ child: Text(
719
+ connecting
720
+ ? 'Saving...'
721
+ : tokenInputController.text.trim().isNotEmpty
722
+ ? hasConnectedAccount
723
+ ? 'Replace Account'
724
+ : 'Connect Account'
725
+ : 'Save Setup',
726
+ ),
727
+ ),
728
+ ],
729
+ );
730
+ },
731
+ );
732
+ },
733
+ );
734
+
735
+ apiKeyController.dispose();
736
+ tokenInputController.dispose();
737
+ }
738
+
739
+ class _TrelloStatusItem extends StatelessWidget {
740
+ const _TrelloStatusItem({
741
+ required this.label,
742
+ required this.status,
743
+ required this.isConnected,
744
+ });
745
+
746
+ final String label;
747
+ final String status;
748
+ final bool isConnected;
749
+
750
+ @override
751
+ Widget build(BuildContext context) {
752
+ return Container(
753
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
754
+ decoration: BoxDecoration(
755
+ color: _bgSecondary,
756
+ borderRadius: BorderRadius.circular(8),
757
+ border: Border.all(
758
+ color: isConnected ? _success.withOpacity(0.3) : _border,
759
+ ),
760
+ ),
761
+ child: Row(
762
+ children: <Widget>[
763
+ Icon(
764
+ isConnected ? Icons.check_circle_outlined : Icons.circle_outlined,
765
+ size: 18,
766
+ color: isConnected ? _success : _textSecondary,
767
+ ),
768
+ const SizedBox(width: 8),
769
+ Expanded(
770
+ child: Column(
771
+ crossAxisAlignment: CrossAxisAlignment.start,
772
+ children: <Widget>[
773
+ Text(
774
+ label,
775
+ style: TextStyle(fontSize: 12, color: _textSecondary),
776
+ ),
777
+ Text(
778
+ status,
779
+ style: TextStyle(
780
+ fontSize: 13,
781
+ fontWeight: FontWeight.w500,
782
+ color: isConnected ? _success : _textPrimary,
783
+ ),
784
+ ),
785
+ ],
786
+ ),
787
+ ),
788
+ ],
789
+ ),
790
+ );
791
+ }
792
+ }
793
+
794
+ class _OfficialIntegrationAppCard extends StatelessWidget {
795
+ const _OfficialIntegrationAppCard({
796
+ required this.controller,
797
+ required this.provider,
798
+ required this.app,
799
+ });
800
+
801
+ final NeoAgentController controller;
802
+ final OfficialIntegrationItem provider;
803
+ final OfficialIntegrationAppItem app;
804
+
805
+ @override
806
+ Widget build(BuildContext context) {
807
+ final connectBusy = controller.isOfficialIntegrationBusy(
808
+ '${provider.id}:${app.id}:connect',
809
+ );
810
+
811
+ return Container(
812
+ padding: const EdgeInsets.all(14),
813
+ decoration: BoxDecoration(
814
+ color: _bgPrimary,
815
+ borderRadius: BorderRadius.circular(14),
816
+ border: Border.all(color: _border),
817
+ ),
818
+ child: Column(
819
+ crossAxisAlignment: CrossAxisAlignment.start,
820
+ children: <Widget>[
821
+ Column(
822
+ crossAxisAlignment: CrossAxisAlignment.start,
823
+ children: <Widget>[
824
+ Row(
825
+ crossAxisAlignment: CrossAxisAlignment.start,
826
+ children: <Widget>[
827
+ Expanded(
828
+ child: Column(
829
+ crossAxisAlignment: CrossAxisAlignment.start,
830
+ children: <Widget>[
831
+ Row(
832
+ children: <Widget>[
833
+ Expanded(
834
+ child: Text(
835
+ app.label,
836
+ style: TextStyle(
837
+ fontSize: 16,
838
+ fontWeight: FontWeight.w700,
839
+ ),
840
+ ),
841
+ ),
842
+ _StatusPill(
843
+ label: app.statusLabel,
844
+ color: app.isConnected
845
+ ? _success
846
+ : app.hasExpiredAccounts
847
+ ? _warning
848
+ : _textSecondary,
849
+ ),
850
+ ],
851
+ ),
852
+ if ((app.description ?? '')
853
+ .trim()
854
+ .isNotEmpty) ...<Widget>[
855
+ const SizedBox(height: 4),
856
+ Text(
857
+ app.description!,
858
+ style: TextStyle(color: _textSecondary),
859
+ ),
860
+ ],
861
+ const SizedBox(height: 8),
862
+ Wrap(
863
+ spacing: 8,
864
+ runSpacing: 8,
865
+ children: <Widget>[
866
+ _MetaPill(
867
+ label: '${app.accounts.length} accounts',
868
+ icon: Icons.account_circle_outlined,
869
+ ),
870
+ _MetaPill(
871
+ label: '${app.availableToolCount} tools',
872
+ icon: Icons.build_circle_outlined,
873
+ ),
874
+ ],
875
+ ),
876
+ ],
877
+ ),
878
+ ),
879
+ ],
880
+ ),
881
+ const SizedBox(height: 12),
882
+ Align(
883
+ alignment: Alignment.centerLeft,
884
+ child: _buildIntegrationActionButton(context, connectBusy),
885
+ ),
886
+ ],
887
+ ),
888
+ const SizedBox(height: 12),
889
+ if (app.accounts.isEmpty)
890
+ Text(
891
+ 'No accounts connected yet.',
892
+ style: TextStyle(color: _textSecondary),
893
+ )
894
+ else
895
+ Column(
896
+ children: app.accounts.map((account) {
897
+ final disconnectBusy = controller.isOfficialIntegrationBusy(
898
+ '${provider.id}:${account.id}:disconnect',
899
+ );
900
+ final accessBusy = controller.isOfficialIntegrationBusy(
901
+ '${provider.id}:${account.id}:access_mode',
902
+ );
903
+ return Container(
904
+ margin: const EdgeInsets.only(bottom: 8),
905
+ padding: const EdgeInsets.all(12),
906
+ decoration: BoxDecoration(
907
+ color: _bgSecondary,
908
+ borderRadius: BorderRadius.circular(12),
909
+ border: Border.all(
910
+ color: account.connected ? _accentMuted : _border,
911
+ ),
912
+ ),
913
+ child: Column(
914
+ crossAxisAlignment: CrossAxisAlignment.start,
915
+ children: <Widget>[
916
+ Text(
917
+ account.accountEmail ?? 'Unknown account',
918
+ style: TextStyle(fontWeight: FontWeight.w700),
919
+ ),
920
+ const SizedBox(height: 4),
921
+ Text(
922
+ 'Connection #${account.id}',
923
+ style: TextStyle(color: _textSecondary),
924
+ ),
925
+ const SizedBox(height: 4),
926
+ Text(
927
+ 'Access: ${account.accessModeLabel}',
928
+ style: TextStyle(color: _textSecondary),
929
+ ),
930
+ const SizedBox(height: 12),
931
+ Wrap(
932
+ spacing: 8,
933
+ runSpacing: 8,
934
+ crossAxisAlignment: WrapCrossAlignment.center,
935
+ children: <Widget>[
936
+ PopupMenuButton<String>(
937
+ enabled: !accessBusy,
938
+ tooltip: 'Access mode',
939
+ onSelected: (value) {
940
+ if (value == account.accessMode) return;
941
+ controller.setOfficialIntegrationAccessMode(
942
+ provider.id,
943
+ connectionId: account.id,
944
+ accessMode: value,
945
+ );
946
+ },
947
+ itemBuilder: (context) =>
948
+ const <PopupMenuEntry<String>>[
949
+ PopupMenuItem<String>(
950
+ value: 'read_write',
951
+ child: Text('Read / Write'),
952
+ ),
953
+ PopupMenuItem<String>(
954
+ value: 'read_only',
955
+ child: Text('Read Only'),
956
+ ),
957
+ ],
958
+ child: Container(
959
+ padding: const EdgeInsets.symmetric(
960
+ horizontal: 10,
961
+ vertical: 8,
962
+ ),
963
+ decoration: BoxDecoration(
964
+ borderRadius: BorderRadius.circular(10),
965
+ border: Border.all(color: _border),
966
+ ),
967
+ child: Row(
968
+ mainAxisSize: MainAxisSize.min,
969
+ children: <Widget>[
970
+ Icon(
971
+ Icons.lock_open_rounded,
972
+ size: 16,
973
+ color: _textSecondary,
974
+ ),
975
+ const SizedBox(width: 6),
976
+ Text(
977
+ accessBusy
978
+ ? 'Saving...'
979
+ : account.accessModeLabel,
980
+ style: TextStyle(color: _textSecondary),
981
+ ),
982
+ ],
983
+ ),
984
+ ),
985
+ ),
986
+ _StatusPill(
987
+ label: account.statusLabel,
988
+ color: account.connected
989
+ ? _success
990
+ : account.isExpired
991
+ ? _warning
992
+ : _textSecondary,
993
+ ),
994
+ OutlinedButton.icon(
995
+ onPressed: disconnectBusy
996
+ ? null
997
+ : () =>
998
+ controller.disconnectOfficialIntegration(
999
+ provider.id,
1000
+ connectionId: account.id,
1001
+ ),
1002
+ icon: Icon(Icons.link_off_rounded),
1003
+ label: Text(
1004
+ disconnectBusy ? 'Working...' : 'Disconnect',
1005
+ ),
1006
+ ),
1007
+ ],
1008
+ ),
1009
+ ],
1010
+ ),
1011
+ );
1012
+ }).toList(),
1013
+ ),
1014
+ ],
1015
+ ),
1016
+ );
1017
+ }
1018
+
1019
+ Widget _buildIntegrationActionButton(BuildContext context, bool connectBusy) {
1020
+ if (provider.connectionMethod == 'user_config') {
1021
+ return FilledButton.icon(
1022
+ onPressed: () => _openOfficialIntegrationSetupDialog(
1023
+ context,
1024
+ controller,
1025
+ provider.id,
1026
+ ),
1027
+ icon: const Icon(Icons.settings_rounded),
1028
+ label: Text(
1029
+ provider.env.configured ? 'Manage Setup' : 'Complete Setup',
1030
+ ),
1031
+ );
1032
+ }
1033
+
1034
+ if (!provider.env.configured) {
1035
+ return provider.env.setupMode == 'user'
1036
+ ? FilledButton.icon(
1037
+ onPressed: () => _openOfficialIntegrationSetupDialog(
1038
+ context,
1039
+ controller,
1040
+ provider.id,
1041
+ ),
1042
+ icon: const Icon(Icons.settings_rounded),
1043
+ label: const Text('Configure'),
1044
+ )
1045
+ : OutlinedButton.icon(
1046
+ onPressed: null,
1047
+ icon: const Icon(Icons.settings_suggest_outlined),
1048
+ label: const Text('Admin Setup Required'),
1049
+ );
1050
+ }
1051
+
1052
+ return FilledButton.icon(
1053
+ onPressed: connectBusy
1054
+ ? null
1055
+ : () => controller.connectOfficialIntegration(
1056
+ provider.id,
1057
+ appId: app.id,
1058
+ ),
1059
+ icon: const Icon(Icons.link_rounded),
1060
+ label: Text(
1061
+ connectBusy
1062
+ ? 'Connecting...'
1063
+ : provider.supportsMultipleAccounts && app.isConnected
1064
+ ? 'Add Account'
1065
+ : 'Connect Account',
1066
+ ),
1067
+ );
1068
+ }
1069
+ }
1070
+
1071
+ class _OfficialIntegrationIcon extends StatelessWidget {
1072
+ const _OfficialIntegrationIcon({required this.item});
1073
+
1074
+ final OfficialIntegrationItem item;
1075
+
1076
+ @override
1077
+ Widget build(BuildContext context) {
1078
+ final color = switch (item.icon) {
1079
+ 'google' => const Color(0xFF4285F4),
1080
+ 'trello' => const Color(0xFF0C66E4),
1081
+ _ => _accent,
1082
+ };
1083
+ final label = switch (item.icon) {
1084
+ 'google' => 'G',
1085
+ 'trello' => 'T',
1086
+ _ => item.label.isNotEmpty ? item.label[0] : '?',
1087
+ };
1088
+ return Container(
1089
+ width: 44,
1090
+ height: 44,
1091
+ decoration: BoxDecoration(
1092
+ color: color.withValues(alpha: 0.18),
1093
+ borderRadius: BorderRadius.circular(14),
1094
+ border: Border.all(color: color.withValues(alpha: 0.36)),
1095
+ ),
1096
+ alignment: Alignment.center,
1097
+ child: Text(
1098
+ label,
1099
+ style: TextStyle(
1100
+ color: color,
1101
+ fontSize: 20,
1102
+ fontWeight: FontWeight.w800,
1103
+ ),
1104
+ ),
1105
+ );
1106
+ }
1107
+ }
1108
+
1109
+ class _IntegrationSectionTitle extends StatelessWidget {
1110
+ const _IntegrationSectionTitle({required this.title});
1111
+
1112
+ final String title;
1113
+
1114
+ @override
1115
+ Widget build(BuildContext context) {
1116
+ return Padding(
1117
+ padding: const EdgeInsets.only(top: 8, bottom: 12),
1118
+ child: Text(
1119
+ title,
1120
+ style: TextStyle(
1121
+ fontSize: 14,
1122
+ fontWeight: FontWeight.w700,
1123
+ color: _textSecondary,
1124
+ letterSpacing: 0.5,
1125
+ ),
1126
+ ),
1127
+ );
1128
+ }
1129
+ }