@@ -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+
874934def 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+
9941082CSS = """
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" ):
0 commit comments