Skip to content

Conversation

@cboos
Copy link
Collaborator

@cboos cboos commented Dec 18, 2025

Replace the if/elif type dispatch chain in HtmlRenderer._format_message_content() with a declarative dispatcher pattern in the base Renderer class:

  • Add _build_dispatcher() method to base Renderer returning type->formatter mapping
  • Add format_content() method with MRO walking for automatic fallback to parent handlers
  • HtmlRenderer overrides _build_dispatcher() with all content type formatters
  • Complex formatters (UserTextContent, ToolResultContentModel) use instance methods
  • Simple formatters use lambdas with cast() for type safety

Benefits:

  • Explicit & declarative - all handlers visible in one dict
  • MRO-aware fallback - handlers for parent classes serve as defaults
  • Easy subclass customization via {**super()._build_dispatcher(), ...}
  • Clean separation - dispatch logic in base, handlers in subclass

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Messages now support interleaved content (text, images, IDE notifications) and slash‑command prompts rendered as collapsible markdown.
  • Refactor
    • Rendering pipeline reorganized to chunk mixed content and dispatch formatting more reliably.
  • Documentation
    • Updated docs to describe the new slash‑command content model and mixed-content semantics.
  • Tests
    • Added tests covering chunking behavior and multi-item message formatting.

✏️ Tip: You can customize this high-level summary in your review settings.

Replace the if/elif type dispatch chain in HtmlRenderer._format_message_content()
with a declarative dispatcher pattern in the base Renderer class:

- Add _build_dispatcher() method to base Renderer returning type->formatter mapping
- Add format_content() method with MRO walking for automatic fallback to parent handlers
- HtmlRenderer overrides _build_dispatcher() with all content type formatters
- Complex formatters (UserTextContent, ToolResultContentModel) use instance methods
- Simple formatters use lambdas with cast() for type safety

Benefits:
- Explicit & declarative - all handlers visible in one dict
- MRO-aware fallback - handlers for parent classes serve as defaults
- Easy subclass customization via {**super()._build_dispatcher(), ...}
- Clean separation - dispatch logic in base, handlers in subclass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Dec 18, 2025

Walkthrough

Introduces a dispatcher-based formatting and chunked-content model: Renderer gains init, _build_dispatcher, and format_content; message content is split into chunks and parsed into new content-item models (TextContent/ImageContent/UserSlashCommandContent). HtmlRenderer and HTML formatters switched to dispatcher handlers; parser and models updated for interleaved items.

Changes

Cohort / File(s) Summary
Core renderer & chunking
claude_code_log/renderer.py
Added __init__, _build_dispatcher, format_content, ContentChunk type, _is_special_item, chunk_message_content; rewrote _render_messages to operate on chunks, added per-chunk TemplateMessage creation and new process* helpers (tool_use/result/thinking); removed image-specific old paths.
HTML rendering & dispatcher
claude_code_log/html/renderer.py
Replaced monolithic if/elif content formatter with _build_dispatcher mapping and format_content usage; added _format_tool_result_content; replaced _format_message_content with dispatcher-driven flow and updated _flatten_preorder.
HTML assistant/user formatters
claude_code_log/html/assistant_formatters.py, claude_code_log/html/user_formatters.py
Assistant and User formatters updated to iterate content.items and render mixed item types (TextContent, ImageContent, IdeNotificationContent); added format_user_slash_command_content; image rendering integrated into user/assistant paths.
Content models
claude_code_log/models.py
Added TextContent, ImageSource, ImageContent, UserSlashCommandContent; changed UserTextContent and AssistantTextContent to items: list[...]; updated AssistantMessage.from_anthropic_message to use list conversion.
Parsing & normalization
claude_code_log/parser.py
parse_* functions updated to produce itemized content models; parse_user_message_content gained is_slash_command flow returning UserSlashCommandContent; _parse_text_content simplified to TextContent.model_validate.
Docs
dev-docs/messages.md
Documented new UserSlashCommandContent and updated examples/comments for isMeta/slash-command flow.
Tests
test/test_renderer.py, test/test_user_renderer.py, test/test_ide_tags.py
Added chunking tests for chunk_message_content; updated tests to construct/assert on itemized content (items[0].text, IdeNotificationContent presence) and updated expectations to item-based models.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

  • Reviewers should focus on:
    • Correctness and completeness of dispatcher mappings (_build_dispatcher) across all content types.
    • Chunking logic in chunk_message_content and its impact on message ordering, token accounting, and ancestry/uuid derivation.
    • Model changes: duplicated/overlapping declarations of TextContent/ImageContent and their public exposure (shadowing/duplicate definitions).
    • Parser adaptations to ensure all original Anthropic content cases (thinking, tool_use, images) are preserved and not regressions.
    • HTML formatter changes for mixed items and image embedding correctness and security (base64 handling, escaping).
    • Tests alignment with new semantics and potential gaps for edge cases (empty items, unknown content types).

Possibly related PRs

Poem

🐰 I hopped through code and hopped some more,
I sorted items, built a map and door,
From tangled ifs to tidy calls,
Chunks and slash‑commands line the halls,
A carrot for tests — hooray, explore! 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Refactor content formatting to use dispatcher pattern' directly matches the main objective of the pull request: replacing if/elif dispatch with a dispatcher pattern.
Docstring Coverage ✅ Passed Docstring coverage is 95.12% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev/render-dispatching

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

cboos and others added 2 commits December 20, 2025 01:11
This refactoring fixes a bug where user images were lost during rendering
and simplifies the message processing pipeline.

Key changes:
- Add chunk_message_content() to split content into special vs regular chunks
- UserTextContent and AssistantTextContent now use items lists to preserve
  content order (text + images interleaved)
- Remove ContentBlock from ContentItem union, use only our own types
- Simplify parser to not depend on Anthropic SDK types
- Simplify _process_regular_message by filtering empty text in chunking
- Move TextContent, ImageContent, ImageSource earlier in models.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Introduce a dedicated content model for slash command expanded prompts
(isMeta=True) instead of reusing UserTextContent. This follows the same
pattern as CompactedSummaryContent and UserMemoryContent.

Changes:
- Add UserSlashCommandContent(text: str) model in models.py
- Update parse_user_message_content() to accept is_slash_command param
- Add format_user_slash_command_content() formatter
- Add dispatcher entry for UserSlashCommandContent
- Update _process_regular_message to detect content type
- Update dev-docs/messages.md to distinguish isMeta vs tags variants

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
claude_code_log/html/user_formatters.py (1)

194-211: Docstring mentions fallback that isn't implemented.

Similar to assistant_formatters.py, the docstring at lines 200-205 describes "Falls back to legacy text-only behavior when items is None" but the code unconditionally iterates content.items. Either update the docstring or add the fallback logic.

🧹 Nitpick comments (4)
claude_code_log/html/user_formatters.py (1)

213-228: Same pattern issue: else branch assumes TextContent without type verification.

Similar to assistant_formatters.py, the else branch at line 224 assumes any non-IdeNotificationContent, non-ImageContent item is TextContent. If an unexpected type is passed, accessing item.text could fail.

Consider adding an explicit type check for robustness.

🔎 Proposed fix
+from ..models import TextContent  # add to imports if not present
     for item in content.items:
         if isinstance(item, IdeNotificationContent):
             notifications = format_ide_notification_content(item)
             parts.extend(notifications)
         elif isinstance(item, ImageContent):
             parts.append(format_image_content(item))
-        else:  # TextContent
+        elif isinstance(item, TextContent):
             # Regular user text as preformatted
             if item.text.strip():
                 parts.append(format_user_text_content(item.text))
+        # Other types silently skipped
claude_code_log/html/renderer.py (1)

128-140: Consider adding a docstring note about the ToolResultContent vs ToolResultContentModel distinction.

The method creates a ToolResultContent from ToolResultContentModel to delegate to format_tool_result_content. A brief comment explaining why this wrapper exists (the model carries rendering context like file_path and tool_name that the raw ToolResultContent lacks) would help future maintainers.

claude_code_log/renderer.py (2)

2148-2151: UUID generation for multi-chunk messages could benefit from a more deterministic approach.

When a message produces multiple chunks, each chunk gets a UUID like {original_uuid}-chunk-{chunk_idx}. This is reasonable, but if the same message is re-rendered, the UUIDs will match, which is good for consistency. However, consider whether the chunk index should be based on position among all chunks or just among chunks of the same type.


2218-2224: UUID generation for tool messages uses tool_use_id or fallback.

Using tool_use_id when available provides meaningful IDs for tool messages. The fallback {msg_uuid}-tool-{len(template_messages)} ensures uniqueness but depends on the current list length, which could be fragile if the processing order changes. Consider using chunk_idx instead for consistency.

🔎 Suggested improvement for more deterministic tool UUIDs
                 # Generate unique UUID for this tool message
                 # Use tool_use_id if available, otherwise fall back to msg UUID + index
                 tool_uuid = (
                     tool_result.tool_use_id
                     if tool_result.tool_use_id
-                    else f"{msg_uuid}-tool-{len(template_messages)}"
+                    else f"{msg_uuid}-tool-{chunk_idx}"
                 )
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0de3d6c and 28730e5.

📒 Files selected for processing (10)
  • claude_code_log/html/assistant_formatters.py (1 hunks)
  • claude_code_log/html/renderer.py (5 hunks)
  • claude_code_log/html/user_formatters.py (3 hunks)
  • claude_code_log/models.py (6 hunks)
  • claude_code_log/parser.py (6 hunks)
  • claude_code_log/renderer.py (9 hunks)
  • dev-docs/messages.md (3 hunks)
  • test/test_ide_tags.py (3 hunks)
  • test/test_renderer.py (1 hunks)
  • test/test_user_renderer.py (3 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use ruff for code formatting and linting, with ruff check --fix for automatic fixes
Use pyright and mypy for type checking in Python code
Target Python 3.10+ with support for modern Python features and type hints

Files:

  • claude_code_log/html/renderer.py
  • test/test_ide_tags.py
  • claude_code_log/parser.py
  • test/test_user_renderer.py
  • claude_code_log/models.py
  • claude_code_log/renderer.py
  • claude_code_log/html/user_formatters.py
  • test/test_renderer.py
  • claude_code_log/html/assistant_formatters.py
test/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Organize tests into categories with pytest markers to avoid async event loop conflicts: unit tests (no mark), TUI tests (@pytest.mark.tui), browser tests (@pytest.mark.browser), and snapshot tests

Files:

  • test/test_ide_tags.py
  • test/test_user_renderer.py
  • test/test_renderer.py
claude_code_log/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use dateparser for natural language date parsing to support date range filtering with expressions like 'today', 'yesterday', 'last week', and relative dates

Files:

  • claude_code_log/parser.py
  • claude_code_log/models.py
  • claude_code_log/renderer.py
claude_code_log/models.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use Pydantic models for parsing and validating transcript JSON data, including TranscriptEntry (union of UserTranscriptEntry, AssistantTranscriptEntry, SummaryTranscriptEntry), UsageInfo, and ContentItem

Files:

  • claude_code_log/models.py
claude_code_log/renderer.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use mistune for quick Markdown rendering with syntax highlighting support in server-side template rendering

Files:

  • claude_code_log/renderer.py
🧠 Learnings (3)
📚 Learning: 2025-11-30T17:16:32.495Z
Learnt from: CR
Repo: daaain/claude-code-log PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-30T17:16:32.495Z
Learning: Applies to claude_code_log/renderer.py : Use mistune for quick Markdown rendering with syntax highlighting support in server-side template rendering

Applied to files:

  • claude_code_log/html/renderer.py
📚 Learning: 2025-11-30T17:16:32.495Z
Learnt from: CR
Repo: daaain/claude-code-log PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-30T17:16:32.495Z
Learning: Applies to claude_code_log/models.py : Use Pydantic models for parsing and validating transcript JSON data, including TranscriptEntry (union of UserTranscriptEntry, AssistantTranscriptEntry, SummaryTranscriptEntry), UsageInfo, and ContentItem

Applied to files:

  • claude_code_log/html/renderer.py
  • claude_code_log/parser.py
  • claude_code_log/models.py
  • dev-docs/messages.md
📚 Learning: 2025-12-09T23:52:47.578Z
Learnt from: daaain
Repo: daaain/claude-code-log PR: 59
File: test/test_cache.py:135-165
Timestamp: 2025-12-09T23:52:47.578Z
Learning: SQLite supports NULLS FIRST and NULLS LAST in ORDER BY since v3.30.0 (Oct 2019). Do not flag SQL that uses these clauses as an error when reviewing Python tests or code that interacts with SQLite. If reviewing SQL strings, verify the target SQLite version supports NULLS FIRST/LAST and ensure the syntax is used correctly for the database in use.

Applied to files:

  • test/test_ide_tags.py
  • test/test_user_renderer.py
  • test/test_renderer.py
🧬 Code graph analysis (6)
test/test_ide_tags.py (1)
claude_code_log/models.py (5)
  • IdeNotificationContent (258-272)
  • UserTextContent (309-323)
  • TextContent (282-286)
  • ImageContent (297-305)
  • AssistantTextContent (334-350)
claude_code_log/parser.py (1)
claude_code_log/models.py (9)
  • UserSlashCommandContent (221-229)
  • ThinkingContent (773-776)
  • CompactedSummaryContent (196-205)
  • UserMemoryContent (209-217)
  • UserTextContent (309-323)
  • TextContent (282-286)
  • ImageContent (297-305)
  • IdeNotificationContent (258-272)
  • ToolUseContent (739-762)
test/test_user_renderer.py (1)
claude_code_log/models.py (2)
  • TextContent (282-286)
  • UserTextContent (309-323)
claude_code_log/renderer.py (5)
claude_code_log/models.py (13)
  • UserSlashCommandContent (221-229)
  • CompactedSummaryContent (196-205)
  • UserMemoryContent (209-217)
  • MessageType (16-51)
  • AssistantTextContent (334-350)
  • ToolUseContent (739-762)
  • ToolResultContent (765-770)
  • ThinkingContent (773-776)
  • TextContent (282-286)
  • MessageContent (84-94)
  • QueueOperationTranscriptEntry (885-902)
  • UnknownContent (369-376)
  • MessageModifiers (55-73)
claude_code_log/parser.py (1)
  • parse_user_message_content (354-433)
claude_code_log/utils.py (1)
  • format_timestamp (21-32)
claude_code_log/tui.py (1)
  • format_timestamp (469-482)
claude_code_log/html/renderer.py (1)
  • _build_dispatcher (97-126)
claude_code_log/html/user_formatters.py (3)
claude_code_log/models.py (5)
  • ImageContent (297-305)
  • SlashCommandContent (136-145)
  • UserMemoryContent (209-217)
  • UserSlashCommandContent (221-229)
  • IdeNotificationContent (258-272)
claude_code_log/html/assistant_formatters.py (1)
  • format_image_content (87-97)
claude_code_log/html/utils.py (1)
  • render_markdown_collapsible (222-260)
test/test_renderer.py (2)
claude_code_log/models.py (5)
  • TextContent (282-286)
  • ToolUseContent (739-762)
  • ThinkingContent (773-776)
  • ImageContent (297-305)
  • ImageSource (289-294)
claude_code_log/renderer.py (1)
  • chunk_message_content (847-894)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: test (windows-latest, 3.13)
  • GitHub Check: test (windows-latest, 3.14)
  • GitHub Check: test (windows-latest, 3.11)
  • GitHub Check: test (windows-latest, 3.12)
  • GitHub Check: test (ubuntu-latest, 3.13)
  • GitHub Check: test (ubuntu-latest, 3.10)
  • GitHub Check: test (windows-latest, 3.10)
  • GitHub Check: test (ubuntu-latest, 3.11)
  • GitHub Check: test (ubuntu-latest, 3.14)
  • GitHub Check: test (ubuntu-latest, 3.12)
🔇 Additional comments (29)
test/test_user_renderer.py (2)

210-212: LGTM!

The test correctly verifies the new items-based UserTextContent structure, checking that items is a list with one TextContent element containing the expected text.


291-312: LGTM!

The test updates correctly construct UserTextContent with items=[TextContent(...)] and properly test the HTML escaping behavior through the formatting pipeline.

dev-docs/messages.md (2)

171-178: LGTM!

The documentation correctly describes the new UserSlashCommandContent model with its text: str field and clarifies that these are LLM-generated markdown instruction prompts rendered as collapsible markdown.


24-24: Minor: Data flow diagram shows both content types for slash commands.

Line 24 lists both UserSlashCommandContent (isMeta) and SlashCommandContent (<command-name> tags) which correctly reflects the two different slash command content types. This is accurate and helpful for understanding the distinction.

test/test_renderer.py (6)

373-390: LGTM!

The test correctly verifies that consecutive text items are grouped into a single list chunk, matching the expected behavior from chunk_message_content.


392-408: LGTM!

Good test coverage for the tool_use chunking behavior - verifies that tool_use content is isolated into its own chunk with surrounding text in separate list chunks.


410-424: LGTM!

Correctly tests that thinking content forms its own chunk, separate from text items.


426-444: LGTM!

Comprehensive test for interleaved content types, verifying the expected [text], thinking, [text], tool_use chunking pattern.


446-466: LGTM!

Correctly verifies that images are treated as "regular" items (not "special") and remain grouped with text in the same chunk.


468-473: LGTM!

Simple but important edge case test for empty input returning an empty list.

test/test_ide_tags.py (2)

314-330: LGTM! Test correctly validates the new items-based content model.

The assertions properly verify the ordering and types of items returned by parse_user_message_content: IDE notification first, remaining text second, image third. This aligns with the updated UserTextContent model that uses an interleaved items list.


352-354: LGTM! Test updated to use the new items-based AssistantTextContent.

The test correctly constructs AssistantTextContent with an items list containing TextContent, matching the model changes in models.py.

claude_code_log/html/renderer.py (2)

97-126: Well-structured dispatcher implementation with clear content-type mappings.

The dispatcher pattern provides a clean, declarative mapping from content types to formatters. The use of partial(format_thinking_content, line_threshold=10) elegantly binds the threshold parameter. Using an instance method for ToolResultContentModel enables access to additional context.


158-159: LGTM! Clean integration with base class dispatcher.

The change from direct _format_message_content call to self.format_content(msg) properly delegates to the MRO-aware dispatcher in the base Renderer class.

claude_code_log/parser.py (4)

73-81: Simplified and more robust text extraction.

The updated logic handles both ThinkingContent instances and duck-typed thinking items via getattr(item, "type", None). Using hasattr(item, "text") for text extraction is cleaner than the previous approach.


378-387: Good early return for slash command content.

The is_slash_command path correctly combines all text items and returns a UserSlashCommandContent immediately. The if all_text check prevents returning empty content.


406-433: Well-implemented items-based content parsing with proper order preservation.

The loop correctly:

  1. Extracts IDE notifications from text content first
  2. Adds remaining text as TextContent only if non-empty
  3. Handles both native ImageContent and Anthropic SDK image types

428-430: No action needed. Anthropic SDK responses are Pydantic models, and model_dump() is the standard method to convert Pydantic models to dictionaries. The SDK maintains compatibility across Pydantic v1 and v2 through a compatibility layer, with model_dump() working across both versions. The runtime checks (hasattr(item, "source") and getattr(item, "type", None) == "image") provide appropriate defensive validation before calling the method.

claude_code_log/models.py (5)

220-229: LGTM! Clean model for slash command expanded prompts.

The UserSlashCommandContent dataclass correctly captures LLM-generated instruction text from slash commands. The docstring clearly explains the use case and references the formatter location.


275-306: Well-defined content item models with proper Pydantic validation.

The TextContent, ImageSource, and ImageContent models provide strong typing for content arrays. Using Literal["text"], Literal["base64"], and Literal["image"] for type discrimination enables clean pattern matching.


320-323: The pyright: ignore comment is appropriate for this pattern.

The union type in a dataclass field with field(default_factory=list) triggers pyright warnings that aren't actionable. The ignore is correctly scoped to just this line.


347-350: Consistent pattern with UserTextContent for assistant items.

The AssistantTextContent.items field mirrors the pattern used in UserTextContent, enabling unified handling of interleaved text and image content.


779-786: Verify ImageContent is correctly included in ContentItem union.

The ContentItem union now includes ImageContent, which is necessary for the items-based content models. This aligns with the model definitions earlier in the file.

claude_code_log/renderer.py (6)

831-844: Good type alias and helper function for content chunking.

The ContentChunk type alias and _is_special_item helper cleanly distinguish between regular items (text/image) that accumulate and special items (tool_use, tool_result, thinking) that become standalone chunks.


847-894: Well-implemented chunking algorithm with clear documentation.

The chunk_message_content function correctly:

  1. Accumulates regular items into list chunks
  2. Flushes accumulated items before special items
  3. Skips empty text content
  4. Handles empty input gracefully

The docstring example clearly illustrates the expected behavior.


708-753: Updated _process_regular_message correctly handles items-based content.

The function now:

  1. Accepts items: list[ContentItem] instead of extracting text
  2. Creates UserSlashCommandContent for slash command prompts via parse_user_message_content
  3. Creates AssistantTextContent directly from items for assistant messages

The type ignore on line 752 is appropriate since the items have already been filtered to text/image content.


2074-2076: Token usage correctly shown only on the first content chunk.

The token_shown flag ensures token usage is displayed once per message, even when the message produces multiple chunks. This prevents duplicate token counts in the UI.


2381-2417: Well-designed dispatcher pattern in base Renderer class.

The implementation:

  1. Caches the dispatcher dict in __init__ for efficiency
  2. Provides _build_dispatcher as an override point for subclasses
  3. Walks the content type's MRO to find handlers, enabling inheritance-based fallbacks
  4. Returns empty string if no handler found (safe default)

The MRO walking is a nice touch that allows registering a handler for a parent class (like MessageContent) as a catch-all.


2003-2010: Edge case: String content wrapping creates nested list.

When message_content is a string, it's wrapped as [[TextContent(...)]] (a list containing a list with one item). This is correct since chunk_message_content expects a list, and this will be treated as a single regular chunk. The logic is sound.

Comment on lines +34 to +61
When `items` is set, iterates through the content items preserving order:
- TextContent: Rendered as markdown with collapsible support
- ImageContent: Rendered as inline <img> tag with base64 data URL
Falls back to legacy text-only behavior when `items` is None.
Args:
content: AssistantTextContent with the text to render
content: AssistantTextContent with text/items to render
line_threshold: Number of lines before content becomes collapsible
preview_line_count: Number of preview lines to show when collapsed
Returns:
HTML string with markdown-rendered, optionally collapsible content
"""
return render_markdown_collapsible(
content.text,
"assistant-text",
line_threshold=line_threshold,
preview_line_count=preview_line_count,
)
parts: list[str] = []
for item in content.items:
if isinstance(item, ImageContent):
parts.append(format_image_content(item))
else: # TextContent
if item.text.strip():
text_html = render_markdown_collapsible(
item.text,
"assistant-text",
line_threshold=line_threshold,
preview_line_count=preview_line_count,
)
parts.append(text_html)
return "\n".join(parts)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Docstring claims fallback behavior that isn't implemented.

The docstring at line 38 states "Falls back to legacy text-only behavior when items is None", but the code unconditionally iterates over content.items without checking for None. If items can be None (as the docstring implies), this would raise a TypeError.

Either:

  1. Remove the fallback claim from the docstring if items is always populated, or
  2. Add the fallback logic if backward compatibility is needed.

Additionally, the else branch at line 52 assumes any non-ImageContent item is TextContent, which could cause AttributeError if an unexpected type is encountered. Consider adding an explicit type check.

🔎 Proposed fix with explicit type handling
 from ..models import (
     AssistantTextContent,
     ImageContent,
+    TextContent,
     ThinkingContentModel,
     UnknownContent,
 )
     parts: list[str] = []
     for item in content.items:
         if isinstance(item, ImageContent):
             parts.append(format_image_content(item))
-        else:  # TextContent
+        elif isinstance(item, TextContent):
             if item.text.strip():
                 text_html = render_markdown_collapsible(
                     item.text,
                     "assistant-text",
                     line_threshold=line_threshold,
                     preview_line_count=preview_line_count,
                 )
                 parts.append(text_html)
+        # Other types are silently skipped
     return "\n".join(parts)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In claude_code_log/html/assistant_formatters.py around lines 34 to 61, the
docstring claims a fallback when content.items is None but the code
unconditionally iterates content.items and assumes non-ImageContent are
TextContent; update the implementation to match the docstring by checking if
content.items is None and fall back to the legacy text-only rendering path
(render content.text via render_markdown_collapsible), or if you prefer to keep
items always present remove the fallback claim from the docstring; additionally
replace the unguarded else with an explicit isinstance(item, TextContent) branch
(render it) and otherwise skip/log unexpected item types to avoid
AttributeError.

Comment on lines +268 to +285
def format_user_slash_command_content(content: UserSlashCommandContent) -> str:
"""Format slash command expanded prompt (isMeta) as HTML.
These are LLM-generated instruction text from slash commands,
rendered as collapsible markdown.
Args:
content: UserSlashCommandContent with markdown text
Returns:
HTML string with collapsible markdown rendering
"""
return render_markdown_collapsible(
content.text,
"slash-command",
line_threshold=30,
preview_line_count=10,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

New function format_user_slash_command_content is not exported in __all__.

The function is defined but not included in the __all__ list at lines 374-385. If this function is intended to be part of the public API (as indicated by the AI summary), it should be added to the exports.

🔎 Proposed fix
 __all__ = [
     # Formatting functions
     "format_slash_command_content",
     "format_command_output_content",
     "format_bash_input_content",
     "format_bash_output_content",
     "format_user_text_content",
     "format_user_text_model_content",
     "format_compacted_summary_content",
     "format_user_memory_content",
     "format_ide_notification_content",
+    "format_user_slash_command_content",
 ]
🤖 Prompt for AI Agents
In claude_code_log/html/user_formatters.py around lines 268 and the export block
at lines 374-385, the new function format_user_slash_command_content is defined
but not exported; add "format_user_slash_command_content" to the module's
__all__ list in the export block so the function is part of the public API,
placing it alongside the other formatter names and keeping the list
alphabetized/consistent with the existing style.

Copy link
Owner

@daaain daaain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll let CodeRabbit comment on the nits, but from a high-level perspective it makes sense!

I had one thought, if you feel like checking which types we could replace with ones from the Anthropic SDK that could be a useful refactor, for example our ImageSource could be their Base64ImageSourceParam

@cboos
Copy link
Collaborator Author

cboos commented Dec 20, 2025

I'll let CodeRabbit comment on the nits, but from a high-level perspective it makes sense!

Thanks for the feedback! I was about to merge this one, as I have pending changes that will go into another PR (to keep each reasonably small), and after that, I'm about to embark on yet an even bigger refactoring...

I had one thought, if you feel like checking which types we could replace with ones from the Anthropic SDK that could be a useful refactor, for example our ImageSource could be their Base64ImageSourceParam

Well, actually as you will see in e2e7e9f, I went the opposite direction, as from my perspective, mixing our own types and the Anthropic ones was adding complexity for no apparent benefits (that I or my Claudes could see). We had from_anthropic/to_anthropic methods that were never called, for example.

@cboos cboos merged commit 731b551 into main Dec 20, 2025
15 checks passed
@daaain
Copy link
Owner

daaain commented Dec 20, 2025

Well, actually as you will see in e2e7e9f, I went the opposite direction, as from my perspective, mixing our own types and the Anthropic ones was adding complexity for no apparent benefits (that I or my Claudes could see). We had from_anthropic/to_anthropic methods that were never called, for example.

Yeah, I guess another option is to rip those out completely 🤷 I included them thinking that using existing Anthropic types would make life easier, but I think they don't always fit and Claude is almost always too lazy to look them up, so maybe more hassle than what they worth...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants