Skip to content

Commit fa37e14

Browse files
gnymanCodex
andcommitted
Make pagination optional by default
Co-authored-by: Codex <[email protected]>
1 parent b7669be commit fa37e14

File tree

5 files changed

+217
-27
lines changed

5 files changed

+217
-27
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![Tests](https://github.com/simonw/claude-code-transcripts/workflows/Test/badge.svg)](https://github.com/simonw/claude-code-transcripts/actions?query=workflow%3ATest)
66
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/claude-code-transcripts/blob/main/LICENSE)
77

8-
Convert Claude Code session files (JSON or JSONL) to clean, mobile-friendly HTML pages with pagination.
8+
Convert Claude Code session files (JSON or JSONL) to clean, mobile-friendly HTML pages.
99

1010
[Example transcript](https://static.simonwillison.net/static/2025/claude-code-microjs/index.html) produced using this tool.
1111

@@ -51,10 +51,11 @@ All commands support these options:
5151
- `--open` - open the generated `index.html` in your default browser (default if no `-o` specified)
5252
- `--gist` - upload the generated HTML files to a GitHub Gist and output a preview URL
5353
- `--json` - include the original session file in the output directory
54+
- `--paginate` - enable pagination (default: single page)
5455

5556
The generated output includes:
56-
- `index.html` - an index page with a timeline of prompts and commits
57-
- `page-001.html`, `page-002.html`, etc. - paginated transcript pages
57+
- `index.html` - the transcript (single page by default)
58+
- `page-001.html`, `page-002.html`, etc. - paginated transcript pages when `--paginate` is used
5859

5960
### Local sessions
6061

src/claude_code_transcripts/__init__.py

Lines changed: 132 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,11 @@ def find_all_sessions(folder, include_agents=False):
304304

305305

306306
def generate_batch_html(
307-
source_folder, output_dir, include_agents=False, progress_callback=None
307+
source_folder,
308+
output_dir,
309+
include_agents=False,
310+
progress_callback=None,
311+
paginate=False,
308312
):
309313
"""Generate HTML archive for all sessions in a Claude projects folder.
310314
@@ -319,6 +323,7 @@ def generate_batch_html(
319323
include_agents: Whether to include agent-* session files
320324
progress_callback: Optional callback(project_name, session_name, current, total)
321325
called after each session is processed
326+
paginate: Whether to generate paginated output
322327
323328
Returns statistics dict with total_projects, total_sessions, failed_sessions, output_dir.
324329
"""
@@ -347,7 +352,7 @@ def generate_batch_html(
347352

348353
# Generate transcript HTML with error handling
349354
try:
350-
generate_html(session["path"], session_dir)
355+
generate_html(session["path"], session_dir, paginate=paginate)
351356
successful_sessions += 1
352357
except Exception as e:
353358
failed_sessions.append(
@@ -1153,7 +1158,7 @@ def generate_index_pagination_html(total_pages):
11531158
return _macros.index_pagination(total_pages)
11541159

11551160

1156-
def generate_html(json_path, output_dir, github_repo=None):
1161+
def generate_html(json_path, output_dir, github_repo=None, paginate=False):
11571162
output_dir = Path(output_dir)
11581163
output_dir.mkdir(exist_ok=True)
11591164

@@ -1212,6 +1217,58 @@ def generate_html(json_path, output_dir, github_repo=None):
12121217
total_convs = len(conversations)
12131218
total_pages = (total_convs + PROMPTS_PER_PAGE - 1) // PROMPTS_PER_PAGE
12141219

1220+
if not paginate:
1221+
messages_html = []
1222+
for conv in conversations:
1223+
is_first = True
1224+
for log_type, message_json, timestamp in conv["messages"]:
1225+
msg_html = render_message(log_type, message_json, timestamp)
1226+
if msg_html:
1227+
if is_first and conv.get("is_continuation"):
1228+
msg_html = f'<details class="continuation"><summary>Session continuation summary</summary>{msg_html}</details>'
1229+
messages_html.append(msg_html)
1230+
is_first = False
1231+
page_template = get_template("page.html")
1232+
page_content = page_template.render(
1233+
css=CSS,
1234+
js=JS,
1235+
page_num=1,
1236+
total_pages=1,
1237+
pagination_html="",
1238+
messages_html="".join(messages_html),
1239+
paginate=False,
1240+
)
1241+
index_path = output_dir / "index.html"
1242+
index_path.write_text(page_content, encoding="utf-8")
1243+
print(f"Generated {index_path.resolve()} ({total_convs} prompts)")
1244+
return
1245+
1246+
if not paginate:
1247+
messages_html = []
1248+
for conv in conversations:
1249+
is_first = True
1250+
for log_type, message_json, timestamp in conv["messages"]:
1251+
msg_html = render_message(log_type, message_json, timestamp)
1252+
if msg_html:
1253+
if is_first and conv.get("is_continuation"):
1254+
msg_html = f'<details class="continuation"><summary>Session continuation summary</summary>{msg_html}</details>'
1255+
messages_html.append(msg_html)
1256+
is_first = False
1257+
page_template = get_template("page.html")
1258+
page_content = page_template.render(
1259+
css=CSS,
1260+
js=JS,
1261+
page_num=1,
1262+
total_pages=1,
1263+
pagination_html="",
1264+
messages_html="".join(messages_html),
1265+
paginate=False,
1266+
)
1267+
index_path = output_dir / "index.html"
1268+
index_path.write_text(page_content, encoding="utf-8")
1269+
print(f"Generated {index_path.resolve()} ({total_convs} prompts)")
1270+
return
1271+
12151272
for page_num in range(1, total_pages + 1):
12161273
start_idx = (page_num - 1) * PROMPTS_PER_PAGE
12171274
end_idx = min(start_idx + PROMPTS_PER_PAGE, total_convs)
@@ -1236,6 +1293,7 @@ def generate_html(json_path, output_dir, github_repo=None):
12361293
total_pages=total_pages,
12371294
pagination_html=pagination_html,
12381295
messages_html="".join(messages_html),
1296+
paginate=True,
12391297
)
12401298
(output_dir / f"page-{page_num:03d}.html").write_text(
12411299
page_content, encoding="utf-8"
@@ -1369,12 +1427,26 @@ def cli():
13691427
is_flag=True,
13701428
help="Open the generated index.html in your default browser (default if no -o specified).",
13711429
)
1430+
@click.option(
1431+
"--paginate",
1432+
is_flag=True,
1433+
help="Enable pagination (default: single page).",
1434+
)
13721435
@click.option(
13731436
"--limit",
13741437
default=10,
13751438
help="Maximum number of sessions to show (default: 10)",
13761439
)
1377-
def local_cmd(output, output_auto, repo, gist, include_json, open_browser, limit):
1440+
def local_cmd(
1441+
output,
1442+
output_auto,
1443+
repo,
1444+
gist,
1445+
include_json,
1446+
open_browser,
1447+
paginate,
1448+
limit,
1449+
):
13781450
"""Select and convert a local Claude Code session to HTML."""
13791451
projects_folder = Path.home() / ".claude" / "projects"
13801452

@@ -1425,7 +1497,7 @@ def local_cmd(output, output_auto, repo, gist, include_json, open_browser, limit
14251497
output = Path(tempfile.gettempdir()) / f"claude-session-{session_file.stem}"
14261498

14271499
output = Path(output)
1428-
generate_html(session_file, output, github_repo=repo)
1500+
generate_html(session_file, output, github_repo=repo, paginate=paginate)
14291501

14301502
# Show output directory
14311503
click.echo(f"Output: {output.resolve()}")
@@ -1487,7 +1559,14 @@ def local_cmd(output, output_auto, repo, gist, include_json, open_browser, limit
14871559
is_flag=True,
14881560
help="Open the generated index.html in your default browser (default if no -o specified).",
14891561
)
1490-
def json_cmd(json_file, output, output_auto, repo, gist, include_json, open_browser):
1562+
@click.option(
1563+
"--paginate",
1564+
is_flag=True,
1565+
help="Enable pagination (default: single page).",
1566+
)
1567+
def json_cmd(
1568+
json_file, output, output_auto, repo, gist, include_json, open_browser, paginate
1569+
):
14911570
"""Convert a Claude Code session JSON/JSONL file to HTML."""
14921571
# Determine output directory and whether to open browser
14931572
# If no -o specified, use temp dir and open browser by default
@@ -1500,7 +1579,7 @@ def json_cmd(json_file, output, output_auto, repo, gist, include_json, open_brow
15001579
output = Path(tempfile.gettempdir()) / f"claude-session-{Path(json_file).stem}"
15011580

15021581
output = Path(output)
1503-
generate_html(json_file, output, github_repo=repo)
1582+
generate_html(json_file, output, github_repo=repo, paginate=paginate)
15041583

15051584
# Show output directory
15061585
click.echo(f"Output: {output.resolve()}")
@@ -1574,7 +1653,9 @@ def format_session_for_display(session_data):
15741653
return f"{session_id} {created_at[:19] if created_at else 'N/A':19} {title}"
15751654

15761655

1577-
def generate_html_from_session_data(session_data, output_dir, github_repo=None):
1656+
def generate_html_from_session_data(
1657+
session_data, output_dir, github_repo=None, paginate=False
1658+
):
15781659
"""Generate HTML from session data dict (instead of file path)."""
15791660
output_dir = Path(output_dir)
15801661
output_dir.mkdir(exist_ok=True, parents=True)
@@ -1627,6 +1708,32 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
16271708
total_convs = len(conversations)
16281709
total_pages = (total_convs + PROMPTS_PER_PAGE - 1) // PROMPTS_PER_PAGE
16291710

1711+
if not paginate:
1712+
messages_html = []
1713+
for conv in conversations:
1714+
is_first = True
1715+
for log_type, message_json, timestamp in conv["messages"]:
1716+
msg_html = render_message(log_type, message_json, timestamp)
1717+
if msg_html:
1718+
if is_first and conv.get("is_continuation"):
1719+
msg_html = f'<details class="continuation"><summary>Session continuation summary</summary>{msg_html}</details>'
1720+
messages_html.append(msg_html)
1721+
is_first = False
1722+
page_template = get_template("page.html")
1723+
page_content = page_template.render(
1724+
css=CSS,
1725+
js=JS,
1726+
page_num=1,
1727+
total_pages=1,
1728+
pagination_html="",
1729+
messages_html="".join(messages_html),
1730+
paginate=False,
1731+
)
1732+
index_path = output_dir / "index.html"
1733+
index_path.write_text(page_content, encoding="utf-8")
1734+
print(f"Generated {index_path.resolve()} ({total_convs} prompts)")
1735+
return
1736+
16301737
for page_num in range(1, total_pages + 1):
16311738
start_idx = (page_num - 1) * PROMPTS_PER_PAGE
16321739
end_idx = min(start_idx + PROMPTS_PER_PAGE, total_convs)
@@ -1651,6 +1758,7 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
16511758
total_pages=total_pages,
16521759
pagination_html=pagination_html,
16531760
messages_html="".join(messages_html),
1761+
paginate=True,
16541762
)
16551763
(output_dir / f"page-{page_num:03d}.html").write_text(
16561764
page_content, encoding="utf-8"
@@ -1782,6 +1890,11 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
17821890
is_flag=True,
17831891
help="Open the generated index.html in your default browser (default if no -o specified).",
17841892
)
1893+
@click.option(
1894+
"--paginate",
1895+
is_flag=True,
1896+
help="Enable pagination (default: single page).",
1897+
)
17851898
def web_cmd(
17861899
session_id,
17871900
output,
@@ -1792,6 +1905,7 @@ def web_cmd(
17921905
gist,
17931906
include_json,
17941907
open_browser,
1908+
paginate,
17951909
):
17961910
"""Select and convert a web session from the Claude API to HTML.
17971911
@@ -1863,7 +1977,9 @@ def web_cmd(
18631977

18641978
output = Path(output)
18651979
click.echo(f"Generating HTML in {output}/...")
1866-
generate_html_from_session_data(session_data, output, github_repo=repo)
1980+
generate_html_from_session_data(
1981+
session_data, output, github_repo=repo, paginate=paginate
1982+
)
18671983

18681984
# Show output directory
18691985
click.echo(f"Output: {output.resolve()}")
@@ -1921,13 +2037,18 @@ def web_cmd(
19212037
is_flag=True,
19222038
help="Open the generated archive in your default browser.",
19232039
)
2040+
@click.option(
2041+
"--paginate",
2042+
is_flag=True,
2043+
help="Enable pagination (default: single page).",
2044+
)
19242045
@click.option(
19252046
"-q",
19262047
"--quiet",
19272048
is_flag=True,
19282049
help="Suppress all output except errors.",
19292050
)
1930-
def all_cmd(source, output, include_agents, dry_run, open_browser, quiet):
2051+
def all_cmd(source, output, include_agents, dry_run, open_browser, paginate, quiet):
19312052
"""Convert all local Claude Code sessions to a browsable HTML archive.
19322053
19332054
Creates a directory structure with:
@@ -1993,6 +2114,7 @@ def on_progress(project_name, session_name, current, total):
19932114
output,
19942115
include_agents=include_agents,
19952116
progress_callback=on_progress,
2117+
paginate=paginate,
19962118
)
19972119

19982120
# Report any failures
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
{% extends "base.html" %}
22

3-
{% block title %}Claude Code transcript - page {{ page_num }}{% endblock %}
3+
{% block title %}Claude Code transcript{% if paginate %} - page {{ page_num }}{% endif %}{% endblock %}
44

55
{% block content %}
6+
{% if paginate -%}
67
<h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude Code transcript</a> - page {{ page_num }}/{{ total_pages }}</h1>
78
{{ pagination_html|safe }}
89
{{ messages_html|safe }}
910
{{ pagination_html|safe }}
11+
{%- else -%}
12+
<h1>Claude Code transcript</h1>
13+
{{ messages_html|safe }}
14+
{%- endif %}
1015
{%- endblock %}

tests/test_all.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,10 +281,14 @@ def test_handles_failed_session_gracefully(self, output_dir):
281281
# Patch generate_html to fail on one specific session
282282
original_generate_html = __import__("claude_code_transcripts").generate_html
283283

284-
def mock_generate_html(json_path, output_dir, github_repo=None):
284+
def mock_generate_html(
285+
json_path, output_dir, github_repo=None, paginate=False
286+
):
285287
if "session1" in str(json_path):
286288
raise RuntimeError("Simulated failure")
287-
return original_generate_html(json_path, output_dir, github_repo)
289+
return original_generate_html(
290+
json_path, output_dir, github_repo, paginate=paginate
291+
)
288292

289293
with patch(
290294
"claude_code_transcripts.generate_html", side_effect=mock_generate_html

0 commit comments

Comments
 (0)