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>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user