feat: eval --file, tab-list URL truncation, skill doc fixes (issue #2/#3 + Hermes)
- `eval --file <path>`: read JS from a file, sent verbatim — avoids shell-mangling of non-ASCII identifiers/strings (Chinese), quotes, and large scripts (issue #3). - `tab list`: truncate multi-KB URLs (JWT/OTP login links) middle-out with a char count so the list stays readable (issue #3). - skill: fix the snapshot example to match real output (`- role "name" [ref=eN]`, not `@e1 [role]`); document that eval runs in the page MAIN world with persistent state (top-level `const` collides — use IIFE / window / unique names) and to prefer --file/--stdin/-b for non-ASCII or big JS.
This commit is contained in:
+20
-6
@@ -841,17 +841,31 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
|||||||
|
|
||||||
// === Eval ===
|
// === Eval ===
|
||||||
"eval" => {
|
"eval" => {
|
||||||
// Check for flags: -b/--base64 or --stdin
|
// Check for flags: -b/--base64, --stdin, or --file <path>
|
||||||
let (is_base64, is_stdin, script_parts): (bool, bool, &[&str]) =
|
let (is_base64, is_stdin, is_file, script_parts): (bool, bool, bool, &[&str]) =
|
||||||
if rest.first() == Some(&"-b") || rest.first() == Some(&"--base64") {
|
if rest.first() == Some(&"-b") || rest.first() == Some(&"--base64") {
|
||||||
(true, false, &rest[1..])
|
(true, false, false, &rest[1..])
|
||||||
} else if rest.first() == Some(&"--stdin") {
|
} else if rest.first() == Some(&"--stdin") {
|
||||||
(false, true, &rest[1..])
|
(false, true, false, &rest[1..])
|
||||||
|
} else if rest.first() == Some(&"--file") {
|
||||||
|
(false, false, true, &rest[1..])
|
||||||
} else {
|
} else {
|
||||||
(false, false, rest.as_slice())
|
(false, false, false, rest.as_slice())
|
||||||
};
|
};
|
||||||
|
|
||||||
let script = if is_stdin {
|
let script = if is_file {
|
||||||
|
// Read the script from a file. Avoids shell-mangling of inline JS
|
||||||
|
// (non-ASCII identifiers/strings, quotes, large scripts) — the file
|
||||||
|
// is read as UTF-8 and sent verbatim.
|
||||||
|
let path = script_parts.first().ok_or(ParseError::InvalidValue {
|
||||||
|
message: "eval --file requires a path".to_string(),
|
||||||
|
usage: "eval --file <path>",
|
||||||
|
})?;
|
||||||
|
std::fs::read_to_string(path).map_err(|e| ParseError::InvalidValue {
|
||||||
|
message: format!("eval --file: cannot read {path}: {e}"),
|
||||||
|
usage: "eval --file <path>",
|
||||||
|
})?
|
||||||
|
} else if is_stdin {
|
||||||
// Read script from stdin
|
// Read script from stdin
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
let lines: Vec<String> = stdin
|
let lines: Vec<String> = stdin
|
||||||
|
|||||||
@@ -130,6 +130,20 @@ fn format_stream_status_text(action: Option<&str>, data: &serde_json::Value) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shorten an over-long string by keeping its head and tail and eliding the
|
||||||
|
/// middle, with a char count. Used so multi-KB URLs (JWT/OTP login links) don't
|
||||||
|
/// flood `tab list`.
|
||||||
|
fn truncate_middle(s: &str, max: usize) -> String {
|
||||||
|
let n = s.chars().count();
|
||||||
|
if n <= max {
|
||||||
|
return s.to_string();
|
||||||
|
}
|
||||||
|
let keep = max.saturating_sub(1) / 2;
|
||||||
|
let head: String = s.chars().take(keep).collect();
|
||||||
|
let tail: String = s.chars().skip(n - keep).collect();
|
||||||
|
format!("{head}…{tail} [{n} chars]")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &OutputOptions) {
|
pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &OutputOptions) {
|
||||||
if opts.json {
|
if opts.json {
|
||||||
if opts.content_boundaries {
|
if opts.content_boundaries {
|
||||||
@@ -422,6 +436,9 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("Untitled");
|
.unwrap_or("Untitled");
|
||||||
let url = tab.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
let url = tab.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
// Truncate very long URLs (e.g. multi-KB JWT/OTP login links) so
|
||||||
|
// the list stays readable instead of flooding the terminal.
|
||||||
|
let url = truncate_middle(url, 120);
|
||||||
let active = tab.get("active").and_then(|v| v.as_bool()).unwrap_or(false);
|
let active = tab.get("active").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
let marker = if active {
|
let marker = if active {
|
||||||
color::cyan("→")
|
color::cyan("→")
|
||||||
|
|||||||
@@ -182,14 +182,17 @@ Snapshot output looks like:
|
|||||||
Page: Example - Log in
|
Page: Example - Log in
|
||||||
URL: https://example.com/login
|
URL: https://example.com/login
|
||||||
|
|
||||||
@e1 [heading] "Log in"
|
- heading "Log in" [level=1, ref=e1]
|
||||||
@e2 [form]
|
- textbox "Email" [ref=e2]
|
||||||
@e3 [input type="email"] placeholder="Email"
|
- textbox "Password" [ref=e3]
|
||||||
@e4 [input type="password"] placeholder="Password"
|
- button "Continue" [ref=e4]
|
||||||
@e5 [button type="submit"] "Continue"
|
- link "Forgot password?" [ref=e5]
|
||||||
@e6 [link] "Forgot password?"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Each line is `- <role> "<accessible name>" [<attrs>, ref=eN]`, indented by nesting
|
||||||
|
depth. You pass the ref to commands as `@eN` (e.g. `click @e4`). Refs are
|
||||||
|
assigned fresh on every snapshot.
|
||||||
|
|
||||||
For unstructured reading (no refs needed):
|
For unstructured reading (no refs needed):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -386,9 +389,16 @@ Array.from(rows).map(r => ({
|
|||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
Prefer `eval --stdin` (heredoc) or `eval -b <base64>` for any JS with
|
Prefer `eval --stdin` (heredoc), `eval --file <path>`, or `eval -b <base64>`
|
||||||
quotes or special characters. Inline `agent-browser eval "..."` works
|
for any JS with quotes, **non-ASCII identifiers/strings (e.g. Chinese)**, or
|
||||||
only for simple expressions.
|
large scripts — inline `agent-browser eval "..."` is shell-mangled and works
|
||||||
|
only for simple ASCII expressions.
|
||||||
|
|
||||||
|
**`eval` runs in the page's MAIN world and state persists across calls**, so a
|
||||||
|
top-level `const x`/`let x`/`var x` in one call collides with the next
|
||||||
|
(`SyntaxError: Identifier 'x' has already been declared`). Either use unique
|
||||||
|
names, assign to `window.x`, or wrap the body in an IIFE
|
||||||
|
(`(() => { const x = …; return x; })()`).
|
||||||
|
|
||||||
### Screenshot
|
### Screenshot
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user