use super::time::now_iso;
use super::traits::EntryType;
#[derive(serde::Serialize)]
pub(crate) struct MessageBody {
pub role: EntryType,
pub content: MessageContent,
}
#[derive(serde::Serialize)]
#[serde(untagged)]
pub(crate) enum MessageContent {
Text(String),
Blocks(Vec<ContentBlock>),
}
#[derive(serde::Serialize)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub(crate) enum ContentBlock {
Text { text: String },
}
#[derive(serde::Serialize)]
pub(crate) struct PersistedMessage {
#[serde(rename = "type")]
pub entry_type: EntryType,
pub message: MessageBody,
pub uuid: String,
#[serde(rename = "parentUuid", skip_serializing_if = "Option::is_none")]
pub parent_uuid: Option<String>,
#[serde(rename = "sessionId")]
pub session_id: String,
pub timestamp: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
}
pub(crate) fn make_persisted(
entry_type: EntryType,
content: &str,
session_id: &str,
parent_uuid: Option<&str>,
) -> PersistedMessage {
let message = MessageBody {
role: entry_type,
content: match entry_type {
EntryType::User | EntryType::System => MessageContent::Text(content.to_string()),
EntryType::Assistant | EntryType::Tool => {
MessageContent::Blocks(vec![ContentBlock::Text {
text: content.to_string(),
}])
}
},
};
PersistedMessage {
entry_type,
message,
uuid: uuid::Uuid::now_v7().to_string(),
parent_uuid: parent_uuid.map(String::from),
session_id: session_id.to_string(),
timestamp: now_iso(),
cwd: std::env::current_dir()
.ok()
.and_then(|p| p.to_str().map(String::from)),
}
}
pub(crate) fn parse_entry(value: &serde_json::Value) -> Option<(EntryType, String)> {
if let Some(type_str) = value["type"].as_str() {
let entry_type = EntryType::parse(type_str)?;
let content = extract_content(&value["message"])?;
if !content.trim().is_empty() {
return Some((entry_type, content));
}
return None;
}
let entry_type = EntryType::parse(value["role"].as_str()?)?;
let content = value["content"].as_str()?;
if !content.trim().is_empty() {
return Some((entry_type, content.to_string()));
}
None
}
fn extract_content(message: &serde_json::Value) -> Option<String> {
let content = &message["content"];
if let Some(arr) = content.as_array() {
let parts: Vec<String> = arr
.iter()
.filter_map(|block| {
match block["type"].as_str()? {
"text" => block["text"].as_str().map(String::from),
"tool_use" => {
Some(format!("[tool: {}]", block["name"].as_str().unwrap_or("?")))
}
"tool_result" => block["content"]
.as_str()
.map(|s| format!("[result: {}]", super::time::truncate_str(s, 200))),
_ => None, }
})
.collect();
return if parts.is_empty() {
None
} else {
Some(parts.join("\n"))
};
}
if let Some(s) = content.as_str() {
return Some(s.to_string());
}
if let Some(s) = message.as_str() {
return Some(s.to_string());
}
None
}