neoagent 3.0.1-beta.8 → 3.1.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 (100) hide show
  1. package/.env.example +2 -10
  2. package/README.md +12 -3
  3. package/docs/automation.md +37 -0
  4. package/docs/benchmarking.md +102 -0
  5. package/docs/billing.md +34 -8
  6. package/docs/configuration.md +11 -2
  7. package/docs/getting-started.md +10 -8
  8. package/docs/operations.md +1 -1
  9. package/flutter_app/lib/main.dart +3 -0
  10. package/flutter_app/lib/main_account_settings.dart +138 -0
  11. package/flutter_app/lib/main_app_shell.dart +38 -13
  12. package/flutter_app/lib/main_billing.dart +1465 -0
  13. package/flutter_app/lib/main_chat.dart +1612 -214
  14. package/flutter_app/lib/main_controller.dart +398 -26
  15. package/flutter_app/lib/main_devices.dart +293 -207
  16. package/flutter_app/lib/main_models.dart +142 -0
  17. package/flutter_app/lib/main_navigation.dart +19 -1
  18. package/flutter_app/lib/main_operations.dart +288 -9
  19. package/flutter_app/lib/main_settings.dart +510 -300
  20. package/flutter_app/lib/main_shared.dart +2 -0
  21. package/flutter_app/lib/main_timeline.dart +1378 -0
  22. package/flutter_app/lib/src/backend_client.dart +149 -19
  23. package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
  24. package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
  25. package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
  26. package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
  27. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
  28. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
  29. package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
  30. package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
  31. package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
  32. package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
  33. package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
  34. package/flutter_app/lib/src/recording_payloads.dart +9 -0
  35. package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
  36. package/flutter_app/windows/runner/flutter_window.cpp +75 -0
  37. package/landing/index.html +3 -3
  38. package/lib/manager.js +184 -66
  39. package/package.json +4 -1
  40. package/server/admin/access.js +12 -7
  41. package/server/admin/admin.css +78 -0
  42. package/server/admin/admin.js +436 -10
  43. package/server/admin/billing.js +165 -42
  44. package/server/admin/index.html +80 -5
  45. package/server/admin/users.js +15 -15
  46. package/server/db/database.js +125 -20
  47. package/server/http/routes.js +1 -0
  48. package/server/http/static.js +4 -2
  49. package/server/public/.last_build_id +1 -1
  50. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  51. package/server/public/canvaskit/wimp.js.symbols +8475 -8467
  52. package/server/public/canvaskit/wimp.wasm +0 -0
  53. package/server/public/flutter_bootstrap.js +2 -2
  54. package/server/public/main.dart.js +91077 -87037
  55. package/server/routes/account.js +53 -0
  56. package/server/routes/admin.js +345 -64
  57. package/server/routes/agents.js +203 -21
  58. package/server/routes/billing.js +5 -0
  59. package/server/routes/browser.js +8 -1
  60. package/server/routes/recordings.js +96 -6
  61. package/server/routes/screenHistory.js +140 -2
  62. package/server/routes/timeline.js +43 -0
  63. package/server/services/account/erasure.js +263 -0
  64. package/server/services/ai/hooks.js +4 -1
  65. package/server/services/ai/loop/agent_engine_core.js +8 -1
  66. package/server/services/ai/loop/blank_recovery.js +36 -0
  67. package/server/services/ai/loop/conversation_loop.js +166 -33
  68. package/server/services/ai/messagingFallback.js +22 -0
  69. package/server/services/ai/rate_limits.js +28 -5
  70. package/server/services/ai/systemPrompt.js +6 -5
  71. package/server/services/ai/taskAnalysis.js +2 -0
  72. package/server/services/ai/toolEvidence.js +15 -0
  73. package/server/services/ai/toolResult.js +40 -0
  74. package/server/services/ai/tools.js +163 -4
  75. package/server/services/android/controller.js +6 -2
  76. package/server/services/billing/plans.js +2 -1
  77. package/server/services/browser/anti_detection.js +192 -0
  78. package/server/services/browser/controller.js +180 -54
  79. package/server/services/desktop/auth.js +3 -0
  80. package/server/services/desktop/registry.js +50 -2
  81. package/server/services/integrations/google/calendar.js +22 -14
  82. package/server/services/manager.js +12 -42
  83. package/server/services/memory/ingestion_chunking.js +268 -0
  84. package/server/services/messaging/telnyx.js +9 -8
  85. package/server/services/recordings/manager.js +60 -27
  86. package/server/services/runtime/backends/local-vm.js +40 -22
  87. package/server/services/runtime/docker-vm-manager.js +157 -266
  88. package/server/services/runtime/guest_bootstrap.js +17 -5
  89. package/server/services/runtime/guest_image.js +188 -0
  90. package/server/services/runtime/manager.js +0 -1
  91. package/server/services/runtime/validation.js +3 -8
  92. package/server/services/social_video/service.js +60 -10
  93. package/server/services/tasks/runtime.js +234 -9
  94. package/server/services/tasks/schedule_utils.js +5 -5
  95. package/server/services/tasks/task_repository.js +13 -0
  96. package/server/services/timeline/service.js +558 -0
  97. package/server/services/wearable/gateway.js +1 -1
  98. package/server/services/websocket.js +21 -3
  99. package/server/services/desktop/screenRecorder.js +0 -292
  100. package/server/services/desktop/screen_recorder_support.js +0 -46
package/.env.example CHANGED
@@ -253,14 +253,6 @@ OLLAMA_URL=http://localhost:11434
253
253
  # Connected integration memory refresh cadence in milliseconds.
254
254
  NEOAGENT_MEMORY_INGESTION_INTERVAL_MS=600000
255
255
 
256
- # Local screen OCR capture. This captures the backend host's macOS desktop,
257
- # so enable it only when that host is the configured user's desktop.
258
- NEOAGENT_SCREEN_RECORDER_ENABLED=false
259
- # Existing NeoAgent user ID that owns all captured screen history. Required when enabled.
260
- NEOAGENT_SCREEN_RECORDER_USER_ID=
261
- NEOAGENT_SCREEN_RECORDER_INTERVAL_MS=10000
262
- NEOAGENT_SCREEN_RECORDER_RETENTION_DAYS=7
263
-
264
256
  ########################################
265
257
  # Messaging and voice integrations
266
258
  ########################################
@@ -278,8 +270,8 @@ TELNYX_WEBHOOK_TOKEN=
278
270
 
279
271
  # Stripe API keys — required when billing is enabled.
280
272
  # Use test keys (sk_test_...) during development; live keys in production.
281
- # STRIPE_SECRET_KEY=sk_live_...
282
- # STRIPE_PUBLISHABLE_KEY=pk_live_...
273
+ # STRIPE_SECRET_KEY=sk_test_...
274
+ # STRIPE_PUBLISHABLE_KEY=pk_test_...
283
275
 
284
276
  # Webhook signing secret from your Stripe dashboard (Developers > Webhooks).
285
277
  # STRIPE_WEBHOOK_SECRET=whsec_...
package/README.md CHANGED
@@ -34,9 +34,10 @@
34
34
 
35
35
  ## 🚀 Install
36
36
 
37
- NeoAgent supports macOS and Linux hosts. You need Node.js 20 or newer; the
38
- installer handles the rest of the application setup and attempts to install
39
- QEMU for the isolated browser and terminal runtime.
37
+ NeoAgent supports macOS and Linux hosts. You need Node.js 20 or newer and
38
+ Docker for the isolated browser and terminal runtime; the installer handles the
39
+ rest of the application setup and pre-builds the guest runtime image when Docker
40
+ is available.
40
41
 
41
42
  ```bash
42
43
  npm install -g neoagent
@@ -90,8 +91,16 @@ changes and rough edges. Review the
90
91
  [security boundaries](docs/security-boundaries.md) before connecting sensitive
91
92
  accounts or giving the agent access to a personal workstation.
92
93
 
94
+ ## 📊 Benchmarks
95
+
96
+ ![NeoAgent benchmark dashboard](static/benchmarks/latest-dashboard.png)
97
+ See [Benchmarking](docs/benchmarking.md) for the reproducibility details and suite coverage.
98
+
93
99
  Start with the [documentation](https://neolabs-systems.github.io/NeoAgent/docs/).
94
100
  Use [GitHub Discussions](https://github.com/NeoLabs-Systems/NeoAgent/discussions)
95
101
  for questions and [GitHub Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
96
102
  for reproducible bugs. Security reports belong in the process described by
97
103
  [SECURITY.md](SECURITY.md).
104
+
105
+ *Made with ❤️ by [Neo](https://github.com/neooriginal) · [NeoLabs Systems](https://github.com/NeoLabs-Systems)*
106
+
@@ -62,12 +62,49 @@ when no reply is needed.
62
62
  Prefer official integrations or MCP tools over browser automation when both
63
63
  can perform the action. Structured tools are easier to restrict and diagnose.
64
64
 
65
+ ## Bot-protected pages
66
+
67
+ The isolated VM browser uses native anti-detection hardening inspired by
68
+ Botasaurus' public anti-detect browser patterns: stable browser profiles,
69
+ common desktop viewport sizes, less mechanical pointer movement, referrer-aware
70
+ navigation, and challenge detection telemetry. NeoAgent does not embed or
71
+ depend on Botasaurus for this path.
72
+
73
+ `browser_navigate` accepts optional `referrerMode` values of `direct`,
74
+ `google`, or `current`. By default it also retries once with a Google referrer
75
+ when a known bot challenge is detected. Browser results can include
76
+ `botDetection` so runs can report that a page is blocked instead of treating the
77
+ challenge page as ordinary content.
78
+
79
+ Browser automation still must comply with the target site's terms and applicable
80
+ law. URL safety checks and the VM device-access restrictions remain in effect.
81
+
65
82
  ## Runs and delivery
66
83
 
67
84
  Open **Runs** to inspect the trigger, tool calls, approvals, output, and error
68
85
  for each execution. Delivery requires a configured messaging destination; a
69
86
  completed run can still have a delivery error.
70
87
 
88
+ ## Loop budgets
89
+
90
+ Scheduled and event-triggered tasks run autonomously under a per-task loop
91
+ budget. By default, a task can run 24 times or spend 250k model tokens per day.
92
+ At the cap, or when paused, the task is skipped before a model call.
93
+
94
+ Advanced task configs can override the defaults:
95
+
96
+ ```json
97
+ {
98
+ "loopBudget": {
99
+ "maxRunsPerDay": 12,
100
+ "maxTokensPerDay": 150000,
101
+ "paused": false
102
+ }
103
+ }
104
+ ```
105
+
106
+ Set `loopBudget.paused` or `loopPaused` to `true` as a per-task kill switch.
107
+
71
108
  ## Safety
72
109
 
73
110
  - Start with read-only integration accounts.
@@ -0,0 +1,102 @@
1
+ # Benchmarking
2
+
3
+ NeoAgent includes an end-to-end benchmark harness that drives the app through
4
+ its authenticated HTTP surface instead of calling internal engine methods
5
+ directly. The harness configures OpenRouter models, submits benchmark tasks,
6
+ collects run events and usage data, and writes normalized result artifacts.
7
+
8
+ ## Latest dashboard
9
+
10
+ ![Latest benchmark dashboard](/benchmarks/latest-dashboard.png)
11
+
12
+ The generated image is backed by these local outputs:
13
+
14
+ - `benchmark/results/latest-results.json`
15
+ - `benchmark/results/latest-summary.json`
16
+ - `benchmark/results/latest-summary.md`
17
+ - `static/benchmarks/latest-dashboard.png`
18
+
19
+ ## Commands
20
+
21
+ ```bash
22
+ npm run benchmark:setup
23
+ npm run benchmark:run
24
+ npm run benchmark:report
25
+ ```
26
+
27
+ ## Model selection
28
+
29
+ The benchmark harness is OpenRouter-first. Edit the `OPENROUTER_MODEL_IDS`
30
+ variable in `benchmark/config.js` to pin exact model IDs, or leave it empty to
31
+ use the current cheap-tier OpenRouter models discovered at runtime.
32
+
33
+ ## Benchmark coverage
34
+
35
+ - `GAIA`, `BrowseComp`, `WebArena`, `VisualWebArena`, and `SWE-bench` are wired
36
+ as exact public-suite adapters. When their official datasets or external
37
+ runners are not installed, they are reported as blocked instead of being
38
+ approximated.
39
+ - `NeoAgent Representative Tasks` runs first-party agent tasks through
40
+ `/api/agents` and scores them from the persisted run details.
41
+ - `NeoAgent Memory Retrieval` seeds memories over `/api/memory`, recalls them
42
+ through the app routes, and scores retrieval quality with the same metric
43
+ code used by the server evaluation module.
44
+
45
+ ## Reproducibility
46
+
47
+ - Public-suite setup manifests are written under `benchmark/workdir/<suite>/`.
48
+ - The harness uses one benchmark account and persists per-run usage summaries
49
+ through the existing NeoAgent run detail endpoints.
50
+ - Blocked suites are intentional and explicit. The harness never substitutes a
51
+ simplified local task and labels it as a public benchmark result.
52
+
53
+ ## Public benchmark prerequisites
54
+
55
+ ### GAIA
56
+
57
+ - Access to the gated [GAIA dataset](https://huggingface.co/datasets/gaia-benchmark/GAIA).
58
+ - `HF_TOKEN` in the environment.
59
+ - A Python environment with `datasets` installed if you want `benchmark:setup`
60
+ to export a normalized local cache automatically.
61
+ - Either an exact evaluator wired through
62
+ `NEOAGENT_BENCHMARK_GAIA_RUNNER` or a repository-local exact runner added to
63
+ the harness. The current adapter intentionally reports `blocked` until an
64
+ official evaluator path is configured.
65
+
66
+ ### BrowseComp
67
+
68
+ - A checkout of [openai/simple-evals](https://github.com/openai/simple-evals),
69
+ which `benchmark:setup` can clone into `benchmark/workdir/browsecomp/`.
70
+ - Exact BrowseComp case extraction from that repo.
71
+ - An official evaluator command exposed through
72
+ `NEOAGENT_BENCHMARK_BROWSECOMP_RUNNER`.
73
+
74
+ ### WebArena
75
+
76
+ - Docker installed and running.
77
+ - The official [WebArena](https://webarena.dev/) environment and task sites.
78
+ - Browser automation dependencies required by the official runner.
79
+ - An exact runner command exposed through
80
+ `NEOAGENT_BENCHMARK_WEBARENA_RUNNER`.
81
+
82
+ ### VisualWebArena
83
+
84
+ - Docker installed and running.
85
+ - The official VisualWebArena environment.
86
+ - A vision-capable configured benchmark model.
87
+ - An exact runner command exposed through
88
+ `NEOAGENT_BENCHMARK_VISUAL_WEBARENA_RUNNER`.
89
+
90
+ ### SWE-bench
91
+
92
+ - Docker installed and running.
93
+ - A checkout of [SWE-bench](https://github.com/SWE-bench/SWE-bench), which
94
+ `benchmark:setup` can clone into `benchmark/workdir/swebench/`.
95
+ - The official dataset/runtime and enough disk/network budget for repo/image
96
+ setup.
97
+ - An exact runner command exposed through
98
+ `NEOAGENT_BENCHMARK_SWEBENCH_RUNNER`.
99
+
100
+ On this machine, `docker` is currently missing, so exact `WebArena`,
101
+ `VisualWebArena`, and `SWE-bench` execution remain blocked until Docker and the
102
+ official runners are installed.
package/docs/billing.md CHANGED
@@ -7,19 +7,40 @@ anywhere in the application.
7
7
 
8
8
  ## Enable billing
9
9
 
10
- Set the following environment variables before starting the server:
10
+ Run the interactive setup wizard:
11
11
 
12
12
  ```bash
13
- neoagent env set NEOAGENT_BILLING_ENABLED true
14
- neoagent env set STRIPE_SECRET_KEY sk_live_...
15
- neoagent env set STRIPE_PUBLISHABLE_KEY pk_live_...
16
- neoagent env set STRIPE_WEBHOOK_SECRET whsec_...
13
+ neoagent billing setup
17
14
  ```
18
15
 
19
- Restart the server. The admin dashboard will show a **Billing** navigation
20
- item and the `/api/billing/*` endpoints will become active.
16
+ The wizard prompts for your Stripe API keys, webhook secret, and trial length,
17
+ then asks whether to enable billing immediately. Use Stripe test keys
18
+ (`sk_test_...`, `pk_test_...`) during development.
21
19
 
22
- Use Stripe test keys (`sk_test_...`, `pk_test_...`) during development.
20
+ To check the current configuration at any time:
21
+
22
+ ```bash
23
+ neoagent billing # or: neoagent billing status
24
+ ```
25
+
26
+ To toggle billing without re-running setup:
27
+
28
+ ```bash
29
+ neoagent billing enable # set NEOAGENT_BILLING_ENABLED=true and restart
30
+ neoagent billing disable # set NEOAGENT_BILLING_ENABLED=false and restart
31
+ ```
32
+
33
+ When enabled the admin dashboard will show a **Billing** navigation item and
34
+ the `/api/billing/*` endpoints become active.
35
+
36
+ > **Manual alternative** — you can also set variables directly and restart:
37
+ > ```bash
38
+ > neoagent env set STRIPE_SECRET_KEY sk_live_...
39
+ > neoagent env set STRIPE_PUBLISHABLE_KEY pk_live_...
40
+ > neoagent env set STRIPE_WEBHOOK_SECRET whsec_...
41
+ > neoagent env set NEOAGENT_BILLING_ENABLED true
42
+ > neoagent restart
43
+ > ```
23
44
 
24
45
  ## Subscription plans
25
46
 
@@ -40,6 +61,11 @@ Token limits are written directly to the user account when a subscription
40
61
  becomes active or is updated via webhook. The existing rate-limit enforcement
41
62
  in the runtime reads them with no additional configuration.
42
63
 
64
+ Token limits gate interactive run starts. Background task executions are not
65
+ blocked by the internal token admission check, so scheduled and integration
66
+ tasks can still run while a user is over quota. Their token usage is still
67
+ recorded and included in account and admin usage snapshots.
68
+
43
69
  ### Free plan
44
70
 
45
71
  Create a plan with **Price = 0** to serve as the default for new users. If a
@@ -99,8 +99,17 @@ reset, email changes, and security notifications.
99
99
 
100
100
  ## Billing
101
101
 
102
- Billing is disabled by default. See [Billing](billing.md) for the full setup
103
- guide, webhook configuration, and plan management.
102
+ Billing is disabled by default. Use the interactive wizard to configure it:
103
+
104
+ ```bash
105
+ neoagent billing setup # guided Stripe key and webhook setup
106
+ neoagent billing # show current status
107
+ neoagent billing enable # activate billing and restart
108
+ neoagent billing disable # deactivate billing and restart
109
+ ```
110
+
111
+ See [Billing](billing.md) for the full setup guide, webhook configuration, and
112
+ plan management.
104
113
 
105
114
  | Variable | Default | Purpose |
106
115
  |---|---|---|
@@ -9,18 +9,20 @@ remote access to work.
9
9
  - Node.js 20 or newer with npm
10
10
  - A macOS or Linux user account that can install packages
11
11
  - Enough disk and memory for the models and runtime you choose
12
- - QEMU for the isolated browser and terminal runtime
12
+ - Docker for the isolated browser and terminal runtime
13
13
 
14
- The installer attempts to install QEMU with Homebrew, `apt`, `dnf`, or `yum`.
15
- If the host uses another package manager, install QEMU first.
14
+ The isolated browser and CLI run each user's guest agent in a Docker container.
15
+ Install Docker first; the NeoAgent installer detects it and pre-builds the guest
16
+ runtime image (browser and dependencies baked in) so the first browser or CLI
17
+ action is instant. If Docker is missing, the rest of NeoAgent still installs and
18
+ the cloud browser/CLI become available once Docker is running.
16
19
 
17
20
  ```bash
18
- # macOS
19
- brew install qemu
21
+ # macOS — Docker Desktop
22
+ # https://www.docker.com/products/docker-desktop/
20
23
 
21
- # Ubuntu or Debian
22
- sudo apt-get update
23
- sudo apt-get install -y qemu-system qemu-utils
24
+ # Ubuntu or Debian — Docker Engine
25
+ # https://docs.docker.com/engine/install/
24
26
  ```
25
27
 
26
28
  ## Install NeoAgent
@@ -66,7 +66,7 @@ Protect backups as credentials and personal data.
66
66
  |---|---|
67
67
  | Service does not start | `neoagent status`, then `neoagent logs` |
68
68
  | Chat has no response | Provider connection, selected model, provider quota |
69
- | Browser or shell unavailable | QEMU installation and first runtime boot |
69
+ | Browser or shell unavailable | Docker installed and running, then first runtime boot |
70
70
  | OAuth fails | Reachable HTTPS `PUBLIC_URL` and exact callback URI |
71
71
  | Messaging receives nothing | Channel credentials, webhook, and allowlists |
72
72
  | Task does not run | Enabled state, trigger account, next run, **Runs** |
@@ -20,6 +20,7 @@ import 'package:image/image.dart' as img;
20
20
  import 'package:mobile_scanner/mobile_scanner.dart';
21
21
  import 'package:qr_flutter/qr_flutter.dart';
22
22
  import 'package:shared_preferences/shared_preferences.dart';
23
+ import 'package:url_launcher/url_launcher.dart' as url_launcher;
23
24
  import 'package:socket_io_client/socket_io_client.dart' as io;
24
25
  import 'package:audioplayers/audioplayers.dart';
25
26
  import 'package:tray_manager/tray_manager.dart';
@@ -60,6 +61,7 @@ part 'main_models.dart';
60
61
  part 'main_shared.dart';
61
62
  part 'main_voice_assistant.dart';
62
63
  part 'main_navigation.dart';
64
+ part 'main_timeline.dart';
63
65
  part 'main_runtime.dart';
64
66
  part 'main_controller.dart';
65
67
  part 'main_devices.dart';
@@ -71,6 +73,7 @@ part 'main_security.dart';
71
73
  part 'main_model_picker.dart';
72
74
  part 'main_operations.dart';
73
75
  part 'main_admin.dart';
76
+ part 'main_billing.dart';
74
77
  part 'main_unified.dart';
75
78
  part 'main_install.dart';
76
79
 
@@ -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;
@@ -205,9 +205,15 @@ class _BackendSetupViewState extends State<BackendSetupView> {
205
205
  const SizedBox(height: 10),
206
206
  Center(
207
207
  child: TextButton.icon(
208
- onPressed: () => setState(() => _localInstall = true),
209
- icon: const Icon(Icons.download_outlined, size: 16),
210
- label: const Text('Install NeoAgent on this machine'),
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
+ ),
211
217
  ),
212
218
  ),
213
219
  ],
@@ -268,7 +274,9 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
268
274
  if (!mounted) return;
269
275
  setState(() => _phase = _LocalInstallPhase.checking);
270
276
 
271
- final r = await Process.run(Platform.isWindows ? 'where' : 'which', ['node']);
277
+ final r = await Process.run(Platform.isWindows ? 'where' : 'which', [
278
+ 'node',
279
+ ]);
272
280
  if (r.exitCode == 0) {
273
281
  _nodePath = (r.stdout as String).trim().split('\n').first.trim();
274
282
  } else if (!Platform.isWindows) {
@@ -315,7 +323,8 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
315
323
  if (!_ensureEnvFile()) {
316
324
  setState(() {
317
325
  _phase = _LocalInstallPhase.failed;
318
- _errorMsg = 'Could not create ~/.neoagent/runtime/.env — check directory permissions.';
326
+ _errorMsg =
327
+ 'Could not create ~/.neoagent/runtime/.env — check directory permissions.';
319
328
  });
320
329
  return;
321
330
  }
@@ -324,11 +333,10 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
324
333
  _log.clear();
325
334
  });
326
335
 
327
- final process = await Process.start(
328
- _nodePath!,
329
- ['bin/neoagent.js', 'install'],
330
- workingDirectory: _installDir,
331
- );
336
+ final process = await Process.start(_nodePath!, [
337
+ 'bin/neoagent.js',
338
+ 'install',
339
+ ], workingDirectory: _installDir);
332
340
  _proc = process;
333
341
 
334
342
  void onChunk(String data) {
@@ -349,7 +357,8 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
349
357
  if (!mounted) return;
350
358
  setState(() {
351
359
  _phase = exit == 0 ? _LocalInstallPhase.done : _LocalInstallPhase.failed;
352
- if (exit != 0) _errorMsg = 'Installation failed (exit $exit). Check the log above.';
360
+ if (exit != 0)
361
+ _errorMsg = 'Installation failed (exit $exit). Check the log above.';
353
362
  });
354
363
  }
355
364
 
@@ -388,7 +397,10 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
388
397
  const SizedBox(height: 16),
389
398
  const _BrandLockup(logoSize: 60),
390
399
  const SizedBox(height: 22),
391
- Text('INSTALL LOCALLY', style: _sectionEyebrowStyle()),
400
+ Text(
401
+ 'INSTALL LOCALLY',
402
+ style: _sectionEyebrowStyle(),
403
+ ),
392
404
  const SizedBox(height: 10),
393
405
  Text(
394
406
  'Install NeoAgent as a background service',
@@ -459,7 +471,8 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
459
471
  final proc = _proc;
460
472
  proc?.kill(ProcessSignal.sigterm);
461
473
  await proc?.exitCode;
462
- if (mounted) setState(() => _phase = _LocalInstallPhase.ready);
474
+ if (mounted)
475
+ setState(() => _phase = _LocalInstallPhase.ready);
463
476
  },
464
477
  child: const Text('Cancel'),
465
478
  ),
@@ -1695,6 +1708,7 @@ class _HomeViewState extends State<HomeView> {
1695
1708
  .allowMessagingSuggestion(
1696
1709
  notice.platform,
1697
1710
  suggestion,
1711
+ chatId: notice.chatId,
1698
1712
  );
1699
1713
  },
1700
1714
  icon: Icon(Icons.verified_user_outlined),
@@ -1716,6 +1730,13 @@ class _HomeViewState extends State<HomeView> {
1716
1730
  },
1717
1731
  child: Text('Open Messaging'),
1718
1732
  ),
1733
+ TextButton(
1734
+ onPressed: () async {
1735
+ Navigator.of(dialogContext).pop();
1736
+ await widget.controller.ignoreBlockedSender(notice);
1737
+ },
1738
+ child: Text('Ignore'),
1739
+ ),
1719
1740
  TextButton(
1720
1741
  onPressed: () => Navigator.of(dialogContext).pop(),
1721
1742
  child: Text('Dismiss'),
@@ -2488,6 +2509,8 @@ class _SectionBody extends StatelessWidget {
2488
2509
  switch (controller.selectedSection) {
2489
2510
  case AppSection.chat:
2490
2511
  return ChatPanel(controller: controller);
2512
+ case AppSection.timeline:
2513
+ return TimelinePanel(controller: controller);
2491
2514
  case AppSection.voiceAssistant:
2492
2515
  return VoiceAssistantPanel(controller: controller);
2493
2516
  case AppSection.devices:
@@ -2522,6 +2545,8 @@ class _SectionBody extends StatelessWidget {
2522
2545
  : ChatPanel(controller: controller);
2523
2546
  case AppSection.server:
2524
2547
  return ServerPanel(controller: controller);
2548
+ case AppSection.billing:
2549
+ return BillingPanel(controller: controller);
2525
2550
  }
2526
2551
  }
2527
2552
  }