diff --git a/.github/docker/Dockerfile.ci b/.github/docker/Dockerfile.ci index ebf4a4d13..2be739d4d 100644 --- a/.github/docker/Dockerfile.ci +++ b/.github/docker/Dockerfile.ci @@ -70,11 +70,11 @@ RUN curl --retry 5 --retry-delay 5 --retry-connrefused -fsSL https://bun.sh/inst RUN npm i -g @anthropic-ai/claude-code # Playwright system deps (Chromium) — needed for browse E2E tests -RUN npx playwright install-deps chromium +RUN npx playwright-core install-deps chromium # Linux has neither Helvetica nor Arial. make-pdf's print CSS stacks fall back # to Liberation Sans (metric-compatible Arial clone, SIL OFL 1.1) so PDFs don't -# render in DejaVu Sans. playwright install-deps happens to pull this in today, +# render in DejaVu Sans. playwright-core install-deps happens to pull this in today, # but the dep is implicit and could change — install explicitly so upgrades # can't silently regress rendering. # @@ -100,12 +100,12 @@ RUN bun install --frozen-lockfile && rm -rf /tmp/* # Install Playwright Chromium to a shared location accessible by all users ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers -RUN npx playwright install chromium \ +RUN npx playwright-core install chromium \ && chmod -R a+rX /opt/playwright-browsers # Verify everything works RUN bun --version && node --version && claude --version && jq --version && gh --version \ - && npx playwright --version \ + && npx playwright-core --version \ && fc-match "Liberation Sans" | grep -qi "Liberation" \ || (echo "ERROR: fonts-liberation not installed — make-pdf PDFs will render in DejaVu Sans" && exit 1) diff --git a/.github/workflows/make-pdf-gate.yml b/.github/workflows/make-pdf-gate.yml index 9a3d13538..69c4d7eb1 100644 --- a/.github/workflows/make-pdf-gate.yml +++ b/.github/workflows/make-pdf-gate.yml @@ -66,7 +66,7 @@ jobs: fc-match -f '%{family[0]}\t%{color}\n' ':lang=und-zsye:charset=1F600' || true - name: Install Playwright Chromium - run: bunx playwright install chromium + run: bunx playwright-core install chromium - name: Build binaries run: bun run build diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index b8c56f7ca..5cdfbb8ec 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -382,6 +382,7 @@ export class BrowserManager { // BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory. // Extensions only work in headed mode, so we use an off-screen window. const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR; + if (extensionsDir) assertHeadedBrowserProvider(); const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth'); const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()]; let useHeadless = true; @@ -1587,6 +1588,7 @@ export class BrowserManager { * If step 2 fails → return error, headless browser untouched */ async handoff(message: string): Promise { + assertHeadedBrowserProvider(); if (this.connectionMode === 'headed' || this.isHeaded) { return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`; } diff --git a/browse/src/cli.ts b/browse/src/cli.ts index dee7b642f..93e4f17da 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -118,7 +118,7 @@ interface ServerState { serverPath: string; binaryVersion?: string; mode?: 'launched' | 'headed'; - /** Hash of (proxyUrl + headed flag), used by D2 daemon-mismatch check. */ + /** Hash of proxy, headed mode, and browser-provider intent, used by daemon-mismatch checks. */ configHash?: string; /** Xvfb child PID for cleanup on disconnect. */ xvfbPid?: number; @@ -431,8 +431,8 @@ async function ensureServer(flags?: GlobalFlags): Promise { // hint. No silent restart — that would drop tab state, cookies, and // logged-in sessions without warning. if (desiredHash && state.configHash && state.configHash !== desiredHash) { - console.error(`[browse] existing daemon has different config (proxy/headed mismatch).`); - console.error(`[browse] run 'browse disconnect' first to apply --proxy/--headed.`); + console.error(`[browse] existing daemon has different config (browser provider, proxy, or headed mode).`); + console.error(`[browse] run 'browse disconnect' first to apply the selected browser configuration.`); process.exit(1); } // Same path: existing daemon is plain (no flags) but caller passes @@ -782,7 +782,7 @@ export interface GlobalFlags { proxyUrl: string | null; /** Whether --headed was passed. */ headed: boolean; - /** Hash of (proxy + headed) for daemon-mismatch check. */ + /** Hash of proxy, headed mode, and browser-provider intent for daemon-mismatch checks. */ configHash: string; /** Redacted form of proxyUrl, safe for logs. */ redactedProxyUrl: string; @@ -842,7 +842,12 @@ export function extractGlobalFlags(rawArgs: string[], env: NodeJS.ProcessEnv): G args: out, proxyUrl: canonicalProxyUrl, headed, - configHash: computeConfigHash({ proxyUrl: canonicalProxyUrl, headed }), + configHash: computeConfigHash({ + proxyUrl: canonicalProxyUrl, + headed, + browserProvider: env.GSTACK_BROWSER_PROVIDER, + browserExecutable: env.GSTACK_CHROMIUM_PATH, + }), redactedProxyUrl: redactProxyUrl(canonicalProxyUrl), }; } diff --git a/browse/src/proxy-config.ts b/browse/src/proxy-config.ts index 161475825..e03f0f318 100644 --- a/browse/src/proxy-config.ts +++ b/browse/src/proxy-config.ts @@ -125,7 +125,7 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig { } /** - * Compute a stable hash of (proxyUrl + headed flag) for daemon-mismatch + * Compute a stable hash of proxy, headed mode, and browser-provider intent for daemon-mismatch * detection (D2). The hash is deterministic across CLI invocations on the * same machine and survives daemon restarts via the state file. * @@ -135,9 +135,18 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig { export function computeConfigHash(opts: { proxyUrl: string | null | undefined; headed: boolean; + browserProvider?: string | null; + browserExecutable?: string | null; }): string { const proxyKey = canonicalizeProxyUrl(opts.proxyUrl); - const input = JSON.stringify({ proxy: proxyKey, headed: opts.headed }); + const browserProvider = opts.browserProvider || null; + const browserExecutable = browserProvider === "installed" ? opts.browserExecutable || null : null; + const input = JSON.stringify({ + proxy: proxyKey, + headed: opts.headed, + browserProvider, + browserExecutable, + }); return createHash('sha256').update(input).digest('hex').slice(0, 16); } diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index 4a847141d..626ba8794 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -355,11 +355,18 @@ export async function handleWriteCommand( } } catch (err: any) { // Enhanced error guidance: clicking