feat(connect): per-session Chrome tab groups on the shared real browser
Shared browser, separate tab groups: when an agent drives the user's real Chrome via ab-connect, every tab it opens lands in a Chrome tab group named after its --session (stable color per name). Each agent's tabs stay visually separated from other agents' and from the user's own (ungrouped) tabs. Visibility is NOT restricted — all agents still see all tabs (per design). - CreateTargetParams gains an optional non-CDP `agentGroup` hint (skip-if-none), so a strict real-Chrome endpoint never receives it - BrowserManager.agent_group(): Some(session) only when ws_url == the live ab-connect relay URL (never on launched/direct CDP); DAEMON_SESSION set at daemon start supplies the name; emitted at all createTarget sites (transient storage target stays None) - ab-connect: +tabGroups permission; Target.createTarget reads agentGroup and groups the new tab (create/reuse by title, deterministic color), best-effort - extension 0.3.0 -> 0.4.0; re-signed crx + zip (id unchanged) Needs the v0.4.0 extension reloaded + a build with this change to take effect.
This commit is contained in:
@@ -12,6 +12,11 @@ use super::cdp::lightpanda::{launch_lightpanda, LightpandaLaunchOptions, Lightpa
|
||||
use super::cdp::types::*;
|
||||
use super::element::{resolve_element_object_id, RefMap};
|
||||
|
||||
/// The daemon's session name, set once at daemon start. Names the Chrome tab
|
||||
/// group that abs-created tabs land in when driving the user's real Chrome via
|
||||
/// the `ab-connect` extension, so each agent/session gets its own group.
|
||||
pub static DAEMON_SESSION: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Launch validation
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -568,12 +573,14 @@ impl BrowserManager {
|
||||
|
||||
if page_targets.is_empty() {
|
||||
// Create a new tab
|
||||
let agent_group = self.agent_group();
|
||||
let result: CreateTargetResult = self
|
||||
.client
|
||||
.send_command_typed(
|
||||
"Target.createTarget",
|
||||
&CreateTargetParams {
|
||||
url: "about:blank".to_string(),
|
||||
agent_group,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -958,12 +965,14 @@ impl BrowserManager {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let agent_group = self.agent_group();
|
||||
let result: CreateTargetResult = self
|
||||
.client
|
||||
.send_command_typed(
|
||||
"Target.createTarget",
|
||||
&CreateTargetParams {
|
||||
url: "about:blank".to_string(),
|
||||
agent_group,
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -1072,6 +1081,27 @@ impl BrowserManager {
|
||||
self.pages.iter().any(|p| p.label.as_deref() == Some(label))
|
||||
}
|
||||
|
||||
/// Chrome tab-group name for tabs this manager creates, or `None` when not
|
||||
/// driving the user's real Chrome via the `ab-connect` extension relay.
|
||||
///
|
||||
/// Grouping only makes sense on the shared real browser (one Chrome, many
|
||||
/// agents): each session's tabs go into its own group. On a launched / direct
|
||||
/// CDP browser the endpoint is strict, so we must NOT send the custom param —
|
||||
/// hence `None` there. We detect the relay by matching our `ws_url` against
|
||||
/// the live relay URL the native-messaging host published.
|
||||
fn agent_group(&self) -> Option<String> {
|
||||
let via_relay = crate::connect::relay_url().as_deref() == Some(self.ws_url.as_str());
|
||||
if !via_relay {
|
||||
return None;
|
||||
}
|
||||
let name = DAEMON_SESSION.get().map(String::as_str).unwrap_or("default");
|
||||
if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn tab_new(
|
||||
&mut self,
|
||||
url: Option<&str>,
|
||||
@@ -1096,12 +1126,14 @@ impl BrowserManager {
|
||||
|
||||
let target_url = url.unwrap_or("about:blank");
|
||||
|
||||
let agent_group = self.agent_group();
|
||||
let result: CreateTargetResult = self
|
||||
.client
|
||||
.send_command_typed(
|
||||
"Target.createTarget",
|
||||
&CreateTargetParams {
|
||||
url: target_url.to_string(),
|
||||
agent_group,
|
||||
},
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -141,6 +141,12 @@ pub struct SetDiscoverTargetsParams {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateTargetParams {
|
||||
pub url: String,
|
||||
/// Non-CDP hint consumed only by the `ab-connect` extension: the Chrome
|
||||
/// tab-group name to drop the new tab into (per-session grouping on the
|
||||
/// shared real Chrome). `None` on the normal CDP path so a strict real-Chrome
|
||||
/// endpoint never receives an unknown parameter.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_group: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
@@ -17,6 +17,10 @@ use super::state;
|
||||
use super::stream::StreamServer;
|
||||
|
||||
pub async fn run_daemon(session: &str) {
|
||||
// Record this daemon's session so tabs it opens on the shared real Chrome
|
||||
// (via the ab-connect extension) land in a per-session Chrome tab group.
|
||||
let _ = super::browser::DAEMON_SESSION.set(session.to_string());
|
||||
|
||||
let socket_dir = get_daemon_socket_dir();
|
||||
if !socket_dir.exists() {
|
||||
let _ = fs::create_dir_all(&socket_dir);
|
||||
|
||||
@@ -119,6 +119,8 @@ async fn collect_storage_via_temp_target(
|
||||
"Target.createTarget",
|
||||
&CreateTargetParams {
|
||||
url: "about:blank".to_string(),
|
||||
// Transient internal target (storage collection) — never grouped.
|
||||
agent_group: None,
|
||||
},
|
||||
None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user