use crate::types::permission::Ruleset;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Session {
pub id: String,
pub slug: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub directory: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<SessionSummary>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub share: Option<ShareInfo>,
#[serde(default)]
pub title: String,
#[serde(default)]
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time: Option<SessionTime>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission: Option<Ruleset>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revert: Option<RevertInfo>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionSummary {
pub additions: u64,
pub deletions: u64,
pub files: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diffs: Option<Vec<FileDiffLite>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FileDiffLite {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additions: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deletions: Option<u64>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShareInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub secret: Option<String>,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionTime {
pub created: i64,
pub updated: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compacting: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub archived: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RevertInfo {
pub message_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub part_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snapshot: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diff: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateSessionRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission: Option<Ruleset>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSessionRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SummarizeRequest {
pub provider_id: String,
pub model_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RevertRequest {
pub message_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub part_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionStatus {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_session_id: Option<String>,
#[serde(default)]
pub busy: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum SessionStatusInfo {
Idle,
Busy,
Retry {
attempt: u64,
message: String,
next: u64,
},
}
#[derive(Debug, Clone)]
pub enum SessionStatusResponse {
Legacy(SessionStatus),
Map(HashMap<String, SessionStatusInfo>),
}
impl SessionStatusResponse {
pub fn status_for(&self, session_id: &str) -> SessionStatusInfo {
match self {
Self::Map(map) => map
.get(session_id)
.cloned()
.unwrap_or(SessionStatusInfo::Idle),
Self::Legacy(status) => {
if status.busy && status.active_session_id.as_deref() == Some(session_id) {
SessionStatusInfo::Busy
} else if status.busy && status.active_session_id.is_none() {
SessionStatusInfo::Busy
} else {
SessionStatusInfo::Idle
}
}
}
}
pub fn into_legacy_summary(self) -> SessionStatus {
match self {
Self::Legacy(status) => status,
Self::Map(map) => {
let active: Vec<String> = map
.into_iter()
.filter_map(|(sid, status)| {
if matches!(
status,
SessionStatusInfo::Busy | SessionStatusInfo::Retry { .. }
) {
Some(sid)
} else {
None
}
})
.collect();
let busy = !active.is_empty();
let active_session_id = if active.len() == 1 {
active.into_iter().next()
} else {
None
};
SessionStatus {
active_session_id,
busy,
}
}
}
}
}
impl<'de> Deserialize<'de> for SessionStatusResponse {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let is_legacy = value.get("busy").is_some()
|| value.get("activeSessionId").is_some()
|| value.get("active_session_id").is_some();
if is_legacy {
let legacy: SessionStatus =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(Self::Legacy(legacy))
} else {
let map: HashMap<String, SessionStatusInfo> =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(Self::Map(map))
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionDiff {
#[serde(default)]
pub diff: String,
#[serde(default)]
pub files: Vec<String>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TodoItem {
pub id: String,
pub content: String,
#[serde(default)]
pub completed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_deserialize() {
let json = r#"{
"id": "s1",
"slug": "s1",
"projectId": "p1",
"directory": "/path/to/project",
"title": "Test Session",
"version": "1.0",
"time": {"created": 1234567890, "updated": 1234567890}
}"#;
let session: Session = serde_json::from_str(json).unwrap();
assert_eq!(session.id, "s1");
assert_eq!(session.slug, "s1");
assert_eq!(session.title, "Test Session");
}
#[test]
fn test_session_minimal_upstream() {
let json = r#"{"id": "s1", "slug": "s1"}"#;
let session: Session = serde_json::from_str(json).unwrap();
assert_eq!(session.id, "s1");
assert_eq!(session.slug, "s1");
assert!(session.project_id.is_none());
}
#[test]
fn test_session_missing_slug_fails() {
let json = r#"{"id": "s1"}"#;
assert!(serde_json::from_str::<Session>(json).is_err());
}
#[test]
fn test_session_with_optional_fields() {
let json = r#"{
"id": "s1",
"slug": "s1",
"projectId": "p1",
"directory": "/path",
"title": "Test",
"version": "1.0",
"time": {"created": 1234567890, "updated": 1234567890},
"parentId": "s0",
"share": {"url": "https://example.com/share/s1"}
}"#;
let session: Session = serde_json::from_str(json).unwrap();
assert_eq!(session.slug, "s1");
assert_eq!(session.parent_id, Some("s0".to_string()));
assert!(session.share.is_some());
}
#[test]
fn parse_legacy_status() {
let json = r#"{"busy": true, "activeSessionId": "s1"}"#;
let resp: SessionStatusResponse = serde_json::from_str(json).unwrap();
assert!(matches!(resp, SessionStatusResponse::Legacy(_)));
assert!(matches!(resp.status_for("s1"), SessionStatusInfo::Busy));
assert!(matches!(resp.status_for("s2"), SessionStatusInfo::Idle));
}
#[test]
fn parse_map_status() {
let json = r#"{"s1": {"type": "busy"}, "s2": {"type": "retry", "attempt": 2, "message": "rate limited", "next": 12345}}"#;
let resp: SessionStatusResponse = serde_json::from_str(json).unwrap();
assert!(matches!(resp, SessionStatusResponse::Map(_)));
assert!(matches!(resp.status_for("s1"), SessionStatusInfo::Busy));
assert!(matches!(
resp.status_for("s2"),
SessionStatusInfo::Retry { attempt: 2, .. }
));
assert!(matches!(resp.status_for("s3"), SessionStatusInfo::Idle));
}
#[test]
fn parse_empty_map_status() {
let json = r"{}";
let resp: SessionStatusResponse = serde_json::from_str(json).unwrap();
assert!(matches!(resp, SessionStatusResponse::Map(_)));
assert!(matches!(resp.status_for("any"), SessionStatusInfo::Idle));
}
}