feat(adopt): read a pre-existing tab without opening a new one
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
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
New `chrome-use adopt <url-substring|targetId>`: drive a tab the user (or another session) already has open, with ZERO new tabs. After group-scoped isolation (#40) a session can't see foreign tabs, so adopt adds an explicit, opt-in path: - Relay (relay.rs): `ABRelay.getAllTargets` returns every attached target UNSCOPED (ignores group scoping), so the agent can find a specific tab by URL or targetId. +1 unit test. - Daemon (browser.rs): `collect_all_targets` (unscoped, falls back to scoped on older relays) + `adopt_existing_target` — matches by exact targetId or case-insensitive URL substring, attaches it (the relay re-tags it into the adopter's group, so isolation holds), pins it; never creates a tab. On no match it errors AND lists the open tabs it can see, rather than launching. discover_and_attach_targets honors AGENT_BROWSER_ADOPT at first connect, so no about:blank is ever created. - CLI (main.rs): `adopt` sets the env, forces a fresh daemon, and rewrites into `connect <relay-url>` (like `extension connect`) so the daemon attaches to the user's real Chrome before parse_command. Extension (ab-connect 0.4.11): `reannounceAttachedTabs` now re-sends each tab's url/title (it previously sent neither) so the relay's target list stays matchable by URL after the MV3 service worker reconnects — otherwise reannounced tabs show a blank url and `adopt <url>` can't find them. Repacked upload zip + crx. Mechanism verified live (enumerated all 11 of the user's open tabs incl. the target). 862 tests pass.
This commit is contained in:
@@ -824,6 +824,106 @@ impl BrowserManager {
|
||||
Ok(by_id.into_values().collect())
|
||||
}
|
||||
|
||||
/// Every tab the relay knows, UNSCOPED (ignores group scoping) — for explicit
|
||||
/// cross-group adoption (`chrome-use adopt`). Falls back to the scoped
|
||||
/// `collect_page_targets` on a relay/browser that doesn't support the
|
||||
/// unscoped query. Retries a few times over the relay (discovery is eventual).
|
||||
async fn collect_all_targets(&self) -> Result<Vec<TargetInfo>, String> {
|
||||
let rounds = if crate::connect::relay_url().is_some() {
|
||||
3
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let mut by_id: HashMap<String, TargetInfo> = HashMap::new();
|
||||
let mut any_ok = false;
|
||||
for i in 0..rounds {
|
||||
if i > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||
}
|
||||
if let Ok(result) = self
|
||||
.client
|
||||
.send_command_typed::<_, GetTargetsResult>(
|
||||
"ABRelay.getAllTargets",
|
||||
&json!({}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
any_ok = true;
|
||||
for t in result.target_infos.into_iter().filter(should_track_target) {
|
||||
by_id.entry(t.target_id.clone()).or_insert(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
if any_ok {
|
||||
Ok(by_id.into_values().collect())
|
||||
} else {
|
||||
// Older relay without ABRelay.getAllTargets → best-effort scoped list.
|
||||
self.collect_page_targets().await
|
||||
}
|
||||
}
|
||||
|
||||
/// Adopt a specific pre-existing tab matched by `spec` (an exact CDP
|
||||
/// `targetId`, or a case-insensitive substring of the tab URL) WITHOUT opening
|
||||
/// a new tab — for `chrome-use adopt`. Attaches it (the relay tags it into our
|
||||
/// group), tracks + pins it. Errors if nothing matches (never creates a tab).
|
||||
async fn adopt_existing_target(&mut self, spec: &str) -> Result<(), String> {
|
||||
let all = self.collect_all_targets().await?;
|
||||
let spec_l = spec.to_lowercase();
|
||||
let target = all
|
||||
.iter()
|
||||
.find(|t| t.target_id == spec)
|
||||
.or_else(|| all.iter().find(|t| t.url.to_lowercase().contains(&spec_l)))
|
||||
.ok_or_else(|| {
|
||||
let mut open: Vec<String> = all
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let u = if t.url.len() > 80 {
|
||||
&t.url[..80]
|
||||
} else {
|
||||
&t.url
|
||||
};
|
||||
u.to_string()
|
||||
})
|
||||
.collect();
|
||||
open.sort();
|
||||
open.dedup();
|
||||
format!(
|
||||
"adopt: no open tab matching `{spec}` (by targetId or URL substring).\n\
|
||||
{} tab(s) the extension can see:\n {}",
|
||||
open.len(),
|
||||
open.join("\n ")
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
|
||||
let attach: AttachToTargetResult = self
|
||||
.client
|
||||
.send_command_typed(
|
||||
"Target.attachToTarget",
|
||||
&AttachToTargetParams {
|
||||
target_id: target.target_id.clone(),
|
||||
flatten: true,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let tab_id = self.assign_tab_id();
|
||||
self.pages.push(PageInfo {
|
||||
tab_id,
|
||||
label: None,
|
||||
target_id: target.target_id.clone(),
|
||||
session_id: attach.session_id.clone(),
|
||||
url: target.url.clone(),
|
||||
title: sanitize_title(&target.title),
|
||||
target_type: target.target_type.clone(),
|
||||
});
|
||||
self.active_page_index = self.pages.len() - 1;
|
||||
self.pin_active_target();
|
||||
self.enable_domains(&attach.session_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn discover_and_attach_targets(&mut self) -> Result<(), String> {
|
||||
self.client
|
||||
.send_command_typed::<_, Value>(
|
||||
@@ -837,6 +937,17 @@ impl BrowserManager {
|
||||
// own tab group (issue #40). On a launched browser this is a no-op.
|
||||
let scoped = self.announce_group().await;
|
||||
|
||||
// `chrome-use adopt <spec>`: adopt a specific PRE-EXISTING tab instead of
|
||||
// creating one — true zero-new-tab reading of the user's own tab. The
|
||||
// directive rides in via env so it takes effect at first connect (before
|
||||
// any about:blank would be made). If nothing matches, error out rather
|
||||
// than fall back to creating a tab.
|
||||
if let Ok(spec) = std::env::var("AGENT_BROWSER_ADOPT") {
|
||||
if !spec.trim().is_empty() {
|
||||
return self.adopt_existing_target(spec.trim()).await;
|
||||
}
|
||||
}
|
||||
|
||||
let page_targets: Vec<TargetInfo> = self.collect_page_targets().await?;
|
||||
|
||||
if page_targets.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user