diff --git a/lib/features/assistant/assistant_page.dart b/lib/features/assistant/assistant_page.dart index b4dc74c5..0147fce3 100644 --- a/lib/features/assistant/assistant_page.dart +++ b/lib/features/assistant/assistant_page.dart @@ -6,7 +6,6 @@ export 'assistant_page_composer_support.dart'; export 'assistant_page_tooltip_labels.dart'; export 'assistant_page_message_widgets.dart'; export 'assistant_page_task_models.dart'; -export 'assistant_page_composer_skill_models.dart'; export 'assistant_page_composer_skill_picker.dart'; export 'assistant_page_composer_clipboard.dart'; export 'assistant_page_components_core.dart'; diff --git a/lib/features/assistant/assistant_page_components.dart b/lib/features/assistant/assistant_page_components.dart index 9f5a8f6d..fe9b5e12 100644 --- a/lib/features/assistant/assistant_page_components.dart +++ b/lib/features/assistant/assistant_page_components.dart @@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_message_widgets.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_page_components_core.dart'; diff --git a/lib/features/assistant/assistant_page_components_core.dart b/lib/features/assistant/assistant_page_components_core.dart index 7c2f3eaa..ad698c6c 100644 --- a/lib/features/assistant/assistant_page_components_core.dart +++ b/lib/features/assistant/assistant_page_components_core.dart @@ -33,7 +33,6 @@ import 'assistant_page_composer_support.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_message_widgets.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; diff --git a/lib/features/assistant/assistant_page_composer_bar.dart b/lib/features/assistant/assistant_page_composer_bar.dart index c8363d34..c95fdf93 100644 --- a/lib/features/assistant/assistant_page_composer_bar.dart +++ b/lib/features/assistant/assistant_page_composer_bar.dart @@ -31,7 +31,6 @@ import 'assistant_page_composer_support.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_message_widgets.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_page_components_core.dart'; diff --git a/lib/features/assistant/assistant_page_composer_clipboard.dart b/lib/features/assistant/assistant_page_composer_clipboard.dart index 41e16383..6a7d8090 100644 --- a/lib/features/assistant/assistant_page_composer_clipboard.dart +++ b/lib/features/assistant/assistant_page_composer_clipboard.dart @@ -33,7 +33,6 @@ import 'assistant_page_composer_support.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_message_widgets.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_components_core.dart'; diff --git a/lib/features/assistant/assistant_page_composer_skill_models.dart b/lib/features/assistant/assistant_page_composer_skill_models.dart deleted file mode 100644 index 49d12623..00000000 --- a/lib/features/assistant/assistant_page_composer_skill_models.dart +++ /dev/null @@ -1,124 +0,0 @@ -// ignore_for_file: unused_import, unnecessary_import - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math' as math; -import 'package:file_selector/file_selector.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:markdown/markdown.dart' as md; -import 'package:path_provider/path_provider.dart'; -import 'package:super_clipboard/super_clipboard.dart'; -import '../../app/app_controller.dart'; -import '../../app/app_metadata.dart'; -import '../../app/ui_feature_manifest.dart'; -import '../../i18n/app_language.dart'; -import '../../models/app_models.dart'; -import '../../runtime/multi_agent_orchestrator.dart'; -import '../../runtime/runtime_models.dart'; -import '../../theme/app_palette.dart'; -import '../../theme/app_theme.dart'; -import '../../widgets/assistant_focus_panel.dart'; -import '../../widgets/assistant_artifact_sidebar.dart'; -import '../../widgets/desktop_workspace_scaffold.dart'; -import '../../widgets/pane_resize_handle.dart'; -import '../../widgets/surface_card.dart'; -import 'assistant_page_main.dart'; -import 'assistant_page_components.dart'; -import 'assistant_page_composer_bar.dart'; -import 'assistant_page_composer_state_helpers.dart'; -import 'assistant_page_composer_support.dart'; -import 'assistant_page_tooltip_labels.dart'; -import 'assistant_page_message_widgets.dart'; -import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_picker.dart'; -import 'assistant_page_composer_clipboard.dart'; -import 'assistant_page_components_core.dart'; - -ComposerSkillOptionInternal skillOptionFromGatewayInternal( - GatewaySkillSummary skill, -) { - final key = skill.skillKey.trim().isEmpty - ? skill.name.trim().toLowerCase() - : skill.skillKey.trim(); - final label = skill.name.trim().isEmpty ? key : skill.name.trim(); - final sourceLabel = skill.source.trim().isEmpty ? 'Gateway' : skill.source; - final group = skillGroupForSourceInternal(skill.source); - final description = skill.description.trim().isEmpty - ? appText('可在当前任务中调用的技能。', 'Skill available in the current task.') - : skill.description.trim(); - - return ComposerSkillOptionInternal( - key: key, - label: label, - description: description, - sourceLabel: sourceLabel, - groupLabel: group.label, - groupSortOrder: group.sortOrder, - icon: Icons.key_rounded, - ); -} - -ComposerSkillGroupInternal skillGroupForSourceInternal(String source) { - final normalized = source.trim().toLowerCase(); - if (normalized == 'openclaw-workspace') { - return const ComposerSkillGroupInternal( - label: 'Workspace Skills', - sortOrder: 0, - ); - } - if (normalized.startsWith('agents-skills-') || - normalized == 'agent' || - normalized.startsWith('agent-') || - normalized.contains('personal')) { - return const ComposerSkillGroupInternal( - label: 'Agent Skills', - sortOrder: 1, - ); - } - if (normalized == 'bridge' || normalized == 'gateway') { - return const ComposerSkillGroupInternal( - label: 'Gateway Skills', - sortOrder: 2, - ); - } - if (normalized.isEmpty) { - return const ComposerSkillGroupInternal( - label: 'Gateway Skills', - sortOrder: 2, - ); - } - return const ComposerSkillGroupInternal(label: 'Other Skills', sortOrder: 3); -} - -class ComposerSkillGroupInternal { - const ComposerSkillGroupInternal({ - required this.label, - required this.sortOrder, - }); - - final String label; - final int sortOrder; -} - -class ComposerSkillOptionInternal { - const ComposerSkillOptionInternal({ - required this.key, - required this.label, - required this.description, - required this.sourceLabel, - required this.groupLabel, - required this.groupSortOrder, - required this.icon, - }); - - final String key; - final String label; - final String description; - final String sourceLabel; - final String groupLabel; - final int groupSortOrder; - final IconData icon; -} diff --git a/lib/features/assistant/assistant_page_composer_skill_picker.dart b/lib/features/assistant/assistant_page_composer_skill_picker.dart index c3e77241..7d2d2e86 100644 --- a/lib/features/assistant/assistant_page_composer_skill_picker.dart +++ b/lib/features/assistant/assistant_page_composer_skill_picker.dart @@ -33,7 +33,6 @@ import 'assistant_page_composer_support.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_message_widgets.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_page_components_core.dart'; @@ -349,3 +348,89 @@ class SkillPickerTileInternal extends StatelessWidget { ); } } + +ComposerSkillOptionInternal skillOptionFromGatewayInternal( + GatewaySkillSummary skill, +) { + final key = skill.skillKey.trim().isEmpty + ? skill.name.trim().toLowerCase() + : skill.skillKey.trim(); + final label = skill.name.trim().isEmpty ? key : skill.name.trim(); + final sourceLabel = skill.source.trim().isEmpty ? 'Gateway' : skill.source; + final group = skillGroupForSourceInternal(skill.source); + final description = skill.description.trim().isEmpty + ? appText('可在当前任务中调用的技能。', 'Skill available in the current task.') + : skill.description.trim(); + + return ComposerSkillOptionInternal( + key: key, + label: label, + description: description, + sourceLabel: sourceLabel, + groupLabel: group.label, + groupSortOrder: group.sortOrder, + icon: Icons.key_rounded, + ); +} + +ComposerSkillGroupInternal skillGroupForSourceInternal(String source) { + final normalized = source.trim().toLowerCase(); + if (normalized == 'openclaw-workspace') { + return const ComposerSkillGroupInternal( + label: 'Workspace Skills', + sortOrder: 0, + ); + } + if (normalized.startsWith('agents-skills-') || + normalized == 'agent' || + normalized.startsWith('agent-') || + normalized.contains('personal')) { + return const ComposerSkillGroupInternal( + label: 'Agent Skills', + sortOrder: 1, + ); + } + if (normalized == 'bridge' || normalized == 'gateway') { + return const ComposerSkillGroupInternal( + label: 'Gateway Skills', + sortOrder: 2, + ); + } + if (normalized.isEmpty) { + return const ComposerSkillGroupInternal( + label: 'Gateway Skills', + sortOrder: 2, + ); + } + return const ComposerSkillGroupInternal(label: 'Other Skills', sortOrder: 3); +} + +class ComposerSkillGroupInternal { + const ComposerSkillGroupInternal({ + required this.label, + required this.sortOrder, + }); + + final String label; + final int sortOrder; +} + +class ComposerSkillOptionInternal { + const ComposerSkillOptionInternal({ + required this.key, + required this.label, + required this.description, + required this.sourceLabel, + required this.groupLabel, + required this.groupSortOrder, + required this.icon, + }); + + final String key; + final String label; + final String description; + final String sourceLabel; + final String groupLabel; + final int groupSortOrder; + final IconData icon; +} diff --git a/lib/features/assistant/assistant_page_composer_state_helpers.dart b/lib/features/assistant/assistant_page_composer_state_helpers.dart index 5a5cc18b..68e078d4 100644 --- a/lib/features/assistant/assistant_page_composer_state_helpers.dart +++ b/lib/features/assistant/assistant_page_composer_state_helpers.dart @@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_message_widgets.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_page_components_core.dart'; diff --git a/lib/features/assistant/assistant_page_composer_support.dart b/lib/features/assistant/assistant_page_composer_support.dart index caea08bf..b1fd2c98 100644 --- a/lib/features/assistant/assistant_page_composer_support.dart +++ b/lib/features/assistant/assistant_page_composer_support.dart @@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_message_widgets.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_page_components_core.dart'; diff --git a/lib/features/assistant/assistant_page_main.dart b/lib/features/assistant/assistant_page_main.dart index 3d014e39..251a5c55 100644 --- a/lib/features/assistant/assistant_page_main.dart +++ b/lib/features/assistant/assistant_page_main.dart @@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_message_widgets.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_page_components_core.dart'; diff --git a/lib/features/assistant/assistant_page_message_widgets.dart b/lib/features/assistant/assistant_page_message_widgets.dart index 5fa86c9f..b2c108cf 100644 --- a/lib/features/assistant/assistant_page_message_widgets.dart +++ b/lib/features/assistant/assistant_page_message_widgets.dart @@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart'; import 'assistant_page_composer_support.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_page_components_core.dart'; diff --git a/lib/features/assistant/assistant_page_state_actions.dart b/lib/features/assistant/assistant_page_state_actions.dart index 9cd1bcab..7660b885 100644 --- a/lib/features/assistant/assistant_page_state_actions.dart +++ b/lib/features/assistant/assistant_page_state_actions.dart @@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_message_widgets.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_attachment_payloads.dart'; diff --git a/lib/features/assistant/assistant_page_state_closure.dart b/lib/features/assistant/assistant_page_state_closure.dart index d6327e4e..ca287118 100644 --- a/lib/features/assistant/assistant_page_state_closure.dart +++ b/lib/features/assistant/assistant_page_state_closure.dart @@ -35,7 +35,6 @@ import 'assistant_page_composer_support.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_message_widgets.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_page_components_core.dart'; diff --git a/lib/features/assistant/assistant_page_task_models.dart b/lib/features/assistant/assistant_page_task_models.dart index 7fa2c9f4..1caf98c7 100644 --- a/lib/features/assistant/assistant_page_task_models.dart +++ b/lib/features/assistant/assistant_page_task_models.dart @@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart'; import 'assistant_page_composer_support.dart'; import 'assistant_page_tooltip_labels.dart'; import 'assistant_page_message_widgets.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_page_components_core.dart'; diff --git a/lib/features/assistant/assistant_page_tooltip_labels.dart b/lib/features/assistant/assistant_page_tooltip_labels.dart index 45e1730c..3847fae1 100644 --- a/lib/features/assistant/assistant_page_tooltip_labels.dart +++ b/lib/features/assistant/assistant_page_tooltip_labels.dart @@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart'; import 'assistant_page_composer_support.dart'; import 'assistant_page_message_widgets.dart'; import 'assistant_page_task_models.dart'; -import 'assistant_page_composer_skill_models.dart'; import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_page_components_core.dart'; diff --git a/lib/features/desktop/desktop_view.dart b/lib/features/desktop/desktop_view.dart index 6b5ccda9..8151f21c 100644 --- a/lib/features/desktop/desktop_view.dart +++ b/lib/features/desktop/desktop_view.dart @@ -6,6 +6,7 @@ import 'desktop_client.dart'; import 'desktop_input_handler.dart'; import '../../app/app_controller.dart'; import '../../widgets/surface_card.dart'; +import '../../i18n/app_language.dart'; class DesktopView extends StatefulWidget { const DesktopView({ @@ -141,7 +142,9 @@ class _DesktopViewState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Failed to connect remote desktop: $e'), + content: Text( + appText('连接AI工作空间失败: $e', 'Failed to connect AI Workspace: $e'), + ), backgroundColor: Colors.redAccent, ), ); @@ -204,10 +207,10 @@ class _DesktopViewState extends State { ), label: Text( _connectionState == 'connected' - ? '断开连接' + ? appText('断开连接', 'Disconnect') : (_connectionState == 'connecting' - ? '正在连接...' - : '连接桌面'), + ? appText('正在连接...', 'Connecting...') + : appText('连接AI工作空间', 'Connect AI Workspace')), style: const TextStyle(fontWeight: FontWeight.bold), ), ), @@ -251,10 +254,10 @@ class _DesktopViewState extends State { const SizedBox(width: 8), Text( _connectionState == 'connected' - ? '已连接' + ? appText('已连接', 'Connected') : (_connectionState == 'connecting' - ? '连接中' - : '未连接'), + ? appText('连接中', 'Connecting') + : appText('已断开', 'Disconnected')), style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, @@ -508,8 +511,8 @@ class _DesktopViewState extends State { const SizedBox(height: 16), Text( _connectionState == 'connecting' - ? '正在建立 WebRTC 连接,请稍候...' - : '未开启远程桌面流。点击“连接桌面”启动视频流。', + ? appText('正在建立 WebRTC 连接,请稍候...', 'Establishing WebRTC connection, please wait...') + : appText('未开启 AI 工作空间流。点击“连接AI工作空间”启动视频流。', 'AI Workspace stream not enabled. Click "Connect AI Workspace" to start the video stream.'), style: TextStyle( color: theme.colorScheme.onSurface .withValues(alpha: 0.6), diff --git a/lib/features/settings/settings_remote_desktop_panel.dart b/lib/features/settings/settings_remote_desktop_panel.dart index 5581c573..5eb04383 100644 --- a/lib/features/settings/settings_remote_desktop_panel.dart +++ b/lib/features/settings/settings_remote_desktop_panel.dart @@ -59,7 +59,7 @@ class _SettingsRemoteDesktopPanelState extends State const SizedBox(width: 10), Expanded( child: Text( - appText('远程桌面', 'Remote Desktop'), + appText('AI工作空间', 'AI Workspace'), style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), diff --git a/lib/models/app_models.dart b/lib/models/app_models.dart index 88edc9e7..3e05dd8c 100644 --- a/lib/models/app_models.dart +++ b/lib/models/app_models.dart @@ -155,7 +155,7 @@ extension SettingsTabCopy on SettingsTab { String get label => switch (this) { SettingsTab.gateway => appText('集成', 'Integrations'), SettingsTab.archivedTasks => appText('归档任务', 'Archived tasks'), - SettingsTab.remoteDesktop => appText('远程桌面', 'Remote Desktop'), + SettingsTab.remoteDesktop => appText('AI工作空间', 'AI Workspace'), SettingsTab.logs => appText('运行日志', 'Runtime Logs'), }; } diff --git a/lib/runtime/runtime_controllers_entities.dart b/lib/runtime/runtime_controllers_entities.dart index 232e205b..9e146943 100644 --- a/lib/runtime/runtime_controllers_entities.dart +++ b/lib/runtime/runtime_controllers_entities.dart @@ -37,9 +37,6 @@ class SkillsController extends ChangeNotifier { errorInternal = null; notifyListeners(); try { - await runtimeInternal.ensureBridgeSessionConnected( - selectedAgentId: agentId?.trim() ?? '', - ); itemsInternal = await runtimeInternal.listSkills(agentId: agentId); } catch (error) { errorInternal = error.toString(); diff --git a/test/features/assistant/assistant_lower_pane_test.dart b/test/features/assistant/assistant_lower_pane_test.dart index 032fad5a..11c107dc 100644 --- a/test/features/assistant/assistant_lower_pane_test.dart +++ b/test/features/assistant/assistant_lower_pane_test.dart @@ -5,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:xworkmate/app/app_controller.dart'; import 'package:xworkmate/app/ui_feature_manifest.dart'; import 'package:xworkmate/features/assistant/assistant_page_composer_clipboard.dart'; -import 'package:xworkmate/features/assistant/assistant_page_composer_skill_models.dart'; import 'package:xworkmate/features/assistant/assistant_page_composer_skill_picker.dart'; import 'package:xworkmate/features/assistant/assistant_page_main.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; diff --git a/test/features/settings/settings_remote_desktop_panel_test.dart b/test/features/settings/settings_remote_desktop_panel_test.dart index 6298498f..96c8054e 100644 --- a/test/features/settings/settings_remote_desktop_panel_test.dart +++ b/test/features/settings/settings_remote_desktop_panel_test.dart @@ -29,8 +29,8 @@ void main() { ); // Verify the panel headers and titles - expect(find.text('远程桌面'), findsOneWidget); - expect(find.text('连接桌面'), findsOneWidget); + expect(find.text('AI工作空间'), findsOneWidget); + expect(find.text('连接AI工作空间'), findsOneWidget); // Verify advanced options are hidden initially expect(find.text('GPU 加速'), findsNothing); diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index d1299a75..ef4f9d87 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -8,7 +8,7 @@ import 'package:xworkmate/app/app_controller.dart'; import 'package:xworkmate/app/app_controller_desktop_external_acp_routing.dart'; import 'package:xworkmate/app/app_controller_openclaw_task_queue.dart'; import 'package:xworkmate/app/ui_feature_manifest.dart'; -import 'package:xworkmate/features/assistant/assistant_page_composer_skill_models.dart'; +import 'package:xworkmate/features/assistant/assistant_page_composer_skill_picker.dart'; import 'package:xworkmate/runtime/gateway_acp_client.dart'; import 'package:xworkmate/runtime/go_task_service_client.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; diff --git a/test/runtime/gateway_runtime_bridge_skills_test.dart b/test/runtime/gateway_runtime_bridge_skills_test.dart index bd80d942..84bfd2d4 100644 --- a/test/runtime/gateway_runtime_bridge_skills_test.dart +++ b/test/runtime/gateway_runtime_bridge_skills_test.dart @@ -11,7 +11,7 @@ import 'package:xworkmate/runtime/secure_config_store.dart'; void main() { test( - 'SkillsController lazily connects and loads OpenClaw skills through bridge gateway request', + 'SkillsController loads OpenClaw skills through bridge request without legacy gateway connect', () async { final observedMethods = []; final observedGatewayRequests = >[]; @@ -23,31 +23,6 @@ void main() { observedMethods.add(method); request.response.headers.contentType = ContentType.json; - if (method == 'xworkmate.gateway.connect') { - request.response.write( - jsonEncode({ - 'jsonrpc': '2.0', - 'id': rpc['id'], - 'result': { - 'ok': true, - 'snapshot': { - 'status': 'connected', - 'mode': 'remote', - 'statusText': 'Connected', - 'mainSessionKey': 'main', - }, - 'auth': { - 'role': 'operator', - 'scopes': ['operator.read', 'operator.write'], - }, - 'returnedDeviceToken': '', - }, - }), - ); - await request.response.close(); - return; - } - if (method == 'xworkmate.gateway.request') { final params = (rpc['params'] as Map).cast(); observedGatewayRequests.add(params); @@ -159,10 +134,7 @@ void main() { final controller = SkillsController(runtime); await controller.refresh(agentId: 'main'); - expect(observedMethods, [ - 'xworkmate.gateway.connect', - 'xworkmate.gateway.request', - ]); + expect(observedMethods, const ['xworkmate.gateway.request']); expect(observedGatewayRequests.single['method'], 'skills.status'); expect( (observedGatewayRequests.single['params'] as Map)['agentId'], @@ -176,4 +148,92 @@ void main() { expect(controller.items.first.eligible, isTrue); }, ); + + test( + 'SkillsController keeps bridge skill payload when OpenClaw gateway is offline', + () async { + final observedGatewayRequests = >[]; + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + final subscription = server.listen((request) async { + final body = await utf8.decoder.bind(request).join(); + final rpc = jsonDecode(body) as Map; + request.response.headers.contentType = ContentType.json; + + if (rpc['method'] == 'xworkmate.gateway.request') { + final params = (rpc['params'] as Map).cast(); + observedGatewayRequests.add(params); + request.response.write( + jsonEncode({ + 'jsonrpc': '2.0', + 'id': rpc['id'], + 'result': { + 'ok': false, + 'error': { + 'code': 'OFFLINE', + 'message': 'gateway not connected', + }, + 'payload': { + 'skills': >[ + { + 'name': 'PDF Writer', + 'description': 'Write PDF documents', + 'source': 'openclaw-workspace', + 'skillKey': 'pdf', + 'eligible': false, + 'disabled': false, + 'missing': { + 'bins': [], + 'env': [], + 'config': [], + }, + }, + ], + }, + }, + }), + ); + await request.response.close(); + return; + } + + request.response.statusCode = HttpStatus.badRequest; + await request.response.close(); + }); + + final tempDir = await Directory.systemTemp.createTemp( + 'xworkmate-bridge-skills-offline-test-', + ); + final store = SecureConfigStore( + enableSecureStorage: false, + appDataRootPathResolver: () async => '${tempDir.path}/settings.sqlite3', + secretRootPathResolver: () async => tempDir.path, + ); + final acpClient = GatewayAcpClient( + endpointResolver: () => Uri.parse('http://127.0.0.1:${server.port}'), + authorizationResolver: (_) async => 'bridge-token', + ); + final runtime = GatewayRuntime( + store: store, + identityStore: DeviceIdentityStore(store), + sessionClient: GatewayAcpRuntimeSessionClient(client: acpClient), + ); + await runtime.initialize(); + addTearDown(() async { + runtime.dispose(); + await subscription.cancel(); + await server.close(force: true); + await tempDir.delete(recursive: true); + }); + + final controller = SkillsController(runtime); + await controller.refresh(agentId: 'main'); + + expect(observedGatewayRequests.single['method'], 'skills.status'); + expect(controller.error, isNull); + expect(controller.items.map((item) => item.skillKey), const [ + 'pdf', + ]); + expect(controller.items.single.eligible, isFalse); + }, + ); }