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. 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).

Expand Down
4 changes: 4 additions & 0 deletions docs/specs/dor-browser.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <url>` 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. 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

**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-<hex>` browser nobody could see or close.
Expand Down
31 changes: 28 additions & 3 deletions dor/src/commands/browser-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', '--hide-scrollbars', '--allowed-domains',
]);

/** 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. */
Expand Down Expand Up @@ -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'
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
38 changes: 38 additions & 0 deletions dor/test/browser-viewport.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,44 @@ 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'],
['--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 = [];
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 = {
Expand Down
Loading