use serde_json::Value; use super::{ConversationSource, TranscriptMessage, TranscriptRole}; pub struct ClaudeSource; impl ConversationSource for ClaudeSource { fn parse_message(&self, obj: &Value, offset: u64) -> Option { parse_message(obj, offset) } fn is_compaction(&self, obj: &Value) -> bool { is_compaction(obj) } fn may_contain_compaction(&self, obj_bytes: &[u8]) -> bool { contains_bytes(obj_bytes, b"This session is being continued") } } fn text_content(value: &Value) -> Option { let text = match value { Value::String(s) => s.clone(), Value::Array(arr) => { arr.iter() .filter(|b| b.get("type").and_then(|v| v.as_str()) == Some("text")) .filter_map(|b| b.get("text").and_then(|v| v.as_str())) .collect::>() .join(" ") } _ => return None, }; (!text.is_empty()).then_some(text) } pub(crate) fn parse_message(obj: &Value, offset: u64) -> Option { let role = match obj.get("type").and_then(|v| v.as_str()) { Some("user") => TranscriptRole::User, Some("assistant") => TranscriptRole::Assistant, _ => return None, }; let msg = obj.get("message").unwrap_or(obj); let text = msg.get("content").and_then(text_content)?; let timestamp = obj.get("timestamp") .and_then(|v| v.as_str()) .map(str::to_string); Some(TranscriptMessage { role, text, timestamp, offset }) } pub(crate) fn is_compaction(obj: &Value) -> bool { obj.get("type").and_then(|v| v.as_str()) == Some("user") && obj.get("message") .and_then(|m| m.get("content")) .and_then(|c| c.as_str()) .is_some_and(|content| content.starts_with("This session is being continued")) } fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool { haystack.windows(needle.len()).any(|w| w == needle) } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn parses_string_and_array_content() { let user = json!({ "timestamp": "2026-06-15T15:00:00.000Z", "type": "user", "message": { "content": "hello" } }); let assistant = json!({ "timestamp": "2026-06-15T15:00:01.000Z", "type": "assistant", "message": { "content": [ { "type": "text", "text": "hi" }, { "type": "tool_use", "name": "ignored" }, { "type": "text", "text": "there" } ] } }); assert_eq!( parse_message(&user, 7).unwrap(), TranscriptMessage { role: TranscriptRole::User, text: "hello".to_string(), timestamp: Some("2026-06-15T15:00:00.000Z".to_string()), offset: 7, } ); assert_eq!(parse_message(&assistant, 9).unwrap().text, "hi there"); } #[test] fn detects_compaction_marker() { let obj = json!({ "timestamp": "2026-06-15T15:00:01.000Z", "type": "user", "message": { "content": "This session is being continued from a previous conversation." } }); assert!(is_compaction(&obj)); } }