Skip to main content

agent_tui_app/app/
commands.rs

1//! CLI command parsing and configuration.
2
3use clap::ArgGroup;
4use clap::Args;
5use clap::Parser;
6use clap::Subcommand;
7use clap::ValueEnum;
8use clap::ValueHint;
9use std::collections::HashMap;
10use std::num::NonZeroU16;
11use std::path::PathBuf;
12
13pub use crate::adapters::presenter::OutputFormat;
14use crate::app::attach::DetachKeys;
15use crate::domain::TerminalSize;
16
17const AFTER_HELP: &str =
18    "Use --help for full details and examples. Use --format json for machine-readable output.";
19
20const LONG_ABOUT: &str = "\
21Drive TUI (text UI) applications programmatically or interactively.\n\
22\n\
23Common flow: run -> screenshot -> press/type/scroll -> wait -> kill.\n\
24Use --format json for automation-friendly output.\n\
25\n\
26Supported platforms: Unix-like systems only (Linux, macOS, and environments\n\
27with PTYs, Unix domain sockets, and POSIX signals).";
28
29const AFTER_LONG_HELP: &str = r#"WORKFLOW:
30    1. Run a TUI application
31    2. View the screenshot
32    3. Interact with keys/text or scroll
33    4. Wait for UI changes
34    5. Kill the session when done
35
36OUTPUT:
37    --format json  Machine-readable JSON (recommended for automation)
38    --format text  Human-readable text (default)
39
40CONFIGURATION:
41    AGENT_TUI_NO_INPUT          Disable prompts and interactive TTY behavior (default: false)
42    AGENT_TUI_TRANSPORT         IPC transport (unix or ws; default: unix)
43    AGENT_TUI_WS_ADDR           Remote WS-RPC target when transport is ws (e.g. ws://host:port/ws)
44    AGENT_TUI_DETACH_KEYS       Detach keys for `sessions attach` (default: Ctrl-P Ctrl-B)
45    AGENT_TUI_WS_LISTEN         Daemon WS bind address (default: 127.0.0.1:0)
46    AGENT_TUI_WS_ALLOW_REMOTE   Allow non-loopback WS bind (default: false)
47    AGENT_TUI_WS_STATE          Daemon WS state file path (default: ~/.agent-tui/api.json)
48    AGENT_TUI_WS_DISABLED       Disable daemon WS server (default: false)
49    AGENT_TUI_WS_MAX_CONNECTIONS  Max WebSocket connections (default: 32)
50    AGENT_TUI_WS_QUEUE          WS outbound queue size (default: 128)
51    AGENT_TUI_SESSION_STORE     Session metadata log path (default: ~/.agent-tui/sessions.jsonl)
52    AGENT_TUI_LOG               Log file path (optional)
53    AGENT_TUI_LOG_FORMAT        Log format (text or json; default: text)
54    AGENT_TUI_LOG_STREAM        Log output stream (stderr or stdout; default: stderr)
55    AGENT_TUI_UI_URL            Same-origin UI URL or path override for live preview opening (optional)
56    AGENT_TUI_UI_MODE           UI mode override (optional)
57    AGENT_TUI_UI_PORT           UI port override (optional)
58    AGENT_TUI_UI_ROOT           UI root path override (optional)
59    AGENT_TUI_UI_STATE          UI state file path (optional)
60
61EXAMPLES:
62    # Start and interact with a TUI app
63    agent-tui run "npx create-next-app"
64    agent-tui screenshot
65    agent-tui type "my-project"         # Type text
66    agent-tui press Ctrl+M              # Submit the current input
67    agent-tui wait "success"
68    agent-tui kill
69
70    # Navigate menus efficiently
71    agent-tui run htop
72    agent-tui press F10
73    agent-tui press ArrowDown ArrowDown Enter
74
75    # Scroll using directional terminal input
76    agent-tui scroll down
77    agent-tui scroll up 5
78
79PLATFORM SUPPORT:
80    Supported: Linux, macOS, and other Unix-like systems with PTYs,
81    Unix domain sockets, and POSIX signals.
82    Unsupported: Windows and non-Unix runtimes.
83
84    "#;
85
86fn parse_terminal_cols(value: &str) -> Result<u16, String> {
87    let cols = value
88        .parse::<u16>()
89        .map_err(|err| format!("invalid column count '{value}': {err}"))?;
90
91    if cols < TerminalSize::MIN_COLS {
92        return Err(format!(
93            "columns must be at least {}",
94            TerminalSize::MIN_COLS
95        ));
96    }
97    if cols > TerminalSize::MAX_COLS {
98        return Err(format!(
99            "columns must be at most {}",
100            TerminalSize::MAX_COLS
101        ));
102    }
103
104    Ok(cols)
105}
106
107fn parse_terminal_rows(value: &str) -> Result<u16, String> {
108    let rows = value
109        .parse::<u16>()
110        .map_err(|err| format!("invalid row count '{value}': {err}"))?;
111
112    if rows < TerminalSize::MIN_ROWS {
113        return Err(format!("rows must be at least {}", TerminalSize::MIN_ROWS));
114    }
115    if rows > TerminalSize::MAX_ROWS {
116        return Err(format!("rows must be at most {}", TerminalSize::MAX_ROWS));
117    }
118
119    Ok(rows)
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct EnvAssignment {
124    pub key: String,
125    pub value: String,
126}
127
128fn parse_env_assignment(value: &str) -> Result<EnvAssignment, String> {
129    let Some((key, env_value)) = value.split_once('=') else {
130        return Err("expected KEY=VALUE".to_string());
131    };
132    if key.is_empty() {
133        return Err("environment variable name cannot be empty".to_string());
134    }
135    if key.contains('\0') || env_value.contains('\0') {
136        return Err("environment variables cannot contain NUL bytes".to_string());
137    }
138
139    Ok(EnvAssignment {
140        key: key.to_string(),
141        value: env_value.to_string(),
142    })
143}
144
145pub(crate) fn env_assignments_to_map(
146    assignments: Vec<EnvAssignment>,
147) -> Option<HashMap<String, String>> {
148    if assignments.is_empty() {
149        return None;
150    }
151
152    Some(
153        assignments
154            .into_iter()
155            .map(|assignment| (assignment.key, assignment.value))
156            .collect(),
157    )
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
161pub enum CompletionShell {
162    Bash,
163    Zsh,
164    Fish,
165    Elvish,
166}
167
168impl CompletionShell {
169    pub const fn clap_shell(self) -> clap_complete::Shell {
170        match self {
171            Self::Bash => clap_complete::Shell::Bash,
172            Self::Zsh => clap_complete::Shell::Zsh,
173            Self::Fish => clap_complete::Shell::Fish,
174            Self::Elvish => clap_complete::Shell::Elvish,
175        }
176    }
177}
178
179#[derive(Parser)]
180#[command(name = "agent-tui")]
181#[command(author, version, propagate_version = true)]
182#[command(about = "CLI tool for AI agents to interact with TUI applications")]
183#[command(long_about = LONG_ABOUT)]
184#[command(after_help = AFTER_HELP)]
185#[command(after_long_help = AFTER_LONG_HELP)]
186#[command(subcommand_required = true, arg_required_else_help = true)]
187pub struct Cli {
188    #[command(subcommand)]
189    pub command: Commands,
190
191    /// Session ID to use (defaults to the most recent session)
192    #[arg(
193        short,
194        long,
195        global = true,
196        value_name = "ID",
197        help_heading = "Session Options"
198    )]
199    pub session: Option<String>,
200
201    /// Output format (text or json)
202    #[arg(
203        short,
204        long,
205        global = true,
206        value_enum,
207        value_name = "FORMAT",
208        default_value_t = OutputFormat::Text,
209        help_heading = "Output Options"
210    )]
211    pub format: OutputFormat,
212
213    /// Shorthand for --format json (overrides --format if both are set)
214    #[arg(long, global = true, help_heading = "Output Options")]
215    pub json: bool,
216
217    /// Disable colored output (also respects NO_COLOR)
218    #[arg(
219        long,
220        global = true,
221        env = "NO_COLOR",
222        value_parser = clap::builder::BoolishValueParser::new(),
223        help_heading = "Output Options"
224    )]
225    pub no_color: bool,
226
227    /// Disable prompts and interactive TTY behavior; require explicit flags instead
228    #[arg(
229        long,
230        global = true,
231        env = "AGENT_TUI_NO_INPUT",
232        value_parser = clap::builder::BoolishValueParser::new(),
233        help_heading = "Interaction Options"
234    )]
235    pub no_input: bool,
236}
237
238impl Cli {
239    pub fn effective_format(&self) -> OutputFormat {
240        if self.json {
241            OutputFormat::Json
242        } else {
243            self.format
244        }
245    }
246}
247
248#[derive(Debug, PartialEq, Eq)]
249pub(crate) enum LegacyActionOperation {
250    Click,
251    Fill(String),
252}
253
254#[derive(Debug, PartialEq, Eq)]
255pub(crate) struct LegacyActionInvocation {
256    pub operation: LegacyActionOperation,
257}
258
259#[derive(Debug, PartialEq, Eq)]
260pub(crate) struct LegacyActionParseError {
261    selector: String,
262    operation: String,
263}
264
265impl LegacyActionParseError {
266    pub fn selector(&self) -> &str {
267        &self.selector
268    }
269
270    pub fn operation(&self) -> &str {
271        &self.operation
272    }
273}
274
275fn legacy_action_parse_error(
276    selector: impl Into<String>,
277    operation: impl Into<String>,
278) -> LegacyActionParseError {
279    let selector = selector.into();
280    let operation = operation.into();
281    LegacyActionParseError {
282        selector: if selector.is_empty() {
283            "<missing>".to_string()
284        } else {
285            selector
286        },
287        operation: if operation.is_empty() {
288            "<missing>".to_string()
289        } else {
290            operation
291        },
292    }
293}
294
295pub(crate) fn parse_legacy_action_invocation(
296    form: &[String],
297) -> Result<LegacyActionInvocation, LegacyActionParseError> {
298    let [selector, operation_name, rest @ ..] = form else {
299        let selector = form.first().map(String::as_str).unwrap_or("<missing>");
300        return Err(legacy_action_parse_error(selector, "missing"));
301    };
302
303    match operation_name.as_str() {
304        "click" if rest.is_empty() => Ok(LegacyActionInvocation {
305            operation: LegacyActionOperation::Click,
306        }),
307        "fill" if !rest.is_empty() => Ok(LegacyActionInvocation {
308            operation: LegacyActionOperation::Fill(rest.join(" ")),
309        }),
310        _ => Err(legacy_action_parse_error(selector, operation_name)),
311    }
312}
313
314#[derive(Debug, Subcommand)]
315pub enum Commands {
316    /// Run a TUI application in a virtual terminal
317    #[command(long_about = "\
318Run a new TUI application in a virtual terminal.
319
320Creates a new PTY session with the specified command and returns a session ID.
321The session runs in the background and can be interacted with using other commands.
322Use `--` before COMMAND args that start with `-` (e.g., `run -- vim -n`).")]
323    #[command(after_long_help = "\
324EXAMPLES:
325    agent-tui run bash
326    agent-tui run --env FOO=bar --env BAZ=qux bash
327    agent-tui run htop
328    agent-tui run \"npx create-next-app\"
329    agent-tui run vim -- file.txt
330    agent-tui run --cols 80 --rows 24 nano")]
331    Run {
332        /// Command to run inside the virtual terminal
333        #[arg(value_name = "COMMAND", value_hint = ValueHint::CommandName)]
334        command: String,
335
336        /// Arguments for the command (use -- to pass flags through)
337        #[arg(trailing_var_arg = true, value_name = "ARG")]
338        args: Vec<String>,
339
340        /// Working directory for the command
341        #[arg(short = 'd', long, value_name = "DIR", value_hint = ValueHint::DirPath)]
342        cwd: Option<PathBuf>,
343
344        /// Environment variable override for the spawned session (repeatable)
345        #[arg(
346            long = "env",
347            value_name = "KEY=VALUE",
348            value_parser = parse_env_assignment,
349            help_heading = "Environment"
350        )]
351        env: Vec<EnvAssignment>,
352
353        /// Terminal columns (default: 120)
354        #[arg(
355            long,
356            default_value_t = 120,
357            value_name = "COLS",
358            value_parser = parse_terminal_cols,
359            help_heading = "Terminal Size"
360        )]
361        cols: u16,
362
363        /// Terminal rows (default: 40)
364        #[arg(
365            long,
366            default_value_t = 40,
367            value_name = "ROWS",
368            value_parser = parse_terminal_rows,
369            help_heading = "Terminal Size"
370        )]
371        rows: u16,
372    },
373
374    /// Capture a screenshot of the current session
375    #[command(long_about = "\
376View the current screenshot state.
377
378Returns the current terminal screenshot content.")]
379    #[command(after_long_help = "\
380EXAMPLES:
381    agent-tui screenshot               # Screenshot with terminal colors/styles
382    agent-tui screenshot --retain-ansi # Explicitly preserve terminal colors/styles
383    agent-tui screenshot --strip-ansi  # Plain text without colors
384
385LEGACY COMPATIBILITY:
386    agent-tui screenshot -e             # Deprecated; returns the standard screenshot
387    agent-tui screenshot -a             # Deprecated; returns the standard screenshot
388    agent-tui screenshot --interactive-only # Deprecated; returns the standard screenshot")]
389    Screenshot {
390        /// Reserved for future named regions; currently rejected if provided
391        #[arg(long, value_name = "REGION", help_heading = "Filtering")]
392        region: Option<String>,
393
394        /// Strip ANSI color codes from output
395        #[arg(long, conflicts_with = "retain_ansi", help_heading = "Output Options")]
396        strip_ansi: bool,
397
398        /// Preserve ANSI color/style codes in output (default)
399        #[arg(long, conflicts_with = "strip_ansi", help_heading = "Output Options")]
400        retain_ansi: bool,
401
402        /// Include cursor position in output
403        #[arg(long, help_heading = "Output Options")]
404        include_cursor: bool,
405
406        /// Deprecated compatibility flag; returns the standard terminal screenshot
407        #[arg(short = 'e', help_heading = "Legacy Compatibility")]
408        legacy_element: bool,
409
410        /// Deprecated compatibility flag; returns the standard terminal screenshot
411        #[arg(short = 'a', help_heading = "Legacy Compatibility")]
412        legacy_accessibility: bool,
413
414        /// Deprecated compatibility flag; returns the standard terminal screenshot
415        #[arg(long = "interactive-only", help_heading = "Legacy Compatibility")]
416        legacy_interactive_only: bool,
417    },
418
419    /// Deprecated selector action compatibility command
420    #[command(long_about = "\
421Deprecated compatibility command for old selector-based action workflows.
422
423Use current terminal commands (`press`, `type`, and `scroll`) for new scripts.")]
424    #[command(after_long_help = "\
425SUPPORTED COMPATIBILITY FORMS:
426    agent-tui action <selector> click        # Sends Enter with `agent-tui press Enter`
427    agent-tui action <selector> fill <text>  # Types text with `agent-tui type <text>`
428
429Unsupported selector actions return a compatibility error with migration guidance.")]
430    Action {
431        /// Legacy selector/action form
432        #[arg(value_name = "FORM", required = true, num_args = 1.., allow_hyphen_values = true)]
433        form: Vec<String>,
434    },
435    /// Resize the session terminal
436    #[command(long_about = "\
437Resize the current session terminal.")]
438    #[command(after_long_help = "\
439EXAMPLES:
440    agent-tui resize --cols 120 --rows 40")]
441    Resize {
442        /// Terminal columns
443        #[arg(long, value_name = "COLS", value_parser = parse_terminal_cols)]
444        cols: u16,
445
446        /// Terminal rows
447        #[arg(long, value_name = "ROWS", value_parser = parse_terminal_rows)]
448        rows: u16,
449    },
450
451    /// Restart the current session
452    #[command(long_about = "\
453Restart the current session command, creating a new session.")]
454    #[command(after_long_help = "\
455EXAMPLES:
456    agent-tui restart --yes
457    agent-tui --session abc123 restart --dry-run")]
458    Restart {
459        /// Preview the restart without changing the session
460        #[arg(long)]
461        dry_run: bool,
462
463        /// Skip interactive confirmation
464        #[arg(short = 'y', long)]
465        yes: bool,
466    },
467    /// Send key press(es) to the terminal (supports modifier hold/release)
468    #[command(after_long_help = "\
469NOTES:
470    --hold/--release require a single modifier key (Ctrl, Alt, Shift, Meta)
471
472EXAMPLES:
473    agent-tui press Enter
474    agent-tui press Ctrl+C
475    agent-tui press ArrowDown ArrowDown Enter
476    agent-tui press Shift --hold
477    agent-tui press Shift --release")]
478    Press {
479        /// Keys to press (e.g., Enter, Ctrl+C, ArrowDown)
480        #[arg(required = true, value_name = "KEY")]
481        keys: Vec<String>,
482
483        /// Hold a modifier key down (Ctrl, Alt, Shift, Meta)
484        #[arg(long, conflicts_with = "release", help_heading = "Modifiers")]
485        hold: bool,
486
487        /// Release a held modifier key (Ctrl, Alt, Shift, Meta)
488        #[arg(long, conflicts_with = "hold", help_heading = "Modifiers")]
489        release: bool,
490    },
491
492    /// Type literal text character by character
493    #[command(long_about = "\
494Type literal text character by character.
495
496Pass `-` to read the text payload from stdin in non-interactive pipelines.")]
497    #[command(after_long_help = "\
498EXAMPLES:
499    agent-tui type \"hello world\"
500    agent-tui type \"user@example.com\"
501    printf 'project-name' | agent-tui type -")]
502    Type {
503        /// Text to type
504        #[arg(value_name = "TEXT", allow_hyphen_values = true)]
505        text: String,
506    },
507
508    /// Legacy alias for `type`
509    #[command(long_about = "\
510Legacy alias for `agent-tui type`.
511
512Use `agent-tui type` for new scripts.")]
513    Input {
514        /// Text to type
515        #[arg(value_name = "TEXT", allow_hyphen_values = true)]
516        text: String,
517    },
518
519    /// Scroll using repeated directional terminal input
520    #[command(long_about = "\
521Send repeated directional input to the terminal.
522
523This is a thin convenience wrapper over terminal keys:
524    up    -> ArrowUp
525    down  -> ArrowDown
526    left  -> ArrowLeft
527    right -> ArrowRight")]
528    #[command(after_long_help = "\
529EXAMPLES:
530    agent-tui scroll down
531    agent-tui scroll up 10
532    agent-tui scroll right 3")]
533    Scroll {
534        /// Direction to move
535        #[arg(value_enum, value_name = "DIRECTION")]
536        direction: ScrollDirection,
537
538        /// Number of steps to send
539        #[arg(default_value_t = NonZeroU16::MIN, value_name = "AMOUNT")]
540        amount: NonZeroU16,
541    },
542
543    /// Deprecated element scroll compatibility command
544    #[command(long_about = "\
545Deprecated compatibility command for old element scroll workflows.
546
547The current CLI has no element selector engine. This command does not send terminal input; use `scroll` or `press` for new scripts.")]
548    #[command(after_long_help = "\
549SUPPORTED COMPATIBILITY FORMS:
550    agent-tui scroll-into-view <selector>  # No-op compatibility success
551
552Unsupported selector options return a compatibility error with migration guidance.")]
553    ScrollIntoView {
554        /// Legacy selector form
555        #[arg(value_name = "FORM", required = true, num_args = 1.., allow_hyphen_values = true)]
556        form: Vec<String>,
557    },
558
559    /// Wait for text or screenshot stability
560    #[command(long_about = "\
561Wait for a condition to be met before continuing.
562
563Waits for text to appear/disappear or the screenshot to stabilize.
564Returns success if the condition is met within the timeout period.
565
566WAIT CONDITIONS:
567    <text>       Wait for text to appear on screenshot
568    --stable     Wait for screenshot to stop changing
569    -g, --gone   Modifier: wait for text to disappear
570    -e <ref>     Deprecated: treats element ref as literal text
571
572ASSERT MODE:
573    --assert            Exit with code 0 if condition met, 75 if timeout.
574                        Without --assert, always exit 0 (timeout still reported).")]
575    #[command(after_long_help = "\
576EXAMPLES:
577    agent-tui wait \"Continue\"           # Wait for text
578    agent-tui wait --stable             # Wait for screenshot stability
579    agent-tui wait \"Loading\" --gone     # Wait for text to disappear
580    agent-tui wait -t 5000 \"Done\"       # 5 second timeout")]
581    Wait {
582        #[command(flatten)]
583        params: WaitParams,
584    },
585
586    /// Kill the current session
587    #[command(after_long_help = "\
588EXAMPLES:
589    agent-tui kill --yes
590    agent-tui --session abc123 kill --dry-run")]
591    Kill {
592        /// Preview the kill without changing the session
593        #[arg(long)]
594        dry_run: bool,
595
596        /// Skip interactive confirmation
597        #[arg(short = 'y', long)]
598        yes: bool,
599    },
600
601    /// List and manage sessions
602    #[command(long_about = "\
603Manage sessions - list, show details, attach, switch active, or cleanup.
604
605By default, lists all active sessions.
606
607MODES:
608    list              List active sessions (default)
609    show <id>         Show details for a session
610    attach            Attach with TTY (defaults to --session or active)
611    switch <id>       Set the active session
612    cleanup [--all]   Remove dead/orphaned sessions")]
613    #[command(after_long_help = "\
614EXAMPLES:
615    agent-tui sessions                    # List sessions
616    agent-tui sessions list               # List sessions (explicit)
617    agent-tui sessions show abc123        # Show session details
618    agent-tui sessions attach             # Attach to active session (TTY)
619    agent-tui -s abc123 sessions attach   # Attach to session by id (TTY)
620    agent-tui sessions switch abc123      # Set active session
621    agent-tui -s abc123 sessions attach -T # Attach without TTY (stream output only)
622    agent-tui sessions attach --detach-keys 'ctrl-]'  # Custom detach sequence
623    agent-tui sessions cleanup --yes            # Remove dead sessions
624    agent-tui sessions cleanup --all --dry-run  # Preview removing all sessions")]
625    #[command(after_help = "Default action: list (same as `sessions list`).")]
626    Sessions {
627        #[command(subcommand)]
628        command: Option<SessionsCommand>,
629    },
630
631    /// Live preview API exposed by the local daemon
632    #[command(long_about = "\
633Show the local daemon's live preview WebSocket endpoints.
634
635The daemon serves a built-in web UI at /ui and exposes JSON-RPC over WebSocket at /ws.
636Use this command to print WS/UI URLs so external frontends can connect.
637This command always inspects the local daemon and does not use AGENT_TUI_TRANSPORT.
638
639CONFIGURATION:
640    AGENT_TUI_WS_LISTEN          Bind address (default: 127.0.0.1:0)
641    AGENT_TUI_WS_ALLOW_REMOTE    Allow non-loopback bind (default: false)
642    AGENT_TUI_WS_STATE           State file path (default: ~/.agent-tui/api.json)
643    AGENT_TUI_UI_URL             Same-origin UI URL or path to open with --open (CLI appends ws/session/auto)
644
645SECURITY:
646    Remote exposure is opt-in. Set AGENT_TUI_WS_ALLOW_REMOTE=1 for non-loopback binds.")]
647    #[command(after_long_help = "\
648EXAMPLES:
649    agent-tui live start
650    agent-tui live status
651    agent-tui live stop")]
652    #[command(after_help = "Default action: info (same as `live start`).")]
653    Live {
654        #[command(subcommand)]
655        command: Option<LiveCommand>,
656    },
657    /// Manage the background daemon
658    #[command(long_about = "\
659Manage the background daemon lifecycle.
660
661Use `daemon start` to launch in the background, `daemon run` for foreground
662debugging, and `daemon status` to inspect the local daemon state.")]
663    #[command(after_long_help = "\
664EXAMPLES:
665    agent-tui daemon start
666    agent-tui daemon status
667    agent-tui daemon stop --yes")]
668    #[command(subcommand)]
669    Daemon(DaemonCommand),
670
671    /// Show version information
672    #[command(long_about = "\
673Show detailed version information.
674
675Shows version info for both the CLI binary and the local running daemon.
676Useful for verifying CLI/daemon compatibility.")]
677    #[command(after_long_help = "\
678EXAMPLES:
679    agent-tui version
680    agent-tui --format json version")]
681    Version,
682
683    /// Show environment diagnostics
684    #[command(long_about = "\
685Show environment diagnostics.
686
687Displays all environment variables and configuration that affect
688agent-tui behavior. Useful for troubleshooting connection issues.")]
689    #[command(after_long_help = "\
690EXAMPLES:
691    agent-tui env
692    agent-tui --format json env")]
693    Env,
694    /// Generate or install shell completions
695    #[command(long_about = "\
696Generate or install shell completions for bash, zsh, fish, or elvish.
697
698Runs an interactive setup by default (auto-detects your shell) and checks
699whether your installed completions are up-to-date. Use --print to output the
700raw completion script for scripting or redirection.
701
702Use --no-input to disable prompts and require explicit shell selection.")]
703    #[command(after_long_help = "\
704EXAMPLES:
705    agent-tui completions
706    agent-tui completions zsh
707    agent-tui completions --print bash
708    agent-tui completions --install fish
709
710INSTALLATION:
711    # Bash - add to ~/.bashrc
712    source <(agent-tui completions bash --print)
713
714    # Zsh - add to ~/.zshrc
715    source <(agent-tui completions zsh --print)
716
717    # Fish - run once
718    agent-tui completions fish --print > ~/.config/fish/completions/agent-tui.fish
719
720    # Elvish - run once
721    agent-tui completions elvish --print > ~/.elvish/lib/agent-tui.elv")]
722    Completions {
723        #[arg(value_enum, value_name = "SHELL")]
724        shell: Option<CompletionShell>,
725        /// Print the completion script to stdout
726        #[arg(long, conflicts_with = "install")]
727        print: bool,
728        /// Install completions to the default location for the shell
729        #[arg(long, conflicts_with = "print")]
730        install: bool,
731        /// Skip prompts and accept defaults
732        #[arg(short = 'y', long)]
733        yes: bool,
734    },
735}
736
737#[derive(Debug, Subcommand)]
738pub enum SessionsCommand {
739    /// List active sessions
740    #[command(alias = "ls")]
741    #[command(after_long_help = "\
742EXAMPLES:
743    agent-tui sessions list
744    agent-tui --json sessions list")]
745    List,
746
747    /// Show details for a specific session
748    #[command(after_long_help = "\
749EXAMPLES:
750    agent-tui sessions show abc123
751    agent-tui --json sessions show abc123")]
752    Show {
753        #[arg(value_name = "ID")]
754        session_id: String,
755    },
756
757    /// Attach to the active session (TTY by default; detach with Ctrl-P Ctrl-B or --detach-keys)
758    #[command(after_long_help = "\
759NOTES:
760    --no-input implies --no-tty for automation-safe streaming.
761
762EXAMPLES:
763    agent-tui sessions attach
764    agent-tui -s abc123 sessions attach --no-tty
765    agent-tui --no-input sessions attach")]
766    Attach {
767        /// Disable TTY mode (stream output only)
768        #[arg(short = 'T', long = "no-tty")]
769        no_tty: bool,
770        /// Detach key sequence (docker-style, e.g. "ctrl-p,ctrl-b"; use "none" to disable)
771        #[arg(
772            long = "detach-keys",
773            value_name = "KEYS",
774            env = "AGENT_TUI_DETACH_KEYS"
775        )]
776        detach_keys: Option<DetachKeys>,
777    },
778
779    /// Set the active session without attaching
780    #[command(alias = "select")]
781    #[command(after_long_help = "\
782EXAMPLES:
783    agent-tui sessions switch abc123
784    agent-tui sessions select abc123")]
785    Switch {
786        #[arg(value_name = "ID")]
787        session_id: String,
788    },
789
790    /// Remove dead/orphaned sessions
791    #[command(after_long_help = "\
792EXAMPLES:
793    agent-tui sessions cleanup --yes
794    agent-tui sessions cleanup --all --dry-run")]
795    Cleanup {
796        /// Remove all sessions (including active)
797        #[arg(long)]
798        all: bool,
799
800        /// Preview which sessions would be cleaned without killing them
801        #[arg(long)]
802        dry_run: bool,
803
804        /// Skip interactive confirmation
805        #[arg(short = 'y', long)]
806        yes: bool,
807    },
808}
809
810#[derive(Debug, Subcommand)]
811pub enum LiveCommand {
812    /// Show the live preview API details
813    #[command(alias = "info")]
814    #[command(after_long_help = "\
815EXAMPLES:
816    agent-tui live start
817    agent-tui live start --open")]
818    Start(LiveStartArgs),
819
820    /// Stop any managed UI server and show how to stop daemon-backed live preview
821    #[command(after_long_help = "\
822EXAMPLES:
823    agent-tui live stop
824    agent-tui daemon stop --yes   # Stop daemon-backed live preview")]
825    Stop,
826
827    /// Show live preview API status
828    #[command(after_long_help = "\
829EXAMPLES:
830    agent-tui live status
831    agent-tui --json live status")]
832    Status,
833}
834
835#[derive(Debug, Clone, Default, Args)]
836pub struct LiveStartArgs {
837    /// Open the preview URL in a browser (uses AGENT_TUI_UI_URL if set)
838    #[arg(long)]
839    pub open: bool,
840
841    /// Browser command to use (overrides $BROWSER)
842    #[arg(long, value_name = "CMD", value_hint = ValueHint::CommandName)]
843    pub browser: Option<String>,
844}
845
846#[derive(Debug, Subcommand)]
847#[command(subcommand_required = true, arg_required_else_help = true)]
848pub enum DaemonCommand {
849    /// Start the daemon process
850    #[command(long_about = "\
851Start the daemon process.
852
853Starts the daemon in the background. Use `daemon run` to keep it in the
854foreground.")]
855    #[command(after_long_help = "\
856EXAMPLES:
857    agent-tui daemon start              # Start in background")]
858    Start {},
859
860    /// Run the daemon in the foreground
861    #[command(long_about = "\
862Run the daemon in the foreground.
863
864This is the UNIX-style form for supervisors and local debugging when you want
865the daemon attached to the current process instead of forking to the
866background.")]
867    #[command(after_long_help = "\
868EXAMPLES:
869    agent-tui daemon run
870    AGENT_TUI_WS_LISTEN=0.0.0.0:8080 agent-tui daemon run")]
871    Run,
872
873    /// Stop the running daemon
874    #[command(long_about = "\
875Stop the running daemon.
876
877Sends SIGTERM to gracefully stop the daemon, allowing it to clean up
878sessions and resources. Use --force to send SIGKILL for immediate
879termination (not recommended unless daemon is unresponsive).")]
880    #[command(after_long_help = "\
881EXAMPLES:
882    agent-tui daemon stop --yes          # Graceful stop
883    agent-tui daemon stop --force --yes  # Force kill")]
884    Stop {
885        /// Force kill the daemon (SIGKILL)
886        #[arg(long)]
887        force: bool,
888
889        /// Preview the stop without changing daemon state
890        #[arg(long)]
891        dry_run: bool,
892
893        /// Skip interactive confirmation
894        #[arg(short = 'y', long)]
895        yes: bool,
896    },
897
898    /// Show daemon status
899    #[command(long_about = "\
900Show daemon status.
901
902Reports whether the daemon is running, its PID, versions, and any discovered
903WS/UI endpoints.
904
905EXIT CODES (LSB init script conventions):
906    0 - Daemon is running
907    3 - Daemon is not running")]
908    #[command(after_long_help = "\
909EXAMPLES:
910    agent-tui daemon status
911    agent-tui --json daemon status")]
912    Status,
913
914    /// Restart the daemon
915    #[command(long_about = "\
916Restart the daemon.
917
918Stops the running daemon and starts a new one. Useful after updating
919the agent-tui binary to ensure the daemon is running the new version.
920
921All active sessions will be terminated during restart.")]
922    #[command(after_long_help = "\
923EXAMPLES:
924    agent-tui daemon restart --yes
925    agent-tui daemon restart --dry-run")]
926    Restart {
927        /// Preview the restart without changing daemon state
928        #[arg(long)]
929        dry_run: bool,
930
931        /// Skip interactive confirmation
932        #[arg(short = 'y', long)]
933        yes: bool,
934    },
935}
936
937#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
938pub enum ScrollDirection {
939    Up,
940    Down,
941    Left,
942    Right,
943}
944
945impl ScrollDirection {
946    pub fn as_str(self) -> &'static str {
947        match self {
948            Self::Up => "up",
949            Self::Down => "down",
950            Self::Left => "left",
951            Self::Right => "right",
952        }
953    }
954}
955
956#[derive(Debug, Clone, Default, Args)]
957#[command(
958    group = ArgGroup::new("wait_condition")
959        .multiple(false)
960        .required(true)
961        .args(&["text", "stable", "legacy_element"]),
962    group = ArgGroup::new("wait_text_condition")
963        .multiple(false)
964        .required(false)
965        .args(&["text", "legacy_element"])
966)]
967pub struct WaitParams {
968    /// Text to wait for (positional)
969    #[arg(value_name = "TEXT", allow_hyphen_values = true)]
970    pub text: Option<String>,
971
972    /// Timeout in milliseconds (default: 30000)
973    #[arg(
974        short,
975        long,
976        default_value_t = 30_000,
977        value_name = "MILLIS",
978        help_heading = "Timing"
979    )]
980    pub timeout: u64,
981
982    /// Wait for the screenshot to stop changing
983    #[arg(long, group = "wait_condition", help_heading = "Wait Condition")]
984    pub stable: bool,
985
986    /// Wait for the text to disappear
987    #[arg(
988        short = 'g',
989        long,
990        requires = "wait_text_condition",
991        help_heading = "Wait Condition"
992    )]
993    pub gone: bool,
994
995    /// Deprecated compatibility flag; treats the element ref as literal text
996    #[arg(
997        short = 'e',
998        value_name = "REF",
999        allow_hyphen_values = true,
1000        group = "wait_condition",
1001        help_heading = "Legacy Compatibility"
1002    )]
1003    pub legacy_element: Option<String>,
1004
1005    /// Exit with status 0 if met, 75 on timeout
1006    #[arg(long, help_heading = "Behavior")]
1007    pub assert: bool,
1008}
1009
1010#[cfg(test)]
1011#[path = "commands_tests.rs"]
1012mod tests;