neoagent 2.3.1-beta.2 → 2.3.1-beta.21

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