Skip to content

Commit 6ee2372

Browse files
simonwclaude
andcommitted
Render images in tool_result content arrays
Also disables truncation for tool results containing images. https://gisthost.github.io/?f48425d2e819740d770a52a4e05199a3/index.html Closes #53 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 854a4f8 commit 6ee2372

File tree

4 files changed

+102
-3
lines changed

4 files changed

+102
-3
lines changed

src/claude_code_transcripts/__init__.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@ def render_content_block(block):
708708
elif block_type == "tool_result":
709709
content = block.get("content", "")
710710
is_error = block.get("is_error", False)
711+
has_images = False
711712

712713
# Check for git commits and render with styled cards
713714
if isinstance(content, str):
@@ -737,11 +738,35 @@ def render_content_block(block):
737738
content_html = "".join(parts)
738739
else:
739740
content_html = f"<pre>{html.escape(content)}</pre>"
740-
elif isinstance(content, list) or is_json_like(content):
741+
elif isinstance(content, list):
742+
# Handle tool result content that contains multiple blocks (text, images, etc.)
743+
parts = []
744+
for item in content:
745+
if isinstance(item, dict):
746+
item_type = item.get("type", "")
747+
if item_type == "text":
748+
text = item.get("text", "")
749+
if text:
750+
parts.append(f"<pre>{html.escape(text)}</pre>")
751+
elif item_type == "image":
752+
source = item.get("source", {})
753+
media_type = source.get("media_type", "image/png")
754+
data = source.get("data", "")
755+
if data:
756+
parts.append(_macros.image_block(media_type, data))
757+
has_images = True
758+
else:
759+
# Unknown type, render as JSON
760+
parts.append(format_json(item))
761+
else:
762+
# Non-dict item, escape as text
763+
parts.append(f"<pre>{html.escape(str(item))}</pre>")
764+
content_html = "".join(parts) if parts else format_json(content)
765+
elif is_json_like(content):
741766
content_html = format_json(content)
742767
else:
743768
content_html = format_json(content)
744-
return _macros.tool_result(content_html, is_error)
769+
return _macros.tool_result(content_html, is_error, has_images)
745770
else:
746771
return format_json(block)
747772

src/claude_code_transcripts/templates/macros.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,14 @@
111111
{%- endmacro %}
112112

113113
{# Tool result - content_html is pre-rendered so needs |safe #}
114-
{% macro tool_result(content_html, is_error) %}
114+
{# has_images=True disables truncation so images are always visible #}
115+
{% macro tool_result(content_html, is_error, has_images=False) %}
115116
{%- set error_class = ' tool-error' if is_error else '' -%}
117+
{%- if has_images -%}
118+
<div class="tool-result{{ error_class }}">{{ content_html|safe }}</div>
119+
{%- else -%}
116120
<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>
121+
{%- endif -%}
117122
{%- endmacro %}
118123

119124
{# Thinking block - content_html is pre-rendered markdown so needs |safe #}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div class="tool-result"><pre>Successfully captured screenshot (807x782, jpeg) - ID: ss_123</pre><pre>
2+
3+
Tab Context:
4+
- Executed on tabId: 12345</pre>
5+
<div class="image-block"><img src="data:image/gif;base64,R0lGODlhyADIAIAAAAAAAAAAACwAAAAAyADIAAAIAgQBADs=" style="max-width: 100%"></div></div>

tests/test_generate_html.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,70 @@ def test_tool_result_with_commit(self, snapshot_html):
302302
finally:
303303
claude_code_transcripts._github_repo = old_repo
304304

305+
def test_tool_result_with_image(self, snapshot_html):
306+
"""Test tool result containing image blocks in content array.
307+
308+
This tests the case where a tool (like a screenshot tool) returns
309+
both text and image content in the same tool_result.
310+
"""
311+
import base64
312+
313+
# Create a minimal GIF image
314+
gif_data = (
315+
b"GIF89a" # Header
316+
b"\xc8\x00\xc8\x00" # Width 200, Height 200
317+
b"\x80" # Global color table flag
318+
b"\x00" # Background color index
319+
b"\x00" # Pixel aspect ratio
320+
b"\x00\x00\x00" # Color 0: black
321+
b"\x00\x00\x00" # Color 1: black
322+
b"," # Image separator
323+
b"\x00\x00\x00\x00" # Left, Top
324+
b"\xc8\x00\xc8\x00" # Width 200, Height 200
325+
b"\x00" # No local color table
326+
b"\x08" # LZW minimum code size
327+
b"\x02\x04\x01\x00" # Compressed data
328+
b";" # GIF trailer
329+
)
330+
gif_base64 = base64.b64encode(gif_data).decode("ascii")
331+
332+
block = {
333+
"type": "tool_result",
334+
"content": [
335+
{
336+
"type": "text",
337+
"text": "Successfully captured screenshot (807x782, jpeg) - ID: ss_123",
338+
},
339+
{
340+
"type": "text",
341+
"text": "\n\nTab Context:\n- Executed on tabId: 12345",
342+
},
343+
{
344+
"type": "image",
345+
"source": {
346+
"type": "base64",
347+
"media_type": "image/gif",
348+
"data": gif_base64,
349+
},
350+
},
351+
],
352+
"is_error": False,
353+
}
354+
result = render_content_block(block)
355+
356+
# The result should contain the text content
357+
assert "Successfully captured screenshot" in result
358+
assert "Tab Context" in result
359+
360+
# The result should contain an img tag with data URL for the image
361+
assert 'src="data:image/gif;base64,' in result
362+
assert "max-width: 100%" in result
363+
364+
# Tool results with images should NOT be truncatable
365+
assert "truncatable" not in result
366+
367+
assert result == snapshot_html
368+
305369

306370
class TestAnalyzeConversation:
307371
"""Tests for conversation analysis."""

0 commit comments

Comments
 (0)