neoagent 3.2.1-beta.2 → 3.2.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.
Files changed (144) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +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 +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +347 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +8 -41
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +15 -28
  52. package/server/services/ai/providers/githubCopilot.js +36 -16
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +13 -22
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +102 -82
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/network/http.js +210 -0
  116. package/server/services/network/safe_request.js +307 -0
  117. package/server/services/runtime/backends/local-vm.js +214 -66
  118. package/server/services/runtime/manager.js +17 -12
  119. package/server/services/social_reach/channels/github.js +10 -4
  120. package/server/services/social_reach/channels/reddit.js +4 -4
  121. package/server/services/social_reach/channels/rss.js +2 -2
  122. package/server/services/social_reach/channels/social_video.js +12 -7
  123. package/server/services/social_reach/channels/v2ex.js +21 -8
  124. package/server/services/social_reach/channels/x.js +2 -2
  125. package/server/services/social_reach/channels/xueqiu.js +5 -5
  126. package/server/services/social_reach/service.js +9 -6
  127. package/server/services/social_reach/utils.js +65 -14
  128. package/server/services/social_video/service.js +160 -50
  129. package/server/services/tasks/integration_runtime.js +18 -8
  130. package/server/services/tasks/runtime.js +39 -4
  131. package/server/services/voice/agentBridge.js +17 -4
  132. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  133. package/server/services/voice/liveSession.js +31 -0
  134. package/server/services/voice/openaiSpeech.js +33 -8
  135. package/server/services/voice/providers.js +233 -151
  136. package/server/services/voice/runtimeManager.js +118 -20
  137. package/server/services/voice/turnRunner.js +6 -0
  138. package/server/services/wearable/firmware_manifest.js +51 -13
  139. package/server/services/wearable/service.js +1 -0
  140. package/server/utils/abort.js +96 -0
  141. package/server/utils/cloud-security.js +110 -3
  142. package/server/utils/files.js +31 -0
  143. package/server/utils/image_payload.js +95 -0
  144. package/server/utils/retry.js +107 -0
@@ -343,10 +343,11 @@ class _SettingsPanelState extends State<SettingsPanel> {
343
343
  onPopInvokedWithResult: (didPop, result) async {
344
344
  if (didPop) return;
345
345
  final action = await _showLeaveDialog(context);
346
- if (action == _LeaveAction.save && mounted) {
346
+ if (!context.mounted) return;
347
+ if (action == _LeaveAction.save) {
347
348
  await _doSave();
348
- if (mounted) Navigator.of(context).pop();
349
- } else if (action == _LeaveAction.discard && mounted) {
349
+ if (context.mounted) Navigator.of(context).pop();
350
+ } else if (action == _LeaveAction.discard) {
350
351
  _hydrate();
351
352
  setState(() => _hasUnsavedChanges = false);
352
353
  Navigator.of(context).pop();
@@ -345,7 +345,6 @@ class _EntranceMotionState extends State<_EntranceMotion> {
345
345
 
346
346
  class _GlassSurface extends StatelessWidget {
347
347
  const _GlassSurface({
348
- super.key,
349
348
  required this.child,
350
349
  this.width,
351
350
  this.padding,
@@ -2850,17 +2849,24 @@ String _ensureModelValue(
2850
2849
  if (allowAuto && value == 'auto') {
2851
2850
  return 'auto';
2852
2851
  }
2853
- for (final model in models) {
2854
- if (model.id == value) {
2855
- return value;
2856
- }
2857
- }
2852
+ final model = _modelForValue(value, models);
2853
+ if (model != null) return model.id;
2858
2854
  if (allowAuto) {
2859
2855
  return 'auto';
2860
2856
  }
2861
2857
  return models.isNotEmpty ? models.first.id : value;
2862
2858
  }
2863
2859
 
2860
+ ModelMeta? _modelForValue(String value, List<ModelMeta> models) {
2861
+ for (final model in models) {
2862
+ if (model.id == value) return model;
2863
+ }
2864
+ for (final model in models) {
2865
+ if (model.modelId == value) return model;
2866
+ }
2867
+ return null;
2868
+ }
2869
+
2864
2870
  String _firstAvailableModelId(List<ModelMeta> models) {
2865
2871
  for (final model in models) {
2866
2872
  if (model.available) {
@@ -2874,11 +2880,8 @@ String _modelLabelForValue(String value, List<ModelMeta> models) {
2874
2880
  if (value == 'auto' || value.trim().isEmpty) {
2875
2881
  return 'Auto';
2876
2882
  }
2877
- for (final model in models) {
2878
- if (model.id == value) {
2879
- return model.label;
2880
- }
2881
- }
2883
+ final model = _modelForValue(value, models);
2884
+ if (model != null) return model.label;
2882
2885
  return value;
2883
2886
  }
2884
2887
 
@@ -17,6 +17,59 @@ import 'desktop_screen_capture.dart';
17
17
 
18
18
  typedef _JpegArgs = ({Uint8List bytes, int quality});
19
19
 
20
+ String resolveDesktopDisplaySelection(
21
+ Object? rawDisplays,
22
+ String requested, {
23
+ String? activeDisplayId,
24
+ }) {
25
+ final normalized = requested.trim();
26
+ if (normalized.isEmpty) {
27
+ throw ArgumentError.value(
28
+ requested,
29
+ 'displayId',
30
+ 'Display ID is required.',
31
+ );
32
+ }
33
+ final displays = rawDisplays is List
34
+ ? rawDisplays
35
+ .whereType<Map>()
36
+ .map(
37
+ (display) =>
38
+ display.map((key, value) => MapEntry(key.toString(), value)),
39
+ )
40
+ .where(
41
+ (display) => display['id']?.toString().trim().isNotEmpty == true,
42
+ )
43
+ .toList(growable: false)
44
+ : const <Map<String, Object?>>[];
45
+ if (displays.isEmpty) {
46
+ throw StateError('No desktop displays are currently available.');
47
+ }
48
+
49
+ if (normalized.toLowerCase() == 'primary') {
50
+ for (final display in displays) {
51
+ if (display['primary'] == true) {
52
+ return display['id'].toString().trim();
53
+ }
54
+ }
55
+ final active = activeDisplayId?.trim() ?? '';
56
+ if (active.isNotEmpty &&
57
+ displays.any((display) => display['id']?.toString().trim() == active)) {
58
+ return active;
59
+ }
60
+ }
61
+
62
+ for (final display in displays) {
63
+ final id = display['id']?.toString().trim() ?? '';
64
+ if (id == normalized) return id;
65
+ }
66
+ throw ArgumentError.value(
67
+ requested,
68
+ 'displayId',
69
+ 'The requested desktop display is not available.',
70
+ );
71
+ }
72
+
20
73
  Uint8List _compressJpegInIsolate(_JpegArgs args) {
21
74
  final decoded = img.decodeImage(args.bytes);
22
75
  if (decoded == null) return args.bytes;
@@ -47,6 +100,8 @@ class DesktopCompanionActions {
47
100
 
48
101
  final DesktopScreenCapture _screenCapture;
49
102
  final DesktopNativeBridge _nativeBridge = DesktopNativeBridge();
103
+ final Map<String, Process> _shellProcesses = <String, Process>{};
104
+ final Set<String> _cancelledShellCommandIds = <String>{};
50
105
 
51
106
  bool get isCaptureSupported => _screenCapture.isSupported;
52
107
 
@@ -64,6 +119,7 @@ class DesktopCompanionActions {
64
119
  activeDisplayId: activeDisplayId,
65
120
  platformStatus: platformStatus,
66
121
  );
122
+ final reportedDisplays = _coerceDisplays(platformStatus['displays']);
67
123
  final packageInfo = await PackageInfo.fromPlatform();
68
124
  return <String, Object?>{
69
125
  'deviceId': deviceId,
@@ -77,7 +133,7 @@ class DesktopCompanionActions {
77
133
  'paused': paused,
78
134
  'permissions': _permissions(capabilities, platformStatus: platformStatus),
79
135
  'capabilities': capabilities,
80
- 'displays': snapshot?.displays ?? const <Map<String, Object?>>[],
136
+ 'displays': snapshot?.displays ?? reportedDisplays,
81
137
  'activeDisplayId':
82
138
  snapshot?.activeDisplayId ??
83
139
  platformStatus['activeDisplayId']?.toString() ??
@@ -144,10 +200,10 @@ class DesktopCompanionActions {
144
200
  contentType: capture.mimeType,
145
201
  width: width,
146
202
  height: height,
147
- activeDisplayId: activeDisplayId ?? 'primary',
203
+ activeDisplayId: 'primary',
148
204
  displays: <Map<String, Object?>>[
149
205
  <String, Object?>{
150
- 'id': activeDisplayId ?? 'primary',
206
+ 'id': 'primary',
151
207
  'label': 'Primary Display',
152
208
  'width': width,
153
209
  'height': height,
@@ -169,6 +225,7 @@ class DesktopCompanionActions {
169
225
  activeDisplayId: activeDisplayId,
170
226
  platformStatus: platformStatus,
171
227
  );
228
+ final reportedDisplays = _coerceDisplays(platformStatus['displays']);
172
229
  return <String, Object?>{
173
230
  'paused': paused,
174
231
  'label': label,
@@ -177,7 +234,7 @@ class DesktopCompanionActions {
177
234
  platformStatus['activeDisplayId']?.toString() ??
178
235
  activeDisplayId ??
179
236
  'primary',
180
- 'displays': snapshot?.displays ?? const <Map<String, Object?>>[],
237
+ 'displays': snapshot?.displays ?? reportedDisplays,
181
238
  'permissions': _permissions(capabilities, platformStatus: platformStatus),
182
239
  'capabilities': capabilities,
183
240
  if (platformStatus['frontmostApp'] != null)
@@ -461,11 +518,17 @@ class DesktopCompanionActions {
461
518
  }
462
519
 
463
520
  Future<Map<String, Object?>> executeShellCommand({
521
+ required String commandId,
464
522
  required String command,
465
523
  String? cwd,
466
524
  int? timeoutMs,
467
525
  String? stdinInput,
526
+ bool requestedPty = false,
527
+ List<String> inputs = const <String>[],
468
528
  }) async {
529
+ if (command.trim().isEmpty) {
530
+ throw ArgumentError.value(command, 'command', 'Command is required.');
531
+ }
469
532
  final shell = Platform.isWindows
470
533
  ? 'cmd.exe'
471
534
  : (Platform.environment['SHELL'] ?? '/bin/sh');
@@ -483,9 +546,18 @@ class DesktopCompanionActions {
483
546
  workingDirectory: workingDir,
484
547
  runInShell: false,
485
548
  );
549
+ if (commandId.isNotEmpty) {
550
+ _shellProcesses[commandId] = process;
551
+ }
486
552
 
487
- if (stdinInput != null && stdinInput.isNotEmpty) {
488
- process.stdin.write(stdinInput);
553
+ if (_cancelledShellCommandIds.contains(commandId)) {
554
+ await _terminateShellProcess(process);
555
+ } else if ((stdinInput != null && stdinInput.isNotEmpty) ||
556
+ inputs.isNotEmpty) {
557
+ if (stdinInput != null) process.stdin.write(stdinInput);
558
+ for (final input in inputs) {
559
+ process.stdin.write(input);
560
+ }
489
561
  await process.stdin.close();
490
562
  } else {
491
563
  unawaited(process.stdin.close());
@@ -494,13 +566,41 @@ class DesktopCompanionActions {
494
566
  const maxChars = 50000;
495
567
  final stdoutBuf = StringBuffer();
496
568
  final stderrBuf = StringBuffer();
497
-
498
- final stdoutSub = process.stdout.transform(utf8.decoder).listen((data) {
499
- stdoutBuf.write(data);
500
- });
501
- final stderrSub = process.stderr.transform(utf8.decoder).listen((data) {
502
- stderrBuf.write(data);
503
- });
569
+ var stdoutChars = 0;
570
+ var stderrChars = 0;
571
+ final stdoutDone = Completer<void>();
572
+ final stderrDone = Completer<void>();
573
+
574
+ final stdoutSub = process.stdout
575
+ .transform(utf8.decoder)
576
+ .listen(
577
+ (data) {
578
+ stdoutChars += data.length;
579
+ final remaining = maxChars - stdoutBuf.length;
580
+ if (remaining > 0) {
581
+ stdoutBuf.write(
582
+ data.substring(0, data.length.clamp(0, remaining)),
583
+ );
584
+ }
585
+ },
586
+ onError: stdoutDone.completeError,
587
+ onDone: stdoutDone.complete,
588
+ );
589
+ final stderrSub = process.stderr
590
+ .transform(utf8.decoder)
591
+ .listen(
592
+ (data) {
593
+ stderrChars += data.length;
594
+ final remaining = maxChars - stderrBuf.length;
595
+ if (remaining > 0) {
596
+ stderrBuf.write(
597
+ data.substring(0, data.length.clamp(0, remaining)),
598
+ );
599
+ }
600
+ },
601
+ onError: stderrDone.completeError,
602
+ onDone: stderrDone.complete,
603
+ );
504
604
 
505
605
  final effectiveTimeout = Duration(
506
606
  milliseconds: (timeoutMs != null && timeoutMs > 0)
@@ -509,38 +609,81 @@ class DesktopCompanionActions {
509
609
  );
510
610
 
511
611
  bool timedOut = false;
612
+ bool externallyCancelled = false;
512
613
  int? exitCode;
513
614
  try {
514
615
  exitCode = await process.exitCode.timeout(effectiveTimeout);
515
616
  } on TimeoutException {
516
617
  timedOut = true;
517
- process.kill(ProcessSignal.sigterm);
518
- exitCode = null;
618
+ exitCode = await _terminateShellProcess(process);
619
+ } finally {
620
+ externallyCancelled = _cancelledShellCommandIds.contains(commandId);
621
+ if (commandId.isNotEmpty) {
622
+ _shellProcesses.remove(commandId);
623
+ _cancelledShellCommandIds.remove(commandId);
624
+ }
519
625
  }
520
626
 
521
- await stdoutSub.cancel();
522
- await stderrSub.cancel();
627
+ try {
628
+ await Future.wait<void>(<Future<void>>[
629
+ stdoutDone.future,
630
+ stderrDone.future,
631
+ ]).timeout(const Duration(seconds: 2));
632
+ } on TimeoutException {
633
+ await stdoutSub.cancel();
634
+ await stderrSub.cancel();
635
+ }
523
636
 
524
- String trimOutput(StringBuffer buf) {
637
+ String trimOutput(StringBuffer buf, int totalChars) {
525
638
  final s = buf.toString().trim();
526
- return s.length > maxChars
527
- ? '${s.substring(0, maxChars)}\n...[truncated, ${s.length} total chars]'
639
+ return totalChars > maxChars
640
+ ? '$s\n...[truncated, $totalChars total chars]'
528
641
  : s;
529
642
  }
530
643
 
531
644
  return <String, Object?>{
532
645
  'exitCode': exitCode,
533
- 'stdout': trimOutput(stdoutBuf),
534
- 'stderr': trimOutput(stderrBuf),
646
+ 'stdout': trimOutput(stdoutBuf, stdoutChars),
647
+ 'stderr': trimOutput(stderrBuf, stderrChars),
535
648
  'timedOut': timedOut,
536
- 'killed': timedOut,
649
+ 'killed': timedOut || externallyCancelled,
650
+ 'cancelled': externallyCancelled,
537
651
  'durationMs': DateTime.now().difference(startedAt).inMilliseconds,
538
652
  'command': command,
539
653
  'cwd': workingDir,
540
654
  'backend': 'desktop-companion',
655
+ 'ptyRequested': requestedPty,
656
+ 'ptyAllocated': false,
541
657
  };
542
658
  }
543
659
 
660
+ Future<Map<String, Object?>> cancelShellCommand(String commandId) async {
661
+ if (commandId.isEmpty) {
662
+ return <String, Object?>{'success': false, 'cancelled': false};
663
+ }
664
+ _cancelledShellCommandIds.add(commandId);
665
+ final process = _shellProcesses[commandId];
666
+ if (process == null) {
667
+ return <String, Object?>{'success': true, 'cancelled': true};
668
+ }
669
+ await _terminateShellProcess(process);
670
+ return <String, Object?>{'success': true, 'cancelled': true};
671
+ }
672
+
673
+ Future<int?> _terminateShellProcess(Process process) async {
674
+ process.kill(ProcessSignal.sigterm);
675
+ try {
676
+ return await process.exitCode.timeout(const Duration(seconds: 2));
677
+ } on TimeoutException {
678
+ process.kill(ProcessSignal.sigkill);
679
+ try {
680
+ return await process.exitCode.timeout(const Duration(seconds: 2));
681
+ } on TimeoutException {
682
+ return null;
683
+ }
684
+ }
685
+ }
686
+
544
687
  Future<Map<String, Object?>> _capabilities({
545
688
  Map<String, Object?>? platformStatus,
546
689
  }) async {
@@ -687,12 +830,13 @@ class DesktopCompanionActions {
687
830
  'IdleHint',
688
831
  ]);
689
832
  if (result.exitCode == 0) {
690
- final lines = result.stdout
691
- ?.toString()
692
- .split(RegExp(r'\r?\n'))
693
- .map((line) => line.trim())
694
- .where((line) => line.isNotEmpty)
695
- .toList(growable: false) ??
833
+ final lines =
834
+ result.stdout
835
+ ?.toString()
836
+ .split(RegExp(r'\r?\n'))
837
+ .map((line) => line.trim())
838
+ .where((line) => line.isNotEmpty)
839
+ .toList(growable: false) ??
696
840
  const <String>[];
697
841
  for (final line in lines) {
698
842
  if (line.startsWith('LockedHint=')) {
@@ -713,8 +857,7 @@ class DesktopCompanionActions {
713
857
  if (idleMs != null) {
714
858
  final idleSeconds = idleMs / 1000;
715
859
  state['idleSeconds'] = idleSeconds;
716
- state['userIdle'] =
717
- (state['userIdle'] == true) || idleSeconds >= 300;
860
+ state['userIdle'] = (state['userIdle'] == true) || idleSeconds >= 300;
718
861
  }
719
862
  }
720
863
  } catch (_) {}
@@ -814,6 +957,17 @@ class DesktopCompanionActions {
814
957
  ];
815
958
  }
816
959
 
960
+ List<Map<String, Object?>> _coerceDisplays(Object? raw) {
961
+ if (raw is! List) return const <Map<String, Object?>>[];
962
+ return raw
963
+ .whereType<Map>()
964
+ .map(
965
+ (item) => item.map((key, value) => MapEntry(key.toString(), value)),
966
+ )
967
+ .where((item) => item['id']?.toString().trim().isNotEmpty == true)
968
+ .toList(growable: false);
969
+ }
970
+
817
971
  Future<void> _run(_ShellCommand command) async {
818
972
  final result = await Process.run(command.command, command.args);
819
973
  if (result.exitCode != 0) {