perf: Phase 1 critical performance fixes + production macOS build
Performance: - Move JSON parsing to background isolate via Isolate.run() (eliminates 2-4s UI freeze) - Cache filteredEntries with key-based invalidation (eliminates O(n) recomputation) - Debounce search queries at 300ms (prevents cascade rebuilds on keystroke) - Flatten timeline from 2-level turn×response Column to single virtualized ListView.builder - Add RepaintBoundary per timeline item (isolates repaints during scroll) - Use context.select for granular rebuilds instead of top-level context.watch - Lazy ExpandableCard: child not built until first expand (replaces AnimatedCrossFade) - Use IndexedStack in AppShell (preserves screen state across tab switches) Fixes: - Collect parse errors instead of silently swallowing them - Show parse error count in timeline filter bar - Fix overflow in tokens screen pie chart legend Build: - Configure Developer ID signing with hardened runtime for production distribution - Enable secure timestamps for notarization - Update app name to Claude Session Viewer - Signed, notarized, stapled DMG distribution
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/agent_info.dart';
|
||||
import '../models/log_entry.dart';
|
||||
@@ -10,12 +12,21 @@ class SessionProvider with ChangeNotifier {
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String? _filePath;
|
||||
List<ParseError> _parseErrors = const [];
|
||||
|
||||
// Filter state
|
||||
String? _selectedAgentId;
|
||||
Set<String> _visibleTypes = {'user', 'assistant', 'system'};
|
||||
String _searchQuery = '';
|
||||
|
||||
// Cached filtered results
|
||||
List<LogEntry> _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;
|
||||
@@ -23,15 +34,30 @@ class SessionProvider with ChangeNotifier {
|
||||
String? get selectedAgentId => _selectedAgentId;
|
||||
Set<String> get visibleTypes => _visibleTypes;
|
||||
String get searchQuery => _searchQuery;
|
||||
List<ParseError> get parseErrors => _parseErrors;
|
||||
|
||||
AgentInfo? get selectedAgent {
|
||||
if (_session == null || _selectedAgentId == null) return null;
|
||||
return _session!.agents
|
||||
.where((a) => a.id == _selectedAgentId)
|
||||
.firstOrNull;
|
||||
return _session!.agents.where((a) => a.id == _selectedAgentId).firstOrNull;
|
||||
}
|
||||
|
||||
List<LogEntry> 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<LogEntry> _computeFilteredEntries() {
|
||||
if (_session == null) return [];
|
||||
|
||||
List<LogEntry> entries;
|
||||
@@ -73,10 +99,14 @@ class SessionProvider with ChangeNotifier {
|
||||
_filePath = filePath;
|
||||
_selectedAgentId = null;
|
||||
_searchQuery = '';
|
||||
_parseErrors = const [];
|
||||
_invalidateCache();
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_session = await _parser.parseFile(filePath);
|
||||
final result = await _parser.parseFile(filePath);
|
||||
_session = result.sessionLog;
|
||||
_parseErrors = result.errors;
|
||||
_error = null;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
@@ -89,6 +119,7 @@ class SessionProvider with ChangeNotifier {
|
||||
|
||||
void selectAgent(String? agentId) {
|
||||
_selectedAgentId = agentId;
|
||||
_invalidateCache();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -98,12 +129,18 @@ class SessionProvider with ChangeNotifier {
|
||||
} else {
|
||||
_visibleTypes = Set.from(_visibleTypes)..add(type);
|
||||
}
|
||||
_invalidateCache();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setSearchQuery(String query) {
|
||||
_searchQuery = query;
|
||||
notifyListeners();
|
||||
// Update cache immediately but debounce notifyListeners
|
||||
_invalidateCache();
|
||||
_searchDebounce?.cancel();
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
void clearSession() {
|
||||
@@ -112,6 +149,55 @@ class SessionProvider with ChangeNotifier {
|
||||
_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<String> 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<T>(Set<T> a, Set<T> b) {
|
||||
if (a.length != b.length) return false;
|
||||
for (final v in a) {
|
||||
if (!b.contains(v)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user