Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0644fb2d0b | ||
|
|
1ea6b1a2c5 | ||
|
|
7bb50d54b3 |
@@ -133,6 +133,9 @@ jobs:
|
|||||||
- name: Generate changelog
|
- name: Generate changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
run: |
|
run: |
|
||||||
|
# fetch-depth:0 gets history, but the tag refs the changelog needs
|
||||||
|
# aren't always present in a detached-HEAD tag checkout — pull them in.
|
||||||
|
git fetch --tags --force --quiet origin 2>/dev/null || true
|
||||||
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||||||
PREV="$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true)"
|
PREV="$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true)"
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
+1
-1
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrome-use"
|
name = "chrome-use"
|
||||||
version = "1.2.2"
|
version = "1.2.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chrome-use"
|
name = "chrome-use"
|
||||||
version = "1.2.2"
|
version = "1.2.3"
|
||||||
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"
|
||||||
|
|||||||
+50
-3
@@ -408,6 +408,12 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
|||||||
"back" => Ok(json!({ "id": id, "action": "back" })),
|
"back" => Ok(json!({ "id": id, "action": "back" })),
|
||||||
"forward" => Ok(json!({ "id": id, "action": "forward" })),
|
"forward" => Ok(json!({ "id": id, "action": "forward" })),
|
||||||
"reload" => Ok(json!({ "id": id, "action": "reload" })),
|
"reload" => Ok(json!({ "id": id, "action": "reload" })),
|
||||||
|
// Explicit opt-in to raise the active tab to the foreground (the core
|
||||||
|
// skill references it; the daemon handler existed but the CLI didn't map
|
||||||
|
// it — issue #19). Accept the documented camelCase + kebab/lowercase.
|
||||||
|
"bringToFront" | "bring-to-front" | "bringtofront" => {
|
||||||
|
Ok(json!({ "id": id, "action": "bringtofront" }))
|
||||||
|
}
|
||||||
|
|
||||||
// === Core Actions ===
|
// === Core Actions ===
|
||||||
"click" => {
|
"click" => {
|
||||||
@@ -1496,7 +1502,12 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
|||||||
// `tabs` (plural) is a natural guess for the `tab` subcommand tree —
|
// `tabs` (plural) is a natural guess for the `tab` subcommand tree —
|
||||||
// alias it so `tabs` / `tabs list` / `tabs new` all work (issue #8.4).
|
// alias it so `tabs` / `tabs list` / `tabs new` all work (issue #8.4).
|
||||||
"tab" | "tabs" => {
|
"tab" | "tabs" => {
|
||||||
match rest.first().copied() {
|
// `--full` makes `tab list` emit untruncated URLs (needed to re-open
|
||||||
|
// a long SSO/redirect URL after a stale session — issue #19). Pick
|
||||||
|
// the subcommand as the first non-flag arg so the flag can appear
|
||||||
|
// anywhere (`tab --full`, `tab list --full`).
|
||||||
|
let full = rest.contains(&"--full");
|
||||||
|
match rest.iter().find(|a| !a.starts_with("--")).copied() {
|
||||||
Some("new") => {
|
Some("new") => {
|
||||||
// Accepted forms:
|
// Accepted forms:
|
||||||
// tab new [url]
|
// tab new [url]
|
||||||
@@ -1528,7 +1539,13 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
|||||||
}
|
}
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
Some("list") => Ok(json!({ "id": id, "action": "tab_list" })),
|
Some("list") => {
|
||||||
|
let mut cmd = json!({ "id": id, "action": "tab_list" });
|
||||||
|
if full {
|
||||||
|
cmd["full"] = json!(true);
|
||||||
|
}
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
Some("close") => {
|
Some("close") => {
|
||||||
let mut cmd = json!({ "id": id, "action": "tab_close" });
|
let mut cmd = json!({ "id": id, "action": "tab_close" });
|
||||||
if let Some(tab_ref) = rest.get(1) {
|
if let Some(tab_ref) = rest.get(1) {
|
||||||
@@ -1541,7 +1558,13 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
|||||||
"action": "tab_switch",
|
"action": "tab_switch",
|
||||||
"tabId": tab_ref,
|
"tabId": tab_ref,
|
||||||
})),
|
})),
|
||||||
None => Ok(json!({ "id": id, "action": "tab_list" })),
|
None => {
|
||||||
|
let mut cmd = json!({ "id": id, "action": "tab_list" });
|
||||||
|
if full {
|
||||||
|
cmd["full"] = json!(true);
|
||||||
|
}
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3716,6 +3739,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_list_full_flag() {
|
||||||
|
// issue #19: `--full` → untruncated URLs; works as `tab list --full`,
|
||||||
|
// `tab --full`, and `tabs --full`. Plain list has no `full`.
|
||||||
|
for inv in ["tab list --full", "tab --full", "tabs --full"] {
|
||||||
|
let cmd = parse_command(&args(inv), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "tab_list", "{inv}");
|
||||||
|
assert_eq!(cmd["full"], true, "{inv}");
|
||||||
|
}
|
||||||
|
let plain = parse_command(&args("tab list"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(plain["action"], "tab_list");
|
||||||
|
assert!(plain.get("full").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bring_to_front_aliases() {
|
||||||
|
// issue #19: the documented `bringToFront` (+ kebab/lowercase) maps to
|
||||||
|
// the existing daemon action.
|
||||||
|
for inv in ["bringToFront", "bring-to-front", "bringtofront"] {
|
||||||
|
let cmd = parse_command(&args(inv), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "bringtofront", "{inv}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_text_hyphen_and_underscore_aliases() {
|
fn test_get_text_hyphen_and_underscore_aliases() {
|
||||||
for verb in ["get-text", "get_text"] {
|
for verb in ["get-text", "get_text"] {
|
||||||
|
|||||||
@@ -1364,7 +1364,7 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
|
|||||||
"recording_stop" => handle_recording_stop(state).await,
|
"recording_stop" => handle_recording_stop(state).await,
|
||||||
"recording_restart" => handle_recording_restart(cmd, state).await,
|
"recording_restart" => handle_recording_restart(cmd, state).await,
|
||||||
"pdf" => handle_pdf(cmd, state).await,
|
"pdf" => handle_pdf(cmd, state).await,
|
||||||
"tab_list" => handle_tab_list(state).await,
|
"tab_list" => handle_tab_list(cmd, state).await,
|
||||||
"tab_new" => handle_tab_new(cmd, state).await,
|
"tab_new" => handle_tab_new(cmd, state).await,
|
||||||
"tab_switch" => handle_tab_switch(cmd, state).await,
|
"tab_switch" => handle_tab_switch(cmd, state).await,
|
||||||
"tab_close" => handle_tab_close(cmd, state).await,
|
"tab_close" => handle_tab_close(cmd, state).await,
|
||||||
@@ -4355,10 +4355,15 @@ async fn handle_keyboard(cmd: &Value, state: &DaemonState) -> Result<Value, Stri
|
|||||||
// Phase 5 handlers
|
// Phase 5 handlers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async fn handle_tab_list(state: &DaemonState) -> Result<Value, String> {
|
async fn handle_tab_list(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
||||||
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||||
let tabs = mgr.tab_list();
|
let tabs = mgr.tab_list();
|
||||||
Ok(json!({ "tabs": tabs }))
|
// Echo `full` so the formatter prints untruncated URLs (issue #19).
|
||||||
|
if cmd.get("full").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||||
|
Ok(json!({ "tabs": tabs, "full": true }))
|
||||||
|
} else {
|
||||||
|
Ok(json!({ "tabs": tabs }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_tab_new(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
async fn handle_tab_new(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||||
|
|||||||
+10
-2
@@ -477,6 +477,9 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
|||||||
}
|
}
|
||||||
// Tabs
|
// Tabs
|
||||||
if let Some(tabs) = data.get("tabs").and_then(|v| v.as_array()) {
|
if let Some(tabs) = data.get("tabs").and_then(|v| v.as_array()) {
|
||||||
|
// `tab list --full` prints untruncated URLs so a long SSO/redirect
|
||||||
|
// URL can actually be re-opened after a stale session (issue #19).
|
||||||
|
let full = data.get("full").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
for tab in tabs {
|
for tab in tabs {
|
||||||
let tab_id = tab.get("tabId").and_then(|v| v.as_str()).unwrap_or("?");
|
let tab_id = tab.get("tabId").and_then(|v| v.as_str()).unwrap_or("?");
|
||||||
let tab_label = tab.get("label").and_then(|v| v.as_str());
|
let tab_label = tab.get("label").and_then(|v| v.as_str());
|
||||||
@@ -491,8 +494,13 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
|||||||
let title = title.as_str();
|
let title = title.as_str();
|
||||||
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
|
// Truncate very long URLs (e.g. multi-KB JWT/OTP login links) so
|
||||||
// the list stays readable instead of flooding the terminal.
|
// the list stays readable instead of flooding the terminal —
|
||||||
let url = truncate_middle(url, 120);
|
// unless `--full` was asked for (to re-open the exact URL).
|
||||||
|
let url = if full {
|
||||||
|
url.to_string()
|
||||||
|
} else {
|
||||||
|
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("→")
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chrome-use",
|
"name": "chrome-use",
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"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",
|
||||||
|
|||||||
@@ -610,6 +610,13 @@ forbids debugging). The session no longer has a live tab — re-run
|
|||||||
replaces the old silent behaviour where the command ran on some *other*
|
replaces the old silent behaviour where the command ran on some *other*
|
||||||
tab and returned wrong data.
|
tab and returned wrong data.
|
||||||
|
|
||||||
|
To recover, you need the tab's **exact** URL (query params and all — a long
|
||||||
|
SSO/redirect link breaks if truncated). `tab list` shortens long URLs with
|
||||||
|
`…`; use **`tab list --full`** to print them untruncated, then re-`open` the
|
||||||
|
right one. For multi-redirect SSO flows, re-open the **stable entry URL**
|
||||||
|
(not the mid-redirect one) and `wait` a few seconds for the SPA to settle
|
||||||
|
before snapshotting.
|
||||||
|
|
||||||
**Reads landing on the wrong page**
|
**Reads landing on the wrong page**
|
||||||
`eval`, `screenshot`, and `network requests` print the page they ran
|
`eval`, `screenshot`, and `network requests` print the page they ran
|
||||||
against to stderr: `eval @ <url>`, `screenshot @ <url>`, `network @ <url>`.
|
against to stderr: `eval @ <url>`, `screenshot @ <url>`, `network @ <url>`.
|
||||||
|
|||||||
Reference in New Issue
Block a user