neoagent 2.3.1-beta.62 → 2.3.1-beta.64

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 (45) hide show
  1. package/.env.example +9 -0
  2. package/docs/configuration.md +11 -0
  3. package/flutter_app/lib/main.dart +2 -0
  4. package/flutter_app/lib/main_account_settings.dart +50 -22
  5. package/flutter_app/lib/main_admin.dart +24 -10
  6. package/flutter_app/lib/main_app_shell.dart +10 -9
  7. package/flutter_app/lib/main_chat.dart +433 -309
  8. package/flutter_app/lib/main_controller.dart +164 -6
  9. package/flutter_app/lib/main_integrations.dart +15 -7
  10. package/flutter_app/lib/main_models.dart +116 -7
  11. package/flutter_app/lib/main_navigation.dart +27 -18
  12. package/flutter_app/lib/main_operations.dart +162 -91
  13. package/flutter_app/lib/main_runtime.dart +22 -0
  14. package/flutter_app/lib/main_settings.dart +287 -75
  15. package/flutter_app/lib/main_shared.dart +165 -6
  16. package/flutter_app/lib/main_unified.dart +388 -0
  17. package/flutter_app/lib/src/analytics_service.dart +294 -0
  18. package/flutter_app/lib/src/backend_client.dart +4 -0
  19. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
  20. package/flutter_app/pubspec.lock +8 -0
  21. package/flutter_app/pubspec.yaml +1 -0
  22. package/flutter_app/web/index.html +1 -0
  23. package/package.json +1 -1
  24. package/server/config/analytics.js +30 -0
  25. package/server/db/database.js +52 -0
  26. package/server/http/routes.js +1 -0
  27. package/server/public/.last_build_id +1 -1
  28. package/server/public/assets/AssetManifest.bin +1 -1
  29. package/server/public/assets/AssetManifest.bin.json +1 -1
  30. package/server/public/assets/NOTICES +183 -0
  31. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  32. package/server/public/assets/packages/mixpanel_flutter/assets/mixpanel.js +3 -0
  33. package/server/public/flutter_bootstrap.js +1 -1
  34. package/server/public/index.html +1 -0
  35. package/server/public/main.dart.js +83685 -82046
  36. package/server/routes/agents.js +2 -1
  37. package/server/routes/memory.js +75 -3
  38. package/server/routes/runtime.js +14 -0
  39. package/server/routes/widgets.js +4 -4
  40. package/server/services/ai/engine.js +86 -0
  41. package/server/services/ai/runEvents.js +100 -0
  42. package/server/services/memory/manager.js +242 -26
  43. package/server/services/websocket.js +3 -1
  44. package/server/services/widgets/focus_widget.js +126 -0
  45. package/server/services/widgets/service.js +130 -2
@@ -37,6 +37,7 @@ class NeoAgentController extends ChangeNotifier {
37
37
  final OAuthLauncher _oauthLauncher;
38
38
  final app_release_updater.AppReleaseUpdater _appReleaseUpdater =
39
39
  app_release_updater.AppReleaseUpdater();
40
+ final AppAnalytics _analytics = AppAnalytics();
40
41
  final LiveVoiceCapture _liveVoiceCapture = LiveVoiceCapture();
41
42
  final DesktopScreenCapture _desktopScreenCapture =
42
43
  createDesktopScreenCapture();
@@ -98,6 +99,10 @@ class NeoAgentController extends ChangeNotifier {
98
99
  bool networkStatusKnown = false;
99
100
  bool _desktopFloatingToolbarPopupRequested = false;
100
101
  bool _voiceAssistantIncludeScreenContext = false;
102
+ bool _didTrackAppOpen = false;
103
+ bool _analyticsConfigured = false;
104
+ bool _analyticsConsentResolved = false;
105
+ bool _analyticsConsentGranted = false;
101
106
 
102
107
  bool hasUser = true;
103
108
  bool registrationOpen = false;
@@ -319,6 +324,7 @@ class NeoAgentController extends ChangeNotifier {
319
324
  _diagnosticLogSubscription?.cancel();
320
325
  _connectivitySubscription?.cancel();
321
326
  _appReleaseUpdater.dispose();
327
+ unawaited(_analytics.dispose());
322
328
  _desktopCompanion.removeListener(notifyListeners);
323
329
  unawaited(_desktopCompanion.disconnect());
324
330
  _recordingBridge.removeListener(_handleRecordingBridgeChanged);
@@ -349,6 +355,12 @@ class NeoAgentController extends ChangeNotifier {
349
355
  bool get canCaptureVoiceAssistantScreenContext =>
350
356
  _desktopScreenCapture.isSupported;
351
357
 
358
+ bool get showAnalyticsConsentBanner =>
359
+ kIsWeb && _analyticsConfigured && !_analyticsConsentResolved;
360
+
361
+ bool get analyticsConsentGranted =>
362
+ _analyticsConsentResolved && _analyticsConsentGranted;
363
+
352
364
  bool get isLiveVoiceCaptureEngaged =>
353
365
  _isStartingLiveVoice || _liveVoiceCaptureActive;
354
366
 
@@ -701,6 +713,7 @@ class NeoAgentController extends ChangeNotifier {
701
713
  appUpdateAutoCheckEnabled =
702
714
  _prefs?.getBool('app.update.autoCheckEnabled') ?? true;
703
715
  installedAppVersion = await _safeLoadInstalledAppVersion();
716
+ await _initializeAnalytics();
704
717
  await refreshConnectivityStatus();
705
718
  if (_connectivityPluginAvailable && _connectivitySubscription == null) {
706
719
  try {
@@ -862,6 +875,101 @@ class NeoAgentController extends ChangeNotifier {
862
875
  }
863
876
  }
864
877
 
878
+ Future<void> _initializeAnalytics() async {
879
+ try {
880
+ final runtimeConfig = await _backendClient.fetchRuntimeConfig(backendUrl);
881
+ final analyticsConfig = runtimeConfig['analytics'];
882
+ final mixpanelConfig = analyticsConfig is Map
883
+ ? analyticsConfig['mixpanel']
884
+ : null;
885
+ final token = mixpanelConfig is Map
886
+ ? mixpanelConfig['token']?.toString()
887
+ : null;
888
+ _analyticsConfigured = token != null && token.trim().isNotEmpty;
889
+ final consentState = _prefs?.getBool('analytics.cookieConsent');
890
+ _analyticsConsentResolved = consentState != null;
891
+ _analyticsConsentGranted = consentState == true;
892
+ await _analytics.initialize(
893
+ token: token,
894
+ consentGranted: _analyticsConsentGranted,
895
+ );
896
+ _trackAppOpenedIfNeeded();
897
+ } catch (_) {
898
+ _analyticsConfigured = false;
899
+ _analyticsConsentResolved = false;
900
+ _analyticsConsentGranted = false;
901
+ await _analytics.initialize(token: null, consentGranted: false);
902
+ }
903
+ }
904
+
905
+ void _trackAppOpenedIfNeeded({String? consentSource}) {
906
+ if (!_analytics.enabled || _didTrackAppOpen) {
907
+ return;
908
+ }
909
+ _didTrackAppOpen = true;
910
+ unawaited(
911
+ _analytics.trackAppOpened(
912
+ appMode: appMode.name,
913
+ platform: _analyticsPlatformLabel(),
914
+ backendMode: backendUrl.trim().isEmpty ? 'same_origin' : 'custom',
915
+ selectedSection: selectedSection.label,
916
+ deploymentProfile: deploymentProfile,
917
+ authenticated: isAuthenticated,
918
+ ),
919
+ );
920
+ if (consentSource != null) {
921
+ unawaited(
922
+ _analytics.track(
923
+ 'analytics_consent_granted',
924
+ properties: <String, Object?>{'consent_source': consentSource},
925
+ ),
926
+ );
927
+ }
928
+ }
929
+
930
+ String _analyticsPlatformLabel() {
931
+ if (kIsWeb) {
932
+ return 'web';
933
+ }
934
+ switch (defaultTargetPlatform) {
935
+ case TargetPlatform.android:
936
+ return 'android';
937
+ case TargetPlatform.iOS:
938
+ return 'ios';
939
+ case TargetPlatform.macOS:
940
+ return 'macos';
941
+ case TargetPlatform.windows:
942
+ return 'windows';
943
+ case TargetPlatform.linux:
944
+ return 'linux';
945
+ case TargetPlatform.fuchsia:
946
+ return 'fuchsia';
947
+ }
948
+ }
949
+
950
+ Future<void> acceptAnalyticsConsent() async {
951
+ if (!_analyticsConfigured) {
952
+ return;
953
+ }
954
+ _analyticsConsentResolved = true;
955
+ _analyticsConsentGranted = true;
956
+ await _prefs?.setBool('analytics.cookieConsent', true);
957
+ await _analytics.setConsentGranted(true);
958
+ _trackAppOpenedIfNeeded(consentSource: 'banner');
959
+ notifyListeners();
960
+ }
961
+
962
+ Future<void> declineAnalyticsConsent() async {
963
+ if (!_analyticsConfigured) {
964
+ return;
965
+ }
966
+ _analyticsConsentResolved = true;
967
+ _analyticsConsentGranted = false;
968
+ await _prefs?.setBool('analytics.cookieConsent', false);
969
+ await _analytics.setConsentGranted(false);
970
+ notifyListeners();
971
+ }
972
+
865
973
  Future<void> setAppUpdateChannel(String channel) async {
866
974
  final normalized = channel.trim().toLowerCase() == 'beta'
867
975
  ? 'beta'
@@ -886,6 +994,7 @@ class NeoAgentController extends ChangeNotifier {
886
994
  if (isCheckingAppUpdate) {
887
995
  return;
888
996
  }
997
+ unawaited(_analytics.trackAppUpdateCheck(silent: silent));
889
998
  if (!appUpdaterConfigured) {
890
999
  appUpdateErrorMessage = kIsWeb
891
1000
  ? 'Client app update checks are unavailable in the web app.'
@@ -975,6 +1084,11 @@ class NeoAgentController extends ChangeNotifier {
975
1084
  isBooting = true;
976
1085
  notifyListeners();
977
1086
  await bootstrap();
1087
+ unawaited(
1088
+ _analytics.trackBackendUrlSaved(
1089
+ backendMode: backendUrl.trim().isEmpty ? 'same_origin' : 'custom',
1090
+ ),
1091
+ );
978
1092
  return true;
979
1093
  } catch (error) {
980
1094
  errorMessage = _friendlyErrorMessage(error);
@@ -1147,6 +1261,7 @@ class NeoAgentController extends ChangeNotifier {
1147
1261
  response,
1148
1262
  retentionErrorMessage:
1149
1263
  'QR login completed, but NeoAgent could not keep the session. Please try again.',
1264
+ authMethod: 'qr',
1150
1265
  );
1151
1266
  } catch (error) {
1152
1267
  final message = _friendlyErrorMessage(error);
@@ -1199,6 +1314,7 @@ class NeoAgentController extends ChangeNotifier {
1199
1314
  String? fallbackUsername,
1200
1315
  String? retentionErrorMessage,
1201
1316
  bool isRegistration = false,
1317
+ String authMethod = 'password',
1202
1318
  }) async {
1203
1319
  user = Map<String, dynamic>.from(
1204
1320
  response['user'] as Map<dynamic, dynamic>? ??
@@ -1220,6 +1336,14 @@ class NeoAgentController extends ChangeNotifier {
1220
1336
  _clearQrLoginChallenge();
1221
1337
  await _persistCredentials();
1222
1338
  await refresh();
1339
+ if (isAuthenticated) {
1340
+ unawaited(
1341
+ _analytics.trackSignedIn(
1342
+ authMethod: authMethod,
1343
+ isRegistration: isRegistration,
1344
+ ),
1345
+ );
1346
+ }
1223
1347
  if (!isAuthenticated && retentionErrorMessage != null) {
1224
1348
  errorMessage = retentionErrorMessage;
1225
1349
  }
@@ -1289,6 +1413,7 @@ class NeoAgentController extends ChangeNotifier {
1289
1413
  await _completeAuthenticatedResponse(
1290
1414
  response,
1291
1415
  isRegistration: register,
1416
+ authMethod: 'oauth',
1292
1417
  retentionErrorMessage:
1293
1418
  'Sign-in completed, but NeoAgent could not keep the browser session. Please sign in again. If this keeps happening, check backend session cookie settings.',
1294
1419
  );
@@ -1315,6 +1440,7 @@ class NeoAgentController extends ChangeNotifier {
1315
1440
  await _completeAuthenticatedResponse(
1316
1441
  response,
1317
1442
  fallbackUsername: pendingTwoFactorUsername,
1443
+ authMethod: 'two_factor',
1318
1444
  retentionErrorMessage:
1319
1445
  'Two-factor sign-in completed, but NeoAgent could not keep the browser session. Please sign in again.',
1320
1446
  );
@@ -1405,6 +1531,7 @@ class NeoAgentController extends ChangeNotifier {
1405
1531
  response,
1406
1532
  fallbackUsername: username,
1407
1533
  isRegistration: register,
1534
+ authMethod: 'password',
1408
1535
  retentionErrorMessage:
1409
1536
  'Sign-in completed, but NeoAgent could not keep the browser session. Please sign in again. If this keeps happening, the backend session cookie is likely not being retained.',
1410
1537
  );
@@ -1435,6 +1562,7 @@ class NeoAgentController extends ChangeNotifier {
1435
1562
  final logoutFuture = _backendClient.logout(backendUrl);
1436
1563
  _authCycle += 1;
1437
1564
  _clearAuthenticatedState();
1565
+ unawaited(_analytics.trackSignedOut());
1438
1566
  isAuthenticating = true;
1439
1567
  notifyListeners();
1440
1568
 
@@ -1451,6 +1579,7 @@ class NeoAgentController extends ChangeNotifier {
1451
1579
  notifyListeners();
1452
1580
  try {
1453
1581
  await _backendClient.completeOnboarding(backendUrl);
1582
+ unawaited(_analytics.trackOnboardingDismissed());
1454
1583
  if (isAuthenticated && user != null) {
1455
1584
  user!['hasCompletedOnboarding'] = true;
1456
1585
  }
@@ -1604,6 +1733,7 @@ class NeoAgentController extends ChangeNotifier {
1604
1733
  }
1605
1734
 
1606
1735
  void setSelectedSection(AppSection section) {
1736
+ final previousSection = selectedSection;
1607
1737
  selectedSection = section;
1608
1738
  unawaited(_prefs?.setString(_selectedSectionPrefsKey, section.name));
1609
1739
  if (section == AppSection.devices) {
@@ -1612,6 +1742,14 @@ class NeoAgentController extends ChangeNotifier {
1612
1742
  if (section == AppSection.accountSettings) {
1613
1743
  unawaited(refreshAccountSettings());
1614
1744
  }
1745
+ if (previousSection != section) {
1746
+ unawaited(
1747
+ _analytics.trackSectionChanged(
1748
+ section: section.label,
1749
+ previousSection: previousSection.label,
1750
+ ),
1751
+ );
1752
+ }
1615
1753
  notifyListeners();
1616
1754
  }
1617
1755
 
@@ -2422,6 +2560,9 @@ class NeoAgentController extends ChangeNotifier {
2422
2560
  }
2423
2561
 
2424
2562
  Future<void> refreshWidgets({bool all = false}) async {
2563
+ if (all) {
2564
+ unawaited(_analytics.trackWidgetRefreshRequested(all: all));
2565
+ }
2425
2566
  widgets = _decodeModelList(
2426
2567
  'widgets',
2427
2568
  await _backendClient.fetchWidgets(
@@ -3224,6 +3365,7 @@ class NeoAgentController extends ChangeNotifier {
3224
3365
  }
3225
3366
 
3226
3367
  Future<void> startBackgroundRecording() async {
3368
+ unawaited(_analytics.trackRecordingStarted(kind: 'background'));
3227
3369
  await _startRecordingCapture(
3228
3370
  logKey: 'start_background',
3229
3371
  payload: buildAndroidBackgroundRecordingPayload(),
@@ -3250,6 +3392,7 @@ class NeoAgentController extends ChangeNotifier {
3250
3392
  }
3251
3393
  return;
3252
3394
  }
3395
+ unawaited(_analytics.trackRecordingStarted(kind: 'desktop'));
3253
3396
  await _startRecordingCapture(
3254
3397
  logKey: 'start_desktop',
3255
3398
  payload: buildDesktopRecordingPayload(),
@@ -3633,6 +3776,16 @@ class NeoAgentController extends ChangeNotifier {
3633
3776
  notifyListeners();
3634
3777
  try {
3635
3778
  _desktopFloatingToolbarPopupRequested = false;
3779
+ unawaited(
3780
+ _analytics.trackRecordingStopped(
3781
+ kind:
3782
+ recordingRuntime.supportsBackgroundMic &&
3783
+ !recordingRuntime.supportsScreenAndMic
3784
+ ? 'background'
3785
+ : 'desktop',
3786
+ stopReason: stopReason,
3787
+ ),
3788
+ );
3636
3789
  await _recordingBridge.stopActiveRecording();
3637
3790
  if (isAndroidBackgroundStop) {
3638
3791
  await Future<void>.delayed(const Duration(milliseconds: 600));
@@ -4261,6 +4414,12 @@ class NeoAgentController extends ChangeNotifier {
4261
4414
  if (trimmed.isEmpty || (isSendingMessage && !canSteerLiveRun)) {
4262
4415
  return;
4263
4416
  }
4417
+ unawaited(
4418
+ _analytics.trackChatMessageSent(
4419
+ length: trimmed.length,
4420
+ steeringLiveRun: canSteerLiveRun,
4421
+ ),
4422
+ );
4264
4423
 
4265
4424
  final optimistic = ChatEntry(
4266
4425
  id: '',
@@ -4682,6 +4841,7 @@ class NeoAgentController extends ChangeNotifier {
4682
4841
  errorMessage = null;
4683
4842
  notifyListeners();
4684
4843
  try {
4844
+ unawaited(_analytics.trackAppUpdateTriggered());
4685
4845
  await _backendClient.triggerUpdate(backendUrl);
4686
4846
  await refreshUpdateStatus();
4687
4847
  } catch (error) {
@@ -5412,8 +5572,7 @@ class NeoAgentController extends ChangeNotifier {
5412
5572
  return;
5413
5573
  }
5414
5574
  _pendingChatDraft = normalized;
5415
- selectedSection = AppSection.chat;
5416
- notifyListeners();
5575
+ setSelectedSection(AppSection.chat);
5417
5576
  }
5418
5577
 
5419
5578
  String? takePendingChatDraft() {
@@ -5432,17 +5591,15 @@ class NeoAgentController extends ChangeNotifier {
5432
5591
 
5433
5592
  void openWidgetSurface(String widgetId) {
5434
5593
  final normalized = widgetId.trim();
5435
- selectedSection = AppSection.widgets;
5594
+ setSelectedSection(AppSection.widgets);
5436
5595
  if (normalized.isNotEmpty) {
5437
5596
  _selectedWidgetId = normalized;
5438
5597
  unawaited(refreshWidgets(all: true));
5439
5598
  }
5440
- notifyListeners();
5441
5599
  }
5442
5600
 
5443
5601
  void openVoiceAssistantSurface() {
5444
- selectedSection = AppSection.voiceAssistant;
5445
- notifyListeners();
5602
+ setSelectedSection(AppSection.voiceAssistant);
5446
5603
  }
5447
5604
 
5448
5605
  String? get selectedWidgetId => _selectedWidgetId;
@@ -5475,6 +5632,7 @@ class NeoAgentController extends ChangeNotifier {
5475
5632
  return;
5476
5633
  }
5477
5634
  _startManualRunCooldown('task', '$id');
5635
+ unawaited(_analytics.trackTaskRunRequested(taskId: id));
5478
5636
  await _backendClient.runSavedTask(backendUrl, id);
5479
5637
  await refreshTasks();
5480
5638
  await refreshRunsOnly();
@@ -1,26 +1,34 @@
1
1
  part of 'main.dart';
2
2
 
3
3
  class IntegrationsPanel extends StatelessWidget {
4
- const IntegrationsPanel({super.key, required this.controller});
4
+ const IntegrationsPanel({
5
+ super.key,
6
+ required this.controller,
7
+ this.embedded = false,
8
+ });
5
9
 
6
10
  final NeoAgentController controller;
11
+ final bool embedded;
7
12
 
8
13
  @override
9
14
  Widget build(BuildContext context) {
10
- return Padding(
11
- padding: _pagePadding(context),
12
- child: Column(
13
- children: <Widget>[
15
+ final body = Column(
16
+ children: <Widget>[
17
+ if (!embedded) ...<Widget>[
14
18
  _PageTitle(
15
19
  title: 'Integrations',
16
20
  subtitle:
17
21
  'Connect and manage official integrations separately from reusable skills.',
18
22
  ),
19
23
  const SizedBox(height: 12),
20
- Expanded(child: OfficialIntegrationsTab(controller: controller)),
21
24
  ],
22
- ),
25
+ Expanded(child: OfficialIntegrationsTab(controller: controller)),
26
+ ],
23
27
  );
28
+ if (embedded) {
29
+ return body;
30
+ }
31
+ return Padding(padding: _pagePadding(context), child: body);
24
32
  }
25
33
  }
26
34
 
@@ -832,7 +832,7 @@ class MessagingAccessCapabilities {
832
832
  });
833
833
 
834
834
  factory MessagingAccessCapabilities.fromJson(Map<String, dynamic> json) {
835
- List<String> _stringList(dynamic value) {
835
+ List<String> stringList(dynamic value) {
836
836
  if (value is! List) return const <String>[];
837
837
  return value
838
838
  .map((item) => item.toString())
@@ -845,9 +845,9 @@ class MessagingAccessCapabilities {
845
845
  supportsSharedPolicy: json['supportsSharedPolicy'] != false,
846
846
  supportsMentionGate: json['supportsMentionGate'] == true,
847
847
  supportsDiscovery: json['supportsDiscovery'] == true,
848
- directRuleScopes: _stringList(json['directRuleScopes']),
849
- sharedSpaceRuleScopes: _stringList(json['sharedSpaceRuleScopes']),
850
- sharedActorRuleScopes: _stringList(json['sharedActorRuleScopes']),
848
+ directRuleScopes: stringList(json['directRuleScopes']),
849
+ sharedSpaceRuleScopes: stringList(json['sharedSpaceRuleScopes']),
850
+ sharedActorRuleScopes: stringList(json['sharedActorRuleScopes']),
851
851
  manualEntryHint: json['manualEntryHint']?.toString() ?? '',
852
852
  );
853
853
  }
@@ -932,7 +932,7 @@ class MessagingAccessCatalog {
932
932
  String platform,
933
933
  Map<String, dynamic> json,
934
934
  ) {
935
- List<MessagingAccessTarget> _targets(dynamic raw) {
935
+ List<MessagingAccessTarget> parseTargets(dynamic raw) {
936
936
  if (raw is! List) return const <MessagingAccessTarget>[];
937
937
  return raw
938
938
  .whereType<Map>()
@@ -950,8 +950,8 @@ class MessagingAccessCatalog {
950
950
  capabilities: MessagingAccessCapabilities.fromJson(
951
951
  _jsonMap(json['capabilities']),
952
952
  ),
953
- discoveredTargets: _targets(json['discoveredTargets']),
954
- suggestedTargets: _targets(json['suggestedTargets']),
953
+ discoveredTargets: parseTargets(json['discoveredTargets']),
954
+ suggestedTargets: parseTargets(json['suggestedTargets']),
955
955
  summary: json['summary']?.toString() ?? 'Access policy',
956
956
  );
957
957
  }
@@ -1434,6 +1434,7 @@ class RunDetailSnapshot {
1434
1434
  const RunDetailSnapshot({
1435
1435
  required this.run,
1436
1436
  required this.steps,
1437
+ required this.events,
1437
1438
  required this.response,
1438
1439
  });
1439
1440
 
@@ -1444,12 +1445,17 @@ class RunDetailSnapshot {
1444
1445
  json['steps'],
1445
1446
  fallbackToMapValues: true,
1446
1447
  ).map(RunStepItem.fromJson).toList(),
1448
+ events: _jsonMapList(
1449
+ json['events'],
1450
+ fallbackToMapValues: true,
1451
+ ).map(RunEventItem.fromJson).toList(),
1447
1452
  response: json['response']?.toString() ?? '',
1448
1453
  );
1449
1454
  }
1450
1455
 
1451
1456
  final RunSummary run;
1452
1457
  final List<RunStepItem> steps;
1458
+ final List<RunEventItem> events;
1453
1459
  final String response;
1454
1460
 
1455
1461
  int get completedTools => steps
@@ -1469,6 +1475,95 @@ class RunDetailSnapshot {
1469
1475
  steps.where((step) => step.isPlanningRelated).length;
1470
1476
  }
1471
1477
 
1478
+ class RunEventItem {
1479
+ const RunEventItem({
1480
+ required this.id,
1481
+ required this.eventType,
1482
+ required this.sequenceIndex,
1483
+ required this.requestId,
1484
+ required this.stepId,
1485
+ required this.payload,
1486
+ required this.createdAt,
1487
+ });
1488
+
1489
+ factory RunEventItem.fromJson(Map<dynamic, dynamic> json) {
1490
+ return RunEventItem(
1491
+ id: _asInt(json['id']),
1492
+ eventType:
1493
+ json['eventType']?.toString().ifEmpty(
1494
+ json['event_type']?.toString() ?? 'event',
1495
+ ) ??
1496
+ 'event',
1497
+ sequenceIndex: _asInt(json['sequenceIndex'] ?? json['sequence_index']),
1498
+ requestId:
1499
+ json['requestId']?.toString() ?? json['request_id']?.toString(),
1500
+ stepId: json['stepId']?.toString() ?? json['step_id']?.toString(),
1501
+ payload: json['payload'] is Map
1502
+ ? Map<String, dynamic>.from(json['payload'] as Map)
1503
+ : (json['payload_json'] is Map
1504
+ ? Map<String, dynamic>.from(json['payload_json'] as Map)
1505
+ : const <String, dynamic>{}),
1506
+ createdAt: _parseOptionalTimestamp(
1507
+ json['createdAt']?.toString() ?? json['created_at']?.toString(),
1508
+ ),
1509
+ );
1510
+ }
1511
+
1512
+ final int id;
1513
+ final String eventType;
1514
+ final int sequenceIndex;
1515
+ final String? requestId;
1516
+ final String? stepId;
1517
+ final Map<String, dynamic> payload;
1518
+ final DateTime? createdAt;
1519
+
1520
+ String get title {
1521
+ switch (eventType) {
1522
+ case 'run_started':
1523
+ return 'Run started';
1524
+ case 'memory_injected':
1525
+ return 'Memory injected';
1526
+ case 'model_turn_started':
1527
+ return 'Model turn started';
1528
+ case 'model_turn_completed':
1529
+ return 'Model turn completed';
1530
+ case 'tool_started':
1531
+ return 'Tool started';
1532
+ case 'tool_completed':
1533
+ return 'Tool completed';
1534
+ case 'tool_failed':
1535
+ return 'Tool failed';
1536
+ case 'run_completed':
1537
+ return 'Run completed';
1538
+ case 'run_failed':
1539
+ return 'Run failed';
1540
+ case 'run_stopped':
1541
+ return 'Run stopped';
1542
+ default:
1543
+ return _titleCase(eventType.replaceAll('_', ' '));
1544
+ }
1545
+ }
1546
+
1547
+ String get detail {
1548
+ final toolName = payload['toolName']?.toString() ?? '';
1549
+ if (toolName.trim().isNotEmpty) return toolName;
1550
+ final preview =
1551
+ payload['contentPreview']?.toString() ??
1552
+ payload['recallPreview']?.toString() ??
1553
+ '';
1554
+ if (preview.trim().isNotEmpty) return preview;
1555
+ final error = payload['error']?.toString() ?? '';
1556
+ if (error.trim().isNotEmpty) return error;
1557
+ final titleValue = payload['title']?.toString() ?? '';
1558
+ return titleValue;
1559
+ }
1560
+
1561
+ String get createdAtLabel =>
1562
+ createdAt == null ? '' : _formatTimestamp(createdAt!);
1563
+
1564
+ bool get isFailure => eventType == 'tool_failed' || eventType == 'run_failed';
1565
+ }
1566
+
1472
1567
  class RunStepItem {
1473
1568
  const RunStepItem({
1474
1569
  required this.id,
@@ -2802,6 +2897,9 @@ class AiWidgetItem {
2802
2897
  required this.userId,
2803
2898
  required this.agentId,
2804
2899
  required this.name,
2900
+ required this.widgetKind,
2901
+ required this.systemKey,
2902
+ required this.isSystem,
2805
2903
  required this.template,
2806
2904
  required this.layoutVariant,
2807
2905
  required this.definition,
@@ -2823,6 +2921,14 @@ class AiWidgetItem {
2823
2921
  userId: _asInt(json['userId'] ?? json['user_id']),
2824
2922
  agentId: json['agentId']?.toString() ?? json['agent_id']?.toString(),
2825
2923
  name: json['name']?.toString().ifEmpty('Widget') ?? 'Widget',
2924
+ widgetKind:
2925
+ json['widgetKind']?.toString().ifEmpty(
2926
+ json['widget_kind']?.toString() ?? 'custom',
2927
+ ) ??
2928
+ 'custom',
2929
+ systemKey:
2930
+ json['systemKey']?.toString() ?? json['system_key']?.toString(),
2931
+ isSystem: json['isSystem'] == true || json['is_system'] == true,
2826
2932
  template: json['template']?.toString().ifEmpty('summary') ?? 'summary',
2827
2933
  layoutVariant:
2828
2934
  json['layoutVariant']?.toString().ifEmpty(
@@ -2876,6 +2982,9 @@ class AiWidgetItem {
2876
2982
  final int userId;
2877
2983
  final String? agentId;
2878
2984
  final String name;
2985
+ final String widgetKind;
2986
+ final String? systemKey;
2987
+ final bool isSystem;
2879
2988
  final String template;
2880
2989
  final String layoutVariant;
2881
2990
  final Map<String, dynamic> definition;