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
@@ -1,9 +1,14 @@
1
1
  part of 'main.dart';
2
2
 
3
3
  class SettingsPanel extends StatefulWidget {
4
- const SettingsPanel({super.key, required this.controller});
4
+ const SettingsPanel({
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
  State<SettingsPanel> createState() => _SettingsPanelState();
@@ -69,7 +74,99 @@ const Map<String, List<String>> _voiceLiveVoicesByProvider =
69
74
  ],
70
75
  };
71
76
 
77
+ class _SettingsSection {
78
+ const _SettingsSection(
79
+ this.title,
80
+ this.keywords, {
81
+ this.requiresDesktop = false,
82
+ });
83
+
84
+ final String title;
85
+ final List<String> keywords;
86
+ final bool requiresDesktop;
87
+ }
88
+
89
+ const _overviewSettingsSection = _SettingsSection('overview', <String>[
90
+ 'overview',
91
+ 'summary',
92
+ 'onboarding',
93
+ 'platform',
94
+ 'providers',
95
+ ]);
96
+
97
+ const _workspaceSettingsSection = _SettingsSection('workspace', <String>[
98
+ 'workspace',
99
+ 'browser',
100
+ 'extension',
101
+ 'headless',
102
+ 'routing',
103
+ ]);
104
+
105
+ const _modelsSettingsSection = _SettingsSection('models', <String>[
106
+ 'models',
107
+ 'providers',
108
+ 'routing',
109
+ 'fallback',
110
+ 'chat',
111
+ 'sub-agent',
112
+ 'subagent',
113
+ 'smart selector',
114
+ ]);
115
+
116
+ const _voiceRecordingSettingsSection = _SettingsSection(
117
+ 'voice recording',
118
+ <String>[
119
+ 'voice',
120
+ 'recording',
121
+ 'transcription',
122
+ 'summary',
123
+ 'speech',
124
+ 'tts',
125
+ 'stt',
126
+ 'live',
127
+ ],
128
+ );
129
+
130
+ const _desktopSettingsSection = _SettingsSection('desktop', <String>[
131
+ 'desktop',
132
+ 'permissions',
133
+ 'capture',
134
+ 'companion',
135
+ 'screen recording',
136
+ 'accessibility',
137
+ 'input',
138
+ ], requiresDesktop: true);
139
+
140
+ const _updatesSettingsSection = _SettingsSection('updates', <String>[
141
+ 'updates',
142
+ 'release',
143
+ 'channel',
144
+ 'version',
145
+ 'self update',
146
+ 'upgrade',
147
+ ]);
148
+
149
+ const _diagnosticsSettingsSection = _SettingsSection('diagnostics', <String>[
150
+ 'diagnostics',
151
+ 'logs',
152
+ 'token',
153
+ 'usage',
154
+ 'debug',
155
+ 'health',
156
+ ]);
157
+
158
+ const List<_SettingsSection> _settingsSearchSections = <_SettingsSection>[
159
+ _overviewSettingsSection,
160
+ _workspaceSettingsSection,
161
+ _modelsSettingsSection,
162
+ _voiceRecordingSettingsSection,
163
+ _desktopSettingsSection,
164
+ _updatesSettingsSection,
165
+ _diagnosticsSettingsSection,
166
+ ];
167
+
72
168
  class _SettingsPanelState extends State<SettingsPanel> {
169
+ late final TextEditingController _searchController;
73
170
  late bool _headlessBrowser;
74
171
  late String _browserBackend;
75
172
  late bool _smarterSelector;
@@ -91,11 +188,13 @@ class _SettingsPanelState extends State<SettingsPanel> {
91
188
  @override
92
189
  void initState() {
93
190
  super.initState();
191
+ _searchController = TextEditingController();
94
192
  _hydrate();
95
193
  }
96
194
 
97
195
  @override
98
196
  void dispose() {
197
+ _searchController.dispose();
99
198
  for (final controller in _providerBaseUrlControllers.values) {
100
199
  controller.dispose();
101
200
  }
@@ -180,12 +279,13 @@ class _SettingsPanelState extends State<SettingsPanel> {
180
279
 
181
280
  String _normalizeBrowserBackend(String value) {
182
281
  final normalized = value.trim().toLowerCase();
183
- return normalized == 'extension' ? 'extension' : 'cloud';
282
+ return normalized == 'extension' ? 'extension' : 'vm';
184
283
  }
185
284
 
186
285
  @override
187
286
  Widget build(BuildContext context) {
188
287
  final controller = widget.controller;
288
+ final searchQuery = _searchController.text.trim().toLowerCase();
189
289
  final availableModels = controller.supportedModels
190
290
  .where((model) => model.available)
191
291
  .toList();
@@ -215,93 +315,190 @@ class _SettingsPanelState extends State<SettingsPanel> {
215
315
  final enabledSmartModels = _enabledModels
216
316
  .where((id) => routingModels.any((model) => model.id == id))
217
317
  .length;
318
+ final visibleSearchSections = _settingsSearchSections
319
+ .where((section) => !section.requiresDesktop || _supportsDesktopShell)
320
+ .toSet();
218
321
 
219
322
  return ListView(
220
- padding: _pagePadding(context),
323
+ padding: widget.embedded ? EdgeInsets.zero : _pagePadding(context),
221
324
  children: <Widget>[
222
- _PageTitle(
223
- title: 'Settings',
224
- subtitle:
225
- 'Workspace, models, recording, update, and diagnostics controls.',
226
- trailing: FilledButton.icon(
227
- onPressed: controller.isSavingSettings
228
- ? null
229
- : () => controller.saveSettings(
230
- headlessBrowser: _headlessBrowser,
231
- browserBackend: _browserBackend == 'extension'
232
- ? 'extension'
233
- : controller.cloudBrowserBackend,
234
- smarterSelector: _smarterSelector,
235
- enabledModels: _enabledModels.toList(),
236
- defaultChatModel: _defaultChatModel,
237
- defaultSubagentModel: _defaultSubagentModel,
238
- defaultRecordingTranscriptionProvider: 'deepgram',
239
- defaultRecordingTranscriptionModel:
240
- _defaultRecordingTranscriptionModel,
241
- defaultRecordingSummaryProvider: _providerForSelectedModel(
242
- _defaultRecordingSummaryModel,
243
- controller.supportedModels,
244
- ),
245
- defaultRecordingSummaryModel: _defaultRecordingSummaryModel,
246
- fallbackModel: _fallbackModel,
247
- defaultSpeechModel: _defaultSpeechModel,
248
- voiceSttProvider: controller.voiceSttProvider,
249
- voiceSttModel: controller.voiceSttModel,
250
- voiceTtsProvider: controller.voiceTtsProvider,
251
- voiceTtsModel: controller.voiceTtsModel,
252
- voiceTtsVoice: controller.voiceTtsVoice,
253
- voiceRuntimeMode: 'live',
254
- voiceLiveProvider: _voiceLiveProvider,
255
- voiceLiveModel: _voiceLiveModel,
256
- voiceLiveVoice: _voiceLiveVoice,
257
- aiProviderConfigs: _buildProviderPayload(),
258
- ),
259
- style: FilledButton.styleFrom(backgroundColor: _accent),
260
- icon: controller.isSavingSettings
261
- ? const SizedBox.square(
262
- dimension: 16,
263
- child: CircularProgressIndicator(
264
- strokeWidth: 2,
265
- color: Colors.white,
266
- ),
267
- )
268
- : Icon(Icons.save_outlined),
269
- label: Text('Save'),
325
+ if (!widget.embedded)
326
+ _PageTitle(
327
+ title: 'Settings',
328
+ subtitle:
329
+ 'Workspace, models, recording, update, and diagnostics controls.',
330
+ trailing: _settingsSaveButton(controller),
331
+ )
332
+ else
333
+ Align(
334
+ alignment: Alignment.centerRight,
335
+ child: Padding(
336
+ padding: const EdgeInsets.only(bottom: 12),
337
+ child: _settingsSaveButton(controller),
338
+ ),
270
339
  ),
271
- ),
272
340
  if (controller.errorMessage != null) ...<Widget>[
273
341
  _InlineError(message: controller.errorMessage!),
274
342
  const SizedBox(height: 16),
275
343
  ],
276
- _buildSettingsOverview(controller, availableModels.length),
277
- const SizedBox(height: 16),
278
- _buildWorkspaceSection(controller),
279
- const SizedBox(height: 16),
280
- _buildModelsSection(
281
- controller: controller,
282
- modelChoices: modelChoices,
283
- routingModels: routingModels,
284
- availableModels: availableModels,
285
- enabledSmartModels: enabledSmartModels,
286
- ),
287
- const SizedBox(height: 16),
288
- _buildVoiceAndRecordingSection(
289
- controller: controller,
290
- modelChoices: modelChoices,
291
- routingModels: routingModels,
344
+ TextField(
345
+ controller: _searchController,
346
+ onChanged: (_) => setState(() {}),
347
+ decoration: InputDecoration(
348
+ labelText: 'Search settings',
349
+ hintText: 'Models, browser, voice, updates, diagnostics...',
350
+ prefixIcon: const Icon(Icons.search),
351
+ suffixIcon: searchQuery.isEmpty
352
+ ? null
353
+ : IconButton(
354
+ onPressed: () {
355
+ _searchController.clear();
356
+ setState(() {});
357
+ },
358
+ icon: const Icon(Icons.close),
359
+ ),
360
+ ),
292
361
  ),
293
362
  const SizedBox(height: 16),
294
- if (_supportsDesktopShell) ...<Widget>[
363
+ if (_matchesSettingsSection(
364
+ searchQuery,
365
+ _overviewSettingsSection,
366
+ )) ...<Widget>[
367
+ _buildSettingsOverview(controller, availableModels.length),
368
+ const SizedBox(height: 16),
369
+ ],
370
+ if (_matchesSettingsSection(
371
+ searchQuery,
372
+ _workspaceSettingsSection,
373
+ )) ...<Widget>[
374
+ _buildWorkspaceSection(controller),
375
+ const SizedBox(height: 16),
376
+ ],
377
+ if (_matchesSettingsSection(
378
+ searchQuery,
379
+ _modelsSettingsSection,
380
+ )) ...<Widget>[
381
+ _buildModelsSection(
382
+ controller: controller,
383
+ modelChoices: modelChoices,
384
+ routingModels: routingModels,
385
+ availableModels: availableModels,
386
+ enabledSmartModels: enabledSmartModels,
387
+ ),
388
+ const SizedBox(height: 16),
389
+ ],
390
+ if (_matchesSettingsSection(
391
+ searchQuery,
392
+ _voiceRecordingSettingsSection,
393
+ )) ...<Widget>[
394
+ _buildVoiceAndRecordingSection(
395
+ controller: controller,
396
+ modelChoices: modelChoices,
397
+ routingModels: routingModels,
398
+ ),
399
+ const SizedBox(height: 16),
400
+ ],
401
+ if (visibleSearchSections.contains(_desktopSettingsSection) &&
402
+ _matchesSettingsSection(
403
+ searchQuery,
404
+ _desktopSettingsSection,
405
+ )) ...<Widget>[
295
406
  _buildDesktopSection(controller),
296
407
  const SizedBox(height: 16),
297
408
  ],
298
- _buildUpdatesSection(controller),
299
- const SizedBox(height: 16),
300
- _buildDiagnosticsSection(controller),
409
+ if (_matchesSettingsSection(
410
+ searchQuery,
411
+ _updatesSettingsSection,
412
+ )) ...<Widget>[
413
+ _buildUpdatesSection(controller),
414
+ const SizedBox(height: 16),
415
+ ],
416
+ if (_matchesSettingsSection(
417
+ searchQuery,
418
+ _diagnosticsSettingsSection,
419
+ )) ...<Widget>[_buildDiagnosticsSection(controller)],
420
+ if (_noSettingsMatches(searchQuery, visibleSearchSections)) ...<Widget>[
421
+ const _EmptyCard(
422
+ title: 'No matching settings',
423
+ subtitle:
424
+ 'Try a broader search like models, browser, voice, or updates.',
425
+ ),
426
+ ],
301
427
  ],
302
428
  );
303
429
  }
304
430
 
431
+ bool _matchesSettingsSection(String query, _SettingsSection section) {
432
+ if (query.isEmpty) {
433
+ return true;
434
+ }
435
+ final haystack = <String>[
436
+ section.title,
437
+ ...section.keywords,
438
+ ].join(' ').toLowerCase();
439
+ return haystack.contains(query);
440
+ }
441
+
442
+ bool _noSettingsMatches(
443
+ String query,
444
+ Iterable<_SettingsSection> visibleSections,
445
+ ) {
446
+ if (query.isEmpty) {
447
+ return false;
448
+ }
449
+ return !visibleSections.any(
450
+ (section) => _matchesSettingsSection(query, section),
451
+ );
452
+ }
453
+
454
+ Widget _settingsSaveButton(NeoAgentController controller) {
455
+ return FilledButton.icon(
456
+ onPressed: controller.isSavingSettings
457
+ ? null
458
+ : () => controller.saveSettings(
459
+ headlessBrowser: _headlessBrowser,
460
+ browserBackend: _browserBackend == 'extension'
461
+ ? 'extension'
462
+ : 'vm',
463
+ smarterSelector: _smarterSelector,
464
+ enabledModels: _enabledModels.toList(),
465
+ defaultChatModel: _defaultChatModel,
466
+ defaultSubagentModel: _defaultSubagentModel,
467
+ defaultRecordingTranscriptionProvider: 'deepgram',
468
+ defaultRecordingTranscriptionModel:
469
+ _defaultRecordingTranscriptionModel,
470
+ defaultRecordingSummaryProvider: _providerForSelectedModel(
471
+ _defaultRecordingSummaryModel,
472
+ controller.supportedModels,
473
+ ),
474
+ defaultRecordingSummaryModel: _defaultRecordingSummaryModel,
475
+ fallbackModel: _fallbackModel,
476
+ defaultSpeechModel: _defaultSpeechModel,
477
+ voiceSttProvider: controller.voiceSttProvider,
478
+ voiceSttModel: controller.voiceSttModel,
479
+ voiceTtsProvider: controller.voiceTtsProvider,
480
+ voiceTtsModel: controller.voiceTtsModel,
481
+ voiceTtsVoice: controller.voiceTtsVoice,
482
+ voiceRuntimeMode: 'live',
483
+ voiceLiveProvider: _voiceLiveProvider,
484
+ voiceLiveModel: _voiceLiveModel,
485
+ voiceLiveVoice: _voiceLiveVoice,
486
+ aiProviderConfigs: _buildProviderPayload(),
487
+ ),
488
+ style: FilledButton.styleFrom(backgroundColor: _accent),
489
+ icon: controller.isSavingSettings
490
+ ? const SizedBox.square(
491
+ dimension: 16,
492
+ child: CircularProgressIndicator(
493
+ strokeWidth: 2,
494
+ color: Colors.white,
495
+ ),
496
+ )
497
+ : Icon(Icons.save_outlined),
498
+ label: Text('Save'),
499
+ );
500
+ }
501
+
305
502
  Widget _buildSettingsOverview(
306
503
  NeoAgentController controller,
307
504
  int availableModelCount,
@@ -409,12 +606,12 @@ class _SettingsPanelState extends State<SettingsPanel> {
409
606
  decoration: const InputDecoration(
410
607
  labelText: 'Browser backend',
411
608
  helperText:
412
- '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.',
413
610
  ),
414
611
  items: const <DropdownMenuItem<String>>[
415
612
  DropdownMenuItem<String>(
416
- value: 'cloud',
417
- child: Text('Cloud (local)'),
613
+ value: 'vm',
614
+ child: Text('Cloud'),
418
615
  ),
419
616
  DropdownMenuItem<String>(
420
617
  value: 'extension',
@@ -433,9 +630,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
433
630
  ? (controller.browserExtensionConnected
434
631
  ? 'Chrome extension connected.'
435
632
  : 'Chrome extension selected. Download it here, load it unpacked in Chrome on the remote machine, then pair after login.')
436
- : controller.cloudBrowserBackend == 'vm'
437
- ? "Cloud uses this deployment's isolated VM browser runtime."
438
- : "Cloud uses this deployment's local host browser runtime.",
633
+ : 'Cloud browser runtime is active.',
439
634
  style: TextStyle(color: _textSecondary, height: 1.4),
440
635
  ),
441
636
  const SizedBox(height: 10),
@@ -232,13 +232,10 @@ List<AppSection> _mainSections(NeoAgentController controller) {
232
232
  AppSection.chat,
233
233
  AppSection.recordings,
234
234
  AppSection.runs,
235
- AppSection.logs,
236
235
  AppSection.devices,
237
236
  AppSection.tasks,
238
237
  AppSection.widgets,
239
- AppSection.skills,
240
238
  AppSection.integrations,
241
- AppSection.mcp,
242
239
  AppSection.memory,
243
240
  if (controller.showHealthSection) AppSection.health,
244
241
  AppSection.settings,
@@ -256,7 +253,7 @@ List<Widget> _buildSidebarItems(
256
253
  final widgets = <Widget>[];
257
254
  final mainSections = _mainSections(controller);
258
255
  final selectedSidebarSection = mainSections.contains(
259
- controller.selectedSection,
256
+ controller.selectedSection.sidebarSection,
260
257
  );
261
258
  for (final group in SidebarGroup.values) {
262
259
  final sections = mainSections
@@ -267,7 +264,8 @@ List<Widget> _buildSidebarItems(
267
264
  }
268
265
 
269
266
  final active =
270
- selectedSidebarSection && controller.selectedSection.group == group;
267
+ selectedSidebarSection &&
268
+ controller.selectedSection.sidebarSection.group == group;
271
269
  final defaultSection = sections.first;
272
270
  final hasChildren = sections.length > 1;
273
271
  final expanded = expandedGroup == group;
@@ -299,7 +297,7 @@ List<Widget> _buildSidebarItems(
299
297
  _SidebarButton(
300
298
  label: section.label,
301
299
  icon: section.icon,
302
- active: controller.selectedSection == section,
300
+ active: controller.selectedSection.sidebarSection == section,
303
301
  indent: 18,
304
302
  iconSize: 16,
305
303
  fontSize: 12,
@@ -1135,6 +1133,10 @@ class _ChatBubble extends StatelessWidget {
1135
1133
  final isUser = entry.role == 'user';
1136
1134
  final isTransient = entry.transient;
1137
1135
 
1136
+ if (entry.typing) {
1137
+ return const _TypingIndicatorBubble();
1138
+ }
1139
+
1138
1140
  return Opacity(
1139
1141
  opacity: isTransient ? 0.92 : 1,
1140
1142
  child: Row(