From 17a2321861cc05370f686097cb1c0e1ce7ad428a Mon Sep 17 00:00:00 2001 From: Stan Date: Wed, 25 Mar 2026 15:22:47 +0100 Subject: [PATCH] Collapse skill content blocks in user messages When Claude Code injects skill definitions (SKILL.md content) into user messages, they can be 5-9KB each and appear multiple times per session, making transcripts unreadable. This change detects skill content blocks (identified by "Base directory for this skill:" pattern) and renders them as collapsed
elements showing only the skill name. - Add detect_skill_content() to identify skill injection blocks - Add render_user_content_block() to handle skill collapsing in user messages - Add skill_content macro and CSS for collapsed display - Filter skill content from index text extraction and long_texts analysis - Skill content remains accessible via click-to-expand Related to #33 (long user message content should be truncatable). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude_code_transcripts/__init__.py | 58 ++++++++++++++++++- .../templates/macros.html | 5 ++ ...enerateHtml.test_generates_index_html.html | 5 ++ ...rateHtml.test_generates_page_001_html.html | 5 ++ ...rateHtml.test_generates_page_002_html.html | 5 ++ ...SessionFile.test_jsonl_generates_html.html | 5 ++ 6 files changed, 80 insertions(+), 3 deletions(-) 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) %} 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); }