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.
- claude_code_log-0.2.2/CHANGELOG.md +165 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/PKG-INFO +5 -2
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/README.md +3 -1
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/renderer.py +36 -16
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/templates/index.html +42 -18
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/templates/transcript.html +69 -81
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/justfile +2 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/pyproject.toml +2 -1
- claude_code_log-0.2.2/test/test_markdown_rendering.py +128 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_template_rendering.py +27 -14
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/uv.lock +12 -1
- claude_code_log-0.2.1/CHANGELOG.md +0 -73
- claude_code_log-0.2.1/test/test_markdown_rendering.py +0 -56
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/.claude/settings.local.json +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/.github/workflows/ci.yml +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/.gitignore +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/CLAUDE.md +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/LICENSE +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/__init__.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/cli.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/converter.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/models.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/parser.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/claude_code_log/py.typed +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/scripts/generate_style_guide.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/scripts/style_guide_output/index.html +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/scripts/style_guide_output/index_style_guide.html +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/scripts/style_guide_output/transcript_style_guide.html +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/README.md +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/__init__.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_command_handling.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_data/edge_cases.jsonl +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_data/representative_messages.jsonl +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_data/session_b.jsonl +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_data/todowrite_examples.jsonl +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_date_filtering.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_filtering.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_message_filtering.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_message_types.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_path_conversion.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_template_data.py +0 -0
- {claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_template_utils.py +0 -0
- {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.
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
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:
|
|
32
|
+
background-color: #ffffff66;
|
|
29
33
|
border-radius: 8px;
|
|
30
34
|
padding: 20px;
|
|
31
|
-
box-shadow:
|
|
32
|
-
border-left:
|
|
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:
|
|
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:
|
|
81
|
+
background-color: #ffffff66;
|
|
68
82
|
border-radius: 8px;
|
|
69
|
-
box-shadow:
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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:
|
|
48
|
-
padding:
|
|
49
|
-
border-radius:
|
|
50
|
-
border-left:
|
|
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:
|
|
55
|
-
|
|
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: #
|
|
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: #
|
|
109
|
-
border-left:
|
|
77
|
+
background-color: #e8f5e866;
|
|
78
|
+
border-left: #4caf5088 1px solid;
|
|
110
79
|
}
|
|
111
80
|
|
|
112
81
|
.tool-use {
|
|
113
|
-
background-color: #
|
|
114
|
-
border-left:
|
|
82
|
+
background-color: #e3f2fd66;
|
|
83
|
+
border-left: #2196f388 1px solid;
|
|
115
84
|
}
|
|
116
85
|
|
|
117
86
|
.thinking-content {
|
|
118
|
-
background-color: #
|
|
119
|
-
border-left:
|
|
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: #
|
|
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: #
|
|
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: #
|
|
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: #
|
|
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: #
|
|
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-
|
|
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: #
|
|
234
|
-
color:
|
|
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:
|
|
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: #
|
|
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: #
|
|
261
|
-
border-left: 4px solid
|
|
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: #
|
|
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: #
|
|
288
|
-
border-left: 3px solid
|
|
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:
|
|
284
|
+
background-color: #ffffff66;
|
|
301
285
|
border-radius: 6px;
|
|
302
286
|
padding: 8px;
|
|
303
|
-
|
|
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.
|
|
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 %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "claude-code-log"
|
|
3
|
-
version = "0.2.
|
|
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
|
|
58
|
-
assert
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
|
257
|
-
"""Test that
|
|
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
|
-
#
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_log-0.2.1 → claude_code_log-0.2.2}/scripts/style_guide_output/index_style_guide.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_log-0.2.1 → claude_code_log-0.2.2}/test/test_data/representative_messages.jsonl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|