diff --git a/src/claude_code_transcripts/__init__.py b/src/claude_code_transcripts/__init__.py index e4854a3..f5646dd 100644 --- a/src/claude_code_transcripts/__init__.py +++ b/src/claude_code_transcripts/__init__.py @@ -48,6 +48,30 @@ def get_template(name): 300 # Characters - text blocks longer than this are shown in index ) +# Regex to detect skill content blocks injected by Claude Code +SKILL_CONTENT_PATTERN = re.compile( + r"^Base directory for this skill:\s*(.+?)$", re.MULTILINE +) +# Regex to extract skill name from the first markdown heading +SKILL_NAME_PATTERN = re.compile(r"^#\s+(.+?)$", re.MULTILINE) + + +def detect_skill_content(text): + """Detect if a text block is a skill definition injected by Claude Code. + + Returns (skill_name, True) if skill content detected, (None, False) otherwise. + """ + match = SKILL_CONTENT_PATTERN.search(text) + if match: + # Try to extract skill name from the first heading + name_match = SKILL_NAME_PATTERN.search(text) + if name_match: + return name_match.group(1).strip(), True + # Fall back to directory name + path = match.group(1).strip() + return path.rsplit("/", 1)[-1], True + return None, False + def extract_text_from_content(content): """Extract plain text from message content. @@ -62,6 +86,9 @@ def extract_text_from_content(content): The extracted text as a string, or empty string if no text found. """ if isinstance(content, str): + _, is_skill = detect_skill_content(content) + if is_skill: + return "" return content.strip() elif isinstance(content, list): # Extract text from content blocks of type "text" @@ -70,7 +97,10 @@ def extract_text_from_content(content): if isinstance(block, dict) and block.get("type") == "text": text = block.get("text", "") if text: - texts.append(text) + # Skip skill content from text extraction + _, is_skill = detect_skill_content(text) + if not is_skill: + texts.append(text) return " ".join(texts).strip() return "" @@ -840,14 +870,29 @@ def render_content_block(block): return format_json(block) +def render_user_content_block(block): + """Render a content block within a user message, collapsing skill content.""" + if isinstance(block, dict) and block.get("type") == "text": + text = block.get("text", "") + skill_name, is_skill = detect_skill_content(text) + if is_skill: + content_html = render_markdown_text(text) + return _macros.skill_content(skill_name, content_html) + return render_content_block(block) + + def render_user_message_content(message_data): content = message_data.get("content", "") if isinstance(content, str): + skill_name, is_skill = detect_skill_content(content) + if is_skill: + content_html = render_markdown_text(content) + return _macros.skill_content(skill_name, content_html) if is_json_like(content): return _macros.user_content(format_json(content)) return _macros.user_content(render_markdown_text(content)) elif isinstance(content, list): - return "".join(render_content_block(block) for block in content) + return "".join(render_user_content_block(block) for block in content) return f"

{html.escape(str(content))}

" @@ -896,7 +941,9 @@ def analyze_conversation(messages): commits.append((match.group(1), match.group(2), timestamp)) elif block_type == "text": text = block.get("text", "") - if len(text) >= LONG_TEXT_THRESHOLD: + # Skip skill content from index long texts + _, is_skill = detect_skill_content(text) + if not is_skill and len(text) >= LONG_TEXT_THRESHOLD: long_texts.append(text) return { @@ -1067,6 +1114,11 @@ def render_message(log_type, message_json, timestamp): details.continuation summary { cursor: pointer; padding: 12px 16px; background: var(--user-bg); border-left: 4px solid var(--user-border); border-radius: 12px; font-weight: 500; color: var(--text-muted); } details.continuation summary:hover { background: rgba(25, 118, 210, 0.15); } details.continuation[open] summary { border-radius: 12px 12px 0 0; margin-bottom: 0; } +details.skill-content { margin-bottom: 8px; } +details.skill-content summary { cursor: pointer; padding: 8px 12px; background: rgba(0,0,0,0.03); border-left: 3px solid var(--text-muted); border-radius: 8px; font-size: 0.85rem; color: var(--text-muted); font-style: italic; } +details.skill-content summary:hover { background: rgba(0,0,0,0.06); } +details.skill-content[open] summary { border-radius: 8px 8px 0 0; margin-bottom: 0; } +details.skill-content .message-content { padding: 8px 12px; font-size: 0.85rem; max-height: 400px; overflow-y: auto; } .index-item { margin-bottom: 16px; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); background: var(--user-bg); border-left: 4px solid var(--user-border); } .index-item a { display: block; text-decoration: none; color: inherit; } .index-item a:hover { background: rgba(25, 118, 210, 0.1); } diff --git a/src/claude_code_transcripts/templates/macros.html b/src/claude_code_transcripts/templates/macros.html index 06018d3..6d233fd 100644 --- a/src/claude_code_transcripts/templates/macros.html +++ b/src/claude_code_transcripts/templates/macros.html @@ -161,6 +161,11 @@
Session continuation summary{{ content_html|safe }}
{%- endmacro %} +{# Skill content - collapsed by default, shows only skill name #} +{% macro skill_content(skill_name, content_html) %} +
Skill: {{ skill_name }}{{ content_html|safe }}
+{%- endmacro %} + {# Index item (prompt) - rendered_content and stats_html are pre-rendered so need |safe #} {% macro index_item(prompt_num, link, timestamp, rendered_content, stats_html) %}
#{{ prompt_num }}
{{ rendered_content|safe }}
{{ stats_html|safe }}
diff --git a/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_index_html.html b/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_index_html.html index 693c48f..97331dd 100644 --- a/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_index_html.html +++ b/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_index_html.html @@ -100,6 +100,11 @@ details.continuation summary { cursor: pointer; padding: 12px 16px; background: var(--user-bg); border-left: 4px solid var(--user-border); border-radius: 12px; font-weight: 500; color: var(--text-muted); } details.continuation summary:hover { background: rgba(25, 118, 210, 0.15); } details.continuation[open] summary { border-radius: 12px 12px 0 0; margin-bottom: 0; } +details.skill-content { margin-bottom: 8px; } +details.skill-content summary { cursor: pointer; padding: 8px 12px; background: rgba(0,0,0,0.03); border-left: 3px solid var(--text-muted); border-radius: 8px; font-size: 0.85rem; color: var(--text-muted); font-style: italic; } +details.skill-content summary:hover { background: rgba(0,0,0,0.06); } +details.skill-content[open] summary { border-radius: 8px 8px 0 0; margin-bottom: 0; } +details.skill-content .message-content { padding: 8px 12px; font-size: 0.85rem; max-height: 400px; overflow-y: auto; } .index-item { margin-bottom: 16px; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); background: var(--user-bg); border-left: 4px solid var(--user-border); } .index-item a { display: block; text-decoration: none; color: inherit; } .index-item a:hover { background: rgba(25, 118, 210, 0.1); } diff --git a/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_page_001_html.html b/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_page_001_html.html index cdc794b..01a11bf 100644 --- a/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_page_001_html.html +++ b/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_page_001_html.html @@ -100,6 +100,11 @@ details.continuation summary { cursor: pointer; padding: 12px 16px; background: var(--user-bg); border-left: 4px solid var(--user-border); border-radius: 12px; font-weight: 500; color: var(--text-muted); } details.continuation summary:hover { background: rgba(25, 118, 210, 0.15); } details.continuation[open] summary { border-radius: 12px 12px 0 0; margin-bottom: 0; } +details.skill-content { margin-bottom: 8px; } +details.skill-content summary { cursor: pointer; padding: 8px 12px; background: rgba(0,0,0,0.03); border-left: 3px solid var(--text-muted); border-radius: 8px; font-size: 0.85rem; color: var(--text-muted); font-style: italic; } +details.skill-content summary:hover { background: rgba(0,0,0,0.06); } +details.skill-content[open] summary { border-radius: 8px 8px 0 0; margin-bottom: 0; } +details.skill-content .message-content { padding: 8px 12px; font-size: 0.85rem; max-height: 400px; overflow-y: auto; } .index-item { margin-bottom: 16px; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); background: var(--user-bg); border-left: 4px solid var(--user-border); } .index-item a { display: block; text-decoration: none; color: inherit; } .index-item a:hover { background: rgba(25, 118, 210, 0.1); } diff --git a/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_page_002_html.html b/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_page_002_html.html index 2d46a78..fe3ef4d 100644 --- a/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_page_002_html.html +++ b/tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_page_002_html.html @@ -100,6 +100,11 @@ details.continuation summary { cursor: pointer; padding: 12px 16px; background: var(--user-bg); border-left: 4px solid var(--user-border); border-radius: 12px; font-weight: 500; color: var(--text-muted); } details.continuation summary:hover { background: rgba(25, 118, 210, 0.15); } details.continuation[open] summary { border-radius: 12px 12px 0 0; margin-bottom: 0; } +details.skill-content { margin-bottom: 8px; } +details.skill-content summary { cursor: pointer; padding: 8px 12px; background: rgba(0,0,0,0.03); border-left: 3px solid var(--text-muted); border-radius: 8px; font-size: 0.85rem; color: var(--text-muted); font-style: italic; } +details.skill-content summary:hover { background: rgba(0,0,0,0.06); } +details.skill-content[open] summary { border-radius: 8px 8px 0 0; margin-bottom: 0; } +details.skill-content .message-content { padding: 8px 12px; font-size: 0.85rem; max-height: 400px; overflow-y: auto; } .index-item { margin-bottom: 16px; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); background: var(--user-bg); border-left: 4px solid var(--user-border); } .index-item a { display: block; text-decoration: none; color: inherit; } .index-item a:hover { background: rgba(25, 118, 210, 0.1); } diff --git a/tests/__snapshots__/test_generate_html/TestParseSessionFile.test_jsonl_generates_html.html b/tests/__snapshots__/test_generate_html/TestParseSessionFile.test_jsonl_generates_html.html index e83424a..84ed8d5 100644 --- a/tests/__snapshots__/test_generate_html/TestParseSessionFile.test_jsonl_generates_html.html +++ b/tests/__snapshots__/test_generate_html/TestParseSessionFile.test_jsonl_generates_html.html @@ -100,6 +100,11 @@ details.continuation summary { cursor: pointer; padding: 12px 16px; background: var(--user-bg); border-left: 4px solid var(--user-border); border-radius: 12px; font-weight: 500; color: var(--text-muted); } details.continuation summary:hover { background: rgba(25, 118, 210, 0.15); } details.continuation[open] summary { border-radius: 12px 12px 0 0; margin-bottom: 0; } +details.skill-content { margin-bottom: 8px; } +details.skill-content summary { cursor: pointer; padding: 8px 12px; background: rgba(0,0,0,0.03); border-left: 3px solid var(--text-muted); border-radius: 8px; font-size: 0.85rem; color: var(--text-muted); font-style: italic; } +details.skill-content summary:hover { background: rgba(0,0,0,0.06); } +details.skill-content[open] summary { border-radius: 8px 8px 0 0; margin-bottom: 0; } +details.skill-content .message-content { padding: 8px 12px; font-size: 0.85rem; max-height: 400px; overflow-y: auto; } .index-item { margin-bottom: 16px; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); background: var(--user-bg); border-left: 4px solid var(--user-border); } .index-item a { display: block; text-decoration: none; color: inherit; } .index-item a:hover { background: rgba(25, 118, 210, 0.1); }