import 'dart:convert'; import 'dart:io'; import '../models/agent_info.dart'; import '../models/content_block.dart'; import '../models/log_entry.dart'; import '../models/token_usage.dart'; class JsonlParser { Future parseFile(String filePath) async { final file = File(filePath); final lines = await file.readAsLines(encoding: utf8); final entries = []; for (final line in lines) { if (line.trim().isEmpty) continue; try { final json = jsonDecode(line) as Map; entries.add(LogEntry.fromJson(json)); } catch (_) {} } // Resolve the subagents directory from the session file path // e.g. /path/to/project/64a8386a.jsonl → /path/to/project/64a8386a/subagents/ final sessionFileName = file.uri.pathSegments.last.replaceAll('.jsonl', ''); final parentDir = file.parent.path; final subagentsDir = Directory('$parentDir/$sessionFileName/subagents'); // Load all subagent .jsonl files final subagentFiles = >{}; final subagentMeta = >{}; if (await subagentsDir.exists()) { await for (final entity in subagentsDir.list()) { if (entity is File && entity.path.endsWith('.jsonl')) { final agentFileName = entity.uri.pathSegments.last; // agent-a014e30b71de602bb.jsonl → a014e30b71de602bb final agentId = agentFileName .replaceAll('.jsonl', '') .replaceFirst('agent-', ''); final agentEntries = []; try { final agentLines = await entity.readAsLines(encoding: utf8); for (final line in agentLines) { if (line.trim().isEmpty) continue; try { final json = jsonDecode(line) as Map; agentEntries.add(LogEntry.fromJson(json)); } catch (_) {} } } catch (_) {} if (agentEntries.isNotEmpty) { subagentFiles[agentId] = agentEntries; } } else if (entity is File && entity.path.endsWith('.meta.json')) { try { final metaContent = await entity.readAsString(); final meta = jsonDecode(metaContent) as Map; final agentFileName = entity.uri.pathSegments.last; final agentId = agentFileName .replaceAll('.meta.json', '') .replaceFirst('agent-', ''); subagentMeta[agentId] = meta; } catch (_) {} } } } return _buildSessionLog(entries, subagentFiles, subagentMeta); } 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; } } // Link tool results to tool use blocks in main session _linkToolResults(entries); // Link tool results inside each subagent session for (final agentEntries in subagentFiles.values) { _linkToolResults(agentEntries); } // Separate main conversation from progress entries final mainConversation = entries .where((e) => e.type != 'progress' && e.type != 'file-history-snapshot') .toList(); // Build agent info final agents = _buildAgents( mainConversation, subagentFiles, subagentMeta, ); final mainAgent = agents.firstWhere((a) => a.id == 'main'); // Compute total usage var totalUsage = const TokenUsage(); for (final agent in agents) { totalUsage = totalUsage + agent.aggregatedUsage; } // Build tools by name index final toolsByName = >{}; for (final agent in agents) { for (final tool in agent.toolsUsed) { toolsByName.putIfAbsent(tool.name, () => []).add(tool); } } // Timestamps 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 = []; // Main agent tools and usage 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, )); // Find Agent tool calls from the main conversation to get descriptions final agentToolCalls = {}; for (final entry in mainEntries) { if (entry is AssistantEntry) { for (final block in entry.toolUseBlocks) { if (block.isAgentCall) { agentToolCalls[block.id] = block; } } } } // Build subagent infos from the actual subagent .jsonl files for (final entry in subagentFiles.entries) { final agentId = entry.key; final agentEntries = entry.value; final meta = subagentMeta[agentId] ?? {}; // Extract tool uses, model, and usage from subagent entries 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); } } } } // Try to match this subagent to an Agent tool call for description/prompt. // The agentId in the file might be a prefix of the tool_use id. ToolUseBlock? matchedCall; for (final call in agentToolCalls.entries) { if (call.key.contains(agentId) || agentId.contains(call.key)) { matchedCall = call.value; break; } } // Also check the slug from the first entry to match 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; } }