Merge branch 'codex/remove-auto-mode'

This commit is contained in:
Haitao Pan 2026-04-11 14:49:21 +08:00
commit fd9ddbb792
21 changed files with 112 additions and 66 deletions

View File

@ -588,11 +588,7 @@ class AppController extends ChangeNotifier {
);
List<SingleAgentProvider> get configuredSingleAgentProviders =>
normalizeSingleAgentProviderList(
bridgeAdvertisedProvidersInternal.where(
(item) => item != SingleAgentProvider.auto,
),
);
normalizeSingleAgentProviderList(bridgeAdvertisedProvidersInternal);
List<SingleAgentProvider> 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;

View File

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

View File

@ -311,7 +311,7 @@ extension AppControllerDesktopSettings on AppController {
currentSettings.assistantExecutionTarget,
),
messageViewMode: AssistantMessageViewMode.rendered,
singleAgentProvider: SingleAgentProvider.auto,
singleAgentProvider: SingleAgentProvider.unspecified,
);
await setCurrentAssistantSessionKeyInternal(
'main',

View File

@ -98,7 +98,7 @@ Future<void> 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<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
routingResolution,
),
routingHint: 'single-agent',
provider: effectiveProvider ?? SingleAgentProvider.auto,
provider: effectiveProvider ?? SingleAgentProvider.unspecified,
remoteWorkingDirectoryHint:
controller
.requireTaskThreadForSessionInternal(sessionKey)

View File

@ -306,7 +306,7 @@ extension AppControllerDesktopSkillPermissions on AppController {
executionBinding?.providerId ??
existing?.executionBinding.providerId,
))
: SingleAgentProvider.auto;
: SingleAgentProvider.unspecified;
final nextProviderSource =
nextExecutionTarget == AssistantExecutionTarget.singleAgent
? (singleAgentProviderSource ??

View File

@ -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,

View File

@ -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');

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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');

View File

@ -201,7 +201,7 @@ class ExternalCodeAgentAcpDesktopTransport
providerId,
label: label?.isNotEmpty == true ? label : null,
);
if (provider != SingleAgentProvider.auto) {
if (!provider.isUnspecified) {
providers.add(provider);
}
}

View File

@ -158,7 +158,7 @@ class GatewayAcpClient {
providerId,
label: label?.isNotEmpty == true ? label : null,
);
if (provider != SingleAgentProvider.auto) {
if (!provider.isUnspecified) {
providers.add(provider);
}
}

View File

@ -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();

View File

@ -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 = <String, String>{
'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),

View File

@ -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<ExternalAcpEndpointProfile> 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<ExternalAcpEndpointProfile> 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;
}

View File

@ -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 =

View File

@ -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,

View File

@ -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 <String>[],
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
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 {

View File

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

View File

@ -14,7 +14,7 @@ void main() {
TaskThread buildThread({
required String threadId,
required ThreadExecutionMode mode,
String providerId = 'auto',
String providerId = '',
}) {
return TaskThread(
threadId: threadId,