diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 374cf1e..73c9332 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -403,9 +403,18 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result { + // `type --focused ` types into whatever element currently has + // focus (no selector) — for custom widgets that move focus to a hidden + // input after you open them. + if rest.first() == Some(&"--focused") { + return Ok(json!({ + "id": id, "action": "type", "focused": true, + "text": rest[1..].join(" "), + })); + } let sel = rest.first().ok_or_else(|| ParseError::MissingArguments { context: "type".to_string(), - usage: "type ", + usage: "type (or: type --focused )", })?; Ok(json!({ "id": id, "action": "type", "selector": sel, "text": rest[1..].join(" ") })) } diff --git a/cli/src/connection.rs b/cli/src/connection.rs index e394113..07e5805 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -832,7 +832,19 @@ pub fn send_command(mut cmd: Value, session: &str) -> Result { obj.insert("_clickMode".to_string(), Value::String(m)); } if let Ok(h) = std::env::var("AGENT_BROWSER_HUMANIZE") { - obj.insert("_humanize".to_string(), Value::String(h)); + // Only forward a recognized level; warn once (like the --humanize flag + // does) when the env var is set to garbage, instead of silently + // ignoring it. + if crate::native::humanize::HumanizeLevel::parse(&h).is_some() { + obj.insert("_humanize".to_string(), Value::String(h)); + } else { + static WARNED: std::sync::Once = std::sync::Once::new(); + WARNED.call_once(|| { + eprintln!( + "warning: AGENT_BROWSER_HUMANIZE must be off|fast|human, got {h:?} (ignored)" + ); + }); + } } } diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index 636a599..254b17e 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -3031,6 +3031,22 @@ async fn handle_fill(cmd: &Value, state: &mut DaemonState) -> Result Result { let mgr = state.browser.as_ref().ok_or("Browser not launched")?; let session_id = mgr.active_session_id()?.to_string(); + + // `type --focused `: type into the currently-focused element without a + // selector (custom widgets that move focus to a hidden input on open). + if cmd + .get("focused") + .and_then(|v| v.as_bool()) + .unwrap_or(false) + { + let text = cmd + .get("text") + .and_then(|v| v.as_str()) + .ok_or("Missing 'text' parameter")?; + interaction::type_text_into_active_context(&mgr.client, &session_id, text, None).await?; + return Ok(json!({ "typed": text, "focused": true })); + } + let selector = cmd .get("selector") .and_then(|v| v.as_str()) diff --git a/cli/src/output.rs b/cli/src/output.rs index 41cc230..ac6d1cf 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -358,6 +358,16 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou } // Eval result if let Some(result) = data.get("result") { + // Surface which page the eval actually ran on — to stderr, so it + // never corrupts the parsed value on stdout. Lets an agent catch tab + // drift (commands landing on the wrong tab) before trusting a result, + // e.g. a logged-in `fetch` that hit the wrong origin. (In + // content-boundaries mode the origin is already in the banner.) + if !opts.content_boundaries { + if let Some(o) = origin.filter(|o| !o.is_empty()) { + eprintln!("eval @ {o}"); + } + } let formatted = serde_json::to_string_pretty(result).unwrap_or_default(); print_with_boundaries(&formatted, origin, opts); return;