From f2e3cfa7909bc7bc9ec62efb93a2943079f54332 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 30 Apr 2026 12:14:13 +0800 Subject: [PATCH] feat: pass remote working directory as hint while preserving local cwd Distinguishes between the local working directory (used for execution) and the remote working directory hint. Captures remote working directory from execution results and persists it for future turns. --- ...ler_desktop_runtime_coordination_impl.dart | 23 ++++++------ ...pp_controller_desktop_runtime_helpers.dart | 7 ++++ ...app_controller_desktop_thread_actions.dart | 15 ++++++-- ...op_thread_sessions_collaboration_impl.dart | 12 ++++++ ...troller_thread_workspace_binding_test.dart | 9 ++++- .../runtime/gateway_acp_client_auth_test.dart | 37 +++++++++++++++++++ 6 files changed, 88 insertions(+), 15 deletions(-) diff --git a/lib/app/app_controller_desktop_runtime_coordination_impl.dart b/lib/app/app_controller_desktop_runtime_coordination_impl.dart index 3e1edd3a..480b3e60 100644 --- a/lib/app/app_controller_desktop_runtime_coordination_impl.dart +++ b/lib/app/app_controller_desktop_runtime_coordination_impl.dart @@ -158,26 +158,27 @@ mergeAcpCapabilitiesIntoMountTargetsRuntimeInternal( String? assistantWorkingDirectoryForSessionRuntimeInternal( AppController controller, String sessionKey, +) { + return resolveLocalAssistantWorkingDirectoryForSessionRuntimeInternal( + controller, + sessionKey, + requireLocalExistence: false, + ); +} + +String? assistantRemoteWorkingDirectoryHintForSessionRuntimeInternal( + AppController controller, + String sessionKey, ) { final normalizedSessionKey = controller.normalizedAssistantSessionKeyInternal( sessionKey, ); - final remoteCandidate = + final candidate = controller .assistantThreadRecordsInternal[normalizedSessionKey] ?.lastRemoteWorkingDirectory ?.trim() ?? ''; - if (remoteCandidate.isNotEmpty) { - return remoteCandidate; - } - final candidate = - controller - .assistantThreadRecordsInternal[normalizedSessionKey] - ?.workspaceBinding - .workspacePath - .trim() ?? - ''; if (candidate.isEmpty) { return null; } diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index 89349967..19f14678 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -419,6 +419,13 @@ extension AppControllerDesktopRuntimeHelpers on AppController { String? assistantWorkingDirectoryForSessionInternal(String sessionKey) => assistantWorkingDirectoryForSessionRuntimeInternal(this, sessionKey); + String? assistantRemoteWorkingDirectoryHintForSessionInternal( + String sessionKey, + ) => assistantRemoteWorkingDirectoryHintForSessionRuntimeInternal( + this, + sessionKey, + ); + String? resolveLocalAssistantWorkingDirectoryForSessionInternal( String sessionKey, { bool requireLocalExistence = true, diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index 0ea1a8b9..d71e6495 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -235,9 +235,7 @@ extension AppControllerDesktopThreadActions on AppController { final currentTarget = assistantExecutionTargetForSession(currentSessionKey); final connectionState = currentAssistantConnectionState; if (!connectionState.connected) { - final error = StateError( - connectionState.detailLabel, - ); + final error = StateError(connectionState.detailLabel); appendAssistantThreadMessageInternal( currentSessionKey, assistantErrorMessageInternal(error.message), @@ -256,6 +254,11 @@ extension AppControllerDesktopThreadActions on AppController { currentSessionKey, )?.trim() ?? ''; + final remoteWorkingDirectoryHint = + assistantRemoteWorkingDirectoryHintForSessionInternal( + currentSessionKey, + )?.trim() ?? + ''; if (workingDirectory.isEmpty) { final error = StateError( appText( @@ -347,6 +350,7 @@ extension AppControllerDesktopThreadActions on AppController { provider: assistantProviderForSession(sessionKey), prompt: message, workingDirectory: workingDirectory, + remoteWorkingDirectoryHint: remoteWorkingDirectoryHint, model: assistantModelForSession(sessionKey), thinking: thinking, selectedSkills: selectedSkillLabels, @@ -372,6 +376,11 @@ extension AppControllerDesktopThreadActions on AppController { result: result, ), latestResolvedRuntimeModel: result.resolvedModel.trim(), + lastRemoteWorkingDirectory: + result.remoteWorkingDirectory.trim().isNotEmpty + ? result.remoteWorkingDirectory.trim() + : null, + lastRemoteWorkspaceRefKind: result.remoteWorkspaceRefKind, lifecycleStatus: 'ready', lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), lastResultCode: result.success ? 'success' : 'error', diff --git a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart index 8ceadb60..bb49413d 100644 --- a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart +++ b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart @@ -114,6 +114,8 @@ Future runMultiAgentCollaborationThreadSessionInternal( ); final workingDirectory = controller .assistantWorkingDirectoryForSessionInternal(sessionKey); + final remoteWorkingDirectoryHint = controller + .assistantRemoteWorkingDirectoryHintForSessionInternal(sessionKey); if (workingDirectory == null || workingDirectory.trim().isEmpty) { final error = StateError( appText( @@ -163,6 +165,7 @@ Future runMultiAgentCollaborationThreadSessionInternal( target: controller.assistantExecutionTargetForSession(sessionKey), prompt: composedPrompt, workingDirectory: workingDirectory, + remoteWorkingDirectoryHint: remoteWorkingDirectoryHint?.trim() ?? '', model: controller.assistantModelForSession(sessionKey), thinking: 'medium', selectedSkills: selectedSkillLabels, @@ -201,6 +204,15 @@ Future runMultiAgentCollaborationThreadSessionInternal( sessionKey, result, ); + controller.upsertTaskThreadInternal( + sessionKey, + lastRemoteWorkingDirectory: + result.remoteWorkingDirectory.trim().isNotEmpty + ? result.remoteWorkingDirectory.trim() + : null, + lastRemoteWorkspaceRefKind: result.remoteWorkspaceRefKind, + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); controller.appendLocalSessionMessageInternal( sessionKey, GatewayChatMessage( diff --git a/test/runtime/app_controller_thread_workspace_binding_test.dart b/test/runtime/app_controller_thread_workspace_binding_test.dart index 8649432f..01a7232a 100644 --- a/test/runtime/app_controller_thread_workspace_binding_test.dart +++ b/test/runtime/app_controller_thread_workspace_binding_test.dart @@ -42,7 +42,7 @@ void main() { controller, 'session-1', ), - remoteWorkspace.path, + localWorkspace.path, ); expect( resolveLocalAssistantWorkingDirectoryForSessionRuntimeInternal( @@ -51,6 +51,13 @@ void main() { ), localWorkspace.path, ); + expect( + assistantRemoteWorkingDirectoryHintForSessionRuntimeInternal( + controller, + 'session-1', + ), + remoteWorkspace.path, + ); }, ); diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index 3cde78b2..d9f4c4a5 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -577,6 +577,41 @@ void main() { expect(capture.requestBody, isNot(contains('"method":"turn/start"'))); }); + test( + 'desktop execution keeps local cwd and sends remote workspace as hint', + () async { + final capture = await _startAcpHttpServer(); + addTearDown(capture.close); + final client = GatewayAcpClient( + endpointResolver: () => capture.baseEndpoint, + authorizationResolver: (_) async => 'bridge-token', + ); + + final transport = ExternalCodeAgentAcpDesktopTransport( + client: client, + endpointResolver: (_) => capture.baseEndpoint, + taskEndpointResolver: (_) => capture.baseEndpoint, + ); + + await transport.executeTask( + _taskRequest( + target: AssistantExecutionTarget.agent, + provider: SingleAgentProvider.codex, + remoteWorkingDirectoryHint: '/owners/local/user/demo/threads/main', + ), + onUpdate: (_) {}, + ); + + expect(capture.requestBody, contains('"workingDirectory":"/tmp"')); + expect( + capture.requestBody, + contains( + '"remoteWorkingDirectoryHint":"/owners/local/user/demo/threads/main"', + ), + ); + }, + ); + test('multi-agent execution uses session lifecycle methods', () async { final capture = await _startAcpHttpServer(); addTearDown(capture.close); @@ -668,6 +703,7 @@ GoTaskServiceRequest _taskRequest({ required AssistantExecutionTarget target, required SingleAgentProvider provider, bool resumeSession = false, + String remoteWorkingDirectoryHint = '', }) { return GoTaskServiceRequest( sessionId: 'session-1', @@ -683,6 +719,7 @@ GoTaskServiceRequest _taskRequest({ agentId: '', metadata: const {}, provider: provider, + remoteWorkingDirectoryHint: remoteWorkingDirectoryHint, resumeSession: resumeSession, ); }