Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ee9b7c4
Add --compact rendering option to show only user and assistant text
cboos Feb 28, 2026
38122cb
Add tests for --compact rendering mode
cboos Mar 1, 2026
638cb30
Fix compact mode to filter slash commands, sidechains, and other user…
cboos Mar 1, 2026
860d0a6
Fix pyright type errors in compact filter
cboos Mar 1, 2026
66f76ef
Fix ty type warnings in compact mode tests
cboos Mar 1, 2026
8a88067
Rename --compact to --shallow across codebase
cboos Mar 1, 2026
b0e67dd
Fix rebase integration: session-id tuple unpack and shallow regeneration
cboos Apr 16, 2026
3f362d0
Replace --shallow with --detail level system
cboos Apr 16, 2026
70158c8
Add tests for HIGH and LOW detail levels
cboos Apr 16, 2026
0433734
Add --compact flag for Markdown heading merging
cboos Apr 16, 2026
076491c
Add compact to should_regenerate conditions
cboos Apr 16, 2026
dbf7293
Drop dead branch nav links and auto-expand on anchor jump
cboos Apr 17, 2026
fbd10c8
Collapse structural side-branches instead of faking forks
cboos Apr 17, 2026
c6aa714
Nest branch-headers under parent session for fold cascade
cboos Apr 17, 2026
b5daacc
Surface compact_boundary ruptures as nav landmarks
cboos Apr 17, 2026
109388e
Update dev-docs/dag.md with phase-2 DAG remediation
cboos Apr 17, 2026
fe41768
Collapse mixed structural side-branches (regression fix)
cboos Apr 17, 2026
4f13d1a
Enrich compact landmark labels with preTokens and timestamp
cboos Apr 17, 2026
176a6ed
Document generalized structural collapse and enriched compact label
cboos Apr 17, 2026
7f4c826
Preserve RenderingContext index invariant after detail filter
cboos Apr 18, 2026
b2a1455
Wire --detail and --compact into --session-id export path
cboos Apr 18, 2026
590acc0
Reject boolean preTokens explicitly in compact-boundary parsing
cboos Apr 18, 2026
29e191d
Track rendered heading category in compact mode
cboos Apr 18, 2026
13c94b5
Fall back to fold-one-level when no fold-all-levels control exists
cboos Apr 18, 2026
356b708
Broaden detail param type to DetailLevel | str
cboos Apr 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions claude_code_log/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,23 @@ def _clear_output_files(input_path: Path, all_projects: bool, file_ext: str) ->
default=None,
help="Export a single session by ID (full ID or prefix). Project path is optional — looks up the session globally via cache.",
)
@click.option(
"--detail",
type=click.Choice(["full", "high", "low", "minimal"], case_sensitive=False),
default="full",
help=(
"Detail level for output. "
"full: everything; "
"high: detailed but cleaned (no system/hook noise); "
"low: interaction-focused + key signals; "
"minimal: user + assistant messages only."
),
)
@click.option(
"--compact",
is_flag=True,
help="Merge consecutive same-type headings in Markdown output.",
)
Comment on lines +516 to +532
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect generate_single_session_file signature and all call sites.
# Expectation: either generate_single_session_file accepts detail/compact, or it needs to be extended before cli.py passes them.

rg -nP -C5 '\bdef\s+generate_single_session_file\s*\('
rg -nP -C5 '\bgenerate_single_session_file\s*\('

Repository: daaain/claude-code-log

Length of output: 10029


🏁 Script executed:

# Get the full signature of generate_single_session_file
sed -n '1838,1850p' claude_code_log/converter.py

Repository: daaain/claude-code-log

Length of output: 478


🏁 Script executed:

# Check the --session-id code path around line 732
sed -n '700,750p' claude_code_log/cli.py | cat -n

Repository: daaain/claude-code-log

Length of output: 2552


🏁 Script executed:

# Search for convert_jsonl_to calls within generate_single_session_file
sed -n '1838,2000p' claude_code_log/converter.py | rg -C3 'convert_jsonl_to'

Repository: daaain/claude-code-log

Length of output: 48


🏁 Script executed:

# Get the signature of convert_jsonl_to
rg -nP -C2 'def\s+convert_jsonl_to\s*\(' claude_code_log/converter.py | head -30

Repository: daaain/claude-code-log

Length of output: 153


🏁 Script executed:

# Get the full signature of convert_jsonl_to
sed -n '1116,1130p' claude_code_log/converter.py

Repository: daaain/claude-code-log

Length of output: 553


🏁 Script executed:

# Get the full body of generate_single_session_file
sed -n '1838,1920p' claude_code_log/converter.py

Repository: daaain/claude-code-log

Length of output: 3031


🏁 Script executed:

# Continue reading generate_single_session_file to see what it calls
sed -n '1920,2000p' claude_code_log/converter.py

Repository: daaain/claude-code-log

Length of output: 2795


🏁 Script executed:

# Search for where the actual conversion happens in generate_single_session_file
rg -n 'def generate_single_session_file' -A 150 claude_code_log/converter.py | tail -100

Repository: daaain/claude-code-log

Length of output: 3913


🏁 Script executed:

# Check if renderer.generate_session has any reference to detail/compact
rg -n 'def generate_session' claude_code_log/converter.py

Repository: daaain/claude-code-log

Length of output: 48


🏁 Script executed:

# Check the CLI to understand the detail and compact options better
sed -n '516,550p' claude_code_log/cli.py | cat -n

Repository: daaain/claude-code-log

Length of output: 1216


🏁 Script executed:

# See the full main function signature including detail and compact parameters
sed -n '516,580p' claude_code_log/cli.py | cat -n

Repository: daaain/claude-code-log

Length of output: 2492


🏁 Script executed:

# Search for where detail_level is used in cli.py
rg -n 'detail_level\|detail:' claude_code_log/cli.py | head -20

Repository: daaain/claude-code-log

Length of output: 48


🏁 Script executed:

# Search for convert_jsonl_to calls in cli.py to see if detail_level is passed
rg -n 'convert_jsonl_to' claude_code_log/cli.py -A 10

Repository: daaain/claude-code-log

Length of output: 1352


🏁 Script executed:

# Check line 732 and surrounding context to find the early return for --session-id
sed -n '690,740p' claude_code_log/cli.py | cat -n

Repository: daaain/claude-code-log

Length of output: 2608


🏁 Script executed:

# Find the renderer classes and generate_session method
rg -n 'class.*Renderer' claude_code_log/ -A 5

Repository: daaain/claude-code-log

Length of output: 1297


🏁 Script executed:

# Search for generate_session implementations
rg -n 'def generate_session' claude_code_log/ -B 2 -A 10 | head -50

Repository: daaain/claude-code-log

Length of output: 3350


🏁 Script executed:

# Check how convert_jsonl_to calls the renderer methods
rg -n 'def convert_jsonl_to' -A 100 claude_code_log/converter.py | grep -A 20 'renderer\.' | head -40

Repository: daaain/claude-code-log

Length of output: 48


🏁 Script executed:

# Check the full generate_session signature in MarkdownRenderer to see if it accepts detail/compact
sed -n '920,960p' claude_code_log/markdown/renderer.py

Repository: daaain/claude-code-log

Length of output: 1643


🏁 Script executed:

# Check if generate_session calls generate() and if generate() accepts/uses detail or compact
sed -n '920,945p' claude_code_log/markdown/renderer.py

Repository: daaain/claude-code-log

Length of output: 1027


🏁 Script executed:

# Look at the generate method signature
rg -n 'def generate\(' claude_code_log/markdown/renderer.py -A 8 | head -30

Repository: daaain/claude-code-log

Length of output: 436


🏁 Script executed:

# Search for where detail or compact is actually used in the rendering
rg -n 'detail|compact' claude_code_log/renderer.py | head -30

Repository: daaain/claude-code-log

Length of output: 1999


🏁 Script executed:

# Check how convert_jsonl_to uses detail_level and compact
sed -n '1116,1200p' claude_code_log/converter.py

Repository: daaain/claude-code-log

Length of output: 3701


🏁 Script executed:

# Check if renderer.generate_session actually uses detail/compact by searching for where these would be used
rg -n 'generate_individual_sessions' claude_code_log/converter.py -B 5 -A 15 | head -50

Repository: daaain/claude-code-log

Length of output: 1805


🏁 Script executed:

# Check the full flow in convert_jsonl_to to see how it uses detail/compact with individual sessions
sed -n '1170,1230p' claude_code_log/converter.py

Repository: daaain/claude-code-log

Length of output: 2796


🏁 Script executed:

# Check if detail is used in renderer.generate() method
sed -n '879,950p' claude_code_log/markdown/renderer.py | grep -i 'detail\|compact'

Repository: daaain/claude-code-log

Length of output: 133


🏁 Script executed:

# Check how convert_jsonl_to passes detail to the rendering process
sed -n '1220,1280p' claude_code_log/converter.py

Repository: daaain/claude-code-log

Length of output: 2612


🏁 Script executed:

# Check how get_renderer is called in the session path vs main path
rg -n 'get_renderer' claude_code_log/

Repository: daaain/claude-code-log

Length of output: 757


🏁 Script executed:

# Check how the Renderer is initialized with detail/compact
sed -n '2633,2700p' claude_code_log/renderer.py | head -60

Repository: daaain/claude-code-log

Length of output: 2413


🏁 Script executed:

# Get the full get_renderer signature
sed -n '2923,2960p' claude_code_log/renderer.py

Repository: daaain/claude-code-log

Length of output: 1484


🏁 Script executed:

# Confirm the issue: Line 1951 in converter.py (generate_single_session_file)
sed -n '1948,1958p' claude_code_log/converter.py

Repository: daaain/claude-code-log

Length of output: 478


Wire --detail and --compact into --session-id exports.

The --session-id branch returns at line 733 before reaching the convert_jsonl_to(...) calls that receive these options, so claude-code-log --session-id ... --detail minimal and --compact are silently ignored. The generate_single_session_file() function needs to accept these parameters and pass them to get_renderer().

🔎 Required changes
  1. Extend generate_single_session_file() signature in converter.py to accept detail and compact parameters
  2. Pass these to get_renderer() at line 1951
  3. Update the call site in cli.py (lines 721–729) to pass detail=detail_level and compact=compact
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@claude_code_log/cli.py` around lines 516 - 532, The --detail and --compact
CLI options are never forwarded when exporting a single session because
generate_single_session_file() doesn't accept them and the CLI returns before
calling convert_jsonl_to; update generate_single_session_file() in converter.py
to add detail and compact parameters, pass those through to get_renderer() where
it's invoked (around the get_renderer(...) call), and then update the CLI call
site that invokes generate_single_session_file() (the --session-id branch in
cli.py) to call it with detail=detail_level and compact=compact so the renderer
receives and applies those options.

@click.option(
"--debug",
is_flag=True,
Expand All @@ -536,6 +553,8 @@ def main(
image_export_mode: Optional[str],
page_size: int,
session_id: Optional[str],
detail: str,
compact: bool,
debug: bool,
) -> None:
"""Convert Claude transcript JSONL files to HTML or Markdown.
Expand All @@ -545,6 +564,10 @@ def main(
# Configure logging to show warnings and above
logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s")

from .models import DetailLevel

detail_level = DetailLevel(detail.lower())

try:
# Handle TUI mode
if tui:
Expand Down Expand Up @@ -702,6 +725,8 @@ def main(
output,
not no_cache,
image_export_mode,
detail=detail_level,
compact=compact,
)
click.echo(f"Successfully exported session to {output_path}")
if open_browser:
Expand Down Expand Up @@ -745,6 +770,8 @@ def main(
output_format,
image_export_mode,
page_size=page_size,
detail=detail_level,
compact=compact,
)

# Count processed projects
Expand Down Expand Up @@ -797,6 +824,8 @@ def main(
not no_cache,
image_export_mode=image_export_mode,
page_size=page_size,
detail=detail_level,
compact=compact,
)
if input_path.is_file():
click.echo(f"Successfully converted {input_path} to {output_path}")
Expand Down
43 changes: 36 additions & 7 deletions claude_code_log/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from .factories import create_transcript_entry
from .models import (
BaseTranscriptEntry,
DetailLevel,
PassthroughTranscriptEntry,
TranscriptEntry,
AssistantTranscriptEntry,
Expand Down Expand Up @@ -898,6 +899,8 @@ def _generate_paginated_html(
working_directories: List[str],
silent: bool = False,
session_tree: Optional[SessionTree] = None,
detail: DetailLevel = DetailLevel.FULL,
compact: bool = False,
) -> Path:
"""Generate paginated HTML files for combined transcript.

Expand All @@ -914,7 +917,7 @@ def _generate_paginated_html(
Returns:
Path to the first page (combined_transcripts.html)
"""
from .html.renderer import generate_html
from .html.renderer import HtmlRenderer
from .utils import format_timestamp

# Check if page size changed - if so, invalidate all pages
Expand Down Expand Up @@ -1053,7 +1056,10 @@ def _generate_paginated_html(

# Generate HTML for this page
page_title = f"{title} - Page {page_num}" if page_num > 1 else title
html_content = generate_html(
page_renderer = HtmlRenderer()
page_renderer.detail = detail
page_renderer.compact = compact
html_content = page_renderer.generate(
page_messages,
page_title,
page_info=page_info,
Expand Down Expand Up @@ -1118,6 +1124,8 @@ def convert_jsonl_to(
silent: bool = False,
image_export_mode: Optional[str] = None,
page_size: int = 2000,
detail: DetailLevel = DetailLevel.FULL,
compact: bool = False,
) -> Path:
"""Convert JSONL transcript(s) to the specified format.

Expand All @@ -1133,6 +1141,7 @@ def convert_jsonl_to(
image_export_mode: Image export mode ("placeholder", "embedded", "referenced").
page_size: Maximum messages per page for combined transcript pagination.
If None, uses format default (embedded for HTML, referenced for Markdown).
detail: Output detail level (full, high, low, minimal).
"""
if not input_path.exists():
raise FileNotFoundError(f"Input path not found: {input_path}")
Expand Down Expand Up @@ -1231,7 +1240,7 @@ def convert_jsonl_to(

# Generate combined output file (check if regeneration needed)
assert output_path is not None
renderer = get_renderer(format, image_export_mode)
renderer = get_renderer(format, image_export_mode, detail=detail, compact=compact)

# Decide whether to use pagination (HTML only, directory mode, no date filter)
use_pagination = False
Expand Down Expand Up @@ -1283,6 +1292,8 @@ def convert_jsonl_to(
working_directories,
silent=silent,
session_tree=session_tree,
detail=detail,
compact=compact,
)
else:
# Use single-file generation for small projects or filtered views
Expand All @@ -1295,6 +1306,8 @@ def convert_jsonl_to(
or from_date is not None
or to_date is not None
or not output_path.exists()
or detail != DetailLevel.FULL
or compact
)
else:
# Fallback: old logic for single file mode or no cache
Expand All @@ -1304,6 +1317,8 @@ def convert_jsonl_to(
or to_date is not None
or not output_path.exists()
or (input_path.is_dir() and cache_was_updated)
or detail != DetailLevel.FULL
or compact
)

if should_regenerate:
Expand Down Expand Up @@ -1338,6 +1353,8 @@ def convert_jsonl_to(
image_export_mode,
silent=silent,
session_tree=session_tree,
detail=detail,
compact=compact,
)

return output_path
Expand Down Expand Up @@ -1689,6 +1706,8 @@ def _generate_individual_session_files(
image_export_mode: Optional[str] = None,
silent: bool = False,
session_tree: Optional[SessionTree] = None,
detail: DetailLevel = DetailLevel.FULL,
compact: bool = False,
) -> int:
"""Generate individual files for each session in the specified format.

Expand Down Expand Up @@ -1728,7 +1747,7 @@ def _generate_individual_session_files(
project_title = get_project_display_name(output_dir.name, working_directories)

# Get renderer once outside the loop
renderer = get_renderer(format, image_export_mode)
renderer = get_renderer(format, image_export_mode, detail=detail, compact=compact)
regenerated_count = 0

# Generate HTML file for each session
Expand Down Expand Up @@ -1823,6 +1842,8 @@ def generate_single_session_file(
output: Optional[Path] = None,
use_cache: bool = True,
image_export_mode: Optional[str] = None,
detail: DetailLevel = DetailLevel.FULL,
compact: bool = False,
) -> Path:
"""Generate a single session output file for the given session ID.

Expand All @@ -1833,6 +1854,8 @@ def generate_single_session_file(
output: Optional output file path (defaults to session-{id}.{ext} in input_path)
use_cache: Whether to use caching
image_export_mode: Image export mode
detail: Output detail level.
compact: Whether to merge consecutive same-type headings (Markdown only).

Returns:
Path to the generated file
Expand Down Expand Up @@ -1929,7 +1952,7 @@ def generate_single_session_file(
output_file = input_path / f"session-{matched_id}.{ext}"

# Generate content and write
renderer = get_renderer(format, image_export_mode)
renderer = get_renderer(format, image_export_mode, detail=detail, compact=compact)
session_content = renderer.generate_session(
session_messages, matched_id, session_title, cache_manager, output_dir
)
Expand Down Expand Up @@ -1992,6 +2015,8 @@ def process_projects_hierarchy(
image_export_mode: Optional[str] = None,
silent: bool = True,
page_size: int = 2000,
detail: DetailLevel = DetailLevel.FULL,
compact: bool = False,
) -> Path:
"""Process the entire ~/.claude/projects/ hierarchy and create linked output files.

Expand Down Expand Up @@ -2148,6 +2173,8 @@ def process_projects_hierarchy(
silent=silent,
image_export_mode=image_export_mode,
page_size=page_size,
detail=detail,
compact=compact,
)

# Track timing
Expand All @@ -2158,9 +2185,11 @@ def process_projects_hierarchy(
progress_parts.append(f"{stats.files_updated} files updated")
if stats.sessions_regenerated > 0:
progress_parts.append(f"{stats.sessions_regenerated} sessions")
detail = ", ".join(progress_parts) if progress_parts else "regenerated"
progress_detail = (
", ".join(progress_parts) if progress_parts else "regenerated"
)
print(
f" {project_dir.name}: {detail}{archived_suffix} ({stats.total_time:.1f}s)"
f" {project_dir.name}: {progress_detail}{archived_suffix} ({stats.total_time:.1f}s)"
)

# Get project info for index - use cached data if available
Expand Down
Loading
Loading