Merge remote-tracking branch 'upstream/main'

# Conflicts:
#	README.md
#	cli/src/connection.rs
#	cli/src/flags.rs
#	cli/src/main.rs
#	src/browser.ts
#	src/protocol.ts
This commit is contained in:
leeguooooo
2026-02-25 10:06:20 +09:00
22 changed files with 1467 additions and 59 deletions
+25 -25
View File
@@ -998,41 +998,41 @@ async function handleWait(command: WaitCommand, browser: BrowserManager): Promis
async function handleScroll(command: ScrollCommand, browser: BrowserManager): Promise<Response> {
const page = browser.getPage();
let deltaX = command.x ?? 0;
let deltaY = command.y ?? 0;
const hasExplicitDelta = command.x !== undefined || command.y !== undefined;
if (command.direction) {
const amount = command.amount ?? 100;
switch (command.direction) {
case 'up':
deltaY = -amount;
break;
case 'down':
deltaY = amount;
break;
case 'left':
deltaX = -amount;
break;
case 'right':
deltaX = amount;
break;
}
}
if (command.selector) {
const element = browser.getLocator(command.selector);
await element.scrollIntoViewIfNeeded();
if (command.x !== undefined || command.y !== undefined) {
if (hasExplicitDelta || deltaX !== 0 || deltaY !== 0) {
await element.evaluate(
(el, { x, y }) => {
el.scrollBy(x ?? 0, y ?? 0);
el.scrollBy(x, y);
},
{ x: command.x, y: command.y }
{ x: deltaX, y: deltaY }
);
}
} else {
// Scroll the page
let deltaX = command.x ?? 0;
let deltaY = command.y ?? 0;
if (command.direction) {
const amount = command.amount ?? 100;
switch (command.direction) {
case 'up':
deltaY = -amount;
break;
case 'down':
deltaY = amount;
break;
case 'left':
deltaX = -amount;
break;
case 'right':
deltaX = amount;
break;
}
}
await page.evaluate(`window.scrollBy(${deltaX}, ${deltaY})`);
}
+39 -1
View File
@@ -16,7 +16,7 @@ import {
} from 'playwright-core';
import path from 'node:path';
import os from 'node:os';
import { existsSync, mkdirSync, rmSync, readFileSync } from 'node:fs';
import { existsSync, mkdirSync, rmSync, readFileSync, statSync } 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';
@@ -160,6 +160,7 @@ export class BrowserManager {
private contextTimezoneId: string | undefined = undefined;
private contextHeaders: Record<string, string> | undefined = undefined;
private contextUserAgent: string | undefined = undefined;
private downloadPath: string | null = null;
/**
* Set the persistent color scheme preference.
@@ -1567,6 +1568,17 @@ export class BrowserManager {
}
this.logStealthPolicy('launch policy', options.browser ?? 'chromium');
if (options.downloadPath) {
this.downloadPath = options.downloadPath;
}
if (this.downloadPath && (cdpEndpoint || options.autoConnect)) {
const warning =
"--download-path is ignored when connecting via CDP or auto-connect (downloads use the remote browser's configuration)";
this.launchWarnings.push(warning);
console.error(`[WARN] ${warning}`);
}
if (cdpEndpoint) {
await this.connectViaCDP(cdpEndpoint);
return;
@@ -1578,6 +1590,13 @@ export class BrowserManager {
}
// Cloud browser providers require explicit opt-in via -p flag or AGENT_BROWSER_PROVIDER env var
// -p flag takes precedence over AGENT_BROWSER_PROVIDER.
if (this.downloadPath && provider) {
const warning =
"--download-path is ignored when using a cloud provider (downloads use the remote browser's configuration)";
this.launchWarnings.push(warning);
console.error(`[WARN] ${warning}`);
}
if (provider === 'browserbase') {
await this.connectToBrowserbase();
return;
@@ -1593,6 +1612,23 @@ export class BrowserManager {
return;
}
if (this.downloadPath) {
const resolved = path.resolve(this.downloadPath);
const stat = statSync(resolved, { throwIfNoEntry: false });
if (stat && !stat.isDirectory()) {
throw new Error(`Download path is not a directory: ${resolved}`);
}
if (!stat) {
try {
mkdirSync(resolved, { recursive: true });
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
throw new Error(`Cannot create download directory '${resolved}': ${msg}`);
}
}
this.downloadPath = resolved;
}
const browserType = options.browser ?? 'chromium';
if (hasExtensions && browserType !== 'chromium') {
throw new Error('Extensions are only supported in Chromium');
@@ -1682,6 +1718,7 @@ export class BrowserManager {
...(options.proxy && { proxy: options.proxy }),
ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false,
...(this.colorScheme && { colorScheme: this.colorScheme }),
...(this.downloadPath && { downloadsPath: this.downloadPath }),
}
);
this.isPersistentContext = true;
@@ -1692,6 +1729,7 @@ export class BrowserManager {
executablePath: options.executablePath,
...(chromeChannel && { channel: chromeChannel }),
args: baseArgs,
...(this.downloadPath && { downloadsPath: this.downloadPath }),
});
this.cdpEndpoint = null;
+1
View File
@@ -50,6 +50,7 @@ const launchSchema = baseCommandSchema.extend({
ignoreHTTPSErrors: z.boolean().optional(),
allowFileAccess: z.boolean().optional(),
colorScheme: z.enum(['light', 'dark', 'no-preference']).optional(),
downloadPath: z.string().optional(),
storageState: z.string().optional(),
});
+1
View File
@@ -31,6 +31,7 @@ export interface LaunchCommand extends BaseCommand {
ignoreHTTPSErrors?: boolean;
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)
// Auto-load state file for session persistence
autoStateFilePath?: string;
}