neoagent 2.4.4-beta.8 → 2.5.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 +17 -0
- package/README.md +64 -57
- package/docs/agent-run-lifecycle.md +65 -0
- package/docs/agents-and-users.md +48 -0
- package/docs/architecture.md +59 -0
- package/docs/automation-architecture.md +50 -0
- package/docs/automation.md +54 -70
- package/docs/capabilities.md +16 -118
- package/docs/clients-and-devices.md +43 -0
- package/docs/configuration.md +104 -116
- package/docs/development.md +82 -0
- package/docs/devices.md +62 -0
- package/docs/getting-started.md +47 -57
- package/docs/index.md +41 -30
- package/docs/integrations-architecture.md +48 -0
- package/docs/integrations.md +56 -51
- package/docs/memory-architecture.md +90 -0
- package/docs/memory.md +78 -0
- package/docs/migration.md +12 -5
- package/docs/models.md +64 -0
- package/docs/operations.md +46 -30
- package/docs/persistence.md +49 -0
- package/docs/recordings-and-health.md +41 -0
- package/docs/runtime-and-tools.md +52 -0
- package/docs/security-boundaries.md +65 -111
- package/docs/skills.md +32 -45
- package/docs/why-neoagent.md +47 -16
- package/flutter_app/lib/main_account_settings.dart +79 -15
- package/flutter_app/lib/main_chat.dart +155 -8
- package/flutter_app/lib/main_controller.dart +38 -4
- package/flutter_app/lib/main_devices.dart +9 -3
- package/flutter_app/lib/main_models.dart +32 -0
- package/landing/index.html +0 -4
- package/lib/install_helpers.js +1 -0
- package/lib/manager.js +63 -1
- package/package.json +2 -2
- package/server/admin/admin.js +151 -0
- package/server/admin/index.html +55 -3
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +50995 -50850
- package/server/routes/account.js +2 -23
- package/server/routes/admin.js +18 -2
- package/server/routes/agents.js +5 -1
- package/server/routes/memory.js +2 -2
- package/server/services/account/service_email_settings.js +167 -0
- package/server/services/ai/engine.js +2 -17
- package/server/services/ai/rate_limits.js +150 -0
- package/server/services/ai/tools.js +11 -8
- package/server/services/memory/manager.js +0 -3
- package/server/services/websocket.js +5 -1
- package/docs/supermemory-memory-review.md +0 -852
package/docs/why-neoagent.md
CHANGED
|
@@ -1,21 +1,52 @@
|
|
|
1
|
-
#
|
|
1
|
+
# NeoAgent, OpenClaw, and Hermes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This page describes product emphasis, not a universal ranking. The projects
|
|
4
|
+
change quickly, and the right choice depends on the interfaces and operating
|
|
5
|
+
model you need.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
The comparison was reviewed on June 14, 2026 against the public
|
|
8
|
+
[OpenClaw repository](https://github.com/openclaw/openclaw),
|
|
9
|
+
[Hermes Agent repository](https://github.com/NousResearch/hermes-agent), and
|
|
10
|
+
the current NeoAgent source.
|
|
6
11
|
|
|
7
|
-
| | NeoAgent | OpenClaw |
|
|
8
|
-
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
| **Recovery** | `neoagent status`, `neoagent logs`, `neoagent update`, `neoagent fix` | Doctor-style diagnostics plus channel/node tooling |
|
|
12
|
+
| Area | NeoAgent | OpenClaw | Hermes Agent |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
| Primary experience | Operator UI backed by a self-hosted service | Gateway and assistant across channels, nodes, and companion surfaces | Terminal UI or messaging gateway |
|
|
15
|
+
| Persistent memory | Local structured facts, temporal updates, provenance, hybrid retrieval | Project-defined memory and plugin surfaces | Built-in Markdown memory with optional external memory providers |
|
|
16
|
+
| Android operation | Direct ADB control of a host-attached device or emulator | Android companion/node capabilities | Android and Termux-oriented capabilities |
|
|
17
|
+
| Desktop and browser | Isolated runtime, paired desktop companion, paired Chrome extension | Node and companion ecosystem | Local and remote execution backends |
|
|
18
|
+
| Multi-user administration | User accounts, admin interface, optional service email, per-user runtime | Gateway-centered deployment model | Primarily personal-agent configuration |
|
|
19
|
+
| Automation | Schedules, account events, weather events, webhooks, notification runs | Cron, hooks, webhooks, and channel/node events | Cron, gateway events, and tool-driven workflows |
|
|
20
|
+
| Interfaces | Web, Android, desktop, launcher, messaging, extension, wearable firmware | Broad channel, platform, node, and companion ecosystem | Terminal plus Telegram, Discord, Slack, WhatsApp, Signal, email, and other integrations |
|
|
21
|
+
| Migration into NeoAgent | Native importer target | Skills, memory files, and selected keys can be imported | Skills, memory files, and selected keys can be imported |
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
## Choose NeoAgent when
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
- You want a visual operator interface for agents, runs, memory, tasks,
|
|
26
|
+
accounts, devices, and administration.
|
|
27
|
+
- Direct Android control over ADB is central to the deployment.
|
|
28
|
+
- You want structured memory and integration ingestion without a separate
|
|
29
|
+
memory service.
|
|
30
|
+
- You need multiple users or specialist agents with separate state.
|
|
31
|
+
- Launcher mode, Health Connect, recordings, or the bundled device interfaces
|
|
32
|
+
are part of the use case.
|
|
33
|
+
|
|
34
|
+
## Consider OpenClaw when
|
|
35
|
+
|
|
36
|
+
- Its larger gateway, node, channel, plugin, or companion ecosystem is the main
|
|
37
|
+
requirement.
|
|
38
|
+
- You need an existing OpenClaw-specific integration or community resource.
|
|
39
|
+
- Its distributed assistant architecture better matches your devices.
|
|
40
|
+
|
|
41
|
+
## Consider Hermes when
|
|
42
|
+
|
|
43
|
+
- A terminal-first workflow is preferable.
|
|
44
|
+
- Its skill ecosystem, remote execution options, or model workflow fits the
|
|
45
|
+
task.
|
|
46
|
+
- Built-in Markdown memory or one of its external memory providers is already
|
|
47
|
+
part of your setup.
|
|
48
|
+
|
|
49
|
+
NeoAgent's memory architecture is documented in [Memory](memory.md). The
|
|
50
|
+
project does not claim superior recall without a reproducible, published
|
|
51
|
+
benchmark that records the dataset, models, retrieval settings, judge, token
|
|
52
|
+
budget, latency, and run artifacts.
|
|
@@ -403,7 +403,8 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
403
403
|
controller: _displayNameController,
|
|
404
404
|
decoration: const InputDecoration(
|
|
405
405
|
labelText: 'Display name',
|
|
406
|
-
helperText:
|
|
406
|
+
helperText:
|
|
407
|
+
'Shown in the sidebar. Leave blank to use your username.',
|
|
407
408
|
),
|
|
408
409
|
),
|
|
409
410
|
if (_displayNameInlineError != null) ...<Widget>[
|
|
@@ -607,6 +608,16 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
607
608
|
return amount.toString();
|
|
608
609
|
}
|
|
609
610
|
|
|
611
|
+
String? _nextUsageDropLabel(DateTime? value) {
|
|
612
|
+
if (value == null) return null;
|
|
613
|
+
final remaining = value.difference(DateTime.now());
|
|
614
|
+
if (remaining.isNegative) return 'Usage updates shortly';
|
|
615
|
+
if (remaining.inHours > 0) {
|
|
616
|
+
return 'Next usage drop in ${remaining.inHours}h ${remaining.inMinutes.remainder(60)}m';
|
|
617
|
+
}
|
|
618
|
+
return 'Next usage drop in ${remaining.inMinutes + 1}m';
|
|
619
|
+
}
|
|
620
|
+
|
|
610
621
|
Widget _buildUsagePanel() {
|
|
611
622
|
if (widget.controller.isLoadingAccountSettings) {
|
|
612
623
|
return const Center(
|
|
@@ -616,7 +627,7 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
616
627
|
),
|
|
617
628
|
);
|
|
618
629
|
}
|
|
619
|
-
|
|
630
|
+
|
|
620
631
|
final usage = widget.controller.usageAndLimits;
|
|
621
632
|
if (usage == null) {
|
|
622
633
|
return Center(
|
|
@@ -639,12 +650,21 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
639
650
|
);
|
|
640
651
|
}
|
|
641
652
|
|
|
642
|
-
Widget buildStatBox(
|
|
653
|
+
Widget buildStatBox(
|
|
654
|
+
String label,
|
|
655
|
+
int current,
|
|
656
|
+
int? limit, {
|
|
657
|
+
required int remaining,
|
|
658
|
+
required bool reached,
|
|
659
|
+
required DateTime? nextDecreaseAt,
|
|
660
|
+
bool isCustom = false,
|
|
661
|
+
}) {
|
|
643
662
|
final double progress = limit != null
|
|
644
663
|
? (limit <= 0 ? 1.0 : (current / limit).clamp(0.0, 1.0))
|
|
645
664
|
: 0.0;
|
|
646
665
|
final bool nearLimit = progress > 0.8;
|
|
647
|
-
final bool atLimit = progress >= 1.0;
|
|
666
|
+
final bool atLimit = reached || progress >= 1.0;
|
|
667
|
+
final nextDrop = _nextUsageDropLabel(nextDecreaseAt);
|
|
648
668
|
|
|
649
669
|
return Container(
|
|
650
670
|
width: double.infinity,
|
|
@@ -656,8 +676,8 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
656
676
|
color: atLimit
|
|
657
677
|
? _danger.withValues(alpha: 0.6)
|
|
658
678
|
: nearLimit
|
|
659
|
-
|
|
660
|
-
|
|
679
|
+
? _warning.withValues(alpha: 0.5)
|
|
680
|
+
: _borderLight,
|
|
661
681
|
),
|
|
662
682
|
),
|
|
663
683
|
child: Column(
|
|
@@ -665,11 +685,20 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
665
685
|
children: <Widget>[
|
|
666
686
|
Row(
|
|
667
687
|
children: <Widget>[
|
|
668
|
-
Text(
|
|
688
|
+
Text(
|
|
689
|
+
label,
|
|
690
|
+
style: const TextStyle(
|
|
691
|
+
fontSize: 16,
|
|
692
|
+
fontWeight: FontWeight.w600,
|
|
693
|
+
),
|
|
694
|
+
),
|
|
669
695
|
const Spacer(),
|
|
670
696
|
if (limit != null)
|
|
671
697
|
Container(
|
|
672
|
-
padding: const EdgeInsets.symmetric(
|
|
698
|
+
padding: const EdgeInsets.symmetric(
|
|
699
|
+
horizontal: 8,
|
|
700
|
+
vertical: 3,
|
|
701
|
+
),
|
|
673
702
|
decoration: BoxDecoration(
|
|
674
703
|
color: isCustom
|
|
675
704
|
? _warning.withValues(alpha: 0.12)
|
|
@@ -696,7 +725,11 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
696
725
|
style: TextStyle(
|
|
697
726
|
fontSize: 28,
|
|
698
727
|
fontWeight: FontWeight.w800,
|
|
699
|
-
color: atLimit
|
|
728
|
+
color: atLimit
|
|
729
|
+
? _danger
|
|
730
|
+
: nearLimit
|
|
731
|
+
? _warning
|
|
732
|
+
: null,
|
|
700
733
|
),
|
|
701
734
|
),
|
|
702
735
|
Padding(
|
|
@@ -728,16 +761,27 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
728
761
|
value: progress,
|
|
729
762
|
backgroundColor: _border,
|
|
730
763
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
731
|
-
atLimit
|
|
764
|
+
atLimit
|
|
765
|
+
? _danger
|
|
766
|
+
: nearLimit
|
|
767
|
+
? _warning
|
|
768
|
+
: _accent,
|
|
732
769
|
),
|
|
733
770
|
),
|
|
734
771
|
),
|
|
735
772
|
const SizedBox(height: 6),
|
|
736
773
|
Text(
|
|
737
|
-
|
|
774
|
+
atLimit
|
|
775
|
+
? 'Limit reached${nextDrop == null ? '' : ' · $nextDrop'}'
|
|
776
|
+
: '${(progress * 100).toStringAsFixed(0)}% used · ${_formatTokens(remaining)} remaining'
|
|
777
|
+
'${nextDrop == null ? '' : ' · $nextDrop'}',
|
|
738
778
|
style: TextStyle(
|
|
739
779
|
fontSize: 11,
|
|
740
|
-
color: atLimit
|
|
780
|
+
color: atLimit
|
|
781
|
+
? _danger
|
|
782
|
+
: nearLimit
|
|
783
|
+
? _warning
|
|
784
|
+
: _textMuted,
|
|
741
785
|
),
|
|
742
786
|
),
|
|
743
787
|
],
|
|
@@ -756,9 +800,25 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
756
800
|
style: TextStyle(color: _textSecondary, height: 1.4),
|
|
757
801
|
),
|
|
758
802
|
const SizedBox(height: 24),
|
|
759
|
-
buildStatBox(
|
|
803
|
+
buildStatBox(
|
|
804
|
+
'Recent Usage (4 Hours)',
|
|
805
|
+
usage.fourHourUsage,
|
|
806
|
+
usage.fourHourLimit,
|
|
807
|
+
remaining: usage.fourHourRemaining,
|
|
808
|
+
reached: usage.fourHourReached,
|
|
809
|
+
nextDecreaseAt: usage.fourHourNextDecreaseAt,
|
|
810
|
+
isCustom: usage.fourHourIsCustom,
|
|
811
|
+
),
|
|
760
812
|
const SizedBox(height: 16),
|
|
761
|
-
buildStatBox(
|
|
813
|
+
buildStatBox(
|
|
814
|
+
'Weekly Usage',
|
|
815
|
+
usage.weeklyUsage,
|
|
816
|
+
usage.weeklyLimit,
|
|
817
|
+
remaining: usage.weeklyRemaining,
|
|
818
|
+
reached: usage.weeklyReached,
|
|
819
|
+
nextDecreaseAt: usage.weeklyNextDecreaseAt,
|
|
820
|
+
isCustom: usage.weeklyIsCustom,
|
|
821
|
+
),
|
|
762
822
|
],
|
|
763
823
|
);
|
|
764
824
|
}
|
|
@@ -1128,7 +1188,11 @@ class _AccountSettingsTabs extends StatelessWidget {
|
|
|
1128
1188
|
Widget build(BuildContext context) {
|
|
1129
1189
|
final buttons = <Widget>[
|
|
1130
1190
|
_tabButton(AccountSettingsTab.account, Icons.person_outline, 'Account'),
|
|
1131
|
-
_tabButton(
|
|
1191
|
+
_tabButton(
|
|
1192
|
+
AccountSettingsTab.usage,
|
|
1193
|
+
Icons.data_usage_outlined,
|
|
1194
|
+
'Usage & Limits',
|
|
1195
|
+
),
|
|
1132
1196
|
_tabButton(
|
|
1133
1197
|
AccountSettingsTab.security,
|
|
1134
1198
|
Icons.security_outlined,
|
|
@@ -295,12 +295,13 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
295
295
|
transitionBuilder: (ctx, animation, secondary, child) => FadeTransition(
|
|
296
296
|
opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
|
|
297
297
|
child: SlideTransition(
|
|
298
|
-
position:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
298
|
+
position:
|
|
299
|
+
Tween<Offset>(
|
|
300
|
+
begin: const Offset(0, 0.04),
|
|
301
|
+
end: Offset.zero,
|
|
302
|
+
).animate(
|
|
303
|
+
CurvedAnimation(parent: animation, curve: Curves.easeOutCubic),
|
|
304
|
+
),
|
|
304
305
|
child: child,
|
|
305
306
|
),
|
|
306
307
|
),
|
|
@@ -398,6 +399,11 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
398
399
|
_maybeFollowChatContent(messages, controller);
|
|
399
400
|
|
|
400
401
|
final threadChildren = <Widget>[
|
|
402
|
+
if (controller.usageAndLimits case final usage?
|
|
403
|
+
when usage.hasLimits) ...<Widget>[
|
|
404
|
+
_RateLimitStatusCard(usage: usage),
|
|
405
|
+
const SizedBox(height: 16),
|
|
406
|
+
],
|
|
401
407
|
if (controller.errorMessage != null) ...<Widget>[
|
|
402
408
|
_InlineError(message: controller.errorMessage!),
|
|
403
409
|
const SizedBox(height: 16),
|
|
@@ -471,8 +477,12 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
471
477
|
SelectionArea(
|
|
472
478
|
child: ListView(
|
|
473
479
|
controller: _scrollController,
|
|
474
|
-
padding:
|
|
475
|
-
|
|
480
|
+
padding: EdgeInsets.fromLTRB(
|
|
481
|
+
sidePadding,
|
|
482
|
+
30,
|
|
483
|
+
sidePadding,
|
|
484
|
+
18,
|
|
485
|
+
),
|
|
476
486
|
children: <Widget>[
|
|
477
487
|
Center(
|
|
478
488
|
child: ConstrainedBox(
|
|
@@ -715,6 +725,143 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
715
725
|
}
|
|
716
726
|
}
|
|
717
727
|
|
|
728
|
+
class _RateLimitStatusCard extends StatelessWidget {
|
|
729
|
+
const _RateLimitStatusCard({required this.usage});
|
|
730
|
+
|
|
731
|
+
final AccountUsageAndLimits usage;
|
|
732
|
+
|
|
733
|
+
String _formatTokens(int amount) {
|
|
734
|
+
if (amount >= 1000000) {
|
|
735
|
+
final value = amount / 1000000;
|
|
736
|
+
return '${value.toStringAsFixed(value == value.roundToDouble() ? 0 : 1)}M';
|
|
737
|
+
}
|
|
738
|
+
if (amount >= 1000) {
|
|
739
|
+
final value = amount / 1000;
|
|
740
|
+
return '${value.toStringAsFixed(value == value.roundToDouble() ? 0 : 1)}k';
|
|
741
|
+
}
|
|
742
|
+
return amount.toString();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
String? _nextDropLabel(DateTime? value) {
|
|
746
|
+
if (value == null) return null;
|
|
747
|
+
final remaining = value.difference(DateTime.now());
|
|
748
|
+
if (remaining.isNegative) return 'Usage updates shortly';
|
|
749
|
+
if (remaining.inHours > 0) {
|
|
750
|
+
return 'Next usage drop in ${remaining.inHours}h ${remaining.inMinutes.remainder(60)}m';
|
|
751
|
+
}
|
|
752
|
+
return 'Next usage drop in ${remaining.inMinutes + 1}m';
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
Widget _buildWindow({
|
|
756
|
+
required String label,
|
|
757
|
+
required int usageAmount,
|
|
758
|
+
required int? limit,
|
|
759
|
+
required int remaining,
|
|
760
|
+
required bool reached,
|
|
761
|
+
required DateTime? nextDecreaseAt,
|
|
762
|
+
}) {
|
|
763
|
+
if (limit == null || limit <= 0) return const SizedBox.shrink();
|
|
764
|
+
final progress = (usageAmount / limit).clamp(0.0, 1.0);
|
|
765
|
+
final color = reached
|
|
766
|
+
? _danger
|
|
767
|
+
: progress >= 0.8
|
|
768
|
+
? _warning
|
|
769
|
+
: _accent;
|
|
770
|
+
final nextDrop = _nextDropLabel(nextDecreaseAt);
|
|
771
|
+
return Column(
|
|
772
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
773
|
+
children: <Widget>[
|
|
774
|
+
Row(
|
|
775
|
+
children: <Widget>[
|
|
776
|
+
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
|
|
777
|
+
const Spacer(),
|
|
778
|
+
Text(
|
|
779
|
+
reached ? 'Limit reached' : '${_formatTokens(remaining)} left',
|
|
780
|
+
style: TextStyle(
|
|
781
|
+
color: color,
|
|
782
|
+
fontSize: 12,
|
|
783
|
+
fontWeight: FontWeight.w600,
|
|
784
|
+
),
|
|
785
|
+
),
|
|
786
|
+
],
|
|
787
|
+
),
|
|
788
|
+
const SizedBox(height: 8),
|
|
789
|
+
ClipRRect(
|
|
790
|
+
borderRadius: BorderRadius.circular(999),
|
|
791
|
+
child: LinearProgressIndicator(
|
|
792
|
+
minHeight: 7,
|
|
793
|
+
value: progress,
|
|
794
|
+
backgroundColor: _border,
|
|
795
|
+
valueColor: AlwaysStoppedAnimation<Color>(color),
|
|
796
|
+
),
|
|
797
|
+
),
|
|
798
|
+
const SizedBox(height: 6),
|
|
799
|
+
Text(
|
|
800
|
+
'${_formatTokens(usageAmount)} / ${_formatTokens(limit)} tokens'
|
|
801
|
+
'${nextDrop == null ? '' : ' · $nextDrop'}',
|
|
802
|
+
style: TextStyle(color: _textMuted, fontSize: 11),
|
|
803
|
+
),
|
|
804
|
+
],
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
@override
|
|
809
|
+
Widget build(BuildContext context) {
|
|
810
|
+
final windows = <Widget>[
|
|
811
|
+
_buildWindow(
|
|
812
|
+
label: '4-hour usage',
|
|
813
|
+
usageAmount: usage.fourHourUsage,
|
|
814
|
+
limit: usage.fourHourLimit,
|
|
815
|
+
remaining: usage.fourHourRemaining,
|
|
816
|
+
reached: usage.fourHourReached,
|
|
817
|
+
nextDecreaseAt: usage.fourHourNextDecreaseAt,
|
|
818
|
+
),
|
|
819
|
+
_buildWindow(
|
|
820
|
+
label: '7-day usage',
|
|
821
|
+
usageAmount: usage.weeklyUsage,
|
|
822
|
+
limit: usage.weeklyLimit,
|
|
823
|
+
remaining: usage.weeklyRemaining,
|
|
824
|
+
reached: usage.weeklyReached,
|
|
825
|
+
nextDecreaseAt: usage.weeklyNextDecreaseAt,
|
|
826
|
+
),
|
|
827
|
+
];
|
|
828
|
+
return Container(
|
|
829
|
+
width: double.infinity,
|
|
830
|
+
padding: const EdgeInsets.all(14),
|
|
831
|
+
decoration: BoxDecoration(
|
|
832
|
+
color: usage.isReached ? _danger.withValues(alpha: 0.08) : _bgSecondary,
|
|
833
|
+
borderRadius: BorderRadius.circular(16),
|
|
834
|
+
border: Border.all(
|
|
835
|
+
color: usage.isReached
|
|
836
|
+
? _danger.withValues(alpha: 0.55)
|
|
837
|
+
: _borderLight,
|
|
838
|
+
),
|
|
839
|
+
),
|
|
840
|
+
child: LayoutBuilder(
|
|
841
|
+
builder: (context, constraints) {
|
|
842
|
+
if (constraints.maxWidth < 620) {
|
|
843
|
+
return Column(
|
|
844
|
+
children: <Widget>[
|
|
845
|
+
windows.first,
|
|
846
|
+
const SizedBox(height: 14),
|
|
847
|
+
windows.last,
|
|
848
|
+
],
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
return Row(
|
|
852
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
853
|
+
children: <Widget>[
|
|
854
|
+
Expanded(child: windows.first),
|
|
855
|
+
const SizedBox(width: 24),
|
|
856
|
+
Expanded(child: windows.last),
|
|
857
|
+
],
|
|
858
|
+
);
|
|
859
|
+
},
|
|
860
|
+
),
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
718
865
|
double _chatSidePadding(BuildContext context) {
|
|
719
866
|
final width = MediaQuery.sizeOf(context).width;
|
|
720
867
|
if (width >= 1280) return 40;
|
|
@@ -2033,6 +2033,11 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
2033
2033
|
_backendClient.fetchTokenUsageSummary(backendUrl, agentId: agentId),
|
|
2034
2034
|
const <String, dynamic>{},
|
|
2035
2035
|
);
|
|
2036
|
+
final rateLimitFuture = _softRefreshLoad<Map<String, dynamic>>(
|
|
2037
|
+
'rate_limits',
|
|
2038
|
+
_backendClient.fetchAccountUsage(backendUrl),
|
|
2039
|
+
const <String, dynamic>{},
|
|
2040
|
+
);
|
|
2036
2041
|
final updateFuture = _backendClient
|
|
2037
2042
|
.fetchUpdateStatus(backendUrl)
|
|
2038
2043
|
.catchError((_) => const <String, dynamic>{});
|
|
@@ -2142,6 +2147,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
2142
2147
|
final runsResponse = await runsFuture;
|
|
2143
2148
|
final versionResponse = await versionFuture;
|
|
2144
2149
|
final tokenResponse = await tokenFuture;
|
|
2150
|
+
final rateLimitResponse = await rateLimitFuture;
|
|
2145
2151
|
final updateResponse = await updateFuture;
|
|
2146
2152
|
final messagingResponse = await messagingFuture;
|
|
2147
2153
|
final messagingMessagesResponse = await messagingMessagesFuture;
|
|
@@ -2193,6 +2199,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
2193
2199
|
versionInfo = versionResponse;
|
|
2194
2200
|
backendHealthStatus = healthResponse;
|
|
2195
2201
|
tokenUsage = TokenUsageSnapshot.fromJson(tokenResponse);
|
|
2202
|
+
usageAndLimits = AccountUsageAndLimits.fromJson(rateLimitResponse);
|
|
2196
2203
|
updateStatus = UpdateStatusSnapshot.fromJson(updateResponse);
|
|
2197
2204
|
messagingStatuses = messagingResponse.map(
|
|
2198
2205
|
(key, value) => MapEntry(
|
|
@@ -4640,20 +4647,24 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4640
4647
|
}
|
|
4641
4648
|
activeRun = null;
|
|
4642
4649
|
await refreshRunsOnly();
|
|
4650
|
+
await refreshRateLimitUsage();
|
|
4643
4651
|
} catch (error) {
|
|
4652
|
+
final friendlyError = _friendlyErrorMessage(error);
|
|
4644
4653
|
chatMessages = <ChatEntry>[
|
|
4645
4654
|
...chatMessages,
|
|
4646
4655
|
ChatEntry(
|
|
4647
4656
|
id: '',
|
|
4648
4657
|
role: 'assistant',
|
|
4649
|
-
content:
|
|
4650
|
-
'I could not complete that request right now. Please try again in a moment.',
|
|
4658
|
+
content: friendlyError,
|
|
4651
4659
|
platform: 'flutter',
|
|
4652
4660
|
createdAt: DateTime.now(),
|
|
4653
4661
|
),
|
|
4654
4662
|
];
|
|
4655
4663
|
activeRun = null;
|
|
4656
|
-
errorMessage =
|
|
4664
|
+
errorMessage = friendlyError;
|
|
4665
|
+
if (error is BackendException && error.statusCode == 429) {
|
|
4666
|
+
await refreshRateLimitUsage();
|
|
4667
|
+
}
|
|
4657
4668
|
} finally {
|
|
4658
4669
|
if (_socket == null || !socketConnected) {
|
|
4659
4670
|
isSendingMessage = false;
|
|
@@ -4786,6 +4797,16 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4786
4797
|
}
|
|
4787
4798
|
}
|
|
4788
4799
|
|
|
4800
|
+
Future<void> refreshRateLimitUsage() async {
|
|
4801
|
+
if (!isAuthenticated) return;
|
|
4802
|
+
try {
|
|
4803
|
+
usageAndLimits = AccountUsageAndLimits.fromJson(
|
|
4804
|
+
await _backendClient.fetchAccountUsage(backendUrl),
|
|
4805
|
+
);
|
|
4806
|
+
notifyListeners();
|
|
4807
|
+
} catch (_) {}
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4789
4810
|
Future<bool> updateAccountEmail({
|
|
4790
4811
|
required String email,
|
|
4791
4812
|
required String currentPassword,
|
|
@@ -7292,11 +7313,13 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
7292
7313
|
final payload = _jsonMap(data);
|
|
7293
7314
|
final runId = payload['runId']?.toString() ?? '';
|
|
7294
7315
|
if (_voiceRunIds.remove(runId)) {
|
|
7316
|
+
unawaited(refreshRateLimitUsage());
|
|
7295
7317
|
return;
|
|
7296
7318
|
}
|
|
7297
7319
|
if (_backgroundRunIds.remove(runId)) {
|
|
7298
7320
|
unawaited(refreshRunsOnly());
|
|
7299
7321
|
unawaited(refreshMemory());
|
|
7322
|
+
unawaited(refreshRateLimitUsage());
|
|
7300
7323
|
notifyListeners();
|
|
7301
7324
|
return;
|
|
7302
7325
|
}
|
|
@@ -7320,6 +7343,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
7320
7343
|
);
|
|
7321
7344
|
}
|
|
7322
7345
|
unawaited(refreshRunsOnly());
|
|
7346
|
+
unawaited(refreshRateLimitUsage());
|
|
7323
7347
|
notifyListeners();
|
|
7324
7348
|
});
|
|
7325
7349
|
socket.on('run:stopped', (dynamic data) {
|
|
@@ -7370,8 +7394,18 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
7370
7394
|
streamingAssistant = '';
|
|
7371
7395
|
activeRun = null;
|
|
7372
7396
|
isSendingMessage = false;
|
|
7373
|
-
|
|
7397
|
+
final message =
|
|
7398
|
+
payload['error']?.toString().trim() ??
|
|
7374
7399
|
'I could not complete that request right now. Please try again in a moment.';
|
|
7400
|
+
errorMessage = _friendlyErrorMessage(
|
|
7401
|
+
BackendException(
|
|
7402
|
+
message,
|
|
7403
|
+
statusCode: payload['code'] == 'RATE_LIMIT_EXCEEDED' ? 429 : null,
|
|
7404
|
+
),
|
|
7405
|
+
);
|
|
7406
|
+
if (payload['code'] == 'RATE_LIMIT_EXCEEDED') {
|
|
7407
|
+
unawaited(refreshRateLimitUsage());
|
|
7408
|
+
}
|
|
7375
7409
|
notifyListeners();
|
|
7376
7410
|
});
|
|
7377
7411
|
socket.on('tasks:task_complete', (dynamic _) {
|
|
@@ -1734,6 +1734,14 @@ class _InteractiveSurfacePreviewState
|
|
|
1734
1734
|
Widget build(BuildContext context) {
|
|
1735
1735
|
final path = widget.screenshotPath;
|
|
1736
1736
|
final hasImage = path != null && path.isNotEmpty;
|
|
1737
|
+
final viewportHeight = MediaQuery.sizeOf(context).height;
|
|
1738
|
+
final surfaceHeightCap = widget.surface == _DeviceSurface.android
|
|
1739
|
+
? 640.0
|
|
1740
|
+
: 560.0;
|
|
1741
|
+
final previewMaxHeight = math.min(
|
|
1742
|
+
surfaceHeightCap,
|
|
1743
|
+
viewportHeight * 0.48,
|
|
1744
|
+
);
|
|
1737
1745
|
final aspectRatio = switch (widget.surface) {
|
|
1738
1746
|
_DeviceSurface.browser => 16 / 10,
|
|
1739
1747
|
_DeviceSurface.android => 10 / 16,
|
|
@@ -1755,9 +1763,7 @@ class _InteractiveSurfacePreviewState
|
|
|
1755
1763
|
child: Column(
|
|
1756
1764
|
children: <Widget>[
|
|
1757
1765
|
ConstrainedBox(
|
|
1758
|
-
constraints: BoxConstraints(
|
|
1759
|
-
maxHeight: widget.surface == _DeviceSurface.android ? 640 : 560,
|
|
1760
|
-
),
|
|
1766
|
+
constraints: BoxConstraints(maxHeight: previewMaxHeight),
|
|
1761
1767
|
child: AspectRatio(
|
|
1762
1768
|
aspectRatio: aspectRatio,
|
|
1763
1769
|
child: ClipRRect(
|
|
@@ -4111,6 +4111,12 @@ class AccountUsageAndLimits {
|
|
|
4111
4111
|
this.weeklyLimit,
|
|
4112
4112
|
this.fourHourUsage = 0,
|
|
4113
4113
|
this.weeklyUsage = 0,
|
|
4114
|
+
this.fourHourRemaining = 0,
|
|
4115
|
+
this.weeklyRemaining = 0,
|
|
4116
|
+
this.fourHourReached = false,
|
|
4117
|
+
this.weeklyReached = false,
|
|
4118
|
+
this.fourHourNextDecreaseAt,
|
|
4119
|
+
this.weeklyNextDecreaseAt,
|
|
4114
4120
|
this.fourHourIsCustom = false,
|
|
4115
4121
|
this.weeklyIsCustom = false,
|
|
4116
4122
|
});
|
|
@@ -4118,11 +4124,28 @@ class AccountUsageAndLimits {
|
|
|
4118
4124
|
factory AccountUsageAndLimits.fromJson(Map<String, dynamic> json) {
|
|
4119
4125
|
final limits = json['limits'] is Map ? json['limits'] as Map : const {};
|
|
4120
4126
|
final usage = json['usage'] is Map ? json['usage'] as Map : const {};
|
|
4127
|
+
final remaining = json['remaining'] is Map
|
|
4128
|
+
? json['remaining'] as Map
|
|
4129
|
+
: const {};
|
|
4130
|
+
final reached = json['reached'] is Map ? json['reached'] as Map : const {};
|
|
4131
|
+
final nextDecreaseAt = json['nextDecreaseAt'] is Map
|
|
4132
|
+
? json['nextDecreaseAt'] as Map
|
|
4133
|
+
: const {};
|
|
4121
4134
|
return AccountUsageAndLimits(
|
|
4122
4135
|
fourHourLimit: int.tryParse(limits['fourHour']?.toString() ?? ''),
|
|
4123
4136
|
weeklyLimit: int.tryParse(limits['weekly']?.toString() ?? ''),
|
|
4124
4137
|
fourHourUsage: _asInt(usage['fourHour']),
|
|
4125
4138
|
weeklyUsage: _asInt(usage['weekly']),
|
|
4139
|
+
fourHourRemaining: _asInt(remaining['fourHour']),
|
|
4140
|
+
weeklyRemaining: _asInt(remaining['weekly']),
|
|
4141
|
+
fourHourReached: reached['fourHour'] == true,
|
|
4142
|
+
weeklyReached: reached['weekly'] == true,
|
|
4143
|
+
fourHourNextDecreaseAt: DateTime.tryParse(
|
|
4144
|
+
nextDecreaseAt['fourHour']?.toString() ?? '',
|
|
4145
|
+
),
|
|
4146
|
+
weeklyNextDecreaseAt: DateTime.tryParse(
|
|
4147
|
+
nextDecreaseAt['weekly']?.toString() ?? '',
|
|
4148
|
+
),
|
|
4126
4149
|
fourHourIsCustom: limits['fourHourIsCustom'] == true,
|
|
4127
4150
|
weeklyIsCustom: limits['weeklyIsCustom'] == true,
|
|
4128
4151
|
);
|
|
@@ -4132,6 +4155,15 @@ class AccountUsageAndLimits {
|
|
|
4132
4155
|
final int? weeklyLimit;
|
|
4133
4156
|
final int fourHourUsage;
|
|
4134
4157
|
final int weeklyUsage;
|
|
4158
|
+
final int fourHourRemaining;
|
|
4159
|
+
final int weeklyRemaining;
|
|
4160
|
+
final bool fourHourReached;
|
|
4161
|
+
final bool weeklyReached;
|
|
4162
|
+
final DateTime? fourHourNextDecreaseAt;
|
|
4163
|
+
final DateTime? weeklyNextDecreaseAt;
|
|
4135
4164
|
final bool fourHourIsCustom;
|
|
4136
4165
|
final bool weeklyIsCustom;
|
|
4166
|
+
|
|
4167
|
+
bool get hasLimits => fourHourLimit != null || weeklyLimit != null;
|
|
4168
|
+
bool get isReached => fourHourReached || weeklyReached;
|
|
4137
4169
|
}
|
package/landing/index.html
CHANGED
|
@@ -890,7 +890,6 @@ p { margin: 0; text-wrap: pretty; }
|
|
|
890
890
|
</nav>
|
|
891
891
|
<div class="nav-right">
|
|
892
892
|
<a class="signin" href="/app">Sign in</a>
|
|
893
|
-
<a class="btn btn-primary" href="#cta">Get early access</a>
|
|
894
893
|
</div>
|
|
895
894
|
<button class="nav-toggle" aria-label="Menu">
|
|
896
895
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M3 12h18M3 18h18" stroke-linecap="round"></path></svg>
|
|
@@ -900,7 +899,6 @@ p { margin: 0; text-wrap: pretty; }
|
|
|
900
899
|
<a href="#features">Features</a>
|
|
901
900
|
<a href="#channels">Channels</a>
|
|
902
901
|
<a href="#how">How it works</a>
|
|
903
|
-
<a class="btn btn-primary" href="#cta">Get early access</a>
|
|
904
902
|
</div>
|
|
905
903
|
</header>
|
|
906
904
|
|
|
@@ -919,7 +917,6 @@ p { margin: 0; text-wrap: pretty; }
|
|
|
919
917
|
</h1>
|
|
920
918
|
<p class="lede">NeoAgent watches, listens, and acts across your devices, browser, voice, and messaging — one operator that quietly handles your digital world.</p>
|
|
921
919
|
<div class="hero-actions">
|
|
922
|
-
<a class="btn btn-primary btn-lg" href="#cta">Get early access</a>
|
|
923
920
|
<a class="btn btn-soft btn-lg" href="#features">Explore features
|
|
924
921
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
|
925
922
|
</a>
|
|
@@ -1201,7 +1198,6 @@ p { margin: 0; text-wrap: pretty; }
|
|
|
1201
1198
|
<h2>The agent that just <em>handles it.</em></h2>
|
|
1202
1199
|
<p>Be first to put NeoAgent to work across your devices, channels, and life. We'll send your invite as spots open up.</p>
|
|
1203
1200
|
<div class="hero-actions" style="justify-content:center;">
|
|
1204
|
-
<a class="btn btn-primary btn-lg" href="#">Get early access</a>
|
|
1205
1201
|
<a class="btn btn-soft btn-lg" href="#features">Explore features</a>
|
|
1206
1202
|
</div>
|
|
1207
1203
|
</div>
|