Skip to main content

pty_mcp/session/
model.rs

1use std::{collections::BTreeMap, fmt, str::FromStr};
2
3use chrono::{DateTime, Utc};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8use crate::ssh::SshConnectionId;
9
10#[derive(
11    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
12)]
13#[serde(transparent)]
14pub struct SessionId(String);
15
16impl SessionId {
17    pub fn new() -> Self {
18        Self(format!("pty_{}", Uuid::new_v4().simple()))
19    }
20
21    pub fn as_str(&self) -> &str {
22        &self.0
23    }
24}
25
26impl Default for SessionId {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl fmt::Display for SessionId {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        f.write_str(&self.0)
35    }
36}
37
38impl FromStr for SessionId {
39    type Err = &'static str;
40
41    fn from_str(value: &str) -> Result<Self, Self::Err> {
42        if value.trim().is_empty() {
43            return Err("session id cannot be empty");
44        }
45
46        Ok(Self(value.to_string()))
47    }
48}
49
50impl From<String> for SessionId {
51    fn from(value: String) -> Self {
52        Self(value)
53    }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
57#[serde(rename_all = "snake_case")]
58pub enum SessionStatus {
59    Starting,
60    Running,
61    Exited,
62    FailedToSpawn,
63    Closing,
64    Killed,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
68#[schemars(inline)]
69#[serde(rename_all = "snake_case")]
70pub enum SessionTransport {
71    #[default]
72    Local,
73    Ssh,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
77#[schemars(inline)]
78#[serde(rename_all = "snake_case")]
79pub enum ReadView {
80    Plain,
81    Ansi,
82    Raw,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
86#[schemars(inline)]
87#[serde(rename_all = "snake_case")]
88pub enum SignalKind {
89    Sigint,
90    Sigterm,
91    Sigkill,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
95pub struct Pagination {
96    pub offset: usize,
97    pub limit: usize,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
101pub struct BufferStats {
102    pub line_count: usize,
103    pub byte_count: usize,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
107pub struct ExitInfo {
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub exit_code: Option<i32>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub exit_signal: Option<String>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub finished_at: Option<DateTime<Utc>>,
114}
115
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
117pub struct SessionSummary {
118    pub session_id: SessionId,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub title: Option<String>,
121    pub description: String,
122    pub command: String,
123    #[serde(default, skip_serializing_if = "Vec::is_empty")]
124    pub args: Vec<String>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub cwd: Option<String>,
127    #[serde(default)]
128    pub transport: SessionTransport,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub connection_id: Option<SshConnectionId>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub target_summary: Option<String>,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub remote_cwd: Option<String>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub remote_command: Option<String>,
137    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
138    pub remote_env_preview: BTreeMap<String, String>,
139    pub status: SessionStatus,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub pid: Option<u32>,
142    pub started_at: DateTime<Utc>,
143    pub buffer_stats: BufferStats,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub exit_info: Option<ExitInfo>,
146}
147
148impl SessionSummary {
149    pub fn placeholder(description: impl Into<String>, command: impl Into<String>) -> Self {
150        Self {
151            session_id: SessionId::new(),
152            title: None,
153            description: description.into(),
154            command: command.into(),
155            args: Vec::new(),
156            cwd: None,
157            transport: SessionTransport::Local,
158            connection_id: None,
159            target_summary: None,
160            remote_cwd: None,
161            remote_command: None,
162            remote_env_preview: BTreeMap::new(),
163            status: SessionStatus::Starting,
164            pid: None,
165            started_at: Utc::now(),
166            buffer_stats: BufferStats::default(),
167            exit_info: None,
168        }
169    }
170}