From 8d70cfedea6dfa72aea404d22b3c81604042d2ef Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 11 Apr 2026 09:02:22 +0800 Subject: [PATCH] refactor: remove stale thread target fallback --- ...p_controller_desktop_settings_runtime.dart | 31 ++---- ..._controller_desktop_skill_permissions.dart | 2 +- ...pp_controller_desktop_thread_sessions.dart | 34 +++++-- ...op_thread_sessions_collaboration_impl.dart | 11 +-- ...ontroller_desktop_workspace_execution.dart | 6 +- ...er_desktop_thread_target_cleanup_test.dart | 94 +++++++++++++++++++ 6 files changed, 139 insertions(+), 39 deletions(-) create mode 100644 test/app_controller_desktop_thread_target_cleanup_test.dart diff --git a/lib/app/app_controller_desktop_settings_runtime.dart b/lib/app/app_controller_desktop_settings_runtime.dart index 00d0033f..fc4b8ab0 100644 --- a/lib/app/app_controller_desktop_settings_runtime.dart +++ b/lib/app/app_controller_desktop_settings_runtime.dart @@ -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( diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart index 541613a5..55a3b31b 100644 --- a/lib/app/app_controller_desktop_skill_permissions.dart +++ b/lib/app/app_controller_desktop_skill_permissions.dart @@ -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 ?? diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index ec190305..f640b7d0 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -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, + ); +} 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 c63e531e..5d59ad20 100644 --- a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart +++ b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart @@ -323,15 +323,8 @@ List 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 = diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index 681d9a9a..67610488 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -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, diff --git a/test/app_controller_desktop_thread_target_cleanup_test.dart b/test/app_controller_desktop_thread_target_cleanup_test.dart new file mode 100644 index 00000000..4482aefd --- /dev/null +++ b/test/app_controller_desktop_thread_target_cleanup_test.dart @@ -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, + ); + } +}