diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index 31ec2e02..58827cd2 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -588,11 +588,7 @@ class AppController extends ChangeNotifier { ); List get configuredSingleAgentProviders => - normalizeSingleAgentProviderList( - bridgeAdvertisedProvidersInternal.where( - (item) => item != SingleAgentProvider.auto, - ), - ); + normalizeSingleAgentProviderList(bridgeAdvertisedProvidersInternal); List get availableSingleAgentProviders => availableSingleAgentProvidersOverrideInternal != null @@ -638,11 +634,10 @@ class AppController extends ChangeNotifier { bool canUseSingleAgentProviderInternal(SingleAgentProvider provider) { final override = availableSingleAgentProvidersOverrideInternal; if (override != null) { - return provider != SingleAgentProvider.auto && - override.contains(provider); + return !provider.isUnspecified && override.contains(provider); } - if (provider == SingleAgentProvider.auto) { - return hasAnyAvailableSingleAgentProvider; + if (provider.isUnspecified) { + return false; } final capabilities = singleAgentCapabilitiesByProviderInternal[provider]; return capabilities?.available == true && @@ -652,7 +647,7 @@ class AppController extends ChangeNotifier { SingleAgentProvider? resolvedSingleAgentProviderInternal( SingleAgentProvider selection, ) { - if (selection == SingleAgentProvider.auto) { + if (selection.isUnspecified) { return null; } return canUseSingleAgentProviderInternal(selection) ? selection : null; diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index c3c0e33e..21a1c96b 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -235,7 +235,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { ); final provider = resolvedSingleAgentProviderInternal(selection) ?? selection; - final providerLabel = provider == SingleAgentProvider.auto + final providerLabel = provider.isUnspecified ? appText('Bridge Provider', 'Bridge Provider') : provider.label; final address = _extractGatewayAddressFromErrorInternal(raw); diff --git a/lib/app/app_controller_desktop_settings.dart b/lib/app/app_controller_desktop_settings.dart index 912f7cf7..92118e80 100644 --- a/lib/app/app_controller_desktop_settings.dart +++ b/lib/app/app_controller_desktop_settings.dart @@ -311,7 +311,7 @@ extension AppControllerDesktopSettings on AppController { currentSettings.assistantExecutionTarget, ), messageViewMode: AssistantMessageViewMode.rendered, - singleAgentProvider: SingleAgentProvider.auto, + singleAgentProvider: SingleAgentProvider.unspecified, ); await setCurrentAssistantSessionKeyInternal( 'main', diff --git a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart index 369717cd..929b54af 100644 --- a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart +++ b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart @@ -98,7 +98,7 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( effectiveProvider == null) ? (routingResolution.unavailableMessage.isNotEmpty ? routingResolution.unavailableMessage - : selection == SingleAgentProvider.auto + : selection.isUnspecified ? appText( '当前没有可用的 GoTaskService Provider。', 'No GoTaskService provider is currently available.', @@ -177,7 +177,7 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( routingResolution, ), routingHint: 'single-agent', - provider: effectiveProvider ?? SingleAgentProvider.auto, + provider: effectiveProvider ?? SingleAgentProvider.unspecified, remoteWorkingDirectoryHint: controller .requireTaskThreadForSessionInternal(sessionKey) diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart index 9672ff15..9b183c6a 100644 --- a/lib/app/app_controller_desktop_skill_permissions.dart +++ b/lib/app/app_controller_desktop_skill_permissions.dart @@ -306,7 +306,7 @@ extension AppControllerDesktopSkillPermissions on AppController { executionBinding?.providerId ?? existing?.executionBinding.providerId, )) - : SingleAgentProvider.auto; + : SingleAgentProvider.unspecified; final nextProviderSource = nextExecutionTarget == AssistantExecutionTarget.singleAgent ? (singleAgentProviderSource ?? diff --git a/lib/app/app_controller_desktop_thread_binding.dart b/lib/app/app_controller_desktop_thread_binding.dart index 671852c4..156b02eb 100644 --- a/lib/app/app_controller_desktop_thread_binding.dart +++ b/lib/app/app_controller_desktop_thread_binding.dart @@ -258,7 +258,7 @@ extension AppControllerDesktopThreadBinding on AppController { final sanitizedProvider = executionTarget == AssistantExecutionTarget.singleAgent ? settings.sanitizeSingleAgentProviderSelection(singleAgentProvider) - : SingleAgentProvider.auto; + : SingleAgentProvider.unspecified; return (existingBinding ?? ExecutionBinding( executionMode: ThreadExecutionMode.localAgent, diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index 659d8899..54c8461b 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -254,7 +254,12 @@ extension AppControllerDesktopThreadSessions on AppController { )?.executionBinding.providerId ?? '', ); - return settings.sanitizeSingleAgentProviderSelection(stored); + final sanitized = settings.sanitizeSingleAgentProviderSelection(stored); + if (!sanitized.isUnspecified) { + return sanitized; + } + final options = singleAgentProviderOptions; + return options.isEmpty ? SingleAgentProvider.unspecified : options.first; } SingleAgentProvider get currentSingleAgentProvider => @@ -304,7 +309,7 @@ extension AppControllerDesktopThreadSessions on AppController { return false; } final selection = singleAgentProviderForSession(normalizedSessionKey); - if (selection == SingleAgentProvider.auto) { + if (selection.isUnspecified) { return false; } return !canUseSingleAgentProviderInternal(selection) && @@ -385,7 +390,7 @@ extension AppControllerDesktopThreadSessions on AppController { return resolvedProvider.label; } final provider = currentSingleAgentProvider; - if (provider != SingleAgentProvider.auto) { + if (!provider.isUnspecified) { return provider.label; } return appText('单机智能体', 'Single Agent'); diff --git a/lib/app/app_controller_desktop_thread_storage.dart b/lib/app/app_controller_desktop_thread_storage.dart index 0b1e3035..db2575a8 100644 --- a/lib/app/app_controller_desktop_thread_storage.dart +++ b/lib/app/app_controller_desktop_thread_storage.dart @@ -693,7 +693,7 @@ extension AppControllerDesktopThreadStorage on AppController { record.executionBinding.providerId, ), ) - : SingleAgentProvider.auto; + : SingleAgentProvider.unspecified; final workspaceBinding = record.workspaceBinding.copyWith( workspaceId: sessionKey, displayPath: record.workspaceKind == WorkspaceKind.localFs diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index 06ba4ddd..c86efa47 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -361,7 +361,7 @@ extension AppControllerDesktopWorkspaceExecution on AppController { singleAgentProvider: singleAgentProvider ?? singleAgentProviderForSession(currentSessionKey), - singleAgentProviderSource: ThreadSelectionSource.explicit, + singleAgentProviderSource: ThreadSelectionSource.inherited, updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), ); // Re-read the current thread target when the async binding sync runs so a diff --git a/lib/features/assistant/assistant_page_composer_support.dart b/lib/features/assistant/assistant_page_composer_support.dart index 89db6c89..7b727538 100644 --- a/lib/features/assistant/assistant_page_composer_support.dart +++ b/lib/features/assistant/assistant_page_composer_support.dart @@ -219,24 +219,19 @@ class SingleAgentProviderBadgeInternal extends StatelessWidget { final candidate = provider.badge.trim().isEmpty ? provider.label : provider.badge; - final display = candidate.length <= 2 + final display = candidate.trim().isEmpty + ? '?' + : candidate.length <= 2 ? candidate : candidate.substring(0, 2); - final isAuto = provider == SingleAgentProvider.auto; return Container( width: 18, height: 18, alignment: Alignment.center, decoration: BoxDecoration( - color: isAuto - ? palette.accent.withValues(alpha: 0.16) - : palette.surfaceSecondary, + color: palette.surfaceSecondary, borderRadius: BorderRadius.circular(999), - border: Border.all( - color: isAuto - ? palette.accent.withValues(alpha: 0.4) - : palette.strokeSoft, - ), + border: Border.all(color: palette.strokeSoft), ), child: Text( display, diff --git a/lib/features/assistant/assistant_page_tooltip_labels.dart b/lib/features/assistant/assistant_page_tooltip_labels.dart index 4c300d60..d2649475 100644 --- a/lib/features/assistant/assistant_page_tooltip_labels.dart +++ b/lib/features/assistant/assistant_page_tooltip_labels.dart @@ -43,11 +43,12 @@ String executionTargetTooltipInternal(AssistantExecutionTarget target) => 'Task dialog mode: ${target.compactLabel}', ); -String singleAgentProviderTooltipInternal(SingleAgentProvider provider) => - appText( - 'Bridge Provider: ${provider.label}', - 'Bridge Provider: ${provider.label}', - ); +String singleAgentProviderTooltipInternal( + SingleAgentProvider provider, +) => appText( + 'Bridge Provider: ${provider.label.trim().isEmpty ? appText('未配置', 'Unconfigured') : provider.label}', + 'Bridge Provider: ${provider.label.trim().isEmpty ? appText('未配置', 'Unconfigured') : provider.label}', +); String modelTooltipInternal(String modelLabel) => appText('模型: $modelLabel', 'Model: $modelLabel'); diff --git a/lib/runtime/external_code_agent_acp_desktop_transport.dart b/lib/runtime/external_code_agent_acp_desktop_transport.dart index 5c746c82..0f13fed5 100644 --- a/lib/runtime/external_code_agent_acp_desktop_transport.dart +++ b/lib/runtime/external_code_agent_acp_desktop_transport.dart @@ -201,7 +201,7 @@ class ExternalCodeAgentAcpDesktopTransport providerId, label: label?.isNotEmpty == true ? label : null, ); - if (provider != SingleAgentProvider.auto) { + if (!provider.isUnspecified) { providers.add(provider); } } diff --git a/lib/runtime/gateway_acp_client.dart b/lib/runtime/gateway_acp_client.dart index d7792c1c..881f6da4 100644 --- a/lib/runtime/gateway_acp_client.dart +++ b/lib/runtime/gateway_acp_client.dart @@ -158,7 +158,7 @@ class GatewayAcpClient { providerId, label: label?.isNotEmpty == true ? label : null, ); - if (provider != SingleAgentProvider.auto) { + if (!provider.isUnspecified) { providers.add(provider); } } diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index a6cc23de..2210c6bb 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -219,7 +219,7 @@ class GoTaskServiceRequest { required this.metadata, this.routing, this.routingHint = '', - this.provider = SingleAgentProvider.auto, + this.provider = SingleAgentProvider.unspecified, this.remoteWorkingDirectoryHint = '', this.resumeSession = false, this.collaborationMode = GoTaskServiceCollaborationMode.standard, @@ -322,7 +322,7 @@ class GoTaskServiceRequest { }, ) .toList(growable: false), - if (provider != SingleAgentProvider.auto) 'provider': provider.providerId, + if (!provider.isUnspecified) 'provider': provider.providerId, if (remoteWorkingDirectoryHint.trim().isNotEmpty) 'remoteWorkingDirectoryHint': remoteWorkingDirectoryHint.trim(), if (model.trim().isNotEmpty) 'model': model.trim(), @@ -353,7 +353,7 @@ class GoTaskServiceRequest { AssistantExecutionTarget.remote => 'remote', AssistantExecutionTarget.singleAgent => 'singleAgent', }; - final explicitProviderId = provider == SingleAgentProvider.auto + final explicitProviderId = provider.isUnspecified ? '' : provider.providerId; final explicitModelValue = model.trim(); diff --git a/lib/runtime/runtime_models_connection.dart b/lib/runtime/runtime_models_connection.dart index f63321aa..e7cc58ac 100644 --- a/lib/runtime/runtime_models_connection.dart +++ b/lib/runtime/runtime_models_connection.dart @@ -167,7 +167,7 @@ String normalizeSingleAgentProviderId(String value) { String singleAgentProviderFallbackLabelInternal(String providerId) { final normalized = normalizeSingleAgentProviderId(providerId); if (normalized.isEmpty) { - return 'Custom Agent'; + return appText('Bridge Provider', 'Bridge Provider'); } return normalized .split(RegExp(r'[-_.]+')) @@ -182,7 +182,6 @@ String singleAgentProviderFallbackBadgeInternal({ }) { final normalized = normalizeSingleAgentProviderId(providerId); final known = { - 'auto': 'A', 'codex': 'C', 'opencode': 'O', 'claude': 'Cl', @@ -225,10 +224,10 @@ class SingleAgentProvider { this.source = SingleAgentProviderSource.externalExtension, }); - static const SingleAgentProvider auto = SingleAgentProvider( - providerId: 'auto', - label: 'Auto', - badge: 'A', + static const SingleAgentProvider unspecified = SingleAgentProvider( + providerId: '', + label: '', + badge: '', ); static const SingleAgentProvider codex = SingleAgentProvider( @@ -260,7 +259,7 @@ class SingleAgentProvider { final String badge; final SingleAgentProviderSource source; - bool get isAuto => providerId == auto.providerId; + bool get isUnspecified => providerId.trim().isEmpty; bool get isExternalExtension => source == SingleAgentProviderSource.externalExtension; @@ -301,7 +300,7 @@ class SingleAgentProvider { 'opencode' => opencode, 'claude' => claude, 'gemini' => gemini, - 'auto' || '' => auto, + 'auto' || '' => unspecified, _ => SingleAgentProvider( providerId: normalized, label: singleAgentProviderFallbackLabelInternal(normalized), diff --git a/lib/runtime/runtime_models_profiles.dart b/lib/runtime/runtime_models_profiles.dart index a526565f..a45e7726 100644 --- a/lib/runtime/runtime_models_profiles.dart +++ b/lib/runtime/runtime_models_profiles.dart @@ -100,7 +100,7 @@ class ExternalAcpEndpointProfile { json['providerKey']?.toString() ?? '', ); final builtin = SingleAgentProviderCopy.fromJsonValue(providerKey); - final fallbackLabel = builtin.isAuto ? providerKey : builtin.label; + final fallbackLabel = builtin.isUnspecified ? providerKey : builtin.label; final label = json['label']?.toString().trim().isNotEmpty == true ? json['label'].toString().trim() : fallbackLabel; @@ -132,9 +132,6 @@ List normalizeExternalAcpEndpoints({ ) { final key = profile.providerKey.trim().toLowerCase(); for (final provider in kKnownSingleAgentProviders) { - if (provider.isAuto) { - continue; - } if (provider.providerId == key) { return provider; } @@ -142,9 +139,6 @@ List normalizeExternalAcpEndpoints({ final label = profile.label.trim(); final badge = profile.badge.trim(); for (final provider in kKnownSingleAgentProviders) { - if (provider.isAuto) { - continue; - } if (provider.label == label && provider.badge == badge) { return provider; } diff --git a/lib/runtime/runtime_models_runtime_payloads.dart b/lib/runtime/runtime_models_runtime_payloads.dart index b79dc69a..6dc56fa5 100644 --- a/lib/runtime/runtime_models_runtime_payloads.dart +++ b/lib/runtime/runtime_models_runtime_payloads.dart @@ -1019,8 +1019,8 @@ class TaskThread { executionBinding ?? ExecutionBinding( executionMode: ThreadExecutionMode.localAgent, - executorId: SingleAgentProvider.auto.providerId, - providerId: SingleAgentProvider.auto.providerId, + executorId: SingleAgentProvider.unspecified.providerId, + providerId: SingleAgentProvider.unspecified.providerId, endpointId: '', ), contextState = diff --git a/lib/runtime/runtime_models_settings_snapshot.dart b/lib/runtime/runtime_models_settings_snapshot.dart index 952da557..42e5839c 100644 --- a/lib/runtime/runtime_models_settings_snapshot.dart +++ b/lib/runtime/runtime_models_settings_snapshot.dart @@ -447,8 +447,8 @@ class SettingsSnapshot { } SingleAgentProvider resolveSingleAgentProvider(SingleAgentProvider provider) { - if (provider.isAuto) { - return SingleAgentProvider.auto; + if (provider.isUnspecified) { + return SingleAgentProvider.unspecified; } final profile = providerSyncDefinitionForProviderId(provider.providerId); if (profile != null) { @@ -459,8 +459,8 @@ class SettingsSnapshot { SingleAgentProvider singleAgentProviderForId(String providerId) { final resolved = normalizeSingleAgentProviderId(providerId); - if (resolved.isEmpty || resolved == SingleAgentProvider.auto.providerId) { - return SingleAgentProvider.auto; + if (resolved.isEmpty || resolved == 'auto') { + return SingleAgentProvider.unspecified; } final normalizedSelection = SingleAgentProvider.fromJsonValue(resolved); final profile = providerSyncDefinitionForProviderId( @@ -476,8 +476,8 @@ class SettingsSnapshot { SingleAgentProvider provider, ) { final resolved = resolveSingleAgentProvider(provider); - if (resolved.isAuto) { - return SingleAgentProvider.auto; + if (resolved.isUnspecified) { + return SingleAgentProvider.unspecified; } if (kKnownSingleAgentProviders.any( (item) => item.providerId == resolved.providerId, diff --git a/test/app_controller_desktop_runtime_cleanup_test.dart b/test/app_controller_desktop_runtime_cleanup_test.dart index b5a18544..b61525e7 100644 --- a/test/app_controller_desktop_runtime_cleanup_test.dart +++ b/test/app_controller_desktop_runtime_cleanup_test.dart @@ -175,6 +175,63 @@ void main() { ); }, ); + + test( + 'single-agent threads default to bridge catalog providers without reviving auto mode', + () async { + final root = Directory.systemTemp.createTempSync( + 'xworkmate-provider-selection-test-', + ); + final store = SecureConfigStore( + enableSecureStorage: false, + appDataRootPathResolver: () async => root.path, + secretRootPathResolver: () async => root.path, + supportRootPathResolver: () async => root.path, + ); + final controller = AppController( + store: store, + desktopPlatformService: UnsupportedDesktopPlatformService(), + skillDirectoryAccessService: _FakeSkillDirectoryAccessService( + root.path, + ), + goTaskServiceClient: const _FakeGoTaskServiceClient(), + singleAgentSharedSkillScanRootOverrides: const [], + availableSingleAgentProvidersOverride: const [ + SingleAgentProvider.codex, + ], + ); + addTearDown(() async { + controller.dispose(); + if (root.existsSync()) { + await root.delete(recursive: true); + } + }); + + expect( + controller.singleAgentProviderForSession('draft:bridge-default'), + SingleAgentProvider.codex, + ); + + controller.initializeAssistantThreadContext( + 'draft:bridge-default', + executionTarget: AssistantExecutionTarget.singleAgent, + ); + + final thread = controller.taskThreadForSessionInternal( + 'draft:bridge-default', + ); + expect(thread, isNotNull); + expect( + thread!.executionBinding.providerId, + SingleAgentProvider.codex.providerId, + ); + expect( + thread.executionBinding.providerSource, + ThreadSelectionSource.inherited, + ); + expect(thread.hasExplicitProviderSelection, isFalse); + }, + ); } class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService { diff --git a/test/app_controller_desktop_thread_binding_test.dart b/test/app_controller_desktop_thread_binding_test.dart index 09191f12..99ce5892 100644 --- a/test/app_controller_desktop_thread_binding_test.dart +++ b/test/app_controller_desktop_thread_binding_test.dart @@ -102,7 +102,7 @@ void main() { ); expect(snapshot.executionTarget, AssistantExecutionTarget.remote); - expect(snapshot.singleAgentProvider, SingleAgentProvider.auto); + expect(snapshot.singleAgentProvider.isUnspecified, isTrue); expect(snapshot.record, isNull); expect(staleRecord.executionBinding.providerId, isNotEmpty); }); diff --git a/test/app_controller_desktop_thread_target_cleanup_test.dart b/test/app_controller_desktop_thread_target_cleanup_test.dart index 4482aefd..bdcfe8f3 100644 --- a/test/app_controller_desktop_thread_target_cleanup_test.dart +++ b/test/app_controller_desktop_thread_target_cleanup_test.dart @@ -14,7 +14,7 @@ void main() { TaskThread buildThread({ required String threadId, required ThreadExecutionMode mode, - String providerId = 'auto', + String providerId = '', }) { return TaskThread( threadId: threadId,