feat(model): Update auto-select model description and behavior

This commit is contained in:
cogwheel
2025-12-22 16:19:50 +05:30
parent 5fd68f86fe
commit 51d9412876
15 changed files with 84 additions and 75 deletions

View File

@@ -55,8 +55,6 @@ PODS:
- SDWebImageWebPCoder
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_secure_storage_darwin (10.0.0):
- Flutter
- FlutterMacOS
@@ -148,7 +146,6 @@ DEPENDENCIES:
- flutter_callkit_incoming (from `.symlinks/plugins/flutter_callkit_incoming/ios`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
@@ -203,8 +200,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage_darwin:
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
flutter_tts:
@@ -262,7 +257,6 @@ SPEC CHECKSUMS:
flutter_callkit_incoming: cb8138af67cda6dd981f7101a5d709003af21502
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f

View File

@@ -877,17 +877,6 @@ class IsManualModelSelection extends _$IsManualModelSelection {
void set(bool value) => state = value;
}
// Listen for settings changes and reset manual selection when default model changes
// keepAlive to maintain listener throughout app lifecycle
final _settingsWatcherProvider = Provider<void>((ref) {
ref.listen<AppSettings>(appSettingsProvider, (previous, next) {
if (previous?.defaultModel != next.defaultModel) {
// Reset manual selection when default model changes
ref.read(isManualModelSelectionProvider.notifier).set(false);
}
});
});
// Auto-apply model-specific tools when model changes or tools load
final modelToolsAutoSelectionProvider = Provider<void>((ref) {
// Prevent disposal so listeners remain active throughout app lifecycle
@@ -1039,11 +1028,29 @@ final defaultModelAutoSelectionProvider = Provider<void>((ref) {
// Only react when default model value changes
if (previous?.defaultModel == next.defaultModel) return;
// Do not override manual selections
if (ref.read(isManualModelSelectionProvider)) return;
// Reset manual selection flag when default model setting changes
ref.read(isManualModelSelectionProvider.notifier).set(false);
final desired = next.defaultModel;
if (desired == null || desired.isEmpty) return;
// If auto-select (null), invalidate defaultModelProvider to re-fetch server default
if (desired == null || desired.isEmpty) {
DebugLogger.log('auto-select-enabled', scope: 'models/default');
ref.invalidate(defaultModelProvider);
// Trigger re-read to apply server default
Future(() async {
try {
await ref.read(defaultModelProvider.future);
} catch (e) {
DebugLogger.error(
'auto-select-failed',
scope: 'models/default',
error: e,
);
}
});
return;
}
// Resolve the desired model against available models (by ID only)
Future(() async {
@@ -1514,8 +1521,6 @@ Future<Conversation> loadConversation(Ref ref, String conversationId) async {
Future<Model?> defaultModel(Ref ref) async {
DebugLogger.log('provider-called', scope: 'models/default');
// Initialize the settings watcher (side-effect only)
ref.read(_settingsWatcherProvider);
final storage = ref.read(optimizedStorageServiceProvider);
// Read settings without subscribing to rebuilds to avoid watch/await hazards
final reviewerMode = ref.read(reviewerModeProvider);
@@ -1570,7 +1575,8 @@ Future<Model?> defaultModel(Ref ref) async {
// 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 ??
final storedDefaultId =
settingsDefaultId ??
await SettingsService.getDefaultModel().catchError((_) => null);
if (storedDefaultId != null && storedDefaultId.isNotEmpty) {
@@ -1578,7 +1584,9 @@ Future<Model?> defaultModel(Ref ref) async {
final cachedMatch = await selectCachedModel(storage, storedDefaultId);
if (cachedMatch != null && !ref.read(isManualModelSelectionProvider)) {
ref.read(selectedModelProvider.notifier).set(cachedMatch);
unawaited(storage.saveLocalDefaultModel(cachedMatch).catchError((_) {}));
unawaited(
storage.saveLocalDefaultModel(cachedMatch).catchError((_) {}),
);
DebugLogger.log(
'settings-default',
scope: 'models/default',

View File

@@ -12,6 +12,7 @@ import '../../../core/models/chat_message.dart';
import '../../../core/models/conversation.dart';
import '../../../core/providers/app_providers.dart';
import '../../../core/services/conversation_delta_listener.dart';
import '../../../core/services/settings_service.dart';
import '../../../core/services/streaming_helper.dart';
import '../../../core/services/streaming_response_controller.dart';
import '../../../core/services/worker_manager.dart';
@@ -904,11 +905,20 @@ void startNewChat(dynamic ref) {
}
/// Restores the selected model to the user's configured default model.
/// Call this when starting a new conversation.
/// Call this when starting a new conversation or when settings change.
Future<void> restoreDefaultModel(dynamic ref) async {
// Mark that this is not a manual selection
ref.read(isManualModelSelectionProvider.notifier).set(false);
// If auto-select (no explicit default), clear the cached default model
// so defaultModelProvider will fetch from server
final settingsDefault = ref.read(appSettingsProvider).defaultModel;
if (settingsDefault == null || settingsDefault.isEmpty) {
final storage = ref.read(optimizedStorageServiceProvider);
await storage.saveLocalDefaultModel(null);
DebugLogger.log('cleared-cached-default', scope: 'chat/model');
}
// Invalidate and re-read to force defaultModelProvider to use settings priority
ref.invalidate(defaultModelProvider);

View File

@@ -16,6 +16,7 @@ import '../../../shared/widgets/sheet_handle.dart';
import '../../../shared/widgets/conduit_components.dart';
import '../../../core/providers/app_providers.dart';
import '../../../core/services/navigation_service.dart';
import '../../chat/providers/chat_providers.dart' show restoreDefaultModel;
import '../../auth/providers/unified_auth_providers.dart';
import '../../../core/services/settings_service.dart';
import '../../../core/models/model.dart';
@@ -648,6 +649,9 @@ class ProfilePage extends ConsumerWidget {
await ref
.read(appSettingsProvider.notifier)
.setDefaultModel(modelIdToSave);
// Immediately apply the new default model selection
await restoreDefaultModel(ref);
}
}

View File

@@ -24,7 +24,7 @@
"signOut": "Abmelden",
"endYourSession": "Sitzung beenden",
"defaultModel": "Standardmodell",
"autoSelect": "Automatische Auswahl",
"autoSelect": "Serverstandard",
"loadingModels": "Modelle werden geladen...",
"failedToLoadModels": "Modelle konnten nicht geladen werden",
"availableModels": "Verfügbare Modelle",
@@ -364,9 +364,9 @@
}
}
},
"autoSelectDescription": "Lass die App das beste Modell auswählen",
"autoSelectDescription": "Das auf dem Server konfigurierte Standardmodell verwenden",
"@autoSelectDescription": {
"description": "Explains what the auto-select model setting does."
"description": "Explains what the server-provided model setting does."
},
"ttsEngineLabel": "Engine",
"@ttsEngineLabel": {

View File

@@ -72,7 +72,7 @@
"@defaultModel": {
"description": "Label for choosing a default AI model."
},
"autoSelect": "Auto-select",
"autoSelect": "Server provided",
"@autoSelect": {
"description": "Option to let the app pick a suitable model automatically."
},
@@ -1297,9 +1297,9 @@
}
}
},
"autoSelectDescription": "Let the app choose the best model",
"autoSelectDescription": "Use the default model configured on the server",
"@autoSelectDescription": {
"description": "Explains what the auto-select model setting does."
"description": "Explains what the server-provided model setting does."
},
"chatSettings": "Chat",
"@chatSettings": {

View File

@@ -24,7 +24,7 @@
"signOut": "Cerrar sesión",
"endYourSession": "Finalizar tu sesión",
"defaultModel": "Modelo predeterminado",
"autoSelect": "Selección automática",
"autoSelect": "Predeterminado del servidor",
"loadingModels": "Cargando modelos...",
"failedToLoadModels": "No se pudieron cargar los modelos",
"availableModels": "Modelos disponibles",
@@ -364,9 +364,9 @@
}
}
},
"autoSelectDescription": "Deja que la aplicación elija el mejor modelo",
"autoSelectDescription": "Usar el modelo predeterminado configurado en el servidor",
"@autoSelectDescription": {
"description": "Explains what the auto-select model setting does."
"description": "Explains what the server-provided model setting does."
},
"ttsEngineLabel": "Motor",
"@ttsEngineLabel": {

View File

@@ -24,7 +24,7 @@
"signOut": "Se déconnecter",
"endYourSession": "Terminer votre session",
"defaultModel": "Modèle par défaut",
"autoSelect": "Sélection automatique",
"autoSelect": "Fourni par le serveur",
"loadingModels": "Chargement des modèles...",
"failedToLoadModels": "Échec du chargement des modèles",
"availableModels": "Modèles disponibles",
@@ -364,9 +364,9 @@
}
}
},
"autoSelectDescription": "Laissez l'application choisir le meilleur modèle",
"autoSelectDescription": "Utiliser le modèle par défaut configuré sur le serveur",
"@autoSelectDescription": {
"description": "Explains what the auto-select model setting does."
"description": "Explains what the server-provided model setting does."
},
"ttsEngineLabel": "Moteur",
"@ttsEngineLabel": {

View File

@@ -24,7 +24,7 @@
"signOut": "Esci",
"endYourSession": "Termina la sessione",
"defaultModel": "Modello predefinito",
"autoSelect": "Selezione automatica",
"autoSelect": "Predefinito del server",
"loadingModels": "Caricamento modelli...",
"failedToLoadModels": "Impossibile caricare i modelli",
"availableModels": "Modelli disponibili",
@@ -364,9 +364,9 @@
}
}
},
"autoSelectDescription": "Lascia che l'app scelga il modello migliore",
"autoSelectDescription": "Usa il modello predefinito configurato sul server",
"@autoSelectDescription": {
"description": "Explains what the auto-select model setting does."
"description": "Explains what the server-provided model setting does."
},
"ttsEngineLabel": "Motore",
"@ttsEngineLabel": {

View File

@@ -18,7 +18,7 @@
"signOut": "로그아웃",
"endYourSession": "세션 종료",
"defaultModel": "기본 모델",
"autoSelect": "자동 선택",
"autoSelect": "서버 기본값",
"loadingModels": "모델 로딩 중...",
"failedToLoadModels": "모델을 불러오지 못했습니다",
"availableModels": "사용 가능한 모델",
@@ -436,7 +436,7 @@
}
}
},
"autoSelectDescription": "앱이 최적의 모델을 선택하도록 허용",
"autoSelectDescription": "서버에 구성된 기본 모델 사용",
"chatSettings": "채팅",
"sendOnEnter": "Enter로 전송",
"sendOnEnterDescription": "Enter로 전송 (소프트 키보드). Cmd/Ctrl+Enter도 사용 가능",

View File

@@ -24,7 +24,7 @@
"signOut": "Uitloggen",
"endYourSession": "Beëindig je sessie",
"defaultModel": "Standaardmodel",
"autoSelect": "Automatisch selecteren",
"autoSelect": "Serverstandaard",
"loadingModels": "Modellen laden...",
"failedToLoadModels": "Kan modellen niet laden",
"availableModels": "Beschikbare modellen",
@@ -364,9 +364,9 @@
}
}
},
"autoSelectDescription": "Laat de app het beste model kiezen",
"autoSelectDescription": "Gebruik het standaardmodel dat op de server is geconfigureerd",
"@autoSelectDescription": {
"description": "Explains what the auto-select model setting does."
"description": "Explains what the server-provided model setting does."
},
"ttsEngineLabel": "Engine",
"@ttsEngineLabel": {

View File

@@ -24,7 +24,7 @@
"signOut": "Выйти",
"endYourSession": "Завершить сеанс",
"defaultModel": "Модель по умолчанию",
"autoSelect": "Автовыбор",
"autoSelect": "По умолчанию сервера",
"loadingModels": "Загрузка моделей...",
"failedToLoadModels": "Не удалось загрузить модели",
"availableModels": "Доступные модели",
@@ -364,9 +364,9 @@
}
}
},
"autoSelectDescription": "Позвольте приложению выбрать лучшую модель",
"autoSelectDescription": "Использовать модель по умолчанию, настроенную на сервере",
"@autoSelectDescription": {
"description": "Explains what the auto-select model setting does."
"description": "Explains what the server-provided model setting does."
},
"ttsEngineLabel": "Движок",
"@ttsEngineLabel": {

View File

@@ -24,7 +24,7 @@
"signOut": "退出登录",
"endYourSession": "结束您的会话",
"defaultModel": "默认模型",
"autoSelect": "自动选择",
"autoSelect": "服务器默认",
"loadingModels": "加载模型中...",
"failedToLoadModels": "无法加载模型",
"availableModels": "可用模型",
@@ -364,9 +364,9 @@
}
}
},
"autoSelectDescription": "让应用自动选择最佳模型",
"autoSelectDescription": "使用服务器上配置的默认模型",
"@autoSelectDescription": {
"description": "Explains what the auto-select model setting does."
"description": "Explains what the server-provided model setting does."
},
"ttsEngineLabel": "引擎",
"@ttsEngineLabel": {

View File

@@ -24,7 +24,7 @@
"signOut": "退出登錄",
"endYourSession": "結束您的會話",
"defaultModel": "默認模型",
"autoSelect": "自動選擇",
"autoSelect": "伺服器預設",
"loadingModels": "加載模型中...",
"failedToLoadModels": "無法加載模型",
"availableModels": "可用模型",
@@ -364,9 +364,9 @@
}
}
},
"autoSelectDescription": "讓應用自動選擇最佳模型",
"autoSelectDescription": "使用伺服器上配置的預設模型",
"@autoSelectDescription": {
"description": "Explains what the auto-select model setting does."
"description": "Explains what the server-provided model setting does."
},
"ttsEngineLabel": "引擎",
"@ttsEngineLabel": {

View File

@@ -159,9 +159,7 @@ class _ConduitMobileMenuBuilder extends mobile.MobileMenuWidgetBuilder {
final veilColor = theme.isDark
? const Color(0x4D000000) // ~30% black
: const Color(0x4D424242); // ~30% grey
return SizedBox.expand(
child: ColoredBox(color: veilColor),
);
return SizedBox.expand(child: ColoredBox(color: veilColor));
}
@override
@@ -189,10 +187,7 @@ class _ConduitMobileMenuBuilder extends mobile.MobileMenuWidgetBuilder {
Padding(
padding: const EdgeInsets.only(right: Spacing.md),
child: IconTheme(
data: IconThemeData(
color: iconColor,
size: IconSize.medium,
),
data: IconThemeData(color: iconColor, size: IconSize.medium),
child: imageWidget,
),
),
@@ -218,10 +213,7 @@ class _ConduitMobileMenuBuilder extends mobile.MobileMenuWidgetBuilder {
);
if (state.pressed) {
content = ColoredBox(
color: theme.surfaceContainer,
child: content,
);
content = ColoredBox(color: theme.surfaceContainer, child: content);
}
return content;
@@ -295,9 +287,7 @@ class _ConduitMobileMenuBuilder extends mobile.MobileMenuWidgetBuilder {
// even when the overlay is visually transparent
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: SizedBox.expand(
child: ColoredBox(color: overlayColor),
),
child: SizedBox.expand(child: ColoredBox(color: overlayColor)),
);
}
@@ -398,10 +388,10 @@ List<ConduitContextMenuAction> buildConversationActions({
return [
ConduitContextMenuAction(
cupertinoIcon:
isPinned ? CupertinoIcons.pin_slash : CupertinoIcons.pin_fill,
materialIcon:
isPinned ? Icons.push_pin_outlined : Icons.push_pin_rounded,
cupertinoIcon: isPinned
? CupertinoIcons.pin_slash
: CupertinoIcons.pin_fill,
materialIcon: isPinned ? Icons.push_pin_outlined : Icons.push_pin_rounded,
label: isPinned ? l10n.unpin : l10n.pin,
onBeforeClose: () => HapticFeedback.lightImpact(),
onSelected: togglePin,
@@ -410,8 +400,9 @@ List<ConduitContextMenuAction> buildConversationActions({
cupertinoIcon: isArchived
? CupertinoIcons.archivebox_fill
: CupertinoIcons.archivebox,
materialIcon:
isArchived ? Icons.unarchive_rounded : Icons.archive_rounded,
materialIcon: isArchived
? Icons.unarchive_rounded
: Icons.archive_rounded,
label: isArchived ? l10n.unarchive : l10n.archive,
onBeforeClose: () => HapticFeedback.lightImpact(),
onSelected: toggleArchive,
@@ -477,7 +468,9 @@ Future<void> _renameConversation(
if (api == null) throw Exception('No API service');
await api.updateConversation(conversationId, title: newName);
HapticFeedback.selectionClick();
ref.read(conversationsProvider.notifier).updateConversation(
ref
.read(conversationsProvider.notifier)
.updateConversation(
conversationId,
(conversation) =>
conversation.copyWith(title: newName, updatedAt: DateTime.now()),