@tduncan / Cortana / commits / d0b7418

refactor: remove unused imports, properties, and simplify streaming completion logic in chat notifier

abdulmominsakib committed Jun 26, 2026 at 18:47 UTC d0b7418669e920ae48abc3e08629e12e24d89249
10 files changed +119 -135
lib/features/chat/providers/chat_notifier.dart
+118 -122
@@ -2,7 +2,6 @@ import 'dart:async';
2 import 'dart:io';
3 import 'dart:math';
4
5 -import 'package:dio/dio.dart';
5 import 'package:flutter_riverpod/flutter_riverpod.dart';
6 import 'package:localmind/core/logger/app_logger.dart';
7 import 'package:localmind/core/models/enums.dart';
@@ -22,7 +21,6 @@ import 'package:localmind/features/servers/data/models/server.dart';
21 import 'package:localmind/features/servers/providers/server_providers.dart';
22 import 'package:localmind/objectbox.g.dart';
23 import '../data/chat_service.dart';
25 -import '../data/models/chat_parameters.dart';
24 import '../data/models/message.dart' hide ToolCallData;
25 import '../data/tools/tool_definition.dart';
26 import '../data/tools/tool_execution_loop.dart';
@@ -559,141 +557,139 @@ class ChatNotifier extends Notifier<ChatState> {
557 final streamConvId = assistantMessage.conversationId;
558 final isCurrentContext = _currentConversationId == streamConvId;
559 final streamingMessage = streamingAssistantMessage;
562 - if (streamingMessage != null) {
563 - final hasContent =
564 - streamingMessage.content.isNotEmpty ||
565 - (streamingMessage.reasoningContent?.isNotEmpty ??
566 - false) ||
567 - collectedToolCalls.isNotEmpty;
568 -
569 - if (!hasContent) {
570 - final errorMessage = streamingMessage.copyWith(
571 - status: MessageStatus.error,
572 - errorMessage:
573 - 'Model failed to respond. This may happen with free tier models that refuse certain prompts or when the service is busy.',
574 - isProcessing: false,
560 + final hasContent =
561 + streamingMessage.content.isNotEmpty ||
562 + (streamingMessage.reasoningContent?.isNotEmpty ??
563 + false) ||
564 + collectedToolCalls.isNotEmpty;
565 +
566 + if (!hasContent) {
567 + final errorMessage = streamingMessage.copyWith(
568 + status: MessageStatus.error,
569 + errorMessage:
570 + 'Model failed to respond. This may happen with free tier models that refuse certain prompts or when the service is busy.',
571 + isProcessing: false,
572 + );
573 + await _saveMessage(errorMessage);
574 + if (isCurrentContext) {
575 + _replaceMessageInState(
576 + errorMessage,
577 + clearStreaming: true,
578 );
576 - await _saveMessage(errorMessage);
577 - if (isCurrentContext) {
578 - _replaceMessageInState(
579 - errorMessage,
580 - clearStreaming: true,
581 - );
582 - state = state.copyWith(
583 - isStreaming: false,
584 - errorMessage: errorMessage.errorMessage,
585 - );
586 - }
587 - ref.read(isStreamingProvider.notifier).setStreaming(false);
588 - _latestStreamingMessage = null;
589 - } else {
590 - var finalMessage = streamingMessage.copyWith(
591 - status: MessageStatus.complete,
592 - isProcessing: false,
579 + state = state.copyWith(
580 + isStreaming: false,
581 + errorMessage: errorMessage.errorMessage,
582 );
583 + }
584 + ref.read(isStreamingProvider.notifier).setStreaming(false);
585 + _latestStreamingMessage = null;
586 + } else {
587 + var finalMessage = streamingMessage.copyWith(
588 + status: MessageStatus.complete,
589 + isProcessing: false,
590 + );
591 +
592 + if (mcpConfig.enabled && collectedToolCalls.isNotEmpty) {
593 + try {
594 + final registry = ref.read(toolRegistryProvider);
595 + final adapter = createAdapterForServerType(server.type);
596 +
597 + final dedupedCalls = <String, ToolCallData>{};
598 + for (final tc in collectedToolCalls) {
599 + dedupedCalls[tc.tool] = tc;
600 + }
601
595 - if (mcpConfig.enabled && collectedToolCalls.isNotEmpty) {
596 - try {
597 - final registry = ref.read(toolRegistryProvider);
598 - final adapter = createAdapterForServerType(server.type);
599 -
600 - final dedupedCalls = <String, ToolCallData>{};
601 - for (final tc in collectedToolCalls) {
602 - dedupedCalls[tc.tool] = tc;
603 - }
604 -
605 - final preParsedCalls = dedupedCalls.values
606 - .map(
607 - (tc) => ParsedToolCall(
608 - id: tc.tool,
609 - name: tc.tool,
610 - arguments: tc.arguments,
611 - ),
612 - )
613 - .toList();
614 -
615 - final loop = ToolExecutionLoop(
616 - adapter: adapter,
617 - registry: registry,
618 - onRequestApproval: (call) async {
619 - final completer = Completer<bool>();
620 - state = state.copyWith(
621 - pendingToolApproval: PendingToolApproval(
622 - toolCall: call,
623 - completer: completer,
624 - ),
625 - );
626 -
627 - final result = await completer.future;
628 -
629 - state = state.copyWith(clearPendingApproval: true);
630 -
631 - return result;
632 - },
633 - );
602 + final preParsedCalls = dedupedCalls.values
603 + .map(
604 + (tc) => ParsedToolCall(
605 + id: tc.tool,
606 + name: tc.tool,
607 + arguments: tc.arguments,
608 + ),
609 + )
610 + .toList();
611 +
612 + final loop = ToolExecutionLoop(
613 + adapter: adapter,
614 + registry: registry,
615 + onRequestApproval: (call) async {
616 + final completer = Completer<bool>();
617 + state = state.copyWith(
618 + pendingToolApproval: PendingToolApproval(
619 + toolCall: call,
620 + completer: completer,
621 + ),
622 + );
623
635 - final loopResult = await loop.run(
636 - initialUserMessage: content,
637 - assistantContent: streamingAssistantMessage.content,
638 - preParsedCalls: preParsedCalls,
639 - );
624 + final result = await completer.future;
625
641 - if (loopResult.events.isNotEmpty) {
642 - finalMessage = finalMessage.copyWith(
643 - toolSessionId: loop.sessionId,
644 - toolEvents: loopResult.events,
645 - );
646 - }
647 - } catch (e) {
648 - Log.error('Tool execution loop failed: $e');
649 - }
650 - }
626 + state = state.copyWith(clearPendingApproval: true);
627
652 - await _saveMessage(finalMessage);
653 - if (isCurrentContext) {
654 - _replaceMessageInState(
655 - finalMessage,
656 - clearStreaming: true,
628 + return result;
629 + },
630 + );
631 +
632 + final loopResult = await loop.run(
633 + initialUserMessage: content,
634 + assistantContent: streamingAssistantMessage.content,
635 + preParsedCalls: preParsedCalls,
636 );
658 - state = state.copyWith(isStreaming: false);
659 - }
660 - ref.read(isStreamingProvider.notifier).setStreaming(false);
661 - _latestStreamingMessage = null;
662 - if (_currentConversationId != null) {
663 - final preview = finalMessage.content.length > 100
664 - ? '${finalMessage.content.substring(0, 100)}...'
665 - : finalMessage.content;
666 - await ref
667 - .read(conv.conversationsProvider.notifier)
668 - .updatePreview(
669 - _currentConversationId!,
670 - preview,
671 - DateTime.now(),
672 - );
637
674 - final userMessage = state.messages
675 - .where((m) => m.role == MessageRole.user)
676 - .firstOrNull;
677 - if (userMessage != null &&
678 - userMessage.content.length > 10) {
679 - _autoGenerateTitle(
680 - userMessage.content,
681 - finalMessage.content,
638 + if (loopResult.events.isNotEmpty) {
639 + finalMessage = finalMessage.copyWith(
640 + toolSessionId: loop.sessionId,
641 + toolEvents: loopResult.events,
642 );
643 }
644 + } catch (e) {
645 + Log.error('Tool execution loop failed: $e');
646 }
647 + }
648
686 - _maybeRequestReviewAfterSuccessfulCompletion(
687 - finalMessage: finalMessage,
688 - server: server,
689 - selectedModel: selectedModel,
649 + await _saveMessage(finalMessage);
650 + if (isCurrentContext) {
651 + _replaceMessageInState(
652 + finalMessage,
653 + clearStreaming: true,
654 );
655 + state = state.copyWith(isStreaming: false);
656 + }
657 + ref.read(isStreamingProvider.notifier).setStreaming(false);
658 + _latestStreamingMessage = null;
659 + if (_currentConversationId != null) {
660 + final preview = finalMessage.content.length > 100
661 + ? '${finalMessage.content.substring(0, 100)}...'
662 + : finalMessage.content;
663 + await ref
664 + .read(conv.conversationsProvider.notifier)
665 + .updatePreview(
666 + _currentConversationId!,
667 + preview,
668 + DateTime.now(),
669 + );
670
692 - _chunkCount = 0;
693 - _lastCheckpointTime = null;
694 - _lastSavedContentLength = 0;
695 - _lastSavedReasoningLength = 0;
671 + final userMessage = state.messages
672 + .where((m) => m.role == MessageRole.user)
673 + .firstOrNull;
674 + if (userMessage != null &&
675 + userMessage.content.length > 10) {
676 + _autoGenerateTitle(
677 + userMessage.content,
678 + finalMessage.content,
679 + );
680 + }
681 }
682 +
683 + _maybeRequestReviewAfterSuccessfulCompletion(
684 + finalMessage: finalMessage,
685 + server: server,
686 + selectedModel: selectedModel,
687 + );
688 +
689 + _chunkCount = 0;
690 + _lastCheckpointTime = null;
691 + _lastSavedContentLength = 0;
692 + _lastSavedReasoningLength = 0;
693 }
694 },
695 onError: (error) async {
lib/features/chat/providers/model_selection_providers.dart
-1
@@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
4 import 'package:localmind/core/models/enums.dart';
5 import 'package:localmind/core/providers/service_providers.dart';
6 import 'package:localmind/features/models/data/models/model_info.dart';
7 -import 'package:localmind/features/servers/data/models/server.dart';
7 import 'package:localmind/features/servers/providers/server_providers.dart';
8 import 'package:localmind/features/on_device/providers/on_device_providers.dart';
9
lib/features/chat/providers/smart_reply_providers.dart
-2
@@ -6,8 +6,6 @@ import 'package:localmind/core/providers/app_providers.dart';
6 import 'package:localmind/features/conversations/providers/conversation_providers.dart'
7 as conv;
8 import 'package:localmind/features/servers/providers/server_providers.dart';
9 -import '../data/chat_service.dart';
10 -import '../data/smart_reply_service.dart';
9 import 'chat_notifier.dart';
10 import 'chat_params_providers.dart';
11 import 'chat_service_providers.dart';
lib/features/chat/views/chat_screen.dart
-1
@@ -283,7 +283,6 @@ class _ChatBody extends ConsumerWidget {
283
284 @override
285 Widget build(BuildContext context, WidgetRef ref) {
286 - final l10n = AppLocalizations.of(context)!;
286 final theme = Theme.of(context);
287 final isDark = theme.brightness == Brightness.dark;
288 final isLoading = ref.watch(chatProvider.select((s) => s.isLoading));
lib/features/chat/views/components/message_list/message_list.dart
-1
@@ -121,7 +121,6 @@ class _MessageList extends StatelessWidget {
121
122 return ListView.builder(
123 controller: scrollController,
124 - cacheExtent: 1000,
124 padding: EdgeInsets.only(
125 top: 16,
126 bottom: 120 + (hasSmartReplies ? 64 : 0) + bottomInset,
lib/features/models/components/model_list.dart
-1
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 import 'package:localmind/features/chat/providers/model_loading_providers.dart';
4 import 'package:localmind/core/models/enums.dart';
5 -import 'package:localmind/core/providers/app_providers.dart';
5 import 'package:localmind/core/providers/service_providers.dart';
6 import 'package:localmind/core/theme/colors.dart';
7 import 'package:localmind/features/chat/providers/model_selection_providers.dart';
lib/features/on_device/components/memory_stats_panel.dart
+1 -1
@@ -190,7 +190,7 @@ class _MemoryPanelContent extends StatelessWidget {
190 mainAxisAlignment: MainAxisAlignment.spaceBetween,
191 children: [
192 Text(
193 - l10n.ram_used('${(usagePercent * 100).toStringAsFixed(0)}'),
193 + l10n.ram_used((usagePercent * 100).toStringAsFixed(0)),
194 style: theme.textTheme.labelSmall?.copyWith(
195 fontWeight: FontWeight.w600,
196 ),
lib/features/on_device/components/on_device_picker_section.dart
-1
@@ -7,7 +7,6 @@ import 'package:localmind/core/device/device_memory_service.dart';
7 import 'package:localmind/core/models/enums.dart';
8 import 'package:localmind/core/providers/app_providers.dart';
9 import 'package:localmind/core/providers/device_info_providers.dart';
10 -import 'package:localmind/core/providers/service_providers.dart';
10 import 'package:localmind/core/theme/colors.dart';
11 import 'package:localmind/features/chat/providers/model_selection_providers.dart';
12 import 'package:localmind/features/models/data/models/model_info.dart';
lib/features/on_device/views/model_manager_screen.dart
-2
@@ -1,5 +1,3 @@
1 -import 'dart:io';
2 -
1 import 'package:file_picker/file_picker.dart';
2 import 'package:flutter/foundation.dart';
3 import 'package:flutter/material.dart';
lib/l10n/app_localizations_zh.dart
-3
@@ -2455,19 +2455,16 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
2455 @override
2456 String get server_type_on_device => '裝置端';
2457
2458 - @override
2458 String get server_type_on_device_sub => '無需伺服器';
2459
2460 @override
2461 String get server_type_lm_studio => 'LM Studio';
2462
2464 - @override
2463 String get server_type_lm_studio_sub => '本機 API';
2464
2465 @override
2466 String get server_type_ollama => 'Ollama';
2467
2470 - @override
2468 String get server_type_ollama_sub => 'CLI 引擎';
2469
2470 @override