Refine assistant task dialog mode mapping

This commit is contained in:
Haitao Pan 2026-04-12 19:51:04 +08:00
parent bafcf3e25d
commit 155c4e3de1
20 changed files with 279 additions and 90 deletions

View File

@ -480,7 +480,8 @@ class AppController extends ChangeNotifier {
bool get hasPendingSettingsApply => pendingSettingsApplyInternal;
String get settingsDraftStatusMessage => settingsDraftStatusMessageInternal;
List<GatewayAgentSummary> get agents => agentsControllerInternal.agents;
List<GatewaySessionSummary> get sessions => sessionsControllerInternal.sessions;
List<GatewaySessionSummary> get sessions =>
sessionsControllerInternal.sessions;
List<GatewaySessionSummary> get assistantSessions =>
assistantSessionsInternal();
List<GatewayInstanceSummary> get instances =>
@ -568,7 +569,9 @@ class AppController extends ChangeNotifier {
normalizeSingleAgentProviderList(bridgeProviderCatalogInternal);
List<SingleAgentProvider> get assistantProviderCatalog {
final catalog = bridgeProviderCatalog;
final catalog = normalizeBridgeOwnedSingleAgentProviderList(
bridgeProviderCatalogInternal,
);
if (catalog.isNotEmpty) {
return catalog;
}
@ -589,7 +592,9 @@ class AppController extends ChangeNotifier {
}
SingleAgentProvider resolveAssistantProvider(String? providerId) {
final normalizedProviderId = normalizeSingleAgentProviderId(providerId ?? '');
final normalizedProviderId = normalizeSingleAgentProviderId(
providerId ?? '',
);
final catalog = assistantProviderCatalog;
if (normalizedProviderId.isNotEmpty) {
for (final provider in catalog) {
@ -609,12 +614,18 @@ class AppController extends ChangeNotifier {
sessionKey,
);
final thread = taskThreadForSessionInternal(normalizedSessionKey);
final executionTarget = assistantExecutionTargetForSession(
normalizedSessionKey,
);
if (executionTarget.isGateway) {
return SingleAgentProvider.openclaw;
}
return resolveAssistantProvider(thread?.executionBinding.providerId);
}
List<AssistantExecutionTarget> visibleAssistantExecutionTargets(
Iterable<AssistantExecutionTarget> supportedTargets,
) => const <AssistantExecutionTarget>[AssistantExecutionTarget.gateway];
) => compactAssistantExecutionTargets(supportedTargets);
List<String> get aiGatewayConversationModelChoices {
final availableModels =

View File

@ -64,10 +64,14 @@ extension AppControllerDesktopExternalAcpRouting on AppController {
.where((item) => item.trim().isNotEmpty)
.toList(growable: false);
final currentTarget = assistantExecutionTargetForSession(
normalizedSessionKey,
);
final resolvedProvider = assistantProviderForSession(normalizedSessionKey);
final resolvedExplicitProviderId =
thread?.hasExplicitProviderSelection == true &&
!resolvedProvider.isUnspecified
final resolvedExplicitProviderId = currentTarget.isGateway
? kCanonicalGatewayProviderId
: thread?.hasExplicitProviderSelection == true &&
!resolvedProvider.isUnspecified
? resolvedProvider.providerId
: '';
final resolvedExplicitModel = thread?.hasExplicitModelSelection ?? false
@ -85,9 +89,7 @@ extension AppControllerDesktopExternalAcpRouting on AppController {
explicitExecutionTarget?.trim().isNotEmpty == true
? explicitExecutionTarget!.trim()
: hasAnyExplicitSelection
? _routingExecutionTargetValueInternal(
assistantExecutionTargetForSession(normalizedSessionKey),
)
? _routingExecutionTargetValueInternal(currentTarget)
: '';
final hasExplicitSelection =
resolvedExplicitExecutionTarget.isNotEmpty ||
@ -115,8 +117,6 @@ extension AppControllerDesktopExternalAcpRouting on AppController {
}
String _routingExecutionTargetValueInternal(AssistantExecutionTarget target) {
return switch (target) {
AssistantExecutionTarget.gateway => 'gateway',
};
return target.promptValue;
}
}

View File

@ -187,6 +187,7 @@ GatewayMode bridgeGatewayModeRuntimeInternal(AppController controller) {
return GatewayMode.offline;
}
return switch (controller.currentAssistantExecutionTarget) {
AssistantExecutionTarget.agent => GatewayMode.remote,
AssistantExecutionTarget.gateway => GatewayMode.remote,
};
}

View File

@ -250,8 +250,9 @@ extension AppControllerDesktopSkillPermissions on AppController {
final nextExecutionTarget =
executionTarget ??
switch (existing?.executionBinding.executionMode) {
ThreadExecutionMode.agent => AssistantExecutionTarget.agent,
ThreadExecutionMode.gateway => AssistantExecutionTarget.gateway,
null => AssistantExecutionTarget.gateway,
null => AssistantExecutionTarget.agent,
};
final nextImportedSkills =
importedSkills ??
@ -293,11 +294,15 @@ extension AppControllerDesktopSkillPermissions on AppController {
final requestedProvider = singleAgentProvider?.isUnspecified == false
? singleAgentProvider
: null;
final nextProvider = resolveAssistantProvider(
final nextProviderId = normalizeSingleAgentProviderId(
requestedProvider?.providerId ??
existing?.executionBinding.providerId ??
existing?.contextState.latestResolvedProviderId,
existing?.contextState.latestResolvedProviderId ??
'',
);
final nextProvider = nextProviderId.isEmpty
? SingleAgentProvider.unspecified
: resolveAssistantProvider(nextProviderId);
final nextProviderSource =
singleAgentProviderSource ??
existing?.executionBinding.providerSource ??
@ -306,13 +311,18 @@ extension AppControllerDesktopSkillPermissions on AppController {
(executionBinding ??
existing?.executionBinding ??
ExecutionBinding(
executionMode: ThreadExecutionMode.gateway,
executionMode:
threadExecutionModeFromAssistantExecutionTarget(
nextExecutionTarget,
),
executorId: nextProvider.providerId,
providerId: nextProvider.providerId,
endpointId: '',
))
.copyWith(
executionMode: ThreadExecutionMode.gateway,
executionMode: threadExecutionModeFromAssistantExecutionTarget(
nextExecutionTarget,
),
executorId: nextProvider.providerId,
providerId: nextProvider.providerId,
executionModeSource:

View File

@ -219,22 +219,30 @@ extension AppControllerDesktopThreadBinding on AppController {
required AssistantExecutionTarget executionTarget,
ExecutionBinding? existingBinding,
}) {
final selectedProvider = resolveAssistantProvider(
existingBinding?.providerId,
final persistedProviderId = normalizeSingleAgentProviderId(
existingBinding?.providerId ?? '',
);
final selectedProvider = persistedProviderId.isEmpty
? SingleAgentProvider.unspecified
: resolveAssistantProvider(persistedProviderId);
return (existingBinding ??
ExecutionBinding(
executionMode: ThreadExecutionMode.gateway,
executionMode: threadExecutionModeFromAssistantExecutionTarget(
executionTarget,
),
executorId: selectedProvider.providerId,
providerId: selectedProvider.providerId,
endpointId: '',
))
.copyWith(
executionMode: ThreadExecutionMode.gateway,
executionMode: threadExecutionModeFromAssistantExecutionTarget(
executionTarget,
),
executorId: selectedProvider.providerId,
providerId: selectedProvider.providerId,
providerSource:
existingBinding?.providerSource ?? ThreadSelectionSource.inherited,
existingBinding?.providerSource ??
ThreadSelectionSource.inherited,
);
}

View File

@ -303,7 +303,9 @@ extension AppControllerDesktopThreadSessions on AppController {
final sessionKey = normalizedAssistantSessionKeyInternal(
sessionsControllerInternal.currentSessionKey,
);
final items = List<GatewayChatMessage>.from(chatControllerInternal.messages);
final items = List<GatewayChatMessage>.from(
chatControllerInternal.messages,
);
final threadItems = assistantThreadMessagesInternal[sessionKey];
if (threadItems != null && threadItems.isNotEmpty) {
items.addAll(threadItems);
@ -312,7 +314,8 @@ extension AppControllerDesktopThreadSessions on AppController {
if (localItems != null && localItems.isNotEmpty) {
items.addAll(localItems);
}
final streaming = chatControllerInternal.streamingAssistantText?.trim() ?? '';
final streaming =
chatControllerInternal.streamingAssistantText?.trim() ?? '';
if (streaming.isNotEmpty) {
items.add(
GatewayChatMessage(
@ -419,8 +422,17 @@ AssistantExecutionTarget resolveAssistantExecutionTargetFromRecordsForTest(
}) {
final record = primaryRecord ?? fallbackRecord;
return record == null
? AssistantExecutionTarget.gateway
: assistantExecutionTargetFromExecutionMode(
record.executionBinding.executionMode,
);
? AssistantExecutionTarget.agent
: (() {
final resolved = assistantExecutionTargetFromExecutionMode(
record.executionBinding.executionMode,
);
if (resolved.isGateway &&
isBridgeOwnedSingleAgentProviderId(
record.executionBinding.providerId,
)) {
return AssistantExecutionTarget.agent;
}
return resolved;
})();
}

View File

@ -62,7 +62,9 @@ extension AppControllerDesktopThreadStorage on AppController {
}
Future<void> ensureActiveAssistantThreadInternal() async {
if (!isAssistantTaskArchived(sessionsControllerInternal.currentSessionKey)) {
if (!isAssistantTaskArchived(
sessionsControllerInternal.currentSessionKey,
)) {
return;
}
final fallback = assistantSessionSummariesInternal().firstWhere(
@ -683,11 +685,18 @@ extension AppControllerDesktopThreadStorage on AppController {
record.executionBinding.executionMode,
),
);
const recordProvider = SingleAgentProvider(
providerId: kCanonicalGatewayProviderId,
label: kCanonicalGatewayProviderLabel,
badge: 'OC',
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 workspaceBinding = record.workspaceBinding.copyWith(
workspaceId: sessionKey,
displayPath: record.workspaceKind == WorkspaceKind.localFs
@ -707,19 +716,19 @@ extension AppControllerDesktopThreadStorage on AppController {
)
.toList(growable: false),
assistantModelId: record.assistantModelId.trim().isEmpty
? resolvedAssistantModelForTargetInternal(recordExecutionTarget)
? resolvedAssistantModelForTargetInternal(normalizedExecutionTarget)
: record.assistantModelId.trim(),
gatewayEntryState: (record.gatewayEntryState ?? '').trim().isEmpty
? gatewayEntryStateForTargetInternal(recordExecutionTarget)
? gatewayEntryStateForTargetInternal(normalizedExecutionTarget)
: record.gatewayEntryState,
workspaceBinding: workspaceBinding,
executionBinding: record.executionBinding.copyWith(
executionMode: threadExecutionModeFromAssistantExecutionTarget(
recordExecutionTarget,
normalizedExecutionTarget,
),
executorId: recordProvider.providerId,
providerId: recordProvider.providerId,
providerSource: ThreadSelectionSource.inherited,
providerSource: record.executionBinding.providerSource,
),
lifecycleState: record.lifecycleState.copyWith(status: 'ready'),
);

View File

@ -122,17 +122,27 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
if (!assistantThreadRecordsInternal.containsKey(sessionKey)) {
initializeAssistantThreadContext(
sessionKey,
executionTarget: assistantExecutionTargetForSession(sessionKey),
executionTarget: AssistantExecutionTarget.agent,
messageViewMode: assistantMessageViewModeForSession(sessionKey),
);
}
upsertTaskThreadInternal(
sessionKey,
executionTarget: AssistantExecutionTarget.agent,
executionTargetSource: ThreadSelectionSource.explicit,
singleAgentProvider: resolvedProvider,
singleAgentProviderSource: ThreadSelectionSource.explicit,
gatewayEntryState: gatewayEntryStateForTargetInternal(
AssistantExecutionTarget.agent,
),
latestResolvedProviderId: '',
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
await applyAssistantExecutionTargetInternal(
AssistantExecutionTarget.agent,
sessionKey: sessionKey,
persistDefaultSelection: true,
);
await flushAssistantThreadPersistenceInternal();
recomputeTasksInternal();
notifyIfActiveInternal();

View File

@ -520,16 +520,19 @@ class UiFeatureAccess {
}
List<AssistantExecutionTarget> get availableExecutionTargets {
if (supportsRelayGateway) {
return const <AssistantExecutionTarget>[AssistantExecutionTarget.gateway];
}
return const <AssistantExecutionTarget>[AssistantExecutionTarget.gateway];
return const <AssistantExecutionTarget>[
AssistantExecutionTarget.agent,
AssistantExecutionTarget.gateway,
];
}
AssistantExecutionTarget sanitizeExecutionTarget(
AssistantExecutionTarget? target,
) {
return AssistantExecutionTarget.gateway;
final resolved = target ?? AssistantExecutionTarget.agent;
return availableExecutionTargets.contains(resolved)
? resolved
: AssistantExecutionTarget.agent;
}
}

View File

@ -82,7 +82,7 @@ class AssistantTaskRailStateInternal extends State<AssistantTaskRailInternal> {
final groupedTasks = groupTasksForRailInternal(
tasks,
widget.controller.visibleAssistantExecutionTargets(
const <AssistantExecutionTarget>[AssistantExecutionTarget.gateway],
AssistantExecutionTarget.values,
),
);
final runningCount = tasks
@ -578,10 +578,7 @@ class AssistantEmptyStateInternal extends StatelessWidget {
? appText('开始输入', 'Start typing')
: reconnectAvailable
? appText('重新连接 Bridge', 'Reconnect bridge')
: appText(
'连接 Bridge',
'Connect xworkmate-bridge',
),
: appText('连接 Bridge', 'Connect xworkmate-bridge'),
),
style: FilledButton.styleFrom(
minimumSize: const Size(0, 28),

View File

@ -198,7 +198,7 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
}
void handleControllerChangedInternal() {
if (!mounted || !skillPickerPortalControllerInternal.isShowing) {
if (!mounted) {
return;
}
setState(() {});
@ -379,13 +379,18 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
PopupMenuButton<AssistantExecutionTarget>(
key: const Key('assistant-execution-target-button'),
tooltip: appText('任务对话模式', 'Task Dialog Mode'),
onSelected: (value) {
onSelected: (value) async {
final resolvedTarget =
resolveGatewayExecutionTargetFromVisibleTargets(
resolveAssistantExecutionTargetFromVisibleTargets(
visibleExecutionTargets,
currentTarget: executionTarget,
currentTarget: value,
);
controller.setAssistantExecutionTarget(resolvedTarget);
await controller.setAssistantExecutionTarget(
resolvedTarget,
);
if (mounted) {
setState(() {});
}
},
itemBuilder: (context) => compactExecutionTargets
.map(
@ -420,7 +425,7 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
),
const SizedBox(width: 4),
],
if (availableProviders.isNotEmpty) ...[
if (executionTarget.isAgent && availableProviders.isNotEmpty) ...[
PopupMenuButton<String>(
key: const Key('assistant-provider-button'),
tooltip: appText('智能体 Provider', 'Agent Provider'),

View File

@ -195,6 +195,7 @@ class ComposerToolbarChipStateInternal
extension AssistantExecutionTargetIconInternal on AssistantExecutionTarget {
IconData get icon => switch (this) {
AssistantExecutionTarget.agent => Icons.cloud_outlined,
AssistantExecutionTarget.gateway => Icons.cloud_outlined,
};
}

View File

@ -120,9 +120,7 @@ extension AssistantPageStateActionsInternal on AssistantPageStateInternal {
taskSeedsInternal[controller.currentSessionKey]?.title ??
fallbackSessionTitleInternal(controller.currentSessionKey),
preview: rawPrompt,
status:
controller.hasAssistantPendingRun ||
connectionState.connected
status: controller.hasAssistantPendingRun || connectionState.connected
? 'running'
: 'queued',
owner: autoAgent?.name ?? conversationOwnerLabelInternal(controller),
@ -408,7 +406,7 @@ extension AssistantPageStateActionsInternal on AssistantPageStateInternal {
final inheritedTarget = pickDraftThreadExecutionTargetInternal(
currentTarget: widget.controller.currentAssistantExecutionTarget,
visibleTargets: widget.controller.visibleAssistantExecutionTargets(
const <AssistantExecutionTarget>[AssistantExecutionTarget.gateway],
AssistantExecutionTarget.values,
),
localWorkspaceAvailable: widget.controller.settings.workspacePath
.trim()
@ -515,9 +513,7 @@ extension AssistantPageStateActionsInternal on AssistantPageStateInternal {
surface: 'Assistant',
executionTarget: resolvedVisibleExecutionTargetInternal(
widget.controller,
supportedTargets: const <AssistantExecutionTarget>[
AssistantExecutionTarget.gateway,
],
supportedTargets: AssistantExecutionTarget.values,
),
isCurrent: true,
draft: true,

View File

@ -228,8 +228,7 @@ class GoTaskServiceRequest {
multiAgent ||
collaborationMode == GoTaskServiceCollaborationMode.multiAgent;
AssistantExecutionTarget get normalizedTarget =>
target.isGateway ? AssistantExecutionTarget.gateway : target;
AssistantExecutionTarget get normalizedTarget => target;
GoTaskServiceRoute get route {
if (isMultiAgentRequest) {
@ -243,7 +242,7 @@ class GoTaskServiceRequest {
}
String get routingExecutionTarget {
return 'gateway';
return normalizedTarget.promptValue;
}
bool get hasInlineAttachments => inlineAttachments.isNotEmpty;
@ -307,9 +306,11 @@ class GoTaskServiceRequest {
ExternalCodeAgentAcpRoutingConfig _synthesizedRouting() {
final gatewayTarget = normalizedTarget;
final preferredGatewayTarget = switch (gatewayTarget) {
AssistantExecutionTarget.agent => kCanonicalGatewayProviderId,
AssistantExecutionTarget.gateway => kCanonicalGatewayProviderId,
};
final explicitExecutionTarget = switch (gatewayTarget) {
AssistantExecutionTarget.agent => 'agent',
AssistantExecutionTarget.gateway => 'gateway',
};
final explicitProviderId = provider.isUnspecified

View File

@ -42,55 +42,63 @@ bool isLegacyAutoAssistantExecutionTargetValue(String? value) {
return value?.trim().toLowerCase() == 'auto';
}
enum AssistantExecutionTarget { gateway }
enum AssistantExecutionTarget { agent, gateway }
extension AssistantExecutionTargetCopy on AssistantExecutionTarget {
String get label => switch (this) {
AssistantExecutionTarget.agent => appText('智能体', 'Agent'),
AssistantExecutionTarget.gateway => appText('Gateway', 'Gateway'),
};
String get promptValue => switch (this) {
AssistantExecutionTarget.agent => 'agent',
AssistantExecutionTarget.gateway => 'gateway',
};
bool get isAgent => this == AssistantExecutionTarget.agent;
bool get isGateway => this == AssistantExecutionTarget.gateway;
String get compactLabel => switch (this) {
AssistantExecutionTarget.agent => appText('智能体', 'Agent'),
AssistantExecutionTarget.gateway => appText('Gateway', 'Gateway'),
};
static AssistantExecutionTarget fromJsonValue(String? value) {
return AssistantExecutionTarget.gateway;
return AssistantExecutionTarget.values.firstWhere(
(item) => item.name == value?.trim() || item.promptValue == value?.trim(),
orElse: () => AssistantExecutionTarget.agent,
);
}
}
List<AssistantExecutionTarget> compactAssistantExecutionTargets(
Iterable<AssistantExecutionTarget> targets,
) {
if (targets.contains(AssistantExecutionTarget.gateway)) {
return const <AssistantExecutionTarget>[AssistantExecutionTarget.gateway];
final ordered = <AssistantExecutionTarget>[];
for (final candidate in AssistantExecutionTarget.values) {
if (targets.contains(candidate)) {
ordered.add(candidate);
}
}
return const <AssistantExecutionTarget>[AssistantExecutionTarget.gateway];
return ordered.isEmpty ? AssistantExecutionTarget.values : ordered;
}
AssistantExecutionTarget collapseAssistantExecutionTargetForDisplay(
AssistantExecutionTarget target,
) => target;
AssistantExecutionTarget resolveGatewayExecutionTargetFromVisibleTargets(
AssistantExecutionTarget resolveAssistantExecutionTargetFromVisibleTargets(
Iterable<AssistantExecutionTarget> visibleTargets, {
AssistantExecutionTarget? currentTarget,
}) {
final visible = visibleTargets.toList(growable: false);
if (currentTarget != null && currentTarget.isGateway) {
if (visible.contains(AssistantExecutionTarget.gateway)) {
return AssistantExecutionTarget.gateway;
}
if (currentTarget != null && visible.contains(currentTarget)) {
return currentTarget;
}
if (visible.contains(AssistantExecutionTarget.gateway)) {
return AssistantExecutionTarget.gateway;
if (visible.isNotEmpty) {
return visible.first;
}
return AssistantExecutionTarget.gateway;
return AssistantExecutionTarget.agent;
}
String normalizeSingleAgentProviderId(String value) {
@ -211,6 +219,12 @@ class SingleAgentProvider {
badge: 'G',
);
static const SingleAgentProvider openclaw = SingleAgentProvider(
providerId: kCanonicalGatewayProviderId,
label: kCanonicalGatewayProviderLabel,
badge: 'OC',
);
final String providerId;
final String label;
final String badge;
@ -257,6 +271,7 @@ class SingleAgentProvider {
'opencode' => opencode,
'claude' => claude,
'gemini' => gemini,
kCanonicalGatewayProviderId => openclaw,
'auto' || '' => unspecified,
_ => SingleAgentProvider(
providerId: normalized,

View File

@ -513,11 +513,14 @@ bool isLegacyAutoThreadExecutionModeValue(String? value) {
return value?.trim().toLowerCase() == 'auto';
}
enum ThreadExecutionMode { gateway }
enum ThreadExecutionMode { agent, gateway }
extension ThreadExecutionModeCopy on ThreadExecutionMode {
static ThreadExecutionMode fromJsonValue(String? value) {
return ThreadExecutionMode.gateway;
return ThreadExecutionMode.values.firstWhere(
(item) => item.name == value?.trim(),
orElse: () => ThreadExecutionMode.gateway,
);
}
}
@ -711,13 +714,19 @@ class ExecutionBinding {
ThreadExecutionMode threadExecutionModeFromAssistantExecutionTarget(
AssistantExecutionTarget target,
) {
return ThreadExecutionMode.gateway;
return switch (target) {
AssistantExecutionTarget.agent => ThreadExecutionMode.agent,
AssistantExecutionTarget.gateway => ThreadExecutionMode.gateway,
};
}
AssistantExecutionTarget assistantExecutionTargetFromExecutionMode(
ThreadExecutionMode mode,
) {
return AssistantExecutionTarget.gateway;
return switch (mode) {
ThreadExecutionMode.agent => AssistantExecutionTarget.agent,
ThreadExecutionMode.gateway => AssistantExecutionTarget.gateway,
};
}
WorkspaceRefKind workspaceRefKindFromWorkspaceKind(WorkspaceKind kind) {

View File

@ -111,7 +111,7 @@ class SettingsSnapshot {
accountLocalMode: true,
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults(),
linuxDesktop: LinuxDesktopConfig.defaults(),
assistantExecutionTarget: AssistantExecutionTarget.gateway,
assistantExecutionTarget: AssistantExecutionTarget.agent,
assistantPermissionLevel: AssistantPermissionLevel.defaultAccess,
);
}

View File

@ -646,6 +646,7 @@ String _sidebarTaskUpdatedAtLabel(double? updatedAtMs) {
IconData _sidebarTaskTargetIcon(AssistantExecutionTarget target) {
return switch (target) {
AssistantExecutionTarget.agent => Icons.hub_rounded,
AssistantExecutionTarget.gateway => Icons.cloud_outlined,
};
}

View File

@ -9,6 +9,60 @@ import 'package:xworkmate/widgets/surface_card.dart';
void main() {
group('AssistantLowerPaneInternal', () {
testWidgets('shows agent and gateway task dialog modes', (tester) async {
final controller = AppController();
addTearDown(controller.dispose);
await controller.sessionsController.switchSession('session-1');
await tester.pumpWidget(
_buildTestApp(child: _buildLowerPane(controller: controller)),
);
await tester.pumpAndSettle();
expect(
find.byKey(const Key('assistant-provider-button')),
findsOneWidget,
);
await tester.tap(
find.byKey(const Key('assistant-execution-target-button')),
);
await tester.pumpAndSettle();
expect(
find.byKey(const Key('assistant-execution-target-menu-item-agent')),
findsOneWidget,
);
expect(
find.byKey(const Key('assistant-execution-target-menu-item-gateway')),
findsOneWidget,
);
await tester.tap(
find.byKey(const Key('assistant-execution-target-menu-item-gateway')),
);
await tester.pumpAndSettle();
expect(controller.assistantExecutionTarget.name, 'gateway');
expect(find.byKey(const Key('assistant-provider-button')), findsNothing);
await tester.tap(
find.byKey(const Key('assistant-execution-target-button')),
);
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const Key('assistant-execution-target-menu-item-agent')),
);
await tester.pumpAndSettle();
expect(controller.assistantExecutionTarget.name, 'agent');
expect(
find.byKey(const Key('assistant-provider-button')),
findsOneWidget,
);
});
testWidgets('shows assistant providers and allows switching provider', (
tester,
) async {
@ -18,9 +72,7 @@ void main() {
await controller.sessionsController.switchSession('session-1');
await tester.pumpWidget(
_buildTestApp(
child: _buildLowerPane(controller: controller),
),
_buildTestApp(child: _buildLowerPane(controller: controller)),
);
await tester.pumpAndSettle();
@ -46,7 +98,8 @@ void main() {
await tester.pumpAndSettle();
expect(
controller.assistantProviderForSession(controller.currentSessionKey)
controller
.assistantProviderForSession(controller.currentSessionKey)
.providerId,
'opencode',
);
@ -88,9 +141,7 @@ Widget _buildTestApp({required Widget child}) {
return MaterialApp(
theme: AppTheme.light(),
home: Material(
child: Center(
child: SizedBox(width: 1400, height: 360, child: child),
),
child: Center(child: SizedBox(width: 1400, height: 360, child: child)),
),
);
}

View File

@ -0,0 +1,49 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
void main() {
group('AssistantExecutionTarget', () {
test('maps agent and gateway values without collapsing them', () {
expect(
threadExecutionModeFromAssistantExecutionTarget(
AssistantExecutionTarget.agent,
),
ThreadExecutionMode.agent,
);
expect(
threadExecutionModeFromAssistantExecutionTarget(
AssistantExecutionTarget.gateway,
),
ThreadExecutionMode.gateway,
);
expect(
assistantExecutionTargetFromExecutionMode(ThreadExecutionMode.agent),
AssistantExecutionTarget.agent,
);
expect(
assistantExecutionTargetFromExecutionMode(ThreadExecutionMode.gateway),
AssistantExecutionTarget.gateway,
);
});
test('keeps both task dialog modes visible when both are supported', () {
expect(
compactAssistantExecutionTargets(const <AssistantExecutionTarget>[
AssistantExecutionTarget.agent,
AssistantExecutionTarget.gateway,
]),
const <AssistantExecutionTarget>[
AssistantExecutionTarget.agent,
AssistantExecutionTarget.gateway,
],
);
});
test('recognizes openclaw as the canonical gateway provider', () {
final provider = SingleAgentProvider.fromJsonValue('openclaw');
expect(provider.providerId, kCanonicalGatewayProviderId);
expect(provider.label, kCanonicalGatewayProviderLabel);
});
});
}