feat: eval prints its origin URL, type --focused, AGENT_BROWSER_HUMANIZE bogus warn
- eval now prints `eval @ <url>` to stderr (stdout stays the raw value) so an agent can catch tab drift — e.g. a logged-in fetch that hit the wrong origin — before trusting the result. Mitigates the issue #2/#3 P0 safety concern. (eval already returned the origin; the default output just never surfaced it.) - `type --focused <text>`: type into the currently-focused element with no selector, for custom widgets that move focus to a hidden input (issue #2 P3). - AGENT_BROWSER_HUMANIZE set to an unrecognized value now warns once (like the --humanize flag) instead of being silently ignored (Hermes #3).
This commit is contained in:
+10
-1
@@ -403,9 +403,18 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
|||||||
Ok(json!({ "id": id, "action": "fill", "selector": sel, "value": rest[1..].join(" ") }))
|
Ok(json!({ "id": id, "action": "fill", "selector": sel, "value": rest[1..].join(" ") }))
|
||||||
}
|
}
|
||||||
"type" => {
|
"type" => {
|
||||||
|
// `type --focused <text>` 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 {
|
let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {
|
||||||
context: "type".to_string(),
|
context: "type".to_string(),
|
||||||
usage: "type <selector> <text>",
|
usage: "type <selector> <text> (or: type --focused <text>)",
|
||||||
})?;
|
})?;
|
||||||
Ok(json!({ "id": id, "action": "type", "selector": sel, "text": rest[1..].join(" ") }))
|
Ok(json!({ "id": id, "action": "type", "selector": sel, "text": rest[1..].join(" ") }))
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -832,7 +832,19 @@ pub fn send_command(mut cmd: Value, session: &str) -> Result<Response, String> {
|
|||||||
obj.insert("_clickMode".to_string(), Value::String(m));
|
obj.insert("_clickMode".to_string(), Value::String(m));
|
||||||
}
|
}
|
||||||
if let Ok(h) = std::env::var("AGENT_BROWSER_HUMANIZE") {
|
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)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3031,6 +3031,22 @@ async fn handle_fill(cmd: &Value, state: &mut DaemonState) -> Result<Value, Stri
|
|||||||
async fn handle_type(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
async fn handle_type(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||||
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||||
let session_id = mgr.active_session_id()?.to_string();
|
let session_id = mgr.active_session_id()?.to_string();
|
||||||
|
|
||||||
|
// `type --focused <text>`: 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
|
let selector = cmd
|
||||||
.get("selector")
|
.get("selector")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
|
|||||||
@@ -358,6 +358,16 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
|||||||
}
|
}
|
||||||
// Eval result
|
// Eval result
|
||||||
if let Some(result) = data.get("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();
|
let formatted = serde_json::to_string_pretty(result).unwrap_or_default();
|
||||||
print_with_boundaries(&formatted, origin, opts);
|
print_with_boundaries(&formatted, origin, opts);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user