use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Workspace {
pub hash: String,
pub project_path: Option<String>,
pub workspace_path: std::path::PathBuf,
pub chat_sessions_path: std::path::PathBuf,
pub chat_session_count: usize,
pub has_chat_sessions: bool,
#[allow(dead_code)]
pub last_modified: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceJson {
pub folder: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatSession {
#[serde(default = "default_version")]
pub version: u32,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub creation_date: i64,
#[serde(default)]
pub last_message_date: i64,
#[serde(default)]
pub is_imported: bool,
#[serde(default = "default_location")]
pub initial_location: String,
#[serde(default)]
pub custom_title: Option<String>,
#[serde(default)]
pub requester_username: Option<String>,
#[serde(default)]
pub requester_avatar_icon_uri: Option<serde_json::Value>,
#[serde(default)]
pub responder_username: Option<String>,
#[serde(default)]
pub responder_avatar_icon_uri: Option<serde_json::Value>,
#[serde(default)]
pub requests: Vec<ChatRequest>,
}
impl ChatSession {
pub fn collect_all_text(&self) -> String {
self.requests
.iter()
.flat_map(|req| {
let mut texts = Vec::new();
if let Some(msg) = &req.message {
if let Some(text) = &msg.text {
texts.push(text.clone());
}
}
if let Some(resp) = &req.response {
if let Some(text) = extract_response_text(resp) {
texts.push(text);
}
}
texts
})
.collect::<Vec<_>>()
.join(" ")
}
pub fn user_messages(&self) -> Vec<&str> {
self.requests
.iter()
.filter_map(|req| req.message.as_ref().and_then(|m| m.text.as_deref()))
.collect()
}
pub fn assistant_responses(&self) -> Vec<String> {
self.requests
.iter()
.filter_map(|req| req.response.as_ref().and_then(|r| extract_response_text(r)))
.collect()
}
}
fn default_version() -> u32 {
3
}
fn default_location() -> String {
"panel".to_string()
}
fn default_response_state() -> u8 {
1
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatRequest {
#[serde(default)]
pub timestamp: Option<i64>,
#[serde(default)]
pub message: Option<ChatMessage>,
#[serde(default)]
pub response: Option<serde_json::Value>,
#[serde(default)]
pub variable_data: Option<serde_json::Value>,
#[serde(default)]
pub request_id: Option<String>,
#[serde(default)]
pub response_id: Option<String>,
#[serde(default)]
pub model_id: Option<String>,
#[serde(default)]
pub agent: Option<serde_json::Value>,
#[serde(default)]
pub result: Option<serde_json::Value>,
#[serde(default)]
pub followups: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub is_canceled: Option<bool>,
#[serde(default)]
pub content_references: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub code_citations: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub response_markdown_info: Option<Vec<serde_json::Value>>,
#[serde(rename = "_sourceSession", skip_serializing_if = "Option::is_none")]
pub source_session: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_state: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time_spent_waiting: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatMessage {
#[serde(alias = "content")]
pub text: Option<String>,
#[serde(default)]
pub parts: Option<Vec<serde_json::Value>>,
}
impl ChatMessage {
pub fn get_text(&self) -> String {
self.text.clone().unwrap_or_default()
}
}
pub fn extract_response_text(response: &serde_json::Value) -> Option<String> {
if let Some(parts) = response.as_array() {
let texts: Vec<&str> = parts
.iter()
.filter_map(|part| {
let kind = part.get("kind").and_then(|k| k.as_str()).unwrap_or("");
match kind {
"" => part.get("value").and_then(|v| v.as_str()),
"thinking" => part.get("value").and_then(|v| v.as_str()),
_ => None,
}
})
.collect();
if !texts.is_empty() {
return Some(texts.join("\n"));
}
}
if let Some(value) = response.get("value").and_then(|v| v.as_array()) {
let parts: Vec<String> = value
.iter()
.filter_map(|v| v.get("value").and_then(|v| v.as_str()))
.map(String::from)
.collect();
if !parts.is_empty() {
return Some(parts.join("\n"));
}
}
if let Some(text) = response.get("text").and_then(|v| v.as_str()) {
return Some(text.to_string());
}
if let Some(result) = response.get("result").and_then(|v| v.as_str()) {
return Some(result.to_string());
}
if let Some(content) = response.get("content").and_then(|v| v.as_str()) {
return Some(content.to_string());
}
None
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct ChatResponse {
#[serde(alias = "content")]
pub text: Option<String>,
#[serde(default)]
pub parts: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub result: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatSessionIndex {
#[serde(default = "default_index_version")]
pub version: u32,
#[serde(default, deserialize_with = "deserialize_index_entries")]
pub entries: HashMap<String, ChatSessionIndexEntry>,
}
fn deserialize_index_entries<'de, D>(
deserializer: D,
) -> std::result::Result<HashMap<String, ChatSessionIndexEntry>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de;
struct EntriesVisitor;
impl<'de> de::Visitor<'de> for EntriesVisitor {
type Value = HashMap<String, ChatSessionIndexEntry>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a map of session entries or an array of session ID strings")
}
fn visit_map<M>(self, mut access: M) -> std::result::Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let mut map = HashMap::new();
while let Some((key, value)) = access.next_entry::<String, ChatSessionIndexEntry>()? {
map.insert(key, value);
}
Ok(map)
}
fn visit_seq<S>(self, mut seq: S) -> std::result::Result<Self::Value, S::Error>
where
S: de::SeqAccess<'de>,
{
let mut map = HashMap::new();
while let Some(id) = seq.next_element::<String>()? {
let entry = ChatSessionIndexEntry {
session_id: id.clone(),
title: String::new(),
last_message_date: 0,
timing: None,
last_response_state: 0,
initial_location: "panel".to_string(),
is_empty: false,
is_imported: None,
has_pending_edits: None,
is_external: None,
};
map.insert(id, entry);
}
Ok(map)
}
}
deserializer.deserialize_any(EntriesVisitor)
}
fn default_index_version() -> u32 {
1
}
impl Default for ChatSessionIndex {
fn default() -> Self {
Self {
version: 1,
entries: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatSessionTiming {
#[serde(default, alias = "startTime")]
pub created: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_request_started: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "endTime")]
pub last_request_ended: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatSessionIndexEntry {
pub session_id: String,
pub title: String,
pub last_message_date: i64,
#[serde(default)]
pub timing: Option<ChatSessionTiming>,
#[serde(default = "default_response_state")]
pub last_response_state: u8,
#[serde(default = "default_location")]
pub initial_location: String,
#[serde(default)]
pub is_empty: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_imported: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub has_pending_edits: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_external: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelCacheEntry {
#[serde(default)]
pub provider_type: String,
#[serde(default)]
pub provider_label: String,
pub resource: String,
#[serde(default)]
pub icon: String,
#[serde(default)]
pub label: String,
#[serde(default)]
pub status: u8,
#[serde(default)]
pub timing: ChatSessionTiming,
#[serde(default)]
pub initial_location: String,
#[serde(default)]
pub has_pending_edits: bool,
#[serde(default)]
pub is_empty: bool,
#[serde(default)]
pub is_external: bool,
#[serde(default)]
pub last_response_state: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StateCacheEntry {
pub resource: String,
#[serde(default)]
pub read: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct SessionWithPath {
pub path: std::path::PathBuf,
pub session: ChatSession,
}
impl SessionWithPath {
#[allow(dead_code)]
pub fn get_session_id(&self) -> String {
self.session.session_id.clone().unwrap_or_else(|| {
self.path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
})
}
}
impl ChatSession {
#[allow(dead_code)]
pub fn get_session_id(&self) -> String {
self.session_id
.clone()
.unwrap_or_else(|| "unknown".to_string())
}
pub fn title(&self) -> String {
if let Some(title) = &self.custom_title {
if !title.is_empty() {
return title.clone();
}
}
if let Some(first_req) = self.requests.first() {
if let Some(msg) = &first_req.message {
if let Some(text) = &msg.text {
let title: String = text.chars().take(50).collect();
if !title.is_empty() {
if title.len() < text.len() {
return format!("{}...", title);
}
return title;
}
}
}
}
"Untitled".to_string()
}
pub fn is_empty(&self) -> bool {
self.requests.is_empty()
}
pub fn request_count(&self) -> usize {
self.requests.len()
}
pub fn timestamp_range(&self) -> Option<(i64, i64)> {
if self.requests.is_empty() {
return None;
}
let timestamps: Vec<i64> = self.requests.iter().filter_map(|r| r.timestamp).collect();
if timestamps.is_empty() {
return None;
}
let min = *timestamps.iter().min().unwrap();
let max = *timestamps.iter().max().unwrap();
Some((min, max))
}
}