Skip to main content

agent_tui_usecases/usecases/
session.rs

1//! Session use cases.
2
3use std::sync::Arc;
4
5use crate::domain::AssertConditionType;
6use crate::domain::AssertInput;
7use crate::domain::AssertOutput;
8use crate::domain::AttachInput;
9use crate::domain::AttachOutput;
10use crate::domain::CleanupFailure;
11use crate::domain::CleanupInput;
12use crate::domain::CleanupOutput;
13use crate::domain::KillOutput;
14use crate::domain::ResizeInput;
15use crate::domain::ResizeOutput;
16use crate::domain::RestartOutput;
17use crate::domain::SessionInput;
18use crate::domain::SessionsOutput;
19use crate::domain::SpawnInput;
20use crate::domain::SpawnOutput;
21use crate::usecases::SpawnError;
22use crate::usecases::ports::SessionError;
23use crate::usecases::ports::SessionRepository;
24use crate::usecases::ports::SpawnErrorKind;
25use crate::usecases::ports::TerminalError;
26
27pub trait SpawnUseCase: Send + Sync {
28    fn execute(&self, input: SpawnInput) -> Result<SpawnOutput, SpawnError>;
29}
30
31pub struct SpawnUseCaseImpl<R: SessionRepository> {
32    repository: Arc<R>,
33}
34
35impl<R: SessionRepository> SpawnUseCaseImpl<R> {
36    pub fn new(repository: Arc<R>) -> Self {
37        Self { repository }
38    }
39}
40
41impl<R: SessionRepository> SpawnUseCase for SpawnUseCaseImpl<R> {
42    fn execute(&self, input: SpawnInput) -> Result<SpawnOutput, SpawnError> {
43        let SpawnInput {
44            command,
45            args,
46            cwd,
47            env,
48            session_id,
49            size,
50        } = input;
51
52        match self.repository.spawn(
53            &command,
54            &args,
55            cwd.as_deref(),
56            env.as_ref(),
57            session_id,
58            size,
59        ) {
60            Ok((session_id, pid)) => Ok(SpawnOutput { session_id, pid }),
61            Err(SessionError::LimitReached(max)) => Err(SpawnError::SessionLimitReached { max }),
62            Err(SessionError::AlreadyExists(session_id)) => {
63                Err(SpawnError::SessionAlreadyExists { session_id })
64            }
65            Err(SessionError::Terminal(TerminalError::Spawn { kind, reason })) => match kind {
66                SpawnErrorKind::NotFound => Err(SpawnError::CommandNotFound { command }),
67                SpawnErrorKind::PermissionDenied => Err(SpawnError::PermissionDenied { command }),
68                SpawnErrorKind::Other => Err(SpawnError::TerminalError {
69                    operation: "spawn".to_string(),
70                    reason,
71                }),
72            },
73            Err(SessionError::Terminal(term_err)) => Err(SpawnError::TerminalError {
74                operation: term_err.operation().to_string(),
75                reason: term_err.reason().to_string(),
76            }),
77            Err(e) => Err(SpawnError::TerminalError {
78                operation: "spawn".to_string(),
79                reason: e.to_string(),
80            }),
81        }
82    }
83}
84
85pub trait SessionsUseCase: Send + Sync {
86    fn execute(&self) -> SessionsOutput;
87}
88
89pub struct SessionsUseCaseImpl<R: SessionRepository> {
90    repository: Arc<R>,
91}
92
93impl<R: SessionRepository> SessionsUseCaseImpl<R> {
94    pub fn new(repository: Arc<R>) -> Self {
95        Self { repository }
96    }
97}
98
99impl<R: SessionRepository> SessionsUseCase for SessionsUseCaseImpl<R> {
100    fn execute(&self) -> SessionsOutput {
101        let sessions = self.repository.list();
102        let active_session = self.repository.active_session_id();
103
104        SessionsOutput {
105            sessions,
106            active_session,
107        }
108    }
109}
110
111pub trait KillUseCase: Send + Sync {
112    fn execute(&self, input: SessionInput) -> Result<KillOutput, SessionError>;
113}
114
115pub struct KillUseCaseImpl<R: SessionRepository> {
116    repository: Arc<R>,
117}
118
119impl<R: SessionRepository> KillUseCaseImpl<R> {
120    pub fn new(repository: Arc<R>) -> Self {
121        Self { repository }
122    }
123}
124
125impl<R: SessionRepository> KillUseCase for KillUseCaseImpl<R> {
126    fn execute(&self, input: SessionInput) -> Result<KillOutput, SessionError> {
127        let session = self.repository.resolve(input.session_id.as_ref())?;
128        let session_id = session.session_id();
129
130        self.repository.kill(&session_id)?;
131
132        Ok(KillOutput {
133            session_id,
134            success: true,
135        })
136    }
137}
138
139pub trait RestartUseCase: Send + Sync {
140    fn execute(&self, input: SessionInput) -> Result<RestartOutput, SessionError>;
141}
142
143pub struct RestartUseCaseImpl<R: SessionRepository> {
144    repository: Arc<R>,
145}
146
147impl<R: SessionRepository> RestartUseCaseImpl<R> {
148    pub fn new(repository: Arc<R>) -> Self {
149        Self { repository }
150    }
151}
152
153impl<R: SessionRepository> RestartUseCase for RestartUseCaseImpl<R> {
154    fn execute(&self, input: SessionInput) -> Result<RestartOutput, SessionError> {
155        self.repository.restart(input.session_id.as_ref())
156    }
157}
158
159pub trait AttachUseCase: Send + Sync {
160    fn execute(&self, input: AttachInput) -> Result<AttachOutput, SessionError>;
161}
162
163pub struct AttachUseCaseImpl<R: SessionRepository> {
164    repository: Arc<R>,
165}
166
167impl<R: SessionRepository> AttachUseCaseImpl<R> {
168    pub fn new(repository: Arc<R>) -> Self {
169        Self { repository }
170    }
171}
172
173impl<R: SessionRepository> AttachUseCase for AttachUseCaseImpl<R> {
174    fn execute(&self, input: AttachInput) -> Result<AttachOutput, SessionError> {
175        let session = self.repository.resolve(Some(&input.session_id))?;
176
177        if !session.is_running() {
178            return Err(SessionError::NotRunning {
179                session_id: input.session_id.to_string(),
180            });
181        }
182
183        self.repository.set_active(&input.session_id)?;
184
185        Ok(AttachOutput {
186            session_id: input.session_id,
187            success: true,
188        })
189    }
190}
191
192pub trait ResizeUseCase: Send + Sync {
193    fn execute(&self, input: ResizeInput) -> Result<ResizeOutput, SessionError>;
194}
195
196pub struct ResizeUseCaseImpl<R: SessionRepository> {
197    repository: Arc<R>,
198}
199
200impl<R: SessionRepository> ResizeUseCaseImpl<R> {
201    pub fn new(repository: Arc<R>) -> Self {
202        Self { repository }
203    }
204}
205
206impl<R: SessionRepository> ResizeUseCase for ResizeUseCaseImpl<R> {
207    fn execute(&self, input: ResizeInput) -> Result<ResizeOutput, SessionError> {
208        let session = self.repository.resolve(input.session_id.as_ref())?;
209        session.resize(input.size)?;
210
211        Ok(ResizeOutput {
212            session_id: session.session_id(),
213            success: true,
214            size: input.size,
215        })
216    }
217}
218
219pub trait CleanupUseCase: Send + Sync {
220    fn execute(&self, input: CleanupInput) -> CleanupOutput;
221}
222
223pub struct CleanupUseCaseImpl<R: SessionRepository> {
224    repository: Arc<R>,
225}
226
227impl<R: SessionRepository> CleanupUseCaseImpl<R> {
228    pub fn new(repository: Arc<R>) -> Self {
229        Self { repository }
230    }
231}
232
233impl<R: SessionRepository> CleanupUseCase for CleanupUseCaseImpl<R> {
234    fn execute(&self, input: CleanupInput) -> CleanupOutput {
235        let sessions = self.repository.list();
236        let mut cleaned = 0;
237        let mut failures = Vec::new();
238
239        for session in sessions {
240            let should_cleanup = input.all || !session.running;
241            if !should_cleanup {
242                continue;
243            }
244
245            match self.repository.kill(&session.id) {
246                Ok(()) => cleaned += 1,
247                Err(err) => failures.push(CleanupFailure {
248                    session_id: session.id,
249                    error: err.to_string(),
250                }),
251            }
252        }
253
254        CleanupOutput { cleaned, failures }
255    }
256}
257
258pub trait AssertUseCase: Send + Sync {
259    fn execute(&self, input: AssertInput) -> Result<AssertOutput, SessionError>;
260}
261
262pub struct AssertUseCaseImpl<R: SessionRepository> {
263    repository: Arc<R>,
264}
265
266impl<R: SessionRepository> AssertUseCaseImpl<R> {
267    pub fn new(repository: Arc<R>) -> Self {
268        Self { repository }
269    }
270}
271
272impl<R: SessionRepository> AssertUseCase for AssertUseCaseImpl<R> {
273    fn execute(&self, input: AssertInput) -> Result<AssertOutput, SessionError> {
274        let condition = format!("{}:{}", input.condition_type.as_str(), input.value);
275
276        let passed = match input.condition_type {
277            AssertConditionType::Text => {
278                let session = self.repository.resolve(input.session_id.as_ref())?;
279                session.update()?;
280                let screen = session.screen_text();
281                screen.contains(&input.value)
282            }
283            AssertConditionType::Session => {
284                let sessions = self.repository.list();
285                sessions
286                    .iter()
287                    .any(|s| s.id.as_str() == input.value && s.is_active())
288            }
289            other => {
290                return Err(SessionError::InvalidKey(format!(
291                    "Unsupported assert condition: {}",
292                    other.as_str()
293                )));
294            }
295        };
296
297        Ok(AssertOutput { passed, condition })
298    }
299}
300
301#[cfg(test)]
302#[path = "session_tests.rs"]
303mod tests;