claude-code-log 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. claude_code_log-0.2.2/CHANGELOG.md +165 -0
  2. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/PKG-INFO +5 -2
  3. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/README.md +3 -1
  4. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/renderer.py +36 -16
  5. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/templates/index.html +42 -18
  6. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/templates/transcript.html +69 -81
  7. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/justfile +2 -0
  8. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/pyproject.toml +2 -1
  9. claude_code_log-0.2.2/test/test_markdown_rendering.py +128 -0
  10. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_template_rendering.py +27 -14
  11. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/uv.lock +12 -1
  12. claude_code_log-0.2.1/CHANGELOG.md +0 -73
  13. claude_code_log-0.2.1/test/test_markdown_rendering.py +0 -56
  14. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/.claude/settings.local.json +0 -0
  15. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/.github/workflows/ci.yml +0 -0
  16. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/.gitignore +0 -0
  17. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/CLAUDE.md +0 -0
  18. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/LICENSE +0 -0
  19. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/__init__.py +0 -0
  20. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/cli.py +0 -0
  21. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/converter.py +0 -0
  22. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/models.py +0 -0
  23. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/parser.py +0 -0
  24. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/py.typed +0 -0
  25. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/scripts/generate_style_guide.py +0 -0
  26. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/scripts/style_guide_output/index.html +0 -0
  27. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/scripts/style_guide_output/index_style_guide.html +0 -0
  28. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/scripts/style_guide_output/transcript_style_guide.html +0 -0
  29. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/README.md +0 -0
  30. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/__init__.py +0 -0
  31. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_command_handling.py +0 -0
  32. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_data/edge_cases.jsonl +0 -0
  33. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_data/representative_messages.jsonl +0 -0
  34. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_data/session_b.jsonl +0 -0
  35. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_data/todowrite_examples.jsonl +0 -0
  36. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_date_filtering.py +0 -0
  37. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_filtering.py +0 -0
  38. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_message_filtering.py +0 -0
  39. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_message_types.py +0 -0
  40. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_path_conversion.py +0 -0
  41. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_template_data.py +0 -0
  42. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_template_utils.py +0 -0
  43. {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_todowrite_rendering.py +0 -0
@@ -0,0 +1,165 @@
1
+ # Changelog
2
+
3
+ All notable changes to claude-code-log will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.2] - 2025-06-16
9
+
10
+ ### Changed
11
+
12
+ - **Static Markdown**: Render Markdown in Python to make it easier to test and not require Javascipt
13
+ - **Visual Design**: Make it nicer to look at
14
+
15
+ ## [0.2.1] - 2025-06-15
16
+
17
+ ### Added
18
+
19
+ - **Table of Contents & Session Navigation**: Added comprehensive session navigation system
20
+ - Interactive table of contents with session summaries and quick navigation
21
+ - Timestamp ranges showing first-to-last timestamp for each session
22
+ - Session-based organization with clickable navigation links
23
+ - Floating "back to top" button for easy navigation
24
+
25
+ - **Token Usage Tracking**: Complete token consumption display and tracking
26
+ - Individual assistant messages show token usage in headers
27
+ - Session-level token aggregation in table of contents
28
+ - Detailed breakdown: Input, Output, Cache Creation, Cache Read tokens
29
+ - Data extracted from AssistantMessage.usage field in JSONL files
30
+
31
+ - **Enhanced Content Support**: Expanded message type and content handling
32
+ - **Tool Use Rendering**: Proper display of tool invocations and results
33
+ - **Thinking Content**: Support for Claude's internal thinking processes
34
+ - **Image Handling**: Display of pasted images in transcript conversations
35
+ - **Todo List Rendering**: Support for structured todo lists in messages
36
+
37
+ - **Project Hierarchy Processing**: Complete project management system
38
+ - Process entire `~/.claude/projects/` directory by default
39
+ - Master index page with project cards and statistics
40
+ - Linked navigation between index and individual project pages
41
+ - Project statistics including file counts and recent activity
42
+
43
+ - **Improved User Experience**: Enhanced interface and navigation
44
+ - Chronological ordering of all messages across sessions
45
+ - Session demarcation with clear visual separators
46
+ - Always-visible scroll-to-top button
47
+ - Space-efficient, content-dense layout design
48
+
49
+ ### Changed
50
+
51
+ - **Default Behavior**: Changed default mode to process all projects instead of requiring explicit input
52
+ - `claude-code-log` now processes `~/.claude/projects/` by default
53
+ - Added `--all-projects` flag for explicit project processing
54
+ - Maintained backward compatibility for single file/directory processing
55
+
56
+ - **Output Structure**: Restructured HTML output for better organization
57
+ - Session-based navigation replaces simple chronological listing
58
+ - Enhanced template system with comprehensive session metadata
59
+ - Improved visual hierarchy with table of contents integration
60
+
61
+ - **Data Models**: Expanded Pydantic models for richer data representation
62
+ - Enhanced TranscriptEntry with proper content type handling
63
+ - Added UsageInfo model for token usage tracking
64
+ - Improved ContentItem unions for diverse content types
65
+
66
+ ### Technical
67
+
68
+ - **Template System**: Major improvements to Jinja2 template architecture
69
+ - New session navigation template components
70
+ - Token usage display templates
71
+ - Enhanced message rendering with rich content support
72
+ - Responsive design improvements
73
+
74
+ - **Testing Infrastructure**: Comprehensive test coverage expansion
75
+ - Increased test coverage to 78%+ across all modules
76
+ - Added visual style guide generation
77
+ - Representative test data based on real transcript files
78
+ - Extensive test documentation in test/README.md
79
+
80
+ - **Code Quality**: Significant refactoring and quality improvements
81
+ - Complete Pydantic migration with proper error handling
82
+ - Improved type hints and function documentation
83
+ - Enhanced CLI interface with better argument parsing
84
+ - Comprehensive linting and formatting standards
85
+
86
+ ### Fixed
87
+
88
+ - **Data Processing**: Improved robustness of transcript processing
89
+ - Better handling of malformed or incomplete JSONL entries
90
+ - More reliable session detection and grouping
91
+ - Enhanced error handling for edge cases in data parsing
92
+ - Fixed HTML escaping issues in message content
93
+
94
+ - **Template Rendering**: Resolved template and rendering issues
95
+ - Fixed session summary attachment logic
96
+ - Improved timestamp handling and formatting
97
+ - Better handling of mixed content types in templates
98
+ - Resolved CSS and styling inconsistencies
99
+
100
+ ## [0.1.0]
101
+
102
+ ### Added
103
+
104
+ - **Summary Message Support**: Added support for `summary` type messages in JSONL transcripts
105
+ - Summary messages are displayed with green styling and "Summary:" prefix
106
+ - Includes special CSS class `.summary` for custom styling
107
+
108
+ - **System Command Visibility**: System commands (like `init`) are now shown instead of being filtered out
109
+ - Commands appear in expandable `<details>` elements
110
+ - Shows command name in the summary (e.g., "Command: init")
111
+ - Full command content is revealed when expanded
112
+ - Uses orange styling with `.system` CSS class
113
+
114
+ - **Markdown Rendering Support**: Automatic client-side markdown rendering
115
+ - Uses marked.js ESM module loaded from CDN
116
+ - Supports GitHub Flavored Markdown (GFM)
117
+ - Renders headers, emphasis, code blocks, lists, links, and images
118
+ - Preserves existing HTML content when present
119
+
120
+ - **Enhanced CSS Styling**: New styles for better visual organization
121
+ - Added styles for `.summary` messages (green theme)
122
+ - Added styles for `.system` messages (orange theme)
123
+ - Added styles for `<details>` elements with proper spacing and cursor behavior
124
+ - Improved overall visual hierarchy
125
+
126
+ ### Changed
127
+
128
+ - **System Message Filtering**: Modified system message handling logic
129
+ - System messages with `<command-name>` tags are no longer filtered out
130
+ - Added `extract_command_name()` function to parse command names
131
+ - Updated `is_system_message()` function to handle command messages differently
132
+ - Other system messages (stdout, caveats) are still filtered as before
133
+
134
+ - **Message Type Support**: Extended message type handling in `load_transcript()`
135
+ - Now accepts `"summary"` type in addition to `"user"` and `"assistant"`
136
+ - Updated message processing logic to handle different content structures
137
+
138
+ ### Technical
139
+
140
+ - **Dependencies**: No new Python dependencies added
141
+ - marked.js is loaded via CDN for client-side rendering
142
+ - Maintains existing minimal dependency approach
143
+
144
+ - **Testing**: Added comprehensive test coverage
145
+ - New test file `test_new_features.py` with tests for:
146
+ - Summary message type support
147
+ - System command message handling
148
+ - Markdown script inclusion
149
+ - System message filtering behavior
150
+ - Tests use anonymized fixtures based on real transcript data
151
+
152
+ - **Code Quality**: Improved type hints and function documentation
153
+ - Added proper docstrings for new functions
154
+ - Enhanced error handling for edge cases
155
+ - Maintained backward compatibility with existing functionality
156
+
157
+ ### Fixed
158
+
159
+ - **Message Processing**: Improved robustness of message content extraction
160
+ - Better handling of mixed content types in transcript files
161
+ - More reliable text extraction from complex message structures
162
+
163
+ ## Previous Versions
164
+
165
+ Earlier versions focused on basic JSONL to HTML conversion with session demarcation and date filtering capabilities.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-log
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Convert Claude Code transcript JSONL files to HTML
5
5
  Project-URL: Homepage, https://github.com/daaain/claude-code-log
6
6
  Project-URL: Issues, https://github.com/daaain/claude-code-log/issues
@@ -13,6 +13,7 @@ Requires-Python: >=3.12
13
13
  Requires-Dist: click>=8.0.0
14
14
  Requires-Dist: dateparser>=1.0.0
15
15
  Requires-Dist: jinja2>=3.0.0
16
+ Requires-Dist: mistune>=3.1.3
16
17
  Requires-Dist: pydantic>=2.0.0
17
18
  Description-Content-Type: text/markdown
18
19
 
@@ -22,6 +23,8 @@ A Python CLI tool that converts Claude Code transcript JSONL files into readable
22
23
 
23
24
  ## Project Overview
24
25
 
26
+ 📋 **[View Changelog](CHANGELOG.md)** - See what's new in each release
27
+
25
28
  This tool generates clean, minimalist HTML pages showing user prompts and assistant responses chronologically. It's designed to create a readable log of your Claude Code interactions with support for both individual files and entire project hierarchies.
26
29
 
27
30
  ## Quickstart
@@ -229,6 +232,6 @@ uv run claude-code-log
229
232
 
230
233
  ## TODO
231
234
 
232
- - **Enhanced UI**: Make it look even nicer with improved styling
235
+ - Show top level stats on index page token usage added up + time of last interaction
233
236
  - **Tool Use Preview**: Show first few lines of tool use and other collapsed details
234
237
  - **In-page Filtering**: Client-side filtering and search
@@ -4,6 +4,8 @@ A Python CLI tool that converts Claude Code transcript JSONL files into readable
4
4
 
5
5
  ## Project Overview
6
6
 
7
+ 📋 **[View Changelog](CHANGELOG.md)** - See what's new in each release
8
+
7
9
  This tool generates clean, minimalist HTML pages showing user prompts and assistant responses chronologically. It's designed to create a readable log of your Claude Code interactions with support for both individual files and entire project hierarchies.
8
10
 
9
11
  ## Quickstart
@@ -211,6 +213,6 @@ uv run claude-code-log
211
213
 
212
214
  ## TODO
213
215
 
214
- - **Enhanced UI**: Make it look even nicer with improved styling
216
+ - Show top level stats on index page token usage added up + time of last interaction
215
217
  - **Tool Use Preview**: Show first few lines of tool use and other collapsed details
216
218
  - **In-page Filtering**: Client-side filtering and search
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from typing import List, Optional, Union, Dict, Any, cast
7
7
  from datetime import datetime
8
8
  import html
9
+ import mistune
9
10
  from jinja2 import Environment, FileSystemLoader
10
11
 
11
12
  from .models import (
@@ -36,6 +37,23 @@ def escape_html(text: str) -> str:
36
37
  return html.escape(text)
37
38
 
38
39
 
40
+ def render_markdown(text: str) -> str:
41
+ """Convert markdown text to HTML using mistune."""
42
+ # Configure mistune with GitHub-flavored markdown features
43
+ renderer = mistune.create_markdown(
44
+ plugins=[
45
+ "strikethrough",
46
+ "footnotes",
47
+ "table",
48
+ "url",
49
+ "task_lists",
50
+ "def_list",
51
+ ],
52
+ escape=False, # Don't escape HTML since we want to render markdown properly
53
+ )
54
+ return str(renderer(text))
55
+
56
+
39
57
  def extract_command_info(text_content: str) -> tuple[str, str, str]:
40
58
  """Extract command info from system message with command tags."""
41
59
  import re
@@ -157,7 +175,7 @@ def format_tool_use_content(tool_use: ToolUseContent) -> str:
157
175
  return f"""
158
176
  <div class="tool-content tool-use">
159
177
  <details>
160
- <summary><strong>Tool Use:</strong> {escaped_name} (ID: {escaped_id})</summary>
178
+ <summary><strong>🛠️ Tool Use:</strong> {escaped_name} (ID: {escaped_id})</summary>
161
179
  <div class="tool-input">
162
180
  <strong>Input:</strong>
163
181
  <pre>{escaped_input}</pre>
@@ -182,12 +200,12 @@ def format_tool_result_content(tool_result: ToolResultContent) -> str:
182
200
  content_parts.append(item.get("text", ""))
183
201
  escaped_content = escape_html("\n".join(content_parts))
184
202
 
185
- error_indicator = " (Error)" if tool_result.is_error else ""
203
+ error_indicator = " (🚨 Error)" if tool_result.is_error else ""
186
204
 
187
205
  return f"""
188
206
  <div class="tool-content tool-result">
189
207
  <details>
190
- <summary><strong>Tool Result{error_indicator}:</strong> {escaped_id}</summary>
208
+ <summary><strong>🧰 Tool Result{error_indicator}:</strong> {escaped_id}</summary>
191
209
  <div class="tool-input">
192
210
  <pre>{escaped_content}</pre>
193
211
  </div>
@@ -203,7 +221,7 @@ def format_thinking_content(thinking: ThinkingContent) -> str:
203
221
  return f"""
204
222
  <div class="tool-content thinking-content">
205
223
  <details>
206
- <summary><strong>Thinking</strong></summary>
224
+ <summary><strong>💭 Thinking</strong></summary>
207
225
  <div class="thinking-text">
208
226
  <pre>{escaped_thinking}</pre>
209
227
  </div>
@@ -229,24 +247,26 @@ def render_message_content(
229
247
  ) -> str:
230
248
  """Render message content with proper tool use and tool result formatting."""
231
249
  if isinstance(content, str):
232
- escaped_text = escape_html(content)
233
- return (
234
- "<pre>" + escaped_text + "</pre>"
235
- if message_type == "user"
236
- else escaped_text
237
- )
250
+ if message_type == "user":
251
+ # User messages are shown as-is in preformatted blocks
252
+ escaped_text = escape_html(content)
253
+ return "<pre>" + escaped_text + "</pre>"
254
+ else:
255
+ # Assistant messages get markdown rendering
256
+ return render_markdown(content)
238
257
 
239
258
  # content is a list of ContentItem objects
240
259
  rendered_parts: List[str] = []
241
260
 
242
261
  for item in content:
243
262
  if type(item) is TextContent:
244
- escaped_text = escape_html(item.text)
245
- rendered_parts.append(
246
- "<pre>" + escaped_text + "</pre>"
247
- if message_type == "user"
248
- else escaped_text
249
- )
263
+ if message_type == "user":
264
+ # User messages are shown as-is in preformatted blocks
265
+ escaped_text = escape_html(item.text)
266
+ rendered_parts.append("<pre>" + escaped_text + "</pre>")
267
+ else:
268
+ # Assistant messages get markdown rendering
269
+ rendered_parts.append(render_markdown(item.text))
250
270
  elif type(item) is ToolUseContent:
251
271
  rendered_parts.append(format_tool_use_content(item)) # type: ignore
252
272
  elif type(item) is ToolResultContent:
@@ -1,5 +1,6 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang='en'>
3
+
3
4
  <head>
4
5
  <meta charset='UTF-8'>
5
6
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
@@ -11,43 +12,54 @@
11
12
  max-width: 1200px;
12
13
  margin: 0 auto;
13
14
  padding: 20px;
14
- background-color: #fafafa;
15
+ background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf);
15
16
  color: #333;
16
17
  }
18
+
17
19
  h1 {
18
20
  text-align: center;
19
21
  color: #2c3e50;
20
22
  margin-bottom: 30px;
21
23
  font-size: 2em;
22
24
  }
25
+
23
26
  .project-list {
24
27
  display: grid;
25
28
  gap: 15px;
26
29
  }
30
+
27
31
  .project-card {
28
- background: white;
32
+ background-color: #ffffff66;
29
33
  border-radius: 8px;
30
34
  padding: 20px;
31
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
32
- border-left: 4px solid #2196f3;
35
+ box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011;
36
+ border-left: #ffffff66 1px solid;
37
+ border-top: #ffffff66 1px solid;
38
+ border-bottom: #00000017 1px solid;
39
+ border-right: #00000017 1px solid;
33
40
  }
41
+
34
42
  .project-card:hover {
35
- box-shadow: 0 4px 8px rgba(0,0,0,0.15);
43
+ box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022;
36
44
  transform: translateY(-1px);
37
45
  transition: all 0.2s ease;
38
46
  }
47
+
39
48
  .project-name {
40
49
  font-size: 1.2em;
41
50
  font-weight: 600;
42
51
  margin-bottom: 10px;
43
52
  }
53
+
44
54
  .project-name a {
45
55
  text-decoration: none;
46
56
  color: #2196f3;
47
57
  }
58
+
48
59
  .project-name a:hover {
49
60
  text-decoration: underline;
50
61
  }
62
+
51
63
  .project-stats {
52
64
  color: #666;
53
65
  font-size: 0.9em;
@@ -55,42 +67,53 @@
55
67
  gap: 20px;
56
68
  flex-wrap: wrap;
57
69
  }
70
+
58
71
  .stat {
59
72
  display: flex;
60
73
  align-items: center;
61
74
  gap: 5px;
62
75
  }
76
+
63
77
  .summary {
64
78
  text-align: center;
65
79
  margin-bottom: 30px;
66
80
  padding: 15px;
67
- background: white;
81
+ background-color: #ffffff66;
68
82
  border-radius: 8px;
69
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
83
+ box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011;
84
+ border-left: #ffffff66 1px solid;
85
+ border-top: #ffffff66 1px solid;
86
+ border-bottom: #00000017 1px solid;
87
+ border-right: #00000017 1px solid;
70
88
  }
89
+
71
90
  .summary-stats {
72
91
  display: flex;
73
92
  justify-content: center;
74
93
  gap: 30px;
75
94
  flex-wrap: wrap;
76
95
  }
96
+
77
97
  .summary-stat {
78
98
  text-align: center;
79
99
  }
100
+
80
101
  .summary-stat .number {
81
102
  font-size: 1.5em;
82
103
  font-weight: 600;
83
104
  color: #2196f3;
84
105
  }
106
+
85
107
  .summary-stat .label {
86
108
  color: #666;
87
109
  font-size: 0.9em;
88
110
  }
89
111
  </style>
90
112
  </head>
113
+
91
114
  <body>
92
115
  <h1>{{ title }}</h1>
93
-
116
+
94
117
  <div class='summary'>
95
118
  <div class='summary-stats'>
96
119
  <div class='summary-stat'>
@@ -107,20 +130,21 @@
107
130
  </div>
108
131
  </div>
109
132
  </div>
110
-
133
+
111
134
  <div class='project-list'>
112
135
  {% for project in projects %}
113
- <div class='project-card'>
114
- <div class='project-name'>
115
- <a href='{{ project.html_file }}'>{{ project.display_name }}</a>
116
- </div>
117
- <div class='project-stats'>
118
- <div class='stat'>📁 {{ project.jsonl_count }} transcript files</div>
119
- <div class='stat'>💬 {{ project.message_count }} messages</div>
120
- <div class='stat'>🕒 {{ project.formatted_date }}</div>
121
- </div>
136
+ <div class='project-card'>
137
+ <div class='project-name'>
138
+ <a href='{{ project.html_file }}'>{{ project.display_name }}</a>
122
139
  </div>
140
+ <div class='project-stats'>
141
+ <div class='stat'>📁 {{ project.jsonl_count }} transcript files</div>
142
+ <div class='stat'>💬 {{ project.message_count }} messages</div>
143
+ <div class='stat'>🕒 {{ project.formatted_date }}</div>
144
+ </div>
145
+ </div>
123
146
  {% endfor %}
124
147
  </div>
125
148
  </body>
149
+
126
150
  </html>
@@ -5,33 +5,6 @@
5
5
  <meta charset='UTF-8'>
6
6
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
7
7
  <title>{{ title }}</title>
8
- <script type="module">
9
- import { marked } from 'https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js';
10
-
11
- // Configure marked options
12
- marked.setOptions({
13
- breaks: true,
14
- gfm: true
15
- });
16
-
17
- document.addEventListener('DOMContentLoaded', function () {
18
- // Find all content divs and render markdown
19
- const contentDivs = document.querySelectorAll('.content');
20
- contentDivs.forEach(div => {
21
- // Skip if it's already HTML (contains tags or tool content)
22
- if (div.innerHTML.includes('<pre>') ||
23
- div.innerHTML.includes('<div class="tool-content') ||
24
- div.innerHTML.includes('todo-write')) {
25
- return;
26
- }
27
-
28
- const markdownText = div.textContent;
29
- if (markdownText.trim()) {
30
- div.innerHTML = marked.parse(markdownText);
31
- }
32
- });
33
- });
34
- </script>
35
8
  <style>
36
9
  body {
37
10
  font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', 'Consolas', monospace;
@@ -39,44 +12,36 @@
39
12
  max-width: 1200px;
40
13
  margin: 0 auto;
41
14
  padding: 10px;
42
- background-color: #fafafa;
15
+ background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf);
43
16
  color: #333;
44
17
  }
45
18
 
46
19
  .message {
47
- margin-bottom: 12px;
48
- padding: 12px;
49
- border-radius: 6px;
50
- border-left: 3px solid;
20
+ margin-bottom: 1em;
21
+ padding: 1em;
22
+ border-radius: 8px;
23
+ border-left: #ffffff66 1px solid;
24
+ background-color: #e3f2fd55;
25
+ box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011;
26
+ border-top: #ffffff66 1px solid;
27
+ border-bottom: #00000017 1px solid;
28
+ border-right: #00000017 1px solid;
51
29
  }
52
30
 
53
31
  .session-divider {
54
- margin: 30px 0;
55
- padding: 8px 0;
56
- border-top: 2px solid #ddd;
57
- text-align: center;
58
- font-weight: 600;
59
- color: #666;
60
- font-size: 0.9em;
32
+ margin: 70px 0;
33
+ border-top: 2px solid #fff;
61
34
  }
62
35
 
63
36
  .user {
64
- background-color: #e3f2fd;
65
37
  border-left-color: #2196f3;
66
38
  }
67
39
 
68
40
  .assistant {
69
- background-color: #f3e5f5;
70
41
  border-left-color: #9c27b0;
71
42
  }
72
43
 
73
- .summary {
74
- background-color: #e8f5e8;
75
- border-left-color: #4caf50;
76
- }
77
-
78
44
  .system {
79
- background-color: #fff8e1;
80
45
  border-left-color: #ff9800;
81
46
  }
82
47
 
@@ -96,27 +61,31 @@
96
61
  }
97
62
 
98
63
  .tool-content {
99
- background-color: #f8f9fa;
100
- border: 1px solid #e9ecef;
64
+ background-color: #f8f9fa66;
101
65
  border-radius: 4px;
102
66
  padding: 8px;
103
67
  margin: 8px 0;
104
68
  overflow-x: auto;
69
+ box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007;
70
+ border-left: #ffffff66 1px solid;
71
+ border-top: #ffffff66 1px solid;
72
+ border-bottom: #00000017 1px solid;
73
+ border-right: #00000017 1px solid;
105
74
  }
106
75
 
107
76
  .tool-result {
108
- background-color: #e8f5e8;
109
- border-left: 3px solid #4caf50;
77
+ background-color: #e8f5e866;
78
+ border-left: #4caf5088 1px solid;
110
79
  }
111
80
 
112
81
  .tool-use {
113
- background-color: #e3f2fd;
114
- border-left: 3px solid #2196f3;
82
+ background-color: #e3f2fd66;
83
+ border-left: #2196f388 1px solid;
115
84
  }
116
85
 
117
86
  .thinking-content {
118
- background-color: #f0f0f0;
119
- border-left: 3px solid #666;
87
+ background-color: #f0f0f066;
88
+ border-left: #66666688 1px solid;
120
89
  }
121
90
 
122
91
  .thinking-text {
@@ -125,12 +94,16 @@
125
94
  }
126
95
 
127
96
  .tool-input {
128
- background-color: #fff3cd;
129
- border: 1px solid #ffeaa7;
97
+ background-color: #fff3cd66;
130
98
  border-radius: 4px;
131
99
  padding: 6px;
132
100
  margin: 4px 0;
133
101
  font-size: 0.9em;
102
+ box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011;
103
+ border-left: #ffffff66 1px solid;
104
+ border-top: #ffffff66 1px solid;
105
+ border-bottom: #00000017 1px solid;
106
+ border-right: #00000017 1px solid;
134
107
  }
135
108
 
136
109
  .header {
@@ -167,11 +140,16 @@
167
140
  }
168
141
 
169
142
  .navigation {
170
- background-color: #f8f9fa;
171
- border: 1px solid #e9ecef;
143
+ background-color: #f8f9fa66;
172
144
  border-radius: 8px;
173
145
  padding: 16px;
174
146
  margin-bottom: 24px;
147
+ box-shadow: -7px -7px 10px #eeeeee44,
148
+ 7px 7px 10px #00000011;
149
+ border-left: #ffffff66 1px solid;
150
+ border-top: #ffffff66 1px solid;
151
+ border-bottom: #00000017 1px solid;
152
+ border-right: #00000017 1px solid;
175
153
  }
176
154
 
177
155
  .navigation h2 {
@@ -188,7 +166,7 @@
188
166
 
189
167
  .session-link {
190
168
  padding: 8px 12px;
191
- background-color: #fff;
169
+ background-color: #ffffff66;
192
170
  border: 1px solid #dee2e6;
193
171
  border-radius: 4px;
194
172
  text-decoration: none;
@@ -197,7 +175,7 @@
197
175
  }
198
176
 
199
177
  .session-link:hover {
200
- background-color: #e9ecef;
178
+ background-color: #ffffff99;
201
179
  }
202
180
 
203
181
  .session-link-title {
@@ -212,26 +190,28 @@
212
190
  }
213
191
 
214
192
  .session-header {
215
- background-color: #e8f4fd;
216
- border: 2px solid #2196f3;
193
+ background-color: #e8f4fd66;
217
194
  border-radius: 8px;
218
195
  padding: 16px;
219
196
  margin: 30px 0 20px 0;
197
+ box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011;
198
+ border-left: #ffffff66 1px solid;
199
+ border-top: #ffffff66 1px solid;
200
+ border-bottom: #00000017 1px solid;
201
+ border-right: #00000017 1px solid;
220
202
  }
221
203
 
222
204
  .session-header .header {
223
205
  margin-bottom: 8px;
224
- font-weight: 600;
225
- color: #1976d2;
226
- font-size: 1.1em;
206
+ font-size: 1.2em;
227
207
  }
228
208
 
229
209
  .scroll-top {
230
210
  position: fixed;
231
211
  bottom: 20px;
232
212
  right: 20px;
233
- background-color: #2196f3;
234
- color: white;
213
+ background-color: #e8f4fd66;
214
+ color: #ccc;
235
215
  border: none;
236
216
  border-radius: 50%;
237
217
  width: 50px;
@@ -242,13 +222,13 @@
242
222
  display: flex;
243
223
  align-items: center;
244
224
  justify-content: center;
245
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
225
+ box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
246
226
  transition: background-color 0.3s, transform 0.2s;
247
227
  z-index: 1000;
248
228
  }
249
229
 
250
230
  .scroll-top:hover {
251
- background-color: #1976d2;
231
+ background-color: #e8f4fdcc;
252
232
  transform: translateY(-2px);
253
233
  }
254
234
 
@@ -257,12 +237,16 @@
257
237
  }
258
238
 
259
239
  .session-summary {
260
- background-color: #fff;
261
- border-left: 4px solid #4caf50;
240
+ background-color: #ffffff66;
241
+ border-left: #4caf5088 4px solid;
262
242
  padding: 12px;
263
243
  margin: 8px 0;
264
244
  border-radius: 0 4px 4px 0;
265
245
  font-style: italic;
246
+ box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011;
247
+ border-top: #ffffff66 1px solid;
248
+ border-bottom: #00000017 1px solid;
249
+ border-right: #00000017 1px solid;
266
250
  }
267
251
 
268
252
  code {
@@ -273,7 +257,7 @@
273
257
  }
274
258
 
275
259
  pre {
276
- background-color: #f5f5f5;
260
+ background-color: #12121212;
277
261
  padding: 10px;
278
262
  border-radius: 5px;
279
263
  white-space: pre-wrap;
@@ -284,8 +268,8 @@
284
268
 
285
269
  /* TodoWrite tool styling */
286
270
  .todo-write {
287
- background-color: #f0f8ff;
288
- border-left: 3px solid #4169e1;
271
+ background-color: #f0f8ff66;
272
+ border-left: #4169e188 3px solid;
289
273
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
290
274
  }
291
275
 
@@ -297,10 +281,14 @@
297
281
  }
298
282
 
299
283
  .todo-list {
300
- background-color: white;
284
+ background-color: #ffffff66;
301
285
  border-radius: 6px;
302
286
  padding: 8px;
303
- border: 1px solid #e1e8ed;
287
+ box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011;
288
+ border-left: #ffffff66 1px solid;
289
+ border-top: #ffffff66 1px solid;
290
+ border-bottom: #00000017 1px solid;
291
+ border-right: #00000017 1px solid;
304
292
  }
305
293
 
306
294
  .todo-item {
@@ -399,8 +387,7 @@
399
387
  {% endif %}
400
388
  </div>
401
389
  {% if session.first_user_message %}
402
- <pre class='session-preview'
403
- style='font-size: 0.75em; color: #888; margin-top: 4px; line-height: 1.3; white-space: pre-wrap; word-wrap: break-word;'>
390
+ <pre class='session-preview' style='font-size: 0.75em; line-height: 1.3; padding-bottom: 0;'>
404
391
  {{- session.first_user_message[:500]|e -}}{% if session.first_user_message|length > 500 %}...{% endif %}
405
392
  </pre>
406
393
  {% endif %}
@@ -414,7 +401,7 @@
414
401
  {% if message.is_session_header %}
415
402
  <div class="session-divider"></div>
416
403
  <div id='session-{{ message.session_id }}' class='message session-header'>
417
- <div class='header'>{{ message.content_html }}</div>
404
+ <div class='header'>Session: {{ message.content_html }}</div>
418
405
  {% if message.session_subtitle %}
419
406
  <div class='session-subtitle' style='font-size: 0.9em; color: #666; margin-top: 4px;'>{{
420
407
  message.session_subtitle }} ({{message.session_subtitle.session_id}})</div>
@@ -424,7 +411,8 @@
424
411
  {% else %}
425
412
  <div class='message {{ message.css_class }}'>
426
413
  <div class='header'>
427
- <span>{{ message.display_type }}</span>
414
+ <span>{% if message.css_class == 'user' %}🤷 {% elif message.css_class == 'assistant' %}🤖 {% elif
415
+ message.css_class == 'system' %}⚙️ {% endif %}{{ message.display_type }}</span>
428
416
  <div style='display: flex; flex-direction: column; align-items: flex-end; gap: 2px;'>
429
417
  <span class='timestamp'>{{ message.formatted_timestamp }}</span>
430
418
  {% if message.token_usage %}
@@ -20,6 +20,8 @@ lint:
20
20
  typecheck:
21
21
  uv run pyright
22
22
 
23
+ ci: format test lint typecheck
24
+
23
25
  build:
24
26
  rm dist/*
25
27
  uv build
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-code-log"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Convert Claude Code transcript JSONL files to HTML"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -20,6 +20,7 @@ dependencies = [
20
20
  "dateparser>=1.0.0",
21
21
  "pydantic>=2.0.0",
22
22
  "jinja2>=3.0.0",
23
+ "mistune>=3.1.3",
23
24
  ]
24
25
 
25
26
  [project.urls]
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """Test cases for server-side markdown rendering."""
3
+
4
+ import json
5
+ import tempfile
6
+ from pathlib import Path
7
+ from claude_code_log.converter import (
8
+ load_transcript,
9
+ generate_html,
10
+ )
11
+
12
+
13
+ def test_server_side_markdown_rendering():
14
+ """Test that markdown is rendered server-side and marked.js is not included."""
15
+ # Assistant message with markdown content
16
+ assistant_message = {
17
+ "type": "assistant",
18
+ "timestamp": "2025-06-11T22:44:17.436Z",
19
+ "parentUuid": None,
20
+ "isSidechain": False,
21
+ "userType": "assistant",
22
+ "cwd": "/tmp",
23
+ "sessionId": "test_session",
24
+ "version": "1.0.0",
25
+ "uuid": "test_md_001",
26
+ "requestId": "req_001",
27
+ "message": {
28
+ "id": "msg_001",
29
+ "type": "message",
30
+ "role": "assistant",
31
+ "model": "claude-3-5-sonnet-20241022",
32
+ "content": [
33
+ {
34
+ "type": "text",
35
+ "text": "# Test Markdown\n\nThis is **bold** text and `code` inline.",
36
+ }
37
+ ],
38
+ "stop_reason": "end_turn",
39
+ "stop_sequence": None,
40
+ },
41
+ }
42
+
43
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
44
+ f.write(json.dumps(assistant_message) + "\n")
45
+ f.flush()
46
+ test_file_path = Path(f.name)
47
+
48
+ try:
49
+ messages = load_transcript(test_file_path)
50
+ html = generate_html(messages, "Test Transcript")
51
+
52
+ # Should NOT include marked.js script references
53
+ assert "marked" not in html, "Should not include marked.js reference"
54
+ assert "import { marked }" not in html, "Should not import marked module"
55
+ assert "marked.parse" not in html, "Should not use marked.parse function"
56
+ assert "DOMContentLoaded" not in html or "marked" not in html, (
57
+ "Should not have markdown-related DOM handlers"
58
+ )
59
+
60
+ # Should include rendered HTML from markdown
61
+ assert "<h1>Test Markdown</h1>" in html, (
62
+ "Should render markdown heading as HTML"
63
+ )
64
+ assert "<strong>bold</strong>" in html, "Should render bold text as HTML"
65
+ assert "<code>code</code>" in html, "Should render inline code as HTML"
66
+
67
+ print("✓ Test passed: Markdown is rendered server-side")
68
+
69
+ finally:
70
+ test_file_path.unlink()
71
+
72
+
73
+ def test_user_message_not_markdown_rendered():
74
+ """Test that user messages are not markdown rendered (shown as-is in pre tags)."""
75
+ user_message = {
76
+ "type": "user",
77
+ "timestamp": "2025-06-11T22:44:17.436Z",
78
+ "parentUuid": None,
79
+ "isSidechain": False,
80
+ "userType": "human",
81
+ "cwd": "/tmp",
82
+ "sessionId": "test_session",
83
+ "version": "1.0.0",
84
+ "uuid": "test_md_002",
85
+ "message": {
86
+ "role": "user",
87
+ "content": [
88
+ {
89
+ "type": "text",
90
+ "text": "# This should NOT be rendered\n\n**This should stay bold**",
91
+ }
92
+ ],
93
+ },
94
+ }
95
+
96
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
97
+ f.write(json.dumps(user_message) + "\n")
98
+ f.flush()
99
+ test_file_path = Path(f.name)
100
+
101
+ try:
102
+ messages = load_transcript(test_file_path)
103
+ html = generate_html(messages, "Test Transcript")
104
+
105
+ # User messages should be shown as-is in pre tags, not rendered as HTML
106
+ assert "<pre># This should NOT be rendered" in html, (
107
+ "User markdown should remain as text in pre tags"
108
+ )
109
+ assert "**This should stay bold**</pre>" in html, (
110
+ "User markdown asterisks should remain literal"
111
+ )
112
+ assert "<h1>This should NOT be rendered</h1>" not in html, (
113
+ "User markdown should not be rendered as HTML"
114
+ )
115
+ assert "<strong>This should stay bold</strong>" not in html, (
116
+ "User markdown should not be rendered as HTML"
117
+ )
118
+
119
+ print("✓ Test passed: User messages are not markdown rendered")
120
+
121
+ finally:
122
+ test_file_path.unlink()
123
+
124
+
125
+ if __name__ == "__main__":
126
+ test_server_side_markdown_rendering()
127
+ test_user_message_not_markdown_rendered()
128
+ print("\n✅ All markdown rendering tests passed!")
@@ -54,9 +54,13 @@ class TestTemplateRendering:
54
54
  assert "Tool Use:" in html_content
55
55
  assert "Tool Result:" in html_content
56
56
 
57
- # Check that markdown elements are preserved for client-side rendering
58
- assert "```python" in html_content
57
+ # Check that markdown elements are rendered server-side
58
+ assert (
59
+ "<code>@time_it" in html_content
60
+ ) # Inline code blocks are rendered to HTML
59
61
  assert "decorator factory" in html_content
62
+ assert "<strong>" in html_content # Bold text is rendered to strong tags
63
+ assert "<code>" in html_content # Inline code is rendered to code tags
60
64
 
61
65
  def test_edge_cases_render(self):
62
66
  """Test that edge cases render without errors."""
@@ -70,16 +74,16 @@ class TestTemplateRendering:
70
74
  assert "<!DOCTYPE html>" in html_content
71
75
  assert "<title>Claude Transcript - edge_cases</title>" in html_content
72
76
 
73
- # Check markdown content is preserved
74
- assert "**markdown**" in html_content
75
- assert "`inline code`" in html_content
76
- assert "[link](https://example.com)" in html_content
77
+ # Check markdown content is rendered to HTML (for assistant messages)
78
+ # User messages should remain as-is in pre tags, assistant messages should be rendered
79
+ # Note: Need to check which messages are user vs assistant to know what to expect
77
80
 
78
81
  # Check long text handling
79
82
  assert "Lorem ipsum dolor sit amet" in html_content
80
83
 
81
84
  # Check tool error handling
82
- assert "Tool Result (Error):" in html_content
85
+ assert "Tool Result" in html_content
86
+ assert "Error):" in html_content
83
87
  assert "Tool execution failed" in html_content
84
88
 
85
89
  # Check system message filtering (caveat should be filtered out)
@@ -253,8 +257,8 @@ class TestTemplateRendering:
253
257
  assert 'class="tool-content tool-use"' in html_content
254
258
  assert 'class="tool-content tool-result"' in html_content
255
259
 
256
- def test_javascript_markdown_setup(self):
257
- """Test that JavaScript for markdown rendering is included."""
260
+ def test_server_side_markdown_rendering(self):
261
+ """Test that markdown is rendered server-side, not client-side."""
258
262
  test_data_path = (
259
263
  Path(__file__).parent / "test_data" / "representative_messages.jsonl"
260
264
  )
@@ -262,11 +266,20 @@ class TestTemplateRendering:
262
266
  html_file = convert_jsonl_to_html(test_data_path)
263
267
  html_content = html_file.read_text()
264
268
 
265
- # Check for marked.js import and setup
266
- assert "marked" in html_content
267
- assert "DOMContentLoaded" in html_content
268
- assert "querySelectorAll('.content')" in html_content
269
- assert "marked.parse" in html_content
269
+ # Should NOT have client-side JavaScript for markdown rendering
270
+ assert "marked" not in html_content
271
+ assert "DOMContentLoaded" not in html_content or "marked" not in html_content
272
+ assert "querySelectorAll('.content')" not in html_content
273
+ assert "marked.parse" not in html_content
274
+
275
+ # Should have server-side rendered markdown in assistant messages
276
+ # Check for elements that indicate markdown was rendered
277
+ assert "<strong>" in html_content # Bold text should be rendered
278
+ assert "<code>" in html_content # Code should be rendered
279
+ assert "<p>" in html_content # Paragraphs should be rendered
280
+ assert (
281
+ "<ul>" in html_content or "<ol>" in html_content
282
+ ) # Lists should be rendered
270
283
 
271
284
  def test_html_escaping(self):
272
285
  """Test that HTML special characters are properly escaped."""
@@ -13,12 +13,13 @@ wheels = [
13
13
 
14
14
  [[package]]
15
15
  name = "claude-code-log"
16
- version = "0.1.0"
16
+ version = "0.2.1"
17
17
  source = { editable = "." }
18
18
  dependencies = [
19
19
  { name = "click" },
20
20
  { name = "dateparser" },
21
21
  { name = "jinja2" },
22
+ { name = "mistune" },
22
23
  { name = "pydantic" },
23
24
  ]
24
25
 
@@ -38,6 +39,7 @@ requires-dist = [
38
39
  { name = "click", specifier = ">=8.0.0" },
39
40
  { name = "dateparser", specifier = ">=1.0.0" },
40
41
  { name = "jinja2", specifier = ">=3.0.0" },
42
+ { name = "mistune", specifier = ">=3.1.3" },
41
43
  { name = "pydantic", specifier = ">=2.0.0" },
42
44
  ]
43
45
 
@@ -198,6 +200,15 @@ wheels = [
198
200
  { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload_time = "2024-10-18T15:21:42.784Z" },
199
201
  ]
200
202
 
203
+ [[package]]
204
+ name = "mistune"
205
+ version = "3.1.3"
206
+ source = { registry = "https://pypi.org/simple" }
207
+ sdist = { url = "https://files.pythonhosted.org/packages/c4/79/bda47f7dd7c3c55770478d6d02c9960c430b0cf1773b72366ff89126ea31/mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0", size = 94347, upload_time = "2025-03-19T14:27:24.955Z" }
208
+ wheels = [
209
+ { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410, upload_time = "2025-03-19T14:27:23.451Z" },
210
+ ]
211
+
201
212
  [[package]]
202
213
  name = "nodeenv"
203
214
  version = "1.9.1"
@@ -1,73 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to claude-code-log will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ### Added
11
-
12
- - **Summary Message Support**: Added support for `summary` type messages in JSONL transcripts
13
- - Summary messages are displayed with green styling and "Summary:" prefix
14
- - Includes special CSS class `.summary` for custom styling
15
-
16
- - **System Command Visibility**: System commands (like `init`) are now shown instead of being filtered out
17
- - Commands appear in expandable `<details>` elements
18
- - Shows command name in the summary (e.g., "Command: init")
19
- - Full command content is revealed when expanded
20
- - Uses orange styling with `.system` CSS class
21
-
22
- - **Markdown Rendering Support**: Automatic client-side markdown rendering
23
- - Uses marked.js ESM module loaded from CDN
24
- - Supports GitHub Flavored Markdown (GFM)
25
- - Renders headers, emphasis, code blocks, lists, links, and images
26
- - Preserves existing HTML content when present
27
-
28
- - **Enhanced CSS Styling**: New styles for better visual organization
29
- - Added styles for `.summary` messages (green theme)
30
- - Added styles for `.system` messages (orange theme)
31
- - Added styles for `<details>` elements with proper spacing and cursor behavior
32
- - Improved overall visual hierarchy
33
-
34
- ### Changed
35
-
36
- - **System Message Filtering**: Modified system message handling logic
37
- - System messages with `<command-name>` tags are no longer filtered out
38
- - Added `extract_command_name()` function to parse command names
39
- - Updated `is_system_message()` function to handle command messages differently
40
- - Other system messages (stdout, caveats) are still filtered as before
41
-
42
- - **Message Type Support**: Extended message type handling in `load_transcript()`
43
- - Now accepts `"summary"` type in addition to `"user"` and `"assistant"`
44
- - Updated message processing logic to handle different content structures
45
-
46
- ### Technical
47
-
48
- - **Dependencies**: No new Python dependencies added
49
- - marked.js is loaded via CDN for client-side rendering
50
- - Maintains existing minimal dependency approach
51
-
52
- - **Testing**: Added comprehensive test coverage
53
- - New test file `test_new_features.py` with tests for:
54
- - Summary message type support
55
- - System command message handling
56
- - Markdown script inclusion
57
- - System message filtering behavior
58
- - Tests use anonymized fixtures based on real transcript data
59
-
60
- - **Code Quality**: Improved type hints and function documentation
61
- - Added proper docstrings for new functions
62
- - Enhanced error handling for edge cases
63
- - Maintained backward compatibility with existing functionality
64
-
65
- ### Fixed
66
-
67
- - **Message Processing**: Improved robustness of message content extraction
68
- - Better handling of mixed content types in transcript files
69
- - More reliable text extraction from complex message structures
70
-
71
- ## Previous Versions
72
-
73
- Earlier versions focused on basic JSONL to HTML conversion with session demarcation and date filtering capabilities.
@@ -1,56 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Test cases for markdown rendering and JavaScript setup."""
3
-
4
- import json
5
- import tempfile
6
- from pathlib import Path
7
- from claude_code_log.converter import (
8
- load_transcript,
9
- generate_html,
10
- )
11
-
12
-
13
- def test_markdown_script_inclusion():
14
- """Test that marked.js script is included in HTML output."""
15
- user_message = {
16
- "type": "user",
17
- "timestamp": "2025-06-11T22:44:17.436Z",
18
- "parentUuid": None,
19
- "isSidechain": False,
20
- "userType": "human",
21
- "cwd": "/tmp",
22
- "sessionId": "test_session",
23
- "version": "1.0.0",
24
- "uuid": "test_md_001",
25
- "message": {
26
- "role": "user",
27
- "content": [
28
- {"type": "text", "text": "# Test Markdown\n\nThis is **bold** text."}
29
- ],
30
- },
31
- }
32
-
33
- with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
34
- f.write(json.dumps(user_message) + "\n")
35
- f.flush()
36
- test_file_path = Path(f.name)
37
-
38
- try:
39
- messages = load_transcript(test_file_path)
40
- html = generate_html(messages, "Test Transcript")
41
-
42
- # Should include marked.js script
43
- assert "marked" in html, "Should include marked.js reference"
44
- assert "import { marked }" in html, "Should import marked module"
45
- assert "marked.parse" in html, "Should use marked.parse function"
46
- assert "DOMContentLoaded" in html, "Should wait for DOM to load"
47
-
48
- print("✓ Test passed: Markdown script is included in HTML")
49
-
50
- finally:
51
- test_file_path.unlink()
52
-
53
-
54
- if __name__ == "__main__":
55
- test_markdown_script_inclusion()
56
- print("\n✅ All markdown rendering tests passed!")
File without changes