1use serde::Deserialize;
6
7#[derive(Debug, Deserialize)]
10#[serde(tag = "type", rename_all = "kebab-case")]
11pub enum Record {
12 User(MessageRecord),
13 Assistant(MessageRecord),
14 System(MessageRecord),
15 FileHistorySnapshot(serde_json::Value),
16 Progress(serde_json::Value),
17 #[serde(other)]
18 Unknown,
19}
20
21impl Record {
22 pub fn as_message(&self) -> Option<&MessageRecord> {
23 match self {
24 Record::User(r) | Record::Assistant(r) | Record::System(r) => Some(r),
25 _ => None,
26 }
27 }
28
29 pub fn role(&self) -> &'static str {
30 match self {
31 Record::User(_) => "user",
32 Record::Assistant(_) => "assistant",
33 Record::System(_) => "system",
34 _ => "other",
35 }
36 }
37
38 pub fn is_message(&self) -> bool {
39 matches!(self, Record::User(_) | Record::Assistant(_) | Record::System(_))
40 }
41}
42
43#[derive(Debug, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct MessageRecord {
48 pub uuid: Option<String>,
49 pub parent_uuid: Option<serde_json::Value>,
50 pub session_id: Option<String>,
51 pub timestamp: Option<String>,
52 pub cwd: Option<String>,
53 pub git_branch: Option<String>,
54 pub version: Option<String>,
55 pub message: Message,
56}
57
58#[derive(Debug, Deserialize)]
59pub struct Message {
60 pub role: String,
61 pub content: MessageContent,
62}
63
64#[derive(Debug, Deserialize)]
65#[serde(untagged)]
66pub enum MessageContent {
67 Text(String),
68 Blocks(Vec<ContentBlock>),
69}
70
71#[derive(Debug, Deserialize)]
72#[serde(tag = "type", rename_all = "snake_case")]
73pub enum ContentBlock {
74 Text { text: String },
75 Thinking { thinking: String },
76 ToolUse {
77 id: Option<String>,
78 name: String,
79 input: serde_json::Value,
80 },
81 ToolResult {
82 tool_use_id: Option<String>,
83 content: Option<serde_json::Value>,
84 },
85 #[serde(other)]
86 Other,
87}
88
89impl MessageRecord {
92 pub fn text_content(&self) -> String {
94 match &self.message.content {
95 MessageContent::Text(s) => s.clone(),
96 MessageContent::Blocks(blocks) => {
97 let mut parts = Vec::new();
98 for block in blocks {
99 match block {
100 ContentBlock::Text { text } => parts.push(text.as_str()),
101 ContentBlock::Thinking { thinking } => parts.push(thinking.as_str()),
102 ContentBlock::ToolUse { .. } | ContentBlock::ToolResult { .. } => {}
103 ContentBlock::Other => {}
104 }
105 }
106 parts.join("\n")
107 }
108 }
109 }
110
111 pub fn text_no_thinking(&self) -> String {
113 match &self.message.content {
114 MessageContent::Text(s) => s.clone(),
115 MessageContent::Blocks(blocks) => {
116 let mut parts = Vec::new();
117 for block in blocks {
118 if let ContentBlock::Text { text } = block {
119 parts.push(text.as_str());
120 }
121 }
122 parts.join("\n")
123 }
124 }
125 }
126
127 pub fn thinking_content(&self) -> String {
129 match &self.message.content {
130 MessageContent::Blocks(blocks) => {
131 let mut parts = Vec::new();
132 for block in blocks {
133 if let ContentBlock::Thinking { thinking } = block {
134 parts.push(thinking.as_str());
135 }
136 }
137 parts.join("\n")
138 }
139 _ => String::new(),
140 }
141 }
142
143 pub fn tool_input_content(&self) -> String {
145 match &self.message.content {
146 MessageContent::Blocks(blocks) => {
147 let mut parts = Vec::new();
148 for block in blocks {
149 if let ContentBlock::ToolUse { name, input, .. } = block {
150 parts.push(format!("[{}] {}", name, input));
151 }
152 }
153 parts.join("\n")
154 }
155 _ => String::new(),
156 }
157 }
158
159 pub fn tool_names(&self) -> Vec<&str> {
161 match &self.message.content {
162 MessageContent::Blocks(blocks) => blocks
163 .iter()
164 .filter_map(|b| match b {
165 ContentBlock::ToolUse { name, .. } => Some(name.as_str()),
166 _ => None,
167 })
168 .collect(),
169 _ => vec![],
170 }
171 }
172
173 pub fn touches_file(&self, path: &str) -> bool {
175 let path_lower = path.to_lowercase();
176 match &self.message.content {
177 MessageContent::Blocks(blocks) => blocks.iter().any(|block| match block {
178 ContentBlock::ToolUse { input, .. } => {
179 input.to_string().to_lowercase().contains(&path_lower)
180 }
181 ContentBlock::ToolResult { content: Some(c), .. } => {
182 c.to_string().to_lowercase().contains(&path_lower)
183 }
184 _ => false,
185 }),
186 _ => false,
187 }
188 }
189
190 pub fn full_content(&self) -> String {
192 match &self.message.content {
193 MessageContent::Text(s) => s.clone(),
194 MessageContent::Blocks(blocks) => {
195 let mut parts = Vec::new();
196 for block in blocks {
197 match block {
198 ContentBlock::Text { text } => parts.push(text.clone()),
199 ContentBlock::Thinking { thinking } => parts.push(thinking.clone()),
200 ContentBlock::ToolUse { name, input, .. } => {
201 parts.push(format!("[tool: {}] {}", name, input));
202 }
203 ContentBlock::ToolResult { content: Some(c), .. } => {
204 parts.push(format!("[result] {}", c));
205 }
206 _ => {}
207 }
208 }
209 parts.join("\n")
210 }
211 }
212 }
213}