agent_tui/daemon/usecases/
snapshot.rs

1use std::sync::Arc;
2
3use crate::common::mutex_lock_or_recover;
4use crate::core::vom::snapshot::{SnapshotOptions, format_snapshot};
5
6use crate::daemon::adapters::{
7    core_cursor_to_domain, core_elements_to_domain, core_snapshot_to_domain,
8};
9use crate::daemon::domain::{
10    AccessibilitySnapshotInput, AccessibilitySnapshotOutput, SnapshotInput, SnapshotOutput,
11};
12use crate::daemon::error::SessionError;
13use crate::daemon::repository::SessionRepository;
14use crate::daemon::session::SessionId;
15
16pub trait SnapshotUseCase: Send + Sync {
17    fn execute(&self, input: SnapshotInput) -> Result<SnapshotOutput, SessionError>;
18}
19
20pub struct SnapshotUseCaseImpl<R: SessionRepository> {
21    repository: Arc<R>,
22}
23
24impl<R: SessionRepository> SnapshotUseCaseImpl<R> {
25    pub fn new(repository: Arc<R>) -> Self {
26        Self { repository }
27    }
28}
29
30impl<R: SessionRepository> SnapshotUseCase for SnapshotUseCaseImpl<R> {
31    /// Executes the snapshot use case.
32    ///
33    /// **Side effect**: Calls `session_guard.update()` which reads pending PTY
34    /// output and updates the terminal emulator state before capturing the snapshot.
35    fn execute(&self, input: SnapshotInput) -> Result<SnapshotOutput, SessionError> {
36        let session = self.repository.resolve(input.session_id.as_deref())?;
37        let mut session_guard = mutex_lock_or_recover(&session);
38
39        // Update terminal state before capturing snapshot
40        session_guard.update()?;
41
42        let screen = session_guard.screen_text();
43        let session_id = SessionId::from(session_guard.id.as_str());
44
45        let elements = if input.include_elements {
46            Some(core_elements_to_domain(session_guard.detect_elements()))
47        } else {
48            None
49        };
50
51        let cursor = if input.include_cursor {
52            Some(core_cursor_to_domain(&session_guard.cursor()))
53        } else {
54            None
55        };
56
57        Ok(SnapshotOutput {
58            session_id,
59            screen,
60            elements,
61            cursor,
62        })
63    }
64}
65
66pub trait AccessibilitySnapshotUseCase: Send + Sync {
67    fn execute(
68        &self,
69        input: AccessibilitySnapshotInput,
70    ) -> Result<AccessibilitySnapshotOutput, SessionError>;
71}
72
73pub struct AccessibilitySnapshotUseCaseImpl<R: SessionRepository> {
74    repository: Arc<R>,
75}
76
77impl<R: SessionRepository> AccessibilitySnapshotUseCaseImpl<R> {
78    pub fn new(repository: Arc<R>) -> Self {
79        Self { repository }
80    }
81}
82
83impl<R: SessionRepository> AccessibilitySnapshotUseCase for AccessibilitySnapshotUseCaseImpl<R> {
84    /// Executes the accessibility snapshot use case.
85    ///
86    /// **Side effect**: Calls `session_guard.update()` which reads pending PTY
87    /// output and updates the terminal emulator state before capturing the snapshot.
88    /// This ensures the snapshot reflects the most recent terminal content.
89    fn execute(
90        &self,
91        input: AccessibilitySnapshotInput,
92    ) -> Result<AccessibilitySnapshotOutput, SessionError> {
93        let session = self.repository.resolve(input.session_id.as_deref())?;
94        let mut session_guard = mutex_lock_or_recover(&session);
95
96        // Update terminal state before capturing snapshot
97        session_guard.update()?;
98
99        let session_id = SessionId::from(session_guard.id.as_str());
100        let components = session_guard.analyze_screen();
101
102        let options = SnapshotOptions {
103            interactive_only: input.interactive_only,
104            ..Default::default()
105        };
106        let core_snapshot = format_snapshot(&components, &options);
107        let snapshot = core_snapshot_to_domain(&core_snapshot);
108
109        Ok(AccessibilitySnapshotOutput {
110            session_id,
111            snapshot,
112        })
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::daemon::domain::SessionId;
120    use crate::daemon::test_support::MockSessionRepository;
121
122    #[test]
123    fn test_snapshot_usecase_returns_error_when_no_session() {
124        let repository = Arc::new(MockSessionRepository::new());
125        let usecase = SnapshotUseCaseImpl::new(repository);
126
127        let input = SnapshotInput::default();
128        let result = usecase.execute(input);
129
130        assert!(result.is_err());
131    }
132
133    #[test]
134    fn test_enhanced_snapshot_usecase_returns_error_when_no_session() {
135        let repository = Arc::new(MockSessionRepository::new());
136        let usecase = AccessibilitySnapshotUseCaseImpl::new(repository);
137
138        let input = AccessibilitySnapshotInput::default();
139        let result = usecase.execute(input);
140
141        assert!(result.is_err());
142    }
143
144    #[test]
145    fn test_enhanced_snapshot_input_default() {
146        let input = AccessibilitySnapshotInput::default();
147
148        assert!(input.session_id.is_none());
149        assert!(!input.interactive_only);
150    }
151
152    #[test]
153    fn test_enhanced_snapshot_input_with_options() {
154        let input = AccessibilitySnapshotInput {
155            session_id: Some(SessionId::new("test-session")),
156            interactive_only: true,
157        };
158
159        assert_eq!(input.session_id, Some(SessionId::new("test-session")));
160        assert!(input.interactive_only);
161    }
162}