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, ); }