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" => {
|
||||
// Check for flags: -b/--base64 or --stdin
|
||||
let (is_base64, is_stdin, script_parts): (bool, bool, &[&str]) =
|
||||
// Check for flags: -b/--base64, --stdin, or --file <path>
|
||||
let (is_base64, is_stdin, is_file, script_parts): (bool, bool, bool, &[&str]) =
|
||||
if rest.first() == Some(&"-b") || rest.first() == Some(&"--base64") {
|
||||
(true, false, &rest[1..])
|
||||
(true, false, false, &rest[1..])
|
||||
} 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 {
|
||||
(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
|
||||
let stdin = io::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) {
|
||||
if opts.json {
|
||||
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())
|
||||
.unwrap_or("Untitled");
|
||||
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 marker = if active {
|
||||
color::cyan("→")
|
||||
|
||||
@@ -182,14 +182,17 @@ Snapshot output looks like:
|
||||
Page: Example - Log in
|
||||
URL: https://example.com/login
|
||||
|
||||
@e1 [heading] "Log in"
|
||||
@e2 [form]
|
||||
@e3 [input type="email"] placeholder="Email"
|
||||
@e4 [input type="password"] placeholder="Password"
|
||||
@e5 [button type="submit"] "Continue"
|
||||
@e6 [link] "Forgot password?"
|
||||
- heading "Log in" [level=1, ref=e1]
|
||||
- textbox "Email" [ref=e2]
|
||||
- textbox "Password" [ref=e3]
|
||||
- button "Continue" [ref=e4]
|
||||
- link "Forgot password?" [ref=e5]
|
||||
```
|
||||
|
||||
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):
|
||||
|
||||
```bash
|
||||
@@ -386,9 +389,16 @@ Array.from(rows).map(r => ({
|
||||
EOF
|
||||
```
|
||||
|
||||
Prefer `eval --stdin` (heredoc) or `eval -b <base64>` for any JS with
|
||||
quotes or special characters. Inline `agent-browser eval "..."` works
|
||||
only for simple expressions.
|
||||
Prefer `eval --stdin` (heredoc), `eval --file <path>`, or `eval -b <base64>`
|
||||
for any JS with quotes, **non-ASCII identifiers/strings (e.g. Chinese)**, or
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user