fix: keep agent mode selectable and omit OpenClaw model

This commit is contained in:
Haitao Pan 2026-05-13 17:44:39 +08:00
parent b9a9999291
commit bc468d8f2e
6 changed files with 118 additions and 19 deletions

View File

@ -73,7 +73,8 @@ extension AppControllerDesktopExternalAcpRouting on AppController {
!resolvedProvider.isUnspecified
? resolvedProvider.providerId
: '';
final resolvedExplicitModel = thread?.hasExplicitModelSelection ?? false
final resolvedExplicitModel =
!currentTarget.isGateway && (thread?.hasExplicitModelSelection ?? false)
? assistantModelForSession(normalizedSessionKey)
: '';
final resolvedExplicitSkills = thread?.hasExplicitSkillSelection ?? false

View File

@ -336,7 +336,9 @@ extension AppControllerDesktopThreadActions on AppController {
}
}
final provider = assistantProviderForSession(sessionKey);
final model = assistantModelForSession(sessionKey);
final model = currentTarget.isGateway
? ''
: assistantModelForSession(sessionKey);
final routing = buildExternalAcpRoutingForSessionInternal(sessionKey);
final dispatch = await codeAgentNodeOrchestratorInternal
.buildGatewayDispatch(

View File

@ -29,16 +29,9 @@ class AssistantTaskDialogModeControlsInternal extends StatelessWidget {
if (supportedExecutionTargets.isEmpty) {
return const SizedBox.shrink();
}
final visibleExecutionTargets = controller.visibleAssistantExecutionTargets(
supportedExecutionTargets,
);
final resolutionTargets = visibleExecutionTargets.isNotEmpty
? visibleExecutionTargets
: supportedExecutionTargets;
final currentExecutionTarget =
resolveAssistantExecutionTargetFromVisibleTargets(
resolutionTargets,
supportedExecutionTargets,
currentTarget: controller.assistantExecutionTarget,
);
final executionTarget = collapseAssistantExecutionTargetForDisplay(
@ -64,7 +57,6 @@ class AssistantTaskDialogModeControlsInternal extends StatelessWidget {
controller: controller,
executionTarget: executionTarget,
supportedExecutionTargets: supportedExecutionTargets,
visibleExecutionTargets: visibleExecutionTargets,
),
_TaskDialogProviderMenuButtonInternal(
controller: controller,
@ -81,13 +73,11 @@ class _TaskDialogExecutionTargetMenuButtonInternal extends StatelessWidget {
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) {
@ -102,10 +92,8 @@ class _TaskDialogExecutionTargetMenuButtonInternal extends StatelessWidget {
},
itemBuilder: (context) => supportedExecutionTargets
.map((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: [
@ -131,7 +119,7 @@ class _TaskDialogExecutionTargetMenuButtonInternal extends StatelessWidget {
AssistantExecutionTarget value,
) async {
final resolvedTarget = resolveAssistantExecutionTargetFromVisibleTargets(
visibleExecutionTargets,
supportedExecutionTargets,
currentTarget: value,
);
await controller.setAssistantExecutionTarget(resolvedTarget);

View File

@ -140,6 +140,23 @@ class ExternalCodeAgentAcpRoutingConfig {
bool get isAuto => mode == ExternalCodeAgentAcpRoutingMode.auto;
ExternalCodeAgentAcpRoutingConfig withoutExplicitModel() {
if (explicitModel.trim().isEmpty) {
return this;
}
return ExternalCodeAgentAcpRoutingConfig(
mode: mode,
preferredGatewayTarget: preferredGatewayTarget,
explicitExecutionTarget: explicitExecutionTarget,
explicitProviderId: explicitProviderId,
explicitModel: '',
explicitSkills: explicitSkills,
allowSkillInstall: allowSkillInstall,
availableSkills: availableSkills,
installApproval: installApproval,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'routingMode': mode.name,
@ -254,7 +271,10 @@ class GoTaskServiceRequest {
routing ?? _synthesizedRouting();
Map<String, dynamic> toExternalAcpParams() {
final resolvedRouting = effectiveRouting;
final resolvedRouting = normalizedTarget.isGateway
? effectiveRouting.withoutExplicitModel()
: effectiveRouting;
final requestModel = normalizedTarget.isGateway ? '' : model.trim();
final providerId = provider.isUnspecified ? '' : provider.providerId;
final agentProviderId = normalizedTarget.isGateway ? '' : providerId;
final params = <String, dynamic>{
@ -294,7 +314,7 @@ class GoTaskServiceRequest {
if (agentProviderId.isNotEmpty) 'provider': agentProviderId,
if (remoteWorkingDirectoryHint.trim().isNotEmpty)
'remoteWorkingDirectoryHint': remoteWorkingDirectoryHint.trim(),
if (model.trim().isNotEmpty) 'model': model.trim(),
if (requestModel.isNotEmpty) 'model': requestModel,
if (thinking.trim().isNotEmpty) 'thinking': thinking.trim(),
'routing': resolvedRouting.toJson(),
if (routingHint.trim().isNotEmpty) 'routingHint': routingHint.trim(),
@ -321,7 +341,7 @@ class GoTaskServiceRequest {
final explicitProviderId = provider.isUnspecified || gatewayTarget.isGateway
? ''
: provider.providerId;
final explicitModelValue = model.trim();
final explicitModelValue = gatewayTarget.isGateway ? '' : model.trim();
final explicitSkillsValue = selectedSkills
.map((item) => item.trim())
.where((item) => item.isNotEmpty)

View File

@ -317,6 +317,50 @@ void main() {
);
});
testWidgets(
'keeps task dialog modes selectable when only OpenClaw is live',
(tester) async {
final controller = AppController(
environmentOverride: const <String, String>{},
initialGatewayProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.openclaw,
],
initialAvailableExecutionTargets: const <AssistantExecutionTarget>[
AssistantExecutionTarget.gateway,
],
);
addTearDown(controller.dispose);
await controller.sessionsController.switchSession('draft:test-task-a');
controller.initializeAssistantThreadContext(
'draft:test-task-a',
executionTarget: AssistantExecutionTarget.gateway,
messageViewMode: controller.assistantMessageViewModeForSession(
'draft:test-task-a',
),
);
controller.notifyListeners();
await tester.pumpWidget(
_buildTestApp(child: _buildLowerPane(controller: controller)),
);
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const Key('assistant-execution-target-button')),
);
await tester.pumpAndSettle();
final agentItem = tester
.widget<PopupMenuItem<AssistantExecutionTarget>>(
find.byKey(
const Key('assistant-execution-target-menu-item-agent'),
),
);
expect(agentItem.enabled, isTrue);
},
);
testWidgets('uses submit button instead of connect action', (tester) async {
final controller = AppController(
environmentOverride: const <String, String>{},

View File

@ -2066,6 +2066,50 @@ void main() {
},
);
test('OpenClaw gateway task uses the server default model', () async {
final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedGatewayController(fakeGoTaskService);
addTearDown(() {
fakeGoTaskService.completeAll();
controller.dispose();
});
await _selectGatewaySession(controller, 'openclaw-default-model-task');
await controller.selectAssistantModelForSession(
'openclaw-default-model-task',
'ollama/kimi-k2.5',
);
final taskFuture = controller.sendChatMessage('use OpenClaw default');
await fakeGoTaskService.waitForRequestCount(1);
final request = fakeGoTaskService.requests.single;
expect(request.target, AssistantExecutionTarget.gateway);
expect(request.provider, SingleAgentProvider.openclaw);
expect(request.model, isEmpty);
final params = request.toExternalAcpParams();
expect(params.containsKey('model'), isFalse);
expect(
params['routing'],
isNot(containsPair('explicitModel', 'ollama/kimi-k2.5')),
);
fakeGoTaskService.complete(
'openclaw-default-model-task',
const GoTaskServiceResult(
success: true,
message: 'result',
turnId: 'turn-openclaw-default-model',
raw: <String, dynamic>{},
errorMessage: '',
resolvedModel: '',
route: GoTaskServiceRoute.externalAcpSingle,
),
);
await taskFuture;
});
test(
'abortRun removes a queued OpenClaw task without bridge cancel',
() async {