refactor: remove stale thread target fallback

This commit is contained in:
Haitao Pan 2026-04-11 09:02:22 +08:00
parent 5aed098e6c
commit 8d70cfedea
6 changed files with 139 additions and 39 deletions

View File

@ -835,28 +835,15 @@ extension AppControllerDesktopSettingsRuntime on AppController {
final sessionKey = normalizedAssistantSessionKeyInternal(
sessionsControllerInternal.currentSessionKey,
);
final thread = taskThreadForSessionInternal(sessionKey);
final target = thread?.hasExplicitExecutionTargetSelection ?? false
? assistantExecutionTargetForSession(sessionKey)
: sanitizePersistedExecutionTargetInternal(
snapshot.assistantExecutionTarget,
);
if (thread?.hasExplicitExecutionTargetSelection ?? false) {
upsertTaskThreadInternal(
sessionKey,
gatewayEntryState: gatewayEntryStateForTargetInternal(target),
latestResolvedRuntimeModel: '',
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
} else {
upsertTaskThreadInternal(
sessionKey,
executionTarget: target,
gatewayEntryState: gatewayEntryStateForTargetInternal(target),
latestResolvedRuntimeModel: '',
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
}
final target = assistantExecutionTargetForSession(sessionKey);
upsertTaskThreadInternal(
sessionKey,
executionTarget: target,
executionTargetSource: ThreadSelectionSource.explicit,
gatewayEntryState: gatewayEntryStateForTargetInternal(target),
latestResolvedRuntimeModel: '',
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
recomputeTasksInternal();
notifyIfActiveInternal();
await applyAssistantExecutionTargetInternal(

View File

@ -292,7 +292,7 @@ extension AppControllerDesktopSkillPermissions on AppController {
AssistantExecutionTarget.singleAgent,
ThreadExecutionMode.gatewayLocal => AssistantExecutionTarget.local,
ThreadExecutionMode.gatewayRemote => AssistantExecutionTarget.remote,
null => settings.assistantExecutionTarget,
null => AssistantExecutionTarget.singleAgent,
};
final nextImportedSkills =
importedSkills ??

View File

@ -48,6 +48,16 @@ import 'app_controller_desktop_thread_sessions_collaboration_impl.dart';
// ignore_for_file: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
extension AppControllerDesktopThreadSessions on AppController {
AssistantExecutionTarget resolveAssistantExecutionTargetFromRecordsInternal(
TaskThread? primaryRecord, {
TaskThread? fallbackRecord,
}) {
return resolveAssistantExecutionTargetFromRecordsForTest(
primaryRecord,
fallbackRecord: fallbackRecord,
);
}
TaskThread? taskThreadForSessionInternal(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
@ -544,12 +554,12 @@ extension AppControllerDesktopThreadSessions on AppController {
sessionKey,
);
final record = taskThreadForSessionInternal(normalizedSessionKey);
return sanitizePersistedExecutionTargetInternal(
record == null
? settings.assistantExecutionTarget
: assistantExecutionTargetFromExecutionMode(
record.executionBinding.executionMode,
),
final mainRecord = matchesSessionKey(normalizedSessionKey, 'main')
? null
: taskThreadForSessionInternal('main');
return resolveAssistantExecutionTargetFromRecordsInternal(
record,
fallbackRecord: mainRecord,
);
}
@ -621,3 +631,15 @@ extension AppControllerDesktopThreadSessions on AppController {
return items;
}
}
AssistantExecutionTarget resolveAssistantExecutionTargetFromRecordsForTest(
TaskThread? primaryRecord, {
TaskThread? fallbackRecord,
}) {
final record = primaryRecord ?? fallbackRecord;
return record == null
? AssistantExecutionTarget.singleAgent
: assistantExecutionTargetFromExecutionMode(
record.executionBinding.executionMode,
);
}

View File

@ -323,15 +323,8 @@ List<String> assistantModelChoicesForSessionThreadSessionInternal(
final normalizedSessionKey = normalizeAssistantSessionKeyThreadInternal(
sessionKey,
);
final target = controller.sanitizePersistedExecutionTargetInternal(
controller.taskThreadForSessionInternal(normalizedSessionKey) == null
? controller.settings.assistantExecutionTarget
: assistantExecutionTargetFromExecutionMode(
controller
.requireTaskThreadForSessionInternal(normalizedSessionKey)
.executionBinding
.executionMode,
),
final target = controller.assistantExecutionTargetForSession(
normalizedSessionKey,
);
if (target == AssistantExecutionTarget.singleAgent) {
final singleAgentUsesAiGatewayFallback =

View File

@ -358,6 +358,7 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
title: title.trim(),
ownerScope: initialOwnerScope,
executionTarget: resolvedTarget,
executionTargetSource: ThreadSelectionSource.explicit,
workspaceBinding: initialWorkspaceBinding,
messageViewMode:
messageViewMode ??
@ -365,6 +366,7 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
singleAgentProvider:
singleAgentProvider ??
singleAgentProviderForSession(currentSessionKey),
singleAgentProviderSource: ThreadSelectionSource.explicit,
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
// Re-read the current thread target when the async binding sync runs so a
@ -405,7 +407,9 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
return;
}
final authorizationOverride =
await resolveSingleAgentAuthorizationHeaderForProviderInternal(provider);
await resolveSingleAgentAuthorizationHeaderForProviderInternal(
provider,
);
await replaceSingleAgentThreadSkillsInternal(
normalizedSessionKey,
localSkills,

View File

@ -0,0 +1,94 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/app/app_controller_desktop_thread_sessions.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
void main() {
group('resolveAssistantExecutionTargetFromRecordsInternal', () {
const owner = ThreadOwnerScope(
realm: ThreadRealm.local,
subjectType: ThreadSubjectType.user,
subjectId: 'u1',
displayName: 'User',
);
TaskThread buildThread({
required String threadId,
required ThreadExecutionMode mode,
String providerId = 'auto',
}) {
return TaskThread(
threadId: threadId,
ownerScope: owner,
workspaceBinding: const WorkspaceBinding(
workspaceId: 'ws-1',
workspaceKind: WorkspaceKind.localFs,
workspacePath: '/tmp/ws',
displayPath: '/tmp/ws',
writable: true,
),
executionBinding: ExecutionBinding(
executionMode: mode,
executorId: providerId,
providerId: providerId,
endpointId: '',
),
);
}
test('defaults to single-agent when no thread record exists', () {
final resolved = _ThreadSessionTargetResolverHarness().resolveTarget(
primary: null,
);
expect(resolved, AssistantExecutionTarget.singleAgent);
});
test('prefers the current thread record over the main thread fallback', () {
final primary = buildThread(
threadId: 'draft:1',
mode: ThreadExecutionMode.gatewayRemote,
);
final fallback = buildThread(
threadId: 'main',
mode: ThreadExecutionMode.gatewayLocal,
);
final resolved = _ThreadSessionTargetResolverHarness().resolveTarget(
primary: primary,
fallback: fallback,
);
expect(resolved, AssistantExecutionTarget.remote);
});
test(
'uses main thread record instead of settings when current is missing',
() {
final fallback = buildThread(
threadId: 'main',
mode: ThreadExecutionMode.localAgent,
providerId: SingleAgentProvider.opencode.providerId,
);
final resolved = _ThreadSessionTargetResolverHarness().resolveTarget(
primary: null,
fallback: fallback,
);
expect(resolved, AssistantExecutionTarget.singleAgent);
},
);
});
}
class _ThreadSessionTargetResolverHarness {
AssistantExecutionTarget resolveTarget({
required TaskThread? primary,
TaskThread? fallback,
}) {
return resolveAssistantExecutionTargetFromRecordsForTest(
primary,
fallbackRecord: fallback,
);
}
}