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
186impl SessionMetadata {
187 pub fn from_path(path: PathBuf, project_path: String) -> Self {
189 let id = path
190 .file_stem()
191 .and_then(|s| s.to_str())
192 .unwrap_or("unknown")
193 .to_string();
194
195 let file_size_bytes = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
196
197 Self {
198 id,
199 file_path: path,
200 project_path,
201 first_timestamp: None,
202 last_timestamp: None,
203 message_count: 0,
204 total_tokens: 0,
205 input_tokens: 0,
206 output_tokens: 0,
207 cache_creation_tokens: 0,
208 cache_read_tokens: 0,
209 models_used: Vec::new(),
210 file_size_bytes,
211 first_user_message: None,
212 has_subagents: false,
213 duration_seconds: None,
214 branch: None,
215 tool_usage: std::collections::HashMap::new(),
216 }
217 }
218
219 pub fn duration_display(&self) -> String {
221 match self.duration_seconds {
222 Some(s) if s >= 3600 => format!("{}h {}m", s / 3600, (s % 3600) / 60),
223 Some(s) if s >= 60 => format!("{}m {}s", s / 60, s % 60),
224 Some(s) => format!("{}s", s),
225 None => "unknown".to_string(),
226 }
227 }
228
229 pub fn size_display(&self) -> String {
231 let bytes = self.file_size_bytes;
232 if bytes >= 1_000_000 {
233 format!("{:.1} MB", bytes as f64 / 1_000_000.0)
234 } else if bytes >= 1_000 {
235 format!("{:.1} KB", bytes as f64 / 1_000.0)
236 } else {
237 format!("{} B", bytes)
238 }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_token_usage_total() {
248 let usage = TokenUsage {
249 input_tokens: 100,
250 output_tokens: 50,
251 ..Default::default()
252 };
253 assert_eq!(usage.total(), 150);
254 }
255
256 #[test]
257 fn test_session_metadata_duration_display() {
258 let mut meta = SessionMetadata::from_path(PathBuf::from("/test.jsonl"), "test".to_string());
259
260 meta.duration_seconds = Some(90);
261 assert_eq!(meta.duration_display(), "1m 30s");
262
263 meta.duration_seconds = Some(3665);
264 assert_eq!(meta.duration_display(), "1h 1m");
265
266 meta.duration_seconds = Some(45);
267 assert_eq!(meta.duration_display(), "45s");
268 }
269
270 #[test]
271 fn test_session_metadata_size_display() {
272 let mut meta = SessionMetadata::from_path(PathBuf::from("/test.jsonl"), "test".to_string());
273
274 meta.file_size_bytes = 500;
275 assert_eq!(meta.size_display(), "500 B");
276
277 meta.file_size_bytes = 5_000;
278 assert_eq!(meta.size_display(), "5.0 KB");
279
280 meta.file_size_bytes = 2_500_000;
281 assert_eq!(meta.size_display(), "2.5 MB");
282 }
283}
284
285#[cfg(test)]
286mod token_tests {
287 use super::*;
288
289 #[test]
290 fn test_real_claude_token_format_deserialization() {
291 let json = r#"{
293 "input_tokens": 10,
294 "cache_creation_input_tokens": 64100,
295 "cache_read_input_tokens": 19275,
296 "cache_creation": {
297 "ephemeral_5m_input_tokens": 0,
298 "ephemeral_1h_input_tokens": 64100
299 },
300 "output_tokens": 1,
301 "service_tier": "standard"
302 }"#;
303
304 let result: Result<TokenUsage, _> = serde_json::from_str(json);
305
306 assert!(
307 result.is_ok(),
308 "Deserialization MUST succeed for real Claude format. Error: {:?}",
309 result.err()
310 );
311
312 let usage = result.unwrap();
313 assert_eq!(usage.input_tokens, 10);
314 assert_eq!(usage.output_tokens, 1);
315 assert_eq!(usage.cache_read_tokens, 19275);
316 assert_eq!(usage.cache_write_tokens, 64100);
317
318 let total = usage.total();
319 assert_eq!(total, 83386, "Total should be 10+1+19275+64100 = 83386");
320 }
321}