claude_session_viewer/lib/providers/session_provider.dart
Mathias Beaulieu-Duncan 364877d376 Initial commit: Claude Code session viewer (Flutter macOS)
A desktop app that parses Claude Code .jsonl session logs and provides
a rich UI for exploring conversations, tool usage, subagents, and token
consumption. Features include project browser with auto-discovery of
~/.claude/projects, conversation timeline with inline subagent expansion,
agents overview, toolbelt chart, and token usage dashboard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:17:23 -04:00

118 lines
3.0 KiB
Dart

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;
// Filter state
String? _selectedAgentId;
Set<String> _visibleTypes = {'user', 'assistant', 'system'};
String _searchQuery = '';
SessionLog? get session => _session;
bool get isLoading => _isLoading;
String? get error => _error;
String? get filePath => _filePath;
String? get selectedAgentId => _selectedAgentId;
Set<String> get visibleTypes => _visibleTypes;
String get searchQuery => _searchQuery;
AgentInfo? get selectedAgent {
if (_session == null || _selectedAgentId == null) return null;
return _session!.agents
.where((a) => a.id == _selectedAgentId)
.firstOrNull;
}
List<LogEntry> get filteredEntries {
if (_session == null) return [];
List<LogEntry> 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<void> loadSession(String filePath) async {
_isLoading = true;
_error = null;
_filePath = filePath;
_selectedAgentId = null;
_searchQuery = '';
notifyListeners();
try {
_session = await _parser.parseFile(filePath);
_error = null;
} catch (e) {
_error = e.toString();
_session = null;
}
_isLoading = false;
notifyListeners();
}
void selectAgent(String? agentId) {
_selectedAgentId = agentId;
notifyListeners();
}
void toggleTypeFilter(String type) {
if (_visibleTypes.contains(type)) {
_visibleTypes = Set.from(_visibleTypes)..remove(type);
} else {
_visibleTypes = Set.from(_visibleTypes)..add(type);
}
notifyListeners();
}
void setSearchQuery(String query) {
_searchQuery = query;
notifyListeners();
}
void clearSession() {
_session = null;
_filePath = null;
_error = null;
_selectedAgentId = null;
_searchQuery = '';
notifyListeners();
}
}