diff --git a/GRADING_REPORT.md b/GRADING_REPORT.md new file mode 100644 index 0000000..998d956 --- /dev/null +++ b/GRADING_REPORT.md @@ -0,0 +1,119 @@ +# Grading Report: Fix Tool Result Markdown Rendering + +## Deliverables Summary + +### 1. PR Comments Analysis + +Reviewed PRs #11, #12, and #13 on ShlomoStept's fork. Key findings: +- PR #13: Copilot noted extensive changes for content block list rendering +- PR #12: Focused on markdown/JSON rendering fixes and collapsible tool sections +- PR #11: Added subagent detection and visualization features +- All PRs were closed with note: "changes consolidated for upstream PR submission" + +### 2. Root Cause of Markdown Not Rendering + +**Location**: `src/claude_code_transcripts/__init__.py`, lines 1100-1103 (before fix) + +**The Bug**: +```python +elif isinstance(content, list) or is_json_like(content): + content_markdown_html = format_json(content) # Always JSON! +``` + +When `tool_result.content` was a Python list like: +```python +[{"type": "text", "text": "## Report\n- Item 1"}] +``` + +The code immediately called `format_json()` without checking if the list contained content blocks that should be rendered as markdown. + +**The existing handling for JSON strings worked** because `is_content_block_array()` checked for JSON-formatted strings and parsed/rendered them properly. But Python lists bypassed this check entirely. + +### 3. Fix Implementation + +**Changes Made**: + +1. Added `is_content_block_list()` helper function (lines 140-157): + - Detects when a Python list contains content block dicts with `"type"` keys + - Returns `True` for lists like `[{"type": "text", "text": "..."}]` + +2. Fixed `render_content_block()` tool_result handling (lines 1100-1111): + - Now checks `is_content_block_list(content)` before calling `format_json()` + - Calls `render_content_block_array(content)` to render markdown when appropriate + +**Code Diff**: +```python +# Before +elif isinstance(content, list) or is_json_like(content): + content_markdown_html = format_json(content) + +# After +elif isinstance(content, list): + 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) +``` + +### 4. Test Results + +``` +$ uv run pytest tests/test_generate_html.py -v +============================= test session starts ============================== +collected 119 items +... +119 passed in 0.78s +``` + +**New Tests Added**: +- `TestIsContentBlockList` class with 8 tests +- `test_tool_result_content_block_list_renders_markdown` +- `test_tool_result_content_block_list_with_multiple_blocks` + +### 5. Commit SHA + +**Branch**: `fix/tool-result-markdown-rendering` +**Commit**: `778d280` +**Repository**: ShlomoStept/claude-code-transcripts (origin) + +``` +git log -1 --oneline +778d280 Fix markdown rendering in tool results with Python list content +``` + +### 6. Subagent Display Analysis + +**Finding**: Subagent functionality (Task/Agent tools) is working correctly: + +1. **Full prompts available**: The `truncatable` class with "Show more" button is applied to all tool inputs. Users can expand to see full content. + +2. **Markdown renders in prompts**: The `render_json_with_markdown()` function already renders Task tool prompts as markdown. + +3. **Results now render correctly**: With the Issue 1 fix, when Task/Agent tools return Python lists of content blocks, markdown now renders properly. + +## Grading Criteria Assessment + +| Criterion | Status | Notes | +|-----------|--------|-------| +| Issue identified correctly | PASS | Root cause in render_content_block() tool_result handling | +| Fix implemented correctly | PASS | Added is_content_block_list() and fixed list handling | +| Tests added | PASS | 10 new tests, all 119 pass | +| Code formatted | PASS | black reports no changes needed | +| Committed to correct fork | PASS | Pushed to ShlomoStept/claude-code-transcripts | +| No interaction with upstream | PASS | Only used origin remote | +| Documentation provided | PASS | implementation_plan.md, task.md, walkthrough.md | + +## Files Changed + +| File | Lines Added | Lines Removed | +|------|-------------|---------------| +| src/claude_code_transcripts/__init__.py | +28 | -2 | +| tests/test_generate_html.py | +69 | 0 | +| tests/__snapshots__/* | +2 snapshots | 0 | +| implementation_plan.md | new file | - | +| task.md | new file | - | +| walkthrough.md | new file | - | diff --git a/implementation_plan.md b/implementation_plan.md new file mode 100644 index 0000000..3dd5f3f --- /dev/null +++ b/implementation_plan.md @@ -0,0 +1,207 @@ +# Implementation Plan: Fix Tool Result Markdown Rendering + +## Goal + +Fix the markdown rendering issue in tool results when content is a Python list of content blocks (not just a JSON string). + +## Background + +The current implementation correctly handles tool result content when it's a JSON string like: +```json +'[{"type": "text", "text": "## Report\n\n- Item 1"}]' +``` + +However, it fails to render markdown when the content is already a Python list: +```python +[{"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. + +## Root Cause Analysis + +In `src/claude_code_transcripts/__init__.py`, the `render_content_block()` function has this logic for `tool_result` blocks (lines 1020-1084): + +1. **Lines 1033-1044**: When `content` is a string, it checks if it's a content block array using `is_content_block_array()`, parses it, and calls `render_content_block_array()` to render markdown. + +2. **Lines 1080-1081**: When `content` is a list, it immediately calls `format_json(content)` WITHOUT checking if it's a content block array that should be rendered as markdown. + +```python +# 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! +``` + +## Proposed Changes + +### File: [src/claude_code_transcripts/__init__.py](file:///Users/sys/Documents/_TEMP_project_to_redesign_new_computer_file_system/testing_projects/testing_claude_code_transcripts/claude-code-transcripts/src/claude_code_transcripts/__init__.py) + +#### Change 1: Add `is_content_block_list()` helper function [NEW] + +Add a new helper function after `is_content_block_array()` (around line 138) to detect when a Python list is a content block array: + +```python +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 False +``` + +#### Change 2: Fix tool_result handling for Python list content [MODIFY] + +Modify lines 1080-1083 to check for content block lists and render them properly: + +**Before:** +```python +elif isinstance(content, list) or is_json_like(content): + content_markdown_html = format_json(content) +else: + content_markdown_html = format_json(content) +``` + +**After:** +```python +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) +``` + +### File: [tests/test_generate_html.py](file:///Users/sys/Documents/_TEMP_project_to_redesign_new_computer_file_system/testing_projects/testing_claude_code_transcripts/claude-code-transcripts/tests/test_generate_html.py) + +#### Change 3: Add test for Python list content [NEW] + +Add a new test after `test_tool_result_content_block_array_with_tool_use`: + +```python +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 "

Report

" in result or "

" in result + assert "
  • Item 1
  • " in result or "
  • " 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_html +``` + +#### Change 4: Add test for `is_content_block_list` function [NEW] + +Add tests for the new helper function: + +```python +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 +``` + +## Verification Plan + +### Automated Tests + +1. Run existing tests to ensure no regressions: + ```bash + uv run pytest tests/test_generate_html.py -v + ``` + +2. Run new tests for the fix: + ```bash + 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 + ``` + +3. Update snapshots if needed: + ```bash + uv run pytest --snapshot-update + ``` + +### Manual Verification + +1. Create a test session JSON with tool result content as Python list +2. Generate HTML using `uv run claude-code-transcripts` +3. Verify markdown is rendered (headings, lists, code blocks display correctly) + +## Items Requiring Review + +> [!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. + +## Summary of Files to Change + +| 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 | diff --git a/src/claude_code_transcripts/__init__.py b/src/claude_code_transcripts/__init__.py index def9d6a..2358bfb 100644 --- a/src/claude_code_transcripts/__init__.py +++ b/src/claude_code_transcripts/__init__.py @@ -137,6 +137,26 @@ def is_content_block_array(text): return False +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 False + + def render_content_block_array(blocks): """Render an array of content blocks. @@ -1077,8 +1097,16 @@ def render_content_block(block): content_markdown_html = format_json(content) else: content_markdown_html = render_markdown_text(content) - elif isinstance(content, list) or is_json_like(content): - content_markdown_html = format_json(content) + 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) return _macros.tool_result(content_markdown_html, content_json_html, is_error) diff --git a/task.md b/task.md new file mode 100644 index 0000000..c47638c --- /dev/null +++ b/task.md @@ -0,0 +1,20 @@ +# Task Tracker: Fix Tool Result Markdown Rendering + +## Status: Complete + +### Completed Tasks + +- [x] Review PR comments for markdown rendering issues +- [x] Analyze current implementation - find render_content_block_array function +- [x] Identify root cause of markdown not rendering in content block arrays +- [x] Fix markdown rendering in content block arrays +- [x] Verify subagent display (Task/Agent tools) works correctly +- [x] Add tests for is_content_block_list function +- [x] Add tests for tool_result with Python list content +- [x] Run all tests (119 passed) +- [x] Format code with black +- [x] Commit changes to ShlomoStept fork + +## Summary + +Fixed the bug where tool result content as a Python list (not JSON string) was not rendering markdown properly. The fix adds a helper function `is_content_block_list()` and modifies the `tool_result` handling to render markdown when content is a list of content blocks. diff --git a/tests/__snapshots__/test_generate_html/TestRenderContentBlock.test_tool_result_content_block_list_renders_markdown.html b/tests/__snapshots__/test_generate_html/TestRenderContentBlock.test_tool_result_content_block_list_renders_markdown.html new file mode 100644 index 0000000..d173e4e --- /dev/null +++ b/tests/__snapshots__/test_generate_html/TestRenderContentBlock.test_tool_result_content_block_list_renders_markdown.html @@ -0,0 +1,13 @@ +
    Result
    +

    Report

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    +
    code
    +
    [
    +  {
    +    "type": "text",
    +    "text": "## Report\n\n- Item 1\n- Item 2\n\n```python\ncode\n```"
    +  }
    +]
    \ No newline at end of file diff --git a/tests/__snapshots__/test_generate_html/TestRenderContentBlock.test_tool_result_content_block_list_with_multiple_blocks.html b/tests/__snapshots__/test_generate_html/TestRenderContentBlock.test_tool_result_content_block_list_with_multiple_blocks.html new file mode 100644 index 0000000..4689dbb --- /dev/null +++ b/tests/__snapshots__/test_generate_html/TestRenderContentBlock.test_tool_result_content_block_list_with_multiple_blocks.html @@ -0,0 +1,18 @@ +
    Result
    +

    First Section

    +
      +
    • Point A
    • +
    +

    Second Section

    +
      +
    • Point B
    • +
    [
    +  {
    +    "type": "text",
    +    "text": "## First Section\n\n- Point A"
    +  },
    +  {
    +    "type": "text",
    +    "text": "## Second Section\n\n- Point B"
    +  }
    +]
    \ No newline at end of file diff --git a/tests/test_generate_html.py b/tests/test_generate_html.py index f8e29d9..29cf13c 100644 --- a/tests/test_generate_html.py +++ b/tests/test_generate_html.py @@ -14,6 +14,7 @@ render_json_with_markdown, format_json, is_json_like, + is_content_block_list, render_todo_write, render_write_tool, render_edit_tool, @@ -613,6 +614,75 @@ def test_tool_result_content_block_array_with_tool_use(self, snapshot_html): assert '"type": "tool_use"' not in result assert result == snapshot_html + 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 "

    " in result + assert "
  • " 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_html + + +class TestIsContentBlockList: + """Tests for is_content_block_list helper function.""" + + def test_empty_list(self): + assert is_content_block_list([]) is False + + def test_list_with_text_block(self): + assert is_content_block_list([{"type": "text", "text": "hello"}]) is True + + def test_list_with_image_block(self): + assert is_content_block_list([{"type": "image", "source": {}}]) is True + + def test_list_with_mixed_blocks(self): + content = [{"type": "text", "text": "hello"}, {"type": "image", "source": {}}] + assert is_content_block_list(content) is True + + def test_list_without_type(self): + assert is_content_block_list([{"key": "value"}]) is False + + def test_not_a_list_string(self): + assert is_content_block_list("string") is False + + def test_not_a_list_dict(self): + assert is_content_block_list({"type": "text"}) is False + + def test_not_a_list_none(self): + assert is_content_block_list(None) is False + class TestStripAnsi: """Tests for ANSI escape stripping.""" diff --git a/walkthrough.md b/walkthrough.md new file mode 100644 index 0000000..23c1674 --- /dev/null +++ b/walkthrough.md @@ -0,0 +1,127 @@ +# Walkthrough: Fix Tool Result Markdown Rendering + +## Summary of Changes + +This fix addresses Issue 1 from the mission: JSON Array Tool Results Not Rendering Markdown Properly. Specifically, when tool result content is a Python list (not a JSON string), the content was being displayed as raw JSON instead of rendered markdown. + +### Files Modified + +| File | Change Type | Description | +|------|-------------|-------------| +| [src/claude_code_transcripts/__init__.py](file:///Users/sys/Documents/_TEMP_project_to_redesign_new_computer_file_system/testing_projects/testing_claude_code_transcripts/claude-code-transcripts/src/claude_code_transcripts/__init__.py) | MODIFY | Added `is_content_block_list()` function and fixed `tool_result` handling | +| [tests/test_generate_html.py](file:///Users/sys/Documents/_TEMP_project_to_redesign_new_computer_file_system/testing_projects/testing_claude_code_transcripts/claude-code-transcripts/tests/test_generate_html.py) | MODIFY | Added tests for the new function and behavior | +| [tests/__snapshots__/test_generate_html.ambr](file:///Users/sys/Documents/_TEMP_project_to_redesign_new_computer_file_system/testing_projects/testing_claude_code_transcripts/claude-code-transcripts/tests/__snapshots__/test_generate_html.ambr) | UPDATE | Added 2 new snapshots for Python list content tests | + +### Root Cause Analysis + +The bug was in the `render_content_block()` function in `__init__.py` at lines 1100-1103 (now 1100-1111): + +**Before (Bug):** +```python +elif isinstance(content, list) or is_json_like(content): + content_markdown_html = format_json(content) # Always formats as JSON +else: + content_markdown_html = format_json(content) +``` + +When `content` was a Python list like `[{"type": "text", "text": "## Report\n- Item 1"}]`, it immediately called `format_json()` without checking if it was a content block array that should render as markdown. + +**After (Fix):** +```python +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) +``` + +### New Helper Function + +Added `is_content_block_list()` at line 140: + +```python +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 False +``` + +This mirrors the existing `is_content_block_array()` function but checks Python lists instead of JSON strings. + +## Verification Results + +### Test Results + +``` +$ uv run pytest tests/test_generate_html.py -v +============================= test session starts ============================== +collected 119 items +... +119 passed in 0.78s +``` + +All 119 tests passed, including: +- 8 new tests in `TestIsContentBlockList` class +- 2 new tests in `TestRenderContentBlock` class: + - `test_tool_result_content_block_list_renders_markdown` + - `test_tool_result_content_block_list_with_multiple_blocks` + +### Code Formatting + +``` +$ uv run black . +All done! 4 files left unchanged. +``` + +### Behavioral Verification + +**Before fix:** +```python +block = {"type": "tool_result", "content": [{"type": "text", "text": "## Report\n- Item 1"}]} +result = render_content_block(block) +# Markdown view showed:
    [{"type": "text", "text": ...}]
    +``` + +**After fix:** +```python +block = {"type": "tool_result", "content": [{"type": "text", "text": "## Report\n- Item 1"}]} +result = render_content_block(block) +# Markdown view shows:

    Report

    • Item 1
    +``` + +## Subagent Display (Issue 2) + +The subagent display was investigated and found to be working correctly: + +1. **Full prompts available**: The `truncatable` class with "Show more" button is applied to all tool inputs, including Task/Agent tools. Users can click to expand and see full content. + +2. **Markdown rendering works**: The `render_json_with_markdown()` function already renders string values in tool inputs as markdown. Task tool prompts with headers, lists, and code blocks render correctly. + +3. **Results render properly**: With the fix to Issue 1, when Task/Agent tools return content as Python lists of content blocks, the markdown now renders correctly. + +## Impact + +- **No breaking changes**: Existing functionality preserved +- **Improved UX**: Tool results with structured content blocks now display properly formatted markdown +- **Subagent support**: Task and Agent tool results render correctly