Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8f484eded | ||
|
|
1eb40eabd5 | ||
|
|
601404ba72 | ||
|
|
fd10766762 | ||
|
|
ba9b167ede | ||
|
|
c077593e99 |
Generated
+1
-1
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrome-use"
|
name = "chrome-use"
|
||||||
version = "1.5.24"
|
version = "1.5.27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chrome-use"
|
name = "chrome-use"
|
||||||
version = "1.5.24"
|
version = "1.5.27"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Fast browser automation CLI for AI agents"
|
description = "Fast browser automation CLI for AI agents"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
+7
-2
@@ -84,6 +84,7 @@ const KNOWN_COMMANDS: &[&str] = &[
|
|||||||
"canvas",
|
"canvas",
|
||||||
"viewport",
|
"viewport",
|
||||||
"resize",
|
"resize",
|
||||||
|
"keep",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Levenshtein distance, capped — small inputs only (command names).
|
/// Levenshtein distance, capped — small inputs only (command names).
|
||||||
@@ -1375,6 +1376,10 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
|||||||
// `url` read so the response confirms which tab got adopted.
|
// `url` read so the response confirms which tab got adopted.
|
||||||
"adopt" => Ok(json!({ "id": id, "action": "url" })),
|
"adopt" => Ok(json!({ "id": id, "action": "url" })),
|
||||||
|
|
||||||
|
// `keep`: leave the active tab for the user — exempt it from the daemon's
|
||||||
|
// auto-close/idle cleanup and remove it from the session's tab group.
|
||||||
|
"keep" => Ok(json!({ "id": id, "action": "keep" })),
|
||||||
|
|
||||||
// === Stealth self-check ===
|
// === Stealth self-check ===
|
||||||
"stealth" => {
|
"stealth" => {
|
||||||
// `stealth [status]` — local stealth self-check: mode, live probes
|
// `stealth [status]` — local stealth self-check: mode, live probes
|
||||||
@@ -3180,7 +3185,7 @@ fn parse_viewport(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
|
|
||||||
let (w, h, scale_tok): (i32, i32, Option<&str>) = match positionals.first() {
|
let (w, h, scale_tok): (i32, i32, Option<&str>) = match positionals.first() {
|
||||||
Some(first) if first.contains('x') || first.contains('X') => {
|
Some(first) if first.contains('x') || first.contains('X') => {
|
||||||
let mut parts = first.split(|c| c == 'x' || c == 'X');
|
let mut parts = first.split(['x', 'X']);
|
||||||
let w = parts.next().and_then(|s| s.parse::<i32>().ok());
|
let w = parts.next().and_then(|s| s.parse::<i32>().ok());
|
||||||
let h = parts.next().and_then(|s| s.parse::<i32>().ok());
|
let h = parts.next().and_then(|s| s.parse::<i32>().ok());
|
||||||
match (w, h) {
|
match (w, h) {
|
||||||
@@ -3238,7 +3243,7 @@ fn parse_viewport(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
if let Some(s) = scale {
|
if let Some(s) = scale {
|
||||||
cmd["deviceScaleFactor"] = json!(s);
|
cmd["deviceScaleFactor"] = json!(s);
|
||||||
}
|
}
|
||||||
if rest.iter().any(|a| *a == "--mobile") {
|
if rest.contains(&"--mobile") {
|
||||||
cmd["mobile"] = json!(true);
|
cmd["mobile"] = json!(true);
|
||||||
}
|
}
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
|
|||||||
@@ -305,6 +305,49 @@ fn run_session(args: &[String], session: &str, json_mode: bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Stop a specific session daemon (issue #48). Graceful: kill_stale_daemon
|
||||||
|
// sends SIGTERM first, so the daemon's shutdown handler runs `close()` and
|
||||||
|
// tidies the tabs IT created (its tab group) before exiting.
|
||||||
|
Some("stop") => {
|
||||||
|
let target = args.get(2).map(|s| s.as_str()).unwrap_or(session);
|
||||||
|
connection::kill_stale_daemon(target);
|
||||||
|
if json_mode {
|
||||||
|
print_json_value(json!({ "success": true, "data": { "stopped": target } }));
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"{} stopped session daemon: {}",
|
||||||
|
color::success_indicator(),
|
||||||
|
target
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reclaim ALL session daemons now (issue #48) — for clearing the pile of
|
||||||
|
// idle daemons left after a round of automation/debugging without waiting
|
||||||
|
// for the idle timeout. Each is stopped gracefully (closes its own tabs);
|
||||||
|
// they respawn clean on next use. The `__nm-host` relay is not a tracked
|
||||||
|
// session daemon, so the extension/live-Chrome connection survives.
|
||||||
|
Some("prune") => {
|
||||||
|
let sessions: Vec<String> = walk_daemons()
|
||||||
|
.sessions
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.name)
|
||||||
|
.collect();
|
||||||
|
for s in &sessions {
|
||||||
|
connection::kill_stale_daemon(s);
|
||||||
|
}
|
||||||
|
if json_mode {
|
||||||
|
print_json_value(json!({ "success": true, "data": { "pruned": sessions } }));
|
||||||
|
} else if sessions.is_empty() {
|
||||||
|
println!("No session daemons to prune");
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"{} pruned {} session daemon(s): {}",
|
||||||
|
color::success_indicator(),
|
||||||
|
sessions.len(),
|
||||||
|
sessions.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
None | Some(_) => {
|
None | Some(_) => {
|
||||||
// Just show current session
|
// Just show current session
|
||||||
if json_mode {
|
if json_mode {
|
||||||
|
|||||||
@@ -1317,6 +1317,7 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
|
|||||||
"evaluate" => handle_evaluate(cmd, state).await,
|
"evaluate" => handle_evaluate(cmd, state).await,
|
||||||
"site" => handle_site(cmd, state).await,
|
"site" => handle_site(cmd, state).await,
|
||||||
"close" => handle_close(state).await,
|
"close" => handle_close(state).await,
|
||||||
|
"keep" => handle_keep(state).await,
|
||||||
"stealth_status" => handle_stealth_status(state).await,
|
"stealth_status" => handle_stealth_status(state).await,
|
||||||
"snapshot" => handle_snapshot(cmd, state).await,
|
"snapshot" => handle_snapshot(cmd, state).await,
|
||||||
"screenshot" => handle_screenshot(cmd, state).await,
|
"screenshot" => handle_screenshot(cmd, state).await,
|
||||||
@@ -2855,6 +2856,34 @@ async fn handle_stealth_status(state: &DaemonState) -> Result<Value, String> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `keep` — leave the ACTIVE tab for the user: stop owning it (so the daemon's
|
||||||
|
/// `close()`/idle-shutdown won't close it) and best-effort remove it from this
|
||||||
|
/// session's tab group so it looks like a normal user tab. The "leave for the
|
||||||
|
/// user" half of the auto-close-on-idle cleanup: scratch tabs get closed, tabs
|
||||||
|
/// the agent explicitly `keep`s stay. (Adopted user tabs are never owned, so
|
||||||
|
/// they're already safe.)
|
||||||
|
async fn handle_keep(state: &mut DaemonState) -> Result<Value, String> {
|
||||||
|
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
|
||||||
|
let target_id = mgr.active_target_id()?.to_string();
|
||||||
|
let session_id = mgr.active_session_id()?.to_string();
|
||||||
|
let was_owned = mgr.unown_target(&target_id);
|
||||||
|
// Best-effort: ask the extension to ungroup the tab (relay only; no-ops on a
|
||||||
|
// launched browser or an older extension that doesn't know ABExt.ungroupTab).
|
||||||
|
let _ = mgr
|
||||||
|
.client
|
||||||
|
.send_command_typed::<_, Value>(
|
||||||
|
"ABExt.ungroupTab",
|
||||||
|
&json!({ "sessionId": session_id, "targetId": target_id }),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
Ok(json!({
|
||||||
|
"kept": target_id,
|
||||||
|
"wasOwned": was_owned,
|
||||||
|
"note": "tab left for the user — exempt from auto-close, removed from the session tab group",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_close(state: &mut DaemonState) -> Result<Value, String> {
|
async fn handle_close(state: &mut DaemonState) -> Result<Value, String> {
|
||||||
if let Some(ref mgr) = state.browser {
|
if let Some(ref mgr) = state.browser {
|
||||||
if let Some(ref session_name) = state.session_name {
|
if let Some(ref session_name) = state.session_name {
|
||||||
|
|||||||
@@ -1546,6 +1546,13 @@ impl BrowserManager {
|
|||||||
.ok_or_else(|| "No active page".to_string())
|
.ok_or_else(|| "No active page".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stop owning a tab — drop it from `created_targets` so it survives `close()`
|
||||||
|
/// and idle-shutdown (the agent is leaving it for the user). Returns true if it
|
||||||
|
/// was owned. Used by `keep`.
|
||||||
|
pub fn unown_target(&mut self, target_id: &str) -> bool {
|
||||||
|
self.created_targets.remove(target_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if this manager was connected via CDP (as opposed to local launch).
|
/// Returns true if this manager was connected via CDP (as opposed to local launch).
|
||||||
pub fn is_cdp_connection(&self) -> bool {
|
pub fn is_cdp_connection(&self) -> bool {
|
||||||
self.browser_process.is_none()
|
self.browser_process.is_none()
|
||||||
@@ -2063,6 +2070,46 @@ impl BrowserManager {
|
|||||||
self.active_page_index = index;
|
self.active_page_index = index;
|
||||||
self.pin_active_target();
|
self.pin_active_target();
|
||||||
|
|
||||||
|
// Close the daemon's leftover initial `about:blank` scratch tab once this
|
||||||
|
// real tab exists, so the session's tab group isn't left showing a stray
|
||||||
|
// blank page beside the work tab (every group otherwise carried one). Only
|
||||||
|
// on the RELAY — there the about:blank is a tab WE created as scratch; on a
|
||||||
|
// launched browser the initial about:blank is the browser's own first tab,
|
||||||
|
// which we must not close. Only when opening a real url, OWNED, still-blank.
|
||||||
|
if target_url != "about:blank" && self.agent_group().is_some() {
|
||||||
|
if let Some(new_tid) = self.pages.get(index).map(|p| p.target_id.clone()) {
|
||||||
|
let blanks: Vec<String> = self
|
||||||
|
.pages
|
||||||
|
.iter()
|
||||||
|
.filter(|p| {
|
||||||
|
p.target_id != new_tid
|
||||||
|
&& self.created_targets.contains(&p.target_id)
|
||||||
|
&& (p.url == "about:blank" || p.url.is_empty())
|
||||||
|
})
|
||||||
|
.map(|p| p.target_id.clone())
|
||||||
|
.collect();
|
||||||
|
for tid in blanks {
|
||||||
|
let _ = self
|
||||||
|
.client
|
||||||
|
.send_command_typed::<_, Value>(
|
||||||
|
"Target.closeTarget",
|
||||||
|
&CloseTargetParams {
|
||||||
|
target_id: tid.clone(),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
self.created_targets.remove(&tid);
|
||||||
|
self.remove_page_by_target_id(&tid);
|
||||||
|
}
|
||||||
|
// Removing earlier pages shifts indices — re-pin the new tab.
|
||||||
|
if let Some(i) = self.pages.iter().position(|p| p.target_id == new_tid) {
|
||||||
|
self.active_page_index = i;
|
||||||
|
self.pin_active_target();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"tabId": format_tab_id(tab_id),
|
"tabId": format_tab_id(tab_id),
|
||||||
"label": label,
|
"label": label,
|
||||||
|
|||||||
@@ -130,12 +130,21 @@ pub async fn run_daemon(session: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-shutdown the daemon after this many ms of inactivity (no commands received).
|
// Auto-shutdown the daemon after this many ms of inactivity (no commands
|
||||||
// Disabled when unset or 0.
|
// received). On shutdown the daemon closes the tabs IT created (its per-session
|
||||||
let idle_timeout_ms = env::var("AGENT_BROWSER_IDLE_TIMEOUT_MS")
|
// tab group), so an agent that finishes a task and just stops — without ever
|
||||||
.ok()
|
// calling `close` — no longer leaves a pile of scratch tabs and a lingering
|
||||||
.and_then(|s| s.parse::<u64>().ok())
|
// tab group in the user's Chrome. The timer resets on every command, so active
|
||||||
.filter(|&ms| ms > 0);
|
// sessions are never interrupted; only genuinely-idle ones clean up.
|
||||||
|
//
|
||||||
|
// Defaults to 10 minutes. Set AGENT_BROWSER_IDLE_TIMEOUT_MS to override, or 0
|
||||||
|
// to disable (keep the daemon alive forever — the old behaviour). Adopted
|
||||||
|
// tabs (the user's own, via `adopt`) are never closed: only `created_targets`.
|
||||||
|
const DEFAULT_IDLE_TIMEOUT_MS: u64 = 600_000;
|
||||||
|
let idle_timeout_ms = match env::var("AGENT_BROWSER_IDLE_TIMEOUT_MS") {
|
||||||
|
Ok(s) => s.trim().parse::<u64>().ok().filter(|&ms| ms > 0),
|
||||||
|
Err(_) => Some(DEFAULT_IDLE_TIMEOUT_MS),
|
||||||
|
};
|
||||||
|
|
||||||
let result = run_socket_server(
|
let result = run_socket_server(
|
||||||
&socket_path,
|
&socket_path,
|
||||||
|
|||||||
@@ -3326,6 +3326,9 @@ Core Commands:
|
|||||||
snapshot Accessibility tree with refs (for AI)
|
snapshot Accessibility tree with refs (for AI)
|
||||||
eval <js> Run JavaScript
|
eval <js> Run JavaScript
|
||||||
connect <port|url> Connect to browser via CDP
|
connect <port|url> Connect to browser via CDP
|
||||||
|
keep Leave the active tab for the user — exempt it from
|
||||||
|
auto-close/idle cleanup + remove it from the session
|
||||||
|
tab group (so scratch tabs get cleaned, this one stays)
|
||||||
close [--all] Close browser (--all closes every session)
|
close [--all] Close browser (--all closes every session)
|
||||||
|
|
||||||
Navigation:
|
Navigation:
|
||||||
@@ -3452,11 +3455,21 @@ Confirmation:
|
|||||||
Sessions:
|
Sessions:
|
||||||
session Show current session name
|
session Show current session name
|
||||||
session list List active sessions
|
session list List active sessions
|
||||||
|
session stop [name] Stop one session daemon (default: current) — graceful,
|
||||||
|
closes the tabs it created
|
||||||
|
session prune Stop ALL session daemons now (closes their tabs; they
|
||||||
|
respawn clean on next use). For clearing idle daemons.
|
||||||
sessions List running session daemons (alias of daemon status)
|
sessions List running session daemons (alias of daemon status)
|
||||||
daemon status List running session daemons (+ relay state)
|
daemon status List running session daemons (+ relay state)
|
||||||
daemon restart Kill all session daemons; keeps the extension relay
|
daemon restart Kill all session daemons; keeps the extension relay
|
||||||
up. Clears stale/cross-leaked state after an upgrade.
|
up. Clears stale/cross-leaked state after an upgrade.
|
||||||
|
|
||||||
|
Lifecycle: each --session <name> spawns a background daemon that drives that
|
||||||
|
session's tabs. A daemon auto-shuts-down after 10 min idle (no commands) —
|
||||||
|
AGENT_BROWSER_IDLE_TIMEOUT_MS overrides, 0 disables — and on shutdown closes
|
||||||
|
the scratch tabs IT created (its tab group). Use `keep` to leave a tab for the
|
||||||
|
user (exempt from auto-close), `session stop/prune` to reclaim now.
|
||||||
|
|
||||||
Chat (AI):
|
Chat (AI):
|
||||||
chat <message> Send a natural language instruction (single-shot)
|
chat <message> Send a natural language instruction (single-shot)
|
||||||
chat Start interactive chat (REPL mode when stdin is a TTY)
|
chat Start interactive chat (REPL mode when stdin is a TTY)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -288,6 +288,19 @@ async function handleForwardCdpCommand(msg) {
|
|||||||
const params = msg?.params?.params || undefined
|
const params = msg?.params?.params || undefined
|
||||||
const sessionId = typeof msg?.params?.sessionId === 'string' ? msg.params.sessionId : undefined
|
const sessionId = typeof msg?.params?.sessionId === 'string' ? msg.params.sessionId : undefined
|
||||||
|
|
||||||
|
// Non-CDP extension commands (ABExt.*) the daemon sends. `ungroupTab` removes a
|
||||||
|
// tab from its per-session tab group so a `keep`-marked tab is left for the user
|
||||||
|
// as a normal, ungrouped tab (the group can then be cleaned up). Best-effort.
|
||||||
|
if (method === 'ABExt.ungroupTab') {
|
||||||
|
const tabId = tabIdFromSession(sessionId) ?? tabForSession(sessionId)
|
||||||
|
if (tabId != null && chrome.tabs.ungroup) {
|
||||||
|
try {
|
||||||
|
await chrome.tabs.ungroup(tabId)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return { ungrouped: tabId ?? null }
|
||||||
|
}
|
||||||
|
|
||||||
// Browser-level Target methods that map onto chrome.tabs.
|
// Browser-level Target methods that map onto chrome.tabs.
|
||||||
if (method === 'Target.createTarget') {
|
if (method === 'Target.createTarget') {
|
||||||
const url = typeof params?.url === 'string' && params.url ? params.url : 'about:blank'
|
const url = typeof params?.url === 'string' && params.url ? params.url : 'about:blank'
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "chrome-use",
|
"name": "chrome-use",
|
||||||
"version": "0.4.11",
|
"version": "0.4.12",
|
||||||
"description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
|
"description": "Let chrome-use drive your logged-in Chrome — install once, no token, no per-use confirmation.",
|
||||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/icon16.png",
|
"16": "icons/icon16.png",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chrome-use",
|
"name": "chrome-use",
|
||||||
"version": "1.5.24",
|
"version": "1.5.27",
|
||||||
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
|
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@11.1.3",
|
"packageManager": "pnpm@11.1.3",
|
||||||
|
|||||||
@@ -791,6 +791,29 @@ chrome-use snapshot -i
|
|||||||
chrome-use frame main # back to main frame
|
chrome-use frame main # back to main frame
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Viewport / window size (responsive & overflow debugging)
|
||||||
|
|
||||||
|
To reproduce width-dependent bugs (responsive breakpoints, horizontal-overflow
|
||||||
|
hunts, mobile layouts) set the viewport. This is a **CDP virtual viewport**
|
||||||
|
(`Emulation.setDeviceMetricsOverride`) — it changes the layout viewport *for the
|
||||||
|
tab* without physically resizing the OS window, so it works headless **and** over
|
||||||
|
the extension relay without yanking the user's real Chrome window around.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chrome-use viewport 1280 800 # set width x height (alias: resize)
|
||||||
|
chrome-use viewport 375x812 # WxH shorthand
|
||||||
|
chrome-use viewport 375 812 --dpr 3 --mobile # retina + mobile emulation
|
||||||
|
chrome-use viewport reset # clear the override, restore real size
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find what's overflowing at a narrow width:
|
||||||
|
chrome-use viewport 375 812
|
||||||
|
chrome-use eval 'document.documentElement.scrollWidth + " vs " + innerWidth'
|
||||||
|
```
|
||||||
|
|
||||||
|
`set viewport <w> <h> [scale]` is an equivalent alias.
|
||||||
|
|
||||||
### Dialogs
|
### Dialogs
|
||||||
|
|
||||||
`alert` and `beforeunload` are auto-accepted so agents never block. For
|
`alert` and `beforeunload` are auto-accepted so agents never block. For
|
||||||
|
|||||||
Reference in New Issue
Block a user