Fix the markdown rendering issue in tool results when content is a Python list of content blocks (not just a JSON string).
The current implementation correctly handles tool result content when it's a JSON string like:
'[{"type": "text", "text": "## Report\n\n- Item 1"}]'However, it fails to render markdown when the content is already a Python list:
[{"type": "text", "text": "## Report\n\n- Item 1"}]This causes markdown headers, lists, and code blocks to appear as raw text instead of rendered HTML.
In src/claude_code_transcripts/__init__.py, the render_content_block() function has this logic for tool_result blocks (lines 1020-1084):
-
Lines 1033-1044: When
contentis a string, it checks if it's a content block array usingis_content_block_array(), parses it, and callsrender_content_block_array()to render markdown. -
Lines 1080-1081: When
contentis a list, it immediately callsformat_json(content)WITHOUT checking if it's a content block array that should be rendered as markdown.
# BUG: Line 1080 - doesn't check for content block array
elif isinstance(content, list) or is_json_like(content):
content_markdown_html = format_json(content) # Should render as markdown!Add a new helper function after is_content_block_array() (around line 138) to detect when a Python list is a content block array:
def is_content_block_list(content):
"""Check if content is a Python list of content blocks.
Args:
content: Content to check.
Returns:
True if content is a list containing dict items with 'type' keys.
"""
if not isinstance(content, list):
return False
if not content:
return False
# Check if items look like content blocks
for item in content:
if isinstance(item, dict) and "type" in item:
return True
return FalseModify lines 1080-1083 to check for content block lists and render them properly:
Before:
elif isinstance(content, list) or is_json_like(content):
content_markdown_html = format_json(content)
else:
content_markdown_html = format_json(content)After:
elif isinstance(content, list):
# Check if it's a content block array that should be rendered as markdown
if is_content_block_list(content):
rendered = render_content_block_array(content)
if rendered:
content_markdown_html = rendered
else:
content_markdown_html = format_json(content)
else:
content_markdown_html = format_json(content)
else:
content_markdown_html = format_json(content)Add a new test after test_tool_result_content_block_array_with_tool_use:
def test_tool_result_content_block_list_renders_markdown(self, snapshot_html):
"""Test that tool_result with content as Python list renders markdown properly."""
block = {
"type": "tool_result",
"content": [
{"type": "text", "text": "## Report\n\n- Item 1\n- Item 2\n\n```python\ncode\n```"}
],
"is_error": False,
}
result = render_content_block(block)
# Should render as HTML, not raw JSON
assert "<h2>Report</h2>" in result or "<h2>" in result
assert "<li>Item 1</li>" in result or "<li>" in result
# Should not show raw JSON structure
assert '"type": "text"' not in result
assert result == snapshot_html
def test_tool_result_content_block_list_with_multiple_blocks(self, snapshot_html):
"""Test that tool_result with multiple text blocks renders all as markdown."""
block = {
"type": "tool_result",
"content": [
{"type": "text", "text": "## First Section\n\n- Point A"},
{"type": "text", "text": "## Second Section\n\n- Point B"}
],
"is_error": False,
}
result = render_content_block(block)
# Both sections should be rendered
assert "First Section" in result
assert "Second Section" in result
assert "Point A" in result
assert "Point B" in result
# Should not show raw JSON
assert '"type": "text"' not in result
assert result == snapshot_htmlAdd tests for the new helper function:
class TestIsContentBlockList:
"""Tests for is_content_block_list helper function."""
def test_empty_list(self):
assert is_content_block_list([]) == False
def test_list_with_text_block(self):
assert is_content_block_list([{"type": "text", "text": "hello"}]) == True
def test_list_with_image_block(self):
assert is_content_block_list([{"type": "image", "source": {}}]) == True
def test_list_with_mixed_blocks(self):
content = [
{"type": "text", "text": "hello"},
{"type": "image", "source": {}}
]
assert is_content_block_list(content) == True
def test_list_without_type(self):
assert is_content_block_list([{"key": "value"}]) == False
def test_not_a_list(self):
assert is_content_block_list("string") == False
assert is_content_block_list({"type": "text"}) == False
assert is_content_block_list(None) == False-
Run existing tests to ensure no regressions:
uv run pytest tests/test_generate_html.py -v
-
Run new tests for the fix:
uv run pytest tests/test_generate_html.py::TestRenderContentBlock::test_tool_result_content_block_list_renders_markdown -v uv run pytest tests/test_generate_html.py::TestIsContentBlockList -v
-
Update snapshots if needed:
uv run pytest --snapshot-update
- Create a test session JSON with tool result content as Python list
- Generate HTML using
uv run claude-code-transcripts - Verify markdown is rendered (headings, lists, code blocks display correctly)
Important
The fix adds a new helper function is_content_block_list() which is similar to is_content_block_array() but for Python lists instead of JSON strings. Consider if these could be unified.
| File | Action | Description |
|---|---|---|
src/claude_code_transcripts/__init__.py |
MODIFY | Add is_content_block_list() function and fix tool_result handling |
tests/test_generate_html.py |
MODIFY | Add tests for new functionality |
tests/__snapshots__/*.ambr |
UPDATE | Snapshot updates from new tests |