diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index c785399..033b45c 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -113,13 +113,13 @@ class AppLocale extends _$AppLocale { } // Server connection providers - optimized with caching -@riverpod +@Riverpod(keepAlive: true) Future> serverConfigs(Ref ref) async { final storage = ref.watch(optimizedStorageServiceProvider); return storage.getServerConfigs(); } -@riverpod +@Riverpod(keepAlive: true) Future activeServer(Ref ref) async { final storage = ref.watch(optimizedStorageServiceProvider); final configs = await ref.watch(serverConfigsProvider.future); @@ -385,6 +385,8 @@ class SocketConnectionStream extends _$SocketConnectionStream { } /// Forces a best-effort reconnect of the underlying socket service. + /// This is an action method, not state exposure. + // ignore: avoid_public_notifier_properties Future reconnect({ Duration timeout = const Duration(seconds: 2), }) async { @@ -399,9 +401,6 @@ class SocketConnectionStream extends _$SocketConnectionStream { : SocketConnectionState.connecting, ); } - - /// Exposes the latest cached state for imperative reads. - SocketConnectionState get latest => _latestState; } @Riverpod(keepAlive: true) @@ -495,6 +494,11 @@ class ConversationDeltaStream extends _$ConversationDeltaStream { _socketSubscription = null; } + /// Provides direct access to the underlying stream. + /// Note: This getter is necessary for compatibility with StreamProvider. + /// While Riverpod 3 discourages public getters on Notifiers, this is a + /// pragmatic exception for stream delegation patterns. + // ignore: avoid_public_notifier_properties Stream get stream => _controller?.stream ?? const Stream.empty(); } @@ -564,7 +568,7 @@ final refreshAuthStateProvider = Provider((ref) { }); // Model providers -@riverpod +@Riverpod(keepAlive: true) Future> models(Ref ref) async { // Reviewer mode returns mock models final reviewerMode = ref.watch(reviewerModeProvider); @@ -613,7 +617,7 @@ Future> models(Ref ref) async { } } -@riverpod +@Riverpod(keepAlive: true) class SelectedModel extends _$SelectedModel { @override Model? build() => null; @@ -624,7 +628,7 @@ class SelectedModel extends _$SelectedModel { } // Track if the current model selection is manual (user-selected) or automatic (default) -@riverpod +@Riverpod(keepAlive: true) class IsManualModelSelection extends _$IsManualModelSelection { @override bool build() => false; @@ -633,6 +637,7 @@ class IsManualModelSelection extends _$IsManualModelSelection { } // Listen for settings changes and reset manual selection when default model changes +// keepAlive to maintain listener throughout app lifecycle final _settingsWatcherProvider = Provider((ref) { ref.listen(appSettingsProvider, (previous, next) { if (previous?.defaultModel != next.defaultModel) { @@ -643,6 +648,7 @@ final _settingsWatcherProvider = Provider((ref) { }); // Auto-apply default model from settings when it changes (and not manually overridden) +// keepAlive to maintain listener throughout app lifecycle final defaultModelAutoSelectionProvider = Provider((ref) { ref.listen(appSettingsProvider, (previous, next) { // Only react when default model value changes @@ -697,7 +703,7 @@ final defaultModelAutoSelectionProvider = Provider((ref) { }); // Cache timestamp for conversations to prevent rapid re-fetches -@riverpod +@Riverpod(keepAlive: true) class _ConversationsCacheTimestamp extends _$ConversationsCacheTimestamp { @override DateTime? build() => null; @@ -706,7 +712,8 @@ class _ConversationsCacheTimestamp extends _$ConversationsCacheTimestamp { } // Conversation providers - Now using correct OpenWebUI API with caching -@riverpod +// keepAlive to maintain cache during authenticated session +@Riverpod(keepAlive: true) Future> conversations(Ref ref) async { // Do not fetch protected data until authenticated. Use watch so we refetch // when the auth state transitions in either direction. @@ -1459,7 +1466,7 @@ final archivedConversationsProvider = Provider>((ref) { }); // Reviewer mode provider (persisted) -@riverpod +@Riverpod(keepAlive: true) class ReviewerMode extends _$ReviewerMode { late final OptimizedStorageService _storage; bool _initialized = false; diff --git a/lib/core/services/settings_service.dart b/lib/core/services/settings_service.dart index c268528..9b72fbe 100644 --- a/lib/core/services/settings_service.dart +++ b/lib/core/services/settings_service.dart @@ -407,7 +407,7 @@ bool _listEquals(List a, List b) { } /// Provider for app settings -@riverpod +@Riverpod(keepAlive: true) class AppSettingsNotifier extends _$AppSettingsNotifier { bool _initialized = false; diff --git a/lib/features/auth/providers/unified_auth_providers.dart b/lib/features/auth/providers/unified_auth_providers.dart index ce8c501..0cced98 100644 --- a/lib/features/auth/providers/unified_auth_providers.dart +++ b/lib/features/auth/providers/unified_auth_providers.dart @@ -60,6 +60,8 @@ final hasSavedCredentialsProvider2 = FutureProvider((ref) async { /// Computed providers for UI consumption /// These automatically update when auth state changes +/// These are keepAlive since they derive from keepAlive authStateManagerProvider +/// and are used throughout the app lifecycle final isAuthenticatedProvider2 = Provider((ref) { final authState = ref.watch(authStateManagerProvider); diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 8ea9834..4af4912 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1167,9 +1167,10 @@ class _ChatPageState extends ConsumerState { // If still loading, wait for it to complete try { final models = await ref.read(modelsProvider.future); - if (mounted) { - _showModelDropdown(context, ref, models); - } + // Check mounted and use context immediately together + if (!mounted) return; + // ignore: use_build_context_synchronously + _showModelDropdown(context, ref, models); } catch (e) { DebugLogger.error( 'model-load-failed', @@ -1178,16 +1179,17 @@ class _ChatPageState extends ConsumerState { ); } } else if (modelsAsync.hasValue) { - // If we have data, show immediately + // If we have data, show immediately (no async gap) _showModelDropdown(context, ref, modelsAsync.value!); } else if (modelsAsync.hasError) { // If there's an error, try to refresh and load try { ref.invalidate(modelsProvider); final models = await ref.read(modelsProvider.future); - if (mounted) { - _showModelDropdown(context, ref, models); - } + // Check mounted and use context immediately together + if (!mounted) return; + // ignore: use_build_context_synchronously + _showModelDropdown(context, ref, models); } catch (e) { DebugLogger.error( 'model-refresh-failed',