Compare commits

..
4 Commits
Author SHA1 Message Date
leeguooooo cd47ec43d0 chore(release): 1.5.10 — warn on debug-port launch while relay is up (#32)
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
2026-06-16 14:32:29 +09:00
leeguooooo 2cd361817d fix(launch): warn when launching a debug-port Chrome while the relay is up (#32)
The connect-mode diagnostic (1.5.5) proved the 'Allow remote debugging?' modal
is NOT Chrome 149 UX (my earlier hypothesis) — it's chrome-use launching a fresh
debug-port Chrome on session=default while the ab-connect relay is up (32 logged
CONSENT-MODAL-RISK launches), almost always from a stray --launch/--no-auto-connect.
A launch now warns loudly when the relay is available, naming the modal and how
to avoid it (drop --launch/--new, don't pass --no-auto-connect), so the modal is
self-explained and the offending caller is fixable.
2026-06-16 14:32:28 +09:00
leeguooooo 42f47c49aa chore(release): 1.5.9 — strip zero-width title unicode (#33) + screenshot --clip/element (#34)
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
2026-06-16 14:26:41 +09:00
leeguooooo e29800df72 fix(tab-list): strip zero-width unicode from titles (#33); feat(screenshot): --clip pixel region + documented element capture (#34)
#33: some sites prepend runs of ZWJ/word-joiner/invisible-times/BOM to
document.title (badging/anti-scrape); left in, they polluted 'tab list', broke
text matching, and wrecked column alignment. sanitize_title() now strips
zero-width/bidi-format chars at every title ingestion point + get_title().

#34: 'screenshot <selector>' (element capture) already worked but was
undocumented; added 'screenshot --clip x,y,w,h' for an explicit pixel region
(CDP captureScreenshot clip), documented both in --help. Verified live.
2026-06-16 14:26:40 +09:00
9 changed files with 136 additions and 22 deletions
+1 -1
View File
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrome-use"
version = "1.5.8"
version = "1.5.10"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "chrome-use"
version = "1.5.8"
version = "1.5.10"
edition = "2021"
description = "Fast browser automation CLI for AI agents"
license = "Apache-2.0"
+48 -10
View File
@@ -907,17 +907,37 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
// selector: @ref or CSS selector
// path: file path (contains / or . or ends with known extension)
let mut full_page = false;
let positional: Vec<&str> = rest
.iter()
.filter(|arg| match **arg {
"--full" | "-f" => {
full_page = true;
false
let mut clip: Option<Value> = None;
let mut positional: Vec<&str> = Vec::new();
let mut i = 0;
while i < rest.len() {
match rest[i] {
"--full" | "-f" => full_page = true,
// `--clip x,y,w,h` captures a pixel region (issue #34).
"--clip" => {
let raw = rest.get(i + 1).ok_or_else(|| ParseError::MissingArguments {
context: "screenshot --clip".to_string(),
usage: "screenshot --clip <x,y,w,h> [path]",
})?;
let nums: Vec<f64> = raw
.split(',')
.filter_map(|n| n.trim().parse::<f64>().ok())
.collect();
if nums.len() != 4 {
return Err(ParseError::InvalidValue {
message: format!("--clip expects 'x,y,w,h' (4 numbers), got '{raw}'"),
usage: "screenshot --clip <x,y,w,h> [path]",
});
}
clip = Some(json!({
"x": nums[0], "y": nums[1], "width": nums[2], "height": nums[3]
}));
i += 1;
}
_ => true,
})
.copied()
.collect();
other => positional.push(other),
}
i += 1;
}
let (selector, path) = match (positional.first(), positional.get(1)) {
(Some(first), Some(second)) => {
// Two args: first is selector, second is path
@@ -948,6 +968,9 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
"path": path, "selector": selector,
"fullPage": full_page, "annotate": flags.annotate
});
if let Some(c) = clip {
cmd["clip"] = c;
}
if let Some(ref fmt) = flags.screenshot_format {
cmd["format"] = json!(fmt);
}
@@ -4327,6 +4350,21 @@ mod tests {
assert_eq!(cmd["fullPage"], true);
}
#[test]
fn test_screenshot_clip() {
// `--clip x,y,w,h` captures a pixel region (issue #34); the path still parses.
let cmd = parse_command(&args("screenshot --clip 10,20,200,40 out.png"), &default_flags())
.unwrap();
assert_eq!(cmd["action"], "screenshot");
assert_eq!(cmd["clip"]["x"], 10.0);
assert_eq!(cmd["clip"]["y"], 20.0);
assert_eq!(cmd["clip"]["width"], 200.0);
assert_eq!(cmd["clip"]["height"], 40.0);
assert_eq!(cmd["path"], "out.png");
// Bad clip is a clear error, not silent.
assert!(parse_command(&args("screenshot --clip 1,2,3"), &default_flags()).is_err());
}
#[test]
fn test_screenshot_with_ref() {
let cmd = parse_command(&args("screenshot @e1"), &default_flags()).unwrap();
+14
View File
@@ -1369,6 +1369,20 @@ fn main() {
&& flags.provider.is_none()
&& (flags.force_launch || !flags.auto_connect)
{
// Launching a debug-port Chrome pops Chrome's "Allow remote debugging?"
// consent modal (Chrome 136+). When the ab-connect relay is already up,
// this is almost always unintended — the relay drives the user's real
// Chrome with NO modal. Warn so the modal is self-explained and the
// caller (often a stray --launch / --no-auto-connect) is fixable (#32).
if !flags.json && connect::relay_url().is_some() {
eprintln!(
"{} launching a new Chrome with a debug port — this pops Chrome's \
\"Allow remote debugging?\" modal.\n The ab-connect relay is up; \
drop --launch/--new (and don't pass --no-auto-connect) to drive your \
real Chrome with no modal.",
color::warning_indicator()
);
}
let mut launch_cmd = json!({
"id": gen_id(),
"action": "launch",
+9
View File
@@ -3000,6 +3000,14 @@ async fn handle_screenshot(cmd: &Value, state: &mut DaemonState) -> Result<Value
.get("screenshotDir")
.and_then(|v| v.as_str())
.map(String::from),
clip: cmd.get("clip").and_then(|c| {
Some((
c.get("x")?.as_f64()?,
c.get("y")?.as_f64()?,
c.get("width")?.as_f64()?,
c.get("height")?.as_f64()?,
))
}),
};
if annotate {
@@ -7446,6 +7454,7 @@ async fn handle_diff_screenshot(cmd: &Value, state: &DaemonState) -> Result<Valu
quality: None,
annotate: false,
output_dir: None,
clip: None,
};
let result = screenshot::take_screenshot(
+44 -8
View File
@@ -121,7 +121,7 @@ fn normalize_url_for_match(url: &str) -> String {
fn update_page_target_info_in_pages(pages: &mut [PageInfo], target: &TargetInfo) -> bool {
if let Some(page) = pages.iter_mut().find(|p| p.target_id == target.target_id) {
page.url = target.url.clone();
page.title = target.title.clone();
page.title = sanitize_title(&target.title);
page.target_type = target.target_type.clone();
return true;
}
@@ -166,6 +166,29 @@ fn resolve_active_index(
active_page_index
}
/// Strip zero-width / invisible / bidi-format Unicode from a page title before
/// we store it. Some sites prepend runs of ZWJ / word-joiner / invisible-times /
/// BOM to `document.title` (badging, watermarking, anti-scrape); left in, they
/// pollute `tab list`, break text matching, and wreck column alignment (#33).
fn sanitize_title(s: &str) -> String {
s.chars()
.filter(|&c| {
!matches!(c as u32,
0x00AD // soft hyphen
| 0x200B..=0x200F // ZWSP, ZWNJ, ZWJ, LRM, RLM
| 0x2028 | 0x2029 // line / paragraph separators
| 0x202A..=0x202E // bidi embedding/override
| 0x2060..=0x2064 // word joiner, invisible operators
| 0x2066..=0x2069 // bidi isolates
| 0x180E // Mongolian vowel separator
| 0xFEFF // BOM / ZW no-break space
)
})
.collect::<String>()
.trim()
.to_string()
}
/// Best-effort MIME type from a filename extension, for the relay file-upload
/// fallback (the page-constructed `File` needs a sensible `type`). Covers the
/// common upload kinds; anything unknown falls back to a generic binary type.
@@ -773,7 +796,7 @@ impl BrowserManager {
target_id: target.target_id.clone(),
session_id: attach_result.session_id.clone(),
url: target.url.clone(),
title: target.title.clone(),
title: sanitize_title(&target.title),
target_type: target.target_type.clone(),
});
}
@@ -999,7 +1022,7 @@ impl BrowserManager {
self.active_page_index = self.resolved_active_index();
if let Some(page) = self.pages.get_mut(self.active_page_index) {
page.url = page_url.clone();
page.title = title.clone();
page.title = sanitize_title(&title);
}
self.pin_active_target();
@@ -1061,7 +1084,7 @@ impl BrowserManager {
pub async fn get_title(&self) -> Result<String, String> {
let result = self.evaluate_simple("document.title").await?;
Ok(result.as_str().unwrap_or("").to_string())
Ok(sanitize_title(result.as_str().unwrap_or("")))
}
pub async fn get_content(&self) -> Result<String, String> {
@@ -1404,7 +1427,7 @@ impl BrowserManager {
target_id: target.target_id.clone(),
session_id: attach.session_id.clone(),
url: target.url.clone(),
title: target.title.clone(),
title: sanitize_title(&target.title),
target_type: target.target_type.clone(),
};
self.add_background_page(page.clone());
@@ -1465,7 +1488,7 @@ impl BrowserManager {
target_id: target.target_id.clone(),
session_id: attach_result.session_id.clone(),
url: target.url.clone(),
title: target.title.clone(),
title: sanitize_title(&target.title),
target_type: target.target_type.clone(),
});
let _ = self.enable_domains(&attach_result.session_id).await;
@@ -1508,7 +1531,7 @@ impl BrowserManager {
}
}
if let Some(t) = ti.get("title").and_then(|v| v.as_str()) {
page.title = t.to_string();
page.title = sanitize_title(t);
}
}
}
@@ -1696,7 +1719,7 @@ impl BrowserManager {
if let Some(page) = self.pages.get_mut(index) {
page.url = url.clone();
page.title = title.clone();
page.title = sanitize_title(&title);
}
let page = &self.pages[index];
@@ -2767,6 +2790,19 @@ mod tests {
assert!(!active_index_is_owned(&[], None, 0, &created));
}
#[test]
fn test_sanitize_title() {
// The exact pollution from #33: ZWJ / word-joiner / invisible-times / BOM
// prepended to "GitHub".
let dirty = "\u{200d}\u{2061}\u{200d}\u{2063}\u{200b}\u{2062}\u{feff}GitHub";
assert_eq!(sanitize_title(dirty), "GitHub");
// Clean titles (incl. CJK + normal punctuation) pass through untouched.
assert_eq!(sanitize_title("購入手続きへ - メルカリ"), "購入手続きへ - メルカリ");
assert_eq!(sanitize_title(" Hello World "), "Hello World");
// Emoji and real content survive; only the invisibles are dropped.
assert_eq!(sanitize_title("✓ Done\u{200b}"), "✓ Done");
}
#[test]
fn test_mime_for_path() {
assert_eq!(mime_for_path("a.png"), "image/png");
+14 -1
View File
@@ -60,6 +60,9 @@ pub struct ScreenshotOptions {
pub quality: Option<i32>,
pub annotate: bool,
pub output_dir: Option<String>,
/// Explicit pixel region (x, y, width, height) — `--clip` (issue #34). Takes
/// precedence over selector/full_page.
pub clip: Option<(f64, f64, f64, f64)>,
}
impl Default for ScreenshotOptions {
@@ -72,6 +75,7 @@ impl Default for ScreenshotOptions {
quality: None,
annotate: false,
output_dir: None,
clip: None,
}
}
}
@@ -187,7 +191,16 @@ async fn capture_screenshot_base64(
capture_beyond_viewport: if options.full_page { Some(true) } else { None },
};
if options.full_page {
if let Some((x, y, width, height)) = options.clip {
// Explicit pixel region wins over selector/full_page (issue #34).
params.clip = Some(Viewport {
x,
y,
width,
height,
scale: 1.0,
});
} else if options.full_page {
let metrics: Value = client
.send_command_no_params("Page.getLayoutMetrics", Some(session_id))
.await?;
+4
View File
@@ -1832,6 +1832,8 @@ Pass --hide-scrollbars false when launching to keep native scrollbars visible.
Options:
--full, -f Capture full page (not just viewport)
[selector] Capture just an element (CSS or @ref), e.g. `screenshot ".header" h.png`
--clip <x,y,w,h> Capture a pixel region, e.g. `screenshot --clip 0,0,200,40 corner.png`
--annotate Overlay numbered labels on interactive elements.
Each label [N] corresponds to ref @eN from snapshot.
Prints a legend mapping labels to element roles/names.
@@ -1852,6 +1854,8 @@ Examples:
chrome-use screenshot
chrome-use screenshot ./screenshot.png
chrome-use screenshot --full ./full-page.png
chrome-use screenshot ".header .indicator" corner.png # just one element
chrome-use screenshot --clip 1600,0,200,40 corner.png # a pixel region
chrome-use screenshot --annotate # Labeled screenshot + legend
chrome-use screenshot --annotate ./page.png # Save annotated screenshot
chrome-use screenshot --annotate --json # JSON output with annotations
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "chrome-use",
"version": "1.5.8",
"version": "1.5.10",
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
"type": "module",
"packageManager": "pnpm@11.1.3",