diff --git a/docs/specs/dor-browser.md b/docs/specs/dor-browser.md index 085578669..f12bcdb4e 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. **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 size managed CLI, GUI and Tool browsers before destination scripts run.** **Must preserve launch settings during preparation and stream discovery; apply `--state` once, on the blank page** (rationale). Deferred `pane-sync` starts fixed until placement. **Must engage sync when resolving that preset**, sending 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 56d5a10de..fa5d31c8d 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. 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). +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. Repeating `--state` on the destination navigation started another browser at 1254×620 instead of the prepared 1440×900; applying it to the blank page once retains the loaded state and viewport. 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 7803d5263..8ad15136a 100644 --- a/dor/src/commands/browser-cli.ts +++ b/dor/src/commands/browser-cli.ts @@ -255,21 +255,25 @@ function agentBrowserLaunchOptions(args: string[]): string[] { return options; } -/** Build a blank navigation with every launch option preserved. Unknown flags +/** Apply saved state to the blank page only: repeating --state starts a fresh + * browser and loses the prepared viewport. Unknown flags * are refused: guessing their arity could replace a flag value and let the * destination page run before its viewport is ready. */ -function blankNavigationArgs(args: string[]): string[] | Error { +function agentBrowserNavigation(args: string[]): { blank: string[]; destination: string[] } | Error { const result = [...args]; const verbIndex = nativeCommandIndex('agent-browser', result); if (verbIndex === undefined || !['open', 'goto', 'navigate'].includes(result[verbIndex]!)) return new Error('No navigation command to prepare'); const positions: number[] = []; + const statePositions = new Set(); for (let i = 0; i < result.length; i += 1) { const arg = result[i] ?? ''; if (i === verbIndex) continue; if (arg.startsWith('-')) { const [name] = arg.split('=', 1); if (AGENT_BROWSER_VALUE_FLAGS.has(name)) { + const flagIndex = i; if (!arg.includes('=')) { if (i + 1 >= result.length) return new Error(`${name} requires a value`); i += 1; } + if (name === '--state') { statePositions.add(flagIndex); statePositions.add(i); } } else if (AGENT_BROWSER_BOOLEAN_FLAGS.has(name)) { if (result[i + 1] === 'true' || result[i + 1] === 'false') i += 1; } else return new Error(`Cannot prepare viewport with unknown agent-browser option '${arg}'`); @@ -279,7 +283,7 @@ function blankNavigationArgs(args: string[]): string[] | Error { } if (positions.length !== 1) return new Error('Cannot determine the agent-browser navigation URL for viewport preparation'); result[positions[0]!] = 'about:blank'; - return result; + return { blank: result, destination: args.filter((_, i) => !statePositions.has(i)) }; } function hasExplicitAgentBrowserDevice(args: string[], env: Record): boolean { @@ -462,10 +466,12 @@ export async function runBrowserCli(d: BrowserCliDescriptor, args: string[], opt const sessionArgs = binding.session ? spec.sessionArgs(binding.session) : []; const launching = fresh === true && d.provider === 'agent-browser' && d.navigationVerbs.has(command ?? '') && launchViewport !== undefined && !explicitDevice; + let destinationArgs = rest; if (launching) { - const blankArgs = blankNavigationArgs(rest); - if (blankArgs instanceof Error) return fail(blankArgs.message); - const blank = await run([...sessionArgs, ...blankArgs]); + const navigation = agentBrowserNavigation(rest); + if (navigation instanceof Error) return fail(navigation.message); + destinationArgs = navigation.destination; + const blank = await run([...sessionArgs, ...navigation.blank]); 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) { @@ -511,7 +517,7 @@ export async function runBrowserCli(d: BrowserCliDescriptor, args: string[], opt } result = await run([...sessionArgs, ...navigation.goto], undefined, 'inherit'); } else { - result = await run([...sessionArgs, ...rest], launchEnv, 'inherit'); + result = await run([...sessionArgs, ...destinationArgs], launchEnv, 'inherit'); } } catch (error) { return isMissingBinaryError(error) ? fail(d.missingBinaryMessage(binary)) : fail(errorMessage(error)); diff --git a/dor/test/browser-viewport.test.mjs b/dor/test/browser-viewport.test.mjs index c5a20fad2..c7863bf86 100644 --- a/dor/test/browser-viewport.test.mjs +++ b/dor/test/browser-viewport.test.mjs @@ -169,6 +169,43 @@ test('fresh agent-browser goto and navigate prepare before destination scripts', } }); +for (const stateArgs of [['--state', '/tmp/saved state.json'], ['--state=/tmp/state.json']]) { + test('saved state is applied once before managed navigation: ' + stateArgs[0], async () => { + const calls = []; + let viewport; + let destinationViewport; + const result = await runCli(['agent-browser', ...stateArgs, 'open', 'http://localhost:5173'], { + 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); + // Native --state starts a clean browser, clearing its previous viewport. + if (args.some(arg => arg === '--state' || arg.startsWith('--state='))) viewport = undefined; + if (args.includes('viewport')) viewport = args.slice(args.indexOf('viewport') + 1); + if (args.includes('http://localhost:5173')) destinationViewport = viewport; + return { exitCode: 0, stdout: args.includes('eval') ? '1\n' : '', stderr: '' }; + }, + }); + assert.equal(result.exitCode, 0, result.stderr); + assert.deepEqual(calls[0], ['--session', 'dormouse.1.default', ...stateArgs, 'open', 'about:blank']); + assert.deepEqual(destinationViewport, ['1440', '900', '1']); + assert.deepEqual(calls[3], ['--session', 'dormouse.1.default', 'open', 'http://localhost:5173']); + }); +} + +test('reusing a browser keeps an explicit state request on the native command', async () => { + const calls = []; + await runCli(['agent-browser', 'open', 'http://localhost:5173', '--state', '/tmp/state.json'], { + client: { resolveBrowser: async () => ({ binding: { session: 'dormouse.1.default' }, fresh: false }), browserSurface: async () => ({}) }, + execAgentBrowser: async (_binary, args) => { calls.push(args); return { exitCode: 0, stdout: '', stderr: '' }; }, + }); + assert.deepEqual(calls[0], ['--session', 'dormouse.1.default', 'open', 'http://localhost:5173', '--state', '/tmp/state.json']); +}); + test('unknown agent-browser option fails before destination navigation', async () => { const argsSeen = []; const options = {