From 44e26069874e145dd25e530fc2e4602ad83561da Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Mar 2026 02:04:49 +0000 Subject: [PATCH 1/7] Add --session-id CLI flag for single session export Adds a new `--session-id` option to the CLI that exports a single conversation to HTML or Markdown without launching the TUI. Supports full session IDs and 8-char short prefixes, including archived sessions. https://claude.ai/code/session_01S99zF7j4LnfKCqbvyCXJV5 --- claude_code_log/cli.py | 39 +++++++++++ claude_code_log/converter.py | 129 +++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) diff --git a/claude_code_log/cli.py b/claude_code_log/cli.py index 2c6e3af..ad14d93 100644 --- a/claude_code_log/cli.py +++ b/claude_code_log/cli.py @@ -14,6 +14,7 @@ convert_jsonl_to, convert_jsonl_to_html, ensure_fresh_cache, + generate_single_session_file, get_file_extension, process_projects_hierarchy, ) @@ -506,6 +507,11 @@ def _clear_output_files(input_path: Path, all_projects: bool, file_ext: str) -> default=2000, help="Maximum messages per page for combined transcript (default: 2000). Sessions are never split across pages.", ) +@click.option( + "--session-id", + default=None, + help="Export a single session by ID (full ID or 8-char prefix). Requires a project directory path.", +) @click.option( "--debug", is_flag=True, @@ -528,6 +534,7 @@ def main( output_format: str, image_export_mode: Optional[str], page_size: int, + session_id: Optional[str], debug: bool, ) -> None: """Convert Claude transcript JSONL files to HTML or Markdown. @@ -648,6 +655,38 @@ def main( _launch_tui_with_cache_check(input_path) return + # Handle --session-id: export a single session by ID + if session_id is not None: + if input_path is None: + click.echo( + "Error: --session-id requires a project directory path as argument", + err=True, + ) + sys.exit(1) + + # Convert project path if needed + if not input_path.exists() or ( + input_path.is_dir() and not list(input_path.glob("*.jsonl")) + ): + claude_path = convert_project_path_to_claude_dir( + input_path, projects_dir + ) + if claude_path.exists(): + input_path = claude_path + + output_path = generate_single_session_file( + output_format, + input_path, + session_id, + output, + not no_cache, + image_export_mode, + ) + click.echo(f"Successfully exported session to {output_path}") + if open_browser: + click.launch(str(output_path)) + return + # Handle default case - process all projects hierarchy if no input path and --all-projects flag if input_path is None: input_path = projects_dir or get_default_projects_dir() diff --git a/claude_code_log/converter.py b/claude_code_log/converter.py index 2a67308..bb44c2a 100644 --- a/claude_code_log/converter.py +++ b/claude_code_log/converter.py @@ -1609,6 +1609,135 @@ def _generate_individual_session_files( return regenerated_count +def generate_single_session_file( + format: str, + input_path: Path, + session_id: str, + output: Optional[Path] = None, + use_cache: bool = True, + image_export_mode: Optional[str] = None, +) -> Path: + """Generate a single session output file for the given session ID. + + Args: + format: Output format ('html', 'md', 'markdown') + input_path: Project directory containing JSONL files + session_id: Full or 8-char prefix session ID + 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 + + Returns: + Path to the generated file + + Raises: + ValueError: If session ID not found or ambiguous + FileNotFoundError: If input_path doesn't exist or is not a directory + """ + if not input_path.exists() or not input_path.is_dir(): + raise FileNotFoundError(f"Project directory not found: {input_path}") + + # Setup cache + cache_manager = ( + CacheManager(input_path, get_library_version()) if use_cache else None + ) + + # Ensure fresh cache + ensure_fresh_cache(input_path, cache_manager, silent=True) + + # Load messages from JSONL files + messages = load_directory_transcripts(input_path, cache_manager) + + # Collect all known session IDs: from loaded messages + cache metadata + all_session_ids: set[str] = { + getattr(msg, "sessionId") + for msg in messages + if hasattr(msg, "sessionId") and getattr(msg, "sessionId") + } + if cache_manager: + project_cache = cache_manager.get_cached_project_data() + if project_cache: + all_session_ids |= set(project_cache.sessions.keys()) + + # Resolve short ID prefix to full ID + matched_id: Optional[str] = None + if session_id in all_session_ids: + matched_id = session_id + else: + matches = [sid for sid in all_session_ids if sid.startswith(session_id)] + if len(matches) == 1: + matched_id = matches[0] + elif len(matches) > 1: + raise ValueError( + f"Ambiguous session ID prefix '{session_id}' matches multiple sessions: " + + ", ".join(sorted(m[:8] for m in matches)) + ) + + if matched_id is None: + raise ValueError(f"Session '{session_id}' not found in {input_path}") + + # For archived sessions, load messages from cache if not in JSONL files + session_messages = [ + m + for m in messages + if hasattr(m, "sessionId") and getattr(m, "sessionId") == matched_id + ] + if not session_messages and cache_manager: + archived = cache_manager.load_session_entries(matched_id) + if archived: + messages = archived + session_messages = archived + + if not session_messages: + raise ValueError(f"No messages found for session '{matched_id[:8]}'") + + # Build session title from cache metadata + session_data: dict[str, Any] = {} + working_directories: list[str] = [] + if cache_manager: + project_cache = cache_manager.get_cached_project_data() + if project_cache: + session_data = {s.session_id: s for s in project_cache.sessions.values()} + working_directories = cache_manager.get_working_directories() + + project_title = get_project_display_name(input_path.name, working_directories) + + if matched_id in session_data: + session_cache_data = session_data[matched_id] + if session_cache_data.summary: + session_title = f"{project_title}: {session_cache_data.summary}" + else: + preview = session_cache_data.first_user_message + if preview and len(preview) > 50: + preview = preview[:50] + "..." + session_title = ( + f"{project_title}: {preview}" + if preview + else f"{project_title}: Session {matched_id[:8]}" + ) + else: + session_title = f"{project_title}: Session {matched_id[:8]}" + + # Determine output path + ext = get_file_extension(format) + output_dir = input_path + if output is not None: + output_file = output + output_dir = output.parent + else: + output_file = input_path / f"session-{matched_id}.{ext}" + + # Generate content and write + renderer = get_renderer(format, image_export_mode) + session_content = renderer.generate_session( + messages, matched_id, session_title, cache_manager, output_dir + ) + assert session_content is not None + output_file.write_text(session_content, encoding="utf-8") + + return output_file + + def _get_cleanup_period_days() -> Optional[int]: """Read cleanupPeriodDays from Claude Code settings. From 599f93806e4af83a731b189b6ce3f52399015b93 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Mar 2026 15:27:53 +0000 Subject: [PATCH 2/7] Add unit tests for --session-id flag and generate_single_session_file() Tests cover input validation, session ID resolution (exact/prefix/ambiguous), output path logic, no-cache mode, and session title generation from cache metadata. https://claude.ai/code/session_01Gbx7zBdyjKkQq8frBfcP5M --- test/test_cli.py | 76 ++++++++++++++ test/test_session_export.py | 198 ++++++++++++++++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 test/test_session_export.py diff --git a/test/test_cli.py b/test/test_cli.py index 320614e..e948a85 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -429,6 +429,82 @@ def test_output_option( assert output_path.exists() +class TestSessionIdOption: + """Tests for --session-id CLI option.""" + + def test_session_id_no_path_errors( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ): + """--session-id without a project path argument exits with error.""" + monkeypatch.setenv("CLAUDE_CODE_LOG_CACHE_PATH", str(tmp_path / "test.db")) + runner = CliRunner() + result = runner.invoke(main, ["--session-id", "abc12345"]) + assert result.exit_code != 0 + assert "error" in result.output.lower() or "Error" in result.output + + def test_session_id_valid_full_id( + self, cli_projects_setup: ProjectsSetup, sample_jsonl_content: list[dict] + ): + """--session-id with a valid full session ID generates file and prints success.""" + project_dir = create_project_with_jsonl( + cli_projects_setup.projects_dir, "my-project", sample_jsonl_content + ) + runner = CliRunner() + result = runner.invoke(main, [str(project_dir), "--session-id", "session-1"]) + assert result.exit_code == 0 + assert "Successfully exported session" in result.output + # Generated file must exist + assert len(list(project_dir.glob("session-session-1.*"))) > 0 + + def test_session_id_prefix( + self, cli_projects_setup: ProjectsSetup, sample_jsonl_content: list[dict] + ): + """--session-id with a unique prefix resolves to the full ID.""" + project_dir = create_project_with_jsonl( + cli_projects_setup.projects_dir, "my-project", sample_jsonl_content + ) + runner = CliRunner() + # "sess" is a valid prefix of "session-1" + result = runner.invoke(main, [str(project_dir), "--session-id", "sess"]) + assert result.exit_code == 0 + assert "Successfully exported session" in result.output + + def test_session_id_unknown_exits_nonzero( + self, cli_projects_setup: ProjectsSetup, sample_jsonl_content: list[dict] + ): + """--session-id with an unknown ID exits non-zero.""" + project_dir = create_project_with_jsonl( + cli_projects_setup.projects_dir, "my-project", sample_jsonl_content + ) + runner = CliRunner() + result = runner.invoke( + main, [str(project_dir), "--session-id", "zzzzzzzz-does-not-exist"] + ) + assert result.exit_code != 0 + + def test_session_id_with_output_flag( + self, cli_projects_setup: ProjectsSetup, sample_jsonl_content: list[dict] + ): + """--session-id combined with --output writes to the specified path.""" + project_dir = create_project_with_jsonl( + cli_projects_setup.projects_dir, "my-project", sample_jsonl_content + ) + output_path = cli_projects_setup.projects_dir / "custom-session.html" + runner = CliRunner() + result = runner.invoke( + main, + [ + str(project_dir), + "--session-id", + "session-1", + "--output", + str(output_path), + ], + ) + assert result.exit_code == 0 + assert output_path.exists() + + class TestCLIErrorHandling: """Tests for CLI error handling paths.""" diff --git a/test/test_session_export.py b/test/test_session_export.py new file mode 100644 index 0000000..4346715 --- /dev/null +++ b/test/test_session_export.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Unit tests for generate_single_session_file() in converter.py.""" + +import json +from pathlib import Path +from typing import Generator + +import pytest + +from claude_code_log.converter import generate_single_session_file + +# A UUID-style session ID whose 8-char prefix is "abc12345" +SESSION_ID_A = "abc12345-1111-2222-3333-444444444444" +SESSION_ID_B = "abc1xxxx-5555-6666-7777-888888888888" + + +def _make_jsonl_entries(session_id: str, user_message: str = "Hello") -> list[dict]: + """Minimal valid JSONL entries for a session.""" + return [ + { + "type": "user", + "uuid": f"{session_id}-user", + "timestamp": "2023-01-01T10:00:00Z", + "sessionId": session_id, + "version": "1.0.0", + "parentUuid": None, + "isSidechain": False, + "userType": "user", + "cwd": "/test", + "message": {"role": "user", "content": user_message}, + }, + { + "type": "assistant", + "uuid": f"{session_id}-asst", + "timestamp": "2023-01-01T10:01:00Z", + "sessionId": session_id, + "version": "1.0.0", + "parentUuid": None, + "isSidechain": False, + "userType": "assistant", + "cwd": "/test", + "requestId": "req-1", + "message": { + "id": "msg-1", + "type": "message", + "role": "assistant", + "model": "claude-3", + "content": [{"type": "text", "text": "Hi!"}], + "usage": {"input_tokens": 5, "output_tokens": 5}, + }, + }, + ] + + +def _write_jsonl(path: Path, entries: list[dict]) -> None: + with open(path, "w") as f: + for entry in entries: + f.write(json.dumps(entry) + "\n") + + +@pytest.fixture +def project_dir( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> Generator[Path, None, None]: + """Isolated project directory with one session (SESSION_ID_A).""" + proj = tmp_path / "my-project" + proj.mkdir() + _write_jsonl(proj / "session-a.jsonl", _make_jsonl_entries(SESSION_ID_A)) + monkeypatch.setenv("CLAUDE_CODE_LOG_CACHE_PATH", str(tmp_path / "test.db")) + yield proj + + +class TestInputValidation: + def test_raises_file_not_found_for_missing_dir( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + monkeypatch.setenv("CLAUDE_CODE_LOG_CACHE_PATH", str(tmp_path / "test.db")) + missing = tmp_path / "does-not-exist" + with pytest.raises(FileNotFoundError): + generate_single_session_file("html", missing, SESSION_ID_A) + + def test_raises_file_not_found_for_file_not_dir( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + monkeypatch.setenv("CLAUDE_CODE_LOG_CACHE_PATH", str(tmp_path / "test.db")) + f = tmp_path / "not-a-dir.jsonl" + f.write_text("{}") + with pytest.raises(FileNotFoundError): + generate_single_session_file("html", f, SESSION_ID_A) + + def test_raises_value_error_for_unknown_session(self, project_dir: Path): + with pytest.raises(ValueError, match="not found"): + generate_single_session_file( + "html", project_dir, "zzzzzzzz-0000-0000-0000-000000000000" + ) + + +class TestSessionIdResolution: + def test_exact_session_id_match(self, project_dir: Path): + result = generate_single_session_file("html", project_dir, SESSION_ID_A) + assert result.exists() + + def test_short_prefix_match(self, project_dir: Path): + # "abc12345" is the first 8 chars of SESSION_ID_A + result = generate_single_session_file("html", project_dir, "abc12345") + assert result.exists() + + def test_ambiguous_prefix_raises( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + proj = tmp_path / "ambiguous-project" + proj.mkdir() + # Both IDs start with "abc1" — providing that prefix is ambiguous + entries = _make_jsonl_entries(SESSION_ID_A) + _make_jsonl_entries(SESSION_ID_B) + _write_jsonl(proj / "sessions.jsonl", entries) + monkeypatch.setenv("CLAUDE_CODE_LOG_CACHE_PATH", str(tmp_path / "test.db")) + + with pytest.raises(ValueError, match="[Aa]mbiguous"): + generate_single_session_file("html", proj, "abc1") + + +class TestOutputPath: + def test_default_output_path(self, project_dir: Path): + result = generate_single_session_file("html", project_dir, SESSION_ID_A) + expected = project_dir / f"session-{SESSION_ID_A}.html" + assert result == expected + assert result.exists() + + def test_custom_output_path(self, project_dir: Path, tmp_path: Path): + custom = tmp_path / "out" / "my-session.html" + custom.parent.mkdir() + result = generate_single_session_file( + "html", project_dir, SESSION_ID_A, output=custom + ) + assert result == custom + assert custom.exists() + + def test_markdown_format_uses_md_extension(self, project_dir: Path): + result = generate_single_session_file("md", project_dir, SESSION_ID_A) + assert result.suffix == ".md" + assert result.exists() + + +class TestNoCacheMode: + def test_generates_file_without_cache(self, project_dir: Path): + result = generate_single_session_file( + "html", project_dir, SESSION_ID_A, use_cache=False + ) + assert result.exists() + assert result.stat().st_size > 0 + + +class TestSessionTitle: + def test_title_uses_summary(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + proj = tmp_path / "proj-summary" + proj.mkdir() + entries = _make_jsonl_entries(SESSION_ID_A) + entries.append( + { + "type": "summary", + "summary": "My special summary", + "leafUuid": f"{SESSION_ID_A}-asst", + } + ) + _write_jsonl(proj / "session-a.jsonl", entries) + monkeypatch.setenv("CLAUDE_CODE_LOG_CACHE_PATH", str(tmp_path / "test.db")) + + result = generate_single_session_file("html", proj, SESSION_ID_A) + content = result.read_text(encoding="utf-8") + assert "My special summary" in content + + def test_title_uses_first_user_message_truncated( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + long_msg = "A" * 60 # > 50 chars, no trailing summary entry + proj = tmp_path / "proj-preview" + proj.mkdir() + _write_jsonl( + proj / "session-a.jsonl", + _make_jsonl_entries(SESSION_ID_A, user_message=long_msg), + ) + monkeypatch.setenv("CLAUDE_CODE_LOG_CACHE_PATH", str(tmp_path / "test.db")) + + result = generate_single_session_file("html", proj, SESSION_ID_A) + content = result.read_text(encoding="utf-8") + # Truncated preview ends with "..." + assert "..." in content + # The first 50 chars of the long message should appear + assert long_msg[:50] in content + + def test_title_falls_back_to_session_short_id(self, project_dir: Path): + # use_cache=False means no session metadata → falls back to short ID + result = generate_single_session_file( + "html", project_dir, SESSION_ID_A, use_cache=False + ) + content = result.read_text(encoding="utf-8") + # The first 8 chars of the session ID must appear as the fallback label + assert SESSION_ID_A[:8] in content From deacb9475fa970f4569c4a74cea22579e9db4c8c Mon Sep 17 00:00:00 2001 From: chloelee767 Date: Sat, 14 Mar 2026 00:21:20 +0800 Subject: [PATCH 3/7] fix type issues from ty check --- claude_code_log/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/claude_code_log/cli.py b/claude_code_log/cli.py index ad14d93..160bb2a 100644 --- a/claude_code_log/cli.py +++ b/claude_code_log/cli.py @@ -665,6 +665,7 @@ def main( sys.exit(1) # Convert project path if needed + assert input_path is not None if not input_path.exists() or ( input_path.is_dir() and not list(input_path.glob("*.jsonl")) ): From 142112b6655062a0d03683f1102faac318354c60 Mon Sep 17 00:00:00 2001 From: chloelee767 Date: Sat, 14 Mar 2026 00:31:57 +0800 Subject: [PATCH 4/7] minor updates --- claude_code_log/cli.py | 2 +- claude_code_log/converter.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/claude_code_log/cli.py b/claude_code_log/cli.py index 160bb2a..83091c5 100644 --- a/claude_code_log/cli.py +++ b/claude_code_log/cli.py @@ -510,7 +510,7 @@ def _clear_output_files(input_path: Path, all_projects: bool, file_ext: str) -> @click.option( "--session-id", default=None, - help="Export a single session by ID (full ID or 8-char prefix). Requires a project directory path.", + help="Export a single session by ID (full ID or prefix). Requires a project directory path.", ) @click.option( "--debug", diff --git a/claude_code_log/converter.py b/claude_code_log/converter.py index bb44c2a..fbda9b6 100644 --- a/claude_code_log/converter.py +++ b/claude_code_log/converter.py @@ -1685,7 +1685,6 @@ def generate_single_session_file( if not session_messages and cache_manager: archived = cache_manager.load_session_entries(matched_id) if archived: - messages = archived session_messages = archived if not session_messages: @@ -1730,7 +1729,7 @@ def generate_single_session_file( # Generate content and write renderer = get_renderer(format, image_export_mode) session_content = renderer.generate_session( - messages, matched_id, session_title, cache_manager, output_dir + session_messages, matched_id, session_title, cache_manager, output_dir ) assert session_content is not None output_file.write_text(session_content, encoding="utf-8") From a2a67ac9a30e6b2f5c924039a2500cb493aac143 Mon Sep 17 00:00:00 2001 From: chloelee767 Date: Sat, 14 Mar 2026 09:09:11 +0800 Subject: [PATCH 5/7] address pr comments --- claude_code_log/converter.py | 69 ++++++++++++++++++------------------ claude_code_log/tui.py | 11 ++---- 2 files changed, 36 insertions(+), 44 deletions(-) diff --git a/claude_code_log/converter.py b/claude_code_log/converter.py index fbda9b6..81abd54 100644 --- a/claude_code_log/converter.py +++ b/claude_code_log/converter.py @@ -1469,6 +1469,22 @@ def _collect_project_sessions(messages: list[TranscriptEntry]) -> list[dict[str, ) +def build_session_title( + project_title: str, + session_id: str, + session_cache: Optional[Any], +) -> str: + if session_cache: + if session_cache.summary: + return f"{project_title}: {session_cache.summary}" + preview = session_cache.first_user_message + if preview: + if len(preview) > 50: + preview = preview[:50] + "..." + return f"{project_title}: {preview}" + return f"{project_title}: Session {session_id[:8]}" + + def _generate_individual_session_files( format: str, messages: list[TranscriptEntry], @@ -1520,23 +1536,11 @@ def _generate_individual_session_files( # Generate HTML file for each session for session_id in session_ids: # Create session-specific title using cache data if available - if session_id in session_data: - session_cache = session_data[session_id] - if session_cache.summary: - session_title = f"{project_title}: {session_cache.summary}" - else: - # Fall back to first user message preview - preview = session_cache.first_user_message - if preview and len(preview) > 50: - preview = preview[:50] + "..." - session_title = ( - f"{project_title}: {preview}" - if preview - else f"{project_title}: Session {session_id[:8]}" - ) - else: - # Fall back to basic session title - session_title = f"{project_title}: Session {session_id[:8]}" + session_title = build_session_title( + project_title, + session_id, + session_data.get(session_id), + ) # Add date range if specified if from_date or to_date: @@ -1638,9 +1642,12 @@ def generate_single_session_file( raise FileNotFoundError(f"Project directory not found: {input_path}") # Setup cache - cache_manager = ( - CacheManager(input_path, get_library_version()) if use_cache else None - ) + cache_manager = None + if use_cache: + try: + cache_manager = CacheManager(input_path, get_library_version()) + except Exception as e: + print(f"Warning: Failed to initialize cache manager: {e}") # Ensure fresh cache ensure_fresh_cache(input_path, cache_manager, silent=True) @@ -1687,6 +1694,8 @@ def generate_single_session_file( if archived: session_messages = archived + session_messages = deduplicate_messages(session_messages) + if not session_messages: raise ValueError(f"No messages found for session '{matched_id[:8]}'") @@ -1701,21 +1710,11 @@ def generate_single_session_file( project_title = get_project_display_name(input_path.name, working_directories) - if matched_id in session_data: - session_cache_data = session_data[matched_id] - if session_cache_data.summary: - session_title = f"{project_title}: {session_cache_data.summary}" - else: - preview = session_cache_data.first_user_message - if preview and len(preview) > 50: - preview = preview[:50] + "..." - session_title = ( - f"{project_title}: {preview}" - if preview - else f"{project_title}: Session {matched_id[:8]}" - ) - else: - session_title = f"{project_title}: Session {matched_id[:8]}" + session_title = build_session_title( + project_title, + matched_id, + session_data.get(matched_id), + ) # Determine output path ext = get_file_extension(format) diff --git a/claude_code_log/tui.py b/claude_code_log/tui.py index 47c5959..e88279c 100644 --- a/claude_code_log/tui.py +++ b/claude_code_log/tui.py @@ -24,6 +24,7 @@ from .cache import CacheManager, SessionCacheData, get_library_version from .converter import ( + build_session_title, ensure_fresh_cache, get_file_extension, load_directory_transcripts, @@ -1849,15 +1850,7 @@ def _ensure_session_file( self.project_path.name, project_cache.working_directories if project_cache else None, ) - if session_data and session_data.summary: - session_title = f"{project_name}: {session_data.summary}" - elif session_data and session_data.first_user_message: - preview = session_data.first_user_message - if len(preview) > 50: - preview = preview[:50] + "..." - session_title = f"{project_name}: {preview}" - else: - session_title = f"{project_name}: Session {session_id[:8]}" + session_title = build_session_title(project_name, session_id, session_data) # Generate session content session_content = renderer.generate_session( From f8d32b92bfd46adadcfdb6d18a22b54dbd982de4 Mon Sep 17 00:00:00 2001 From: chloelee767 Date: Sat, 14 Mar 2026 10:04:09 +0800 Subject: [PATCH 6/7] improve type --- claude_code_log/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude_code_log/converter.py b/claude_code_log/converter.py index 81abd54..a014d20 100644 --- a/claude_code_log/converter.py +++ b/claude_code_log/converter.py @@ -1472,7 +1472,7 @@ def _collect_project_sessions(messages: list[TranscriptEntry]) -> list[dict[str, def build_session_title( project_title: str, session_id: str, - session_cache: Optional[Any], + session_cache: Optional[SessionCacheData], ) -> str: if session_cache: if session_cache.summary: From 3b0c9f8bf3f7ecc56a6d23dfd49c18b09d358ec9 Mon Sep 17 00:00:00 2001 From: chloelee767 Date: Sat, 14 Mar 2026 10:06:24 +0800 Subject: [PATCH 7/7] add docstring --- claude_code_log/converter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/claude_code_log/converter.py b/claude_code_log/converter.py index a014d20..bcd686d 100644 --- a/claude_code_log/converter.py +++ b/claude_code_log/converter.py @@ -1474,6 +1474,11 @@ def build_session_title( session_id: str, session_cache: Optional[SessionCacheData], ) -> str: + """Build a display title for a session. + + Uses the session summary if available, otherwise the first user message + preview (truncated to 50 chars), falling back to "Session {id[:8]}". + """ if session_cache: if session_cache.summary: return f"{project_title}: {session_cache.summary}"