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>
118 lines
3.0 KiB
Dart
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();
|
|
}
|
|
}
|