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
+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;
}