1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct Workspace {
12 pub hash: String,
14 pub project_path: Option<String>,
16 pub workspace_path: std::path::PathBuf,
18 pub chat_sessions_path: std::path::PathBuf,
20 pub chat_session_count: usize,
22 pub has_chat_sessions: bool,
24 #[allow(dead_code)]
26 pub last_modified: Option<DateTime<Utc>>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct WorkspaceJson {
32 pub folder: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct ChatSession {
39 #[serde(default = "default_version")]
41 pub version: u32,
42
43 #[serde(default)]
45 pub session_id: Option<String>,
46
47 #[serde(default)]
49 pub creation_date: i64,
50
51 #[serde(default)]
53 pub last_message_date: i64,
54
55 #[serde(default)]
57 pub is_imported: bool,
58
59 #[serde(default = "default_location")]
61 pub initial_location: String,
62
63 #[serde(default)]
65 pub custom_title: Option<String>,
66
67 #[serde(default)]
69 pub requester_username: Option<String>,
70
71 #[serde(default)]
73 pub requester_avatar_icon_uri: Option<serde_json::Value>,
74
75 #[serde(default)]
77 pub responder_username: Option<String>,
78
79 #[serde(default)]
81 pub responder_avatar_icon_uri: Option<serde_json::Value>,
82
83 #[serde(default)]
85 pub requests: Vec<ChatRequest>,
86}
87
88impl ChatSession {
89 pub fn collect_all_text(&self) -> String {
91 self.requests
92 .iter()
93 .flat_map(|req| {
94 let mut texts = Vec::new();
95 if let Some(msg) = &req.message {
96 if let Some(text) = &msg.text {
97 texts.push(text.as_str());
98 }
99 }
100 if let Some(resp) = &req.response {
101 if let Some(result) = resp.get("result").and_then(|v| v.as_str()) {
102 texts.push(result);
103 }
104 }
105 texts
106 })
107 .collect::<Vec<_>>()
108 .join(" ")
109 }
110
111 pub fn user_messages(&self) -> Vec<&str> {
113 self.requests
114 .iter()
115 .filter_map(|req| req.message.as_ref().and_then(|m| m.text.as_deref()))
116 .collect()
117 }
118
119 pub fn assistant_responses(&self) -> Vec<String> {
121 self.requests
122 .iter()
123 .filter_map(|req| {
124 req.response.as_ref().and_then(|r| {
125 r.get("result")
126 .and_then(|v| v.as_str())
127 .map(|s| s.to_string())
128 })
129 })
130 .collect()
131 }
132}
133
134fn default_version() -> u32 {
135 3
136}
137
138fn default_location() -> String {
139 "panel".to_string()
140}
141
142fn default_response_state() -> u8 {
144 1
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct ChatRequest {
151 #[serde(default)]
153 pub timestamp: Option<i64>,
154
155 #[serde(default)]
157 pub message: Option<ChatMessage>,
158
159 #[serde(default)]
161 pub response: Option<serde_json::Value>,
162
163 #[serde(default)]
165 pub variable_data: Option<serde_json::Value>,
166
167 #[serde(default)]
169 pub request_id: Option<String>,
170
171 #[serde(default)]
173 pub response_id: Option<String>,
174
175 #[serde(default)]
177 pub model_id: Option<String>,
178
179 #[serde(default)]
181 pub agent: Option<serde_json::Value>,
182
183 #[serde(default)]
185 pub result: Option<serde_json::Value>,
186
187 #[serde(default)]
189 pub followups: Option<Vec<serde_json::Value>>,
190
191 #[serde(default)]
193 pub is_canceled: Option<bool>,
194
195 #[serde(default)]
197 pub content_references: Option<Vec<serde_json::Value>>,
198
199 #[serde(default)]
201 pub code_citations: Option<Vec<serde_json::Value>>,
202
203 #[serde(default)]
205 pub response_markdown_info: Option<Vec<serde_json::Value>>,
206
207 #[serde(rename = "_sourceSession", skip_serializing_if = "Option::is_none")]
209 pub source_session: Option<String>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct ChatMessage {
216 #[serde(alias = "content")]
218 pub text: Option<String>,
219
220 #[serde(default)]
222 pub parts: Option<Vec<serde_json::Value>>,
223}
224
225impl ChatMessage {
226 pub fn get_text(&self) -> String {
228 self.text.clone().unwrap_or_default()
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235#[allow(dead_code)]
236pub struct ChatResponse {
237 #[serde(alias = "content")]
239 pub text: Option<String>,
240
241 #[serde(default)]
243 pub parts: Option<Vec<serde_json::Value>>,
244
245 #[serde(default)]
247 pub result: Option<serde_json::Value>,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ChatSessionIndex {
253 #[serde(default = "default_index_version")]
255 pub version: u32,
256
257 #[serde(default)]
259 pub entries: HashMap<String, ChatSessionIndexEntry>,
260}
261
262fn default_index_version() -> u32 {
263 1
264}
265
266impl Default for ChatSessionIndex {
267 fn default() -> Self {
268 Self {
269 version: 1,
270 entries: HashMap::new(),
271 }
272 }
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct ChatSessionTiming {
280 #[serde(default, alias = "startTime")]
283 pub created: i64,
284
285 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub last_request_started: Option<i64>,
288
289 #[serde(default, skip_serializing_if = "Option::is_none", alias = "endTime")]
292 pub last_request_ended: Option<i64>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct ChatSessionIndexEntry {
299 pub session_id: String,
301
302 pub title: String,
304
305 pub last_message_date: i64,
307
308 #[serde(default)]
310 pub timing: Option<ChatSessionTiming>,
311
312 #[serde(default = "default_response_state")]
314 pub last_response_state: u8,
315
316 #[serde(default = "default_location")]
318 pub initial_location: String,
319
320 #[serde(default)]
322 pub is_empty: bool,
323
324 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub is_imported: Option<bool>,
327
328 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub has_pending_edits: Option<bool>,
331
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub is_external: Option<bool>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct ModelCacheEntry {
343 pub provider_type: String,
345
346 pub provider_label: String,
348
349 pub resource: String,
351
352 pub icon: String,
354
355 pub label: String,
357
358 pub status: u8,
360
361 pub timing: ChatSessionTiming,
363
364 pub initial_location: String,
366
367 pub has_pending_edits: bool,
369
370 pub is_empty: bool,
372
373 pub is_external: bool,
375
376 pub last_response_state: u8,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
383#[serde(rename_all = "camelCase")]
384pub struct StateCacheEntry {
385 pub resource: String,
387
388 #[serde(default)]
390 pub read: Option<i64>,
391}
392
393#[derive(Debug, Clone)]
395pub struct SessionWithPath {
396 pub path: std::path::PathBuf,
397 pub session: ChatSession,
398}
399
400impl SessionWithPath {
401 #[allow(dead_code)]
403 pub fn get_session_id(&self) -> String {
404 self.session.session_id.clone().unwrap_or_else(|| {
405 self.path
406 .file_stem()
407 .map(|s| s.to_string_lossy().to_string())
408 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
409 })
410 }
411}
412
413impl ChatSession {
414 #[allow(dead_code)]
416 pub fn get_session_id(&self) -> String {
417 self.session_id
418 .clone()
419 .unwrap_or_else(|| "unknown".to_string())
420 }
421
422 pub fn title(&self) -> String {
424 if let Some(title) = &self.custom_title {
426 if !title.is_empty() {
427 return title.clone();
428 }
429 }
430
431 if let Some(first_req) = self.requests.first() {
433 if let Some(msg) = &first_req.message {
434 if let Some(text) = &msg.text {
435 let title: String = text.chars().take(50).collect();
437 if !title.is_empty() {
438 if title.len() < text.len() {
439 return format!("{}...", title);
440 }
441 return title;
442 }
443 }
444 }
445 }
446
447 "Untitled".to_string()
448 }
449
450 pub fn is_empty(&self) -> bool {
452 self.requests.is_empty()
453 }
454
455 pub fn request_count(&self) -> usize {
457 self.requests.len()
458 }
459
460 pub fn timestamp_range(&self) -> Option<(i64, i64)> {
462 if self.requests.is_empty() {
463 return None;
464 }
465
466 let timestamps: Vec<i64> = self.requests.iter().filter_map(|r| r.timestamp).collect();
467
468 if timestamps.is_empty() {
469 return None;
470 }
471
472 let min = *timestamps.iter().min().unwrap();
473 let max = *timestamps.iter().max().unwrap();
474 Some((min, max))
475 }
476}