agent_tui/daemon/domain/
types.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::str::FromStr;
4
5use super::session_types::ErrorEntry;
6use super::session_types::RecordingFrame;
7use super::session_types::RecordingStatus;
8use super::session_types::SessionId;
9use super::session_types::SessionInfo;
10use super::session_types::TraceEntry;
11
12/// Error returned when ScrollDirection parsing fails.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ScrollDirectionError {
15    pub invalid_value: String,
16}
17
18/// Error returned when WaitConditionType parsing fails.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct WaitConditionTypeError {
21    pub invalid_value: String,
22}
23
24impl fmt::Display for WaitConditionTypeError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        write!(
27            f,
28            "Invalid wait condition type '{}'. Must be one of: text, element, focused, not_visible, stable, text_gone, value",
29            self.invalid_value
30        )
31    }
32}
33
34impl std::error::Error for WaitConditionTypeError {}
35
36/// Type of wait condition for the wait use case.
37///
38/// This represents the kind of condition to wait for, without the associated data.
39/// The actual condition data is provided separately.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum WaitConditionType {
42    /// Wait for specific text to appear on screen.
43    Text,
44    /// Wait for an element to exist.
45    Element,
46    /// Wait for an element to be focused.
47    Focused,
48    /// Wait for an element to disappear.
49    NotVisible,
50    /// Wait for screen to stabilize (no changes).
51    Stable,
52    /// Wait for specific text to disappear.
53    TextGone,
54    /// Wait for an element to have a specific value.
55    Value,
56}
57
58impl WaitConditionType {
59    /// Parse a wait condition type from a string (case-insensitive).
60    pub fn parse(s: &str) -> Result<Self, WaitConditionTypeError> {
61        match s.to_lowercase().as_str() {
62            "text" => Ok(Self::Text),
63            "element" => Ok(Self::Element),
64            "focused" => Ok(Self::Focused),
65            "not_visible" => Ok(Self::NotVisible),
66            "stable" => Ok(Self::Stable),
67            "text_gone" => Ok(Self::TextGone),
68            "value" => Ok(Self::Value),
69            _ => Err(WaitConditionTypeError {
70                invalid_value: s.to_string(),
71            }),
72        }
73    }
74
75    /// Get the condition type as a lowercase string.
76    pub fn as_str(&self) -> &'static str {
77        match self {
78            Self::Text => "text",
79            Self::Element => "element",
80            Self::Focused => "focused",
81            Self::NotVisible => "not_visible",
82            Self::Stable => "stable",
83            Self::TextGone => "text_gone",
84            Self::Value => "value",
85        }
86    }
87
88    /// Returns true if this condition requires a target element reference.
89    pub fn requires_target(&self) -> bool {
90        matches!(
91            self,
92            Self::Element | Self::Focused | Self::NotVisible | Self::Value
93        )
94    }
95
96    /// Returns true if this condition requires text to match.
97    pub fn requires_text(&self) -> bool {
98        matches!(self, Self::Text | Self::TextGone | Self::Value)
99    }
100}
101
102impl fmt::Display for WaitConditionType {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        write!(f, "{}", self.as_str())
105    }
106}
107
108impl FromStr for WaitConditionType {
109    type Err = WaitConditionTypeError;
110
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        Self::parse(s)
113    }
114}
115
116impl fmt::Display for ScrollDirectionError {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(
119            f,
120            "Invalid scroll direction '{}'. Must be one of: up, down, left, right",
121            self.invalid_value
122        )
123    }
124}
125
126impl std::error::Error for ScrollDirectionError {}
127
128/// Direction for scrolling operations.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130pub enum ScrollDirection {
131    Up,
132    Down,
133    Left,
134    Right,
135}
136
137impl ScrollDirection {
138    /// Parse a scroll direction from a string (case-insensitive).
139    pub fn parse(s: &str) -> Result<Self, ScrollDirectionError> {
140        match s.to_lowercase().as_str() {
141            "up" => Ok(Self::Up),
142            "down" => Ok(Self::Down),
143            "left" => Ok(Self::Left),
144            "right" => Ok(Self::Right),
145            _ => Err(ScrollDirectionError {
146                invalid_value: s.to_string(),
147            }),
148        }
149    }
150
151    /// Get the direction as a lowercase string.
152    pub fn as_str(&self) -> &'static str {
153        match self {
154            Self::Up => "up",
155            Self::Down => "down",
156            Self::Left => "left",
157            Self::Right => "right",
158        }
159    }
160}
161
162impl fmt::Display for ScrollDirection {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(f, "{}", self.as_str())
165    }
166}
167
168impl FromStr for ScrollDirection {
169    type Err = ScrollDirectionError;
170
171    fn from_str(s: &str) -> Result<Self, Self::Err> {
172        Self::parse(s)
173    }
174}
175
176#[derive(Debug, Clone, Copy)]
177pub struct DomainCursorPosition {
178    pub row: u16,
179    pub col: u16,
180    pub visible: bool,
181}
182
183#[derive(Debug, Clone)]
184pub struct DomainPosition {
185    pub row: u16,
186    pub col: u16,
187    pub width: Option<u16>,
188    pub height: Option<u16>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Hash)]
192pub enum DomainElementType {
193    Button,
194    Input,
195    Checkbox,
196    Radio,
197    Select,
198    MenuItem,
199    ListItem,
200    Spinner,
201    Progress,
202    Link,
203}
204
205impl DomainElementType {
206    pub fn as_str(&self) -> &'static str {
207        match self {
208            DomainElementType::Button => "button",
209            DomainElementType::Input => "input",
210            DomainElementType::Checkbox => "checkbox",
211            DomainElementType::Radio => "radio",
212            DomainElementType::Select => "select",
213            DomainElementType::MenuItem => "menuitem",
214            DomainElementType::ListItem => "listitem",
215            DomainElementType::Spinner => "spinner",
216            DomainElementType::Progress => "progress",
217            DomainElementType::Link => "link",
218        }
219    }
220
221    /// Returns true if this element type represents an interactive element.
222    pub fn is_interactive(&self) -> bool {
223        matches!(
224            self,
225            DomainElementType::Button
226                | DomainElementType::Input
227                | DomainElementType::Checkbox
228                | DomainElementType::Radio
229                | DomainElementType::Select
230                | DomainElementType::MenuItem
231                | DomainElementType::Link
232        )
233    }
234
235    /// Returns true if this element type can receive text input.
236    pub fn accepts_input(&self) -> bool {
237        matches!(self, DomainElementType::Input)
238    }
239
240    /// Returns true if this element type can be toggled.
241    pub fn is_toggleable(&self) -> bool {
242        matches!(self, DomainElementType::Checkbox | DomainElementType::Radio)
243    }
244}
245
246#[derive(Debug, Clone)]
247pub struct DomainElement {
248    pub element_ref: String,
249    pub element_type: DomainElementType,
250    pub label: Option<String>,
251    pub value: Option<String>,
252    pub position: DomainPosition,
253    pub focused: bool,
254    pub selected: bool,
255    pub checked: Option<bool>,
256    pub disabled: Option<bool>,
257    pub hint: Option<String>,
258}
259
260impl DomainElement {
261    /// Returns true if this element is interactive (can be clicked, typed into, etc.).
262    pub fn is_interactive(&self) -> bool {
263        self.element_type.is_interactive()
264    }
265
266    /// Returns true if this element can be clicked.
267    ///
268    /// An element can be clicked if it's interactive and not disabled.
269    pub fn can_click(&self) -> bool {
270        self.is_interactive() && !self.is_disabled()
271    }
272
273    /// Returns true if this element can receive text input.
274    pub fn can_type(&self) -> bool {
275        self.element_type.accepts_input() && !self.is_disabled()
276    }
277
278    /// Returns true if this element is disabled.
279    pub fn is_disabled(&self) -> bool {
280        self.disabled.unwrap_or(false)
281    }
282
283    /// Returns true if this element is enabled (not disabled).
284    pub fn is_enabled(&self) -> bool {
285        !self.is_disabled()
286    }
287
288    /// Returns true if this element is currently focused.
289    pub fn is_focused(&self) -> bool {
290        self.focused
291    }
292
293    /// Returns true if this element is currently selected.
294    pub fn is_selected(&self) -> bool {
295        self.selected
296    }
297
298    /// Returns true if this element is checked (for checkboxes/radios).
299    ///
300    /// Returns None if the element doesn't support checked state.
301    pub fn is_checked(&self) -> Option<bool> {
302        self.checked
303    }
304
305    /// Returns the display text for this element (label or value).
306    pub fn display_text(&self) -> Option<&str> {
307        self.label.as_deref().or(self.value.as_deref())
308    }
309}
310
311#[derive(Debug, Clone)]
312pub struct SpawnInput {
313    pub command: String,
314    pub args: Vec<String>,
315    pub cwd: Option<String>,
316    pub env: Option<HashMap<String, String>>,
317    pub session_id: Option<SessionId>,
318    pub cols: u16,
319    pub rows: u16,
320}
321
322#[derive(Debug, Clone)]
323pub struct SpawnOutput {
324    pub session_id: SessionId,
325    pub pid: u32,
326}
327
328#[derive(Debug, Clone)]
329pub struct RestartOutput {
330    pub old_session_id: SessionId,
331    pub new_session_id: SessionId,
332    pub command: String,
333    pub pid: u32,
334}
335
336#[derive(Debug, Clone, Default)]
337pub struct SnapshotInput {
338    pub session_id: Option<SessionId>,
339    pub include_elements: bool,
340    pub region: Option<String>,
341    pub strip_ansi: bool,
342    pub include_cursor: bool,
343}
344
345#[derive(Debug, Clone)]
346pub struct SnapshotOutput {
347    pub session_id: SessionId,
348    pub screen: String,
349    pub elements: Option<Vec<DomainElement>>,
350    pub cursor: Option<DomainCursorPosition>,
351}
352
353#[derive(Debug, Clone, Default)]
354pub struct AccessibilitySnapshotInput {
355    pub session_id: Option<SessionId>,
356    pub interactive_only: bool,
357}
358
359/// Error returned when DomainBounds validation fails.
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct DomainBoundsError {
362    pub message: String,
363}
364
365impl fmt::Display for DomainBoundsError {
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        write!(f, "{}", self.message)
368    }
369}
370
371impl std::error::Error for DomainBoundsError {}
372
373/// Role of a UI element in the accessibility tree.
374///
375/// This mirrors the VOM Role enum in agent-tui-core but is defined
376/// independently to maintain domain layer isolation.
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
378pub enum DomainRole {
379    Button,
380    Tab,
381    Input,
382    StaticText,
383    Panel,
384    Checkbox,
385    MenuItem,
386    Status,
387    ToolBlock,
388    PromptMarker,
389    ProgressBar,
390    Link,
391    ErrorMessage,
392    DiffLine,
393    CodeBlock,
394}
395
396impl DomainRole {
397    /// Returns the role as a lowercase string for serialization.
398    pub fn as_str(&self) -> &'static str {
399        match self {
400            DomainRole::Button => "button",
401            DomainRole::Tab => "tab",
402            DomainRole::Input => "input",
403            DomainRole::StaticText => "text",
404            DomainRole::Panel => "panel",
405            DomainRole::Checkbox => "checkbox",
406            DomainRole::MenuItem => "menuitem",
407            DomainRole::Status => "status",
408            DomainRole::ToolBlock => "toolblock",
409            DomainRole::PromptMarker => "prompt",
410            DomainRole::ProgressBar => "progressbar",
411            DomainRole::Link => "link",
412            DomainRole::ErrorMessage => "error",
413            DomainRole::DiffLine => "diff",
414            DomainRole::CodeBlock => "codeblock",
415        }
416    }
417
418    /// Returns true if this role represents an interactive element.
419    pub fn is_interactive(&self) -> bool {
420        matches!(
421            self,
422            DomainRole::Button
423                | DomainRole::Tab
424                | DomainRole::Input
425                | DomainRole::Checkbox
426                | DomainRole::MenuItem
427                | DomainRole::PromptMarker
428                | DomainRole::Link
429        )
430    }
431}
432
433impl fmt::Display for DomainRole {
434    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435        write!(f, "{}", self.as_str())
436    }
437}
438
439/// Bounding rectangle for UI elements with validation.
440///
441/// # Invariants
442/// - Width must be at least 1
443/// - Height must be at least 1
444#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445pub struct DomainBounds {
446    x: u16,
447    y: u16,
448    width: u16,
449    height: u16,
450}
451
452impl DomainBounds {
453    /// Create a new DomainBounds with validation.
454    ///
455    /// # Errors
456    /// Returns `DomainBoundsError` if width or height is zero.
457    pub fn new(x: u16, y: u16, width: u16, height: u16) -> Result<Self, DomainBoundsError> {
458        if width == 0 {
459            return Err(DomainBoundsError {
460                message: "Width must be at least 1".to_string(),
461            });
462        }
463        if height == 0 {
464            return Err(DomainBoundsError {
465                message: "Height must be at least 1".to_string(),
466            });
467        }
468        Ok(Self {
469            x,
470            y,
471            width,
472            height,
473        })
474    }
475
476    /// Create DomainBounds without validation.
477    ///
478    /// # Safety
479    /// Use only when bounds are known to be valid (e.g., from trusted sources).
480    pub fn new_unchecked(x: u16, y: u16, width: u16, height: u16) -> Self {
481        Self {
482            x,
483            y,
484            width,
485            height,
486        }
487    }
488
489    /// Get the x coordinate.
490    pub fn x(&self) -> u16 {
491        self.x
492    }
493
494    /// Get the y coordinate.
495    pub fn y(&self) -> u16 {
496        self.y
497    }
498
499    /// Get the width.
500    pub fn width(&self) -> u16 {
501        self.width
502    }
503
504    /// Get the height.
505    pub fn height(&self) -> u16 {
506        self.height
507    }
508
509    /// Check if a point is contained within these bounds.
510    pub fn contains(&self, x: u16, y: u16) -> bool {
511        x >= self.x
512            && x < self.x.saturating_add(self.width)
513            && y >= self.y
514            && y < self.y.saturating_add(self.height)
515    }
516}
517
518#[derive(Debug, Clone)]
519pub struct DomainElementRef {
520    pub role: DomainRole,
521    pub name: Option<String>,
522    pub bounds: DomainBounds,
523    pub visual_hash: u64,
524    pub nth: Option<usize>,
525    pub selected: bool,
526}
527
528#[derive(Debug, Clone, Default)]
529pub struct DomainRefMap {
530    pub refs: HashMap<String, DomainElementRef>,
531}
532
533impl DomainRefMap {
534    pub fn get(&self, ref_id: &str) -> Option<&DomainElementRef> {
535        self.refs.get(ref_id)
536    }
537}
538
539#[derive(Debug, Clone)]
540pub struct DomainSnapshotStats {
541    pub total: usize,
542    pub interactive: usize,
543    pub lines: usize,
544}
545
546#[derive(Debug, Clone)]
547pub struct DomainAccessibilitySnapshot {
548    pub tree: String,
549    pub refs: DomainRefMap,
550    pub stats: DomainSnapshotStats,
551}
552
553#[derive(Debug, Clone)]
554pub struct AccessibilitySnapshotOutput {
555    pub session_id: SessionId,
556    pub snapshot: DomainAccessibilitySnapshot,
557}
558
559#[derive(Debug, Clone)]
560pub struct ClickInput {
561    pub session_id: Option<SessionId>,
562    pub element_ref: String,
563}
564
565#[derive(Debug, Clone)]
566pub struct ClickOutput {
567    pub success: bool,
568    pub message: Option<String>,
569    pub warning: Option<String>,
570}
571
572#[derive(Debug, Clone)]
573pub struct FillInput {
574    pub session_id: Option<SessionId>,
575    pub element_ref: String,
576    pub value: String,
577}
578
579#[derive(Debug, Clone)]
580pub struct FillOutput {
581    pub success: bool,
582    pub message: Option<String>,
583}
584
585#[derive(Debug, Clone)]
586pub struct KeystrokeInput {
587    pub session_id: Option<SessionId>,
588    pub key: String,
589}
590
591#[derive(Debug, Clone)]
592pub struct KeystrokeOutput {
593    pub success: bool,
594}
595
596#[derive(Debug, Clone)]
597pub struct TypeInput {
598    pub session_id: Option<SessionId>,
599    pub text: String,
600}
601
602#[derive(Debug, Clone)]
603pub struct TypeOutput {
604    pub success: bool,
605}
606
607#[derive(Debug, Clone)]
608pub struct KeydownInput {
609    pub session_id: Option<SessionId>,
610    pub key: String,
611}
612
613#[derive(Debug, Clone)]
614pub struct KeydownOutput {
615    pub success: bool,
616}
617
618#[derive(Debug, Clone)]
619pub struct KeyupInput {
620    pub session_id: Option<SessionId>,
621    pub key: String,
622}
623
624#[derive(Debug, Clone)]
625pub struct KeyupOutput {
626    pub success: bool,
627}
628
629#[derive(Debug, Clone)]
630pub struct WaitInput {
631    pub session_id: Option<SessionId>,
632    pub text: Option<String>,
633    pub timeout_ms: u64,
634    pub condition: Option<String>,
635    pub target: Option<String>,
636}
637
638#[derive(Debug, Clone)]
639pub struct WaitOutput {
640    pub found: bool,
641    pub elapsed_ms: u64,
642}
643
644#[derive(Debug, Clone, Default)]
645pub struct FindInput {
646    pub session_id: Option<SessionId>,
647    pub role: Option<String>,
648    pub name: Option<String>,
649    pub text: Option<String>,
650    pub placeholder: Option<String>,
651    pub focused: Option<bool>,
652    pub nth: Option<usize>,
653    pub exact: bool,
654}
655
656#[derive(Debug, Clone)]
657pub struct FindOutput {
658    pub elements: Vec<DomainElement>,
659    pub count: usize,
660}
661
662#[derive(Debug, Clone)]
663pub struct ScrollInput {
664    pub session_id: Option<SessionId>,
665    pub direction: String,
666    pub amount: u16,
667}
668
669#[derive(Debug, Clone)]
670pub struct ScrollOutput {
671    pub success: bool,
672}
673
674#[derive(Debug, Clone)]
675pub struct ResizeInput {
676    pub session_id: Option<SessionId>,
677    pub cols: u16,
678    pub rows: u16,
679}
680
681#[derive(Debug, Clone)]
682pub struct ResizeOutput {
683    pub session_id: SessionId,
684    pub success: bool,
685    pub cols: u16,
686    pub rows: u16,
687}
688
689#[derive(Debug, Clone)]
690pub struct SessionsOutput {
691    pub sessions: Vec<SessionInfo>,
692    pub active_session: Option<SessionId>,
693}
694
695#[derive(Debug, Clone)]
696pub struct KillOutput {
697    pub session_id: SessionId,
698    pub success: bool,
699}
700
701#[derive(Debug, Clone)]
702pub struct ElementStateInput {
703    pub session_id: Option<SessionId>,
704    pub element_ref: String,
705}
706
707#[derive(Debug, Clone)]
708pub struct VisibilityOutput {
709    pub found: bool,
710    pub visible: bool,
711}
712
713#[derive(Debug, Clone)]
714pub struct FocusCheckOutput {
715    pub found: bool,
716    pub focused: bool,
717}
718
719#[derive(Debug, Clone)]
720pub struct IsEnabledOutput {
721    pub found: bool,
722    pub enabled: bool,
723}
724
725#[derive(Debug, Clone)]
726pub struct IsCheckedOutput {
727    pub found: bool,
728    pub checked: bool,
729    pub message: Option<String>,
730}
731
732#[derive(Debug, Clone)]
733pub struct GetTextOutput {
734    pub found: bool,
735    pub text: String,
736}
737
738#[derive(Debug, Clone)]
739pub struct GetValueOutput {
740    pub found: bool,
741    pub value: String,
742}
743
744#[derive(Debug, Clone)]
745pub struct GetFocusedOutput {
746    pub found: bool,
747    pub element: Option<DomainElement>,
748}
749
750#[derive(Debug, Clone)]
751pub struct DoubleClickInput {
752    pub session_id: Option<SessionId>,
753    pub element_ref: String,
754}
755
756#[derive(Debug, Clone)]
757pub struct DoubleClickOutput {
758    pub success: bool,
759}
760
761#[derive(Debug, Clone)]
762pub struct FocusInput {
763    pub session_id: Option<SessionId>,
764    pub element_ref: String,
765}
766
767#[derive(Debug, Clone)]
768pub struct FocusOutput {
769    pub success: bool,
770}
771
772#[derive(Debug, Clone)]
773pub struct ClearInput {
774    pub session_id: Option<SessionId>,
775    pub element_ref: String,
776}
777
778#[derive(Debug, Clone)]
779pub struct ClearOutput {
780    pub success: bool,
781}
782
783#[derive(Debug, Clone)]
784pub struct SelectAllInput {
785    pub session_id: Option<SessionId>,
786    pub element_ref: String,
787}
788
789#[derive(Debug, Clone)]
790pub struct SelectAllOutput {
791    pub success: bool,
792}
793
794#[derive(Debug, Clone)]
795pub struct ToggleInput {
796    pub session_id: Option<SessionId>,
797    pub element_ref: String,
798    pub state: Option<bool>,
799}
800
801#[derive(Debug, Clone)]
802pub struct ToggleOutput {
803    pub success: bool,
804    pub checked: bool,
805    pub message: Option<String>,
806}
807
808#[derive(Debug, Clone)]
809pub struct SelectInput {
810    pub session_id: Option<SessionId>,
811    pub element_ref: String,
812    pub option: String,
813}
814
815#[derive(Debug, Clone)]
816pub struct SelectOutput {
817    pub success: bool,
818    pub selected_option: String,
819    pub message: Option<String>,
820}
821
822#[derive(Debug, Clone)]
823pub struct MultiselectInput {
824    pub session_id: Option<SessionId>,
825    pub element_ref: String,
826    pub options: Vec<String>,
827}
828
829#[derive(Debug, Clone)]
830pub struct MultiselectOutput {
831    pub success: bool,
832    pub selected_options: Vec<String>,
833    pub message: Option<String>,
834}
835
836#[derive(Debug, Clone)]
837pub struct RecordStartInput {
838    pub session_id: Option<SessionId>,
839}
840
841#[derive(Debug, Clone)]
842pub struct RecordStopInput {
843    pub session_id: Option<SessionId>,
844    pub format: Option<String>,
845}
846
847#[derive(Debug, Clone)]
848pub struct RecordStatusInput {
849    pub session_id: Option<SessionId>,
850}
851
852#[derive(Debug, Clone)]
853pub struct RecordStartOutput {
854    pub session_id: SessionId,
855    pub success: bool,
856}
857
858#[derive(Debug, Clone)]
859pub struct RecordStopOutput {
860    pub session_id: SessionId,
861    pub frame_count: usize,
862    pub frames: Vec<RecordingFrame>,
863    pub format: String,
864    pub cols: u16,
865    pub rows: u16,
866}
867
868pub type RecordStatusOutput = RecordingStatus;
869
870#[derive(Debug, Clone)]
871pub struct TraceInput {
872    pub session_id: Option<SessionId>,
873    pub start: bool,
874    pub stop: bool,
875    pub count: usize,
876}
877
878#[derive(Debug, Clone)]
879pub struct TraceOutput {
880    pub tracing: bool,
881    pub entries: Vec<TraceEntry>,
882}
883
884#[derive(Debug, Clone)]
885pub struct ConsoleInput {
886    pub session_id: Option<SessionId>,
887    pub count: usize,
888    pub clear: bool,
889}
890
891#[derive(Debug, Clone)]
892pub struct ConsoleOutput {
893    pub lines: Vec<String>,
894}
895
896#[derive(Debug, Clone)]
897pub struct ErrorsInput {
898    pub session_id: Option<SessionId>,
899    pub count: usize,
900    pub clear: bool,
901}
902
903#[derive(Debug, Clone)]
904pub struct ErrorsOutput {
905    pub errors: Vec<ErrorEntry>,
906    pub total_count: usize,
907}
908
909#[derive(Debug, Clone)]
910pub struct CountInput {
911    pub session_id: Option<SessionId>,
912    pub role: Option<String>,
913    pub name: Option<String>,
914    pub text: Option<String>,
915}
916
917#[derive(Debug, Clone)]
918pub struct CountOutput {
919    pub count: usize,
920}
921
922#[derive(Debug, Clone)]
923pub struct GetTitleOutput {
924    pub session_id: SessionId,
925    pub title: String,
926}
927
928#[derive(Debug, Clone)]
929pub struct ScrollIntoViewInput {
930    pub session_id: Option<SessionId>,
931    pub element_ref: String,
932}
933
934#[derive(Debug, Clone)]
935pub struct ScrollIntoViewOutput {
936    pub success: bool,
937    pub scrolls_needed: usize,
938    pub message: Option<String>,
939}
940
941#[derive(Debug, Clone)]
942pub struct PtyReadInput {
943    pub session_id: Option<SessionId>,
944    pub max_bytes: usize,
945}
946
947#[derive(Debug, Clone)]
948pub struct PtyReadOutput {
949    pub session_id: SessionId,
950    pub data: String,
951    pub bytes_read: usize,
952}
953
954#[derive(Debug, Clone)]
955pub struct PtyWriteInput {
956    pub session_id: Option<SessionId>,
957    pub data: String,
958}
959
960#[derive(Debug, Clone)]
961pub struct PtyWriteOutput {
962    pub session_id: SessionId,
963    pub bytes_written: usize,
964    pub success: bool,
965}
966
967#[derive(Debug, Clone)]
968pub struct SessionInput {
969    pub session_id: Option<SessionId>,
970}
971
972/// Input for attaching to a session.
973#[derive(Debug, Clone)]
974pub struct AttachInput {
975    /// Session ID to attach to (required).
976    pub session_id: SessionId,
977}
978
979/// Output for attach operation.
980#[derive(Debug, Clone)]
981pub struct AttachOutput {
982    /// Session ID that was attached.
983    pub session_id: SessionId,
984    /// Whether the attach was successful.
985    pub success: bool,
986    /// Human-readable message about the result.
987    pub message: String,
988}
989
990#[derive(Debug, Clone, Default)]
991pub struct HealthInput;
992
993#[derive(Debug, Clone)]
994pub struct HealthOutput {
995    pub status: String,
996    pub pid: u32,
997    pub uptime_ms: u64,
998    pub session_count: usize,
999    pub version: String,
1000    pub active_connections: usize,
1001    pub total_requests: u64,
1002    pub error_count: u64,
1003}
1004
1005#[derive(Debug, Clone, Default)]
1006pub struct MetricsInput;
1007
1008#[derive(Debug, Clone)]
1009pub struct MetricsOutput {
1010    pub requests_total: u64,
1011    pub errors_total: u64,
1012    pub lock_timeouts: u64,
1013    pub poison_recoveries: u64,
1014    pub uptime_ms: u64,
1015    pub active_connections: usize,
1016    pub session_count: usize,
1017}
1018
1019/// Input for cleanup operation.
1020#[derive(Debug, Clone)]
1021pub struct CleanupInput {
1022    /// If true, clean all sessions. If false, only clean non-running sessions.
1023    pub all: bool,
1024}
1025
1026/// A failed cleanup attempt for a single session.
1027#[derive(Debug, Clone)]
1028pub struct CleanupFailure {
1029    /// The session ID that failed to clean up.
1030    pub session_id: SessionId,
1031    /// The error message describing why cleanup failed.
1032    pub error: String,
1033}
1034
1035/// Output for cleanup operation.
1036#[derive(Debug, Clone)]
1037pub struct CleanupOutput {
1038    /// Number of sessions successfully cleaned.
1039    pub cleaned: usize,
1040    /// Sessions that failed to clean up.
1041    pub failures: Vec<CleanupFailure>,
1042}
1043
1044/// The type of assertion to perform.
1045#[derive(Debug, Clone, PartialEq, Eq)]
1046pub enum AssertConditionType {
1047    /// Assert that text is visible on screen.
1048    Text,
1049    /// Assert that an element exists and is visible.
1050    Element,
1051    /// Assert that a session exists and is running.
1052    Session,
1053}
1054
1055impl AssertConditionType {
1056    /// Parse a condition type string.
1057    pub fn parse(s: &str) -> Result<Self, String> {
1058        match s.to_lowercase().as_str() {
1059            "text" => Ok(Self::Text),
1060            "element" => Ok(Self::Element),
1061            "session" => Ok(Self::Session),
1062            _ => Err(format!(
1063                "Unknown condition type: {}. Use: text, element, or session",
1064                s
1065            )),
1066        }
1067    }
1068
1069    /// Get the string representation.
1070    pub fn as_str(&self) -> &'static str {
1071        match self {
1072            Self::Text => "text",
1073            Self::Element => "element",
1074            Self::Session => "session",
1075        }
1076    }
1077}
1078
1079/// Input for assert operation.
1080#[derive(Debug, Clone)]
1081pub struct AssertInput {
1082    /// Session to use for text/element checks (not needed for session condition).
1083    pub session_id: Option<SessionId>,
1084    /// The type of condition to assert.
1085    pub condition_type: AssertConditionType,
1086    /// The value to check (text pattern, element ref, or session id).
1087    pub value: String,
1088}
1089
1090/// Output for assert operation.
1091#[derive(Debug, Clone)]
1092pub struct AssertOutput {
1093    /// Whether the assertion passed.
1094    pub passed: bool,
1095    /// The full condition string (e.g., "text:Hello").
1096    pub condition: String,
1097}
1098
1099#[cfg(test)]
1100mod tests {
1101    use super::*;
1102
1103    // ============================================================
1104    // TDD RED PHASE: ScrollDirection Enum Tests
1105    // These tests should FAIL until ScrollDirection is implemented.
1106    // ============================================================
1107
1108    mod scroll_direction_tests {
1109        use super::*;
1110
1111        #[test]
1112        fn test_scroll_direction_from_str_up() {
1113            let dir = ScrollDirection::parse("up").expect("Should parse 'up'");
1114            assert_eq!(dir, ScrollDirection::Up);
1115        }
1116
1117        #[test]
1118        fn test_scroll_direction_from_str_down() {
1119            let dir = ScrollDirection::parse("down").expect("Should parse 'down'");
1120            assert_eq!(dir, ScrollDirection::Down);
1121        }
1122
1123        #[test]
1124        fn test_scroll_direction_from_str_left() {
1125            let dir = ScrollDirection::parse("left").expect("Should parse 'left'");
1126            assert_eq!(dir, ScrollDirection::Left);
1127        }
1128
1129        #[test]
1130        fn test_scroll_direction_from_str_right() {
1131            let dir = ScrollDirection::parse("right").expect("Should parse 'right'");
1132            assert_eq!(dir, ScrollDirection::Right);
1133        }
1134
1135        #[test]
1136        fn test_scroll_direction_from_str_invalid() {
1137            let result = ScrollDirection::parse("diagonal");
1138            assert!(result.is_err(), "Invalid direction should be rejected");
1139        }
1140
1141        #[test]
1142        fn test_scroll_direction_from_str_empty() {
1143            let result = ScrollDirection::parse("");
1144            assert!(result.is_err(), "Empty string should be rejected");
1145        }
1146
1147        #[test]
1148        fn test_scroll_direction_case_insensitive() {
1149            assert_eq!(ScrollDirection::parse("UP").unwrap(), ScrollDirection::Up);
1150            assert_eq!(
1151                ScrollDirection::parse("Down").unwrap(),
1152                ScrollDirection::Down
1153            );
1154            assert_eq!(
1155                ScrollDirection::parse("LEFT").unwrap(),
1156                ScrollDirection::Left
1157            );
1158            assert_eq!(
1159                ScrollDirection::parse("RIGHT").unwrap(),
1160                ScrollDirection::Right
1161            );
1162        }
1163
1164        #[test]
1165        fn test_scroll_direction_as_str() {
1166            assert_eq!(ScrollDirection::Up.as_str(), "up");
1167            assert_eq!(ScrollDirection::Down.as_str(), "down");
1168            assert_eq!(ScrollDirection::Left.as_str(), "left");
1169            assert_eq!(ScrollDirection::Right.as_str(), "right");
1170        }
1171
1172        #[test]
1173        fn test_scroll_direction_display() {
1174            assert_eq!(format!("{}", ScrollDirection::Up), "up");
1175            assert_eq!(format!("{}", ScrollDirection::Down), "down");
1176        }
1177    }
1178
1179    // ============================================================
1180    // TDD GREEN PHASE: WaitConditionType Enum Tests
1181    // ============================================================
1182
1183    mod wait_condition_type_tests {
1184        use super::*;
1185
1186        #[test]
1187        fn test_wait_condition_type_from_str_text() {
1188            let cond = WaitConditionType::parse("text").expect("Should parse 'text'");
1189            assert_eq!(cond, WaitConditionType::Text);
1190        }
1191
1192        #[test]
1193        fn test_wait_condition_type_from_str_element() {
1194            let cond = WaitConditionType::parse("element").expect("Should parse 'element'");
1195            assert_eq!(cond, WaitConditionType::Element);
1196        }
1197
1198        #[test]
1199        fn test_wait_condition_type_from_str_focused() {
1200            let cond = WaitConditionType::parse("focused").expect("Should parse 'focused'");
1201            assert_eq!(cond, WaitConditionType::Focused);
1202        }
1203
1204        #[test]
1205        fn test_wait_condition_type_from_str_not_visible() {
1206            let cond = WaitConditionType::parse("not_visible").expect("Should parse 'not_visible'");
1207            assert_eq!(cond, WaitConditionType::NotVisible);
1208        }
1209
1210        #[test]
1211        fn test_wait_condition_type_from_str_stable() {
1212            let cond = WaitConditionType::parse("stable").expect("Should parse 'stable'");
1213            assert_eq!(cond, WaitConditionType::Stable);
1214        }
1215
1216        #[test]
1217        fn test_wait_condition_type_from_str_text_gone() {
1218            let cond = WaitConditionType::parse("text_gone").expect("Should parse 'text_gone'");
1219            assert_eq!(cond, WaitConditionType::TextGone);
1220        }
1221
1222        #[test]
1223        fn test_wait_condition_type_from_str_value() {
1224            let cond = WaitConditionType::parse("value").expect("Should parse 'value'");
1225            assert_eq!(cond, WaitConditionType::Value);
1226        }
1227
1228        #[test]
1229        fn test_wait_condition_type_from_str_invalid() {
1230            let result = WaitConditionType::parse("invalid");
1231            assert!(result.is_err(), "Invalid condition should be rejected");
1232        }
1233
1234        #[test]
1235        fn test_wait_condition_type_from_str_empty() {
1236            let result = WaitConditionType::parse("");
1237            assert!(result.is_err(), "Empty string should be rejected");
1238        }
1239
1240        #[test]
1241        fn test_wait_condition_type_case_insensitive() {
1242            assert_eq!(
1243                WaitConditionType::parse("TEXT").unwrap(),
1244                WaitConditionType::Text
1245            );
1246            assert_eq!(
1247                WaitConditionType::parse("Element").unwrap(),
1248                WaitConditionType::Element
1249            );
1250            assert_eq!(
1251                WaitConditionType::parse("STABLE").unwrap(),
1252                WaitConditionType::Stable
1253            );
1254        }
1255
1256        #[test]
1257        fn test_wait_condition_type_as_str() {
1258            assert_eq!(WaitConditionType::Text.as_str(), "text");
1259            assert_eq!(WaitConditionType::Element.as_str(), "element");
1260            assert_eq!(WaitConditionType::Focused.as_str(), "focused");
1261            assert_eq!(WaitConditionType::NotVisible.as_str(), "not_visible");
1262            assert_eq!(WaitConditionType::Stable.as_str(), "stable");
1263            assert_eq!(WaitConditionType::TextGone.as_str(), "text_gone");
1264            assert_eq!(WaitConditionType::Value.as_str(), "value");
1265        }
1266
1267        #[test]
1268        fn test_wait_condition_type_display() {
1269            assert_eq!(format!("{}", WaitConditionType::Text), "text");
1270            assert_eq!(format!("{}", WaitConditionType::Stable), "stable");
1271        }
1272
1273        #[test]
1274        fn test_wait_condition_type_requires_target() {
1275            assert!(!WaitConditionType::Text.requires_target());
1276            assert!(WaitConditionType::Element.requires_target());
1277            assert!(WaitConditionType::Focused.requires_target());
1278            assert!(WaitConditionType::NotVisible.requires_target());
1279            assert!(!WaitConditionType::Stable.requires_target());
1280            assert!(!WaitConditionType::TextGone.requires_target());
1281            assert!(WaitConditionType::Value.requires_target());
1282        }
1283
1284        #[test]
1285        fn test_wait_condition_type_requires_text() {
1286            assert!(WaitConditionType::Text.requires_text());
1287            assert!(!WaitConditionType::Element.requires_text());
1288            assert!(!WaitConditionType::Focused.requires_text());
1289            assert!(!WaitConditionType::NotVisible.requires_text());
1290            assert!(!WaitConditionType::Stable.requires_text());
1291            assert!(WaitConditionType::TextGone.requires_text());
1292            assert!(WaitConditionType::Value.requires_text());
1293        }
1294
1295        #[test]
1296        fn test_wait_condition_type_error_message() {
1297            let err = WaitConditionType::parse("invalid").unwrap_err();
1298            assert!(err.to_string().contains("invalid"));
1299            assert!(err.to_string().contains("text"));
1300        }
1301    }
1302
1303    // ============================================================
1304    // TDD GREEN PHASE: DomainBounds Validation Tests
1305    // ============================================================
1306
1307    mod domain_bounds_tests {
1308        use super::*;
1309
1310        #[test]
1311        fn test_domain_bounds_valid() {
1312            let bounds = DomainBounds::new(10, 20, 100, 50).expect("Valid bounds");
1313            assert_eq!(bounds.x(), 10);
1314            assert_eq!(bounds.y(), 20);
1315            assert_eq!(bounds.width(), 100);
1316            assert_eq!(bounds.height(), 50);
1317        }
1318
1319        #[test]
1320        fn test_domain_bounds_rejects_zero_width() {
1321            let result = DomainBounds::new(0, 0, 0, 10);
1322            assert!(result.is_err(), "Zero width should be rejected");
1323            assert!(result.unwrap_err().message.contains("Width"));
1324        }
1325
1326        #[test]
1327        fn test_domain_bounds_rejects_zero_height() {
1328            let result = DomainBounds::new(0, 0, 10, 0);
1329            assert!(result.is_err(), "Zero height should be rejected");
1330            assert!(result.unwrap_err().message.contains("Height"));
1331        }
1332
1333        #[test]
1334        fn test_domain_bounds_accepts_minimum() {
1335            let bounds = DomainBounds::new(0, 0, 1, 1).expect("Minimum valid bounds");
1336            assert_eq!(bounds.width(), 1);
1337            assert_eq!(bounds.height(), 1);
1338        }
1339
1340        #[test]
1341        fn test_domain_bounds_at_origin() {
1342            let bounds = DomainBounds::new(0, 0, 10, 10).expect("Bounds at origin");
1343            assert_eq!(bounds.x(), 0);
1344            assert_eq!(bounds.y(), 0);
1345        }
1346
1347        #[test]
1348        fn test_domain_bounds_contains() {
1349            let bounds = DomainBounds::new(10, 10, 20, 10).expect("Valid bounds");
1350            assert!(bounds.contains(10, 10)); // Top-left corner
1351            assert!(bounds.contains(15, 15)); // Middle
1352            assert!(bounds.contains(29, 19)); // Bottom-right inside
1353            assert!(!bounds.contains(30, 10)); // Just outside right
1354            assert!(!bounds.contains(10, 20)); // Just outside bottom
1355            assert!(!bounds.contains(9, 10)); // Just outside left
1356            assert!(!bounds.contains(10, 9)); // Just outside top
1357        }
1358
1359        #[test]
1360        fn test_domain_bounds_unchecked() {
1361            // new_unchecked allows invalid bounds (for trusted sources)
1362            let bounds = DomainBounds::new_unchecked(0, 0, 0, 0);
1363            assert_eq!(bounds.width(), 0);
1364            assert_eq!(bounds.height(), 0);
1365        }
1366
1367        #[test]
1368        fn test_domain_bounds_error_display() {
1369            let err = DomainBounds::new(0, 0, 0, 10).unwrap_err();
1370            assert!(!err.to_string().is_empty());
1371        }
1372    }
1373
1374    // ============================================================
1375    // DomainElementType Behavior Tests
1376    // ============================================================
1377
1378    mod domain_element_type_tests {
1379        use super::*;
1380
1381        #[test]
1382        fn test_element_type_is_interactive() {
1383            // Interactive types
1384            assert!(DomainElementType::Button.is_interactive());
1385            assert!(DomainElementType::Input.is_interactive());
1386            assert!(DomainElementType::Checkbox.is_interactive());
1387            assert!(DomainElementType::Radio.is_interactive());
1388            assert!(DomainElementType::Select.is_interactive());
1389            assert!(DomainElementType::MenuItem.is_interactive());
1390            assert!(DomainElementType::Link.is_interactive());
1391
1392            // Non-interactive types
1393            assert!(!DomainElementType::ListItem.is_interactive());
1394            assert!(!DomainElementType::Spinner.is_interactive());
1395            assert!(!DomainElementType::Progress.is_interactive());
1396        }
1397
1398        #[test]
1399        fn test_element_type_accepts_input() {
1400            assert!(DomainElementType::Input.accepts_input());
1401            assert!(!DomainElementType::Button.accepts_input());
1402            assert!(!DomainElementType::Checkbox.accepts_input());
1403        }
1404
1405        #[test]
1406        fn test_element_type_is_toggleable() {
1407            assert!(DomainElementType::Checkbox.is_toggleable());
1408            assert!(DomainElementType::Radio.is_toggleable());
1409            assert!(!DomainElementType::Button.is_toggleable());
1410            assert!(!DomainElementType::Input.is_toggleable());
1411        }
1412    }
1413
1414    // ============================================================
1415    // DomainElement Behavior Tests
1416    // ============================================================
1417
1418    mod domain_element_tests {
1419        use super::*;
1420
1421        fn make_element(element_type: DomainElementType, disabled: Option<bool>) -> DomainElement {
1422            DomainElement {
1423                element_ref: "test".to_string(),
1424                element_type,
1425                label: Some("Test".to_string()),
1426                value: None,
1427                position: DomainPosition {
1428                    row: 0,
1429                    col: 0,
1430                    width: Some(10),
1431                    height: Some(1),
1432                },
1433                focused: false,
1434                selected: false,
1435                checked: None,
1436                disabled,
1437                hint: None,
1438            }
1439        }
1440
1441        #[test]
1442        fn test_element_is_interactive() {
1443            let button = make_element(DomainElementType::Button, None);
1444            assert!(button.is_interactive());
1445
1446            let progress = make_element(DomainElementType::Progress, None);
1447            assert!(!progress.is_interactive());
1448        }
1449
1450        #[test]
1451        fn test_element_can_click_enabled() {
1452            let button = make_element(DomainElementType::Button, None);
1453            assert!(button.can_click());
1454
1455            let disabled_button = make_element(DomainElementType::Button, Some(true));
1456            assert!(!disabled_button.can_click());
1457        }
1458
1459        #[test]
1460        fn test_element_can_click_non_interactive() {
1461            let progress = make_element(DomainElementType::Progress, None);
1462            assert!(!progress.can_click());
1463        }
1464
1465        #[test]
1466        fn test_element_can_type() {
1467            let input = make_element(DomainElementType::Input, None);
1468            assert!(input.can_type());
1469
1470            let disabled_input = make_element(DomainElementType::Input, Some(true));
1471            assert!(!disabled_input.can_type());
1472
1473            let button = make_element(DomainElementType::Button, None);
1474            assert!(!button.can_type());
1475        }
1476
1477        #[test]
1478        fn test_element_is_disabled() {
1479            let enabled = make_element(DomainElementType::Button, None);
1480            assert!(!enabled.is_disabled());
1481            assert!(enabled.is_enabled());
1482
1483            let explicit_enabled = make_element(DomainElementType::Button, Some(false));
1484            assert!(!explicit_enabled.is_disabled());
1485            assert!(explicit_enabled.is_enabled());
1486
1487            let disabled = make_element(DomainElementType::Button, Some(true));
1488            assert!(disabled.is_disabled());
1489            assert!(!disabled.is_enabled());
1490        }
1491
1492        #[test]
1493        fn test_element_display_text() {
1494            let with_label = DomainElement {
1495                element_ref: "test".to_string(),
1496                element_type: DomainElementType::Button,
1497                label: Some("Click Me".to_string()),
1498                value: Some("ignored".to_string()),
1499                position: DomainPosition {
1500                    row: 0,
1501                    col: 0,
1502                    width: None,
1503                    height: None,
1504                },
1505                focused: false,
1506                selected: false,
1507                checked: None,
1508                disabled: None,
1509                hint: None,
1510            };
1511            assert_eq!(with_label.display_text(), Some("Click Me"));
1512
1513            let with_value_only = DomainElement {
1514                element_ref: "test".to_string(),
1515                element_type: DomainElementType::Input,
1516                label: None,
1517                value: Some("typed text".to_string()),
1518                position: DomainPosition {
1519                    row: 0,
1520                    col: 0,
1521                    width: None,
1522                    height: None,
1523                },
1524                focused: false,
1525                selected: false,
1526                checked: None,
1527                disabled: None,
1528                hint: None,
1529            };
1530            assert_eq!(with_value_only.display_text(), Some("typed text"));
1531
1532            let no_text = DomainElement {
1533                element_ref: "test".to_string(),
1534                element_type: DomainElementType::Button,
1535                label: None,
1536                value: None,
1537                position: DomainPosition {
1538                    row: 0,
1539                    col: 0,
1540                    width: None,
1541                    height: None,
1542                },
1543                focused: false,
1544                selected: false,
1545                checked: None,
1546                disabled: None,
1547                hint: None,
1548            };
1549            assert_eq!(no_text.display_text(), None);
1550        }
1551
1552        #[test]
1553        fn test_element_state_methods() {
1554            let focused_selected = DomainElement {
1555                element_ref: "test".to_string(),
1556                element_type: DomainElementType::MenuItem,
1557                label: None,
1558                value: None,
1559                position: DomainPosition {
1560                    row: 0,
1561                    col: 0,
1562                    width: None,
1563                    height: None,
1564                },
1565                focused: true,
1566                selected: true,
1567                checked: Some(true),
1568                disabled: None,
1569                hint: None,
1570            };
1571            assert!(focused_selected.is_focused());
1572            assert!(focused_selected.is_selected());
1573            assert_eq!(focused_selected.is_checked(), Some(true));
1574        }
1575    }
1576}