neoagent 2.3.1-beta.64 → 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 (34) hide show
  1. package/docs/capabilities.md +1 -1
  2. package/docs/configuration.md +2 -2
  3. package/flutter_app/lib/main_app_shell.dart +1 -1
  4. package/flutter_app/lib/main_chat.dart +198 -9
  5. package/flutter_app/lib/main_controller.dart +27 -7
  6. package/flutter_app/lib/main_devices.dart +4 -6
  7. package/flutter_app/lib/main_models.dart +91 -1
  8. package/flutter_app/lib/main_navigation.dart +9 -0
  9. package/flutter_app/lib/main_settings.dart +6 -8
  10. package/flutter_app/lib/main_shared.dart +7 -3
  11. package/package.json +1 -1
  12. package/server/public/.last_build_id +1 -1
  13. package/server/public/assets/AssetManifest.json +1 -1
  14. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  15. package/server/public/flutter_bootstrap.js +1 -1
  16. package/server/public/main.dart.js +52468 -52276
  17. package/server/routes/browser.js +1 -14
  18. package/server/routes/settings.js +1 -5
  19. package/server/services/ai/capabilityHealth.js +1 -10
  20. package/server/services/ai/deliverables/artifact_helpers.js +190 -0
  21. package/server/services/ai/deliverables/contracts.js +113 -0
  22. package/server/services/ai/deliverables/deliverables.test.js +76 -0
  23. package/server/services/ai/deliverables/index.js +20 -0
  24. package/server/services/ai/deliverables/selector.js +94 -0
  25. package/server/services/ai/deliverables/validator.js +63 -0
  26. package/server/services/ai/deliverables/workflows.js +195 -0
  27. package/server/services/ai/engine.js +173 -1
  28. package/server/services/ai/systemPrompt.js +6 -0
  29. package/server/services/ai/tools.js +1 -5
  30. package/server/services/manager.js +5 -56
  31. package/server/services/runtime/manager.js +2 -6
  32. package/server/services/runtime/settings.js +6 -12
  33. package/server/services/widgets/focus_widget.js +10 -2
  34. package/server/utils/deployment.js +4 -3
@@ -112,7 +112,7 @@ Runtime settings let operators choose where higher-risk work runs:
112
112
 
113
113
  | Profile | Runtime shape |
114
114
  |---|---|
115
- | `trusted-host` | CLI, browser, and Android tools run on the host |
115
+ | `trusted-host` | CLI and Android tools run on the host; browser runs in the VM or paired extension |
116
116
  | `secure-vm` | CLI, browser, and Android tools run through the local VM backend |
117
117
 
118
118
  Production policy can require the secure VM profile and a strong VM guest token.
@@ -130,11 +130,11 @@ Telnyx webhook verification is configured through the environment.
130
130
 
131
131
  ## Runtime Isolation
132
132
 
133
- Runtime profile and backend selection are stored in user settings, not normally in `.env`. The main profiles are `trusted-host` and `secure-vm`. They control whether CLI, browser, and Android tools run on the host or through the local VM backend.
133
+ Runtime profile and backend selection are stored in user settings, not normally in `.env`. The main profiles are `trusted-host` and `secure-vm`. CLI and Android tools may still follow the selected runtime profile, but browser control is always isolated: the non-extension browser backend runs in the VM, not on the host device.
134
134
 
135
135
  Production policy can require the VM backend. In that case, set a strong `NEOAGENT_VM_GUEST_TOKEN` of at least 32 characters and avoid placeholder values.
136
136
 
137
- The app exposes two browser backend choices: Cloud and Chrome extension. Cloud uses the current deployment policy, which means host browser control for trusted private installs and VM browser control for isolated production installs. Chrome extension uses the paired extension connection instead of the server-local Puppeteer browser. To install only the extension on a remote machine, open NeoAgent, download `/api/browser-extension/download`, unzip it, load the folder through `chrome://extensions` with Developer mode enabled, then pair after logging in to NeoAgent. Unpacked Chrome extensions cannot replace themselves automatically; use the extension popup's update check to compare against the server bundle, then download and reload the latest ZIP when needed.
137
+ The app exposes two browser backend choices: VM and Chrome extension. VM uses the local isolated runtime. Chrome extension uses the paired extension connection on the remote machine instead of the server-local browser. To install only the extension on a remote machine, open NeoAgent, download `/api/browser-extension/download`, unzip it, load the folder through `chrome://extensions` with Developer mode enabled, then pair after logging in to NeoAgent. Unpacked Chrome extensions cannot replace themselves automatically; use the extension popup's update check to compare against the server bundle, then download and reload the latest ZIP when needed.
138
138
 
139
139
  ## Secrets Guidance
140
140
 
@@ -1120,7 +1120,7 @@ class _HomeViewState extends State<HomeView> {
1120
1120
  }
1121
1121
 
1122
1122
  SidebarGroup? _sidebarGroupForSection(AppSection section) {
1123
- final visibleSection = section.canonicalSection;
1123
+ final visibleSection = section.sidebarSection;
1124
1124
  if (!_mainSections(widget.controller).contains(visibleSection)) {
1125
1125
  return null;
1126
1126
  }
@@ -43,6 +43,23 @@ class _ChatPanelState extends State<ChatPanel> {
43
43
  ..selection = TextSelection.collapsed(offset: draft.length);
44
44
  }
45
45
 
46
+ void _scrollToBottom() {
47
+ WidgetsBinding.instance.addPostFrameCallback((_) {
48
+ if (!mounted || !_scrollController.hasClients) {
49
+ return;
50
+ }
51
+ _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
52
+ unawaited(
53
+ WidgetsBinding.instance.endOfFrame.then((_) {
54
+ if (!mounted || !_scrollController.hasClients) {
55
+ return;
56
+ }
57
+ _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
58
+ }),
59
+ );
60
+ });
61
+ }
62
+
46
63
  @override
47
64
  Widget build(BuildContext context) {
48
65
  final controller = widget.controller;
@@ -53,15 +70,7 @@ class _ChatPanelState extends State<ChatPanel> {
53
70
  _lastMessageCount = messages.length;
54
71
  _lastToolCount = controller.toolEvents.length;
55
72
  _lastStream = controller.streamingAssistant;
56
- WidgetsBinding.instance.addPostFrameCallback((_) {
57
- if (_scrollController.hasClients) {
58
- _scrollController.animateTo(
59
- _scrollController.position.maxScrollExtent,
60
- duration: const Duration(milliseconds: 220),
61
- curve: Curves.easeOut,
62
- );
63
- }
64
- });
73
+ _scrollToBottom();
65
74
  }
66
75
 
67
76
  return Column(
@@ -259,6 +268,87 @@ class _ChatPanelState extends State<ChatPanel> {
259
268
  }
260
269
  }
261
270
 
271
+ class _TypingIndicatorBubble extends StatefulWidget {
272
+ const _TypingIndicatorBubble();
273
+
274
+ @override
275
+ State<_TypingIndicatorBubble> createState() => _TypingIndicatorBubbleState();
276
+ }
277
+
278
+ class _TypingIndicatorBubbleState extends State<_TypingIndicatorBubble>
279
+ with SingleTickerProviderStateMixin {
280
+ late final AnimationController _controller;
281
+
282
+ @override
283
+ void initState() {
284
+ super.initState();
285
+ _controller = AnimationController(
286
+ vsync: this,
287
+ duration: const Duration(milliseconds: 1200),
288
+ )..repeat();
289
+ }
290
+
291
+ @override
292
+ void dispose() {
293
+ _controller.dispose();
294
+ super.dispose();
295
+ }
296
+
297
+ @override
298
+ Widget build(BuildContext context) {
299
+ return Row(
300
+ crossAxisAlignment: CrossAxisAlignment.start,
301
+ children: <Widget>[
302
+ const _MessageAvatar(assistant: true),
303
+ const SizedBox(width: 12),
304
+ Flexible(
305
+ child: Container(
306
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
307
+ decoration: BoxDecoration(
308
+ color: _bgCard,
309
+ borderRadius: BorderRadius.circular(14),
310
+ border: Border.all(color: _border),
311
+ ),
312
+ child: AnimatedBuilder(
313
+ animation: _controller,
314
+ builder: (context, _) {
315
+ return Row(
316
+ mainAxisSize: MainAxisSize.min,
317
+ children: List<Widget>.generate(3, (index) {
318
+ final phase = ((_controller.value * 3) - index).clamp(
319
+ 0.0,
320
+ 1.0,
321
+ );
322
+ final offset = Curves.easeOut.transform(
323
+ phase > 0.5 ? 1 - phase : phase,
324
+ );
325
+ return Padding(
326
+ padding: EdgeInsets.only(
327
+ right: index == 2 ? 0 : 6,
328
+ top: (1 - offset) * 6,
329
+ ),
330
+ child: Container(
331
+ width: 8,
332
+ height: 8,
333
+ decoration: BoxDecoration(
334
+ color: _textSecondary.withValues(
335
+ alpha: 0.45 + (offset * 0.5),
336
+ ),
337
+ shape: BoxShape.circle,
338
+ ),
339
+ ),
340
+ );
341
+ }),
342
+ );
343
+ },
344
+ ),
345
+ ),
346
+ ),
347
+ ],
348
+ );
349
+ }
350
+ }
351
+
262
352
  class MessagingPanel extends StatefulWidget {
263
353
  const MessagingPanel({super.key, required this.controller});
264
354
 
@@ -2493,6 +2583,13 @@ class _RunHistoryRow extends StatelessWidget {
2493
2583
  '${run.modelLabel} • ${run.totalTokensLabel} tokens',
2494
2584
  style: TextStyle(color: _textSecondary, fontSize: 12),
2495
2585
  ),
2586
+ if (run.deliverableType.trim().isNotEmpty) ...<Widget>[
2587
+ const SizedBox(height: 4),
2588
+ Text(
2589
+ 'Deliverable • ${run.deliverableType.replaceAll('_', ' ')}',
2590
+ style: TextStyle(color: _accent, fontSize: 12),
2591
+ ),
2592
+ ],
2496
2593
  if (run.error.trim().isNotEmpty) ...<Widget>[
2497
2594
  const SizedBox(height: 8),
2498
2595
  Text(
@@ -2624,6 +2721,10 @@ class _RunDetailWorkspace extends StatelessWidget {
2624
2721
  response: snapshot.response,
2625
2722
  onCopy: () => onCopyResponse(snapshot.response),
2626
2723
  ),
2724
+ if (snapshot.run.deliverableType.trim().isNotEmpty) ...<Widget>[
2725
+ const SizedBox(height: 16),
2726
+ _DeliverableSummaryCard(run: snapshot.run),
2727
+ ],
2627
2728
  const SizedBox(height: 16),
2628
2729
  _RunTimelineCard(steps: snapshot.steps, loading: loading),
2629
2730
  const SizedBox(height: 16),
@@ -2687,6 +2788,11 @@ class _RunHeroCard extends StatelessWidget {
2687
2788
  label: run.modelLabel,
2688
2789
  icon: Icons.memory_outlined,
2689
2790
  ),
2791
+ if (run.deliverableType.trim().isNotEmpty)
2792
+ _MetaPill(
2793
+ label: run.deliverableType.replaceAll('_', ' '),
2794
+ icon: Icons.inventory_2_outlined,
2795
+ ),
2690
2796
  ],
2691
2797
  ),
2692
2798
  const SizedBox(height: 16),
@@ -2812,6 +2918,89 @@ class _RunResponseCard extends StatelessWidget {
2812
2918
  }
2813
2919
  }
2814
2920
 
2921
+ class _DeliverableSummaryCard extends StatelessWidget {
2922
+ const _DeliverableSummaryCard({required this.run});
2923
+
2924
+ final RunSummary run;
2925
+
2926
+ @override
2927
+ Widget build(BuildContext context) {
2928
+ final artifacts = run.deliverableArtifacts;
2929
+ return Card(
2930
+ child: Padding(
2931
+ padding: const EdgeInsets.all(18),
2932
+ child: Column(
2933
+ crossAxisAlignment: CrossAxisAlignment.start,
2934
+ children: <Widget>[
2935
+ _SectionTitle('Deliverable'),
2936
+ const SizedBox(height: 12),
2937
+ Text(
2938
+ run.deliverableSummary.ifEmpty(
2939
+ 'Workflow: ${run.deliverableType.replaceAll('_', ' ')}',
2940
+ ),
2941
+ style: TextStyle(color: _textPrimary, height: 1.45),
2942
+ ),
2943
+ if (artifacts.isNotEmpty) ...<Widget>[
2944
+ const SizedBox(height: 14),
2945
+ ...artifacts.map((artifact) {
2946
+ final meta = <String>[
2947
+ artifact.kind,
2948
+ if (artifact.mimeType.trim().isNotEmpty) artifact.mimeType,
2949
+ if (artifact.size > 0) '${artifact.size} bytes',
2950
+ ].join(' • ');
2951
+ final location = artifact.uri.ifEmpty(artifact.path);
2952
+ return Padding(
2953
+ padding: const EdgeInsets.only(bottom: 10),
2954
+ child: Container(
2955
+ width: double.infinity,
2956
+ padding: const EdgeInsets.all(12),
2957
+ decoration: BoxDecoration(
2958
+ color: _bgSecondary,
2959
+ borderRadius: BorderRadius.circular(14),
2960
+ border: Border.all(color: _border),
2961
+ ),
2962
+ child: Column(
2963
+ crossAxisAlignment: CrossAxisAlignment.start,
2964
+ children: <Widget>[
2965
+ Text(
2966
+ artifact.displayLabel,
2967
+ style: TextStyle(fontWeight: FontWeight.w700),
2968
+ ),
2969
+ if (meta.trim().isNotEmpty) ...<Widget>[
2970
+ const SizedBox(height: 4),
2971
+ Text(
2972
+ meta,
2973
+ style: TextStyle(
2974
+ color: _textSecondary,
2975
+ fontSize: 12,
2976
+ ),
2977
+ ),
2978
+ ],
2979
+ if (location.trim().isNotEmpty) ...<Widget>[
2980
+ const SizedBox(height: 6),
2981
+ SelectableText(
2982
+ location,
2983
+ style: TextStyle(
2984
+ color: _textSecondary,
2985
+ fontSize: 12,
2986
+ fontFamily:
2987
+ GoogleFonts.jetBrainsMono().fontFamily,
2988
+ ),
2989
+ ),
2990
+ ],
2991
+ ],
2992
+ ),
2993
+ ),
2994
+ );
2995
+ }),
2996
+ ],
2997
+ ],
2998
+ ),
2999
+ ),
3000
+ );
3001
+ }
3002
+ }
3003
+
2815
3004
  class _RunTimelineCard extends StatelessWidget {
2816
3005
  const _RunTimelineCard({required this.steps, required this.loading});
2817
3006
 
@@ -5572,9 +5572,18 @@ class NeoAgentController extends ChangeNotifier {
5572
5572
  return;
5573
5573
  }
5574
5574
  _pendingChatDraft = normalized;
5575
- setSelectedSection(AppSection.chat);
5575
+ if (!_isMobilePlatform) {
5576
+ setSelectedSection(AppSection.chat);
5577
+ } else {
5578
+ notifyListeners();
5579
+ }
5576
5580
  }
5577
5581
 
5582
+ bool get _isMobilePlatform =>
5583
+ !kIsWeb &&
5584
+ (defaultTargetPlatform == TargetPlatform.android ||
5585
+ defaultTargetPlatform == TargetPlatform.iOS);
5586
+
5578
5587
  String? takePendingChatDraft() {
5579
5588
  final draft = _pendingChatDraft;
5580
5589
  _pendingChatDraft = null;
@@ -5953,7 +5962,7 @@ class NeoAgentController extends ChangeNotifier {
5953
5962
  settings['headless_browser'] != 'false';
5954
5963
 
5955
5964
  String get browserBackend =>
5956
- settings['browser_backend']?.toString().trim().toLowerCase() ?? 'host';
5965
+ settings['browser_backend']?.toString().trim().toLowerCase() ?? 'vm';
5957
5966
 
5958
5967
  String get cloudBrowserBackend {
5959
5968
  final browser = browserBackend;
@@ -5969,11 +5978,10 @@ class NeoAgentController extends ChangeNotifier {
5969
5978
  profile == 'secure-vm') {
5970
5979
  return 'vm';
5971
5980
  }
5972
- if (browser == 'host' || browser == 'vm') {
5973
- return browser;
5974
- }
5981
+ if (browser == 'extension') return 'vm';
5982
+ if (browser == 'vm') return 'vm';
5975
5983
  if (runtime == 'vm') return 'vm';
5976
- return 'host';
5984
+ return 'vm';
5977
5985
  }
5978
5986
 
5979
5987
  bool get browserExtensionConnected =>
@@ -6158,7 +6166,19 @@ class NeoAgentController extends ChangeNotifier {
6158
6166
 
6159
6167
  List<ChatEntry> get visibleChatMessages {
6160
6168
  final entries = <ChatEntry>[...chatMessages];
6161
- 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) {
6162
6182
  entries.add(
6163
6183
  ChatEntry(
6164
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 =>
@@ -1475,6 +1475,38 @@ class RunDetailSnapshot {
1475
1475
  steps.where((step) => step.isPlanningRelated).length;
1476
1476
  }
1477
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
+
1478
1510
  class RunEventItem {
1479
1511
  const RunEventItem({
1480
1512
  required this.id,
@@ -1519,6 +1551,18 @@ class RunEventItem {
1519
1551
 
1520
1552
  String get title {
1521
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';
1522
1566
  case 'run_started':
1523
1567
  return 'Run started';
1524
1568
  case 'memory_injected':
@@ -1554,6 +1598,13 @@ class RunEventItem {
1554
1598
  if (preview.trim().isNotEmpty) return preview;
1555
1599
  final error = payload['error']?.toString() ?? '';
1556
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;
1557
1608
  final titleValue = payload['title']?.toString() ?? '';
1558
1609
  return titleValue;
1559
1610
  }
@@ -1745,6 +1796,17 @@ dynamic _decodeMaybeJson(dynamic value) {
1745
1796
  return value;
1746
1797
  }
1747
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
+
1748
1810
  class ChatEntry {
1749
1811
  const ChatEntry({
1750
1812
  required this.id,
@@ -1757,6 +1819,7 @@ class ChatEntry {
1757
1819
  this.metadata = const <String, dynamic>{},
1758
1820
  this.toolCalls = const <Map<String, dynamic>>[],
1759
1821
  this.transient = false,
1822
+ this.typing = false,
1760
1823
  });
1761
1824
 
1762
1825
  factory ChatEntry.fromJson(Map<dynamic, dynamic> json) {
@@ -1786,6 +1849,7 @@ class ChatEntry {
1786
1849
  final List<Map<String, dynamic>> toolCalls;
1787
1850
  final DateTime createdAt;
1788
1851
  final bool transient;
1852
+ final bool typing;
1789
1853
 
1790
1854
  String get createdAtLabel => _formatTimestamp(createdAt);
1791
1855
 
@@ -2059,9 +2123,16 @@ class RunSummary {
2059
2123
  required this.createdAt,
2060
2124
  this.completedAt,
2061
2125
  this.error = '',
2126
+ this.metadata = const <String, dynamic>{},
2062
2127
  });
2063
2128
 
2064
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
+ );
2065
2136
  return RunSummary(
2066
2137
  id: json['id']?.toString() ?? '',
2067
2138
  title: json['title']?.toString() ?? 'Untitled',
@@ -2072,6 +2143,7 @@ class RunSummary {
2072
2143
  createdAt: _parseTimestamp(json['created_at']?.toString()),
2073
2144
  completedAt: _parseOptionalTimestamp(json['completed_at']?.toString()),
2074
2145
  error: json['error']?.toString() ?? '',
2146
+ metadata: metadata,
2075
2147
  );
2076
2148
  }
2077
2149
 
@@ -2084,6 +2156,24 @@ class RunSummary {
2084
2156
  final DateTime createdAt;
2085
2157
  final DateTime? completedAt;
2086
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
+ }
2087
2177
 
2088
2178
  bool get isFailure => status == 'failed' || status == 'error';
2089
2179
 
@@ -2257,7 +2347,7 @@ class UpdateStatusSnapshot {
2257
2347
  deploymentProfile.toLowerCase() == 'prod' ? 'Production' : 'Private';
2258
2348
 
2259
2349
  String get runtimeModeLabel => deploymentProfile.toLowerCase() == 'prod'
2260
- ? 'Per-user isolated VM runtime'
2350
+ ? 'Cloud runtime'
2261
2351
  : 'Trusted host runtime';
2262
2352
 
2263
2353
  String get runtimeValidationLabel =>
@@ -179,6 +179,15 @@ extension AppSectionX on AppSection {
179
179
  }
180
180
  }
181
181
 
182
+ AppSection get sidebarSection {
183
+ switch (this) {
184
+ case AppSection.accountSettings:
185
+ return AppSection.settings;
186
+ default:
187
+ return canonicalSection;
188
+ }
189
+ }
190
+
182
191
  String get navigationTitle {
183
192
  final effectiveSection = canonicalSection;
184
193
  final groupLabel = effectiveSection.group.label;
@@ -279,7 +279,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
279
279
 
280
280
  String _normalizeBrowserBackend(String value) {
281
281
  final normalized = value.trim().toLowerCase();
282
- return normalized == 'extension' ? 'extension' : 'cloud';
282
+ return normalized == 'extension' ? 'extension' : 'vm';
283
283
  }
284
284
 
285
285
  @override
@@ -459,7 +459,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
459
459
  headlessBrowser: _headlessBrowser,
460
460
  browserBackend: _browserBackend == 'extension'
461
461
  ? 'extension'
462
- : controller.cloudBrowserBackend,
462
+ : 'vm',
463
463
  smarterSelector: _smarterSelector,
464
464
  enabledModels: _enabledModels.toList(),
465
465
  defaultChatModel: _defaultChatModel,
@@ -606,12 +606,12 @@ class _SettingsPanelState extends State<SettingsPanel> {
606
606
  decoration: const InputDecoration(
607
607
  labelText: 'Browser backend',
608
608
  helperText:
609
- 'Cloud uses this deployment. Extension uses a paired Chrome browser.',
609
+ 'Cloud uses the isolated browser runtime. Extension uses a paired Chrome browser on the remote machine.',
610
610
  ),
611
611
  items: const <DropdownMenuItem<String>>[
612
612
  DropdownMenuItem<String>(
613
- value: 'cloud',
614
- child: Text('Cloud (local)'),
613
+ value: 'vm',
614
+ child: Text('Cloud'),
615
615
  ),
616
616
  DropdownMenuItem<String>(
617
617
  value: 'extension',
@@ -630,9 +630,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
630
630
  ? (controller.browserExtensionConnected
631
631
  ? 'Chrome extension connected.'
632
632
  : 'Chrome extension selected. Download it here, load it unpacked in Chrome on the remote machine, then pair after login.')
633
- : controller.cloudBrowserBackend == 'vm'
634
- ? "Cloud uses this deployment's isolated VM browser runtime."
635
- : "Cloud uses this deployment's local host browser runtime.",
633
+ : 'Cloud browser runtime is active.',
636
634
  style: TextStyle(color: _textSecondary, height: 1.4),
637
635
  ),
638
636
  const SizedBox(height: 10),
@@ -253,7 +253,7 @@ List<Widget> _buildSidebarItems(
253
253
  final widgets = <Widget>[];
254
254
  final mainSections = _mainSections(controller);
255
255
  final selectedSidebarSection = mainSections.contains(
256
- controller.selectedSection.canonicalSection,
256
+ controller.selectedSection.sidebarSection,
257
257
  );
258
258
  for (final group in SidebarGroup.values) {
259
259
  final sections = mainSections
@@ -265,7 +265,7 @@ List<Widget> _buildSidebarItems(
265
265
 
266
266
  final active =
267
267
  selectedSidebarSection &&
268
- controller.selectedSection.canonicalSection.group == group;
268
+ controller.selectedSection.sidebarSection.group == group;
269
269
  final defaultSection = sections.first;
270
270
  final hasChildren = sections.length > 1;
271
271
  final expanded = expandedGroup == group;
@@ -297,7 +297,7 @@ List<Widget> _buildSidebarItems(
297
297
  _SidebarButton(
298
298
  label: section.label,
299
299
  icon: section.icon,
300
- active: controller.selectedSection.canonicalSection == section,
300
+ active: controller.selectedSection.sidebarSection == section,
301
301
  indent: 18,
302
302
  iconSize: 16,
303
303
  fontSize: 12,
@@ -1133,6 +1133,10 @@ class _ChatBubble extends StatelessWidget {
1133
1133
  final isUser = entry.role == 'user';
1134
1134
  final isTransient = entry.transient;
1135
1135
 
1136
+ if (entry.typing) {
1137
+ return const _TypingIndicatorBubble();
1138
+ }
1139
+
1136
1140
  return Opacity(
1137
1141
  opacity: isTransient ? 0.92 : 1,
1138
1142
  child: Row(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.64",
3
+ "version": "2.3.1-beta.65",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -1 +1 @@
1
- 041b637a102d15b05aa2cd1af9c6e61e
1
+ f5b685e19d2bcb3cb22b0f5b5df858c9
@@ -1 +1 @@
1
- {"assets/branding/app_icon_256.png":["assets/branding/app_icon_256.png"],"assets/branding/tray_icon_template.png":["assets/branding/tray_icon_template.png"],"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"],"packages/record_web/assets/js/record.fixwebmduration.js":["packages/record_web/assets/js/record.fixwebmduration.js"],"packages/record_web/assets/js/record.worklet.js":["packages/record_web/assets/js/record.worklet.js"],"web/icons/Icon-192.png":["web/icons/Icon-192.png"]}
1
+ {"assets/branding/app_icon_256.png":["assets/branding/app_icon_256.png"],"assets/branding/onboarding_intro.mp4":["assets/branding/onboarding_intro.mp4"],"assets/branding/tray_icon_template.png":["assets/branding/tray_icon_template.png"],"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"],"packages/mixpanel_flutter/assets/mixpanel.js":["packages/mixpanel_flutter/assets/mixpanel.js"],"packages/record_web/assets/js/record.fixwebmduration.js":["packages/record_web/assets/js/record.fixwebmduration.js"],"packages/record_web/assets/js/record.worklet.js":["packages/record_web/assets/js/record.worklet.js"],"web/icons/Icon-192.png":["web/icons/Icon-192.png"]}
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "4064509463" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "1598577597" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });