claude-code-log 0.2.0__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.0 → claude_code_log-0.2.2}/PKG-INFO +6 -3
  3. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/README.md +4 -2
  4. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/claude_code_log/renderer.py +36 -16
  5. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/claude_code_log/templates/index.html +42 -18
  6. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/claude_code_log/templates/transcript.html +69 -81
  7. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/justfile +3 -0
  8. {claude_code_log-0.2.0 → 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.0 → claude_code_log-0.2.2}/test/test_template_rendering.py +27 -14
  11. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/uv.lock +12 -1
  12. claude_code_log-0.2.0/CHANGELOG.md +0 -73
  13. claude_code_log-0.2.0/test/test_markdown_rendering.py +0 -56
  14. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/.claude/settings.local.json +0 -0
  15. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/.github/workflows/ci.yml +0 -0
  16. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/.gitignore +0 -0
  17. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/CLAUDE.md +0 -0
  18. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/LICENSE +0 -0
  19. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/claude_code_log/__init__.py +0 -0
  20. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/claude_code_log/cli.py +0 -0
  21. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/claude_code_log/converter.py +0 -0
  22. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/claude_code_log/models.py +0 -0
  23. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/claude_code_log/parser.py +0 -0
  24. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/claude_code_log/py.typed +0 -0
  25. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/scripts/generate_style_guide.py +0 -0
  26. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/scripts/style_guide_output/index.html +0 -0
  27. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/scripts/style_guide_output/index_style_guide.html +0 -0
  28. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/scripts/style_guide_output/transcript_style_guide.html +0 -0
  29. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/README.md +0 -0
  30. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/__init__.py +0 -0
  31. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_command_handling.py +0 -0
  32. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_data/edge_cases.jsonl +0 -0
  33. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_data/representative_messages.jsonl +0 -0
  34. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_data/session_b.jsonl +0 -0
  35. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_data/todowrite_examples.jsonl +0 -0
  36. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_date_filtering.py +0 -0
  37. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_filtering.py +0 -0
  38. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_message_filtering.py +0 -0
  39. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_message_types.py +0 -0
  40. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_path_conversion.py +0 -0
  41. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_template_data.py +0 -0
  42. {claude_code_log-0.2.0 → claude_code_log-0.2.2}/test/test_template_utils.py +0 -0
  43. {claude_code_log-0.2.0 → 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.0
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
@@ -212,7 +215,7 @@ When processing all projects, the tool generates:
212
215
 
213
216
  ## Installation
214
217
 
215
- Install using pip (coming soon to PyPI):
218
+ Install using pip:
216
219
 
217
220
  ```bash
218
221
  pip install claude-code-log
@@ -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
@@ -194,7 +196,7 @@ When processing all projects, the tool generates:
194
196
 
195
197
  ## Installation
196
198
 
197
- Install using pip (coming soon to PyPI):
199
+ Install using pip:
198
200
 
199
201
  ```bash
200
202
  pip install claude-code-log
@@ -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>