feat: v0.4.0 — rich content support with typed blocks and loss visibility
Extracts per-message content into a typed `blocks` list (text, code, thinking, tool_use, tool_result, image_placeholder, file_placeholder, unknown) and renders them at exporter write time. Voice transcripts, Custom Instructions, and image references now appear in exports instead of being silently dropped. Foundation: - src/blocks.py: pure block constructors, _safe_fence (fence-corruption defense, verified live in Joplin), _blockquote_prefix, render - src/loss_report.py: per-run tally surfaced as INFO summary at end of export so silently-dropped data becomes visible Providers: - ChatGPT: dispatch on content_type produces typed blocks; voice shapes (audio_transcription, audio_asset_pointer, real_time_user_audio_video_ asset_pointer) locked from live DevTools capture; Custom Instructions bug fix (parts-vs-direct-fields); role filter lifted; hidden-context marker driven by is_visually_hidden_from_conversation flag - Claude: defensive dispatch for text/thinking/tool_use/tool_result/image with recursive nested-block flattening; untested against real rich- content data — fix-forward in v0.4.1 Exporter: - Markdown renders from blocks at write time via render_blocks_to_markdown; backward-compat fallback to content for any pre-v0.4.0 cached data Tests: - 27 new tests across providers, exporters, CLI; fixtures rebuilt with real-shape ChatGPT voice + Custom Instructions cases - 181/181 pass Behavior changes (intentional): - JSON output omits content; consumers should read blocks - Per-conversation message counts increase (Custom Instructions, image- only, tool-only messages now appear) - Existing exports not auto-re-rendered; users wanting fresh output run cache --clear then export Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
4798edcea7
commit
473d02f71a
@@ -6,6 +6,7 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from src.blocks import render_blocks_to_markdown
|
||||
from src.utils import build_export_path, generate_filename
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -125,10 +126,17 @@ class MarkdownExporter:
|
||||
# Messages
|
||||
for msg in messages:
|
||||
role = msg.get("role", "user")
|
||||
content = msg.get("content", "")
|
||||
blocks = msg.get("blocks") or []
|
||||
timestamp = msg.get("timestamp")
|
||||
|
||||
if not content or not content.strip():
|
||||
# Prefer rendering from blocks (v0.4.0+). Backward-compat fallback:
|
||||
# if blocks is missing/empty AND content exists, render content as-is.
|
||||
if blocks:
|
||||
body = render_blocks_to_markdown(blocks)
|
||||
else:
|
||||
body = msg.get("content", "") or ""
|
||||
|
||||
if not body or not body.strip():
|
||||
logger.warning(
|
||||
"[markdown] Skipping empty/whitespace message in conversation %s",
|
||||
conv_id[:8],
|
||||
@@ -143,7 +151,7 @@ class MarkdownExporter:
|
||||
else:
|
||||
lines.append("")
|
||||
|
||||
lines.append(content)
|
||||
lines.append(body)
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
Reference in New Issue
Block a user