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.
This commit is contained in:
Haitao Pan 2026-04-30 12:14:13 +08:00
parent 16f214a6c6
commit f2e3cfa790
6 changed files with 88 additions and 15 deletions

View File

@ -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;
}

View File

@ -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,

View File

@ -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',

View File

@ -114,6 +114,8 @@ Future<void> 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<void> 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<void> 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(

View File

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

View File

@ -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 <String, dynamic>{},
provider: provider,
remoteWorkingDirectoryHint: remoteWorkingDirectoryHint,
resumeSession: resumeSession,
);
}