import 'dart:convert'; import 'dart:io' as io; import 'dart:isolate'; import '../models/agent_info.dart'; import '../models/content_block.dart'; import '../models/log_entry.dart'; import '../models/token_usage.dart'; class ParseError { final int? line; final String? source; final String error; const ParseError({this.line, this.source, required this.error}); } class ParseResult { final SessionLog sessionLog; final List errors; const ParseResult(this.sessionLog, this.errors); } class _IsolateInput { final List mainLines; final String sessionFileName; final Map> subagentLines; final Map subagentMetaRaw; const _IsolateInput({ required this.mainLines, required this.sessionFileName, required this.subagentLines, required this.subagentMetaRaw, }); } class JsonlParser { Future parseFile(String filePath) async { final file = io.File(filePath); final mainLines = await file.readAsLines(encoding: utf8); final sessionFileName = file.uri.pathSegments.last.replaceAll('.jsonl', ''); final parentDir = file.parent.path; final subagentsDir = io.Directory('$parentDir/$sessionFileName/subagents'); final subagentLines = >{}; final subagentMetaRaw = {}; if (await subagentsDir.exists()) { await for (final entity in subagentsDir.list()) { if (entity is io.File) { if (entity.path.endsWith('.jsonl')) { final agentFileName = entity.uri.pathSegments.last; final agentId = agentFileName .replaceAll('.jsonl', '') .replaceFirst('agent-', ''); try { final lines = await entity.readAsLines(encoding: utf8); subagentLines[agentId] = lines; } catch (_) {} } else if (entity.path.endsWith('.meta.json')) { final agentFileName = entity.uri.pathSegments.last; final agentId = agentFileName .replaceAll('.meta.json', '') .replaceFirst('agent-', ''); try { subagentMetaRaw[agentId] = await entity.readAsString(); } catch (_) {} } } } } // Run CPU-bound parsing in a background isolate return Isolate.run( () => _parseInIsolate(_IsolateInput( mainLines: mainLines, sessionFileName: sessionFileName, subagentLines: subagentLines, subagentMetaRaw: subagentMetaRaw, )), ); } } ParseResult _parseInIsolate(_IsolateInput input) { final errors = []; final entries = []; for (var i = 0; i < input.mainLines.length; i++) { final line = input.mainLines[i]; if (line.trim().isEmpty) continue; try { final json = jsonDecode(line) as Map; entries.add(LogEntry.fromJson(json)); } catch (e) { errors.add(ParseError(line: i, error: e.toString())); } } // Parse subagent entries final subagentFiles = >{}; for (final entry in input.subagentLines.entries) { final agentEntries = []; for (var i = 0; i < entry.value.length; i++) { final line = entry.value[i]; if (line.trim().isEmpty) continue; try { final json = jsonDecode(line) as Map; agentEntries.add(LogEntry.fromJson(json)); } catch (e) { errors.add(ParseError( line: i, source: entry.key, error: e.toString())); } } if (agentEntries.isNotEmpty) { subagentFiles[entry.key] = agentEntries; } } // Parse subagent metadata final subagentMeta = >{}; for (final entry in input.subagentMetaRaw.entries) { try { subagentMeta[entry.key] = jsonDecode(entry.value) as Map; } catch (e) { errors.add(ParseError(source: entry.key, error: e.toString())); } } final sessionLog = _buildSessionLog(entries, subagentFiles, subagentMeta); return ParseResult(sessionLog, errors); } SessionLog _buildSessionLog( List entries, Map> subagentFiles, Map> subagentMeta, ) { // Extract metadata from first user/assistant entry String? sessionId, cwd, version, gitBranch; for (final entry in entries) { if (entry.sessionId != null) { sessionId = entry.sessionId; cwd = entry.cwd; version = entry.version; gitBranch = entry.gitBranch; break; } } _linkToolResults(entries); for (final agentEntries in subagentFiles.values) { _linkToolResults(agentEntries); } final mainConversation = entries .where( (e) => e.type != 'progress' && e.type != 'file-history-snapshot') .toList(); final agents = _buildAgents(mainConversation, subagentFiles, subagentMeta); final mainAgent = agents.firstWhere((a) => a.id == 'main'); var totalUsage = const TokenUsage(); for (final agent in agents) { totalUsage = totalUsage + agent.aggregatedUsage; } final toolsByName = >{}; for (final agent in agents) { for (final tool in agent.toolsUsed) { toolsByName.putIfAbsent(tool.name, () => []).add(tool); } } final timestamps = entries .where((e) => e.timestamp != null) .map((e) => e.timestamp!) .toList(); timestamps.sort(); return SessionLog( sessionId: sessionId, cwd: cwd, version: version, gitBranch: gitBranch, allEntries: entries, mainConversation: mainConversation, agents: agents, mainAgent: mainAgent, totalUsage: totalUsage, toolsByName: toolsByName, startTime: timestamps.isNotEmpty ? timestamps.first : null, endTime: timestamps.isNotEmpty ? timestamps.last : null, ); } void _linkToolResults(List entries) { final resultMap = {}; for (final entry in entries) { if (entry is UserEntry && entry.isToolResult) { for (final result in entry.toolResults) { resultMap[result.toolUseId] = result; } } } for (final entry in entries) { if (entry is AssistantEntry) { for (final block in entry.toolUseBlocks) { block.linkedResult = resultMap[block.id]; } } } } List _buildAgents( List mainEntries, Map> subagentFiles, Map> subagentMeta, ) { final agents = []; final mainToolUses = []; String? mainModel; var mainUsage = const TokenUsage(); for (final entry in mainEntries) { if (entry is AssistantEntry) { mainModel ??= entry.model; if (entry.usage != null) { mainUsage = mainUsage + entry.usage!; } for (final block in entry.toolUseBlocks) { if (!block.isAgentCall) { mainToolUses.add(block); } } } } agents.add(AgentInfo( id: 'main', name: 'Main Assistant', model: mainModel, messages: mainEntries, toolsUsed: mainToolUses, aggregatedUsage: mainUsage, )); final agentToolCalls = {}; for (final entry in mainEntries) { if (entry is AssistantEntry) { for (final block in entry.toolUseBlocks) { if (block.isAgentCall) { agentToolCalls[block.id] = block; } } } } for (final entry in subagentFiles.entries) { final agentId = entry.key; final agentEntries = entry.value; final meta = subagentMeta[agentId] ?? {}; final subToolUses = []; var subUsage = const TokenUsage(); String? subModel; for (final e in agentEntries) { if (e is AssistantEntry) { subModel ??= e.model; if (e.usage != null) { subUsage = subUsage + e.usage!; } for (final block in e.toolUseBlocks) { if (!block.isAgentCall) { subToolUses.add(block); } } } } ToolUseBlock? matchedCall; for (final call in agentToolCalls.entries) { if (call.key.contains(agentId) || agentId.contains(call.key)) { matchedCall = call.value; break; } } String? slug; for (final e in agentEntries) { if (e.raw.containsKey('slug')) { slug = e.raw['slug'] as String?; break; } } final agentType = meta['agentType'] as String? ?? matchedCall?.subagentType; final description = matchedCall?.agentDescription ?? slug ?? agentType ?? 'Subagent'; agents.add(AgentInfo( id: agentId, name: description, subagentType: agentType, description: matchedCall?.agentDescription, prompt: matchedCall?.agentPrompt, model: subModel, messages: agentEntries, toolsUsed: subToolUses, aggregatedUsage: subUsage, )); } return agents; }