neoagent 2.3.1-beta.99 → 2.4.1-beta.10
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.
- package/.env.example +6 -3
- package/LICENSE +619 -21
- package/README.md +1 -1
- package/extensions/chrome-browser/background.mjs +19 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +12 -0
- package/extensions/chrome-browser/manifest.json +13 -2
- package/extensions/chrome-browser/popup.css +5 -0
- package/extensions/chrome-browser/popup.html +7 -5
- package/extensions/chrome-browser/popup.js +16 -7
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +391 -0
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +6 -0
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +1 -1
- package/flutter_app/lib/main.dart +2 -0
- package/flutter_app/lib/main_controller.dart +156 -3
- package/flutter_app/lib/main_devices.dart +485 -119
- package/flutter_app/lib/main_integrations.dart +21 -2
- package/flutter_app/lib/main_models.dart +60 -0
- package/flutter_app/lib/main_settings.dart +289 -30
- package/flutter_app/lib/main_theme.dart +31 -2
- package/flutter_app/lib/src/backend_client.dart +89 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +144 -0
- package/flutter_app/lib/src/desktop_companion_io.dart +145 -4
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/stream_renderer.dart +286 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +67 -2
- package/flutter_app/macos/Runner/DebugProfile.entitlements +4 -0
- package/flutter_app/macos/Runner/Release.entitlements +4 -0
- package/flutter_app/pubspec.lock +5 -5
- package/lib/manager.js +164 -2
- package/package.json +2 -2
- package/server/db/database.js +85 -0
- package/server/guest_agent.js +19 -1
- package/server/http/routes.js +191 -0
- package/server/http/socket.js +1 -1
- package/server/index.js +4 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +971 -1066
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/shaders/ink_sparkle.frag +1 -1
- package/server/public/assets/shaders/stretch_effect.frag +1 -1
- package/server/public/canvaskit/canvaskit.js +2 -2
- package/server/public/canvaskit/canvaskit.js.symbols +11796 -11733
- package/server/public/canvaskit/canvaskit.wasm +0 -0
- package/server/public/canvaskit/chromium/canvaskit.js +2 -2
- package/server/public/canvaskit/chromium/canvaskit.js.symbols +10706 -10643
- package/server/public/canvaskit/chromium/canvaskit.wasm +0 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.js +171 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.js.symbols +9134 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.wasm +0 -0
- package/server/public/canvaskit/skwasm.js +14 -14
- package/server/public/canvaskit/skwasm.js.symbols +12787 -12676
- package/server/public/canvaskit/skwasm.wasm +0 -0
- package/server/public/canvaskit/skwasm_heavy.js +14 -14
- package/server/public/canvaskit/skwasm_heavy.js.symbols +14400 -14286
- package/server/public/canvaskit/skwasm_heavy.wasm +0 -0
- package/server/public/canvaskit/wimp.js +94 -95
- package/server/public/canvaskit/wimp.js.symbols +11325 -11177
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +86174 -83144
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/integrations.js +2 -2
- package/server/routes/memory.js +73 -0
- package/server/routes/settings.js +4 -0
- package/server/routes/stream.js +187 -0
- package/server/services/ai/engine.js +65 -26
- package/server/services/ai/models.js +21 -0
- package/server/services/ai/preModelCompaction.js +191 -0
- package/server/services/ai/providers/claudeCode.js +273 -0
- package/server/services/ai/providers/openaiCodex.js +212 -40
- package/server/services/ai/settings.js +12 -2
- package/server/services/ai/tools.js +40 -29
- package/server/services/android/controller.js +41 -2
- package/server/services/browser/controller.js +34 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +188 -18
- package/server/services/desktop/gateway.js +28 -3
- package/server/services/desktop/protocol.js +34 -0
- package/server/services/desktop/provider.js +25 -0
- package/server/services/desktop/registry.js +92 -10
- package/server/services/integrations/google/provider.js +78 -0
- package/server/services/integrations/manager.js +29 -13
- package/server/services/manager.js +44 -2
- package/server/services/memory/ingestion.js +486 -0
- package/server/services/memory/manager.js +422 -0
- package/server/services/memory/openhuman_uplift.test.js +98 -0
- package/server/services/runtime/backends/local-vm.js +6 -0
- package/server/services/runtime/docker-vm-manager.js +26 -3
- package/server/services/runtime/manager.js +36 -5
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +298 -0
- package/server/services/streaming/browser-stream.js +87 -0
- package/server/services/streaming/stream-hub.js +231 -0
- package/server/services/websocket.js +73 -0
- package/server/services/widgets/focus_widget.js +45 -4
|
@@ -175,6 +175,12 @@ class OfficialIntegrationsTab extends StatelessWidget {
|
|
|
175
175
|
label: '${item.availableToolCount} tools',
|
|
176
176
|
icon: Icons.build_outlined,
|
|
177
177
|
),
|
|
178
|
+
_MetaPill(
|
|
179
|
+
label: item.memoryCoverage.supported
|
|
180
|
+
? 'Memory ${item.memoryCoverage.statusLabel}'
|
|
181
|
+
: 'No memory sync',
|
|
182
|
+
icon: Icons.psychology_alt_outlined,
|
|
183
|
+
),
|
|
178
184
|
],
|
|
179
185
|
),
|
|
180
186
|
const SizedBox(height: 10),
|
|
@@ -255,6 +261,7 @@ Future<void> _showTrelloSetupDialog(
|
|
|
255
261
|
final apiKeyController = TextEditingController(text: savedApiKey);
|
|
256
262
|
final tokenInputController = TextEditingController();
|
|
257
263
|
|
|
264
|
+
if (!context.mounted) return;
|
|
258
265
|
await showDialog<void>(
|
|
259
266
|
context: context,
|
|
260
267
|
barrierDismissible: false,
|
|
@@ -426,8 +433,7 @@ Future<void> _showTrelloSetupDialog(
|
|
|
426
433
|
}
|
|
427
434
|
final url = authorizeUrl.isNotEmpty
|
|
428
435
|
? authorizeUrl
|
|
429
|
-
: 'https://trello.com/1/authorize?expiration=never&scope=read,write,account&response_type=token&key
|
|
430
|
-
Uri.encodeComponent(effectiveApiKey);
|
|
436
|
+
: 'https://trello.com/1/authorize?expiration=never&scope=read,write,account&response_type=token&key=${Uri.encodeComponent(effectiveApiKey)}';
|
|
431
437
|
final result = await controller._oauthLauncher
|
|
432
438
|
.openExternal(url: url, label: 'Trello');
|
|
433
439
|
if (!result.launched) {
|
|
@@ -659,6 +665,12 @@ class _OfficialIntegrationAppCard extends StatelessWidget {
|
|
|
659
665
|
label: '${app.availableToolCount} tools',
|
|
660
666
|
icon: Icons.build_circle_outlined,
|
|
661
667
|
),
|
|
668
|
+
_MetaPill(
|
|
669
|
+
label: app.memoryCoverage.supported
|
|
670
|
+
? 'Memory ${app.memoryCoverage.statusLabel}'
|
|
671
|
+
: 'No memory sync',
|
|
672
|
+
icon: Icons.psychology_alt_outlined,
|
|
673
|
+
),
|
|
662
674
|
],
|
|
663
675
|
),
|
|
664
676
|
],
|
|
@@ -715,6 +727,13 @@ class _OfficialIntegrationAppCard extends StatelessWidget {
|
|
|
715
727
|
'Access: ${account.accessModeLabel}',
|
|
716
728
|
style: TextStyle(color: _textSecondary),
|
|
717
729
|
),
|
|
730
|
+
if (account.memoryCoverage.supported) ...<Widget>[
|
|
731
|
+
const SizedBox(height: 4),
|
|
732
|
+
Text(
|
|
733
|
+
'Memory: ${account.memoryCoverage.statusLabel}',
|
|
734
|
+
style: TextStyle(color: _textSecondary),
|
|
735
|
+
),
|
|
736
|
+
],
|
|
718
737
|
const SizedBox(height: 12),
|
|
719
738
|
Wrap(
|
|
720
739
|
spacing: 8,
|
|
@@ -2565,6 +2565,7 @@ class OfficialIntegrationAppItem {
|
|
|
2565
2565
|
),
|
|
2566
2566
|
this.accounts = const <OfficialIntegrationAccountItem>[],
|
|
2567
2567
|
this.availableToolCount = 0,
|
|
2568
|
+
this.memoryCoverage = const OfficialIntegrationMemoryCoverage(),
|
|
2568
2569
|
});
|
|
2569
2570
|
|
|
2570
2571
|
factory OfficialIntegrationAppItem.fromJson(Map<dynamic, dynamic> json) {
|
|
@@ -2583,6 +2584,9 @@ class OfficialIntegrationAppItem {
|
|
|
2583
2584
|
.toList()
|
|
2584
2585
|
: const <OfficialIntegrationAccountItem>[],
|
|
2585
2586
|
availableToolCount: _asInt(json['availableToolCount']),
|
|
2587
|
+
memoryCoverage: OfficialIntegrationMemoryCoverage.fromJson(
|
|
2588
|
+
_jsonMap(json['memoryCoverage']),
|
|
2589
|
+
),
|
|
2586
2590
|
);
|
|
2587
2591
|
}
|
|
2588
2592
|
|
|
@@ -2592,6 +2596,7 @@ class OfficialIntegrationAppItem {
|
|
|
2592
2596
|
final OfficialIntegrationConnectionStatus connection;
|
|
2593
2597
|
final List<OfficialIntegrationAccountItem> accounts;
|
|
2594
2598
|
final int availableToolCount;
|
|
2599
|
+
final OfficialIntegrationMemoryCoverage memoryCoverage;
|
|
2595
2600
|
|
|
2596
2601
|
bool get isConnected => connection.connected;
|
|
2597
2602
|
|
|
@@ -2676,6 +2681,51 @@ class OfficialIntegrationConnectionStatus {
|
|
|
2676
2681
|
}
|
|
2677
2682
|
}
|
|
2678
2683
|
|
|
2684
|
+
class OfficialIntegrationMemoryCoverage {
|
|
2685
|
+
const OfficialIntegrationMemoryCoverage({
|
|
2686
|
+
this.supported = false,
|
|
2687
|
+
this.contributesToMemory = false,
|
|
2688
|
+
this.contributesToTaskExecution = false,
|
|
2689
|
+
this.status = 'not_supported',
|
|
2690
|
+
this.dataDomains = const <String>[],
|
|
2691
|
+
this.documentCount = 0,
|
|
2692
|
+
this.lastRefreshAt,
|
|
2693
|
+
this.nextRefreshAt,
|
|
2694
|
+
this.error,
|
|
2695
|
+
});
|
|
2696
|
+
|
|
2697
|
+
factory OfficialIntegrationMemoryCoverage.fromJson(
|
|
2698
|
+
Map<dynamic, dynamic> json,
|
|
2699
|
+
) {
|
|
2700
|
+
final domainsRaw = json['dataDomains'];
|
|
2701
|
+
return OfficialIntegrationMemoryCoverage(
|
|
2702
|
+
supported: json['supported'] == true,
|
|
2703
|
+
contributesToMemory: json['contributesToMemory'] == true,
|
|
2704
|
+
contributesToTaskExecution: json['contributesToTaskExecution'] == true,
|
|
2705
|
+
status: json['status']?.toString() ?? 'not_supported',
|
|
2706
|
+
dataDomains: domainsRaw is List
|
|
2707
|
+
? domainsRaw.map((item) => item.toString()).toList()
|
|
2708
|
+
: const <String>[],
|
|
2709
|
+
documentCount: _asInt(json['documentCount']),
|
|
2710
|
+
lastRefreshAt: _parseOptionalTimestamp(json['lastRefreshAt']?.toString()),
|
|
2711
|
+
nextRefreshAt: _parseOptionalTimestamp(json['nextRefreshAt']?.toString()),
|
|
2712
|
+
error: json['error']?.toString(),
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
final bool supported;
|
|
2717
|
+
final bool contributesToMemory;
|
|
2718
|
+
final bool contributesToTaskExecution;
|
|
2719
|
+
final String status;
|
|
2720
|
+
final List<String> dataDomains;
|
|
2721
|
+
final int documentCount;
|
|
2722
|
+
final DateTime? lastRefreshAt;
|
|
2723
|
+
final DateTime? nextRefreshAt;
|
|
2724
|
+
final String? error;
|
|
2725
|
+
|
|
2726
|
+
String get statusLabel => _titleCase(status.replaceAll('_', ' '));
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2679
2729
|
class OfficialIntegrationAccountItem {
|
|
2680
2730
|
const OfficialIntegrationAccountItem({
|
|
2681
2731
|
required this.id,
|
|
@@ -2684,6 +2734,7 @@ class OfficialIntegrationAccountItem {
|
|
|
2684
2734
|
this.accountEmail,
|
|
2685
2735
|
this.lastConnectedAt,
|
|
2686
2736
|
this.accessMode = 'read_write',
|
|
2737
|
+
this.memoryCoverage = const OfficialIntegrationMemoryCoverage(),
|
|
2687
2738
|
});
|
|
2688
2739
|
|
|
2689
2740
|
factory OfficialIntegrationAccountItem.fromJson(Map<dynamic, dynamic> json) {
|
|
@@ -2696,6 +2747,9 @@ class OfficialIntegrationAccountItem {
|
|
|
2696
2747
|
json['lastConnectedAt']?.toString(),
|
|
2697
2748
|
),
|
|
2698
2749
|
accessMode: json['accessMode']?.toString() ?? 'read_write',
|
|
2750
|
+
memoryCoverage: OfficialIntegrationMemoryCoverage.fromJson(
|
|
2751
|
+
_jsonMap(json['memoryCoverage']),
|
|
2752
|
+
),
|
|
2699
2753
|
);
|
|
2700
2754
|
}
|
|
2701
2755
|
|
|
@@ -2705,6 +2759,7 @@ class OfficialIntegrationAccountItem {
|
|
|
2705
2759
|
final String? accountEmail;
|
|
2706
2760
|
final DateTime? lastConnectedAt;
|
|
2707
2761
|
final String accessMode;
|
|
2762
|
+
final OfficialIntegrationMemoryCoverage memoryCoverage;
|
|
2708
2763
|
|
|
2709
2764
|
bool get isExpired => status == 'expired';
|
|
2710
2765
|
|
|
@@ -2733,6 +2788,7 @@ class OfficialIntegrationItem {
|
|
|
2733
2788
|
this.connectPrompt,
|
|
2734
2789
|
this.supportsMultipleAccounts = true,
|
|
2735
2790
|
this.connectionMethod = 'oauth',
|
|
2791
|
+
this.memoryCoverage = const OfficialIntegrationMemoryCoverage(),
|
|
2736
2792
|
});
|
|
2737
2793
|
|
|
2738
2794
|
factory OfficialIntegrationItem.fromJson(Map<dynamic, dynamic> json) {
|
|
@@ -2756,6 +2812,9 @@ class OfficialIntegrationItem {
|
|
|
2756
2812
|
connectPrompt: json['connectPrompt']?.toString(),
|
|
2757
2813
|
supportsMultipleAccounts: json['supportsMultipleAccounts'] != false,
|
|
2758
2814
|
connectionMethod: json['connectionMethod']?.toString() ?? 'oauth',
|
|
2815
|
+
memoryCoverage: OfficialIntegrationMemoryCoverage.fromJson(
|
|
2816
|
+
_jsonMap(json['memoryCoverage']),
|
|
2817
|
+
),
|
|
2759
2818
|
);
|
|
2760
2819
|
}
|
|
2761
2820
|
|
|
@@ -2770,6 +2829,7 @@ class OfficialIntegrationItem {
|
|
|
2770
2829
|
final String? connectPrompt;
|
|
2771
2830
|
final bool supportsMultipleAccounts;
|
|
2772
2831
|
final String connectionMethod;
|
|
2832
|
+
final OfficialIntegrationMemoryCoverage memoryCoverage;
|
|
2773
2833
|
|
|
2774
2834
|
bool get isConnected => connection.connected;
|
|
2775
2835
|
|
|
@@ -170,7 +170,9 @@ const List<_SettingsSection> _settingsSearchSections = <_SettingsSection>[
|
|
|
170
170
|
class _SettingsPanelState extends State<SettingsPanel> {
|
|
171
171
|
late final TextEditingController _searchController;
|
|
172
172
|
late String _browserBackend;
|
|
173
|
+
String? _browserExtensionTokenId;
|
|
173
174
|
late String _cliBackend;
|
|
175
|
+
String? _cliDesktopDeviceId;
|
|
174
176
|
late bool _smarterSelector;
|
|
175
177
|
late Set<String> _enabledModels;
|
|
176
178
|
late String _defaultChatModel;
|
|
@@ -187,6 +189,14 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
187
189
|
<String, TextEditingController>{};
|
|
188
190
|
final Set<String> _expandedProviderIds = <String>{};
|
|
189
191
|
|
|
192
|
+
// Inline runtime test state — ephemeral, not stored in controller.
|
|
193
|
+
bool _cliTestRunning = false;
|
|
194
|
+
Map<String, dynamic>? _cliTestResult;
|
|
195
|
+
bool _extensionTestRunning = false;
|
|
196
|
+
Map<String, dynamic>? _extensionTestResult;
|
|
197
|
+
bool _desktopTestRunning = false;
|
|
198
|
+
Map<String, dynamic>? _desktopTestResult;
|
|
199
|
+
|
|
190
200
|
@override
|
|
191
201
|
void initState() {
|
|
192
202
|
super.initState();
|
|
@@ -224,7 +234,11 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
224
234
|
.map((model) => model.id)
|
|
225
235
|
.toSet();
|
|
226
236
|
_browserBackend = _normalizeBrowserBackend(controller.browserBackend);
|
|
237
|
+
_browserExtensionTokenId =
|
|
238
|
+
controller.browserExtensionTokenId ??
|
|
239
|
+
controller.selectedBrowserExtensionTokenId;
|
|
227
240
|
_cliBackend = _normalizeCliBackend(controller.cliBackend);
|
|
241
|
+
_cliDesktopDeviceId = controller.cliDesktopDeviceId;
|
|
228
242
|
_smarterSelector = controller.smarterSelector;
|
|
229
243
|
_enabledModels = controller.enabledModelIds
|
|
230
244
|
.where((id) => knownModels.contains(id))
|
|
@@ -466,7 +480,11 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
466
480
|
browserBackend: _browserBackend == 'extension'
|
|
467
481
|
? 'extension'
|
|
468
482
|
: 'vm',
|
|
483
|
+
browserExtensionTokenId: _browserBackend == 'extension'
|
|
484
|
+
? _browserExtensionTokenId
|
|
485
|
+
: null,
|
|
469
486
|
cliBackend: _cliBackend == 'desktop' ? 'desktop' : 'vm',
|
|
487
|
+
cliDesktopDeviceId: _cliDesktopDeviceId,
|
|
470
488
|
smarterSelector: _smarterSelector,
|
|
471
489
|
enabledModels: _enabledModels.toList(),
|
|
472
490
|
defaultChatModel: _defaultChatModel,
|
|
@@ -631,36 +649,107 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
631
649
|
],
|
|
632
650
|
onChanged: (value) {
|
|
633
651
|
if (value != null) {
|
|
634
|
-
setState(()
|
|
652
|
+
setState(() {
|
|
653
|
+
_browserBackend = value;
|
|
654
|
+
_browserExtensionTokenId ??=
|
|
655
|
+
controller.selectedBrowserExtensionTokenId;
|
|
656
|
+
});
|
|
635
657
|
}
|
|
636
658
|
},
|
|
637
659
|
),
|
|
638
660
|
const SizedBox(height: 10),
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
+
if (_browserBackend == 'extension') ...<Widget>[
|
|
662
|
+
if (controller.browserExtensionTokens.isNotEmpty) ...<Widget>[
|
|
663
|
+
DropdownButtonFormField<String>(
|
|
664
|
+
initialValue: controller.browserExtensionTokens.any(
|
|
665
|
+
(token) => token['tokenId']?.toString() == _browserExtensionTokenId,
|
|
666
|
+
)
|
|
667
|
+
? _browserExtensionTokenId
|
|
668
|
+
: null,
|
|
669
|
+
decoration: const InputDecoration(
|
|
670
|
+
labelText: 'Default extension',
|
|
671
|
+
helperText: 'Choose which paired Chrome extension controls browser actions.',
|
|
672
|
+
),
|
|
673
|
+
items: controller.browserExtensionTokens.map((token) {
|
|
674
|
+
final tokenId = token['tokenId']?.toString() ?? '';
|
|
675
|
+
final label = token['name']?.toString().trim().isNotEmpty == true
|
|
676
|
+
? token['name'].toString()
|
|
677
|
+
: tokenId;
|
|
678
|
+
final online = token['online'] == true || token['connected'] == true;
|
|
679
|
+
return DropdownMenuItem<String>(
|
|
680
|
+
value: tokenId,
|
|
681
|
+
child: Row(
|
|
682
|
+
children: <Widget>[
|
|
683
|
+
Icon(
|
|
684
|
+
online ? Icons.circle : Icons.circle_outlined,
|
|
685
|
+
size: 10,
|
|
686
|
+
color: online ? Colors.green : Colors.grey,
|
|
687
|
+
),
|
|
688
|
+
const SizedBox(width: 8),
|
|
689
|
+
Flexible(
|
|
690
|
+
child: Text(
|
|
691
|
+
label,
|
|
692
|
+
maxLines: 1,
|
|
693
|
+
overflow: TextOverflow.ellipsis,
|
|
694
|
+
),
|
|
695
|
+
),
|
|
696
|
+
],
|
|
697
|
+
),
|
|
698
|
+
);
|
|
699
|
+
}).toList(),
|
|
700
|
+
onChanged: (value) {
|
|
701
|
+
if (value != null) {
|
|
702
|
+
setState(() => _browserExtensionTokenId = value);
|
|
703
|
+
}
|
|
704
|
+
},
|
|
661
705
|
),
|
|
706
|
+
const SizedBox(height: 10),
|
|
662
707
|
],
|
|
663
|
-
|
|
708
|
+
_buildInlineTestRow(
|
|
709
|
+
label: 'Chrome extension',
|
|
710
|
+
running: _extensionTestRunning,
|
|
711
|
+
result: _extensionTestResult,
|
|
712
|
+
note: controller.browserExtensionConnected
|
|
713
|
+
? 'Connected — tap Test to verify the live link.'
|
|
714
|
+
: 'Not connected — download the extension, load it in Chrome, then pair after login.',
|
|
715
|
+
onTest: () async {
|
|
716
|
+
setState(() { _extensionTestRunning = true; _extensionTestResult = null; });
|
|
717
|
+
try {
|
|
718
|
+
final r = await controller.testBrowserExtension();
|
|
719
|
+
if (mounted) {
|
|
720
|
+
setState(() => _extensionTestResult = r);
|
|
721
|
+
}
|
|
722
|
+
} catch (e) {
|
|
723
|
+
if (mounted) {
|
|
724
|
+
setState(() => _extensionTestResult = <String, dynamic>{'passed': false, 'detail': e.toString()});
|
|
725
|
+
}
|
|
726
|
+
} finally {
|
|
727
|
+
if (mounted) {
|
|
728
|
+
setState(() => _extensionTestRunning = false);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
),
|
|
733
|
+
const SizedBox(height: 10),
|
|
734
|
+
Wrap(
|
|
735
|
+
spacing: 10,
|
|
736
|
+
runSpacing: 10,
|
|
737
|
+
children: <Widget>[
|
|
738
|
+
OutlinedButton.icon(
|
|
739
|
+
onPressed: controller.downloadBrowserExtension,
|
|
740
|
+
icon: Icon(Icons.download_outlined),
|
|
741
|
+
label: Text('Download extension'),
|
|
742
|
+
),
|
|
743
|
+
OutlinedButton.icon(
|
|
744
|
+
onPressed: controller.refreshBrowserExtensionStatus,
|
|
745
|
+
icon: Icon(Icons.sync),
|
|
746
|
+
label: Text('Refresh'),
|
|
747
|
+
),
|
|
748
|
+
],
|
|
749
|
+
),
|
|
750
|
+
] else ...<Widget>[
|
|
751
|
+
Text('Cloud browser runtime is active.', style: TextStyle(color: _textSecondary, height: 1.4)),
|
|
752
|
+
],
|
|
664
753
|
const Divider(height: 32),
|
|
665
754
|
Text(
|
|
666
755
|
'CLI Runtime',
|
|
@@ -691,14 +780,73 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
691
780
|
}
|
|
692
781
|
},
|
|
693
782
|
),
|
|
783
|
+
if (_cliBackend == 'desktop' && controller.desktopDevices.length > 1) ...<Widget>[
|
|
784
|
+
const SizedBox(height: 12),
|
|
785
|
+
DropdownButtonFormField<String>(
|
|
786
|
+
initialValue: controller.desktopDevices.any(
|
|
787
|
+
(d) => d['deviceId']?.toString() == _cliDesktopDeviceId,
|
|
788
|
+
)
|
|
789
|
+
? _cliDesktopDeviceId
|
|
790
|
+
: null,
|
|
791
|
+
decoration: const InputDecoration(
|
|
792
|
+
labelText: 'Desktop device',
|
|
793
|
+
helperText: 'Choose which desktop companion runs CLI commands.',
|
|
794
|
+
),
|
|
795
|
+
items: controller.desktopDevices.map((device) {
|
|
796
|
+
final deviceId = device['deviceId']?.toString() ?? '';
|
|
797
|
+
final label = device['hostname']?.toString().isNotEmpty == true
|
|
798
|
+
? device['hostname']!.toString()
|
|
799
|
+
: deviceId;
|
|
800
|
+
final online = device['online'] == true;
|
|
801
|
+
return DropdownMenuItem<String>(
|
|
802
|
+
value: deviceId,
|
|
803
|
+
child: Row(
|
|
804
|
+
children: <Widget>[
|
|
805
|
+
Icon(
|
|
806
|
+
online ? Icons.circle : Icons.circle_outlined,
|
|
807
|
+
size: 10,
|
|
808
|
+
color: online ? Colors.green : Colors.grey,
|
|
809
|
+
),
|
|
810
|
+
const SizedBox(width: 8),
|
|
811
|
+
Text(label),
|
|
812
|
+
],
|
|
813
|
+
),
|
|
814
|
+
);
|
|
815
|
+
}).toList(),
|
|
816
|
+
onChanged: (value) {
|
|
817
|
+
if (value != null) {
|
|
818
|
+
setState(() => _cliDesktopDeviceId = value);
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
),
|
|
822
|
+
],
|
|
694
823
|
const SizedBox(height: 10),
|
|
695
|
-
|
|
696
|
-
|
|
824
|
+
_buildInlineTestRow(
|
|
825
|
+
label: 'CLI',
|
|
826
|
+
running: _cliTestRunning,
|
|
827
|
+
result: _cliTestResult,
|
|
828
|
+
note: _cliBackend == 'desktop'
|
|
697
829
|
? (controller.desktopCompanionConnected
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
: 'Cloud
|
|
701
|
-
|
|
830
|
+
? 'Desktop app connected — commands route locally through the companion.'
|
|
831
|
+
: 'Desktop app selected but not connected. Commands fall back to cloud VM until the companion is online.')
|
|
832
|
+
: 'Cloud VM — commands run in an isolated container.',
|
|
833
|
+
onTest: () async {
|
|
834
|
+
setState(() { _cliTestRunning = true; _cliTestResult = null; });
|
|
835
|
+
try {
|
|
836
|
+
final r = await controller.testCliRuntime();
|
|
837
|
+
if (mounted) {
|
|
838
|
+
setState(() => _cliTestResult = r);
|
|
839
|
+
}
|
|
840
|
+
} catch (e) {
|
|
841
|
+
if (mounted) {
|
|
842
|
+
setState(() => _cliTestResult = <String, dynamic>{'passed': false, 'detail': e.toString()});
|
|
843
|
+
}
|
|
844
|
+
} finally {
|
|
845
|
+
if (mounted) {
|
|
846
|
+
setState(() => _cliTestRunning = false);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
},
|
|
702
850
|
),
|
|
703
851
|
const Divider(height: 32),
|
|
704
852
|
Text(
|
|
@@ -1344,6 +1492,53 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1344
1492
|
const SizedBox(height: 12),
|
|
1345
1493
|
_InlineError(message: message),
|
|
1346
1494
|
],
|
|
1495
|
+
const SizedBox(height: 12),
|
|
1496
|
+
_buildInlineTestRow(
|
|
1497
|
+
label: 'Desktop companion',
|
|
1498
|
+
running: _desktopTestRunning,
|
|
1499
|
+
result: _desktopTestResult != null
|
|
1500
|
+
? <String, dynamic>{
|
|
1501
|
+
'passed': _desktopTestResult!['passed'] == true,
|
|
1502
|
+
'detail': _desktopTestResult!['detail']?.toString() ?? '',
|
|
1503
|
+
}
|
|
1504
|
+
: null,
|
|
1505
|
+
note: controller.desktopCompanionConnected
|
|
1506
|
+
? 'Connected — tap Test to fetch live device status from the server.'
|
|
1507
|
+
: 'Not connected. Make sure the desktop app is running on the target machine.',
|
|
1508
|
+
onTest: () async {
|
|
1509
|
+
setState(() { _desktopTestRunning = true; _desktopTestResult = null; });
|
|
1510
|
+
try {
|
|
1511
|
+
final r = await controller.testDesktopCompanion();
|
|
1512
|
+
final active = r['activeDevice'];
|
|
1513
|
+
final multi = r['multipleOnline'] == true;
|
|
1514
|
+
String detail = r['detail']?.toString() ?? '';
|
|
1515
|
+
if (r['passed'] == true && active != null) {
|
|
1516
|
+
final label = active['label']?.toString() ?? 'Device';
|
|
1517
|
+
final plat = active['platform']?.toString() ?? '';
|
|
1518
|
+
final sc = active['permissions']?['screenCapture'] == true;
|
|
1519
|
+
final ic = active['permissions']?['inputControl'] == true;
|
|
1520
|
+
detail = '$label${plat.isNotEmpty ? " ($plat)" : ""}'
|
|
1521
|
+
' — screen: ${sc ? "✓" : "✗"}, input: ${ic ? "✓" : "✗"}';
|
|
1522
|
+
} else if (multi) {
|
|
1523
|
+
detail = '${r['onlineCount']} devices online — select one in Desktop › Devices';
|
|
1524
|
+
}
|
|
1525
|
+
if (mounted) {
|
|
1526
|
+
setState(() => _desktopTestResult = <String, dynamic>{
|
|
1527
|
+
...r,
|
|
1528
|
+
'detail': detail,
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
} catch (e) {
|
|
1532
|
+
if (mounted) {
|
|
1533
|
+
setState(() => _desktopTestResult = <String, dynamic>{'passed': false, 'detail': e.toString()});
|
|
1534
|
+
}
|
|
1535
|
+
} finally {
|
|
1536
|
+
if (mounted) {
|
|
1537
|
+
setState(() => _desktopTestRunning = false);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
},
|
|
1541
|
+
),
|
|
1347
1542
|
const SizedBox(height: 14),
|
|
1348
1543
|
Builder(
|
|
1349
1544
|
builder: (context) {
|
|
@@ -1999,6 +2194,70 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1999
2194
|
)
|
|
2000
2195
|
.toList();
|
|
2001
2196
|
}
|
|
2197
|
+
|
|
2198
|
+
// Shared helper: small "Test" button + inline result row.
|
|
2199
|
+
Widget _buildInlineTestRow({
|
|
2200
|
+
required String label,
|
|
2201
|
+
required bool running,
|
|
2202
|
+
required Map<String, dynamic>? result,
|
|
2203
|
+
required VoidCallback onTest,
|
|
2204
|
+
String? note,
|
|
2205
|
+
}) {
|
|
2206
|
+
final passed = result?['passed'] == true;
|
|
2207
|
+
final detail = result?['detail']?.toString() ?? '';
|
|
2208
|
+
return Row(
|
|
2209
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2210
|
+
children: <Widget>[
|
|
2211
|
+
Expanded(
|
|
2212
|
+
child: Column(
|
|
2213
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2214
|
+
children: <Widget>[
|
|
2215
|
+
if (result != null)
|
|
2216
|
+
Row(
|
|
2217
|
+
children: <Widget>[
|
|
2218
|
+
Icon(
|
|
2219
|
+
passed ? Icons.check_circle_rounded : Icons.cancel_rounded,
|
|
2220
|
+
size: 15,
|
|
2221
|
+
color: passed ? const Color(0xFF22C55E) : const Color(0xFFEF4444),
|
|
2222
|
+
),
|
|
2223
|
+
const SizedBox(width: 6),
|
|
2224
|
+
Expanded(
|
|
2225
|
+
child: Text(
|
|
2226
|
+
passed ? (detail.isNotEmpty ? detail : '$label: OK') : detail,
|
|
2227
|
+
style: TextStyle(
|
|
2228
|
+
fontSize: 13,
|
|
2229
|
+
color: passed ? null : const Color(0xFFEF4444),
|
|
2230
|
+
),
|
|
2231
|
+
),
|
|
2232
|
+
),
|
|
2233
|
+
],
|
|
2234
|
+
)
|
|
2235
|
+
else if (note != null)
|
|
2236
|
+
Text(note, style: TextStyle(fontSize: 13, color: _textSecondary, height: 1.4)),
|
|
2237
|
+
],
|
|
2238
|
+
),
|
|
2239
|
+
),
|
|
2240
|
+
const SizedBox(width: 12),
|
|
2241
|
+
SizedBox(
|
|
2242
|
+
width: 80,
|
|
2243
|
+
child: OutlinedButton(
|
|
2244
|
+
onPressed: running ? null : onTest,
|
|
2245
|
+
style: OutlinedButton.styleFrom(
|
|
2246
|
+
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
2247
|
+
textStyle: const TextStyle(fontSize: 12),
|
|
2248
|
+
),
|
|
2249
|
+
child: running
|
|
2250
|
+
? const SizedBox(
|
|
2251
|
+
width: 13,
|
|
2252
|
+
height: 13,
|
|
2253
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
2254
|
+
)
|
|
2255
|
+
: const Text('Test'),
|
|
2256
|
+
),
|
|
2257
|
+
),
|
|
2258
|
+
],
|
|
2259
|
+
);
|
|
2260
|
+
}
|
|
2002
2261
|
}
|
|
2003
2262
|
|
|
2004
2263
|
class _AiProviderCard extends StatelessWidget {
|
|
@@ -224,8 +224,8 @@ ThemeData _buildNeoAgentTheme(NeoAgentPalette palette, Brightness brightness) {
|
|
|
224
224
|
pageTransitionsTheme: const PageTransitionsTheme(
|
|
225
225
|
builders: <TargetPlatform, PageTransitionsBuilder>{
|
|
226
226
|
TargetPlatform.android: FadeForwardsPageTransitionsBuilder(),
|
|
227
|
-
TargetPlatform.iOS:
|
|
228
|
-
TargetPlatform.macOS:
|
|
227
|
+
TargetPlatform.iOS: _NeoAgentCupertinoPageTransitionsBuilder(),
|
|
228
|
+
TargetPlatform.macOS: _NeoAgentCupertinoPageTransitionsBuilder(),
|
|
229
229
|
TargetPlatform.windows: FadeForwardsPageTransitionsBuilder(),
|
|
230
230
|
TargetPlatform.linux: FadeForwardsPageTransitionsBuilder(),
|
|
231
231
|
},
|
|
@@ -274,3 +274,32 @@ ThemeData _buildNeoAgentTheme(NeoAgentPalette palette, Brightness brightness) {
|
|
|
274
274
|
),
|
|
275
275
|
);
|
|
276
276
|
}
|
|
277
|
+
|
|
278
|
+
class _NeoAgentCupertinoPageTransitionsBuilder extends PageTransitionsBuilder {
|
|
279
|
+
const _NeoAgentCupertinoPageTransitionsBuilder();
|
|
280
|
+
|
|
281
|
+
@override
|
|
282
|
+
Duration get transitionDuration =>
|
|
283
|
+
cupertino.CupertinoRouteTransitionMixin.kTransitionDuration;
|
|
284
|
+
|
|
285
|
+
@override
|
|
286
|
+
DelegatedTransitionBuilder? get delegatedTransition =>
|
|
287
|
+
cupertino.CupertinoPageTransition.delegatedTransition;
|
|
288
|
+
|
|
289
|
+
@override
|
|
290
|
+
Widget buildTransitions<T>(
|
|
291
|
+
PageRoute<T> route,
|
|
292
|
+
BuildContext context,
|
|
293
|
+
Animation<double> animation,
|
|
294
|
+
Animation<double> secondaryAnimation,
|
|
295
|
+
Widget child,
|
|
296
|
+
) {
|
|
297
|
+
return cupertino.CupertinoRouteTransitionMixin.buildPageTransitions<T>(
|
|
298
|
+
route,
|
|
299
|
+
context,
|
|
300
|
+
animation,
|
|
301
|
+
secondaryAnimation,
|
|
302
|
+
child,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|