Compare commits

...
3 Commits
Author SHA1 Message Date
leeguooooo d99a223d23 chore(release): 1.4.1 — hold-to-move (keydown/keyup full descriptor, #game) + expects ab-connect 0.4.8 (#23 reattach hardening)
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-14 00:04:08 +09:00
leeguooooo 4e7e80a596 fix(ab-connect): bind to stable tabId as the primary key + retry on mid-flight detach (0.4.8, #23)
claude-in-chrome completes the Rakuten cart→購入手続き→checkout flow that
chrome-use 1.2.3 couldn't, because it binds to the browser-level tabId (survives
renderer-process swaps) rather than a CDP target/sessionId (torn down by the
cross-origin OAuth/SSO nav). chrome-use's relay is already keyed to the stable
tabId (cb-tab-<tabId>, #17) and sends commands by {tabId} — the gap was purely
that the extension treated the session→tab map as the source of truth and only
reactively re-attached after a failed lookup.

Make the tabId the PRIMARY resolution path: derive it straight from the session
id (tabIdFromSession), ensure-attach with short retries across the swap window
(recoverSessionTab now loops), and route every send through sendCdpToTab, which
on a detached-style error drops the stale handle, re-attaches the stable tab, and
retries once. So a cross-process nav never surfaces as a hard error — there's no
'session gone' window, matching claude-in-chrome. Builds on 0.4.5/0.4.6 reattach;
makes it primary + bulletproof rather than a fallback.
2026-06-14 00:02:22 +09:00
leeguooooo 9bf79a4242 fix(keyboard): keydown/keyup send full key descriptor so hold-to-move works
`keydown`/`keyup` dispatched a minimal Input.dispatchKeyEvent carrying only
{key}, so games/handlers that read event.code ("KeyD", "ArrowRight") or
event.keyCode saw nothing — a held key set no movement flag and the player
barely moved (dogfood: Dead Cell). They now build the same descriptor `press`
uses (key + code + windows/nativeVirtualKeyCode + printable text on down) via a
shared interaction::dispatch_single_key. Verified live: holding a direction now
drives continuous movement (player ran into an enemy and took damage), where
before it nudged ~80px.
2026-06-13 23:58:30 +09:00
9 changed files with 114 additions and 50 deletions
+1 -1
View File
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrome-use"
version = "1.4.0"
version = "1.4.1"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "chrome-use"
version = "1.4.0"
version = "1.4.1"
edition = "2021"
description = "Fast browser automation CLI for AI agents"
license = "Apache-2.0"
+2 -14
View File
@@ -8785,13 +8785,7 @@ async fn handle_keydown(cmd: &Value, state: &DaemonState) -> Result<Value, Strin
.and_then(|v| v.as_str())
.ok_or("Missing 'key' parameter")?;
mgr.client
.send_command(
"Input.dispatchKeyEvent",
Some(json!({ "type": "keyDown", "key": key })),
Some(&session_id),
)
.await?;
interaction::dispatch_single_key(&mgr.client, &session_id, key, "keyDown").await?;
Ok(json!({ "keydown": key }))
}
@@ -8803,13 +8797,7 @@ async fn handle_keyup(cmd: &Value, state: &DaemonState) -> Result<Value, String>
.and_then(|v| v.as_str())
.ok_or("Missing 'key' parameter")?;
mgr.client
.send_command(
"Input.dispatchKeyEvent",
Some(json!({ "type": "keyUp", "key": key })),
Some(&session_id),
)
.await?;
interaction::dispatch_single_key(&mgr.client, &session_id, key, "keyUp").await?;
Ok(json!({ "keyup": key }))
}
+42
View File
@@ -557,6 +557,48 @@ pub async fn press_key_with_modifiers(
Ok(())
}
/// Dispatch a SINGLE key event (`keyDown` or `keyUp`) carrying the full key
/// descriptor — `key`, `code`, `windowsVirtualKeyCode`/`nativeVirtualKeyCode`,
/// and (on key-down) printable `text`. Powers the `keydown`/`keyup` commands.
///
/// The previous implementation sent only `{key}`, so games and shortcut handlers
/// that read `event.code` (e.g. `"KeyD"`, `"ArrowRight"`) or `event.keyCode` saw
/// nothing — a held key set no movement flag and did nothing (dogfood: holding a
/// direction in a canvas platformer barely nudged the player). Sending the same
/// descriptor `press` uses makes hold-to-move work regardless of which field the
/// page keys off.
pub async fn dispatch_single_key(
client: &CdpClient,
session_id: &str,
key: &str,
event_type: &str,
) -> Result<(), String> {
let (key_name, code, key_code) = named_key_info(key);
// Printable text is only meaningful on key-down; key-up never inserts.
let text = if event_type == "keyDown" {
key_text(&key_name)
} else {
None
};
client
.send_command_typed::<_, Value>(
"Input.dispatchKeyEvent",
&DispatchKeyEventParams {
event_type: event_type.to_string(),
key: Some(key_name),
code: Some(code),
text: text.clone(),
unmodified_text: text,
windows_virtual_key_code: Some(key_code),
native_virtual_key_code: Some(key_code),
modifiers: None,
},
Some(session_id),
)
.await?;
Ok(())
}
pub async fn scroll(
client: &CdpClient,
session_id: &str,
Binary file not shown.
Binary file not shown.
+66 -32
View File
@@ -155,23 +155,56 @@ function tabForTarget(targetId) {
return null
}
// Best-effort recovery for a stale `cb-tab-<tabId>` session: the handle is gone
// from our maps, but if the underlying Chrome tab still exists and is eligible,
// re-attach to it and return its id so the in-flight command can be retried.
// Returns null when the tab is genuinely gone (closed / restricted), in which
// case the caller surfaces the stale-session error. (issue #20.1)
// The STABLE Chrome tabId encoded in a `cb-tab-<tabId>` session id (#17), or
// null for any other session shape (child/iframe sessions). The tabId is the
// real source of truth: it survives the renderer-process swaps (cross-origin
// OAuth/SSO navs) that tear down the page's CDP target — which is why binding to
// it (like claude-in-chrome) rides through the hop that killed the old
// target/sessionId binding (issue #23).
function tabIdFromSession(sessionId) {
const m = /^cb-tab-(\d+)$/.exec(sessionId || '')
return m ? Number(m[1]) : null
}
// Ensure the debugger is attached to a `cb-tab-<tabId>` session's tab, re-attaching
// across the transient window of a process swap (with a couple of short retries).
// Returns the tabId on success, or null when the tab is genuinely gone
// (closed / restricted). (issues #20.1, #23)
async function recoverSessionTab(sessionId) {
const m = /^cb-tab-(\d+)$/.exec(sessionId)
if (!m) return null
const tabId = Number(m[1])
const tab = await chrome.tabs.get(tabId).catch(() => null)
if (!eligible(tab)) return null
try {
await attachTab(tabId)
} catch {
return null
const tabId = tabIdFromSession(sessionId)
if (tabId == null) return null
for (let i = 0; i < 3; i++) {
const tab = await chrome.tabs.get(tabId).catch(() => null)
if (!eligible(tab)) return null
try {
await attachTab(tabId)
if (tabs.has(tabId)) return tabId
} catch {
// mid-swap: the tab exists but isn't attachable yet — back off and retry.
}
await new Promise((r) => setTimeout(r, 120 + i * 150))
}
return null
}
// Send a CDP command to a tab, riding a debugger detach that can happen between
// our attach check and the command itself (a renderer-process swap mid-flight).
// On a detached-style failure, drop the stale handle, re-attach the stable tab,
// and retry once — so a cross-process nav never surfaces as a hard error (#23).
async function sendCdpToTab(tabId, method, params) {
const dbg = { tabId }
try {
return await chrome.debugger.sendCommand(dbg, method, params)
} catch (e) {
const msg = String((e && e.message) || e)
if (!/detached|not attached|target.*(closed|gone)|no target|cannot access|frame.*detached/i.test(msg)) {
throw e
}
detachTab(tabId, false)
const ok = await recoverSessionTab(`cb-tab-${tabId}`)
if (!ok) throw e
return await chrome.debugger.sendCommand(dbg, method, params)
}
return tabs.has(tabId) ? tabId : null
}
function anyConnectedTab() {
@@ -232,24 +265,26 @@ async function handleForwardCdpCommand(msg) {
// Fail loudly instead so the agent sees an actionable error, not bad data.
let tabId
if (sessionId) {
tabId = tabForSession(sessionId)
if (!tabId) {
// The session's debugger handle is gone, but `cb-tab-<tabId>` encodes the
// STABLE Chrome tabId (#17). A cross-process navigation (e.g. an SSO
// redirect to another origin), a service-worker restart, or DevTools
// briefly stealing the debugger all tear the handle down while the tab
// itself lives on. Before failing, try to transparently re-attach to that
// same tab and retry — so `open`/`navigate`/`eval` self-heal instead of
// dead-ending the agent (issue #20.1). attachTab re-mints the identical
// `cb-tab-<tabId>` session, so the daemon's binding stays valid.
tabId = await recoverSessionTab(sessionId)
if (!tabId) {
// The stable Chrome tabId encoded in `cb-tab-<tabId>` is the source of truth
// (it survives renderer-process swaps; the CDP target/sessionId does not).
// Resolve via it primarily — don't depend on a session→tab map entry that the
// detach handler may have cleared — and ensure the debugger is attached,
// re-attaching across a cross-process nav before failing (issues #20.1, #23).
// `tabForSession` still covers child/iframe sessions that aren't `cb-tab-*`.
tabId = tabIdFromSession(sessionId) ?? tabForSession(sessionId)
if (tabId == null) {
throw new Error(`unknown sessionId ${sessionId} for ${method}`)
}
if (!tabs.has(tabId)) {
const recovered = await recoverSessionTab(sessionId)
if (!recovered) {
throw new Error(
`stale sessionId ${sessionId} for ${method}: its tab is gone (closed, ` +
`navigated across processes, or lost after an extension restart). ` +
`Re-attach by re-opening your target URL before retrying.`,
)
}
tabId = recovered
}
} else if (typeof params?.targetId === 'string') {
tabId = tabForTarget(params.targetId)
@@ -259,18 +294,17 @@ async function handleForwardCdpCommand(msg) {
// applies to any attached tab.
tabId = anyConnectedTab()
}
if (!tabId) throw new Error(`no attached tab for ${method}`)
const dbg = { tabId }
if (tabId == null) throw new Error(`no attached tab for ${method}`)
// Re-enabling Runtime can leave a stale state; bounce it (matches upstream).
if (method === 'Runtime.enable') {
try {
await chrome.debugger.sendCommand(dbg, 'Runtime.disable')
await sendCdpToTab(tabId, 'Runtime.disable', undefined)
await new Promise((r) => setTimeout(r, 30))
} catch {}
return await chrome.debugger.sendCommand(dbg, 'Runtime.enable', params)
return await sendCdpToTab(tabId, 'Runtime.enable', params)
}
return await chrome.debugger.sendCommand(dbg, method, params)
return await sendCdpToTab(tabId, method, params)
}
// ---- attach / detach ------------------------------------------------------
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "chrome-use",
"version": "0.4.7",
"version": "0.4.8",
"description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
"icons": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "chrome-use",
"version": "1.4.0",
"version": "1.4.1",
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
"type": "module",
"packageManager": "pnpm@11.1.3",