Skip to content

Commit e3ab28b

Browse files
Extract JSONL parser monolith into focused modules
1 parent 5e4df43 commit e3ab28b

5 files changed

Lines changed: 483 additions & 425 deletions

File tree

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171

7272
## Dispatch table
7373

74-
In `utils/jsonl_parser.py`, tool results are classified through `_parse_tool_result`, a **predicate-ordered dispatch table** (not a simple `if tool_name == ...` chain). **Order is load-bearing**: the first matching predicate wins. Tests in `tests/test_jsonl_parser.py` guard ordering regressions.
74+
In `utils/tool_dispatch.py`, tool results are classified through `_parse_tool_result`, a **predicate-ordered dispatch table** (not a simple `if tool_name == ...` chain). **Order is load-bearing**: the first matching predicate wins. Tests in `tests/test_jsonl_parser.py` and `tests/test_real_session_fixtures.py` guard ordering regressions.
7575

7676
When adding a new tool renderer:
7777

utils/jsonl_helpers.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Shared content helpers for JSONL parsing and session peek."""
2+
3+
import re
4+
from typing import Any
5+
6+
from models.session import MessageDict
7+
8+
9+
def entry_message(entry: dict[str, Any]) -> dict[str, Any]:
10+
m = entry.get("message")
11+
return m if isinstance(m, dict) else {}
12+
13+
14+
def normalize_content(content: Any) -> list[dict[str, Any]]:
15+
"""Content can be a plain string, a list of strings, or a list of typed
16+
blocks. Normalize everything into [{type, text}, ...] form."""
17+
if isinstance(content, str):
18+
return [{"type": "text", "text": content}]
19+
if isinstance(content, list):
20+
result = []
21+
for part in content:
22+
if isinstance(part, str):
23+
result.append({"type": "text", "text": part})
24+
elif isinstance(part, dict):
25+
result.append(part)
26+
return result
27+
return []
28+
29+
30+
def extract_text(content_parts: Any) -> str:
31+
"""Grab just the text blocks out of a content array, ignore tool_use/thinking."""
32+
parts = normalize_content(content_parts)
33+
texts = []
34+
for part in parts:
35+
if part.get("type") == "text":
36+
texts.append(part.get("text", ""))
37+
return "\n".join(texts)
38+
39+
40+
def extract_images(content_parts: Any) -> list[dict[str, Any]]:
41+
"""Pull base64 image blocks out of a content array.
42+
Also looks inside nested tool_result content blocks."""
43+
parts = normalize_content(content_parts)
44+
images = []
45+
for part in parts:
46+
if part.get("type") == "image":
47+
source = part.get("source", {})
48+
if source.get("type") == "base64" and source.get("data"):
49+
images.append({
50+
"media_type": source.get("media_type", "image/png"),
51+
"data": source["data"],
52+
})
53+
elif part.get("type") == "tool_result":
54+
nested = part.get("content", [])
55+
if isinstance(nested, list):
56+
for sub in nested:
57+
if isinstance(sub, dict) and sub.get("type") == "image":
58+
source = sub.get("source", {})
59+
if source.get("type") == "base64" and source.get("data"):
60+
images.append({
61+
"media_type": source.get("media_type", "image/png"),
62+
"data": source["data"],
63+
})
64+
return images
65+
66+
67+
def infer_title(messages: list[MessageDict]) -> str:
68+
"""Use the first line of the first real user message as the session title."""
69+
for msg in messages:
70+
if msg["role"] == "user" and msg.get("text"):
71+
text = strip_system_tags(msg["text"]).strip()
72+
first_line = text.split("\n")[0][:100]
73+
if first_line:
74+
return first_line
75+
return "Untitled Session"
76+
77+
78+
def strip_system_tags(text: str) -> str:
79+
"""Strip out the internal XML tags Claude Code injects (system-reminder,
80+
ide_opened_file, etc.) so exported text is clean."""
81+
# Remove block tags and their content
82+
for tag in (
83+
"system-reminder", "ide_opened_file", "user-prompt-submit-hook",
84+
"claude_background_info", "fast_mode_info", "env",
85+
):
86+
text = re.sub(rf"<{tag}>[\s\S]*?</{tag}>", "", text)
87+
# Strip remaining known opening/closing tags
88+
text = re.sub(
89+
r"</?(?:ide_selection|local-command-stdout|local-command-stderr|"
90+
r"command-name|antml:\w+|function_calls|example\w*)>",
91+
"",
92+
text,
93+
)
94+
return text.strip()

0 commit comments

Comments
 (0)