@tduncan / Cortana / commits / dcba5a4

Refactor toward feature-based architecture for backup and search.

Extract backup import helpers and message search service into dedicated modules, and move persona picker UI into the personas feature. Co-authored-by: Cursor <cursoragent@cursor.com>

onlypuppy7 committed Jul 1, 2026 at 10:42 UTC dcba5a4767a32f6bcd98c4a7b549dee7456c7438
7 files changed +118 -88
lib/core/services/data_backup_import_helpers.dart new
+11
@@ -0,0 +1,11 @@
1 +String? backupImportString(Object? value) => value is String ? value : null;
2 +
3 +DateTime? backupImportDateTime(Object? value) {
4 + final raw = backupImportString(value);
5 + if (raw == null) return null;
6 + try {
7 + return DateTime.parse(raw);
8 + } catch (_) {
9 + return null;
10 + }
11 +}
lib/core/services/data_backup_service.dart
+22 -33
@@ -5,6 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart';
5
6 import '../models/enums.dart';
7 import '../storage/entities.dart';
8 +import 'data_backup_import_helpers.dart';
9 import '../../features/chat/data/models/message.dart';
10 import '../../features/conversations/data/models/conversation.dart';
11 import '../../features/personas/data/models/persona.dart';
@@ -310,10 +311,10 @@ class DataBackupService {
311 if (personas is List) {
312 for (final item in personas) {
313 if (item is! Map) continue;
313 - final id = _backupString(item['id']);
314 - final name = _backupString(item['name']);
315 - final createdAt = _backupDateTime(item['createdAt']);
316 - final updatedAt = _backupDateTime(item['updatedAt']);
314 + final id = backupImportString(item['id']);
315 + final name = backupImportString(item['name']);
316 + final createdAt = backupImportDateTime(item['createdAt']);
317 + final updatedAt = backupImportDateTime(item['updatedAt']);
318 if (id == null || name == null || createdAt == null || updatedAt == null) {
319 continue;
320 }
@@ -345,10 +346,10 @@ class DataBackupService {
346 if (servers is List) {
347 for (final item in servers) {
348 if (item is! Map) continue;
348 - final id = _backupString(item['id']);
349 - final name = _backupString(item['name']);
350 - final createdAt = _backupDateTime(item['createdAt']);
351 - final lastConnectedAt = _backupDateTime(item['lastConnectedAt']);
349 + final id = backupImportString(item['id']);
350 + final name = backupImportString(item['name']);
351 + final createdAt = backupImportDateTime(item['createdAt']);
352 + final lastConnectedAt = backupImportDateTime(item['lastConnectedAt']);
353 if (id == null ||
354 name == null ||
355 createdAt == null ||
@@ -384,9 +385,9 @@ class DataBackupService {
385 if (savedFolders is List) {
386 for (final item in savedFolders) {
387 if (item is! Map) continue;
387 - final id = _backupString(item['id']);
388 - final folderName = _backupString(item['name']);
389 - final createdAt = _backupDateTime(item['createdAt']);
388 + final id = backupImportString(item['id']);
389 + final folderName = backupImportString(item['name']);
390 + final createdAt = backupImportDateTime(item['createdAt']);
391 if (id == null || folderName == null || createdAt == null) continue;
392 final entity = SavedMessageFolderEntity(
393 id: id,
@@ -408,10 +409,10 @@ class DataBackupService {
409 if (savedMessages is List) {
410 for (final item in savedMessages) {
411 if (item is! Map) continue;
411 - final id = _backupString(item['id']);
412 - final sourceMessageId = _backupString(item['sourceMessageId']);
413 - final conversationId = _backupString(item['conversationId']);
414 - final savedAt = _backupDateTime(item['savedAt']);
412 + final id = backupImportString(item['id']);
413 + final sourceMessageId = backupImportString(item['sourceMessageId']);
414 + final conversationId = backupImportString(item['conversationId']);
415 + final savedAt = backupImportDateTime(item['savedAt']);
416 if (id == null ||
417 sourceMessageId == null ||
418 conversationId == null ||
@@ -442,9 +443,9 @@ class DataBackupService {
443 if (conversations is List) {
444 for (final item in conversations) {
445 if (item is! Map) continue;
445 - final id = _backupString(item['id']);
446 - final createdAt = _backupDateTime(item['createdAt']);
447 - final updatedAt = _backupDateTime(item['updatedAt']);
446 + final id = backupImportString(item['id']);
447 + final createdAt = backupImportDateTime(item['createdAt']);
448 + final updatedAt = backupImportDateTime(item['updatedAt']);
449 if (id == null || createdAt == null || updatedAt == null) continue;
450 final conversation = Conversation(
451 id: id,
@@ -484,9 +485,9 @@ class DataBackupService {
485 if (messages is List) {
486 for (final item in messages) {
487 if (item is! Map) continue;
487 - final conversationId = _backupString(item['conversationId']);
488 - final id = _backupString(item['id']);
489 - final createdAt = _backupDateTime(item['createdAt']);
488 + final conversationId = backupImportString(item['conversationId']);
489 + final id = backupImportString(item['id']);
490 + final createdAt = backupImportDateTime(item['createdAt']);
491 if (conversationId == null || id == null || createdAt == null) {
492 continue;
493 }
@@ -545,15 +546,3 @@ class DataBackupService {
546 }
547 }
548 }
548 -
549 -String? _backupString(Object? value) => value is String ? value : null;
550 -
551 -DateTime? _backupDateTime(Object? value) {
552 - final raw = _backupString(value);
553 - if (raw == null) return null;
554 - try {
555 - return DateTime.parse(raw);
556 - } catch (_) {
557 - return null;
558 - }
559 -}
lib/features/chat/views/chat_screen.dart
+1 -1
@@ -41,7 +41,7 @@ import 'components/top_bar/model_top_bar.dart';
41 import 'components/top_bar/connection_banner.dart';
42 import 'components/top_bar/persona_indicator.dart';
43 import 'components/top_bar/smart_reply_chips.dart';
44 -import 'components/persona_picker_sheet.dart';
44 +import 'package:localmind/features/personas/views/components/persona_picker_sheet.dart';
45
46 class ChatScreen extends ConsumerStatefulWidget {
47 const ChatScreen({super.key});
lib/features/conversations/data/message_search_service.dart new
+60
@@ -0,0 +1,60 @@
1 +import '../../../core/models/enums.dart';
2 +import '../../../core/storage/objectbox_store.dart';
3 +import '../../../objectbox.g.dart';
4 +import 'models/message_search_hit.dart';
5 +
6 +class MessageSearchService {
7 + const MessageSearchService();
8 +
9 + List<MessageSearchHit> searchMessages(
10 + ObjectBoxStore db, {
11 + required String query,
12 + int limit = 50,
13 + }) {
14 + final normalizedQuery = query.trim().toLowerCase();
15 + if (normalizedQuery.isEmpty) return const [];
16 +
17 + final conversations = db.conversationBox.getAll();
18 + final titleById = {
19 + for (final conversation in conversations)
20 + conversation.id: conversation.title,
21 + };
22 +
23 + final hits = <MessageSearchHit>[];
24 + final messageQuery = db.messageBox
25 + .query(
26 + MessageEntity_.content.contains(
27 + normalizedQuery,
28 + caseSensitive: false,
29 + ),
30 + )
31 + .build();
32 + final messages = messageQuery.find();
33 + messageQuery.close();
34 +
35 + for (final entity in messages) {
36 + if (!entity.content.toLowerCase().contains(normalizedQuery)) continue;
37 +
38 + final title = titleById[entity.conversationUid] ?? 'Chat';
39 + final content = entity.content;
40 + final index = content.toLowerCase().indexOf(normalizedQuery);
41 + final start = index > 40 ? index - 40 : 0;
42 + final end = (index + normalizedQuery.length + 40).clamp(0, content.length);
43 + var snippet = content.substring(start, end);
44 + if (start > 0) snippet = '…$snippet';
45 + if (end < content.length) snippet = '$snippet…';
46 +
47 + hits.add(
48 + MessageSearchHit(
49 + messageId: entity.id,
50 + conversationId: entity.conversationUid,
51 + conversationTitle: title,
52 + snippet: snippet,
53 + role: MessageRole.values[entity.roleIndex],
54 + ),
55 + );
56 + if (hits.length >= limit) break;
57 + }
58 + return hits;
59 + }
60 +}
lib/features/conversations/data/models/message_search_hit.dart new
+17
@@ -0,0 +1,17 @@
1 +import '../../../../core/models/enums.dart';
2 +
3 +class MessageSearchHit {
4 + const MessageSearchHit({
5 + required this.messageId,
6 + required this.conversationId,
7 + required this.conversationTitle,
8 + required this.snippet,
9 + required this.role,
10 + });
11 +
12 + final String messageId;
13 + final String conversationId;
14 + final String conversationTitle;
15 + final String snippet;
16 + final MessageRole role;
17 +}
lib/features/conversations/providers/conversation_providers.dart
+7 -54
@@ -1,9 +1,10 @@
1 import 'package:flutter_riverpod/flutter_riverpod.dart';
2
3 -import '../../../core/models/enums.dart';
3 import '../../../core/providers/storage_providers.dart';
4 import '../../../core/storage/entities.dart';
5 import '../../../objectbox.g.dart';
6 +import '../data/message_search_service.dart';
7 +import '../data/models/message_search_hit.dart';
8 import '../data/models/conversation.dart';
9 import '../data/models/conversation_folder.dart';
10
@@ -715,22 +716,6 @@ class ConversationFoldersNotifier extends AsyncNotifier<List<ConversationFolder>
716 }
717 }
718
718 -class MessageSearchHit {
719 - const MessageSearchHit({
720 - required this.messageId,
721 - required this.conversationId,
722 - required this.conversationTitle,
723 - required this.snippet,
724 - required this.role,
725 - });
726 -
727 - final String messageId;
728 - final String conversationId;
729 - final String conversationTitle;
730 - final String snippet;
731 - final MessageRole role;
732 -}
733 -
719 final searchMessageContentsProvider =
720 NotifierProvider<SearchMessageContentsNotifier, bool>(() {
721 return SearchMessageContentsNotifier();
@@ -773,46 +758,14 @@ class ScrollToMessageNotifier extends Notifier<String?> {
758 void clear() => state = null;
759 }
760
761 +const _messageSearchService = MessageSearchService();
762 +
763 final messageSearchResultsProvider = Provider<List<MessageSearchHit>>((ref) {
777 - final query = ref.watch(conversationSearchProvider).trim().toLowerCase();
778 - if (query.isEmpty || !ref.watch(searchMessageContentsProvider)) {
764 + final query = ref.watch(conversationSearchProvider);
765 + if (query.trim().isEmpty || !ref.watch(searchMessageContentsProvider)) {
766 return const [];
767 }
768
769 final db = ref.watch(databaseProvider);
783 - final conversations = db.conversationBox.getAll();
784 - final titleById = {
785 - for (final conv in conversations) conv.id: conv.title,
786 - };
787 -
788 - final hits = <MessageSearchHit>[];
789 - final messageQuery = db.messageBox
790 - .query(MessageEntity_.content.contains(query, caseSensitive: false))
791 - .build();
792 - final messages = messageQuery.find();
793 - messageQuery.close();
794 -
795 - for (final entity in messages) {
796 - if (!entity.content.toLowerCase().contains(query)) continue;
797 - final title = titleById[entity.conversationUid] ?? 'Chat';
798 - final content = entity.content;
799 - final index = content.toLowerCase().indexOf(query);
800 - final start = index > 40 ? index - 40 : 0;
801 - final end = (index + query.length + 40).clamp(0, content.length);
802 - var snippet = content.substring(start, end);
803 - if (start > 0) snippet = '…$snippet';
804 - if (end < content.length) snippet = '$snippet…';
805 -
806 - hits.add(
807 - MessageSearchHit(
808 - messageId: entity.id,
809 - conversationId: entity.conversationUid,
810 - conversationTitle: title,
811 - snippet: snippet,
812 - role: MessageRole.values[entity.roleIndex],
813 - ),
814 - );
815 - if (hits.length >= 50) break;
816 - }
817 - return hits;
770 + return _messageSearchService.searchMessages(db, query: query);
771 });
lib/features/personas/views/components/persona_picker_sheet.dart renamed