1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct SessionLine {
12 #[serde(default)]
14 pub session_id: Option<String>,
15
16 #[serde(rename = "type")]
18 pub line_type: String,
19
20 #[serde(default)]
22 pub timestamp: Option<DateTime<Utc>>,
23
24 #[serde(default)]
26 pub cwd: Option<String>,
27
28 #[serde(default)]
30 pub git_branch: Option<String>,
31
32 #[serde(default)]
34 pub message: Option<SessionMessage>,
35
36 #[serde(default)]
38 pub model: Option<String>,
39
40 #[serde(default)]
42 pub usage: Option<TokenUsage>,
43
44 #[serde(default)]
46 pub summary: Option<SessionSummary>,
47
48 #[serde(default)]
50 pub parent_session_id: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct SessionMessage {
57 #[serde(default)]
59 pub role: Option<String>,
60
61 #[serde(default)]
63 pub content: Option<Value>,
64
65 #[serde(default)]
67 pub tool_calls: Option<Vec<serde_json::Value>>,
68
69 #[serde(default)]
71 pub tool_results: Option<Vec<serde_json::Value>>,
72
73 #[serde(default)]
75 pub usage: Option<TokenUsage>,
76}
77
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
80pub struct TokenUsage {
81 #[serde(default)]
82 pub input_tokens: u64,
83
84 #[serde(default)]
85 pub output_tokens: u64,
86
87 #[serde(default, alias = "cache_read_input_tokens")]
89 pub cache_read_tokens: u64,
90
91 #[serde(default, alias = "cache_creation_input_tokens")]
93 pub cache_write_tokens: u64,
94}
95
96impl TokenUsage {
97 pub fn total(&self) -> u64 {
105 self.input_tokens + self.output_tokens + self.cache_read_tokens + self.cache_write_tokens
106 }
107}
108
109#[derive(Debug, Clone, Default, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct SessionSummary {
113 #[serde(default)]
114 pub total_tokens: u64,
115 #[serde(default)]
116 pub total_input_tokens: u64,
117 #[serde(default)]
118 pub total_output_tokens: u64,
119 #[serde(default)]
120 pub total_cache_read_tokens: u64,
121 #[serde(default)]
122 pub total_cache_write_tokens: u64,
123 #[serde(default)]
124 pub message_count: u64,
125 #[serde(default)]
126 pub duration_seconds: Option<u64>,
127 #[serde(default)]
128 pub models_used: Option<Vec<String>>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct SessionMetadata {
136 pub id: String,
138
139 pub file_path: PathBuf,
141
142 pub project_path: String,
144
145 pub first_timestamp: Option<DateTime<Utc>>,
147
148 pub last_timestamp: Option<DateTime<Utc>>,
150
151 pub message_count: u64,
153
154 pub total_tokens: u64,
156
157 pub input_tokens: u64,
159 pub output_tokens: u64,
160 pub cache_creation_tokens: u64,
161 pub cache_read_tokens: u64,
162
163 pub models_used: Vec<String>,
165
166 pub file_size_bytes: u64,
168
169 pub first_user_message: Option<String>,
171
172 pub has_subagents: bool,
174
175 pub duration_seconds: Option<u64>,
177
178 pub branch: Option<String>,
180
181 pub tool_usage: std::collections::HashMap<String, usize>,
184
185 #[serde(default)]
188 pub tool_token_usage: std::collections::HashMap<String, u64>,
189}
190
191impl SessionMetadata {
192 pub fn from_path(path: PathBuf, project_path: String) -> Self {
194 let id = path
195 .file_stem()
196 .and_then(|s| s.to_str())
197 .unwrap_or("unknown")
198 .to_string();
199
200 let file_size_bytes = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
201
202 Self {
203 id,
204 file_path: path,
205 project_path,
206 first_timestamp: None,
207 last_timestamp: None,
208 message_count: 0,
209 total_tokens: 0,
210 input_tokens: 0,
211 output_tokens: 0,
212 cache_creation_tokens: 0,
213 cache_read_tokens: 0,
214 models_used: Vec::new(),
215 file_size_bytes,
216 first_user_message: None,
217 has_subagents: false,
218 duration_seconds: None,
219 branch: None,
220 tool_usage: std::collections::HashMap::new(),
221 tool_token_usage: std::collections::HashMap::new(),
222 }
223 }
224
225 pub fn duration_display(&self) -> String {
227 match self.duration_seconds {
228 Some(s) if s >= 3600 => format!("{}h {}m", s / 3600, (s % 3600) / 60),
229 Some(s) if s >= 60 => format!("{}m {}s", s / 60, s % 60),
230 Some(s) => format!("{}s", s),
231 None => "unknown".to_string(),
232 }
233 }
234
235 pub fn size_display(&self) -> String {
237 let bytes = self.file_size_bytes;
238 if bytes >= 1_000_000 {
239 format!("{:.1} MB", bytes as f64 / 1_000_000.0)
240 } else if bytes >= 1_000 {
241 format!("{:.1} KB", bytes as f64 / 1_000.0)
242 } else {
243 format!("{} B", bytes)
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_token_usage_total() {
254 let usage = TokenUsage {
255 input_tokens: 100,
256 output_tokens: 50,
257 ..Default::default()
258 };
259 assert_eq!(usage.total(), 150);
260 }
261
262 #[test]
263 fn test_session_metadata_duration_display() {
264 let mut meta = SessionMetadata::from_path(PathBuf::from("/test.jsonl"), "test".to_string());
265
266 meta.duration_seconds = Some(90);
267 assert_eq!(meta.duration_display(), "1m 30s");
268
269 meta.duration_seconds = Some(3665);
270 assert_eq!(meta.duration_display(), "1h 1m");
271
272 meta.duration_seconds = Some(45);
273 assert_eq!(meta.duration_display(), "45s");
274 }
275
276 #[test]
277 fn test_session_metadata_size_display() {
278 let mut meta = SessionMetadata::from_path(PathBuf::from("/test.jsonl"), "test".to_string());
279
280 meta.file_size_bytes = 500;
281 assert_eq!(meta.size_display(), "500 B");
282
283 meta.file_size_bytes = 5_000;
284 assert_eq!(meta.size_display(), "5.0 KB");
285
286 meta.file_size_bytes = 2_500_000;
287 assert_eq!(meta.size_display(), "2.5 MB");
288 }
289}
290
291#[cfg(test)]
292mod token_tests {
293 use super::*;
294
295 #[test]
296 fn test_real_claude_token_format_deserialization() {
297 let json = r#"{
299 "input_tokens": 10,
300 "cache_creation_input_tokens": 64100,
301 "cache_read_input_tokens": 19275,
302 "cache_creation": {
303 "ephemeral_5m_input_tokens": 0,
304 "ephemeral_1h_input_tokens": 64100
305 },
306 "output_tokens": 1,
307 "service_tier": "standard"
308 }"#;
309
310 let result: Result<TokenUsage, _> = serde_json::from_str(json);
311
312 assert!(
313 result.is_ok(),
314 "Deserialization MUST succeed for real Claude format. Error: {:?}",
315 result.err()
316 );
317
318 let usage = result.unwrap();
319 assert_eq!(usage.input_tokens, 10);
320 assert_eq!(usage.output_tokens, 1);
321 assert_eq!(usage.cache_read_tokens, 19275);
322 assert_eq!(usage.cache_write_tokens, 64100);
323
324 let total = usage.total();
325 assert_eq!(total, 83386, "Total should be 10+1+19275+64100 = 83386");
326 }
327}