Skip to main content

smc_cli_cc/
models.rs

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    /// Extract only tool input content (the arguments passed to tools).
129    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    /// Extract only thinking block content.
145    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    /// Extract text content excluding thinking blocks.
161    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    /// Check if any tool input references a file path (substring match).
186    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}