@@ -52,6 +52,45 @@ def get_template(name):
5252 300 # Characters - text blocks longer than this are shown in index
5353)
5454
55+ # Tool type icons for display in tool headers
56+ TOOL_ICONS = {
57+ # File operations
58+ "Read" : "📖" ,
59+ "Write" : "📝" ,
60+ "Edit" : "✏️" ,
61+ "NotebookEdit" : "📓" ,
62+ # Search/find operations
63+ "Glob" : "🔍" ,
64+ "Grep" : "🔎" ,
65+ # Terminal operations
66+ "Bash" : "$" ,
67+ # Web operations
68+ "WebFetch" : "🌐" ,
69+ "WebSearch" : "🔎" ,
70+ # Task management
71+ "TodoWrite" : "☰" ,
72+ "Task" : "📋" ,
73+ # Other tools
74+ "Skill" : "⚡" ,
75+ "Agent" : "🤖" ,
76+ }
77+
78+ # Default icon for tools not in the mapping
79+ DEFAULT_TOOL_ICON = "⚙"
80+
81+
82+ def get_tool_icon (tool_name ):
83+ """Get the appropriate icon for a tool name.
84+
85+ Args:
86+ tool_name: The name of the tool.
87+
88+ Returns:
89+ The icon string for the tool.
90+ """
91+ return TOOL_ICONS .get (tool_name , DEFAULT_TOOL_ICON )
92+
93+
5594# Regex to strip ANSI escape sequences from terminal output
5695ANSI_ESCAPE_PATTERN = re .compile (
5796 r"""
@@ -141,6 +180,61 @@ def highlight_code(code, filename=None, language=None):
141180 return highlighted
142181
143182
183+ def calculate_message_metadata (message_data ):
184+ """Calculate metadata for a message.
185+
186+ Args:
187+ message_data: Parsed message JSON data.
188+
189+ Returns:
190+ Dict with char_count, token_estimate, and tool_counts.
191+ """
192+ content = message_data .get ("content" , "" )
193+
194+ # Calculate character count from all text content
195+ if isinstance (content , str ):
196+ char_count = len (content )
197+ elif isinstance (content , list ):
198+ char_count = 0
199+ for block in content :
200+ if isinstance (block , dict ):
201+ block_type = block .get ("type" , "" )
202+ if block_type == "text" :
203+ char_count += len (block .get ("text" , "" ))
204+ elif block_type == "thinking" :
205+ char_count += len (block .get ("thinking" , "" ))
206+ elif block_type == "tool_use" :
207+ # Count the input JSON as text
208+ char_count += len (json .dumps (block .get ("input" , {})))
209+ elif block_type == "tool_result" :
210+ result_content = block .get ("content" , "" )
211+ if isinstance (result_content , str ):
212+ char_count += len (result_content )
213+ elif isinstance (result_content , list ):
214+ for item in result_content :
215+ if isinstance (item , dict ) and item .get ("type" ) == "text" :
216+ char_count += len (item .get ("text" , "" ))
217+ else :
218+ char_count = len (str (content ))
219+
220+ # Token estimate (approximately 4 characters per token)
221+ token_estimate = char_count // 4
222+
223+ # Count tool calls
224+ tool_counts = {}
225+ if isinstance (content , list ):
226+ for block in content :
227+ if isinstance (block , dict ) and block .get ("type" ) == "tool_use" :
228+ tool_name = block .get ("name" , "Unknown" )
229+ tool_counts [tool_name ] = tool_counts .get (tool_name , 0 ) + 1
230+
231+ return {
232+ "char_count" : char_count ,
233+ "token_estimate" : token_estimate ,
234+ "tool_counts" : tool_counts ,
235+ }
236+
237+
144238def extract_text_from_content (content ):
145239 """Extract plain text from message content.
146240
@@ -872,8 +966,14 @@ def render_content_block(block):
872966 display_input = {k : v for k , v in tool_input .items () if k != "description" }
873967 input_markdown_html = render_json_with_markdown (display_input )
874968 input_json_html = format_json (display_input )
969+ tool_icon = get_tool_icon (tool_name )
875970 return _macros .tool_use (
876- tool_name , description_html , input_markdown_html , input_json_html , tool_id
971+ tool_name ,
972+ tool_icon ,
973+ description_html ,
974+ input_markdown_html ,
975+ input_json_html ,
976+ tool_id ,
877977 )
878978 elif block_type == "tool_result" :
879979 content = block .get ("content" , "" )
@@ -1290,7 +1390,14 @@ def render_message(log_type, message_json, timestamp):
12901390 if not content_html .strip ():
12911391 return ""
12921392 msg_id = make_msg_id (timestamp )
1293- return _macros .message (role_class , role_label , msg_id , timestamp , content_html )
1393+ # Calculate and render metadata
1394+ metadata = calculate_message_metadata (message_data )
1395+ metadata_html = _macros .metadata (
1396+ metadata ["char_count" ], metadata ["token_estimate" ], metadata ["tool_counts" ]
1397+ )
1398+ return _macros .message (
1399+ role_class , role_label , msg_id , timestamp , content_html , metadata_html
1400+ )
12941401
12951402
12961403def render_message_with_tool_pairs (
@@ -1318,7 +1425,14 @@ def render_message_with_tool_pairs(
13181425 if not content_html .strip ():
13191426 return ""
13201427 msg_id = make_msg_id (timestamp )
1321- return _macros .message (role_class , role_label , msg_id , timestamp , content_html )
1428+ # Calculate and render metadata
1429+ metadata = calculate_message_metadata (message_data )
1430+ metadata_html = _macros .metadata (
1431+ metadata ["char_count" ], metadata ["token_estimate" ], metadata ["tool_counts" ]
1432+ )
1433+ return _macros .message (
1434+ role_class , role_label , msg_id , timestamp , content_html , metadata_html
1435+ )
13221436
13231437
13241438CSS = """
@@ -1460,7 +1574,7 @@ def render_message_with_tool_pairs(
14601574.cell-copy-btn.copied { background: var(--accent-green-bg); color: var(--accent-green); border-color: var(--accent-green); }
14611575.tool-use { background: var(--tool-bg); border: 1px solid var(--tool-border); border-radius: var(--border-radius-md); padding: var(--spacing-md); margin: var(--spacing-md) 0; }
14621576.tool-header { font-weight: 600; color: var(--accent-purple); margin-bottom: var(--spacing-sm); display: flex; align-items: center; gap: var(--spacing-sm); position: sticky; top: calc(var(--sticky-level-0) + var(--sticky-level-1)); z-index: 10; background: var(--glass-bg); backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur); padding: var(--spacing-xs) 0; flex-wrap: wrap; }
1463- .tool-icon { font-size: var(--font-size-lg); }
1577+ .tool-icon { font-size: var(--font-size-lg); min-width: 1.5em; text-align: center; }
14641578.tool-description { font-size: var(--font-size-sm); color: var(--text-muted); margin-bottom: var(--spacing-sm); font-style: italic; }
14651579.tool-description p { margin: 0; }
14661580.tool-input-rendered { font-family: monospace; white-space: pre-wrap; font-size: var(--font-size-sm); line-height: 1.5; }
@@ -1629,6 +1743,16 @@ def render_message_with_tool_pairs(
16291743.search-result-page { padding: var(--spacing-sm) var(--spacing-md); background: var(--border-light); font-size: var(--font-size-xs); color: var(--text-muted); border-bottom: 1px solid var(--border-light); }
16301744.search-result-content { padding: var(--spacing-md); }
16311745.search-result mark { background: rgba(245, 158, 11, 0.3); padding: 1px 2px; border-radius: 2px; }
1746+ /* Metadata subsection */
1747+ .message-metadata { margin: 0; border-radius: var(--border-radius-sm); font-size: var(--font-size-xs); }
1748+ .message-metadata summary { cursor: pointer; padding: var(--spacing-xs) var(--spacing-sm); color: var(--text-muted); list-style: none; display: flex; align-items: center; gap: var(--spacing-xs); }
1749+ .message-metadata summary::-webkit-details-marker { display: none; }
1750+ .message-metadata summary::before { content: 'i'; display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; font-size: 10px; font-weight: 600; font-style: italic; font-family: Georgia, serif; background: var(--border-light); border-radius: 50%; color: var(--text-muted); }
1751+ .message-metadata[open] summary { border-bottom: 1px solid var(--border-light); }
1752+ .metadata-content { padding: var(--spacing-sm); background: var(--bg-secondary); border-radius: 0 0 var(--border-radius-sm) var(--border-radius-sm); display: flex; flex-wrap: wrap; gap: var(--spacing-sm) var(--spacing-md); }
1753+ .metadata-item { display: flex; align-items: center; gap: var(--spacing-xs); }
1754+ .metadata-label { color: var(--text-muted); font-weight: 500; }
1755+ .metadata-value { color: var(--text-secondary); font-family: monospace; }
16321756@media (max-width: 600px) { body { padding: var(--spacing-sm); } .message, .index-item { border-radius: var(--border-radius-md); } .message-content, .index-item-content { padding: var(--spacing-md); } pre { font-size: var(--font-size-xs); padding: var(--spacing-sm); } #search-box input { width: 120px; } #search-modal[open] { width: 95vw; height: 90vh; } }
16331757"""
16341758
0 commit comments