neoagent 2.4.4-beta.7 → 2.4.4-beta.9
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/docs/configuration.md +1 -0
- package/docs/supermemory-memory-review.md +1 -1
- 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/flutter_app/lib/main_settings.dart +30 -35
- package/lib/manager.js +52 -0
- package/lib/schema_migrations.js +27 -2
- package/package.json +1 -1
- 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/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +53288 -53147
- 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/.env.example
CHANGED
|
@@ -42,6 +42,11 @@ NEOAGENT_PROFILE=prod
|
|
|
42
42
|
# can always register regardless of this setting).
|
|
43
43
|
# NEOAGENT_ALLOW_SIGNUP=false
|
|
44
44
|
|
|
45
|
+
# Rolling per-user AI token limits. Users can have custom overrides in the
|
|
46
|
+
# admin dashboard.
|
|
47
|
+
NEOAGENT_RATE_LIMIT_4H=2500000
|
|
48
|
+
NEOAGENT_RATE_LIMIT_WEEKLY=10000000
|
|
49
|
+
|
|
45
50
|
# VM runtime settings used by `prod`.
|
|
46
51
|
# Set these before switching NEOAGENT_PROFILE=prod.
|
|
47
52
|
# The app can cache a shared base image automatically from a URL, then create
|
|
@@ -80,6 +85,18 @@ NEOAGENT_EMAIL_SMTP_HOST=
|
|
|
80
85
|
NEOAGENT_EMAIL_SMTP_PORT=587
|
|
81
86
|
NEOAGENT_EMAIL_SMTP_USER=
|
|
82
87
|
NEOAGENT_EMAIL_SMTP_PASS=
|
|
88
|
+
NEOAGENT_EMAIL_SMTP_SECURE=false
|
|
89
|
+
NEOAGENT_EMAIL_SMTP_REQUIRE_TLS=true
|
|
90
|
+
NEOAGENT_EMAIL_SMTP_REJECT_UNAUTHORIZED=true
|
|
91
|
+
NEOAGENT_EMAIL_REPLY_TO=
|
|
92
|
+
NEOAGENT_EMAIL_REQUIRE_SIGNUP_CONFIRMATION=true
|
|
93
|
+
NEOAGENT_EMAIL_REQUIRE_EMAIL_CHANGE_CONFIRMATION=true
|
|
94
|
+
NEOAGENT_EMAIL_NOTIFY_UNUSUAL_LOGIN=true
|
|
95
|
+
NEOAGENT_EMAIL_NOTIFY_ACCOUNT_CHANGES=true
|
|
96
|
+
NEOAGENT_EMAIL_BRAND_NAME=NeoAgent
|
|
97
|
+
NEOAGENT_EMAIL_SUPPORT_URL=
|
|
98
|
+
NEOAGENT_EMAIL_PUBLIC_URL=
|
|
99
|
+
NEOAGENT_EMAIL_TOKEN_TTL_HOURS=24
|
|
83
100
|
|
|
84
101
|
########################################
|
|
85
102
|
# Hosted AI providers
|
package/docs/configuration.md
CHANGED
|
@@ -117,6 +117,7 @@ Optional. When configured, NeoAgent uses SMTP for account flows: signup confirma
|
|
|
117
117
|
| `NEOAGENT_EMAIL_NOTIFY_ACCOUNT_CHANGES` | `true` | Notices for password and email changes |
|
|
118
118
|
| `NEOAGENT_EMAIL_BRAND_NAME` | `NeoAgent` | Display name in email templates |
|
|
119
119
|
| `NEOAGENT_EMAIL_SUPPORT_URL` | optional | Support link for email templates |
|
|
120
|
+
| `NEOAGENT_EMAIL_PUBLIC_URL` | `PUBLIC_URL` | Public base URL used in confirmation and reset links |
|
|
120
121
|
| `NEOAGENT_EMAIL_TOKEN_TTL_HOURS` | `24` | Confirmation link expiry |
|
|
121
122
|
|
|
122
123
|
## Runtime Isolation
|
|
@@ -279,7 +279,7 @@ Supermemory can still be used as a benchmark comparison provider, not as NeoAgen
|
|
|
279
279
|
5. Temporal fact columns.
|
|
280
280
|
6. Integration ingestion jobs and freshness policies.
|
|
281
281
|
7. Conversation summaries and working state.
|
|
282
|
-
8. Manual core memory
|
|
282
|
+
8. Manual core memory management.
|
|
283
283
|
9. UI and API support for memory inspection.
|
|
284
284
|
|
|
285
285
|
### Critical Gaps Before This Review
|
|
@@ -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
|
}
|
|
@@ -1513,42 +1513,37 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1513
1513
|
|
|
1514
1514
|
Widget _buildSecuritySection(BuildContext context, NeoAgentController controller) {
|
|
1515
1515
|
return Card(
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
title: const Text('Security', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
1527
|
-
subtitle: const Text('Tool allowlists, approval gates, and process isolation.'),
|
|
1528
|
-
trailing: const SizedBox.shrink(),
|
|
1529
|
-
),
|
|
1530
|
-
const Divider(height: 1),
|
|
1531
|
-
ListTile(
|
|
1532
|
-
leading: const Icon(Icons.checklist_outlined),
|
|
1533
|
-
title: const Text('Tool Permissions'),
|
|
1534
|
-
subtitle: const Text('Set block / ask / allow per tool category, or pick a global mode.'),
|
|
1535
|
-
trailing: Row(
|
|
1536
|
-
mainAxisSize: MainAxisSize.min,
|
|
1537
|
-
children: const <Widget>[
|
|
1538
|
-
Icon(Icons.shield_rounded, size: 16, color: Colors.green),
|
|
1539
|
-
SizedBox(width: 4),
|
|
1540
|
-
Icon(Icons.chevron_right),
|
|
1541
|
-
],
|
|
1516
|
+
child: Padding(
|
|
1517
|
+
padding: const EdgeInsets.all(20),
|
|
1518
|
+
child: Column(
|
|
1519
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1520
|
+
children: <Widget>[
|
|
1521
|
+
const _SectionTitle('Security'),
|
|
1522
|
+
const SizedBox(height: 10),
|
|
1523
|
+
Text(
|
|
1524
|
+
'Per-tool permission policies, approval gates, and process isolation for shell execution.',
|
|
1525
|
+
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
1542
1526
|
),
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1527
|
+
const SizedBox(height: 8),
|
|
1528
|
+
ListTile(
|
|
1529
|
+
contentPadding: EdgeInsets.zero,
|
|
1530
|
+
leading: Icon(Icons.checklist_outlined, color: _accentAlt),
|
|
1531
|
+
title: const Text('Tool Permissions'),
|
|
1532
|
+
subtitle: Text(
|
|
1533
|
+
'Set block / ask / allow per tool category, or pick a global mode.',
|
|
1534
|
+
style: TextStyle(color: _textSecondary),
|
|
1535
|
+
),
|
|
1536
|
+
trailing: const Icon(Icons.chevron_right),
|
|
1537
|
+
onTap: () {
|
|
1538
|
+
Navigator.of(context).push(
|
|
1539
|
+
MaterialPageRoute<void>(
|
|
1540
|
+
builder: (_) => MainSecurity(controller: controller),
|
|
1541
|
+
),
|
|
1542
|
+
);
|
|
1543
|
+
},
|
|
1544
|
+
),
|
|
1545
|
+
],
|
|
1546
|
+
),
|
|
1552
1547
|
),
|
|
1553
1548
|
);
|
|
1554
1549
|
}
|