neoagent 2.3.1-beta.63 → 2.3.1-beta.65

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 (48) hide show
  1. package/docs/capabilities.md +1 -1
  2. package/docs/configuration.md +2 -2
  3. package/flutter_app/lib/main.dart +1 -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 +631 -318
  8. package/flutter_app/lib/main_controller.dart +29 -8
  9. package/flutter_app/lib/main_devices.dart +4 -6
  10. package/flutter_app/lib/main_integrations.dart +15 -7
  11. package/flutter_app/lib/main_models.dart +207 -8
  12. package/flutter_app/lib/main_navigation.dart +36 -18
  13. package/flutter_app/lib/main_operations.dart +162 -91
  14. package/flutter_app/lib/main_settings.dart +273 -78
  15. package/flutter_app/lib/main_shared.dart +8 -6
  16. package/flutter_app/lib/main_unified.dart +388 -0
  17. package/package.json +1 -1
  18. package/server/db/database.js +52 -0
  19. package/server/public/.last_build_id +1 -1
  20. package/server/public/assets/AssetManifest.json +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +79074 -78010
  24. package/server/routes/agents.js +2 -1
  25. package/server/routes/browser.js +1 -14
  26. package/server/routes/memory.js +75 -3
  27. package/server/routes/settings.js +1 -5
  28. package/server/routes/widgets.js +4 -4
  29. package/server/services/ai/capabilityHealth.js +1 -10
  30. package/server/services/ai/deliverables/artifact_helpers.js +190 -0
  31. package/server/services/ai/deliverables/contracts.js +113 -0
  32. package/server/services/ai/deliverables/deliverables.test.js +76 -0
  33. package/server/services/ai/deliverables/index.js +20 -0
  34. package/server/services/ai/deliverables/selector.js +94 -0
  35. package/server/services/ai/deliverables/validator.js +63 -0
  36. package/server/services/ai/deliverables/workflows.js +195 -0
  37. package/server/services/ai/engine.js +259 -1
  38. package/server/services/ai/runEvents.js +100 -0
  39. package/server/services/ai/systemPrompt.js +6 -0
  40. package/server/services/ai/tools.js +1 -5
  41. package/server/services/manager.js +5 -56
  42. package/server/services/memory/manager.js +242 -26
  43. package/server/services/runtime/manager.js +2 -6
  44. package/server/services/runtime/settings.js +6 -12
  45. package/server/services/websocket.js +3 -1
  46. package/server/services/widgets/focus_widget.js +134 -0
  47. package/server/services/widgets/service.js +130 -2
  48. package/server/utils/deployment.js +4 -3
@@ -3778,7 +3778,8 @@ class NeoAgentController extends ChangeNotifier {
3778
3778
  _desktopFloatingToolbarPopupRequested = false;
3779
3779
  unawaited(
3780
3780
  _analytics.trackRecordingStopped(
3781
- kind: recordingRuntime.supportsBackgroundMic &&
3781
+ kind:
3782
+ recordingRuntime.supportsBackgroundMic &&
3782
3783
  !recordingRuntime.supportsScreenAndMic
3783
3784
  ? 'background'
3784
3785
  : 'desktop',
@@ -5571,9 +5572,18 @@ class NeoAgentController extends ChangeNotifier {
5571
5572
  return;
5572
5573
  }
5573
5574
  _pendingChatDraft = normalized;
5574
- setSelectedSection(AppSection.chat);
5575
+ if (!_isMobilePlatform) {
5576
+ setSelectedSection(AppSection.chat);
5577
+ } else {
5578
+ notifyListeners();
5579
+ }
5575
5580
  }
5576
5581
 
5582
+ bool get _isMobilePlatform =>
5583
+ !kIsWeb &&
5584
+ (defaultTargetPlatform == TargetPlatform.android ||
5585
+ defaultTargetPlatform == TargetPlatform.iOS);
5586
+
5577
5587
  String? takePendingChatDraft() {
5578
5588
  final draft = _pendingChatDraft;
5579
5589
  _pendingChatDraft = null;
@@ -5952,7 +5962,7 @@ class NeoAgentController extends ChangeNotifier {
5952
5962
  settings['headless_browser'] != 'false';
5953
5963
 
5954
5964
  String get browserBackend =>
5955
- settings['browser_backend']?.toString().trim().toLowerCase() ?? 'host';
5965
+ settings['browser_backend']?.toString().trim().toLowerCase() ?? 'vm';
5956
5966
 
5957
5967
  String get cloudBrowserBackend {
5958
5968
  final browser = browserBackend;
@@ -5968,11 +5978,10 @@ class NeoAgentController extends ChangeNotifier {
5968
5978
  profile == 'secure-vm') {
5969
5979
  return 'vm';
5970
5980
  }
5971
- if (browser == 'host' || browser == 'vm') {
5972
- return browser;
5973
- }
5981
+ if (browser == 'extension') return 'vm';
5982
+ if (browser == 'vm') return 'vm';
5974
5983
  if (runtime == 'vm') return 'vm';
5975
- return 'host';
5984
+ return 'vm';
5976
5985
  }
5977
5986
 
5978
5987
  bool get browserExtensionConnected =>
@@ -6157,7 +6166,19 @@ class NeoAgentController extends ChangeNotifier {
6157
6166
 
6158
6167
  List<ChatEntry> get visibleChatMessages {
6159
6168
  final entries = <ChatEntry>[...chatMessages];
6160
- if (streamingAssistant.trim().isNotEmpty) {
6169
+ if (activeRun != null && streamingAssistant.trim().isEmpty) {
6170
+ entries.add(
6171
+ ChatEntry(
6172
+ id: '',
6173
+ role: 'assistant',
6174
+ content: '',
6175
+ platform: 'live',
6176
+ createdAt: DateTime.now(),
6177
+ transient: true,
6178
+ typing: true,
6179
+ ),
6180
+ );
6181
+ } else if (streamingAssistant.trim().isNotEmpty) {
6161
6182
  entries.add(
6162
6183
  ChatEntry(
6163
6184
  id: '',
@@ -378,9 +378,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
378
378
  final prefersExtension = controller.browserBackend == 'extension';
379
379
  final extensionConnected = controller.browserExtensionConnected;
380
380
  final usingExtension = prefersExtension && extensionConnected;
381
- final browserFallbackLabel = controller.cloudBrowserBackend == 'vm'
382
- ? 'cloud VM'
383
- : 'local host';
381
+ final browserFallbackLabel = 'cloud browser runtime';
384
382
  final browserPageInfo = browserStatus['pageInfo'] is Map<dynamic, dynamic>
385
383
  ? Map<String, dynamic>.from(browserStatus['pageInfo'] as Map)
386
384
  : const <String, dynamic>{};
@@ -682,9 +680,9 @@ class _DeviceSurfaceHeader extends StatelessWidget {
682
680
  : 'Desktop Companion',
683
681
  };
684
682
  final subtitle = switch (surface) {
685
- _DeviceSurface.browser =>
686
- browserExtensionPreferred && !browserExtensionActive
687
- ? 'No extension device is active. Using the $browserFallbackLabel browser fallback.'
683
+ _DeviceSurface.browser =>
684
+ browserExtensionPreferred && !browserExtensionActive
685
+ ? 'No extension device is active. Using the $browserFallbackLabel.'
688
686
  : (browserPageInfo['url']?.toString() ??
689
687
  'Ready for navigation'),
690
688
  _DeviceSurface.android =>
@@ -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,146 @@ class RunDetailSnapshot {
1469
1475
  steps.where((step) => step.isPlanningRelated).length;
1470
1476
  }
1471
1477
 
1478
+ class ArtifactContractItem {
1479
+ const ArtifactContractItem({
1480
+ required this.kind,
1481
+ required this.path,
1482
+ required this.uri,
1483
+ required this.label,
1484
+ required this.mimeType,
1485
+ required this.size,
1486
+ });
1487
+
1488
+ factory ArtifactContractItem.fromJson(Map<dynamic, dynamic> json) {
1489
+ return ArtifactContractItem(
1490
+ kind: json['kind']?.toString() ?? 'artifact',
1491
+ path: json['path']?.toString() ?? '',
1492
+ uri: json['uri']?.toString() ?? json['url']?.toString() ?? '',
1493
+ label: json['label']?.toString() ?? '',
1494
+ mimeType:
1495
+ json['mimeType']?.toString() ?? json['mime_type']?.toString() ?? '',
1496
+ size: _asInt(json['size'] ?? json['byte_size']),
1497
+ );
1498
+ }
1499
+
1500
+ final String kind;
1501
+ final String path;
1502
+ final String uri;
1503
+ final String label;
1504
+ final String mimeType;
1505
+ final int size;
1506
+
1507
+ String get displayLabel => label.ifEmpty(path.ifEmpty(uri.ifEmpty(kind)));
1508
+ }
1509
+
1510
+ class RunEventItem {
1511
+ const RunEventItem({
1512
+ required this.id,
1513
+ required this.eventType,
1514
+ required this.sequenceIndex,
1515
+ required this.requestId,
1516
+ required this.stepId,
1517
+ required this.payload,
1518
+ required this.createdAt,
1519
+ });
1520
+
1521
+ factory RunEventItem.fromJson(Map<dynamic, dynamic> json) {
1522
+ return RunEventItem(
1523
+ id: _asInt(json['id']),
1524
+ eventType:
1525
+ json['eventType']?.toString().ifEmpty(
1526
+ json['event_type']?.toString() ?? 'event',
1527
+ ) ??
1528
+ 'event',
1529
+ sequenceIndex: _asInt(json['sequenceIndex'] ?? json['sequence_index']),
1530
+ requestId:
1531
+ json['requestId']?.toString() ?? json['request_id']?.toString(),
1532
+ stepId: json['stepId']?.toString() ?? json['step_id']?.toString(),
1533
+ payload: json['payload'] is Map
1534
+ ? Map<String, dynamic>.from(json['payload'] as Map)
1535
+ : (json['payload_json'] is Map
1536
+ ? Map<String, dynamic>.from(json['payload_json'] as Map)
1537
+ : const <String, dynamic>{}),
1538
+ createdAt: _parseOptionalTimestamp(
1539
+ json['createdAt']?.toString() ?? json['created_at']?.toString(),
1540
+ ),
1541
+ );
1542
+ }
1543
+
1544
+ final int id;
1545
+ final String eventType;
1546
+ final int sequenceIndex;
1547
+ final String? requestId;
1548
+ final String? stepId;
1549
+ final Map<String, dynamic> payload;
1550
+ final DateTime? createdAt;
1551
+
1552
+ String get title {
1553
+ switch (eventType) {
1554
+ case 'deliverable_workflow_selected':
1555
+ return 'Deliverable selected';
1556
+ case 'deliverable_execution_started':
1557
+ return 'Deliverable execution started';
1558
+ case 'deliverable_artifact_produced':
1559
+ return 'Deliverable artifact produced';
1560
+ case 'deliverable_validation_started':
1561
+ return 'Deliverable validation started';
1562
+ case 'deliverable_validation_failed':
1563
+ return 'Deliverable validation failed';
1564
+ case 'deliverable_completed':
1565
+ return 'Deliverable completed';
1566
+ case 'run_started':
1567
+ return 'Run started';
1568
+ case 'memory_injected':
1569
+ return 'Memory injected';
1570
+ case 'model_turn_started':
1571
+ return 'Model turn started';
1572
+ case 'model_turn_completed':
1573
+ return 'Model turn completed';
1574
+ case 'tool_started':
1575
+ return 'Tool started';
1576
+ case 'tool_completed':
1577
+ return 'Tool completed';
1578
+ case 'tool_failed':
1579
+ return 'Tool failed';
1580
+ case 'run_completed':
1581
+ return 'Run completed';
1582
+ case 'run_failed':
1583
+ return 'Run failed';
1584
+ case 'run_stopped':
1585
+ return 'Run stopped';
1586
+ default:
1587
+ return _titleCase(eventType.replaceAll('_', ' '));
1588
+ }
1589
+ }
1590
+
1591
+ String get detail {
1592
+ final toolName = payload['toolName']?.toString() ?? '';
1593
+ if (toolName.trim().isNotEmpty) return toolName;
1594
+ final preview =
1595
+ payload['contentPreview']?.toString() ??
1596
+ payload['recallPreview']?.toString() ??
1597
+ '';
1598
+ if (preview.trim().isNotEmpty) return preview;
1599
+ final error = payload['error']?.toString() ?? '';
1600
+ if (error.trim().isNotEmpty) return error;
1601
+ final artifactLabel = payload['artifact'] is Map
1602
+ ? (payload['artifact']['label']?.toString() ??
1603
+ payload['artifact']['path']?.toString() ??
1604
+ payload['artifact']['uri']?.toString() ??
1605
+ '')
1606
+ : '';
1607
+ if (artifactLabel.trim().isNotEmpty) return artifactLabel;
1608
+ final titleValue = payload['title']?.toString() ?? '';
1609
+ return titleValue;
1610
+ }
1611
+
1612
+ String get createdAtLabel =>
1613
+ createdAt == null ? '' : _formatTimestamp(createdAt!);
1614
+
1615
+ bool get isFailure => eventType == 'tool_failed' || eventType == 'run_failed';
1616
+ }
1617
+
1472
1618
  class RunStepItem {
1473
1619
  const RunStepItem({
1474
1620
  required this.id,
@@ -1650,6 +1796,17 @@ dynamic _decodeMaybeJson(dynamic value) {
1650
1796
  return value;
1651
1797
  }
1652
1798
 
1799
+ Map<String, dynamic> _decodeJsonMap(
1800
+ String? value, {
1801
+ Map<String, dynamic> fallback = const <String, dynamic>{},
1802
+ }) {
1803
+ final decoded = _decodeMaybeJson(value);
1804
+ if (decoded is Map) {
1805
+ return Map<String, dynamic>.from(decoded);
1806
+ }
1807
+ return fallback;
1808
+ }
1809
+
1653
1810
  class ChatEntry {
1654
1811
  const ChatEntry({
1655
1812
  required this.id,
@@ -1662,6 +1819,7 @@ class ChatEntry {
1662
1819
  this.metadata = const <String, dynamic>{},
1663
1820
  this.toolCalls = const <Map<String, dynamic>>[],
1664
1821
  this.transient = false,
1822
+ this.typing = false,
1665
1823
  });
1666
1824
 
1667
1825
  factory ChatEntry.fromJson(Map<dynamic, dynamic> json) {
@@ -1691,6 +1849,7 @@ class ChatEntry {
1691
1849
  final List<Map<String, dynamic>> toolCalls;
1692
1850
  final DateTime createdAt;
1693
1851
  final bool transient;
1852
+ final bool typing;
1694
1853
 
1695
1854
  String get createdAtLabel => _formatTimestamp(createdAt);
1696
1855
 
@@ -1964,9 +2123,16 @@ class RunSummary {
1964
2123
  required this.createdAt,
1965
2124
  this.completedAt,
1966
2125
  this.error = '',
2126
+ this.metadata = const <String, dynamic>{},
1967
2127
  });
1968
2128
 
1969
2129
  factory RunSummary.fromJson(Map<dynamic, dynamic> json) {
2130
+ final metadata = _decodeJsonMap(
2131
+ json['metadata_json']?.toString(),
2132
+ fallback: json['metadata'] is Map
2133
+ ? Map<String, dynamic>.from(json['metadata'] as Map)
2134
+ : const <String, dynamic>{},
2135
+ );
1970
2136
  return RunSummary(
1971
2137
  id: json['id']?.toString() ?? '',
1972
2138
  title: json['title']?.toString() ?? 'Untitled',
@@ -1977,6 +2143,7 @@ class RunSummary {
1977
2143
  createdAt: _parseTimestamp(json['created_at']?.toString()),
1978
2144
  completedAt: _parseOptionalTimestamp(json['completed_at']?.toString()),
1979
2145
  error: json['error']?.toString() ?? '',
2146
+ metadata: metadata,
1980
2147
  );
1981
2148
  }
1982
2149
 
@@ -1989,6 +2156,24 @@ class RunSummary {
1989
2156
  final DateTime createdAt;
1990
2157
  final DateTime? completedAt;
1991
2158
  final String error;
2159
+ final Map<String, dynamic> metadata;
2160
+
2161
+ Map<String, dynamic> get deliverable => metadata['deliverable'] is Map
2162
+ ? Map<String, dynamic>.from(metadata['deliverable'] as Map)
2163
+ : const <String, dynamic>{};
2164
+
2165
+ String get deliverableType => deliverable['type']?.toString() ?? '';
2166
+
2167
+ String get deliverableSummary => deliverable['summary']?.toString() ?? '';
2168
+
2169
+ List<ArtifactContractItem> get deliverableArtifacts {
2170
+ final raw = deliverable['artifacts'];
2171
+ if (raw is! List) return const <ArtifactContractItem>[];
2172
+ return raw
2173
+ .whereType<Map>()
2174
+ .map(ArtifactContractItem.fromJson)
2175
+ .toList(growable: false);
2176
+ }
1992
2177
 
1993
2178
  bool get isFailure => status == 'failed' || status == 'error';
1994
2179
 
@@ -2162,7 +2347,7 @@ class UpdateStatusSnapshot {
2162
2347
  deploymentProfile.toLowerCase() == 'prod' ? 'Production' : 'Private';
2163
2348
 
2164
2349
  String get runtimeModeLabel => deploymentProfile.toLowerCase() == 'prod'
2165
- ? 'Per-user isolated VM runtime'
2350
+ ? 'Cloud runtime'
2166
2351
  : 'Trusted host runtime';
2167
2352
 
2168
2353
  String get runtimeValidationLabel =>
@@ -2802,6 +2987,9 @@ class AiWidgetItem {
2802
2987
  required this.userId,
2803
2988
  required this.agentId,
2804
2989
  required this.name,
2990
+ required this.widgetKind,
2991
+ required this.systemKey,
2992
+ required this.isSystem,
2805
2993
  required this.template,
2806
2994
  required this.layoutVariant,
2807
2995
  required this.definition,
@@ -2823,6 +3011,14 @@ class AiWidgetItem {
2823
3011
  userId: _asInt(json['userId'] ?? json['user_id']),
2824
3012
  agentId: json['agentId']?.toString() ?? json['agent_id']?.toString(),
2825
3013
  name: json['name']?.toString().ifEmpty('Widget') ?? 'Widget',
3014
+ widgetKind:
3015
+ json['widgetKind']?.toString().ifEmpty(
3016
+ json['widget_kind']?.toString() ?? 'custom',
3017
+ ) ??
3018
+ 'custom',
3019
+ systemKey:
3020
+ json['systemKey']?.toString() ?? json['system_key']?.toString(),
3021
+ isSystem: json['isSystem'] == true || json['is_system'] == true,
2826
3022
  template: json['template']?.toString().ifEmpty('summary') ?? 'summary',
2827
3023
  layoutVariant:
2828
3024
  json['layoutVariant']?.toString().ifEmpty(
@@ -2876,6 +3072,9 @@ class AiWidgetItem {
2876
3072
  final int userId;
2877
3073
  final String? agentId;
2878
3074
  final String name;
3075
+ final String widgetKind;
3076
+ final String? systemKey;
3077
+ final bool isSystem;
2879
3078
  final String template;
2880
3079
  final String layoutVariant;
2881
3080
  final Map<String, dynamic> definition;
@@ -32,7 +32,7 @@ enum AppSection {
32
32
  health,
33
33
  }
34
34
 
35
- enum SidebarGroup { chat, recordings, activity, automation, settings }
35
+ enum SidebarGroup { chat, recordings, automation, settings }
36
36
 
37
37
  extension SidebarGroupX on SidebarGroup {
38
38
  String get label {
@@ -41,8 +41,6 @@ extension SidebarGroupX on SidebarGroup {
41
41
  return 'Chat';
42
42
  case SidebarGroup.recordings:
43
43
  return 'Recordings';
44
- case SidebarGroup.activity:
45
- return 'Activity';
46
44
  case SidebarGroup.automation:
47
45
  return 'Automation';
48
46
  case SidebarGroup.settings:
@@ -56,8 +54,6 @@ extension SidebarGroupX on SidebarGroup {
56
54
  return Icons.chat_bubble_outline;
57
55
  case SidebarGroup.recordings:
58
56
  return Icons.fiber_smart_record_outlined;
59
- case SidebarGroup.activity:
60
- return Icons.insights_outlined;
61
57
  case SidebarGroup.automation:
62
58
  return Icons.auto_awesome_outlined;
63
59
  case SidebarGroup.settings:
@@ -80,7 +76,7 @@ extension AppSectionX on AppSection {
80
76
  case AppSection.messaging:
81
77
  return 'Messaging';
82
78
  case AppSection.runs:
83
- return 'Runs';
79
+ return 'Runs & Logs';
84
80
  case AppSection.settings:
85
81
  return 'Settings';
86
82
  case AppSection.accountSettings:
@@ -92,7 +88,7 @@ extension AppSectionX on AppSection {
92
88
  case AppSection.agents:
93
89
  return 'Agents';
94
90
  case AppSection.integrations:
95
- return 'Integrations';
91
+ return 'Tools';
96
92
  case AppSection.memory:
97
93
  return 'Memory';
98
94
  case AppSection.tasks:
@@ -119,7 +115,7 @@ extension AppSectionX on AppSection {
119
115
  case AppSection.messaging:
120
116
  return Icons.forum_outlined;
121
117
  case AppSection.runs:
122
- return Icons.history;
118
+ return Icons.monitor_heart_outlined;
123
119
  case AppSection.settings:
124
120
  return Icons.tune;
125
121
  case AppSection.accountSettings:
@@ -131,7 +127,7 @@ extension AppSectionX on AppSection {
131
127
  case AppSection.agents:
132
128
  return Icons.smart_toy_outlined;
133
129
  case AppSection.integrations:
134
- return Icons.integration_instructions_outlined;
130
+ return Icons.handyman_outlined;
135
131
  case AppSection.memory:
136
132
  return Icons.psychology_outlined;
137
133
  case AppSection.tasks:
@@ -152,9 +148,6 @@ extension AppSectionX on AppSection {
152
148
  return SidebarGroup.chat;
153
149
  case AppSection.recordings:
154
150
  return SidebarGroup.recordings;
155
- case AppSection.runs:
156
- case AppSection.logs:
157
- return SidebarGroup.activity;
158
151
  case AppSection.devices:
159
152
  case AppSection.skills:
160
153
  case AppSection.integrations:
@@ -164,6 +157,8 @@ extension AppSectionX on AppSection {
164
157
  case AppSection.mcp:
165
158
  case AppSection.health:
166
159
  return SidebarGroup.automation;
160
+ case AppSection.runs:
161
+ case AppSection.logs:
167
162
  case AppSection.settings:
168
163
  case AppSection.accountSettings:
169
164
  case AppSection.messaging:
@@ -172,17 +167,40 @@ extension AppSectionX on AppSection {
172
167
  }
173
168
  }
174
169
 
170
+ AppSection get canonicalSection {
171
+ switch (this) {
172
+ case AppSection.logs:
173
+ return AppSection.runs;
174
+ case AppSection.skills:
175
+ case AppSection.mcp:
176
+ return AppSection.integrations;
177
+ default:
178
+ return this;
179
+ }
180
+ }
181
+
182
+ AppSection get sidebarSection {
183
+ switch (this) {
184
+ case AppSection.accountSettings:
185
+ return AppSection.settings;
186
+ default:
187
+ return canonicalSection;
188
+ }
189
+ }
190
+
175
191
  String get navigationTitle {
176
- final groupLabel = group.label;
177
- if (this == AppSection.voiceAssistant) {
178
- return label;
192
+ final effectiveSection = canonicalSection;
193
+ final groupLabel = effectiveSection.group.label;
194
+ if (effectiveSection == AppSection.voiceAssistant) {
195
+ return effectiveSection.label;
179
196
  }
180
- if (group == SidebarGroup.chat || group == SidebarGroup.recordings) {
197
+ if (effectiveSection.group == SidebarGroup.chat ||
198
+ effectiveSection.group == SidebarGroup.recordings) {
181
199
  return groupLabel;
182
200
  }
183
- if (groupLabel == label) {
201
+ if (groupLabel == effectiveSection.label) {
184
202
  return groupLabel;
185
203
  }
186
- return '$groupLabel · $label';
204
+ return '$groupLabel · ${effectiveSection.label}';
187
205
  }
188
206
  }