Skip to content

Commit ef4b819

Browse files
committed
Merge feature/tool-use-result-pairing: pair tool_use with tool_result by ID
2 parents 82a533f + 8981b63 commit ef4b819

File tree

5 files changed

+189
-34
lines changed

5 files changed

+189
-34
lines changed

src/claude_code_transcripts/__init__.py

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,66 @@ def render_user_message_content(message_data):
871871
return f"<p>{html.escape(str(content))}</p>"
872872

873873

874+
def filter_tool_result_blocks(content, paired_tool_ids):
875+
if not isinstance(content, list):
876+
return content
877+
filtered = []
878+
for block in content:
879+
if (
880+
isinstance(block, dict)
881+
and block.get("type") == "tool_result"
882+
and block.get("tool_use_id") in paired_tool_ids
883+
):
884+
continue
885+
filtered.append(block)
886+
return filtered
887+
888+
889+
def is_tool_result_content(content):
890+
if not isinstance(content, list) or not content:
891+
return False
892+
return all(
893+
isinstance(block, dict) and block.get("type") == "tool_result"
894+
for block in content
895+
)
896+
897+
898+
def render_user_message_content_with_tool_pairs(message_data, paired_tool_ids):
899+
content = message_data.get("content", "")
900+
if isinstance(content, str):
901+
return render_user_message_content(message_data)
902+
if isinstance(content, list):
903+
filtered = filter_tool_result_blocks(content, paired_tool_ids)
904+
if not filtered:
905+
return ""
906+
return "".join(render_content_block(block) for block in filtered)
907+
return f"<p>{html.escape(str(content))}</p>"
908+
909+
910+
def render_assistant_message_with_tool_pairs(
911+
message_data, tool_result_lookup, paired_tool_ids
912+
):
913+
content = message_data.get("content", [])
914+
if not isinstance(content, list):
915+
return f"<p>{html.escape(str(content))}</p>"
916+
parts = []
917+
for block in content:
918+
if not isinstance(block, dict):
919+
parts.append(f"<p>{html.escape(str(block))}</p>")
920+
continue
921+
if block.get("type") == "tool_use":
922+
tool_id = block.get("id", "")
923+
tool_result = tool_result_lookup.get(tool_id)
924+
if tool_result:
925+
paired_tool_ids.add(tool_id)
926+
tool_use_html = render_content_block(block)
927+
tool_result_html = render_content_block(tool_result)
928+
parts.append(_macros.tool_pair(tool_use_html, tool_result_html))
929+
continue
930+
parts.append(render_content_block(block))
931+
return "".join(parts)
932+
933+
874934
def render_assistant_message(message_data):
875935
content = message_data.get("content", [])
876936
if not isinstance(content, list):
@@ -991,6 +1051,34 @@ def render_message(log_type, message_json, timestamp):
9911051
return _macros.message(role_class, role_label, msg_id, timestamp, content_html)
9921052

9931053

1054+
def render_message_with_tool_pairs(
1055+
log_type, message_data, timestamp, tool_result_lookup, paired_tool_ids
1056+
):
1057+
if log_type == "user":
1058+
content = message_data.get("content", "")
1059+
filtered = filter_tool_result_blocks(content, paired_tool_ids)
1060+
content_html = render_user_message_content_with_tool_pairs(
1061+
message_data, paired_tool_ids
1062+
)
1063+
if not content_html.strip():
1064+
return ""
1065+
if is_tool_result_content(filtered):
1066+
role_class, role_label = "tool-reply", "Tool reply"
1067+
else:
1068+
role_class, role_label = "user", "User"
1069+
elif log_type == "assistant":
1070+
content_html = render_assistant_message_with_tool_pairs(
1071+
message_data, tool_result_lookup, paired_tool_ids
1072+
)
1073+
role_class, role_label = "assistant", "Assistant"
1074+
else:
1075+
return ""
1076+
if not content_html.strip():
1077+
return ""
1078+
msg_id = make_msg_id(timestamp)
1079+
return _macros.message(role_class, role_label, msg_id, timestamp, content_html)
1080+
1081+
9941082
CSS = """
9951083
:root { --bg-color: #f5f5f5; --card-bg: #ffffff; --user-bg: #e3f2fd; --user-border: #1976d2; --assistant-bg: #f5f5f5; --assistant-border: #9e9e9e; --thinking-bg: #fff8e1; --thinking-border: #ffc107; --thinking-text: #666; --tool-bg: #f3e5f5; --tool-border: #9c27b0; --tool-result-bg: #e8f5e9; --tool-error-bg: #ffebee; --text-color: #212121; --text-muted: #757575; --code-bg: #263238; --code-text: #aed581; }
9961084
* { box-sizing: border-box; }
@@ -1027,6 +1115,8 @@ def render_message(log_type, message_json, timestamp):
10271115
.tool-description { font-size: 0.9rem; color: var(--text-muted); margin-bottom: 8px; font-style: italic; }
10281116
.tool-result { background: var(--tool-result-bg); border-radius: 8px; padding: 12px; margin: 12px 0; }
10291117
.tool-result.tool-error { background: var(--tool-error-bg); }
1118+
.tool-pair { border: 1px solid var(--tool-border); border-radius: 8px; padding: 8px; margin: 12px 0; background: rgba(156, 39, 176, 0.06); }
1119+
.tool-pair .tool-use, .tool-pair .tool-result { margin: 8px 0; }
10301120
.file-tool { border-radius: 8px; padding: 12px; margin: 12px 0; }
10311121
.write-tool { background: linear-gradient(135deg, #e3f2fd 0%, #e8f5e9 100%); border: 1px solid #4caf50; }
10321122
.edit-tool { background: linear-gradient(135deg, #fff3e0 0%, #fce4ec 100%); border: 1px solid #ff9800; }
@@ -1394,8 +1484,32 @@ def generate_html(json_path, output_dir, github_repo=None):
13941484
messages_html = []
13951485
for conv in page_convs:
13961486
is_first = True
1487+
parsed_messages = []
13971488
for log_type, message_json, timestamp in conv["messages"]:
1398-
msg_html = render_message(log_type, message_json, timestamp)
1489+
try:
1490+
message_data = json.loads(message_json)
1491+
except json.JSONDecodeError:
1492+
continue
1493+
parsed_messages.append((log_type, message_data, timestamp))
1494+
tool_result_lookup = {}
1495+
for log_type, message_data, _ in parsed_messages:
1496+
content = message_data.get("content", [])
1497+
if not isinstance(content, list):
1498+
continue
1499+
for block in content:
1500+
if (
1501+
isinstance(block, dict)
1502+
and block.get("type") == "tool_result"
1503+
and block.get("tool_use_id")
1504+
):
1505+
tool_id = block.get("tool_use_id")
1506+
if tool_id not in tool_result_lookup:
1507+
tool_result_lookup[tool_id] = block
1508+
paired_tool_ids = set()
1509+
for log_type, message_data, timestamp in parsed_messages:
1510+
msg_html = render_message_with_tool_pairs(
1511+
log_type, message_data, timestamp, tool_result_lookup, paired_tool_ids
1512+
)
13991513
if msg_html:
14001514
# Wrap continuation summaries in collapsed details
14011515
if is_first and conv.get("is_continuation"):
@@ -1864,8 +1978,32 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
18641978
messages_html = []
18651979
for conv in page_convs:
18661980
is_first = True
1981+
parsed_messages = []
18671982
for log_type, message_json, timestamp in conv["messages"]:
1868-
msg_html = render_message(log_type, message_json, timestamp)
1983+
try:
1984+
message_data = json.loads(message_json)
1985+
except json.JSONDecodeError:
1986+
continue
1987+
parsed_messages.append((log_type, message_data, timestamp))
1988+
tool_result_lookup = {}
1989+
for log_type, message_data, _ in parsed_messages:
1990+
content = message_data.get("content", [])
1991+
if not isinstance(content, list):
1992+
continue
1993+
for block in content:
1994+
if (
1995+
isinstance(block, dict)
1996+
and block.get("type") == "tool_result"
1997+
and block.get("tool_use_id")
1998+
):
1999+
tool_id = block.get("tool_use_id")
2000+
if tool_id not in tool_result_lookup:
2001+
tool_result_lookup[tool_id] = block
2002+
paired_tool_ids = set()
2003+
for log_type, message_data, timestamp in parsed_messages:
2004+
msg_html = render_message_with_tool_pairs(
2005+
log_type, message_data, timestamp, tool_result_lookup, paired_tool_ids
2006+
)
18692007
if msg_html:
18702008
# Wrap continuation summaries in collapsed details
18712009
if is_first and conv.get("is_continuation"):

src/claude_code_transcripts/templates/macros.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@
116116
<div class="tool-result{{ error_class }}"><div class="truncatable"><div class="truncatable-content">{{ content_html|safe }}</div><button class="expand-btn">Show more</button></div></div>
117117
{%- endmacro %}
118118

119+
{# Tool pair wrapper - tool_use_html/tool_result_html are pre-rendered #}
120+
{% macro tool_pair(tool_use_html, tool_result_html) %}
121+
<div class="tool-pair">{{ tool_use_html|safe }}{{ tool_result_html|safe }}</div>
122+
{%- endmacro %}
123+
119124
{# Thinking block - content_html is pre-rendered markdown so needs |safe #}
120125
{% macro thinking(content_html) %}
121126
<div class="thinking"><div class="thinking-label">Thinking</div>{{ content_html|safe }}</div>

0 commit comments

Comments
 (0)