import 'dart:async'; import 'package:flutter/foundation.dart'; import '../models/agent_info.dart'; import '../models/log_entry.dart'; import '../services/jsonl_parser.dart'; class SessionProvider with ChangeNotifier { final JsonlParser _parser = JsonlParser(); SessionLog? _session; bool _isLoading = false; String? _error; String? _filePath; List _parseErrors = const []; // Filter state String? _selectedAgentId; Set _visibleTypes = {'user', 'assistant', 'system'}; String _searchQuery = ''; // Cached filtered results List _cachedFilteredEntries = const []; // Cache key — tracks what was used to compute the cache _FilterKey? _cachedFilterKey; // Debounce timer for search Timer? _searchDebounce; SessionLog? get session => _session; bool get isLoading => _isLoading; String? get error => _error; String? get filePath => _filePath; String? get selectedAgentId => _selectedAgentId; Set get visibleTypes => _visibleTypes; String get searchQuery => _searchQuery; List get parseErrors => _parseErrors; AgentInfo? get selectedAgent { if (_session == null || _selectedAgentId == null) return null; return _session!.agents.where((a) => a.id == _selectedAgentId).firstOrNull; } List get filteredEntries { final key = _FilterKey( selectedAgentId: _selectedAgentId, visibleTypes: _visibleTypes, searchQuery: _searchQuery, hasSession: _session != null, ); if (_cachedFilterKey == key) return _cachedFilteredEntries; // Recompute _cachedFilterKey = key; _cachedFilteredEntries = _computeFilteredEntries(); return _cachedFilteredEntries; } List _computeFilteredEntries() { if (_session == null) return []; List entries; if (_selectedAgentId != null && _selectedAgentId != 'main') { final agent = selectedAgent; entries = agent?.messages ?? []; } else { entries = _session!.mainConversation; } return entries.where((e) { if (!_visibleTypes.contains(e.type)) return false; if (_searchQuery.isNotEmpty) { return _entryMatchesSearch(e, _searchQuery.toLowerCase()); } return true; }).toList(); } bool _entryMatchesSearch(LogEntry entry, String query) { if (entry is UserEntry) { return entry.promptText.toLowerCase().contains(query); } if (entry is AssistantEntry) { for (final block in entry.textBlocks) { if (block.text.toLowerCase().contains(query)) return true; } for (final block in entry.toolUseBlocks) { if (block.name.toLowerCase().contains(query)) return true; if (block.input.toString().toLowerCase().contains(query)) return true; } } return false; } Future loadSession(String filePath) async { _isLoading = true; _error = null; _filePath = filePath; _selectedAgentId = null; _searchQuery = ''; _parseErrors = const []; _invalidateCache(); notifyListeners(); try { final result = await _parser.parseFile(filePath); _session = result.sessionLog; _parseErrors = result.errors; _error = null; } catch (e) { _error = e.toString(); _session = null; } _isLoading = false; notifyListeners(); } void selectAgent(String? agentId) { _selectedAgentId = agentId; _invalidateCache(); notifyListeners(); } void toggleTypeFilter(String type) { if (_visibleTypes.contains(type)) { _visibleTypes = Set.from(_visibleTypes)..remove(type); } else { _visibleTypes = Set.from(_visibleTypes)..add(type); } _invalidateCache(); notifyListeners(); } void setSearchQuery(String query) { _searchQuery = query; // Update cache immediately but debounce notifyListeners _invalidateCache(); _searchDebounce?.cancel(); _searchDebounce = Timer(const Duration(milliseconds: 300), () { notifyListeners(); }); } void clearSession() { _session = null; _filePath = null; _error = null; _selectedAgentId = null; _searchQuery = ''; _parseErrors = const []; _searchDebounce?.cancel(); _invalidateCache(); notifyListeners(); } void _invalidateCache() { _cachedFilterKey = null; } @override void dispose() { _searchDebounce?.cancel(); super.dispose(); } } /// Immutable key to detect whether the filter result has changed. class _FilterKey { final String? selectedAgentId; final Set visibleTypes; final String searchQuery; final bool hasSession; const _FilterKey({ required this.selectedAgentId, required this.visibleTypes, required this.searchQuery, required this.hasSession, }); @override bool operator ==(Object other) => identical(this, other) || other is _FilterKey && selectedAgentId == other.selectedAgentId && _setEquals(visibleTypes, other.visibleTypes) && searchQuery == other.searchQuery && hasSession == other.hasSession; @override int get hashCode => Object.hash(selectedAgentId, Object.hashAll(visibleTypes), searchQuery, hasSession); static bool _setEquals(Set a, Set b) { if (a.length != b.length) return false; for (final v in a) { if (!b.contains(v)) return false; } return true; } }