neoagent 3.1.1-beta.9 → 3.2.0

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 (150) hide show
  1. package/.env.example +2 -2
  2. package/README.md +5 -1
  3. package/docs/architecture.md +1 -1
  4. package/docs/benchmarking.md +68 -87
  5. package/docs/capabilities.md +1 -1
  6. package/docs/clients-and-devices.md +3 -4
  7. package/docs/configuration.md +1 -1
  8. package/docs/devices.md +1 -2
  9. package/docs/health.md +24 -0
  10. package/docs/index.md +2 -2
  11. package/docs/integrations-architecture.md +6 -0
  12. package/docs/integrations.md +15 -1
  13. package/docs/models.md +1 -1
  14. package/docs/persistence.md +1 -1
  15. package/docs/why-neoagent.md +1 -1
  16. package/flutter_app/README.md +0 -5
  17. package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
  19. package/flutter_app/lib/main.dart +0 -6
  20. package/flutter_app/lib/main_app_shell.dart +0 -2
  21. package/flutter_app/lib/main_controller.dart +2 -867
  22. package/flutter_app/lib/main_devices.dart +0 -73
  23. package/flutter_app/lib/main_integrations.dart +339 -187
  24. package/flutter_app/lib/main_launcher.dart +2 -212
  25. package/flutter_app/lib/main_models.dart +0 -259
  26. package/flutter_app/lib/main_navigation.dart +2 -14
  27. package/flutter_app/lib/main_operations.dart +322 -364
  28. package/flutter_app/lib/main_runtime.dart +9 -208
  29. package/flutter_app/lib/main_settings.dart +17 -199
  30. package/flutter_app/lib/main_shared.dart +20 -591
  31. package/flutter_app/lib/main_timeline.dart +26 -122
  32. package/flutter_app/lib/main_voice_assistant.dart +0 -5
  33. package/flutter_app/lib/src/backend_client.dart +0 -102
  34. package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
  35. package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
  36. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
  37. package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
  38. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
  39. package/flutter_app/macos/Podfile.lock +56 -5
  40. package/flutter_app/pubspec.lock +0 -7
  41. package/flutter_app/pubspec.yaml +0 -2
  42. package/flutter_app/web/index.html +174 -0
  43. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
  44. package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
  45. package/landing/index.html +0 -2
  46. package/lib/manager.js +1 -1
  47. package/lib/schema_migrations.js +75 -0
  48. package/package.json +1 -4
  49. package/server/db/database.js +1 -133
  50. package/server/http/middleware.js +2 -16
  51. package/server/http/routes.js +0 -2
  52. package/server/public/.last_build_id +1 -1
  53. package/server/public/assets/NOTICES +0 -25
  54. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  55. package/server/public/flutter_bootstrap.js +1 -1
  56. package/server/public/index.html +174 -0
  57. package/server/public/main.dart.js +88081 -90036
  58. package/server/routes/auth.js +11 -2
  59. package/server/routes/settings.js +0 -5
  60. package/server/routes/voice_assistant.js +1 -128
  61. package/server/services/account/erasure.js +0 -3
  62. package/server/services/ai/deliverables/artifact_helpers.js +1 -1
  63. package/server/services/ai/loop/conversation_loop.js +6 -2
  64. package/server/services/ai/loop/tool_dispatch.js +0 -3
  65. package/server/services/ai/messagingFallback.js +1 -1
  66. package/server/services/ai/settings.js +0 -6
  67. package/server/services/ai/toolEvidence.js +1 -4
  68. package/server/services/ai/tools.js +0 -234
  69. package/server/services/desktop/auth.js +0 -3
  70. package/server/services/desktop/registry.js +2 -50
  71. package/server/services/integrations/neoarchive/provider.js +616 -0
  72. package/server/services/integrations/neorecall/client.js +140 -0
  73. package/server/services/integrations/neorecall/constants.js +87 -0
  74. package/server/services/integrations/neorecall/provider.js +163 -0
  75. package/server/services/integrations/registry.js +4 -2
  76. package/server/services/manager.js +0 -22
  77. package/server/services/memory/manager.js +44 -19
  78. package/server/services/memory/retrieval_reasoning.js +36 -14
  79. package/server/services/messaging/whatsapp.js +18 -0
  80. package/server/services/security/tool_categories.js +0 -5
  81. package/server/services/social_video/service.js +2 -20
  82. package/server/services/tasks/adapters/index.js +1 -1
  83. package/server/services/tasks/integration_runtime.js +3 -67
  84. package/server/services/tasks/runtime.js +1 -6
  85. package/server/services/tasks/task_repository.js +6 -13
  86. package/server/services/timeline/service.js +0 -224
  87. package/server/services/voice/message.js +0 -24
  88. package/server/services/voice/providers.js +42 -1
  89. package/server/services/voice/runtimeManager.js +9 -5
  90. package/server/services/voice/turnRunner.js +0 -22
  91. package/server/services/wearable/service.js +0 -5
  92. package/docs/recordings-and-health.md +0 -41
  93. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
  94. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
  95. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
  96. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
  97. package/flutter_app/lib/main_recordings.dart +0 -920
  98. package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
  99. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
  100. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
  101. package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
  102. package/flutter_app/lib/src/recording_bridge.dart +0 -232
  103. package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
  104. package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
  105. package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
  106. package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
  107. package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
  108. package/flutter_app/lib/src/recording_payloads.dart +0 -95
  109. package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
  110. package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
  111. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
  112. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
  113. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
  114. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
  115. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
  116. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
  117. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
  118. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
  119. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
  120. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
  121. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
  122. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
  123. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
  124. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
  125. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
  126. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
  127. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
  128. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
  129. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
  130. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
  131. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
  132. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
  133. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
  134. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
  135. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
  136. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
  137. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
  138. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
  139. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
  140. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
  141. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
  142. package/server/routes/recordings.js +0 -335
  143. package/server/routes/screenHistory.js +0 -190
  144. package/server/services/ai/recordingInsights.js +0 -105
  145. package/server/services/integrations/neomail/provider.js +0 -993
  146. package/server/services/recordings/deepgram.js +0 -58
  147. package/server/services/recordings/manager.js +0 -1273
  148. package/server/services/tasks/adapters/neomail_email_received.js +0 -46
  149. package/server/services/voice/screenshotContext.js +0 -73
  150. package/server/services/voice/turnCoordinator.js +0 -51
@@ -233,8 +233,11 @@ void _openOfficialIntegrationSetupDialog(
233
233
  case 'home_assistant':
234
234
  _showHomeAssistantSetupDialog(context, controller);
235
235
  return;
236
- case 'neomail':
237
- _showNeoMailSetupDialog(context, controller);
236
+ case 'neoarchive':
237
+ _showNeoArchiveSetupDialog(context, controller);
238
+ return;
239
+ case 'neorecall':
240
+ _showNeoRecallSetupDialog(context, controller);
238
241
  return;
239
242
  case 'trello':
240
243
  _showTrelloSetupDialog(context, controller);
@@ -242,13 +245,13 @@ void _openOfficialIntegrationSetupDialog(
242
245
  }
243
246
  }
244
247
 
245
- Future<void> _showNeoMailSetupDialog(
248
+ Future<void> _showNeoRecallSetupDialog(
246
249
  BuildContext context,
247
250
  NeoAgentController controller,
248
251
  ) async {
249
252
  Map<String, dynamic> existing;
250
253
  try {
251
- existing = await controller.getOfficialIntegrationConfig('neomail');
254
+ existing = await controller.getOfficialIntegrationConfig('neorecall');
252
255
  } catch (error) {
253
256
  if (context.mounted) {
254
257
  ScaffoldMessenger.of(context).showSnackBar(
@@ -257,57 +260,241 @@ Future<void> _showNeoMailSetupDialog(
257
260
  }
258
261
  return;
259
262
  }
260
-
261
263
  final savedBaseUrl = existing['baseUrl']?.toString() ?? '';
262
264
  final accountCount = (existing['accountCount'] as num?)?.toInt() ?? 0;
263
- final hasConnectedAccount =
264
- existing['hasConnectedAccount'] == true || accountCount > 0;
265
- var formError = '';
265
+ final connected = existing['hasConnectedAccount'] == true || accountCount > 0;
266
+ final baseUrlController = TextEditingController(text: savedBaseUrl);
267
+ var errorText = '';
266
268
  var busy = false;
267
269
 
270
+ Future<void> save(
271
+ StateSetter setState,
272
+ BuildContext dialogContext, {
273
+ required bool connect,
274
+ }) async {
275
+ setState(() {
276
+ errorText = '';
277
+ busy = true;
278
+ });
279
+ try {
280
+ final baseUrl = baseUrlController.text.trim();
281
+ if (baseUrl.isEmpty) {
282
+ setState(() {
283
+ errorText = 'NeoRecall backend URL is required.';
284
+ busy = false;
285
+ });
286
+ return;
287
+ }
288
+ await controller.saveOfficialIntegrationConfig(
289
+ 'neorecall',
290
+ config: <String, dynamic>{'baseUrl': baseUrl},
291
+ );
292
+ if (connect) {
293
+ await controller.connectOfficialIntegration(
294
+ 'neorecall',
295
+ appId: 'recall',
296
+ );
297
+ if ((controller.errorMessage ?? '').trim().isNotEmpty) {
298
+ setState(() {
299
+ errorText = controller.errorMessage!;
300
+ busy = false;
301
+ });
302
+ return;
303
+ }
304
+ }
305
+ if (dialogContext.mounted) Navigator.of(dialogContext).pop();
306
+ } catch (_) {
307
+ setState(() {
308
+ errorText = controller.errorMessage ?? 'Could not save NeoRecall setup.';
309
+ busy = false;
310
+ });
311
+ }
312
+ }
313
+
314
+ if (!context.mounted) return;
315
+ await showDialog<void>(
316
+ context: context,
317
+ barrierDismissible: false,
318
+ builder: (dialogContext) => StatefulBuilder(
319
+ builder: (dialogContext, setState) => AlertDialog(
320
+ title: const Text('NeoRecall Setup'),
321
+ content: SizedBox(
322
+ width: 540,
323
+ child: Column(
324
+ mainAxisSize: MainAxisSize.min,
325
+ crossAxisAlignment: CrossAxisAlignment.start,
326
+ children: <Widget>[
327
+ Text(
328
+ 'Connect your self-hosted NeoRecall server. NeoAgent receives read-only access to local search, memories, and transcript evidence after you approve the OAuth screen.',
329
+ style: TextStyle(color: _textSecondary),
330
+ ),
331
+ const SizedBox(height: 16),
332
+ const _IntegrationSetupStatusItem(
333
+ label: 'Connection Method',
334
+ status: 'OAuth with PKCE',
335
+ isConnected: true,
336
+ ),
337
+ const SizedBox(height: 12),
338
+ _IntegrationSetupStatusItem(
339
+ label: 'Connected NeoRecall User',
340
+ status: connected
341
+ ? '$accountCount ${accountCount == 1 ? 'connected user' : 'connected users'}'
342
+ : 'Not connected',
343
+ isConnected: connected,
344
+ ),
345
+ const SizedBox(height: 12),
346
+ TextField(
347
+ controller: baseUrlController,
348
+ keyboardType: TextInputType.url,
349
+ decoration: const InputDecoration(
350
+ labelText: 'NeoRecall Backend URL',
351
+ hintText: 'https://recall.example.com',
352
+ border: OutlineInputBorder(),
353
+ ),
354
+ ),
355
+ const SizedBox(height: 8),
356
+ Text(
357
+ 'Local and private-network URLs are supported when the NeoAgent server can reach them. Audio is never exposed to NeoAgent.',
358
+ style: TextStyle(color: _textSecondary, fontSize: 12),
359
+ ),
360
+ if (errorText.isNotEmpty) ...<Widget>[
361
+ const SizedBox(height: 12),
362
+ Text(errorText, style: TextStyle(color: _danger, fontSize: 12)),
363
+ ],
364
+ ],
365
+ ),
366
+ ),
367
+ actions: <Widget>[
368
+ if (savedBaseUrl.isNotEmpty)
369
+ TextButton(
370
+ onPressed: busy
371
+ ? null
372
+ : () async {
373
+ final confirm =
374
+ await showDialog<bool>(
375
+ context: dialogContext,
376
+ builder: (context) => AlertDialog(
377
+ title: const Text('Disconnect NeoRecall?'),
378
+ content: const Text(
379
+ 'This removes the NeoRecall backend URL and all connected NeoRecall accounts for this agent.',
380
+ ),
381
+ actions: <Widget>[
382
+ TextButton(
383
+ onPressed: () => Navigator.of(context).pop(false),
384
+ child: const Text('Cancel'),
385
+ ),
386
+ FilledButton(
387
+ onPressed: () => Navigator.of(context).pop(true),
388
+ child: const Text('Disconnect'),
389
+ ),
390
+ ],
391
+ ),
392
+ ) ??
393
+ false;
394
+ if (!confirm) return;
395
+ setState(() {
396
+ busy = true;
397
+ errorText = '';
398
+ });
399
+ try {
400
+ await controller.clearOfficialIntegrationConfig('neorecall');
401
+ if (dialogContext.mounted) Navigator.of(dialogContext).pop();
402
+ } catch (_) {
403
+ setState(() {
404
+ errorText = controller.errorMessage ?? 'Could not disconnect NeoRecall.';
405
+ busy = false;
406
+ });
407
+ }
408
+ },
409
+ child: const Text('Disconnect'),
410
+ ),
411
+ TextButton(
412
+ onPressed: busy ? null : () => Navigator.of(dialogContext).pop(),
413
+ child: const Text('Close'),
414
+ ),
415
+ TextButton(
416
+ onPressed: busy ? null : () => save(setState, dialogContext, connect: false),
417
+ child: const Text('Save Only'),
418
+ ),
419
+ FilledButton(
420
+ onPressed: busy ? null : () => save(setState, dialogContext, connect: true),
421
+ child: Text(
422
+ busy
423
+ ? 'Working...'
424
+ : connected
425
+ ? 'Connect Another Account'
426
+ : 'Save & Connect',
427
+ ),
428
+ ),
429
+ ],
430
+ ),
431
+ ),
432
+ );
433
+ baseUrlController.dispose();
434
+ }
435
+
436
+ Future<void> _showNeoArchiveSetupDialog(
437
+ BuildContext context,
438
+ NeoAgentController controller,
439
+ ) async {
440
+ Map<String, dynamic> existing;
441
+ try {
442
+ existing = await controller.getOfficialIntegrationConfig('neoarchive');
443
+ } catch (error) {
444
+ if (context.mounted) {
445
+ ScaffoldMessenger.of(context).showSnackBar(
446
+ SnackBar(content: Text(controller.errorMessage ?? error.toString())),
447
+ );
448
+ }
449
+ return;
450
+ }
451
+ final savedBaseUrl = existing['baseUrl']?.toString() ?? '';
452
+ final accountCount = (existing['accountCount'] as num?)?.toInt() ?? 0;
453
+ final connected = existing['hasConnectedAccount'] == true || accountCount > 0;
268
454
  final baseUrlController = TextEditingController(text: savedBaseUrl);
455
+ var errorText = '';
456
+ var busy = false;
269
457
 
270
- Future<void> saveConfig(
271
- StateSetter setState, {
272
- required BuildContext dialogContext,
273
- required bool connectAfterSave,
458
+ Future<void> save(
459
+ StateSetter setState,
460
+ BuildContext dialogContext, {
461
+ required bool connect,
274
462
  }) async {
275
463
  setState(() {
276
- formError = '';
464
+ errorText = '';
277
465
  busy = true;
278
466
  });
279
467
  try {
280
468
  final baseUrl = baseUrlController.text.trim();
281
469
  if (baseUrl.isEmpty) {
282
470
  setState(() {
283
- formError = 'NeoMail backend URL is required.';
471
+ errorText = 'NeoArchive backend URL is required.';
284
472
  busy = false;
285
473
  });
286
474
  return;
287
475
  }
288
476
  await controller.saveOfficialIntegrationConfig(
289
- 'neomail',
477
+ 'neoarchive',
290
478
  config: <String, dynamic>{'baseUrl': baseUrl},
291
479
  );
292
- if (connectAfterSave) {
480
+ if (connect) {
293
481
  await controller.connectOfficialIntegration(
294
- 'neomail',
295
- appId: 'mailbox',
482
+ 'neoarchive',
483
+ appId: 'archive',
296
484
  );
297
485
  if ((controller.errorMessage ?? '').trim().isNotEmpty) {
298
486
  setState(() {
299
- formError = controller.errorMessage!;
487
+ errorText = controller.errorMessage!;
300
488
  busy = false;
301
489
  });
302
490
  return;
303
491
  }
304
492
  }
305
- if (dialogContext.mounted) {
306
- Navigator.of(dialogContext).pop();
307
- }
493
+ if (dialogContext.mounted) Navigator.of(dialogContext).pop();
308
494
  } catch (_) {
309
495
  setState(() {
310
- formError = controller.errorMessage ?? 'Could not save NeoMail setup.';
496
+ errorText =
497
+ controller.errorMessage ?? 'Could not save NeoArchive setup.';
311
498
  busy = false;
312
499
  });
313
500
  }
@@ -317,175 +504,134 @@ Future<void> _showNeoMailSetupDialog(
317
504
  await showDialog<void>(
318
505
  context: context,
319
506
  barrierDismissible: false,
320
- builder: (dialogContext) {
321
- return StatefulBuilder(
322
- builder: (dialogContext, setState) {
323
- return AlertDialog(
324
- title: const Text('NeoMail Setup'),
325
- content: SizedBox(
326
- width: 540,
327
- child: Column(
328
- mainAxisSize: MainAxisSize.min,
329
- crossAxisAlignment: CrossAxisAlignment.start,
330
- children: <Widget>[
331
- Text(
332
- 'Add the NeoMail backend URL once. NeoAgent will then open NeoMail OAuth so the user can sign in and approve mailbox access without API keys.',
333
- style: TextStyle(color: _textSecondary),
334
- ),
335
- const SizedBox(height: 16),
336
- const _IntegrationSetupStatusItem(
337
- label: 'Connection Method',
338
- status: 'OAuth companion flow',
339
- isConnected: true,
340
- ),
341
- const SizedBox(height: 12),
342
- _IntegrationSetupStatusItem(
343
- label: 'Backend URL',
344
- status: savedBaseUrl.trim().isNotEmpty
345
- ? 'Configured'
346
- : 'Not configured',
347
- isConnected: savedBaseUrl.trim().isNotEmpty,
348
- ),
349
- const SizedBox(height: 12),
350
- _IntegrationSetupStatusItem(
351
- label: 'Connected NeoMail User',
352
- status: hasConnectedAccount
353
- ? '$accountCount ${accountCount == 1 ? 'connected user' : 'connected users'}'
354
- : 'Not connected',
355
- isConnected: hasConnectedAccount,
356
- ),
357
- const SizedBox(height: 12),
358
- TextField(
359
- controller: baseUrlController,
360
- onChanged: (_) => setState(() {}),
361
- keyboardType: TextInputType.url,
362
- decoration: const InputDecoration(
363
- labelText: 'NeoMail Backend URL',
364
- hintText: 'https://mail.example.com',
365
- border: OutlineInputBorder(),
366
- ),
367
- ),
368
- const SizedBox(height: 8),
369
- Text(
370
- 'Use the public base URL of the NeoMail server. Local self-hosted URLs are also supported when NeoAgent can reach them.',
371
- style: TextStyle(color: _textSecondary, fontSize: 12),
372
- ),
373
- if (formError.isNotEmpty) ...<Widget>[
374
- const SizedBox(height: 12),
375
- Container(
376
- padding: const EdgeInsets.all(8),
377
- decoration: BoxDecoration(
378
- color: _danger.withValues(alpha: 0.1),
379
- borderRadius: BorderRadius.circular(4),
380
- border: Border.all(
381
- color: _danger.withValues(alpha: 0.3),
382
- ),
383
- ),
384
- child: Text(
385
- formError,
386
- style: TextStyle(color: _danger, fontSize: 12),
387
- ),
388
- ),
389
- ],
390
- ],
507
+ builder: (dialogContext) => StatefulBuilder(
508
+ builder: (dialogContext, setState) => AlertDialog(
509
+ title: const Text('NeoArchive Setup'),
510
+ content: SizedBox(
511
+ width: 540,
512
+ child: Column(
513
+ mainAxisSize: MainAxisSize.min,
514
+ crossAxisAlignment: CrossAxisAlignment.start,
515
+ children: <Widget>[
516
+ Text(
517
+ 'Add the NeoArchive backend URL once. NeoAgent will open NeoArchive OAuth so the user can sign in and approve archive access without API keys.',
518
+ style: TextStyle(color: _textSecondary),
391
519
  ),
392
- ),
393
- actions: <Widget>[
394
- if (savedBaseUrl.trim().isNotEmpty)
395
- TextButton(
396
- onPressed: busy
397
- ? null
398
- : () async {
399
- final shouldClear =
400
- await showDialog<bool>(
401
- context: dialogContext,
402
- builder: (context) {
403
- return AlertDialog(
404
- title: const Text('Disconnect NeoMail?'),
405
- content: const Text(
406
- 'This removes the NeoMail backend URL and all connected NeoMail accounts for this agent.',
407
- ),
408
- actions: [
409
- TextButton(
410
- onPressed: () =>
411
- Navigator.of(context).pop(false),
412
- child: const Text('Cancel'),
413
- ),
414
- FilledButton(
415
- onPressed: () =>
416
- Navigator.of(context).pop(true),
417
- child: const Text('Disconnect'),
418
- ),
419
- ],
420
- );
421
- },
422
- ) ??
423
- false;
424
- if (!shouldClear) {
425
- return;
426
- }
427
- setState(() {
428
- formError = '';
429
- busy = true;
430
- });
431
- try {
432
- await controller.clearOfficialIntegrationConfig(
433
- 'neomail',
434
- );
435
- if (dialogContext.mounted) {
436
- Navigator.of(dialogContext).pop();
437
- }
438
- } catch (_) {
439
- setState(() {
440
- formError =
441
- controller.errorMessage ??
442
- 'Could not disconnect NeoMail.';
443
- busy = false;
444
- });
445
- }
446
- },
447
- child: const Text('Disconnect'),
448
- ),
449
- TextButton(
450
- onPressed: busy
451
- ? null
452
- : () => Navigator.of(dialogContext).pop(),
453
- child: const Text('Close'),
520
+ const SizedBox(height: 16),
521
+ const _IntegrationSetupStatusItem(
522
+ label: 'Connection Method',
523
+ status: 'OAuth companion flow',
524
+ isConnected: true,
454
525
  ),
455
- if (!hasConnectedAccount)
456
- TextButton(
457
- onPressed: busy
458
- ? null
459
- : () => saveConfig(
460
- setState,
461
- dialogContext: dialogContext,
462
- connectAfterSave: false,
463
- ),
464
- child: const Text('Save Only'),
465
- ),
466
- FilledButton(
467
- onPressed: busy
468
- ? null
469
- : () => saveConfig(
470
- setState,
471
- dialogContext: dialogContext,
472
- connectAfterSave: !hasConnectedAccount,
473
- ),
474
- child: Text(
475
- busy
476
- ? 'Working...'
477
- : hasConnectedAccount
478
- ? 'Update Setup'
479
- : 'Save & Connect',
526
+ const SizedBox(height: 12),
527
+ _IntegrationSetupStatusItem(
528
+ label: 'Connected NeoArchive User',
529
+ status: connected
530
+ ? '$accountCount ${accountCount == 1 ? 'connected user' : 'connected users'}'
531
+ : 'Not connected',
532
+ isConnected: connected,
533
+ ),
534
+ const SizedBox(height: 12),
535
+ TextField(
536
+ controller: baseUrlController,
537
+ keyboardType: TextInputType.url,
538
+ decoration: const InputDecoration(
539
+ labelText: 'NeoArchive Backend URL',
540
+ hintText: 'https://archive.example.com',
541
+ border: OutlineInputBorder(),
480
542
  ),
481
543
  ),
544
+ const SizedBox(height: 8),
545
+ Text(
546
+ 'Use the public base URL of the NeoArchive server. Local self-hosted URLs are supported when NeoAgent can reach them.',
547
+ style: TextStyle(color: _textSecondary, fontSize: 12),
548
+ ),
549
+ if (errorText.isNotEmpty) ...<Widget>[
550
+ const SizedBox(height: 12),
551
+ Text(errorText, style: TextStyle(color: _danger, fontSize: 12)),
552
+ ],
482
553
  ],
483
- );
484
- },
485
- );
486
- },
554
+ ),
555
+ ),
556
+ actions: <Widget>[
557
+ if (savedBaseUrl.isNotEmpty)
558
+ TextButton(
559
+ onPressed: busy
560
+ ? null
561
+ : () async {
562
+ final confirm =
563
+ await showDialog<bool>(
564
+ context: dialogContext,
565
+ builder: (context) => AlertDialog(
566
+ title: const Text('Disconnect NeoArchive?'),
567
+ content: const Text(
568
+ 'This removes the NeoArchive backend URL and all connected NeoArchive accounts for this agent.',
569
+ ),
570
+ actions: <Widget>[
571
+ TextButton(
572
+ onPressed: () =>
573
+ Navigator.of(context).pop(false),
574
+ child: const Text('Cancel'),
575
+ ),
576
+ FilledButton(
577
+ onPressed: () =>
578
+ Navigator.of(context).pop(true),
579
+ child: const Text('Disconnect'),
580
+ ),
581
+ ],
582
+ ),
583
+ ) ??
584
+ false;
585
+ if (!confirm) return;
586
+ setState(() {
587
+ busy = true;
588
+ errorText = '';
589
+ });
590
+ try {
591
+ await controller.clearOfficialIntegrationConfig(
592
+ 'neoarchive',
593
+ );
594
+ if (dialogContext.mounted) {
595
+ Navigator.of(dialogContext).pop();
596
+ }
597
+ } catch (_) {
598
+ setState(() {
599
+ errorText =
600
+ controller.errorMessage ??
601
+ 'Could not disconnect NeoArchive.';
602
+ busy = false;
603
+ });
604
+ }
605
+ },
606
+ child: const Text('Disconnect'),
607
+ ),
608
+ TextButton(
609
+ onPressed: busy ? null : () => Navigator.of(dialogContext).pop(),
610
+ child: const Text('Close'),
611
+ ),
612
+ if (!connected)
613
+ TextButton(
614
+ onPressed: busy
615
+ ? null
616
+ : () => save(setState, dialogContext, connect: false),
617
+ child: const Text('Save Only'),
618
+ ),
619
+ FilledButton(
620
+ onPressed: busy
621
+ ? null
622
+ : () => save(setState, dialogContext, connect: !connected),
623
+ child: Text(
624
+ busy
625
+ ? 'Working...'
626
+ : connected
627
+ ? 'Update Setup'
628
+ : 'Save & Connect',
629
+ ),
630
+ ),
631
+ ],
632
+ ),
633
+ ),
487
634
  );
488
-
489
635
  baseUrlController.dispose();
490
636
  }
491
637
 
@@ -1394,12 +1540,16 @@ class _OfficialIntegrationIcon extends StatelessWidget {
1394
1540
  );
1395
1541
  }
1396
1542
  final color = switch (item.icon) {
1543
+ 'neoarchive' => const Color(0xFFE3B655),
1544
+ 'neorecall' => const Color(0xFFD98AA6),
1397
1545
  'google' => const Color(0xFF4285F4),
1398
1546
  'home_assistant' => const Color(0xFF41BDF5),
1399
1547
  'trello' => const Color(0xFF0C66E4),
1400
1548
  _ => _accent,
1401
1549
  };
1402
1550
  final label = switch (item.icon) {
1551
+ 'neoarchive' => 'A',
1552
+ 'neorecall' => 'R',
1403
1553
  'google' => 'G',
1404
1554
  'home_assistant' => 'H',
1405
1555
  'trello' => 'T',
@@ -1439,8 +1589,10 @@ int _compareOfficialIntegrationItems(
1439
1589
 
1440
1590
  int _officialIntegrationRank(OfficialIntegrationItem item) {
1441
1591
  return switch (item.id) {
1592
+ 'neoarchive' => 1,
1442
1593
  'neomail' => 0,
1443
- 'google_workspace' => 1,
1594
+ 'neorecall' => 2,
1595
+ 'google_workspace' => 3,
1444
1596
  _ => 10,
1445
1597
  };
1446
1598
  }