add --color-scheme flag for persistent dark/light mode (#528)

Fixes #519. Playwright defaults `colorScheme` to `light` on all new contexts, overriding the browser/OS dark mode setting. This is especially disruptive in CDP mode, where every reconnection resets the scheme. The `set media dark` command also didn't persist its choice to new tabs or pages.

- Add `--color-scheme <dark|light|no-preference>` flag, config key (`colorScheme`), and env var (`AGENT_BROWSER_COLOR_SCHEME`)
- Store the preference in `BrowserManager` and automatically apply it to all new contexts (via Playwright's context option) and all new pages (via `page.emulateMedia` in `setupPageTracking`)
- `set media dark/light` now also persists its choice for subsequent pages and tabs
This commit is contained in:
Chris Tate
2026-02-23 01:50:17 -06:00
committed by GitHub
parent 467b830974
commit 12d79e4428
14 changed files with 112 additions and 4 deletions
+1
View File
@@ -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 <port\|url>` | 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 <scheme>` | Color scheme: `dark`, `light`, `no-preference` (or `AGENT_BROWSER_COLOR_SCHEME` env) |
| `--config <path>` | Use a custom config file (or `AGENT_BROWSER_CONFIG` env) |
| `--debug` | Debug output |
+1
View File
@@ -1864,6 +1864,7 @@ mod tests {
cli_proxy_bypass: false,
cli_allow_file_access: false,
annotate: false,
color_scheme: None,
}
}
+13
View File
@@ -33,6 +33,7 @@ pub struct Config {
pub auto_connect: Option<bool>,
pub headers: Option<String>,
pub annotate: Option<bool>,
pub color_scheme: Option<String>,
}
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<Option<String>> {
"--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<String>,
pub annotate: bool,
pub color_scheme: Option<String>,
// 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<String> {
"--provider",
"--device",
"--session-name",
"--color-scheme",
"--config",
];
+19 -2
View File
@@ -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)
+3
View File
@@ -2062,6 +2062,7 @@ Options:
--headed Show browser window (not headless)
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
--auto-connect Auto-discover and connect to running Chrome
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
--session-name <name> Auto-save/restore session state (cookies, localStorage)
--config <path> 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
+16
View File
@@ -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:
<tr><td><code>--headed</code></td><td>Show browser window</td></tr>
<tr><td><code>{"--cdp <port|url>"}</code></td><td>CDP connection (port or WebSocket URL)</td></tr>
<tr><td><code>--auto-connect</code></td><td>Auto-discover and connect to running Chrome</td></tr>
<tr><td><code>--color-scheme &lt;scheme&gt;</code></td><td>Persistent color scheme (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>
<tr><td><code>--debug</code></td><td>Debug output</td></tr>
</tbody>
</table>
+7 -1
View File
@@ -125,7 +125,13 @@ agent-browser set geo <lat> <lng> # Set geolocation
agent-browser set offline [on|off] # Toggle offline mode
agent-browser set headers <json> # Extra HTTP headers
agent-browser set credentials <u> <p> # 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
+1
View File
@@ -72,6 +72,7 @@ Every CLI flag can be set in the config file using its camelCase equivalent:
<tr><td><code>allowFileAccess</code></td><td><code>--allow-file-access</code></td><td>boolean</td></tr>
<tr><td><code>cdp</code></td><td><code>--cdp</code></td><td>string</td></tr>
<tr><td><code>autoConnect</code></td><td><code>--auto-connect</code></td><td>boolean</td></tr>
<tr><td><code>colorScheme</code></td><td><code>--color-scheme</code></td><td>string (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>
<tr><td><code>headers</code></td><td><code>--headers</code></td><td>string (JSON)</td></tr>
</tbody>
</table>
+13
View File
@@ -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
+3
View File
@@ -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 });
}
+25 -1
View File
@@ -97,6 +97,15 @@ export class BrowserManager {
private refMap: RefMap = {};
private lastSnapshot: string = '';
private scopedHeaderRoutes: Map<string, (route: Route) => Promise<void>> = 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;
+8
View File
@@ -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(),
});
}
+1
View File
@@ -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(),
});
+1
View File
@@ -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;
}