Drive task dialog providers from bridge catalog
This commit is contained in:
parent
69f0d451d9
commit
d4e09d7113
@ -29,7 +29,7 @@ flowchart TD
|
||||
|
||||
subgraph APPSTATE["App-side derived state"]
|
||||
F["refreshSingleAgentCapabilitiesRuntimeInternal()"]
|
||||
G["bridgeProviderCatalogInternal"]
|
||||
G["bridgeAgentProviderCatalogInternal<br/>bridgeGatewayProviderCatalogInternal<br/>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 只保留消费与展示
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -120,6 +120,8 @@ class AppController extends ChangeNotifier {
|
||||
DesktopPlatformService? desktopPlatformService,
|
||||
UiFeatureManifest? uiFeatureManifest,
|
||||
List<SingleAgentProvider>? initialBridgeProviderCatalog,
|
||||
List<SingleAgentProvider>? initialGatewayProviderCatalog,
|
||||
List<AssistantExecutionTarget>? initialAvailableExecutionTargets,
|
||||
SkillDirectoryAccessService? skillDirectoryAccessService,
|
||||
AccountRuntimeClient Function(String baseUrl)? accountClientFactory,
|
||||
Map<String, String>? environmentOverride,
|
||||
@ -225,9 +227,15 @@ class AppController extends ChangeNotifier {
|
||||
endpointResolver: resolveGatewayAcpEndpointInternal,
|
||||
),
|
||||
);
|
||||
bridgeProviderCatalogInternal = normalizeBridgeOwnedSingleAgentProviderList(
|
||||
bridgeAgentProviderCatalogInternal = normalizeSingleAgentProviderList(
|
||||
initialBridgeProviderCatalog ?? const <SingleAgentProvider>[],
|
||||
);
|
||||
bridgeGatewayProviderCatalogInternal = normalizeSingleAgentProviderList(
|
||||
initialGatewayProviderCatalog ?? const <SingleAgentProvider>[],
|
||||
);
|
||||
bridgeAvailableExecutionTargetsInternal = compactAssistantExecutionTargets(
|
||||
initialAvailableExecutionTargets ?? const <AssistantExecutionTarget>[],
|
||||
);
|
||||
|
||||
attachChildListenersInternal();
|
||||
unawaited(initializeInternal());
|
||||
@ -290,8 +298,12 @@ class AppController extends ChangeNotifier {
|
||||
|
||||
GatewayAcpClient get gatewayAcpClientForTest => gatewayAcpClientInternal;
|
||||
|
||||
List<SingleAgentProvider> bridgeProviderCatalogInternal =
|
||||
List<SingleAgentProvider> bridgeAgentProviderCatalogInternal =
|
||||
const <SingleAgentProvider>[];
|
||||
List<SingleAgentProvider> bridgeGatewayProviderCatalogInternal =
|
||||
const <SingleAgentProvider>[];
|
||||
List<AssistantExecutionTarget> bridgeAvailableExecutionTargetsInternal =
|
||||
const <AssistantExecutionTarget>[];
|
||||
final Map<String, List<GatewayChatMessage>> assistantThreadMessagesInternal =
|
||||
<String, List<GatewayChatMessage>>{};
|
||||
late final DesktopTaskThreadRepository taskThreadRepositoryInternal =
|
||||
@ -546,19 +558,30 @@ class AppController extends ChangeNotifier {
|
||||
);
|
||||
|
||||
List<SingleAgentProvider> get bridgeProviderCatalog =>
|
||||
normalizeSingleAgentProviderList(bridgeProviderCatalogInternal);
|
||||
normalizeSingleAgentProviderList(<SingleAgentProvider>[
|
||||
...bridgeAgentProviderCatalogInternal,
|
||||
...bridgeGatewayProviderCatalogInternal,
|
||||
]);
|
||||
|
||||
List<SingleAgentProvider> get assistantProviderCatalog =>
|
||||
normalizeBridgeOwnedSingleAgentProviderList(
|
||||
bridgeProviderCatalogInternal,
|
||||
);
|
||||
normalizeSingleAgentProviderList(bridgeAgentProviderCatalogInternal);
|
||||
|
||||
List<SingleAgentProvider> get gatewayProviderCatalog =>
|
||||
normalizeSingleAgentProviderList(bridgeGatewayProviderCatalogInternal);
|
||||
|
||||
List<AssistantExecutionTarget> get bridgeAvailableExecutionTargets =>
|
||||
compactAssistantExecutionTargets(bridgeAvailableExecutionTargetsInternal);
|
||||
|
||||
List<SingleAgentProvider> get assistantProviderCatalogForDisplay {
|
||||
final liveCatalog = assistantProviderCatalog;
|
||||
if (liveCatalog.isNotEmpty) {
|
||||
return liveCatalog;
|
||||
}
|
||||
return kBridgeOwnedSingleAgentProviders;
|
||||
return assistantProviderCatalog;
|
||||
}
|
||||
|
||||
List<SingleAgentProvider> 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: <AssistantExecutionTarget>[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<AssistantExecutionTarget> visibleAssistantExecutionTargets(
|
||||
Iterable<AssistantExecutionTarget> 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<String> get aiGatewayConversationModelChoices {
|
||||
final availableModels =
|
||||
|
||||
@ -59,9 +59,13 @@ Future<void> 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<void> 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 <SingleAgentProvider>[];
|
||||
controller.bridgeAgentProviderCatalogInternal =
|
||||
const <SingleAgentProvider>[];
|
||||
controller.bridgeGatewayProviderCatalogInternal =
|
||||
const <SingleAgentProvider>[];
|
||||
controller.bridgeAvailableExecutionTargetsInternal =
|
||||
const <AssistantExecutionTarget>[];
|
||||
}
|
||||
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(
|
||||
|
||||
@ -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 ??
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@ -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<SingleAgentProvider> _taskDialogProviderCatalogForTarget({
|
||||
required AppController controller,
|
||||
required AssistantExecutionTarget executionTarget,
|
||||
}) {
|
||||
if (executionTarget.isGateway) {
|
||||
return <SingleAgentProvider>[
|
||||
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<AssistantExecutionTarget> supportedExecutionTargets;
|
||||
final List<AssistantExecutionTarget> 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<AssistantExecutionTarget>(
|
||||
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<AssistantExecutionTarget>(
|
||||
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<SingleAgentProvider>(
|
||||
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: <AssistantExecutionTarget>[executionTarget],
|
||||
enabled: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleProviderSelected(SingleAgentProvider provider) async {
|
||||
if (executionTarget.isGateway) {
|
||||
if (executionTarget.isGateway || providers.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await controller.setAssistantSingleAgentProvider(provider);
|
||||
|
||||
@ -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 <Object?>[];
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _castMapList(Object? raw) {
|
||||
return _asList(raw).map(_castMap).toList(growable: false);
|
||||
}
|
||||
|
||||
bool? _boolValue(Object? raw) {
|
||||
if (raw is bool) {
|
||||
return raw;
|
||||
@ -190,7 +196,10 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
return null;
|
||||
}
|
||||
|
||||
List<SingleAgentProvider> _parseProviderCatalog(Object? raw) {
|
||||
List<SingleAgentProvider> _parseProviderCatalog(
|
||||
Object? raw, {
|
||||
required AssistantExecutionTarget defaultTarget,
|
||||
}) {
|
||||
final providers = <SingleAgentProvider>[];
|
||||
for (final item in _asList(raw)) {
|
||||
final entry = _castMap(item);
|
||||
@ -199,14 +208,85 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
continue;
|
||||
}
|
||||
final label = entry['label']?.toString().trim();
|
||||
final providerDisplay = _castMap(entry['providerDisplay']);
|
||||
final targets = _parseProviderTargets(
|
||||
entry['targets'] ?? entry['executionTarget'],
|
||||
defaultTarget: defaultTarget,
|
||||
);
|
||||
final provider = SingleAgentProviderCopy.fromJsonValue(
|
||||
providerId,
|
||||
label: label?.isNotEmpty == true ? label : null,
|
||||
badge: entry['badge']?.toString().trim().isNotEmpty == true
|
||||
? entry['badge']?.toString().trim()
|
||||
: providerDisplay['badge']?.toString().trim(),
|
||||
logoEmoji: entry['logoEmoji']?.toString().trim().isNotEmpty == true
|
||||
? entry['logoEmoji']?.toString().trim()
|
||||
: providerDisplay['logoEmoji']?.toString().trim(),
|
||||
supportedTargets: targets,
|
||||
enabled: _boolValue(entry['enabled']) ?? true,
|
||||
unavailableReason:
|
||||
entry['unavailableReason']?.toString().trim().isNotEmpty == true
|
||||
? entry['unavailableReason']?.toString().trim()
|
||||
: '',
|
||||
);
|
||||
if (!provider.isUnspecified) {
|
||||
providers.add(provider);
|
||||
}
|
||||
}
|
||||
return normalizeBridgeOwnedSingleAgentProviderList(providers);
|
||||
return normalizeSingleAgentProviderList(providers);
|
||||
}
|
||||
|
||||
List<AssistantExecutionTarget> _parseAvailableExecutionTargets(
|
||||
Object? raw, {
|
||||
required bool singleAgent,
|
||||
required List<SingleAgentProvider> gatewayProviders,
|
||||
}) {
|
||||
final parsed = <AssistantExecutionTarget>[];
|
||||
for (final item in _asList(raw)) {
|
||||
final normalized = item?.toString().trim().toLowerCase() ?? '';
|
||||
if (normalized == 'agent' || normalized == 'single-agent') {
|
||||
if (!parsed.contains(AssistantExecutionTarget.agent)) {
|
||||
parsed.add(AssistantExecutionTarget.agent);
|
||||
}
|
||||
} else if (normalized == 'gateway') {
|
||||
if (!parsed.contains(AssistantExecutionTarget.gateway)) {
|
||||
parsed.add(AssistantExecutionTarget.gateway);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsed.isNotEmpty) {
|
||||
return parsed;
|
||||
}
|
||||
if (singleAgent) {
|
||||
parsed.add(AssistantExecutionTarget.agent);
|
||||
}
|
||||
if (gatewayProviders.isNotEmpty) {
|
||||
parsed.add(AssistantExecutionTarget.gateway);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
List<AssistantExecutionTarget> _parseProviderTargets(
|
||||
Object? raw, {
|
||||
required AssistantExecutionTarget defaultTarget,
|
||||
}) {
|
||||
final parsed = <AssistantExecutionTarget>[];
|
||||
final items = raw is List ? raw : <Object?>[raw];
|
||||
for (final item in items) {
|
||||
final normalized = item?.toString().trim().toLowerCase() ?? '';
|
||||
if (normalized == 'agent' || normalized == 'single-agent') {
|
||||
if (!parsed.contains(AssistantExecutionTarget.agent)) {
|
||||
parsed.add(AssistantExecutionTarget.agent);
|
||||
}
|
||||
} else if (normalized == 'gateway') {
|
||||
if (!parsed.contains(AssistantExecutionTarget.gateway)) {
|
||||
parsed.add(AssistantExecutionTarget.gateway);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsed.isNotEmpty) {
|
||||
return parsed;
|
||||
}
|
||||
return <AssistantExecutionTarget>[defaultTarget];
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,9 @@ class GatewayAcpCapabilities {
|
||||
const GatewayAcpCapabilities({
|
||||
required this.singleAgent,
|
||||
required this.multiAgent,
|
||||
required this.availableExecutionTargets,
|
||||
required this.providerCatalog,
|
||||
required this.gatewayProviderCatalog,
|
||||
required this.raw,
|
||||
this.diagnostics = const <String, dynamic>{},
|
||||
});
|
||||
@ -28,13 +30,17 @@ class GatewayAcpCapabilities {
|
||||
const GatewayAcpCapabilities.empty()
|
||||
: singleAgent = false,
|
||||
multiAgent = false,
|
||||
availableExecutionTargets = const <AssistantExecutionTarget>[],
|
||||
providerCatalog = const <SingleAgentProvider>[],
|
||||
gatewayProviderCatalog = const <SingleAgentProvider>[],
|
||||
raw = const <String, dynamic>{},
|
||||
diagnostics = const <String, dynamic>{};
|
||||
|
||||
final bool singleAgent;
|
||||
final bool multiAgent;
|
||||
final List<AssistantExecutionTarget> availableExecutionTargets;
|
||||
final List<SingleAgentProvider> providerCatalog;
|
||||
final List<SingleAgentProvider> gatewayProviderCatalog;
|
||||
final Map<String, dynamic> raw;
|
||||
final Map<String, dynamic> diagnostics;
|
||||
}
|
||||
@ -121,6 +127,11 @@ class GatewayAcpClient {
|
||||
final caps = asMap(result['capabilities']);
|
||||
final providerCatalog = _parseProviderCatalog(
|
||||
result['providerCatalog'] ?? caps['providerCatalog'],
|
||||
defaultTarget: AssistantExecutionTarget.agent,
|
||||
);
|
||||
final gatewayProviderCatalog = _parseProviderCatalog(
|
||||
result['gatewayProviders'] ?? caps['gatewayProviders'],
|
||||
defaultTarget: AssistantExecutionTarget.gateway,
|
||||
);
|
||||
final singleAgent =
|
||||
boolValue(result['singleAgent']) ??
|
||||
@ -133,7 +144,13 @@ class GatewayAcpClient {
|
||||
_cachedCapabilities = GatewayAcpCapabilities(
|
||||
singleAgent: singleAgent,
|
||||
multiAgent: multiAgent,
|
||||
availableExecutionTargets: _parseAvailableExecutionTargets(
|
||||
result['availableExecutionTargets'] ?? caps['availableExecutionTargets'],
|
||||
singleAgent: singleAgent,
|
||||
gatewayProviderCatalog: gatewayProviderCatalog,
|
||||
),
|
||||
providerCatalog: providerCatalog,
|
||||
gatewayProviderCatalog: gatewayProviderCatalog,
|
||||
raw: result,
|
||||
diagnostics: asMap(response['_xworkmateDiagnostics']),
|
||||
);
|
||||
@ -141,7 +158,10 @@ class GatewayAcpClient {
|
||||
return _cachedCapabilities;
|
||||
}
|
||||
|
||||
List<SingleAgentProvider> _parseProviderCatalog(Object? raw) {
|
||||
List<SingleAgentProvider> _parseProviderCatalog(
|
||||
Object? raw, {
|
||||
required AssistantExecutionTarget defaultTarget,
|
||||
}) {
|
||||
final providers = <SingleAgentProvider>[];
|
||||
for (final item in asList(raw)) {
|
||||
final entry = asMap(item);
|
||||
@ -150,15 +170,86 @@ class GatewayAcpClient {
|
||||
continue;
|
||||
}
|
||||
final label = entry['label']?.toString().trim();
|
||||
final providerDisplay = asMap(entry['providerDisplay']);
|
||||
final targets = _parseProviderTargets(
|
||||
entry['targets'] ?? entry['executionTarget'],
|
||||
defaultTarget: defaultTarget,
|
||||
);
|
||||
final provider = SingleAgentProviderCopy.fromJsonValue(
|
||||
providerId,
|
||||
label: label?.isNotEmpty == true ? label : null,
|
||||
badge: entry['badge']?.toString().trim().isNotEmpty == true
|
||||
? entry['badge']?.toString().trim()
|
||||
: providerDisplay['badge']?.toString().trim(),
|
||||
logoEmoji: entry['logoEmoji']?.toString().trim().isNotEmpty == true
|
||||
? entry['logoEmoji']?.toString().trim()
|
||||
: providerDisplay['logoEmoji']?.toString().trim(),
|
||||
supportedTargets: targets,
|
||||
enabled: boolValue(entry['enabled']) ?? true,
|
||||
unavailableReason:
|
||||
entry['unavailableReason']?.toString().trim().isNotEmpty == true
|
||||
? entry['unavailableReason']?.toString().trim()
|
||||
: '',
|
||||
);
|
||||
if (!provider.isUnspecified) {
|
||||
providers.add(provider);
|
||||
}
|
||||
}
|
||||
return normalizeBridgeOwnedSingleAgentProviderList(providers);
|
||||
return normalizeSingleAgentProviderList(providers);
|
||||
}
|
||||
|
||||
List<AssistantExecutionTarget> _parseAvailableExecutionTargets(
|
||||
Object? raw, {
|
||||
required bool singleAgent,
|
||||
required List<SingleAgentProvider> gatewayProviderCatalog,
|
||||
}) {
|
||||
final parsed = <AssistantExecutionTarget>[];
|
||||
for (final item in asList(raw)) {
|
||||
final normalized = item?.toString().trim().toLowerCase() ?? '';
|
||||
if (normalized == 'agent' || normalized == 'single-agent') {
|
||||
if (!parsed.contains(AssistantExecutionTarget.agent)) {
|
||||
parsed.add(AssistantExecutionTarget.agent);
|
||||
}
|
||||
} else if (normalized == 'gateway') {
|
||||
if (!parsed.contains(AssistantExecutionTarget.gateway)) {
|
||||
parsed.add(AssistantExecutionTarget.gateway);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsed.isNotEmpty) {
|
||||
return parsed;
|
||||
}
|
||||
if (singleAgent) {
|
||||
parsed.add(AssistantExecutionTarget.agent);
|
||||
}
|
||||
if (gatewayProviderCatalog.isNotEmpty) {
|
||||
parsed.add(AssistantExecutionTarget.gateway);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
List<AssistantExecutionTarget> _parseProviderTargets(
|
||||
Object? raw, {
|
||||
required AssistantExecutionTarget defaultTarget,
|
||||
}) {
|
||||
final parsed = <AssistantExecutionTarget>[];
|
||||
final items = raw is List ? raw : <Object?>[raw];
|
||||
for (final item in items) {
|
||||
final normalized = item?.toString().trim().toLowerCase() ?? '';
|
||||
if (normalized == 'agent' || normalized == 'single-agent') {
|
||||
if (!parsed.contains(AssistantExecutionTarget.agent)) {
|
||||
parsed.add(AssistantExecutionTarget.agent);
|
||||
}
|
||||
} else if (normalized == 'gateway') {
|
||||
if (!parsed.contains(AssistantExecutionTarget.gateway)) {
|
||||
parsed.add(AssistantExecutionTarget.gateway);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsed.isNotEmpty) {
|
||||
return parsed;
|
||||
}
|
||||
return <AssistantExecutionTarget>[defaultTarget];
|
||||
}
|
||||
|
||||
Stream<MultiAgentRunEvent> runMultiAgent(
|
||||
|
||||
@ -8,6 +8,7 @@ class ExternalCodeAgentAcpCapabilities {
|
||||
const ExternalCodeAgentAcpCapabilities({
|
||||
required this.singleAgent,
|
||||
required this.multiAgent,
|
||||
required this.availableExecutionTargets,
|
||||
required this.providerCatalog,
|
||||
required this.gatewayProviders,
|
||||
required this.raw,
|
||||
@ -16,14 +17,16 @@ class ExternalCodeAgentAcpCapabilities {
|
||||
const ExternalCodeAgentAcpCapabilities.empty()
|
||||
: singleAgent = false,
|
||||
multiAgent = false,
|
||||
availableExecutionTargets = const <AssistantExecutionTarget>[],
|
||||
providerCatalog = const <SingleAgentProvider>[],
|
||||
gatewayProviders = const <Map<String, dynamic>>[],
|
||||
gatewayProviders = const <SingleAgentProvider>[],
|
||||
raw = const <String, dynamic>{};
|
||||
|
||||
final bool singleAgent;
|
||||
final bool multiAgent;
|
||||
final List<AssistantExecutionTarget> availableExecutionTargets;
|
||||
final List<SingleAgentProvider> providerCatalog;
|
||||
final List<Map<String, dynamic>> gatewayProviders;
|
||||
final List<SingleAgentProvider> gatewayProviders;
|
||||
final Map<String, dynamic> raw;
|
||||
}
|
||||
|
||||
|
||||
@ -185,6 +185,10 @@ class SingleAgentProvider {
|
||||
required this.providerId,
|
||||
required this.label,
|
||||
required this.badge,
|
||||
this.logoEmoji = '',
|
||||
this.supportedTargets = const <AssistantExecutionTarget>[],
|
||||
this.enabled = true,
|
||||
this.unavailableReason = '',
|
||||
this.source = SingleAgentProviderSource.externalExtension,
|
||||
});
|
||||
|
||||
@ -227,6 +231,10 @@ class SingleAgentProvider {
|
||||
final String providerId;
|
||||
final String label;
|
||||
final String badge;
|
||||
final String logoEmoji;
|
||||
final List<AssistantExecutionTarget> supportedTargets;
|
||||
final bool enabled;
|
||||
final String unavailableReason;
|
||||
final SingleAgentProviderSource source;
|
||||
|
||||
bool get isUnspecified => providerId.trim().isEmpty;
|
||||
@ -237,6 +245,10 @@ class SingleAgentProvider {
|
||||
String? providerId,
|
||||
String? label,
|
||||
String? badge,
|
||||
String? logoEmoji,
|
||||
List<AssistantExecutionTarget>? supportedTargets,
|
||||
bool? enabled,
|
||||
String? unavailableReason,
|
||||
SingleAgentProviderSource? source,
|
||||
}) {
|
||||
final resolvedProviderId = normalizeSingleAgentProviderId(
|
||||
@ -255,6 +267,13 @@ class SingleAgentProvider {
|
||||
label: resolvedLabel,
|
||||
)
|
||||
: resolvedBadge,
|
||||
logoEmoji: (logoEmoji ?? this.logoEmoji).trim(),
|
||||
supportedTargets:
|
||||
supportedTargets ??
|
||||
List<AssistantExecutionTarget>.from(this.supportedTargets),
|
||||
enabled: enabled ?? this.enabled,
|
||||
unavailableReason:
|
||||
(unavailableReason ?? this.unavailableReason).trim(),
|
||||
source: source ?? this.source,
|
||||
);
|
||||
}
|
||||
@ -263,6 +282,10 @@ class SingleAgentProvider {
|
||||
String? value, {
|
||||
String? label,
|
||||
String? badge,
|
||||
String? logoEmoji,
|
||||
List<AssistantExecutionTarget>? supportedTargets,
|
||||
bool? enabled,
|
||||
String? unavailableReason,
|
||||
}) {
|
||||
final normalized = normalizeSingleAgentProviderId(value ?? '');
|
||||
final base = switch (normalized) {
|
||||
@ -281,7 +304,14 @@ class SingleAgentProvider {
|
||||
),
|
||||
),
|
||||
};
|
||||
return base.copyWith(label: label, badge: badge);
|
||||
return base.copyWith(
|
||||
label: label,
|
||||
badge: badge,
|
||||
logoEmoji: logoEmoji,
|
||||
supportedTargets: supportedTargets,
|
||||
enabled: enabled,
|
||||
unavailableReason: unavailableReason,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -298,7 +328,19 @@ extension SingleAgentProviderCopy on SingleAgentProvider {
|
||||
String? value, {
|
||||
String? label,
|
||||
String? badge,
|
||||
}) => SingleAgentProvider.fromJsonValue(value, label: label, badge: badge);
|
||||
String? logoEmoji,
|
||||
List<AssistantExecutionTarget>? supportedTargets,
|
||||
bool? enabled,
|
||||
String? unavailableReason,
|
||||
}) => SingleAgentProvider.fromJsonValue(
|
||||
value,
|
||||
label: label,
|
||||
badge: badge,
|
||||
logoEmoji: logoEmoji,
|
||||
supportedTargets: supportedTargets,
|
||||
enabled: enabled,
|
||||
unavailableReason: unavailableReason,
|
||||
);
|
||||
}
|
||||
|
||||
enum SingleAgentProviderSource { externalExtension }
|
||||
@ -319,26 +361,9 @@ List<SingleAgentProvider> normalizeSingleAgentProviderList(
|
||||
const String kCanonicalGatewayProviderId = 'openclaw';
|
||||
const String kCanonicalGatewayProviderLabel = 'OpenClaw';
|
||||
|
||||
const List<SingleAgentProvider> kBridgeOwnedSingleAgentProviders =
|
||||
<SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.opencode,
|
||||
SingleAgentProvider.gemini,
|
||||
];
|
||||
|
||||
bool isBridgeOwnedSingleAgentProviderId(String providerId) {
|
||||
final normalized = normalizeSingleAgentProviderId(providerId);
|
||||
return kBridgeOwnedSingleAgentProviders.any(
|
||||
(item) => item.providerId == normalized,
|
||||
);
|
||||
}
|
||||
|
||||
List<SingleAgentProvider> normalizeBridgeOwnedSingleAgentProviderList(
|
||||
Iterable<SingleAgentProvider> providers,
|
||||
) {
|
||||
return normalizeSingleAgentProviderList(
|
||||
providers.where(
|
||||
(provider) => isBridgeOwnedSingleAgentProviderId(provider.providerId),
|
||||
),
|
||||
);
|
||||
return normalized == 'codex' ||
|
||||
normalized == 'opencode' ||
|
||||
normalized == 'gemini';
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import 'package:xworkmate/widgets/surface_card.dart';
|
||||
void main() {
|
||||
group('AssistantLowerPaneInternal', () {
|
||||
testWidgets(
|
||||
'keeps canonical agent providers visible when live capabilities are unavailable',
|
||||
'does not fabricate providers when live capabilities are unavailable',
|
||||
(tester) async {
|
||||
final controller = AppController();
|
||||
addTearDown(controller.dispose);
|
||||
@ -33,15 +33,15 @@ void main() {
|
||||
|
||||
expect(
|
||||
find.byKey(const Key('assistant-provider-menu-item-codex')),
|
||||
findsOneWidget,
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-provider-menu-item-opencode')),
|
||||
findsOneWidget,
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-provider-menu-item-gemini')),
|
||||
findsOneWidget,
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-provider-menu-item-openclaw')),
|
||||
@ -57,6 +57,18 @@ void main() {
|
||||
SingleAgentProvider.opencode,
|
||||
SingleAgentProvider.gemini,
|
||||
],
|
||||
initialGatewayProviderCatalog: <SingleAgentProvider>[
|
||||
SingleAgentProvider.openclaw.copyWith(
|
||||
logoEmoji: '🦞',
|
||||
supportedTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.gateway,
|
||||
],
|
||||
),
|
||||
],
|
||||
initialAvailableExecutionTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.agent,
|
||||
AssistantExecutionTarget.gateway,
|
||||
],
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
|
||||
@ -51,6 +51,78 @@ void main() {
|
||||
expect(provider.label, kCanonicalGatewayProviderLabel);
|
||||
});
|
||||
|
||||
test(
|
||||
'switching a session to gateway uses the bridge-provided gateway catalog',
|
||||
() async {
|
||||
final controller = AppController(
|
||||
initialBridgeProviderCatalog: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.opencode,
|
||||
SingleAgentProvider.gemini,
|
||||
],
|
||||
initialGatewayProviderCatalog: <SingleAgentProvider>[
|
||||
SingleAgentProvider.openclaw.copyWith(
|
||||
logoEmoji: '🦞',
|
||||
supportedTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.gateway,
|
||||
],
|
||||
),
|
||||
],
|
||||
initialAvailableExecutionTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.agent,
|
||||
AssistantExecutionTarget.gateway,
|
||||
],
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession('session-1');
|
||||
|
||||
expect(controller.currentAssistantExecutionTarget.isAgent, isTrue);
|
||||
expect(
|
||||
controller.assistantProviderForSession(controller.currentSessionKey),
|
||||
SingleAgentProvider.codex,
|
||||
);
|
||||
|
||||
await controller.setAssistantExecutionTarget(
|
||||
AssistantExecutionTarget.gateway,
|
||||
);
|
||||
|
||||
final record = controller.requireTaskThreadForSessionInternal(
|
||||
'session-1',
|
||||
);
|
||||
expect(
|
||||
controller.assistantExecutionTargetForSession('session-1').isGateway,
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
assistantExecutionTargetFromExecutionMode(
|
||||
record.executionBinding.executionMode,
|
||||
),
|
||||
AssistantExecutionTarget.gateway,
|
||||
);
|
||||
expect(
|
||||
controller.assistantProviderForSession('session-1'),
|
||||
SingleAgentProvider.openclaw,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'returns an unavailable provider placeholder when a saved provider is no longer in the bridge catalog',
|
||||
() {
|
||||
final controller = AppController();
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
final unavailableProvider = controller.resolveProviderForExecutionTarget(
|
||||
'gemini',
|
||||
executionTarget: AssistantExecutionTarget.agent,
|
||||
);
|
||||
|
||||
expect(unavailableProvider.providerId, 'gemini');
|
||||
expect(unavailableProvider.enabled, isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'refreshes agent provider catalog when agent mode is selected with an empty catalog',
|
||||
() async {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user