diff --git a/README.md b/README.md index 7b0dfba..9d4618a 100644 --- a/README.md +++ b/README.md @@ -463,6 +463,7 @@ This is useful for multimodal AI models that can reason about visual layout, unl | `--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 (or `AGENT_BROWSER_AUTO_CONNECT` env) | +| `--color-scheme ` | Color scheme: `dark`, `light`, `no-preference` (or `AGENT_BROWSER_COLOR_SCHEME` env) | | `--config ` | Use a custom config file (or `AGENT_BROWSER_CONFIG` env) | | `--debug` | Debug output | diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 1f6307c..bdd9965 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -1864,6 +1864,7 @@ mod tests { cli_proxy_bypass: false, cli_allow_file_access: false, annotate: false, + color_scheme: None, } } diff --git a/cli/src/flags.rs b/cli/src/flags.rs index 14ced63..21b4049 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -33,6 +33,7 @@ pub struct Config { pub auto_connect: Option, pub headers: Option, pub annotate: Option, + pub color_scheme: Option, } impl Config { @@ -66,6 +67,7 @@ impl Config { auto_connect: other.auto_connect.or(self.auto_connect), headers: other.headers.or(self.headers), annotate: other.annotate.or(self.annotate), + color_scheme: other.color_scheme.or(self.color_scheme), } } } @@ -129,6 +131,7 @@ fn extract_config_path(args: &[String]) -> Option> { "--provider", "--device", "--session-name", + "--color-scheme", ]; let mut i = 0; while i < args.len() { @@ -199,6 +202,7 @@ pub struct Flags { pub auto_connect: bool, pub session_name: Option, pub annotate: bool, + pub color_scheme: Option, // Track which launch-time options were explicitly passed via CLI // (as opposed to being set only via environment variables) @@ -278,6 +282,8 @@ pub fn parse_flags(args: &[String]) -> Flags { .or(config.session_name), annotate: env_var_is_truthy("AGENT_BROWSER_ANNOTATE") || config.annotate.unwrap_or(false), + color_scheme: env::var("AGENT_BROWSER_COLOR_SCHEME").ok() + .or(config.color_scheme), cli_executable_path: false, cli_extensions: false, cli_profile: false, @@ -425,6 +431,12 @@ pub fn parse_flags(args: &[String]) -> Flags { flags.annotate = val; if consumed { i += 1; } } + "--color-scheme" => { + if let Some(s) = args.get(i + 1) { + flags.color_scheme = Some(s.clone()); + i += 1; + } + } "--config" => { // Already handled by load_config(); skip the value i += 1; @@ -468,6 +480,7 @@ pub fn clean_args(args: &[String]) -> Vec { "--provider", "--device", "--session-name", + "--color-scheme", "--config", ]; diff --git a/cli/src/main.rs b/cli/src/main.rs index 185a922..73d6c08 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -358,6 +358,10 @@ fn main() { launch_cmd["ignoreHTTPSErrors"] = json!(true); } + if let Some(ref cs) = flags.color_scheme { + launch_cmd["colorScheme"] = json!(cs); + } + let err = match send_command(launch_cmd, &flags.session) { Ok(resp) if resp.success => None, Ok(resp) => Some( @@ -440,6 +444,10 @@ fn main() { launch_cmd["ignoreHTTPSErrors"] = json!(true); } + if let Some(ref cs) = flags.color_scheme { + launch_cmd["colorScheme"] = json!(cs); + } + let err = match send_command(launch_cmd, &flags.session) { Ok(resp) if resp.success => None, Ok(resp) => Some( @@ -461,12 +469,16 @@ fn main() { // Launch with cloud provider if -p flag is set if let Some(ref provider) = flags.provider { - let launch_cmd = json!({ + let mut launch_cmd = json!({ "id": gen_id(), "action": "launch", "provider": provider }); + if let Some(ref cs) = flags.color_scheme { + launch_cmd["colorScheme"] = json!(cs); + } + let err = match send_command(launch_cmd, &flags.session) { Ok(resp) if resp.success => None, Ok(resp) => Some( @@ -494,7 +506,8 @@ fn main() { || flags.proxy.is_some() || flags.args.is_some() || flags.user_agent.is_some() - || flags.allow_file_access) + || flags.allow_file_access + || flags.color_scheme.is_some()) && flags.cdp.is_none() && flags.provider.is_none() { @@ -556,6 +569,10 @@ fn main() { launch_cmd["allowFileAccess"] = json!(true); } + if let Some(ref cs) = flags.color_scheme { + launch_cmd["colorScheme"] = json!(cs); + } + match send_command(launch_cmd, &flags.session) { Ok(resp) if !resp.success => { // Launch command failed (e.g., invalid state file, profile error) diff --git a/cli/src/output.rs b/cli/src/output.rs index 82fced5..9e62539 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -2062,6 +2062,7 @@ Options: --headed Show browser window (not headless) --cdp Connect via CDP (Chrome DevTools Protocol) --auto-connect Auto-discover and connect to running Chrome + --color-scheme Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME) --session-name Auto-save/restore session state (cookies, localStorage) --config Use a custom config file (or AGENT_BROWSER_CONFIG env) --debug Debug output @@ -2103,6 +2104,7 @@ Environment: AGENT_BROWSER_PROVIDER Browser provider (ios, browserbase, kernel, browseruse) AGENT_BROWSER_AUTO_CONNECT Auto-discover and connect to running Chrome AGENT_BROWSER_ALLOW_FILE_ACCESS Allow file:// URLs to access local files + AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference) AGENT_BROWSER_STREAM_PORT Enable WebSocket streaming on port (e.g., 9223) AGENT_BROWSER_IOS_DEVICE Default iOS device name AGENT_BROWSER_IOS_UDID Default iOS device UDID @@ -2126,6 +2128,7 @@ Examples: agent-browser wait --load networkidle # Wait for slow pages to load agent-browser --cdp 9222 snapshot # Connect via CDP port agent-browser --auto-connect snapshot # Auto-discover running Chrome + agent-browser --color-scheme dark open example.com # Dark mode agent-browser --profile ~/.myapp open example.com # Persistent profile agent-browser --session-name myapp open example.com # Auto-save/restore state diff --git a/docs/src/app/cdp-mode/page.mdx b/docs/src/app/cdp-mode/page.mdx index 7edec00..dae316d 100644 --- a/docs/src/app/cdp-mode/page.mdx +++ b/docs/src/app/cdp-mode/page.mdx @@ -60,6 +60,21 @@ This is useful when: - You want a zero-configuration connection to your existing browser - You don't want to track which port Chrome is using +## Color scheme + +Playwright overrides the browser's color scheme to `light` by default when connecting via CDP. Use `--color-scheme` to set a persistent preference: + +```bash +agent-browser --cdp 9222 --color-scheme dark open https://example.com +agent-browser --cdp 9222 snapshot # stays in dark mode +``` + +Or set it globally via config or environment variable: + +```bash +AGENT_BROWSER_COLOR_SCHEME=dark agent-browser --cdp 9222 open https://example.com +``` + ## Use cases This enables control of: @@ -93,6 +108,7 @@ This enables control of: --headedShow browser window {"--cdp "}CDP connection (port or WebSocket URL) --auto-connectAuto-discover and connect to running Chrome + --color-scheme <scheme>Persistent color scheme (dark, light, no-preference) --debugDebug output diff --git a/docs/src/app/commands/page.mdx b/docs/src/app/commands/page.mdx index 5e9e793..6227821 100644 --- a/docs/src/app/commands/page.mdx +++ b/docs/src/app/commands/page.mdx @@ -125,7 +125,13 @@ agent-browser set geo # Set geolocation agent-browser set offline [on|off] # Toggle offline mode agent-browser set headers # Extra HTTP headers agent-browser set credentials

# HTTP basic auth -agent-browser set media [dark|light] # Emulate color scheme +agent-browser set media [dark|light] # Emulate color scheme (persists for session) +``` + +Use `--color-scheme` for persistent dark/light mode across all commands: + +```bash +agent-browser --color-scheme dark open https://example.com ``` ## Cookies & storage diff --git a/docs/src/app/configuration/page.mdx b/docs/src/app/configuration/page.mdx index ff93d10..723618f 100644 --- a/docs/src/app/configuration/page.mdx +++ b/docs/src/app/configuration/page.mdx @@ -72,6 +72,7 @@ Every CLI flag can be set in the config file using its camelCase equivalent: allowFileAccess--allow-file-accessboolean cdp--cdpstring autoConnect--auto-connectboolean + colorScheme--color-schemestring (dark, light, no-preference) headers--headersstring (JSON) diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index 78538df..834f3e3 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -182,6 +182,19 @@ agent-browser --auto-connect snapshot agent-browser --cdp 9222 snapshot ``` +### Color Scheme (Dark Mode) + +```bash +# Persistent dark mode via flag (applies to all pages and new tabs) +agent-browser --color-scheme dark open https://example.com + +# Or via environment variable +AGENT_BROWSER_COLOR_SCHEME=dark agent-browser open https://example.com + +# Or set during session (persists for subsequent commands) +agent-browser set media dark +``` + ### Visual Browser (Debugging) ```bash diff --git a/src/actions.ts b/src/actions.ts index 3af3d07..4017940 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -2069,6 +2069,9 @@ async function handleEmulateMedia( reducedMotion: command.reducedMotion, forcedColors: command.forcedColors, }); + if (command.colorScheme) { + browser.setColorScheme(command.colorScheme); + } return successResponse(command.id, { emulated: true }); } diff --git a/src/browser.ts b/src/browser.ts index f78e83d..7e46a18 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -97,6 +97,15 @@ export class BrowserManager { private refMap: RefMap = {}; private lastSnapshot: string = ''; private scopedHeaderRoutes: Map Promise> = new Map(); + private colorScheme: 'light' | 'dark' | 'no-preference' | null = null; + + /** + * Set the persistent color scheme preference. + * Applied automatically to all new pages and contexts. + */ + setColorScheme(scheme: 'light' | 'dark' | 'no-preference' | null): void { + this.colorScheme = scheme; + } // CDP session for screencast and input injection private cdpSession: CDPSession | null = null; @@ -252,7 +261,9 @@ export class BrowserManager { if (this.contexts.length > 0) { context = this.contexts[this.contexts.length - 1]; } else if (this.browser) { - context = await this.browser.newContext(); + context = await this.browser.newContext({ + ...(this.colorScheme && { colorScheme: this.colorScheme }), + }); context.setDefaultTimeout(60000); this.contexts.push(context); this.setupContextTracking(context); @@ -1140,6 +1151,10 @@ export class BrowserManager { } } + if (options.colorScheme) { + this.colorScheme = options.colorScheme; + } + if (cdpEndpoint) { await this.connectViaCDP(cdpEndpoint); return; @@ -1224,6 +1239,7 @@ export class BrowserManager { userAgent: options.userAgent, ...(options.proxy && { proxy: options.proxy }), ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false, + ...(this.colorScheme && { colorScheme: this.colorScheme }), } ); this.isPersistentContext = true; @@ -1240,6 +1256,7 @@ export class BrowserManager { userAgent: options.userAgent, ...(options.proxy && { proxy: options.proxy }), ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false, + ...(this.colorScheme && { colorScheme: this.colorScheme }), }); this.isPersistentContext = true; } else { @@ -1325,6 +1342,7 @@ export class BrowserManager { storageState, ...(options.proxy && { proxy: options.proxy }), ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false, + ...(this.colorScheme && { colorScheme: this.colorScheme }), }); } @@ -1555,6 +1573,10 @@ export class BrowserManager { * Set up console, error, and close tracking for a page */ private setupPageTracking(page: Page): void { + if (this.colorScheme) { + page.emulateMedia({ colorScheme: this.colorScheme }).catch(() => {}); + } + page.on('console', (msg) => { this.consoleMessages.push({ type: msg.type(), @@ -1642,6 +1664,7 @@ export class BrowserManager { const context = await this.browser.newContext({ viewport: viewport === undefined ? { width: 1280, height: 720 } : viewport, + ...(this.colorScheme && { colorScheme: this.colorScheme }), }); context.setDefaultTimeout(60000); this.contexts.push(context); @@ -2381,6 +2404,7 @@ export class BrowserManager { this.kernelApiKey = null; this.isPersistentContext = false; this.activePageIndex = 0; + this.colorScheme = null; this.refMap = {}; this.lastSnapshot = ''; this.frameCallback = null; diff --git a/src/daemon.ts b/src/daemon.ts index 8a331df..8fe416e 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -418,6 +418,13 @@ export async function startDaemon(options?: { const ignoreHTTPSErrors = process.env.AGENT_BROWSER_IGNORE_HTTPS_ERRORS === '1'; const allowFileAccess = process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1'; + const colorSchemeEnv = process.env.AGENT_BROWSER_COLOR_SCHEME; + const colorScheme = + colorSchemeEnv === 'dark' || + colorSchemeEnv === 'light' || + colorSchemeEnv === 'no-preference' + ? colorSchemeEnv + : undefined; await manager.launch({ id: 'auto', action: 'launch' as const, @@ -431,6 +438,7 @@ export async function startDaemon(options?: { proxy, ignoreHTTPSErrors: ignoreHTTPSErrors, allowFileAccess: allowFileAccess, + colorScheme, autoStateFilePath: getSessionAutoStatePath(), }); } diff --git a/src/protocol.ts b/src/protocol.ts index dfc9dc8..d5a7ee0 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -49,6 +49,7 @@ const launchSchema = baseCommandSchema.extend({ provider: z.string().optional(), ignoreHTTPSErrors: z.boolean().optional(), allowFileAccess: z.boolean().optional(), + colorScheme: z.enum(['light', 'dark', 'no-preference']).optional(), profile: z.string().optional(), storageState: z.string().optional(), }); diff --git a/src/types.ts b/src/types.ts index 4cd7992..f539705 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,6 +31,7 @@ export interface LaunchCommand extends BaseCommand { provider?: string; ignoreHTTPSErrors?: boolean; allowFileAccess?: boolean; // Enable file:// URL access and cross-origin file requests + colorScheme?: 'light' | 'dark' | 'no-preference'; // Persistent color scheme override // Auto-load state file for session persistence autoStateFilePath?: string; }