extractToolStats function

ToolStatsResult extractToolStats(
  1. List<Map<String, dynamic>> messages
)

Extract tool usage statistics from session messages.

Processes each message to count tool invocations, detect language usage, track git operations, measure response times, and identify errors.

Implementation

ToolStatsResult extractToolStats(List<Map<String, dynamic>> messages) {
  final stats = ToolStatsResult();
  String? lastAssistantTimestamp;

  for (final msg in messages) {
    final type = msg['type'] as String?;
    final message = msg['message'] as Map<String, dynamic>?;
    final msgTimestamp = msg['timestamp'] as String?;

    if (type == 'assistant' && message != null) {
      if (msgTimestamp != null) {
        lastAssistantTimestamp = msgTimestamp;
      }

      // Track token usage.
      final usage = message['usage'] as Map<String, dynamic>?;
      if (usage != null) {
        stats.inputTokens += (usage['input_tokens'] as int?) ?? 0;
        stats.outputTokens += (usage['output_tokens'] as int?) ?? 0;
      }

      // Process content blocks.
      final content = message['content'];
      if (content is List) {
        for (final block in content) {
          if (block is! Map<String, dynamic>) continue;
          if (block['type'] == 'tool_use' && block['name'] != null) {
            final toolName = block['name'] as String;
            stats.toolCounts[toolName] = (stats.toolCounts[toolName] ?? 0) + 1;

            // Check for special tool usage.
            if (toolName == 'Task' || toolName == 'dispatch_agent') {
              stats.usesTaskAgent = true;
            }
            if (toolName.startsWith('mcp__')) stats.usesMcp = true;
            if (toolName == 'WebSearch') stats.usesWebSearch = true;
            if (toolName == 'WebFetch') stats.usesWebFetch = true;

            final input = block['input'] as Map<String, dynamic>?;
            if (input != null) {
              final filePath = input['file_path'] as String? ?? '';
              if (filePath.isNotEmpty) {
                final lang = getLanguageFromPath(filePath);
                if (lang != null) {
                  stats.languages[lang] = (stats.languages[lang] ?? 0) + 1;
                }
                // Track files modified by Edit/Write tools.
                if (toolName == 'Edit' || toolName == 'Write') {
                  stats.filesModified.add(filePath);
                }
              }

              // Count lines changed in Edit operations.
              if (toolName == 'Edit') {
                final oldString = input['old_string'] as String? ?? '';
                final newString = input['new_string'] as String? ?? '';
                final oldLines = oldString.split('\n').length;
                final newLines = newString.split('\n').length;
                if (newLines > oldLines) {
                  stats.linesAdded += newLines - oldLines;
                } else {
                  stats.linesRemoved += oldLines - newLines;
                }
              }

              // Track lines from Write tool (all added).
              if (toolName == 'Write') {
                final writeContent = input['content'] as String? ?? '';
                if (writeContent.isNotEmpty) {
                  stats.linesAdded += _countChar(writeContent, '\n') + 1;
                }
              }

              // Track git operations.
              final command = input['command'] as String? ?? '';
              if (command.contains('git commit')) stats.gitCommits++;
              if (command.contains('git push')) stats.gitPushes++;
            }
          }
        }
      }
    }

    // Process user messages.
    if (type == 'user' && message != null) {
      final content = message['content'];
      bool isHumanMessage = false;

      if (content is String && content.trim().isNotEmpty) {
        isHumanMessage = true;
      } else if (content is List) {
        for (final block in content) {
          if (block is Map<String, dynamic> &&
              block['type'] == 'text' &&
              block['text'] != null) {
            isHumanMessage = true;
            break;
          }
        }
      }

      // Track message hours and response times for actual human messages.
      if (isHumanMessage && msgTimestamp != null) {
        try {
          final msgDate = DateTime.parse(msgTimestamp);
          stats.messageHours.add(msgDate.hour);
          stats.userMessageTimestamps.add(msgTimestamp);
        } catch (_) {
          // Skip invalid timestamps.
        }

        // Calculate response time.
        if (lastAssistantTimestamp != null) {
          try {
            final assistantTime = DateTime.parse(
              lastAssistantTimestamp,
            ).millisecondsSinceEpoch;
            final userTime = DateTime.parse(
              msgTimestamp,
            ).millisecondsSinceEpoch;
            final responseTimeSec = (userTime - assistantTime) / 1000.0;
            // Only count reasonable response times (2s-1 hour).
            if (responseTimeSec > 2 && responseTimeSec < 3600) {
              stats.userResponseTimes.add(responseTimeSec);
            }
          } catch (_) {
            // Skip invalid timestamps.
          }
        }
      }

      // Process tool results for error tracking.
      if (content is List) {
        for (final block in content) {
          if (block is Map<String, dynamic> &&
              block['type'] == 'tool_result' &&
              block['is_error'] == true) {
            stats.toolErrors++;
            final resultContent = block['content'] as String? ?? '';
            final category = categorizeToolError(resultContent);
            stats.toolErrorCategories[category] =
                (stats.toolErrorCategories[category] ?? 0) + 1;
          }
        }
      }

      // Check for interruptions.
      if (content is String &&
          content.contains('[Request interrupted by user')) {
        stats.userInterruptions++;
      } else if (content is List) {
        for (final block in content) {
          if (block is Map<String, dynamic> &&
              block['type'] == 'text' &&
              (block['text'] as String? ?? '').contains(
                '[Request interrupted by user',
              )) {
            stats.userInterruptions++;
            break;
          }
        }
      }
    }
  }

  return stats;
}