diff --git a/docs/architecture/settings-integration-configuration-model.md b/docs/architecture/settings-integration-configuration-model.md
index 8fff4fc5..13201ac5 100644
--- a/docs/architecture/settings-integration-configuration-model.md
+++ b/docs/architecture/settings-integration-configuration-model.md
@@ -29,7 +29,7 @@ flowchart TD
subgraph APPSTATE["App-side derived state"]
F["refreshSingleAgentCapabilitiesRuntimeInternal()"]
- G["bridgeProviderCatalogInternal"]
+ G["bridgeAgentProviderCatalogInternal
bridgeGatewayProviderCatalogInternal
bridgeAvailableExecutionTargetsInternal"]
H["singleAgentCapabilitiesByProviderInternal"]
I["refreshAcpCapabilitiesRuntimeInternal()"]
J["GatewayAcpCapabilities"]
@@ -78,7 +78,7 @@ flowchart TD
## Notes
-- `providerCatalog` 只负责 assistant provider picker;不会因为线程里保存过 `providerId` 就被 app 反向重建
+- provider picker 的真源只来自 bridge 返回的 target-scoped catalog;不会因为线程里保存过 `providerId` 就被 app 反向重建
- gateway runtime 可见性来自 bridge capability snapshot 与 `xworkmate.gateway.*` 返回,不来自旧设置页枚举
- bridge 若返回额外 capability flag,这些 flag 只属于合同元数据,不会自动生成新的 settings tab 或 module page
- production provider / gateway 选择继续由 bridge 拥有,app 只保留消费与展示
diff --git a/docs/architecture/task-control-plane-unification.md b/docs/architecture/task-control-plane-unification.md
index 414f4e1e..74619ddd 100644
--- a/docs/architecture/task-control-plane-unification.md
+++ b/docs/architecture/task-control-plane-unification.md
@@ -76,12 +76,14 @@ flowchart TD
### Provider Truth
-- `acp.capabilities.providerCatalog` 是 assistant provider picker 的唯一上游真源
+- `acp.capabilities` 是任务对话模式与 provider picker 的唯一上游真源
- 持久化在线程上的 `providerId` 只表示用户历史选择,不负责反向生成 catalog
- provider unavailable 文案与 resolved provider 都来自 `xworkmate.routing.resolve`
-- 任务对话模式的 provider 菜单按 execution target 分流:
- - `agent` 只展示 bridge-owned provider catalog,即 `codex / opencode / gemini`
- - `gateway` 只展示 canonical gateway provider,即 `OpenClaw`
+- bridge 返回 `availableExecutionTargets` 与 target-scoped provider catalog;app 只做目标切换与展示,不做静态拆分或 canonical 单项硬编码
+- app 只负责:
+ - 展示 `agent` / `gateway` 目标切换
+ - 请求 bridge contract
+ - 按 bridge 返回结果渲染 provider 菜单与默认项
- 这里不保留旧的 provider matrix、preset fallback 或双真源选择路径
### Gateway Truth
diff --git a/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md b/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md
index d644048d..39f86e93 100644
--- a/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md
+++ b/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md
@@ -149,7 +149,8 @@ Status: `Active`
当前 Assistant 事实:
- provider catalog 只来自 bridge capabilities,不再恢复任何 preset / backfill / fallback provider truth
-- 任务对话模式按 execution target 分流:`智能体` 只提供 `codex / opencode / gemini`,`Gateway` 只提供 `OpenClaw`
+- 任务对话模式只保留两类一级目标:`agent` / `gateway`
+- 每个目标下的 provider 菜单都只消费 `xworkmate-bridge` 返回的动态 catalog;app 不维护 `codex / opencode / gemini / openclaw` 这类本地固定列表
- task state 仍在 assistant 内被消费,但不再拥有独立 `TasksPage`
- skills 数据仍在 assistant 内被消费,但不再拥有独立 `SkillsPage`
- assistant focus 只保留仍有真实落点的 `settings / language / theme`
diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart
index eb486aef..172df643 100644
--- a/lib/app/app_controller_desktop_core.dart
+++ b/lib/app/app_controller_desktop_core.dart
@@ -120,6 +120,8 @@ class AppController extends ChangeNotifier {
DesktopPlatformService? desktopPlatformService,
UiFeatureManifest? uiFeatureManifest,
List? initialBridgeProviderCatalog,
+ List? initialGatewayProviderCatalog,
+ List? initialAvailableExecutionTargets,
SkillDirectoryAccessService? skillDirectoryAccessService,
AccountRuntimeClient Function(String baseUrl)? accountClientFactory,
Map? environmentOverride,
@@ -225,9 +227,15 @@ class AppController extends ChangeNotifier {
endpointResolver: resolveGatewayAcpEndpointInternal,
),
);
- bridgeProviderCatalogInternal = normalizeBridgeOwnedSingleAgentProviderList(
+ bridgeAgentProviderCatalogInternal = normalizeSingleAgentProviderList(
initialBridgeProviderCatalog ?? const [],
);
+ bridgeGatewayProviderCatalogInternal = normalizeSingleAgentProviderList(
+ initialGatewayProviderCatalog ?? const [],
+ );
+ bridgeAvailableExecutionTargetsInternal = compactAssistantExecutionTargets(
+ initialAvailableExecutionTargets ?? const [],
+ );
attachChildListenersInternal();
unawaited(initializeInternal());
@@ -290,8 +298,12 @@ class AppController extends ChangeNotifier {
GatewayAcpClient get gatewayAcpClientForTest => gatewayAcpClientInternal;
- List bridgeProviderCatalogInternal =
+ List bridgeAgentProviderCatalogInternal =
const [];
+ List bridgeGatewayProviderCatalogInternal =
+ const [];
+ List bridgeAvailableExecutionTargetsInternal =
+ const [];
final Map> assistantThreadMessagesInternal =
>{};
late final DesktopTaskThreadRepository taskThreadRepositoryInternal =
@@ -546,19 +558,30 @@ class AppController extends ChangeNotifier {
);
List get bridgeProviderCatalog =>
- normalizeSingleAgentProviderList(bridgeProviderCatalogInternal);
+ normalizeSingleAgentProviderList([
+ ...bridgeAgentProviderCatalogInternal,
+ ...bridgeGatewayProviderCatalogInternal,
+ ]);
List get assistantProviderCatalog =>
- normalizeBridgeOwnedSingleAgentProviderList(
- bridgeProviderCatalogInternal,
- );
+ normalizeSingleAgentProviderList(bridgeAgentProviderCatalogInternal);
+
+ List get gatewayProviderCatalog =>
+ normalizeSingleAgentProviderList(bridgeGatewayProviderCatalogInternal);
+
+ List get bridgeAvailableExecutionTargets =>
+ compactAssistantExecutionTargets(bridgeAvailableExecutionTargetsInternal);
List get assistantProviderCatalogForDisplay {
- final liveCatalog = assistantProviderCatalog;
- if (liveCatalog.isNotEmpty) {
- return liveCatalog;
- }
- return kBridgeOwnedSingleAgentProviders;
+ return assistantProviderCatalog;
+ }
+
+ List providerCatalogForExecutionTarget(
+ AssistantExecutionTarget executionTarget,
+ ) {
+ return executionTarget.isGateway
+ ? gatewayProviderCatalog
+ : assistantProviderCatalogForDisplay;
}
SingleAgentProvider? bridgeProviderForId(String providerId) {
@@ -574,11 +597,14 @@ class AppController extends ChangeNotifier {
return null;
}
- SingleAgentProvider resolveAssistantProvider(String? providerId) {
+ SingleAgentProvider resolveProviderForExecutionTarget(
+ String? providerId, {
+ required AssistantExecutionTarget executionTarget,
+ }) {
final normalizedProviderId = normalizeSingleAgentProviderId(
providerId ?? '',
);
- final catalog = assistantProviderCatalogForDisplay;
+ final catalog = providerCatalogForExecutionTarget(executionTarget);
if (normalizedProviderId.isNotEmpty) {
for (final provider in catalog) {
if (provider.providerId == normalizedProviderId) {
@@ -589,9 +615,23 @@ class AppController extends ChangeNotifier {
if (catalog.isNotEmpty) {
return catalog.first;
}
+ if (normalizedProviderId.isNotEmpty) {
+ return SingleAgentProvider.fromJsonValue(
+ normalizedProviderId,
+ supportedTargets: [executionTarget],
+ enabled: false,
+ );
+ }
return SingleAgentProvider.unspecified;
}
+ SingleAgentProvider resolveAssistantProvider(String? providerId) {
+ return resolveProviderForExecutionTarget(
+ providerId,
+ executionTarget: AssistantExecutionTarget.agent,
+ );
+ }
+
SingleAgentProvider assistantProviderForSession(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
@@ -600,10 +640,10 @@ class AppController extends ChangeNotifier {
final executionTarget = assistantExecutionTargetForSession(
normalizedSessionKey,
);
- if (executionTarget.isGateway) {
- return SingleAgentProvider.openclaw;
- }
- return resolveAssistantProvider(thread?.executionBinding.providerId);
+ return resolveProviderForExecutionTarget(
+ thread?.executionBinding.providerId,
+ executionTarget: executionTarget,
+ );
}
UiFeatureManifest loadRepoUiFeatureManifestSyncInternal() {
@@ -618,7 +658,16 @@ class AppController extends ChangeNotifier {
List visibleAssistantExecutionTargets(
Iterable supportedTargets,
- ) => compactAssistantExecutionTargets(supportedTargets);
+ ) {
+ final visible = compactAssistantExecutionTargets(supportedTargets);
+ final bridgeVisible = bridgeAvailableExecutionTargets;
+ if (bridgeVisible.isEmpty) {
+ return visible;
+ }
+ return visible
+ .where((item) => bridgeVisible.contains(item))
+ .toList(growable: false);
+ }
List get aiGatewayConversationModelChoices {
final availableModels =
diff --git a/lib/app/app_controller_desktop_runtime_coordination_impl.dart b/lib/app/app_controller_desktop_runtime_coordination_impl.dart
index 485f159b..3718d197 100644
--- a/lib/app/app_controller_desktop_runtime_coordination_impl.dart
+++ b/lib/app/app_controller_desktop_runtime_coordination_impl.dart
@@ -59,9 +59,13 @@ Future refreshAcpCapabilitiesRuntimeInternal(
// Keep mount refresh resilient when ACP is temporarily unavailable.
}
if (capabilities != null) {
- controller.bridgeProviderCatalogInternal =
- normalizeBridgeOwnedSingleAgentProviderList(
- capabilities.providerCatalog,
+ controller.bridgeAgentProviderCatalogInternal =
+ normalizeSingleAgentProviderList(capabilities.providerCatalog);
+ controller.bridgeGatewayProviderCatalogInternal =
+ normalizeSingleAgentProviderList(capabilities.gatewayProviderCatalog);
+ controller.bridgeAvailableExecutionTargetsInternal =
+ compactAssistantExecutionTargets(
+ capabilities.availableExecutionTargets,
);
}
if (persistMountTargets && !controller.disposedInternal) {
@@ -90,12 +94,21 @@ Future refreshSingleAgentCapabilitiesRuntimeInternal(
try {
final capabilities = await controller.gatewayAcpClientInternal
.loadCapabilities(forceRefresh: forceRefresh);
- controller.bridgeProviderCatalogInternal =
- normalizeBridgeOwnedSingleAgentProviderList(
- capabilities.providerCatalog,
+ controller.bridgeAgentProviderCatalogInternal =
+ normalizeSingleAgentProviderList(capabilities.providerCatalog);
+ controller.bridgeGatewayProviderCatalogInternal =
+ normalizeSingleAgentProviderList(capabilities.gatewayProviderCatalog);
+ controller.bridgeAvailableExecutionTargetsInternal =
+ compactAssistantExecutionTargets(
+ capabilities.availableExecutionTargets,
);
} catch (_) {
- controller.bridgeProviderCatalogInternal = const [];
+ controller.bridgeAgentProviderCatalogInternal =
+ const [];
+ controller.bridgeGatewayProviderCatalogInternal =
+ const [];
+ controller.bridgeAvailableExecutionTargetsInternal =
+ const [];
}
if (!controller.disposedInternal) {
controller.notifyListeners();
@@ -109,16 +122,19 @@ mergeAcpCapabilitiesIntoMountTargetsRuntimeInternal(
GatewayAcpCapabilities capabilities,
) {
final source = current.isEmpty ? ManagedMountTargetState.defaults() : current;
- final providers = capabilities.providerCatalog
+ final agentProviders = capabilities.providerCatalog
+ .map((item) => item.providerId)
+ .toSet();
+ final gatewayProviders = capabilities.gatewayProviderCatalog
.map((item) => item.providerId)
.toSet();
return source
.map((item) {
final available = switch (item.targetId) {
- 'codex' => providers.contains('codex'),
- 'opencode' => providers.contains('opencode'),
- 'gemini' => providers.contains('gemini'),
- 'openclaw' => capabilities.multiAgent || capabilities.singleAgent,
+ 'codex' => agentProviders.contains('codex'),
+ 'opencode' => agentProviders.contains('opencode'),
+ 'gemini' => agentProviders.contains('gemini'),
+ 'openclaw' => gatewayProviders.contains('openclaw'),
_ => false,
};
return item.copyWith(
diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart
index 4ab8d94f..c7dc4e3c 100644
--- a/lib/app/app_controller_desktop_skill_permissions.dart
+++ b/lib/app/app_controller_desktop_skill_permissions.dart
@@ -300,9 +300,10 @@ extension AppControllerDesktopSkillPermissions on AppController {
existing?.contextState.latestResolvedProviderId ??
'',
);
- final nextProvider = nextProviderId.isEmpty
- ? SingleAgentProvider.unspecified
- : resolveAssistantProvider(nextProviderId);
+ final nextProvider = resolveProviderForExecutionTarget(
+ nextProviderId,
+ executionTarget: nextExecutionTarget,
+ );
final nextProviderSource =
singleAgentProviderSource ??
existing?.executionBinding.providerSource ??
diff --git a/lib/app/app_controller_desktop_thread_binding.dart b/lib/app/app_controller_desktop_thread_binding.dart
index 74add700..7c0e470f 100644
--- a/lib/app/app_controller_desktop_thread_binding.dart
+++ b/lib/app/app_controller_desktop_thread_binding.dart
@@ -222,9 +222,10 @@ extension AppControllerDesktopThreadBinding on AppController {
final persistedProviderId = normalizeSingleAgentProviderId(
existingBinding?.providerId ?? '',
);
- final selectedProvider = persistedProviderId.isEmpty
- ? SingleAgentProvider.unspecified
- : resolveAssistantProvider(persistedProviderId);
+ final selectedProvider = resolveProviderForExecutionTarget(
+ persistedProviderId,
+ executionTarget: executionTarget,
+ );
return (existingBinding ??
ExecutionBinding(
executionMode: threadExecutionModeFromAssistantExecutionTarget(
diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart
index 81a9e3eb..e5d617ae 100644
--- a/lib/app/app_controller_desktop_thread_sessions.dart
+++ b/lib/app/app_controller_desktop_thread_sessions.dart
@@ -458,16 +458,7 @@ AssistantExecutionTarget resolveAssistantExecutionTargetFromRecordsForTest(
final record = primaryRecord ?? fallbackRecord;
return record == null
? AssistantExecutionTarget.agent
- : (() {
- final resolved = assistantExecutionTargetFromExecutionMode(
- record.executionBinding.executionMode,
- );
- if (resolved.isGateway &&
- isBridgeOwnedSingleAgentProviderId(
- record.executionBinding.providerId,
- )) {
- return AssistantExecutionTarget.agent;
- }
- return resolved;
- })();
+ : assistantExecutionTargetFromExecutionMode(
+ record.executionBinding.executionMode,
+ );
}
diff --git a/lib/app/app_controller_desktop_thread_storage.dart b/lib/app/app_controller_desktop_thread_storage.dart
index 7173547f..5c6436ed 100644
--- a/lib/app/app_controller_desktop_thread_storage.dart
+++ b/lib/app/app_controller_desktop_thread_storage.dart
@@ -688,15 +688,11 @@ extension AppControllerDesktopThreadStorage on AppController {
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 normalizedExecutionTarget = recordExecutionTarget;
+ final recordProvider = resolveProviderForExecutionTarget(
+ recordProviderId,
+ executionTarget: normalizedExecutionTarget,
+ );
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 e7a02d89..ae4c2522 100644
--- a/lib/app/app_controller_desktop_workspace_execution.dart
+++ b/lib/app/app_controller_desktop_workspace_execution.dart
@@ -54,14 +54,14 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
sessionsControllerInternal.currentSessionKey,
);
final shouldRefreshAgentProviders =
- resolvedTarget.isAgent && assistantProviderCatalog.isEmpty;
+ providerCatalogForExecutionTarget(resolvedTarget).isEmpty;
if (shouldRefreshAgentProviders) {
try {
await refreshSingleAgentCapabilitiesInternal(forceRefresh: true);
} catch (_) {
// Keep target selection interactive even when a just-in-time
- // capabilities refresh fails. The dialog still shows the canonical
- // single-agent providers while the live catalog catches up.
+ // capabilities refresh fails. The dialog stays interactive while the
+ // live catalog catches up from bridge capabilities.
}
if (currentTarget == resolvedTarget &&
settings.assistantExecutionTarget == resolvedTarget) {
@@ -99,6 +99,13 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
sessionsControllerInternal.currentSessionKey,
executionTarget: resolvedTarget,
executionTargetSource: ThreadSelectionSource.explicit,
+ singleAgentProvider: resolveProviderForExecutionTarget(
+ taskThreadForSessionInternal(
+ sessionsControllerInternal.currentSessionKey,
+ )?.executionBinding.providerId,
+ executionTarget: resolvedTarget,
+ ),
+ singleAgentProviderSource: ThreadSelectionSource.explicit,
gatewayEntryState: gatewayEntryStateForTargetInternal(resolvedTarget),
latestResolvedRuntimeModel: '',
latestResolvedProviderId: '',
@@ -215,6 +222,13 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
);
upsertTaskThreadInternal(
normalizedSessionKey,
+ singleAgentProvider: resolveProviderForExecutionTarget(
+ taskThreadForSessionInternal(normalizedSessionKey)
+ ?.executionBinding
+ .providerId,
+ executionTarget: resolvedTarget,
+ ),
+ singleAgentProviderSource: ThreadSelectionSource.explicit,
latestResolvedRuntimeModel: '',
latestResolvedProviderId: '',
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
diff --git a/lib/features/assistant/assistant_page_composer_support.dart b/lib/features/assistant/assistant_page_composer_support.dart
index b8d56878..caea08bf 100644
--- a/lib/features/assistant/assistant_page_composer_support.dart
+++ b/lib/features/assistant/assistant_page_composer_support.dart
@@ -217,6 +217,7 @@ class SingleAgentProviderBadgeInternal extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = context.palette;
+ final logoEmoji = provider.logoEmoji.trim();
final candidate = provider.badge.trim().isEmpty
? provider.label
: provider.badge;
@@ -235,13 +236,13 @@ class SingleAgentProviderBadgeInternal extends StatelessWidget {
border: Border.all(color: palette.strokeSoft),
),
child: Text(
- display,
+ logoEmoji.isEmpty ? display : logoEmoji,
maxLines: 1,
overflow: TextOverflow.clip,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: palette.textMuted,
fontWeight: FontWeight.w700,
- fontSize: 9,
+ fontSize: logoEmoji.isEmpty ? 9 : 11,
height: 1,
),
),
diff --git a/lib/features/assistant/assistant_page_task_dialog_controls.dart b/lib/features/assistant/assistant_page_task_dialog_controls.dart
index a05109f7..31861713 100644
--- a/lib/features/assistant/assistant_page_task_dialog_controls.dart
+++ b/lib/features/assistant/assistant_page_task_dialog_controls.dart
@@ -23,16 +23,22 @@ class AssistantTaskDialogModeControlsInternal extends StatelessWidget {
final uiFeatures = controller.featuresFor(
resolveUiFeaturePlatformFromContext(context),
);
- final visibleExecutionTargets = controller.visibleAssistantExecutionTargets(
+ final supportedExecutionTargets = compactAssistantExecutionTargets(
uiFeatures.availableExecutionTargets,
);
- if (visibleExecutionTargets.isEmpty) {
+ if (supportedExecutionTargets.isEmpty) {
return const SizedBox.shrink();
}
+ final visibleExecutionTargets = controller.visibleAssistantExecutionTargets(
+ supportedExecutionTargets,
+ );
+ final resolutionTargets = visibleExecutionTargets.isNotEmpty
+ ? visibleExecutionTargets
+ : supportedExecutionTargets;
final currentExecutionTarget =
resolveAssistantExecutionTargetFromVisibleTargets(
- visibleExecutionTargets,
+ resolutionTargets,
currentTarget: controller.assistantExecutionTarget,
);
final executionTarget = collapseAssistantExecutionTargetForDisplay(
@@ -51,17 +57,17 @@ class AssistantTaskDialogModeControlsInternal extends StatelessWidget {
_TaskDialogExecutionTargetMenuButtonInternal(
controller: controller,
executionTarget: executionTarget,
+ supportedExecutionTargets: supportedExecutionTargets,
visibleExecutionTargets: visibleExecutionTargets,
),
- if (providerMenuProviders.isNotEmpty)
- _TaskDialogProviderMenuButtonInternal(
- controller: controller,
- executionTarget: executionTarget,
- selectedProvider: controller.assistantProviderForSession(
- controller.currentSessionKey,
- ),
- providers: providerMenuProviders,
+ _TaskDialogProviderMenuButtonInternal(
+ controller: controller,
+ executionTarget: executionTarget,
+ selectedProvider: controller.assistantProviderForSession(
+ controller.currentSessionKey,
),
+ providers: providerMenuProviders,
+ ),
],
);
}
@@ -71,30 +77,24 @@ List _taskDialogProviderCatalogForTarget({
required AppController controller,
required AssistantExecutionTarget executionTarget,
}) {
- if (executionTarget.isGateway) {
- return [
- controller.assistantProviderForSession(controller.currentSessionKey),
- ];
- }
- return controller.assistantProviderCatalogForDisplay;
+ return controller.providerCatalogForExecutionTarget(executionTarget);
}
class _TaskDialogExecutionTargetMenuButtonInternal extends StatelessWidget {
const _TaskDialogExecutionTargetMenuButtonInternal({
required this.controller,
required this.executionTarget,
+ required this.supportedExecutionTargets,
required this.visibleExecutionTargets,
});
final AppController controller;
final AssistantExecutionTarget executionTarget;
+ final List supportedExecutionTargets;
final List visibleExecutionTargets;
@override
Widget build(BuildContext context) {
- final compactExecutionTargets = compactAssistantExecutionTargets(
- visibleExecutionTargets,
- );
final palette = context.palette;
final selectedLabel = executionTarget.label;
@@ -104,21 +104,25 @@ class _TaskDialogExecutionTargetMenuButtonInternal extends StatelessWidget {
onSelected: (value) {
unawaited(_handleExecutionTargetSelected(value));
},
- itemBuilder: (context) => compactExecutionTargets
+ itemBuilder: (context) => supportedExecutionTargets
.map(
- (value) => PopupMenuItem(
- value: value,
- key: Key('assistant-execution-target-menu-item-${value.name}'),
- child: Row(
- children: [
- Icon(value.icon, size: 18),
- const SizedBox(width: 10),
- Expanded(child: Text(value.label)),
- if (value == executionTarget)
- const Icon(Icons.check_rounded, size: 18),
- ],
- ),
- ),
+ (value) {
+ final enabled = visibleExecutionTargets.contains(value);
+ return PopupMenuItem(
+ value: value,
+ enabled: enabled,
+ key: Key('assistant-execution-target-menu-item-${value.name}'),
+ child: Row(
+ children: [
+ Icon(value.icon, size: 18),
+ const SizedBox(width: 10),
+ Expanded(child: Text(value.label)),
+ if (value == executionTarget)
+ const Icon(Icons.check_rounded, size: 18),
+ ],
+ ),
+ );
+ },
)
.toList(growable: false),
child: _TaskDialogSelectorChipInternal(
@@ -156,11 +160,13 @@ class _TaskDialogProviderMenuButtonInternal extends StatelessWidget {
@override
Widget build(BuildContext context) {
final displayProvider = selectedProvider.isUnspecified
- ? _fallbackDisplayProvider()
+ ? _fallbackDisplayProvider(context)
: selectedProvider;
+ final isEnabled = providers.isNotEmpty;
return PopupMenuButton(
key: const Key('assistant-provider-button'),
+ enabled: isEnabled,
tooltip: appText('智能体 Provider', 'Agent Provider'),
onSelected: (provider) {
unawaited(_handleProviderSelected(provider));
@@ -198,18 +204,21 @@ class _TaskDialogProviderMenuButtonInternal extends StatelessWidget {
);
}
- SingleAgentProvider _fallbackDisplayProvider() {
- if (executionTarget.isGateway) {
- return SingleAgentProvider.openclaw;
- }
+ SingleAgentProvider _fallbackDisplayProvider(BuildContext context) {
if (providers.isNotEmpty) {
return providers.first;
}
- return SingleAgentProvider.codex;
+ return SingleAgentProvider(
+ providerId: '',
+ label: appText('未提供', 'Unavailable'),
+ badge: '?',
+ supportedTargets: [executionTarget],
+ enabled: false,
+ );
}
Future _handleProviderSelected(SingleAgentProvider provider) async {
- if (executionTarget.isGateway) {
+ if (executionTarget.isGateway || providers.isEmpty) {
return;
}
await controller.setAssistantSingleAgentProvider(provider);
diff --git a/lib/runtime/external_code_agent_acp_desktop_transport.dart b/lib/runtime/external_code_agent_acp_desktop_transport.dart
index 6577c9b9..90fbdbeb 100644
--- a/lib/runtime/external_code_agent_acp_desktop_transport.dart
+++ b/lib/runtime/external_code_agent_acp_desktop_transport.dart
@@ -34,9 +34,11 @@ class ExternalCodeAgentAcpDesktopTransport
final caps = _castMap(result['capabilities']);
final providerCatalog = _parseProviderCatalog(
result['providerCatalog'] ?? caps['providerCatalog'],
+ defaultTarget: AssistantExecutionTarget.agent,
);
- final gatewayProviders = _castMapList(
+ final gatewayProviders = _parseProviderCatalog(
result['gatewayProviders'] ?? caps['gatewayProviders'],
+ defaultTarget: AssistantExecutionTarget.gateway,
);
return ExternalCodeAgentAcpCapabilities(
singleAgent:
@@ -47,6 +49,14 @@ class ExternalCodeAgentAcpDesktopTransport
_boolValue(result['multiAgent']) ??
_boolValue(caps['multi_agent']) ??
true,
+ availableExecutionTargets: _parseAvailableExecutionTargets(
+ result['availableExecutionTargets'] ?? caps['availableExecutionTargets'],
+ singleAgent:
+ _boolValue(result['singleAgent']) ??
+ _boolValue(caps['single_agent']) ??
+ providerCatalog.isNotEmpty,
+ gatewayProviders: gatewayProviders,
+ ),
providerCatalog: providerCatalog,
gatewayProviders: gatewayProviders,
raw: result,
@@ -166,10 +176,6 @@ class ExternalCodeAgentAcpDesktopTransport
return const