@@ -757,6 +757,66 @@ def render_user_message_content(message_data):
757757 return f"<p>{ html .escape (str (content ))} </p>"
758758
759759
760+ def filter_tool_result_blocks (content , paired_tool_ids ):
761+ if not isinstance (content , list ):
762+ return content
763+ filtered = []
764+ for block in content :
765+ if (
766+ isinstance (block , dict )
767+ and block .get ("type" ) == "tool_result"
768+ and block .get ("tool_use_id" ) in paired_tool_ids
769+ ):
770+ continue
771+ filtered .append (block )
772+ return filtered
773+
774+
775+ def is_tool_result_content (content ):
776+ if not isinstance (content , list ) or not content :
777+ return False
778+ return all (
779+ isinstance (block , dict ) and block .get ("type" ) == "tool_result"
780+ for block in content
781+ )
782+
783+
784+ def render_user_message_content_with_tool_pairs (message_data , paired_tool_ids ):
785+ content = message_data .get ("content" , "" )
786+ if isinstance (content , str ):
787+ return render_user_message_content (message_data )
788+ if isinstance (content , list ):
789+ filtered = filter_tool_result_blocks (content , paired_tool_ids )
790+ if not filtered :
791+ return ""
792+ return "" .join (render_content_block (block ) for block in filtered )
793+ return f"<p>{ html .escape (str (content ))} </p>"
794+
795+
796+ def render_assistant_message_with_tool_pairs (
797+ message_data , tool_result_lookup , paired_tool_ids
798+ ):
799+ content = message_data .get ("content" , [])
800+ if not isinstance (content , list ):
801+ return f"<p>{ html .escape (str (content ))} </p>"
802+ parts = []
803+ for block in content :
804+ if not isinstance (block , dict ):
805+ parts .append (f"<p>{ html .escape (str (block ))} </p>" )
806+ continue
807+ if block .get ("type" ) == "tool_use" :
808+ tool_id = block .get ("id" , "" )
809+ tool_result = tool_result_lookup .get (tool_id )
810+ if tool_result :
811+ paired_tool_ids .add (tool_id )
812+ tool_use_html = render_content_block (block )
813+ tool_result_html = render_content_block (tool_result )
814+ parts .append (_macros .tool_pair (tool_use_html , tool_result_html ))
815+ continue
816+ parts .append (render_content_block (block ))
817+ return "" .join (parts )
818+
819+
760820def render_assistant_message (message_data ):
761821 content = message_data .get ("content" , [])
762822 if not isinstance (content , list ):
@@ -877,6 +937,34 @@ def render_message(log_type, message_json, timestamp):
877937 return _macros .message (role_class , role_label , msg_id , timestamp , content_html )
878938
879939
940+ def render_message_with_tool_pairs (
941+ log_type , message_data , timestamp , tool_result_lookup , paired_tool_ids
942+ ):
943+ if log_type == "user" :
944+ content = message_data .get ("content" , "" )
945+ filtered = filter_tool_result_blocks (content , paired_tool_ids )
946+ content_html = render_user_message_content_with_tool_pairs (
947+ message_data , paired_tool_ids
948+ )
949+ if not content_html .strip ():
950+ return ""
951+ if is_tool_result_content (filtered ):
952+ role_class , role_label = "tool-reply" , "Tool reply"
953+ else :
954+ role_class , role_label = "user" , "User"
955+ elif log_type == "assistant" :
956+ content_html = render_assistant_message_with_tool_pairs (
957+ message_data , tool_result_lookup , paired_tool_ids
958+ )
959+ role_class , role_label = "assistant" , "Assistant"
960+ else :
961+ return ""
962+ if not content_html .strip ():
963+ return ""
964+ msg_id = make_msg_id (timestamp )
965+ return _macros .message (role_class , role_label , msg_id , timestamp , content_html )
966+
967+
880968CSS = """
881969: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; }
882970* { box-sizing: border-box; }
@@ -913,6 +1001,8 @@ def render_message(log_type, message_json, timestamp):
9131001.tool-description { font-size: 0.9rem; color: var(--text-muted); margin-bottom: 8px; font-style: italic; }
9141002.tool-result { background: var(--tool-result-bg); border-radius: 8px; padding: 12px; margin: 12px 0; }
9151003.tool-result.tool-error { background: var(--tool-error-bg); }
1004+ .tool-pair { border: 1px solid var(--tool-border); border-radius: 8px; padding: 8px; margin: 12px 0; background: rgba(156, 39, 176, 0.06); }
1005+ .tool-pair .tool-use, .tool-pair .tool-result { margin: 8px 0; }
9161006.file-tool { border-radius: 8px; padding: 12px; margin: 12px 0; }
9171007.write-tool { background: linear-gradient(135deg, #e3f2fd 0%, #e8f5e9 100%); border: 1px solid #4caf50; }
9181008.edit-tool { background: linear-gradient(135deg, #fff3e0 0%, #fce4ec 100%); border: 1px solid #ff9800; }
@@ -1219,8 +1309,32 @@ def generate_html(json_path, output_dir, github_repo=None):
12191309 messages_html = []
12201310 for conv in page_convs :
12211311 is_first = True
1312+ parsed_messages = []
12221313 for log_type , message_json , timestamp in conv ["messages" ]:
1223- msg_html = render_message (log_type , message_json , timestamp )
1314+ try :
1315+ message_data = json .loads (message_json )
1316+ except json .JSONDecodeError :
1317+ continue
1318+ parsed_messages .append ((log_type , message_data , timestamp ))
1319+ tool_result_lookup = {}
1320+ for log_type , message_data , _ in parsed_messages :
1321+ content = message_data .get ("content" , [])
1322+ if not isinstance (content , list ):
1323+ continue
1324+ for block in content :
1325+ if (
1326+ isinstance (block , dict )
1327+ and block .get ("type" ) == "tool_result"
1328+ and block .get ("tool_use_id" )
1329+ ):
1330+ tool_id = block .get ("tool_use_id" )
1331+ if tool_id not in tool_result_lookup :
1332+ tool_result_lookup [tool_id ] = block
1333+ paired_tool_ids = set ()
1334+ for log_type , message_data , timestamp in parsed_messages :
1335+ msg_html = render_message_with_tool_pairs (
1336+ log_type , message_data , timestamp , tool_result_lookup , paired_tool_ids
1337+ )
12241338 if msg_html :
12251339 # Wrap continuation summaries in collapsed details
12261340 if is_first and conv .get ("is_continuation" ):
@@ -1689,8 +1803,32 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
16891803 messages_html = []
16901804 for conv in page_convs :
16911805 is_first = True
1806+ parsed_messages = []
16921807 for log_type , message_json , timestamp in conv ["messages" ]:
1693- msg_html = render_message (log_type , message_json , timestamp )
1808+ try :
1809+ message_data = json .loads (message_json )
1810+ except json .JSONDecodeError :
1811+ continue
1812+ parsed_messages .append ((log_type , message_data , timestamp ))
1813+ tool_result_lookup = {}
1814+ for log_type , message_data , _ in parsed_messages :
1815+ content = message_data .get ("content" , [])
1816+ if not isinstance (content , list ):
1817+ continue
1818+ for block in content :
1819+ if (
1820+ isinstance (block , dict )
1821+ and block .get ("type" ) == "tool_result"
1822+ and block .get ("tool_use_id" )
1823+ ):
1824+ tool_id = block .get ("tool_use_id" )
1825+ if tool_id not in tool_result_lookup :
1826+ tool_result_lookup [tool_id ] = block
1827+ paired_tool_ids = set ()
1828+ for log_type , message_data , timestamp in parsed_messages :
1829+ msg_html = render_message_with_tool_pairs (
1830+ log_type , message_data , timestamp , tool_result_lookup , paired_tool_ids
1831+ )
16941832 if msg_html :
16951833 # Wrap continuation summaries in collapsed details
16961834 if is_first and conv .get ("is_continuation" ):
0 commit comments