fix(assistant): auto-bind workspace for only-chat fallback runs

This commit is contained in:
Haitao Pan 2026-04-04 16:12:57 +08:00
parent 0c800f8b83
commit d69bcf1d13
2 changed files with 123 additions and 1 deletions

View File

@ -260,9 +260,16 @@ extension AppControllerDesktopThreadActions on AppController {
currentSessionKey,
executionTarget: currentTarget,
);
final workspacePath = assistantWorkspacePathForSession(
var workspacePath = assistantWorkspacePathForSession(
currentSessionKey,
).trim();
if (workspacePath.isEmpty) {
await tryBindWorkspaceForOnlyChatFallbackInternal(
currentSessionKey,
currentTarget,
);
workspacePath = assistantWorkspacePathForSession(currentSessionKey).trim();
}
if (workspacePath.isEmpty) {
final error = StateError(
appText(
@ -586,4 +593,60 @@ extension AppControllerDesktopThreadActions on AppController {
}
return value.isEmpty ? null : value;
}
Future<void> tryBindWorkspaceForOnlyChatFallbackInternal(
String sessionKey,
AssistantExecutionTarget currentTarget,
) async {
if (currentTarget != AssistantExecutionTarget.singleAgent &&
currentTarget != AssistantExecutionTarget.auto) {
return;
}
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
if (!singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) {
return;
}
if (assistantWorkspacePathForSession(normalizedSessionKey).trim().isNotEmpty) {
return;
}
final candidateRoots = <String>{
settings.workspacePath.trim(),
Directory.current.path.trim(),
settings.remoteProjectRoot.trim(),
}.where((item) => item.isNotEmpty);
for (final root in candidateRoots) {
final threadWorkspace =
'${trimTrailingPathSeparatorInternal(root)}/.xworkmate/threads/${threadWorkspaceDirectoryNameInternal(normalizedSessionKey)}';
if (!ensureLocalWorkspaceDirectoryInternal(threadWorkspace)) {
continue;
}
final existing = assistantThreadRecordsInternal[normalizedSessionKey];
upsertTaskThreadInternal(
normalizedSessionKey,
workspaceBinding: WorkspaceBinding(
workspaceId: normalizedSessionKey,
workspaceKind: WorkspaceKind.localFs,
workspacePath: threadWorkspace,
displayPath: threadWorkspace,
writable: existing?.workspaceBinding.writable ?? true,
),
lifecycleState:
(existing?.lifecycleState ??
const ThreadLifecycleState(
archived: false,
status: 'ready',
lastRunAtMs: null,
lastResultCode: null,
))
.copyWith(status: 'ready'),
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
await flushAssistantThreadPersistenceInternal();
recomputeTasksInternal();
return;
}
}
}

View File

@ -443,6 +443,65 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
);
},
);
test(
'AppController auto-binds a thread workspace in AI Chat fallback when workspace root is missing',
() async {
final tempDirectory = await createTempDirectoryInternal(
'xworkmate-single-agent-fallback-missing-workspace-',
);
final server = await FakeAiGatewayServerInternal.start(
responseMode: AiGatewayResponseModeInternal.json,
);
addTearDown(() async {
await server.close();
});
final store = createStoreFromTempDirectoryInternal(tempDirectory);
final client = FallbackOnlyGoAgentCoreClientInternal();
final controller = await createAppControllerInternal(
store: store,
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
runtimeCoordinator: RuntimeCoordinator(
gateway: FakeGatewayRuntimeInternal(store: store),
codex: FakeCodexRuntimeInternal(),
),
goAgentCoreClient: client,
);
await controller.settingsController.saveAiGatewayApiKey('live-key');
await controller.saveSettings(
controller.settings.copyWith(
workspacePath: '',
aiGateway: controller.settings.aiGateway.copyWith(
baseUrl: server.baseUrl,
availableModels: const <String>['moonshotai/kimi-k2.5'],
selectedModels: const <String>['moonshotai/kimi-k2.5'],
),
defaultModel: 'moonshotai/kimi-k2.5',
),
refreshAfterSave: false,
);
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.singleAgent,
);
await controller.setSingleAgentProvider(SingleAgentProvider.opencode);
await controller.sendChatMessage('你好', thinking: 'low');
final workspacePath = controller.assistantWorkspacePathForSession(
controller.currentSessionKey,
);
expect(client.capabilitiesCalls, greaterThanOrEqualTo(1));
expect(client.executeCalls, 0);
expect(server.requestCount, 1);
expect(workspacePath, isNotEmpty);
expect(
workspacePath,
contains('.xworkmate/threads/'),
);
},
);
});
group('Single Agent workspace resolution', () {