applyGrouping function

GroupingResult applyGrouping({
  1. required List<NormalizedMessage> messages,
  2. required Set<String> toolsWithGrouping,
  3. bool verbose = false,
})

Groups tool uses by message.id (same API response) if the tool supports grouped rendering.

Only groups 2+ tools of the same type from the same message. Also collects corresponding tool_results and attaches them to the grouped message. When verbose is true, skips grouping so messages render at original positions.

Implementation

GroupingResult applyGrouping({
  required List<NormalizedMessage> messages,
  required Set<String> toolsWithGrouping,
  bool verbose = false,
}) {
  if (verbose) {
    return GroupingResult(messages: messages);
  }

  // First pass: group tool uses by message.id + tool name
  final groups = <String, List<NormalizedMessage>>{};

  for (final msg in messages) {
    final info = getToolUseInfo(msg);
    if (info != null && toolsWithGrouping.contains(info.toolName)) {
      final key = '${info.messageId}:${info.toolName}';
      groups.putIfAbsent(key, () => []).add(msg);
    }
  }

  // Identify valid groups (2+ items) and collect their tool use IDs
  final validGroups = <String, List<NormalizedMessage>>{};
  final groupedToolUseIds = <String>{};

  for (final entry in groups.entries) {
    if (entry.value.length >= 2) {
      validGroups[entry.key] = entry.value;
      for (final msg in entry.value) {
        final info = getToolUseInfo(msg);
        if (info != null) {
          groupedToolUseIds.add(info.toolUseId);
        }
      }
    }
  }

  // Collect result messages for grouped tool_uses
  final resultsByToolUseId = <String, NormalizedMessage>{};

  for (final msg in messages) {
    if (msg.type != 'user') continue;
    final content = msg.message['content'];
    if (content is! List) continue;
    for (final block in content) {
      if (block is Map<String, dynamic> &&
          block['type'] == 'tool_result' &&
          groupedToolUseIds.contains(block['tool_use_id'])) {
        resultsByToolUseId[block['tool_use_id'] as String] = msg;
      }
    }
  }

  // Second pass: build output, emitting each group only once
  final result = <dynamic>[];
  final emittedGroups = <String>{};

  for (final msg in messages) {
    final info = getToolUseInfo(msg);

    if (info != null) {
      final key = '${info.messageId}:${info.toolName}';
      final group = validGroups[key];

      if (group != null) {
        if (!emittedGroups.contains(key)) {
          emittedGroups.add(key);
          final firstMsg = group.first;

          // Collect results for this group
          final results = <NormalizedMessage>[];
          for (final assistantMsg in group) {
            final content = assistantMsg.message['content'];
            if (content is List && content.isNotEmpty) {
              final toolUseId =
                  (content[0] as Map<String, dynamic>)['id'] as String?;
              if (toolUseId != null) {
                final resultMsg = resultsByToolUseId[toolUseId];
                if (resultMsg != null) results.add(resultMsg);
              }
            }
          }

          result.add(
            GroupedToolUseMessage(
              toolName: info.toolName,
              messages: group,
              results: results,
              displayMessage: firstMsg,
              uuid: 'grouped-${firstMsg.uuid}',
              timestamp: firstMsg.timestamp,
              messageId: info.messageId,
            ),
          );
        }
        continue;
      }
    }

    // Skip user messages whose tool_results are all grouped
    if (msg.type == 'user') {
      final content = msg.message['content'];
      if (content is List) {
        final toolResults = content
            .whereType<Map<String, dynamic>>()
            .where((c) => c['type'] == 'tool_result')
            .toList();
        if (toolResults.isNotEmpty) {
          final allGrouped = toolResults.every(
            (tr) => groupedToolUseIds.contains(tr['tool_use_id']),
          );
          if (allGrouped) continue;
        }
      }
    }

    result.add(msg);
  }

  return GroupingResult(messages: result);
}