neoagent 2.4.1-beta.8 → 2.4.1

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 (248) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +45 -7
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -2
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +15 -2
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +3 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +338 -46
  52. package/flutter_app/lib/main_devices.dart +851 -122
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +764 -887
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +79 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
  70. package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
  71. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  72. package/flutter_app/lib/src/stream_renderer.dart +205 -35
  73. package/flutter_app/lib/src/theme/palette.dart +76 -34
  74. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  75. package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  81. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  82. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  83. package/flutter_app/pubspec.lock +2 -2
  84. package/flutter_app/pubspec.yaml +7 -1
  85. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  94. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  95. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  96. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  97. package/flutter_app/web/favicon.png +0 -0
  98. package/flutter_app/web/favicon.svg +40 -9
  99. package/flutter_app/web/favicon_light.svg +43 -0
  100. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-192.png +0 -0
  102. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-512.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  106. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  107. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  108. package/flutter_app/windows/runner/main.cpp +7 -1
  109. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  110. package/lib/manager.js +445 -83
  111. package/package.json +17 -3
  112. package/runtime/paths.js +3 -1
  113. package/server/admin/access.js +198 -0
  114. package/server/admin/admin.css +268 -0
  115. package/server/admin/admin.js +348 -0
  116. package/server/admin/analytics.js +128 -0
  117. package/server/admin/index.html +1015 -0
  118. package/server/admin/login.html +290 -0
  119. package/server/admin/logo.svg +43 -0
  120. package/server/admin/sql.js +134 -0
  121. package/server/admin/users.js +147 -0
  122. package/server/config/origins.js +3 -1
  123. package/server/db/database.js +92 -0
  124. package/server/db/ftsQuery.js +27 -0
  125. package/server/http/routes.js +1 -0
  126. package/server/http/static.js +23 -6
  127. package/server/index.js +1 -1
  128. package/server/middleware/adminAuth.js +48 -0
  129. package/server/middleware/auth.js +1 -40
  130. package/server/public/.last_build_id +1 -1
  131. package/server/public/app_icon.png +0 -0
  132. package/server/public/assets/AssetManifest.bin +1 -1
  133. package/server/public/assets/AssetManifest.bin.json +1 -1
  134. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  138. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  139. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  140. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  141. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  142. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  143. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  144. package/server/public/assets/web/icons/Icon-192.png +0 -0
  145. package/server/public/favicon.png +0 -0
  146. package/server/public/favicon.svg +40 -9
  147. package/server/public/favicon_light.svg +43 -0
  148. package/server/public/flutter_bootstrap.js +2 -2
  149. package/server/public/icons/Icon-192-light.png +0 -0
  150. package/server/public/icons/Icon-192.png +0 -0
  151. package/server/public/icons/Icon-512-light.png +0 -0
  152. package/server/public/icons/Icon-512.png +0 -0
  153. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-192.png +0 -0
  155. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  156. package/server/public/icons/Icon-maskable-512.png +0 -0
  157. package/server/public/main.dart.js +88944 -85720
  158. package/server/routes/admin.js +632 -0
  159. package/server/routes/agent_profiles.js +3 -0
  160. package/server/routes/agents.js +1 -1
  161. package/server/routes/auth.js +3 -1
  162. package/server/routes/browser.js +14 -0
  163. package/server/routes/browser_extension.js +21 -4
  164. package/server/routes/desktop.js +10 -0
  165. package/server/routes/mcp.js +29 -13
  166. package/server/routes/memory.js +23 -1
  167. package/server/routes/messaging.js +2 -0
  168. package/server/routes/screenHistory.js +14 -8
  169. package/server/routes/settings.js +14 -2
  170. package/server/routes/stream.js +12 -3
  171. package/server/routes/triggers.js +4 -4
  172. package/server/routes/voice_assistant.js +36 -1
  173. package/server/routes/workspace.js +86 -0
  174. package/server/services/account/admin_two_factor.js +132 -0
  175. package/server/services/account/password_policy.js +6 -1
  176. package/server/services/ai/completion.js +44 -0
  177. package/server/services/ai/engine.js +321 -500
  178. package/server/services/ai/imageAnalysis.js +9 -5
  179. package/server/services/ai/logFormat.js +46 -0
  180. package/server/services/ai/loopPolicy.js +11 -0
  181. package/server/services/ai/messagingFallback.js +228 -0
  182. package/server/services/ai/models.js +194 -55
  183. package/server/services/ai/providerRetry.js +169 -0
  184. package/server/services/ai/providers/anthropic.js +15 -4
  185. package/server/services/ai/providers/google.js +38 -11
  186. package/server/services/ai/providers/grok.js +19 -68
  187. package/server/services/ai/providers/grokOauth.js +141 -0
  188. package/server/services/ai/providers/nvidia.js +154 -0
  189. package/server/services/ai/providers/ollama.js +76 -39
  190. package/server/services/ai/providers/openai.js +20 -31
  191. package/server/services/ai/providers/openaiCodex.js +6 -2
  192. package/server/services/ai/providers/openaiCompatible.js +70 -0
  193. package/server/services/ai/providers/openrouter.js +162 -0
  194. package/server/services/ai/settings.js +30 -0
  195. package/server/services/ai/systemPrompt.js +51 -29
  196. package/server/services/ai/taskAnalysis.js +60 -1
  197. package/server/services/ai/toolEvidence.js +207 -0
  198. package/server/services/ai/toolSelector.js +8 -1
  199. package/server/services/ai/tools.js +80 -15
  200. package/server/services/android/controller.js +36 -1
  201. package/server/services/browser/controller.js +18 -0
  202. package/server/services/browser/extension/manifest.js +33 -0
  203. package/server/services/browser/extension/provider.js +12 -6
  204. package/server/services/browser/extension/registry.js +229 -18
  205. package/server/services/desktop/protocol.js +1 -0
  206. package/server/services/desktop/provider.js +4 -0
  207. package/server/services/desktop/registry.js +104 -1
  208. package/server/services/desktop/screenRecorder.js +208 -98
  209. package/server/services/desktop/screen_recorder_support.js +46 -0
  210. package/server/services/integrations/google/provider.js +13 -0
  211. package/server/services/integrations/home_assistant/constants.js +88 -0
  212. package/server/services/integrations/home_assistant/network.js +207 -0
  213. package/server/services/integrations/home_assistant/provider.js +249 -0
  214. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  215. package/server/services/integrations/home_assistant/tools.js +101 -0
  216. package/server/services/integrations/manager.js +5 -0
  217. package/server/services/integrations/registry.js +2 -0
  218. package/server/services/integrations/trello/provider.js +8 -2
  219. package/server/services/manager.js +51 -47
  220. package/server/services/mcp/client.js +208 -268
  221. package/server/services/mcp/client_support.js +172 -0
  222. package/server/services/mcp/recovery.js +116 -0
  223. package/server/services/mcp/tool_operations.js +123 -0
  224. package/server/services/memory/ingestion.js +185 -370
  225. package/server/services/memory/ingestion_coverage.js +129 -0
  226. package/server/services/memory/ingestion_documents.js +123 -0
  227. package/server/services/memory/ingestion_support.js +171 -0
  228. package/server/services/memory/intelligence.js +181 -0
  229. package/server/services/memory/manager.js +476 -32
  230. package/server/services/messaging/automation.js +71 -134
  231. package/server/services/messaging/formatting_guides.js +26 -1
  232. package/server/services/messaging/inbound_queue.js +111 -0
  233. package/server/services/messaging/manager.js +25 -8
  234. package/server/services/messaging/typing_keepalive.js +110 -0
  235. package/server/services/runtime/manager.js +7 -3
  236. package/server/services/runtime/settings.js +17 -0
  237. package/server/services/streaming/android-stream.js +296 -32
  238. package/server/services/streaming/stream-hub.js +1 -1
  239. package/server/services/tasks/integration_runtime.js +4 -1
  240. package/server/services/tasks/runtime.js +415 -56
  241. package/server/services/tasks/task_repository.js +48 -6
  242. package/server/services/websocket.js +8 -2
  243. package/server/services/widgets/service.js +17 -1
  244. package/server/services/workspace/manager.js +116 -0
  245. package/server/utils/logger.js +44 -6
  246. package/server/utils/security.js +3 -0
  247. package/server/utils/version.js +29 -19
  248. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -188,7 +188,9 @@ class OfficialIntegrationsTab extends StatelessWidget {
188
188
  !item.env.configured
189
189
  ? item.env.summary
190
190
  : item.hasExpiredAccounts
191
- ? 'One or more accounts expired. Reconnect the affected account to restore tool access.'
191
+ ? item.id == 'google_workspace'
192
+ ? 'One or more accounts expired. Reconnect to restore access. If this keeps happening, your Google Cloud OAuth app may be in Testing mode — publish it to Production in Google Cloud Console to get long-lived tokens.'
193
+ : 'One or more accounts expired. Reconnect the affected account to restore tool access.'
192
194
  : !item.supportsMultipleAccounts && item.isConnected
193
195
  ? 'This integration currently supports one connected account per agent. Re-open setup to replace it.'
194
196
  : item.isConnected
@@ -226,12 +228,255 @@ void _openOfficialIntegrationSetupDialog(
226
228
  String providerId,
227
229
  ) {
228
230
  switch (providerId) {
231
+ case 'home_assistant':
232
+ _showHomeAssistantSetupDialog(context, controller);
233
+ return;
229
234
  case 'trello':
230
235
  _showTrelloSetupDialog(context, controller);
231
236
  return;
232
237
  }
233
238
  }
234
239
 
240
+ Future<void> _showHomeAssistantSetupDialog(
241
+ BuildContext context,
242
+ NeoAgentController controller,
243
+ ) async {
244
+ Map<String, dynamic> existing;
245
+ try {
246
+ existing = await controller.getOfficialIntegrationConfig('home_assistant');
247
+ } catch (error) {
248
+ if (context.mounted) {
249
+ ScaffoldMessenger.of(context).showSnackBar(
250
+ SnackBar(content: Text(controller.errorMessage ?? error.toString())),
251
+ );
252
+ }
253
+ return;
254
+ }
255
+
256
+ final savedBaseUrl = existing['baseUrl']?.toString() ?? '';
257
+ final hasToken = existing['hasToken'] == true;
258
+ final accountCount = (existing['accountCount'] as num?)?.toInt() ?? 0;
259
+ final hasConnectedAccount =
260
+ existing['hasConnectedAccount'] == true || accountCount > 0;
261
+ var formError = '';
262
+ var saving = false;
263
+
264
+ final baseUrlController = TextEditingController(text: savedBaseUrl);
265
+ final tokenController = TextEditingController();
266
+
267
+ if (!context.mounted) return;
268
+ await showDialog<void>(
269
+ context: context,
270
+ barrierDismissible: false,
271
+ builder: (dialogContext) {
272
+ return StatefulBuilder(
273
+ builder: (dialogContext, setState) {
274
+ return AlertDialog(
275
+ title: const Text('Home Assistant Setup'),
276
+ content: SizedBox(
277
+ width: 520,
278
+ child: Column(
279
+ mainAxisSize: MainAxisSize.min,
280
+ crossAxisAlignment: CrossAxisAlignment.start,
281
+ children: <Widget>[
282
+ Text(
283
+ 'Connect a public HTTPS Home Assistant endpoint with a Long-Lived Access Token. Local, loopback, and private network addresses are blocked by the server.',
284
+ style: TextStyle(color: _textSecondary),
285
+ ),
286
+ const SizedBox(height: 16),
287
+ _IntegrationSetupStatusItem(
288
+ label: 'Endpoint',
289
+ status: savedBaseUrl.trim().isNotEmpty
290
+ ? 'Configured'
291
+ : 'Not configured',
292
+ isConnected: savedBaseUrl.trim().isNotEmpty,
293
+ ),
294
+ const SizedBox(height: 12),
295
+ _IntegrationSetupStatusItem(
296
+ label: 'Connected Instance',
297
+ status: hasConnectedAccount
298
+ ? '$accountCount ${accountCount == 1 ? 'instance' : 'instances'} connected'
299
+ : 'Not connected',
300
+ isConnected: hasConnectedAccount,
301
+ ),
302
+ const SizedBox(height: 12),
303
+ TextField(
304
+ controller: baseUrlController,
305
+ onChanged: (_) => setState(() {}),
306
+ keyboardType: TextInputType.url,
307
+ decoration: const InputDecoration(
308
+ labelText: 'Home Assistant URL',
309
+ hintText: 'https://your-instance.example.com',
310
+ border: OutlineInputBorder(),
311
+ ),
312
+ ),
313
+ const SizedBox(height: 12),
314
+ TextField(
315
+ controller: tokenController,
316
+ onChanged: (_) => setState(() {}),
317
+ obscureText: true,
318
+ decoration: InputDecoration(
319
+ labelText: hasToken
320
+ ? 'Paste replacement Long-Lived Access Token'
321
+ : 'Long-Lived Access Token',
322
+ border: const OutlineInputBorder(),
323
+ ),
324
+ ),
325
+ if (hasToken) ...<Widget>[
326
+ const SizedBox(height: 8),
327
+ Text(
328
+ 'Leave the token empty to keep the currently stored token.',
329
+ style: TextStyle(color: _textSecondary, fontSize: 12),
330
+ ),
331
+ ],
332
+ if (formError.isNotEmpty) ...<Widget>[
333
+ const SizedBox(height: 12),
334
+ Container(
335
+ padding: const EdgeInsets.all(8),
336
+ decoration: BoxDecoration(
337
+ color: _danger.withValues(alpha: 0.1),
338
+ borderRadius: BorderRadius.circular(4),
339
+ border: Border.all(
340
+ color: _danger.withValues(alpha: 0.3),
341
+ ),
342
+ ),
343
+ child: Text(
344
+ formError,
345
+ style: TextStyle(color: _danger, fontSize: 12),
346
+ ),
347
+ ),
348
+ ],
349
+ ],
350
+ ),
351
+ ),
352
+ actions: <Widget>[
353
+ if (savedBaseUrl.trim().isNotEmpty || hasToken)
354
+ TextButton(
355
+ onPressed: saving
356
+ ? null
357
+ : () async {
358
+ final shouldClear =
359
+ await showDialog<bool>(
360
+ context: dialogContext,
361
+ builder: (context) {
362
+ return AlertDialog(
363
+ title: const Text(
364
+ 'Disconnect Home Assistant?',
365
+ ),
366
+ content: const Text(
367
+ 'This removes the Home Assistant setup and connected instance for this agent.',
368
+ ),
369
+ actions: [
370
+ TextButton(
371
+ onPressed: () =>
372
+ Navigator.of(context).pop(false),
373
+ child: const Text('Cancel'),
374
+ ),
375
+ FilledButton(
376
+ onPressed: () =>
377
+ Navigator.of(context).pop(true),
378
+ child: const Text('Disconnect'),
379
+ ),
380
+ ],
381
+ );
382
+ },
383
+ ) ??
384
+ false;
385
+ if (!shouldClear) {
386
+ return;
387
+ }
388
+ setState(() {
389
+ formError = '';
390
+ saving = true;
391
+ });
392
+ try {
393
+ await controller.clearOfficialIntegrationConfig(
394
+ 'home_assistant',
395
+ );
396
+ if (dialogContext.mounted) {
397
+ Navigator.of(dialogContext).pop();
398
+ }
399
+ } catch (_) {
400
+ setState(() {
401
+ formError =
402
+ controller.errorMessage ??
403
+ 'Could not disconnect Home Assistant.';
404
+ saving = false;
405
+ });
406
+ }
407
+ },
408
+ child: const Text('Disconnect'),
409
+ ),
410
+ TextButton(
411
+ onPressed: saving
412
+ ? null
413
+ : () => Navigator.of(dialogContext).pop(),
414
+ child: const Text('Close'),
415
+ ),
416
+ FilledButton(
417
+ onPressed: saving
418
+ ? null
419
+ : () async {
420
+ setState(() {
421
+ formError = '';
422
+ saving = true;
423
+ });
424
+ try {
425
+ final baseUrl = baseUrlController.text.trim();
426
+ final token = tokenController.text.trim();
427
+ if (baseUrl.isEmpty) {
428
+ setState(() {
429
+ formError = 'Home Assistant URL is required.';
430
+ saving = false;
431
+ });
432
+ return;
433
+ }
434
+ if (token.isEmpty && !hasToken) {
435
+ setState(() {
436
+ formError =
437
+ 'Home Assistant Long-Lived Access Token is required.';
438
+ saving = false;
439
+ });
440
+ return;
441
+ }
442
+ await controller.saveOfficialIntegrationConfig(
443
+ 'home_assistant',
444
+ config: <String, dynamic>{
445
+ 'baseUrl': baseUrl,
446
+ if (token.isNotEmpty) 'token': token,
447
+ },
448
+ );
449
+ if (dialogContext.mounted) {
450
+ Navigator.of(dialogContext).pop();
451
+ }
452
+ } catch (_) {
453
+ setState(() {
454
+ formError =
455
+ controller.errorMessage ??
456
+ 'Could not save Home Assistant setup.';
457
+ saving = false;
458
+ });
459
+ }
460
+ },
461
+ child: Text(
462
+ saving
463
+ ? 'Saving...'
464
+ : hasConnectedAccount
465
+ ? 'Update Instance'
466
+ : 'Connect Instance',
467
+ ),
468
+ ),
469
+ ],
470
+ );
471
+ },
472
+ );
473
+ },
474
+ );
475
+
476
+ baseUrlController.dispose();
477
+ tokenController.dispose();
478
+ }
479
+
235
480
  Future<void> _showTrelloSetupDialog(
236
481
  BuildContext context,
237
482
  NeoAgentController controller,
@@ -281,13 +526,13 @@ Future<void> _showTrelloSetupDialog(
281
526
  style: TextStyle(color: _textSecondary),
282
527
  ),
283
528
  const SizedBox(height: 16),
284
- _TrelloStatusItem(
529
+ _IntegrationSetupStatusItem(
285
530
  label: 'API Key',
286
531
  status: apiKeyConfigured ? 'Configured' : 'Not configured',
287
532
  isConnected: apiKeyConfigured,
288
533
  ),
289
534
  const SizedBox(height: 12),
290
- _TrelloStatusItem(
535
+ _IntegrationSetupStatusItem(
291
536
  label: 'Connected Account',
292
537
  status: hasConnectedAccount
293
538
  ? '$accountCount ${accountCount == 1 ? 'connected account' : 'connected accounts'}'
@@ -335,7 +580,9 @@ Future<void> _showTrelloSetupDialog(
335
580
  decoration: BoxDecoration(
336
581
  color: _danger.withValues(alpha: 0.1),
337
582
  borderRadius: BorderRadius.circular(4),
338
- border: Border.all(color: _danger.withValues(alpha: 0.3)),
583
+ border: Border.all(
584
+ color: _danger.withValues(alpha: 0.3),
585
+ ),
339
586
  ),
340
587
  child: Text(
341
588
  formError,
@@ -530,8 +777,8 @@ Future<void> _showTrelloSetupDialog(
530
777
  tokenInputController.dispose();
531
778
  }
532
779
 
533
- class _TrelloStatusItem extends StatelessWidget {
534
- const _TrelloStatusItem({
780
+ class _IntegrationSetupStatusItem extends StatelessWidget {
781
+ const _IntegrationSetupStatusItem({
535
782
  required this.label,
536
783
  required this.status,
537
784
  required this.isConnected,
@@ -884,11 +1131,13 @@ class _OfficialIntegrationIcon extends StatelessWidget {
884
1131
  Widget build(BuildContext context) {
885
1132
  final color = switch (item.icon) {
886
1133
  'google' => const Color(0xFF4285F4),
1134
+ 'home_assistant' => const Color(0xFF41BDF5),
887
1135
  'trello' => const Color(0xFF0C66E4),
888
1136
  _ => _accent,
889
1137
  };
890
1138
  final label = switch (item.icon) {
891
1139
  'google' => 'G',
1140
+ 'home_assistant' => 'H',
892
1141
  'trello' => 'T',
893
1142
  _ => item.label.isNotEmpty ? item.label[0] : '?',
894
1143
  };
@@ -822,7 +822,7 @@ class _LauncherHomeViewState extends State<LauncherHomeView> {
822
822
  const SizedBox(height: 10),
823
823
  Text(
824
824
  controller.backendUrl,
825
- style: GoogleFonts.jetBrainsMono(
825
+ style: GoogleFonts.geistMono(
826
826
  fontSize: 12,
827
827
  color: _textSecondary,
828
828
  ),