neoagent 2.3.1-beta.63 → 2.3.1-beta.64

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.
@@ -343,9 +343,7 @@ class _MessagingPanelState extends State<MessagingPanel> {
343
343
  const (
344
344
  'Hardware Bridges',
345
345
  'Local device bridges and TCP-connected integrations.',
346
- [
347
- 'meshtastic',
348
- ],
346
+ ['meshtastic'],
349
347
  ),
350
348
  const ('Voice', 'Telephony integrations.', ['telnyx']),
351
349
  ];
@@ -440,7 +438,11 @@ class _MessagingPanelState extends State<MessagingPanel> {
440
438
  accessCatalog: controller
441
439
  .currentMessagingAccessCatalog(platform.id),
442
440
  controller: controller,
443
- onConnect: () => openMessagingConfig(context, controller, platform),
441
+ onConnect: () => openMessagingConfig(
442
+ context,
443
+ controller,
444
+ platform,
445
+ ),
444
446
  onDisconnect: () => controller
445
447
  .disconnectMessagingPlatform(platform.id),
446
448
  onLogout: () => controller
@@ -1017,9 +1019,10 @@ class _MessagingActivityItem extends StatelessWidget {
1017
1019
  }
1018
1020
 
1019
1021
  class RunsPanel extends StatefulWidget {
1020
- const RunsPanel({super.key, required this.controller});
1022
+ const RunsPanel({super.key, required this.controller, this.embedded = false});
1021
1023
 
1022
1024
  final NeoAgentController controller;
1025
+ final bool embedded;
1023
1026
 
1024
1027
  @override
1025
1028
  State<RunsPanel> createState() => _RunsPanelState();
@@ -1210,18 +1213,31 @@ class _RunsPanelState extends State<RunsPanel> {
1210
1213
  final detail = _detail?.run.id == selected?.id ? _detail : null;
1211
1214
 
1212
1215
  return ListView(
1213
- padding: _pagePadding(context),
1216
+ padding: widget.embedded ? EdgeInsets.zero : _pagePadding(context),
1214
1217
  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'),
1218
+ if (!widget.embedded)
1219
+ _PageTitle(
1220
+ title: 'Runs',
1221
+ subtitle:
1222
+ 'Inspect recent runs, failures, tool steps, and final responses.',
1223
+ trailing: OutlinedButton.icon(
1224
+ onPressed: _refreshRuns,
1225
+ icon: Icon(Icons.refresh),
1226
+ label: Text('Refresh'),
1227
+ ),
1228
+ )
1229
+ else
1230
+ Align(
1231
+ alignment: Alignment.centerRight,
1232
+ child: Padding(
1233
+ padding: const EdgeInsets.only(bottom: 12),
1234
+ child: OutlinedButton.icon(
1235
+ onPressed: _refreshRuns,
1236
+ icon: const Icon(Icons.refresh),
1237
+ label: const Text('Refresh'),
1238
+ ),
1239
+ ),
1223
1240
  ),
1224
- ),
1225
1241
  if (controller.errorMessage != null) ...<Widget>[
1226
1242
  _InlineError(message: controller.errorMessage!),
1227
1243
  const SizedBox(height: 16),
@@ -2595,6 +2611,12 @@ class _RunDetailWorkspace extends StatelessWidget {
2595
2611
  helper: 'Subagents or helpers',
2596
2612
  color: _accentHover,
2597
2613
  ),
2614
+ _RunMetricCard(
2615
+ title: 'Trace events',
2616
+ value: '${snapshot.events.length}',
2617
+ helper: 'Structured run timeline',
2618
+ color: _warning,
2619
+ ),
2598
2620
  ],
2599
2621
  ),
2600
2622
  const SizedBox(height: 16),
@@ -2604,6 +2626,8 @@ class _RunDetailWorkspace extends StatelessWidget {
2604
2626
  ),
2605
2627
  const SizedBox(height: 16),
2606
2628
  _RunTimelineCard(steps: snapshot.steps, loading: loading),
2629
+ const SizedBox(height: 16),
2630
+ _RunEventTimelineCard(events: snapshot.events, loading: loading),
2607
2631
  ] else
2608
2632
  const _EmptyCard(
2609
2633
  title: 'No detail available',
@@ -2832,6 +2856,105 @@ class _RunTimelineCard extends StatelessWidget {
2832
2856
  }
2833
2857
  }
2834
2858
 
2859
+ class _RunEventTimelineCard extends StatelessWidget {
2860
+ const _RunEventTimelineCard({required this.events, required this.loading});
2861
+
2862
+ final List<RunEventItem> events;
2863
+ final bool loading;
2864
+
2865
+ @override
2866
+ Widget build(BuildContext context) {
2867
+ return Card(
2868
+ child: Padding(
2869
+ padding: const EdgeInsets.all(18),
2870
+ child: Column(
2871
+ crossAxisAlignment: CrossAxisAlignment.start,
2872
+ children: <Widget>[
2873
+ Row(
2874
+ children: <Widget>[
2875
+ Expanded(child: _SectionTitle('Execution Trace')),
2876
+ if (loading)
2877
+ const SizedBox.square(
2878
+ dimension: 16,
2879
+ child: CircularProgressIndicator(strokeWidth: 2),
2880
+ ),
2881
+ ],
2882
+ ),
2883
+ const SizedBox(height: 12),
2884
+ if (events.isEmpty)
2885
+ Text(
2886
+ 'No structured run events recorded yet.',
2887
+ style: TextStyle(color: _textSecondary),
2888
+ )
2889
+ else
2890
+ ...events.map((event) {
2891
+ return Padding(
2892
+ padding: const EdgeInsets.only(bottom: 10),
2893
+ child: _RunEventCard(event: event),
2894
+ );
2895
+ }),
2896
+ ],
2897
+ ),
2898
+ ),
2899
+ );
2900
+ }
2901
+ }
2902
+
2903
+ class _RunEventCard extends StatelessWidget {
2904
+ const _RunEventCard({required this.event});
2905
+
2906
+ final RunEventItem event;
2907
+
2908
+ @override
2909
+ Widget build(BuildContext context) {
2910
+ final accent = event.isFailure ? _danger : _info;
2911
+ return Container(
2912
+ padding: const EdgeInsets.all(14),
2913
+ decoration: BoxDecoration(
2914
+ color: _bgSecondary,
2915
+ borderRadius: BorderRadius.circular(16),
2916
+ border: Border.all(color: accent.withValues(alpha: 0.24)),
2917
+ ),
2918
+ child: Column(
2919
+ crossAxisAlignment: CrossAxisAlignment.start,
2920
+ children: <Widget>[
2921
+ Row(
2922
+ children: <Widget>[
2923
+ Container(
2924
+ width: 10,
2925
+ height: 10,
2926
+ decoration: BoxDecoration(
2927
+ color: accent,
2928
+ shape: BoxShape.circle,
2929
+ ),
2930
+ ),
2931
+ const SizedBox(width: 10),
2932
+ Expanded(
2933
+ child: Text(
2934
+ event.title,
2935
+ style: const TextStyle(fontWeight: FontWeight.w700),
2936
+ ),
2937
+ ),
2938
+ if (event.createdAtLabel.isNotEmpty)
2939
+ Text(
2940
+ event.createdAtLabel,
2941
+ style: TextStyle(color: _textSecondary, fontSize: 12),
2942
+ ),
2943
+ ],
2944
+ ),
2945
+ if (event.detail.trim().isNotEmpty) ...<Widget>[
2946
+ const SizedBox(height: 8),
2947
+ Text(
2948
+ event.detail,
2949
+ style: TextStyle(color: _textSecondary, height: 1.4),
2950
+ ),
2951
+ ],
2952
+ ],
2953
+ ),
2954
+ );
2955
+ }
2956
+ }
2957
+
2835
2958
  class _RunStepCard extends StatelessWidget {
2836
2959
  const _RunStepCard({required this.step});
2837
2960
 
@@ -2994,323 +3117,324 @@ class _RunDetailBlock extends StatelessWidget {
2994
3117
  }
2995
3118
  }
2996
3119
 
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,
3120
+ Future<void> openMessagingConfig(
3121
+ BuildContext context,
3122
+ NeoAgentController controller,
3123
+ MessagingPlatformDescriptor platform,
3124
+ ) async {
3125
+ switch (platform.id) {
3126
+ case 'whatsapp':
3127
+ await _connectMessagingPlatformHelper(
3128
+ context,
3129
+ controller,
3130
+ platform: 'whatsapp',
3131
+ platformLabel: platform.label,
3025
3132
  );
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
- ),
3036
- );
3037
- return false;
3038
- }
3133
+ return;
3134
+ case 'telnyx':
3135
+ return _openTelnyxConfigHelper(context, controller);
3136
+ default:
3137
+ return _openGenericMessagingConfigHelper(context, controller, platform);
3039
3138
  }
3139
+ }
3040
3140
 
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() ?? '',
3141
+ Future<bool> _connectMessagingPlatformHelper(
3142
+ BuildContext context,
3143
+ NeoAgentController controller, {
3144
+ required String platform,
3145
+ required String platformLabel,
3146
+ Map<String, dynamic>? config,
3147
+ Map<String, dynamic>? configSnapshot,
3148
+ }) async {
3149
+ try {
3150
+ await controller.connectMessagingPlatform(
3151
+ platform: platform,
3152
+ config: config,
3153
+ configSnapshot: configSnapshot,
3053
3154
  );
3054
- final webhookUrl = TextEditingController(
3055
- text: saved['webhookUrl']?.toString() ?? controller.backendUrl,
3155
+ return true;
3156
+ } catch (error) {
3157
+ if (!context.mounted) return false;
3158
+ final messenger = ScaffoldMessenger.maybeOf(context);
3159
+ messenger?.showSnackBar(
3160
+ SnackBar(
3161
+ content: Text(
3162
+ 'Failed to connect $platformLabel: ${controller.friendlyErrorMessage(error)}',
3163
+ ),
3164
+ ),
3056
3165
  );
3166
+ return false;
3167
+ }
3168
+ }
3057
3169
 
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
- ),
3170
+ Future<void> _openTelnyxConfigHelper(
3171
+ BuildContext context,
3172
+ NeoAgentController controller,
3173
+ ) async {
3174
+ final saved = _jsonMap(
3175
+ _decodeMaybeJson(controller.settings['telnyx_config']),
3176
+ );
3177
+ final apiKey = TextEditingController(text: saved['apiKey']?.toString() ?? '');
3178
+ final phoneNumber = TextEditingController(
3179
+ text: saved['phoneNumber']?.toString() ?? '',
3180
+ );
3181
+ final connectionId = TextEditingController(
3182
+ text: saved['connectionId']?.toString() ?? '',
3183
+ );
3184
+ final webhookUrl = TextEditingController(
3185
+ text: saved['webhookUrl']?.toString() ?? controller.backendUrl,
3186
+ );
3187
+
3188
+ try {
3189
+ await showDialog<void>(
3190
+ context: context,
3191
+ builder: (context) {
3192
+ return StatefulBuilder(
3193
+ builder: (context, setLocalState) {
3194
+ return AlertDialog(
3195
+ backgroundColor: _bgCard,
3196
+ title: Text('Telnyx Voice'),
3197
+ content: SizedBox(
3198
+ width: 620,
3199
+ child: SingleChildScrollView(
3200
+ child: Column(
3201
+ mainAxisSize: MainAxisSize.min,
3202
+ children: <Widget>[
3203
+ TextField(
3204
+ controller: apiKey,
3205
+ obscureText: true,
3206
+ decoration: const InputDecoration(labelText: 'API Key'),
3207
+ ),
3208
+ const SizedBox(height: 12),
3209
+ TextField(
3210
+ controller: phoneNumber,
3211
+ decoration: const InputDecoration(
3212
+ labelText: 'Phone Number',
3093
3213
  ),
3094
- const SizedBox(height: 12),
3095
- TextField(
3096
- controller: webhookUrl,
3097
- decoration: const InputDecoration(
3098
- labelText: 'Webhook Base URL',
3099
- ),
3214
+ ),
3215
+ const SizedBox(height: 12),
3216
+ TextField(
3217
+ controller: connectionId,
3218
+ decoration: const InputDecoration(
3219
+ labelText: 'Connection ID',
3100
3220
  ),
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),
3221
+ ),
3222
+ const SizedBox(height: 12),
3223
+ TextField(
3224
+ controller: webhookUrl,
3225
+ decoration: const InputDecoration(
3226
+ labelText: 'Webhook Base URL',
3105
3227
  ),
3106
- ],
3107
- ),
3228
+ ),
3229
+ const SizedBox(height: 12),
3230
+ Text(
3231
+ 'Voice STT/TTS providers and models are configured in global Settings > Voice.',
3232
+ style: TextStyle(color: _textSecondary),
3233
+ ),
3234
+ ],
3108
3235
  ),
3109
3236
  ),
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
- }
3237
+ ),
3238
+ actions: <Widget>[
3239
+ TextButton(
3240
+ onPressed: () => Navigator.of(context).pop(),
3241
+ child: Text('Cancel'),
3242
+ ),
3243
+ FilledButton(
3244
+ onPressed: () async {
3245
+ final config = <String, dynamic>{
3246
+ 'apiKey': apiKey.text.trim(),
3247
+ 'phoneNumber': phoneNumber.text.trim(),
3248
+ 'connectionId': connectionId.text.trim(),
3249
+ 'webhookUrl': webhookUrl.text.trim(),
3250
+ };
3251
+ final connected = await _connectMessagingPlatformHelper(
3252
+ context,
3253
+ controller,
3254
+ platform: 'telnyx',
3255
+ platformLabel: 'Telnyx Voice',
3256
+ config: config,
3257
+ configSnapshot: <String, dynamic>{
3258
+ 'telnyx_config': jsonEncode(config),
3259
+ },
3260
+ );
3261
+ if (connected && context.mounted) {
3262
+ Navigator.of(context).pop();
3263
+ }
3264
+ },
3265
+ child: Text('Connect'),
3266
+ ),
3267
+ ],
3268
+ );
3269
+ },
3270
+ );
3271
+ },
3272
+ );
3273
+ } finally {
3274
+ apiKey.dispose();
3275
+ phoneNumber.dispose();
3276
+ connectionId.dispose();
3277
+ webhookUrl.dispose();
3149
3278
  }
3279
+ }
3150
3280
 
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
- }
3281
+ Future<void> _openGenericMessagingConfigHelper(
3282
+ BuildContext context,
3283
+ NeoAgentController controller,
3284
+ MessagingPlatformDescriptor platform,
3285
+ ) async {
3286
+ final saved = _jsonMap(
3287
+ _decodeMaybeJson(controller.settings[platform.settingsKey]),
3288
+ );
3289
+ final textControllers = <String, TextEditingController>{};
3290
+ final boolValues = <String, bool>{};
3291
+ for (final field in platform.configFields) {
3292
+ final savedValue = field.settingsKey == null
3293
+ ? saved[field.key]
3294
+ : controller.settings[field.storageKey];
3295
+ if (field.kind == MessagingConfigFieldKind.boolean) {
3296
+ boolValues[field.key] =
3297
+ savedValue == true || savedValue?.toString() == 'true';
3298
+ } else {
3299
+ textControllers[field.key] = TextEditingController(
3300
+ text: savedValue?.toString() ?? field.defaultValue ?? '',
3301
+ );
3171
3302
  }
3303
+ }
3172
3304
 
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
- ),
3305
+ try {
3306
+ await showDialog<void>(
3307
+ context: context,
3308
+ builder: (context) {
3309
+ return StatefulBuilder(
3310
+ builder: (context, setLocalState) {
3311
+ return AlertDialog(
3312
+ backgroundColor: _bgCard,
3313
+ title: Text(platform.label),
3314
+ content: SizedBox(
3315
+ width: 620,
3316
+ child: SingleChildScrollView(
3317
+ child: Column(
3318
+ mainAxisSize: MainAxisSize.min,
3319
+ children: <Widget>[
3320
+ if (platform.configFields.isEmpty)
3321
+ Text(
3322
+ 'No extra settings are required.',
3323
+ style: TextStyle(color: _textSecondary),
3324
+ )
3325
+ else
3326
+ ...platform.configFields.map((field) {
3327
+ if (field.kind == MessagingConfigFieldKind.boolean) {
3328
+ return SwitchListTile(
3329
+ contentPadding: EdgeInsets.zero,
3330
+ title: Text(field.label),
3331
+ value: boolValues[field.key] ?? false,
3332
+ onChanged: (value) {
3333
+ setLocalState(() {
3334
+ boolValues[field.key] = value;
3335
+ });
3336
+ },
3231
3337
  );
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,
3338
+ }
3339
+ final controller = textControllers[field.key]!;
3340
+ return Padding(
3341
+ padding: const EdgeInsets.only(bottom: 12),
3342
+ child: TextField(
3343
+ controller: controller,
3344
+ obscureText:
3345
+ field.obscure ||
3346
+ field.kind ==
3347
+ MessagingConfigFieldKind.password,
3348
+ minLines:
3349
+ field.kind ==
3350
+ MessagingConfigFieldKind.multiline
3351
+ ? 4
3352
+ : 1,
3353
+ maxLines:
3354
+ field.kind ==
3355
+ MessagingConfigFieldKind.multiline
3356
+ ? 8
3357
+ : 1,
3358
+ decoration: InputDecoration(
3359
+ labelText: field.label,
3360
+ ),
3248
3361
  ),
3249
- ),
3250
- ],
3251
- ),
3362
+ );
3363
+ }),
3364
+ const SizedBox(height: 8),
3365
+ if (platform.id == 'meshtastic')
3366
+ Text(
3367
+ 'Meshtastic connects directly to the device TCP API on port 4403 by default. Normal chat is limited to the configured channel.',
3368
+ style: TextStyle(color: _textSecondary, fontSize: 12),
3369
+ )
3370
+ else
3371
+ SelectableText(
3372
+ 'Inbound webhook: ${controller.backendUrl}/api/messaging/webhook/${platform.id}',
3373
+ style: TextStyle(color: _textSecondary, fontSize: 12),
3374
+ ),
3375
+ ],
3252
3376
  ),
3253
3377
  ),
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;
3378
+ ),
3379
+ actions: <Widget>[
3380
+ TextButton(
3381
+ onPressed: () => Navigator.of(context).pop(),
3382
+ child: Text('Cancel'),
3383
+ ),
3384
+ FilledButton(
3385
+ onPressed: () async {
3386
+ final config = <String, dynamic>{};
3387
+ final snapshot = <String, dynamic>{};
3388
+ for (final field in platform.configFields) {
3389
+ if (field.kind == MessagingConfigFieldKind.boolean ||
3390
+ !field.includeInConfig) {
3391
+ continue;
3392
+ }
3393
+ final controller = textControllers[field.key];
3394
+ final value = controller?.text.trim() ?? '';
3395
+ if (value.isNotEmpty) config[field.key] = value;
3396
+ }
3397
+ for (final field in platform.configFields) {
3398
+ if (field.kind == MessagingConfigFieldKind.boolean) {
3399
+ final value = boolValues[field.key] ?? false;
3400
+ if (field.includeInConfig) {
3401
+ config[field.key] = value;
3402
+ }
3403
+ if (field.settingsKey != null) {
3404
+ snapshot[field.storageKey] = value;
3267
3405
  }
3406
+ } else if (field.settingsKey != null) {
3268
3407
  final controller = textControllers[field.key];
3269
3408
  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
- }
3409
+ if (value.isNotEmpty) {
3410
+ snapshot[field.storageKey] = value;
3287
3411
  }
3288
3412
  }
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
- }
3413
+ }
3414
+ snapshot[platform.settingsKey] = jsonEncode(config);
3415
+ final connected = await _connectMessagingPlatformHelper(
3416
+ context,
3417
+ controller,
3418
+ platform: platform.id,
3419
+ platformLabel: platform.label,
3420
+ config: config,
3421
+ configSnapshot: snapshot,
3422
+ );
3423
+ if (connected && context.mounted) {
3424
+ Navigator.of(context).pop();
3425
+ }
3426
+ },
3427
+ child: Text('Connect'),
3428
+ ),
3429
+ ],
3430
+ );
3431
+ },
3432
+ );
3433
+ },
3434
+ );
3435
+ } finally {
3436
+ for (final controller in textControllers.values) {
3437
+ controller.dispose();
3314
3438
  }
3315
3439
  }
3316
-
3440
+ }