use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum TranscriptEntry {
Summary {
summary: String,
#[serde(rename = "leafUuid")]
leaf_uuid: Option<String>,
},
#[serde(rename = "file-history-snapshot")]
FileHistorySnapshot {
#[serde(rename = "messageId")]
message_id: Option<String>,
},
User {
uuid: String,
#[serde(rename = "parentUuid")]
parent_uuid: Option<String>,
#[serde(rename = "sessionId")]
session_id: Option<String>,
timestamp: Option<String>,
message: UserMessage,
},
Assistant {
uuid: String,
#[serde(rename = "parentUuid")]
parent_uuid: Option<String>,
#[serde(rename = "sessionId")]
session_id: Option<String>,
timestamp: Option<String>,
message: AssistantMessage,
},
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserMessage {
pub role: String,
pub content: UserContent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UserContent {
Text(String),
Blocks(Vec<UserContentBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserContentBlock {
#[serde(rename = "type")]
pub block_type: String,
pub text: Option<String>,
#[serde(rename = "tool_use_id")]
pub tool_use_id: Option<String>,
pub content: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantMessage {
pub role: String,
pub content: Vec<AssistantContentBlock>,
pub model: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantContentBlock {
#[serde(rename = "type")]
pub block_type: String,
pub text: Option<String>,
pub thinking: Option<String>,
pub name: Option<String>,
pub input: Option<serde_json::Value>,
}
impl TranscriptEntry {
pub fn session_id(&self) -> Option<&str> {
match self {
TranscriptEntry::User { session_id, .. } => session_id.as_deref(),
TranscriptEntry::Assistant { session_id, .. } => session_id.as_deref(),
_ => None,
}
}
pub fn timestamp(&self) -> Option<&str> {
match self {
TranscriptEntry::User { timestamp, .. } => timestamp.as_deref(),
TranscriptEntry::Assistant { timestamp, .. } => timestamp.as_deref(),
_ => None,
}
}
pub fn is_user(&self) -> bool {
matches!(self, TranscriptEntry::User { .. })
}
pub fn is_assistant(&self) -> bool {
matches!(self, TranscriptEntry::Assistant { .. })
}
pub fn is_message(&self) -> bool {
self.is_user() || self.is_assistant()
}
pub fn is_summary(&self) -> bool {
matches!(self, TranscriptEntry::Summary { .. })
}
pub fn summary_text(&self) -> Option<&str> {
match self {
TranscriptEntry::Summary { summary, .. } => Some(summary.as_str()),
_ => None,
}
}
pub fn assistant_thinking(&self) -> Option<String> {
match self {
TranscriptEntry::Assistant { message, .. } => {
let thoughts: Vec<&str> = message
.content
.iter()
.filter(|b| b.block_type == "thinking")
.filter_map(|b| b.thinking.as_deref())
.collect();
if thoughts.is_empty() {
None
} else {
Some(thoughts.join("\n"))
}
}
_ => None,
}
}
pub fn user_text(&self) -> Option<String> {
match self {
TranscriptEntry::User { message, .. } => match &message.content {
UserContent::Text(text) => Some(text.clone()),
UserContent::Blocks(blocks) => {
let texts: Vec<&str> = blocks
.iter()
.filter(|b| b.block_type == "text")
.filter_map(|b| b.text.as_deref())
.collect();
if texts.is_empty() {
None
} else {
Some(texts.join("\n"))
}
}
},
_ => None,
}
}
pub fn tool_results(&self) -> Vec<(Option<&str>, String)> {
match self {
TranscriptEntry::User { message, .. } => match &message.content {
UserContent::Blocks(blocks) => blocks
.iter()
.filter(|b| b.block_type == "tool_result")
.filter_map(|b| {
let content = b.content.as_ref().map(|c| match c {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
})?;
Some((b.tool_use_id.as_deref(), content))
})
.collect(),
_ => Vec::new(),
},
_ => Vec::new(),
}
}
pub fn assistant_text(&self) -> Option<String> {
match self {
TranscriptEntry::Assistant { message, .. } => {
let texts: Vec<&str> = message
.content
.iter()
.filter(|b| b.block_type == "text")
.filter_map(|b| b.text.as_deref())
.collect();
if texts.is_empty() {
None
} else {
Some(texts.join("\n"))
}
}
_ => None,
}
}
pub fn tool_uses(&self) -> Vec<(&str, Option<&serde_json::Value>)> {
match self {
TranscriptEntry::Assistant { message, .. } => message
.content
.iter()
.filter(|b| b.block_type == "tool_use")
.filter_map(|b| Some((b.name.as_deref()?, b.input.as_ref())))
.collect(),
_ => Vec::new(),
}
}
}