From cd962b57fbbacbee762ae8e5e85f2101934f920e Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 6 Apr 2026 17:02:55 +0800 Subject: [PATCH] fix: keep manual task target selection --- ...app_controller_desktop_thread_binding.dart | 33 +++++++++++++ lib/app/app_shell_desktop.dart | 14 +++--- .../assistant_page_state_actions.dart | 19 ++++--- ...p_controller_draft_thread_target_test.dart | 49 +++++++++++++++++++ 4 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 test/runtime/app_controller_draft_thread_target_test.dart diff --git a/lib/app/app_controller_desktop_thread_binding.dart b/lib/app/app_controller_desktop_thread_binding.dart index 89f827c9..c4e59ea9 100644 --- a/lib/app/app_controller_desktop_thread_binding.dart +++ b/lib/app/app_controller_desktop_thread_binding.dart @@ -196,6 +196,19 @@ extension AppControllerDesktopThreadBinding on AppController { ); } + AssistantExecutionTarget resolveDraftThreadExecutionTargetInternal( + String sessionKey, { + required Iterable supportedTargets, + }) { + return pickDraftThreadExecutionTargetInternal( + currentTarget: assistantExecutionTargetForSession(sessionKey), + visibleTargets: visibleAssistantExecutionTargets(supportedTargets), + localWorkspaceAvailable: localThreadWorkspacePathInternal( + sessionKey, + ).trim().isNotEmpty, + ); + } + ExecutionBinding buildDesktopExecutionBindingInternal({ required AssistantExecutionTarget executionTarget, required SingleAgentProvider singleAgentProvider, @@ -276,3 +289,23 @@ extension AppControllerDesktopThreadBinding on AppController { ); } } + +AssistantExecutionTarget pickDraftThreadExecutionTargetInternal({ + required AssistantExecutionTarget currentTarget, + required Iterable visibleTargets, + required bool localWorkspaceAvailable, +}) { + final orderedTargets = [ + if (visibleTargets.contains(currentTarget)) currentTarget, + ...visibleTargets.where((target) => target != currentTarget), + ]; + for (final target in orderedTargets) { + if (!localWorkspaceAvailable && + (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.local)) { + continue; + } + return target; + } + return currentTarget; +} diff --git a/lib/app/app_shell_desktop.dart b/lib/app/app_shell_desktop.dart index baa7b050..4c5cff95 100644 --- a/lib/app/app_shell_desktop.dart +++ b/lib/app/app_shell_desktop.dart @@ -10,6 +10,7 @@ import '../widgets/detail_drawer.dart'; import '../widgets/pane_resize_handle.dart'; import '../widgets/sidebar_navigation.dart'; import 'app_controller.dart'; +import 'app_controller_desktop_thread_binding.dart'; import 'ui_feature_manifest.dart'; import 'workspace_page_registry.dart'; @@ -82,12 +83,13 @@ class _AppShellState extends State { List visibleTargets, ) async { final sessionKey = 'draft:${DateTime.now().millisecondsSinceEpoch}'; - final target = - visibleTargets.contains(controller.currentAssistantExecutionTarget) - ? controller.currentAssistantExecutionTarget - : (visibleTargets.isNotEmpty - ? visibleTargets.first - : controller.currentAssistantExecutionTarget); + final target = pickDraftThreadExecutionTargetInternal( + currentTarget: controller.currentAssistantExecutionTarget, + visibleTargets: visibleTargets, + localWorkspaceAvailable: controller.settings.workspacePath + .trim() + .isNotEmpty, + ); controller.initializeAssistantThreadContext( sessionKey, title: appText('新对话', 'New conversation'), diff --git a/lib/features/assistant/assistant_page_state_actions.dart b/lib/features/assistant/assistant_page_state_actions.dart index c6afc342..bc14488a 100644 --- a/lib/features/assistant/assistant_page_state_actions.dart +++ b/lib/features/assistant/assistant_page_state_actions.dart @@ -12,6 +12,7 @@ import 'package:markdown/markdown.dart' as md; import 'package:path_provider/path_provider.dart'; import 'package:super_clipboard/super_clipboard.dart'; import '../../app/app_controller.dart'; +import '../../app/app_controller_desktop_thread_binding.dart'; import '../../app/app_metadata.dart'; import '../../app/ui_feature_manifest.dart'; import '../../i18n/app_language.dart'; @@ -437,13 +438,17 @@ extension AssistantPageStateActionsInternal on AssistantPageStateInternal { Future createNewThreadInternal() async { final sessionKey = buildDraftSessionKeyInternal(widget.controller); - final inheritedTarget = resolvedVisibleExecutionTargetInternal( - widget.controller, - supportedTargets: const [ - AssistantExecutionTarget.singleAgent, - AssistantExecutionTarget.local, - AssistantExecutionTarget.remote, - ], + final inheritedTarget = pickDraftThreadExecutionTargetInternal( + currentTarget: widget.controller.currentAssistantExecutionTarget, + visibleTargets: widget.controller + .visibleAssistantExecutionTargets(const [ + AssistantExecutionTarget.singleAgent, + AssistantExecutionTarget.local, + AssistantExecutionTarget.remote, + ]), + localWorkspaceAvailable: widget.controller.settings.workspacePath + .trim() + .isNotEmpty, ); final inheritedViewMode = widget.controller.currentAssistantMessageViewMode; setState(() { diff --git a/test/runtime/app_controller_draft_thread_target_test.dart b/test/runtime/app_controller_draft_thread_target_test.dart new file mode 100644 index 00000000..f6426ddd --- /dev/null +++ b/test/runtime/app_controller_draft_thread_target_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/app/app_controller_desktop_thread_binding.dart'; +import 'package:xworkmate/runtime/runtime_models.dart'; + +void main() { + group('pickDraftThreadExecutionTargetInternal', () { + test('prefers the first visible manual target for new drafts', () { + final target = pickDraftThreadExecutionTargetInternal( + currentTarget: AssistantExecutionTarget.auto, + visibleTargets: const [ + AssistantExecutionTarget.singleAgent, + AssistantExecutionTarget.local, + AssistantExecutionTarget.remote, + ], + localWorkspaceAvailable: true, + ); + + expect(target, AssistantExecutionTarget.singleAgent); + }); + + test('skips local targets when the local workspace is unavailable', () { + final target = pickDraftThreadExecutionTargetInternal( + currentTarget: AssistantExecutionTarget.auto, + visibleTargets: const [ + AssistantExecutionTarget.singleAgent, + AssistantExecutionTarget.local, + AssistantExecutionTarget.remote, + ], + localWorkspaceAvailable: false, + ); + + expect(target, AssistantExecutionTarget.remote); + }); + + test('keeps the current visible manual target when it is usable', () { + final target = pickDraftThreadExecutionTargetInternal( + currentTarget: AssistantExecutionTarget.remote, + visibleTargets: const [ + AssistantExecutionTarget.singleAgent, + AssistantExecutionTarget.local, + AssistantExecutionTarget.remote, + ], + localWorkspaceAvailable: false, + ); + + expect(target, AssistantExecutionTarget.remote); + }); + }); +}