From 24cd132a74a1e2941991f69494bc0e82800bdbcb Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 11 Apr 2026 12:02:32 +0800 Subject: [PATCH] fix: share ACP bridge across desktop runtimes --- ...rnal_code_agent_acp_desktop_transport.dart | 5 +++++ lib/runtime/gateway_runtime_core.dart | 3 +++ lib/runtime/go_acp_stdio_bridge.dart | 15 ++++++++++++- .../go_gateway_runtime_desktop_client.dart | 5 +++++ .../go_task_service_desktop_service.dart | 4 ++++ ...er_desktop_gateway_bridge_client_test.dart | 21 +++++++++++++++++++ 6 files changed, 52 insertions(+), 1 deletion(-) diff --git a/lib/runtime/external_code_agent_acp_desktop_transport.dart b/lib/runtime/external_code_agent_acp_desktop_transport.dart index 911b6431..9e326701 100644 --- a/lib/runtime/external_code_agent_acp_desktop_transport.dart +++ b/lib/runtime/external_code_agent_acp_desktop_transport.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; + import 'gateway_acp_client.dart'; import 'go_acp_stdio_bridge.dart'; import 'go_task_service_client.dart'; @@ -14,6 +16,9 @@ class ExternalCodeAgentAcpDesktopTransport List _syncedProviders = const []; + @visibleForTesting + GoAcpStdioBridge get bridgeForTest => _bridge; + @override Future syncExternalProviders( List providers, diff --git a/lib/runtime/gateway_runtime_core.dart b/lib/runtime/gateway_runtime_core.dart index 9330146b..ae484d22 100644 --- a/lib/runtime/gateway_runtime_core.dart +++ b/lib/runtime/gateway_runtime_core.dart @@ -103,6 +103,9 @@ class GatewayRuntime extends ChangeNotifier with GatewayRuntimeHelpersInternal { @visibleForTesting bool get usesSessionClient => sessionClientInternal != null; + @visibleForTesting + GatewayRuntimeSessionClient? get sessionClientForTest => sessionClientInternal; + Future initialize() async { sessionUpdatesInternal ??= sessionClientInternal?.updates.listen( _handleSessionUpdateInternal, diff --git a/lib/runtime/go_acp_stdio_bridge.dart b/lib/runtime/go_acp_stdio_bridge.dart index f761b8db..e16aa835 100644 --- a/lib/runtime/go_acp_stdio_bridge.dart +++ b/lib/runtime/go_acp_stdio_bridge.dart @@ -41,6 +41,7 @@ class GoAcpStdioBridge { StreamSubscription? _stdoutSubscription; StreamSubscription? _stderrSubscription; Future? _startupFuture; + Future? _disposeFuture; int _requestCounter = 0; Stream> get notifications => @@ -84,6 +85,16 @@ class GoAcpStdioBridge { } Future dispose() async { + final inFlight = _disposeFuture; + if (inFlight != null) { + return inFlight; + } + final next = _disposeInternal(); + _disposeFuture = next; + return next; + } + + Future _disposeInternal() async { final process = _process; _process = null; _startupFuture = null; @@ -111,7 +122,9 @@ class GoAcpStdioBridge { // Best effort only. } } - await _notificationsController.close(); + if (!_notificationsController.isClosed) { + await _notificationsController.close(); + } } Future _ensureStarted() async { diff --git a/lib/runtime/go_gateway_runtime_desktop_client.dart b/lib/runtime/go_gateway_runtime_desktop_client.dart index 0354a4a8..3c8b2f30 100644 --- a/lib/runtime/go_gateway_runtime_desktop_client.dart +++ b/lib/runtime/go_gateway_runtime_desktop_client.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; + import 'gateway_runtime_errors.dart'; import 'gateway_runtime_session_client.dart'; import 'go_acp_stdio_bridge.dart'; @@ -20,6 +22,9 @@ class GoGatewayRuntimeDesktopClient implements GatewayRuntimeSessionClient { final StreamController _updatesController = StreamController.broadcast(); + @visibleForTesting + GoAcpStdioBridge get bridgeForTest => _bridge; + @override Stream get updates => _updatesController.stream; diff --git a/lib/runtime/go_task_service_desktop_service.dart b/lib/runtime/go_task_service_desktop_service.dart index 87c28f01..6d2e4b74 100644 --- a/lib/runtime/go_task_service_desktop_service.dart +++ b/lib/runtime/go_task_service_desktop_service.dart @@ -1,6 +1,7 @@ import 'gateway_runtime.dart'; import 'go_task_service_client.dart'; import 'runtime_models.dart'; +import 'package:flutter/foundation.dart'; class DesktopGoTaskService implements GoTaskServiceClient { DesktopGoTaskService({ @@ -69,6 +70,9 @@ class DesktopGoTaskService implements GoTaskServiceClient { threadId: threadId, ); + @visibleForTesting + ExternalCodeAgentAcpTransport get acpTransportForTest => _acpTransport; + @override Future dispose() async { await _acpTransport.dispose(); diff --git a/test/app_controller_desktop_gateway_bridge_client_test.dart b/test/app_controller_desktop_gateway_bridge_client_test.dart index ea884ec2..4ab9e381 100644 --- a/test/app_controller_desktop_gateway_bridge_client_test.dart +++ b/test/app_controller_desktop_gateway_bridge_client_test.dart @@ -1,5 +1,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:xworkmate/app/app_controller_desktop_core.dart'; +import 'package:xworkmate/runtime/external_code_agent_acp_desktop_transport.dart'; +import 'package:xworkmate/runtime/go_gateway_runtime_desktop_client.dart'; +import 'package:xworkmate/runtime/go_task_service_desktop_service.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -10,4 +13,22 @@ void main() { expect(controller.runtime.usesSessionClient, isTrue); }); + + test( + 'default desktop controller shares one ACP bridge between gateway runtime and task transport', + () { + final controller = AppController(); + addTearDown(controller.dispose); + + final sessionClient = + controller.runtime.sessionClientForTest + as GoGatewayRuntimeDesktopClient; + final taskService = + controller.goTaskServiceClientForTest as DesktopGoTaskService; + final transport = + taskService.acpTransportForTest as ExternalCodeAgentAcpDesktopTransport; + + expect(sessionClient.bridgeForTest, same(transport.bridgeForTest)); + }, + ); }