neoagent 3.1.1-beta.1 → 3.1.1-beta.3

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.
@@ -2796,6 +2796,23 @@ class NeoAgentController extends ChangeNotifier {
2796
2796
  notifyListeners();
2797
2797
  }
2798
2798
 
2799
+ Future<List<TaskDeliveryTarget>> fetchTaskDeliveryTargets({
2800
+ String? query,
2801
+ String? platform,
2802
+ String? agentId,
2803
+ }) async {
2804
+ final rows = await _backendClient.fetchTaskDeliveryTargets(
2805
+ backendUrl,
2806
+ query: query,
2807
+ platform: platform,
2808
+ agentId: agentId ?? _scopedAgentId,
2809
+ );
2810
+ return rows
2811
+ .map(TaskDeliveryTarget.fromJson)
2812
+ .where((target) => target.platform.isNotEmpty && target.to.isNotEmpty)
2813
+ .toList(growable: false);
2814
+ }
2815
+
2799
2816
  Future<void> refreshWidgets({bool all = false}) async {
2800
2817
  widgets = _decodeModelList(
2801
2818
  'widgets',
@@ -643,6 +643,61 @@ class MessagingMessage {
643
643
  }
644
644
  }
645
645
 
646
+ class TaskDeliveryTarget {
647
+ const TaskDeliveryTarget({
648
+ required this.platform,
649
+ required this.platformLabel,
650
+ required this.to,
651
+ required this.label,
652
+ required this.subtitle,
653
+ required this.source,
654
+ required this.connected,
655
+ required this.supportsDelivery,
656
+ });
657
+
658
+ factory TaskDeliveryTarget.fromJson(Map<dynamic, dynamic> json) {
659
+ final platform = json['platform']?.toString() ?? '';
660
+ final to = json['to']?.toString() ?? '';
661
+ return TaskDeliveryTarget(
662
+ platform: platform,
663
+ platformLabel:
664
+ json['platformLabel']?.toString().ifEmpty(platform.toUpperCase()) ??
665
+ platform.toUpperCase(),
666
+ to: to,
667
+ label: json['label']?.toString().ifEmpty(to) ?? to,
668
+ subtitle: json['subtitle']?.toString() ?? '',
669
+ source: json['source']?.toString().ifEmpty('discovered') ?? 'discovered',
670
+ connected: json['connected'] != false,
671
+ supportsDelivery: json['supportsDelivery'] != false,
672
+ );
673
+ }
674
+
675
+ final String platform;
676
+ final String platformLabel;
677
+ final String to;
678
+ final String label;
679
+ final String subtitle;
680
+ final String source;
681
+ final bool connected;
682
+ final bool supportsDelivery;
683
+
684
+ String get id => '$platform:$to';
685
+ bool get selectable => connected && supportsDelivery;
686
+
687
+ String get sourceLabel {
688
+ switch (source) {
689
+ case 'default':
690
+ return 'Default';
691
+ case 'recent':
692
+ return 'Recent';
693
+ case 'manual':
694
+ return 'Manual';
695
+ default:
696
+ return 'Discovered';
697
+ }
698
+ }
699
+ }
700
+
646
701
  class MessagingQrState {
647
702
  const MessagingQrState({required this.platform, required this.qr});
648
703
 
@@ -5289,6 +5289,426 @@ Future<String?> _pickTaskTriggerType(
5289
5289
  );
5290
5290
  }
5291
5291
 
5292
+ IconData _taskDeliveryPlatformIcon(String platform) {
5293
+ for (final descriptor in messagingPlatforms) {
5294
+ if (descriptor.id == platform) return descriptor.icon;
5295
+ }
5296
+ return Icons.forum_rounded;
5297
+ }
5298
+
5299
+ Color _taskDeliveryPlatformColor(String platform) {
5300
+ for (final descriptor in messagingPlatforms) {
5301
+ if (descriptor.id == platform) return descriptor.accent;
5302
+ }
5303
+ return _accent;
5304
+ }
5305
+
5306
+ String _taskDeliveryPlatformLabel(String platform) {
5307
+ for (final descriptor in messagingPlatforms) {
5308
+ if (descriptor.id == platform) return descriptor.label;
5309
+ }
5310
+ return platform.replaceAll('_', ' ').toUpperCase();
5311
+ }
5312
+
5313
+ TaskDeliveryTarget? _taskDeliveryTargetFromTask(TaskItem? task) {
5314
+ final platform = task?.taskConfig['notifyPlatform']?.toString().trim() ?? '';
5315
+ final to = task?.taskConfig['notifyTo']?.toString().trim() ?? '';
5316
+ if (platform.isEmpty || to.isEmpty) return null;
5317
+ return TaskDeliveryTarget(
5318
+ platform: platform,
5319
+ platformLabel: _taskDeliveryPlatformLabel(platform),
5320
+ to: to,
5321
+ label: to,
5322
+ subtitle: 'Saved delivery destination',
5323
+ source: 'manual',
5324
+ connected: true,
5325
+ supportsDelivery: true,
5326
+ );
5327
+ }
5328
+
5329
+ Future<TaskDeliveryTarget?> _pickTaskDeliveryTarget(
5330
+ BuildContext context, {
5331
+ required NeoAgentController controller,
5332
+ required String? agentId,
5333
+ required TaskDeliveryTarget? selected,
5334
+ }) {
5335
+ return showModalBottomSheet<TaskDeliveryTarget?>(
5336
+ context: context,
5337
+ isScrollControlled: true,
5338
+ backgroundColor: _bgCard,
5339
+ builder: (context) => _TaskDeliveryTargetPickerSheet(
5340
+ controller: controller,
5341
+ agentId: agentId,
5342
+ selected: selected,
5343
+ ),
5344
+ );
5345
+ }
5346
+
5347
+ class _TaskDeliveryTargetPickerSheet extends StatefulWidget {
5348
+ const _TaskDeliveryTargetPickerSheet({
5349
+ required this.controller,
5350
+ required this.agentId,
5351
+ required this.selected,
5352
+ });
5353
+
5354
+ final NeoAgentController controller;
5355
+ final String? agentId;
5356
+ final TaskDeliveryTarget? selected;
5357
+
5358
+ @override
5359
+ State<_TaskDeliveryTargetPickerSheet> createState() =>
5360
+ _TaskDeliveryTargetPickerSheetState();
5361
+ }
5362
+
5363
+ class _TaskDeliveryTargetPickerSheetState
5364
+ extends State<_TaskDeliveryTargetPickerSheet> {
5365
+ late final TextEditingController _queryController;
5366
+ late final TextEditingController _manualController;
5367
+ Timer? _debounce;
5368
+ Future<List<TaskDeliveryTarget>>? _targetsFuture;
5369
+ String? _platformFilter;
5370
+ String? _manualPlatform;
5371
+
5372
+ @override
5373
+ void initState() {
5374
+ super.initState();
5375
+ _queryController = TextEditingController();
5376
+ _manualController = TextEditingController();
5377
+ _manualPlatform = widget.selected?.platform;
5378
+ _loadTargets();
5379
+ }
5380
+
5381
+ @override
5382
+ void dispose() {
5383
+ _debounce?.cancel();
5384
+ _queryController.dispose();
5385
+ _manualController.dispose();
5386
+ super.dispose();
5387
+ }
5388
+
5389
+ void _loadTargets() {
5390
+ setState(() {
5391
+ _targetsFuture = widget.controller.fetchTaskDeliveryTargets(
5392
+ query: _queryController.text,
5393
+ platform: _platformFilter,
5394
+ agentId: widget.agentId,
5395
+ );
5396
+ });
5397
+ }
5398
+
5399
+ void _scheduleSearch() {
5400
+ _debounce?.cancel();
5401
+ _debounce = Timer(const Duration(milliseconds: 220), _loadTargets);
5402
+ }
5403
+
5404
+ List<String> _platformIds(List<TaskDeliveryTarget> targets) {
5405
+ final ids = <String>{
5406
+ if (widget.selected?.platform.isNotEmpty == true)
5407
+ widget.selected!.platform,
5408
+ ...targets.map((target) => target.platform),
5409
+ ...messagingPlatforms
5410
+ .where(
5411
+ (platform) =>
5412
+ platform.id == 'whatsapp' ||
5413
+ platform.id == 'discord' ||
5414
+ platform.id == 'telegram' ||
5415
+ platform.id == 'slack',
5416
+ )
5417
+ .map((platform) => platform.id),
5418
+ }.toList();
5419
+ ids.sort(
5420
+ (left, right) => _taskDeliveryPlatformLabel(
5421
+ left,
5422
+ ).compareTo(_taskDeliveryPlatformLabel(right)),
5423
+ );
5424
+ return ids;
5425
+ }
5426
+
5427
+ void _submitManual() {
5428
+ final to = _manualController.text.trim();
5429
+ final platform =
5430
+ _manualPlatform ?? _platformFilter ?? widget.selected?.platform ?? '';
5431
+ if (to.isEmpty || platform.isEmpty) return;
5432
+ Navigator.of(context).pop(
5433
+ TaskDeliveryTarget(
5434
+ platform: platform,
5435
+ platformLabel: _taskDeliveryPlatformLabel(platform),
5436
+ to: to,
5437
+ label: to,
5438
+ subtitle: 'Manual destination',
5439
+ source: 'manual',
5440
+ connected: true,
5441
+ supportsDelivery: true,
5442
+ ),
5443
+ );
5444
+ }
5445
+
5446
+ Widget _buildTargetTile(TaskDeliveryTarget target) {
5447
+ final color = _taskDeliveryPlatformColor(target.platform);
5448
+ final selected = widget.selected?.id == target.id;
5449
+ return ListTile(
5450
+ contentPadding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2),
5451
+ leading: Container(
5452
+ width: 42,
5453
+ height: 42,
5454
+ decoration: BoxDecoration(
5455
+ color: color.withValues(alpha: 0.12),
5456
+ borderRadius: BorderRadius.circular(12),
5457
+ ),
5458
+ child: Icon(_taskDeliveryPlatformIcon(target.platform), color: color),
5459
+ ),
5460
+ title: Text(
5461
+ target.label,
5462
+ maxLines: 1,
5463
+ overflow: TextOverflow.ellipsis,
5464
+ style: const TextStyle(fontWeight: FontWeight.w700),
5465
+ ),
5466
+ subtitle: Text(
5467
+ '${target.platformLabel} · ${target.subtitle.ifEmpty(target.to)}',
5468
+ maxLines: 2,
5469
+ overflow: TextOverflow.ellipsis,
5470
+ ),
5471
+ trailing: Wrap(
5472
+ spacing: 8,
5473
+ crossAxisAlignment: WrapCrossAlignment.center,
5474
+ children: <Widget>[
5475
+ _StatusPill(label: target.sourceLabel, color: color),
5476
+ Icon(
5477
+ selected ? Icons.check_circle_rounded : Icons.arrow_forward_rounded,
5478
+ color: selected ? color : _textSecondary,
5479
+ ),
5480
+ ],
5481
+ ),
5482
+ enabled: target.selectable,
5483
+ onTap: target.selectable ? () => Navigator.of(context).pop(target) : null,
5484
+ );
5485
+ }
5486
+
5487
+ @override
5488
+ Widget build(BuildContext context) {
5489
+ return Padding(
5490
+ padding: EdgeInsets.only(
5491
+ left: 20,
5492
+ right: 20,
5493
+ top: 18,
5494
+ bottom: MediaQuery.of(context).viewInsets.bottom + 20,
5495
+ ),
5496
+ child: ConstrainedBox(
5497
+ constraints: const BoxConstraints(maxWidth: 760, maxHeight: 720),
5498
+ child: FutureBuilder<List<TaskDeliveryTarget>>(
5499
+ future: _targetsFuture,
5500
+ builder: (context, snapshot) {
5501
+ final targets = snapshot.data ?? const <TaskDeliveryTarget>[];
5502
+ final platformIds = _platformIds(targets);
5503
+ return SingleChildScrollView(
5504
+ child: Column(
5505
+ mainAxisSize: MainAxisSize.min,
5506
+ crossAxisAlignment: CrossAxisAlignment.start,
5507
+ children: <Widget>[
5508
+ Row(
5509
+ children: <Widget>[
5510
+ Expanded(
5511
+ child: Text(
5512
+ 'Result Delivery',
5513
+ style: TextStyle(
5514
+ fontSize: 18,
5515
+ fontWeight: FontWeight.w800,
5516
+ ),
5517
+ ),
5518
+ ),
5519
+ TextButton.icon(
5520
+ onPressed: () => Navigator.of(context).pop(
5521
+ const TaskDeliveryTarget(
5522
+ platform: '',
5523
+ platformLabel: '',
5524
+ to: '',
5525
+ label: '',
5526
+ subtitle: '',
5527
+ source: 'default',
5528
+ connected: true,
5529
+ supportsDelivery: true,
5530
+ ),
5531
+ ),
5532
+ icon: const Icon(Icons.auto_mode_rounded),
5533
+ label: const Text('Use default'),
5534
+ ),
5535
+ ],
5536
+ ),
5537
+ const SizedBox(height: 6),
5538
+ Text(
5539
+ 'Search discovered messaging channels, contacts, groups, and recent conversations.',
5540
+ style: TextStyle(color: _textSecondary),
5541
+ ),
5542
+ const SizedBox(height: 14),
5543
+ TextField(
5544
+ controller: _queryController,
5545
+ onChanged: (_) => _scheduleSearch(),
5546
+ decoration: const InputDecoration(
5547
+ prefixIcon: Icon(Icons.search_rounded),
5548
+ labelText: 'Search channels',
5549
+ ),
5550
+ ),
5551
+ const SizedBox(height: 12),
5552
+ SingleChildScrollView(
5553
+ scrollDirection: Axis.horizontal,
5554
+ child: Row(
5555
+ children: <Widget>[
5556
+ Padding(
5557
+ padding: const EdgeInsets.only(right: 8),
5558
+ child: ChoiceChip(
5559
+ label: const Text('All'),
5560
+ selected: _platformFilter == null,
5561
+ onSelected: (_) {
5562
+ _platformFilter = null;
5563
+ _loadTargets();
5564
+ },
5565
+ ),
5566
+ ),
5567
+ ...platformIds.map((platform) {
5568
+ return Padding(
5569
+ padding: const EdgeInsets.only(right: 8),
5570
+ child: ChoiceChip(
5571
+ avatar: Icon(
5572
+ _taskDeliveryPlatformIcon(platform),
5573
+ size: 16,
5574
+ ),
5575
+ label: Text(_taskDeliveryPlatformLabel(platform)),
5576
+ selected: _platformFilter == platform,
5577
+ onSelected: (_) {
5578
+ _platformFilter = platform;
5579
+ _loadTargets();
5580
+ },
5581
+ ),
5582
+ );
5583
+ }),
5584
+ ],
5585
+ ),
5586
+ ),
5587
+ const SizedBox(height: 16),
5588
+ if (snapshot.connectionState == ConnectionState.waiting)
5589
+ const Center(
5590
+ child: Padding(
5591
+ padding: EdgeInsets.all(24),
5592
+ child: CircularProgressIndicator(),
5593
+ ),
5594
+ )
5595
+ else if (snapshot.hasError)
5596
+ _TaskDeliveryNotice(
5597
+ icon: Icons.warning_amber_rounded,
5598
+ title: 'Discovery failed',
5599
+ detail: snapshot.error.toString(),
5600
+ )
5601
+ else if (targets.isEmpty)
5602
+ _TaskDeliveryNotice(
5603
+ icon: Icons.search_off_rounded,
5604
+ title: 'No channels found',
5605
+ detail:
5606
+ 'Try another search, connect a messaging platform, or enter a destination ID manually.',
5607
+ )
5608
+ else
5609
+ ...targets.take(40).map(_buildTargetTile),
5610
+ const Divider(height: 28),
5611
+ Text(
5612
+ 'Manual destination',
5613
+ style: TextStyle(fontWeight: FontWeight.w700),
5614
+ ),
5615
+ const SizedBox(height: 8),
5616
+ Row(
5617
+ children: <Widget>[
5618
+ Expanded(
5619
+ flex: 2,
5620
+ child: DropdownButtonFormField<String>(
5621
+ initialValue: _manualPlatform,
5622
+ decoration: const InputDecoration(
5623
+ labelText: 'Platform',
5624
+ ),
5625
+ items: platformIds
5626
+ .map(
5627
+ (platform) => DropdownMenuItem<String>(
5628
+ value: platform,
5629
+ child: Text(
5630
+ _taskDeliveryPlatformLabel(platform),
5631
+ ),
5632
+ ),
5633
+ )
5634
+ .toList(growable: false),
5635
+ onChanged: (value) =>
5636
+ setState(() => _manualPlatform = value),
5637
+ ),
5638
+ ),
5639
+ const SizedBox(width: 12),
5640
+ Expanded(
5641
+ flex: 3,
5642
+ child: TextField(
5643
+ controller: _manualController,
5644
+ decoration: const InputDecoration(
5645
+ labelText: 'Destination ID',
5646
+ ),
5647
+ onSubmitted: (_) => _submitManual(),
5648
+ ),
5649
+ ),
5650
+ const SizedBox(width: 8),
5651
+ IconButton(
5652
+ tooltip: 'Use manual destination',
5653
+ onPressed: _submitManual,
5654
+ icon: const Icon(Icons.check_rounded),
5655
+ ),
5656
+ ],
5657
+ ),
5658
+ ],
5659
+ ),
5660
+ );
5661
+ },
5662
+ ),
5663
+ ),
5664
+ );
5665
+ }
5666
+ }
5667
+
5668
+ class _TaskDeliveryNotice extends StatelessWidget {
5669
+ const _TaskDeliveryNotice({
5670
+ required this.icon,
5671
+ required this.title,
5672
+ required this.detail,
5673
+ });
5674
+
5675
+ final IconData icon;
5676
+ final String title;
5677
+ final String detail;
5678
+
5679
+ @override
5680
+ Widget build(BuildContext context) {
5681
+ return Container(
5682
+ width: double.infinity,
5683
+ padding: const EdgeInsets.all(14),
5684
+ decoration: BoxDecoration(
5685
+ border: Border.all(color: _border),
5686
+ borderRadius: BorderRadius.circular(12),
5687
+ ),
5688
+ child: Row(
5689
+ crossAxisAlignment: CrossAxisAlignment.start,
5690
+ children: <Widget>[
5691
+ Icon(icon, color: _textSecondary),
5692
+ const SizedBox(width: 12),
5693
+ Expanded(
5694
+ child: Column(
5695
+ crossAxisAlignment: CrossAxisAlignment.start,
5696
+ children: <Widget>[
5697
+ Text(
5698
+ title,
5699
+ style: const TextStyle(fontWeight: FontWeight.w700),
5700
+ ),
5701
+ const SizedBox(height: 4),
5702
+ Text(detail, style: TextStyle(color: _textSecondary)),
5703
+ ],
5704
+ ),
5705
+ ),
5706
+ ],
5707
+ ),
5708
+ );
5709
+ }
5710
+ }
5711
+
5292
5712
  class TasksPanel extends StatefulWidget {
5293
5713
  const TasksPanel({super.key, required this.controller});
5294
5714
 
@@ -5871,6 +6291,9 @@ class _TasksPanelState extends State<TasksPanel> {
5871
6291
  ? task!.triggerConfig['connectionId'] as int
5872
6292
  : int.tryParse(task?.triggerConfig['connectionId']?.toString() ?? ''),
5873
6293
  );
6294
+ final selectedDeliveryTarget = ValueNotifier<TaskDeliveryTarget?>(
6295
+ _taskDeliveryTargetFromTask(task),
6296
+ );
5874
6297
  final queryController = TextEditingController(
5875
6298
  text:
5876
6299
  task?.triggerConfig['query']?.toString() ??
@@ -6203,6 +6626,101 @@ class _TasksPanelState extends State<TasksPanel> {
6203
6626
  decoration: const InputDecoration(labelText: 'Prompt'),
6204
6627
  ),
6205
6628
  const SizedBox(height: 12),
6629
+ ValueListenableBuilder<TaskDeliveryTarget?>(
6630
+ valueListenable: selectedDeliveryTarget,
6631
+ builder: (context, deliveryTarget, _) {
6632
+ final color = deliveryTarget == null
6633
+ ? _textSecondary
6634
+ : _taskDeliveryPlatformColor(
6635
+ deliveryTarget.platform,
6636
+ );
6637
+ return InkWell(
6638
+ borderRadius: BorderRadius.circular(14),
6639
+ onTap: () async {
6640
+ final picked = await _pickTaskDeliveryTarget(
6641
+ context,
6642
+ controller: controller,
6643
+ agentId: selectedAgentId,
6644
+ selected: deliveryTarget,
6645
+ );
6646
+ if (picked == null) return;
6647
+ selectedDeliveryTarget.value =
6648
+ picked.platform.isEmpty ? null : picked;
6649
+ },
6650
+ child: InputDecorator(
6651
+ decoration: const InputDecoration(
6652
+ labelText: 'Result Delivery',
6653
+ ),
6654
+ child: Row(
6655
+ children: <Widget>[
6656
+ Container(
6657
+ width: 40,
6658
+ height: 40,
6659
+ decoration: BoxDecoration(
6660
+ color: color.withValues(alpha: 0.12),
6661
+ borderRadius: BorderRadius.circular(12),
6662
+ ),
6663
+ child: Icon(
6664
+ deliveryTarget == null
6665
+ ? Icons.auto_mode_rounded
6666
+ : _taskDeliveryPlatformIcon(
6667
+ deliveryTarget.platform,
6668
+ ),
6669
+ color: color,
6670
+ ),
6671
+ ),
6672
+ const SizedBox(width: 12),
6673
+ Expanded(
6674
+ child: Column(
6675
+ crossAxisAlignment:
6676
+ CrossAxisAlignment.start,
6677
+ mainAxisSize: MainAxisSize.min,
6678
+ children: <Widget>[
6679
+ Text(
6680
+ deliveryTarget == null
6681
+ ? 'Use default channel'
6682
+ : deliveryTarget.label,
6683
+ style: const TextStyle(
6684
+ fontWeight: FontWeight.w700,
6685
+ ),
6686
+ ),
6687
+ const SizedBox(height: 4),
6688
+ Text(
6689
+ deliveryTarget == null
6690
+ ? 'AI-created and unspecified tasks use the current default.'
6691
+ : '${deliveryTarget.platformLabel} · ${deliveryTarget.to}',
6692
+ maxLines: 2,
6693
+ overflow: TextOverflow.ellipsis,
6694
+ style: TextStyle(
6695
+ color: _textSecondary,
6696
+ fontSize: 12.5,
6697
+ ),
6698
+ ),
6699
+ ],
6700
+ ),
6701
+ ),
6702
+ if (deliveryTarget != null)
6703
+ IconButton(
6704
+ tooltip: 'Use default channel',
6705
+ onPressed: () =>
6706
+ selectedDeliveryTarget.value = null,
6707
+ icon: const Icon(
6708
+ Icons.close_rounded,
6709
+ size: 18,
6710
+ ),
6711
+ )
6712
+ else
6713
+ Icon(
6714
+ Icons.unfold_more_rounded,
6715
+ color: _textSecondary,
6716
+ ),
6717
+ ],
6718
+ ),
6719
+ ),
6720
+ );
6721
+ },
6722
+ ),
6723
+ const SizedBox(height: 12),
6206
6724
  DropdownButtonFormField<String>(
6207
6725
  initialValue: selectedModel,
6208
6726
  decoration: const InputDecoration(
@@ -6322,8 +6840,10 @@ class _TasksPanelState extends State<TasksPanel> {
6322
6840
  ),
6323
6841
  )
6324
6842
  .toList(),
6325
- onChanged: (value) =>
6326
- setLocalState(() => selectedAgentId = value),
6843
+ onChanged: (value) => setLocalState(() {
6844
+ selectedAgentId = value;
6845
+ selectedDeliveryTarget.value = null;
6846
+ }),
6327
6847
  ),
6328
6848
  ],
6329
6849
  const SizedBox(height: 12),
@@ -6459,6 +6979,14 @@ class _TasksPanelState extends State<TasksPanel> {
6459
6979
  'maxTokensPerDay': maxTokens,
6460
6980
  },
6461
6981
  };
6982
+ final deliveryTarget = selectedDeliveryTarget.value;
6983
+ if (deliveryTarget == null) {
6984
+ taskConfig.remove('notifyPlatform');
6985
+ taskConfig.remove('notifyTo');
6986
+ } else {
6987
+ taskConfig['notifyPlatform'] = deliveryTarget.platform;
6988
+ taskConfig['notifyTo'] = deliveryTarget.to;
6989
+ }
6462
6990
  await controller.saveTask(
6463
6991
  id: task?.id,
6464
6992
  name: nameController.text.trim(),
@@ -1660,6 +1660,26 @@ class BackendClient {
1660
1660
  return getList(baseUrl, _withAgentQuery('/api/tasks', agentId));
1661
1661
  }
1662
1662
 
1663
+ Future<List<Map<String, dynamic>>> fetchTaskDeliveryTargets(
1664
+ String baseUrl, {
1665
+ String? query,
1666
+ String? platform,
1667
+ String? agentId,
1668
+ }) async {
1669
+ final params = <String>[
1670
+ if (query != null && query.trim().isNotEmpty)
1671
+ 'q=${Uri.encodeQueryComponent(query.trim())}',
1672
+ if (platform != null && platform.trim().isNotEmpty)
1673
+ 'platform=${Uri.encodeQueryComponent(platform.trim())}',
1674
+ if (agentId != null && agentId.trim().isNotEmpty)
1675
+ 'agentId=${Uri.encodeQueryComponent(agentId.trim())}',
1676
+ ];
1677
+ final path = params.isEmpty
1678
+ ? _withAgentQuery('/api/tasks/delivery-targets', agentId)
1679
+ : '/api/tasks/delivery-targets?${params.join('&')}';
1680
+ return getList(baseUrl, path);
1681
+ }
1682
+
1663
1683
  Future<Map<String, dynamic>> saveTask(
1664
1684
  String baseUrl, {
1665
1685
  int? id,