neoagent 3.1.1-beta.9 → 3.2.1-beta.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.
- package/.env.example +2 -2
- package/README.md +5 -1
- package/docs/architecture.md +1 -1
- package/docs/benchmarking.md +68 -87
- package/docs/capabilities.md +1 -1
- package/docs/clients-and-devices.md +3 -4
- package/docs/configuration.md +1 -1
- package/docs/devices.md +1 -2
- package/docs/health.md +24 -0
- package/docs/index.md +2 -2
- package/docs/integrations-architecture.md +6 -0
- package/docs/integrations.md +15 -1
- package/docs/models.md +1 -1
- package/docs/persistence.md +1 -1
- package/docs/why-neoagent.md +1 -1
- package/flutter_app/README.md +0 -5
- package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
- package/flutter_app/lib/main.dart +0 -6
- package/flutter_app/lib/main_app_shell.dart +0 -2
- package/flutter_app/lib/main_controller.dart +2 -867
- package/flutter_app/lib/main_devices.dart +0 -73
- package/flutter_app/lib/main_integrations.dart +339 -187
- package/flutter_app/lib/main_launcher.dart +2 -212
- package/flutter_app/lib/main_models.dart +0 -259
- package/flutter_app/lib/main_navigation.dart +2 -14
- package/flutter_app/lib/main_operations.dart +875 -383
- package/flutter_app/lib/main_runtime.dart +9 -208
- package/flutter_app/lib/main_settings.dart +17 -199
- package/flutter_app/lib/main_shared.dart +20 -591
- package/flutter_app/lib/main_timeline.dart +26 -122
- package/flutter_app/lib/main_voice_assistant.dart +0 -5
- package/flutter_app/lib/src/backend_client.dart +0 -102
- package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
- package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
- package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
- package/flutter_app/macos/Podfile.lock +56 -5
- package/flutter_app/pubspec.lock +0 -7
- package/flutter_app/pubspec.yaml +0 -2
- package/flutter_app/web/index.html +174 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
- package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
- package/landing/index.html +0 -2
- package/lib/manager.js +1 -1
- package/lib/schema_migrations.js +75 -0
- package/package.json +1 -4
- package/server/db/database.js +1 -133
- package/server/http/middleware.js +2 -16
- package/server/http/routes.js +0 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +0 -25
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/index.html +174 -0
- package/server/public/main.dart.js +92036 -91145
- package/server/routes/auth.js +11 -2
- package/server/routes/settings.js +0 -5
- package/server/routes/voice_assistant.js +1 -128
- package/server/services/account/erasure.js +0 -3
- package/server/services/ai/deliverables/artifact_helpers.js +1 -1
- package/server/services/ai/loop/conversation_loop.js +6 -2
- package/server/services/ai/loop/tool_dispatch.js +0 -3
- package/server/services/ai/messagingFallback.js +1 -1
- package/server/services/ai/settings.js +0 -6
- package/server/services/ai/toolEvidence.js +1 -4
- package/server/services/ai/tools.js +0 -234
- package/server/services/desktop/auth.js +0 -3
- package/server/services/desktop/registry.js +2 -50
- package/server/services/integrations/neoarchive/provider.js +616 -0
- package/server/services/integrations/neorecall/client.js +140 -0
- package/server/services/integrations/neorecall/constants.js +87 -0
- package/server/services/integrations/neorecall/provider.js +163 -0
- package/server/services/integrations/registry.js +4 -2
- package/server/services/manager.js +0 -22
- package/server/services/memory/manager.js +44 -19
- package/server/services/memory/retrieval_reasoning.js +36 -14
- package/server/services/messaging/formatting_guides.js +1 -0
- package/server/services/messaging/whatsapp.js +18 -0
- package/server/services/security/tool_categories.js +0 -5
- package/server/services/social_video/service.js +2 -20
- package/server/services/tasks/adapters/index.js +1 -1
- package/server/services/tasks/integration_runtime.js +3 -67
- package/server/services/tasks/runtime.js +1 -6
- package/server/services/tasks/task_repository.js +6 -13
- package/server/services/timeline/service.js +0 -224
- package/server/services/voice/message.js +0 -24
- package/server/services/voice/providers.js +42 -1
- package/server/services/voice/runtimeManager.js +9 -5
- package/server/services/voice/turnRunner.js +0 -22
- package/server/services/wearable/service.js +0 -5
- package/docs/recordings-and-health.md +0 -41
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
- package/flutter_app/lib/main_recordings.dart +0 -920
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
- package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
- package/flutter_app/lib/src/recording_bridge.dart +0 -232
- package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
- package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
- package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
- package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
- package/flutter_app/lib/src/recording_payloads.dart +0 -95
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
- package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
- package/server/routes/recordings.js +0 -335
- package/server/routes/screenHistory.js +0 -190
- package/server/services/ai/recordingInsights.js +0 -105
- package/server/services/integrations/neomail/provider.js +0 -993
- package/server/services/recordings/deepgram.js +0 -58
- package/server/services/recordings/manager.js +0 -1273
- package/server/services/tasks/adapters/neomail_email_received.js +0 -46
- package/server/services/voice/screenshotContext.js +0 -73
- 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 '
|
|
237
|
-
|
|
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>
|
|
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('
|
|
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
|
-
|
|
265
|
-
var
|
|
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>
|
|
271
|
-
StateSetter setState,
|
|
272
|
-
|
|
273
|
-
required bool
|
|
458
|
+
Future<void> save(
|
|
459
|
+
StateSetter setState,
|
|
460
|
+
BuildContext dialogContext, {
|
|
461
|
+
required bool connect,
|
|
274
462
|
}) async {
|
|
275
463
|
setState(() {
|
|
276
|
-
|
|
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
|
-
|
|
471
|
+
errorText = 'NeoArchive backend URL is required.';
|
|
284
472
|
busy = false;
|
|
285
473
|
});
|
|
286
474
|
return;
|
|
287
475
|
}
|
|
288
476
|
await controller.saveOfficialIntegrationConfig(
|
|
289
|
-
'
|
|
477
|
+
'neoarchive',
|
|
290
478
|
config: <String, dynamic>{'baseUrl': baseUrl},
|
|
291
479
|
);
|
|
292
|
-
if (
|
|
480
|
+
if (connect) {
|
|
293
481
|
await controller.connectOfficialIntegration(
|
|
294
|
-
'
|
|
295
|
-
appId: '
|
|
482
|
+
'neoarchive',
|
|
483
|
+
appId: 'archive',
|
|
296
484
|
);
|
|
297
485
|
if ((controller.errorMessage ?? '').trim().isNotEmpty) {
|
|
298
486
|
setState(() {
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
'
|
|
1594
|
+
'neorecall' => 2,
|
|
1595
|
+
'google_workspace' => 3,
|
|
1444
1596
|
_ => 10,
|
|
1445
1597
|
};
|
|
1446
1598
|
}
|