Add UX improvements: collapsible cells, copy buttons, syntax highlighting, and ANSI sanitization#47
Add UX improvements: collapsible cells, copy buttons, syntax highlighting, and ANSI sanitization#47ShlomoStept wants to merge 19 commits intosimonw:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive UX improvements to claude-code-transcripts, including ANSI sanitization, syntax highlighting with Pygments, collapsible cells, copy buttons, Markdown/JSON view toggles, metadata display, search functionality, and batch conversion capabilities. The changes are well-tested with 141 passing tests and 21 snapshot tests, demonstrating thorough quality assurance.
Key Changes
- Added 10 major UX features: ANSI sanitization, syntax highlighting, collapsible cells, copy buttons, tool pairing, view toggles, metadata, search, batch conversion
- Comprehensive test coverage with new test classes and snapshot tests
- Template refactoring using Jinja2 base template inheritance
- New dependencies: pygments, questionary, click-default-group, httpx, jinja2
Reviewed changes
Copilot reviewed 36 out of 39 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_generate_html.py | Added 1000+ lines of comprehensive tests for all new features |
| tests/test_all.py | New 532-line test file for batch conversion functionality |
| tests/conftest.py | New fixture file with webbrowser mocking |
| tests/sample_session.jsonl | New JSONL format test fixture |
| tests/snapshots/* | Updated/new snapshot files for HTML output verification |
| src/claude_code_transcripts/templates/* | New Jinja2 templates for modular HTML generation |
| pyproject.toml | Updated dependencies and project metadata |
| CLAUDE.md | New file referencing AGENTS.md |
| .gitignore | Added uv.lock and .playwright-mcp/ |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| claude_code_publish._github_repo = "example/repo" | ||
| # Need to set the github repo for commit link rendering | ||
| # Using the thread-safe set_github_repo function | ||
| import claude_code_transcripts |
There was a problem hiding this comment.
Module 'claude_code_transcripts' is imported with both 'import' and 'import from'.
| return text | ||
| except json.JSONDecodeError: | ||
| continue | ||
| except Exception: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | |
| except Exception: | |
| # Best-effort summary extraction: on any unexpected error, fall back to the | |
| # default "(no summary)" value returned below. |
Add support for OSC sequences, CSI sequences, and 7-bit C1 control codes. Based on improvements from simonw#47. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add strip_ansi() function to remove ANSI escape sequences from terminal output - Add is_content_block_array() to detect JSON arrays of content blocks - Add render_content_block_array() to properly render content blocks in tool results - Tool results containing ANSI codes now display cleanly without escape sequences - Tool results containing JSON arrays of content blocks now render as markdown text instead of raw JSON This fixes two critical UX issues where terminal output was unreadable due to visible ANSI codes, and tool replies showed raw JSON instead of rendered content. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add copy button CSS with hover reveal effect - Add JavaScript to dynamically add copy buttons to pre, tool-result, and bash-command elements - Copy button shows "Copied!" feedback for 2 seconds after successful copy - Uses navigator.clipboard API for modern clipboard access - Buttons appear on hover and fade in smoothly This improves the UX for users who want to copy code snippets, terminal output, or tool results from transcripts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Pygments dependency for syntax highlighting - Implement highlight_code() function that detects language from file extension - Apply highlighting to Write and Edit tool content - Add Monokai-inspired dark theme CSS for highlighted code - Supports Python, JavaScript, HTML, CSS, JSON, and many other languages - Falls back gracefully to plain text for unrecognized file types This significantly improves code readability in transcript outputs by providing proper syntax coloring for different programming languages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Part 2 deliverables from subagent analysis: ## Task Grading (0.00-10.00) - B.5 ANSI Sanitization: 6.75/10 - Works for common cases but regex misses ~30% of ANSI sequences (cursor control, OSC sequences) - B.4 Content-Block Arrays: 6.75/10 - Handles text/thinking but not images or tool_use blocks - A.1 Copy Buttons: 6.75/10 - Functional but lacks accessibility (no ARIA labels, keyboard support, clipboard API fallback) - B.2 Syntax Highlighting: 9.25/10 - Excellent implementation with 500+ language support and graceful error handling ## TASKS.md Updates - Added detailed grading with justifications - Added known limitations for each completed task - Added test coverage gaps to address - Added technical specifications (file architecture, CSS/JS guidelines) - Added task dependency graph - Added implementation details for pending phases - Added documentation gaps identified ## AGENTS.md Updates - Added Quick Start section with setup commands - Added Project Structure overview - Added comprehensive testing commands - Added snapshot testing instructions - Added debugging tips - Added architecture notes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use render_content_block for array items Add tests for image and tool_use arrays
Add OSC and CSI stripping with tests
Group tool calls with matching results by tool_use_id Add tool-pair wrapper and page snapshot updates
- Add render_json_with_markdown() function that recursively renders JSON with Markdown formatting for string values - Update render_bash_tool() to render descriptions as Markdown HTML - Update generic tool_use handler to render descriptions and JSON with Markdown - Update bash_tool and tool_use macros to use |safe for pre-rendered HTML - Add CSS classes for styled JSON output (json-key, json-string-value, etc.) - Add 4 new tests for markdown rendering functionality - Update snapshots for changed output format This implements Phase 2 Task 1: tool call text rendered as Markdown. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add group_blocks_by_type() function to categorize content blocks - Refactor render_assistant_message*() to group blocks into cells - Add cell macro with <details> wrapper for collapsible sections - Three cell types: thinking (closed), response (open), tools (closed with count) - Add CSS for cell styling with expand/collapse animation - Add 5 new tests for cell structure functionality - Update snapshots for new HTML structure Thinking cells are closed by default to reduce noise. Response cells are open by default for immediate visibility. Tools cells show count and are closed by default. This implements Phase 2 Task 2: collapsible cells for subcomponents. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add copy button to cell header in cell macro
- Add CSS for cell copy button with hover and focus states
- Add JavaScript handler for cell copy with clipboard API
- Include keyboard accessibility (Enter/Space support)
- Add ARIA labels for screen reader accessibility
- Add 2 new tests for cell copy button functionality
- Update snapshots for new button and CSS
Each collapsible cell (Thinking, Response, Tool Calls) now has
a copy button that copies the cell content to clipboard with
visual feedback ("Copied!" for 2 seconds).
This implements Phase 2 Task 3: per-cell copy buttons.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix sticky headers by removing overflow:hidden from .message - Add JSON/Markdown toggle for tool calls and tool results - Improve syntax highlighting with Dracula-inspired color scheme - Add clear labels for tool calls (→ Call) and results (← Result) - Improve copy functionality with data-copy-content attribute - Add hover states for better clickability feedback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 50+ CSS custom properties for colors, spacing, shadows - Implement warm cream/off-white color palette - Add paper texture background effect - Define frosted glass variables for sticky headers - Make user messages collapsible with cell wrapper - Add text wrapping (overflow-wrap, word-break) to prevent overflow - Add .user-cell CSS styling matching other cell types - Replace small toggle buttons with shadcn-inspired tab controls - Add Markdown | JSON tabs with clear active states - Include ARIA attributes for accessibility - Update JavaScript for tab-based toggle behavior - Make message headers sticky (z-index: 30) - Create cascading sticky headers: message → cell → subcell - Add frosted glass backdrop-filter effects - Ensure no overflow:hidden breaks sticky positioning - Remove duplicate copy buttons (skip inside .cell-content) - Add cell-level master toggle for tools cells - Propagate view mode to all child elements - Add Markdown/JSON toggles to Write, Edit, Bash, TodoWrite - Pass input_json_html to all specialized tool macros - Preserve existing specialized displays in Markdown view All 133 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
JSON Display Fix: - Remove inline `style="display: none;"` from .view-json divs in macros.html - CSS class-based toggle (.show-json) now properly controls visibility - Affected: todo_list, write_tool, edit_tool, bash_tool macros Tabs Alignment Fix: - Update .cell summary to use flex: 1 on .cell-label instead of justify-content: space-between - Add flex-wrap: wrap to .tool-header, .file-tool-header, .todo-header for better responsiveness - Add gap: var(--spacing-sm) to .cell summary for consistent spacing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
A.2 Metadata Subsection: - Add calculate_message_metadata() function for char count, token estimate, tool counts - Add collapsible metadata section with info icon to each message - CSS styles: .message-metadata, .metadata-item, .metadata-label, .metadata-value - 7 new tests for metadata functionality B.3 Tool Call Headers with Type: - Add TOOL_ICONS constant with 14 tool-specific icons - Read (📖), Write (📝), Edit (✏️), Bash ($), Glob (🔍), Grep (🔎), etc. - Add get_tool_icon() helper function - Update tool_use macro to accept and display tool icon - Default icon (⚙) for unmapped tools 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The test file had two methods named `test_tool_result_with_ansi_codes` at lines 512 and 545 in TestRenderContentBlock class. The second one was silently overriding the first. - Renamed second test to `test_tool_result_with_ansi_codes_snapshot` - Updated docstring to clarify the test's purpose - Renamed snapshot file to match new test name - Both tests now run correctly (5 ANSI-related tests pass) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The module-level _github_repo variable posed a thread-safety risk when processing multiple sessions concurrently. This refactors to use Python's contextvars module which provides thread-local storage. Changes: - Add contextvars import - Create _github_repo_var ContextVar with None default - Add get_github_repo() accessor function (thread-safe) - Add set_github_repo() setter function (thread-safe) - Keep _github_repo module variable for backward compatibility - Update all internal usages to use new functions - Update test to use new API The backward-compatible design ensures existing code that reads _github_repo directly continues to work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added a copyToClipboard() helper function that:
1. Uses the modern Clipboard API (navigator.clipboard.writeText)
when available
2. Falls back to document.execCommand('copy') for older browsers
3. Returns a Promise for consistent handling
Also added user-facing error feedback:
- Button now shows "Failed" for 2 seconds on copy failure
- Improves UX by informing users when copy doesn't work
Updated both copy button implementations:
- Dynamically added copy buttons on pre/tool-result elements
- Cell header copy buttons
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
f167416 to
ff4df78
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 21 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| copyBtn.textContent = 'Copy'; | ||
| copyBtn.addEventListener('click', function(e) { | ||
| e.stopPropagation(); | ||
| const textToCopy = el.textContent.replace(/^Copy$/, '').trim(); |
There was a problem hiding this comment.
The per-<pre> copy button logic appends a <button> inside the element, but then copies el.textContent and tries to remove the label with replace(/^Copy$/, ''). That regex only matches when the entire text is exactly "Copy", so the copied text will typically include the trailing "Copy"/"Copied!" label. Consider extracting text from the pre/code node excluding the button (e.g., clone and remove the button, or read from a dedicated child node).
| const textToCopy = el.textContent.replace(/^Copy$/, '').trim(); | |
| const clone = el.cloneNode(true); | |
| clone.querySelectorAll('.copy-btn').forEach(function(btn) { btn.remove(); }); | |
| const textToCopy = clone.textContent.trim(); |
| <button class="view-toggle-tab active" role="tab" data-view="markdown">Markdown</button> | ||
| <button class="view-toggle-tab" role="tab" data-view="json">JSON</button> |
There was a problem hiding this comment.
Several view-toggle tablists (e.g. Todo/Write/Edit/Bash) use role="tablist"/role="tab" but the initial markup doesn’t set aria-selected on tabs (unlike tool_use/tool_result, which do). This makes the initial state ambiguous for assistive tech until a click occurs. Please add consistent aria-selected values for the default active/inactive tabs (and keep JS updates aligned).
| <button class="view-toggle-tab active" role="tab" data-view="markdown">Markdown</button> | |
| <button class="view-toggle-tab" role="tab" data-view="json">JSON</button> | |
| <button class="view-toggle-tab active" role="tab" data-view="markdown" aria-selected="true">Markdown</button> | |
| <button class="view-toggle-tab" role="tab" data-view="json" aria-selected="false">JSON</button> |
| <button class="view-toggle-tab" role="tab" data-view="json">All JSON</button> | ||
| </div> | ||
| {% endif %} | ||
| <button class="cell-copy-btn" aria-label="Copy {{ label }}" tabindex="0"{% if raw_content %} data-copy-content="{{ raw_content | e }}"{% endif %}>Copy</button> |
There was a problem hiding this comment.
cell() stores full cell text in a data-copy-content attribute. For large tool calls/results or long messages this duplicates content that’s already in the DOM, significantly increasing HTML size and memory usage. Consider omitting data-copy-content for large payloads (fallback to DOM extraction), or storing the raw text in a single hidden element referenced by id instead of duplicating it in attributes.
| <button class="cell-copy-btn" aria-label="Copy {{ label }}" tabindex="0"{% if raw_content %} data-copy-content="{{ raw_content | e }}"{% endif %}>Copy</button> | |
| <button class="cell-copy-btn" aria-label="Copy {{ label }}" tabindex="0"{% if raw_content and raw_content|length <= 2000 %} data-copy-content="{{ raw_content | e }}"{% endif %}>Copy</button> |
| elif isinstance(obj, str): | ||
| # Render string value as Markdown, wrap in a styled span | ||
| md_html = render_markdown_text(obj) | ||
| # Strip wrapping <p> tags for inline display if it's a single paragraph | ||
| if ( | ||
| md_html.startswith("<p>") | ||
| and md_html.endswith("</p>") | ||
| and md_html.count("<p>") == 1 | ||
| ): | ||
| md_html = md_html[3:-4] | ||
| return f'<span class="json-string-value">{md_html}</span>' |
There was a problem hiding this comment.
render_json_with_markdown() renders string values through markdown.markdown() and returns the resulting HTML marked safe in templates. Python-Markdown allows raw HTML by default, so untrusted transcript/tool input can inject arbitrary HTML/JS (XSS) via tool inputs/descriptions. Please sanitize/escape Markdown output (e.g., disable raw HTML or run the output through an allowlist sanitizer) before returning it.
| elif isinstance(obj, str): | ||
| # Render string value as Markdown, wrap in a styled span | ||
| md_html = render_markdown_text(obj) | ||
| # Strip wrapping <p> tags for inline display if it's a single paragraph | ||
| if ( | ||
| md_html.startswith("<p>") | ||
| and md_html.endswith("</p>") | ||
| and md_html.count("<p>") == 1 | ||
| ): | ||
| md_html = md_html[3:-4] | ||
| return f'<span class="json-string-value">{md_html}</span>' |
There was a problem hiding this comment.
render_json_with_markdown() wraps Markdown-rendered values in a <span>, but multi-paragraph/complex Markdown will generate block elements like <p>, <ul>, etc., which is invalid inside a span and can break layout/DOM structure. Consider using a block container (e.g., a <div> for markdown output) or ensuring the Markdown renderer returns inline-only markup for these JSON string values.
Summary
This PR adds several significant UX and rendering improvements to claude-code-transcripts, developed and tested across multiple iterations.
New Features
1. ANSI Escape Code Sanitization
2. Content Block Array Rendering
[{"type": "text", ...}]arrays as markdown instead of raw JSON3. Syntax Highlighting
4. Copy Buttons
5. Tool Call/Result Pairing
6. Collapsible Cell Structure
7. Per-Cell Copy Buttons
8. Markdown Rendering in Tools
9. Message Metadata
10. Tool Icons
Bug Fixes
_github_repovariable to use contextvarsTest Coverage
Files Changed
src/claude_code_transcripts/__init__.pysrc/claude_code_transcripts/templates/macros.htmltests/test_generate_html.pypyproject.tomlAGENTS.mdBreaking Changes
None - all changes are additive and backward compatible.
Screenshots
The improvements are best seen in action. Generate HTML from a Claude Code session to see:
Test Plan
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.5 noreply@anthropic.com