neoagent 3.0.1-beta.3 → 3.0.1-beta.31

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 (129) hide show
  1. package/.env.example +19 -8
  2. package/README.md +20 -3
  3. package/docs/automation.md +37 -0
  4. package/docs/benchmarking.md +102 -0
  5. package/docs/billing.md +229 -0
  6. package/docs/configuration.md +22 -0
  7. package/docs/getting-started.md +10 -8
  8. package/docs/index.md +1 -0
  9. package/docs/operations.md +1 -1
  10. package/flutter_app/lib/main.dart +5 -1
  11. package/flutter_app/lib/main_account_settings.dart +138 -0
  12. package/flutter_app/lib/main_app_shell.dart +331 -0
  13. package/flutter_app/lib/main_billing.dart +1465 -0
  14. package/flutter_app/lib/main_chat.dart +1612 -214
  15. package/flutter_app/lib/main_controller.dart +398 -26
  16. package/flutter_app/lib/main_devices.dart +293 -207
  17. package/flutter_app/lib/main_install.dart +1147 -0
  18. package/flutter_app/lib/main_models.dart +142 -0
  19. package/flutter_app/lib/main_navigation.dart +25 -1
  20. package/flutter_app/lib/main_operations.dart +288 -9
  21. package/flutter_app/lib/main_settings.dart +510 -300
  22. package/flutter_app/lib/main_shared.dart +2 -0
  23. package/flutter_app/lib/main_timeline.dart +1257 -0
  24. package/flutter_app/lib/src/backend_client.dart +149 -19
  25. package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
  26. package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
  27. package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
  28. package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
  29. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
  30. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
  31. package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
  32. package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
  33. package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
  34. package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
  35. package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
  36. package/flutter_app/lib/src/recording_payloads.dart +9 -0
  37. package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
  38. package/flutter_app/windows/runner/flutter_window.cpp +75 -0
  39. package/landing/index.html +5 -5
  40. package/lib/manager.js +184 -66
  41. package/lib/schema_migrations.js +84 -0
  42. package/package.json +10 -4
  43. package/server/admin/access.js +12 -7
  44. package/server/admin/admin.css +78 -0
  45. package/server/admin/admin.js +441 -23
  46. package/server/admin/billing.js +415 -0
  47. package/server/admin/index.html +116 -13
  48. package/server/admin/users.js +15 -15
  49. package/server/db/database.js +134 -31
  50. package/server/db/sessions_db.js +8 -0
  51. package/server/http/middleware.js +4 -4
  52. package/server/http/routes.js +10 -0
  53. package/server/http/static.js +4 -2
  54. package/server/middleware/requireBilling.js +12 -0
  55. package/server/public/.last_build_id +1 -1
  56. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  57. package/server/public/canvaskit/wimp.js.symbols +8475 -8467
  58. package/server/public/canvaskit/wimp.wasm +0 -0
  59. package/server/public/flutter_bootstrap.js +2 -2
  60. package/server/public/main.dart.js +91193 -86282
  61. package/server/routes/account.js +53 -0
  62. package/server/routes/admin.js +472 -63
  63. package/server/routes/agents.js +203 -21
  64. package/server/routes/billing.js +127 -0
  65. package/server/routes/billing_webhook.js +43 -0
  66. package/server/routes/browser.js +8 -1
  67. package/server/routes/memory.js +1 -4
  68. package/server/routes/recordings.js +96 -6
  69. package/server/routes/screenHistory.js +140 -2
  70. package/server/routes/settings.js +1 -2
  71. package/server/routes/skills.js +1 -1
  72. package/server/routes/timeline.js +43 -0
  73. package/server/routes/triggers.js +2 -2
  74. package/server/services/account/erasure.js +263 -0
  75. package/server/services/account/sessions.js +1 -9
  76. package/server/services/ai/hooks.js +4 -1
  77. package/server/services/ai/loop/agent_engine_core.js +50 -1
  78. package/server/services/ai/loop/blank_recovery.js +36 -0
  79. package/server/services/ai/loop/completion_judge.js +42 -0
  80. package/server/services/ai/loop/conversation_loop.js +320 -38
  81. package/server/services/ai/loop/progress_monitor.js +6 -6
  82. package/server/services/ai/loopPolicy.js +37 -44
  83. package/server/services/ai/messagingFallback.js +25 -1
  84. package/server/services/ai/models.js +18 -0
  85. package/server/services/ai/preModelCompaction.js +25 -5
  86. package/server/services/ai/rate_limits.js +52 -5
  87. package/server/services/ai/systemPrompt.js +23 -5
  88. package/server/services/ai/taskAnalysis.js +2 -0
  89. package/server/services/ai/toolEvidence.js +15 -0
  90. package/server/services/ai/toolResult.js +40 -0
  91. package/server/services/ai/tools.js +237 -22
  92. package/server/services/android/controller.js +6 -2
  93. package/server/services/billing/billing_email.js +106 -0
  94. package/server/services/billing/config.js +17 -0
  95. package/server/services/billing/plans.js +121 -0
  96. package/server/services/billing/stripe_client.js +21 -0
  97. package/server/services/billing/subscriptions.js +462 -0
  98. package/server/services/billing/trial_guard.js +111 -0
  99. package/server/services/browser/anti_detection.js +192 -0
  100. package/server/services/browser/contentExtractor.js +629 -0
  101. package/server/services/browser/controller.js +181 -59
  102. package/server/services/desktop/auth.js +3 -0
  103. package/server/services/desktop/gateway.js +7 -4
  104. package/server/services/desktop/registry.js +50 -2
  105. package/server/services/integrations/google/calendar.js +30 -2
  106. package/server/services/integrations/secrets.js +7 -4
  107. package/server/services/manager.js +23 -42
  108. package/server/services/memory/ingestion_chunking.js +268 -0
  109. package/server/services/messaging/automation.js +1 -1
  110. package/server/services/messaging/telnyx.js +12 -11
  111. package/server/services/messaging/whatsapp.js +1 -1
  112. package/server/services/recordings/manager.js +73 -34
  113. package/server/services/runtime/backends/local-vm.js +40 -22
  114. package/server/services/runtime/docker-vm-manager.js +157 -266
  115. package/server/services/runtime/guest_bootstrap.js +17 -5
  116. package/server/services/runtime/guest_image.js +188 -0
  117. package/server/services/runtime/manager.js +0 -1
  118. package/server/services/runtime/validation.js +3 -8
  119. package/server/services/social_video/service.js +60 -10
  120. package/server/services/tasks/runtime.js +313 -19
  121. package/server/services/tasks/schedule_utils.js +5 -5
  122. package/server/services/tasks/task_repository.js +13 -0
  123. package/server/services/timeline/service.js +558 -0
  124. package/server/services/voice/runtimeManager.js +19 -10
  125. package/server/services/wearable/gateway.js +8 -5
  126. package/server/services/websocket.js +26 -8
  127. package/server/services/workspace/manager.js +37 -3
  128. package/server/services/desktop/screenRecorder.js +0 -292
  129. package/server/services/desktop/screen_recorder_support.js +0 -46
@@ -161,6 +161,8 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
161
161
  String? _emailInlineError;
162
162
  String? _passwordSuccessMessage;
163
163
  String? _passwordInlineError;
164
+ bool _isExportingData = false;
165
+ bool _isDeletingAccount = false;
164
166
 
165
167
  @override
166
168
  void initState() {
@@ -592,10 +594,146 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
592
594
  .toList(),
593
595
  ),
594
596
  ],
597
+ const SizedBox(height: 28),
598
+ const _SectionTitle('Your data'),
599
+ const SizedBox(height: 10),
600
+ Text(
601
+ 'Download a copy of your data, or permanently delete your account. '
602
+ 'Deletion removes all your conversations, memories, files, tasks and '
603
+ 'settings and cannot be undone.',
604
+ style: TextStyle(color: _textSecondary, height: 1.45),
605
+ ),
606
+ const SizedBox(height: 14),
607
+ Wrap(
608
+ spacing: 12,
609
+ runSpacing: 12,
610
+ children: <Widget>[
611
+ OutlinedButton.icon(
612
+ onPressed: _isExportingData ? null : _exportMyData,
613
+ icon: _isExportingData
614
+ ? const SizedBox.square(
615
+ dimension: 16,
616
+ child: CircularProgressIndicator(strokeWidth: 2),
617
+ )
618
+ : const Icon(Icons.download_outlined),
619
+ label: const Text('Export my data'),
620
+ ),
621
+ OutlinedButton.icon(
622
+ onPressed: _isDeletingAccount ? null : _confirmDeleteAccount,
623
+ style: OutlinedButton.styleFrom(
624
+ foregroundColor: _danger,
625
+ side: BorderSide(color: _danger.withValues(alpha: 0.6)),
626
+ ),
627
+ icon: _isDeletingAccount
628
+ ? const SizedBox.square(
629
+ dimension: 16,
630
+ child: CircularProgressIndicator(strokeWidth: 2),
631
+ )
632
+ : const Icon(Icons.delete_forever_outlined),
633
+ label: const Text('Delete account'),
634
+ ),
635
+ ],
636
+ ),
595
637
  ],
596
638
  );
597
639
  }
598
640
 
641
+ Future<void> _exportMyData() async {
642
+ setState(() => _isExportingData = true);
643
+ try {
644
+ final export = await widget.controller.backendClient.exportMyData(
645
+ widget.controller.backendUrl,
646
+ );
647
+ final pretty = const JsonEncoder.withIndent(' ').convert(export);
648
+ await Clipboard.setData(ClipboardData(text: pretty));
649
+ if (!mounted) return;
650
+ ScaffoldMessenger.of(context).showSnackBar(
651
+ const SnackBar(
652
+ content: Text('Your data export was copied to the clipboard.'),
653
+ ),
654
+ );
655
+ } catch (err) {
656
+ if (!mounted) return;
657
+ ScaffoldMessenger.of(context).showSnackBar(
658
+ SnackBar(content: Text('Could not export your data: $err')),
659
+ );
660
+ } finally {
661
+ if (mounted) setState(() => _isExportingData = false);
662
+ }
663
+ }
664
+
665
+ Future<void> _confirmDeleteAccount() async {
666
+ final username = widget.controller.user?['username']?.toString() ?? '';
667
+ final confirmController = TextEditingController();
668
+ final confirmed = await showDialog<bool>(
669
+ context: context,
670
+ builder: (dialogContext) {
671
+ return StatefulBuilder(
672
+ builder: (context, setDialogState) {
673
+ final matches = confirmController.text.trim() == username;
674
+ return AlertDialog(
675
+ title: const Text('Delete account permanently?'),
676
+ content: Column(
677
+ mainAxisSize: MainAxisSize.min,
678
+ crossAxisAlignment: CrossAxisAlignment.start,
679
+ children: <Widget>[
680
+ const Text(
681
+ 'This permanently erases all of your data and cannot be '
682
+ 'undone. Type your username to confirm.',
683
+ ),
684
+ const SizedBox(height: 14),
685
+ TextField(
686
+ controller: confirmController,
687
+ autofocus: true,
688
+ onChanged: (_) => setDialogState(() {}),
689
+ decoration: InputDecoration(
690
+ labelText: 'Username',
691
+ hintText: username,
692
+ ),
693
+ ),
694
+ ],
695
+ ),
696
+ actions: <Widget>[
697
+ TextButton(
698
+ onPressed: () => Navigator.of(dialogContext).pop(false),
699
+ child: const Text('Cancel'),
700
+ ),
701
+ FilledButton(
702
+ style: FilledButton.styleFrom(backgroundColor: _danger),
703
+ onPressed: matches
704
+ ? () => Navigator.of(dialogContext).pop(true)
705
+ : null,
706
+ child: const Text('Delete forever'),
707
+ ),
708
+ ],
709
+ );
710
+ },
711
+ );
712
+ },
713
+ );
714
+ confirmController.dispose();
715
+ if (confirmed != true) return;
716
+
717
+ setState(() => _isDeletingAccount = true);
718
+ try {
719
+ await widget.controller.backendClient.deleteMyAccount(
720
+ baseUrl: widget.controller.backendUrl,
721
+ confirmUsername: username,
722
+ );
723
+ if (!mounted) return;
724
+ ScaffoldMessenger.of(context).showSnackBar(
725
+ const SnackBar(content: Text('Your account has been deleted.')),
726
+ );
727
+ await widget.controller.logout();
728
+ } catch (err) {
729
+ if (!mounted) return;
730
+ setState(() => _isDeletingAccount = false);
731
+ ScaffoldMessenger.of(context).showSnackBar(
732
+ SnackBar(content: Text('Could not delete your account: $err')),
733
+ );
734
+ }
735
+ }
736
+
599
737
  String _formatTokens(int amount) {
600
738
  if (amount >= 1000000) {
601
739
  final value = amount / 1000000;
@@ -57,6 +57,7 @@ class BackendSetupView extends StatefulWidget {
57
57
 
58
58
  class _BackendSetupViewState extends State<BackendSetupView> {
59
59
  late final TextEditingController _backendUrlController;
60
+ bool _localInstall = false;
60
61
 
61
62
  @override
62
63
  void initState() {
@@ -78,6 +79,12 @@ class _BackendSetupViewState extends State<BackendSetupView> {
78
79
 
79
80
  @override
80
81
  Widget build(BuildContext context) {
82
+ if (_localInstall) {
83
+ return _LocalInstallWidget(
84
+ controller: widget.controller,
85
+ onBack: () => setState(() => _localInstall = false),
86
+ );
87
+ }
81
88
  final controller = widget.controller;
82
89
  return _AmbientBackdrop(
83
90
  child: Scaffold(
@@ -195,6 +202,212 @@ class _BackendSetupViewState extends State<BackendSetupView> {
195
202
  ),
196
203
  ),
197
204
  ),
205
+ const SizedBox(height: 10),
206
+ Center(
207
+ child: TextButton.icon(
208
+ onPressed: () =>
209
+ setState(() => _localInstall = true),
210
+ icon: const Icon(
211
+ Icons.download_outlined,
212
+ size: 16,
213
+ ),
214
+ label: const Text(
215
+ 'Install NeoAgent on this machine',
216
+ ),
217
+ ),
218
+ ),
219
+ ],
220
+ ),
221
+ ),
222
+ ),
223
+ ),
224
+ ),
225
+ ),
226
+ ),
227
+ ),
228
+ ),
229
+ );
230
+ }
231
+ }
232
+
233
+ // ─── Local Install Widget ────────────────────────────────────────────────────
234
+
235
+ enum _LocalInstallPhase { checking, ready, installing, done, failed }
236
+
237
+ class _LocalInstallWidget extends StatefulWidget {
238
+ const _LocalInstallWidget({required this.controller, required this.onBack});
239
+
240
+ final NeoAgentController controller;
241
+ final VoidCallback onBack;
242
+
243
+ @override
244
+ State<_LocalInstallWidget> createState() => _LocalInstallWidgetState();
245
+ }
246
+
247
+ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
248
+ _LocalInstallPhase _phase = _LocalInstallPhase.checking;
249
+ String? _nodePath;
250
+ String? _installDir;
251
+ final List<String> _log = [];
252
+ final _logScrollCtrl = ScrollController();
253
+ Process? _proc;
254
+ String? _errorMsg;
255
+
256
+ String get _home => Platform.isWindows
257
+ ? (Platform.environment['USERPROFILE'] ?? '')
258
+ : (Platform.environment['HOME'] ?? '');
259
+
260
+ @override
261
+ void initState() {
262
+ super.initState();
263
+ _check();
264
+ }
265
+
266
+ @override
267
+ void dispose() {
268
+ _logScrollCtrl.dispose();
269
+ _proc?.kill(ProcessSignal.sigkill);
270
+ super.dispose();
271
+ }
272
+
273
+ Future<void> _check() async {
274
+ if (!mounted) return;
275
+ setState(() => _phase = _LocalInstallPhase.checking);
276
+
277
+ final r = await Process.run(Platform.isWindows ? 'where' : 'which', [
278
+ 'node',
279
+ ]);
280
+ if (r.exitCode == 0) {
281
+ _nodePath = (r.stdout as String).trim().split('\n').first.trim();
282
+ } else if (!Platform.isWindows) {
283
+ for (final p in ['/opt/homebrew/bin/node', '/usr/local/bin/node']) {
284
+ if (File(p).existsSync()) {
285
+ _nodePath = p;
286
+ break;
287
+ }
288
+ }
289
+ }
290
+
291
+ final defaultDir = '$_home/NeoAgent';
292
+ if (File('$defaultDir/bin/neoagent.js').existsSync()) {
293
+ _installDir = defaultDir;
294
+ }
295
+
296
+ if (!mounted) return;
297
+ if (_nodePath != null && _installDir != null) {
298
+ setState(() => _phase = _LocalInstallPhase.ready);
299
+ } else {
300
+ setState(() {
301
+ _phase = _LocalInstallPhase.failed;
302
+ _errorMsg = _nodePath == null
303
+ ? 'Node.js not found. Install it from nodejs.org then retry.'
304
+ : 'NeoAgent not found at ~/NeoAgent. Run: npm install -g neoagent && neoagent install';
305
+ });
306
+ }
307
+ }
308
+
309
+ bool _ensureEnvFile() {
310
+ final envPath = '$_home/.neoagent/runtime/.env';
311
+ if (!File(envPath).existsSync()) {
312
+ try {
313
+ Directory('$_home/.neoagent/runtime').createSync(recursive: true);
314
+ File(envPath).writeAsStringSync('NODE_ENV=production\nPORT=3333\n');
315
+ } catch (_) {
316
+ return false;
317
+ }
318
+ }
319
+ return true;
320
+ }
321
+
322
+ Future<void> _install() async {
323
+ if (!_ensureEnvFile()) {
324
+ setState(() {
325
+ _phase = _LocalInstallPhase.failed;
326
+ _errorMsg =
327
+ 'Could not create ~/.neoagent/runtime/.env — check directory permissions.';
328
+ });
329
+ return;
330
+ }
331
+ setState(() {
332
+ _phase = _LocalInstallPhase.installing;
333
+ _log.clear();
334
+ });
335
+
336
+ final process = await Process.start(_nodePath!, [
337
+ 'bin/neoagent.js',
338
+ 'install',
339
+ ], workingDirectory: _installDir);
340
+ _proc = process;
341
+
342
+ void onChunk(String data) {
343
+ if (!mounted) return;
344
+ setState(() => _log.add(data));
345
+ WidgetsBinding.instance.addPostFrameCallback((_) {
346
+ if (_logScrollCtrl.hasClients) {
347
+ _logScrollCtrl.jumpTo(_logScrollCtrl.position.maxScrollExtent);
348
+ }
349
+ });
350
+ }
351
+
352
+ process.stdout.transform(utf8.decoder).listen(onChunk);
353
+ process.stderr.transform(utf8.decoder).listen(onChunk);
354
+
355
+ final exit = await process.exitCode;
356
+ _proc = null;
357
+ if (!mounted) return;
358
+ setState(() {
359
+ _phase = exit == 0 ? _LocalInstallPhase.done : _LocalInstallPhase.failed;
360
+ if (exit != 0)
361
+ _errorMsg = 'Installation failed (exit $exit). Check the log above.';
362
+ });
363
+ }
364
+
365
+ @override
366
+ Widget build(BuildContext context) {
367
+ return _AmbientBackdrop(
368
+ child: Scaffold(
369
+ backgroundColor: Colors.transparent,
370
+ body: SafeArea(
371
+ child: Center(
372
+ child: SingleChildScrollView(
373
+ padding: const EdgeInsets.all(24),
374
+ child: ConstrainedBox(
375
+ constraints: const BoxConstraints(maxWidth: 560),
376
+ child: _EntranceMotion(
377
+ child: _GlassSurface(
378
+ borderRadius: BorderRadius.circular(34),
379
+ blurSigma: 28,
380
+ boxShadow: _softPanelShadow,
381
+ overlayGradient: _panelGradient,
382
+ fillColor: _glassFill,
383
+ child: Padding(
384
+ padding: const EdgeInsets.fromLTRB(34, 28, 34, 30),
385
+ child: Column(
386
+ crossAxisAlignment: CrossAxisAlignment.start,
387
+ children: <Widget>[
388
+ TextButton.icon(
389
+ onPressed: widget.onBack,
390
+ icon: const Icon(Icons.arrow_back, size: 16),
391
+ label: const Text('Back'),
392
+ style: TextButton.styleFrom(
393
+ padding: EdgeInsets.zero,
394
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
395
+ ),
396
+ ),
397
+ const SizedBox(height: 16),
398
+ const _BrandLockup(logoSize: 60),
399
+ const SizedBox(height: 22),
400
+ Text(
401
+ 'INSTALL LOCALLY',
402
+ style: _sectionEyebrowStyle(),
403
+ ),
404
+ const SizedBox(height: 10),
405
+ Text(
406
+ 'Install NeoAgent as a background service',
407
+ style: _displayTitleStyle(28),
408
+ ),
409
+ const SizedBox(height: 20),
410
+ _buildContent(),
198
411
  ],
199
412
  ),
200
413
  ),
@@ -207,6 +420,110 @@ class _BackendSetupViewState extends State<BackendSetupView> {
207
420
  ),
208
421
  );
209
422
  }
423
+
424
+ Widget _buildContent() {
425
+ switch (_phase) {
426
+ case _LocalInstallPhase.checking:
427
+ return const Center(
428
+ child: Padding(
429
+ padding: EdgeInsets.all(24),
430
+ child: CircularProgressIndicator(),
431
+ ),
432
+ );
433
+ case _LocalInstallPhase.ready:
434
+ return Column(
435
+ crossAxisAlignment: CrossAxisAlignment.start,
436
+ children: <Widget>[
437
+ Text(
438
+ 'NeoAgent found at $_installDir. Click Install to set up the background service.',
439
+ style: TextStyle(color: _textSecondary, height: 1.55),
440
+ ),
441
+ const SizedBox(height: 20),
442
+ SizedBox(
443
+ width: double.infinity,
444
+ child: FilledButton.icon(
445
+ onPressed: _install,
446
+ style: FilledButton.styleFrom(
447
+ backgroundColor: _accent,
448
+ padding: const EdgeInsets.symmetric(vertical: 16),
449
+ ),
450
+ icon: const Icon(Icons.download_outlined),
451
+ label: const Text('Install NeoAgent'),
452
+ ),
453
+ ),
454
+ ],
455
+ );
456
+ case _LocalInstallPhase.installing:
457
+ return Column(
458
+ crossAxisAlignment: CrossAxisAlignment.start,
459
+ children: <Widget>[
460
+ Row(
461
+ children: <Widget>[
462
+ const SizedBox(
463
+ width: 18,
464
+ height: 18,
465
+ child: CircularProgressIndicator(strokeWidth: 2),
466
+ ),
467
+ const SizedBox(width: 12),
468
+ const Expanded(child: Text('Installing...')),
469
+ TextButton(
470
+ onPressed: () async {
471
+ final proc = _proc;
472
+ proc?.kill(ProcessSignal.sigterm);
473
+ await proc?.exitCode;
474
+ if (mounted)
475
+ setState(() => _phase = _LocalInstallPhase.ready);
476
+ },
477
+ child: const Text('Cancel'),
478
+ ),
479
+ ],
480
+ ),
481
+ const SizedBox(height: 12),
482
+ _InstallLogCard(lines: _log, scrollController: _logScrollCtrl),
483
+ ],
484
+ );
485
+ case _LocalInstallPhase.done:
486
+ return Column(
487
+ crossAxisAlignment: CrossAxisAlignment.start,
488
+ children: <Widget>[
489
+ Row(
490
+ children: <Widget>[
491
+ Icon(Icons.check_circle_outline, color: _success),
492
+ const SizedBox(width: 8),
493
+ const Text('NeoAgent is running.'),
494
+ ],
495
+ ),
496
+ const SizedBox(height: 20),
497
+ SizedBox(
498
+ width: double.infinity,
499
+ child: FilledButton.icon(
500
+ onPressed: () =>
501
+ widget.controller.saveBackendUrl('http://localhost:3333'),
502
+ style: FilledButton.styleFrom(
503
+ backgroundColor: _accent,
504
+ padding: const EdgeInsets.symmetric(vertical: 16),
505
+ ),
506
+ icon: const Icon(Icons.arrow_forward_rounded),
507
+ label: const Text('Connect to local NeoAgent'),
508
+ ),
509
+ ),
510
+ ],
511
+ );
512
+ case _LocalInstallPhase.failed:
513
+ return Column(
514
+ crossAxisAlignment: CrossAxisAlignment.start,
515
+ children: <Widget>[
516
+ _InlineError(message: _errorMsg ?? 'Installation failed.'),
517
+ const SizedBox(height: 16),
518
+ OutlinedButton.icon(
519
+ onPressed: _check,
520
+ icon: const Icon(Icons.refresh),
521
+ label: const Text('Retry'),
522
+ ),
523
+ ],
524
+ );
525
+ }
526
+ }
210
527
  }
211
528
 
212
529
  class AuthView extends StatefulWidget {
@@ -1391,6 +1708,7 @@ class _HomeViewState extends State<HomeView> {
1391
1708
  .allowMessagingSuggestion(
1392
1709
  notice.platform,
1393
1710
  suggestion,
1711
+ chatId: notice.chatId,
1394
1712
  );
1395
1713
  },
1396
1714
  icon: Icon(Icons.verified_user_outlined),
@@ -1412,6 +1730,13 @@ class _HomeViewState extends State<HomeView> {
1412
1730
  },
1413
1731
  child: Text('Open Messaging'),
1414
1732
  ),
1733
+ TextButton(
1734
+ onPressed: () async {
1735
+ Navigator.of(dialogContext).pop();
1736
+ await widget.controller.ignoreBlockedSender(notice);
1737
+ },
1738
+ child: Text('Ignore'),
1739
+ ),
1415
1740
  TextButton(
1416
1741
  onPressed: () => Navigator.of(dialogContext).pop(),
1417
1742
  child: Text('Dismiss'),
@@ -2184,6 +2509,8 @@ class _SectionBody extends StatelessWidget {
2184
2509
  switch (controller.selectedSection) {
2185
2510
  case AppSection.chat:
2186
2511
  return ChatPanel(controller: controller);
2512
+ case AppSection.timeline:
2513
+ return TimelinePanel(controller: controller);
2187
2514
  case AppSection.voiceAssistant:
2188
2515
  return VoiceAssistantPanel(controller: controller);
2189
2516
  case AppSection.devices:
@@ -2216,6 +2543,10 @@ class _SectionBody extends StatelessWidget {
2216
2543
  return controller.showHealthSection
2217
2544
  ? HealthPanel(controller: controller)
2218
2545
  : ChatPanel(controller: controller);
2546
+ case AppSection.server:
2547
+ return ServerPanel(controller: controller);
2548
+ case AppSection.billing:
2549
+ return BillingPanel(controller: controller);
2219
2550
  }
2220
2551
  }
2221
2552
  }