From 51c90e961313ca7cab2e81c2083bf9e9808f84e4 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Tue, 14 Apr 2026 12:43:05 +0800 Subject: [PATCH] refactor: remove legacy single-agent provider selection flow --- lib/app/app_controller_desktop_core.dart | 22 ++--- ..._controller_desktop_skill_permissions.dart | 10 +- ...ontroller_desktop_workspace_execution.dart | 33 +++---- .../assistant_page_task_dialog_controls.dart | 20 ++-- lib/runtime/runtime_models_connection.dart | 14 +-- .../assistant/assistant_lower_pane_test.dart | 95 +++++++++++++++++++ 6 files changed, 139 insertions(+), 55 deletions(-) diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index 172df643..7218be00 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -572,16 +572,19 @@ class AppController extends ChangeNotifier { List get bridgeAvailableExecutionTargets => compactAssistantExecutionTargets(bridgeAvailableExecutionTargetsInternal); - List get assistantProviderCatalogForDisplay { - return assistantProviderCatalog; - } - List providerCatalogForExecutionTarget( AssistantExecutionTarget executionTarget, ) { - return executionTarget.isGateway + final source = executionTarget.isGateway ? gatewayProviderCatalog - : assistantProviderCatalogForDisplay; + : assistantProviderCatalog; + return source + .where( + (provider) => + provider.supportedTargets.isEmpty || + provider.supportedTargets.contains(executionTarget), + ) + .toList(growable: false); } SingleAgentProvider? bridgeProviderForId(String providerId) { @@ -625,13 +628,6 @@ class AppController extends ChangeNotifier { return SingleAgentProvider.unspecified; } - SingleAgentProvider resolveAssistantProvider(String? providerId) { - return resolveProviderForExecutionTarget( - providerId, - executionTarget: AssistantExecutionTarget.agent, - ); - } - SingleAgentProvider assistantProviderForSession(String sessionKey) { final normalizedSessionKey = normalizedAssistantSessionKeyInternal( sessionKey, diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart index c7dc4e3c..01783877 100644 --- a/lib/app/app_controller_desktop_skill_permissions.dart +++ b/lib/app/app_controller_desktop_skill_permissions.dart @@ -227,9 +227,9 @@ extension AppControllerDesktopSkillPermissions on AppController { List? importedSkills, List? selectedSkillKeys, String? assistantModelId, - SingleAgentProvider? singleAgentProvider, + SingleAgentProvider? selectedProvider, ThreadSelectionSource? executionTargetSource, - ThreadSelectionSource? singleAgentProviderSource, + ThreadSelectionSource? selectedProviderSource, ThreadSelectionSource? assistantModelSource, ThreadSelectionSource? selectedSkillsSource, String? gatewayEntryState, @@ -291,8 +291,8 @@ extension AppControllerDesktopSkillPermissions on AppController { 'TaskThread $normalizedSessionKey is missing a complete workspaceBinding.', ); } - final requestedProvider = singleAgentProvider?.isUnspecified == false - ? singleAgentProvider + final requestedProvider = selectedProvider?.isUnspecified == false + ? selectedProvider : null; final nextProviderId = normalizeSingleAgentProviderId( requestedProvider?.providerId ?? @@ -305,7 +305,7 @@ extension AppControllerDesktopSkillPermissions on AppController { executionTarget: nextExecutionTarget, ); final nextProviderSource = - singleAgentProviderSource ?? + selectedProviderSource ?? existing?.executionBinding.providerSource ?? ThreadSelectionSource.inherited; final nextExecutionBinding = diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index ae4c2522..c8318469 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -99,13 +99,13 @@ extension AppControllerDesktopWorkspaceExecution on AppController { sessionsControllerInternal.currentSessionKey, executionTarget: resolvedTarget, executionTargetSource: ThreadSelectionSource.explicit, - singleAgentProvider: resolveProviderForExecutionTarget( + selectedProvider: resolveProviderForExecutionTarget( taskThreadForSessionInternal( sessionsControllerInternal.currentSessionKey, )?.executionBinding.providerId, executionTarget: resolvedTarget, ), - singleAgentProviderSource: ThreadSelectionSource.explicit, + selectedProviderSource: ThreadSelectionSource.explicit, gatewayEntryState: gatewayEntryStateForTargetInternal(resolvedTarget), latestResolvedRuntimeModel: '', latestResolvedProviderId: '', @@ -125,10 +125,16 @@ extension AppControllerDesktopWorkspaceExecution on AppController { notifyIfActiveInternal(); } - Future setAssistantSingleAgentProvider( + Future setAssistantProvider( SingleAgentProvider provider, ) async { - final resolvedProvider = resolveAssistantProvider(provider.providerId); + final executionTarget = assistantExecutionTargetForSession( + sessionsControllerInternal.currentSessionKey, + ); + final resolvedProvider = resolveProviderForExecutionTarget( + provider.providerId, + executionTarget: executionTarget, + ); final sessionKey = normalizedAssistantSessionKeyInternal( sessionsControllerInternal.currentSessionKey, ); @@ -146,27 +152,22 @@ extension AppControllerDesktopWorkspaceExecution on AppController { if (!assistantThreadRecordsInternal.containsKey(sessionKey)) { initializeAssistantThreadContext( sessionKey, - executionTarget: AssistantExecutionTarget.agent, + executionTarget: executionTarget, messageViewMode: assistantMessageViewModeForSession(sessionKey), ); } upsertTaskThreadInternal( sessionKey, - executionTarget: AssistantExecutionTarget.agent, + executionTarget: executionTarget, executionTargetSource: ThreadSelectionSource.explicit, - singleAgentProvider: resolvedProvider, - singleAgentProviderSource: ThreadSelectionSource.explicit, + selectedProvider: resolvedProvider, + selectedProviderSource: ThreadSelectionSource.explicit, gatewayEntryState: gatewayEntryStateForTargetInternal( - AssistantExecutionTarget.agent, + executionTarget, ), latestResolvedProviderId: '', updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), ); - await applyAssistantExecutionTargetInternal( - AssistantExecutionTarget.agent, - sessionKey: sessionKey, - persistDefaultSelection: true, - ); await flushAssistantThreadPersistenceInternal(); recomputeTasksInternal(); notifyIfActiveInternal(); @@ -222,13 +223,13 @@ extension AppControllerDesktopWorkspaceExecution on AppController { ); upsertTaskThreadInternal( normalizedSessionKey, - singleAgentProvider: resolveProviderForExecutionTarget( + selectedProvider: resolveProviderForExecutionTarget( taskThreadForSessionInternal(normalizedSessionKey) ?.executionBinding .providerId, executionTarget: resolvedTarget, ), - singleAgentProviderSource: ThreadSelectionSource.explicit, + selectedProviderSource: ThreadSelectionSource.explicit, latestResolvedRuntimeModel: '', latestResolvedProviderId: '', updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), diff --git a/lib/features/assistant/assistant_page_task_dialog_controls.dart b/lib/features/assistant/assistant_page_task_dialog_controls.dart index 31861713..1fbf2f42 100644 --- a/lib/features/assistant/assistant_page_task_dialog_controls.dart +++ b/lib/features/assistant/assistant_page_task_dialog_controls.dart @@ -44,9 +44,8 @@ class AssistantTaskDialogModeControlsInternal extends StatelessWidget { final executionTarget = collapseAssistantExecutionTargetForDisplay( currentExecutionTarget, ); - final providerMenuProviders = _taskDialogProviderCatalogForTarget( - controller: controller, - executionTarget: executionTarget, + final providerMenuProviders = controller.providerCatalogForExecutionTarget( + executionTarget, ); return Wrap( @@ -73,13 +72,6 @@ class AssistantTaskDialogModeControlsInternal extends StatelessWidget { } } -List _taskDialogProviderCatalogForTarget({ - required AppController controller, - required AssistantExecutionTarget executionTarget, -}) { - return controller.providerCatalogForExecutionTarget(executionTarget); -} - class _TaskDialogExecutionTargetMenuButtonInternal extends StatelessWidget { const _TaskDialogExecutionTargetMenuButtonInternal({ required this.controller, @@ -160,7 +152,7 @@ class _TaskDialogProviderMenuButtonInternal extends StatelessWidget { @override Widget build(BuildContext context) { final displayProvider = selectedProvider.isUnspecified - ? _fallbackDisplayProvider(context) + ? _fallbackDisplayProvider() : selectedProvider; final isEnabled = providers.isNotEmpty; @@ -204,7 +196,7 @@ class _TaskDialogProviderMenuButtonInternal extends StatelessWidget { ); } - SingleAgentProvider _fallbackDisplayProvider(BuildContext context) { + SingleAgentProvider _fallbackDisplayProvider() { if (providers.isNotEmpty) { return providers.first; } @@ -218,10 +210,10 @@ class _TaskDialogProviderMenuButtonInternal extends StatelessWidget { } Future _handleProviderSelected(SingleAgentProvider provider) async { - if (executionTarget.isGateway || providers.isEmpty) { + if (providers.isEmpty) { return; } - await controller.setAssistantSingleAgentProvider(provider); + await controller.setAssistantProvider(provider); } } diff --git a/lib/runtime/runtime_models_connection.dart b/lib/runtime/runtime_models_connection.dart index 11617e5f..d290fd25 100644 --- a/lib/runtime/runtime_models_connection.dart +++ b/lib/runtime/runtime_models_connection.dart @@ -128,7 +128,7 @@ String normalizeSingleAgentProviderId(String value) { return buffer.toString().replaceAll(RegExp(r'^[-_.]+|[-_.]+$'), ''); } -String singleAgentProviderFallbackLabelInternal(String providerId) { +String providerFallbackLabelInternal(String providerId) { final normalized = normalizeSingleAgentProviderId(providerId); if (normalized.isEmpty) { return appText('Bridge Provider', 'Bridge Provider'); @@ -140,7 +140,7 @@ String singleAgentProviderFallbackLabelInternal(String providerId) { .join(' '); } -String singleAgentProviderFallbackBadgeInternal({ +String providerFallbackBadgeInternal({ required String providerId, required String label, }) { @@ -259,10 +259,10 @@ class SingleAgentProvider { return SingleAgentProvider( providerId: resolvedProviderId, label: resolvedLabel.isEmpty - ? singleAgentProviderFallbackLabelInternal(resolvedProviderId) + ? providerFallbackLabelInternal(resolvedProviderId) : resolvedLabel, badge: resolvedBadge.isEmpty - ? singleAgentProviderFallbackBadgeInternal( + ? providerFallbackBadgeInternal( providerId: resolvedProviderId, label: resolvedLabel, ) @@ -297,10 +297,10 @@ class SingleAgentProvider { 'auto' || '' => unspecified, _ => SingleAgentProvider( providerId: normalized, - label: singleAgentProviderFallbackLabelInternal(normalized), - badge: singleAgentProviderFallbackBadgeInternal( + label: providerFallbackLabelInternal(normalized), + badge: providerFallbackBadgeInternal( providerId: normalized, - label: singleAgentProviderFallbackLabelInternal(normalized), + label: providerFallbackLabelInternal(normalized), ), ), }; diff --git a/test/features/assistant/assistant_lower_pane_test.dart b/test/features/assistant/assistant_lower_pane_test.dart index cbb48ea9..d3a6a0d5 100644 --- a/test/features/assistant/assistant_lower_pane_test.dart +++ b/test/features/assistant/assistant_lower_pane_test.dart @@ -253,6 +253,101 @@ void main() { ); }); + testWidgets('allows switching gateway providers from the dynamic catalog', ( + tester, + ) async { + final controller = AppController( + initialBridgeProviderCatalog: const [ + SingleAgentProvider.codex, + SingleAgentProvider.opencode, + SingleAgentProvider.gemini, + ], + initialGatewayProviderCatalog: [ + SingleAgentProvider.openclaw.copyWith( + logoEmoji: '🦞', + supportedTargets: const [ + AssistantExecutionTarget.gateway, + ], + ), + SingleAgentProvider.fromJsonValue( + 'hermes', + label: 'Hermes', + badge: 'H', + supportedTargets: const [ + AssistantExecutionTarget.gateway, + ], + ), + ], + initialAvailableExecutionTargets: const [ + AssistantExecutionTarget.agent, + AssistantExecutionTarget.gateway, + ], + ); + addTearDown(controller.dispose); + + await controller.sessionsController.switchSession('session-1'); + controller.initializeAssistantThreadContext( + 'session-1', + executionTarget: AssistantExecutionTarget.gateway, + messageViewMode: controller.assistantMessageViewModeForSession( + 'session-1', + ), + ); + final gatewayThread = controller + .requireTaskThreadForSessionInternal('session-1') + .copyWith( + executionBinding: ExecutionBinding( + executionMode: threadExecutionModeFromAssistantExecutionTarget( + AssistantExecutionTarget.gateway, + ), + executorId: SingleAgentProvider.openclaw.providerId, + providerId: SingleAgentProvider.openclaw.providerId, + endpointId: '', + executionModeSource: ThreadSelectionSource.explicit, + providerSource: ThreadSelectionSource.explicit, + ), + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); + controller.taskThreadRepositoryInternal.replace( + gatewayThread, + persist: false, + ); + controller.notifyListeners(); + + await tester.pumpWidget( + _buildTestApp(child: _buildLowerPane(controller: controller)), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('assistant-provider-button'))); + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('assistant-provider-menu-item-openclaw')), + findsOneWidget, + ); + expect( + find.byKey(const Key('assistant-provider-menu-item-hermes')), + findsOneWidget, + ); + expect( + find.byKey(const Key('assistant-provider-menu-item-codex')), + findsNothing, + ); + + await tester.tap( + find.byKey(const Key('assistant-provider-menu-item-hermes')), + ); + await tester.pumpAndSettle(); + + expect( + controller + .assistantProviderForSession(controller.currentSessionKey) + .providerId, + 'hermes', + ); + }); + testWidgets('uses submit button instead of connect action', (tester) async { final controller = AppController(); addTearDown(controller.dispose);