From a5a53af121956daa0be0f1c95c6ff11c9e8c86ad Mon Sep 17 00:00:00 2001 From: Ned Date: Wed, 21 Oct 2026 04:29:14 -0700 Subject: [PATCH 1/2] Preserve browser launch options through internal CLI calls --- docs/specs/dor-browser.md | 2 +- docs/specs/dor-browser.rationale.md | 4 ++++ dor/src/commands/browser-cli.ts | 31 +++++++++++++++++++++--- dor/test/browser-viewport.test.mjs | 37 +++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/docs/specs/dor-browser.md b/docs/specs/dor-browser.md index 782de341e..c279f228e 100644 --- a/docs/specs/dor-browser.md +++ b/docs/specs/dor-browser.md @@ -111,7 +111,7 @@ Source of truth: `lib/src/components/wall/AgentBrowserScreenModal.tsx`, `offered **Must preserve the current device-pixel ratio when DPR is omitted.** A ratio the provider cannot apply fails before changing dimensions; Playwright accepts only its context's current ratio. **Never turn an observed Playwright DPR into an explicit request for future contexts.** Presets describe viewport geometry, not devices. -**Must apply initial dimensions before the destination page's first script runs**, for managed CLI, GUI and Tool launches. Deferred `pane-sync` launches start at a fixed size until placement. **Must engage sync when resolving that preset**, sending the actual pane dimensions on first attach. A failed initialization never navigates at a silently substituted size. **Must apply a renderer and viewport chosen together to the new renderer.** +**Must apply initial dimensions before the destination page's first script runs**, for managed CLI, GUI and Tool launches. **Must preserve explicit launch configuration through preparation and stream discovery** (rationale). Deferred `pane-sync` starts fixed until placement. **Must engage sync when resolving that preset**, sending the actual pane dimensions on first attach. Failed initialization never navigates at a substituted size. **Must apply a renderer and viewport chosen together to the new renderer.** **Must query the active page's measured CSS dimensions and DPR**, never infer them from the pane or a screenshot. `dor`'s sizing commands share the Display modal's serialized writes (`docs/specs/dor-cli.md` → Browser viewport control). diff --git a/docs/specs/dor-browser.rationale.md b/docs/specs/dor-browser.rationale.md index c3cf14b5b..2f61bd3d8 100644 --- a/docs/specs/dor-browser.rationale.md +++ b/docs/specs/dor-browser.rationale.md @@ -16,6 +16,10 @@ **Why a reuse is one intent.** Revealing the target and then asking for its mode and its URL separately sent `open ` in the same tick as a pop-out began its close/reopen, deterministically: the navigation raced the relaunch, which had already captured the old URL. +## Viewport presets + +Viewport preparation with agent-browser 0.27.0 (Windows, 2026-10-21): dropping `--config` after the blank navigation replaced the temporary profile with the occupied default profile, failing the DPR query. The native daemon compares launch settings on each call; preparation and stream discovery therefore keep the browser configuration consistent while leaving output controls on the user command. + ## Display Modal And Render Swaps **Why the iframe swap is eager.** A 1–3s daemon boot behind a modal that has already closed; and while the swap awaited `open`, a slow page held the iframe on screen for the whole load and a timed-out `open` dropped the swap silently — leaving an orphan `gui-` browser nobody could see or close. diff --git a/dor/src/commands/browser-cli.ts b/dor/src/commands/browser-cli.ts index 04b53e71b..5f6a7d427 100644 --- a/dor/src/commands/browser-cli.ts +++ b/dor/src/commands/browser-cli.ts @@ -231,6 +231,30 @@ const AGENT_BROWSER_BOOLEAN_FLAGS = new Set([ '--confirm-interactive', '--no-auto-dialog', '--verbose', '-v', '--quiet', '-q', '--debug', '--restore', ]); +// Each native CLI call can reconfigure the daemon. Changing these options +// between the blank page and the viewport writes can replace its browser. +// Output flags stay on the user's command: quiet/boundaries would hide the DPR. +const AGENT_BROWSER_LAUNCH_FLAGS = new Set([ + '--config', '--profile', '--executable-path', '--extension', '--args', '--proxy', '--proxy-bypass', + '--user-agent', '--allow-file-access', '--headed', '--ignore-https-errors', '--color-scheme', + '--download-path', '--engine', '--enable', '--init-script', +]); + +/** The same launch settings must accompany every internal native call. */ +function agentBrowserLaunchOptions(args: string[]): string[] { + const options: string[] = []; + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]!; + if (arg === '--') break; + const name = arg.split('=', 1)[0]!; + const start = i; + if (AGENT_BROWSER_VALUE_FLAGS.has(name) && !arg.includes('=')) i += 1; + else if (AGENT_BROWSER_BOOLEAN_FLAGS.has(name) && ['true', 'false'].includes(args[i + 1] ?? '')) i += 1; + if (AGENT_BROWSER_LAUNCH_FLAGS.has(name)) options.push(...args.slice(start, i + 1)); + } + return options; +} + /** Build a blank navigation with every launch option preserved. Unknown flags * are refused: guessing their arity could replace a flag value and let the * destination page run before its viewport is ready. */ @@ -429,6 +453,7 @@ export async function runBrowserCli(d: BrowserCliDescriptor, args: string[], opt ? exec(binaryPath ?? binary, argv, cwd, envOverride) : cwd === undefined ? exec(binaryPath ?? binary, argv) : exec(binaryPath ?? binary, argv, cwd); + const nativeLaunchOptions = d.provider === 'agent-browser' ? agentBrowserLaunchOptions(rest) : []; let result: BrowserExecResult; let prebound = false; const explicitDevice = d.provider === 'agent-browser' @@ -445,11 +470,11 @@ export async function runBrowserCli(d: BrowserCliDescriptor, args: string[], opt if (blank.exitCode !== 0) return fail(`Could not prepare the agent-browser viewport before navigation: ${blank.stderr.trim() || `agent-browser exited ${blank.exitCode}`}`); let dpr = launchViewport.dpr; if (dpr === undefined) { - const ratio = await run([...sessionArgs, 'eval', 'window.devicePixelRatio']); + const ratio = await run([...sessionArgs, ...nativeLaunchOptions, 'eval', 'window.devicePixelRatio']); dpr = ratio.exitCode === 0 ? parseNativeDpr(ratio.stdout) : undefined; if (dpr === undefined) return fail('Could not measure agent-browser DPR before navigation'); } - const viewport = await run([...sessionArgs, 'set', 'viewport', String(launchViewport.width), String(launchViewport.height), String(dpr)]); + const viewport = await run([...sessionArgs, ...nativeLaunchOptions, 'set', 'viewport', String(launchViewport.width), String(launchViewport.height), String(dpr)]); if (viewport.exitCode !== 0) return fail(`Could not set the agent-browser viewport before navigation: ${viewport.stderr.trim() || `agent-browser exited ${viewport.exitCode}`}`); } const playwrightOpen = d.provider === 'playwright' && command === 'open' && launchViewport !== undefined && !explicitDevice; @@ -499,7 +524,7 @@ export async function runBrowserCli(d: BrowserCliDescriptor, args: string[], opt if (result.exitCode === 0 && mayBind && binding.session !== undefined && !(client instanceof Error) && !prebound) { try { const statusArgs = d.streamStatus?.(binding.session); - const wsPort = statusArgs === undefined ? undefined : parseStreamPort((await run(statusArgs)).stdout); + const wsPort = statusArgs === undefined ? undefined : parseStreamPort((await run([...statusArgs, ...nativeLaunchOptions])).stdout); await client.browserSurface({ provider: d.provider, key: flags.key, diff --git a/dor/test/browser-viewport.test.mjs b/dor/test/browser-viewport.test.mjs index 4654db6cc..692f96345 100644 --- a/dor/test/browser-viewport.test.mjs +++ b/dor/test/browser-viewport.test.mjs @@ -103,6 +103,43 @@ test('agent-browser preparation preserves valued flags and only replaces the des assert.deepEqual(argsSeen[3].slice(2), ['--proxy', 'http://localhost:7890', 'open', 'http://localhost:5173', '--headers', '{"Authorization":"token"}']); }); +for (const flags of [ + ['--config', '/tmp/browser.json', '--profile=/tmp/browser profile'], + ['--headed', 'false', '--args', '--disable-gpu', '--proxy=http://localhost:7890', '--allow-file-access'], +]) test('viewport preparation keeps browser launch options: ' + flags[0], async () => { + const calls = []; + const result = await runCli(['agent-browser', 'open', 'http://localhost:5173', ...flags, '--quiet'], { + env: { PWD: process.cwd() }, + client: { + resolveBrowser: async () => ({ binding: { session: 'dormouse.1.default' }, fresh: true, + initialViewport: { mode: 'fixed', width: 1440, height: 900 }, launchViewport: { width: 1440, height: 900 } }), + browserSurface: async () => ({}), + }, + execAgentBrowser: async (_binary, args) => { + calls.push(args); + return { exitCode: 0, stdout: args.includes('eval') ? '2\n' : '', stderr: '' }; + }, + }); + assert.equal(result.exitCode, 0, result.stderr); + assert.deepEqual(calls[1], ['--session', 'dormouse.1.default', ...flags, 'eval', 'window.devicePixelRatio']); + assert.deepEqual(calls[2], ['--session', 'dormouse.1.default', ...flags, 'set', 'viewport', '1440', '900', '2']); + assert.deepEqual(calls[3], ['--session', 'dormouse.1.default', 'open', 'http://localhost:5173', ...flags, '--quiet']); + assert.deepEqual(calls[4], ['--session', 'dormouse.1.default', 'stream', 'status', '--json', ...flags]); +}); + +test('stream discovery preserves custom configuration after a non-navigation command', async () => { + const calls = []; + const result = await runCli(['agent-browser', '--config=/tmp/browser.json', 'eval', 'window.devicePixelRatio'], { + client: { + resolveBrowser: async () => ({ binding: { session: 'dormouse.1.default' }, fresh: false }), + browserSurface: async () => ({}), + }, + execAgentBrowser: async (_binary, args) => { calls.push(args); return { exitCode: 0, stdout: '1', stderr: '' }; }, + }); + assert.equal(result.exitCode, 0, result.stderr); + assert.deepEqual(calls[1], ['--session', 'dormouse.1.default', 'stream', 'status', '--json', '--config=/tmp/browser.json']); +}); + test('open-target sugar ignores a proxy flag value', async () => { const argsSeen = []; const options = { From 10167f5854153f52770cbec11032385d49f68a8e Mon Sep 17 00:00:00 2001 From: Ned Date: Wed, 21 Oct 2026 04:38:30 -0700 Subject: [PATCH 2/2] Preserve newer browser launch restrictions during setup --- docs/specs/dor-browser.rationale.md | 2 +- dor/src/commands/browser-cli.ts | 2 +- dor/test/browser-viewport.test.mjs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/specs/dor-browser.rationale.md b/docs/specs/dor-browser.rationale.md index 2f61bd3d8..56d5a10de 100644 --- a/docs/specs/dor-browser.rationale.md +++ b/docs/specs/dor-browser.rationale.md @@ -18,7 +18,7 @@ ## Viewport presets -Viewport preparation with agent-browser 0.27.0 (Windows, 2026-10-21): dropping `--config` after the blank navigation replaced the temporary profile with the occupied default profile, failing the DPR query. The native daemon compares launch settings on each call; preparation and stream discovery therefore keep the browser configuration consistent while leaving output controls on the user command. +Viewport preparation with agent-browser 0.27.0 (Windows, 2026-10-21): dropping `--config` after the blank navigation replaced the temporary profile with the occupied default profile, failing the DPR query. The native daemon compares launch settings on each call; preparation and stream discovery therefore keep the browser configuration consistent while leaving output controls on the user command. The [0.39.1 launch hash](https://github.com/vercel-labs/agent-browser/blob/1165952d74ceed98fdc2dd97190ad41f44dd0e24/cli/src/native/actions.rs#L293-L330) also includes scrollbar and domain restrictions (checked 2026-10-21). ## Display Modal And Render Swaps diff --git a/dor/src/commands/browser-cli.ts b/dor/src/commands/browser-cli.ts index 5f6a7d427..e3cb954e8 100644 --- a/dor/src/commands/browser-cli.ts +++ b/dor/src/commands/browser-cli.ts @@ -237,7 +237,7 @@ const AGENT_BROWSER_BOOLEAN_FLAGS = new Set([ const AGENT_BROWSER_LAUNCH_FLAGS = new Set([ '--config', '--profile', '--executable-path', '--extension', '--args', '--proxy', '--proxy-bypass', '--user-agent', '--allow-file-access', '--headed', '--ignore-https-errors', '--color-scheme', - '--download-path', '--engine', '--enable', '--init-script', + '--download-path', '--engine', '--enable', '--init-script', '--hide-scrollbars', '--allowed-domains', ]); /** The same launch settings must accompany every internal native call. */ diff --git a/dor/test/browser-viewport.test.mjs b/dor/test/browser-viewport.test.mjs index 692f96345..c5a20fad2 100644 --- a/dor/test/browser-viewport.test.mjs +++ b/dor/test/browser-viewport.test.mjs @@ -105,6 +105,7 @@ test('agent-browser preparation preserves valued flags and only replaces the des for (const flags of [ ['--config', '/tmp/browser.json', '--profile=/tmp/browser profile'], + ['--profile', '/tmp/browser', '--hide-scrollbars', 'true', '--allowed-domains=localhost'], ['--headed', 'false', '--args', '--disable-gpu', '--proxy=http://localhost:7890', '--allow-file-access'], ]) test('viewport preparation keeps browser launch options: ' + flags[0], async () => { const calls = [];