neoagent 3.0.1-beta.14 → 3.0.1-beta.16

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.
@@ -11,6 +11,7 @@ class ChatPanel extends StatefulWidget {
11
11
 
12
12
  class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
13
13
  static const double _autoScrollBottomThreshold = 120;
14
+ static const double _olderHistoryLoadThreshold = 180;
14
15
  static const int _autoScrollSettlePasses = 4;
15
16
 
16
17
  late final TextEditingController _composerController;
@@ -21,6 +22,7 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
21
22
  String _lastScrollContentSignature = '';
22
23
  bool _stickToBottom = true;
23
24
  bool _ignoreScrollUpdates = false;
25
+ bool _loadingOlderHistory = false;
24
26
  int _scrollGeneration = 0;
25
27
  bool _isSendingChatMessage = false;
26
28
  bool _isDictating = false;
@@ -267,8 +269,18 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
267
269
  return pos.pixels >= pos.maxScrollExtent - _autoScrollBottomThreshold;
268
270
  }
269
271
 
272
+ bool get _isNearTop {
273
+ if (!_scrollController.hasClients) return false;
274
+ final pos = _scrollController.position;
275
+ if (!pos.hasContentDimensions) return false;
276
+ return pos.pixels <= _olderHistoryLoadThreshold;
277
+ }
278
+
270
279
  void _handleScrollPositionChanged() {
271
280
  if (_ignoreScrollUpdates || !_scrollController.hasClients) return;
281
+ if (_isNearTop) {
282
+ unawaited(_maybeLoadOlderHistory());
283
+ }
272
284
  final nearBottom = _isNearBottom;
273
285
  if (_stickToBottom && !nearBottom) {
274
286
  _scrollGeneration++;
@@ -278,6 +290,46 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
278
290
  }
279
291
  }
280
292
 
293
+ Future<void> _maybeLoadOlderHistory() async {
294
+ final controller = widget.controller;
295
+ if (_loadingOlderHistory ||
296
+ controller.isLoadingOlderChatHistory ||
297
+ !controller.chatHistoryHasMore ||
298
+ !_isNearTop) {
299
+ return;
300
+ }
301
+ _loadingOlderHistory = true;
302
+ final hasClients = _scrollController.hasClients;
303
+ final previousPixels = hasClients ? _scrollController.position.pixels : 0.0;
304
+ final previousMaxExtent = hasClients
305
+ ? _scrollController.position.maxScrollExtent
306
+ : 0.0;
307
+ final loaded = await controller.loadOlderChatHistory();
308
+ if (!mounted || !loaded) {
309
+ _loadingOlderHistory = false;
310
+ return;
311
+ }
312
+ WidgetsBinding.instance.addPostFrameCallback((_) {
313
+ if (!mounted || !_scrollController.hasClients) {
314
+ _loadingOlderHistory = false;
315
+ return;
316
+ }
317
+ final position = _scrollController.position;
318
+ final extentDelta = position.maxScrollExtent - previousMaxExtent;
319
+ final targetOffset = (previousPixels + extentDelta).clamp(
320
+ 0.0,
321
+ position.maxScrollExtent,
322
+ );
323
+ _ignoreScrollUpdates = true;
324
+ _scrollController.jumpTo(targetOffset.toDouble());
325
+ _ignoreScrollUpdates = false;
326
+ _loadingOlderHistory = false;
327
+ if (_isNearTop) {
328
+ unawaited(_maybeLoadOlderHistory());
329
+ }
330
+ });
331
+ }
332
+
281
333
  void _openModelPicker() {
282
334
  final controller = widget.controller;
283
335
  if (controller.hasLiveRun) return;
@@ -396,6 +448,18 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
396
448
  _maybeFollowChatContent(messages, controller);
397
449
 
398
450
  final threadChildren = <Widget>[
451
+ if (controller.isLoadingOlderChatHistory) ...<Widget>[
452
+ const Padding(
453
+ padding: EdgeInsets.only(bottom: 16),
454
+ child: Center(
455
+ child: SizedBox(
456
+ width: 20,
457
+ height: 20,
458
+ child: CircularProgressIndicator(strokeWidth: 2),
459
+ ),
460
+ ),
461
+ ),
462
+ ],
399
463
  if (controller.usageAndLimits case final usage?
400
464
  when usage.hasLimits) ...<Widget>[
401
465
  _RateLimitStatusCard(usage: usage),
@@ -66,6 +66,7 @@ class NeoAgentController extends ChangeNotifier {
66
66
  final Map<String, DateTime> _manualRunCooldowns = <String, DateTime>{};
67
67
  static const Duration _manualRunCooldownDuration = Duration(seconds: 10);
68
68
  static const Duration _homeWidgetSyncCooldown = Duration(seconds: 5);
69
+ static const int _chatHistoryPageSize = 40;
69
70
  DateTime? _lastHomeWidgetSyncAt;
70
71
  int _authCycle = 0;
71
72
  bool _isPollingQrLogin = false;
@@ -144,6 +145,8 @@ class NeoAgentController extends ChangeNotifier {
144
145
  List<RecordingSessionItem> recordingSessions = const <RecordingSessionItem>[];
145
146
 
146
147
  List<ChatEntry> chatMessages = const <ChatEntry>[];
148
+ bool chatHistoryHasMore = false;
149
+ bool isLoadingOlderChatHistory = false;
147
150
  List<AgentProfile> agentProfiles = const <AgentProfile>[];
148
151
  String? selectedAgentId;
149
152
  List<ModelMeta> supportedModels = const <ModelMeta>[];
@@ -205,6 +208,9 @@ class NeoAgentController extends ChangeNotifier {
205
208
  String? _pendingChatDraft;
206
209
  List<SharedChatAttachment> _pendingSharedChatAttachments =
207
210
  const <SharedChatAttachment>[];
211
+ String? _chatHistoryBeforeCreatedAt;
212
+ String? _chatHistoryBeforeSource;
213
+ String? _chatHistoryBeforeId;
208
214
 
209
215
  ActiveRunState? activeRun;
210
216
  List<ToolEventItem> toolEvents = const <ToolEventItem>[];
@@ -500,6 +506,103 @@ class NeoAgentController extends ChangeNotifier {
500
506
  ];
501
507
  }
502
508
 
509
+ void _resetChatHistoryPagination() {
510
+ chatHistoryHasMore = false;
511
+ isLoadingOlderChatHistory = false;
512
+ _chatHistoryBeforeCreatedAt = null;
513
+ _chatHistoryBeforeSource = null;
514
+ _chatHistoryBeforeId = null;
515
+ }
516
+
517
+ void _applyChatHistoryCursor(Map<String, dynamic> history) {
518
+ chatHistoryHasMore = history['hasMore'] == true;
519
+ _chatHistoryBeforeCreatedAt = _optionalIdFrom(
520
+ history['nextBeforeCreatedAt'],
521
+ );
522
+ _chatHistoryBeforeSource = _optionalIdFrom(history['nextBeforeSource']);
523
+ _chatHistoryBeforeId = _optionalIdFrom(history['nextBeforeId']);
524
+ }
525
+
526
+ String _chatEntryKey(ChatEntry entry) {
527
+ final stableId = entry.id.trim();
528
+ if (stableId.isNotEmpty) {
529
+ return [
530
+ stableId,
531
+ entry.platform,
532
+ entry.role,
533
+ entry.createdAt.toIso8601String(),
534
+ ].join('|');
535
+ }
536
+ return [
537
+ entry.role,
538
+ entry.platform,
539
+ entry.createdAt.toIso8601String(),
540
+ entry.content,
541
+ ].join('|');
542
+ }
543
+
544
+ List<ChatEntry> _chatHistoryEntriesFromResponse(
545
+ Map<String, dynamic> history,
546
+ ) {
547
+ return _decodeModelList(
548
+ 'chat_history',
549
+ history['messages'],
550
+ ChatEntry.fromJson,
551
+ fallbackToMapValues: true,
552
+ );
553
+ }
554
+
555
+ Future<bool> loadOlderChatHistory() async {
556
+ if (!isAuthenticated ||
557
+ isLoadingOlderChatHistory ||
558
+ !chatHistoryHasMore ||
559
+ _chatHistoryBeforeCreatedAt == null ||
560
+ _chatHistoryBeforeSource == null ||
561
+ _chatHistoryBeforeId == null) {
562
+ return false;
563
+ }
564
+
565
+ final agentId = _scopedAgentId;
566
+ final beforeCreatedAt = _chatHistoryBeforeCreatedAt;
567
+ final beforeSource = _chatHistoryBeforeSource;
568
+ final beforeId = _chatHistoryBeforeId;
569
+ isLoadingOlderChatHistory = true;
570
+ notifyListeners();
571
+ try {
572
+ final history = await _backendClient.fetchChatHistory(
573
+ backendUrl,
574
+ agentId: agentId,
575
+ limit: _chatHistoryPageSize,
576
+ beforeCreatedAt: beforeCreatedAt,
577
+ beforeSource: beforeSource,
578
+ beforeId: beforeId,
579
+ );
580
+ if (agentId != _scopedAgentId) {
581
+ return false;
582
+ }
583
+ final olderMessages = _chatHistoryEntriesFromResponse(history);
584
+ _applyChatHistoryCursor(history);
585
+ if (olderMessages.isEmpty) {
586
+ return false;
587
+ }
588
+ final existingKeys = chatMessages.map(_chatEntryKey).toSet();
589
+ final prepended = olderMessages
590
+ .where((entry) => existingKeys.add(_chatEntryKey(entry)))
591
+ .toList(growable: false);
592
+ if (prepended.isEmpty) {
593
+ return false;
594
+ }
595
+ chatMessages = <ChatEntry>[...prepended, ...chatMessages];
596
+ return true;
597
+ } catch (error) {
598
+ errorMessage = _friendlyErrorMessage(error);
599
+ return false;
600
+ } finally {
601
+ isLoadingOlderChatHistory = false;
602
+ notifyListeners();
603
+ }
604
+ }
605
+
503
606
  String _settingString(String key, String fallback, {bool lowercase = false}) {
504
607
  final value = settings[key]?.toString().trim() ?? '';
505
608
  if (value.isEmpty) {
@@ -1561,6 +1664,7 @@ class NeoAgentController extends ChangeNotifier {
1561
1664
  linkedAuthProviders = const <LinkedAuthProviderItem>[];
1562
1665
  settings = const <String, dynamic>{};
1563
1666
  chatMessages = const <ChatEntry>[];
1667
+ _resetChatHistoryPagination();
1564
1668
  agentProfiles = const <AgentProfile>[];
1565
1669
  selectedAgentId = null;
1566
1670
  supportedModels = const <ModelMeta>[];
@@ -1723,6 +1827,7 @@ class NeoAgentController extends ChangeNotifier {
1723
1827
  }
1724
1828
  selectedAgentId = id;
1725
1829
  chatMessages = const <ChatEntry>[];
1830
+ _resetChatHistoryPagination();
1726
1831
  recentRuns = const <RunSummary>[];
1727
1832
  messagingStatuses = const <String, MessagingPlatformStatus>{};
1728
1833
  messagingMessages = const <MessagingMessage>[];
@@ -2045,8 +2150,12 @@ class NeoAgentController extends ChangeNotifier {
2045
2150
 
2046
2151
  final historyFuture = _softRefreshLoad<Map<String, dynamic>>(
2047
2152
  'chat_history',
2048
- _backendClient.fetchChatHistory(backendUrl, agentId: agentId),
2049
- const <String, dynamic>{'messages': <dynamic>[]},
2153
+ _backendClient.fetchChatHistory(
2154
+ backendUrl,
2155
+ agentId: agentId,
2156
+ limit: _chatHistoryPageSize,
2157
+ ),
2158
+ const <String, dynamic>{'messages': <dynamic>[], 'hasMore': false},
2050
2159
  );
2051
2160
  final modelsFuture = _softRefreshLoad<Map<String, dynamic>>(
2052
2161
  'supported_models',
@@ -2214,12 +2323,8 @@ class NeoAgentController extends ChangeNotifier {
2214
2323
  return;
2215
2324
  }
2216
2325
 
2217
- chatMessages = _decodeModelList(
2218
- 'chat_history',
2219
- history['messages'],
2220
- ChatEntry.fromJson,
2221
- fallbackToMapValues: true,
2222
- );
2326
+ chatMessages = _chatHistoryEntriesFromResponse(history);
2327
+ _applyChatHistoryCursor(history);
2223
2328
 
2224
2329
  supportedModels = _decodeModelList(
2225
2330
  'supported_models',
@@ -426,10 +426,23 @@ class BackendClient {
426
426
  Future<Map<String, dynamic>> fetchChatHistory(
427
427
  String baseUrl, {
428
428
  String? agentId,
429
+ int limit = 40,
430
+ String? beforeCreatedAt,
431
+ String? beforeSource,
432
+ String? beforeId,
429
433
  }) async {
434
+ final params = <String>[
435
+ 'limit=$limit',
436
+ if (beforeCreatedAt != null && beforeCreatedAt.trim().isNotEmpty)
437
+ 'beforeCreatedAt=${Uri.encodeQueryComponent(beforeCreatedAt.trim())}',
438
+ if (beforeSource != null && beforeSource.trim().isNotEmpty)
439
+ 'beforeSource=${Uri.encodeQueryComponent(beforeSource.trim())}',
440
+ if (beforeId != null && beforeId.trim().isNotEmpty)
441
+ 'beforeId=${Uri.encodeQueryComponent(beforeId.trim())}',
442
+ ];
430
443
  return getMap(
431
444
  baseUrl,
432
- _withAgentQuery('/api/agents/chat-history?limit=120', agentId),
445
+ _withAgentQuery('/api/agents/chat-history?${params.join('&')}', agentId),
433
446
  );
434
447
  }
435
448
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "3.0.1-beta.14",
3
+ "version": "3.0.1-beta.16",
4
4
  "description": "Self-hosted AI agent for long-running tasks, automation, messaging, device control, and local memory",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -78,8 +78,10 @@ function registerStaticRoutes(app) {
78
78
  app.get(/^\/app(\/.*)?$/, serveFlutterApp);
79
79
 
80
80
  // Landing page at /
81
- app.use(express.static(LANDING_DIR));
82
- app.get('/', (req, res) => res.sendFile(path.join(LANDING_DIR, 'index.html')));
81
+ if (fs.existsSync(path.join(LANDING_DIR, 'index.html'))) {
82
+ app.use(express.static(LANDING_DIR));
83
+ app.get('/', (req, res) => res.sendFile(path.join(LANDING_DIR, 'index.html')));
84
+ }
83
85
  }
84
86
 
85
87
  function serveFlutterApp(req, res) {
@@ -1 +1 @@
1
- db44e89e9ef5572c0d6f5081adf1df9c
1
+ 7a01035360d9803433d2977561ec9305
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19c
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "2032137423" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "2846866146" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });