Merge pull request #313 from cogwheel0/refactor-model-selection-restore-logic
feat(chat): Refactor model selection to use shared restore logic
This commit is contained in:
@@ -1561,7 +1561,34 @@ Future<Model?> defaultModel(Ref ref) async {
|
|||||||
DebugLogger.log('api-available', scope: 'models/default');
|
DebugLogger.log('api-available', scope: 'models/default');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Warm restore from cached resolved default model
|
// Respect manual selection if present
|
||||||
|
if (ref.read(isManualModelSelectionProvider)) {
|
||||||
|
final current = ref.read(selectedModelProvider);
|
||||||
|
if (current != null) return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Priority: user's configured default from app settings
|
||||||
|
// This ensures new chats use the user's preference (fixes #296)
|
||||||
|
final settingsDefaultId = ref.read(appSettingsProvider).defaultModel;
|
||||||
|
final storedDefaultId = settingsDefaultId ??
|
||||||
|
await SettingsService.getDefaultModel().catchError((_) => null);
|
||||||
|
|
||||||
|
if (storedDefaultId != null && storedDefaultId.isNotEmpty) {
|
||||||
|
// Try cached models first for speed
|
||||||
|
final cachedMatch = await selectCachedModel(storage, storedDefaultId);
|
||||||
|
if (cachedMatch != null && !ref.read(isManualModelSelectionProvider)) {
|
||||||
|
ref.read(selectedModelProvider.notifier).set(cachedMatch);
|
||||||
|
unawaited(storage.saveLocalDefaultModel(cachedMatch).catchError((_) {}));
|
||||||
|
DebugLogger.log(
|
||||||
|
'settings-default',
|
||||||
|
scope: 'models/default',
|
||||||
|
data: {'name': cachedMatch.name, 'source': 'settings'},
|
||||||
|
);
|
||||||
|
return cachedMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Fallback: cached resolved default model (for offline/fast startup)
|
||||||
try {
|
try {
|
||||||
final cached = await storage.getLocalDefaultModel();
|
final cached = await storage.getLocalDefaultModel();
|
||||||
if (cached != null && !ref.read(isManualModelSelectionProvider)) {
|
if (cached != null && !ref.read(isManualModelSelectionProvider)) {
|
||||||
@@ -1575,102 +1602,7 @@ Future<Model?> defaultModel(Ref ref) async {
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
// Respect manual selection if present
|
// 3) Fast server path: query server default ID without listing all models
|
||||||
if (ref.read(isManualModelSelectionProvider)) {
|
|
||||||
final current = ref.read(selectedModelProvider);
|
|
||||||
if (current != null) return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1) Fast path: read stored default model ID directly and select optimistically
|
|
||||||
try {
|
|
||||||
final storedDefaultId = await SettingsService.getDefaultModel();
|
|
||||||
if (storedDefaultId != null && storedDefaultId.isNotEmpty) {
|
|
||||||
if (!ref.read(isManualModelSelectionProvider)) {
|
|
||||||
final cachedMatch = await selectCachedModel(storage, storedDefaultId);
|
|
||||||
if (cachedMatch != null) {
|
|
||||||
ref.read(selectedModelProvider.notifier).set(cachedMatch);
|
|
||||||
unawaited(
|
|
||||||
storage.saveLocalDefaultModel(cachedMatch).onError((
|
|
||||||
error,
|
|
||||||
stack,
|
|
||||||
) {
|
|
||||||
DebugLogger.error(
|
|
||||||
'Failed to save default model to cache',
|
|
||||||
scope: 'models/default',
|
|
||||||
error: error,
|
|
||||||
stackTrace: stack,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
DebugLogger.log(
|
|
||||||
'cache-select',
|
|
||||||
scope: 'models/default',
|
|
||||||
data: {'name': cachedMatch.name, 'source': 'cache'},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
DebugLogger.log(
|
|
||||||
'cache-skip-missing',
|
|
||||||
scope: 'models/default',
|
|
||||||
data: {'id': storedDefaultId},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Reconcile against real models in background
|
|
||||||
Future.microtask(() async {
|
|
||||||
try {
|
|
||||||
if (!ref.mounted) return;
|
|
||||||
List<Model> models;
|
|
||||||
final modelsAsync = ref.read(modelsProvider);
|
|
||||||
if (modelsAsync.hasValue) {
|
|
||||||
models = modelsAsync.value ?? const <Model>[];
|
|
||||||
} else {
|
|
||||||
models = await ref.read(modelsProvider.future);
|
|
||||||
}
|
|
||||||
if (!ref.mounted) return;
|
|
||||||
|
|
||||||
Model? resolved;
|
|
||||||
try {
|
|
||||||
resolved = models.firstWhere((m) => m.id == storedDefaultId);
|
|
||||||
} catch (_) {
|
|
||||||
final byName = models
|
|
||||||
.where((m) => m.name == storedDefaultId)
|
|
||||||
.toList();
|
|
||||||
if (byName.length == 1) resolved = byName.first;
|
|
||||||
}
|
|
||||||
resolved ??= models.isNotEmpty ? models.first : null;
|
|
||||||
|
|
||||||
if (!ref.mounted) return;
|
|
||||||
if (resolved != null && !ref.read(isManualModelSelectionProvider)) {
|
|
||||||
ref.read(selectedModelProvider.notifier).set(resolved);
|
|
||||||
unawaited(
|
|
||||||
storage.saveLocalDefaultModel(resolved).onError((error, stack) {
|
|
||||||
DebugLogger.error(
|
|
||||||
'Failed to save default model to cache',
|
|
||||||
scope: 'models/default',
|
|
||||||
error: error,
|
|
||||||
stackTrace: stack,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
DebugLogger.log(
|
|
||||||
'reconcile',
|
|
||||||
scope: 'models/default',
|
|
||||||
data: {'name': resolved.name, 'source': 'stored'},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
DebugLogger.error(
|
|
||||||
'reconcile-failed',
|
|
||||||
scope: 'models/default',
|
|
||||||
error: e,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return ref.read(selectedModelProvider);
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
// 2) Fast server path: query server default ID without listing all models
|
|
||||||
try {
|
try {
|
||||||
final serverDefault = await api.getDefaultModel();
|
final serverDefault = await api.getDefaultModel();
|
||||||
if (serverDefault != null && serverDefault.isNotEmpty) {
|
if (serverDefault != null && serverDefault.isNotEmpty) {
|
||||||
|
|||||||
@@ -990,8 +990,27 @@ void startNewChat(dynamic ref) {
|
|||||||
ref.read(pendingFolderIdProvider.notifier).clear();
|
ref.read(pendingFolderIdProvider.notifier).clear();
|
||||||
|
|
||||||
// Reset to default model for new conversations (fixes #296)
|
// Reset to default model for new conversations (fixes #296)
|
||||||
|
restoreDefaultModel(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores the selected model to the user's configured default model.
|
||||||
|
/// Call this when starting a new conversation.
|
||||||
|
Future<void> restoreDefaultModel(dynamic ref) async {
|
||||||
|
// Mark that this is not a manual selection
|
||||||
ref.read(isManualModelSelectionProvider.notifier).set(false);
|
ref.read(isManualModelSelectionProvider.notifier).set(false);
|
||||||
|
|
||||||
|
// Invalidate and re-read to force defaultModelProvider to use settings priority
|
||||||
ref.invalidate(defaultModelProvider);
|
ref.invalidate(defaultModelProvider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(defaultModelProvider.future);
|
||||||
|
} catch (e) {
|
||||||
|
DebugLogger.error(
|
||||||
|
'restore-default-failed',
|
||||||
|
scope: 'chat/model',
|
||||||
|
error: e,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Available tools provider
|
// Available tools provider
|
||||||
|
|||||||
@@ -83,42 +83,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
return fileSize <= (maxSizeMB * 1024 * 1024);
|
return fileSize <= (maxSizeMB * 1024 * 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Model?> _trySelectCachedModel() async {
|
|
||||||
final existing = ref.read(selectedModelProvider);
|
|
||||||
if (existing != null) {
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final storage = ref.read(optimizedStorageServiceProvider);
|
|
||||||
// Prefer the stored default model ID/name if available
|
|
||||||
final settingsDesired = ref.read(appSettingsProvider).defaultModel;
|
|
||||||
final storedDesired = await SettingsService.getDefaultModel().catchError(
|
|
||||||
(_) => null,
|
|
||||||
);
|
|
||||||
final desiredId = settingsDesired ?? storedDesired;
|
|
||||||
|
|
||||||
final match = await selectCachedModel(storage, desiredId);
|
|
||||||
if (match != null) {
|
|
||||||
ref.read(selectedModelProvider.notifier).set(match);
|
|
||||||
DebugLogger.log(
|
|
||||||
'cache-select',
|
|
||||||
scope: 'chat/model',
|
|
||||||
data: {'name': match.name, 'source': 'cache'},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return match;
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
DebugLogger.error(
|
|
||||||
'cache-select-failed',
|
|
||||||
scope: 'chat/model',
|
|
||||||
error: error,
|
|
||||||
stackTrace: stackTrace,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void startNewChat() {
|
void startNewChat() {
|
||||||
// Clear current conversation
|
// Clear current conversation
|
||||||
ref.read(chatMessagesProvider.notifier).clearMessages();
|
ref.read(chatMessagesProvider.notifier).clearMessages();
|
||||||
@@ -131,8 +95,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
ref.read(pendingFolderIdProvider.notifier).clear();
|
ref.read(pendingFolderIdProvider.notifier).clear();
|
||||||
|
|
||||||
// Reset to default model for new conversations (fixes #296)
|
// Reset to default model for new conversations (fixes #296)
|
||||||
ref.read(isManualModelSelectionProvider.notifier).set(false);
|
restoreDefaultModel(ref);
|
||||||
ref.invalidate(defaultModelProvider);
|
|
||||||
|
|
||||||
// Scroll to top
|
// Scroll to top
|
||||||
if (_scrollController.hasClients) {
|
if (_scrollController.hasClients) {
|
||||||
@@ -157,65 +120,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: try cached models + stored default before waiting on providers
|
// Use shared restore logic which handles settings priority and fallbacks
|
||||||
final cached = await _trySelectCachedModel();
|
await restoreDefaultModel(ref);
|
||||||
if (cached != null) {
|
|
||||||
// Still continue to reconcile against remote models below
|
|
||||||
DebugLogger.log(
|
|
||||||
'cache-hit',
|
|
||||||
scope: 'chat/model',
|
|
||||||
data: {'name': cached.name},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugLogger.log('auto-select-start', scope: 'chat/model');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First ensure models are loaded
|
|
||||||
final modelsAsync = ref.read(modelsProvider);
|
|
||||||
List<Model> models;
|
|
||||||
|
|
||||||
if (modelsAsync.hasValue) {
|
|
||||||
models = modelsAsync.value!;
|
|
||||||
} else {
|
|
||||||
DebugLogger.log('models-fetch', scope: 'chat/model');
|
|
||||||
models = await ref.read(modelsProvider.future);
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugLogger.log(
|
|
||||||
'models-count',
|
|
||||||
scope: 'chat/model',
|
|
||||||
data: {'count': models.length},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (models.isEmpty) {
|
|
||||||
DebugLogger.warning('models-empty', scope: 'chat/model');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to use the default model provider
|
|
||||||
try {
|
|
||||||
final Model? model = await ref.read(defaultModelProvider.future);
|
|
||||||
if (model != null) {
|
|
||||||
DebugLogger.log(
|
|
||||||
'auto-select',
|
|
||||||
scope: 'chat/model',
|
|
||||||
data: {'name': model.name},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
DebugLogger.warning('provider-fallback', scope: 'chat/model');
|
|
||||||
// Fallback: select the first available model
|
|
||||||
ref.read(selectedModelProvider.notifier).set(models.first);
|
|
||||||
DebugLogger.log(
|
|
||||||
'fallback',
|
|
||||||
scope: 'chat/model',
|
|
||||||
data: {'name': models.first.name},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
DebugLogger.error('auto-select-failed', scope: 'chat/model', error: e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkAndShowOnboarding() async {
|
Future<void> _checkAndShowOnboarding() async {
|
||||||
|
|||||||
@@ -1342,6 +1342,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
// Clear context attachments (web pages, YouTube, knowledge base docs)
|
// Clear context attachments (web pages, YouTube, knowledge base docs)
|
||||||
ref.read(contextAttachmentsProvider.notifier).clear();
|
ref.read(contextAttachmentsProvider.notifier).clear();
|
||||||
|
|
||||||
|
// Reset to default model for new conversations (fixes #296)
|
||||||
|
chat.restoreDefaultModel(ref);
|
||||||
|
|
||||||
// Close drawer using the responsive layout (same pattern as _selectConversation)
|
// Close drawer using the responsive layout (same pattern as _selectConversation)
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final mediaQuery = MediaQuery.maybeOf(context);
|
final mediaQuery = MediaQuery.maybeOf(context);
|
||||||
|
|||||||
@@ -523,6 +523,8 @@ Future<void> _confirmAndDeleteConversation(
|
|||||||
if (active?.id == conversationId) {
|
if (active?.id == conversationId) {
|
||||||
ref.read(activeConversationProvider.notifier).clear();
|
ref.read(activeConversationProvider.notifier).clear();
|
||||||
ref.read(chat.chatMessagesProvider.notifier).clearMessages();
|
ref.read(chat.chatMessagesProvider.notifier).clearMessages();
|
||||||
|
// Reset to default model for new conversations (fixes #296)
|
||||||
|
chat.restoreDefaultModel(ref);
|
||||||
}
|
}
|
||||||
refreshConversationsCache(ref);
|
refreshConversationsCache(ref);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|||||||
Reference in New Issue
Block a user