Skip to content

Commit 9d76ab8

Browse files
cboosclaude
andauthored
Foldable messages (#42)
* Implement hierarchical conversation structure with margin-based indentation Reorganized message margins to create clear visual hierarchy: RIGHT-ALIGNED (user-initiated, left: 33%, right: 0): - User messages (not compacted) - System commands - System errors LEFT-ALIGNED (assistant-generated, progressive indentation): - Assistant/Thinking: left: 0, right: 8em - Tool use/result: left: 2em, right: 6em - System warning/info: left: 2em, right: 6em - Sidechain user: left: 4em, right: 4em - Sidechain assistant: left: 4em, right: 4em - Sidechain tools: left: 6em, right: 2em Changes: - Grouped all margin rules in dedicated "CONVERSATION STRUCTURE" section - Removed scattered margin-left declarations from individual selectors - Clear visual separation between user (right) and assistant (left) sides - Progressive 2em indentation for nested assistant operations - Sidechain (sub-assistant) hierarchy clearly distinguished This creates a "conversation tree" visual where: - User speaks from the right - Assistant responds from the left with tools indented - Sub-assistants (via Task tool) are further indented - Each level uses 2em indentation increment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Adjust sub-assistant prompt indentation to match Task level Move sidechain user (sub-assistant prompt) from 4em to 2em indentation, placing it at the same level as the Task tool that invokes it. Previous hierarchy: - Task tool: 2em - Sub-assistant prompt: 4em (nested below) - Sub-assistant: 4em - Sub-tools: 6em New hierarchy: - Task tool: 2em - Sub-assistant prompt: 2em (sibling, not child) - Sub-assistant: 4em (nested below prompt) - Sub-tools: 6em (nested below sub-assistant) This better reflects the conversation flow where the Task and its prompt are siblings in the dialogue, with the sub-assistant's response being the nested continuation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Implement hierarchical fold/unfold functionality for conversation structure Added interactive collapsing/expanding of messages based on hierarchical conversation structure: Backend (renderer.py): - Added message_id, ancestry, and has_children fields to TemplateMessage - Implemented _get_message_hierarchy_level() to determine nesting levels: * Level 0 (0em): User, Assistant, Thinking, System * Level 1 (2em): Tool use/result, Sidechain user (sub-assistant prompt) * Level 2 (4em): Sidechain assistant/thinking * Level 3 (6em): Sidechain tools - Implemented _update_hierarchy_stack() to track message ancestry - Added hierarchy tracking to all TemplateMessage creations (system, main, tool messages) - Implemented _mark_messages_with_children() to identify messages with descendants - Reset hierarchy stack at session boundaries Frontend (transcript.html): - Added ancestry classes to message divs (e.g., "d-10 d-23 d-42") - Added data-message-id attribute to each message - Added fold-toggle button for messages with children - Implemented JavaScript fold/unfold logic: * Toggle button changes between ▾ (unfolded) and ▸ (folded) * Clicking folds/unfolds all descendants by hiding/showing messages with parent ID in their class list Styling (message_styles.css): - Added .fold-toggle button styles - Positioned button absolutely on the left edge of messages - Added hover and folded states This creates a tree-like conversation view where users can collapse/expand branches of the conversation hierarchy (e.g., fold all tools under an assistant message, or fold an entire sub-assistant chain). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix hierarchy level logic for correct message nesting Corrected _get_message_hierarchy_level() to properly reflect logical nesting: Before (incorrect): - Level 0: User, Assistant, Thinking, System - Level 1: Tools, System warnings/info, Sidechain user - Level 2: Sidechain assistant/thinking - Level 3: Sidechain tools After (correct): - Level 0: User (triggers everything), System - Level 1: Assistant, Thinking, System warnings/info (nested under user) - Level 2: Tool use/result (nested under assistant), Sidechain user - Level 3: Sidechain assistant/thinking (nested under sidechain user) - Level 4: Sidechain tools (nested under sidechain assistant) This matches the logical conversation flow where: - User messages trigger assistant responses - Assistant responses contain tools - Tools can spawn sub-assistants (sidechains) - Sub-assistants have their own tool calls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Make tool_result one level deeper than tool_use for proper folding Tool results should be children of their corresponding tool_use messages for folding purposes. When a tool_use is folded, its result should be hidden as well. Added special case in hierarchy tracking: tool_result messages get level + 1, making them nested under their tool_use parent. This ensures that folding a tool_use will hide both the tool call and its result. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix tool pair folding and add debug hierarchy display Fixed two critical issues with hierarchical folding: 1. Tool pairs now fold as a unit: - tool_result reuses its paired tool_use's message ID - This makes tool_use + tool_result a single foldable unit - Folding affects what comes AFTER the pair, not the pair itself 2. Assistant/Thinking messages now always created when they have tools: - Previously skipped if no text content - Now created with empty content when tools present - Ensures fold buttons appear on Assistant/Thinking with tools 3. Added debug hierarchy display: - Shows message ID and ancestry in message header - Format: 'd-42 ← [d-10, d-23]' (ID ← ancestors) - Helps troubleshoot hierarchy structure This implements the correct folding logic where: - User can fold everything they trigger - Assistant/Thinking can fold their tools - Tool pairs (use+result) fold as a unit - Sub-assistant chains fold under the Task tool that spawned them 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix spurious empty assistant messages in hierarchy Remove logic that created empty assistant container messages when assistant entries only contained tools. This was causing: - Spurious empty assistant messages before each tool pair - Incorrect hierarchy stack management - Message IDs being incremented for non-existent messages Now only creates assistant/thinking messages when they have text content. Tools become direct children of the current hierarchy level (typically the user message) when assistant has no text. This ensures folding behavior works correctly: folding a user message folds ALL subsequent lower-level messages until the next user message. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove debug ancestry display from message headers The ancestry information (message_id and ancestor chain) was useful for debugging the hierarchy implementation but is not needed in production. This debug feature can be restored by reverting this commit if needed for troubleshooting hierarchy issues. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Implement horizontal fold bar UI with default folded state This commit introduces a new fold/unfold UI based on horizontal bars below messages, replacing the small button approach with a more accessible design. **Key Features:** - Horizontal fold bars below messages with children - Two-zone interaction: fold one level (left) vs fold all (right) - Visual feedback: counts, descriptive labels, double-line when folded - Border colors match parent message type - Default folded state: all messages start collapsed except sessions - Fast animations (200ms transitions) - Efficient O(n) descendant counting during hierarchy building **Implementation:** - Enhanced TemplateMessage with immediate_children_count and total_descendants_count - Optimized _mark_messages_with_children() for O(n) counting - New fold-bar HTML structure with two clickable sections - CSS styling with border-color matching and gradient backgrounds - JavaScript handlers for fold-one and fold-all actions - Auto-fold on page load (top-level messages only) **UI Design:** Folded: ═══════════════ ↓ 2 messages ↓↓ 125 total Unfolded: △ fold 2 △△ fold all 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Refine fold UI: fix initial state, styling, and session support Address all feedback from initial fold UI implementation: 1. Fix initial state synchronization - Remove default 'folded' class - messages start visible - Use expanded icons (▼/▼▼) by default - Remove auto-folding on page load - First click now works correctly 2. Improve visual integration - Set message padding-bottom to 0 when fold-bar present - Use CSS :has() selector for clean integration - Fold bar now looks like natural extension of message 3. Add session header folding support - Sessions now have fold bars like regular messages - Session headers become fold parents via message_id - All session messages can be folded/expanded - Added session-header border color 4. Optimize fold bar layout - Hide "fold all" button when counts match - Show single full-width button for single-level folds - Better use of horizontal space 5. Update fold icons for better aesthetics - 1-level folded: ▶, expanded: ▼ - All levels folded: ▶▶, expanded: ▼▼ - Cleaner, more professional appearance 6. Fix test compatibility - Update todowrite test for new HTML structure - Allow for ancestor IDs in class attribute Files changed: - renderer.py: Session headers as fold parents, type-aware counting - transcript.html: Conditional rendering, updated icons, removed auto-fold - message_styles.css: Padding fix, full-width button support, new colors - global_styles.css: Added message type color variables - test_todowrite_rendering.py: More flexible class matching 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix Pyright type errors in _format_type_counts function Added explicit type annotation `parts: list[str] = []` to resolve type inference issues at lines 1440, 1443, 1444, and 1445. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix message hierarchy structure for proper fold organization Updated hierarchy levels to properly reflect logical nesting: - Level 0: Session headers (unchanged) - Level 1: User messages (was 0) - Level 2: System messages, Assistant, Thinking (was 0-1) - Level 3: Tools, Sidechain user (was 2) - Level 4: Sidechain assistant/thinking (was 3) - Level 5: Sidechain tools (was 4) This ensures: - User messages fold under sessions - System messages fold under user messages (not siblings) - All messages have proper parent-child relationships Also fixed test assertions to be flexible about ancestor IDs in CSS class attributes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Keep images inline for user messages, extract for assistant messages Images in user messages are now rendered inline on the right side with the text content, while images in assistant messages are still extracted as separate message items. This provides better visual grouping - user-uploaded images appear together with the user's text, matching the natural flow of "here's my question and screenshot". 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix session fold-one-level button to recognize session IDs Updated handleFoldOne JavaScript to recognize both regular message IDs (d-XXX) and session IDs (session-XXX) when filtering for ancestor classes. Previously, clicking "fold one level" on a session did nothing because the code only looked for ancestor IDs starting with 'd-', missing session IDs that start with 'session-'. Now both fold buttons work correctly for sessions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Add fold button coordination and intelligent initial state Implemented coordination between fold-one and fold-all buttons: - Folding immediate children also folds all descendants (sync ▶/▶▶) - Unfolding all descendants also unfolds immediate children (sync ▼/▼▼) - Folding all descendants also folds immediate children (sync ▶/▶▶) - Unfolding immediate children keeps fold-all at ▶▶ (independent) Initial page load state: - Sessions: unfolded at first level (▼/▶▶) - shows user messages - User messages: unfolded at first level (▼/▶▶) - shows assistant/system - Assistant/system/thinking/tools: fully folded (▶/▶▶) - hidden This provides a clean initial view showing the conversation structure while hiding implementation details until explicitly expanded. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Small stylistic improvement: less visually intrusive fold bar * Fixup a9cd857: remove 'test_project_fold_ui.html' * Improve fold UI: three-state behavior, styling fixes, and initial states This commit implements six key improvements to the horizontal fold bar UI: 1. **Sub-assistant fold-bar styling**: Sidechain (sub-assistant) fold bars now use dashed borders matching the message's left border style, instead of solid black borders. 2. **Sub-assistant initial visibility**: Fixed bug where sub-assistant messages were incorrectly shown initially even when parent assistant was folded. Newly revealed children are now properly initialized to folded state. 3. **User messages with only tools**: User messages that have only tool use/result pairs as children (no assistant/system/thinking) now start in fully folded state for cleaner initial view. 4. **Tool pair display**: Fixed count display to show "N tool_pairs" instead of "N tools, N results" when tool_use and tool_result counts match, even when other message types are present. 5. **System info indentation**: System and system-info messages now align at level 2 (same as assistant messages) with proper right margins (8em for system, 10em for system-info). 6. **Three-state fold button behavior**: Implemented new state machine where the "fold all" button (▼▼→▶▶) goes back to first level visible (State B) instead of fully folded (State A), creating a natural exploration pattern: nothing → all levels → first level → nothing. Files changed: - claude_code_log/renderer.py: Modified _format_type_counts() to combine tool pairs - claude_code_log/templates/transcript.html: Updated fold button handlers and initial state logic - claude_code_log/templates/components/message_styles.css: Fixed sidechain and system message styling - FOLD_STATE_DIAGRAM.md: Added comprehensive state diagram documenting the three-state behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * OK, keep FOLD_STATE_DIAGRAM.md but out of the way. * Add message structure example to FOLD_STATE_DIAGRAM.md * Fix critical performance issue in fold one level operation The "Fold (to all levels) ▼" operation was taking ~60 seconds for sessions with ~5000 messages due to O(n²) complexity from recursive DOM queries. Problem: The hideMessageAndDescendants() function recursively called document.querySelectorAll() for each message in the hierarchy, resulting in potentially thousands of DOM queries for large sessions. Solution: Replaced recursive approach with single querySelectorAll() call to get ALL descendants at once, then hide them with a simple forEach loop. This matches the efficient pattern used by handleFoldAll(). Changes: - Removed obsolete hideMessageAndDescendants() function - Modified handleFoldOne() to use single DOM query for all descendants - Performance improved from ~60s to ~1-2s (on par with other fold operations) Performance impact: - Before: O(n²) or worse - recursive DOM queries per message - After: O(n) - single DOM query, simple iteration - Real-world: 60s → 1-2s for 5000 message session 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix fold bar styling for compacted user messages Compacted user messages (generated when a session runs out of context) have css_class "user compacted" instead of just "user", which caused the fold bar border colors to not match the CSS selectors. Changes: - Added "user compacted" variant to fold bar border color selector - This ensures compacted user messages have orange fold bars like regular user messages Note: The padding-bottom issue mentioned was due to mistune's HTML structure placing the fold-bar div as a non-immediate child of the message div, causing the :has(.fold-bar) selector to not apply. This will be addressed separately when we add mistune HTML cleanup/validation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Optimize setInitialFoldState() to eliminate O(n²) performance issue Replace repeated querySelectorAll() calls with cached hierarchy lookups: - Build messagesByParentId and allDescendantsByParentId maps in single O(n) pass - Use cached lookups instead of DOM queries inside message processing loop - Reduces complexity from O(n²) to O(n) for n messages For transcripts with 5000 messages, this reduces ~15,000 DOM queries to ~10,000 in-memory operations, significantly improving page load time. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Add dynamic tooltips to fold controls showing action intent Implement context-aware tooltips that change based on fold button state: - ▶ (folded, fold-one): "Unfold (1st level)..." - ▼ (unfolded, fold-one): "Fold (all levels)..." - ▶▶ (folded, fold-all): "Unfold (all levels)..." - ▼▼ (unfolded, fold-all): "Fold (to 1st level)..." Implementation uses data-driven approach: - Store both tooltip variants as data attributes (data-title-folded/unfolded) - Single lightweight updateTooltip() function reads current state - Called only when fold state changes (no performance impact) - Zero code duplication, minimal changes to existing code Benefits: - Users see what WILL happen when clicking (not current state) - < 1ms per update (just attribute reads/writes) - Maintainable: tooltip text lives in template near generation logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix template rendering tests for fold UI ancestry classes Update CSS class assertions to handle ancestry tracking classes added by fold UI. Tests now check for "class='message user" (substring) instead of "class='message user'" (exact match). Messages now include ancestry classes like: - class='message user session-test_session' - class='message assistant session-test_session d-0' 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Enhance system message rendering with memory input, command pairing, and CSS variables This commit improves the handling and display of system messages, user memory inputs, and slash command interactions in the transcript viewer. **Memory Input Support:** - Detect and extract user memory inputs (messages starting with '#' in Claude Code) - Display with "💭 Memory" title instead of "🤷 User" - Extract content from <user-memory-input> tags and hide wrapper **System Command Message Pairing:** - Implement UUID-based pairing for parent-child system messages - Extract command names from <command-name> tags (e.g., /memory, /init) - Extract output from <local-command-stdout> tags - Pair command invocations with their responses using parentUuid relationships - Visual pairing similar to tool_use/tool_result pairs **CSS Class Logic:** - User-initiated commands (with <command-name>): "system" class (right-aligned, orange border) - Command output responses (with <local-command-stdout>): "system system-info" class (blue border) - Paired system-info messages override default left-alignment to stay with parent command **CSS Variable Standardization:** - Replace hardcoded color values (#d98100, #2196f3, #f44336, #4caf50) with CSS variables - Use var(--system-color), var(--system-warning-color), var(--system-error-color), var(--tool-use-color) - Applied in both message_styles.css and filter_styles.css for consistency **Technical Implementation:** - Added uuid and parent_uuid fields to TemplateMessage class - Enhanced _identify_message_pairs() with UUID-based index for system messages - Modified render_user_message_content() to return (content_html, is_compacted, is_memory_input) - Added uuid_to_msg_id mapping for parent-child hierarchy tracking - Hierarchy adjustments based on parentUuid relationships 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix test for render_user_message_content return value change Updated test_render_user_message_with_multi_item_content to handle the new three-value return from render_user_message_content (content_html, is_compacted, is_memory_input). Added assertion to verify is_memory_input is False for non-memory messages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix pyright type errors in renderer.py - Add explicit type annotation for memory_content_list to fix List[ContentItem] type error - Rename loop variable from 'i' to 'idx' to avoid shadowing outer loop variable - Correct hierarchy_stack tuple unpacking order: (stack_level, stack_msg_id) instead of (stack_id, stack_level) - Add explicit type annotation for current_level: int All pyright errors resolved: 0 errors, 0 warnings, 0 informations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Ignore uv's pinned Python version * Improve message counting to treat paired messages as single visual units When counting immediate children and descendants, skip the second message in pairs (pair_last) since pairs visually appear as a single two-sided element. This provides more accurate counts that match the visual presentation and simplifies the counting logic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix missing import in simplified deduplication logic Add AssistantTranscriptEntry import alongside SystemTranscriptEntry since it's still referenced elsewhere in the code (lines 2534, 2711). The simplified deduplication logic uses (message_type, timestamp) as the key, treating messages with identical timestamps as duplicates regardless of content differences (e.g., IDE selection tags). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove unused import * Fix data-border-color CSS to handle multi-class attributes The data-border-color attribute now contains space-separated CSS classes (e.g., "user sidechain", "tool_result error"), but the CSS selectors were only matching single-class values. This caused fold-bar borders to not display with the correct colors for messages with multiple classes. Changes: - Add CSS rules for all multi-class combinations (sidechain, compacted, error) - Add missing CSS rules for new message types (image, unknown, bash-input, bash-output) - Add .markdown to font-family rule for consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Improve sidechain/agent support with automatic file loading and enhanced Task tool rendering This commit adds comprehensive support for Claude Code's agent/sidechain system, including automatic loading of agent files and improved rendering of Task tool calls and their results. Agent File Loading: - Automatically detect agentId references in UserTranscriptEntry messages - Load corresponding agent-{agentId}.jsonl files from the same directory - Insert agent messages at their point of use (preserving hierarchy) - Handle recursive agent loading (agents that spawn sub-agents) - Prevent infinite recursion with cycle detection Models (claude_code_log/models.py): - Add agentId field to ToolResultContent for agent references - Add agentId field to UserTranscriptEntry to preserve agent associations Parser (claude_code_log/parser.py): - Extract agentId from toolUseResult before Pydantic parsing - Load agent files recursively when agentId references are found - Build agent message map and insert at point of use - Add _loaded_files parameter to prevent infinite recursion Renderer (claude_code_log/renderer.py): - Add format_task_tool_content() to render Task prompts as markdown - Add get_tool_summary() support for Task tool description - Add format_tool_result_content() support for Task results as markdown - Add base_type property to TemplateMessage for CSS class extraction - Add Task tool title formatting with subagent_type and description - Track last_task_tool_id for sidechain prompt deduplication - Skip redundant sidechain user prompts that duplicate Task prompts The result is a seamless viewing experience where agent conversations appear inline at their logical point in the main transcript, maintaining the hierarchical structure of agent spawning and execution. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Implement bidirectional Task result deduplication for sidechain assistants Add intelligent deduplication to eliminate redundant content between Task tool results and sub-agent assistant messages. When a Task tool result and a sidechain assistant message contain identical content, the assistant message is replaced with a forward link to the Task result. Key implementation details: - Use unified content_map to track both Task results and sidechain assistants - Support bidirectional matching (either can appear first in message stream) - Extract text content consistently from both message types - Replace duplicate assistant content with HTML forward link - Preserve parent-child relationship in fold structure This reduces visual redundancy when viewing sub-agent transcripts while maintaining full traceability through clickable links. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix forward link targets and add Pygments highlighting to Task content Two bug fixes for Task result deduplication feature: 1. Forward link targets: Add id attributes to message elements - Added id='msg-{{ message.message_id }}' to both session headers and regular messages - Links like #msg-d-3172 now have valid anchor targets - Previously only had data-message-id attributes which aren't valid link targets 2. Pygments syntax highlighting: Use render_markdown() for Task content - Replaced manual mistune instances with render_markdown() function - Applied to both Task tool input (prompt) and tool result (output) - Ensures consistent syntax highlighting across all markdown content - Simplified code by using shared rendering function 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix sidechain agent tests and add comprehensive test data - Fixed all 5 sidechain agent tests (test_agent_insertion, test_deduplication_task_result_vs_sidechain, test_no_deduplication_when_content_different, test_agent_messages_marked_as_sidechain, test_multiple_agent_invocations) - Created proper test data files from real examples with truncated strings: - test/test_data/sidechain_main.jsonl (3 main transcript messages) - test/test_data/sidechain_agent.jsonl (3 agent messages) - test/test_data/dedup_main.jsonl (deduplication scenario - main) - test/test_data/dedup_agent.jsonl (deduplication scenario - agent) - Fixed imports from converter to parser/renderer modules - Removed agent_dir parameter (agent files auto-discovered by load_transcript) - Updated test assertions to use correct agentId (e1c84ba5) and UUIDs from real data - Added Pydantic-compatible inline test data for multiple agent invocations test - All 253 unit tests now passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix duplicate HTML IDs for paired messages Paired messages (tool_use/tool_result, thinking/assistant, etc.) now have unique IDs with -first/-last suffixes instead of sharing the same ID. Changes: - Template: Add pair_role suffix to message IDs (e.g., msg-d-5-first, msg-d-5-last) - Template: Remove duplicate id attribute from session headers - Renderer: Update deduplication links to point to -last suffix for tool results - All 253 unit tests passing Fixes invalid HTML where both messages in a pair had identical id attributes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Improve deduplication message phrasing Changed from technical "Content duplicates Task tool result above" to more user-friendly "Task summary — already displayed in Task tool result above" Changes: - Updated both deduplication link messages (above/below cases) - Updated test assertions to match new phrasing - All 253 unit tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Refactor tool message ID generation to remove brittle pairing logic Remove redundant pairing logic during message creation in favor of the robust _identify_message_pairs() function. This clarifies the architecture by separating message creation (which generates unique IDs) from pairing logic (which marks relationships between messages). Changes: - Reuse tool_is_sidechain variable instead of calling getattr() twice - Remove brittle "look back 10 messages" logic (lines 3210-3222) - Always generate unique message IDs during creation - Add comment clarifying that pairing is handled later The early pairing logic was attempting to make tool_use/tool_result pairs share message IDs for folding, but this was: 1. Brittle - only checked last 10 messages 2. Redundant - _identify_message_pairs() does this robustly with no limits 3. Unnecessary - template now uses -first/-last ID suffixes anyway All sidechain agent tests pass with this cleaner architecture. * Update FOLD_STATE_DIAGRAM.md to reflect implemented behavior Sync documentation with actual implementation before branch merge: - Improve hierarchy diagram with visual pairing notation - Add notes on paired messages, sidechain nesting, and deduplication - Change "Proposed Behavior" to "Fold Bar Behavior" (now implemented) - Fix "four states" to "three states" (A, B, C only) - Add Dynamic Tooltips section documenting context-aware tooltips - Add Implementation Notes on performance and type-aware labels 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove unused .jsonl test data (noted by the CodeRabbit review) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 34422bc commit 9d76ab8

18 files changed

Lines changed: 1619 additions & 133 deletions

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ __pycache__/
88

99
# Distribution / packaging
1010
.Python
11+
.python-version
1112
build/
1213
develop-eggs/
1314
dist/

‎claude_code_log/models.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class ToolResultContent(BaseModel):
7474
tool_use_id: str
7575
content: Union[str, List[Dict[str, Any]]]
7676
is_error: Optional[bool] = None
77+
agentId: Optional[str] = None # Reference to agent file for sub-agent messages
7778

7879

7980
class ThinkingContent(BaseModel):
@@ -202,6 +203,7 @@ class UserTranscriptEntry(BaseTranscriptEntry):
202203
type: Literal["user"]
203204
message: UserMessage
204205
toolUseResult: Optional[ToolUseResult] = None
206+
agentId: Optional[str] = None # From toolUseResult when present
205207

206208

207209
class AssistantTranscriptEntry(BaseTranscriptEntry):

‎claude_code_log/parser.py‎

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from .models import (
1212
TranscriptEntry,
13+
UserTranscriptEntry,
1314
SummaryTranscriptEntry,
1415
parse_transcript_entry,
1516
ContentItem,
@@ -120,8 +121,22 @@ def load_transcript(
120121
from_date: Optional[str] = None,
121122
to_date: Optional[str] = None,
122123
silent: bool = False,
124+
_loaded_files: Optional[set[Path]] = None,
123125
) -> List[TranscriptEntry]:
124-
"""Load and parse JSONL transcript file, using cache if available."""
126+
"""Load and parse JSONL transcript file, using cache if available.
127+
128+
Args:
129+
_loaded_files: Internal parameter to track loaded files and prevent infinite recursion.
130+
"""
131+
# Initialize loaded files set on first call
132+
if _loaded_files is None:
133+
_loaded_files = set()
134+
135+
# Prevent infinite recursion by checking if this file is already being loaded
136+
if jsonl_path in _loaded_files:
137+
return []
138+
139+
_loaded_files.add(jsonl_path)
125140
# Try to load from cache first
126141
if cache_manager is not None:
127142
# Use filtered loading if date parameters are provided
@@ -139,6 +154,7 @@ def load_transcript(
139154

140155
# Parse from source file
141156
messages: List[TranscriptEntry] = []
157+
agent_ids: set[str] = set() # Collect agentId references while parsing
142158

143159
with open(jsonl_path, "r", encoding="utf-8", errors="replace") as f:
144160
if not silent:
@@ -154,6 +170,25 @@ def load_transcript(
154170
)
155171
continue
156172

173+
# Check for agentId BEFORE Pydantic parsing
174+
# agentId can be at top level OR nested in toolUseResult
175+
# For UserTranscriptEntry, we need to copy it to top level so Pydantic preserves it
176+
if "agentId" in entry_dict:
177+
agent_id = entry_dict.get("agentId")
178+
if agent_id:
179+
agent_ids.add(agent_id)
180+
elif "toolUseResult" in entry_dict:
181+
tool_use_result = entry_dict.get("toolUseResult")
182+
if (
183+
isinstance(tool_use_result, dict)
184+
and "agentId" in tool_use_result
185+
):
186+
agent_id_value = tool_use_result.get("agentId") # type: ignore[reportUnknownVariableType, reportUnknownMemberType]
187+
if isinstance(agent_id_value, str):
188+
agent_ids.add(agent_id_value)
189+
# Copy agentId to top level for Pydantic to preserve
190+
entry_dict["agentId"] = agent_id_value
191+
157192
entry_type: str | None = entry_dict.get("type")
158193

159194
if entry_type in [
@@ -195,6 +230,47 @@ def load_transcript(
195230
"\n{traceback.format_exc()}"
196231
)
197232

233+
# Load agent files if any were referenced
234+
# Build a map of agentId -> agent messages
235+
agent_messages_map: dict[str, List[TranscriptEntry]] = {}
236+
if agent_ids:
237+
parent_dir = jsonl_path.parent
238+
for agent_id in agent_ids:
239+
agent_file = parent_dir / f"agent-{agent_id}.jsonl"
240+
# Skip if the agent file is the same as the current file (self-reference)
241+
if agent_file == jsonl_path:
242+
continue
243+
if agent_file.exists():
244+
if not silent:
245+
print(f"Loading agent file {agent_file}...")
246+
# Recursively load the agent file (it might reference other agents)
247+
agent_messages = load_transcript(
248+
agent_file,
249+
cache_manager,
250+
from_date,
251+
to_date,
252+
silent=True,
253+
_loaded_files=_loaded_files,
254+
)
255+
agent_messages_map[agent_id] = agent_messages
256+
257+
# Insert agent messages at their point of use
258+
if agent_messages_map:
259+
# Iterate through messages and insert agent messages after the message
260+
# that references them (via UserTranscriptEntry.agentId)
261+
result_messages: List[TranscriptEntry] = []
262+
for message in messages:
263+
result_messages.append(message)
264+
265+
# Check if this is a UserTranscriptEntry with agentId
266+
if isinstance(message, UserTranscriptEntry) and message.agentId:
267+
agent_id = message.agentId
268+
if agent_id in agent_messages_map:
269+
# Insert agent messages right after this message
270+
result_messages.extend(agent_messages_map[agent_id])
271+
272+
messages = result_messages
273+
198274
# Save to cache if cache manager is available
199275
if cache_manager is not None:
200276
cache_manager.save_cached_entries(jsonl_path, messages)

0 commit comments

Comments
 (0)