xworkmate-app/lib/app/app_controller_desktop_settings_runtime.dart

766 lines
26 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ignore_for_file: unused_import, unnecessary_import
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'app_metadata.dart';
import 'app_capabilities.dart';
import 'app_store_policy.dart';
import 'ui_feature_manifest.dart';
import '../i18n/app_language.dart';
import '../models/app_models.dart';
import '../runtime/device_identity_store.dart';
import '../runtime/runtime_bootstrap.dart';
import '../runtime/desktop_platform_service.dart';
import '../runtime/gateway_runtime.dart';
import '../runtime/runtime_controllers.dart';
import '../runtime/runtime_models.dart';
import '../runtime/secret_store.dart';
import '../runtime/settings_store.dart';
import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart';
import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart';
import '../runtime/code_agent_node_orchestrator.dart';
import '../runtime/assistant_artifacts.dart';
import '../runtime/desktop_thread_artifact_service.dart';
import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart';
import '../runtime/platform_environment.dart';
import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart';
import 'app_controller_desktop_gateway.dart';
import 'app_controller_desktop_settings.dart';
import 'app_controller_desktop_thread_binding.dart';
import 'app_controller_desktop_thread_sessions.dart';
import 'app_controller_desktop_thread_actions.dart';
import 'app_controller_desktop_workspace_execution.dart';
import 'app_controller_desktop_thread_storage.dart';
import 'app_controller_desktop_skill_permissions.dart';
import 'app_controller_desktop_runtime_helpers.dart';
// ignore_for_file: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
extension AppControllerDesktopSettingsRuntime on AppController {
Future<void> updateAiGatewaySelection(List<String> selectedModels) async {
final available = settings.aiGateway.availableModels;
final normalized = selectedModels
.map((item) => item.trim())
.where((item) => item.isNotEmpty && available.contains(item))
.toList(growable: false);
final fallbackSelection = normalized.isNotEmpty
? normalized
: available.isNotEmpty
? <String>[available.first]
: const <String>[];
final currentDefaultModel = settings.defaultModel.trim();
final resolvedDefaultModel = fallbackSelection.contains(currentDefaultModel)
? currentDefaultModel
: fallbackSelection.isNotEmpty
? fallbackSelection.first
: '';
await AppControllerDesktopSettings(this).saveSettings(
settings.copyWith(
aiGateway: settings.aiGateway.copyWith(
selectedModels: fallbackSelection,
),
defaultModel: resolvedDefaultModel,
),
refreshAfterSave: false,
);
}
Future<AiGatewayProfile> syncAiGatewayCatalog(
AiGatewayProfile profile, {
String apiKeyOverride = '',
}) async {
final synced = await settingsControllerInternal.syncAiGatewayCatalog(
profile,
apiKeyOverride: apiKeyOverride,
);
modelsControllerInternal.restoreFromSettings(
settingsControllerInternal.snapshot.aiGateway,
);
recomputeTasksInternal();
return synced;
}
Future<void> refreshDesktopIntegration() async {
desktopPlatformBusyInternal = true;
notifyListeners();
try {
await desktopPlatformServiceInternal.refresh();
} finally {
desktopPlatformBusyInternal = false;
notifyListeners();
}
}
Future<void> saveLinuxDesktopConfig(LinuxDesktopConfig config) async {
await AppControllerDesktopSettings(
this,
).saveSettings(settings.copyWith(linuxDesktop: config));
}
Future<void> setDesktopVpnMode(VpnMode mode) async {
desktopPlatformBusyInternal = true;
notifyListeners();
try {
await AppControllerDesktopSettings(this).saveSettings(
settings.copyWith(
linuxDesktop: settings.linuxDesktop.copyWith(preferredMode: mode),
),
refreshAfterSave: false,
);
await desktopPlatformServiceInternal.setMode(mode);
} finally {
desktopPlatformBusyInternal = false;
notifyListeners();
}
}
Future<void> connectDesktopTunnel() async {
desktopPlatformBusyInternal = true;
notifyListeners();
try {
await desktopPlatformServiceInternal.connectTunnel();
} finally {
desktopPlatformBusyInternal = false;
notifyListeners();
}
}
Future<void> disconnectDesktopTunnel() async {
desktopPlatformBusyInternal = true;
notifyListeners();
try {
await desktopPlatformServiceInternal.disconnectTunnel();
} finally {
desktopPlatformBusyInternal = false;
notifyListeners();
}
}
Future<void> setLaunchAtLogin(bool enabled) async {
await AppControllerDesktopSettings(this).saveSettings(
settings.copyWith(launchAtLogin: enabled),
refreshAfterSave: false,
);
}
Future<void> toggleAssistantNavigationDestination(
AssistantFocusEntry destination,
) async {
if (!kAssistantNavigationDestinationCandidates.contains(destination)) {
return;
}
if (!supportsAssistantFocusEntry(destination)) {
return;
}
final current = assistantNavigationDestinations;
final next = current.contains(destination)
? current.where((item) => item != destination).toList(growable: false)
: <AssistantFocusEntry>[...current, destination];
await saveAppUiStateInternal(
appUiState.copyWith(assistantNavigationDestinations: next),
notify: true,
);
}
Future<void> toggleAccountWorkspaceFollowed() async {
await AppControllerDesktopSettings(this).saveSettings(
settings.copyWith(
accountWorkspaceFollowed: !settings.accountWorkspaceFollowed,
),
refreshAfterSave: false,
);
}
Future<String> testOllamaConnection({required bool cloud}) {
return settingsControllerInternal.testOllamaConnection(cloud: cloud);
}
Future<String> testOllamaConnectionDraft({
required bool cloud,
required SettingsSnapshot snapshot,
String apiKeyOverride = '',
}) {
return settingsControllerInternal.testOllamaConnectionDraft(
cloud: cloud,
localConfig: snapshot.ollamaLocal,
cloudConfig: snapshot.ollamaCloud,
apiKeyOverride: apiKeyOverride,
);
}
Future<String> testVaultConnection() {
return settingsControllerInternal.testVaultConnection();
}
Future<String> testVaultConnectionDraft({
required SettingsSnapshot snapshot,
String tokenOverride = '',
}) {
return settingsControllerInternal.testVaultConnectionDraft(
snapshot.vault,
tokenOverride: tokenOverride,
);
}
Future<({String state, String message, String endpoint})>
testGatewayConnectionDraft({
required GatewayConnectionProfile profile,
required AssistantExecutionTarget executionTarget,
String tokenOverride = '',
String passwordOverride = '',
}) async {
if (profile.mode == RuntimeConnectionMode.unconfigured) {
return (
state: 'inactive',
message: appText(
'当前未配置可用 Gateway 连接。',
'No active gateway connection is configured.',
),
endpoint: '',
);
}
final temporaryRoot = await Directory.systemTemp.createTemp(
'xworkmate-gateway-test-',
);
final temporaryStore = SecureConfigStore(
enableSecureStorage: false,
appDataRootPathResolver: () async =>
'${temporaryRoot.path}/settings.sqlite3',
secretRootPathResolver: () async => temporaryRoot.path,
);
final runtime = GatewayRuntime(
store: temporaryStore,
identityStore: DeviceIdentityStore(temporaryStore),
);
await runtime.initialize();
try {
final existingIdentity = await storeInternal.loadDeviceIdentity();
if (existingIdentity != null) {
await temporaryStore.saveDeviceIdentity(existingIdentity);
final existingOperatorDeviceToken = await storeInternal.loadDeviceToken(
deviceId: existingIdentity.deviceId,
role: 'operator',
);
final trimmedExistingOperatorDeviceToken =
existingOperatorDeviceToken?.trim() ?? '';
if (trimmedExistingOperatorDeviceToken.isNotEmpty) {
await temporaryStore.saveDeviceToken(
deviceId: existingIdentity.deviceId,
role: 'operator',
token: trimmedExistingOperatorDeviceToken,
);
}
}
await runtime.connectProfile(
profile,
authTokenOverride: tokenOverride,
authPasswordOverride: passwordOverride,
);
try {
await runtime.health();
} catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace');
// Connectivity succeeded; health is best-effort for the test path.
}
final endpoint =
runtime.snapshot.remoteAddress ?? '${profile.host}:${profile.port}';
return (
state: 'success',
message: appText('连接成功。', 'Connection succeeded.'),
endpoint: endpoint,
);
} catch (error) {
return (
state: 'error',
message: error.toString(),
endpoint: '${profile.host}:${profile.port}',
);
} finally {
try {
await runtime.disconnect(clearDesiredProfile: false);
} catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace');
// Ignore teardown noise from temporary connectivity checks.
}
runtime.dispose();
temporaryStore.dispose();
try {
await temporaryRoot.delete(recursive: true);
} catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace');
// Ignore cleanup noise for temporary connectivity checks.
}
}
}
void clearRuntimeLogs() {
runtimeCoordinatorInternal.gateway.clearLogs();
notifyIfActiveInternal();
}
List<DerivedTaskItem> taskItemsForTab(String tab) => switch (tab) {
'Queue' => tasksControllerInternal.queue,
'Running' => tasksControllerInternal.running,
'History' => tasksControllerInternal.history,
'Failed' => tasksControllerInternal.failed,
'Scheduled' => tasksControllerInternal.scheduled,
_ => tasksControllerInternal.queue,
};
/// Enable Codex ↔ Gateway bridge
Future<void> enableCodexBridge() async {
if (isCodexBridgeEnabledInternal || isCodexBridgeBusyInternal) return;
if (shouldBlockEmbeddedAgentLaunch(
isAppleHost: Platform.isIOS || Platform.isMacOS,
)) {
throw StateError(
appText(
'App Store 版本不允许在应用内启动或桥接外部 CLI 进程。',
'App Store builds do not allow in-app external CLI bridge processes.',
),
);
}
isCodexBridgeBusyInternal = true;
codexBridgeErrorInternal = null;
try {
final gatewayUrl = aiGatewayUrl;
final apiKey = await loadAiGatewayApiKey();
if (gatewayUrl.isEmpty) {
throw StateError(
appText('LLM API Endpoint 未配置', 'LLM API Endpoint not configured'),
);
}
await refreshAcpCapabilitiesInternal(forceRefresh: true);
await runtimeCoordinatorInternal.configureCodexForGateway(
gatewayUrl: gatewayUrl,
apiKey: apiKey,
);
registerCodexExternalProviderInternal();
isCodexBridgeEnabledInternal = true;
codexCooperationStateInternal = CodexCooperationState.bridgeOnly;
await ensureCodexGatewayRegistrationInternal();
notifyListeners();
} catch (e) {
codexBridgeErrorInternal = e.toString();
notifyListeners();
rethrow;
} finally {
isCodexBridgeBusyInternal = false;
notifyListeners();
}
}
/// Disable Codex ↔ Gateway bridge
Future<void> disableCodexBridge() async {
if (!isCodexBridgeEnabledInternal || isCodexBridgeBusyInternal) return;
isCodexBridgeBusyInternal = true;
try {
if (runtimeInternal.isConnected &&
codeAgentBridgeRegistryInternal.isRegistered) {
await codeAgentBridgeRegistryInternal.unregister();
} else {
codeAgentBridgeRegistryInternal.clearRegistration();
}
isCodexBridgeEnabledInternal = false;
codexCooperationStateInternal = CodexCooperationState.notStarted;
codexBridgeErrorInternal = null;
notifyListeners();
} catch (e) {
codexBridgeErrorInternal = e.toString();
notifyListeners();
rethrow;
} finally {
isCodexBridgeBusyInternal = false;
notifyListeners();
}
}
Future<void> initializeInternal() async {
try {
resolvedUserHomeDirectoryInternal = environmentOverrideInternal == null
? resolveUserHomeDirectory()
: resolveUserHomeDirectoryFromControllerEnvironmentInternal(
environmentOverrideInternal,
);
await settingsControllerInternal.initialize();
final loadedAppUiState = await storeInternal.loadAppUiState();
final sanitizedAppUiState = sanitizeAppUiStateInternal(loadedAppUiState);
appUiStateInternal = sanitizedAppUiState;
if (sanitizedAppUiState.toJsonString() !=
loadedAppUiState.toJsonString()) {
await storeInternal.saveAppUiState(sanitizedAppUiState);
}
final storedAssistantThreads = await storeInternal.loadTaskThreads();
final sanitizedAssistantThreads =
discardKnownPollutedTestTaskThreadsInternal(storedAssistantThreads);
if (sanitizedAssistantThreads.length != storedAssistantThreads.length) {
await storeInternal.saveTaskThreads(sanitizedAssistantThreads);
}
final skippedInvalidThreadRecords =
storeInternal.lastSkippedInvalidTaskThreadRecords;
startupTaskThreadWarningInternal = skippedInvalidThreadRecords.isEmpty
? null
: formatStartupTaskThreadWarningInternal(skippedInvalidThreadRecords);
if (disposedInternal) {
return;
}
final bootstrap = await RuntimeBootstrapConfig.load(
workspacePathHint: settings.workspacePath,
cliPathHint: settings.cliPath,
);
if (disposedInternal) {
return;
}
final seeded = bootstrap.mergeIntoSettings(settings);
if (seeded.toJsonString() != settings.toJsonString()) {
await settingsControllerInternal.saveSnapshot(seeded);
if (disposedInternal) {
return;
}
}
final normalized = sanitizeFeatureFlagSettingsInternal(
sanitizeOllamaCloudSettingsInternal(
sanitizeCodeAgentSettingsInternal(
settingsControllerInternal.snapshot,
),
),
);
if (normalized.toJsonString() !=
settingsControllerInternal.snapshot.toJsonString()) {
await settingsControllerInternal.saveSnapshot(normalized);
if (disposedInternal) {
return;
}
}
try {
await settingsControllerInternal.restoreAccountSession();
} catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace');
// Keep initialization resilient when remote account restore fails.
}
restoreAssistantThreadsInternal(sanitizedAssistantThreads);
if (disposedInternal) {
return;
}
lastObservedSettingsSnapshotInternal = settings;
modelsControllerInternal.restoreFromSettings(settings.aiGateway);
setActiveAppLanguage(settings.appLanguage);
await desktopPlatformServiceInternal.initialize(settings.linuxDesktop);
await desktopPlatformServiceInternal.setLaunchAtLogin(
settings.launchAtLogin,
);
registerCodexExternalProviderInternal();
await refreshAcpCapabilitiesInternal(persistMountTargets: true);
if (disposedInternal) {
return;
}
final startupTarget = sanitizePersistedExecutionTargetInternal(
settings.assistantExecutionTarget,
);
agentsControllerInternal.restoreSelection(
settings
.gatewayProfileForExecutionTarget(startupTarget)
?.selectedAgentId ??
'',
);
sessionsControllerInternal.configure(
selectedAgentId: agentsControllerInternal.selectedAgentId,
defaultAgentId: '',
);
await restoreInitialAssistantSessionSelectionInternal();
await ensureActiveAssistantThreadInternal();
await ensureDesktopTaskThreadBindingInternal(currentSessionKey);
runtimeEventsSubscriptionInternal = runtimeCoordinatorInternal
.gateway
.events
.listen(handleRuntimeEventInternal);
final startupProfile = settings.gatewayProfileForExecutionTarget(
startupTarget,
);
final shouldAutoConnect =
startupProfile != null &&
startupProfile.useSetupCode &&
startupProfile.setupCode.trim().isNotEmpty;
if (shouldAutoConnect) {
try {
await AppControllerDesktopGateway(this).connectProfileInternal(
startupProfile,
profileIndex: gatewayProfileIndexForExecutionTargetInternal(
startupTarget,
),
);
} catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace');
// Keep the shell usable when auto-connect fails.
}
}
settingsDraftInternal = settings;
lastAppliedSettingsInternal = settings;
lastObservedSettingsSnapshotInternal = settings;
settingsDraftInitializedInternal = true;
settingsDraftStatusMessageInternal = '';
} catch (error) {
if (disposedInternal) {
return;
}
bootstrapErrorInternal = error.toString();
} finally {
if (!disposedInternal) {
initializingInternal = false;
notifyIfActiveInternal();
}
}
}
String formatStartupTaskThreadWarningInternal(
List<SkippedTaskThreadRecord> records,
) {
final grouped = <SkippedTaskThreadReason, List<String>>{};
for (final item in records) {
grouped.putIfAbsent(item.reason, () => <String>[]).add(item.threadId);
}
String zhSegment(SkippedTaskThreadReason reason, List<String> threadIds) {
final joined = threadIds.join(', ');
return switch (reason) {
SkippedTaskThreadReason.removedAutoExecutionMode =>
'仍使用已移除 Auto 执行模式: $joined',
SkippedTaskThreadReason.incompleteWorkspaceBinding =>
'缺少完整 workspaceBinding: $joined',
SkippedTaskThreadReason.invalidPersistedThreadData => '数据无效: $joined',
};
}
String enSegment(SkippedTaskThreadReason reason, List<String> threadIds) {
final joined = threadIds.join(', ');
return switch (reason) {
SkippedTaskThreadReason.removedAutoExecutionMode =>
'removed Auto execution mode: $joined',
SkippedTaskThreadReason.incompleteWorkspaceBinding =>
'missing a complete workspaceBinding: $joined',
SkippedTaskThreadReason.invalidPersistedThreadData =>
'invalid persisted data: $joined',
};
}
final reasons = grouped.keys.toList()
..sort((left, right) => left.index.compareTo(right.index));
final zhSummary = reasons
.map((reason) => zhSegment(reason, grouped[reason]!))
.join('');
final enSummary = reasons
.map((reason) => enSegment(reason, grouped[reason]!))
.join('; ');
return appText(
'已跳过 ${records.length} 个旧任务线程:$zhSummary',
'Skipped ${records.length} persisted task threads: $enSummary',
);
}
void markPendingApplyDomainsInternal(
SettingsSnapshot previous,
SettingsSnapshot next,
) {
final gatewayDraftKeys = <String>{
for (final profile in previous.gatewayProfiles) ...[
'secret_ref::${profile.tokenRef.trim().isEmpty ? '' : profile.tokenRef.trim()}',
'secret_ref::${profile.passwordRef.trim().isEmpty ? '' : profile.passwordRef.trim()}',
],
for (final profile in next.gatewayProfiles) ...[
'secret_ref::${profile.tokenRef.trim().isEmpty ? '' : profile.tokenRef.trim()}',
'secret_ref::${profile.passwordRef.trim().isEmpty ? '' : profile.passwordRef.trim()}',
],
}..remove('secret_ref::');
final aiGatewayDraftKeys = <String>{
'secret_ref::${next.aiGateway.apiKeyRef.trim().isEmpty ? AppController.draftAiGatewayApiKeyKeyInternal : next.aiGateway.apiKeyRef.trim()}',
'secret_ref::${previous.aiGateway.apiKeyRef.trim().isEmpty ? AppController.draftAiGatewayApiKeyKeyInternal : previous.aiGateway.apiKeyRef.trim()}',
AppController.draftAiGatewayApiKeyKeyInternal,
};
final hasGatewaySecretDraft = draftSecretValuesInternal.keys.any(
(key) => gatewayDraftKeys.contains(key) || isGatewayDraftKeyInternal(key),
);
final gatewayChanged =
jsonEncode(
previous.gatewayProfiles.map((item) => item.toJson()).toList(),
) !=
jsonEncode(
next.gatewayProfiles.map((item) => item.toJson()).toList(),
) ||
previous.assistantExecutionTarget != next.assistantExecutionTarget ||
hasGatewaySecretDraft;
final hasAiGatewaySecretDraft = draftSecretValuesInternal.keys.any(
aiGatewayDraftKeys.contains,
);
final aiGatewayChanged =
previous.aiGateway.toJson().toString() !=
next.aiGateway.toJson().toString() ||
previous.defaultModel != next.defaultModel ||
hasAiGatewaySecretDraft;
pendingGatewayApplyInternal = pendingGatewayApplyInternal || gatewayChanged;
pendingAiGatewayApplyInternal =
pendingAiGatewayApplyInternal || aiGatewayChanged;
}
Future<void> persistDraftSecretsInternal() async {
for (final entry in draftSecretValuesInternal.entries) {
final key = entry.key.trim();
final value = entry.value.trim();
if (value.isEmpty) {
continue;
}
if (key.startsWith('secret_ref::')) {
final refName = key.substring('secret_ref::'.length).trim();
if (refName.isEmpty) {
continue;
}
await settingsControllerInternal.saveSecretValueByRef(
refName,
value,
provider: settingsControllerInternal.providerNameForSecretInternal(
refName,
),
module: settingsControllerInternal.moduleForSecretInternal(refName),
);
continue;
}
if (key == AppController.draftAiGatewayApiKeyKeyInternal) {
await settingsControllerInternal.saveAiGatewayApiKey(value);
continue;
}
if (key == AppController.draftVaultTokenKeyInternal) {
await settingsControllerInternal.saveVaultToken(value);
continue;
}
if (key == AppController.draftOllamaApiKeyKeyInternal) {
await settingsControllerInternal.saveOllamaCloudApiKey(value);
}
}
draftSecretValuesInternal.clear();
}
String draftGatewayTokenKeyInternal(int profileIndex) =>
'gateway_token_$profileIndex';
String draftGatewayPasswordKeyInternal(int profileIndex) =>
'gateway_password_$profileIndex';
bool isGatewayDraftKeyInternal(String key) =>
key.startsWith('secret_ref::gateway_token_') ||
key.startsWith('secret_ref::gateway_password_') ||
key.startsWith('gateway_token_') ||
key.startsWith('gateway_password_');
Future<void> persistSettingsSnapshotInternal(
SettingsSnapshot snapshot,
) async {
final sanitized = sanitizeFeatureFlagSettingsInternal(
sanitizeOllamaCloudSettingsInternal(
sanitizeCodeAgentSettingsInternal(snapshot),
),
);
lastObservedSettingsSnapshotInternal = sanitized;
await settingsControllerInternal.saveSnapshot(sanitized);
settingsDraftInternal = sanitized;
settingsDraftInitializedInternal = true;
}
Future<void> applyPersistedSettingsSideEffectsInternal({
required SettingsSnapshot previous,
required SettingsSnapshot current,
required bool refreshAfterSave,
}) async {
setActiveAppLanguage(current.appLanguage);
agentsControllerInternal.restoreSelection(
current
.gatewayProfileForExecutionTarget(
sanitizeExecutionTargetInternal(
current.assistantExecutionTarget,
),
)
?.selectedAgentId ??
'',
);
themeModeInternal = current.themeMode;
final bridgeChanged =
previous.acpBridgeServerModeConfig.toJsonString() !=
current.acpBridgeServerModeConfig.toJsonString();
if (refreshAfterSave || bridgeChanged) {
// Re-trigger Bridge capability discovery if the mode or endpoint changed.
unawaited(refreshAcpCapabilitiesInternal());
}
if (disposedInternal) {
return;
}
if (previous.codeAgentRuntimeMode != current.codeAgentRuntimeMode) {
registerCodexExternalProviderInternal();
}
if (previous.linuxDesktop.toJson().toString() !=
current.linuxDesktop.toJson().toString() ||
previous.launchAtLogin != current.launchAtLogin) {
await desktopPlatformServiceInternal.syncConfig(current.linuxDesktop);
await desktopPlatformServiceInternal.setLaunchAtLogin(
current.launchAtLogin,
);
if (disposedInternal) {
return;
}
}
if (previous.workspacePath != current.workspacePath) {
await ensureDesktopTaskThreadBindingInternal(currentSessionKey);
if (disposedInternal) {
return;
}
}
if (refreshAfterSave) {
recomputeTasksInternal();
}
unawaited(
refreshAcpCapabilitiesInternal(
persistMountTargets: true,
).catchError((_) {}),
);
notifyListeners();
}
Future<void> applyPersistedGatewaySettingsInternal(
SettingsSnapshot snapshot,
) async {
final sessionKey = normalizedAssistantSessionKeyInternal(
sessionsControllerInternal.currentSessionKey,
);
final target = assistantExecutionTargetForSession(sessionKey);
upsertTaskThreadInternal(
sessionKey,
executionTarget: target,
executionTargetSource: ThreadSelectionSource.explicit,
gatewayEntryState: gatewayEntryStateForTargetInternal(target),
latestResolvedRuntimeModel: '',
latestResolvedProviderId: '',
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
recomputeTasksInternal();
notifyIfActiveInternal();
await applyAssistantExecutionTargetInternal(
target,
sessionKey: sessionKey,
persistDefaultSelection: false,
);
recomputeTasksInternal();
notifyIfActiveInternal();
}
}