neoagent 2.3.1-beta.93 → 2.3.1-beta.95

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.
@@ -17,7 +17,7 @@
17
17
  <key>EnvironmentVariables</key>
18
18
  <dict>
19
19
  <key>PATH</key>
20
- <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
20
+ <string>/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
21
21
  <key>HOME</key>
22
22
  <string>__HOME__</string>
23
23
  <key>NODE_ENV</key>
@@ -10,9 +10,12 @@ class NotificationInterceptor {
10
10
  factory NotificationInterceptor() => _instance;
11
11
  NotificationInterceptor._internal();
12
12
 
13
+ static const Duration _perAppCooldown = Duration(seconds: 60);
14
+
13
15
  bool _isListening = false;
14
16
  String _backendUrl = '';
15
17
  String _token = '';
18
+ final Map<String, DateTime> _lastTriggerTimes = {};
16
19
 
17
20
  Future<void> initialize(String backendUrl, String token) async {
18
21
  _backendUrl = backendUrl;
@@ -29,14 +32,21 @@ class NotificationInterceptor {
29
32
  NotificationListenerService.notificationsStream.listen((
30
33
  ServiceNotificationEvent event,
31
34
  ) {
32
- // Filter out noisy system notifications or ongoing foreground services
33
35
  if (event.packageName == null ||
34
36
  event.packageName!.contains('android.system')) {
35
37
  return;
36
38
  }
37
39
 
38
- // We only want to intercept newly posted notifications, not removed ones
40
+ // Skip removed notifications and persistent ongoing ones
39
41
  if (event.hasRemoved == true) return;
42
+ if (event.onGoing == true) return;
43
+
44
+ // Per-app cooldown to avoid flooding the backend
45
+ final pkg = event.packageName!;
46
+ final now = DateTime.now();
47
+ final last = _lastTriggerTimes[pkg];
48
+ if (last != null && now.difference(last) < _perAppCooldown) return;
49
+ _lastTriggerTimes[pkg] = now;
40
50
 
41
51
  _sendToBackend(event);
42
52
  });
@@ -128,17 +128,22 @@ class _ChatPanelState extends State<ChatPanel> {
128
128
  Future<void> _attachFiles() async {
129
129
  final result = await FilePicker.platform.pickFiles(
130
130
  allowMultiple: true,
131
- withData: false,
131
+ withData: kIsWeb,
132
132
  type: FileType.any,
133
133
  );
134
134
  if (!mounted || result == null || result.files.isEmpty) {
135
135
  return;
136
136
  }
137
137
  final attachments = result.files
138
- .where((file) => file.path?.trim().isNotEmpty == true)
138
+ .where(
139
+ (file) =>
140
+ kIsWeb
141
+ ? file.bytes != null
142
+ : file.path?.trim().isNotEmpty == true,
143
+ )
139
144
  .map(
140
145
  (file) => SharedChatAttachment(
141
- uri: file.path!,
146
+ uri: kIsWeb ? file.name : file.path!,
142
147
  name: file.name,
143
148
  mimeType: _mimeTypeForFileName(file.name),
144
149
  sizeBytes: file.size,
@@ -158,20 +163,20 @@ class _ChatPanelState extends State<ChatPanel> {
158
163
  });
159
164
  }
160
165
 
166
+ bool get _isNearBottom {
167
+ if (!_scrollController.hasClients) return true;
168
+ final pos = _scrollController.position;
169
+ if (!pos.hasContentDimensions) return true;
170
+ return pos.pixels >= pos.maxScrollExtent - 80;
171
+ }
172
+
161
173
  void _scrollToBottom() {
162
174
  WidgetsBinding.instance.addPostFrameCallback((_) {
163
- if (!mounted || !_scrollController.hasClients) {
164
- return;
175
+ if (!mounted || !_scrollController.hasClients) return;
176
+ final pos = _scrollController.position;
177
+ if (pos.hasContentDimensions) {
178
+ _scrollController.jumpTo(pos.maxScrollExtent);
165
179
  }
166
- _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
167
- unawaited(
168
- WidgetsBinding.instance.endOfFrame.then((_) {
169
- if (!mounted || !_scrollController.hasClients) {
170
- return;
171
- }
172
- _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
173
- }),
174
- );
175
180
  });
176
181
  }
177
182
 
@@ -185,13 +190,14 @@ class _ChatPanelState extends State<ChatPanel> {
185
190
  _lastMessageCount = messages.length;
186
191
  _lastToolCount = controller.toolEvents.length;
187
192
  _lastStream = controller.streamingAssistant;
188
- _scrollToBottom();
193
+ if (_isNearBottom) _scrollToBottom();
189
194
  }
190
195
 
191
196
  return Column(
192
197
  children: <Widget>[
193
198
  Expanded(
194
- child: ListView(
199
+ child: SelectionArea(
200
+ child: ListView(
195
201
  controller: _scrollController,
196
202
  padding: _pagePadding(context),
197
203
  children: <Widget>[
@@ -257,6 +263,7 @@ class _ChatPanelState extends State<ChatPanel> {
257
263
  ),
258
264
  ],
259
265
  ),
266
+ ),
260
267
  ),
261
268
  Container(
262
269
  padding: const EdgeInsets.fromLTRB(20, 14, 20, 20),
@@ -69,6 +69,7 @@ class NeoAgentController extends ChangeNotifier {
69
69
  DateTime? _lastHomeWidgetSyncAt;
70
70
  int _authCycle = 0;
71
71
  bool _isPollingQrLogin = false;
72
+ bool _socketHasConnectedOnce = false;
72
73
  List<LogEntry> _serverLogs = const <LogEntry>[];
73
74
  List<LogEntry> _clientLogs = const <LogEntry>[];
74
75
 
@@ -6335,7 +6336,7 @@ class NeoAgentController extends ChangeNotifier {
6335
6336
 
6336
6337
  List<ChatEntry> get visibleChatMessages {
6337
6338
  final entries = <ChatEntry>[...chatMessages];
6338
- if (activeRun != null && streamingAssistant.trim().isEmpty) {
6339
+ if (isSendingMessage && activeRun != null && streamingAssistant.trim().isEmpty) {
6339
6340
  entries.add(
6340
6341
  ChatEntry(
6341
6342
  id: '',
@@ -6381,6 +6382,7 @@ class NeoAgentController extends ChangeNotifier {
6381
6382
 
6382
6383
  void _disconnectSocket() {
6383
6384
  socketConnected = false;
6385
+ _socketHasConnectedOnce = false;
6384
6386
  if (_liveVoiceSessionOpenCompleter != null &&
6385
6387
  !_liveVoiceSessionOpenCompleter!.isCompleted) {
6386
6388
  _liveVoiceSessionOpenCompleter!.completeError(
@@ -6417,6 +6419,10 @@ class NeoAgentController extends ChangeNotifier {
6417
6419
  socketConnected = true;
6418
6420
  socket.emit('client:request_logs');
6419
6421
  socket.emit('integrations:status');
6422
+ if (_socketHasConnectedOnce && isAuthenticated) {
6423
+ unawaited(refresh());
6424
+ }
6425
+ _socketHasConnectedOnce = true;
6420
6426
  voiceAssistantLiveState = voiceAssistantLiveState.copyWith(
6421
6427
  transportState: 'connected',
6422
6428
  clearError: _hasRecoverableLiveVoiceTurn(),
@@ -7110,6 +7116,7 @@ class NeoAgentController extends ChangeNotifier {
7110
7116
  }
7111
7117
  if (_backgroundRunIds.remove(runId)) {
7112
7118
  unawaited(refreshRunsOnly());
7119
+ unawaited(refreshMemory());
7113
7120
  notifyListeners();
7114
7121
  return;
7115
7122
  }
@@ -7136,6 +7143,7 @@ class NeoAgentController extends ChangeNotifier {
7136
7143
  }
7137
7144
  if (_backgroundRunIds.remove(runId)) {
7138
7145
  unawaited(refreshRunsOnly());
7146
+ unawaited(refreshMemory());
7139
7147
  notifyListeners();
7140
7148
  return;
7141
7149
  }
@@ -7179,6 +7187,24 @@ class NeoAgentController extends ChangeNotifier {
7179
7187
  'I could not complete that request right now. Please try again in a moment.';
7180
7188
  notifyListeners();
7181
7189
  });
7190
+ socket.on('tasks:task_complete', (dynamic _) {
7191
+ unawaited(refreshTasks());
7192
+ });
7193
+ socket.on('tasks:task_running', (dynamic _) {
7194
+ unawaited(refreshTasks());
7195
+ });
7196
+ socket.on('tasks:task_error', (dynamic _) {
7197
+ unawaited(refreshTasks());
7198
+ });
7199
+ socket.on('tasks:task_deleted', (dynamic _) {
7200
+ unawaited(refreshTasks());
7201
+ });
7202
+ socket.on('tasks:task_skipped', (dynamic _) {
7203
+ unawaited(refreshTasks());
7204
+ });
7205
+ socket.on('skill:draft_created', (dynamic _) {
7206
+ unawaited(refreshSkills());
7207
+ });
7182
7208
  socket.connect();
7183
7209
  _socket = socket;
7184
7210
  }
@@ -1191,7 +1191,7 @@ class _ChatBubble extends StatelessWidget {
1191
1191
  ),
1192
1192
  MarkdownBody(
1193
1193
  data: entry.content,
1194
- selectable: true,
1194
+ selectable: false,
1195
1195
  styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context))
1196
1196
  .copyWith(
1197
1197
  p: Theme.of(context).textTheme.bodyMedium?.copyWith(
@@ -1733,14 +1733,13 @@ class BackendClient {
1733
1733
  }
1734
1734
  try {
1735
1735
  final response = await request.timeout(_requestTimeout);
1736
- if (!_isNoisySettingsStatusPoll(method, uri, response.statusCode)) {
1736
+ if (response.statusCode >= 400) {
1737
1737
  _log(
1738
- 'request.response',
1738
+ 'request.error',
1739
1739
  data: <String, Object?>{
1740
1740
  'method': method,
1741
1741
  'uri': uri.toString(),
1742
1742
  'statusCode': response.statusCode,
1743
- 'allowUnauthorized': allowUnauthorized,
1744
1743
  },
1745
1744
  );
1746
1745
  }
@@ -1775,13 +1774,6 @@ class BackendClient {
1775
1774
  }
1776
1775
  }
1777
1776
 
1778
- bool _isNoisySettingsStatusPoll(String method, Uri uri, int statusCode) {
1779
- if (method != 'GET' || statusCode != 200) {
1780
- return false;
1781
- }
1782
- return uri.path == '/api/settings/update/status';
1783
- }
1784
-
1785
1777
  String _describeTransportError(Object error, Uri uri) {
1786
1778
  final text = error.toString().trim();
1787
1779
  final lower = text.toLowerCase();