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