Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5047190
Add slash-command and command-output types to timeline
cboos Dec 14, 2025
beedb70
Fix bash message type and associate with User filter
cboos Dec 14, 2025
9958bc1
Improve bash message titles for clarity
cboos Dec 14, 2025
2ad5564
Move Sub-assistant emoji to template layer
cboos Dec 14, 2025
973f9f3
Make Command output neutral and document slash command ambiguity
cboos Dec 14, 2025
00070e9
Fix traceback formatting in error handlers
cboos Dec 14, 2025
73bf3ba
Fix agent file double-loading and repeated insertion
cboos Dec 14, 2025
50fb5c3
Fix deduplication dropping summary entries
cboos Dec 14, 2025
79cdae0
Validate RGB values in ANSI color parsing
cboos Dec 14, 2025
585489a
Fix security and correctness issues in tool result formatting
cboos Dec 14, 2025
70f3921
Add truncation indicator for long slash command content preview
cboos Dec 14, 2025
c62000e
Use shared markdown renderer for command output
cboos Dec 14, 2025
3a5690f
Preserve indentation in Pygments markdown code blocks
cboos Dec 14, 2025
14303ee
Use splitlines() for accurate line counting in collapsibles
cboos Dec 14, 2025
c87a2b0
Use PrivateAttr for _parsed_input in ToolUseContent
cboos Dec 14, 2025
9e47760
Normalize parse_message_content() to always return List[ContentItem]
cboos Dec 14, 2025
b888d69
Fix outdated file references in FOLD_STATE_DIAGRAM.md
cboos Dec 14, 2025
61c38d8
Fix documentation inconsistencies and MD040 violations
cboos Dec 14, 2025
8e06b73
Add effective assertions to test_user_compacted_sidechain
cboos Dec 14, 2025
dc918b8
Fix timezone handling in date filtering
cboos Dec 14, 2025
5d7fb0b
Handle SGR reset form ESC[m (empty params) in ANSI parser
cboos Dec 14, 2025
0a0556d
Validate base64 image data before embedding in HTML
cboos Dec 14, 2025
a333f72
Fix line counting and truncation indicators in user_formatters
cboos Dec 14, 2025
b890857
Remove duplicate test_extract_text_content_length_empty_list
cboos Dec 14, 2025
1c4c6d4
Remove unused Union import from utils.py
cboos Dec 14, 2025
b2afc49
Use Python 3.10+ built-in generics instead of typing.Dict/List
cboos Dec 14, 2025
1ccee7b
Simplify UTC conversion in format_timestamp
cboos Dec 14, 2025
ec1eb30
Fix test_date_filtering to use UTC timestamps consistently
cboos Dec 14, 2025
55e5b71
Improve test quality and clarity
cboos Dec 14, 2025
5df4b05
Cache renderers and template environment for performance
cboos Dec 14, 2025
d9474f6
Refactor user_formatters.py for consistency and cleaner output
cboos Dec 14, 2025
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
46 changes: 23 additions & 23 deletions claude_code_log/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import json
from pathlib import Path
from typing import Any, Dict, List, Optional, cast
from typing import Any, Optional, cast
from datetime import datetime
from pydantic import BaseModel
from packaging import version
Expand All @@ -18,7 +18,7 @@ class CachedFileInfo(BaseModel):
source_mtime: float
cached_mtime: float
message_count: int
session_ids: List[str]
session_ids: list[str]


class SessionCacheData(BaseModel):
Expand Down Expand Up @@ -46,7 +46,7 @@ class ProjectCache(BaseModel):
project_path: str

# File-level cache information
cached_files: Dict[str, CachedFileInfo]
cached_files: dict[str, CachedFileInfo]

# Aggregated project information
total_message_count: int = 0
Expand All @@ -56,10 +56,10 @@ class ProjectCache(BaseModel):
total_cache_read_tokens: int = 0

# Session metadata
sessions: Dict[str, SessionCacheData]
sessions: dict[str, SessionCacheData]

# Working directories associated with this project
working_directories: List[str] = []
working_directories: list[str] = []

# Timeline information
earliest_timestamp: str = ""
Expand Down Expand Up @@ -154,7 +154,7 @@ def is_file_cached(self, jsonl_path: Path) -> bool:
abs(source_mtime - cached_info.source_mtime) < 1.0 and cache_file.exists()
)

def load_cached_entries(self, jsonl_path: Path) -> Optional[List[TranscriptEntry]]:
def load_cached_entries(self, jsonl_path: Path) -> Optional[list[TranscriptEntry]]:
"""Load cached transcript entries for a JSONL file."""
if not self.is_file_cached(jsonl_path):
return None
Expand All @@ -165,11 +165,11 @@ def load_cached_entries(self, jsonl_path: Path) -> Optional[List[TranscriptEntry
cache_data = json.load(f)

# Expect timestamp-keyed format - flatten all entries
entries_data: List[Dict[str, Any]] = []
entries_data: list[dict[str, Any]] = []
for timestamp_entries in cache_data.values():
if isinstance(timestamp_entries, list):
# Type cast to ensure Pyright knows this is List[Dict[str, Any]]
entries_data.extend(cast(List[Dict[str, Any]], timestamp_entries))
# Type cast to ensure Pyright knows this is list[dict[str, Any]]
entries_data.extend(cast(list[dict[str, Any]], timestamp_entries))

# Deserialize back to TranscriptEntry objects
from .parser import parse_transcript_entry
Expand All @@ -184,7 +184,7 @@ def load_cached_entries(self, jsonl_path: Path) -> Optional[List[TranscriptEntry

def load_cached_entries_filtered(
self, jsonl_path: Path, from_date: Optional[str], to_date: Optional[str]
) -> Optional[List[TranscriptEntry]]:
) -> Optional[list[TranscriptEntry]]:
"""Load cached entries with efficient timestamp-based filtering."""
if not self.is_file_cached(jsonl_path):
return None
Expand Down Expand Up @@ -226,15 +226,15 @@ def load_cached_entries_filtered(
)

# Filter entries by timestamp
filtered_entries_data: List[Dict[str, Any]] = []
filtered_entries_data: list[dict[str, Any]] = []

for timestamp_key, timestamp_entries in cache_data.items():
if timestamp_key == "_no_timestamp":
# Always include entries without timestamps (like summaries)
if isinstance(timestamp_entries, list):
# Type cast to ensure Pyright knows this is List[Dict[str, Any]]
# Type cast to ensure Pyright knows this is list[dict[str, Any]]
filtered_entries_data.extend(
cast(List[Dict[str, Any]], timestamp_entries)
cast(list[dict[str, Any]], timestamp_entries)
)
else:
# Check if timestamp falls within range
Expand All @@ -251,9 +251,9 @@ def load_cached_entries_filtered(
continue

if isinstance(timestamp_entries, list):
# Type cast to ensure Pyright knows this is List[Dict[str, Any]]
# Type cast to ensure Pyright knows this is list[dict[str, Any]]
filtered_entries_data.extend(
cast(List[Dict[str, Any]], timestamp_entries)
cast(list[dict[str, Any]], timestamp_entries)
)

# Deserialize filtered entries
Expand All @@ -271,14 +271,14 @@ def load_cached_entries_filtered(
return None

def save_cached_entries(
self, jsonl_path: Path, entries: List[TranscriptEntry]
self, jsonl_path: Path, entries: list[TranscriptEntry]
) -> None:
"""Save parsed transcript entries to cache with timestamp-based structure."""
cache_file = self._get_cache_file_path(jsonl_path)

try:
# Create timestamp-keyed cache structure for efficient date filtering
cache_data: Dict[str, Any] = {}
cache_data: dict[str, Any] = {}

for entry in entries:
# Get timestamp - use empty string as fallback for entries without timestamps
Expand Down Expand Up @@ -306,7 +306,7 @@ def save_cached_entries(
cached_mtime = cache_file.stat().st_mtime

# Extract session IDs from entries
session_ids: List[str] = []
session_ids: list[str] = []
for entry in entries:
if hasattr(entry, "sessionId"):
session_id = getattr(entry, "sessionId", "")
Expand All @@ -326,7 +326,7 @@ def save_cached_entries(
except Exception as e:
print(f"Warning: Failed to save cached entries to {cache_file}: {e}")

def update_session_cache(self, session_data: Dict[str, SessionCacheData]) -> None:
def update_session_cache(self, session_data: dict[str, SessionCacheData]) -> None:
"""Update cached session information."""
if self._project_cache is None:
return
Expand Down Expand Up @@ -360,17 +360,17 @@ def update_project_aggregates(

self._save_project_cache()

def update_working_directories(self, working_directories: List[str]) -> None:
def update_working_directories(self, working_directories: list[str]) -> None:
"""Update the list of working directories associated with this project."""
if self._project_cache is None:
return

self._project_cache.working_directories = working_directories
self._save_project_cache()

def get_modified_files(self, jsonl_files: List[Path]) -> List[Path]:
def get_modified_files(self, jsonl_files: list[Path]) -> list[Path]:
"""Get list of JSONL files that need to be reprocessed."""
modified_files: List[Path] = []
modified_files: list[Path] = []

for jsonl_file in jsonl_files:
if not self.is_file_cached(jsonl_file):
Expand Down Expand Up @@ -450,7 +450,7 @@ def _is_cache_version_compatible(self, cache_version: str) -> bool:
# If no breaking changes affect this cache version, it's compatible
return True

def get_cache_stats(self) -> Dict[str, Any]:
def get_cache_stats(self) -> dict[str, Any]:
"""Get cache statistics for reporting."""
if self._project_cache is None:
return {"cache_enabled": False}
Expand Down
18 changes: 9 additions & 9 deletions claude_code_log/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os
import sys
from pathlib import Path
from typing import Optional, List
from typing import Optional

import click
from git import Repo, InvalidGitRepositoryError
Expand Down Expand Up @@ -108,7 +108,7 @@ def convert_project_path_to_claude_dir(

def find_projects_by_cwd(
projects_dir: Path, current_cwd: Optional[str] = None
) -> List[Path]:
) -> list[Path]:
"""Find Claude projects that match the current working directory.

Uses three-tier priority matching:
Expand Down Expand Up @@ -148,8 +148,8 @@ def find_projects_by_cwd(


def _find_exact_matches(
project_dirs: List[Path], current_cwd_path: Path, base_projects_dir: Path
) -> List[Path]:
project_dirs: list[Path], current_cwd_path: Path, base_projects_dir: Path
) -> list[Path]:
"""Find projects with exact working directory matches using path-based matching."""
expected_project_dir = convert_project_path_to_claude_dir(
current_cwd_path, base_projects_dir
Expand All @@ -163,8 +163,8 @@ def _find_exact_matches(


def _find_git_root_matches(
project_dirs: List[Path], current_cwd_path: Path, base_projects_dir: Path
) -> List[Path]:
project_dirs: list[Path], current_cwd_path: Path, base_projects_dir: Path
) -> list[Path]:
"""Find projects that match the git repository root using path-based matching."""
try:
# Check if we're inside a git repository
Expand All @@ -182,10 +182,10 @@ def _find_git_root_matches(


def _find_relative_matches(
project_dirs: List[Path], current_cwd_path: Path
) -> List[Path]:
project_dirs: list[Path], current_cwd_path: Path
) -> list[Path]:
"""Find projects using relative path matching (original behavior)."""
relative_matches: List[Path] = []
relative_matches: list[Path] = []

for project_dir in project_dirs:
try:
Expand Down
Loading
Loading