Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/specs/dor-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
2 changes: 1 addition & 1 deletion docs/specs/dor-browser.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 13 additions & 7 deletions dor/src/commands/browser-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>();
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}'`);
Expand All @@ -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<string, string | undefined>): boolean {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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));
Expand Down
37 changes: 37 additions & 0 deletions dor/test/browser-viewport.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading