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
@@ -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
 
@@ -343,9 +433,7 @@ class _MessagingPanelState extends State<MessagingPanel> {
343
433
  const (
344
434
  'Hardware Bridges',
345
435
  'Local device bridges and TCP-connected integrations.',
346
- [
347
- 'meshtastic',
348
- ],
436
+ ['meshtastic'],
349
437
  ),
350
438
  const ('Voice', 'Telephony integrations.', ['telnyx']),
351
439
  ];
@@ -440,7 +528,11 @@ class _MessagingPanelState extends State<MessagingPanel> {
440
528
  accessCatalog: controller
441
529
  .currentMessagingAccessCatalog(platform.id),
442
530
  controller: controller,
443
- onConnect: () => openMessagingConfig(context, controller, platform),
531
+ onConnect: () => openMessagingConfig(
532
+ context,
533
+ controller,
534
+ platform,
535
+ ),
444
536
  onDisconnect: () => controller
445
537
  .disconnectMessagingPlatform(platform.id),
446
538
  onLogout: () => controller
@@ -1017,9 +1109,10 @@ class _MessagingActivityItem extends StatelessWidget {
1017
1109
  }
1018
1110
 
1019
1111
  class RunsPanel extends StatefulWidget {
1020
- const RunsPanel({super.key, required this.controller});
1112
+ const RunsPanel({super.key, required this.controller, this.embedded = false});
1021
1113
 
1022
1114
  final NeoAgentController controller;
1115
+ final bool embedded;
1023
1116
 
1024
1117
  @override
1025
1118
  State<RunsPanel> createState() => _RunsPanelState();
@@ -1210,18 +1303,31 @@ class _RunsPanelState extends State<RunsPanel> {
1210
1303
  final detail = _detail?.run.id == selected?.id ? _detail : null;
1211
1304
 
1212
1305
  return ListView(
1213
- padding: _pagePadding(context),
1306
+ padding: widget.embedded ? EdgeInsets.zero : _pagePadding(context),
1214
1307
  children: <Widget>[
1215
- _PageTitle(
1216
- title: 'Runs',
1217
- subtitle:
1218
- 'Inspect recent runs, failures, tool steps, and final responses.',
1219
- trailing: OutlinedButton.icon(
1220
- onPressed: _refreshRuns,
1221
- icon: Icon(Icons.refresh),
1222
- label: Text('Refresh'),
1308
+ if (!widget.embedded)
1309
+ _PageTitle(
1310
+ title: 'Runs',
1311
+ subtitle:
1312
+ 'Inspect recent runs, failures, tool steps, and final responses.',
1313
+ trailing: OutlinedButton.icon(
1314
+ onPressed: _refreshRuns,
1315
+ icon: Icon(Icons.refresh),
1316
+ label: Text('Refresh'),
1317
+ ),
1318
+ )
1319
+ else
1320
+ Align(
1321
+ alignment: Alignment.centerRight,
1322
+ child: Padding(
1323
+ padding: const EdgeInsets.only(bottom: 12),
1324
+ child: OutlinedButton.icon(
1325
+ onPressed: _refreshRuns,
1326
+ icon: const Icon(Icons.refresh),
1327
+ label: const Text('Refresh'),
1328
+ ),
1329
+ ),
1223
1330
  ),
1224
- ),
1225
1331
  if (controller.errorMessage != null) ...<Widget>[
1226
1332
  _InlineError(message: controller.errorMessage!),
1227
1333
  const SizedBox(height: 16),
@@ -2477,6 +2583,13 @@ class _RunHistoryRow extends StatelessWidget {
2477
2583
  '${run.modelLabel} • ${run.totalTokensLabel} tokens',
2478
2584
  style: TextStyle(color: _textSecondary, fontSize: 12),
2479
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
+ ],
2480
2593
  if (run.error.trim().isNotEmpty) ...<Widget>[
2481
2594
  const SizedBox(height: 8),
2482
2595
  Text(
@@ -2595,6 +2708,12 @@ class _RunDetailWorkspace extends StatelessWidget {
2595
2708
  helper: 'Subagents or helpers',
2596
2709
  color: _accentHover,
2597
2710
  ),
2711
+ _RunMetricCard(
2712
+ title: 'Trace events',
2713
+ value: '${snapshot.events.length}',
2714
+ helper: 'Structured run timeline',
2715
+ color: _warning,
2716
+ ),
2598
2717
  ],
2599
2718
  ),
2600
2719
  const SizedBox(height: 16),
@@ -2602,8 +2721,14 @@ class _RunDetailWorkspace extends StatelessWidget {
2602
2721
  response: snapshot.response,
2603
2722
  onCopy: () => onCopyResponse(snapshot.response),
2604
2723
  ),
2724
+ if (snapshot.run.deliverableType.trim().isNotEmpty) ...<Widget>[
2725
+ const SizedBox(height: 16),
2726
+ _DeliverableSummaryCard(run: snapshot.run),
2727
+ ],
2605
2728
  const SizedBox(height: 16),
2606
2729
  _RunTimelineCard(steps: snapshot.steps, loading: loading),
2730
+ const SizedBox(height: 16),
2731
+ _RunEventTimelineCard(events: snapshot.events, loading: loading),
2607
2732
  ] else
2608
2733
  const _EmptyCard(
2609
2734
  title: 'No detail available',
@@ -2663,6 +2788,11 @@ class _RunHeroCard extends StatelessWidget {
2663
2788
  label: run.modelLabel,
2664
2789
  icon: Icons.memory_outlined,
2665
2790
  ),
2791
+ if (run.deliverableType.trim().isNotEmpty)
2792
+ _MetaPill(
2793
+ label: run.deliverableType.replaceAll('_', ' '),
2794
+ icon: Icons.inventory_2_outlined,
2795
+ ),
2666
2796
  ],
2667
2797
  ),
2668
2798
  const SizedBox(height: 16),
@@ -2788,6 +2918,89 @@ class _RunResponseCard extends StatelessWidget {
2788
2918
  }
2789
2919
  }
2790
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
+
2791
3004
  class _RunTimelineCard extends StatelessWidget {
2792
3005
  const _RunTimelineCard({required this.steps, required this.loading});
2793
3006
 
@@ -2832,6 +3045,105 @@ class _RunTimelineCard extends StatelessWidget {
2832
3045
  }
2833
3046
  }
2834
3047
 
3048
+ class _RunEventTimelineCard extends StatelessWidget {
3049
+ const _RunEventTimelineCard({required this.events, required this.loading});
3050
+
3051
+ final List<RunEventItem> events;
3052
+ final bool loading;
3053
+
3054
+ @override
3055
+ Widget build(BuildContext context) {
3056
+ return Card(
3057
+ child: Padding(
3058
+ padding: const EdgeInsets.all(18),
3059
+ child: Column(
3060
+ crossAxisAlignment: CrossAxisAlignment.start,
3061
+ children: <Widget>[
3062
+ Row(
3063
+ children: <Widget>[
3064
+ Expanded(child: _SectionTitle('Execution Trace')),
3065
+ if (loading)
3066
+ const SizedBox.square(
3067
+ dimension: 16,
3068
+ child: CircularProgressIndicator(strokeWidth: 2),
3069
+ ),
3070
+ ],
3071
+ ),
3072
+ const SizedBox(height: 12),
3073
+ if (events.isEmpty)
3074
+ Text(
3075
+ 'No structured run events recorded yet.',
3076
+ style: TextStyle(color: _textSecondary),
3077
+ )
3078
+ else
3079
+ ...events.map((event) {
3080
+ return Padding(
3081
+ padding: const EdgeInsets.only(bottom: 10),
3082
+ child: _RunEventCard(event: event),
3083
+ );
3084
+ }),
3085
+ ],
3086
+ ),
3087
+ ),
3088
+ );
3089
+ }
3090
+ }
3091
+
3092
+ class _RunEventCard extends StatelessWidget {
3093
+ const _RunEventCard({required this.event});
3094
+
3095
+ final RunEventItem event;
3096
+
3097
+ @override
3098
+ Widget build(BuildContext context) {
3099
+ final accent = event.isFailure ? _danger : _info;
3100
+ return Container(
3101
+ padding: const EdgeInsets.all(14),
3102
+ decoration: BoxDecoration(
3103
+ color: _bgSecondary,
3104
+ borderRadius: BorderRadius.circular(16),
3105
+ border: Border.all(color: accent.withValues(alpha: 0.24)),
3106
+ ),
3107
+ child: Column(
3108
+ crossAxisAlignment: CrossAxisAlignment.start,
3109
+ children: <Widget>[
3110
+ Row(
3111
+ children: <Widget>[
3112
+ Container(
3113
+ width: 10,
3114
+ height: 10,
3115
+ decoration: BoxDecoration(
3116
+ color: accent,
3117
+ shape: BoxShape.circle,
3118
+ ),
3119
+ ),
3120
+ const SizedBox(width: 10),
3121
+ Expanded(
3122
+ child: Text(
3123
+ event.title,
3124
+ style: const TextStyle(fontWeight: FontWeight.w700),
3125
+ ),
3126
+ ),
3127
+ if (event.createdAtLabel.isNotEmpty)
3128
+ Text(
3129
+ event.createdAtLabel,
3130
+ style: TextStyle(color: _textSecondary, fontSize: 12),
3131
+ ),
3132
+ ],
3133
+ ),
3134
+ if (event.detail.trim().isNotEmpty) ...<Widget>[
3135
+ const SizedBox(height: 8),
3136
+ Text(
3137
+ event.detail,
3138
+ style: TextStyle(color: _textSecondary, height: 1.4),
3139
+ ),
3140
+ ],
3141
+ ],
3142
+ ),
3143
+ );
3144
+ }
3145
+ }
3146
+
2835
3147
  class _RunStepCard extends StatelessWidget {
2836
3148
  const _RunStepCard({required this.step});
2837
3149
 
@@ -2994,323 +3306,324 @@ class _RunDetailBlock extends StatelessWidget {
2994
3306
  }
2995
3307
  }
2996
3308
 
2997
- Future<void> openMessagingConfig(BuildContext context, NeoAgentController controller,
2998
- MessagingPlatformDescriptor platform,
2999
- ) async {
3000
- switch (platform.id) {
3001
- case 'whatsapp':
3002
- await _connectMessagingPlatformHelper(context, controller,
3003
- platform: 'whatsapp',
3004
- platformLabel: platform.label,
3005
- );
3006
- return;
3007
- case 'telnyx':
3008
- return _openTelnyxConfigHelper(context, controller);
3009
- default:
3010
- return _openGenericMessagingConfigHelper(context, controller, platform);
3011
- }
3012
- }
3013
-
3014
- Future<bool> _connectMessagingPlatformHelper(BuildContext context, NeoAgentController controller, {
3015
- required String platform,
3016
- required String platformLabel,
3017
- Map<String, dynamic>? config,
3018
- Map<String, dynamic>? configSnapshot,
3019
- }) async {
3020
- try {
3021
- await controller.connectMessagingPlatform(
3022
- platform: platform,
3023
- config: config,
3024
- configSnapshot: configSnapshot,
3025
- );
3026
- return true;
3027
- } catch (error) {
3028
- if (!context.mounted) return false;
3029
- final messenger = ScaffoldMessenger.maybeOf(context);
3030
- messenger?.showSnackBar(
3031
- SnackBar(
3032
- content: Text(
3033
- 'Failed to connect $platformLabel: ${controller.friendlyErrorMessage(error)}',
3034
- ),
3035
- ),
3309
+ Future<void> openMessagingConfig(
3310
+ BuildContext context,
3311
+ NeoAgentController controller,
3312
+ MessagingPlatformDescriptor platform,
3313
+ ) async {
3314
+ switch (platform.id) {
3315
+ case 'whatsapp':
3316
+ await _connectMessagingPlatformHelper(
3317
+ context,
3318
+ controller,
3319
+ platform: 'whatsapp',
3320
+ platformLabel: platform.label,
3036
3321
  );
3037
- return false;
3038
- }
3322
+ return;
3323
+ case 'telnyx':
3324
+ return _openTelnyxConfigHelper(context, controller);
3325
+ default:
3326
+ return _openGenericMessagingConfigHelper(context, controller, platform);
3039
3327
  }
3328
+ }
3040
3329
 
3041
- Future<void> _openTelnyxConfigHelper(BuildContext context, NeoAgentController controller) async {
3042
- final saved = _jsonMap(
3043
- _decodeMaybeJson(controller.settings['telnyx_config']),
3044
- );
3045
- final apiKey = TextEditingController(
3046
- text: saved['apiKey']?.toString() ?? '',
3047
- );
3048
- final phoneNumber = TextEditingController(
3049
- text: saved['phoneNumber']?.toString() ?? '',
3050
- );
3051
- final connectionId = TextEditingController(
3052
- text: saved['connectionId']?.toString() ?? '',
3330
+ Future<bool> _connectMessagingPlatformHelper(
3331
+ BuildContext context,
3332
+ NeoAgentController controller, {
3333
+ required String platform,
3334
+ required String platformLabel,
3335
+ Map<String, dynamic>? config,
3336
+ Map<String, dynamic>? configSnapshot,
3337
+ }) async {
3338
+ try {
3339
+ await controller.connectMessagingPlatform(
3340
+ platform: platform,
3341
+ config: config,
3342
+ configSnapshot: configSnapshot,
3053
3343
  );
3054
- final webhookUrl = TextEditingController(
3055
- text: saved['webhookUrl']?.toString() ?? controller.backendUrl,
3344
+ return true;
3345
+ } catch (error) {
3346
+ if (!context.mounted) return false;
3347
+ final messenger = ScaffoldMessenger.maybeOf(context);
3348
+ messenger?.showSnackBar(
3349
+ SnackBar(
3350
+ content: Text(
3351
+ 'Failed to connect $platformLabel: ${controller.friendlyErrorMessage(error)}',
3352
+ ),
3353
+ ),
3056
3354
  );
3355
+ return false;
3356
+ }
3357
+ }
3057
3358
 
3058
- try {
3059
- await showDialog<void>(
3060
- context: context,
3061
- builder: (context) {
3062
- return StatefulBuilder(
3063
- builder: (context, setLocalState) {
3064
- return AlertDialog(
3065
- backgroundColor: _bgCard,
3066
- title: Text('Telnyx Voice'),
3067
- content: SizedBox(
3068
- width: 620,
3069
- child: SingleChildScrollView(
3070
- child: Column(
3071
- mainAxisSize: MainAxisSize.min,
3072
- children: <Widget>[
3073
- TextField(
3074
- controller: apiKey,
3075
- obscureText: true,
3076
- decoration: const InputDecoration(
3077
- labelText: 'API Key',
3078
- ),
3079
- ),
3080
- const SizedBox(height: 12),
3081
- TextField(
3082
- controller: phoneNumber,
3083
- decoration: const InputDecoration(
3084
- labelText: 'Phone Number',
3085
- ),
3086
- ),
3087
- const SizedBox(height: 12),
3088
- TextField(
3089
- controller: connectionId,
3090
- decoration: const InputDecoration(
3091
- labelText: 'Connection ID',
3092
- ),
3359
+ Future<void> _openTelnyxConfigHelper(
3360
+ BuildContext context,
3361
+ NeoAgentController controller,
3362
+ ) async {
3363
+ final saved = _jsonMap(
3364
+ _decodeMaybeJson(controller.settings['telnyx_config']),
3365
+ );
3366
+ final apiKey = TextEditingController(text: saved['apiKey']?.toString() ?? '');
3367
+ final phoneNumber = TextEditingController(
3368
+ text: saved['phoneNumber']?.toString() ?? '',
3369
+ );
3370
+ final connectionId = TextEditingController(
3371
+ text: saved['connectionId']?.toString() ?? '',
3372
+ );
3373
+ final webhookUrl = TextEditingController(
3374
+ text: saved['webhookUrl']?.toString() ?? controller.backendUrl,
3375
+ );
3376
+
3377
+ try {
3378
+ await showDialog<void>(
3379
+ context: context,
3380
+ builder: (context) {
3381
+ return StatefulBuilder(
3382
+ builder: (context, setLocalState) {
3383
+ return AlertDialog(
3384
+ backgroundColor: _bgCard,
3385
+ title: Text('Telnyx Voice'),
3386
+ content: SizedBox(
3387
+ width: 620,
3388
+ child: SingleChildScrollView(
3389
+ child: Column(
3390
+ mainAxisSize: MainAxisSize.min,
3391
+ children: <Widget>[
3392
+ TextField(
3393
+ controller: apiKey,
3394
+ obscureText: true,
3395
+ decoration: const InputDecoration(labelText: 'API Key'),
3396
+ ),
3397
+ const SizedBox(height: 12),
3398
+ TextField(
3399
+ controller: phoneNumber,
3400
+ decoration: const InputDecoration(
3401
+ labelText: 'Phone Number',
3093
3402
  ),
3094
- const SizedBox(height: 12),
3095
- TextField(
3096
- controller: webhookUrl,
3097
- decoration: const InputDecoration(
3098
- labelText: 'Webhook Base URL',
3099
- ),
3403
+ ),
3404
+ const SizedBox(height: 12),
3405
+ TextField(
3406
+ controller: connectionId,
3407
+ decoration: const InputDecoration(
3408
+ labelText: 'Connection ID',
3100
3409
  ),
3101
- const SizedBox(height: 12),
3102
- Text(
3103
- 'Voice STT/TTS providers and models are configured in global Settings > Voice.',
3104
- style: TextStyle(color: _textSecondary),
3410
+ ),
3411
+ const SizedBox(height: 12),
3412
+ TextField(
3413
+ controller: webhookUrl,
3414
+ decoration: const InputDecoration(
3415
+ labelText: 'Webhook Base URL',
3105
3416
  ),
3106
- ],
3107
- ),
3417
+ ),
3418
+ const SizedBox(height: 12),
3419
+ Text(
3420
+ 'Voice STT/TTS providers and models are configured in global Settings > Voice.',
3421
+ style: TextStyle(color: _textSecondary),
3422
+ ),
3423
+ ],
3108
3424
  ),
3109
3425
  ),
3110
- actions: <Widget>[
3111
- TextButton(
3112
- onPressed: () => Navigator.of(context).pop(),
3113
- child: Text('Cancel'),
3114
- ),
3115
- FilledButton(
3116
- onPressed: () async {
3117
- final config = <String, dynamic>{
3118
- 'apiKey': apiKey.text.trim(),
3119
- 'phoneNumber': phoneNumber.text.trim(),
3120
- 'connectionId': connectionId.text.trim(),
3121
- 'webhookUrl': webhookUrl.text.trim(),
3122
- };
3123
- final connected = await _connectMessagingPlatformHelper(context, controller,
3124
- platform: 'telnyx',
3125
- platformLabel: 'Telnyx Voice',
3126
- config: config,
3127
- configSnapshot: <String, dynamic>{
3128
- 'telnyx_config': jsonEncode(config),
3129
- },
3130
- );
3131
- if (connected && context.mounted) {
3132
- Navigator.of(context).pop();
3133
- }
3134
- },
3135
- child: Text('Connect'),
3136
- ),
3137
- ],
3138
- );
3139
- },
3140
- );
3141
- },
3142
- );
3143
- } finally {
3144
- apiKey.dispose();
3145
- phoneNumber.dispose();
3146
- connectionId.dispose();
3147
- webhookUrl.dispose();
3148
- }
3426
+ ),
3427
+ actions: <Widget>[
3428
+ TextButton(
3429
+ onPressed: () => Navigator.of(context).pop(),
3430
+ child: Text('Cancel'),
3431
+ ),
3432
+ FilledButton(
3433
+ onPressed: () async {
3434
+ final config = <String, dynamic>{
3435
+ 'apiKey': apiKey.text.trim(),
3436
+ 'phoneNumber': phoneNumber.text.trim(),
3437
+ 'connectionId': connectionId.text.trim(),
3438
+ 'webhookUrl': webhookUrl.text.trim(),
3439
+ };
3440
+ final connected = await _connectMessagingPlatformHelper(
3441
+ context,
3442
+ controller,
3443
+ platform: 'telnyx',
3444
+ platformLabel: 'Telnyx Voice',
3445
+ config: config,
3446
+ configSnapshot: <String, dynamic>{
3447
+ 'telnyx_config': jsonEncode(config),
3448
+ },
3449
+ );
3450
+ if (connected && context.mounted) {
3451
+ Navigator.of(context).pop();
3452
+ }
3453
+ },
3454
+ child: Text('Connect'),
3455
+ ),
3456
+ ],
3457
+ );
3458
+ },
3459
+ );
3460
+ },
3461
+ );
3462
+ } finally {
3463
+ apiKey.dispose();
3464
+ phoneNumber.dispose();
3465
+ connectionId.dispose();
3466
+ webhookUrl.dispose();
3149
3467
  }
3468
+ }
3150
3469
 
3151
- Future<void> _openGenericMessagingConfigHelper(BuildContext context, NeoAgentController controller,
3152
- MessagingPlatformDescriptor platform,
3153
- ) async {
3154
- final saved = _jsonMap(
3155
- _decodeMaybeJson(controller.settings[platform.settingsKey]),
3156
- );
3157
- final textControllers = <String, TextEditingController>{};
3158
- final boolValues = <String, bool>{};
3159
- for (final field in platform.configFields) {
3160
- final savedValue = field.settingsKey == null
3161
- ? saved[field.key]
3162
- : controller.settings[field.storageKey];
3163
- if (field.kind == MessagingConfigFieldKind.boolean) {
3164
- boolValues[field.key] =
3165
- savedValue == true || savedValue?.toString() == 'true';
3166
- } else {
3167
- textControllers[field.key] = TextEditingController(
3168
- text: savedValue?.toString() ?? field.defaultValue ?? '',
3169
- );
3170
- }
3470
+ Future<void> _openGenericMessagingConfigHelper(
3471
+ BuildContext context,
3472
+ NeoAgentController controller,
3473
+ MessagingPlatformDescriptor platform,
3474
+ ) async {
3475
+ final saved = _jsonMap(
3476
+ _decodeMaybeJson(controller.settings[platform.settingsKey]),
3477
+ );
3478
+ final textControllers = <String, TextEditingController>{};
3479
+ final boolValues = <String, bool>{};
3480
+ for (final field in platform.configFields) {
3481
+ final savedValue = field.settingsKey == null
3482
+ ? saved[field.key]
3483
+ : controller.settings[field.storageKey];
3484
+ if (field.kind == MessagingConfigFieldKind.boolean) {
3485
+ boolValues[field.key] =
3486
+ savedValue == true || savedValue?.toString() == 'true';
3487
+ } else {
3488
+ textControllers[field.key] = TextEditingController(
3489
+ text: savedValue?.toString() ?? field.defaultValue ?? '',
3490
+ );
3171
3491
  }
3492
+ }
3172
3493
 
3173
- try {
3174
- await showDialog<void>(
3175
- context: context,
3176
- builder: (context) {
3177
- return StatefulBuilder(
3178
- builder: (context, setLocalState) {
3179
- return AlertDialog(
3180
- backgroundColor: _bgCard,
3181
- title: Text(platform.label),
3182
- content: SizedBox(
3183
- width: 620,
3184
- child: SingleChildScrollView(
3185
- child: Column(
3186
- mainAxisSize: MainAxisSize.min,
3187
- children: <Widget>[
3188
- if (platform.configFields.isEmpty)
3189
- Text(
3190
- 'No extra settings are required.',
3191
- style: TextStyle(color: _textSecondary),
3192
- )
3193
- else
3194
- ...platform.configFields.map((field) {
3195
- if (field.kind ==
3196
- MessagingConfigFieldKind.boolean) {
3197
- return SwitchListTile(
3198
- contentPadding: EdgeInsets.zero,
3199
- title: Text(field.label),
3200
- value: boolValues[field.key] ?? false,
3201
- onChanged: (value) {
3202
- setLocalState(() {
3203
- boolValues[field.key] = value;
3204
- });
3205
- },
3206
- );
3207
- }
3208
- final controller = textControllers[field.key]!;
3209
- return Padding(
3210
- padding: const EdgeInsets.only(bottom: 12),
3211
- child: TextField(
3212
- controller: controller,
3213
- obscureText:
3214
- field.obscure ||
3215
- field.kind ==
3216
- MessagingConfigFieldKind.password,
3217
- minLines:
3218
- field.kind ==
3219
- MessagingConfigFieldKind.multiline
3220
- ? 4
3221
- : 1,
3222
- maxLines:
3223
- field.kind ==
3224
- MessagingConfigFieldKind.multiline
3225
- ? 8
3226
- : 1,
3227
- decoration: InputDecoration(
3228
- labelText: field.label,
3229
- ),
3230
- ),
3494
+ try {
3495
+ await showDialog<void>(
3496
+ context: context,
3497
+ builder: (context) {
3498
+ return StatefulBuilder(
3499
+ builder: (context, setLocalState) {
3500
+ return AlertDialog(
3501
+ backgroundColor: _bgCard,
3502
+ title: Text(platform.label),
3503
+ content: SizedBox(
3504
+ width: 620,
3505
+ child: SingleChildScrollView(
3506
+ child: Column(
3507
+ mainAxisSize: MainAxisSize.min,
3508
+ children: <Widget>[
3509
+ if (platform.configFields.isEmpty)
3510
+ Text(
3511
+ 'No extra settings are required.',
3512
+ style: TextStyle(color: _textSecondary),
3513
+ )
3514
+ else
3515
+ ...platform.configFields.map((field) {
3516
+ if (field.kind == MessagingConfigFieldKind.boolean) {
3517
+ return SwitchListTile(
3518
+ contentPadding: EdgeInsets.zero,
3519
+ title: Text(field.label),
3520
+ value: boolValues[field.key] ?? false,
3521
+ onChanged: (value) {
3522
+ setLocalState(() {
3523
+ boolValues[field.key] = value;
3524
+ });
3525
+ },
3231
3526
  );
3232
- }),
3233
- const SizedBox(height: 8),
3234
- if (platform.id == 'meshtastic')
3235
- Text(
3236
- 'Meshtastic connects directly to the device TCP API on port 4403 by default. Normal chat is limited to the configured channel.',
3237
- style: TextStyle(
3238
- color: _textSecondary,
3239
- fontSize: 12,
3240
- ),
3241
- )
3242
- else
3243
- SelectableText(
3244
- 'Inbound webhook: ${controller.backendUrl}/api/messaging/webhook/${platform.id}',
3245
- style: TextStyle(
3246
- color: _textSecondary,
3247
- fontSize: 12,
3527
+ }
3528
+ final controller = textControllers[field.key]!;
3529
+ return Padding(
3530
+ padding: const EdgeInsets.only(bottom: 12),
3531
+ child: TextField(
3532
+ controller: controller,
3533
+ obscureText:
3534
+ field.obscure ||
3535
+ field.kind ==
3536
+ MessagingConfigFieldKind.password,
3537
+ minLines:
3538
+ field.kind ==
3539
+ MessagingConfigFieldKind.multiline
3540
+ ? 4
3541
+ : 1,
3542
+ maxLines:
3543
+ field.kind ==
3544
+ MessagingConfigFieldKind.multiline
3545
+ ? 8
3546
+ : 1,
3547
+ decoration: InputDecoration(
3548
+ labelText: field.label,
3549
+ ),
3248
3550
  ),
3249
- ),
3250
- ],
3251
- ),
3551
+ );
3552
+ }),
3553
+ const SizedBox(height: 8),
3554
+ if (platform.id == 'meshtastic')
3555
+ Text(
3556
+ 'Meshtastic connects directly to the device TCP API on port 4403 by default. Normal chat is limited to the configured channel.',
3557
+ style: TextStyle(color: _textSecondary, fontSize: 12),
3558
+ )
3559
+ else
3560
+ SelectableText(
3561
+ 'Inbound webhook: ${controller.backendUrl}/api/messaging/webhook/${platform.id}',
3562
+ style: TextStyle(color: _textSecondary, fontSize: 12),
3563
+ ),
3564
+ ],
3252
3565
  ),
3253
3566
  ),
3254
- actions: <Widget>[
3255
- TextButton(
3256
- onPressed: () => Navigator.of(context).pop(),
3257
- child: Text('Cancel'),
3258
- ),
3259
- FilledButton(
3260
- onPressed: () async {
3261
- final config = <String, dynamic>{};
3262
- final snapshot = <String, dynamic>{};
3263
- for (final field in platform.configFields) {
3264
- if (field.kind == MessagingConfigFieldKind.boolean ||
3265
- !field.includeInConfig) {
3266
- continue;
3567
+ ),
3568
+ actions: <Widget>[
3569
+ TextButton(
3570
+ onPressed: () => Navigator.of(context).pop(),
3571
+ child: Text('Cancel'),
3572
+ ),
3573
+ FilledButton(
3574
+ onPressed: () async {
3575
+ final config = <String, dynamic>{};
3576
+ final snapshot = <String, dynamic>{};
3577
+ for (final field in platform.configFields) {
3578
+ if (field.kind == MessagingConfigFieldKind.boolean ||
3579
+ !field.includeInConfig) {
3580
+ continue;
3581
+ }
3582
+ final controller = textControllers[field.key];
3583
+ final value = controller?.text.trim() ?? '';
3584
+ if (value.isNotEmpty) config[field.key] = value;
3585
+ }
3586
+ for (final field in platform.configFields) {
3587
+ if (field.kind == MessagingConfigFieldKind.boolean) {
3588
+ final value = boolValues[field.key] ?? false;
3589
+ if (field.includeInConfig) {
3590
+ config[field.key] = value;
3591
+ }
3592
+ if (field.settingsKey != null) {
3593
+ snapshot[field.storageKey] = value;
3267
3594
  }
3595
+ } else if (field.settingsKey != null) {
3268
3596
  final controller = textControllers[field.key];
3269
3597
  final value = controller?.text.trim() ?? '';
3270
- if (value.isNotEmpty) config[field.key] = value;
3271
- }
3272
- for (final field in platform.configFields) {
3273
- if (field.kind == MessagingConfigFieldKind.boolean) {
3274
- final value = boolValues[field.key] ?? false;
3275
- if (field.includeInConfig) {
3276
- config[field.key] = value;
3277
- }
3278
- if (field.settingsKey != null) {
3279
- snapshot[field.storageKey] = value;
3280
- }
3281
- } else if (field.settingsKey != null) {
3282
- final controller = textControllers[field.key];
3283
- final value = controller?.text.trim() ?? '';
3284
- if (value.isNotEmpty) {
3285
- snapshot[field.storageKey] = value;
3286
- }
3598
+ if (value.isNotEmpty) {
3599
+ snapshot[field.storageKey] = value;
3287
3600
  }
3288
3601
  }
3289
- snapshot[platform.settingsKey] = jsonEncode(config);
3290
- final connected = await _connectMessagingPlatformHelper(context, controller,
3291
- platform: platform.id,
3292
- platformLabel: platform.label,
3293
- config: config,
3294
- configSnapshot: snapshot,
3295
- );
3296
- if (connected && context.mounted) {
3297
- Navigator.of(context).pop();
3298
- }
3299
- },
3300
- child: Text(
3301
- 'Connect',
3302
- ),
3303
- ),
3304
- ],
3305
- );
3306
- },
3307
- );
3308
- },
3309
- );
3310
- } finally {
3311
- for (final controller in textControllers.values) {
3312
- controller.dispose();
3313
- }
3602
+ }
3603
+ snapshot[platform.settingsKey] = jsonEncode(config);
3604
+ final connected = await _connectMessagingPlatformHelper(
3605
+ context,
3606
+ controller,
3607
+ platform: platform.id,
3608
+ platformLabel: platform.label,
3609
+ config: config,
3610
+ configSnapshot: snapshot,
3611
+ );
3612
+ if (connected && context.mounted) {
3613
+ Navigator.of(context).pop();
3614
+ }
3615
+ },
3616
+ child: Text('Connect'),
3617
+ ),
3618
+ ],
3619
+ );
3620
+ },
3621
+ );
3622
+ },
3623
+ );
3624
+ } finally {
3625
+ for (final controller in textControllers.values) {
3626
+ controller.dispose();
3314
3627
  }
3315
3628
  }
3316
-
3629
+ }