From 123510db2bdd1d1285ea68de2dd421670295058e Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Thu, 11 Jun 2026 22:22:07 +0900 Subject: [PATCH] feat: eval --file, tab-list URL truncation, skill doc fixes (issue #2/#3 + Hermes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `eval --file `: 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. --- cli/src/commands.rs | 26 ++++++++++++++++++++------ cli/src/output.rs | 17 +++++++++++++++++ skill-data/core/SKILL.md | 28 +++++++++++++++++++--------- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 40907b2..374cf1e 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -841,17 +841,31 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result { - // 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 + 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 ", + })?; + std::fs::read_to_string(path).map_err(|e| ParseError::InvalidValue { + message: format!("eval --file: cannot read {path}: {e}"), + usage: "eval --file ", + })? + } else if is_stdin { // Read script from stdin let stdin = io::stdin(); let lines: Vec = stdin diff --git a/cli/src/output.rs b/cli/src/output.rs index bb88fc1..41cc230 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -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("→") diff --git a/skill-data/core/SKILL.md b/skill-data/core/SKILL.md index 9b1943c..46e45b4 100644 --- a/skill-data/core/SKILL.md +++ b/skill-data/core/SKILL.md @@ -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 `- "" [, 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 ` for any JS with -quotes or special characters. Inline `agent-browser eval "..."` works -only for simple expressions. +Prefer `eval --stdin` (heredoc), `eval --file `, or `eval -b ` +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