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,104 @@
1
+ package com.neoagent.flutter_app.recording
2
+
3
+ import org.json.JSONObject
4
+ import java.io.BufferedReader
5
+ import java.io.File
6
+ import java.io.OutputStreamWriter
7
+ import java.net.HttpURLConnection
8
+ import java.net.URL
9
+ import java.nio.charset.StandardCharsets
10
+
11
+ class RecordingUploadClient {
12
+ fun uploadChunk(
13
+ backendUrl: String,
14
+ sessionCookie: String,
15
+ sessionId: String,
16
+ meta: PendingChunkMeta,
17
+ file: File,
18
+ ) {
19
+ val url = URL(resolveUrl(backendUrl, "/api/recordings/$sessionId/chunks"))
20
+ val connection = (url.openConnection() as HttpURLConnection).apply {
21
+ requestMethod = "POST"
22
+ doOutput = true
23
+ connectTimeout = 20_000
24
+ readTimeout = 60_000
25
+ setRequestProperty("Cookie", sessionCookie)
26
+ setRequestProperty("Content-Type", meta.mimeType)
27
+ setRequestProperty("X-Recording-Source-Key", SOURCE_KEY)
28
+ setRequestProperty("X-Recording-Sequence", meta.sequence.toString())
29
+ setRequestProperty("X-Recording-Start-Ms", meta.startMs.toString())
30
+ setRequestProperty("X-Recording-End-Ms", meta.endMs.toString())
31
+ }
32
+
33
+ file.inputStream().use { input ->
34
+ connection.outputStream.use { output ->
35
+ input.copyTo(output)
36
+ }
37
+ }
38
+
39
+ val statusCode = connection.responseCode
40
+ if (statusCode !in 200..299) {
41
+ val body = readResponse(connection)
42
+ connection.disconnect()
43
+ throw IllegalStateException("Chunk upload failed ($statusCode): $body")
44
+ }
45
+ connection.disconnect()
46
+ }
47
+
48
+ fun finalizeSession(
49
+ backendUrl: String,
50
+ sessionCookie: String,
51
+ sessionId: String,
52
+ stopReason: String,
53
+ ) {
54
+ val url = URL(resolveUrl(backendUrl, "/api/recordings/$sessionId/finalize"))
55
+ val connection = (url.openConnection() as HttpURLConnection).apply {
56
+ requestMethod = "POST"
57
+ doOutput = true
58
+ connectTimeout = 20_000
59
+ readTimeout = 60_000
60
+ setRequestProperty("Cookie", sessionCookie)
61
+ setRequestProperty("Content-Type", "application/json")
62
+ }
63
+
64
+ val payload = JSONObject()
65
+ .put("stopReason", stopReason)
66
+ .toString()
67
+ OutputStreamWriter(connection.outputStream, StandardCharsets.UTF_8).use { writer ->
68
+ writer.write(payload)
69
+ }
70
+
71
+ val statusCode = connection.responseCode
72
+ if (statusCode !in 200..299) {
73
+ val body = readResponse(connection)
74
+ connection.disconnect()
75
+ throw IllegalStateException("Finalize failed ($statusCode): $body")
76
+ }
77
+ connection.disconnect()
78
+ }
79
+
80
+ private fun readResponse(connection: HttpURLConnection): String {
81
+ val stream = connection.errorStream ?: connection.inputStream ?: return ""
82
+ return stream.bufferedReader().use(BufferedReader::readText)
83
+ }
84
+
85
+ private fun resolveUrl(baseUrl: String, path: String): String {
86
+ val trimmed = baseUrl.trim().removeSuffix("/")
87
+ return if (trimmed.isEmpty()) {
88
+ path
89
+ } else {
90
+ "$trimmed$path"
91
+ }
92
+ }
93
+
94
+ companion object {
95
+ const val SOURCE_KEY = "microphone"
96
+ }
97
+ }
98
+
99
+ data class PendingChunkMeta(
100
+ val sequence: Int,
101
+ val startMs: Long,
102
+ val endMs: Long,
103
+ val mimeType: String = "audio/wav",
104
+ )
@@ -0,0 +1,457 @@
1
+ package com.neoagent.flutter_app.widgets
2
+
3
+ import android.app.PendingIntent
4
+ import android.appwidget.AppWidgetManager
5
+ import android.appwidget.AppWidgetProvider
6
+ import android.content.ComponentName
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.graphics.Color
10
+ import android.view.View
11
+ import android.widget.RemoteViews
12
+ import com.neoagent.flutter_app.MainActivity
13
+ import com.neoagent.flutter_app.R
14
+ import org.json.JSONObject
15
+
16
+ class AiHomeWidgetProvider : AppWidgetProvider() {
17
+
18
+
19
+ override fun onReceive(context: Context, intent: Intent) {
20
+ super.onReceive(context, intent)
21
+ if (intent.action == ACTION_TOGGLE_TASKS) {
22
+ val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
23
+ if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
24
+ val store = AiWidgetStore(context)
25
+ store.toggleTasksExpanded(appWidgetId)
26
+ refreshAll(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId))
27
+ }
28
+ } else if (intent.action == ACTION_RUN_TASK) {
29
+ val taskId = intent.getStringExtra(EXTRA_TASK_ID)
30
+ if (taskId != null) {
31
+ WidgetTaskRunWorker.enqueue(context, taskId)
32
+ }
33
+ }
34
+ }
35
+
36
+ override fun onUpdate(
37
+ context: Context,
38
+ appWidgetManager: AppWidgetManager,
39
+ appWidgetIds: IntArray,
40
+ ) {
41
+ refreshAll(context, appWidgetManager, appWidgetIds)
42
+ }
43
+
44
+ override fun onDeleted(context: Context, appWidgetIds: IntArray) {
45
+ val store = AiWidgetStore(context)
46
+ appWidgetIds.forEach(store::clearBinding)
47
+ }
48
+
49
+ companion object {
50
+ fun refreshAll(context: Context) {
51
+ val manager = AppWidgetManager.getInstance(context)
52
+ val componentName = ComponentName(context, AiHomeWidgetProvider::class.java)
53
+ val ids = manager.getAppWidgetIds(componentName)
54
+ refreshAll(context, manager, ids)
55
+ }
56
+
57
+ fun refreshAll(
58
+ context: Context,
59
+ manager: AppWidgetManager,
60
+ appWidgetIds: IntArray,
61
+ ) {
62
+ val store = AiWidgetStore(context)
63
+ appWidgetIds.forEach { appWidgetId ->
64
+ manager.updateAppWidget(
65
+ appWidgetId,
66
+ buildRemoteViews(context, store, appWidgetId),
67
+ )
68
+ }
69
+ }
70
+
71
+ private fun buildRemoteViews(
72
+ context: Context,
73
+ store: AiWidgetStore,
74
+ appWidgetId: Int,
75
+ ): RemoteViews {
76
+ val views = RemoteViews(context.packageName, R.layout.neoagent_ai_widget)
77
+ val widgetId = store.widgetIdForAppWidget(appWidgetId)
78
+ if (widgetId.isNullOrBlank()) {
79
+ bindEmptyState(
80
+ context,
81
+ views,
82
+ "Choose an AI widget",
83
+ "Add this widget again and select one from the list.",
84
+ )
85
+ return views
86
+ }
87
+
88
+ val widget = store.findWidget(widgetId)
89
+ if (widget == null) {
90
+ bindEmptyState(
91
+ context,
92
+ views,
93
+ "Widget unavailable",
94
+ "Open NeoAgent, refresh widgets, and configure this home widget again.",
95
+ )
96
+ return views
97
+ }
98
+
99
+ val snapshot = widget.latestSnapshot
100
+ val accent = accentColor(
101
+ cleanText(snapshot?.optString("accentToken")),
102
+ cleanText(snapshot?.optString("surfaceColor")),
103
+ )
104
+ val displayName = displayName(widget.name)
105
+ val kicker = cleanText(snapshot?.optString("kicker"))
106
+ val metricLabel = cleanText(snapshot?.optString("metricLabel"))
107
+ val secondaryMetric = cleanText(snapshot?.optString("secondaryMetric"))
108
+ val secondaryLabel = cleanText(snapshot?.optString("secondaryLabel"))
109
+ val tertiaryMetric = cleanText(snapshot?.optString("tertiaryMetric"))
110
+ val tertiaryLabel = cleanText(snapshot?.optString("tertiaryLabel"))
111
+ val title =
112
+ cleanText(snapshot?.optString("title"))
113
+ .ifBlank { displayName }
114
+ val subtitle =
115
+ sequenceOf(
116
+ listOf(kicker, cleanText(snapshot?.optString("subtitle"))).filter { it.isNotBlank() }
117
+ .joinToString(" • ")
118
+ .trim(),
119
+ metricLabel,
120
+ displayName,
121
+ ).firstOrNull { it.isNotBlank() }
122
+ ?: cadenceLabel(widget.refreshCron)
123
+ val metric = cleanText(snapshot?.optString("metric"))
124
+ val body = cleanText(snapshot?.optString("body"))
125
+ val chips = joinChips(snapshot)
126
+ val rows = supportingRows(snapshot, secondaryLabel, secondaryMetric, tertiaryLabel, tertiaryMetric)
127
+ val updated = snapshot?.optString("updatedAt").orEmpty().ifBlank {
128
+ formatUpdatedFallback(widget.refreshCron)
129
+ }
130
+ val hasSnapshot = snapshot != null
131
+ val supportSummary =
132
+ listOf(
133
+ labeledValue(secondaryLabel, secondaryMetric),
134
+ labeledValue(tertiaryLabel, tertiaryMetric),
135
+ chips,
136
+ ).firstOrNull { it.isNotBlank() }.orEmpty()
137
+ val bodyText =
138
+ when {
139
+ body.isNotBlank() -> body
140
+ supportSummary.isNotBlank() -> supportSummary
141
+ !hasSnapshot -> "Waiting for first update"
142
+ else -> "Open in NeoAgent for the full view"
143
+ }
144
+
145
+ views.setTextViewText(R.id.widget_title, title)
146
+ views.setTextColor(R.id.widget_title, Color.WHITE)
147
+ views.setTextViewText(R.id.widget_subtitle, subtitle)
148
+ views.setTextColor(R.id.widget_subtitle, 0xFFD1D8E6.toInt())
149
+ views.setTextViewText(R.id.widget_metric, metric)
150
+ views.setTextColor(R.id.widget_metric, accent)
151
+ views.setTextViewText(R.id.widget_body, bodyText)
152
+ views.setTextColor(R.id.widget_body, 0xFFF4F6FA.toInt())
153
+ views.setTextViewText(R.id.widget_meta, updated)
154
+ views.setTextColor(R.id.widget_meta, 0xFF92A1BA.toInt())
155
+
156
+ bindRow(views, R.id.widget_row_1, rows.getOrNull(0))
157
+ bindRow(views, R.id.widget_row_2, rows.getOrNull(1))
158
+ bindRow(views, R.id.widget_row_3, rows.getOrNull(2))
159
+
160
+ val showMetric = metric.isNotBlank()
161
+ views.setViewVisibility(R.id.widget_metric, if (showMetric) View.VISIBLE else View.GONE)
162
+ views.setViewVisibility(
163
+ R.id.widget_rows_group,
164
+ if (rows.isNotEmpty()) View.VISIBLE else View.GONE,
165
+ )
166
+
167
+ val statusText = widget.lastError?.takeIf { it.isNotBlank() }
168
+ ?: if (widget.enabled) cadenceLabel(widget.refreshCron) else "Paused"
169
+ val statusColor =
170
+ if (!widget.lastError.isNullOrBlank()) {
171
+ 0xFFFFB3A9.toInt()
172
+ } else if (widget.enabled) {
173
+ 0xFF8EE0AF.toInt()
174
+ } else {
175
+ 0xFF92A1BA.toInt()
176
+ }
177
+ views.setTextViewText(R.id.widget_status, statusText)
178
+ views.setTextColor(R.id.widget_status, statusColor)
179
+
180
+ views.setViewVisibility(R.id.widget_status, View.VISIBLE)
181
+
182
+ // Tasks Rendering
183
+ val tasks = widget.tasks
184
+ if (tasks.isNotEmpty()) {
185
+ views.setViewVisibility(R.id.widget_tasks_toggle_group, View.VISIBLE)
186
+ val isExpanded = store.isTasksExpanded(appWidgetId)
187
+ views.setTextViewText(R.id.widget_tasks_toggle_text, if (isExpanded) "Tasks (${tasks.size}) ▲" else "Tasks (${tasks.size}) ▼")
188
+ views.setTextColor(R.id.widget_tasks_toggle_text, accent)
189
+
190
+ val toggleIntent = Intent(context, AiHomeWidgetProvider::class.java).apply {
191
+ action = ACTION_TOGGLE_TASKS
192
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
193
+ }
194
+ val togglePendingIntent = PendingIntent.getBroadcast(
195
+ context, appWidgetId, toggleIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
196
+ )
197
+ views.setOnClickPendingIntent(R.id.widget_tasks_toggle_group, togglePendingIntent)
198
+
199
+ if (isExpanded) {
200
+ views.setViewVisibility(R.id.widget_tasks_container, View.VISIBLE)
201
+ views.removeAllViews(R.id.widget_tasks_container)
202
+ tasks.forEach { task ->
203
+ val taskView = RemoteViews(context.packageName, R.layout.neoagent_ai_widget_task_row)
204
+ taskView.setTextViewText(R.id.task_name, task.name)
205
+ if (task.triggerSummary.isNotBlank()) {
206
+ taskView.setTextViewText(R.id.task_schedule, task.triggerSummary)
207
+ taskView.setViewVisibility(R.id.task_schedule, View.VISIBLE)
208
+ } else {
209
+ taskView.setViewVisibility(R.id.task_schedule, View.GONE)
210
+ }
211
+
212
+ val runIntent = Intent(context, AiHomeWidgetProvider::class.java).apply {
213
+ action = ACTION_RUN_TASK
214
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
215
+ putExtra(EXTRA_TASK_ID, task.id)
216
+ }
217
+ val bucket = kotlin.math.abs(task.id.hashCode()) % 1000
218
+ val runPendingIntent = PendingIntent.getBroadcast(
219
+ context, appWidgetId * 1000 + bucket, runIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
220
+ )
221
+ taskView.setOnClickPendingIntent(R.id.task_run_btn, runPendingIntent)
222
+
223
+ views.addView(R.id.widget_tasks_container, taskView)
224
+ }
225
+ } else {
226
+ views.setViewVisibility(R.id.widget_tasks_container, View.GONE)
227
+ views.removeAllViews(R.id.widget_tasks_container)
228
+ }
229
+ } else {
230
+ views.setViewVisibility(R.id.widget_tasks_toggle_group, View.GONE)
231
+ views.setViewVisibility(R.id.widget_tasks_container, View.GONE)
232
+ }
233
+
234
+
235
+ val intent =
236
+ Intent(context, MainActivity::class.java).apply {
237
+ action = ACTION_OPEN_WIDGET
238
+ flags =
239
+ Intent.FLAG_ACTIVITY_NEW_TASK or
240
+ Intent.FLAG_ACTIVITY_CLEAR_TOP or
241
+ Intent.FLAG_ACTIVITY_SINGLE_TOP
242
+ putExtra(EXTRA_WIDGET_ID, widget.id)
243
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
244
+ }
245
+ val pendingIntent =
246
+ PendingIntent.getActivity(
247
+ context,
248
+ appWidgetId,
249
+ intent,
250
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
251
+ )
252
+ views.setOnClickPendingIntent(R.id.widget_root, pendingIntent)
253
+ return views
254
+ }
255
+
256
+ private fun bindEmptyState(
257
+ context: Context,
258
+ views: RemoteViews,
259
+ title: String,
260
+ body: String,
261
+ ) {
262
+ views.setTextViewText(R.id.widget_title, title)
263
+ views.setTextViewText(R.id.widget_subtitle, "")
264
+ views.setTextViewText(R.id.widget_metric, "")
265
+ views.setTextViewText(R.id.widget_body, body)
266
+ views.setTextViewText(R.id.widget_meta, context.getString(R.string.app_name))
267
+ views.setTextViewText(R.id.widget_status, "")
268
+ views.setViewVisibility(R.id.widget_metric, View.GONE)
269
+ views.setViewVisibility(R.id.widget_rows_group, View.GONE)
270
+ views.setViewVisibility(R.id.widget_status, View.GONE)
271
+ }
272
+
273
+ private fun bindRow(
274
+ views: RemoteViews,
275
+ viewId: Int,
276
+ row: Pair<String, String>?,
277
+ ) {
278
+ if (row == null) {
279
+ views.setViewVisibility(viewId, View.GONE)
280
+ return
281
+ }
282
+ views.setViewVisibility(viewId, View.VISIBLE)
283
+ val text = if (row.first.isBlank()) row.second else "${row.first}: ${row.second}"
284
+ views.setTextViewText(viewId, text)
285
+ }
286
+
287
+ private fun rows(snapshot: JSONObject?): List<Pair<String, String>> {
288
+ val array = snapshot?.optJSONArray("rows") ?: return emptyList()
289
+ return buildList {
290
+ for (index in 0 until minOf(array.length(), 3)) {
291
+ val row = array.optJSONObject(index) ?: continue
292
+ val label = cleanText(row.optString("label"))
293
+ val value = cleanText(row.optString("value"))
294
+ if (label.isNotBlank() || value.isNotBlank()) {
295
+ add(label to value)
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ private fun supportingRows(
302
+ snapshot: JSONObject?,
303
+ secondaryLabel: String,
304
+ secondaryMetric: String,
305
+ tertiaryLabel: String,
306
+ tertiaryMetric: String,
307
+ ): List<Pair<String, String>> {
308
+ val explicitRows = rows(snapshot)
309
+ if (explicitRows.isNotEmpty()) {
310
+ return explicitRows
311
+ }
312
+ val progress = progressRow(snapshot)
313
+ return listOfNotNull(
314
+ rowOrNull(secondaryLabel, secondaryMetric),
315
+ rowOrNull(tertiaryLabel, tertiaryMetric),
316
+ progress,
317
+ ).take(3)
318
+ }
319
+
320
+ private fun rowOrNull(label: String, value: String): Pair<String, String>? {
321
+ val safeLabel = cleanText(label)
322
+ val safeValue = cleanText(value)
323
+ if (safeLabel.isBlank() && safeValue.isBlank()) {
324
+ return null
325
+ }
326
+ return safeLabel to safeValue
327
+ }
328
+
329
+ private fun labeledValue(label: String, value: String): String {
330
+ val safeLabel = cleanText(label)
331
+ val safeValue = cleanText(value)
332
+ return when {
333
+ safeLabel.isBlank() -> safeValue
334
+ safeValue.isBlank() -> safeLabel
335
+ else -> "$safeLabel $safeValue"
336
+ }
337
+ }
338
+
339
+ private fun progressRow(snapshot: JSONObject?): Pair<String, String>? {
340
+ val progress = snapshot?.optJSONObject("progress") ?: return null
341
+ val value = cleanText(progress.opt("value")?.toString())
342
+ val max = cleanText(progress.opt("max")?.toString())
343
+ val label = cleanText(progress.optString("label")).ifBlank { "Progress" }
344
+ if (value.isBlank() || max.isBlank()) {
345
+ return null
346
+ }
347
+ return label to "$value / $max"
348
+ }
349
+
350
+ private fun cleanText(value: String?): String {
351
+ val normalized = value?.trim().orEmpty()
352
+ return if (normalized.isBlank() || normalized.equals("null", ignoreCase = true)) {
353
+ ""
354
+ } else {
355
+ normalized
356
+ }
357
+ }
358
+
359
+ private fun displayName(raw: String): String {
360
+ val normalized =
361
+ raw.trim()
362
+ .replace(Regex("[_-]+"), " ")
363
+ .replace(Regex("\\s+"), " ")
364
+ if (normalized.isBlank()) {
365
+ return "AI Widget"
366
+ }
367
+ return normalized.split(" ")
368
+ .filter { it.isNotBlank() }
369
+ .joinToString(" ") { part ->
370
+ if (part.length <= 2 && part.uppercase() == part) {
371
+ part
372
+ } else {
373
+ part.substring(0, 1).uppercase() + part.substring(1)
374
+ }
375
+ }
376
+ }
377
+
378
+ private fun cadenceLabel(refreshCron: String): String {
379
+ val normalized = refreshCron.trim()
380
+ if (normalized == "0 * * * *") {
381
+ return "Updates hourly"
382
+ }
383
+ val hours = Regex("\\*/(\\d+)").find(normalized)?.groupValues?.getOrNull(1)?.toIntOrNull()
384
+ if (hours != null && hours > 1) {
385
+ return "Every $hours hours"
386
+ }
387
+ return "Refreshes automatically"
388
+ }
389
+
390
+ private fun formatUpdatedFallback(refreshCron: String): String {
391
+ val normalized = refreshCron.trim()
392
+ if (normalized.isBlank()) {
393
+ return ""
394
+ }
395
+
396
+ val segments = normalized.split(" ").filter { it.isNotBlank() }
397
+ val looksLikeCron =
398
+ segments.size in 5..7 && normalized.any { it.isDigit() || it == '*' || it == '/' }
399
+ if (!looksLikeCron) {
400
+ return normalized
401
+ }
402
+
403
+ val hours = Regex("\\*/(\\d+)").find(normalized)?.groupValues?.getOrNull(1)?.toIntOrNull()
404
+ return when {
405
+ hours != null && hours > 0 -> "Every $hours hours"
406
+ normalized.startsWith("0 0 ") -> "Daily"
407
+ normalized.startsWith("0 0 1 ") -> "Monthly"
408
+ else -> "Auto-refresh enabled"
409
+ }
410
+ }
411
+
412
+ private fun joinChips(snapshot: JSONObject?): String {
413
+ val array = snapshot?.optJSONArray("chips") ?: return ""
414
+ return buildList {
415
+ for (index in 0 until minOf(array.length(), 3)) {
416
+ val chip = array.optString(index).trim()
417
+ if (chip.isNotBlank()) {
418
+ add(chip)
419
+ }
420
+ }
421
+ }.joinToString(" • ")
422
+ }
423
+
424
+ private fun accentColor(token: String, surfaceColor: String): Int {
425
+ parseColor(surfaceColor)?.let { return it }
426
+ return when (token.trim().lowercase()) {
427
+ "warning", "sun", "sunny", "weather" -> 0xFFFFC370.toInt()
428
+ "success", "health", "growth", "battery", "electric" -> 0xFF8EE0AF.toInt()
429
+ "alert", "error", "storm" -> 0xFFFF9A8A.toInt()
430
+ "sky", "ocean", "summary", "rain", "cloud" -> 0xFF81C7F5.toInt()
431
+ "night" -> 0xFFB7C9FF.toInt()
432
+ else -> 0xFF7BC4FF.toInt()
433
+ }
434
+ }
435
+
436
+ private fun parseColor(raw: String): Int? {
437
+ val normalized = cleanText(raw)
438
+ if (normalized.isBlank()) {
439
+ return null
440
+ }
441
+ val hex = if (normalized.startsWith("#")) normalized else "#$normalized"
442
+ return try {
443
+ Color.parseColor(hex)
444
+ } catch (_: IllegalArgumentException) {
445
+ null
446
+ }
447
+ }
448
+
449
+
450
+ const val ACTION_OPEN_WIDGET = "com.neoagent.flutter_app.widgets.OPEN"
451
+ const val ACTION_TOGGLE_TASKS = "com.neoagent.flutter_app.widgets.TOGGLE_TASKS"
452
+ const val ACTION_RUN_TASK = "com.neoagent.flutter_app.widgets.RUN_TASK"
453
+ const val EXTRA_WIDGET_ID = "widgetId"
454
+ const val EXTRA_TASK_ID = "taskId"
455
+
456
+ }
457
+ }