Skip to content

Commit 84de69d

Browse files
simonwclaude
andcommitted
Fix handling of array content format in user messages
Claude Code v2.0.76+ uses array content format like: {"type": "user", "message": {"content": [{"type": "text", "text": "..."}]}} The code was only handling string content format, causing sessions with array content to show "0 prompts, 0 pages". Added extract_text_from_content() helper function that handles both formats and updated all locations that extract text from user message content. https://gistpreview.github.io/?95c8fedc49e3a34e83559c1ed352287d/index.html Closes #14 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 262a882 commit 84de69d

File tree

2 files changed

+69
-14
lines changed

2 files changed

+69
-14
lines changed

src/claude_code_transcripts/__init__.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,33 @@ def get_template(name):
4848
300 # Characters - text blocks longer than this are shown in index
4949
)
5050

51+
52+
def extract_text_from_content(content):
53+
"""Extract plain text from message content.
54+
55+
Handles both string content (older format) and array content (newer format).
56+
57+
Args:
58+
content: Either a string or a list of content blocks like
59+
[{"type": "text", "text": "..."}, {"type": "image", ...}]
60+
61+
Returns:
62+
The extracted text as a string, or empty string if no text found.
63+
"""
64+
if isinstance(content, str):
65+
return content.strip()
66+
elif isinstance(content, list):
67+
# Extract text from content blocks of type "text"
68+
texts = []
69+
for block in content:
70+
if isinstance(block, dict) and block.get("type") == "text":
71+
text = block.get("text", "")
72+
if text:
73+
texts.append(text)
74+
return " ".join(texts).strip()
75+
return ""
76+
77+
5178
# Module-level variable for GitHub repo (set by generate_html)
5279
_github_repo = None
5380

@@ -75,10 +102,11 @@ def get_session_summary(filepath, max_length=200):
75102
if entry.get("type") == "user":
76103
msg = entry.get("message", {})
77104
content = msg.get("content", "")
78-
if isinstance(content, str) and content.strip():
79-
if len(content) > max_length:
80-
return content[: max_length - 3] + "..."
81-
return content
105+
text = extract_text_from_content(content)
106+
if text:
107+
if len(text) > max_length:
108+
return text[: max_length - 3] + "..."
109+
return text
82110
return "(no summary)"
83111
except Exception:
84112
return "(no summary)"
@@ -117,12 +145,11 @@ def _get_jsonl_summary(filepath, max_length=200):
117145
and obj.get("message", {}).get("content")
118146
):
119147
content = obj["message"]["content"]
120-
if isinstance(content, str):
121-
content = content.strip()
122-
if content and not content.startswith("<"):
123-
if len(content) > max_length:
124-
return content[: max_length - 3] + "..."
125-
return content
148+
text = extract_text_from_content(content)
149+
if text and not text.startswith("<"):
150+
if len(text) > max_length:
151+
return text[: max_length - 3] + "..."
152+
return text
126153
except json.JSONDecodeError:
127154
continue
128155
except Exception:
@@ -1144,9 +1171,10 @@ def generate_html(json_path, output_dir, github_repo=None):
11441171
user_text = None
11451172
if log_type == "user":
11461173
content = message_data.get("content", "")
1147-
if isinstance(content, str) and content.strip():
1174+
text = extract_text_from_content(content)
1175+
if text:
11481176
is_user_prompt = True
1149-
user_text = content
1177+
user_text = text
11501178
if is_user_prompt:
11511179
if current_conv:
11521180
conversations.append(current_conv)
@@ -1558,9 +1586,10 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
15581586
user_text = None
15591587
if log_type == "user":
15601588
content = message_data.get("content", "")
1561-
if isinstance(content, str) and content.strip():
1589+
text = extract_text_from_content(content)
1590+
if text:
15621591
is_user_prompt = True
1563-
user_text = content
1592+
user_text = text
15641593
if is_user_prompt:
15651594
if current_conv:
15661595
conversations.append(current_conv)

tests/test_generate_html.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,32 @@ def test_github_repo_autodetect(self, sample_session):
9191
repo = detect_github_repo(loglines)
9292
assert repo == "example/project"
9393

94+
def test_handles_array_content_format(self, tmp_path):
95+
"""Test that user messages with array content format are recognized.
96+
97+
Claude Code v2.0.76+ uses array content format like:
98+
{"type": "user", "message": {"content": [{"type": "text", "text": "..."}]}}
99+
instead of the simpler string format:
100+
{"type": "user", "message": {"content": "..."}}
101+
"""
102+
jsonl_file = tmp_path / "session.jsonl"
103+
jsonl_file.write_text(
104+
'{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Hello from array format"}]}}\n'
105+
'{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hi there!"}]}}\n'
106+
)
107+
108+
output_dir = tmp_path / "output"
109+
output_dir.mkdir()
110+
111+
generate_html(jsonl_file, output_dir)
112+
113+
index_html = (output_dir / "index.html").read_text(encoding="utf-8")
114+
# Should have 1 prompt, not 0
115+
assert "1 prompts" in index_html or "1 prompt" in index_html
116+
assert "0 prompts" not in index_html
117+
# The page file should exist
118+
assert (output_dir / "page-001.html").exists()
119+
94120

95121
class TestRenderFunctions:
96122
"""Tests for individual render functions."""

0 commit comments

Comments
 (0)