refactor: remove legacy single-agent provider selection flow

This commit is contained in:
Haitao Pan 2026-04-14 12:43:05 +08:00
parent d4e09d7113
commit 51c90e9613
6 changed files with 139 additions and 55 deletions

View File

@ -572,16 +572,19 @@ class AppController extends ChangeNotifier {
List<AssistantExecutionTarget> get bridgeAvailableExecutionTargets =>
compactAssistantExecutionTargets(bridgeAvailableExecutionTargetsInternal);
List<SingleAgentProvider> get assistantProviderCatalogForDisplay {
return assistantProviderCatalog;
}
List<SingleAgentProvider> 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,

View File

@ -227,9 +227,9 @@ extension AppControllerDesktopSkillPermissions on AppController {
List<AssistantThreadSkillEntry>? importedSkills,
List<String>? 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 =

View File

@ -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<void> setAssistantSingleAgentProvider(
Future<void> 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(),

View File

@ -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<SingleAgentProvider> _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<void> _handleProviderSelected(SingleAgentProvider provider) async {
if (executionTarget.isGateway || providers.isEmpty) {
if (providers.isEmpty) {
return;
}
await controller.setAssistantSingleAgentProvider(provider);
await controller.setAssistantProvider(provider);
}
}

View File

@ -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),
),
),
};

View File

@ -253,6 +253,101 @@ void main() {
);
});
testWidgets('allows switching gateway providers from the dynamic catalog', (
tester,
) async {
final controller = AppController(
initialBridgeProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.codex,
SingleAgentProvider.opencode,
SingleAgentProvider.gemini,
],
initialGatewayProviderCatalog: <SingleAgentProvider>[
SingleAgentProvider.openclaw.copyWith(
logoEmoji: '🦞',
supportedTargets: const <AssistantExecutionTarget>[
AssistantExecutionTarget.gateway,
],
),
SingleAgentProvider.fromJsonValue(
'hermes',
label: 'Hermes',
badge: 'H',
supportedTargets: const <AssistantExecutionTarget>[
AssistantExecutionTarget.gateway,
],
),
],
initialAvailableExecutionTargets: const <AssistantExecutionTarget>[
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);