From 794780ba7e93bbf4752e5290203a60c4a25578a3 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 09:22:45 +0100 Subject: [PATCH 01/15] Render slash command content as collapsible markdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slash command prompts (isMeta=true) now render as markdown instead of escaped preformatted text. This makes numbered lists, bullet points, and other markdown formatting display properly. - Render slash command content with render_markdown_collapsible() - Use 20 line threshold with 5 line preview for long prompts - Use hasattr/getattr for type-agnostic content extraction πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/renderer.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/claude_code_log/renderer.py b/claude_code_log/renderer.py index 5b481279..b5e70a54 100644 --- a/claude_code_log/renderer.py +++ b/claude_code_log/renderer.py @@ -2532,18 +2532,32 @@ def _process_regular_message( # Handle user-specific preprocessing if message_type == "user": # Note: sidechain user messages are skipped before reaching this function - content_html, is_compacted, is_memory_input = render_user_message_content( - text_only_content - ) - if is_compacted: - css_class = f"{message_type} compacted" - message_title = "User (compacted conversation)" - elif is_meta: - # Slash command expanded prompts - LLM-generated content + if is_meta: + # Slash command expanded prompts - render as collapsible markdown + # These contain LLM-generated instruction text (markdown formatted) css_class = f"{message_type} slash-command" message_title = "User (slash command)" - elif is_memory_input: - message_title = "Memory" + # Combine all text content (items may be TextContent, dicts, or SDK objects) + all_text = "\n\n".join( + getattr(item, "text", "") + for item in text_only_content + if hasattr(item, "text") + ) + content_html = render_markdown_collapsible( + all_text, + "slash-command-content", + line_threshold=20, + preview_line_count=5, + ) + else: + content_html, is_compacted, is_memory_input = render_user_message_content( + text_only_content + ) + if is_compacted: + css_class = f"{message_type} compacted" + message_title = "User (compacted conversation)" + elif is_memory_input: + message_title = "Memory" else: # Non-user messages: render directly content_html = render_message_content(text_only_content, message_type) From e9f4bf754ce2fa9237e21614ce5637276ab30375 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 09:28:50 +0100 Subject: [PATCH 02/15] Pair system commands with slash-command prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit System messages (like /init) are now visually paired with their corresponding User (slash command) expanded prompts. The pairing is established via parentUuid which links the slash-command message to its triggering system command. Changes: - Pass parent_uuid when creating user message TemplateMessage - Index slash-command messages by parent_uuid for pairing lookup - Add pairing logic for system command + slash-command via UUID - Update reordering to handle slash-command pairs - Fix: Only use parent_uuid for hierarchy nesting on system messages (slash-command messages stay at their natural user level) πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/renderer.py | 63 ++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/claude_code_log/renderer.py b/claude_code_log/renderer.py index b5e70a54..ffe8c52e 100644 --- a/claude_code_log/renderer.py +++ b/claude_code_log/renderer.py @@ -2592,6 +2592,7 @@ def _identify_message_pairs(messages: List[TemplateMessage]) -> None: and tool_result. Session ID is included to prevent cross-session pairing when sessions are resumed (same tool_use_id can appear in multiple sessions). Build index of uuid -> message index for parent-child system messages + Build index of parent_uuid -> message index for slash-command messages 2. Second pass: Sequential scan for adjacent pairs (system+output, bash, thinking+assistant) and match tool_use/tool_result and uuid-based pairs using the index """ @@ -2604,6 +2605,8 @@ def _identify_message_pairs(messages: List[TemplateMessage]) -> None: tuple[str, str], int ] = {} # (session_id, tool_use_id) -> index uuid_index: Dict[str, int] = {} # uuid -> message index for parent-child pairing + # Index slash-command messages by their parent_uuid for pairing with system commands + slash_command_by_parent: Dict[str, int] = {} # parent_uuid -> message index for i, msg in enumerate(messages): if msg.tool_use_id and msg.session_id: @@ -2615,6 +2618,9 @@ def _identify_message_pairs(messages: List[TemplateMessage]) -> None: # Build UUID index for system messages (both parent and child) if msg.uuid and "system" in msg.css_class: uuid_index[msg.uuid] = i + # Index slash-command user messages by parent_uuid + if msg.parent_uuid and "slash-command" in msg.css_class: + slash_command_by_parent[msg.parent_uuid] = i # Pass 2: Sequential scan to identify pairs i = 0 @@ -2663,6 +2669,17 @@ def _identify_message_pairs(messages: List[TemplateMessage]) -> None: current.is_paired = True current.pair_role = "pair_last" + # Check for system command + user slash-command pair (via parent_uuid) + # The slash-command message's parent_uuid points to the system command's uuid + if "system" in current.css_class and current.uuid: + if current.uuid in slash_command_by_parent: + slash_idx = slash_command_by_parent[current.uuid] + slash_msg = messages[slash_idx] + current.is_paired = True + current.pair_role = "pair_first" + slash_msg.is_paired = True + slash_msg.pair_role = "pair_last" + # Check for bash-input + bash-output pair (adjacent only) if current.css_class == "bash-input" and i + 1 < len(messages): next_msg = messages[i + 1] @@ -2697,7 +2714,8 @@ def _reorder_paired_messages(messages: List[TemplateMessage]) -> List[TemplateMe Uses dictionary-based approach to find pairs efficiently: 1. Build index of all pair_last messages by tool_use_id - 2. Single pass through messages, inserting pair_last immediately after pair_first + 2. Build index of slash-command pair_last messages by parent_uuid + 3. Single pass through messages, inserting pair_last immediately after pair_first """ from datetime import datetime @@ -2706,6 +2724,8 @@ def _reorder_paired_messages(messages: List[TemplateMessage]) -> List[TemplateMe pair_last_index: Dict[ tuple[str, str], int ] = {} # (session_id, tool_use_id) -> message index + # Index slash-command pair_last messages by parent_uuid + slash_command_pair_index: Dict[str, int] = {} # parent_uuid -> message index for i, msg in enumerate(messages): if ( @@ -2716,6 +2736,14 @@ def _reorder_paired_messages(messages: List[TemplateMessage]) -> List[TemplateMe ): key = (msg.session_id, msg.tool_use_id) pair_last_index[key] = i + # Index slash-command messages by parent_uuid + if ( + msg.is_paired + and msg.pair_role == "pair_last" + and msg.parent_uuid + and "slash-command" in msg.css_class + ): + slash_command_pair_index[msg.parent_uuid] = i # Create reordered list reordered: List[TemplateMessage] = [] @@ -2729,16 +2757,23 @@ def _reorder_paired_messages(messages: List[TemplateMessage]) -> List[TemplateMe # If this is the first message in a pair, immediately add its pair_last # Key includes session_id to prevent cross-session pairing on resume - if ( - msg.is_paired - and msg.pair_role == "pair_first" - and msg.tool_use_id - and msg.session_id - ): - key = (msg.session_id, msg.tool_use_id) - if key in pair_last_index: - last_idx = pair_last_index[key] + if msg.is_paired and msg.pair_role == "pair_first": + pair_last = None + last_idx = None + + # Check for tool_use_id based pairs + if msg.tool_use_id and msg.session_id: + key = (msg.session_id, msg.tool_use_id) + if key in pair_last_index: + last_idx = pair_last_index[key] + pair_last = messages[last_idx] + + # Check for system + slash-command pairs (via uuid -> parent_uuid) + if pair_last is None and msg.uuid and msg.uuid in slash_command_pair_index: + last_idx = slash_command_pair_index[msg.uuid] pair_last = messages[last_idx] + + if pair_last is not None and last_idx is not None: reordered.append(pair_last) skip_indices.add(last_idx) @@ -2879,7 +2914,12 @@ def _build_message_hierarchy(messages: List[TemplateMessage]) -> None: # Session headers are level 0 if message.is_session_header: current_level = 0 - elif message.parent_uuid and message.parent_uuid in uuid_to_info: + elif ( + message.parent_uuid + and message.parent_uuid in uuid_to_info + and "system" + in message.css_class # Only system messages nest via parent_uuid + ): # System message with known parent - nest under parent _, parent_level = uuid_to_info[message.parent_uuid] current_level = parent_level + 1 @@ -3813,6 +3853,7 @@ def _process_messages_loop( ancestry=[], # Will be assigned by _build_message_hierarchy agent_id=getattr(message, "agentId", None), uuid=getattr(message, "uuid", None), + parent_uuid=getattr(message, "parentUuid", None), ) # Store raw text content for potential future use (e.g., deduplication, From e160463d04dc9c4e44aad21d4c59caea3d8905e2 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 10:58:18 +0100 Subject: [PATCH 03/15] Change TodoWrite priority colors to purple intensity spectrum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace red/yellow/green priority colors with purple intensity: - High: deep violet (#7c3aed) - Medium: medium purple (#a78bfa) - Low: light lavender (#c4b5fd) This avoids confusion with error/warning/success semantics while still conveying urgency through color intensity (darker = more urgent). Other changes: - Remove checkboxes (status emojis are sufficient) - Apply opacity only to .todo-content for completed items - Use normal font weight for pending items πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/renderer.py | 8 +------- .../templates/components/global_styles.css | 11 ++++++++--- claude_code_log/templates/components/todo_styles.css | 10 +++------- test/test_todowrite_rendering.py | 5 ----- 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/claude_code_log/renderer.py b/claude_code_log/renderer.py index ffe8c52e..d12be75b 100644 --- a/claude_code_log/renderer.py +++ b/claude_code_log/renderer.py @@ -567,7 +567,7 @@ def format_exitplanmode_result(content: str) -> str: def format_todowrite_content(tool_use: ToolUseContent) -> str: - """Format TodoWrite tool use content as an actual todo list with checkboxes.""" + """Format TodoWrite tool use content as a todo list.""" # Parse todos from input todos_data = tool_use.input.get("todos", []) if not todos_data: @@ -590,16 +590,11 @@ def format_todowrite_content(tool_use: ToolUseContent) -> str: priority = todo.get("priority", "medium") status_emoji = status_emojis.get(status, "⏳") - # Determine checkbox state - checked = "checked" if status == "completed" else "" - disabled = "disabled" if status == "completed" else "" - # CSS class for styling item_class = f"todo-item {status} {priority}" todo_items.append(f"""
- {status_emoji} {content} #{todo_id} @@ -608,7 +603,6 @@ def format_todowrite_content(tool_use: ToolUseContent) -> str: except AttributeError: todo_items.append(f"""
- ⏳ {str(todo)}
diff --git a/claude_code_log/templates/components/global_styles.css b/claude_code_log/templates/components/global_styles.css index 393b870c..4d4f1b3f 100644 --- a/claude_code_log/templates/components/global_styles.css +++ b/claude_code_log/templates/components/global_styles.css @@ -44,10 +44,15 @@ --answer-accent: #4caf50; --answer-bg: #f0fff4; + /* Priority palette (purple intensity - darker = more urgent) */ + --priority-600: #7c3aed; + --priority-400: #a78bfa; + --priority-300: #c4b5fd; + /* Priority colors for todos */ - --priority-high: #dc3545; - --priority-medium: #ffc107; - --priority-low: #28a745; + --priority-high: var(--priority-600); + --priority-medium: var(--priority-400); + --priority-low: var(--priority-300); /* Status colors */ --status-in-progress: #fff3cd; diff --git a/claude_code_log/templates/components/todo_styles.css b/claude_code_log/templates/components/todo_styles.css index 944ee9ca..22a35140 100644 --- a/claude_code_log/templates/components/todo_styles.css +++ b/claude_code_log/templates/components/todo_styles.css @@ -40,18 +40,14 @@ background-color: var(--bg-hover); } -.todo-item.completed { - opacity: 0.7; -} - .todo-item.completed .todo-content { text-decoration: line-through; color: var(--text-muted); + opacity: 0.7; } -.todo-item input[type="checkbox"] { - margin: 0; - cursor: default; +.todo-item.pending .todo-content { + font-weight: normal; } .todo-status { diff --git a/test/test_todowrite_rendering.py b/test/test_todowrite_rendering.py index 80942ab7..1a3b60a9 100644 --- a/test/test_todowrite_rendering.py +++ b/test/test_todowrite_rendering.py @@ -60,11 +60,6 @@ def test_format_todowrite_basic(self): assert "πŸ”„" in html # in_progress assert "⏳" in html # pending - # Check checkboxes - assert 'type="checkbox"' in html - assert "checked" in html # for completed item - assert "disabled" in html # for completed item - # Check CSS classes assert "todo-item completed high" in html assert "todo-item in_progress medium" in html From eada81b4752145535560db5dd17f8eba5a05ae7b Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 11:35:08 +0100 Subject: [PATCH 04/15] Fix system-info alignment to stay left (assistant-initiated) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove exception that made paired system-info messages right-aligned. System-info messages (like hook notifications) are always assistant- initiated, so they should stay on the left side regardless of pairing. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/templates/components/message_styles.css | 6 ------ 1 file changed, 6 deletions(-) diff --git a/claude_code_log/templates/components/message_styles.css b/claude_code_log/templates/components/message_styles.css index 7fd8472b..f1a8a8ba 100644 --- a/claude_code_log/templates/components/message_styles.css +++ b/claude_code_log/templates/components/message_styles.css @@ -214,12 +214,6 @@ margin-right: 10em; } -/* Exception: paired system-info messages align right (like user commands) */ -.system.system-info.paired-message { - margin-left: 33%; - margin-right: 0; -} - /* Sidechain messages (sub-assistant hierarchy) */ /* Note: .sidechain.user (Sub-assistant prompt) is no longer produced since it duplicates the Task tool input prompt */ From f2b7f3009449aed1c5d155cdaa82306f6cd0c943 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 12:03:09 +0100 Subject: [PATCH 05/15] Put system-info/warning at tool level (level 3) in hierarchy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit System info and warning messages (like hook notifications) are now at level 3 (same as tools) instead of level 2 (same as assistant). This prevents them from incorrectly "swallowing" subsequent tool messages as children. Hierarchy now: - Level 2: System commands/errors, Assistant, Thinking - Level 3: Tool use/result, System info/warning πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/renderer.py | 12 +++++++++--- .../templates/components/message_styles.css | 6 +++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/claude_code_log/renderer.py b/claude_code_log/renderer.py index d12be75b..4b2048d3 100644 --- a/claude_code_log/renderer.py +++ b/claude_code_log/renderer.py @@ -2842,8 +2842,8 @@ def _get_message_hierarchy_level(css_class: str, is_sidechain: bool) -> int: Correct hierarchy based on logical nesting: - Level 0: Session headers - Level 1: User messages - - Level 2: System messages, Assistant, Thinking - - Level 3: Tool use/result (nested under assistant) + - Level 2: System commands/errors, Assistant, Thinking + - Level 3: Tool use/result, System info/warning (nested under assistant) - Level 4: Sidechain assistant/thinking (nested under Task tool result) - Level 5: Sidechain tools (nested under sidechain assistant) @@ -2858,7 +2858,13 @@ def _get_message_hierarchy_level(css_class: str, is_sidechain: bool) -> int: if "user" in css_class and not is_sidechain: return 1 - # System messages at level 2 (siblings to assistant, under user) + # System info/warning at level 3 (tool-related, e.g., hook notifications) + if ( + "system-info" in css_class or "system-warning" in css_class + ) and not is_sidechain: + return 3 + + # System commands/errors at level 2 (siblings to assistant) if "system" in css_class and not is_sidechain: return 2 diff --git a/claude_code_log/templates/components/message_styles.css b/claude_code_log/templates/components/message_styles.css index f1a8a8ba..1d33f29d 100644 --- a/claude_code_log/templates/components/message_styles.css +++ b/claude_code_log/templates/components/message_styles.css @@ -207,11 +207,11 @@ margin-right: 6em; } -/* System warnings/info (assistant-initiated) */ +/* System warnings/info at tool level (e.g., hook notifications) */ .system-warning, .system-info { - margin-left: 0; - margin-right: 10em; + margin-left: 2em; + margin-right: 6em; } /* Sidechain messages (sub-assistant hierarchy) */ From 54302b6cedcca44c62dbe824727b999be0489913 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 14:31:14 +0100 Subject: [PATCH 06/15] Fix system-info margins to match tool-level indentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exclude system-info, system-warning, and system-error from the general .system right-alignment rule using :not() pseudo-classes. Apply 80% font-size to inner .header and .content elements rather than the message container to preserve consistent box sizing. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .vscode/settings.json | 2 +- .../templates/components/message_styles.css | 7 +- test/__snapshots__/test_snapshot_html.ambr | 176 +++++++++--------- 3 files changed, 95 insertions(+), 90 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index eb7cde4c..845c5744 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,4 +5,4 @@ "docs/claude-code-log-transcript.html": true, "test/test_data/cache/**": true, }, -} \ No newline at end of file +} diff --git a/claude_code_log/templates/components/message_styles.css b/claude_code_log/templates/components/message_styles.css index 1d33f29d..05095ed3 100644 --- a/claude_code_log/templates/components/message_styles.css +++ b/claude_code_log/templates/components/message_styles.css @@ -181,7 +181,8 @@ .user:not(.compacted), .system, .bash-input, -.bash-output { +.bash-output, +.system:not(.system-info):not(.system-warning):not(.system-error) { margin-left: 33%; margin-right: 0; } @@ -334,6 +335,10 @@ .system-info { border-left-color: var(--info-dimmed); background-color: var(--highlight-dimmed); +} + +.system-info .header, +.system-info .content { font-size: 80%; } diff --git a/test/__snapshots__/test_snapshot_html.ambr b/test/__snapshots__/test_snapshot_html.ambr index ed1796fb..61e53b8a 100644 --- a/test/__snapshots__/test_snapshot_html.ambr +++ b/test/__snapshots__/test_snapshot_html.ambr @@ -56,10 +56,15 @@ --answer-accent: #4caf50; --answer-bg: #f0fff4; + /* Priority palette (purple intensity - darker = more urgent) */ + --priority-600: #7c3aed; + --priority-400: #a78bfa; + --priority-300: #c4b5fd; + /* Priority colors for todos */ - --priority-high: #dc3545; - --priority-medium: #ffc107; - --priority-low: #28a745; + --priority-high: var(--priority-600); + --priority-medium: var(--priority-400); + --priority-low: var(--priority-300); /* Status colors */ --status-in-progress: #fff3cd; @@ -1901,10 +1906,15 @@ --answer-accent: #4caf50; --answer-bg: #f0fff4; + /* Priority palette (purple intensity - darker = more urgent) */ + --priority-600: #7c3aed; + --priority-400: #a78bfa; + --priority-300: #c4b5fd; + /* Priority colors for todos */ - --priority-high: #dc3545; - --priority-medium: #ffc107; - --priority-low: #28a745; + --priority-high: var(--priority-600); + --priority-medium: var(--priority-400); + --priority-low: var(--priority-300); /* Status colors */ --status-in-progress: #fff3cd; @@ -2263,7 +2273,8 @@ .user:not(.compacted), .system, .bash-input, - .bash-output { + .bash-output, + .system:not(.system-info):not(.system-warning):not(.system-error) { margin-left: 33%; margin-right: 0; } @@ -2289,17 +2300,11 @@ margin-right: 6em; } - /* System warnings/info (assistant-initiated) */ + /* System warnings/info at tool level (e.g., hook notifications) */ .system-warning, .system-info { - margin-left: 0; - margin-right: 10em; - } - - /* Exception: paired system-info messages align right (like user commands) */ - .system.system-info.paired-message { - margin-left: 33%; - margin-right: 0; + margin-left: 2em; + margin-right: 6em; } /* Sidechain messages (sub-assistant hierarchy) */ @@ -2422,6 +2427,10 @@ .system-info { border-left-color: var(--info-dimmed); background-color: var(--highlight-dimmed); + } + + .system-info .header, + .system-info .content { font-size: 80%; } @@ -3308,18 +3317,14 @@ background-color: var(--bg-hover); } - .todo-item.completed { - opacity: 0.7; - } - .todo-item.completed .todo-content { text-decoration: line-through; color: var(--text-muted); + opacity: 0.7; } - .todo-item input[type="checkbox"] { - margin: 0; - cursor: default; + .todo-item.pending .todo-content { + font-weight: normal; } .todo-status { @@ -6632,10 +6637,15 @@ --answer-accent: #4caf50; --answer-bg: #f0fff4; + /* Priority palette (purple intensity - darker = more urgent) */ + --priority-600: #7c3aed; + --priority-400: #a78bfa; + --priority-300: #c4b5fd; + /* Priority colors for todos */ - --priority-high: #dc3545; - --priority-medium: #ffc107; - --priority-low: #28a745; + --priority-high: var(--priority-600); + --priority-medium: var(--priority-400); + --priority-low: var(--priority-300); /* Status colors */ --status-in-progress: #fff3cd; @@ -6994,7 +7004,8 @@ .user:not(.compacted), .system, .bash-input, - .bash-output { + .bash-output, + .system:not(.system-info):not(.system-warning):not(.system-error) { margin-left: 33%; margin-right: 0; } @@ -7020,17 +7031,11 @@ margin-right: 6em; } - /* System warnings/info (assistant-initiated) */ + /* System warnings/info at tool level (e.g., hook notifications) */ .system-warning, .system-info { - margin-left: 0; - margin-right: 10em; - } - - /* Exception: paired system-info messages align right (like user commands) */ - .system.system-info.paired-message { - margin-left: 33%; - margin-right: 0; + margin-left: 2em; + margin-right: 6em; } /* Sidechain messages (sub-assistant hierarchy) */ @@ -7153,6 +7158,10 @@ .system-info { border-left-color: var(--info-dimmed); background-color: var(--highlight-dimmed); + } + + .system-info .header, + .system-info .content { font-size: 80%; } @@ -8039,18 +8048,14 @@ background-color: var(--bg-hover); } - .todo-item.completed { - opacity: 0.7; - } - .todo-item.completed .todo-content { text-decoration: line-through; color: var(--text-muted); + opacity: 0.7; } - .todo-item input[type="checkbox"] { - margin: 0; - cursor: default; + .todo-item.pending .todo-content { + font-weight: normal; } .todo-status { @@ -9905,34 +9910,29 @@
- ⏳ broken_todo
- πŸ”„ Implement core functionality #2
- ⏳ Add comprehensive tests #3
- ⏳ Write user documentation #4
- ⏳ Perform code review #5 @@ -11470,10 +11470,15 @@ --answer-accent: #4caf50; --answer-bg: #f0fff4; + /* Priority palette (purple intensity - darker = more urgent) */ + --priority-600: #7c3aed; + --priority-400: #a78bfa; + --priority-300: #c4b5fd; + /* Priority colors for todos */ - --priority-high: #dc3545; - --priority-medium: #ffc107; - --priority-low: #28a745; + --priority-high: var(--priority-600); + --priority-medium: var(--priority-400); + --priority-low: var(--priority-300); /* Status colors */ --status-in-progress: #fff3cd; @@ -11832,7 +11837,8 @@ .user:not(.compacted), .system, .bash-input, - .bash-output { + .bash-output, + .system:not(.system-info):not(.system-warning):not(.system-error) { margin-left: 33%; margin-right: 0; } @@ -11858,17 +11864,11 @@ margin-right: 6em; } - /* System warnings/info (assistant-initiated) */ + /* System warnings/info at tool level (e.g., hook notifications) */ .system-warning, .system-info { - margin-left: 0; - margin-right: 10em; - } - - /* Exception: paired system-info messages align right (like user commands) */ - .system.system-info.paired-message { - margin-left: 33%; - margin-right: 0; + margin-left: 2em; + margin-right: 6em; } /* Sidechain messages (sub-assistant hierarchy) */ @@ -11991,6 +11991,10 @@ .system-info { border-left-color: var(--info-dimmed); background-color: var(--highlight-dimmed); + } + + .system-info .header, + .system-info .content { font-size: 80%; } @@ -12877,18 +12881,14 @@ background-color: var(--bg-hover); } - .todo-item.completed { - opacity: 0.7; - } - .todo-item.completed .todo-content { text-decoration: line-through; color: var(--text-muted); + opacity: 0.7; } - .todo-item input[type="checkbox"] { - margin: 0; - cursor: default; + .todo-item.pending .todo-content { + font-weight: normal; } .todo-status { @@ -16339,10 +16339,15 @@ --answer-accent: #4caf50; --answer-bg: #f0fff4; + /* Priority palette (purple intensity - darker = more urgent) */ + --priority-600: #7c3aed; + --priority-400: #a78bfa; + --priority-300: #c4b5fd; + /* Priority colors for todos */ - --priority-high: #dc3545; - --priority-medium: #ffc107; - --priority-low: #28a745; + --priority-high: var(--priority-600); + --priority-medium: var(--priority-400); + --priority-low: var(--priority-300); /* Status colors */ --status-in-progress: #fff3cd; @@ -16701,7 +16706,8 @@ .user:not(.compacted), .system, .bash-input, - .bash-output { + .bash-output, + .system:not(.system-info):not(.system-warning):not(.system-error) { margin-left: 33%; margin-right: 0; } @@ -16727,17 +16733,11 @@ margin-right: 6em; } - /* System warnings/info (assistant-initiated) */ + /* System warnings/info at tool level (e.g., hook notifications) */ .system-warning, .system-info { - margin-left: 0; - margin-right: 10em; - } - - /* Exception: paired system-info messages align right (like user commands) */ - .system.system-info.paired-message { - margin-left: 33%; - margin-right: 0; + margin-left: 2em; + margin-right: 6em; } /* Sidechain messages (sub-assistant hierarchy) */ @@ -16860,6 +16860,10 @@ .system-info { border-left-color: var(--info-dimmed); background-color: var(--highlight-dimmed); + } + + .system-info .header, + .system-info .content { font-size: 80%; } @@ -17746,18 +17750,14 @@ background-color: var(--bg-hover); } - .todo-item.completed { - opacity: 0.7; - } - .todo-item.completed .todo-content { text-decoration: line-through; color: var(--text-muted); + opacity: 0.7; } - .todo-item input[type="checkbox"] { - margin: 0; - cursor: default; + .todo-item.pending .todo-content { + font-weight: normal; } .todo-status { From fd5c9b80a4ccbaa613796db6a780d11b61f7dc34 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 19:52:23 +0100 Subject: [PATCH 07/15] fixup-54302b6c --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 845c5744..eb7cde4c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,4 +5,4 @@ "docs/claude-code-log-transcript.html": true, "test/test_data/cache/**": true, }, -} +} \ No newline at end of file From 7bd3789dc79368686156688856a00389f51d4403 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 15:40:12 +0100 Subject: [PATCH 08/15] Keep paired messages together when folding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When showing immediate children during fold operations, also show pair_last messages whose pair_first partner is visible. This ensures paired messages like system command + command output stay together when folding to partial levels. The fix tracks pair_first message IDs when showing immediate children, then queries for matching pair_last messages and shows them too. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/templates/transcript.html | 37 ++++++ test/__snapshots__/test_snapshot_html.ambr | 148 +++++++++++++++++++++ 2 files changed, 185 insertions(+) diff --git a/claude_code_log/templates/transcript.html b/claude_code_log/templates/transcript.html index e01e2300..9fab5227 100644 --- a/claude_code_log/templates/transcript.html +++ b/claude_code_log/templates/transcript.html @@ -503,9 +503,18 @@

πŸ” Search & Filter

if (isFolded) { // Unfold: show immediate children + // Track pair_first messages we've shown so we can show their pair_last partners + const shownPairFirstIds = []; + immediateChildren.forEach(msg => { msg.style.display = ''; + // Track pair_first messages for later pair_last matching + if (msg.classList.contains('pair_first')) { + const msgId = msg.getAttribute('data-message-id'); + if (msgId) shownPairFirstIds.push(msgId); + } + // Set newly revealed children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -524,6 +533,16 @@

πŸ” Search & Filter

} } }); + + // Show pair_last messages whose pair_first partner is shown + // This keeps paired messages (like command + output) together + shownPairFirstIds.forEach(pairFirstId => { + const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); + pairLastMessages.forEach(pairLast => { + pairLast.style.display = ''; + }); + }); + sectionElement.classList.remove('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Ό'; updateTooltip(sectionElement); @@ -592,6 +611,9 @@

πŸ” Search & Filter

} } else { // Fold: show first level only, hide deeper descendants (State C β†’ State B) + // Track pair_first messages we've shown so we can show their pair_last partners + const shownPairFirstIds = []; + descendants.forEach(msg => { const classList = Array.from(msg.classList); const ancestorIds = classList.filter(cls => cls.startsWith('d-') || cls.startsWith('session-')); @@ -601,6 +623,12 @@

πŸ” Search & Filter

if (lastAncestor === targetId) { msg.style.display = ''; // Show immediate child + // Track pair_first messages for later pair_last matching + if (msg.classList.contains('pair_first')) { + const msgId = msg.getAttribute('data-message-id'); + if (msgId) shownPairFirstIds.push(msgId); + } + // Set immediate children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -622,6 +650,15 @@

πŸ” Search & Filter

msg.style.display = 'none'; // Hide deeper descendants } }); + + // Show pair_last messages whose pair_first partner is shown + // This keeps paired messages (like command + output) together + shownPairFirstIds.forEach(pairFirstId => { + const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); + pairLastMessages.forEach(pairLast => { + pairLast.style.display = ''; + }); + }); sectionElement.classList.add('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Άβ–Ά'; updateTooltip(sectionElement); diff --git a/test/__snapshots__/test_snapshot_html.ambr b/test/__snapshots__/test_snapshot_html.ambr index 61e53b8a..d06da2fc 100644 --- a/test/__snapshots__/test_snapshot_html.ambr +++ b/test/__snapshots__/test_snapshot_html.ambr @@ -5580,9 +5580,18 @@ if (isFolded) { // Unfold: show immediate children + // Track pair_first messages we've shown so we can show their pair_last partners + const shownPairFirstIds = []; + immediateChildren.forEach(msg => { msg.style.display = ''; + // Track pair_first messages for later pair_last matching + if (msg.classList.contains('pair_first')) { + const msgId = msg.getAttribute('data-message-id'); + if (msgId) shownPairFirstIds.push(msgId); + } + // Set newly revealed children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -5601,6 +5610,16 @@ } } }); + + // Show pair_last messages whose pair_first partner is shown + // This keeps paired messages (like command + output) together + shownPairFirstIds.forEach(pairFirstId => { + const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); + pairLastMessages.forEach(pairLast => { + pairLast.style.display = ''; + }); + }); + sectionElement.classList.remove('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Ό'; updateTooltip(sectionElement); @@ -5669,6 +5688,9 @@ } } else { // Fold: show first level only, hide deeper descendants (State C β†’ State B) + // Track pair_first messages we've shown so we can show their pair_last partners + const shownPairFirstIds = []; + descendants.forEach(msg => { const classList = Array.from(msg.classList); const ancestorIds = classList.filter(cls => cls.startsWith('d-') || cls.startsWith('session-')); @@ -5678,6 +5700,12 @@ if (lastAncestor === targetId) { msg.style.display = ''; // Show immediate child + // Track pair_first messages for later pair_last matching + if (msg.classList.contains('pair_first')) { + const msgId = msg.getAttribute('data-message-id'); + if (msgId) shownPairFirstIds.push(msgId); + } + // Set immediate children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -5699,6 +5727,15 @@ msg.style.display = 'none'; // Hide deeper descendants } }); + + // Show pair_last messages whose pair_first partner is shown + // This keeps paired messages (like command + output) together + shownPairFirstIds.forEach(pairFirstId => { + const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); + pairLastMessages.forEach(pairLast => { + pairLast.style.display = ''; + }); + }); sectionElement.classList.add('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Άβ–Ά'; updateTooltip(sectionElement); @@ -10413,9 +10450,18 @@ if (isFolded) { // Unfold: show immediate children + // Track pair_first messages we've shown so we can show their pair_last partners + const shownPairFirstIds = []; + immediateChildren.forEach(msg => { msg.style.display = ''; + // Track pair_first messages for later pair_last matching + if (msg.classList.contains('pair_first')) { + const msgId = msg.getAttribute('data-message-id'); + if (msgId) shownPairFirstIds.push(msgId); + } + // Set newly revealed children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -10434,6 +10480,16 @@ } } }); + + // Show pair_last messages whose pair_first partner is shown + // This keeps paired messages (like command + output) together + shownPairFirstIds.forEach(pairFirstId => { + const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); + pairLastMessages.forEach(pairLast => { + pairLast.style.display = ''; + }); + }); + sectionElement.classList.remove('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Ό'; updateTooltip(sectionElement); @@ -10502,6 +10558,9 @@ } } else { // Fold: show first level only, hide deeper descendants (State C β†’ State B) + // Track pair_first messages we've shown so we can show their pair_last partners + const shownPairFirstIds = []; + descendants.forEach(msg => { const classList = Array.from(msg.classList); const ancestorIds = classList.filter(cls => cls.startsWith('d-') || cls.startsWith('session-')); @@ -10511,6 +10570,12 @@ if (lastAncestor === targetId) { msg.style.display = ''; // Show immediate child + // Track pair_first messages for later pair_last matching + if (msg.classList.contains('pair_first')) { + const msgId = msg.getAttribute('data-message-id'); + if (msgId) shownPairFirstIds.push(msgId); + } + // Set immediate children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -10532,6 +10597,15 @@ msg.style.display = 'none'; // Hide deeper descendants } }); + + // Show pair_last messages whose pair_first partner is shown + // This keeps paired messages (like command + output) together + shownPairFirstIds.forEach(pairFirstId => { + const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); + pairLastMessages.forEach(pairLast => { + pairLast.style.display = ''; + }); + }); sectionElement.classList.add('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Άβ–Ά'; updateTooltip(sectionElement); @@ -15282,9 +15356,18 @@ if (isFolded) { // Unfold: show immediate children + // Track pair_first messages we've shown so we can show their pair_last partners + const shownPairFirstIds = []; + immediateChildren.forEach(msg => { msg.style.display = ''; + // Track pair_first messages for later pair_last matching + if (msg.classList.contains('pair_first')) { + const msgId = msg.getAttribute('data-message-id'); + if (msgId) shownPairFirstIds.push(msgId); + } + // Set newly revealed children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -15303,6 +15386,16 @@ } } }); + + // Show pair_last messages whose pair_first partner is shown + // This keeps paired messages (like command + output) together + shownPairFirstIds.forEach(pairFirstId => { + const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); + pairLastMessages.forEach(pairLast => { + pairLast.style.display = ''; + }); + }); + sectionElement.classList.remove('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Ό'; updateTooltip(sectionElement); @@ -15371,6 +15464,9 @@ } } else { // Fold: show first level only, hide deeper descendants (State C β†’ State B) + // Track pair_first messages we've shown so we can show their pair_last partners + const shownPairFirstIds = []; + descendants.forEach(msg => { const classList = Array.from(msg.classList); const ancestorIds = classList.filter(cls => cls.startsWith('d-') || cls.startsWith('session-')); @@ -15380,6 +15476,12 @@ if (lastAncestor === targetId) { msg.style.display = ''; // Show immediate child + // Track pair_first messages for later pair_last matching + if (msg.classList.contains('pair_first')) { + const msgId = msg.getAttribute('data-message-id'); + if (msgId) shownPairFirstIds.push(msgId); + } + // Set immediate children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -15401,6 +15503,15 @@ msg.style.display = 'none'; // Hide deeper descendants } }); + + // Show pair_last messages whose pair_first partner is shown + // This keeps paired messages (like command + output) together + shownPairFirstIds.forEach(pairFirstId => { + const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); + pairLastMessages.forEach(pairLast => { + pairLast.style.display = ''; + }); + }); sectionElement.classList.add('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Άβ–Ά'; updateTooltip(sectionElement); @@ -20013,9 +20124,18 @@ if (isFolded) { // Unfold: show immediate children + // Track pair_first messages we've shown so we can show their pair_last partners + const shownPairFirstIds = []; + immediateChildren.forEach(msg => { msg.style.display = ''; + // Track pair_first messages for later pair_last matching + if (msg.classList.contains('pair_first')) { + const msgId = msg.getAttribute('data-message-id'); + if (msgId) shownPairFirstIds.push(msgId); + } + // Set newly revealed children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -20034,6 +20154,16 @@ } } }); + + // Show pair_last messages whose pair_first partner is shown + // This keeps paired messages (like command + output) together + shownPairFirstIds.forEach(pairFirstId => { + const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); + pairLastMessages.forEach(pairLast => { + pairLast.style.display = ''; + }); + }); + sectionElement.classList.remove('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Ό'; updateTooltip(sectionElement); @@ -20102,6 +20232,9 @@ } } else { // Fold: show first level only, hide deeper descendants (State C β†’ State B) + // Track pair_first messages we've shown so we can show their pair_last partners + const shownPairFirstIds = []; + descendants.forEach(msg => { const classList = Array.from(msg.classList); const ancestorIds = classList.filter(cls => cls.startsWith('d-') || cls.startsWith('session-')); @@ -20111,6 +20244,12 @@ if (lastAncestor === targetId) { msg.style.display = ''; // Show immediate child + // Track pair_first messages for later pair_last matching + if (msg.classList.contains('pair_first')) { + const msgId = msg.getAttribute('data-message-id'); + if (msgId) shownPairFirstIds.push(msgId); + } + // Set immediate children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -20132,6 +20271,15 @@ msg.style.display = 'none'; // Hide deeper descendants } }); + + // Show pair_last messages whose pair_first partner is shown + // This keeps paired messages (like command + output) together + shownPairFirstIds.forEach(pairFirstId => { + const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); + pairLastMessages.forEach(pairLast => { + pairLast.style.display = ''; + }); + }); sectionElement.classList.add('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Άβ–Ά'; updateTooltip(sectionElement); From 1a4b0fa0d98fe1a645bc03d29fd5077cba4619d7 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 15:51:42 +0100 Subject: [PATCH 09/15] Revert "Keep paired messages together when folding" This reverts commit 37a61a963ab1b2ba71e639f284a816fb55310ab2. --- claude_code_log/templates/transcript.html | 37 ------ test/__snapshots__/test_snapshot_html.ambr | 148 --------------------- 2 files changed, 185 deletions(-) diff --git a/claude_code_log/templates/transcript.html b/claude_code_log/templates/transcript.html index 9fab5227..e01e2300 100644 --- a/claude_code_log/templates/transcript.html +++ b/claude_code_log/templates/transcript.html @@ -503,18 +503,9 @@

πŸ” Search & Filter

if (isFolded) { // Unfold: show immediate children - // Track pair_first messages we've shown so we can show their pair_last partners - const shownPairFirstIds = []; - immediateChildren.forEach(msg => { msg.style.display = ''; - // Track pair_first messages for later pair_last matching - if (msg.classList.contains('pair_first')) { - const msgId = msg.getAttribute('data-message-id'); - if (msgId) shownPairFirstIds.push(msgId); - } - // Set newly revealed children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -533,16 +524,6 @@

πŸ” Search & Filter

} } }); - - // Show pair_last messages whose pair_first partner is shown - // This keeps paired messages (like command + output) together - shownPairFirstIds.forEach(pairFirstId => { - const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); - pairLastMessages.forEach(pairLast => { - pairLast.style.display = ''; - }); - }); - sectionElement.classList.remove('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Ό'; updateTooltip(sectionElement); @@ -611,9 +592,6 @@

πŸ” Search & Filter

} } else { // Fold: show first level only, hide deeper descendants (State C β†’ State B) - // Track pair_first messages we've shown so we can show their pair_last partners - const shownPairFirstIds = []; - descendants.forEach(msg => { const classList = Array.from(msg.classList); const ancestorIds = classList.filter(cls => cls.startsWith('d-') || cls.startsWith('session-')); @@ -623,12 +601,6 @@

πŸ” Search & Filter

if (lastAncestor === targetId) { msg.style.display = ''; // Show immediate child - // Track pair_first messages for later pair_last matching - if (msg.classList.contains('pair_first')) { - const msgId = msg.getAttribute('data-message-id'); - if (msgId) shownPairFirstIds.push(msgId); - } - // Set immediate children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -650,15 +622,6 @@

πŸ” Search & Filter

msg.style.display = 'none'; // Hide deeper descendants } }); - - // Show pair_last messages whose pair_first partner is shown - // This keeps paired messages (like command + output) together - shownPairFirstIds.forEach(pairFirstId => { - const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); - pairLastMessages.forEach(pairLast => { - pairLast.style.display = ''; - }); - }); sectionElement.classList.add('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Άβ–Ά'; updateTooltip(sectionElement); diff --git a/test/__snapshots__/test_snapshot_html.ambr b/test/__snapshots__/test_snapshot_html.ambr index d06da2fc..61e53b8a 100644 --- a/test/__snapshots__/test_snapshot_html.ambr +++ b/test/__snapshots__/test_snapshot_html.ambr @@ -5580,18 +5580,9 @@ if (isFolded) { // Unfold: show immediate children - // Track pair_first messages we've shown so we can show their pair_last partners - const shownPairFirstIds = []; - immediateChildren.forEach(msg => { msg.style.display = ''; - // Track pair_first messages for later pair_last matching - if (msg.classList.contains('pair_first')) { - const msgId = msg.getAttribute('data-message-id'); - if (msgId) shownPairFirstIds.push(msgId); - } - // Set newly revealed children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -5610,16 +5601,6 @@ } } }); - - // Show pair_last messages whose pair_first partner is shown - // This keeps paired messages (like command + output) together - shownPairFirstIds.forEach(pairFirstId => { - const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); - pairLastMessages.forEach(pairLast => { - pairLast.style.display = ''; - }); - }); - sectionElement.classList.remove('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Ό'; updateTooltip(sectionElement); @@ -5688,9 +5669,6 @@ } } else { // Fold: show first level only, hide deeper descendants (State C β†’ State B) - // Track pair_first messages we've shown so we can show their pair_last partners - const shownPairFirstIds = []; - descendants.forEach(msg => { const classList = Array.from(msg.classList); const ancestorIds = classList.filter(cls => cls.startsWith('d-') || cls.startsWith('session-')); @@ -5700,12 +5678,6 @@ if (lastAncestor === targetId) { msg.style.display = ''; // Show immediate child - // Track pair_first messages for later pair_last matching - if (msg.classList.contains('pair_first')) { - const msgId = msg.getAttribute('data-message-id'); - if (msgId) shownPairFirstIds.push(msgId); - } - // Set immediate children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -5727,15 +5699,6 @@ msg.style.display = 'none'; // Hide deeper descendants } }); - - // Show pair_last messages whose pair_first partner is shown - // This keeps paired messages (like command + output) together - shownPairFirstIds.forEach(pairFirstId => { - const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); - pairLastMessages.forEach(pairLast => { - pairLast.style.display = ''; - }); - }); sectionElement.classList.add('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Άβ–Ά'; updateTooltip(sectionElement); @@ -10450,18 +10413,9 @@ if (isFolded) { // Unfold: show immediate children - // Track pair_first messages we've shown so we can show their pair_last partners - const shownPairFirstIds = []; - immediateChildren.forEach(msg => { msg.style.display = ''; - // Track pair_first messages for later pair_last matching - if (msg.classList.contains('pair_first')) { - const msgId = msg.getAttribute('data-message-id'); - if (msgId) shownPairFirstIds.push(msgId); - } - // Set newly revealed children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -10480,16 +10434,6 @@ } } }); - - // Show pair_last messages whose pair_first partner is shown - // This keeps paired messages (like command + output) together - shownPairFirstIds.forEach(pairFirstId => { - const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); - pairLastMessages.forEach(pairLast => { - pairLast.style.display = ''; - }); - }); - sectionElement.classList.remove('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Ό'; updateTooltip(sectionElement); @@ -10558,9 +10502,6 @@ } } else { // Fold: show first level only, hide deeper descendants (State C β†’ State B) - // Track pair_first messages we've shown so we can show their pair_last partners - const shownPairFirstIds = []; - descendants.forEach(msg => { const classList = Array.from(msg.classList); const ancestorIds = classList.filter(cls => cls.startsWith('d-') || cls.startsWith('session-')); @@ -10570,12 +10511,6 @@ if (lastAncestor === targetId) { msg.style.display = ''; // Show immediate child - // Track pair_first messages for later pair_last matching - if (msg.classList.contains('pair_first')) { - const msgId = msg.getAttribute('data-message-id'); - if (msgId) shownPairFirstIds.push(msgId); - } - // Set immediate children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -10597,15 +10532,6 @@ msg.style.display = 'none'; // Hide deeper descendants } }); - - // Show pair_last messages whose pair_first partner is shown - // This keeps paired messages (like command + output) together - shownPairFirstIds.forEach(pairFirstId => { - const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); - pairLastMessages.forEach(pairLast => { - pairLast.style.display = ''; - }); - }); sectionElement.classList.add('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Άβ–Ά'; updateTooltip(sectionElement); @@ -15356,18 +15282,9 @@ if (isFolded) { // Unfold: show immediate children - // Track pair_first messages we've shown so we can show their pair_last partners - const shownPairFirstIds = []; - immediateChildren.forEach(msg => { msg.style.display = ''; - // Track pair_first messages for later pair_last matching - if (msg.classList.contains('pair_first')) { - const msgId = msg.getAttribute('data-message-id'); - if (msgId) shownPairFirstIds.push(msgId); - } - // Set newly revealed children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -15386,16 +15303,6 @@ } } }); - - // Show pair_last messages whose pair_first partner is shown - // This keeps paired messages (like command + output) together - shownPairFirstIds.forEach(pairFirstId => { - const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); - pairLastMessages.forEach(pairLast => { - pairLast.style.display = ''; - }); - }); - sectionElement.classList.remove('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Ό'; updateTooltip(sectionElement); @@ -15464,9 +15371,6 @@ } } else { // Fold: show first level only, hide deeper descendants (State C β†’ State B) - // Track pair_first messages we've shown so we can show their pair_last partners - const shownPairFirstIds = []; - descendants.forEach(msg => { const classList = Array.from(msg.classList); const ancestorIds = classList.filter(cls => cls.startsWith('d-') || cls.startsWith('session-')); @@ -15476,12 +15380,6 @@ if (lastAncestor === targetId) { msg.style.display = ''; // Show immediate child - // Track pair_first messages for later pair_last matching - if (msg.classList.contains('pair_first')) { - const msgId = msg.getAttribute('data-message-id'); - if (msgId) shownPairFirstIds.push(msgId); - } - // Set immediate children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -15503,15 +15401,6 @@ msg.style.display = 'none'; // Hide deeper descendants } }); - - // Show pair_last messages whose pair_first partner is shown - // This keeps paired messages (like command + output) together - shownPairFirstIds.forEach(pairFirstId => { - const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); - pairLastMessages.forEach(pairLast => { - pairLast.style.display = ''; - }); - }); sectionElement.classList.add('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Άβ–Ά'; updateTooltip(sectionElement); @@ -20124,18 +20013,9 @@ if (isFolded) { // Unfold: show immediate children - // Track pair_first messages we've shown so we can show their pair_last partners - const shownPairFirstIds = []; - immediateChildren.forEach(msg => { msg.style.display = ''; - // Track pair_first messages for later pair_last matching - if (msg.classList.contains('pair_first')) { - const msgId = msg.getAttribute('data-message-id'); - if (msgId) shownPairFirstIds.push(msgId); - } - // Set newly revealed children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -20154,16 +20034,6 @@ } } }); - - // Show pair_last messages whose pair_first partner is shown - // This keeps paired messages (like command + output) together - shownPairFirstIds.forEach(pairFirstId => { - const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); - pairLastMessages.forEach(pairLast => { - pairLast.style.display = ''; - }); - }); - sectionElement.classList.remove('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Ό'; updateTooltip(sectionElement); @@ -20232,9 +20102,6 @@ } } else { // Fold: show first level only, hide deeper descendants (State C β†’ State B) - // Track pair_first messages we've shown so we can show their pair_last partners - const shownPairFirstIds = []; - descendants.forEach(msg => { const classList = Array.from(msg.classList); const ancestorIds = classList.filter(cls => cls.startsWith('d-') || cls.startsWith('session-')); @@ -20244,12 +20111,6 @@ if (lastAncestor === targetId) { msg.style.display = ''; // Show immediate child - // Track pair_first messages for later pair_last matching - if (msg.classList.contains('pair_first')) { - const msgId = msg.getAttribute('data-message-id'); - if (msgId) shownPairFirstIds.push(msgId); - } - // Set immediate children to folded state const foldBar = msg.querySelector('.fold-bar'); if (foldBar) { @@ -20271,15 +20132,6 @@ msg.style.display = 'none'; // Hide deeper descendants } }); - - // Show pair_last messages whose pair_first partner is shown - // This keeps paired messages (like command + output) together - shownPairFirstIds.forEach(pairFirstId => { - const pairLastMessages = document.querySelectorAll(`.message.pair_last.${pairFirstId}`); - pairLastMessages.forEach(pairLast => { - pairLast.style.display = ''; - }); - }); sectionElement.classList.add('folded'); sectionElement.querySelector('.fold-icon').textContent = 'β–Άβ–Ά'; updateTooltip(sectionElement); From 3bd63d73a914bdfb2604a6a8a8fe2e9586039113 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 15:57:51 +0100 Subject: [PATCH 10/15] Fix command output nesting in hierarchy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Command output messages (like /context results) were incorrectly nesting under their parent command message because their parentUuid pointed to a valid (non-skipped) message. This caused the command output to appear at level 3 while the command was at level 2, breaking fold behavior. The fix excludes command-output from parentUuid-based hierarchy nesting, keeping it as a sibling at the same level as the command message. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/renderer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/claude_code_log/renderer.py b/claude_code_log/renderer.py index 4b2048d3..fd83c496 100644 --- a/claude_code_log/renderer.py +++ b/claude_code_log/renderer.py @@ -2919,6 +2919,8 @@ def _build_message_hierarchy(messages: List[TemplateMessage]) -> None: and message.parent_uuid in uuid_to_info and "system" in message.css_class # Only system messages nest via parent_uuid + and "command-output" + not in message.css_class # But NOT command output - it's a sibling ): # System message with known parent - nest under parent _, parent_level = uuid_to_info[message.parent_uuid] From e745ce04ffa0b8c141ad67f161dc97a1608ce9cc Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 16:21:27 +0100 Subject: [PATCH 11/15] Remove parent_uuid-based hierarchy nesting for system messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instrumentation revealed that the parent_uuid nesting logic was only triggered for system-info messages, and in 97% of cases it produced unwanted deep nesting (levels 4-6 instead of the expected level 3). Analysis: - 124 instances triggered during full test suite - 100% were system-info messages - 116 cases (94%): parent_level=3 β†’ produced level 4 (wrong) - 4 cases (3%): parent_level=2 β†’ produced level 3 (coincidentally correct) - 4 cases (3%): parent_level=4,5 β†’ produced levels 5,6 (way too deep) Since all message types now have well-defined levels via _get_message_hierarchy_level(), the parent_uuid-based nesting adds complexity without benefit. This simplifies the hierarchy logic to always use CSS class-based level determination. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/renderer.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/claude_code_log/renderer.py b/claude_code_log/renderer.py index fd83c496..54f1dd49 100644 --- a/claude_code_log/renderer.py +++ b/claude_code_log/renderer.py @@ -2897,34 +2897,16 @@ def _build_message_hierarchy(messages: List[TemplateMessage]) -> None: The hierarchy is determined by message type using _get_message_hierarchy_level(), and a stack-based approach builds proper parent-child relationships. - For system messages with parent_uuid, the hierarchy level is derived from the - parent's level instead of the default, ensuring proper nesting. - Args: messages: List of template messages in their final order (modified in place) """ hierarchy_stack: List[tuple[int, str]] = [] message_id_counter = 0 - # Build UUID -> (message_id, level) mapping for parent_uuid resolution - # We do this in a single pass by updating the map as we assign IDs - uuid_to_info: Dict[str, tuple[str, int]] = {} - for message in messages: # Session headers are level 0 if message.is_session_header: current_level = 0 - elif ( - message.parent_uuid - and message.parent_uuid in uuid_to_info - and "system" - in message.css_class # Only system messages nest via parent_uuid - and "command-output" - not in message.css_class # But NOT command output - it's a sibling - ): - # System message with known parent - nest under parent - _, parent_level = uuid_to_info[message.parent_uuid] - current_level = parent_level + 1 else: # Determine level from css_class is_sidechain = "sidechain" in message.css_class @@ -2950,10 +2932,6 @@ def _build_message_hierarchy(messages: List[TemplateMessage]) -> None: # Push current message onto stack hierarchy_stack.append((current_level, message_id)) - # Track UUID -> (message_id, level) for parent_uuid resolution - if message.uuid: - uuid_to_info[message.uuid] = (message_id, current_level) - # Update the message message.message_id = message_id message.ancestry = ancestry From 37f9d75b1bdb98ab360f5de06e7fc517ff2fea59 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 6 Dec 2025 16:24:58 +0100 Subject: [PATCH 12/15] Update README: mark completed TODOs and add Textual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove completed TODOs: Markdown renderer, compacted conversation handling, thinking block rendering, system block formatting - Update tool formatting TODO: remove Bash, Read, Edit, Write, TodoWrite, exit_plan_mode (now have special rendering) - Add Textual to Development dependencies section - Fix trailing whitespace πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cca9bf10..9ce42066 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ claude-code-log --all-projects # Process all projects and open in browser claude-code-log --open-browser -# Process all projects with date filtering +# Process all projects with date filtering claude-code-log --from-date "yesterday" --to-date "today" claude-code-log --from-date "last week" @@ -154,6 +154,7 @@ The project uses: - Python 3.10+ with uv package management - Click for CLI interface and argument parsing +- Textual for interactive Terminal User Interface - Pydantic for robust data modeling and validation - dateparser for natural language date parsing - Standard library for JSON/HTML processing @@ -377,17 +378,13 @@ uv run claude-code-log - tutorial overlay - integrate `claude-trace` request logs if present? - convert images to WebP as screenshots are often huge PNGs – this might be time consuming to keep redoing (so would also need some caching) and need heavy dependencies with compilation (unless there are fast pure Python conversation libraries? Or WASM?) -- add special formatting for built-in tools: Bash, Glob, Grep, LS, exit_plan_mode, Read, Edit, MultiEdit, Write, NotebookRead, NotebookEdit, WebFetch, TodoRead, TodoWrite, WebSearch -- do we need to handle compacted conversation? -- Thinking block should have Markdown rendering as sometimes they have formatting -- system blocks like `init` also don't render perfectly, losing new lines +- add special formatting for built-in tools: Glob, Grep, LS, MultiEdit, NotebookRead, NotebookEdit, WebFetch, TodoRead, WebSearch - add `ccusage` like daily summary and maybe some textual summary too based on Claude generate session summaries? – import logs from @claude Github Actions - stream logs from @claude Github Actions, see [octotail](https://github.com/getbettr/octotail) - wrap up CLI as Github Action to run after Cladue Github Action and process [output](https://github.com/anthropics/claude-code-base-action?tab=readme-ov-file#outputs) - feed the filtered user messages to headless claude CLI to distill the user intent from the session - filter message type on Python (CLI) side too, not just UI -- Markdown renderer - add minimalist theme and make it light + dark; animate gradient background in fancy theme - do we need special handling for hooks? - make processing parallel, currently we only use 1 CPU (core) and it's slow From 7ad378f2e081541a7f357d76bce3b70258b2a501 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sun, 7 Dec 2025 02:06:01 +0100 Subject: [PATCH 13/15] Fix XSS vulnerabilities in TodoWrite rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sanitize status and priority values by converting to lowercase strings to prevent CSS class attribute injection. Also escape fallback content in the exception handler to prevent HTML injection. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/renderer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/claude_code_log/renderer.py b/claude_code_log/renderer.py index 54f1dd49..6ea537a0 100644 --- a/claude_code_log/renderer.py +++ b/claude_code_log/renderer.py @@ -586,8 +586,8 @@ def format_todowrite_content(tool_use: ToolUseContent) -> str: try: todo_id = escape_html(str(todo.get("id", ""))) content = escape_html(str(todo.get("content", ""))) - status = todo.get("status", "pending") - priority = todo.get("priority", "medium") + status = str(todo.get("status", "pending")).lower() + priority = str(todo.get("priority", "medium")).lower() status_emoji = status_emojis.get(status, "⏳") # CSS class for styling @@ -601,10 +601,11 @@ def format_todowrite_content(tool_use: ToolUseContent) -> str:
""") except AttributeError: + escaped_fallback = escape_html(str(todo)) todo_items.append(f"""
⏳ - {str(todo)} + {escaped_fallback}
""") From b3ecf8b2ee088b7bf04bfb3b6f0a21868867153e Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sun, 7 Dec 2025 02:07:22 +0100 Subject: [PATCH 14/15] Fix duplicate .system selector from merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove incorrectly duplicated .system selector that was added during a previous merge, causing style conflicts. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/templates/components/message_styles.css | 1 - test/__snapshots__/test_snapshot_html.ambr | 4 ---- 2 files changed, 5 deletions(-) diff --git a/claude_code_log/templates/components/message_styles.css b/claude_code_log/templates/components/message_styles.css index 05095ed3..2bc67bf0 100644 --- a/claude_code_log/templates/components/message_styles.css +++ b/claude_code_log/templates/components/message_styles.css @@ -179,7 +179,6 @@ /* Right-aligned messages (user-initiated, right margin 0, left margin 33%) */ .user:not(.compacted), -.system, .bash-input, .bash-output, .system:not(.system-info):not(.system-warning):not(.system-error) { diff --git a/test/__snapshots__/test_snapshot_html.ambr b/test/__snapshots__/test_snapshot_html.ambr index 61e53b8a..b84f2ece 100644 --- a/test/__snapshots__/test_snapshot_html.ambr +++ b/test/__snapshots__/test_snapshot_html.ambr @@ -2271,7 +2271,6 @@ /* Right-aligned messages (user-initiated, right margin 0, left margin 33%) */ .user:not(.compacted), - .system, .bash-input, .bash-output, .system:not(.system-info):not(.system-warning):not(.system-error) { @@ -7002,7 +7001,6 @@ /* Right-aligned messages (user-initiated, right margin 0, left margin 33%) */ .user:not(.compacted), - .system, .bash-input, .bash-output, .system:not(.system-info):not(.system-warning):not(.system-error) { @@ -11835,7 +11833,6 @@ /* Right-aligned messages (user-initiated, right margin 0, left margin 33%) */ .user:not(.compacted), - .system, .bash-input, .bash-output, .system:not(.system-info):not(.system-warning):not(.system-error) { @@ -16704,7 +16701,6 @@ /* Right-aligned messages (user-initiated, right margin 0, left margin 33%) */ .user:not(.compacted), - .system, .bash-input, .bash-output, .system:not(.system-info):not(.system-warning):not(.system-error) { From 3d4da11a5bb6351d04d880ff60b41628aa22ec79 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sun, 7 Dec 2025 11:44:53 +0100 Subject: [PATCH 15/15] Enable DEBUG_TIMING in CI and justfile for benchmark tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable CLAUDE_CODE_LOG_DEBUG_TIMING=1 in justfile and CI for benchmark tests to cover renderer_timings.py code paths. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci.yml | 2 ++ justfile | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 841c385a..f12f6b9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,8 @@ jobs: - name: Run benchmark tests with coverage append (primary only) if: matrix.is-primary + env: + CLAUDE_CODE_LOG_DEBUG_TIMING: "1" run: uv run pytest -m benchmark --cov=claude_code_log --cov-append --cov-report=xml --cov-report=html --cov-report=term -v - name: Upload coverage HTML report as artifact diff --git a/justfile b/justfile index f72fb3ea..c54251ac 100644 --- a/justfile +++ b/justfile @@ -10,8 +10,9 @@ test: uv run pytest -n auto -m "not (tui or browser or benchmark)" -v # Run benchmark tests (outputs to GITHUB_STEP_SUMMARY in CI) +# DEBUG_TIMING enables coverage of renderer_timings.py test-benchmark: - uv run pytest -m benchmark -v + CLAUDE_CODE_LOG_DEBUG_TIMING=1 uv run pytest -m benchmark -v # Update snapshot tests update-snapshot: @@ -43,7 +44,7 @@ test-all: echo "πŸ”„ Running integration tests..." uv run pytest -n auto -m integration -v echo "πŸ“Š Running benchmark tests..." - uv run pytest -m benchmark -v + CLAUDE_CODE_LOG_DEBUG_TIMING=1 uv run pytest -m benchmark -v echo "βœ… All tests completed!" # Run tests with coverage (all categories) @@ -60,7 +61,7 @@ test-cov: echo "πŸ”„ Running integration tests with coverage append..." uv run pytest -n auto -m integration --cov=claude_code_log --cov-append --cov-report=xml --cov-report=html --cov-report=term -v echo "πŸ“Š Running benchmark tests with coverage append..." - uv run pytest -m benchmark --cov=claude_code_log --cov-append --cov-report=xml --cov-report=html --cov-report=term -v + CLAUDE_CODE_LOG_DEBUG_TIMING=1 uv run pytest -m benchmark --cov=claude_code_log --cov-append --cov-report=xml --cov-report=html --cov-report=term -v echo "βœ… All tests with coverage completed!" format: