agent_tui/
commands.rs

1use clap::Parser;
2use clap::Subcommand;
3use clap::ValueEnum;
4pub use clap_complete::Shell;
5
6const LONG_ABOUT: &str = r#"agent-tui enables AI agents to interact with TUI (Text User Interface) applications.
7
8WORKFLOW:
9    1. Run a TUI application
10    2. Take snapshots to see the screen and detect elements
11    3. Interact using fill, click, key commands
12    4. Wait for UI changes
13    5. Kill the session when done
14
15ELEMENT REFS:
16    Element refs are simple sequential identifiers like @e1, @e2, @e3 that
17    you can use to interact with detected UI elements. Run 'agent-tui snap -e'
18    to see available elements and their refs.
19
20    @e1, @e2, @e3, ...  - Elements in document order (top-to-bottom, left-to-right)
21
22    Refs reset on each snapshot. Always use the latest snapshot's refs.
23
24EXAMPLES:
25    # Start a new Next.js project wizard
26    agent-tui run "npx create-next-app"
27    agent-tui snap -e
28    agent-tui fill @e1 "my-project"
29    agent-tui key Enter
30    agent-tui wait "success"
31    agent-tui kill
32
33    # Interactive menu navigation
34    agent-tui run htop
35    agent-tui key F10
36    agent-tui snap -e
37    agent-tui click @e1
38    agent-tui kill
39
40    # Check daemon status
41    agent-tui status -v"#;
42
43#[derive(Parser)]
44#[command(name = "agent-tui")]
45#[command(author, version)]
46#[command(about = "CLI tool for AI agents to interact with TUI applications")]
47#[command(long_about = LONG_ABOUT)]
48pub struct Cli {
49    #[command(subcommand)]
50    pub command: Commands,
51
52    /// Session ID to use (default: uses the most recent session)
53    #[arg(short, long, global = true)]
54    pub session: Option<String>,
55
56    /// Output format
57    #[arg(short, long, global = true, default_value = "text")]
58    pub format: OutputFormat,
59
60    /// Output as JSON (shorthand for --format json)
61    #[arg(long, global = true)]
62    pub json: bool,
63
64    /// Disable colored output (also respects NO_COLOR env var)
65    #[arg(long, global = true, env = "NO_COLOR")]
66    pub no_color: bool,
67
68    /// Enable verbose output (shows request timing)
69    #[arg(short, long, global = true)]
70    pub verbose: bool,
71}
72
73impl Cli {
74    /// Returns the effective output format, considering --json shorthand.
75    pub fn effective_format(&self) -> OutputFormat {
76        if self.json {
77            OutputFormat::Json
78        } else {
79            self.format
80        }
81    }
82}
83
84#[derive(Debug, Subcommand)]
85pub enum Commands {
86    /// Run a new TUI application in a virtual terminal
87    #[command(name = "run")]
88    #[command(long_about = r#"Run a new TUI application in a virtual terminal.
89
90Creates a new PTY session with the specified command and returns a session ID.
91The session runs in the background and can be interacted with using other commands.
92
93EXAMPLES:
94    agent-tui run bash
95    agent-tui run htop
96    agent-tui run "npx create-next-app"
97    agent-tui run vim -- file.txt
98    agent-tui run --cols 80 --rows 24 nano"#)]
99    Run {
100        /// Command to execute (e.g., bash, htop, vim)
101        command: String,
102
103        /// Additional arguments for the command
104        #[arg(trailing_var_arg = true)]
105        args: Vec<String>,
106
107        /// Working directory for the spawned process
108        #[arg(short = 'd', long)]
109        cwd: Option<String>,
110
111        /// Terminal width in columns
112        #[arg(long, default_value = "120")]
113        cols: u16,
114
115        /// Terminal height in rows
116        #[arg(long, default_value = "40")]
117        rows: u16,
118    },
119
120    /// Take a snapshot of the current screen state
121    #[command(name = "snap")]
122    #[command(long_about = r#"Take a snapshot of the current screen state.
123
124Returns the current terminal screen content and optionally detects
125interactive UI elements like buttons, inputs, and menus.
126
127Element detection uses the Visual Object Model (VOM) which identifies
128UI components based on visual styling (colors, backgrounds) rather than
129text patterns. This provides reliable detection across different TUI frameworks.
130
131ACCESSIBILITY TREE FORMAT (-a):
132    Returns an agent-browser style accessibility tree with refs for elements:
133    - button "Submit" [ref=e1]
134    - textbox "Search" [ref=e2]
135
136EXAMPLES:
137    agent-tui snap              # Just the screen
138    agent-tui snap -e           # Screen + detected elements
139    agent-tui snap -a           # Accessibility tree format
140    agent-tui snap -a --interactive-only  # Only interactive elements
141    agent-tui snap --strip-ansi # Plain text without colors"#)]
142    Snap {
143        /// Include detected UI elements in output
144        #[arg(short = 'i', long)]
145        elements: bool,
146
147        /// Output accessibility tree format (agent-browser style)
148        #[arg(short = 'a', long)]
149        accessibility: bool,
150
151        /// Filter to interactive elements only (used with -a)
152        #[arg(long)]
153        interactive_only: bool,
154
155        /// Limit snapshot to a named region
156        #[arg(long)]
157        region: Option<String>,
158
159        /// Strip ANSI escape codes from output
160        #[arg(long)]
161        strip_ansi: bool,
162
163        /// Include cursor position in output
164        #[arg(long)]
165        include_cursor: bool,
166    },
167
168    /// Click an element by reference
169    Click {
170        /// Element reference (e.g., @btn1, @e3)
171        #[arg(name = "ref")]
172        element_ref: String,
173
174        /// Double-click instead of single click
175        #[arg(short = '2', long)]
176        double: bool,
177    },
178
179    /// Set the value of an input element
180    Fill {
181        /// Element reference (e.g., @inp1)
182        #[arg(name = "ref")]
183        element_ref: String,
184
185        /// Value to set in the input
186        value: String,
187    },
188
189    /// Send keystrokes or type text
190    #[command(long_about = r#"Send keystrokes or type text.
191
192Unified command for all keyboard input. Can press a single key, type text,
193or hold/release modifier keys.
194
195SUPPORTED KEYS:
196    Enter, Tab, Escape, Backspace, Delete
197    ArrowUp, ArrowDown, ArrowLeft, ArrowRight
198    Home, End, PageUp, PageDown
199    F1-F12
200
201MODIFIERS:
202    Ctrl+<key>   - Control modifier (e.g., Ctrl+C, Ctrl+A)
203    Alt+<key>    - Alt modifier (e.g., Alt+F4)
204    Shift+<key>  - Shift modifier
205
206EXAMPLES:
207    agent-tui key Enter              # Press Enter
208    agent-tui key Ctrl+C             # Press Ctrl+C
209    agent-tui key --type "hello"     # Type text char-by-char
210    agent-tui key -t "hello"         # Short form for typing
211    agent-tui key Shift --hold       # Hold Shift down
212    agent-tui key Shift --release    # Release Shift"#)]
213    Key {
214        /// Key name or combination (e.g., Enter, Ctrl+C) - not required if --type is used
215        #[arg(required_unless_present = "text")]
216        key: Option<String>,
217
218        /// Type text character by character
219        #[arg(short = 't', long = "type", conflicts_with = "key")]
220        text: Option<String>,
221
222        /// Hold key down for modifier sequences
223        #[arg(long, conflicts_with_all = ["text", "release"])]
224        hold: bool,
225
226        /// Release a held key
227        #[arg(long, conflicts_with_all = ["text", "hold"])]
228        release: bool,
229    },
230
231    /// Wait for a condition to be met
232    #[command(long_about = r#"Wait for a condition to be met before continuing.
233
234Waits for text to appear, elements to change, or the screen to stabilize.
235Returns success if the condition is met within the timeout period.
236
237WAIT CONDITIONS:
238    <text>              Wait for text to appear on screen
239    -e, --element <ref> Wait for element to appear
240    --focused <ref>     Wait for element to be focused
241    --stable            Wait for screen to stop changing
242    --value <ref>=<val> Wait for input to have specific value
243    -g, --gone          Modifier: wait for element/text to disappear
244
245EXAMPLES:
246    agent-tui wait "Continue"           # Wait for text
247    agent-tui wait -e @btn1             # Wait for element
248    agent-tui wait -e @spinner --gone   # Wait for element to disappear
249    agent-tui wait "Loading" --gone     # Wait for text to disappear
250    agent-tui wait --stable             # Wait for screen stability
251    agent-tui wait --focused @inp1      # Wait for focus
252    agent-tui wait -t 5000 "Done"       # 5 second timeout"#)]
253    Wait {
254        #[command(flatten)]
255        params: WaitParams,
256    },
257
258    /// Terminate the current session
259    Kill,
260
261    /// Restart the TUI application
262    #[command(long_about = r#"Restart the TUI application.
263
264Kills the current session and restarts it with the same command.
265Equivalent to running 'kill' followed by 'run' with the original command.
266
267This is the TUI equivalent of browser's 'reload' command.
268
269EXAMPLES:
270    agent-tui restart                 # Restart current session
271    agent-tui restart -s htop-abc123  # Restart specific session"#)]
272    Restart,
273
274    /// List all active sessions
275    #[command(name = "ls")]
276    Ls,
277
278    /// Check daemon status
279    #[command(name = "status")]
280    Status {
281        /// Show connection details
282        #[arg(short, long)]
283        verbose: bool,
284    },
285
286    /// Select an option from a dropdown or list
287    Select {
288        /// Element reference (e.g., @sel1)
289        #[arg(name = "ref")]
290        element_ref: String,
291
292        /// Option to select
293        option: String,
294    },
295
296    /// Select multiple options in a multi-select list
297    #[command(long_about = r#"Select multiple options in a multi-select list.
298
299This is the TUI equivalent of browser's multi-select functionality.
300Use this for lists where multiple items can be selected simultaneously.
301
302Typical multi-select interaction in TUI:
3031. Focus the list element
3042. Navigate with arrow keys
3053. Press Space to toggle selection for each option
306
307EXAMPLES:
308    agent-tui multiselect @e3 "Option 1" "Option 3"
309    agent-tui multiselect @list1 red blue green"#)]
310    #[command(name = "multiselect")]
311    MultiSelect {
312        /// Element reference (e.g., @list1)
313        #[arg(name = "ref")]
314        element_ref: String,
315
316        /// Options to select
317        #[arg(required = true)]
318        options: Vec<String>,
319    },
320
321    /// Scroll the terminal viewport
322    Scroll {
323        /// Scroll direction (up, down, left, right) - not required if --to is used
324        #[arg(value_enum, required_unless_present = "to_ref")]
325        direction: Option<ScrollDirection>,
326
327        /// Number of lines/columns to scroll
328        #[arg(short, long, default_value = "5")]
329        amount: u16,
330
331        /// Target element for scoped scrolling (not yet implemented)
332        #[arg(short, long)]
333        element: Option<String>,
334
335        /// Scroll until this element is visible
336        #[arg(long = "to")]
337        to_ref: Option<String>,
338    },
339
340    /// Set focus to an element
341    Focus {
342        /// Element reference (e.g., @inp1)
343        #[arg(name = "ref")]
344        element_ref: String,
345    },
346
347    /// Clear an input element's value
348    Clear {
349        /// Element reference (e.g., @inp1)
350        #[arg(name = "ref")]
351        element_ref: String,
352    },
353
354    /// Select all text in an input element
355    #[command(name = "selectall")]
356    SelectAll {
357        /// Element reference (e.g., @inp1)
358        #[arg(name = "ref")]
359        element_ref: String,
360    },
361
362    /// Count elements matching criteria
363    #[command(long_about = r#"Count elements matching criteria.
364
365This is the TUI equivalent of browser's count command.
366Returns the number of elements matching the specified role, name, or text.
367
368EXAMPLES:
369    agent-tui count --role button
370    agent-tui count --text "Submit"
371    agent-tui count --role input --name "Email""#)]
372    Count {
373        /// Element role to match (button, input, etc.)
374        #[arg(long)]
375        role: Option<String>,
376
377        /// Element name/label to match
378        #[arg(long)]
379        name: Option<String>,
380
381        /// Text content to match
382        #[arg(long)]
383        text: Option<String>,
384    },
385
386    /// Toggle a checkbox or radio button
387    #[command(long_about = r#"Toggle a checkbox or radio button.
388
389Use this to toggle the checked state of a checkbox or radio button.
390By default, it inverts the current state. Use --on or --off to force a specific state.
391
392EXAMPLES:
393    agent-tui toggle @e5        # Toggle current state
394    agent-tui toggle @e5 --on   # Force checked (idempotent)
395    agent-tui toggle @e5 --off  # Force unchecked (idempotent)"#)]
396    Toggle {
397        /// Element reference (e.g., @cb1)
398        #[arg(name = "ref")]
399        element_ref: String,
400
401        /// Force checked state (idempotent)
402        #[arg(long, conflicts_with = "off")]
403        on: bool,
404
405        /// Force unchecked state (idempotent)
406        #[arg(long, conflicts_with = "on")]
407        off: bool,
408    },
409
410    /// Debugging subcommands for development and troubleshooting
411    #[command(subcommand)]
412    Debug(DebugCommand),
413
414    /// Start recording screen activity
415    #[command(name = "record-start")]
416    #[command(hide = true)]
417    RecordStart,
418
419    /// Stop recording and save output
420    #[command(name = "record-stop")]
421    #[command(hide = true)]
422    RecordStop {
423        /// Output file path
424        #[arg(short, long)]
425        output: Option<String>,
426
427        /// Recording format
428        #[arg(long, value_enum, default_value = "json")]
429        record_format: RecordFormat,
430    },
431
432    /// Check recording status
433    #[command(name = "record-status")]
434    #[command(hide = true)]
435    RecordStatus,
436
437    /// View performance trace data
438    #[command(hide = true)]
439    Trace {
440        /// Number of trace entries to show
441        #[arg(short = 'n', long, default_value = "10")]
442        count: usize,
443
444        /// Start performance tracing
445        #[arg(long)]
446        start: bool,
447
448        /// Stop performance tracing
449        #[arg(long)]
450        stop: bool,
451    },
452
453    /// View console output
454    #[command(hide = true)]
455    Console {
456        /// Number of lines to show
457        #[arg(short = 'n', long, default_value = "100")]
458        lines: usize,
459
460        /// Clear the console buffer
461        #[arg(long)]
462        clear: bool,
463    },
464
465    /// View captured errors
466    #[command(hide = true)]
467    Errors {
468        /// Number of errors to show
469        #[arg(short = 'n', long, default_value = "10")]
470        count: usize,
471
472        /// Clear the error buffer
473        #[arg(long)]
474        clear: bool,
475    },
476
477    /// Resize the terminal window
478    #[command(long_about = r#"Resize the terminal window dimensions.
479
480Changes the number of columns and rows for the PTY. This affects how
481TUI applications render and can be useful for testing responsive layouts.
482
483EXAMPLES:
484    agent-tui resize --cols 120 --rows 40
485    agent-tui resize --cols 80 --rows 24"#)]
486    Resize {
487        /// Number of columns
488        #[arg(long, default_value = "120")]
489        cols: u16,
490
491        /// Number of rows
492        #[arg(long, default_value = "40")]
493        rows: u16,
494    },
495
496    /// Attach to an existing session
497    #[command(long_about = r#"Attach to an existing session by ID.
498
499By default, makes the specified session the active session for subsequent commands.
500
501With --interactive (-i), attaches your terminal directly to the session
502for a native terminal experience. Your keystrokes go directly to the app,
503and you see its output in real-time. Press Ctrl+\ to detach.
504
505EXAMPLES:
506    agent-tui attach abc123          # Set as active session
507    agent-tui attach -i abc123       # Interactive mode (native terminal)
508    agent-tui ls                     # List session IDs first"#)]
509    Attach {
510        /// Session ID to attach to
511        session_id: String,
512
513        /// Interactive mode: attach terminal directly to the session
514        #[arg(short, long)]
515        interactive: bool,
516    },
517
518    /// Daemon lifecycle management
519    #[command(subcommand)]
520    Daemon(DaemonCommand),
521
522    /// Show version information for CLI and daemon
523    #[command(long_about = r#"Show detailed version information.
524
525Shows version info for both the CLI binary and the running daemon.
526Useful for debugging and ensuring CLI/daemon compatibility.
527
528EXAMPLES:
529    agent-tui version
530    agent-tui version -f json"#)]
531    Version,
532
533    /// Show environment diagnostics
534    #[command(long_about = r#"Show environment diagnostics.
535
536Displays all environment variables and configuration that affect
537agent-tui behavior. Useful for debugging connection issues.
538
539EXAMPLES:
540    agent-tui env
541    agent-tui env -f json"#)]
542    Env,
543
544    /// Assert a condition for testing/scripting
545    #[command(long_about = r#"Assert a condition and exit with status code.
546
547Useful for automated testing and scripting. Exits with code 0 if
548the condition passes, or code 1 if it fails.
549
550CONDITIONS:
551    text:<pattern>        Assert text is visible on screen
552    element:<ref>         Assert element exists
553    session:<id>          Assert session exists and is running
554    exit_code:<expected>  Assert last process exit code (if applicable)
555
556EXAMPLES:
557    agent-tui assert text:Success
558    agent-tui assert element:@btn1
559    agent-tui assert session:main"#)]
560    Assert {
561        /// Condition to assert (text:pattern, element:ref, session:id)
562        condition: String,
563    },
564
565    /// Clean up stale sessions
566    #[command(long_about = r#"Clean up stale sessions.
567
568Removes sessions that are no longer running or have been idle
569for too long. Useful for freeing resources.
570
571EXAMPLES:
572    agent-tui cleanup           # Remove dead sessions
573    agent-tui cleanup --all     # Remove all sessions"#)]
574    Cleanup {
575        /// Remove all sessions (not just dead ones)
576        #[arg(long)]
577        all: bool,
578    },
579
580    /// Find elements by semantic properties (role, name, text)
581    #[command(long_about = r#"Find elements by semantic properties.
582
583This is a semantic locator that allows finding elements without relying
584on refs that may change. Returns matching element refs.
585
586Find modes:
587  - By role and name: agent-tui find --role button --name "Submit"
588  - By text content: agent-tui find --text "Continue"
589  - By focus state: agent-tui find --focused
590  - By placeholder: agent-tui find --placeholder "Enter email"
591  - Select nth result: agent-tui find --role button --nth 1
592
593MATCHING:
594  By default, text matching is case-insensitive and matches substrings.
595  Use --exact for exact string matching.
596
597EXAMPLES:
598    agent-tui find --role button --name "Submit"
599    agent-tui find --text "Continue"
600    agent-tui find --focused
601    agent-tui find --role input
602    agent-tui find --placeholder "Search..."     # Find by placeholder text
603    agent-tui find --role button --nth 1        # Get second button (0-indexed)
604    agent-tui find --text "Log" --exact         # Exact match only"#)]
605    Find {
606        #[command(flatten)]
607        params: FindParams,
608    },
609
610    /// Generate shell completion scripts
611    #[command(
612        long_about = r#"Generate shell completion scripts for bash, zsh, fish, powershell, or elvish.
613
614INSTALLATION:
615    # Bash - add to ~/.bashrc
616    source <(agent-tui completions bash)
617
618    # Zsh - add to ~/.zshrc
619    source <(agent-tui completions zsh)
620
621    # Fish - run once
622    agent-tui completions fish > ~/.config/fish/completions/agent-tui.fish
623
624    # PowerShell - add to $PROFILE
625    agent-tui completions powershell | Out-String | Invoke-Expression
626
627EXAMPLES:
628    agent-tui completions bash
629    agent-tui completions zsh > /usr/local/share/zsh/site-functions/_agent-tui"#
630    )]
631    Completions {
632        /// Shell to generate completions for
633        #[arg(value_enum)]
634        shell: Shell,
635    },
636}
637
638/// Daemon lifecycle management subcommands
639#[derive(Debug, Subcommand)]
640pub enum DaemonCommand {
641    /// Start the daemon
642    #[command(long_about = r#"Start the daemon process.
643
644By default, starts the daemon in the background. Use --foreground to run
645in the current terminal (useful for debugging).
646
647EXAMPLES:
648    agent-tui daemon start              # Start in background
649    agent-tui daemon start --foreground # Run in foreground (blocks)"#)]
650    Start {
651        /// Run in foreground instead of background
652        #[arg(long)]
653        foreground: bool,
654    },
655
656    /// Stop the running daemon gracefully
657    #[command(long_about = r#"Stop the running daemon.
658
659Sends SIGTERM to gracefully stop the daemon, allowing it to clean up
660sessions and resources. Use --force to send SIGKILL for immediate
661termination (not recommended unless daemon is unresponsive).
662
663EXAMPLES:
664    agent-tui daemon stop          # Graceful stop
665    agent-tui daemon stop --force  # Force kill (SIGKILL)"#)]
666    Stop {
667        /// Force immediate termination (SIGKILL instead of SIGTERM)
668        #[arg(long)]
669        force: bool,
670    },
671
672    /// Show daemon status with version info
673    #[command(long_about = r#"Show daemon status and version information.
674
675Displays whether the daemon is running, its PID, uptime, and version.
676Also checks for version mismatch between CLI and daemon.
677
678EXAMPLES:
679    agent-tui daemon status"#)]
680    Status,
681
682    /// Restart the daemon
683    #[command(long_about = r#"Restart the daemon.
684
685Stops the running daemon and starts a new one. Useful after updating
686the agent-tui binary to ensure the daemon is running the new version.
687
688All active sessions will be terminated during restart.
689
690EXAMPLES:
691    agent-tui daemon restart"#)]
692    Restart,
693}
694
695/// Debugging subcommands
696#[derive(Debug, Subcommand)]
697pub enum DebugCommand {
698    /// Recording subcommands
699    #[command(subcommand)]
700    Record(RecordAction),
701
702    /// View performance trace data
703    Trace {
704        /// Number of trace entries to show
705        #[arg(short = 'n', long, default_value = "10")]
706        count: usize,
707
708        /// Start performance tracing
709        #[arg(long)]
710        start: bool,
711
712        /// Stop performance tracing
713        #[arg(long)]
714        stop: bool,
715    },
716
717    /// View console output
718    Console {
719        /// Number of lines to show
720        #[arg(short = 'n', long, default_value = "100")]
721        lines: usize,
722
723        /// Clear the console buffer
724        #[arg(long)]
725        clear: bool,
726    },
727
728    /// View captured errors
729    Errors {
730        /// Number of errors to show
731        #[arg(short = 'n', long, default_value = "10")]
732        count: usize,
733
734        /// Clear the error buffer
735        #[arg(long)]
736        clear: bool,
737    },
738
739    /// Show environment diagnostics
740    Env,
741}
742
743/// Recording subcommands
744#[derive(Debug, Subcommand)]
745pub enum RecordAction {
746    /// Start recording screen activity
747    Start,
748
749    /// Stop recording and save output
750    Stop {
751        /// Output file path
752        #[arg(short, long)]
753        output: Option<String>,
754
755        /// Recording format
756        #[arg(long, value_enum, default_value = "json")]
757        format: RecordFormat,
758    },
759
760    /// Check recording status
761    Status,
762}
763
764#[derive(Clone, Copy, Debug, ValueEnum, Default)]
765pub enum RecordFormat {
766    #[default]
767    Json,
768    Asciicast,
769}
770
771impl RecordFormat {
772    pub fn as_str(self) -> &'static str {
773        match self {
774            RecordFormat::Json => "json",
775            RecordFormat::Asciicast => "asciicast",
776        }
777    }
778}
779
780#[derive(Clone, Copy, Debug, ValueEnum)]
781pub enum ScrollDirection {
782    Up,
783    Down,
784    Left,
785    Right,
786}
787
788impl ScrollDirection {
789    pub fn as_str(self) -> &'static str {
790        match self {
791            ScrollDirection::Up => "up",
792            ScrollDirection::Down => "down",
793            ScrollDirection::Left => "left",
794            ScrollDirection::Right => "right",
795        }
796    }
797}
798
799/// Parameters for the wait command
800#[derive(Debug, Clone, Default, Parser)]
801#[command(group = clap::ArgGroup::new("wait_condition").multiple(false).args(&["element", "focused", "stable", "value"]))]
802pub struct WaitParams {
803    /// Text to wait for on screen
804    pub text: Option<String>,
805
806    /// Timeout in milliseconds (default: 30000)
807    #[arg(short, long, default_value = "30000")]
808    pub timeout: u64,
809
810    /// Wait for element to appear (or disappear with --gone)
811    #[arg(short = 'e', long, group = "wait_condition")]
812    pub element: Option<String>,
813
814    /// Wait for element to be focused
815    #[arg(long, group = "wait_condition")]
816    pub focused: Option<String>,
817
818    /// Wait for screen to stabilize
819    #[arg(long, group = "wait_condition")]
820    pub stable: bool,
821
822    /// Wait for input to have specific value (format: @ref=value)
823    #[arg(long, group = "wait_condition")]
824    pub value: Option<String>,
825
826    /// Wait for element/text to disappear (use with -e or text argument)
827    #[arg(short = 'g', long)]
828    pub gone: bool,
829}
830
831impl WaitParams {
832    /// Resolve wait condition from WaitParams to (condition_type, target) tuple
833    pub fn resolve_condition(&self) -> (Option<String>, Option<String>) {
834        if self.stable {
835            return (Some("stable".to_string()), None);
836        }
837
838        // Handle element with optional --gone modifier
839        if let Some(ref elem) = self.element {
840            let condition = if self.gone { "not_visible" } else { "element" };
841            return (Some(condition.to_string()), Some(elem.clone()));
842        }
843
844        if let Some(ref elem) = self.focused {
845            return (Some("focused".to_string()), Some(elem.clone()));
846        }
847
848        if let Some(ref val) = self.value {
849            return (Some("value".to_string()), Some(val.clone()));
850        }
851
852        // Handle text with optional --gone modifier
853        if let Some(ref txt) = self.text {
854            if self.gone {
855                return (Some("text_gone".to_string()), Some(txt.clone()));
856            }
857            // Text without --gone is handled as default (text condition)
858        }
859
860        (None, None)
861    }
862}
863
864/// Parameters for the find command
865#[derive(Debug, Clone, Default, Parser)]
866pub struct FindParams {
867    /// Element role to find (button, input, checkbox, etc.)
868    #[arg(long)]
869    pub role: Option<String>,
870
871    /// Element name/label to match (supports regex)
872    #[arg(long)]
873    pub name: Option<String>,
874
875    /// Text content to find (searches label and value)
876    #[arg(long)]
877    pub text: Option<String>,
878
879    /// Placeholder text to match (for inputs)
880    #[arg(long)]
881    pub placeholder: Option<String>,
882
883    /// Find the currently focused element
884    #[arg(long)]
885    pub focused: bool,
886
887    /// Select the nth matching element (0-indexed)
888    #[arg(long)]
889    pub nth: Option<usize>,
890
891    /// Use exact string matching instead of substring matching
892    #[arg(long)]
893    pub exact: bool,
894}
895
896#[derive(Clone, Copy, Debug, ValueEnum, Default, PartialEq)]
897pub enum OutputFormat {
898    #[default]
899    Text,
900    Json,
901}
902
903#[cfg(test)]
904mod tests {
905    use super::*;
906    use clap::Parser;
907
908    /// Test that the CLI can be constructed with default values
909    #[test]
910    fn test_cli_defaults() {
911        let cli = Cli::parse_from(["agent-tui", "status"]);
912        assert!(cli.session.is_none());
913        assert_eq!(cli.format, OutputFormat::Text);
914        assert!(!cli.no_color);
915        assert!(!cli.verbose);
916    }
917
918    /// Test global arguments are parsed correctly
919    #[test]
920    fn test_global_args() {
921        let cli = Cli::parse_from([
922            "agent-tui",
923            "--session",
924            "my-session",
925            "--format",
926            "json",
927            "--no-color",
928            "--verbose",
929            "status",
930        ]);
931        assert_eq!(cli.session, Some("my-session".to_string()));
932        assert_eq!(cli.format, OutputFormat::Json);
933        assert!(cli.no_color);
934        assert!(cli.verbose);
935    }
936
937    /// Test run command default values match documentation
938    #[test]
939    fn test_run_defaults() {
940        let cli = Cli::parse_from(["agent-tui", "run", "bash"]);
941        let Commands::Run {
942            command,
943            args,
944            cwd,
945            cols,
946            rows,
947        } = cli.command
948        else {
949            panic!("Expected Run command, got {:?}", cli.command);
950        };
951        assert_eq!(command, "bash");
952        assert!(args.is_empty());
953        assert!(cwd.is_none());
954
955        assert_eq!(cols, 120, "Default cols should be 120");
956        assert_eq!(rows, 40, "Default rows should be 40");
957    }
958
959    /// Test spawn with custom dimensions
960    #[test]
961    fn test_run_custom_dimensions() {
962        let cli = Cli::parse_from(["agent-tui", "run", "--cols", "80", "--rows", "24", "vim"]);
963        let Commands::Run {
964            cols,
965            rows,
966            command,
967            ..
968        } = cli.command
969        else {
970            panic!("Expected Run command, got {:?}", cli.command);
971        };
972        assert_eq!(cols, 80);
973        assert_eq!(rows, 24);
974        assert_eq!(command, "vim");
975    }
976
977    /// Test spawn with trailing arguments
978    #[test]
979    fn test_run_with_args() {
980        let cli = Cli::parse_from(["agent-tui", "run", "vim", "--", "file.txt", "-n"]);
981        let Commands::Run { command, args, .. } = cli.command else {
982            panic!("Expected Run command, got {:?}", cli.command);
983        };
984        assert_eq!(command, "vim");
985        assert_eq!(args, vec!["file.txt".to_string(), "-n".to_string()]);
986    }
987
988    /// Test snap command flags
989    #[test]
990    fn test_snap_flags() {
991        let cli = Cli::parse_from(["agent-tui", "snap", "-i"]);
992        let Commands::Snap {
993            elements,
994            region,
995            strip_ansi,
996            include_cursor,
997            ..
998        } = cli.command
999        else {
1000            panic!("Expected Snap command, got {:?}", cli.command);
1001        };
1002        assert!(elements, "-i should enable elements");
1003        assert!(region.is_none());
1004        assert!(!strip_ansi);
1005        assert!(!include_cursor);
1006    }
1007
1008    /// Test snap with all flags
1009    #[test]
1010    fn test_snap_all_flags() {
1011        let cli = Cli::parse_from([
1012            "agent-tui",
1013            "snap",
1014            "-i",
1015            "--region",
1016            "modal",
1017            "--strip-ansi",
1018            "--include-cursor",
1019        ]);
1020        let Commands::Snap {
1021            elements,
1022            region,
1023            strip_ansi,
1024            include_cursor,
1025            ..
1026        } = cli.command
1027        else {
1028            panic!("Expected Snap command, got {:?}", cli.command);
1029        };
1030        assert!(elements);
1031        assert_eq!(region, Some("modal".to_string()));
1032        assert!(strip_ansi);
1033        assert!(include_cursor);
1034    }
1035
1036    /// Test snap accessibility tree format flag
1037    #[test]
1038    fn test_snap_accessibility_flag() {
1039        let cli = Cli::parse_from(["agent-tui", "snap", "-a"]);
1040        let Commands::Snap {
1041            accessibility,
1042            elements,
1043            ..
1044        } = cli.command
1045        else {
1046            panic!("Expected Snap command, got {:?}", cli.command);
1047        };
1048        assert!(accessibility, "-a should enable accessibility tree format");
1049        assert!(!elements, "elements should be false by default");
1050    }
1051
1052    /// Test snap accessibility with interactive-only filter
1053    #[test]
1054    fn test_snap_accessibility_interactive_only() {
1055        let cli = Cli::parse_from(["agent-tui", "snap", "-a", "--interactive-only"]);
1056        let Commands::Snap {
1057            accessibility,
1058            interactive_only,
1059            ..
1060        } = cli.command
1061        else {
1062            panic!("Expected Snap command, got {:?}", cli.command);
1063        };
1064        assert!(accessibility, "--accessibility should be set");
1065        assert!(
1066            interactive_only,
1067            "--interactive-only should filter to interactive elements"
1068        );
1069    }
1070
1071    /// Test click command requires element ref
1072    #[test]
1073    fn test_click_command() {
1074        let cli = Cli::parse_from(["agent-tui", "click", "@btn1"]);
1075        let Commands::Click {
1076            element_ref,
1077            double,
1078        } = cli.command
1079        else {
1080            panic!("Expected Click command, got {:?}", cli.command);
1081        };
1082        assert_eq!(element_ref, "@btn1");
1083        assert!(!double);
1084    }
1085
1086    /// Test click command with --double flag
1087    #[test]
1088    fn test_click_double_flag() {
1089        let cli = Cli::parse_from(["agent-tui", "click", "@btn1", "--double"]);
1090        let Commands::Click {
1091            element_ref,
1092            double,
1093        } = cli.command
1094        else {
1095            panic!("Expected Click command, got {:?}", cli.command);
1096        };
1097        assert_eq!(element_ref, "@btn1");
1098        assert!(double);
1099
1100        // Test short flag
1101        let cli = Cli::parse_from(["agent-tui", "click", "-2", "@btn1"]);
1102        let Commands::Click { double, .. } = cli.command else {
1103            panic!("Expected Click command, got {:?}", cli.command);
1104        };
1105        assert!(double);
1106    }
1107
1108    /// Test fill command requires element ref and value
1109    #[test]
1110    fn test_fill_command() {
1111        let cli = Cli::parse_from(["agent-tui", "fill", "@inp1", "test value"]);
1112        let Commands::Fill { element_ref, value } = cli.command else {
1113            panic!("Expected Fill command, got {:?}", cli.command);
1114        };
1115        assert_eq!(element_ref, "@inp1");
1116        assert_eq!(value, "test value");
1117    }
1118
1119    /// Test key command
1120    #[test]
1121    fn test_key_command() {
1122        let test_cases = vec![
1123            "Enter",
1124            "Tab",
1125            "Escape",
1126            "Backspace",
1127            "Delete",
1128            "ArrowUp",
1129            "ArrowDown",
1130            "ArrowLeft",
1131            "ArrowRight",
1132            "Home",
1133            "End",
1134            "PageUp",
1135            "PageDown",
1136            "F1",
1137            "F10",
1138            "F12",
1139            "Ctrl+C",
1140            "Alt+F4",
1141            "Shift+Tab",
1142        ];
1143
1144        for k in test_cases {
1145            let cli = Cli::parse_from(["agent-tui", "key", k]);
1146            let Commands::Key {
1147                key,
1148                text,
1149                hold,
1150                release,
1151            } = cli.command
1152            else {
1153                panic!("Expected Key command for key: {k}, got {:?}", cli.command);
1154            };
1155            assert_eq!(key, Some(k.to_string()));
1156            assert!(text.is_none());
1157            assert!(!hold);
1158            assert!(!release);
1159        }
1160    }
1161
1162    /// Test key --type command
1163    #[test]
1164    fn test_key_type_command() {
1165        let cli = Cli::parse_from(["agent-tui", "key", "--type", "Hello, World!"]);
1166        let Commands::Key { key, text, .. } = cli.command else {
1167            panic!("Expected Key command, got {:?}", cli.command);
1168        };
1169        assert!(key.is_none());
1170        assert_eq!(text, Some("Hello, World!".to_string()));
1171
1172        // Short form
1173        let cli = Cli::parse_from(["agent-tui", "key", "-t", "hello"]);
1174        let Commands::Key { key, text, .. } = cli.command else {
1175            panic!("Expected Key command, got {:?}", cli.command);
1176        };
1177        assert!(key.is_none());
1178        assert_eq!(text, Some("hello".to_string()));
1179    }
1180
1181    /// Test key --hold command
1182    #[test]
1183    fn test_key_hold_command() {
1184        let cli = Cli::parse_from(["agent-tui", "key", "Shift", "--hold"]);
1185        let Commands::Key {
1186            key, hold, release, ..
1187        } = cli.command
1188        else {
1189            panic!("Expected Key command, got {:?}", cli.command);
1190        };
1191        assert_eq!(key, Some("Shift".to_string()));
1192        assert!(hold);
1193        assert!(!release);
1194    }
1195
1196    /// Test key --release command
1197    #[test]
1198    fn test_key_release_command() {
1199        let cli = Cli::parse_from(["agent-tui", "key", "Shift", "--release"]);
1200        let Commands::Key {
1201            key, hold, release, ..
1202        } = cli.command
1203        else {
1204            panic!("Expected Key command, got {:?}", cli.command);
1205        };
1206        assert_eq!(key, Some("Shift".to_string()));
1207        assert!(!hold);
1208        assert!(release);
1209    }
1210
1211    /// Test key command flag conflicts
1212    #[test]
1213    fn test_key_flag_conflicts() {
1214        // --hold and --release conflict
1215        assert!(Cli::try_parse_from(["agent-tui", "key", "Shift", "--hold", "--release"]).is_err());
1216
1217        // --type and --hold conflict
1218        assert!(Cli::try_parse_from(["agent-tui", "key", "--type", "hello", "--hold"]).is_err());
1219
1220        // --type and --release conflict
1221        assert!(Cli::try_parse_from(["agent-tui", "key", "--type", "hello", "--release"]).is_err());
1222    }
1223
1224    /// Test wait command defaults
1225    #[test]
1226    fn test_wait_defaults() {
1227        let cli = Cli::parse_from(["agent-tui", "wait", "Loading"]);
1228        let Commands::Wait { params } = cli.command else {
1229            panic!("Expected Wait command, got {:?}", cli.command);
1230        };
1231        assert_eq!(params.text, Some("Loading".to_string()));
1232
1233        assert_eq!(params.timeout, 30000, "Default timeout should be 30000ms");
1234    }
1235
1236    /// Test wait with custom timeout
1237    #[test]
1238    fn test_wait_custom_timeout() {
1239        let cli = Cli::parse_from(["agent-tui", "wait", "-t", "5000", "Done"]);
1240        let Commands::Wait { params } = cli.command else {
1241            panic!("Expected Wait command, got {:?}", cli.command);
1242        };
1243        assert_eq!(params.text, Some("Done".to_string()));
1244        assert_eq!(params.timeout, 5000);
1245    }
1246
1247    /// Test wait with --stable flag
1248    #[test]
1249    fn test_wait_stable() {
1250        let cli = Cli::parse_from(["agent-tui", "wait", "--stable"]);
1251        let Commands::Wait { params } = cli.command else {
1252            panic!("Expected Wait command, got {:?}", cli.command);
1253        };
1254        assert!(params.stable);
1255        assert!(params.text.is_none());
1256    }
1257
1258    /// Test wait with --element flag
1259    #[test]
1260    fn test_wait_element() {
1261        let cli = Cli::parse_from(["agent-tui", "wait", "--element", "@btn1"]);
1262        let Commands::Wait { params } = cli.command else {
1263            panic!("Expected Wait command, got {:?}", cli.command);
1264        };
1265        assert_eq!(params.element, Some("@btn1".to_string()));
1266        assert!(params.text.is_none());
1267    }
1268
1269    /// Test wait with -e short flag for element
1270    #[test]
1271    fn test_wait_element_short_flag() {
1272        let cli = Cli::parse_from(["agent-tui", "wait", "-e", "@btn1"]);
1273        let Commands::Wait { params } = cli.command else {
1274            panic!("Expected Wait command, got {:?}", cli.command);
1275        };
1276        assert_eq!(params.element, Some("@btn1".to_string()));
1277        assert!(!params.gone);
1278    }
1279
1280    /// Test wait with --focused flag
1281    #[test]
1282    fn test_wait_focused() {
1283        let cli = Cli::parse_from(["agent-tui", "wait", "--focused", "@inp1"]);
1284        let Commands::Wait { params } = cli.command else {
1285            panic!("Expected Wait command, got {:?}", cli.command);
1286        };
1287        assert_eq!(params.focused, Some("@inp1".to_string()));
1288        assert!(params.text.is_none());
1289    }
1290
1291    /// Test wait with --gone flag for element disappearing
1292    #[test]
1293    fn test_wait_element_gone() {
1294        let cli = Cli::parse_from(["agent-tui", "wait", "-e", "@spinner", "--gone"]);
1295        let Commands::Wait { params } = cli.command else {
1296            panic!("Expected Wait command, got {:?}", cli.command);
1297        };
1298        assert_eq!(params.element, Some("@spinner".to_string()));
1299        assert!(params.gone);
1300    }
1301
1302    /// Test wait with --gone flag for text disappearing
1303    #[test]
1304    fn test_wait_text_gone() {
1305        let cli = Cli::parse_from(["agent-tui", "wait", "Loading...", "--gone"]);
1306        let Commands::Wait { params } = cli.command else {
1307            panic!("Expected Wait command, got {:?}", cli.command);
1308        };
1309        assert_eq!(params.text, Some("Loading...".to_string()));
1310        assert!(params.gone);
1311    }
1312
1313    /// Test wait with -g short flag for gone
1314    #[test]
1315    fn test_wait_gone_short_flag() {
1316        let cli = Cli::parse_from(["agent-tui", "wait", "-e", "@spinner", "-g"]);
1317        let Commands::Wait { params } = cli.command else {
1318            panic!("Expected Wait command, got {:?}", cli.command);
1319        };
1320        assert_eq!(params.element, Some("@spinner".to_string()));
1321        assert!(params.gone);
1322    }
1323
1324    /// Test wait with --value flag
1325    #[test]
1326    fn test_wait_value() {
1327        let cli = Cli::parse_from(["agent-tui", "wait", "--value", "@inp1=hello"]);
1328        let Commands::Wait { params } = cli.command else {
1329            panic!("Expected Wait command, got {:?}", cli.command);
1330        };
1331        assert_eq!(params.value, Some("@inp1=hello".to_string()));
1332    }
1333
1334    /// Test scroll direction enum values
1335    #[test]
1336    fn test_scroll_directions() {
1337        for (arg, expected) in [
1338            ("up", ScrollDirection::Up),
1339            ("down", ScrollDirection::Down),
1340            ("left", ScrollDirection::Left),
1341            ("right", ScrollDirection::Right),
1342        ] {
1343            let cli = Cli::parse_from(["agent-tui", "scroll", arg]);
1344            let Commands::Scroll {
1345                direction,
1346                amount,
1347                element,
1348                to_ref,
1349            } = cli.command
1350            else {
1351                panic!("Expected Scroll command for {arg}, got {:?}", cli.command);
1352            };
1353            assert_eq!(direction.unwrap() as u8, expected as u8);
1354
1355            assert_eq!(amount, 5, "Default scroll amount should be 5");
1356            assert!(element.is_none());
1357            assert!(to_ref.is_none());
1358        }
1359    }
1360
1361    /// Test scroll --to flag
1362    #[test]
1363    fn test_scroll_to_flag() {
1364        let cli = Cli::parse_from(["agent-tui", "scroll", "--to", "@e5"]);
1365        let Commands::Scroll {
1366            direction, to_ref, ..
1367        } = cli.command
1368        else {
1369            panic!("Expected Scroll command, got {:?}", cli.command);
1370        };
1371        assert!(direction.is_none());
1372        assert_eq!(to_ref, Some("@e5".to_string()));
1373    }
1374
1375    /// Test scroll with custom amount
1376    #[test]
1377    fn test_scroll_custom_amount() {
1378        let cli = Cli::parse_from(["agent-tui", "scroll", "down", "-a", "10"]);
1379        let Commands::Scroll { amount, .. } = cli.command else {
1380            panic!("Expected Scroll command, got {:?}", cli.command);
1381        };
1382        assert_eq!(amount, 10);
1383    }
1384
1385    /// Test resize defaults
1386    #[test]
1387    fn test_resize_defaults() {
1388        let cli = Cli::parse_from(["agent-tui", "resize"]);
1389        let Commands::Resize { cols, rows } = cli.command else {
1390            panic!("Expected Resize command, got {:?}", cli.command);
1391        };
1392
1393        assert_eq!(cols, 120, "Default cols should be 120");
1394        assert_eq!(rows, 40, "Default rows should be 40");
1395    }
1396
1397    /// Test trace defaults
1398    #[test]
1399    fn test_trace_defaults() {
1400        let cli = Cli::parse_from(["agent-tui", "trace"]);
1401        let Commands::Trace { count, start, stop } = cli.command else {
1402            panic!("Expected Trace command, got {:?}", cli.command);
1403        };
1404
1405        assert_eq!(count, 10, "Default trace count should be 10");
1406        assert!(!start);
1407        assert!(!stop);
1408    }
1409
1410    /// Test console defaults
1411    #[test]
1412    fn test_console_defaults() {
1413        let cli = Cli::parse_from(["agent-tui", "console"]);
1414        let Commands::Console { lines, clear } = cli.command else {
1415            panic!("Expected Console command, got {:?}", cli.command);
1416        };
1417
1418        assert_eq!(lines, 100, "Default console lines should be 100");
1419        assert!(!clear, "Default clear should be false");
1420    }
1421
1422    /// Test assert command parsing
1423    #[test]
1424    fn test_assert_command() {
1425        let test_cases = vec!["text:Success", "element:@btn1", "session:main"];
1426
1427        for condition in test_cases {
1428            let cli = Cli::parse_from(["agent-tui", "assert", condition]);
1429            let Commands::Assert { condition: parsed } = cli.command else {
1430                panic!(
1431                    "Expected Assert command for {condition}, got {:?}",
1432                    cli.command
1433                );
1434            };
1435            assert_eq!(parsed, condition);
1436        }
1437    }
1438
1439    /// Test find command combinations
1440    #[test]
1441    fn test_find_command() {
1442        let cli = Cli::parse_from(["agent-tui", "find", "--role", "button"]);
1443        let Commands::Find { params } = cli.command else {
1444            panic!("Expected Find command, got {:?}", cli.command);
1445        };
1446        assert_eq!(params.role, Some("button".to_string()));
1447        assert!(params.name.is_none());
1448        assert!(params.text.is_none());
1449        assert!(params.placeholder.is_none());
1450        assert!(!params.focused);
1451        assert!(params.nth.is_none());
1452        assert!(!params.exact);
1453
1454        let cli = Cli::parse_from(["agent-tui", "find", "--role", "button", "--name", "Submit"]);
1455        let Commands::Find { params } = cli.command else {
1456            panic!("Expected Find command, got {:?}", cli.command);
1457        };
1458        assert_eq!(params.role, Some("button".to_string()));
1459        assert_eq!(params.name, Some("Submit".to_string()));
1460
1461        let cli = Cli::parse_from(["agent-tui", "find", "--focused"]);
1462        let Commands::Find { params } = cli.command else {
1463            panic!("Expected Find command, got {:?}", cli.command);
1464        };
1465        assert!(params.focused);
1466
1467        let cli = Cli::parse_from(["agent-tui", "find", "--role", "button", "--nth", "2"]);
1468        let Commands::Find { params } = cli.command else {
1469            panic!("Expected Find command, got {:?}", cli.command);
1470        };
1471        assert_eq!(params.nth, Some(2));
1472
1473        let cli = Cli::parse_from(["agent-tui", "find", "--text", "Submit", "--exact"]);
1474        let Commands::Find { params } = cli.command else {
1475            panic!("Expected Find command, got {:?}", cli.command);
1476        };
1477        assert_eq!(params.text, Some("Submit".to_string()));
1478        assert!(params.exact);
1479
1480        let cli = Cli::parse_from(["agent-tui", "find", "--placeholder", "Search..."]);
1481        let Commands::Find { params } = cli.command else {
1482            panic!("Expected Find command, got {:?}", cli.command);
1483        };
1484        assert_eq!(params.placeholder, Some("Search...".to_string()));
1485    }
1486
1487    /// Test that missing required arguments fail
1488    #[test]
1489    fn test_missing_required_args() {
1490        assert!(Cli::try_parse_from(["agent-tui", "click"]).is_err());
1491
1492        assert!(Cli::try_parse_from(["agent-tui", "fill"]).is_err());
1493        assert!(Cli::try_parse_from(["agent-tui", "fill", "@inp1"]).is_err());
1494
1495        assert!(Cli::try_parse_from(["agent-tui", "run"]).is_err());
1496
1497        assert!(Cli::try_parse_from(["agent-tui", "scroll"]).is_err());
1498    }
1499
1500    /// Test output format enum values
1501    #[test]
1502    fn test_output_format_values() {
1503        let cli = Cli::parse_from(["agent-tui", "-f", "text", "status"]);
1504        assert_eq!(cli.format, OutputFormat::Text);
1505
1506        let cli = Cli::parse_from(["agent-tui", "-f", "json", "status"]);
1507        assert_eq!(cli.format, OutputFormat::Json);
1508
1509        assert!(Cli::try_parse_from(["agent-tui", "-f", "xml", "status"]).is_err());
1510    }
1511
1512    /// Test --json shorthand flag
1513    #[test]
1514    fn test_json_shorthand_flag() {
1515        let cli = Cli::parse_from(["agent-tui", "--json", "status"]);
1516        assert!(cli.json);
1517    }
1518
1519    /// Test spawn with cwd argument
1520    #[test]
1521    fn test_run_with_cwd() {
1522        let cli = Cli::parse_from(["agent-tui", "run", "-d", "/tmp", "bash"]);
1523        let Commands::Run { command, cwd, .. } = cli.command else {
1524            panic!("Expected Run command, got {:?}", cli.command);
1525        };
1526        assert_eq!(command, "bash");
1527        assert_eq!(cwd, Some("/tmp".to_string()));
1528    }
1529
1530    /// Test toggle command
1531    #[test]
1532    fn test_toggle_command() {
1533        let cli = Cli::parse_from(["agent-tui", "toggle", "@cb1"]);
1534        let Commands::Toggle {
1535            element_ref,
1536            on,
1537            off,
1538        } = cli.command
1539        else {
1540            panic!("Expected Toggle command, got {:?}", cli.command);
1541        };
1542        assert_eq!(element_ref, "@cb1");
1543        assert!(!on);
1544        assert!(!off);
1545    }
1546
1547    /// Test toggle with --on flag
1548    #[test]
1549    fn test_toggle_with_on_flag() {
1550        let cli = Cli::parse_from(["agent-tui", "toggle", "@cb1", "--on"]);
1551        let Commands::Toggle {
1552            element_ref,
1553            on,
1554            off,
1555        } = cli.command
1556        else {
1557            panic!("Expected Toggle command, got {:?}", cli.command);
1558        };
1559        assert_eq!(element_ref, "@cb1");
1560        assert!(on);
1561        assert!(!off);
1562    }
1563
1564    /// Test toggle with --off flag
1565    #[test]
1566    fn test_toggle_with_off_flag() {
1567        let cli = Cli::parse_from(["agent-tui", "toggle", "@cb1", "--off"]);
1568        let Commands::Toggle {
1569            element_ref,
1570            on,
1571            off,
1572        } = cli.command
1573        else {
1574            panic!("Expected Toggle command, got {:?}", cli.command);
1575        };
1576        assert_eq!(element_ref, "@cb1");
1577        assert!(!on);
1578        assert!(off);
1579    }
1580
1581    /// Test toggle --on and --off are mutually exclusive
1582    #[test]
1583    fn test_toggle_on_off_mutually_exclusive() {
1584        let result = Cli::try_parse_from(["agent-tui", "toggle", "@cb1", "--on", "--off"]);
1585        assert!(result.is_err());
1586    }
1587
1588    /// Test errors command with count
1589    #[test]
1590    fn test_errors_command_with_count() {
1591        let cli = Cli::parse_from(["agent-tui", "errors", "-n", "25"]);
1592        let Commands::Errors { count, clear } = cli.command else {
1593            panic!("Expected Errors command, got {:?}", cli.command);
1594        };
1595        assert_eq!(count, 25);
1596        assert!(!clear);
1597    }
1598
1599    /// Test errors command with clear
1600    #[test]
1601    fn test_errors_command_with_clear() {
1602        let cli = Cli::parse_from(["agent-tui", "errors", "--clear"]);
1603        let Commands::Errors { clear, .. } = cli.command else {
1604            panic!("Expected Errors command, got {:?}", cli.command);
1605        };
1606        assert!(clear);
1607    }
1608
1609    /// Test attach command
1610    #[test]
1611    fn test_attach_command() {
1612        let cli = Cli::parse_from(["agent-tui", "attach", "my-session"]);
1613        let Commands::Attach {
1614            session_id,
1615            interactive,
1616        } = cli.command
1617        else {
1618            panic!("Expected Attach command, got {:?}", cli.command);
1619        };
1620        assert_eq!(session_id, "my-session");
1621        assert!(!interactive);
1622    }
1623
1624    /// Test attach command with interactive flag
1625    #[test]
1626    fn test_attach_command_interactive() {
1627        let cli = Cli::parse_from(["agent-tui", "attach", "-i", "my-session"]);
1628        let Commands::Attach {
1629            session_id,
1630            interactive,
1631        } = cli.command
1632        else {
1633            panic!("Expected Attach command, got {:?}", cli.command);
1634        };
1635        assert_eq!(session_id, "my-session");
1636        assert!(interactive);
1637    }
1638
1639    /// Test record-start command
1640    #[test]
1641    fn test_record_start_command() {
1642        let cli = Cli::parse_from(["agent-tui", "record-start"]);
1643        assert!(matches!(cli.command, Commands::RecordStart));
1644    }
1645
1646    /// Test record-stop command
1647    #[test]
1648    fn test_record_stop_command() {
1649        let cli = Cli::parse_from(["agent-tui", "record-stop"]);
1650        let Commands::RecordStop {
1651            output,
1652            record_format,
1653        } = cli.command
1654        else {
1655            panic!("Expected RecordStop command, got {:?}", cli.command);
1656        };
1657        assert!(output.is_none());
1658        assert!(matches!(record_format, RecordFormat::Json));
1659    }
1660
1661    /// Test record-stop with output file
1662    #[test]
1663    fn test_record_stop_with_output() {
1664        let cli = Cli::parse_from(["agent-tui", "record-stop", "-o", "recording.json"]);
1665        let Commands::RecordStop { output, .. } = cli.command else {
1666            panic!("Expected RecordStop command, got {:?}", cli.command);
1667        };
1668        assert_eq!(output, Some("recording.json".to_string()));
1669    }
1670
1671    /// Test record-stop with asciicast format
1672    #[test]
1673    fn test_record_stop_asciicast_format() {
1674        let cli = Cli::parse_from(["agent-tui", "record-stop", "--record-format", "asciicast"]);
1675        let Commands::RecordStop { record_format, .. } = cli.command else {
1676            panic!("Expected RecordStop command, got {:?}", cli.command);
1677        };
1678        assert!(matches!(record_format, RecordFormat::Asciicast));
1679    }
1680
1681    /// Test record-status command
1682    #[test]
1683    fn test_record_status_command() {
1684        let cli = Cli::parse_from(["agent-tui", "record-status"]);
1685        assert!(matches!(cli.command, Commands::RecordStatus));
1686    }
1687
1688    /// Test dblclick command
1689    /// Test restart command
1690    #[test]
1691    fn test_restart_command() {
1692        let cli = Cli::parse_from(["agent-tui", "restart"]);
1693        assert!(matches!(cli.command, Commands::Restart));
1694    }
1695
1696    /// Test selectall command
1697    #[test]
1698    fn test_selectall_command() {
1699        let cli = Cli::parse_from(["agent-tui", "selectall", "@inp1"]);
1700        let Commands::SelectAll { element_ref } = cli.command else {
1701            panic!("Expected SelectAll command, got {:?}", cli.command);
1702        };
1703        assert_eq!(element_ref, "@inp1");
1704    }
1705
1706    /// Test multiselect command
1707    #[test]
1708    fn test_multiselect_command() {
1709        let cli = Cli::parse_from(["agent-tui", "multiselect", "@sel1", "opt1", "opt2", "opt3"]);
1710        let Commands::MultiSelect {
1711            element_ref,
1712            options,
1713        } = cli.command
1714        else {
1715            panic!("Expected MultiSelect command, got {:?}", cli.command);
1716        };
1717        assert_eq!(element_ref, "@sel1");
1718        assert_eq!(options, vec!["opt1", "opt2", "opt3"]);
1719    }
1720
1721    /// Test cleanup command
1722    #[test]
1723    fn test_cleanup_command() {
1724        let cli = Cli::parse_from(["agent-tui", "cleanup"]);
1725        let Commands::Cleanup { all } = cli.command else {
1726            panic!("Expected Cleanup command, got {:?}", cli.command);
1727        };
1728        assert!(!all);
1729    }
1730
1731    /// Test cleanup command with --all
1732    #[test]
1733    fn test_cleanup_command_all() {
1734        let cli = Cli::parse_from(["agent-tui", "cleanup", "--all"]);
1735        let Commands::Cleanup { all } = cli.command else {
1736            panic!("Expected Cleanup command, got {:?}", cli.command);
1737        };
1738        assert!(all);
1739    }
1740
1741    /// Test select command
1742    #[test]
1743    fn test_select_command() {
1744        let cli = Cli::parse_from(["agent-tui", "select", "@sel1", "option1"]);
1745        let Commands::Select {
1746            element_ref,
1747            option,
1748        } = cli.command
1749        else {
1750            panic!("Expected Select command, got {:?}", cli.command);
1751        };
1752        assert_eq!(element_ref, "@sel1");
1753        assert_eq!(option, "option1");
1754    }
1755
1756    /// Test scroll with element target
1757    #[test]
1758    fn test_scroll_with_element() {
1759        let cli = Cli::parse_from(["agent-tui", "scroll", "down", "-e", "@list1"]);
1760        let Commands::Scroll { element, .. } = cli.command else {
1761            panic!("Expected Scroll command, got {:?}", cli.command);
1762        };
1763        assert_eq!(element, Some("@list1".to_string()));
1764    }
1765
1766    /// Test count command with role and name
1767    #[test]
1768    fn test_count_command_role_and_name() {
1769        let cli = Cli::parse_from(["agent-tui", "count", "--role", "button", "--name", "Submit"]);
1770        let Commands::Count { role, name, text } = cli.command else {
1771            panic!("Expected Count command, got {:?}", cli.command);
1772        };
1773        assert_eq!(role, Some("button".to_string()));
1774        assert_eq!(name, Some("Submit".to_string()));
1775        assert!(text.is_none());
1776    }
1777
1778    /// Test version command
1779    #[test]
1780    fn test_version_command() {
1781        let cli = Cli::parse_from(["agent-tui", "version"]);
1782        assert!(matches!(cli.command, Commands::Version));
1783    }
1784
1785    /// Test env command
1786    #[test]
1787    fn test_env_command() {
1788        let cli = Cli::parse_from(["agent-tui", "env"]);
1789        assert!(matches!(cli.command, Commands::Env));
1790    }
1791
1792    /// Test ls command
1793    #[test]
1794    fn test_ls_command() {
1795        let cli = Cli::parse_from(["agent-tui", "ls"]);
1796        assert!(matches!(cli.command, Commands::Ls));
1797    }
1798
1799    /// Test kill command
1800    #[test]
1801    fn test_kill_command() {
1802        let cli = Cli::parse_from(["agent-tui", "kill"]);
1803        assert!(matches!(cli.command, Commands::Kill));
1804    }
1805
1806    /// Test status command verbose flag
1807    #[test]
1808    fn test_status_verbose() {
1809        let cli = Cli::parse_from(["agent-tui", "status", "-v"]);
1810        let Commands::Status { verbose } = cli.command else {
1811            panic!("Expected Status command, got {:?}", cli.command);
1812        };
1813        assert!(verbose);
1814    }
1815
1816    /// Test completions command
1817    #[test]
1818    fn test_completions_command() {
1819        let cli = Cli::parse_from(["agent-tui", "completions", "bash"]);
1820        let Commands::Completions { shell } = cli.command else {
1821            panic!("Expected Completions command, got {:?}", cli.command);
1822        };
1823        assert!(matches!(shell, Shell::Bash));
1824    }
1825
1826    /// Test completions with fish shell
1827    #[test]
1828    fn test_completions_fish() {
1829        let cli = Cli::parse_from(["agent-tui", "completions", "fish"]);
1830        let Commands::Completions { shell } = cli.command else {
1831            panic!("Expected Completions command, got {:?}", cli.command);
1832        };
1833        assert!(matches!(shell, Shell::Fish));
1834    }
1835
1836    /// Test daemon start command (default: background)
1837    #[test]
1838    fn test_daemon_start_default() {
1839        let cli = Cli::parse_from(["agent-tui", "daemon", "start"]);
1840        let Commands::Daemon(DaemonCommand::Start { foreground }) = cli.command else {
1841            panic!("Expected Daemon Start command, got {:?}", cli.command);
1842        };
1843        assert!(!foreground, "Default should be background mode");
1844    }
1845
1846    /// Test daemon start --foreground
1847    #[test]
1848    fn test_daemon_start_foreground() {
1849        let cli = Cli::parse_from(["agent-tui", "daemon", "start", "--foreground"]);
1850        let Commands::Daemon(DaemonCommand::Start { foreground }) = cli.command else {
1851            panic!("Expected Daemon Start command, got {:?}", cli.command);
1852        };
1853        assert!(foreground, "Should be foreground mode");
1854    }
1855
1856    /// Test daemon stop command (default: graceful)
1857    #[test]
1858    fn test_daemon_stop_default() {
1859        let cli = Cli::parse_from(["agent-tui", "daemon", "stop"]);
1860        let Commands::Daemon(DaemonCommand::Stop { force }) = cli.command else {
1861            panic!("Expected Daemon Stop command, got {:?}", cli.command);
1862        };
1863        assert!(!force, "Default should be graceful stop");
1864    }
1865
1866    /// Test daemon stop --force
1867    #[test]
1868    fn test_daemon_stop_force() {
1869        let cli = Cli::parse_from(["agent-tui", "daemon", "stop", "--force"]);
1870        let Commands::Daemon(DaemonCommand::Stop { force }) = cli.command else {
1871            panic!("Expected Daemon Stop command, got {:?}", cli.command);
1872        };
1873        assert!(force, "Should be force stop");
1874    }
1875
1876    /// Test daemon status command
1877    #[test]
1878    fn test_daemon_status() {
1879        let cli = Cli::parse_from(["agent-tui", "daemon", "status"]);
1880        assert!(matches!(
1881            cli.command,
1882            Commands::Daemon(DaemonCommand::Status)
1883        ));
1884    }
1885
1886    /// Test daemon restart command
1887    #[test]
1888    fn test_daemon_restart() {
1889        let cli = Cli::parse_from(["agent-tui", "daemon", "restart"]);
1890        assert!(matches!(
1891            cli.command,
1892            Commands::Daemon(DaemonCommand::Restart)
1893        ));
1894    }
1895}