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.clone());
98 }
99 }
100 if let Some(resp) = &req.response {
101 if let Some(text) = extract_response_text(resp) {
102 texts.push(text);
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| req.response.as_ref().and_then(|r| extract_response_text(r)))
124 .collect()
125 }
126}
127
128fn default_version() -> u32 {
129 3
130}
131
132fn default_location() -> String {
133 "panel".to_string()
134}
135
136fn default_response_state() -> u8 {
138 1
139}
140
141#[derive(Debug, Clone, Default, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct ChatRequest {
145 #[serde(default)]
147 pub timestamp: Option<i64>,
148
149 #[serde(default)]
151 pub message: Option<ChatMessage>,
152
153 #[serde(default)]
155 pub response: Option<serde_json::Value>,
156
157 #[serde(default)]
159 pub variable_data: Option<serde_json::Value>,
160
161 #[serde(default)]
163 pub request_id: Option<String>,
164
165 #[serde(default)]
167 pub response_id: Option<String>,
168
169 #[serde(default)]
171 pub model_id: Option<String>,
172
173 #[serde(default)]
175 pub agent: Option<serde_json::Value>,
176
177 #[serde(default)]
179 pub result: Option<serde_json::Value>,
180
181 #[serde(default)]
183 pub followups: Option<Vec<serde_json::Value>>,
184
185 #[serde(default)]
187 pub is_canceled: Option<bool>,
188
189 #[serde(default)]
191 pub content_references: Option<Vec<serde_json::Value>>,
192
193 #[serde(default)]
195 pub code_citations: Option<Vec<serde_json::Value>>,
196
197 #[serde(default)]
199 pub response_markdown_info: Option<Vec<serde_json::Value>>,
200
201 #[serde(rename = "_sourceSession", skip_serializing_if = "Option::is_none")]
203 pub source_session: Option<String>,
204
205 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub model_state: Option<serde_json::Value>,
209
210 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub time_spent_waiting: Option<i64>,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct ChatMessage {
219 #[serde(alias = "content")]
221 pub text: Option<String>,
222
223 #[serde(default)]
225 pub parts: Option<Vec<serde_json::Value>>,
226}
227
228impl ChatMessage {
229 pub fn get_text(&self) -> String {
231 self.text.clone().unwrap_or_default()
232 }
233}
234
235pub fn extract_response_text(response: &serde_json::Value) -> Option<String> {
244 if let Some(parts) = response.as_array() {
246 let texts: Vec<&str> = parts
247 .iter()
248 .filter_map(|part| {
249 let kind = part.get("kind").and_then(|k| k.as_str()).unwrap_or("");
250 match kind {
251 "" => part.get("value").and_then(|v| v.as_str()),
253 "thinking" => part.get("value").and_then(|v| v.as_str()),
255 _ => None,
257 }
258 })
259 .collect();
260 if !texts.is_empty() {
261 return Some(texts.join("\n"));
262 }
263 }
264
265 if let Some(value) = response.get("value").and_then(|v| v.as_array()) {
267 let parts: Vec<String> = value
268 .iter()
269 .filter_map(|v| v.get("value").and_then(|v| v.as_str()))
270 .map(String::from)
271 .collect();
272 if !parts.is_empty() {
273 return Some(parts.join("\n"));
274 }
275 }
276
277 if let Some(text) = response.get("text").and_then(|v| v.as_str()) {
279 return Some(text.to_string());
280 }
281
282 if let Some(result) = response.get("result").and_then(|v| v.as_str()) {
284 return Some(result.to_string());
285 }
286
287 if let Some(content) = response.get("content").and_then(|v| v.as_str()) {
289 return Some(content.to_string());
290 }
291
292 None
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298#[allow(dead_code)]
299pub struct ChatResponse {
300 #[serde(alias = "content")]
302 pub text: Option<String>,
303
304 #[serde(default)]
306 pub parts: Option<Vec<serde_json::Value>>,
307
308 #[serde(default)]
310 pub result: Option<serde_json::Value>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct ChatSessionIndex {
316 #[serde(default = "default_index_version")]
318 pub version: u32,
319
320 #[serde(default, deserialize_with = "deserialize_index_entries")]
323 pub entries: HashMap<String, ChatSessionIndexEntry>,
324}
325
326fn deserialize_index_entries<'de, D>(
330 deserializer: D,
331) -> std::result::Result<HashMap<String, ChatSessionIndexEntry>, D::Error>
332where
333 D: serde::Deserializer<'de>,
334{
335 use serde::de;
336
337 struct EntriesVisitor;
338
339 impl<'de> de::Visitor<'de> for EntriesVisitor {
340 type Value = HashMap<String, ChatSessionIndexEntry>;
341
342 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
343 f.write_str("a map of session entries or an array of session ID strings")
344 }
345
346 fn visit_map<M>(self, mut access: M) -> std::result::Result<Self::Value, M::Error>
348 where
349 M: de::MapAccess<'de>,
350 {
351 let mut map = HashMap::new();
352 while let Some((key, value)) = access.next_entry::<String, ChatSessionIndexEntry>()? {
353 map.insert(key, value);
354 }
355 Ok(map)
356 }
357
358 fn visit_seq<S>(self, mut seq: S) -> std::result::Result<Self::Value, S::Error>
360 where
361 S: de::SeqAccess<'de>,
362 {
363 let mut map = HashMap::new();
364 while let Some(id) = seq.next_element::<String>()? {
365 let entry = ChatSessionIndexEntry {
366 session_id: id.clone(),
367 title: String::new(),
368 last_message_date: 0,
369 timing: None,
370 last_response_state: 0,
371 initial_location: "panel".to_string(),
372 is_empty: false,
373 is_imported: None,
374 has_pending_edits: None,
375 is_external: None,
376 };
377 map.insert(id, entry);
378 }
379 Ok(map)
380 }
381 }
382
383 deserializer.deserialize_any(EntriesVisitor)
384}
385
386fn default_index_version() -> u32 {
387 1
388}
389
390impl Default for ChatSessionIndex {
391 fn default() -> Self {
392 Self {
393 version: 1,
394 entries: HashMap::new(),
395 }
396 }
397}
398
399#[derive(Debug, Clone, Default, Serialize, Deserialize)]
402#[serde(rename_all = "camelCase")]
403pub struct ChatSessionTiming {
404 #[serde(default, alias = "startTime")]
407 pub created: i64,
408
409 #[serde(default, skip_serializing_if = "Option::is_none")]
411 pub last_request_started: Option<i64>,
412
413 #[serde(default, skip_serializing_if = "Option::is_none", alias = "endTime")]
416 pub last_request_ended: Option<i64>,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
421#[serde(rename_all = "camelCase")]
422pub struct ChatSessionIndexEntry {
423 pub session_id: String,
425
426 pub title: String,
428
429 pub last_message_date: i64,
431
432 #[serde(default)]
434 pub timing: Option<ChatSessionTiming>,
435
436 #[serde(default = "default_response_state")]
438 pub last_response_state: u8,
439
440 #[serde(default = "default_location")]
442 pub initial_location: String,
443
444 #[serde(default)]
446 pub is_empty: bool,
447
448 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub is_imported: Option<bool>,
451
452 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub has_pending_edits: Option<bool>,
455
456 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub is_external: Option<bool>,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct ModelCacheEntry {
467 #[serde(default)]
469 pub provider_type: String,
470
471 #[serde(default)]
473 pub provider_label: String,
474
475 pub resource: String,
477
478 #[serde(default)]
480 pub icon: String,
481
482 #[serde(default)]
484 pub label: String,
485
486 #[serde(default)]
488 pub status: u8,
489
490 #[serde(default)]
492 pub timing: ChatSessionTiming,
493
494 #[serde(default)]
496 pub initial_location: String,
497
498 #[serde(default)]
500 pub has_pending_edits: bool,
501
502 #[serde(default)]
504 pub is_empty: bool,
505
506 #[serde(default)]
508 pub is_external: bool,
509
510 #[serde(default)]
512 pub last_response_state: u8,
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize)]
518#[serde(rename_all = "camelCase")]
519pub struct StateCacheEntry {
520 pub resource: String,
522
523 #[serde(default)]
525 pub read: Option<i64>,
526}
527
528#[derive(Debug, Clone)]
530pub struct SessionWithPath {
531 pub path: std::path::PathBuf,
532 pub session: ChatSession,
533}
534
535impl SessionWithPath {
536 #[allow(dead_code)]
538 pub fn get_session_id(&self) -> String {
539 self.session.session_id.clone().unwrap_or_else(|| {
540 self.path
541 .file_stem()
542 .map(|s| s.to_string_lossy().to_string())
543 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
544 })
545 }
546}
547
548impl ChatSession {
549 #[allow(dead_code)]
551 pub fn get_session_id(&self) -> String {
552 self.session_id
553 .clone()
554 .unwrap_or_else(|| "unknown".to_string())
555 }
556
557 pub fn title(&self) -> String {
559 if let Some(title) = &self.custom_title {
561 if !title.is_empty() {
562 return title.clone();
563 }
564 }
565
566 if let Some(first_req) = self.requests.first() {
568 if let Some(msg) = &first_req.message {
569 if let Some(text) = &msg.text {
570 let title: String = text.chars().take(50).collect();
572 if !title.is_empty() {
573 if title.len() < text.len() {
574 return format!("{}...", title);
575 }
576 return title;
577 }
578 }
579 }
580 }
581
582 "Untitled".to_string()
583 }
584
585 pub fn is_empty(&self) -> bool {
587 self.requests.is_empty()
588 }
589
590 pub fn request_count(&self) -> usize {
592 self.requests.len()
593 }
594
595 pub fn timestamp_range(&self) -> Option<(i64, i64)> {
597 if self.requests.is_empty() {
598 return None;
599 }
600
601 let timestamps: Vec<i64> = self.requests.iter().filter_map(|r| r.timestamp).collect();
602
603 if timestamps.is_empty() {
604 return None;
605 }
606
607 let min = *timestamps.iter().min().unwrap();
608 let max = *timestamps.iter().max().unwrap();
609 Some((min, max))
610 }
611}