From 3cfae35b08da159e22f42b38e1e78733561a94cf Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 12 Apr 2026 16:41:38 +0800 Subject: [PATCH] Clean bridge provider unavailable UX copy --- ...pp_controller_desktop_runtime_helpers.dart | 9 +- ...ler_desktop_single_agent_go_task_flow.dart | 38 +----- ...pp_controller_desktop_thread_sessions.dart | 35 +++--- .../assistant/assistant_page_components.dart | 41 ++----- lib/features/mcp_server/mcp_server_page.dart | 4 +- lib/features/mobile/mobile_shell_core.dart | 4 +- lib/features/mobile/mobile_shell_sheet.dart | 8 +- .../mobile/mobile_shell_workspace.dart | 2 +- lib/features/modules/modules_page.dart | 12 +- ...rnal_code_agent_acp_desktop_transport.dart | 5 - lib/runtime/go_task_service_client.dart | 34 ----- .../go_task_service_desktop_service.dart | 5 - .../assistant_focus_panel_previews.dart | 27 ++-- ...ntroller_desktop_runtime_cleanup_test.dart | 8 +- ...ontroller_desktop_thread_binding_test.dart | 31 ++--- ...sktop_working_directory_dispatch_test.dart | 63 +++------- ...t_execution_target_picker_widget_test.dart | 116 +++++++++++++++++- test/runtime/bridge_copy_cleanup_test.dart | 38 ++++++ ...code_agent_acp_desktop_transport_test.dart | 21 ---- 19 files changed, 245 insertions(+), 256 deletions(-) create mode 100644 test/runtime/bridge_copy_cleanup_test.dart diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index b70b2417..2f931f65 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -258,15 +258,14 @@ extension AppControllerDesktopRuntimeHelpers on AppController { } final profile = gatewayProfileForAssistantExecutionTargetInternal(target); final address = gatewayAddressLabelInternal(profile); - final targetLabel = target.label; return address == appText('未连接目标', 'No target') ? appText( - '当前线程目标网关未连接。请先连接 $targetLabel,然后再重试。', - 'The selected gateway target for this thread is not connected. Connect $targetLabel first, then try again.', + '当前 xworkmate-bridge 未连接。请先恢复 bridge 连接后再重试。', + 'xworkmate-bridge is not connected. Restore the bridge connection, then try again.', ) : appText( - '当前线程目标网关未连接:$address。请先连接后再重试。', - 'The selected gateway target for this thread is not connected: $address. Connect it first, then try again.', + '当前 xworkmate-bridge 未连接:$address。请先恢复 bridge 连接后再重试。', + 'xworkmate-bridge is not connected: $address. Restore the bridge connection, then try again.', ); } return raw; diff --git a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart index d7aad9d5..ddeaae9f 100644 --- a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart +++ b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart @@ -76,32 +76,6 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( ); throw error; } - final preflightUnavailableReason = - controller.singleAgentShouldSuggestAcpSwitchForSession(sessionKey) || - controller.singleAgentNeedsBridgeProviderForSession(sessionKey) - ? singleAgentUnavailableLabelDesktopInternal( - controller, - sessionKey, - null, - ) - : null; - if (preflightUnavailableReason != null) { - controller.upsertTaskThreadInternal( - sessionKey, - lifecycleStatus: 'ready', - lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - lastResultCode: 'error', - updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - ); - controller.appendAssistantThreadMessageInternal( - sessionKey, - assistantErrorMessageSingleAgentDesktopInternal( - controller, - preflightUnavailableReason, - ), - ); - return; - } if (controller.resolveExternalAcpEndpointForTargetInternal( AssistantExecutionTarget.singleAgent, ) == @@ -148,19 +122,13 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( sessionKey, routingResolution.unavailableMessage, ) - : controller.singleAgentShouldSuggestAcpSwitchForSession(sessionKey) - ? singleAgentUnavailableLabelDesktopInternal( - controller, - sessionKey, - null, - ) - : controller.singleAgentNeedsBridgeProviderForSession(sessionKey) + : resolvedProviderId.isEmpty && effectiveProvider.isUnspecified ? singleAgentUnavailableLabelDesktopInternal( controller, sessionKey, appText( - 'Bridge 当前没有同步到可用 Provider。', - 'The bridge does not currently have any synced providers.', + 'Bridge 当前没有广告可用 Provider。', + 'The bridge is not advertising any available providers.', ), ) : null; diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index ea7ffe93..89a0fb0a 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -52,19 +52,11 @@ import 'app_controller_desktop_thread_sessions_collaboration_impl.dart'; AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({ required AssistantExecutionTarget target, required GatewayConnectionSnapshot connection, - required GatewayConnectionProfile targetProfile, }) { - const expectedMode = RuntimeConnectionMode.remote; - final matchesTarget = connection.mode == expectedMode; - final targetAddress = - targetProfile.host.trim().isNotEmpty && targetProfile.port > 0 - ? '${targetProfile.host.trim()}:${targetProfile.port}' - : appText('未连接目标', 'No target'); - final rawStatus = matchesTarget - ? connection.status - : RuntimeConnectionStatus.offline; - final pairingRequired = matchesTarget && connection.pairingRequired; - final gatewayTokenMissing = matchesTarget && connection.gatewayTokenMissing; + final bridgeAddress = connection.remoteAddress?.trim() ?? ''; + final rawStatus = connection.status; + final pairingRequired = connection.pairingRequired; + final gatewayTokenMissing = connection.gatewayTokenMissing; final status = pairingRequired || gatewayTokenMissing ? RuntimeConnectionStatus.error : rawStatus; @@ -77,11 +69,13 @@ AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({ executionTarget: target, status: status, primaryLabel: primaryLabel, - detailLabel: targetAddress, + detailLabel: bridgeAddress.isEmpty + ? appText('xworkmate-bridge 未连接', 'xworkmate-bridge is not connected') + : bridgeAddress, ready: status == RuntimeConnectionStatus.connected, pairingRequired: pairingRequired, gatewayTokenMissing: gatewayTokenMissing, - lastError: matchesTarget ? connection.lastError?.trim() : null, + lastError: connection.lastError?.trim(), ); } @@ -303,6 +297,12 @@ extension AppControllerDesktopThreadSessions on AppController { AssistantExecutionTarget.singleAgent) { return false; } + if (resolveExternalAcpEndpointForTargetInternal( + AssistantExecutionTarget.singleAgent, + ) == + null) { + return false; + } return bridgeProviderCatalog.isEmpty; } @@ -317,6 +317,12 @@ extension AppControllerDesktopThreadSessions on AppController { AssistantExecutionTarget.singleAgent) { return false; } + if (resolveExternalAcpEndpointForTargetInternal( + AssistantExecutionTarget.singleAgent, + ) == + null) { + return false; + } final selection = singleAgentProviderForSession(normalizedSessionKey); if (selection.isUnspecified) { return false; @@ -461,7 +467,6 @@ extension AppControllerDesktopThreadSessions on AppController { return resolveGatewayThreadConnectionStateInternal( target: target, connection: connection, - targetProfile: gatewayProfileForAssistantExecutionTargetInternal(target), ); } diff --git a/lib/features/assistant/assistant_page_components.dart b/lib/features/assistant/assistant_page_components.dart index 1f46e252..e273660b 100644 --- a/lib/features/assistant/assistant_page_components.dart +++ b/lib/features/assistant/assistant_page_components.dart @@ -527,8 +527,8 @@ class AssistantEmptyStateInternal extends StatelessWidget { : connected ? appText('开始对话或运行任务', 'Start a chat or run a task') : connectionState.status == RuntimeConnectionStatus.error - ? appText('Gateway 连接失败', 'Gateway connection failed') - : appText('先连接 Gateway', 'Connect a gateway first'); + ? appText('Bridge 连接失败', 'Bridge connection failed') + : appText('先连接 Bridge', 'Connect xworkmate-bridge first'); final description = singleAgent ? connected ? appText( @@ -566,8 +566,8 @@ class AssistantEmptyStateInternal extends StatelessWidget { ) : !connected ? appText( - '当前线程目标网关尚未连接。请先连接对应 Gateway,再继续当前任务。', - 'The selected gateway target for this thread is not connected yet. Connect that Gateway first, then continue this task.', + '当前 xworkmate-bridge 尚未连接。请先恢复 bridge 连接,再继续当前任务。', + 'xworkmate-bridge is not connected yet. Restore the bridge connection, then continue this task.', ) : (connectionState.lastError?.trim().isNotEmpty == true ? connectionState.lastError!.trim() @@ -627,8 +627,11 @@ class AssistantEmptyStateInternal extends StatelessWidget { : singleAgent ? appText('查看线程工具栏', 'Open toolbar') : reconnectAvailable - ? appText('重新连接', 'Reconnect') - : appText('连接 Gateway', 'Connect gateway'), + ? appText('重新连接 Bridge', 'Reconnect bridge') + : appText( + '连接 Bridge', + 'Connect xworkmate-bridge', + ), ), style: FilledButton.styleFrom( minimumSize: const Size(0, 28), @@ -641,32 +644,6 @@ class AssistantEmptyStateInternal extends StatelessWidget { ), ), ), - if (!connected && !singleAgent) - OutlinedButton.icon( - onPressed: singleAgent - ? onOpenAiGatewaySettings - : onOpenGateway, - icon: Icon( - singleAgent - ? Icons.hub_outlined - : Icons.settings_rounded, - ), - label: Text( - singleAgent - ? appText('打开设置中心', 'Open settings') - : appText('编辑连接', 'Edit connection'), - ), - style: OutlinedButton.styleFrom( - minimumSize: const Size(0, 28), - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 6, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), ], ), ], diff --git a/lib/features/mcp_server/mcp_server_page.dart b/lib/features/mcp_server/mcp_server_page.dart index 7e9a04a0..2570d553 100644 --- a/lib/features/mcp_server/mcp_server_page.dart +++ b/lib/features/mcp_server/mcp_server_page.dart @@ -76,8 +76,8 @@ class McpServerPage extends StatelessWidget { 'No MCP servers connected.', ) : appText( - '连接 Gateway 后可查看 MCP 服务器。', - 'Connect a gateway to view MCP servers.', + '恢复 xworkmate-bridge 连接后可查看 MCP 服务器。', + 'MCP servers are visible again after xworkmate-bridge reconnects.', ), ), ) diff --git a/lib/features/mobile/mobile_shell_core.dart b/lib/features/mobile/mobile_shell_core.dart index e00b1899..81e5e570 100644 --- a/lib/features/mobile/mobile_shell_core.dart +++ b/lib/features/mobile/mobile_shell_core.dart @@ -203,8 +203,8 @@ class MobileShellStateInternal extends State { SnackBar( content: Text( appText( - '已写入配置码并开始连接 Gateway。', - 'Setup code applied and Gateway connection started.', + '已写入配置码并开始连接 xworkmate-bridge。', + 'Setup code applied and xworkmate-bridge connection started.', ), ), ), diff --git a/lib/features/mobile/mobile_shell_sheet.dart b/lib/features/mobile/mobile_shell_sheet.dart index 0066e4c2..ca27de27 100644 --- a/lib/features/mobile/mobile_shell_sheet.dart +++ b/lib/features/mobile/mobile_shell_sheet.dart @@ -245,8 +245,8 @@ class MobileSafeSheetInternal extends StatelessWidget { if (!controller.runtime.isConnected) Text( appText( - '连接 Gateway 后加载待审批设备与已配对设备。', - 'Connect the gateway to load pending and paired devices.', + '恢复 xworkmate-bridge 连接后加载待审批设备与已配对设备。', + 'Pending and paired devices load again after xworkmate-bridge reconnects.', ), style: theme.textTheme.bodyMedium, ) @@ -279,8 +279,8 @@ class MobileSafeSheetInternal extends StatelessWidget { if (!controller.runtime.isConnected) Text( appText( - '连接 Gateway 后可查看 paired device,并在移动端直接吊销。', - 'Connect the gateway to view paired devices and revoke them from mobile.', + '恢复 xworkmate-bridge 连接后可查看 paired device,并在移动端直接吊销。', + 'Paired devices are visible again after xworkmate-bridge reconnects, and can be revoked from mobile.', ), style: theme.textTheme.bodyMedium, ) diff --git a/lib/features/mobile/mobile_shell_workspace.dart b/lib/features/mobile/mobile_shell_workspace.dart index 3f258932..8f205d3c 100644 --- a/lib/features/mobile/mobile_shell_workspace.dart +++ b/lib/features/mobile/mobile_shell_workspace.dart @@ -92,7 +92,7 @@ class MobileWorkspaceLauncherInternal extends StatelessWidget { ), primaryLabel: connection.status == RuntimeConnectionStatus.connected ? appText('查看连接', 'Connection') - : appText('连接 Gateway', 'Connect Gateway'), + : appText('连接 Bridge', 'Connect Bridge'), secondaryLabel: appText('返回助手', 'Open Assistant'), onPrimaryPressed: onOpenGatewayConnect, onSecondaryPressed: () => diff --git a/lib/features/modules/modules_page.dart b/lib/features/modules/modules_page.dart index 892da95c..00f9bfcc 100644 --- a/lib/features/modules/modules_page.dart +++ b/lib/features/modules/modules_page.dart @@ -259,8 +259,8 @@ class _NodesPanel extends StatelessWidget { controller.connection.status == RuntimeConnectionStatus.connected ? appText('暂时还没有上报在线实例。', 'No live instances reported yet.') : appText( - '连接 Gateway 后可加载实例与在线状态。', - 'Connect a gateway to load instances / presence.', + '恢复 xworkmate-bridge 连接后可加载实例与在线状态。', + 'Instances and presence return after xworkmate-bridge reconnects.', ), ), ) @@ -366,8 +366,8 @@ class _AgentsPanel extends StatelessWidget { 'No agents reported by the gateway.', ) : appText( - '连接 Gateway 后可加载代理。', - 'Connect a gateway to load agents.', + '恢复 xworkmate-bridge 连接后可加载代理。', + 'Agents return after xworkmate-bridge reconnects.', ), ), ); @@ -539,8 +539,8 @@ class _SkillsPanel extends StatelessWidget { 'No skills loaded for the active gateway / agent.', ) : appText( - '连接 Gateway 后可加载技能。', - 'Connect a gateway to load skills.', + '恢复 xworkmate-bridge 连接后可加载技能。', + 'Skills return after xworkmate-bridge reconnects.', ), ), ) diff --git a/lib/runtime/external_code_agent_acp_desktop_transport.dart b/lib/runtime/external_code_agent_acp_desktop_transport.dart index 3dd0f290..3da9cff8 100644 --- a/lib/runtime/external_code_agent_acp_desktop_transport.dart +++ b/lib/runtime/external_code_agent_acp_desktop_transport.dart @@ -20,11 +20,6 @@ class ExternalCodeAgentAcpDesktopTransport @visibleForTesting GatewayAcpClient get clientForTest => _client; - @override - Future syncExternalProviders( - List providers, - ) async {} - @override Future loadExternalAcpCapabilities({ required AssistantExecutionTarget target, diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index 7655fc65..bae14f15 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -76,32 +76,6 @@ class ExternalCodeAgentAcpRoutingResolution { raw['unavailableMessage']?.toString().trim() ?? ''; } -class ExternalCodeAgentAcpSyncedProvider { - const ExternalCodeAgentAcpSyncedProvider({ - required this.providerId, - required this.label, - required this.endpoint, - required this.authorizationHeader, - required this.enabled, - }); - - final String providerId; - final String label; - final String endpoint; - final String authorizationHeader; - final bool enabled; - - Map toJson() { - return { - 'providerId': providerId.trim(), - 'label': label.trim(), - 'endpoint': endpoint.trim(), - 'authorizationHeader': authorizationHeader.trim(), - 'enabled': enabled, - }; - } -} - enum ExternalCodeAgentAcpRoutingMode { auto, explicit } class ExternalCodeAgentAcpAvailableSkill { @@ -604,10 +578,6 @@ String? goTaskServiceGatewayEntryState({ } abstract class ExternalCodeAgentAcpTransport { - Future syncExternalProviders( - List providers, - ); - Future loadExternalAcpCapabilities({ required AssistantExecutionTarget target, bool forceRefresh = false, @@ -640,10 +610,6 @@ abstract class ExternalCodeAgentAcpTransport { } abstract class GoTaskServiceClient { - Future syncExternalProviders( - List providers, - ); - Future loadExternalAcpCapabilities({ required AssistantExecutionTarget target, bool forceRefresh = false, diff --git a/lib/runtime/go_task_service_desktop_service.dart b/lib/runtime/go_task_service_desktop_service.dart index e79eed64..77ceaef1 100644 --- a/lib/runtime/go_task_service_desktop_service.dart +++ b/lib/runtime/go_task_service_desktop_service.dart @@ -11,11 +11,6 @@ class DesktopGoTaskService implements GoTaskServiceClient { final ExternalCodeAgentAcpTransport _acpTransport; - @override - Future syncExternalProviders( - List providers, - ) => _acpTransport.syncExternalProviders(providers); - @override Future loadExternalAcpCapabilities({ required AssistantExecutionTarget target, diff --git a/lib/widgets/assistant_focus_panel_previews.dart b/lib/widgets/assistant_focus_panel_previews.dart index 516aa86b..429382de 100644 --- a/lib/widgets/assistant_focus_panel_previews.dart +++ b/lib/widgets/assistant_focus_panel_previews.dart @@ -61,8 +61,8 @@ class TasksFocusPreviewInternal extends StatelessWidget { RuntimeConnectionStatus.connected ? appText('当前没有任务摘要。', 'No task summary yet.') : appText( - '连接 Gateway 后这里会显示任务摘要。', - 'Connect a gateway to load task summaries.', + '恢复 xworkmate-bridge 连接后这里会显示任务摘要。', + 'Task summaries appear here after xworkmate-bridge reconnects.', ), ) else @@ -112,12 +112,23 @@ class SkillsFocusPreviewInternal extends StatelessWidget { .toList(growable: false) : typedController.skills.take(4).toList(growable: false); if (items.isEmpty) { + final bridgeEndpointMissing = + typedController.isSingleAgentMode && + typedController.resolveExternalAcpEndpointForTargetInternal( + AssistantExecutionTarget.singleAgent, + ) == + null; return PreviewEmptyStateInternal( message: typedController.isSingleAgentMode ? (typedController.currentSingleAgentNeedsBridgeProvider ? appText( - '当前没有可用的 Bridge Provider,请先在设置里配置并同步连接。', - 'No bridge provider is available. Configure and sync a connection in Settings first.', + 'Bridge 当前没有广告可用 Provider。恢复后这里会显示线程自己的技能摘要。', + 'The bridge is not advertising any available providers right now. Thread-owned skill summaries will appear here after it recovers.', + ) + : bridgeEndpointMissing + ? appText( + 'Bridge Server 当前不可用。恢复后这里会显示线程自己的技能摘要。', + 'The bridge server is currently unavailable. Thread-owned skill summaries will appear here after it recovers.', ) : appText( '当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。', @@ -130,8 +141,8 @@ class SkillsFocusPreviewInternal extends StatelessWidget { 'No skills are loaded for the active agent.', ) : appText( - '连接 Gateway 后可查看技能摘要。', - 'Connect a gateway to inspect skills here.', + '恢复 xworkmate-bridge 连接后可查看技能摘要。', + 'Skill summaries are available again after xworkmate-bridge reconnects.', ), ); } @@ -236,8 +247,8 @@ class McpFocusPreviewInternal extends StatelessWidget { if (items.isEmpty) { return PreviewEmptyStateInternal( message: appText( - '当前没有 MCP 连接器。连接 Gateway 后这里会显示工具摘要。', - 'No MCP connectors yet. Connect a gateway to load tool summaries here.', + '当前没有 MCP 连接器。恢复 xworkmate-bridge 连接后这里会显示工具摘要。', + 'No MCP connectors yet. Tool summaries appear here after xworkmate-bridge reconnects.', ), ); } diff --git a/test/app_controller_desktop_runtime_cleanup_test.dart b/test/app_controller_desktop_runtime_cleanup_test.dart index 4ec0cf6a..9f47849b 100644 --- a/test/app_controller_desktop_runtime_cleanup_test.dart +++ b/test/app_controller_desktop_runtime_cleanup_test.dart @@ -215,7 +215,7 @@ void main() { ); expect( controller.singleAgentResolvedProviderForSession(sessionKey), - SingleAgentProvider.codex, + isNull, ); await controller.refreshSingleAgentSkillsForSession(sessionKey); @@ -281,7 +281,7 @@ void main() { controller.singleAgentResolvedProviderForSession( 'draft:bridge-default', ), - SingleAgentProvider.codex, + isNull, ); final thread = controller.taskThreadForSessionInternal( @@ -523,8 +523,4 @@ class _FakeGoTaskServiceClient implements GoTaskServiceClient { ); } - @override - Future syncExternalProviders( - List providers, - ) async {} } diff --git a/test/app_controller_desktop_thread_binding_test.dart b/test/app_controller_desktop_thread_binding_test.dart index b889e886..d576cdb9 100644 --- a/test/app_controller_desktop_thread_binding_test.dart +++ b/test/app_controller_desktop_thread_binding_test.dart @@ -170,13 +170,7 @@ void main() { }); group('resolveGatewayThreadConnectionStateInternal', () { - test('uses the thread target profile as the only address source', () { - final targetProfile = GatewayConnectionProfile.defaults().copyWith( - mode: RuntimeConnectionMode.remote, - host: 'bridge.example.internal', - port: 443, - tls: true, - ); + test('uses the current bridge connection address as the only address source', () { final state = resolveGatewayThreadConnectionStateInternal( target: AssistantExecutionTarget.gateway, connection: @@ -184,9 +178,8 @@ void main() { mode: RuntimeConnectionMode.remote, ).copyWith( status: RuntimeConnectionStatus.connected, - remoteAddress: 'legacy-loopback:18789', + remoteAddress: 'bridge.example.internal:443', ), - targetProfile: targetProfile, ); expect(state.status, RuntimeConnectionStatus.connected); @@ -194,13 +187,7 @@ void main() { expect(state.ready, isTrue); }); - test('marks mismatched local snapshot as offline for remote threads', () { - final targetProfile = GatewayConnectionProfile.defaults().copyWith( - mode: RuntimeConnectionMode.remote, - host: 'bridge.example.internal', - port: 443, - tls: true, - ); + test('uses current bridge snapshot even when the connection was established locally before', () { final state = resolveGatewayThreadConnectionStateInternal( target: AssistantExecutionTarget.gateway, connection: @@ -210,19 +197,17 @@ void main() { status: RuntimeConnectionStatus.connected, remoteAddress: 'legacy-loopback:18789', ), - targetProfile: targetProfile, ); - expect(state.status, RuntimeConnectionStatus.offline); - expect(state.detailLabel, 'bridge.example.internal:443'); - expect(state.ready, isFalse); - expect(state.lastError, isNull); + expect(state.status, RuntimeConnectionStatus.connected); + expect(state.detailLabel, 'legacy-loopback:18789'); + expect(state.ready, isTrue); }); }); group('assistantConnectionStateForSession', () { test( - 'uses target profile address instead of connection snapshot address', + 'uses bridge connection address instead of thread target profile address', () { final gateway = _FakeGatewayRuntime( GatewayConnectionSnapshot.initial( @@ -257,7 +242,7 @@ void main() { final state = controller.assistantConnectionStateForSession(sessionKey); expect(state.status, RuntimeConnectionStatus.connected); - expect(state.detailLabel, '未连接目标'); + expect(state.detailLabel, 'legacy-loopback:18789'); expect(state.ready, isTrue); }, ); diff --git a/test/app_controller_desktop_working_directory_dispatch_test.dart b/test/app_controller_desktop_working_directory_dispatch_test.dart index 3d7ccff6..17f51cab 100644 --- a/test/app_controller_desktop_working_directory_dispatch_test.dart +++ b/test/app_controller_desktop_working_directory_dispatch_test.dart @@ -27,28 +27,13 @@ void main() { supportRootPathResolver: () async => root.path, ); await store.initialize(); - await store.saveSettingsSnapshot( - SettingsSnapshot.defaults().copyWith( - acpBridgeServerModeConfig: SettingsSnapshot.defaults() - .acpBridgeServerModeConfig - .copyWith( - cloudSynced: SettingsSnapshot.defaults() - .acpBridgeServerModeConfig - .cloudSynced - .copyWith( - remoteServerSummary: - const AcpBridgeServerRemoteServerSummary( - endpoint: 'https://bridge.customer.example', - hasAdvancedOverrides: false, - ), - ), - ), - ), - ); final client = _CapturingGoTaskServiceClient(); final controller = AppController( store: store, goTaskServiceClient: client, + environmentOverride: const { + 'BRIDGE_SERVER_URL': 'https://bridge.customer.example/acp', + }, ); _seedBridgeProviders(controller, const [ SingleAgentProvider.codex, @@ -138,6 +123,8 @@ void main() { ); await controller.switchSession(sessionKey); + expect(controller.currentSingleAgentNeedsBridgeProvider, isFalse); + await controller.sendChatMessage('first turn'); expect(client.requests, isEmpty); @@ -151,7 +138,7 @@ void main() { ); test( - 'single-agent turns stop before routing when bridge has no advertised provider', + 'single-agent turns still dispatch when bridge routing resolves a provider even if the local catalog is empty', () async { final root = await Directory.systemTemp.createTemp( 'xworkmate-missing-bridge-provider-', @@ -163,28 +150,13 @@ void main() { supportRootPathResolver: () async => root.path, ); await store.initialize(); - await store.saveSettingsSnapshot( - SettingsSnapshot.defaults().copyWith( - acpBridgeServerModeConfig: SettingsSnapshot.defaults() - .acpBridgeServerModeConfig - .copyWith( - cloudSynced: SettingsSnapshot.defaults() - .acpBridgeServerModeConfig - .cloudSynced - .copyWith( - remoteServerSummary: - const AcpBridgeServerRemoteServerSummary( - endpoint: 'https://bridge.customer.example', - hasAdvancedOverrides: false, - ), - ), - ), - ), - ); final client = _CapturingGoTaskServiceClient(); final controller = AppController( store: store, goTaskServiceClient: client, + environmentOverride: const { + 'BRIDGE_SERVER_URL': 'https://bridge.customer.example/acp', + }, ); addTearDown(() async { controller.dispose(); @@ -202,18 +174,19 @@ void main() { await controller.switchSession(sessionKey); _seedBridgeProviders(controller, const []); - expect(controller.currentSingleAgentNeedsBridgeProvider, isTrue); - await controller.sendChatMessage('first turn'); - expect(client.requests, isEmpty); + expect(client.requests, hasLength(1)); expect( client.resolveExternalAcpRoutingCallCount, - 0, + 1, reason: - 'single-agent turns should not call routing.resolve when bridge provider state is already unavailable in app state', + 'single-agent turns should trust bridge routing.resolve instead of short-circuiting on the app-side provider cache', + ); + expect( + controller.chatMessages.last.text, + isNot('Bridge 当前没有可用 Provider。'), ); - expect(controller.chatMessages.last.text, 'Bridge 当前没有可用 Provider。'); }, ); @@ -386,8 +359,4 @@ class _CapturingGoTaskServiceClient implements GoTaskServiceClient { ); } - @override - Future syncExternalProviders( - List providers, - ) async {} } diff --git a/test/assistant_execution_target_picker_widget_test.dart b/test/assistant_execution_target_picker_widget_test.dart index 6ba2332c..68f9a923 100644 --- a/test/assistant_execution_target_picker_widget_test.dart +++ b/test/assistant_execution_target_picker_widget_test.dart @@ -14,6 +14,7 @@ import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; import 'package:xworkmate/runtime/skill_directory_access.dart'; import 'package:xworkmate/theme/app_theme.dart'; +import 'package:xworkmate/widgets/assistant_focus_panel_previews.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -302,12 +303,121 @@ void main() { await tester.pump(); expect(find.textContaining('设置 -> 集成'), findsNothing); - expect(find.textContaining('本地集成配置'), findsOneWidget); + expect(find.textContaining('等待 Bridge 就绪'), findsOneWidget); + expect(find.textContaining('Bridge Provider 尚未就绪'), findsOneWidget); + expect(find.textContaining('本地集成配置'), findsNothing); expect(find.text('打开配置中心'), findsNothing); expect(find.text('打开设置中心'), findsNothing); expect(find.text('查看线程工具栏'), findsOneWidget); }, ); + + testWidgets( + 'single-agent skills focus preview describes bridge recovery instead of settings sync when endpoint is missing', + (tester) async { + final root = Directory.systemTemp.createTempSync( + 'xworkmate-focus-preview-widget-test-', + ); + final store = SecureConfigStore( + enableSecureStorage: false, + appDataRootPathResolver: () async => '${root.path}/settings.sqlite3', + secretRootPathResolver: () async => root.path, + supportRootPathResolver: () async => root.path, + ); + final controller = AppController( + store: store, + desktopPlatformService: UnsupportedDesktopPlatformService(), + skillDirectoryAccessService: _FakeSkillDirectoryAccessService( + root.path, + ), + goTaskServiceClient: const _FakeGoTaskServiceClient(), + singleAgentSharedSkillScanRootOverrides: const [], + ); + addTearDown(() async { + controller.dispose(); + if (root.existsSync()) { + await root.delete(recursive: true); + } + }); + + controller.initializeAssistantThreadContext( + controller.currentSessionKey, + executionTarget: AssistantExecutionTarget.singleAgent, + singleAgentProvider: SingleAgentProvider.codex, + ); + controller.bridgeProviderCatalogInternal = const []; + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light(platform: TargetPlatform.macOS), + home: Scaffold( + body: SkillsFocusPreviewInternal(controller: controller), + ), + ), + ); + await tester.pump(); + + expect(find.textContaining('Bridge Server 当前不可用'), findsOneWidget); + expect(find.textContaining('设置里配置并同步连接'), findsNothing); + }, + ); + + testWidgets( + 'gateway empty state only asks for bridge connectivity and removes edit-connection affordance', + (tester) async { + final root = Directory.systemTemp.createTempSync( + 'xworkmate-gateway-empty-state-widget-test-', + ); + final store = SecureConfigStore( + enableSecureStorage: false, + appDataRootPathResolver: () async => '${root.path}/settings.sqlite3', + secretRootPathResolver: () async => root.path, + supportRootPathResolver: () async => root.path, + ); + final controller = AppController( + store: store, + desktopPlatformService: UnsupportedDesktopPlatformService(), + skillDirectoryAccessService: _FakeSkillDirectoryAccessService( + root.path, + ), + goTaskServiceClient: const _FakeGoTaskServiceClient(), + singleAgentSharedSkillScanRootOverrides: const [], + ); + addTearDown(() async { + controller.dispose(); + if (root.existsSync()) { + await root.delete(recursive: true); + } + }); + + controller.initializeAssistantThreadContext( + controller.currentSessionKey, + executionTarget: AssistantExecutionTarget.gateway, + ); + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light(platform: TargetPlatform.macOS), + home: Scaffold( + body: AssistantEmptyStateInternal( + controller: controller, + onFocusComposer: () {}, + onOpenGateway: () {}, + onOpenAiGatewaySettings: () {}, + onReconnectGateway: () async {}, + ), + ), + ), + ); + await tester.pump(); + + expect(find.textContaining('先连接 Bridge'), findsOneWidget); + expect(find.textContaining('xworkmate-bridge 尚未连接'), findsOneWidget); + expect(find.text('连接 Bridge'), findsOneWidget); + expect(find.text('编辑连接'), findsNothing); + expect(find.text('连接 Gateway'), findsNothing); + }, + ); } void _seedBridgeProviders( @@ -422,8 +532,4 @@ class _FakeGoTaskServiceClient implements GoTaskServiceClient { ); } - @override - Future syncExternalProviders( - List providers, - ) async {} } diff --git a/test/runtime/bridge_copy_cleanup_test.dart b/test/runtime/bridge_copy_cleanup_test.dart new file mode 100644 index 00000000..273783d9 --- /dev/null +++ b/test/runtime/bridge_copy_cleanup_test.dart @@ -0,0 +1,38 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('bridge-only UI copy does not regress to legacy gateway connection wording', () { + final targets = [ + 'lib/features/assistant/assistant_page_components.dart', + 'lib/widgets/assistant_focus_panel_previews.dart', + 'lib/features/mcp_server/mcp_server_page.dart', + 'lib/features/modules/modules_page.dart', + 'lib/features/mobile/mobile_shell_sheet.dart', + 'lib/features/mobile/mobile_shell_core.dart', + 'lib/features/mobile/mobile_shell_workspace.dart', + ]; + + const forbiddenSnippets = [ + '连接 Gateway 后', + 'Connect a gateway', + 'Connect Gateway', + '编辑连接', + '当前线程目标网关尚未连接', + 'Gateway connection failed', + 'Connect gateway', + ]; + + for (final path in targets) { + final source = File(path).readAsStringSync(); + for (final snippet in forbiddenSnippets) { + expect( + source.contains(snippet), + isFalse, + reason: '$path should not contain legacy gateway-only copy: $snippet', + ); + } + } + }); +} diff --git a/test/runtime/external_code_agent_acp_desktop_transport_test.dart b/test/runtime/external_code_agent_acp_desktop_transport_test.dart index 79ed7f6d..1d0bfff2 100644 --- a/test/runtime/external_code_agent_acp_desktop_transport_test.dart +++ b/test/runtime/external_code_agent_acp_desktop_transport_test.dart @@ -84,27 +84,6 @@ void main() { }, ); - test('ignores app-side provider sync in bridge-only mode', () async { - final client = _FakeGatewayAcpClient(); - final transport = ExternalCodeAgentAcpDesktopTransport( - client: client, - endpointResolver: (_) => null, - ); - - await transport - .syncExternalProviders(const [ - ExternalCodeAgentAcpSyncedProvider( - providerId: 'codex', - label: 'Codex', - endpoint: 'https://acp-server.svc.plus/codex/acp/rpc', - authorizationHeader: '', - enabled: true, - ), - ]); - - expect(client.methods, isEmpty); - }); - test( 'uses bridge routing resolve for preflight provider selection', () async {