Refresh agent providers on agent selection

This commit is contained in:
Haitao Pan 2026-04-13 14:03:21 +08:00
parent e7a5a7a5c6
commit fc3c06e605
2 changed files with 147 additions and 0 deletions

View File

@ -53,6 +53,23 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
final currentTarget = assistantExecutionTargetForSession(
sessionsControllerInternal.currentSessionKey,
);
final shouldRefreshAgentProviders =
resolvedTarget.isAgent && assistantProviderCatalog.isEmpty;
if (shouldRefreshAgentProviders) {
try {
await refreshSingleAgentCapabilitiesInternal(forceRefresh: true);
} catch (_) {
// Keep target selection interactive even when a just-in-time
// capabilities refresh fails. The provider picker will remain hidden
// until the next successful refresh.
}
if (currentTarget == resolvedTarget &&
settings.assistantExecutionTarget == resolvedTarget) {
recomputeTasksInternal();
notifyIfActiveInternal();
return;
}
}
if (currentTarget == resolvedTarget &&
settings.assistantExecutionTarget == resolvedTarget) {
return;

View File

@ -1,5 +1,10 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/app/app_controller.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
void main() {
group('AssistantExecutionTarget', () {
@ -45,5 +50,130 @@ void main() {
expect(provider.providerId, kCanonicalGatewayProviderId);
expect(provider.label, kCanonicalGatewayProviderLabel);
});
test(
'refreshes agent provider catalog when agent mode is selected with an empty catalog',
() async {
final capture = await _startCapabilityServer();
addTearDown(capture.close);
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-agent-provider-refresh-',
);
addTearDown(() async {
if (await storeRoot.exists()) {
await storeRoot.delete(recursive: true);
}
});
final store = SecureConfigStore(
secretRootPathResolver: () async => '${storeRoot.path}/secrets',
appDataRootPathResolver: () async => '${storeRoot.path}/app-data',
supportRootPathResolver: () async => '${storeRoot.path}/support',
enableSecureStorage: false,
);
await store.initialize();
await store.saveAccountSyncState(
AccountSyncState.defaults().copyWith(
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
bridgeServerUrl: capture.baseEndpoint.toString(),
),
syncState: 'ready',
),
);
final controller = AppController(
store: store,
environmentOverride: const <String, String>{
'BRIDGE_AUTH_TOKEN': 'bridge-token',
},
);
addTearDown(controller.dispose);
await controller.sessionsController.switchSession('session-1');
await _waitForRequest(capture, minimumCount: 1);
expect(controller.assistantProviderCatalog, isEmpty);
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.agent,
);
expect(
controller.assistantProviderCatalog.map((item) => item.providerId),
containsAll(<String>['codex', 'opencode', 'gemini']),
);
expect(capture.requestCount, greaterThanOrEqualTo(2));
expect(capture.lastAuthorizationHeader, 'Bearer bridge-token');
},
);
});
}
Future<void> _waitForRequest(
_CapabilityServerCapture capture, {
required int minimumCount,
}) async {
for (var index = 0; index < 20; index += 1) {
if (capture.requestCount >= minimumCount) {
return;
}
await Future<void>.delayed(const Duration(milliseconds: 100));
}
fail('Timed out waiting for $minimumCount capability requests');
}
Future<_CapabilityServerCapture> _startCapabilityServer() async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
final capture = _CapabilityServerCapture._(
server,
Uri.parse('http://127.0.0.1:${server.port}'),
);
server.listen((request) async {
capture.requestCount += 1;
capture.lastAuthorizationHeader =
request.headers.value(HttpHeaders.authorizationHeader) ?? '';
await utf8.decoder.bind(request).join();
if (capture.requestCount == 1) {
request.response.statusCode = HttpStatus.internalServerError;
request.response.headers.contentType = ContentType.json;
request.response.write(
jsonEncode(<String, dynamic>{
'error': <String, dynamic>{'message': 'startup refresh failed'},
}),
);
await request.response.close();
return;
}
request.response.headers.contentType = ContentType.json;
request.response.write(
jsonEncode(<String, dynamic>{
'jsonrpc': '2.0',
'id': 'capabilities',
'result': <String, dynamic>{
'singleAgent': true,
'multiAgent': true,
'providerCatalog': <Map<String, dynamic>>[
<String, dynamic>{'providerId': 'codex', 'label': 'Codex'},
<String, dynamic>{'providerId': 'opencode', 'label': 'OpenCode'},
<String, dynamic>{'providerId': 'gemini', 'label': 'Gemini'},
],
},
}),
);
await request.response.close();
});
return capture;
}
class _CapabilityServerCapture {
_CapabilityServerCapture._(this._server, this.baseEndpoint);
final HttpServer _server;
final Uri baseEndpoint;
int requestCount = 0;
String lastAuthorizationHeader = '';
Future<void> close() => _server.close(force: true);
}