1use 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 #[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 #[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 #[arg(long, global = true, help_heading = "Output Options")]
215 pub json: bool,
216
217 #[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 #[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 #[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 #[arg(value_name = "COMMAND", value_hint = ValueHint::CommandName)]
334 command: String,
335
336 #[arg(trailing_var_arg = true, value_name = "ARG")]
338 args: Vec<String>,
339
340 #[arg(short = 'd', long, value_name = "DIR", value_hint = ValueHint::DirPath)]
342 cwd: Option<PathBuf>,
343
344 #[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 #[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 #[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 #[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 #[arg(long, value_name = "REGION", help_heading = "Filtering")]
392 region: Option<String>,
393
394 #[arg(long, conflicts_with = "retain_ansi", help_heading = "Output Options")]
396 strip_ansi: bool,
397
398 #[arg(long, conflicts_with = "strip_ansi", help_heading = "Output Options")]
400 retain_ansi: bool,
401
402 #[arg(long, help_heading = "Output Options")]
404 include_cursor: bool,
405
406 #[arg(short = 'e', help_heading = "Legacy Compatibility")]
408 legacy_element: bool,
409
410 #[arg(short = 'a', help_heading = "Legacy Compatibility")]
412 legacy_accessibility: bool,
413
414 #[arg(long = "interactive-only", help_heading = "Legacy Compatibility")]
416 legacy_interactive_only: bool,
417 },
418
419 #[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 #[arg(value_name = "FORM", required = true, num_args = 1.., allow_hyphen_values = true)]
433 form: Vec<String>,
434 },
435 #[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 #[arg(long, value_name = "COLS", value_parser = parse_terminal_cols)]
444 cols: u16,
445
446 #[arg(long, value_name = "ROWS", value_parser = parse_terminal_rows)]
448 rows: u16,
449 },
450
451 #[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 #[arg(long)]
461 dry_run: bool,
462
463 #[arg(short = 'y', long)]
465 yes: bool,
466 },
467 #[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 #[arg(required = true, value_name = "KEY")]
481 keys: Vec<String>,
482
483 #[arg(long, conflicts_with = "release", help_heading = "Modifiers")]
485 hold: bool,
486
487 #[arg(long, conflicts_with = "hold", help_heading = "Modifiers")]
489 release: bool,
490 },
491
492 #[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 #[arg(value_name = "TEXT", allow_hyphen_values = true)]
505 text: String,
506 },
507
508 #[command(long_about = "\
510Legacy alias for `agent-tui type`.
511
512Use `agent-tui type` for new scripts.")]
513 Input {
514 #[arg(value_name = "TEXT", allow_hyphen_values = true)]
516 text: String,
517 },
518
519 #[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 #[arg(value_enum, value_name = "DIRECTION")]
536 direction: ScrollDirection,
537
538 #[arg(default_value_t = NonZeroU16::MIN, value_name = "AMOUNT")]
540 amount: NonZeroU16,
541 },
542
543 #[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 #[arg(value_name = "FORM", required = true, num_args = 1.., allow_hyphen_values = true)]
556 form: Vec<String>,
557 },
558
559 #[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 #[command(after_long_help = "\
588EXAMPLES:
589 agent-tui kill --yes
590 agent-tui --session abc123 kill --dry-run")]
591 Kill {
592 #[arg(long)]
594 dry_run: bool,
595
596 #[arg(short = 'y', long)]
598 yes: bool,
599 },
600
601 #[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 #[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 #[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 #[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 #[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 #[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 #[arg(long, conflicts_with = "install")]
727 print: bool,
728 #[arg(long, conflicts_with = "print")]
730 install: bool,
731 #[arg(short = 'y', long)]
733 yes: bool,
734 },
735}
736
737#[derive(Debug, Subcommand)]
738pub enum SessionsCommand {
739 #[command(alias = "ls")]
741 #[command(after_long_help = "\
742EXAMPLES:
743 agent-tui sessions list
744 agent-tui --json sessions list")]
745 List,
746
747 #[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 #[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 #[arg(short = 'T', long = "no-tty")]
769 no_tty: bool,
770 #[arg(
772 long = "detach-keys",
773 value_name = "KEYS",
774 env = "AGENT_TUI_DETACH_KEYS"
775 )]
776 detach_keys: Option<DetachKeys>,
777 },
778
779 #[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 #[command(after_long_help = "\
792EXAMPLES:
793 agent-tui sessions cleanup --yes
794 agent-tui sessions cleanup --all --dry-run")]
795 Cleanup {
796 #[arg(long)]
798 all: bool,
799
800 #[arg(long)]
802 dry_run: bool,
803
804 #[arg(short = 'y', long)]
806 yes: bool,
807 },
808}
809
810#[derive(Debug, Subcommand)]
811pub enum LiveCommand {
812 #[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 #[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 #[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 #[arg(long)]
839 pub open: bool,
840
841 #[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 #[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 #[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 #[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 #[arg(long)]
887 force: bool,
888
889 #[arg(long)]
891 dry_run: bool,
892
893 #[arg(short = 'y', long)]
895 yes: bool,
896 },
897
898 #[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 #[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 #[arg(long)]
929 dry_run: bool,
930
931 #[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 #[arg(value_name = "TEXT", allow_hyphen_values = true)]
970 pub text: Option<String>,
971
972 #[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 #[arg(long, group = "wait_condition", help_heading = "Wait Condition")]
984 pub stable: bool,
985
986 #[arg(
988 short = 'g',
989 long,
990 requires = "wait_text_condition",
991 help_heading = "Wait Condition"
992 )]
993 pub gone: bool,
994
995 #[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 #[arg(long, help_heading = "Behavior")]
1007 pub assert: bool,
1008}
1009
1010#[cfg(test)]
1011#[path = "commands_tests.rs"]
1012mod tests;