From 48ea8ebc42d540b4be48fabd4da6e6ead281fe11 Mon Sep 17 00:00:00 2001 From: GraciousGazelles <55840159+GraciousGazelles@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:46:51 +1100 Subject: [PATCH 1/2] Document and verify native Browser viewport contract --- .../src/browser_playwright_evidence_test.mjs | 62 +++++++++++++++++++ codex-rs/browser-computer-use/src/lib.rs | 40 +++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/codex-rs/browser-computer-use/src/browser_playwright_evidence_test.mjs b/codex-rs/browser-computer-use/src/browser_playwright_evidence_test.mjs index 677edf873f76..49c71f63c72e 100644 --- a/codex-rs/browser-computer-use/src/browser_playwright_evidence_test.mjs +++ b/codex-rs/browser-computer-use/src/browser_playwright_evidence_test.mjs @@ -174,6 +174,68 @@ test("provider records selector and wheel settled trail in its manifest", async } }); +test("provider applies a per-call CSS viewport and scroll and returns measured screenshot evidence", async () => { + const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-browser-provider-viewport-")); + const server = http.createServer((_request, response) => { + response.end("
viewport fixture
"); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + const request = { + tool: "browser_observe", + arguments: { + url: `http://127.0.0.1:${address.port}/`, + scope: "viewport_and_page", + view: { viewportWidth: 390, viewportHeight: 844, scrollY: 120 }, + }, + }; + const child = spawn(process.execPath, [fileURLToPath(new URL("./browser_playwright_provider.mjs", import.meta.url))], { + env: { + ...process.env, + CODEX_BROWSER_PLAYWRIGHT_STATE_DIR: stateDir, + CODEX_BROWSER_PLAYWRIGHT_ISOLATION: "shared", + CODEX_BROWSER_PLAYWRIGHT_HEADLESS: "1", + CODEX_BROWSER_PLAYWRIGHT_VIEWPORT_WIDTH: "1280", + CODEX_BROWSER_PLAYWRIGHT_VIEWPORT_HEIGHT: "720", + CODEX_BROWSER_PLAYWRIGHT_CAPTURE_MODE: "viewport", + CODEX_BROWSER_PLAYWRIGHT_ARTIFACT_POLICY: "off", + }, + stdio: ["pipe", "pipe", "inherit"], + }); + child.stdin.end(JSON.stringify(request)); + try { + const output = await new Promise((resolve, reject) => { + let text = ""; + child.stdout.on("data", (chunk) => { text += chunk; }); + child.on("error", reject); + child.on("close", (code, signal) => { + if (code !== 0) { + reject(new Error(`provider exited with code ${code} signal ${signal}: ${text}`)); + } else { + resolve(text); + } + }); + }); + const response = JSON.parse(output); + assert.equal(response.success, true, response.contentItems?.[0]?.text); + const text = response.contentItems[0].text; + assert.match(text, /viewport: 390x844 scroll=0,120 document=\d+x\d+/); + assert.match(text, /capture_metadata: \{"requestedViewport":\{"width":390,"height":844\},"effectiveViewport":\{"width":390,"height":844\},"clientViewport":\{"width":390,"height":844\}/); + + const images = response.contentItems.filter(({ type }) => type === "inputImage"); + assert.equal(images.length, 1); + assert.equal(images[0].detail, "high"); + assert.match(images[0].imageUrl, /^data:image\/png;base64,/); + const png = Buffer.from(images[0].imageUrl.slice("data:image/png;base64,".length), "base64"); + assert.deepEqual([...png.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]); + assert.equal(png.readUInt32BE(16), 390); + assert.equal(png.readUInt32BE(20), 844); + } finally { + await new Promise((resolve) => server.close(resolve)); + await fs.rm(stateDir, { recursive: true, force: true }); + } +}); + test("provider lets a first navigate action replace stale persisted state", async () => { const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-browser-provider-navigate-")); const server = http.createServer((_request, response) => { diff --git a/codex-rs/browser-computer-use/src/lib.rs b/codex-rs/browser-computer-use/src/lib.rs index 805b8264898f..b0ae02fe8ae8 100644 --- a/codex-rs/browser-computer-use/src/lib.rs +++ b/codex-rs/browser-computer-use/src/lib.rs @@ -74,12 +74,12 @@ pub fn configured_browser_dynamic_tools_for_codex_home(codex_home: &Path) -> Vec vec![ browser_dynamic_tool( BROWSER_OBSERVE_TOOL_NAME, - "Capture the current browser viewport as a model-visible screenshot.", + "Capture the current browser viewport as a model-visible screenshot. Optional view.viewportWidth and view.viewportHeight set this call's CSS viewport size; view.scrollY sets its initial vertical scroll. The response reports measured geometry. This is not mobile-device emulation.", "non_mutating", ), browser_dynamic_tool( BROWSER_STEP_TOOL_NAME, - "Perform bounded browser actions, then return a fresh browser screenshot.", + "Perform bounded browser actions, then return a fresh browser screenshot. Optional view.viewportWidth and view.viewportHeight set this call's CSS viewport size; view.scrollY sets its initial vertical scroll. The response reports measured geometry. This is not mobile-device emulation.", "mutating", ), ] @@ -92,6 +92,29 @@ fn browser_dynamic_tool(name: &str, description: &str, mutation_class: &str) -> description: description.to_string(), input_schema: json!({ "type": "object", + "properties": { + "view": { + "type": "object", + "description": "Per-call CSS viewport and vertical scroll. The response reports measured effective viewport and scroll geometry; compare it with the requested values. This does not change the user agent, enable touch input, or change device scale factor.", + "properties": { + "viewportWidth": { + "type": "integer", + "minimum": 1, + "description": "CSS viewport width in pixels for this call." + }, + "viewportHeight": { + "type": "integer", + "minimum": 1, + "description": "CSS viewport height in pixels for this call." + }, + "scrollY": { + "type": "number", + "description": "Vertical page scroll position in CSS pixels to restore before capture or actions." + } + }, + "additionalProperties": true + } + }, "additionalProperties": true }), defer_loading: false, @@ -1524,6 +1547,19 @@ mod tests { .and_then(|capability| capability.family.as_deref()) == Some(COMPUTER_USE_ADAPTER_BROWSER) })); + for tool in tools { + let view = &tool.input_schema["properties"]["view"]; + assert_eq!(view["properties"]["viewportWidth"]["type"], "integer"); + assert_eq!(view["properties"]["viewportWidth"]["minimum"], 1); + assert_eq!(view["properties"]["viewportHeight"]["type"], "integer"); + assert_eq!(view["properties"]["viewportHeight"]["minimum"], 1); + assert_eq!(view["properties"]["scrollY"]["type"], "number"); + assert_eq!(tool.input_schema["additionalProperties"], true); + assert_eq!(view["additionalProperties"], true); + assert!(view["description"] + .as_str() + .is_some_and(|description| description.contains("does not change the user agent"))); + } } #[test] From 07929f1ae706d58cdfb5b8693347a7aaaf3e0fc5 Mon Sep 17 00:00:00 2001 From: GraciousGazelles <55840159+GraciousGazelles@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:53:30 +1100 Subject: [PATCH 2/2] codex: format viewport schema test --- codex-rs/browser-computer-use/src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/codex-rs/browser-computer-use/src/lib.rs b/codex-rs/browser-computer-use/src/lib.rs index b0ae02fe8ae8..d64caa28965b 100644 --- a/codex-rs/browser-computer-use/src/lib.rs +++ b/codex-rs/browser-computer-use/src/lib.rs @@ -1556,9 +1556,11 @@ mod tests { assert_eq!(view["properties"]["scrollY"]["type"], "number"); assert_eq!(tool.input_schema["additionalProperties"], true); assert_eq!(view["additionalProperties"], true); - assert!(view["description"] - .as_str() - .is_some_and(|description| description.contains("does not change the user agent"))); + assert!( + view["description"].as_str().is_some_and( + |description| description.contains("does not change the user agent") + ) + ); } }