neoagent 2.3.1-beta.6 → 2.3.1-beta.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/.env.example +45 -0
  2. package/docs/capabilities.md +2 -2
  3. package/docs/configuration.md +6 -5
  4. package/docs/hardware.md +1 -1
  5. package/docs/integrations.md +2 -3
  6. package/flutter_app/.metadata +42 -0
  7. package/flutter_app/README.md +21 -0
  8. package/flutter_app/analysis_options.yaml +32 -0
  9. package/flutter_app/android/app/build.gradle.kts +111 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +171 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +804 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/auto/NeoAgentCarAppService.kt +114 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnection.kt +118 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnectionService.kt +86 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  30. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  31. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  32. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  33. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  35. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  36. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  37. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  38. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  39. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  43. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  44. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  45. package/flutter_app/android/app/src/main/res/values/arrays.xml +8 -0
  46. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  48. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  49. package/flutter_app/android/app/src/main/res/xml/automotive_app_desc.xml +4 -0
  50. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  51. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  52. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  53. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  54. package/flutter_app/android/build.gradle.kts +24 -0
  55. package/flutter_app/android/ci-release.keystore +0 -0
  56. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  57. package/flutter_app/android/gradle.properties +3 -0
  58. package/flutter_app/android/key.properties +4 -0
  59. package/flutter_app/android/settings.gradle.kts +26 -0
  60. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  62. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  63. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  64. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  65. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  66. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  67. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  68. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  69. package/flutter_app/lib/features/location/location_service.dart +119 -0
  70. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  71. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +569 -0
  72. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +157 -0
  73. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +233 -0
  74. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +57 -0
  75. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +218 -0
  76. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +46 -0
  77. package/flutter_app/lib/main.dart +103 -0
  78. package/flutter_app/lib/main_account_settings.dart +1250 -0
  79. package/flutter_app/lib/main_admin.dart +886 -0
  80. package/flutter_app/lib/main_app_shell.dart +2134 -0
  81. package/flutter_app/lib/main_chat.dart +3316 -0
  82. package/flutter_app/lib/main_controller.dart +6836 -0
  83. package/flutter_app/lib/main_devices.dart +2396 -0
  84. package/flutter_app/lib/main_integrations.dart +909 -0
  85. package/flutter_app/lib/main_launcher.dart +959 -0
  86. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  87. package/flutter_app/lib/main_models.dart +3539 -0
  88. package/flutter_app/lib/main_navigation.dart +188 -0
  89. package/flutter_app/lib/main_operations.dart +4851 -0
  90. package/flutter_app/lib/main_recordings.dart +920 -0
  91. package/flutter_app/lib/main_runtime.dart +838 -0
  92. package/flutter_app/lib/main_settings.dart +2040 -0
  93. package/flutter_app/lib/main_shared.dart +3147 -0
  94. package/flutter_app/lib/main_theme.dart +257 -0
  95. package/flutter_app/lib/main_voice_assistant.dart +957 -0
  96. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  97. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  98. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  99. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  100. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  101. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  102. package/flutter_app/lib/src/android_auto_bridge.dart +59 -0
  103. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  104. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  105. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  106. package/flutter_app/lib/src/backend_client.dart +1837 -0
  107. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  108. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  109. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  110. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  111. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  112. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  113. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  114. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  115. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  116. package/flutter_app/lib/src/health_bridge.dart +136 -0
  117. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  118. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  119. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  120. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  121. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  122. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  123. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  124. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  125. package/flutter_app/lib/src/oauth_launcher_io.dart +95 -0
  126. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  127. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  128. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  129. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  130. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  131. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  132. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  133. package/flutter_app/lib/src/theme/palette.dart +81 -0
  134. package/flutter_app/lib/src/web_app_update_monitor.dart +17 -0
  135. package/flutter_app/lib/src/web_app_update_monitor_stub.dart +24 -0
  136. package/flutter_app/lib/src/web_app_update_monitor_web.dart +123 -0
  137. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  138. package/flutter_app/linux/CMakeLists.txt +128 -0
  139. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  140. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +47 -0
  141. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  142. package/flutter_app/linux/flutter/generated_plugins.cmake +32 -0
  143. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  144. package/flutter_app/linux/runner/main.cc +6 -0
  145. package/flutter_app/linux/runner/my_application.cc +144 -0
  146. package/flutter_app/linux/runner/my_application.h +18 -0
  147. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  148. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  149. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  150. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +44 -0
  151. package/flutter_app/macos/Podfile +42 -0
  152. package/flutter_app/macos/Podfile.lock +87 -0
  153. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  154. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  155. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  156. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  157. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  158. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  159. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  160. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  161. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  162. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  163. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  164. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  165. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  166. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  167. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  168. package/flutter_app/macos/Runner/Info.plist +36 -0
  169. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  170. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  171. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  172. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  173. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  174. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  175. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  176. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  177. package/flutter_app/patch_strings.py +12 -0
  178. package/flutter_app/pubspec.lock +1224 -0
  179. package/flutter_app/pubspec.yaml +57 -0
  180. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  181. package/flutter_app/test/recording_payloads_test.dart +53 -0
  182. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  183. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  184. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  185. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  186. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  187. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  188. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  189. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  190. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  191. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  192. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  193. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  194. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  195. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  196. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  197. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  198. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  199. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  200. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  201. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  202. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  203. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  204. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  205. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  206. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  207. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  208. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  209. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  210. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  211. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  212. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  213. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  214. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  215. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  216. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  217. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  218. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  219. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  220. package/flutter_app/web/favicon.png +0 -0
  221. package/flutter_app/web/favicon.svg +12 -0
  222. package/flutter_app/web/icons/Icon-192.png +0 -0
  223. package/flutter_app/web/icons/Icon-512.png +0 -0
  224. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  225. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  226. package/flutter_app/web/index.html +39 -0
  227. package/flutter_app/web/manifest.json +35 -0
  228. package/flutter_app/windows/CMakeLists.txt +108 -0
  229. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  230. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +50 -0
  231. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  232. package/flutter_app/windows/flutter/generated_plugins.cmake +36 -0
  233. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  234. package/flutter_app/windows/runner/Runner.rc +121 -0
  235. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  236. package/flutter_app/windows/runner/flutter_window.h +37 -0
  237. package/flutter_app/windows/runner/main.cpp +53 -0
  238. package/flutter_app/windows/runner/resource.h +16 -0
  239. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  240. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  241. package/flutter_app/windows/runner/utils.cpp +65 -0
  242. package/flutter_app/windows/runner/utils.h +19 -0
  243. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  244. package/flutter_app/windows/runner/win32_window.h +102 -0
  245. package/lib/install_helpers.js +31 -0
  246. package/lib/manager.js +201 -6
  247. package/package.json +5 -3
  248. package/server/db/database.js +125 -1
  249. package/server/http/middleware.js +55 -2
  250. package/server/http/routes.js +1 -0
  251. package/server/http/static.js +41 -1
  252. package/server/index.js +3 -0
  253. package/server/public/.last_build_id +1 -1
  254. package/server/public/assets/AssetManifest.bin +1 -1
  255. package/server/public/assets/AssetManifest.bin.json +1 -1
  256. package/server/public/assets/NOTICES +281 -1
  257. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  258. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  259. package/server/public/canvaskit/wimp.wasm +0 -0
  260. package/server/public/flutter_bootstrap.js +2 -2
  261. package/server/public/main.dart.js +86314 -82363
  262. package/server/routes/auth.js +26 -5
  263. package/server/routes/integrations.js +108 -1
  264. package/server/routes/memory.js +11 -2
  265. package/server/routes/settings.js +84 -2
  266. package/server/routes/wearable.js +67 -0
  267. package/server/services/ai/engine.js +3 -2
  268. package/server/services/ai/models.js +30 -0
  269. package/server/services/ai/providers/githubCopilot.js +97 -0
  270. package/server/services/ai/providers/openai.js +2 -1
  271. package/server/services/ai/providers/openaiCodex.js +31 -0
  272. package/server/services/ai/settings.js +20 -0
  273. package/server/services/ai/toolSelector.js +54 -1
  274. package/server/services/ai/tools.js +88 -5
  275. package/server/services/desktop/screenRecorder.js +65 -9
  276. package/server/services/integrations/env.js +5 -0
  277. package/server/services/integrations/figma/provider.js +1 -0
  278. package/server/services/integrations/github/common.js +106 -0
  279. package/server/services/integrations/github/provider.js +499 -0
  280. package/server/services/integrations/github/repos.js +1124 -0
  281. package/server/services/integrations/google/provider.js +1 -0
  282. package/server/services/integrations/manager.js +88 -12
  283. package/server/services/integrations/microsoft/provider.js +1 -0
  284. package/server/services/integrations/oauth_provider.js +25 -8
  285. package/server/services/integrations/provider_config_store.js +85 -0
  286. package/server/services/integrations/registry.js +4 -2
  287. package/server/services/integrations/spotify/provider.js +1 -0
  288. package/server/services/integrations/trello/provider.js +842 -0
  289. package/server/services/manager.js +46 -1
  290. package/server/services/mcp/client.js +120 -23
  291. package/server/services/memory/manager.js +39 -2
  292. package/server/services/messaging/access_policy.js +10 -0
  293. package/server/services/messaging/manager.js +49 -0
  294. package/server/services/messaging/meshtastic.js +277 -0
  295. package/server/services/messaging/meshtastic_env.js +40 -0
  296. package/server/services/messaging/meshtastic_protocol.js +530 -0
  297. package/server/services/messaging/meshtastic_protocol.test.js +33 -0
  298. package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
  299. package/server/services/tasks/runtime.js +1 -1
  300. package/server/services/voice/openaiClient.js +4 -1
  301. package/server/services/voice/openaiSpeech.js +6 -1
  302. package/server/services/voice/providers.js +144 -15
  303. package/server/services/voice/runtimeManager.js +156 -19
  304. package/server/services/voice/turnRunner.js +29 -9
  305. package/server/services/wearable/firmware_manifest.js +353 -0
  306. package/server/services/wearable/gateway.js +350 -0
  307. package/server/services/wearable/protocol.js +45 -0
  308. package/server/services/wearable/service.js +240 -0
  309. package/server/services/widgets/service.js +3 -0
  310. package/server/utils/local_secrets.js +56 -0
  311. package/server/utils/logger.js +37 -9
  312. package/server/services/integrations/home_assistant/provider.js +0 -350
@@ -0,0 +1,920 @@
1
+ part of 'main.dart';
2
+
3
+ class RecordingsPanel extends StatefulWidget {
4
+ const RecordingsPanel({super.key, required this.controller});
5
+
6
+ final NeoAgentController controller;
7
+
8
+ @override
9
+ State<RecordingsPanel> createState() => _RecordingsPanelState();
10
+ }
11
+
12
+ class _RecordingsPanelState extends State<RecordingsPanel> {
13
+ Future<void> _deleteSegment(
14
+ BuildContext context,
15
+ RecordingSessionItem session,
16
+ RecordingTranscriptSegment segment,
17
+ ) async {
18
+ await _confirmDelete(
19
+ context,
20
+ title: 'Delete segment?',
21
+ message:
22
+ 'Remove the transcript segment at ${segment.timestampLabel} from "${session.title}"?',
23
+ onConfirm: () =>
24
+ widget.controller.deleteRecordingSegment(session.id, segment.id),
25
+ );
26
+ }
27
+
28
+ Future<void> _deleteRecording(
29
+ BuildContext context,
30
+ RecordingSessionItem session,
31
+ ) async {
32
+ await _confirmDelete(
33
+ context,
34
+ title: 'Delete recording?',
35
+ message:
36
+ 'Remove the full recording "${session.title}", including audio chunks and transcript data?',
37
+ onConfirm: () => widget.controller.deleteRecordingSession(session.id),
38
+ );
39
+ }
40
+
41
+ @override
42
+ Widget build(BuildContext context) {
43
+ final runtime = widget.controller.recordingRuntime;
44
+ final isStarting = widget.controller.isStartingRecording;
45
+ final isStopping = widget.controller.isStoppingRecording;
46
+ final statusLabel = isStarting
47
+ ? 'Starting'
48
+ : isStopping
49
+ ? 'Stopping'
50
+ : runtime.active
51
+ ? (runtime.paused ? 'Paused' : 'Recording')
52
+ : 'Ready';
53
+ final statusColor = isStarting || isStopping
54
+ ? _accent
55
+ : runtime.active
56
+ ? (runtime.paused ? _warning : _danger)
57
+ : _success;
58
+
59
+ return ListView(
60
+ padding: _pagePadding(context),
61
+ children: <Widget>[
62
+ const _PageTitle(
63
+ title: 'Recordings',
64
+ subtitle:
65
+ 'Capture sessions, monitor device state, and review transcripts with clearer status and playback controls.',
66
+ ),
67
+ Card(
68
+ child: Padding(
69
+ padding: const EdgeInsets.all(22),
70
+ child: Column(
71
+ crossAxisAlignment: CrossAxisAlignment.start,
72
+ children: <Widget>[
73
+ Row(
74
+ crossAxisAlignment: CrossAxisAlignment.start,
75
+ children: <Widget>[
76
+ Expanded(
77
+ child: Column(
78
+ crossAxisAlignment: CrossAxisAlignment.start,
79
+ children: <Widget>[
80
+ Text(
81
+ 'Capture controls',
82
+ style: _displayTitleStyle(24),
83
+ ),
84
+ const SizedBox(height: 6),
85
+ Text(
86
+ runtime.platformLabel?.trim().isNotEmpty == true
87
+ ? runtime.platformLabel!
88
+ : 'Browser screen + microphone recorder',
89
+ style: TextStyle(
90
+ color: _textSecondary,
91
+ fontSize: 15,
92
+ height: 1.45,
93
+ ),
94
+ ),
95
+ ],
96
+ ),
97
+ ),
98
+ const SizedBox(width: 16),
99
+ _StatusPill(label: statusLabel, color: statusColor),
100
+ ],
101
+ ),
102
+ const SizedBox(height: 16),
103
+ AnimatedSwitcher(
104
+ duration: const Duration(milliseconds: 220),
105
+ switchInCurve: Curves.easeOutCubic,
106
+ switchOutCurve: Curves.easeInCubic,
107
+ child: isStarting
108
+ ? _GlassSurface(
109
+ key: const ValueKey<String>('starting'),
110
+ width: double.infinity,
111
+ padding: const EdgeInsets.symmetric(
112
+ horizontal: 14,
113
+ vertical: 12,
114
+ ),
115
+ borderRadius: BorderRadius.circular(16),
116
+ blurSigma: 10,
117
+ fillColor: _bgCard.withValues(alpha: 0.88),
118
+ borderColor: _borderLight,
119
+ child: Row(
120
+ children: <Widget>[
121
+ const SizedBox.square(
122
+ dimension: 16,
123
+ child: CircularProgressIndicator(
124
+ strokeWidth: 2,
125
+ ),
126
+ ),
127
+ const SizedBox(width: 12),
128
+ Expanded(
129
+ child: Text(
130
+ 'Starting recording. This can take a few seconds while the session and permissions are prepared.',
131
+ style: TextStyle(
132
+ color: _textSecondary,
133
+ height: 1.4,
134
+ ),
135
+ ),
136
+ ),
137
+ ],
138
+ ),
139
+ )
140
+ : isStopping
141
+ ? _GlassSurface(
142
+ key: const ValueKey<String>('stopping'),
143
+ width: double.infinity,
144
+ padding: const EdgeInsets.symmetric(
145
+ horizontal: 14,
146
+ vertical: 12,
147
+ ),
148
+ borderRadius: BorderRadius.circular(16),
149
+ blurSigma: 10,
150
+ fillColor: _bgCard.withValues(alpha: 0.88),
151
+ borderColor: _borderLight,
152
+ child: Text(
153
+ 'Finalizing recording...',
154
+ style: TextStyle(
155
+ color: _textSecondary,
156
+ height: 1.4,
157
+ ),
158
+ ),
159
+ )
160
+ : const SizedBox.shrink(),
161
+ ),
162
+ if (isStarting || isStopping) const SizedBox(height: 14),
163
+ const SizedBox(height: 18),
164
+ Wrap(
165
+ spacing: 12,
166
+ runSpacing: 12,
167
+ children: <Widget>[
168
+ if (runtime.supportsScreenAndMic)
169
+ FilledButton.icon(
170
+ onPressed:
171
+ widget.controller.isStartingRecording ||
172
+ runtime.active
173
+ ? null
174
+ : widget.controller.startWebRecording,
175
+ icon: Icon(Icons.desktop_windows_outlined),
176
+ label: Text('Screen + mic'),
177
+ ),
178
+ if (runtime.supportsScreenAndMic)
179
+ OutlinedButton.icon(
180
+ onPressed:
181
+ widget.controller.isStartingRecording ||
182
+ runtime.active
183
+ ? null
184
+ : widget.controller.startWebMicrophoneRecording,
185
+ icon: Icon(Icons.graphic_eq_outlined),
186
+ label: Text('Mic only'),
187
+ ),
188
+ if (runtime.supportsBackgroundMic)
189
+ FilledButton.icon(
190
+ onPressed:
191
+ widget.controller.isStartingRecording ||
192
+ runtime.active
193
+ ? null
194
+ : widget.controller.startBackgroundRecording,
195
+ icon: Icon(Icons.mic_none_outlined),
196
+ label: Text('Background mic'),
197
+ ),
198
+ if (runtime.supportsSystemAudio)
199
+ FilledButton.icon(
200
+ onPressed: widget.controller.canStartDesktopRecording
201
+ ? widget.controller.startDesktopRecording
202
+ : null,
203
+ style: FilledButton.styleFrom(
204
+ backgroundColor: _accentAlt,
205
+ foregroundColor: Colors.white,
206
+ ),
207
+ icon: Icon(Icons.surround_sound_outlined),
208
+ label: Text('Desktop studio'),
209
+ ),
210
+ if (runtime.supportsBackgroundMic && runtime.active)
211
+ OutlinedButton.icon(
212
+ onPressed: runtime.paused
213
+ ? widget.controller.resumeBackgroundRecording
214
+ : widget.controller.pauseBackgroundRecording,
215
+ icon: Icon(
216
+ runtime.paused ? Icons.play_arrow : Icons.pause,
217
+ ),
218
+ label: Text(runtime.paused ? 'Resume' : 'Pause'),
219
+ ),
220
+ if (runtime.supportsSystemAudio && runtime.active)
221
+ OutlinedButton.icon(
222
+ onPressed: runtime.paused
223
+ ? widget.controller.resumeDesktopRecording
224
+ : widget.controller.pauseDesktopRecording,
225
+ icon: Icon(
226
+ runtime.paused ? Icons.play_arrow : Icons.pause,
227
+ ),
228
+ label: Text(runtime.paused ? 'Resume' : 'Pause'),
229
+ ),
230
+ if (runtime.active)
231
+ OutlinedButton.icon(
232
+ onPressed: widget.controller.isStoppingRecording
233
+ ? null
234
+ : widget.controller.stopRecording,
235
+ icon: Icon(Icons.stop_circle_outlined),
236
+ label: Text('Stop'),
237
+ ),
238
+ if (runtime.supportsFloatingToolbar)
239
+ OutlinedButton.icon(
240
+ onPressed: !runtime.active
241
+ ? null
242
+ : (runtime.floatingToolbarVisible
243
+ ? widget.controller.hideDesktopFloatingToolbar
244
+ : widget
245
+ .controller
246
+ .showDesktopFloatingToolbar),
247
+ icon: Icon(
248
+ runtime.floatingToolbarVisible
249
+ ? Icons.visibility_off_outlined
250
+ : Icons.open_in_new_rounded,
251
+ ),
252
+ label: Text(
253
+ runtime.floatingToolbarVisible
254
+ ? 'Hide floating bar'
255
+ : 'Show floating bar',
256
+ ),
257
+ ),
258
+ OutlinedButton.icon(
259
+ onPressed: widget.controller.refreshRecordings,
260
+ icon: Icon(Icons.refresh),
261
+ label: Text('Refresh'),
262
+ ),
263
+ ],
264
+ ),
265
+ if (runtime.supportsSystemAudio) ...<Widget>[
266
+ const SizedBox(height: 20),
267
+ _GlassSurface(
268
+ width: double.infinity,
269
+ padding: const EdgeInsets.all(18),
270
+ borderRadius: BorderRadius.circular(22),
271
+ blurSigma: 10,
272
+ fillColor: _bgCard.withValues(alpha: 0.9),
273
+ borderColor: _borderLight,
274
+ child: Column(
275
+ crossAxisAlignment: CrossAxisAlignment.start,
276
+ children: <Widget>[
277
+ Text(
278
+ 'Desktop runtime diagnostics',
279
+ style: TextStyle(
280
+ fontSize: 15,
281
+ fontWeight: FontWeight.w700,
282
+ ),
283
+ ),
284
+ const SizedBox(height: 6),
285
+ Text(
286
+ 'Permissions and live levels stay visible while the floating bar handles quick controls.',
287
+ style: TextStyle(color: _textSecondary, height: 1.45),
288
+ ),
289
+ const SizedBox(height: 16),
290
+ Wrap(
291
+ spacing: 10,
292
+ runSpacing: 10,
293
+ children: <Widget>[
294
+ _RecordingPermissionBadge(
295
+ label: 'Microphone',
296
+ state: runtime.microphonePermission,
297
+ ),
298
+ _RecordingPermissionBadge(
299
+ label: 'System audio',
300
+ state: runtime.systemAudioPermission,
301
+ ),
302
+ _DotStatus(
303
+ label: runtime.backgroundRuntimeActive
304
+ ? 'Background runtime ready'
305
+ : 'Foreground only',
306
+ color: runtime.backgroundRuntimeActive
307
+ ? _success
308
+ : _warning,
309
+ ),
310
+ _DotStatus(
311
+ label: runtime.supportsGlobalHotkeys
312
+ ? 'Hotkey-ready'
313
+ : 'No global hotkeys',
314
+ color: runtime.supportsGlobalHotkeys
315
+ ? _success
316
+ : _warning,
317
+ ),
318
+ ],
319
+ ),
320
+ const SizedBox(height: 18),
321
+ Wrap(
322
+ spacing: 18,
323
+ runSpacing: 18,
324
+ children: <Widget>[
325
+ _AudioLevelBar(
326
+ label: 'Microphone',
327
+ valueDb: runtime.microphoneLevelDb,
328
+ color: _accent,
329
+ ),
330
+ _AudioLevelBar(
331
+ label: 'System audio',
332
+ valueDb: runtime.systemAudioLevelDb,
333
+ color: _accentAlt,
334
+ ),
335
+ ],
336
+ ),
337
+ const SizedBox(height: 18),
338
+ Wrap(
339
+ spacing: 12,
340
+ runSpacing: 12,
341
+ children: <Widget>[
342
+ if ((runtime.selectedInputDeviceName ?? '')
343
+ .trim()
344
+ .isNotEmpty)
345
+ _MetaPill(
346
+ icon: Icons.mic_external_on_outlined,
347
+ label:
348
+ 'Input ${runtime.selectedInputDeviceName!}',
349
+ ),
350
+ _MetaPill(
351
+ icon: Icons.tune_outlined,
352
+ label:
353
+ '${runtime.availableInputDevices.length} input device${runtime.availableInputDevices.length == 1 ? '' : 's'}',
354
+ ),
355
+ if (runtime.activeSources.isNotEmpty)
356
+ _MetaPill(
357
+ icon: Icons.multitrack_audio_outlined,
358
+ label: runtime.activeSources.join(' + '),
359
+ ),
360
+ ],
361
+ ),
362
+ const SizedBox(height: 16),
363
+ Wrap(
364
+ spacing: 12,
365
+ runSpacing: 12,
366
+ children: <Widget>[
367
+ OutlinedButton.icon(
368
+ onPressed: widget
369
+ .controller
370
+ .openDesktopMicrophoneSettings,
371
+ icon: Icon(Icons.settings_voice_outlined),
372
+ label: Text('Mic settings'),
373
+ ),
374
+ OutlinedButton.icon(
375
+ onPressed: widget
376
+ .controller
377
+ .openDesktopSystemAudioSettings,
378
+ icon: Icon(Icons.speaker_group_outlined),
379
+ label: Text('System audio settings'),
380
+ ),
381
+ ],
382
+ ),
383
+ ],
384
+ ),
385
+ ),
386
+ ],
387
+ if (runtime.errorMessage != null &&
388
+ runtime.errorMessage!.trim().isNotEmpty) ...<Widget>[
389
+ const SizedBox(height: 16),
390
+ _InlineError(message: runtime.errorMessage!),
391
+ ],
392
+ ],
393
+ ),
394
+ ),
395
+ ),
396
+ const _SectionTitle('Transcripts'),
397
+ const SizedBox(height: 12),
398
+ if (widget.controller.recordingSessions.isEmpty)
399
+ const _EmptyCard(
400
+ title: 'No recordings yet',
401
+ subtitle: 'Start one and transcripts will appear here.',
402
+ )
403
+ else
404
+ ...widget.controller.recordingSessions.map(
405
+ (session) => Padding(
406
+ key: ValueKey<String>(session.id),
407
+ padding: const EdgeInsets.only(bottom: 12),
408
+ child: _RecordingSessionCard(
409
+ controller: widget.controller,
410
+ session: session,
411
+ onRetry:
412
+ (session.status == 'failed' ||
413
+ (session.status == 'completed' &&
414
+ session.transcriptText.trim().isEmpty &&
415
+ session.transcriptSegments.isEmpty &&
416
+ session.structuredContent.isEmpty))
417
+ ? () => widget.controller.retryRecording(session.id)
418
+ : null,
419
+ onDeleteSegment: (segment) =>
420
+ _deleteSegment(context, session, segment),
421
+ onDeleteRecording: () => _deleteRecording(context, session),
422
+ ),
423
+ ),
424
+ ),
425
+ ],
426
+ );
427
+ }
428
+ }
429
+
430
+ class _RecordingSessionCard extends StatelessWidget {
431
+ const _RecordingSessionCard({
432
+ required this.controller,
433
+ required this.session,
434
+ this.onRetry,
435
+ this.onDeleteSegment,
436
+ this.onDeleteRecording,
437
+ });
438
+
439
+ final NeoAgentController controller;
440
+ final RecordingSessionItem session;
441
+ final VoidCallback? onRetry;
442
+ final Future<void> Function(RecordingTranscriptSegment segment)?
443
+ onDeleteSegment;
444
+ final Future<void> Function()? onDeleteRecording;
445
+
446
+ @override
447
+ Widget build(BuildContext context) {
448
+ final runtime = controller.recordingRuntime;
449
+ final isLiveSession = runtime.active && runtime.sessionId == session.id;
450
+ final canDeleteRecording = onDeleteRecording != null && !isLiveSession;
451
+ return AnimatedContainer(
452
+ duration: const Duration(milliseconds: 240),
453
+ curve: Curves.easeOutCubic,
454
+ child: Card(
455
+ child: Padding(
456
+ padding: const EdgeInsets.all(18),
457
+ child: Column(
458
+ crossAxisAlignment: CrossAxisAlignment.start,
459
+ children: <Widget>[
460
+ Row(
461
+ crossAxisAlignment: CrossAxisAlignment.start,
462
+ children: <Widget>[
463
+ Expanded(
464
+ child: Column(
465
+ crossAxisAlignment: CrossAxisAlignment.start,
466
+ children: <Widget>[
467
+ Text(
468
+ session.title,
469
+ style: TextStyle(
470
+ fontSize: 16,
471
+ fontWeight: FontWeight.w700,
472
+ ),
473
+ ),
474
+ const SizedBox(height: 6),
475
+ Text(
476
+ '${session.startedAtLabel} • ${session.platformLabel} • ${session.durationLabel}',
477
+ style: TextStyle(color: _textSecondary),
478
+ ),
479
+ ],
480
+ ),
481
+ ),
482
+ _StatusPill(
483
+ label: session.statusLabel,
484
+ color: session.statusColor,
485
+ ),
486
+ ],
487
+ ),
488
+ const SizedBox(height: 14),
489
+ Wrap(
490
+ spacing: 8,
491
+ runSpacing: 8,
492
+ children: session.sources
493
+ .map(
494
+ (source) => _GlassSurface(
495
+ padding: const EdgeInsets.symmetric(
496
+ horizontal: 10,
497
+ vertical: 7,
498
+ ),
499
+ borderRadius: BorderRadius.circular(999),
500
+ blurSigma: 8,
501
+ fillColor: _bgCard.withValues(alpha: 0.82),
502
+ borderColor: _border,
503
+ child: Text(
504
+ '${source.label} • ${source.durationLabel}',
505
+ style: TextStyle(fontSize: 12),
506
+ ),
507
+ ),
508
+ )
509
+ .toList(),
510
+ ),
511
+ if (session.sources.any(
512
+ (source) => source.mediaKind == 'audio',
513
+ )) ...<Widget>[
514
+ const SizedBox(height: 12),
515
+ _RecordingSourceAudioControls(
516
+ controller: controller,
517
+ session: session,
518
+ ),
519
+ ],
520
+ if (session.lastError != null &&
521
+ session.lastError!.trim().isNotEmpty)
522
+ Padding(
523
+ padding: const EdgeInsets.only(top: 12),
524
+ child: Text(
525
+ session.lastError!,
526
+ style: TextStyle(color: _danger),
527
+ ),
528
+ ),
529
+ if (session.structuredContent.isNotEmpty) ...<Widget>[
530
+ const SizedBox(height: 16),
531
+ _GlassSurface(
532
+ padding: const EdgeInsets.all(16),
533
+ borderRadius: BorderRadius.circular(18),
534
+ blurSigma: 8,
535
+ fillColor: _accentMuted.withValues(alpha: 0.16),
536
+ borderColor: _accent.withValues(alpha: 0.26),
537
+ child: Column(
538
+ crossAxisAlignment: CrossAxisAlignment.start,
539
+ children: <Widget>[
540
+ Row(
541
+ children: <Widget>[
542
+ Icon(Icons.auto_awesome, size: 16, color: _accent),
543
+ const SizedBox(width: 8),
544
+ Text(
545
+ 'Insights',
546
+ style: TextStyle(
547
+ color: _accent,
548
+ fontWeight: FontWeight.w600,
549
+ ),
550
+ ),
551
+ ],
552
+ ),
553
+ if (session.structuredContent['summary'] !=
554
+ null) ...<Widget>[
555
+ const SizedBox(height: 10),
556
+ Text(
557
+ 'Summary',
558
+ style: TextStyle(
559
+ fontWeight: FontWeight.w700,
560
+ fontSize: 13,
561
+ color: _textSecondary,
562
+ ),
563
+ ),
564
+ const SizedBox(height: 4),
565
+ Text(
566
+ session.structuredContent['summary'].toString(),
567
+ style: TextStyle(height: 1.55, fontSize: 15),
568
+ ),
569
+ ],
570
+ if (session.structuredContent['action_items'] != null &&
571
+ _getStructuredList(
572
+ session,
573
+ 'action_items',
574
+ ).isNotEmpty) ...<Widget>[
575
+ const SizedBox(height: 10),
576
+ Text(
577
+ 'Action Items',
578
+ style: TextStyle(
579
+ fontWeight: FontWeight.w700,
580
+ fontSize: 13,
581
+ color: _textSecondary,
582
+ ),
583
+ ),
584
+ const SizedBox(height: 4),
585
+ ..._getStructuredList(session, 'action_items').map(
586
+ (item) => Padding(
587
+ padding: const EdgeInsets.only(bottom: 4),
588
+ child: Row(
589
+ crossAxisAlignment: CrossAxisAlignment.start,
590
+ children: [
591
+ Text(
592
+ '• ',
593
+ style: TextStyle(
594
+ fontWeight: FontWeight.w700,
595
+ color: _accent,
596
+ ),
597
+ ),
598
+ Expanded(
599
+ child: Text(
600
+ item.toString(),
601
+ style: TextStyle(height: 1.35),
602
+ ),
603
+ ),
604
+ ],
605
+ ),
606
+ ),
607
+ ),
608
+ ],
609
+ if (session.structuredContent['events'] != null &&
610
+ _getStructuredList(
611
+ session,
612
+ 'events',
613
+ ).isNotEmpty) ...<Widget>[
614
+ const SizedBox(height: 10),
615
+ Text(
616
+ 'Events Mentioned',
617
+ style: TextStyle(
618
+ fontWeight: FontWeight.w700,
619
+ fontSize: 13,
620
+ color: _textSecondary,
621
+ ),
622
+ ),
623
+ const SizedBox(height: 4),
624
+ ..._getStructuredList(session, 'events').map(
625
+ (item) => Padding(
626
+ padding: const EdgeInsets.only(bottom: 4),
627
+ child: Row(
628
+ crossAxisAlignment: CrossAxisAlignment.start,
629
+ children: [
630
+ Text(
631
+ '• ',
632
+ style: TextStyle(
633
+ fontWeight: FontWeight.w700,
634
+ color: _accent,
635
+ ),
636
+ ),
637
+ Expanded(
638
+ child: Text(
639
+ item.toString(),
640
+ style: TextStyle(height: 1.35),
641
+ ),
642
+ ),
643
+ ],
644
+ ),
645
+ ),
646
+ ),
647
+ ],
648
+ ],
649
+ ),
650
+ ),
651
+ ],
652
+ if (session.transcriptSegments.isNotEmpty) ...<Widget>[
653
+ const SizedBox(height: 16),
654
+ ...session.transcriptSegments.map(
655
+ (segment) => Padding(
656
+ padding: const EdgeInsets.only(bottom: 10),
657
+ child: _GlassSurface(
658
+ padding: const EdgeInsets.fromLTRB(14, 12, 10, 12),
659
+ borderRadius: BorderRadius.circular(16),
660
+ blurSigma: 6,
661
+ fillColor: _bgCard.withValues(alpha: 0.9),
662
+ borderColor: _border,
663
+ child: Row(
664
+ crossAxisAlignment: CrossAxisAlignment.start,
665
+ children: <Widget>[
666
+ SizedBox(
667
+ width: 88,
668
+ child: Text(
669
+ segment.timestampLabel,
670
+ style: TextStyle(
671
+ color: _textSecondary,
672
+ fontWeight: FontWeight.w600,
673
+ ),
674
+ ),
675
+ ),
676
+ Expanded(
677
+ child: Text(
678
+ segment.displayText,
679
+ style: const TextStyle(
680
+ height: 1.55,
681
+ fontSize: 15,
682
+ ),
683
+ ),
684
+ ),
685
+ if (onDeleteSegment != null &&
686
+ segment.id > 0) ...<Widget>[
687
+ const SizedBox(width: 8),
688
+ IconButton(
689
+ onPressed: () async {
690
+ await onDeleteSegment!(segment);
691
+ },
692
+ icon: Icon(Icons.delete_outline),
693
+ tooltip: 'Delete segment',
694
+ visualDensity: VisualDensity.compact,
695
+ ),
696
+ ],
697
+ ],
698
+ ),
699
+ ),
700
+ ),
701
+ ),
702
+ ] else if (session.transcriptText.isNotEmpty) ...<Widget>[
703
+ const SizedBox(height: 16),
704
+ _GlassSurface(
705
+ width: double.infinity,
706
+ padding: const EdgeInsets.all(16),
707
+ borderRadius: BorderRadius.circular(16),
708
+ blurSigma: 8,
709
+ fillColor: _bgCard.withValues(alpha: 0.92),
710
+ borderColor: _border,
711
+ child: SelectableText(
712
+ session.transcriptText,
713
+ style: const TextStyle(height: 1.6, fontSize: 15),
714
+ ),
715
+ ),
716
+ ] else ...<Widget>[
717
+ const SizedBox(height: 16),
718
+ Text(
719
+ session.status == 'processing'
720
+ ? 'Transcribing...'
721
+ : session.status == 'failed'
722
+ ? 'Transcription failed. Check the error above and retry.'
723
+ : session.status == 'completed'
724
+ ? 'No transcript text was returned. You can retry transcription.'
725
+ : 'Transcript is not available yet.',
726
+ style: TextStyle(color: _textSecondary),
727
+ ),
728
+ ],
729
+ if (onRetry != null || canDeleteRecording) ...<Widget>[
730
+ const SizedBox(height: 14),
731
+ Wrap(
732
+ spacing: 10,
733
+ runSpacing: 10,
734
+ children: <Widget>[
735
+ if (onRetry != null)
736
+ OutlinedButton.icon(
737
+ onPressed: onRetry,
738
+ icon: Icon(Icons.replay),
739
+ label: Text('Retry transcription'),
740
+ ),
741
+ if (canDeleteRecording)
742
+ OutlinedButton.icon(
743
+ onPressed: () async {
744
+ await onDeleteRecording!();
745
+ },
746
+ icon: Icon(Icons.delete_forever_outlined),
747
+ label: Text('Delete recording'),
748
+ style: OutlinedButton.styleFrom(
749
+ foregroundColor: _danger,
750
+ ),
751
+ ),
752
+ ],
753
+ ),
754
+ ],
755
+ ],
756
+ ),
757
+ ),
758
+ ),
759
+ );
760
+ }
761
+
762
+ List<dynamic> _getStructuredList(RecordingSessionItem session, String key) {
763
+ final value = session.structuredContent[key];
764
+ if (value is List) {
765
+ return value;
766
+ }
767
+ return const [];
768
+ }
769
+ }
770
+
771
+ class _RecordingSourceAudioControls extends StatefulWidget {
772
+ const _RecordingSourceAudioControls({
773
+ required this.controller,
774
+ required this.session,
775
+ });
776
+
777
+ final NeoAgentController controller;
778
+ final RecordingSessionItem session;
779
+
780
+ @override
781
+ State<_RecordingSourceAudioControls> createState() =>
782
+ _RecordingSourceAudioControlsState();
783
+ }
784
+
785
+ class _RecordingSourceAudioControlsState
786
+ extends State<_RecordingSourceAudioControls> {
787
+ late final AudioPlayer _player;
788
+ StreamSubscription<void>? _playerCompleteSubscription;
789
+ String? _activeSourceKey;
790
+ bool _isPlaying = false;
791
+ int _loadToken = 0;
792
+
793
+ @override
794
+ void initState() {
795
+ super.initState();
796
+ _player = AudioPlayer();
797
+ _playerCompleteSubscription = _player.onPlayerComplete.listen((_) {
798
+ if (!mounted) {
799
+ return;
800
+ }
801
+ setState(() {
802
+ _isPlaying = false;
803
+ _activeSourceKey = null;
804
+ });
805
+ });
806
+ }
807
+
808
+ @override
809
+ void dispose() {
810
+ _playerCompleteSubscription?.cancel();
811
+ unawaited(_player.dispose());
812
+ super.dispose();
813
+ }
814
+
815
+ Future<void> _toggleSource(RecordingSourceItem source) async {
816
+ final token = ++_loadToken;
817
+ bool isStale() => !mounted || token != _loadToken;
818
+ if (_isPlaying && _activeSourceKey == source.sourceKey) {
819
+ await _player.stop();
820
+ if (isStale()) {
821
+ return;
822
+ }
823
+ setState(() {
824
+ _isPlaying = false;
825
+ _activeSourceKey = null;
826
+ });
827
+ return;
828
+ }
829
+
830
+ try {
831
+ await _player.stop();
832
+ if (isStale()) {
833
+ return;
834
+ }
835
+ final bytes = await widget.controller.fetchRecordingSourceAudioBytes(
836
+ widget.session.id,
837
+ source.sourceKey,
838
+ );
839
+ if (isStale()) {
840
+ return;
841
+ }
842
+ if (bytes.isEmpty) {
843
+ throw StateError('Audio source is empty.');
844
+ }
845
+ final mime = source.mimeType.trim().isNotEmpty
846
+ ? source.mimeType.trim()
847
+ : null;
848
+ await _player.play(BytesSource(bytes, mimeType: mime));
849
+ if (isStale()) {
850
+ await _player.stop();
851
+ return;
852
+ }
853
+ if (!mounted) {
854
+ return;
855
+ }
856
+ setState(() {
857
+ _isPlaying = true;
858
+ _activeSourceKey = source.sourceKey;
859
+ });
860
+ } catch (e) {
861
+ if (isStale()) {
862
+ return;
863
+ }
864
+ AppDiagnostics.log(
865
+ 'recording.playback',
866
+ 'source.play.failed',
867
+ data: <String, Object?>{
868
+ 'sessionId': widget.session.id,
869
+ 'sourceKey': source.sourceKey,
870
+ 'mimeType': source.mimeType,
871
+ },
872
+ error: e,
873
+ );
874
+ if (!mounted) {
875
+ return;
876
+ }
877
+ setState(() {
878
+ _isPlaying = false;
879
+ _activeSourceKey = null;
880
+ });
881
+ }
882
+ }
883
+
884
+ @override
885
+ Widget build(BuildContext context) {
886
+ final audioSources = widget.session.sources
887
+ .where((source) => source.mediaKind == 'audio')
888
+ .toList();
889
+ if (audioSources.isEmpty) {
890
+ return const SizedBox.shrink();
891
+ }
892
+
893
+ return Wrap(
894
+ spacing: 8,
895
+ runSpacing: 8,
896
+ children: audioSources.map((source) {
897
+ final isActive = _isPlaying && _activeSourceKey == source.sourceKey;
898
+ return AnimatedScale(
899
+ duration: const Duration(milliseconds: 180),
900
+ curve: Curves.easeOutCubic,
901
+ scale: isActive ? 1.02 : 1,
902
+ child: OutlinedButton.icon(
903
+ onPressed: () => _toggleSource(source),
904
+ icon: Icon(
905
+ isActive ? Icons.stop_circle_outlined : Icons.play_arrow,
906
+ ),
907
+ label: Text(
908
+ isActive ? 'Stop ${source.label}' : 'Play ${source.label}',
909
+ ),
910
+ style: OutlinedButton.styleFrom(
911
+ backgroundColor: isActive
912
+ ? _accentMuted.withValues(alpha: 0.2)
913
+ : null,
914
+ ),
915
+ ),
916
+ );
917
+ }).toList(),
918
+ );
919
+ }
920
+ }