neoagent 2.4.1-beta.44 → 2.4.1-beta.45

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.
@@ -4,6 +4,12 @@ NeoAgent reads server config from `~/.neoagent/.env`. Run `neoagent setup` to ge
4
4
 
5
5
  All AI provider credentials, OAuth client secrets, and deployment settings are server-side only — never sent to the client or exposed in the UI.
6
6
 
7
+ ## Admin Dashboard
8
+
9
+ The admin dashboard at `/admin` provides a web UI for operator tasks including AI provider key management, server logs, and runtime updates. Credentials are generated during `neoagent setup` (or run `neoagent admin` to view them).
10
+
11
+ Navigate to **Providers** in the sidebar to set or rotate API keys without editing `.env` manually — changes take effect immediately without a server restart.
12
+
7
13
  ## Minimal Config
8
14
 
9
15
  ```dotenv
@@ -2176,8 +2176,6 @@ class _SectionBody extends StatelessWidget {
2176
2176
  return SettingsWorkspacePanel(controller: controller);
2177
2177
  case AppSection.accountSettings:
2178
2178
  return SettingsWorkspacePanel(controller: controller);
2179
- case AppSection.logs:
2180
- return RunsAndLogsPanel(controller: controller);
2181
2179
  case AppSection.skills:
2182
2180
  return ToolsPanel(controller: controller);
2183
2181
  case AppSection.agents:
@@ -428,6 +428,7 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
428
428
  child: _ChatBubble(
429
429
  entry: entry,
430
430
  onLoadRunDetail: controller.fetchRunDetail,
431
+ onSendMessage: controller.sendMessage,
431
432
  ),
432
433
  ),
433
434
  ),
@@ -458,6 +458,7 @@ class NeoAgentController extends ChangeNotifier {
458
458
  required String role,
459
459
  required String platform,
460
460
  bool transient = false,
461
+ Map<String, dynamic> metadata = const <String, dynamic>{},
461
462
  }) {
462
463
  final trimmed = content.trim();
463
464
  if (trimmed.isEmpty) {
@@ -468,7 +469,8 @@ class NeoAgentController extends ChangeNotifier {
468
469
  if (previous != null &&
469
470
  previous.role == role &&
470
471
  previous.platform == platform &&
471
- previous.content.trim() == trimmed) {
472
+ previous.content.trim() == trimmed &&
473
+ metadata.isEmpty) {
472
474
  return;
473
475
  }
474
476
 
@@ -481,6 +483,7 @@ class NeoAgentController extends ChangeNotifier {
481
483
  platform: platform,
482
484
  createdAt: DateTime.now(),
483
485
  transient: transient,
486
+ metadata: metadata,
484
487
  ),
485
488
  ];
486
489
  }
@@ -4633,12 +4636,14 @@ class NeoAgentController extends ChangeNotifier {
4633
4636
  String content, {
4634
4637
  required String platform,
4635
4638
  bool transient = false,
4639
+ Map<String, dynamic> metadata = const <String, dynamic>{},
4636
4640
  }) {
4637
4641
  _appendChatMessage(
4638
4642
  content,
4639
4643
  role: 'assistant',
4640
4644
  platform: platform,
4641
4645
  transient: transient,
4646
+ metadata: metadata,
4642
4647
  );
4643
4648
  }
4644
4649
 
@@ -7418,7 +7423,14 @@ class NeoAgentController extends ChangeNotifier {
7418
7423
  }
7419
7424
  final content = payload['content']?.toString().trim() ?? '';
7420
7425
  if (content.isNotEmpty) {
7421
- _appendAssistantChatMessage(content, platform: 'web');
7426
+ final schema = payload['schema'];
7427
+ _appendAssistantChatMessage(
7428
+ content,
7429
+ platform: 'web',
7430
+ metadata: schema != null
7431
+ ? <String, dynamic>{'schema': schema}
7432
+ : const <String, dynamic>{},
7433
+ );
7422
7434
  }
7423
7435
  streamingAssistant = '';
7424
7436
  isSendingMessage = false;
@@ -1888,6 +1888,27 @@ class ChatEntry {
1888
1888
  }
1889
1889
  return null;
1890
1890
  }
1891
+
1892
+ ChatRichPayload? get richPayload {
1893
+ final schema = metadata['schema'];
1894
+ if (schema is! Map) return null;
1895
+ final type = schema['type']?.toString() ?? '';
1896
+ if (type != 'quick_reply' && type != 'list_picker') return null;
1897
+ final optList = schema['options'];
1898
+ if (optList is! List) return null;
1899
+ final options = optList
1900
+ .whereType<Map>()
1901
+ .map(
1902
+ (item) => ChatPayloadOption(
1903
+ label: item['label']?.toString() ?? '',
1904
+ value: item['value']?.toString() ?? '',
1905
+ ),
1906
+ )
1907
+ .where((o) => o.label.isNotEmpty)
1908
+ .toList(growable: false);
1909
+ if (options.isEmpty) return null;
1910
+ return ChatRichPayload(type: type, options: options);
1911
+ }
1891
1912
  }
1892
1913
 
1893
1914
  class SharedChatAttachment {
@@ -4041,3 +4062,18 @@ class ToolEventItem {
4041
4062
 
4042
4063
  String get compactSummary => _condenseRunText(summary, maxLength: 120);
4043
4064
  }
4065
+
4066
+ class ChatPayloadOption {
4067
+ const ChatPayloadOption({required this.label, required this.value});
4068
+
4069
+ final String label;
4070
+ final String value;
4071
+ }
4072
+
4073
+ class ChatRichPayload {
4074
+ const ChatRichPayload({required this.type, required this.options});
4075
+
4076
+ final String type;
4077
+ final List<ChatPayloadOption> options;
4078
+ }
4079
+
@@ -21,7 +21,6 @@ enum AppSection {
21
21
  runs,
22
22
  settings,
23
23
  accountSettings,
24
- logs,
25
24
  skills,
26
25
  agents,
27
26
  integrations,
@@ -76,13 +75,11 @@ extension AppSectionX on AppSection {
76
75
  case AppSection.messaging:
77
76
  return 'Messaging';
78
77
  case AppSection.runs:
79
- return 'Runs & Logs';
78
+ return 'Runs';
80
79
  case AppSection.settings:
81
80
  return 'Settings';
82
81
  case AppSection.accountSettings:
83
82
  return 'Account settings';
84
- case AppSection.logs:
85
- return 'Logs';
86
83
  case AppSection.skills:
87
84
  return 'Skills';
88
85
  case AppSection.agents:
@@ -120,8 +117,6 @@ extension AppSectionX on AppSection {
120
117
  return Icons.tune;
121
118
  case AppSection.accountSettings:
122
119
  return Icons.manage_accounts_outlined;
123
- case AppSection.logs:
124
- return Icons.article_outlined;
125
120
  case AppSection.skills:
126
121
  return Icons.extension_outlined;
127
122
  case AppSection.agents:
@@ -158,7 +153,6 @@ extension AppSectionX on AppSection {
158
153
  case AppSection.health:
159
154
  return SidebarGroup.automation;
160
155
  case AppSection.runs:
161
- case AppSection.logs:
162
156
  case AppSection.settings:
163
157
  case AppSection.accountSettings:
164
158
  case AppSection.messaging:
@@ -169,8 +163,6 @@ extension AppSectionX on AppSection {
169
163
 
170
164
  AppSection get canonicalSection {
171
165
  switch (this) {
172
- case AppSection.logs:
173
- return AppSection.runs;
174
166
  case AppSection.skills:
175
167
  case AppSection.mcp:
176
168
  return AppSection.integrations;
@@ -139,15 +139,6 @@ const _desktopSettingsSection = _SettingsSection('desktop', <String>[
139
139
  'input',
140
140
  ], requiresDesktop: true);
141
141
 
142
- const _updatesSettingsSection = _SettingsSection('updates', <String>[
143
- 'updates',
144
- 'release',
145
- 'channel',
146
- 'version',
147
- 'self update',
148
- 'upgrade',
149
- ]);
150
-
151
142
  const _diagnosticsSettingsSection = _SettingsSection('diagnostics', <String>[
152
143
  'diagnostics',
153
144
  'logs',
@@ -163,7 +154,6 @@ const List<_SettingsSection> _settingsSearchSections = <_SettingsSection>[
163
154
  _modelsSettingsSection,
164
155
  _voiceRecordingSettingsSection,
165
156
  _desktopSettingsSection,
166
- _updatesSettingsSection,
167
157
  _diagnosticsSettingsSection,
168
158
  ];
169
159
 
@@ -328,7 +318,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
328
318
  _PageTitle(
329
319
  title: 'Settings',
330
320
  subtitle:
331
- 'Workspace, models, recording, update, and diagnostics controls.',
321
+ 'Workspace, models, recording, and diagnostics controls.',
332
322
  trailing: _settingsSaveButton(controller),
333
323
  )
334
324
  else
@@ -348,7 +338,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
348
338
  onChanged: (_) => setState(() {}),
349
339
  decoration: InputDecoration(
350
340
  labelText: 'Search settings',
351
- hintText: 'Models, browser, voice, updates, diagnostics...',
341
+ hintText: 'Models, browser, voice, diagnostics...',
352
342
  prefixIcon: const Icon(Icons.search),
353
343
  suffixIcon: searchQuery.isEmpty
354
344
  ? null
@@ -409,13 +399,6 @@ class _SettingsPanelState extends State<SettingsPanel> {
409
399
  _buildDesktopSection(controller),
410
400
  const SizedBox(height: 16),
411
401
  ],
412
- if (_matchesSettingsSection(
413
- searchQuery,
414
- _updatesSettingsSection,
415
- )) ...<Widget>[
416
- _buildUpdatesSection(controller),
417
- const SizedBox(height: 16),
418
- ],
419
402
  if (_matchesSettingsSection(
420
403
  searchQuery,
421
404
  _diagnosticsSettingsSection,
@@ -424,7 +407,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
424
407
  const _EmptyCard(
425
408
  title: 'No matching settings',
426
409
  subtitle:
427
- 'Try a broader search like models, browser, voice, or updates.',
410
+ 'Try a broader search like models, browser, or voice.',
428
411
  ),
429
412
  ],
430
413
  ],
@@ -534,7 +517,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
534
517
  const _SectionTitle('Overview'),
535
518
  const SizedBox(height: 10),
536
519
  Text(
537
- 'Configure workspace behavior, then models, recording defaults, and updates.',
520
+ 'Configure workspace behavior, models, and recording defaults.',
538
521
  style: TextStyle(color: _textSecondary, height: 1.45),
539
522
  ),
540
523
  const SizedBox(height: 14),
@@ -1523,250 +1506,6 @@ class _SettingsPanelState extends State<SettingsPanel> {
1523
1506
  );
1524
1507
  }
1525
1508
 
1526
- Widget _buildUpdatesSection(NeoAgentController controller) {
1527
- return Card(
1528
- child: Padding(
1529
- padding: const EdgeInsets.all(20),
1530
- child: Column(
1531
- crossAxisAlignment: CrossAxisAlignment.start,
1532
- children: <Widget>[
1533
- const _SectionTitle('Updates'),
1534
- const SizedBox(height: 10),
1535
- Text(
1536
- 'Client and runtime update controls live here.',
1537
- style: TextStyle(color: _textSecondary, height: 1.45),
1538
- ),
1539
- if (!kIsWeb) ...<Widget>[
1540
- const SizedBox(height: 16),
1541
- LayoutBuilder(
1542
- builder: (context, constraints) {
1543
- final compact = constraints.maxWidth < 720;
1544
- final checkButton = FilledButton.icon(
1545
- onPressed:
1546
- controller.isCheckingAppUpdate ||
1547
- !controller.appUpdaterConfigured
1548
- ? null
1549
- : () => controller.checkForAppUpdates(),
1550
- style: FilledButton.styleFrom(backgroundColor: _accent),
1551
- icon: controller.isCheckingAppUpdate
1552
- ? _inlineProgressIndicator()
1553
- : const Icon(Icons.sync),
1554
- label: Text(
1555
- controller.isCheckingAppUpdate
1556
- ? 'Checking...'
1557
- : 'Check now',
1558
- ),
1559
- );
1560
- final appHeading = Text(
1561
- 'Client App',
1562
- style: TextStyle(
1563
- fontWeight: FontWeight.w700,
1564
- color: _textPrimary,
1565
- ),
1566
- );
1567
- if (compact) {
1568
- return Column(
1569
- crossAxisAlignment: CrossAxisAlignment.start,
1570
- children: <Widget>[
1571
- appHeading,
1572
- const SizedBox(height: 10),
1573
- checkButton,
1574
- ],
1575
- );
1576
- }
1577
- return Row(
1578
- children: <Widget>[
1579
- Expanded(child: appHeading),
1580
- checkButton,
1581
- ],
1582
- );
1583
- },
1584
- ),
1585
- const SizedBox(height: 12),
1586
- if (!controller.appUpdaterConfigured)
1587
- Text(
1588
- 'Client app updates are not configured for this build.',
1589
- style: TextStyle(color: _textSecondary, height: 1.5),
1590
- ),
1591
- ],
1592
- const Divider(height: 32),
1593
- if (controller.updateStatus.allowSelfUpdate) ...<Widget>[
1594
- LayoutBuilder(
1595
- builder: (context, constraints) {
1596
- final compact = constraints.maxWidth < 780;
1597
- final channelPicker = DropdownButtonFormField<String>(
1598
- initialValue: controller.updateStatus.releaseChannel,
1599
- decoration: const InputDecoration(
1600
- labelText: 'Runtime release channel',
1601
- ),
1602
- items: const <DropdownMenuItem<String>>[
1603
- DropdownMenuItem<String>(
1604
- value: 'stable',
1605
- child: Text('Stable'),
1606
- ),
1607
- DropdownMenuItem<String>(
1608
- value: 'beta',
1609
- child: Text('Beta'),
1610
- ),
1611
- ],
1612
- onChanged:
1613
- controller.isSavingReleaseChannel ||
1614
- controller.isTriggeringUpdate ||
1615
- controller.updateStatus.state == 'running'
1616
- ? null
1617
- : (value) {
1618
- if (value != null) {
1619
- unawaited(controller.setReleaseChannel(value));
1620
- }
1621
- },
1622
- );
1623
-
1624
- final channelHelper = Text(
1625
- controller.updateStatus.releaseChannel == 'beta'
1626
- ? 'Beta follows preview releases.'
1627
- : 'Stable follows production releases.',
1628
- style: TextStyle(color: _textSecondary),
1629
- );
1630
-
1631
- if (compact) {
1632
- return Column(
1633
- crossAxisAlignment: CrossAxisAlignment.start,
1634
- children: <Widget>[
1635
- channelPicker,
1636
- const SizedBox(height: 8),
1637
- channelHelper,
1638
- const SizedBox(height: 16),
1639
- ],
1640
- );
1641
- }
1642
-
1643
- return Padding(
1644
- padding: const EdgeInsets.only(bottom: 16),
1645
- child: Row(
1646
- crossAxisAlignment: CrossAxisAlignment.start,
1647
- children: <Widget>[
1648
- Expanded(child: channelPicker),
1649
- const SizedBox(width: 12),
1650
- Expanded(child: channelHelper),
1651
- ],
1652
- ),
1653
- );
1654
- },
1655
- ),
1656
- LayoutBuilder(
1657
- builder: (context, constraints) {
1658
- final compact = constraints.maxWidth < 780;
1659
- final runtimeTitle = Text(
1660
- 'Runtime',
1661
- style: TextStyle(
1662
- fontWeight: FontWeight.w700,
1663
- color: _textPrimary,
1664
- ),
1665
- );
1666
- final updateButton = FilledButton.icon(
1667
- onPressed:
1668
- controller.isSavingReleaseChannel ||
1669
- controller.isTriggeringUpdate ||
1670
- controller.updateStatus.state == 'running'
1671
- ? null
1672
- : controller.triggerUpdate,
1673
- style: FilledButton.styleFrom(backgroundColor: _accent),
1674
- icon: controller.isTriggeringUpdate
1675
- ? const SizedBox.square(
1676
- dimension: 16,
1677
- child: CircularProgressIndicator(
1678
- strokeWidth: 2,
1679
- color: Colors.white,
1680
- ),
1681
- )
1682
- : Icon(Icons.system_update),
1683
- label: Text('Update'),
1684
- );
1685
- if (compact) {
1686
- return Column(
1687
- crossAxisAlignment: CrossAxisAlignment.start,
1688
- children: <Widget>[
1689
- runtimeTitle,
1690
- const SizedBox(height: 10),
1691
- updateButton,
1692
- ],
1693
- );
1694
- }
1695
- return Row(
1696
- children: <Widget>[
1697
- Expanded(child: runtimeTitle),
1698
- updateButton,
1699
- ],
1700
- );
1701
- },
1702
- ),
1703
- ] else ...<Widget>[
1704
- Text(
1705
- 'Runtime',
1706
- style: TextStyle(
1707
- fontWeight: FontWeight.w700,
1708
- color: _textPrimary,
1709
- ),
1710
- ),
1711
- const SizedBox(height: 10),
1712
- Text(
1713
- 'Updates and release tracks are managed for this deployment.',
1714
- style: TextStyle(color: _textSecondary),
1715
- ),
1716
- ],
1717
- const SizedBox(height: 12),
1718
- LayoutBuilder(
1719
- builder: (context, constraints) {
1720
- final compact = constraints.maxWidth < 760;
1721
- final statusRow = Wrap(
1722
- spacing: 10,
1723
- runSpacing: 10,
1724
- crossAxisAlignment: WrapCrossAlignment.center,
1725
- children: <Widget>[
1726
- _StatusPill(
1727
- label: controller.updateStatus.badgeLabel,
1728
- color: controller.updateStatus.badgeColor,
1729
- ),
1730
- _StatusPill(
1731
- label: controller.updateStatus.releaseChannelLabel,
1732
- color: controller.updateStatus.releaseChannel == 'beta'
1733
- ? _warning
1734
- : _accent,
1735
- ),
1736
- Text(
1737
- controller.updateStatus.message,
1738
- style: TextStyle(color: _textSecondary),
1739
- ),
1740
- Text('${controller.updateStatus.progress}%'),
1741
- ],
1742
- );
1743
- if (compact) {
1744
- return Column(
1745
- crossAxisAlignment: CrossAxisAlignment.start,
1746
- children: <Widget>[statusRow],
1747
- );
1748
- }
1749
- return statusRow;
1750
- },
1751
- ),
1752
- const SizedBox(height: 10),
1753
- ClipRRect(
1754
- borderRadius: BorderRadius.circular(999),
1755
- child: LinearProgressIndicator(
1756
- minHeight: 8,
1757
- value: controller.updateStatus.progress / 100,
1758
- backgroundColor: _bgSecondary,
1759
- color: _accent,
1760
- ),
1761
- ),
1762
- const SizedBox(height: 12),
1763
- Text(controller.updateStatus.versionLine),
1764
- ],
1765
- ),
1766
- ),
1767
- );
1768
- }
1769
-
1770
1509
  Widget _buildDiagnosticsSection(NeoAgentController controller) {
1771
1510
  return Card(
1772
1511
  child: Padding(