diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 8c52752f..c0fedd25 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -4,6 +4,32 @@ use std::io::{self, BufRead}; use crate::flags::Flags; +/// Parse and extract timeout flag from arguments +/// Returns (timeout_value, filtered_args_without_timeout) +fn extract_timeout<'a>(args: &'a [&'a str]) -> (Option, Vec<&'a str>) { + let mut timeout = None; + let mut filtered = Vec::new(); + let mut skip_next = false; + + for (i, &arg) in args.iter().enumerate() { + if skip_next { + skip_next = false; + continue; + } + + if arg == "--timeout" { + if let Some(&next) = args.get(i + 1) { + timeout = next.parse::().ok(); + skip_next = true; + } + } else { + filtered.push(arg); + } + } + + (timeout, filtered) +} + /// Error type for command parsing with contextual information #[derive(Debug)] pub enum ParseError { @@ -117,11 +143,17 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result { - let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + let (timeout, filtered) = extract_timeout(&rest); + let sel = filtered.get(0).ok_or_else(|| ParseError::MissingArguments { context: "click".to_string(), - usage: "click ", + usage: "click [--timeout ]", })?; - Ok(json!({ "id": id, "action": "click", "selector": sel })) + + let mut cmd = json!({ "id": id, "action": "click", "selector": sel }); + if let Some(t) = timeout { + cmd["timeout"] = json!(t); + } + Ok(cmd) } "dblclick" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index 5cd897ad..d7b005b6 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -41,6 +41,7 @@ agent-browser snapshot -s "#selector" # Scope to CSS selector # Interaction (use @refs from snapshot) agent-browser click @e1 # Click element +agent-browser click @e1 --timeout 500 # Use if page hangs (optional) agent-browser fill @e2 "text" # Clear and type text agent-browser type @e2 "text" # Type without clearing agent-browser select @e1 "option" # Select dropdown option diff --git a/src/actions.ts b/src/actions.ts index c7a626c9..baa78a21 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -505,6 +505,7 @@ async function handleClick(command: ClickCommand, browser: BrowserManager): Prom button: command.button, clickCount: command.clickCount, delay: command.delay, + timeout: command.timeout, }); } catch (error) { throw toAIFriendlyError(error, command.selector); diff --git a/src/protocol.test.ts b/src/protocol.test.ts index 55a4c205..2886c44e 100644 --- a/src/protocol.test.ts +++ b/src/protocol.test.ts @@ -62,10 +62,29 @@ describe('parseCommand', () => { } }); + it('should parse click command with timeout', () => { + const result = parseCommand( + cmd({ id: '1', action: 'click', selector: '#btn', timeout: 500 }) + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('click'); + expect(result.command.selector).toBe('#btn'); + expect(result.command.timeout).toBe(500); + } + }); + it('should reject click without selector', () => { const result = parseCommand(cmd({ id: '1', action: 'click' })); expect(result.success).toBe(false); }); + + it('should reject click with invalid timeout', () => { + const result = parseCommand( + cmd({ id: '1', action: 'click', selector: '#btn', timeout: -100 }) + ); + expect(result.success).toBe(false); + }); }); describe('type', () => { diff --git a/src/protocol.ts b/src/protocol.ts index 52fe07d3..0eaed07d 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -63,6 +63,7 @@ const clickSchema = baseCommandSchema.extend({ button: z.enum(['left', 'right', 'middle']).optional(), clickCount: z.number().positive().optional(), delay: z.number().nonnegative().optional(), + timeout: z.number().positive().optional(), }); const typeSchema = baseCommandSchema.extend({ diff --git a/src/types.ts b/src/types.ts index d1a9f1a4..019a0ea0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,6 +45,7 @@ export interface ClickCommand extends BaseCommand { button?: 'left' | 'right' | 'middle'; clickCount?: number; delay?: number; + timeout?: number; } export interface TypeCommand extends BaseCommand {