diff --git a/lib/app/app_controller_desktop_single_agent.dart b/lib/app/app_controller_desktop_single_agent.dart index c6a5f706..316b9380 100644 --- a/lib/app/app_controller_desktop_single_agent.dart +++ b/lib/app/app_controller_desktop_single_agent.dart @@ -86,11 +86,12 @@ extension AppControllerDesktopSingleAgent on AppController { try { final routing = buildExternalAcpRoutingForSessionInternal(sessionKey); final selection = singleAgentProviderForSession(sessionKey); + await syncExternalAcpProvidersInternal(); final capabilities = await goTaskServiceClientInternal .loadExternalAcpCapabilities( - target: AssistantExecutionTarget.singleAgent, - forceRefresh: true, - ); + target: AssistantExecutionTarget.singleAgent, + forceRefresh: true, + ); final availableProviders = configuredSingleAgentProviders .where(capabilities.providers.contains) .toList(growable: false); @@ -222,12 +223,15 @@ extension AppControllerDesktopSingleAgent on AppController { final resolvedWorkingDirectory = result.resolvedWorkingDirectory.trim(); if (resolvedWorkspaceKind != null && resolvedWorkingDirectory.isNotEmpty) { - final existingThread = requireTaskThreadForSessionInternal(sessionKey); + final existingThread = requireTaskThreadForSessionInternal( + sessionKey, + ); upsertTaskThreadInternal( sessionKey, workspaceBinding: WorkspaceBinding( workspaceId: existingThread.workspaceBinding.workspaceId, - workspaceKind: resolvedWorkspaceKind == WorkspaceRefKind.remotePath + workspaceKind: + resolvedWorkspaceKind == WorkspaceRefKind.remotePath ? WorkspaceKind.remoteFs : WorkspaceKind.localFs, workspacePath: resolvedWorkingDirectory, diff --git a/lib/runtime/external_code_agent_acp_desktop_transport.dart b/lib/runtime/external_code_agent_acp_desktop_transport.dart index 843e6a2e..df6e4b02 100644 --- a/lib/runtime/external_code_agent_acp_desktop_transport.dart +++ b/lib/runtime/external_code_agent_acp_desktop_transport.dart @@ -15,7 +15,8 @@ typedef ExternalCodeAgentAcpProcessStarter = String? workingDirectory, }); -class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransport { +class ExternalCodeAgentAcpDesktopTransport + implements ExternalCodeAgentAcpTransport { ExternalCodeAgentAcpDesktopTransport({ required GatewayAcpClient acpClient, required Uri? Function(AssistantExecutionTarget target) endpointResolver, @@ -32,7 +33,7 @@ class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransp arguments, environment: environment, workingDirectory: workingDirectory, - ); + ); }); final GatewayAcpClient _acpClient; @@ -43,22 +44,21 @@ class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransp Process? _localProcess; Uri? _localEndpoint; Future? _localEndpointFuture; + List _syncedProviders = + const []; @override Future syncExternalProviders( List providers, ) async { + _syncedProviders = List.unmodifiable( + providers, + ); final endpoint = await _ensureLocalEndpoint(); if (endpoint == null) { return; } - await _acpClient.request( - method: 'xworkmate.providers.sync', - params: { - 'providers': providers.map((item) => item.toJson()).toList(growable: false), - }, - endpointOverride: endpoint, - ); + await _syncProvidersToEndpoint(endpoint, _syncedProviders); } @override @@ -70,6 +70,10 @@ class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransp if (endpoint == null) { return const ExternalCodeAgentAcpCapabilities.empty(); } + if (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.auto) { + await _syncProvidersToEndpoint(endpoint, _syncedProviders); + } final capabilities = await _acpClient.loadCapabilities( forceRefresh: forceRefresh, endpointOverride: endpoint, @@ -94,6 +98,10 @@ class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransp code: 'EXTERNAL_ACP_ENDPOINT_MISSING', ); } + if (request.target == AssistantExecutionTarget.singleAgent || + request.target == AssistantExecutionTarget.auto) { + await _syncProvidersToEndpoint(endpoint, _syncedProviders); + } var streamedText = ''; String? completedMessage; final response = await _acpClient.request( @@ -251,4 +259,22 @@ class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransp await dispose(); return null; } + + Future _syncProvidersToEndpoint( + Uri endpoint, + List providers, + ) async { + if (providers.isEmpty) { + return; + } + await _acpClient.request( + method: 'xworkmate.providers.sync', + params: { + 'providers': providers + .map((item) => item.toJson()) + .toList(growable: false), + }, + endpointOverride: endpoint, + ); + } } diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart b/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart index 5e80dab5..306a036f 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart @@ -132,13 +132,21 @@ class FakeGoTaskServiceClientInternal implements GoTaskServiceClient { int capabilitiesCalls = 0; int executeCalls = 0; int cancelCalls = 0; + int syncProvidersCalls = 0; GoTaskServiceRequest? lastRequest; final List requests = []; + final List> syncedProvidersHistory = + >[]; @override Future syncExternalProviders( List providers, - ) async {} + ) async { + syncProvidersCalls += 1; + syncedProvidersHistory.add( + List.unmodifiable(providers), + ); + } @override Future loadExternalAcpCapabilities({ diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart b/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart index 1436469c..bc798d59 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart @@ -41,7 +41,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { raw: {}, errorMessage: '', resolvedModel: 'codex-sonnet', - route: GoTaskServiceRoute.externalAcpSingle, + route: GoTaskServiceRoute.externalAcpSingle, ), ); final controller = await createAppControllerInternal( @@ -100,6 +100,93 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { }, ); + test( + 'AppController syncs custom single-agent providers before execution', + () async { + final tempDirectory = await createTempDirectoryInternal( + 'xworkmate-single-agent-custom-provider-', + ); + final store = createStoreFromTempDirectoryInternal(tempDirectory); + const customProvider = SingleAgentProvider( + providerId: 'custom-agent-1', + label: 'Codex', + badge: 'C', + ); + final client = FakeGoTaskServiceClientInternal( + capabilities: ExternalCodeAgentAcpCapabilities( + singleAgent: true, + multiAgent: false, + providers: {customProvider}, + raw: {}, + ), + result: const GoTaskServiceResult( + success: true, + message: 'CUSTOM_PROVIDER_REPLY', + turnId: 'turn-custom', + raw: {}, + errorMessage: '', + resolvedModel: '', + route: GoTaskServiceRoute.externalAcpSingle, + ), + ); + final controller = await createAppControllerInternal( + store: store, + availableSingleAgentProvidersOverride: const [ + customProvider, + ], + runtimeCoordinator: RuntimeCoordinator( + gateway: FakeGatewayRuntimeInternal(store: store), + codex: FakeCodexRuntimeInternal(), + ), + goTaskServiceClient: client, + ); + await controller.saveSettings( + controller.settings.copyWith( + externalAcpEndpoints: normalizeExternalAcpEndpoints( + profiles: const [ + ExternalAcpEndpointProfile( + providerKey: 'custom-agent-1', + label: 'Codex', + badge: 'C', + endpoint: 'ws://127.0.0.1:9101/acp', + authRef: '', + enabled: true, + ), + ], + ), + multiAgent: controller.settings.multiAgent.copyWith( + autoSync: false, + ), + ), + refreshAfterSave: false, + ); + + await controller.setAssistantExecutionTarget( + AssistantExecutionTarget.singleAgent, + ); + await controller.setSingleAgentProvider(customProvider); + + await controller.sendChatMessage( + '请输出 CUSTOM_PROVIDER_REPLY', + thinking: 'low', + ); + + expect(client.syncProvidersCalls, greaterThanOrEqualTo(1)); + expect(client.executeCalls, 1); + expect(client.lastRequest?.provider, customProvider); + expect( + client.syncedProvidersHistory.any( + (batch) => batch.any( + (provider) => + provider.providerId == 'custom-agent-1' && + provider.endpoint == 'ws://127.0.0.1:9101/acp', + ), + ), + isTrue, + ); + }, + ); + test( 'AppController treats Auto as ready before the first routing resolution when any route is available', () async { @@ -173,7 +260,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { raw: {}, errorMessage: '', resolvedModel: 'codex-sonnet', - route: GoTaskServiceRoute.externalAcpSingle, + route: GoTaskServiceRoute.externalAcpSingle, ), ); final controller = await createAppControllerInternal( @@ -220,7 +307,9 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { final store = createStoreFromTempDirectoryInternal(tempDirectory); await store.initialize(); await store.saveSettingsSnapshot( - SettingsSnapshot.defaults().copyWith(workspacePath: tempDirectory.path), + SettingsSnapshot.defaults().copyWith( + workspacePath: tempDirectory.path, + ), ); final client = FakeGoTaskServiceClientInternal( capabilities: ExternalCodeAgentAcpCapabilities( @@ -236,7 +325,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { raw: {}, errorMessage: '', resolvedModel: 'codex-sonnet', - route: GoTaskServiceRoute.externalAcpSingle, + route: GoTaskServiceRoute.externalAcpSingle, ), ); final controller = await createAppControllerInternal( @@ -311,7 +400,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { raw: {}, errorMessage: '', resolvedModel: 'codex-sonnet', - route: GoTaskServiceRoute.externalAcpSingle, + route: GoTaskServiceRoute.externalAcpSingle, ), ); final controller = await createAppControllerInternal( @@ -607,7 +696,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { raw: {}, errorMessage: '', resolvedModel: '', - route: GoTaskServiceRoute.externalAcpSingle, + route: GoTaskServiceRoute.externalAcpSingle, ), ); final controller = await createAppControllerInternal( @@ -667,7 +756,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { raw: {}, errorMessage: '', resolvedModel: '', - route: GoTaskServiceRoute.externalAcpSingle, + route: GoTaskServiceRoute.externalAcpSingle, ), ); final controller = await createAppControllerInternal( @@ -748,7 +837,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { }, errorMessage: '', resolvedModel: '', - route: GoTaskServiceRoute.externalAcpSingle, + route: GoTaskServiceRoute.externalAcpSingle, ), ); final controller = await createAppControllerInternal( @@ -839,7 +928,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { }, errorMessage: '', resolvedModel: '', - route: GoTaskServiceRoute.externalAcpSingle, + route: GoTaskServiceRoute.externalAcpSingle, ), ); final controller = await createAppControllerInternal(