diff --git a/README.md b/README.md index 3016bac..b3383b6 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,21 @@ agent-browser snapshot -i agent-browser click @e2 ``` +### Default: Auto Group Agent Tabs (Local Chromium) + +```bash +agent-browser open https://example.com +# Local Chromium launch auto-groups tabs under "Agent Browser Stealth" + +# Override group title +agent-browser --tab-group "My Agent Group" open https://example.com +``` + +- Groups agent-opened tabs under a shared Chrome tab group title. +- Supported only for local Chromium launches. +- In CDP (`--cdp` / `--auto-connect`) and cloud provider modes, it is ignored with a warning. +- Env override: `AGENT_BROWSER_TAB_GROUP`. + ## Stealth Architecture ```mermaid diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 959ddcd..95b370d 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -2054,7 +2054,9 @@ mod tests { annotate: false, color_scheme: None, download_path: None, + tab_group: None, risk_mode: None, + cli_tab_group: false, } } diff --git a/cli/src/connection.rs b/cli/src/connection.rs index b54e21d..304b7a8 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -227,6 +227,7 @@ pub fn ensure_daemon( session_name: Option<&str>, debug: bool, download_path: Option<&str>, + tab_group: Option<&str>, ) -> Result { // Check if daemon is running AND responsive if is_daemon_running(session) && daemon_ready(session) { @@ -374,6 +375,9 @@ pub fn ensure_daemon( if let Some(dp) = download_path { cmd.env("AGENT_BROWSER_DOWNLOAD_PATH", dp); } + if let Some(tg) = tab_group { + cmd.env("AGENT_BROWSER_TAB_GROUP", tg); + } // Create new process group and session to fully detach unsafe { @@ -461,6 +465,9 @@ pub fn ensure_daemon( if let Some(dp) = download_path { cmd.env("AGENT_BROWSER_DOWNLOAD_PATH", dp); } + if let Some(tg) = tab_group { + cmd.env("AGENT_BROWSER_TAB_GROUP", tg); + } // CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; diff --git a/cli/src/flags.rs b/cli/src/flags.rs index 53c540e..a3f3488 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -34,6 +34,7 @@ pub struct Config { pub annotate: Option, pub color_scheme: Option, pub download_path: Option, + pub tab_group: Option, pub risk_mode: Option, } @@ -69,6 +70,7 @@ impl Config { annotate: other.annotate.or(self.annotate), color_scheme: other.color_scheme.or(self.color_scheme), download_path: other.download_path.or(self.download_path), + tab_group: other.tab_group.or(self.tab_group), risk_mode: other.risk_mode.or(self.risk_mode), } } @@ -136,6 +138,7 @@ fn extract_config_path(args: &[String]) -> Option> { "--color-scheme", "--channel", "--download-path", + "--tab-group", "--risk-mode", ]; let mut i = 0; @@ -207,6 +210,7 @@ pub struct Flags { pub annotate: bool, pub color_scheme: Option, pub download_path: Option, + pub tab_group: Option, /// How verification/captcha detections are handled on navigation: /// `off` (disable), `warn` (retry and warn), `block` (fail fast). pub risk_mode: Option, @@ -223,6 +227,7 @@ pub struct Flags { pub cli_allow_file_access: bool, pub cli_annotate: bool, pub cli_download_path: bool, + pub cli_tab_group: bool, } pub fn parse_flags(args: &[String]) -> Flags { @@ -291,6 +296,7 @@ pub fn parse_flags(args: &[String]) -> Flags { .or(config.color_scheme), download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH").ok() .or(config.download_path), + tab_group: env::var("AGENT_BROWSER_TAB_GROUP").ok().or(config.tab_group), risk_mode: env::var("AGENT_BROWSER_RISK_MODE") .ok() .or(config.risk_mode) @@ -305,6 +311,7 @@ pub fn parse_flags(args: &[String]) -> Flags { cli_allow_file_access: false, cli_annotate: false, cli_download_path: false, + cli_tab_group: false, }; let mut i = 0; @@ -466,6 +473,13 @@ pub fn parse_flags(args: &[String]) -> Flags { i += 1; } } + "--tab-group" => { + if let Some(s) = args.get(i + 1) { + flags.tab_group = Some(s.clone()); + flags.cli_tab_group = true; + i += 1; + } + } "--risk-mode" => { if let Some(s) = args.get(i + 1) { flags.risk_mode = Some(s.to_ascii_lowercase()); @@ -516,6 +530,7 @@ pub fn clean_args(args: &[String]) -> Vec { "--session-name", "--color-scheme", "--download-path", + "--tab-group", "--risk-mode", "--config", ]; @@ -714,6 +729,24 @@ mod tests { assert!(!flags.cli_download_path); } + #[test] + fn test_parse_tab_group_flag() { + let input = vec![ + "--tab-group".to_string(), + "Agent Browser Stealth".to_string(), + "snapshot".to_string(), + ]; + let flags = parse_flags(&input); + assert_eq!(flags.tab_group.as_deref(), Some("Agent Browser Stealth")); + assert!(flags.cli_tab_group); + } + + #[test] + fn test_clean_args_removes_tab_group() { + let cleaned = clean_args(&args("--tab-group AgentGroup open example.com")); + assert_eq!(cleaned, vec!["open", "example.com"]); + } + #[test] fn test_parse_risk_mode_flag() { let flags = parse_flags(&args("--risk-mode block open example.com")); @@ -762,6 +795,7 @@ mod tests { "cdp": "9222", "autoConnect": true, "headers": "{\"Auth\":\"token\"}", + "tabGroup": "Agent Browser Stealth", "riskMode": "block" }"#; let config: Config = serde_json::from_str(json).unwrap(); @@ -788,6 +822,7 @@ mod tests { assert_eq!(config.cdp.as_deref(), Some("9222")); assert_eq!(config.auto_connect, Some(true)); assert_eq!(config.headers.as_deref(), Some("{\"Auth\":\"token\"}")); + assert_eq!(config.tab_group.as_deref(), Some("Agent Browser Stealth")); assert_eq!(config.risk_mode.as_deref(), Some("block")); } diff --git a/cli/src/main.rs b/cli/src/main.rs index d4411af..1dd61b6 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -287,6 +287,7 @@ fn main() { flags.session_name.as_deref(), flags.debug, flags.download_path.as_deref(), + flags.tab_group.as_deref(), ) { Ok(result) => result, Err(e) => { @@ -338,6 +339,7 @@ fn main() { flags.ignore_https_errors.then_some("--ignore-https-errors"), flags.cli_allow_file_access.then_some("--allow-file-access"), flags.cli_download_path.then_some("--download-path"), + flags.cli_tab_group.then_some("--tab-group"), ] .into_iter() .flatten() @@ -424,6 +426,9 @@ fn main() { if let Some(ref dp) = flags.download_path { launch_cmd["downloadPath"] = json!(dp); } + if let Some(ref tg) = flags.tab_group { + launch_cmd["tabGroup"] = json!(tg); + } let err = match send_command(launch_cmd, &flags.session) { Ok(resp) if resp.success => None, @@ -516,6 +521,9 @@ fn main() { if let Some(ref dp) = flags.download_path { launch_cmd["downloadPath"] = json!(dp); } + if let Some(ref tg) = flags.tab_group { + launch_cmd["tabGroup"] = json!(tg); + } let err = match send_command(launch_cmd, &flags.session) { Ok(resp) if resp.success => None, @@ -549,6 +557,9 @@ fn main() { if let Some(ref cs) = flags.color_scheme { launch_cmd["colorScheme"] = json!(cs); } + if let Some(ref tg) = flags.tab_group { + launch_cmd["tabGroup"] = json!(tg); + } match send_command(launch_cmd, &flags.session) { Ok(resp) => { @@ -589,7 +600,8 @@ fn main() { && flags.user_agent.is_none() && !flags.ignore_https_errors && !flags.allow_file_access - && flags.extensions.is_empty(); + && flags.extensions.is_empty() + && flags.tab_group.is_none(); if can_try_default_cdp { let mut launch_cmd = json!({ @@ -643,7 +655,8 @@ fn main() { || flags.allow_file_access || flags.debug || flags.color_scheme.is_some() - || flags.download_path.is_some()) + || flags.download_path.is_some() + || flags.tab_group.is_some()) && flags.cdp.is_none() && flags.provider.is_none() && !attached_to_existing_browser @@ -708,6 +721,9 @@ fn main() { if let Some(ref dp) = flags.download_path { launch_cmd["downloadPath"] = json!(dp); } + if let Some(ref tg) = flags.tab_group { + launch_cmd["tabGroup"] = json!(tg); + } match send_command(launch_cmd, &flags.session) { Ok(resp) => { diff --git a/cli/src/output.rs b/cli/src/output.rs index ad037db..1140275 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -2409,6 +2409,7 @@ Options: Project default: try localhost:9333 first, then auto-discovery (no managed local-launch fallback) --color-scheme Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME) --download-path Default download directory (or AGENT_BROWSER_DOWNLOAD_PATH) + --tab-group Override default tab group title for agent tabs in Chromium local launch (or AGENT_BROWSER_TAB_GROUP) --risk-mode Verify/captcha handling: off, warn, block (or AGENT_BROWSER_RISK_MODE) --session-name Auto-save/restore session state (cookies, localStorage) --content-boundaries Wrap page output in boundary markers (or AGENT_BROWSER_CONTENT_BOUNDARIES) @@ -2467,6 +2468,7 @@ Environment: AGENT_BROWSER_TIMEZONE Override auto-detected timezone (e.g., Asia/Taipei) AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference) AGENT_BROWSER_DOWNLOAD_PATH Default download directory for browser downloads + AGENT_BROWSER_TAB_GROUP Override default tab group title (Chromium local launch only) AGENT_BROWSER_RISK_MODE Verify/captcha handling mode (off, warn, block) AGENT_BROWSER_DEFAULT_TIMEOUT Default Playwright timeout in ms (default: 25000) AGENT_BROWSER_SESSION_NAME Auto-save/load state persistence name diff --git a/docs/src/app/commands/page.mdx b/docs/src/app/commands/page.mdx index 63477c3..ece23e8 100644 --- a/docs/src/app/commands/page.mdx +++ b/docs/src/app/commands/page.mdx @@ -130,6 +130,22 @@ agent-browser wait --download [path] # Wait for any download to complete Use `--download-path ` (or `AGENT_BROWSER_DOWNLOAD_PATH` env) to set a default download directory. Without it, downloads go to a temporary directory that is deleted when the browser closes. +## Tab grouping + +```bash +agent-browser open https://example.com +# Local Chromium launch auto-groups tabs under "Agent Browser Stealth" + +# Override the default group title +agent-browser --tab-group "My Agent Group" open https://example.com +``` + +Local Chromium launches auto-create/reuse the `Agent Browser Stealth` tab group and move newly opened agent tabs into that group. + +- Supported only for local Chromium launches. +- In CDP (`--cdp` / `--auto-connect`) and cloud provider modes, the flag is ignored with a warning. +- Use `--tab-group` or `AGENT_BROWSER_TAB_GROUP` to override the default group title. + ## Mouse ```bash @@ -280,6 +296,7 @@ agent-browser reload # Reload page --headed # Show browser window (not headless) --cdp # Connect via Chrome DevTools Protocol (port or WebSocket URL) --auto-connect # Auto-discover and connect to running Chrome +--tab-group # Override default agent tab group title (Chromium local launch only) --debug # Debug output (includes stealth connection type + capabilities) ``` diff --git a/docs/src/app/configuration/page.mdx b/docs/src/app/configuration/page.mdx index d864b58..ae33d01 100644 --- a/docs/src/app/configuration/page.mdx +++ b/docs/src/app/configuration/page.mdx @@ -274,6 +274,15 @@ Every CLI flag can be set in the config file using its camelCase equivalent: string + + + tabGroup + + + --tab-group + + string (override default tab group title; Chromium local launch only) + riskMode @@ -406,6 +415,13 @@ These environment variables configure additional daemon and runtime behavior: Default directory for browser downloads. (temp directory) + + + AGENT_BROWSER_TAB_GROUP + + Override default auto-group title for agent tabs (Chromium local launch only). + (disabled) + AGENT_BROWSER_RISK_MODE diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index e039991..c68a098 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -89,6 +89,7 @@ agent-browser wait 2000-5000 # Random wait between 2-5 seconds agent-browser download @e1 ./file.pdf # Click element to trigger download agent-browser wait --download ./output.zip # Wait for any download to complete agent-browser --download-path ./downloads open # Set default download directory +agent-browser --tab-group "My Agent Group" open # Override default tab group title (Chromium local launch) # Capture agent-browser screenshot # Screenshot to temp dir @@ -247,6 +248,25 @@ AGENT_BROWSER_COLOR_SCHEME=dark agent-browser open https://example.com agent-browser set media dark ``` +### Tab Grouping + +```bash +# Local Chromium launch auto-groups under "Agent Browser Stealth" +agent-browser open https://example.com + +# Override the default group title +agent-browser --tab-group "My Agent Group" open https://example.com + +# Or via environment variable +AGENT_BROWSER_TAB_GROUP="My Agent Group" agent-browser open https://example.com +``` + +Notes: + +- Works only for local Chromium launches. +- In CDP/auto-connect and cloud provider modes, `--tab-group` is ignored with a warning. +- New agent tabs are auto-added to the group after each tab loads content. + ### Visual Browser (Debugging) ```bash diff --git a/src/browser.ts b/src/browser.ts index 91e267a..260fa21 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -16,7 +16,15 @@ import { } from 'playwright-core'; import path from 'node:path'; import os from 'node:os'; -import { existsSync, mkdirSync, rmSync, readFileSync, statSync } from 'node:fs'; +import { + existsSync, + mkdirSync, + mkdtempSync, + rmSync, + readFileSync, + statSync, + writeFileSync, +} from 'node:fs'; import { writeFile, mkdir } from 'node:fs/promises'; import type { LaunchCommand, TraceEvent } from './types.js'; import { type RefMap, type EnhancedSnapshot, getEnhancedSnapshot, parseRef } from './snapshot.js'; @@ -127,6 +135,7 @@ interface StealthContextDefaults { } const IGNORED_CDP_PAGE_URL_PREFIXES = ['chrome://omnibox-popup.top-chrome/']; +const DEFAULT_TAB_GROUP_NAME = 'Agent Browser Stealth'; /** * Manages the Playwright browser lifecycle with multiple tabs/windows @@ -163,6 +172,7 @@ export class BrowserManager { private contextUserAgent: string | undefined = undefined; private downloadPath: string | null = null; private allowedDomains: string[] = []; + private tabGroupExtensionDir: string | null = null; /** * Set the persistent color scheme preference. @@ -478,6 +488,125 @@ export class BrowserManager { return warnings; } + private normalizeTabGroupName(name?: string): string | undefined { + if (!name) return undefined; + const trimmed = name.trim(); + if (!trimmed) return undefined; + // Keep the title short for stable UI rendering in Chrome's tab strip. + return trimmed.slice(0, 80); + } + + /** + * Build a temporary MV3 extension that auto-groups managed tabs under a fixed title. + * This is only used for local Chromium launches. + */ + private createTabGroupExtension(groupTitle: string): string { + this.cleanupTabGroupExtension(); + + const extensionDir = mkdtempSync(path.join(os.tmpdir(), 'agent-browser-tab-group-')); + const manifest = { + manifest_version: 3, + name: 'Agent Browser Tab Grouper', + version: '1.0.0', + permissions: ['tabs', 'tabGroups'], + host_permissions: [''], + background: { + service_worker: 'service-worker.js', + }, + content_scripts: [ + { + matches: [''], + js: ['content-script.js'], + run_at: 'document_start', + match_about_blank: true, + }, + ], + }; + + const serviceWorker = `const GROUP_TITLE = ${JSON.stringify(groupTitle)}; +const MESSAGE_TYPE = 'agent-browser-manage-tab'; + +async function findGroupId(windowId) { + const tabs = await chrome.tabs.query({ windowId }); + const checkedGroupIds = new Set(); + for (const tab of tabs) { + if (typeof tab.groupId !== 'number' || tab.groupId < 0 || checkedGroupIds.has(tab.groupId)) { + continue; + } + checkedGroupIds.add(tab.groupId); + try { + const group = await chrome.tabGroups.get(tab.groupId); + if (group.title === GROUP_TITLE) { + return tab.groupId; + } + } catch { + // Ignore stale group IDs and continue searching. + } + } + return null; +} + +async function styleGroup(groupId) { + await chrome.tabGroups.update(groupId, { + title: GROUP_TITLE, + color: 'blue', + collapsed: false, + }); +} + +async function ensureTabGrouped(tabId, windowId) { + let groupId = await findGroupId(windowId); + if (groupId === null) { + groupId = await chrome.tabs.group({ + tabIds: [tabId], + createProperties: { windowId }, + }); + await styleGroup(groupId); + return; + } + await chrome.tabs.group({ + groupId, + tabIds: [tabId], + }); + await styleGroup(groupId); +} + +chrome.runtime.onMessage.addListener((message, sender) => { + if (!message || message.type !== MESSAGE_TYPE) { + return; + } + const tabId = sender.tab?.id; + const windowId = sender.tab?.windowId; + if (typeof tabId !== 'number' || typeof windowId !== 'number') { + return; + } + ensureTabGrouped(tabId, windowId).catch(() => {}); +}); +`; + + const contentScript = `(() => { + try { + chrome.runtime.sendMessage({ type: 'agent-browser-manage-tab' }); + } catch { + // Ignore pages where extension messaging is unavailable. + } +})(); +`; + + writeFileSync(path.join(extensionDir, 'manifest.json'), JSON.stringify(manifest, null, 2)); + writeFileSync(path.join(extensionDir, 'service-worker.js'), serviceWorker); + writeFileSync(path.join(extensionDir, 'content-script.js'), contentScript); + + this.tabGroupExtensionDir = extensionDir; + return extensionDir; + } + + private cleanupTabGroupExtension(): void { + if (!this.tabGroupExtensionDir) return; + rmSync(this.tabGroupExtensionDir, { recursive: true, force: true }); + this.tabGroupExtensionDir = null; + } + // CDP profiling state private static readonly MAX_PROFILE_EVENTS = 5_000_000; private profilingActive: boolean = false; @@ -1588,14 +1717,17 @@ export class BrowserManager { async launch(options: LaunchCommand): Promise { // Determine CDP endpoint: prefer cdpUrl over cdpPort for flexibility const cdpEndpoint = options.cdpUrl ?? (options.cdpPort ? String(options.cdpPort) : undefined); - const hasExtensions = !!options.extensions?.length; + const configuredExtensions = options.extensions ? [...options.extensions] : []; const hasStorageState = !!options.storageState; + const explicitTabGroup = this.normalizeTabGroupName(options.tabGroup); + const requestedTabGroup = explicitTabGroup ?? DEFAULT_TAB_GROUP_NAME; + const tabGroupWasExplicit = explicitTabGroup !== undefined; - if (hasExtensions && cdpEndpoint) { + if (configuredExtensions.length > 0 && cdpEndpoint) { throw new Error('Extensions cannot be used with CDP connection'); } - if (hasStorageState && hasExtensions) { + if (hasStorageState && configuredExtensions.length > 0) { throw new Error( 'Storage state cannot be used with extensions (extensions require persistent context)' ); @@ -1646,6 +1778,42 @@ export class BrowserManager { } this.logStealthPolicy('launch policy', options.browser ?? 'chromium'); + let effectiveExtensions = configuredExtensions; + if (requestedTabGroup) { + const requestedBrowserType = options.browser ?? 'chromium'; + if (this.stealthConnectionKind !== 'local') { + if (tabGroupWasExplicit) { + const warning = `--tab-group "${requestedTabGroup}" is ignored in CDP/provider mode (requires local Chromium launch)`; + this.launchWarnings.push(warning); + console.error(`[WARN] ${warning}`); + } + } else if (requestedBrowserType !== 'chromium') { + if (tabGroupWasExplicit) { + const warning = `--tab-group is only supported in Chromium (requested: ${requestedBrowserType})`; + this.launchWarnings.push(warning); + console.error(`[WARN] ${warning}`); + } + } else if (options.headless === true) { + if (tabGroupWasExplicit) { + const warning = '--tab-group is ignored in headless mode'; + this.launchWarnings.push(warning); + console.error(`[WARN] ${warning}`); + } + } else if (hasStorageState) { + if (tabGroupWasExplicit) { + const warning = + '--tab-group is ignored when storage state is loaded via --state (extensions require persistent context)'; + this.launchWarnings.push(warning); + console.error(`[WARN] ${warning}`); + } + } else { + const tabGroupExtensionPath = this.createTabGroupExtension(requestedTabGroup); + effectiveExtensions = [...effectiveExtensions, tabGroupExtensionPath]; + } + } + + const hasExtensions = effectiveExtensions.length > 0; + if (options.downloadPath) { this.downloadPath = options.downloadPath; } @@ -1785,7 +1953,7 @@ export class BrowserManager { let context: BrowserContext; if (hasExtensions) { // Extensions require persistent context in a temp directory - const extPaths = options.extensions!.join(','); + const extPaths = effectiveExtensions.join(','); const session = process.env.AGENT_BROWSER_SESSION || 'default'; // Combine extension args with custom args and file access args const extArgs = [`--disable-extensions-except=${extPaths}`, `--load-extension=${extPaths}`]; @@ -3117,6 +3285,8 @@ export class BrowserManager { } } + this.cleanupTabGroupExtension(); + this.pages = []; this.contexts = []; this.cdpEndpoint = null; diff --git a/src/daemon.ts b/src/daemon.ts index 117df33..9685f77 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -464,6 +464,7 @@ export async function startDaemon(options?: { colorSchemeEnv === 'no-preference' ? colorSchemeEnv : undefined; + const tabGroup = process.env.AGENT_BROWSER_TAB_GROUP?.trim(); const launchOptions = { id: 'auto', action: 'launch' as const, @@ -478,38 +479,54 @@ export async function startDaemon(options?: { allowFileAccess: allowFileAccess, colorScheme, + tabGroup: tabGroup && tabGroup.length > 0 ? tabGroup : undefined, autoStateFilePath: getSessionAutoStatePath(), }; let attachedToExistingBrowser = false; - try { - // Keep default CDP attempt minimal. Launch-only options like extensions - // are incompatible with CDP and can cause false-negative attach failures. - const cdpLaunchOptions = { - id: launchOptions.id, - action: launchOptions.action, - cdpPort: 9333, - ignoreHTTPSErrors: launchOptions.ignoreHTTPSErrors, - colorScheme: launchOptions.colorScheme, - userAgent: launchOptions.userAgent, - }; - await manager.launch({ - ...cdpLaunchOptions, - }); - attachedToExistingBrowser = true; - if (process.env.AGENT_BROWSER_DEBUG === '1') { - console.error('[DEBUG] Auto-launch connected via default CDP port 9333'); + if (launchOptions.tabGroup) { + try { + await manager.launch(launchOptions); + attachedToExistingBrowser = true; + if (process.env.AGENT_BROWSER_DEBUG === '1') { + console.error('[DEBUG] Auto-launch started local Chromium with --tab-group'); + } + } catch (error) { + if (process.env.AGENT_BROWSER_DEBUG === '1') { + const message = error instanceof Error ? error.message : String(error); + console.error(`[DEBUG] Local launch with --tab-group failed: ${message}`); + } } - } catch (error) { - if (process.env.AGENT_BROWSER_DEBUG === '1') { - const message = error instanceof Error ? error.message : String(error); - console.error( - `[DEBUG] Default CDP port 9333 unavailable, trying auto-connect discovery: ${message}` - ); + } else { + try { + // Keep default CDP attempt minimal. Launch-only options like extensions + // are incompatible with CDP and can cause false-negative attach failures. + const cdpLaunchOptions = { + id: launchOptions.id, + action: launchOptions.action, + cdpPort: 9333, + ignoreHTTPSErrors: launchOptions.ignoreHTTPSErrors, + colorScheme: launchOptions.colorScheme, + userAgent: launchOptions.userAgent, + }; + await manager.launch({ + ...cdpLaunchOptions, + }); + attachedToExistingBrowser = true; + if (process.env.AGENT_BROWSER_DEBUG === '1') { + console.error('[DEBUG] Auto-launch connected via default CDP port 9333'); + } + } catch (error) { + if (process.env.AGENT_BROWSER_DEBUG === '1') { + const message = error instanceof Error ? error.message : String(error); + console.error( + `[DEBUG] Default CDP port 9333 unavailable, trying auto-connect discovery: ${message}` + ); + } } } - if (!attachedToExistingBrowser) { + if (!attachedToExistingBrowser && !launchOptions.tabGroup) { try { await manager.launch({ id: launchOptions.id, @@ -532,6 +549,11 @@ export async function startDaemon(options?: { } if (!attachedToExistingBrowser) { + if (launchOptions.tabGroup) { + throw new Error( + 'Failed to launch local Chromium with tab grouping. Check Chromium availability and extension policy settings.' + ); + } throw new Error( 'Project policy requires using your existing browser. Could not connect to CDP at localhost:9333 and auto-discovery also failed.' ); diff --git a/src/protocol.test.ts b/src/protocol.test.ts index 4a34c97..ff387bf 100644 --- a/src/protocol.test.ts +++ b/src/protocol.test.ts @@ -16,6 +16,17 @@ describe('parseCommand', () => { expect((result.command as any).stealth).toBeUndefined(); } }); + + it('should parse launch command with tabGroup', () => { + const result = parseCommand( + cmd({ id: '1', action: 'launch', headless: false, tabGroup: 'Agent Browser Stealth' }) + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('launch'); + expect(result.command.tabGroup).toBe('Agent Browser Stealth'); + } + }); }); describe('navigation', () => { diff --git a/src/protocol.ts b/src/protocol.ts index 0eb9cf0..ad1d916 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -51,6 +51,7 @@ const launchSchema = baseCommandSchema.extend({ allowFileAccess: z.boolean().optional(), colorScheme: z.enum(['light', 'dark', 'no-preference']).optional(), downloadPath: z.string().optional(), + tabGroup: z.string().min(1).optional(), storageState: z.string().optional(), allowedDomains: z.array(z.string()).optional(), actionPolicy: z.string().optional(), diff --git a/src/types.ts b/src/types.ts index 19d27b2..3406b5b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,7 @@ export interface LaunchCommand extends BaseCommand { allowFileAccess?: boolean; // Enable file:// URL access and cross-origin file requests colorScheme?: 'light' | 'dark' | 'no-preference'; // Persistent color scheme override downloadPath?: string; // Directory for browser downloads (Playwright's downloadsPath) + tabGroup?: string; // Chromium local-launch only: auto-group agent tabs under this title allowedDomains?: string[]; actionPolicy?: string; confirmActions?: string[];