From 155c4e3de1f458b118e233410d925d3bf6771d8d Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 12 Apr 2026 19:51:04 +0800 Subject: [PATCH] Refine assistant task dialog mode mapping --- lib/app/app_controller_desktop_core.dart | 19 ++++-- ...ntroller_desktop_external_acp_routing.dart | 18 ++--- ...ler_desktop_runtime_coordination_impl.dart | 1 + ..._controller_desktop_skill_permissions.dart | 20 ++++-- ...app_controller_desktop_thread_binding.dart | 18 +++-- ...pp_controller_desktop_thread_sessions.dart | 24 +++++-- ...app_controller_desktop_thread_storage.dart | 27 +++++--- ...ontroller_desktop_workspace_execution.dart | 12 +++- lib/app/ui_feature_manifest_core.dart | 13 ++-- .../assistant/assistant_page_components.dart | 7 +- .../assistant_page_composer_bar.dart | 17 +++-- .../assistant_page_composer_support.dart | 1 + .../assistant_page_state_actions.dart | 10 +-- lib/runtime/go_task_service_client.dart | 7 +- lib/runtime/runtime_models_connection.dart | 41 ++++++++---- .../runtime_models_runtime_payloads.dart | 17 +++-- .../runtime_models_settings_snapshot.dart | 2 +- .../sidebar_navigation_task_section.dart | 1 + .../assistant/assistant_lower_pane_test.dart | 65 +++++++++++++++++-- .../assistant_execution_target_test.dart | 49 ++++++++++++++ 20 files changed, 279 insertions(+), 90 deletions(-) create mode 100644 test/runtime/assistant_execution_target_test.dart diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index 6c8b6e8c..57afdfac 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -480,7 +480,8 @@ class AppController extends ChangeNotifier { bool get hasPendingSettingsApply => pendingSettingsApplyInternal; String get settingsDraftStatusMessage => settingsDraftStatusMessageInternal; List get agents => agentsControllerInternal.agents; - List get sessions => sessionsControllerInternal.sessions; + List get sessions => + sessionsControllerInternal.sessions; List get assistantSessions => assistantSessionsInternal(); List get instances => @@ -568,7 +569,9 @@ class AppController extends ChangeNotifier { normalizeSingleAgentProviderList(bridgeProviderCatalogInternal); List get assistantProviderCatalog { - final catalog = bridgeProviderCatalog; + final catalog = normalizeBridgeOwnedSingleAgentProviderList( + bridgeProviderCatalogInternal, + ); if (catalog.isNotEmpty) { return catalog; } @@ -589,7 +592,9 @@ class AppController extends ChangeNotifier { } SingleAgentProvider resolveAssistantProvider(String? providerId) { - final normalizedProviderId = normalizeSingleAgentProviderId(providerId ?? ''); + final normalizedProviderId = normalizeSingleAgentProviderId( + providerId ?? '', + ); final catalog = assistantProviderCatalog; if (normalizedProviderId.isNotEmpty) { for (final provider in catalog) { @@ -609,12 +614,18 @@ class AppController extends ChangeNotifier { sessionKey, ); final thread = taskThreadForSessionInternal(normalizedSessionKey); + final executionTarget = assistantExecutionTargetForSession( + normalizedSessionKey, + ); + if (executionTarget.isGateway) { + return SingleAgentProvider.openclaw; + } return resolveAssistantProvider(thread?.executionBinding.providerId); } List visibleAssistantExecutionTargets( Iterable supportedTargets, - ) => const [AssistantExecutionTarget.gateway]; + ) => compactAssistantExecutionTargets(supportedTargets); List get aiGatewayConversationModelChoices { final availableModels = diff --git a/lib/app/app_controller_desktop_external_acp_routing.dart b/lib/app/app_controller_desktop_external_acp_routing.dart index f9e9f061..7f7095ca 100644 --- a/lib/app/app_controller_desktop_external_acp_routing.dart +++ b/lib/app/app_controller_desktop_external_acp_routing.dart @@ -64,10 +64,14 @@ extension AppControllerDesktopExternalAcpRouting on AppController { .where((item) => item.trim().isNotEmpty) .toList(growable: false); + final currentTarget = assistantExecutionTargetForSession( + normalizedSessionKey, + ); final resolvedProvider = assistantProviderForSession(normalizedSessionKey); - final resolvedExplicitProviderId = - thread?.hasExplicitProviderSelection == true && - !resolvedProvider.isUnspecified + final resolvedExplicitProviderId = currentTarget.isGateway + ? kCanonicalGatewayProviderId + : thread?.hasExplicitProviderSelection == true && + !resolvedProvider.isUnspecified ? resolvedProvider.providerId : ''; final resolvedExplicitModel = thread?.hasExplicitModelSelection ?? false @@ -85,9 +89,7 @@ extension AppControllerDesktopExternalAcpRouting on AppController { explicitExecutionTarget?.trim().isNotEmpty == true ? explicitExecutionTarget!.trim() : hasAnyExplicitSelection - ? _routingExecutionTargetValueInternal( - assistantExecutionTargetForSession(normalizedSessionKey), - ) + ? _routingExecutionTargetValueInternal(currentTarget) : ''; final hasExplicitSelection = resolvedExplicitExecutionTarget.isNotEmpty || @@ -115,8 +117,6 @@ extension AppControllerDesktopExternalAcpRouting on AppController { } String _routingExecutionTargetValueInternal(AssistantExecutionTarget target) { - return switch (target) { - AssistantExecutionTarget.gateway => 'gateway', - }; + return target.promptValue; } } diff --git a/lib/app/app_controller_desktop_runtime_coordination_impl.dart b/lib/app/app_controller_desktop_runtime_coordination_impl.dart index 7adfba87..d9351f4d 100644 --- a/lib/app/app_controller_desktop_runtime_coordination_impl.dart +++ b/lib/app/app_controller_desktop_runtime_coordination_impl.dart @@ -187,6 +187,7 @@ GatewayMode bridgeGatewayModeRuntimeInternal(AppController controller) { return GatewayMode.offline; } return switch (controller.currentAssistantExecutionTarget) { + AssistantExecutionTarget.agent => GatewayMode.remote, AssistantExecutionTarget.gateway => GatewayMode.remote, }; } diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart index c7a7c155..4ab8d94f 100644 --- a/lib/app/app_controller_desktop_skill_permissions.dart +++ b/lib/app/app_controller_desktop_skill_permissions.dart @@ -250,8 +250,9 @@ extension AppControllerDesktopSkillPermissions on AppController { final nextExecutionTarget = executionTarget ?? switch (existing?.executionBinding.executionMode) { + ThreadExecutionMode.agent => AssistantExecutionTarget.agent, ThreadExecutionMode.gateway => AssistantExecutionTarget.gateway, - null => AssistantExecutionTarget.gateway, + null => AssistantExecutionTarget.agent, }; final nextImportedSkills = importedSkills ?? @@ -293,11 +294,15 @@ extension AppControllerDesktopSkillPermissions on AppController { final requestedProvider = singleAgentProvider?.isUnspecified == false ? singleAgentProvider : null; - final nextProvider = resolveAssistantProvider( + final nextProviderId = normalizeSingleAgentProviderId( requestedProvider?.providerId ?? existing?.executionBinding.providerId ?? - existing?.contextState.latestResolvedProviderId, + existing?.contextState.latestResolvedProviderId ?? + '', ); + final nextProvider = nextProviderId.isEmpty + ? SingleAgentProvider.unspecified + : resolveAssistantProvider(nextProviderId); final nextProviderSource = singleAgentProviderSource ?? existing?.executionBinding.providerSource ?? @@ -306,13 +311,18 @@ extension AppControllerDesktopSkillPermissions on AppController { (executionBinding ?? existing?.executionBinding ?? ExecutionBinding( - executionMode: ThreadExecutionMode.gateway, + executionMode: + threadExecutionModeFromAssistantExecutionTarget( + nextExecutionTarget, + ), executorId: nextProvider.providerId, providerId: nextProvider.providerId, endpointId: '', )) .copyWith( - executionMode: ThreadExecutionMode.gateway, + executionMode: threadExecutionModeFromAssistantExecutionTarget( + nextExecutionTarget, + ), executorId: nextProvider.providerId, providerId: nextProvider.providerId, executionModeSource: diff --git a/lib/app/app_controller_desktop_thread_binding.dart b/lib/app/app_controller_desktop_thread_binding.dart index 73b96082..74add700 100644 --- a/lib/app/app_controller_desktop_thread_binding.dart +++ b/lib/app/app_controller_desktop_thread_binding.dart @@ -219,22 +219,30 @@ extension AppControllerDesktopThreadBinding on AppController { required AssistantExecutionTarget executionTarget, ExecutionBinding? existingBinding, }) { - final selectedProvider = resolveAssistantProvider( - existingBinding?.providerId, + final persistedProviderId = normalizeSingleAgentProviderId( + existingBinding?.providerId ?? '', ); + final selectedProvider = persistedProviderId.isEmpty + ? SingleAgentProvider.unspecified + : resolveAssistantProvider(persistedProviderId); return (existingBinding ?? ExecutionBinding( - executionMode: ThreadExecutionMode.gateway, + executionMode: threadExecutionModeFromAssistantExecutionTarget( + executionTarget, + ), executorId: selectedProvider.providerId, providerId: selectedProvider.providerId, endpointId: '', )) .copyWith( - executionMode: ThreadExecutionMode.gateway, + executionMode: threadExecutionModeFromAssistantExecutionTarget( + executionTarget, + ), executorId: selectedProvider.providerId, providerId: selectedProvider.providerId, providerSource: - existingBinding?.providerSource ?? ThreadSelectionSource.inherited, + existingBinding?.providerSource ?? + ThreadSelectionSource.inherited, ); } diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index 980587a2..316e1581 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -303,7 +303,9 @@ extension AppControllerDesktopThreadSessions on AppController { final sessionKey = normalizedAssistantSessionKeyInternal( sessionsControllerInternal.currentSessionKey, ); - final items = List.from(chatControllerInternal.messages); + final items = List.from( + chatControllerInternal.messages, + ); final threadItems = assistantThreadMessagesInternal[sessionKey]; if (threadItems != null && threadItems.isNotEmpty) { items.addAll(threadItems); @@ -312,7 +314,8 @@ extension AppControllerDesktopThreadSessions on AppController { if (localItems != null && localItems.isNotEmpty) { items.addAll(localItems); } - final streaming = chatControllerInternal.streamingAssistantText?.trim() ?? ''; + final streaming = + chatControllerInternal.streamingAssistantText?.trim() ?? ''; if (streaming.isNotEmpty) { items.add( GatewayChatMessage( @@ -419,8 +422,17 @@ AssistantExecutionTarget resolveAssistantExecutionTargetFromRecordsForTest( }) { final record = primaryRecord ?? fallbackRecord; return record == null - ? AssistantExecutionTarget.gateway - : assistantExecutionTargetFromExecutionMode( - record.executionBinding.executionMode, - ); + ? AssistantExecutionTarget.agent + : (() { + final resolved = assistantExecutionTargetFromExecutionMode( + record.executionBinding.executionMode, + ); + if (resolved.isGateway && + isBridgeOwnedSingleAgentProviderId( + record.executionBinding.providerId, + )) { + return AssistantExecutionTarget.agent; + } + return resolved; + })(); } diff --git a/lib/app/app_controller_desktop_thread_storage.dart b/lib/app/app_controller_desktop_thread_storage.dart index 5c9225b6..7173547f 100644 --- a/lib/app/app_controller_desktop_thread_storage.dart +++ b/lib/app/app_controller_desktop_thread_storage.dart @@ -62,7 +62,9 @@ extension AppControllerDesktopThreadStorage on AppController { } Future ensureActiveAssistantThreadInternal() async { - if (!isAssistantTaskArchived(sessionsControllerInternal.currentSessionKey)) { + if (!isAssistantTaskArchived( + sessionsControllerInternal.currentSessionKey, + )) { return; } final fallback = assistantSessionSummariesInternal().firstWhere( @@ -683,11 +685,18 @@ extension AppControllerDesktopThreadStorage on AppController { record.executionBinding.executionMode, ), ); - const recordProvider = SingleAgentProvider( - providerId: kCanonicalGatewayProviderId, - label: kCanonicalGatewayProviderLabel, - badge: 'OC', + final recordProviderId = normalizeSingleAgentProviderId( + record.executionBinding.providerId, ); + final recordProvider = recordProviderId.isEmpty + ? SingleAgentProvider.unspecified + : resolveAssistantProvider(recordProviderId); + final normalizedExecutionTarget = + recordExecutionTarget.isGateway && + recordProviderId.isNotEmpty && + isBridgeOwnedSingleAgentProviderId(recordProviderId) + ? AssistantExecutionTarget.agent + : recordExecutionTarget; final workspaceBinding = record.workspaceBinding.copyWith( workspaceId: sessionKey, displayPath: record.workspaceKind == WorkspaceKind.localFs @@ -707,19 +716,19 @@ extension AppControllerDesktopThreadStorage on AppController { ) .toList(growable: false), assistantModelId: record.assistantModelId.trim().isEmpty - ? resolvedAssistantModelForTargetInternal(recordExecutionTarget) + ? resolvedAssistantModelForTargetInternal(normalizedExecutionTarget) : record.assistantModelId.trim(), gatewayEntryState: (record.gatewayEntryState ?? '').trim().isEmpty - ? gatewayEntryStateForTargetInternal(recordExecutionTarget) + ? gatewayEntryStateForTargetInternal(normalizedExecutionTarget) : record.gatewayEntryState, workspaceBinding: workspaceBinding, executionBinding: record.executionBinding.copyWith( executionMode: threadExecutionModeFromAssistantExecutionTarget( - recordExecutionTarget, + normalizedExecutionTarget, ), executorId: recordProvider.providerId, providerId: recordProvider.providerId, - providerSource: ThreadSelectionSource.inherited, + providerSource: record.executionBinding.providerSource, ), lifecycleState: record.lifecycleState.copyWith(status: 'ready'), ); diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index ac1edafb..af5f8751 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -122,17 +122,27 @@ extension AppControllerDesktopWorkspaceExecution on AppController { if (!assistantThreadRecordsInternal.containsKey(sessionKey)) { initializeAssistantThreadContext( sessionKey, - executionTarget: assistantExecutionTargetForSession(sessionKey), + executionTarget: AssistantExecutionTarget.agent, messageViewMode: assistantMessageViewModeForSession(sessionKey), ); } upsertTaskThreadInternal( sessionKey, + executionTarget: AssistantExecutionTarget.agent, + executionTargetSource: ThreadSelectionSource.explicit, singleAgentProvider: resolvedProvider, singleAgentProviderSource: ThreadSelectionSource.explicit, + gatewayEntryState: gatewayEntryStateForTargetInternal( + AssistantExecutionTarget.agent, + ), latestResolvedProviderId: '', updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), ); + await applyAssistantExecutionTargetInternal( + AssistantExecutionTarget.agent, + sessionKey: sessionKey, + persistDefaultSelection: true, + ); await flushAssistantThreadPersistenceInternal(); recomputeTasksInternal(); notifyIfActiveInternal(); diff --git a/lib/app/ui_feature_manifest_core.dart b/lib/app/ui_feature_manifest_core.dart index e56f2e0c..2d19c058 100644 --- a/lib/app/ui_feature_manifest_core.dart +++ b/lib/app/ui_feature_manifest_core.dart @@ -520,16 +520,19 @@ class UiFeatureAccess { } List get availableExecutionTargets { - if (supportsRelayGateway) { - return const [AssistantExecutionTarget.gateway]; - } - return const [AssistantExecutionTarget.gateway]; + return const [ + AssistantExecutionTarget.agent, + AssistantExecutionTarget.gateway, + ]; } AssistantExecutionTarget sanitizeExecutionTarget( AssistantExecutionTarget? target, ) { - return AssistantExecutionTarget.gateway; + final resolved = target ?? AssistantExecutionTarget.agent; + return availableExecutionTargets.contains(resolved) + ? resolved + : AssistantExecutionTarget.agent; } } diff --git a/lib/features/assistant/assistant_page_components.dart b/lib/features/assistant/assistant_page_components.dart index ae3b9933..373c5872 100644 --- a/lib/features/assistant/assistant_page_components.dart +++ b/lib/features/assistant/assistant_page_components.dart @@ -82,7 +82,7 @@ class AssistantTaskRailStateInternal extends State { final groupedTasks = groupTasksForRailInternal( tasks, widget.controller.visibleAssistantExecutionTargets( - const [AssistantExecutionTarget.gateway], + AssistantExecutionTarget.values, ), ); final runningCount = tasks @@ -578,10 +578,7 @@ class AssistantEmptyStateInternal extends StatelessWidget { ? appText('开始输入', 'Start typing') : reconnectAvailable ? appText('重新连接 Bridge', 'Reconnect bridge') - : appText( - '连接 Bridge', - 'Connect xworkmate-bridge', - ), + : appText('连接 Bridge', 'Connect xworkmate-bridge'), ), style: FilledButton.styleFrom( minimumSize: const Size(0, 28), diff --git a/lib/features/assistant/assistant_page_composer_bar.dart b/lib/features/assistant/assistant_page_composer_bar.dart index 7cc7a986..75eabb15 100644 --- a/lib/features/assistant/assistant_page_composer_bar.dart +++ b/lib/features/assistant/assistant_page_composer_bar.dart @@ -198,7 +198,7 @@ class ComposerBarStateInternal extends State { } void handleControllerChangedInternal() { - if (!mounted || !skillPickerPortalControllerInternal.isShowing) { + if (!mounted) { return; } setState(() {}); @@ -379,13 +379,18 @@ class ComposerBarStateInternal extends State { PopupMenuButton( key: const Key('assistant-execution-target-button'), tooltip: appText('任务对话模式', 'Task Dialog Mode'), - onSelected: (value) { + onSelected: (value) async { final resolvedTarget = - resolveGatewayExecutionTargetFromVisibleTargets( + resolveAssistantExecutionTargetFromVisibleTargets( visibleExecutionTargets, - currentTarget: executionTarget, + currentTarget: value, ); - controller.setAssistantExecutionTarget(resolvedTarget); + await controller.setAssistantExecutionTarget( + resolvedTarget, + ); + if (mounted) { + setState(() {}); + } }, itemBuilder: (context) => compactExecutionTargets .map( @@ -420,7 +425,7 @@ class ComposerBarStateInternal extends State { ), const SizedBox(width: 4), ], - if (availableProviders.isNotEmpty) ...[ + if (executionTarget.isAgent && availableProviders.isNotEmpty) ...[ PopupMenuButton( key: const Key('assistant-provider-button'), tooltip: appText('智能体 Provider', 'Agent Provider'), diff --git a/lib/features/assistant/assistant_page_composer_support.dart b/lib/features/assistant/assistant_page_composer_support.dart index 1abe5551..429ab142 100644 --- a/lib/features/assistant/assistant_page_composer_support.dart +++ b/lib/features/assistant/assistant_page_composer_support.dart @@ -195,6 +195,7 @@ class ComposerToolbarChipStateInternal extension AssistantExecutionTargetIconInternal on AssistantExecutionTarget { IconData get icon => switch (this) { + AssistantExecutionTarget.agent => Icons.cloud_outlined, AssistantExecutionTarget.gateway => Icons.cloud_outlined, }; } diff --git a/lib/features/assistant/assistant_page_state_actions.dart b/lib/features/assistant/assistant_page_state_actions.dart index c5f5d018..dcdf862f 100644 --- a/lib/features/assistant/assistant_page_state_actions.dart +++ b/lib/features/assistant/assistant_page_state_actions.dart @@ -120,9 +120,7 @@ extension AssistantPageStateActionsInternal on AssistantPageStateInternal { taskSeedsInternal[controller.currentSessionKey]?.title ?? fallbackSessionTitleInternal(controller.currentSessionKey), preview: rawPrompt, - status: - controller.hasAssistantPendingRun || - connectionState.connected + status: controller.hasAssistantPendingRun || connectionState.connected ? 'running' : 'queued', owner: autoAgent?.name ?? conversationOwnerLabelInternal(controller), @@ -408,7 +406,7 @@ extension AssistantPageStateActionsInternal on AssistantPageStateInternal { final inheritedTarget = pickDraftThreadExecutionTargetInternal( currentTarget: widget.controller.currentAssistantExecutionTarget, visibleTargets: widget.controller.visibleAssistantExecutionTargets( - const [AssistantExecutionTarget.gateway], + AssistantExecutionTarget.values, ), localWorkspaceAvailable: widget.controller.settings.workspacePath .trim() @@ -515,9 +513,7 @@ extension AssistantPageStateActionsInternal on AssistantPageStateInternal { surface: 'Assistant', executionTarget: resolvedVisibleExecutionTargetInternal( widget.controller, - supportedTargets: const [ - AssistantExecutionTarget.gateway, - ], + supportedTargets: AssistantExecutionTarget.values, ), isCurrent: true, draft: true, diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index 0b6565d3..4edcde17 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -228,8 +228,7 @@ class GoTaskServiceRequest { multiAgent || collaborationMode == GoTaskServiceCollaborationMode.multiAgent; - AssistantExecutionTarget get normalizedTarget => - target.isGateway ? AssistantExecutionTarget.gateway : target; + AssistantExecutionTarget get normalizedTarget => target; GoTaskServiceRoute get route { if (isMultiAgentRequest) { @@ -243,7 +242,7 @@ class GoTaskServiceRequest { } String get routingExecutionTarget { - return 'gateway'; + return normalizedTarget.promptValue; } bool get hasInlineAttachments => inlineAttachments.isNotEmpty; @@ -307,9 +306,11 @@ class GoTaskServiceRequest { ExternalCodeAgentAcpRoutingConfig _synthesizedRouting() { final gatewayTarget = normalizedTarget; final preferredGatewayTarget = switch (gatewayTarget) { + AssistantExecutionTarget.agent => kCanonicalGatewayProviderId, AssistantExecutionTarget.gateway => kCanonicalGatewayProviderId, }; final explicitExecutionTarget = switch (gatewayTarget) { + AssistantExecutionTarget.agent => 'agent', AssistantExecutionTarget.gateway => 'gateway', }; final explicitProviderId = provider.isUnspecified diff --git a/lib/runtime/runtime_models_connection.dart b/lib/runtime/runtime_models_connection.dart index c1fa8b95..12938a82 100644 --- a/lib/runtime/runtime_models_connection.dart +++ b/lib/runtime/runtime_models_connection.dart @@ -42,55 +42,63 @@ bool isLegacyAutoAssistantExecutionTargetValue(String? value) { return value?.trim().toLowerCase() == 'auto'; } -enum AssistantExecutionTarget { gateway } +enum AssistantExecutionTarget { agent, gateway } extension AssistantExecutionTargetCopy on AssistantExecutionTarget { String get label => switch (this) { + AssistantExecutionTarget.agent => appText('智能体', 'Agent'), AssistantExecutionTarget.gateway => appText('Gateway', 'Gateway'), }; String get promptValue => switch (this) { + AssistantExecutionTarget.agent => 'agent', AssistantExecutionTarget.gateway => 'gateway', }; + bool get isAgent => this == AssistantExecutionTarget.agent; bool get isGateway => this == AssistantExecutionTarget.gateway; String get compactLabel => switch (this) { + AssistantExecutionTarget.agent => appText('智能体', 'Agent'), AssistantExecutionTarget.gateway => appText('Gateway', 'Gateway'), }; static AssistantExecutionTarget fromJsonValue(String? value) { - return AssistantExecutionTarget.gateway; + return AssistantExecutionTarget.values.firstWhere( + (item) => item.name == value?.trim() || item.promptValue == value?.trim(), + orElse: () => AssistantExecutionTarget.agent, + ); } } List compactAssistantExecutionTargets( Iterable targets, ) { - if (targets.contains(AssistantExecutionTarget.gateway)) { - return const [AssistantExecutionTarget.gateway]; + final ordered = []; + for (final candidate in AssistantExecutionTarget.values) { + if (targets.contains(candidate)) { + ordered.add(candidate); + } } - return const [AssistantExecutionTarget.gateway]; + return ordered.isEmpty ? AssistantExecutionTarget.values : ordered; } AssistantExecutionTarget collapseAssistantExecutionTargetForDisplay( AssistantExecutionTarget target, ) => target; -AssistantExecutionTarget resolveGatewayExecutionTargetFromVisibleTargets( +AssistantExecutionTarget resolveAssistantExecutionTargetFromVisibleTargets( Iterable visibleTargets, { AssistantExecutionTarget? currentTarget, }) { final visible = visibleTargets.toList(growable: false); - if (currentTarget != null && currentTarget.isGateway) { - if (visible.contains(AssistantExecutionTarget.gateway)) { - return AssistantExecutionTarget.gateway; - } + if (currentTarget != null && visible.contains(currentTarget)) { + return currentTarget; } - if (visible.contains(AssistantExecutionTarget.gateway)) { - return AssistantExecutionTarget.gateway; + if (visible.isNotEmpty) { + return visible.first; } - return AssistantExecutionTarget.gateway; + return AssistantExecutionTarget.agent; } String normalizeSingleAgentProviderId(String value) { @@ -211,6 +219,12 @@ class SingleAgentProvider { badge: 'G', ); + static const SingleAgentProvider openclaw = SingleAgentProvider( + providerId: kCanonicalGatewayProviderId, + label: kCanonicalGatewayProviderLabel, + badge: 'OC', + ); + final String providerId; final String label; final String badge; @@ -257,6 +271,7 @@ class SingleAgentProvider { 'opencode' => opencode, 'claude' => claude, 'gemini' => gemini, + kCanonicalGatewayProviderId => openclaw, 'auto' || '' => unspecified, _ => SingleAgentProvider( providerId: normalized, diff --git a/lib/runtime/runtime_models_runtime_payloads.dart b/lib/runtime/runtime_models_runtime_payloads.dart index 6f4a4716..2b3fb3e6 100644 --- a/lib/runtime/runtime_models_runtime_payloads.dart +++ b/lib/runtime/runtime_models_runtime_payloads.dart @@ -513,11 +513,14 @@ bool isLegacyAutoThreadExecutionModeValue(String? value) { return value?.trim().toLowerCase() == 'auto'; } -enum ThreadExecutionMode { gateway } +enum ThreadExecutionMode { agent, gateway } extension ThreadExecutionModeCopy on ThreadExecutionMode { static ThreadExecutionMode fromJsonValue(String? value) { - return ThreadExecutionMode.gateway; + return ThreadExecutionMode.values.firstWhere( + (item) => item.name == value?.trim(), + orElse: () => ThreadExecutionMode.gateway, + ); } } @@ -711,13 +714,19 @@ class ExecutionBinding { ThreadExecutionMode threadExecutionModeFromAssistantExecutionTarget( AssistantExecutionTarget target, ) { - return ThreadExecutionMode.gateway; + return switch (target) { + AssistantExecutionTarget.agent => ThreadExecutionMode.agent, + AssistantExecutionTarget.gateway => ThreadExecutionMode.gateway, + }; } AssistantExecutionTarget assistantExecutionTargetFromExecutionMode( ThreadExecutionMode mode, ) { - return AssistantExecutionTarget.gateway; + return switch (mode) { + ThreadExecutionMode.agent => AssistantExecutionTarget.agent, + ThreadExecutionMode.gateway => AssistantExecutionTarget.gateway, + }; } WorkspaceRefKind workspaceRefKindFromWorkspaceKind(WorkspaceKind kind) { diff --git a/lib/runtime/runtime_models_settings_snapshot.dart b/lib/runtime/runtime_models_settings_snapshot.dart index 38295978..9c92a8eb 100644 --- a/lib/runtime/runtime_models_settings_snapshot.dart +++ b/lib/runtime/runtime_models_settings_snapshot.dart @@ -111,7 +111,7 @@ class SettingsSnapshot { accountLocalMode: true, acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults(), linuxDesktop: LinuxDesktopConfig.defaults(), - assistantExecutionTarget: AssistantExecutionTarget.gateway, + assistantExecutionTarget: AssistantExecutionTarget.agent, assistantPermissionLevel: AssistantPermissionLevel.defaultAccess, ); } diff --git a/lib/widgets/sidebar_navigation_task_section.dart b/lib/widgets/sidebar_navigation_task_section.dart index d83aea69..0e48d333 100644 --- a/lib/widgets/sidebar_navigation_task_section.dart +++ b/lib/widgets/sidebar_navigation_task_section.dart @@ -646,6 +646,7 @@ String _sidebarTaskUpdatedAtLabel(double? updatedAtMs) { IconData _sidebarTaskTargetIcon(AssistantExecutionTarget target) { return switch (target) { + AssistantExecutionTarget.agent => Icons.hub_rounded, AssistantExecutionTarget.gateway => Icons.cloud_outlined, }; } diff --git a/test/features/assistant/assistant_lower_pane_test.dart b/test/features/assistant/assistant_lower_pane_test.dart index cf45b6c0..3cca7824 100644 --- a/test/features/assistant/assistant_lower_pane_test.dart +++ b/test/features/assistant/assistant_lower_pane_test.dart @@ -9,6 +9,60 @@ import 'package:xworkmate/widgets/surface_card.dart'; void main() { group('AssistantLowerPaneInternal', () { + testWidgets('shows agent and gateway task dialog modes', (tester) async { + final controller = AppController(); + addTearDown(controller.dispose); + + await controller.sessionsController.switchSession('session-1'); + + await tester.pumpWidget( + _buildTestApp(child: _buildLowerPane(controller: controller)), + ); + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('assistant-provider-button')), + findsOneWidget, + ); + + await tester.tap( + find.byKey(const Key('assistant-execution-target-button')), + ); + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('assistant-execution-target-menu-item-agent')), + findsOneWidget, + ); + expect( + find.byKey(const Key('assistant-execution-target-menu-item-gateway')), + findsOneWidget, + ); + + await tester.tap( + find.byKey(const Key('assistant-execution-target-menu-item-gateway')), + ); + await tester.pumpAndSettle(); + + expect(controller.assistantExecutionTarget.name, 'gateway'); + expect(find.byKey(const Key('assistant-provider-button')), findsNothing); + + await tester.tap( + find.byKey(const Key('assistant-execution-target-button')), + ); + await tester.pumpAndSettle(); + await tester.tap( + find.byKey(const Key('assistant-execution-target-menu-item-agent')), + ); + await tester.pumpAndSettle(); + + expect(controller.assistantExecutionTarget.name, 'agent'); + expect( + find.byKey(const Key('assistant-provider-button')), + findsOneWidget, + ); + }); + testWidgets('shows assistant providers and allows switching provider', ( tester, ) async { @@ -18,9 +72,7 @@ void main() { await controller.sessionsController.switchSession('session-1'); await tester.pumpWidget( - _buildTestApp( - child: _buildLowerPane(controller: controller), - ), + _buildTestApp(child: _buildLowerPane(controller: controller)), ); await tester.pumpAndSettle(); @@ -46,7 +98,8 @@ void main() { await tester.pumpAndSettle(); expect( - controller.assistantProviderForSession(controller.currentSessionKey) + controller + .assistantProviderForSession(controller.currentSessionKey) .providerId, 'opencode', ); @@ -88,9 +141,7 @@ Widget _buildTestApp({required Widget child}) { return MaterialApp( theme: AppTheme.light(), home: Material( - child: Center( - child: SizedBox(width: 1400, height: 360, child: child), - ), + child: Center(child: SizedBox(width: 1400, height: 360, child: child)), ), ); } diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart new file mode 100644 index 00000000..6598ffc6 --- /dev/null +++ b/test/runtime/assistant_execution_target_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/runtime/runtime_models.dart'; + +void main() { + group('AssistantExecutionTarget', () { + test('maps agent and gateway values without collapsing them', () { + expect( + threadExecutionModeFromAssistantExecutionTarget( + AssistantExecutionTarget.agent, + ), + ThreadExecutionMode.agent, + ); + expect( + threadExecutionModeFromAssistantExecutionTarget( + AssistantExecutionTarget.gateway, + ), + ThreadExecutionMode.gateway, + ); + expect( + assistantExecutionTargetFromExecutionMode(ThreadExecutionMode.agent), + AssistantExecutionTarget.agent, + ); + expect( + assistantExecutionTargetFromExecutionMode(ThreadExecutionMode.gateway), + AssistantExecutionTarget.gateway, + ); + }); + + test('keeps both task dialog modes visible when both are supported', () { + expect( + compactAssistantExecutionTargets(const [ + AssistantExecutionTarget.agent, + AssistantExecutionTarget.gateway, + ]), + const [ + AssistantExecutionTarget.agent, + AssistantExecutionTarget.gateway, + ], + ); + }); + + test('recognizes openclaw as the canonical gateway provider', () { + final provider = SingleAgentProvider.fromJsonValue('openclaw'); + + expect(provider.providerId, kCanonicalGatewayProviderId); + expect(provider.label, kCanonicalGatewayProviderLabel); + }); + }); +}