@tduncan / Cortana / commits / ac11518

refactor: use active chat target provider for effective model resolution and add unit test

abdulmominsakib committed Sep 22, 2026 at 18:12 UTC ac1151851478f106479ff734fc4929daecf8bcc1
3 files changed +203 -12
lib/features/chat/providers/chat_notifier.dart
+31 -11
@@ -704,8 +704,8 @@ class ChatNotifier extends Notifier<ChatState> {
704 return;
705 }
706
707 - final server = ref.read(activeServerProvider);
708 - final selectedModel = ref.read(selectedModelProvider);
707 + final target = ref.read(activeChatTargetProvider);
708 + final server = target.server;
709 final settings = ref.read(settingsProvider);
710 if (server == null) return;
711
@@ -730,7 +730,7 @@ class ChatNotifier extends Notifier<ChatState> {
730 .createConversation(
731 title: initialTitle,
732 serverId: server.id,
733 - modelId: selectedModel?.id,
733 + modelId: target.effectiveModelId,
734 personaId: preselected.isEmpty
735 ? null
736 : PersonaPromptUtils.joinPersonaIds(
@@ -1927,6 +1927,10 @@ class ChatNotifier extends Notifier<ChatState> {
1927 if (assistant.role != MessageRole.assistant) return;
1928 if (state.isStreaming) return;
1929
1930 + final target = ref.read(activeChatTargetProvider);
1931 + final effectiveModelId = target.effectiveModelId;
1932 + if (target.server == null || effectiveModelId == null) return;
1933 +
1934 final streamingAssistant = assistant.copyWith(
1935 status: MessageStatus.streaming,
1936 isProcessing: true,
@@ -1949,7 +1953,8 @@ class ChatNotifier extends Notifier<ChatState> {
1953
1954 await _runAssistantStream(
1955 streamingAssistant,
1952 - ref.read(selectedModelProvider),
1956 + target.selectedModel,
1957 + effectiveModelId: effectiveModelId,
1958 continueGeneration: true,
1959 );
1960 }
@@ -2042,9 +2047,15 @@ class ChatNotifier extends Notifier<ChatState> {
2047 required int threadOrder,
2048 required int variantIndex,
2049 }) async {
2045 - final selectedModel = ref.read(selectedModelProvider);
2046 - final server = ref.read(activeServerProvider);
2047 - if (server == null || _activeConversationId == null) return;
2050 + final target = ref.read(activeChatTargetProvider);
2051 + final selectedModel = target.selectedModel;
2052 + final server = target.server;
2053 + final effectiveModelId = target.effectiveModelId;
2054 + if (server == null ||
2055 + effectiveModelId == null ||
2056 + _activeConversationId == null) {
2057 + return;
2058 + }
2059
2060 await _abortStreamImmediately();
2061 if (!ref.mounted) return;
@@ -2056,7 +2067,7 @@ class ChatNotifier extends Notifier<ChatState> {
2067 content: '',
2068 createdAt: DateTime.now(),
2069 status: MessageStatus.streaming,
2059 - modelId: selectedModel?.id,
2070 + modelId: effectiveModelId,
2071 variantGroupId: variantGroupId,
2072 variantIndex: variantIndex,
2073 threadOrder: threadOrder,
@@ -2079,7 +2090,11 @@ class ChatNotifier extends Notifier<ChatState> {
2090 _resetCheckpointMetrics();
2091 _updateSavedMetrics(assistantMessage);
2092
2082 - await _runAssistantStream(assistantMessage, selectedModel);
2093 + await _runAssistantStream(
2094 + assistantMessage,
2095 + selectedModel,
2096 + effectiveModelId: effectiveModelId,
2097 + );
2098 }
2099
2100 /// After successful tool execution (issue #77), build `tool` role messages
@@ -2216,12 +2231,17 @@ class ChatNotifier extends Notifier<ChatState> {
2231 // call, the same onDone flow will execute it again until the model
2232 // yields a final answer or `maxIterations` (enforced inside
2233 // `ToolExecutionLoop`) is reached.
2219 - await _runAssistantStream(continuationMessage, selectedModel);
2234 + await _runAssistantStream(
2235 + continuationMessage,
2236 + selectedModel,
2237 + effectiveModelId: effectiveModelId,
2238 + );
2239 }
2240
2241 Future<void> _runAssistantStream(
2242 Message assistantMessage,
2243 ModelInfo? selectedModel, {
2244 + required String effectiveModelId,
2245 bool continueGeneration = false,
2246 }) async {
2247 final server = ref.read(activeServerProvider);
@@ -2287,7 +2307,7 @@ class ChatNotifier extends Notifier<ChatState> {
2307 _streamSubscription = chatService
2308 .sendMessage(
2309 server: server,
2290 - modelId: selectedModel?.id ?? 'default',
2310 + modelId: effectiveModelId,
2311 messages: messagesForApi,
2312 params: chatParams,
2313 integrations: integrations,
pubspec.yaml
+1 -1
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 # In Windows, build-name is used as the major, minor, and patch parts
18 # of the product and file versions while build-number is used as the build suffix.
19 -version: 1.8.2+88
19 +version: 1.8.3+89
20
21 environment:
22 sdk: ">=3.12.0 <4.0.0"
test/features/chat/providers/chat_notifier_effective_model_test.dart new
+171
@@ -0,0 +1,171 @@
1 +import 'dart:async';
2 +
3 +import 'package:flutter_riverpod/flutter_riverpod.dart';
4 +import 'package:flutter_test/flutter_test.dart';
5 +import 'package:localmind/core/models/enums.dart';
6 +import 'package:localmind/core/providers/app_providers.dart';
7 +import 'package:localmind/core/providers/chat_background_service_provider.dart';
8 +import 'package:localmind/core/services/chat_background_service.dart';
9 +import 'package:localmind/features/chat/data/chat_service.dart';
10 +import 'package:localmind/features/chat/data/models/chat_parameters.dart';
11 +import 'package:localmind/features/chat/data/models/message.dart';
12 +import 'package:localmind/features/chat/data/tools/tool_definition.dart';
13 +import 'package:localmind/features/chat/data/tools/tool_registry.dart';
14 +import 'package:localmind/features/chat/providers/chat_mcp_providers.dart';
15 +import 'package:localmind/features/chat/providers/chat_notifier.dart';
16 +import 'package:localmind/features/chat/providers/chat_params_providers.dart';
17 +import 'package:localmind/features/chat/providers/chat_service_providers.dart';
18 +import 'package:localmind/features/chat/providers/model_selection_providers.dart';
19 +import 'package:localmind/features/chat/providers/tooling_providers.dart';
20 +import 'package:localmind/features/models/data/models/model_info.dart';
21 +import 'package:localmind/features/on_device/data/models/on_device_model.dart';
22 +import 'package:localmind/features/on_device/providers/on_device_providers.dart';
23 +import 'package:localmind/features/servers/data/models/server.dart';
24 +import 'package:localmind/features/servers/providers/server_providers.dart';
25 +import 'package:localmind/features/settings/data/models/app_settings.dart';
26 +import 'package:localmind/features/voice_mode/providers/voice_mode_provider.dart';
27 +
28 +void main() {
29 + test(
30 + 'continues with the loaded model when selection metadata is unavailable',
31 + () async {
32 + final chatService = _RecordingChatService();
33 + final container = ProviderContainer(
34 + overrides: [
35 + activeServerProvider.overrideWith(_OnDeviceServerNotifier.new),
36 + onDeviceEngineProvider.overrideWith(_LoadedLlamaEngineNotifier.new),
37 + selectedModelProvider.overrideWith(_UnselectedModelNotifier.new),
38 + chatProvider.overrideWith(_FallbackChatNotifier.new),
39 + chatServiceProvider.overrideWithValue(chatService),
40 + chatParamsProvider.overrideWithValue(ChatParameters.defaults()),
41 + chatMcpConfigProvider.overrideWith(
42 + _DisabledChatMcpConfigNotifier.new,
43 + ),
44 + toolRegistryProvider.overrideWithValue(
45 + ToolRegistry(providers: const []),
46 + ),
47 + chatBackgroundServiceProvider.overrideWithValue(
48 + _TestChatBackgroundService(),
49 + ),
50 + settingsProvider.overrideWith(_TestSettingsNotifier.new),
51 + voiceModeProvider.overrideWith(_IdleVoiceModeNotifier.new),
52 + ],
53 + );
54 + addTearDown(container.dispose);
55 +
56 + await container
57 + .read(chatProvider.notifier)
58 + .continueFromMessage('assistant-1');
59 +
60 + expect(await chatService.requestedModelId.future, 'imported-gguf-model');
61 + },
62 + );
63 +}
64 +
65 +class _FallbackChatNotifier extends ChatNotifier {
66 + @override
67 + ChatState build() {
68 + final messages = [
69 + Message(
70 + id: 'user-1',
71 + conversationId: 'conversation-1',
72 + role: MessageRole.user,
73 + content: 'Hello',
74 + createdAt: DateTime.utc(2026, 9, 22),
75 + ),
76 + Message(
77 + id: 'assistant-1',
78 + conversationId: 'conversation-1',
79 + role: MessageRole.assistant,
80 + content: 'Hello there.',
81 + createdAt: DateTime.utc(2026, 9, 22),
82 + ),
83 + ];
84 + return ChatState(
85 + messages: messages,
86 + allMessages: messages,
87 + isTemporary: true,
88 + );
89 + }
90 +}
91 +
92 +class _RecordingChatService implements ChatService {
93 + final requestedModelId = Completer<String>();
94 +
95 + @override
96 + Stream<ChatResponse> sendMessage({
97 + required Server server,
98 + required String modelId,
99 + required List<Message> messages,
100 + required ChatParameters params,
101 + List<McpIntegration>? integrations,
102 + List<ToolDefinition>? tools,
103 + bool continueGeneration = false,
104 + }) async* {
105 + if (!requestedModelId.isCompleted) {
106 + requestedModelId.complete(modelId);
107 + }
108 + yield const ChatResponse(
109 + type: ChatResponseType.error,
110 + content: 'Expected test failure',
111 + );
112 + }
113 +
114 + @override
115 + void cancelStream() {}
116 +}
117 +
118 +class _DisabledChatMcpConfigNotifier extends ChatMcpConfigNotifier {
119 + @override
120 + ChatMcpConfig build() => const ChatMcpConfig(enabled: false);
121 +}
122 +
123 +class _TestChatBackgroundService extends ChatBackgroundService {
124 + @override
125 + Future<void> start() async {}
126 +
127 + @override
128 + Future<void> stop() async {}
129 +}
130 +
131 +class _OnDeviceServerNotifier extends ActiveServerNotifier {
132 + @override
133 + Server? build() => Server(
134 + id: 'on-device',
135 + name: 'On-Device',
136 + type: ServerType.onDevice,
137 + host: '',
138 + port: 0,
139 + createdAt: DateTime.utc(2026, 9, 22),
140 + lastConnectedAt: DateTime.utc(2026, 9, 22),
141 + status: ConnectionStatus.connected,
142 + );
143 +}
144 +
145 +class _LoadedLlamaEngineNotifier extends OnDeviceEngineNotifier {
146 + @override
147 + OnDeviceEngineState build() => const OnDeviceEngineState(
148 + status: OnDeviceEngineStatus.loaded,
149 + loadedModelId: 'imported-gguf-model',
150 + loadedRuntime: OnDeviceModelRuntime.llamaCpp,
151 + );
152 +}
153 +
154 +class _UnselectedModelNotifier extends SelectedModelNotifier {
155 + @override
156 + ModelInfo? build() => null;
157 +}
158 +
159 +class _TestSettingsNotifier extends SettingsNotifier {
160 + @override
161 + AppSettings build() => AppSettings(
162 + mcpEnabled: false,
163 + newChatMcpEnabled: false,
164 + resumeLastChat: false,
165 + );
166 +}
167 +
168 +class _IdleVoiceModeNotifier extends VoiceModeNotifier {
169 + @override
170 + VoiceModeState build() => const VoiceModeState();
171 +}