From 00ca70544352ff053eb350b50358e8fcdff43ed7 Mon Sep 17 00:00:00 2001 From: shubh24 Date: Thu, 8 Oct 2026 01:04:37 -0700 Subject: [PATCH 1/3] feat: add Live View pointer UI example --- COOKBOOK_AUTHORED.json | 37 +- README.md | 1 + catalog.json | 112 +++++ docs/catalog.md | 5 +- ...examples-javascript-live-view-cursor-ui.md | 85 ++++ docs/topics/agents-and-human-handoff.md | 1 + docs/topics/testing-and-observability.md | 1 + examples/README.md | 4 +- .../live-view-cursor-ui/.env.example | 2 + .../javascript/live-view-cursor-ui/README.md | 41 ++ .../live-view-cursor-ui/package.json | 18 + .../javascript/live-view-cursor-ui/server.mjs | 359 +++++++++++++++ .../javascript/live-view-cursor-ui/ui/app.js | 232 ++++++++++ .../live-view-cursor-ui/ui/index.html | 120 +++++ .../live-view-cursor-ui/ui/styles.css | 419 ++++++++++++++++++ .../references/agents-and-human-handoff.md | 3 +- .../references/testing-and-observability.md | 3 +- 17 files changed, 1437 insertions(+), 6 deletions(-) create mode 100644 docs/recipes/examples-javascript-live-view-cursor-ui.md create mode 100644 examples/javascript/live-view-cursor-ui/.env.example create mode 100644 examples/javascript/live-view-cursor-ui/README.md create mode 100644 examples/javascript/live-view-cursor-ui/package.json create mode 100644 examples/javascript/live-view-cursor-ui/server.mjs create mode 100644 examples/javascript/live-view-cursor-ui/ui/app.js create mode 100644 examples/javascript/live-view-cursor-ui/ui/index.html create mode 100644 examples/javascript/live-view-cursor-ui/ui/styles.css diff --git a/COOKBOOK_AUTHORED.json b/COOKBOOK_AUTHORED.json index d681933..7b9615f 100644 --- a/COOKBOOK_AUTHORED.json +++ b/COOKBOOK_AUTHORED.json @@ -3,7 +3,7 @@ "files": [ { "path": "examples/README.md", - "sha256": "d4b80442ca66ab0f2c3a4d9666ff6b16bdfeed6aa4396ee76ebef5f3253ea63c", + "sha256": "757e05989f94ed1929ff954abd7292c389baa293e9befeda9f73c325bbf92cc2", "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." }, { @@ -11,6 +11,41 @@ "sha256": "f2397eaef401700a0baed905b587b31320715da4655b6edb876906f476d1e351", "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." }, + { + "path": "examples/javascript/live-view-cursor-ui/.env.example", + "sha256": "42b43bad0fa9275b332ccc4eb93bd4f05cc674ad8f219e0e410271d32ba32039", + "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." + }, + { + "path": "examples/javascript/live-view-cursor-ui/README.md", + "sha256": "4bf38408c44420ab9639be6e99659ee08fff86c49264c91ce449cb698d8fa1f6", + "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." + }, + { + "path": "examples/javascript/live-view-cursor-ui/package.json", + "sha256": "2dd68b36eab0409135d534fa1d9b6f2b157450c50f656258c0b4d85fd546b2d7", + "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." + }, + { + "path": "examples/javascript/live-view-cursor-ui/server.mjs", + "sha256": "f0d08d27c23d4ad133cfef3882c0c7b9e32f4108ddd422fa5c535fe6007da371", + "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." + }, + { + "path": "examples/javascript/live-view-cursor-ui/ui/app.js", + "sha256": "4373ed332643025748a513ec1ec322218dc783fc7ae3a8ae48ebf679655ccc69", + "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." + }, + { + "path": "examples/javascript/live-view-cursor-ui/ui/index.html", + "sha256": "6286bc862bd28e322555a9522421962778bc94f3c25cf78bdc1e0cdf8a38052c", + "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." + }, + { + "path": "examples/javascript/live-view-cursor-ui/ui/styles.css", + "sha256": "0fb0c203ef6ef19ea62b59630d5feab754a06788c896a9dd44f91e5d99d91672", + "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." + }, { "path": "examples/python/agentic-kyb/.env.example", "sha256": "f2397eaef401700a0baed905b587b31320715da4655b6edb876906f476d1e351", diff --git a/README.md b/README.md index 94fa47e..ae6e5f7 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ This cookbook brings together Browserbase's public templates, complete projects, | Extract structured product data with Stagehand | [Python](examples/python/amazon-product-scraping/) or [TypeScript](examples/typescript/amazon-product-scraping/) | | Save a login and handle MFA | [Authentication recipes](docs/topics/authentication.md) | | Pause an agent for a person | [Human-in-the-loop app](examples/typescript/agent-with-human-in-loop/) | +| Style Live View pointers for people and agents | [JavaScript pointer UI](examples/javascript/live-view-cursor-ui/) | | Add Browserbase to an agent framework | [Integrations](integrations/README.md) | | Study a complete consumer browser agent | [Browsie](projects/browsie/) | diff --git a/catalog.json b/catalog.json index c83f013..b2999c8 100644 --- a/catalog.json +++ b/catalog.json @@ -332,6 +332,118 @@ "verification": "source-inspected", "working_directory": "examples/go/hackernews" }, + { + "access": "public", + "caveats": [ + "The pointer styling appears in the localhost wrapper, not in a direct Browserbase Live View URL.", + "The sample routes input and pointer events for the main browser tab only; new remote tabs and frames need more routing.", + "The local server gives its signed Live View URL to its own page; keep the server bound to loopback and review access before deployment." + ], + "collection": "examples", + "dependencies": { + "@browserbasehq/sdk": "2.18.0", + "@browserbasehq/stagehand": "4.0.2", + "dotenv": "^16.4.5", + "playwright-core": "1.55.0" + }, + "entrypoints": [ + "examples/javascript/live-view-cursor-ui/server.mjs", + "examples/javascript/live-view-cursor-ui/ui/app.js" + ], + "environment_setup_command": "test -f .env || cp .env.example .env", + "environment_template": "examples/javascript/live-view-cursor-ui/.env.example", + "environment_variables": [ + { + "default": null, + "name": "BROWSERBASE_API_KEY", + "provider": "Browserbase", + "purpose": "Creates and connects to the cloud browser session.", + "requirement": "required", + "safe_example": "", + "secret": true, + "setup_url": "https://www.browserbase.com/settings", + "validation": "Must be a valid Browserbase API key" + }, + { + "default": "4790", + "name": "LIVE_VIEW_UI_PORT", + "provider": "Recipe configuration", + "purpose": "Sets the localhost port for the pointer UI.", + "requirement": "optional", + "safe_example": "4790", + "secret": false, + "setup_url": null, + "validation": "Must be an available TCP port" + } + ], + "frameworks": [ + "Browserbase SDK", + "Stagehand", + "Playwright" + ], + "id": "examples-javascript-live-view-cursor-ui", + "languages": [ + "javascript" + ], + "lifecycle": "current", + "manifests": [ + "examples/javascript/live-view-cursor-ui/package.json" + ], + "path": "examples/javascript/live-view-cursor-ui", + "readme": "docs/recipes/examples-javascript-live-view-cursor-ui.md", + "run_commands": [ + "npm start" + ], + "search_aliases": [ + "live view cursor", + "agent pointer", + "playwright cursor", + "stagehand cursor" + ], + "setup_commands": [ + "npm install" + ], + "source": { + "kind": "cookbook-authored", + "path": "examples/javascript/live-view-cursor-ui" + }, + "summary": "Embed Browserbase Live View in a local page with matching human and agent pointers for Stagehand and Playwright actions.", + "tags": [], + "title": "Live View special pointers (JavaScript)", + "topics": [ + "agents-and-human-handoff", + "testing-and-observability" + ], + "upstream_readme": "examples/javascript/live-view-cursor-ui/README.md", + "verification": "live-tested", + "verification_evidence": [ + { + "command": "npm install --ignore-scripts --no-audit --no-fund --package-lock=false", + "date": "2026-10-08", + "kind": "install-verified", + "limits": "Installed inside a new recipe directory without a shared dependency tree. This did not run a browser session.", + "result": "passed", + "runtime": "Node.js 25.9.0 and npm on macOS arm64" + }, + { + "command": "LIVE_VIEW_UI_PORT=4792 npm start; node /private/tmp/live-view-cursor-smoke.mjs playwright; node /private/tmp/live-view-cursor-smoke.mjs stagehand", + "date": "2026-10-08", + "kind": "live-tested", + "limits": "One Browserbase SauceDemo session. Playwright emitted 6 pointer events and 3 clicks; Stagehand emitted 8 pointer events and 4 clicks. Both ended with the backpack in the cart. Human UI input and other browser clients were not retested in this checkout. The session was closed.", + "result": "passed", + "runtime": "Node.js 25.9.0 on macOS arm64; Browserbase SDK 2.18.0, Stagehand 4.0.2, Playwright Core 1.55.0" + }, + { + "command": "env -u BROWSERBASE_PROJECT_ID LIVE_VIEW_UI_PORT=4793 npm start", + "date": "2026-10-08", + "kind": "live-tested", + "limits": "The Browserbase and Stagehand session initialized with no project ID in the environment or launch options and was closed. This check did not rerun the Playwright or Stagehand cart actions.", + "result": "passed", + "runtime": "Node.js 25.9.0 on macOS arm64; Browserbase SDK 2.18.0 and Stagehand 4.0.2" + } + ], + "working_directory": "examples/javascript/live-view-cursor-ui" + }, { "access": "public", "caveats": [], diff --git a/docs/catalog.md b/docs/catalog.md index 9dd6828..aacc77f 100644 --- a/docs/catalog.md +++ b/docs/catalog.md @@ -12,9 +12,9 @@ Search locally with `python3 scripts/catalog.py search "persistent login" --lang - [Files and documents](topics/downloads-and-documents.md) (21). Upload files, retrieve downloads, capture PDFs, and parse documents. - [Extraction and research](topics/extraction-and-research.md) (54). Find pages and extract structured information from them. - [Forms and transactions](topics/forms-and-transactions.md) (28). Fill forms and prepare application, booking, and payment workflows. -- [Agents and human handoff](topics/agents-and-human-handoff.md) (27). Build browser agents and pause work for human input. +- [Agents and human handoff](topics/agents-and-human-handoff.md) (28). Build browser agents and pause work for human input. - [Integrations and orchestration](topics/integrations-and-orchestration.md) (32). Connect Browserbase to agent frameworks, services, and workflow engines. -- [Testing and observability](topics/testing-and-observability.md) (7). Test sites and inspect browser behavior, reliability, and performance. +- [Testing and observability](topics/testing-and-observability.md) (8). Test sites and inspect browser behavior, reliability, and performance. - [Commerce and travel](topics/commerce-and-travel.md) (14). Compare products, research travel, and inspect booking workflows. - [Business operations](topics/business-operations.md) (11). Automate research and operations across business domains. @@ -23,6 +23,7 @@ Search locally with `python3 scripts/catalog.py search "persistent login" --lang | Recipe | Language | Type | Status | Collection | | --- | --- | --- | --- | --- | | [Hackernews (Go)](recipes/examples-go-hackernews.md) | go | runnable example | current / public | examples | +| [Live View special pointers (JavaScript)](recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | | [Agentic KYB registry research (Python, Stagehand v4)](recipes/examples-python-agentic-kyb.md) | python | runnable example | current / public | examples | | [Amazon global price comparison (Python)](recipes/examples-python-amazon-global-price-comparison.md) | python | runnable example | current / public | examples | | [Amazon product scraping (Python)](recipes/examples-python-amazon-product-scraping.md) | python | runnable example | current / public | examples | diff --git a/docs/recipes/examples-javascript-live-view-cursor-ui.md b/docs/recipes/examples-javascript-live-view-cursor-ui.md new file mode 100644 index 0000000..bae0f0f --- /dev/null +++ b/docs/recipes/examples-javascript-live-view-cursor-ui.md @@ -0,0 +1,85 @@ +# Live View special pointers (JavaScript) + +Embed Browserbase Live View in a local page with matching human and agent pointers for Stagehand and Playwright actions. + +> [!CAUTION] +> Demo and reference code only. This recipe is not a vetted production implementation. Independently review it, obtain authorization, and validate security, privacy, compliance, cost, and site-term requirements before use. Use at your own risk. + +**Status:** current · public · live workflow tested. + +**Recipe type:** runnable example. +**LLM requirement:** required (derived from the inspected framework and configuration metadata). + +## Code and prerequisites + +- Working directory from the cookbook root: `examples/javascript/live-view-cursor-ui`. +- Languages: javascript. +- Frameworks: Browserbase SDK, Stagehand, Playwright. +- [Upstream setup and behavior](../../examples/javascript/live-view-cursor-ui/README.md). +- [Dependency manifest `examples/javascript/live-view-cursor-ui/package.json`](../../examples/javascript/live-view-cursor-ui/package.json). +- [Source `examples/javascript/live-view-cursor-ui/server.mjs`](../../examples/javascript/live-view-cursor-ui/server.mjs). +- [Source `examples/javascript/live-view-cursor-ui/ui/app.js`](../../examples/javascript/live-view-cursor-ui/ui/app.js). + +## Setup and run + +Run from this directory. Commands come from the package manifest, upstream setup guide, or inspected entrypoint. Configure credentials before starting. + +```sh +cd examples/javascript/live-view-cursor-ui +npm install +test -f .env || cp .env.example .env +``` + +Available launch commands are listed below. For applications with separate workers or servers, read the upstream guide for process order. A production `start` command may require a build first. + +```sh +npm start +``` + +## Environment + +[Environment template](../../examples/javascript/live-view-cursor-ui/.env.example) lists example configuration. Fill in your own values locally. + +### Required + +| Variable | Provider and purpose | Safe example | Validation and default | +| --- | --- | --- | --- | +| `BROWSERBASE_API_KEY` | [Browserbase](https://www.browserbase.com/settings). Creates and connects to the cloud browser session. Secret. | `` | Must be a valid Browserbase API key. No default. | + +### Optional + +| Variable | Provider and purpose | Safe example | Validation and default | +| --- | --- | --- | --- | +| `LIVE_VIEW_UI_PORT` | Recipe configuration. Sets the localhost port for the pointer UI. Non-secret. | `4790` | Must be an available TCP port. Default: `4790`. | + + +## Dependencies + +Declared runtime dependencies from [examples/javascript/live-view-cursor-ui/package.json](../../examples/javascript/live-view-cursor-ui/package.json). Alternate manifests may differ; use the documented setup path. + +| Package | Declared version | +| --- | --- | +| `@browserbasehq/sdk` | `2.18.0` | +| `@browserbasehq/stagehand` | `4.0.2` | +| `dotenv` | `^16.4.5` | +| `playwright-core` | `1.55.0` | + +## Caveats and verification + +Source and setup metadata were inspected. A scoped live workflow check is recorded below; only its stated command, date, result, and limits are verified. Other websites, accounts, permissions, costs, and configurations remain unverified. + +| Check | Date | Runtime | Command | Result and limits | +| --- | --- | --- | --- | --- | +| Clean install verified | 2026-10-08 | `Node.js 25.9.0 and npm on macOS arm64` | `npm install --ignore-scripts --no-audit --no-fund --package-lock=false` | passed. Installed inside a new recipe directory without a shared dependency tree. This did not run a browser session. | +| Live workflow tested | 2026-10-08 | `Node.js 25.9.0 on macOS arm64; Browserbase SDK 2.18.0, Stagehand 4.0.2, Playwright Core 1.55.0` | `LIVE_VIEW_UI_PORT=4792 npm start; node /private/tmp/live-view-cursor-smoke.mjs playwright; node /private/tmp/live-view-cursor-smoke.mjs stagehand` | passed. One Browserbase SauceDemo session. Playwright emitted 6 pointer events and 3 clicks; Stagehand emitted 8 pointer events and 4 clicks. Both ended with the backpack in the cart. Human UI input and other browser clients were not retested in this checkout. The session was closed. | +| Live workflow tested | 2026-10-08 | `Node.js 25.9.0 on macOS arm64; Browserbase SDK 2.18.0 and Stagehand 4.0.2` | `env -u BROWSERBASE_PROJECT_ID LIVE_VIEW_UI_PORT=4793 npm start` | passed. The Browserbase and Stagehand session initialized with no project ID in the environment or launch options and was closed. This check did not rerun the Playwright or Stagehand cart actions. | + +- The pointer styling appears in the localhost wrapper, not in a direct Browserbase Live View URL. +- The sample routes input and pointer events for the main browser tab only; new remote tabs and frames need more routing. +- The local server gives its signed Live View URL to its own page; keep the server bound to loopback and review access before deployment. + +## Provenance + +Cookbook-authored example. Its reviewed source is included at the local paths above. + +Related topics: [Agents and human handoff](../topics/agents-and-human-handoff.md), [Testing and observability](../topics/testing-and-observability.md). diff --git a/docs/topics/agents-and-human-handoff.md b/docs/topics/agents-and-human-handoff.md index bec65bd..340e739 100644 --- a/docs/topics/agents-and-human-handoff.md +++ b/docs/topics/agents-and-human-handoff.md @@ -6,6 +6,7 @@ Open a recipe guide for its working directory, commands, environment references, | Recipe | Language | Type | Status | Collection | | --- | --- | --- | --- | --- | +| [Live View special pointers (JavaScript)](../recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | | [Anthropic Claude browser agent (Python)](../recipes/examples-python-anthropic-cua.md) | python | runnable example | current / public | examples | | [Business lookup (Python)](../recipes/examples-python-business-lookup.md) | python | runnable example | current / public | examples | | [Cartesia form filling (Python)](../recipes/examples-python-cartesia-form-filling.md) | python | runnable example | current / public | examples | diff --git a/docs/topics/testing-and-observability.md b/docs/topics/testing-and-observability.md index bc9a785..f2d60c6 100644 --- a/docs/topics/testing-and-observability.md +++ b/docs/topics/testing-and-observability.md @@ -6,6 +6,7 @@ Open a recipe guide for its working directory, commands, environment references, | Recipe | Language | Type | Status | Collection | | --- | --- | --- | --- | --- | +| [Live View special pointers (JavaScript)](../recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | | [Agentic KYB registry research (Python, Stagehand v4)](../recipes/examples-python-agentic-kyb.md) | python | runnable example | current / public | examples | | [Cerebras docs checker (Python)](../recipes/examples-python-cerebras-docs-checker.md) | python | runnable example | current / public | examples | | [Smart fetch scraper (Python)](../recipes/examples-python-smart-fetch-scraper.md) | python | runnable example | current / public | examples | diff --git a/examples/README.md b/examples/README.md index 6191854..0eac5df 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # Browserbase examples -Start with a standalone example in Python, TypeScript, or Go. These templates cover Browserbase APIs, browser frameworks, Stagehand agents, authentication, extraction, and research. +Start with a standalone example in Python, JavaScript, TypeScript, or Go. These templates cover Browserbase APIs, browser frameworks, Stagehand agents, authentication, extraction, and research. ## Choose a starting point @@ -9,6 +9,7 @@ Start with a standalone example in Python, TypeScript, or Go. These templates co - [Saved sessions and MFA](../docs/topics/authentication.md). - [Extraction and research](../docs/topics/extraction-and-research.md). - [Human-in-the-loop agent app](typescript/agent-with-human-in-loop/). +- [Live View pointers for people and agents](javascript/live-view-cursor-ui/). ## Install the selected example @@ -31,6 +32,7 @@ All entries below were inspected at their pinned source revision. Live website b | Recipe | Language | Type | Status | Collection | | --- | --- | --- | --- | --- | | [Hackernews (Go)](../docs/recipes/examples-go-hackernews.md) | go | runnable example | current / public | examples | +| [Live View special pointers (JavaScript)](../docs/recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | | [Agentic KYB registry research (Python, Stagehand v4)](../docs/recipes/examples-python-agentic-kyb.md) | python | runnable example | current / public | examples | | [Amazon global price comparison (Python)](../docs/recipes/examples-python-amazon-global-price-comparison.md) | python | runnable example | current / public | examples | | [Amazon product scraping (Python)](../docs/recipes/examples-python-amazon-product-scraping.md) | python | runnable example | current / public | examples | diff --git a/examples/javascript/live-view-cursor-ui/.env.example b/examples/javascript/live-view-cursor-ui/.env.example new file mode 100644 index 0000000..a283cd3 --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/.env.example @@ -0,0 +1,2 @@ +BROWSERBASE_API_KEY=your-browserbase-api-key +LIVE_VIEW_UI_PORT=4790 diff --git a/examples/javascript/live-view-cursor-ui/README.md b/examples/javascript/live-view-cursor-ui/README.md new file mode 100644 index 0000000..c1a1a31 --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/README.md @@ -0,0 +1,41 @@ +# Special pointers in Browserbase Live View + +This standalone example puts a Browserbase Live View inside a local page. It draws the same black pointer with a white edge and blue glow for human and automated mouse actions. The local page sends human mouse and keyboard input to the Browserbase session through CDP. It draws the agent pointer from real mouse events in the remote page. + +> [!CAUTION] +> Demo and reference code only. Review the code and validate it for your site before production use. + +## Run + +Use Node.js 22.18 or newer. From the cookbook root: + +```sh +cd examples/javascript/live-view-cursor-ui +npm install +cp .env.example .env +``` + +Set `BROWSERBASE_API_KEY` in `.env`, then run: + +```sh +npm start +``` + +Open `http://127.0.0.1:4790` on the same computer. The server starts a 15-minute Browserbase session on [SauceDemo](https://www.saucedemo.com/). Move, click, scroll, or type in the Live View to test the human pointer. Select **Run Playwright** or **Run Stagehand** to sign in and add Sauce Labs Backpack to the cart. Each button uses the same agent pointer overlay. The Stagehand run uses a direct locator click if its model action does not add the item. Select **New session** to reset the demo after a session ends. + +The demo uses SauceDemo's published test account. It adds one item to a test cart. It does not place an order. Stagehand uses the Browserbase Model Gateway and makes a model call. The server releases the Browserbase session when it shuts down. Browserbase selects the project from the API key. + +## When the agent pointer moves + +The overlay receives `mousemove` and `mousedown` events from the remote page while an automated run is active. These commands move it: + +- Playwright `page.mouse.move()`, `page.mouse.click()`, `locator.hover()`, and `locator.click()`. +- Stagehand actions that move or click the mouse, including `stagehand.act()` when it uses a real mouse action and Stagehand locator clicks. + +`fill()`, text insertion, keyboard commands, `page.evaluate(() => element.click())`, and `dispatchEvent("click")` do not make a mouse path. They do not move the pointer. This example marks runs started by its two buttons as agent activity. If you add your own script, run its mouse actions inside the same active-run state or send their positions to the overlay. + +The direct Browserbase Live View URL keeps its normal pointer. The special pointers appear only in this local UI. The server binds to `127.0.0.1`; it sends the signed Live View URL to the local page, and it keeps the Browserbase API key on the server. + +## Change the target + +Change the `page.goto()` call in `createSession()` and the browser steps in `runAgent()` in [`server.mjs`](server.mjs). The viewer is set to 1280 × 800 pixels. Change both the server viewport and UI aspect ratio if you use another size. This example handles the main browser tab. A new remote tab or a frame inside the remote page needs its own input route and pointer listener. diff --git a/examples/javascript/live-view-cursor-ui/package.json b/examples/javascript/live-view-cursor-ui/package.json new file mode 100644 index 0000000..756a49b --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/package.json @@ -0,0 +1,18 @@ +{ + "name": "browserbase-live-view-special-cursors", + "version": "1.0.0", + "private": true, + "type": "module", + "engines": { + "node": ">=22.18.0" + }, + "scripts": { + "start": "node server.mjs" + }, + "dependencies": { + "@browserbasehq/sdk": "2.18.0", + "@browserbasehq/stagehand": "4.0.2", + "playwright-core": "1.55.0", + "dotenv": "^16.4.5" + } +} diff --git a/examples/javascript/live-view-cursor-ui/server.mjs b/examples/javascript/live-view-cursor-ui/server.mjs new file mode 100644 index 0000000..e88f4c7 --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/server.mjs @@ -0,0 +1,359 @@ +import "dotenv/config"; +import http from "node:http"; +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +import Browserbase from "@browserbasehq/sdk"; +import { browserbase, Stagehand } from "@browserbasehq/stagehand"; +import { chromium } from "playwright-core"; + +const apiKey = process.env.BROWSERBASE_API_KEY; +if (!apiKey) { + throw new Error("BROWSERBASE_API_KEY is required."); +} + +const bb = new Browserbase({ apiKey, maxRetries: 0, timeout: 30_000 }); +const uiDir = join(dirname(fileURLToPath(import.meta.url)), "ui"); +const port = Number(process.env.LIVE_VIEW_UI_PORT ?? 4790); +const viewport = { width: 1280, height: 800 }; +const clients = new Set(); +let current; +let agentRunning = false; +let inputQueue = Promise.resolve(); + +function emit(message) { + const line = `data: ${JSON.stringify(message)}\n\n`; + for (const client of clients) client.write(line); +} + +async function closeSession(session) { + if (!session) return; + try { + await session.stagehand?.close(); + } catch {} + try { + await session.stagehandBrowser?.close(); + } catch {} + try { + await session.playwrightBrowser?.close(); + } catch {} + try { + await bb.sessions.update(session.id, { status: "REQUEST_RELEASE" }); + } catch {} +} + +async function createSession() { + agentRunning = false; + emit({ type: "status", text: "Starting a new Browserbase session" }); + const old = current; + current = undefined; + await closeSession(old); + + const stagehandBrowser = await browserbase.launch({ + apiKey, + timeout: 900, + keepAlive: false, + browserSettings: { + recordSession: true, + viewport, + }, + }); + const sessionId = stagehandBrowser.sessionId; + let stagehand; + let playwrightBrowser; + try { + stagehand = await Stagehand.create({ browser: stagehandBrowser }); + const session = await bb.sessions.retrieve(sessionId); + if (!session.connectUrl) throw new Error("Browserbase did not return a connect URL"); + playwrightBrowser = await chromium.connectOverCDP(session.connectUrl); + const context = playwrightBrowser.contexts()[0]; + const page = context.pages()[0] ?? (await context.newPage()); + const cdp = await context.newCDPSession(page); + + await page.exposeFunction("__liveCursorFromPage", (point) => { + if (agentRunning && current?.id === sessionId) { + emit({ type: "agent-pointer", x: point.x, y: point.y, click: point.click }); + } + }); + await page.addInitScript(() => { + const report = (event) => { + void window.__liveCursorFromPage?.({ + x: event.clientX, + y: event.clientY, + click: event.type === "mousedown", + }); + }; + window.addEventListener("mousemove", report, true); + window.addEventListener("mousedown", report, true); + }); + await page.goto("https://www.saucedemo.com/", { + waitUntil: "domcontentloaded", + timeout: 45_000, + }); + + const debug = await bb.sessions.debug(sessionId); + const url = new URL(debug.debuggerFullscreenUrl); + url.searchParams.set("navBar", "false"); + current = { + id: sessionId, + page, + cdp, + stagehand, + stagehandBrowser, + playwrightBrowser, + liveViewUrl: url.toString(), + }; + emit({ type: "session", sessionId }); + emit({ type: "status", text: "Live View is ready" }); + return current; + } catch (error) { + try { + await stagehand?.close(); + } catch {} + try { + await stagehandBrowser.close(); + } catch {} + try { + await playwrightBrowser?.close(); + } catch {} + try { + await bb.sessions.update(sessionId, { status: "REQUEST_RELEASE" }); + } catch {} + throw error; + } +} + +function sendJson(response, status, body) { + response.writeHead(status, { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "X-Content-Type-Options": "nosniff", + }); + response.end(JSON.stringify(body)); +} + +async function readJson(request) { + let raw = ""; + for await (const chunk of request) { + raw += chunk; + if (raw.length > 32_000) throw new Error("Input is too large"); + } + return JSON.parse(raw || "{}"); +} + +function point(input) { + const x = Number(input.x); + const y = Number(input.y); + if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("Invalid pointer position"); + return { + x: Math.max(0, Math.min(viewport.width - 1, x)), + y: Math.max(0, Math.min(viewport.height - 1, y)), + }; +} + +async function dispatchInput(input) { + if (!current || agentRunning) return; + const { cdp, page } = current; + switch (input.type) { + case "move": + await cdp.send("Input.dispatchMouseEvent", { + type: "mouseMoved", + ...point(input), + button: "none", + buttons: input.buttons ? 1 : 0, + }); + break; + case "down": + case "up": + await cdp.send("Input.dispatchMouseEvent", { + type: input.type === "down" ? "mousePressed" : "mouseReleased", + ...point(input), + button: "left", + buttons: input.type === "down" ? 1 : 0, + clickCount: 1, + }); + break; + case "wheel": + await cdp.send("Input.dispatchMouseEvent", { + type: "mouseWheel", + ...point(input), + deltaX: Number(input.deltaX) || 0, + deltaY: Number(input.deltaY) || 0, + }); + break; + case "text": + if (typeof input.text !== "string" || input.text.length > 1000) + throw new Error("Invalid text"); + await page.keyboard.insertText(input.text); + break; + case "key": + if (typeof input.key !== "string" || input.key.length > 60) throw new Error("Invalid key"); + await page.keyboard.press(input.key); + break; + default: + throw new Error("Invalid input type"); + } +} + +async function runAgent(driver) { + if (!current || agentRunning) return; + const session = current; + const label = driver === "playwright" ? "Playwright" : "Stagehand"; + agentRunning = true; + emit({ type: "agent-state", running: true }); + emit({ type: "status", text: `${label} is signing in to SauceDemo` }); + let usedFallback = false; + try { + // Both clients control the same remote tab. The page listener reports their + // real mouse events to the overlay while agentRunning is true. + await session.page.goto("https://www.saucedemo.com/", { + waitUntil: "domcontentloaded", + timeout: 45_000, + }); + if (driver === "playwright") { + const page = session.page; + await page.locator('[data-test="username"]').fill("standard_user"); + await page.locator('[data-test="password"]').fill("secret_sauce"); + await page.locator('[data-test="login-button"]').click(); + await page.waitForURL("**/inventory.html"); + emit({ type: "status", text: "Playwright is adding a backpack" }); + await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click(); + await page.locator(".shopping_cart_link").click(); + const cartText = await page.locator(".cart_item").innerText(); + if (!cartText.includes("Sauce Labs Backpack")) + throw new Error("Backpack was not in the cart"); + } else { + const page = await session.stagehandBrowser.context.activePage(); + if (!page) throw new Error("Stagehand page is not available"); + await page.locator('[data-test="username"]').fill("standard_user"); + await page.locator('[data-test="password"]').fill("secret_sauce"); + await page.locator('[data-test="login-button"]').click(); + if (!(await page.url()).includes("inventory.html")) { + throw new Error("SauceDemo did not open the inventory page"); + } + emit({ type: "status", text: "Stagehand is adding a backpack" }); + const result = await session.stagehand.act( + "Click the Add to cart button for Sauce Labs Backpack.", + { page, timeout: 60_000, cache: false }, + ); + const cartCount = await page.evaluate( + () => document.querySelector(".shopping_cart_badge")?.textContent ?? "0", + ); + if (!result.data.success || cartCount !== "1") { + usedFallback = true; + emit({ type: "status", text: "Using a direct browser click to confirm the cart" }); + await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click(); + } + await page.locator(".shopping_cart_link").click(); + const cartText = await page.locator(".cart_item").innerText(); + if (!cartText.includes("Sauce Labs Backpack")) + throw new Error("Backpack was not in the cart"); + } + const resultText = usedFallback + ? "Done: Backpack is in the cart (direct click confirmed the Stagehand action)" + : `Done: ${label} added Sauce Labs Backpack to the cart`; + emit({ type: "status", text: resultText }); + } catch (error) { + emit({ + type: "status", + text: `${label} failed: ${String(error.message ?? error).replaceAll(apiKey, "[REDACTED]")}`, + }); + } finally { + agentRunning = false; + emit({ type: "agent-state", running: false }); + } +} + +const server = http.createServer(async (request, response) => { + try { + const url = new URL(request.url, `http://${request.headers.host}`); + if (request.method === "GET" && url.pathname === "/api/config") { + return sendJson( + response, + 200, + current + ? { + sessionId: current.id, + liveViewUrl: current.liveViewUrl, + viewport, + agentRunning, + } + : { error: "Session is starting" }, + ); + } + if (request.method === "GET" && url.pathname === "/api/events") { + response.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + response.write("\n"); + clients.add(response); + request.on("close", () => clients.delete(response)); + return; + } + if (request.method === "POST") { + const origin = request.headers.origin; + if ( + origin && + origin !== `http://127.0.0.1:${port}` && + origin !== `http://localhost:${port}` + ) { + return sendJson(response, 403, { error: "Invalid origin" }); + } + if (url.pathname === "/api/input") { + const input = await readJson(request); + inputQueue = inputQueue.catch(() => {}).then(() => dispatchInput(input)); + await inputQueue; + return sendJson(response, 200, { ok: true }); + } + if (url.pathname === "/api/run") { + if (agentRunning) return sendJson(response, 409, { error: "Agent is running" }); + const { driver } = await readJson(request); + if (driver !== "stagehand" && driver !== "playwright") { + return sendJson(response, 400, { error: "Choose stagehand or playwright" }); + } + void runAgent(driver); + return sendJson(response, 202, { ok: true }); + } + if (url.pathname === "/api/new") { + if (agentRunning) return sendJson(response, 409, { error: "Agent is running" }); + await createSession(); + return sendJson(response, 200, { ok: true }); + } + } + const assets = { + "/": ["index.html", "text/html; charset=utf-8"], + "/app.js": ["app.js", "text/javascript; charset=utf-8"], + "/styles.css": ["styles.css", "text/css; charset=utf-8"], + }; + const asset = assets[url.pathname]; + if (request.method !== "GET" || !asset) return sendJson(response, 404, { error: "Not found" }); + const body = await readFile(join(uiDir, asset[0])); + response.writeHead(200, { + "Content-Type": asset[1], + "Cache-Control": "no-store", + "X-Content-Type-Options": "nosniff", + }); + response.end(body); + } catch (error) { + sendJson(response, 500, { + error: String(error.message ?? error).replaceAll(apiKey, "[REDACTED]"), + }); + } +}); + +await createSession(); +server.listen(port, "127.0.0.1", () => { + console.log(`Live View UI: http://127.0.0.1:${port}`); + console.log(`Browserbase session: ${current.id}`); +}); + +async function shutdown() { + server.close(); + await closeSession(current); + process.exit(0); +} +process.once("SIGINT", () => void shutdown()); +process.once("SIGTERM", () => void shutdown()); diff --git a/examples/javascript/live-view-cursor-ui/ui/app.js b/examples/javascript/live-view-cursor-ui/ui/app.js new file mode 100644 index 0000000..a77a4e9 --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/ui/app.js @@ -0,0 +1,232 @@ +const stage = document.querySelector("#stage"); +const layer = document.querySelector("#input-layer"); +const frame = document.querySelector("#live-frame"); +const human = document.querySelector("#human-cursor"); +const agent = document.querySelector("#agent-cursor"); +const status = document.querySelector("#status"); +const sessionLabel = document.querySelector("#session-id"); +const loading = document.querySelector("#loading"); +const runButton = document.querySelector("#run-agent"); +const playwrightButton = document.querySelector("#run-playwright"); +const newButton = document.querySelector("#new-session"); +let viewport = { width: 1280, height: 800 }; +let agentRunning = false; +let humanInside = false; +let agentPoint = null; +let buttons = 0; +let sendQueue = Promise.resolve(); +let pendingMove = null; +let moveTimer = null; + +function moveCursor(element, x, y) { + element.style.transform = `translate3d(${x - 2}px,${y - 2}px,0)`; + element.classList.add("visible"); +} + +function pulse(element) { + const ring = element.querySelector(".click-ring"); + ring.classList.remove("pulse"); + void ring.offsetWidth; + ring.classList.add("pulse"); +} + +function screenPoint(event) { + const rect = stage.getBoundingClientRect(); + const localX = Math.max(0, Math.min(rect.width, event.clientX - rect.left)); + const localY = Math.max(0, Math.min(rect.height, event.clientY - rect.top)); + return { + x: Math.round((localX / rect.width) * viewport.width), + y: Math.round((localY / rect.height) * viewport.height), + localX, + localY, + }; +} + +function send(input) { + sendQueue = sendQueue + .catch(() => {}) + .then(async () => { + const response = await fetch("/api/input", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(input), + }); + if (!response.ok) throw new Error("Browser input failed"); + }); + return sendQueue; +} + +function flushMove() { + moveTimer = null; + if (!pendingMove || agentRunning) return; + const point = pendingMove; + pendingMove = null; + void send({ type: "move", x: point.x, y: point.y, buttons }); +} + +function showAgentIfNeeded() { + if (agentRunning && agentPoint) agent.classList.add("visible"); + else agent.classList.remove("visible"); +} + +layer.addEventListener("pointerenter", () => { + humanInside = true; + showAgentIfNeeded(); +}); +layer.addEventListener("pointerleave", () => { + humanInside = false; + human.classList.remove("visible"); + showAgentIfNeeded(); +}); +layer.addEventListener("pointermove", (event) => { + const point = screenPoint(event); + moveCursor(human, point.localX, point.localY); + if (agentRunning) return; + pendingMove = point; + if (!moveTimer) moveTimer = setTimeout(flushMove, 32); +}); +layer.addEventListener("pointerdown", (event) => { + if (event.button !== 0) return; + event.preventDefault(); + layer.focus(); + layer.setPointerCapture(event.pointerId); + const point = screenPoint(event); + moveCursor(human, point.localX, point.localY); + pulse(human); + if (agentRunning) return; + buttons = 1; + pendingMove = null; + void send({ type: "move", x: point.x, y: point.y, buttons: 0 }); + void send({ type: "down", x: point.x, y: point.y }); +}); +layer.addEventListener("pointerup", (event) => { + if (event.button !== 0 || agentRunning) return; + buttons = 0; + const point = screenPoint(event); + void send({ type: "up", x: point.x, y: point.y }); +}); +layer.addEventListener("pointercancel", (event) => { + if (buttons && !agentRunning) { + const point = screenPoint(event); + void send({ type: "up", x: point.x, y: point.y }); + } + buttons = 0; +}); +layer.addEventListener( + "wheel", + (event) => { + event.preventDefault(); + if (agentRunning) return; + const point = screenPoint(event); + void send({ + type: "wheel", + x: point.x, + y: point.y, + deltaX: event.deltaX, + deltaY: event.deltaY, + }); + }, + { passive: false }, +); +layer.addEventListener("keydown", (event) => { + if (agentRunning || ["Shift", "Control", "Alt", "Meta"].includes(event.key)) { + event.preventDefault(); + return; + } + const key = event.key; + if ((event.metaKey || event.ctrlKey) && key.toLowerCase() === "v") return; + if (!event.metaKey && !event.ctrlKey && !event.altKey && key.length === 1) return; + event.preventDefault(); + const mods = [ + event.ctrlKey && "Control", + event.metaKey && "Meta", + event.altKey && "Alt", + event.shiftKey && "Shift", + ].filter(Boolean); + void send({ type: "key", key: [...mods, key].join("+") }); +}); +layer.addEventListener("beforeinput", (event) => { + event.preventDefault(); + if (agentRunning || !event.data) return; + if (event.inputType === "insertText" || event.inputType === "insertCompositionText") { + void send({ type: "text", text: event.data }); + } +}); +layer.addEventListener("paste", (event) => { + event.preventDefault(); + if (agentRunning) return; + const text = event.clipboardData?.getData("text/plain"); + if (text) void send({ type: "text", text }); +}); +window.addEventListener("blur", () => human.classList.remove("visible")); + +async function loadSession() { + const response = await fetch("/api/config", { cache: "no-store" }); + const config = await response.json(); + if (!config.liveViewUrl) throw new Error(config.error || "Live View is not ready"); + viewport = config.viewport; + sessionLabel.textContent = config.sessionId; + frame.src = config.liveViewUrl; + frame.onload = () => loading.classList.add("hidden"); + status.textContent = "Live View is ready"; +} + +async function startDemo(driver) { + runButton.disabled = true; + playwrightButton.disabled = true; + try { + const response = await fetch("/api/run", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ driver }), + }); + if (!response.ok) throw new Error(`${driver} could not start`); + } catch (error) { + status.textContent = error.message; + runButton.disabled = false; + playwrightButton.disabled = false; + } +} +runButton.addEventListener("click", () => void startDemo("stagehand")); +playwrightButton.addEventListener("click", () => void startDemo("playwright")); +newButton.addEventListener("click", async () => { + newButton.disabled = true; + loading.classList.remove("hidden"); + status.textContent = "Starting a new Browserbase session"; + try { + const response = await fetch("/api/new", { method: "POST" }); + if (!response.ok) throw new Error("New session failed"); + await loadSession(); + } catch (error) { + status.textContent = error.message; + } finally { + newButton.disabled = false; + } +}); + +const events = new EventSource("/api/events"); +events.onmessage = ({ data }) => { + const message = JSON.parse(data); + if (message.type === "status") status.textContent = message.text; + if (message.type === "session") sessionLabel.textContent = message.sessionId; + if (message.type === "agent-state") { + agentRunning = message.running; + runButton.disabled = agentRunning; + playwrightButton.disabled = agentRunning; + if (agentRunning) agentPoint = null; + showAgentIfNeeded(); + } + if (message.type === "agent-pointer") { + agentPoint = message; + moveCursor( + agent, + (message.x / viewport.width) * stage.clientWidth, + (message.y / viewport.height) * stage.clientHeight, + ); + showAgentIfNeeded(); + if (message.click) pulse(agent); + } +}; +loadSession().catch((error) => { + status.textContent = error.message; +}); diff --git a/examples/javascript/live-view-cursor-ui/ui/index.html b/examples/javascript/live-view-cursor-ui/ui/index.html new file mode 100644 index 0000000..b4ea26a --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/ui/index.html @@ -0,0 +1,120 @@ + + + + + + Browserbase Live View · Cursor Lab + + + + +
+
+
+ + browserbase + / + Live View +
+
+ LOCAL VIEWER + +
+
+ +
+
+
+
INTERACTIVE SESSION
+

Live browser

+

+ Use your mouse and keyboard here. Run Stagehand or Playwright to see the agent + pointer. +

+
+
+ + +
+
+ +
+
+
+ Live ViewStarting session… +
+
+ 1280 × 800INTERACTIVE +
+
+
+
+
+ Connecting to Browserbase… +
+ +
+ + +
+ +
+
+ ↗This local page draws both pointers. The Browserbase Live View remains inside the + frame. +
+
+
+ + diff --git a/examples/javascript/live-view-cursor-ui/ui/styles.css b/examples/javascript/live-view-cursor-ui/ui/styles.css new file mode 100644 index 0000000..25856d6 --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/ui/styles.css @@ -0,0 +1,419 @@ +:root { + font-family: + Inter, + ui-sans-serif, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + color: #111827; + background: #f5f7f8; + font-synthesis: none; +} +* { + box-sizing: border-box; +} +body { + margin: 0; + min-width: 0; +} +button { + font: inherit; + cursor: pointer; +} +.app { + min-height: 100vh; +} +.topbar { + height: 64px; + background: #0e1419; + color: #f8fafc; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 clamp(24px, 4vw, 68px); + border-bottom: 1px solid #26313a; +} +.brand, +.top-actions { + display: flex; + align-items: center; +} +.brand { + gap: 11px; + font-weight: 650; + letter-spacing: -0.035em; + font-size: 18px; +} +.brand-mark { + width: 22px; + height: 22px; + display: grid; + grid-template-columns: repeat(2, 8px); + grid-template-rows: repeat(2, 8px); + gap: 3px; + transform: rotate(-10deg); +} +.brand-mark span { + background: #ffc331; + border-radius: 2px; +} +.brand-mark span:nth-child(2) { + opacity: 0.9; +} +.brand-mark span:nth-child(3) { + opacity: 0.76; +} +.slash { + color: #71808d; + font-weight: 400; + margin: 0 2px; +} +.product { + color: #d2dce5; + font-weight: 500; + letter-spacing: -0.02em; +} +.top-actions { + gap: 22px; +} +.local-badge { + display: inline-flex; + align-items: center; + gap: 8px; + color: #a4b0b9; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.085em; +} +.local-dot { + width: 6px; + height: 6px; + background: #66c997; + border-radius: 50%; + box-shadow: 0 0 8px #66c997; +} +.quiet-button { + color: #dce4e9; + background: #1d272d; + border: 1px solid #35424a; + border-radius: 7px; + padding: 8px 12px; + font-size: 12px; + font-weight: 600; +} +.quiet-button:hover { + background: #2a363d; +} +.workspace { + max-width: 1440px; + margin: 0 auto; + padding: clamp(28px, 4vw, 54px) clamp(24px, 4vw, 68px) 55px; +} +.workspace-heading { + display: flex; + align-items: end; + justify-content: space-between; + gap: 32px; + margin-bottom: 26px; +} +.eyebrow { + color: #72818e; + font-size: 10px; + font-weight: 750; + letter-spacing: 0.15em; + margin-bottom: 11px; +} +h1 { + font-size: clamp(29px, 3vw, 38px); + letter-spacing: -0.055em; + font-weight: 720; + line-height: 1.1; + margin: 0 0 9px; +} +p { + color: #687680; + margin: 0; + font-size: 14px; +} +.primary-button { + background: #151d22; + color: #fff; + border: 1px solid #151d22; + border-radius: 8px; + font-size: 13px; + font-weight: 650; + padding: 12px 16px; + white-space: nowrap; + box-shadow: 0 4px 10px #121a2020; +} +.primary-button:hover { + background: #2a363d; +} +.primary-button:disabled, +.quiet-button:disabled { + opacity: 0.55; + cursor: default; +} +.button-icon { + color: #ffc331; + font-size: 16px; + margin-right: 8px; +} +.viewer-shell { + background: #fff; + border: 1px solid #dce2e5; + border-radius: 12px; + box-shadow: + 0 16px 40px #17212a12, + 0 2px 6px #17212a0a; + overflow: hidden; +} +.viewer-toolbar { + height: 48px; + background: #fff; + border-bottom: 1px solid #e9edf0; + padding: 0 18px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 15px; + color: #84919a; + font-size: 11px; +} +.viewer-title, +.viewer-tags { + display: flex; + align-items: center; + gap: 11px; +} +.viewer-title strong { + color: #27343c; + font-size: 12px; + font-weight: 700; +} +.live-dot { + width: 8px; + height: 8px; + background: #38bf80; + border-radius: 50%; + box-shadow: 0 0 0 4px #e4f8ed; +} +.divider { + height: 15px; + width: 1px; + background: #dfe5e8; + margin: 0 2px; +} +.tag { + border: 1px solid #dce7e0; + background: #f1faf5; + color: #4a9a6c; + border-radius: 4px; + padding: 4px 7px; + font-weight: 700; + font-size: 9px; + letter-spacing: 0.065em; +} +.stage { + position: relative; + width: 100%; + aspect-ratio: 1280 / 800; + background: #e9ecee; + overflow: hidden; +} +.stage iframe { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border: 0; + pointer-events: none; + background: #fff; +} +.input-layer { + position: absolute; + inset: 0; + z-index: 3; + cursor: none; + touch-action: none; + outline: none; + caret-color: transparent; + color: transparent; + user-select: none; +} +.loading { + position: absolute; + inset: 0; + z-index: 2; + display: grid; + place-content: center; + justify-items: center; + gap: 14px; + color: #5b6872; + font-size: 13px; + background: #f8fafb; +} +.loading.hidden { + display: none; +} +.spinner { + width: 22px; + height: 22px; + border: 2px solid #dce4ea; + border-top-color: #718592; + border-radius: 50%; + animation: spin 800ms linear infinite; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +.cursor { + position: absolute; + left: 0; + top: 0; + z-index: 5; + pointer-events: none; + opacity: 0; + transform: translate3d(-100px, -100px, 0); + will-change: transform, opacity; + filter: drop-shadow(0 2px 2px rgb(8 16 34 / 0.34)) drop-shadow(0 0 4px rgb(91 166 255 / 0.86)) + drop-shadow(0 0 10px rgb(91 166 255 / 0.48)); + transition: opacity 100ms ease-out; +} +.cursor.visible { + opacity: 1; +} +.cursor svg { + display: block; +} +.click-ring { + position: absolute; + left: 2px; + top: 2px; + width: 8px; + height: 8px; + border: 2px solid #5da7ff; + border-radius: 50%; + opacity: 0; + transform: translate(-50%, -50%) scale(0.3); +} +.click-ring.pulse { + animation: pulse 380ms ease-out; +} +@keyframes pulse { + 0% { + opacity: 0.9; + transform: translate(-50%, -50%) scale(0.3); + } + 100% { + opacity: 0; + transform: translate(-50%, -50%) scale(3.2); + } +} +.viewer-footer { + min-height: 42px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 8px 16px; + color: #8b99a3; + font-size: 11px; + border-top: 1px solid #e9edf0; +} +.footer-status { + display: flex; + align-items: center; + gap: 8px; + color: #53636e; +} +.status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #49bf82; +} +.hint { + display: flex; + align-items: center; + gap: 10px; + color: #87949e; + font-size: 11px; + margin: 15px 0 0 2px; +} +.hint-icon { + color: #5e98ca; + font-size: 15px; +} +@media (max-width: 800px) { + .workspace-heading { + align-items: start; + flex-direction: column; + gap: 18px; + } + .footer-help, + .viewer-tags { + display: none; + } +} +@media (max-width: 520px) { + .topbar { + padding: 0 18px; + } + .brand { + font-size: 15px; + gap: 7px; + } + .product { + font-size: 14px; + } + .local-badge { + display: none; + } + .workspace { + padding: 28px 16px 40px; + } + .viewer-toolbar { + padding: 0 11px; + } + .viewer-title { + gap: 7px; + } + .viewer-title #session-id { + display: inline-block; + max-width: 110px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .viewer-footer { + padding: 8px 11px; + } +} + +.demo-actions { + display: flex; + align-items: center; + gap: 9px; + flex-wrap: wrap; +} +.secondary-button { + background: #fff; + color: #26343d; + border: 1px solid #cad4da; + border-radius: 8px; + font-size: 13px; + font-weight: 650; + padding: 12px 16px; + white-space: nowrap; +} +.secondary-button:hover { + background: #f1f5f7; +} +.secondary-button:disabled { + opacity: 0.55; + cursor: default; +} diff --git a/skills/browserbase-cookbook/references/agents-and-human-handoff.md b/skills/browserbase-cookbook/references/agents-and-human-handoff.md index 82cd830..4d5930c 100644 --- a/skills/browserbase-cookbook/references/agents-and-human-handoff.md +++ b/skills/browserbase-cookbook/references/agents-and-human-handoff.md @@ -4,10 +4,11 @@ Build browser agents and pause work for human input. Paths below are relative to a local cookbook root, not to the installed skill directory. Locate the checkout first. Read the selected guide and source before adapting code. Historical source links identify the imported revision; they predate cookbook migrations and are not runnable fallbacks. -This topic contains all 27 matching entries. Browse `docs/topics/agents-and-human-handoff.md` or run `python3 scripts/catalog.py search "" --language python --limit 5` from the cookbook root for other languages or frameworks. +This topic contains all 28 matching entries. Browse `docs/topics/agents-and-human-handoff.md` or run `python3 scripts/catalog.py search "" --language python --limit 5` from the cookbook root for other languages or frameworks. | Recipe | Language | Local guide | Status | Historical source | | --- | --- | --- | --- | --- | +| Live View special pointers (JavaScript) | javascript | `docs/recipes/examples-javascript-live-view-cursor-ui.md` | current | cookbook-authored | | Anthropic Claude browser agent (Python) | python | `docs/recipes/examples-python-anthropic-cua.md` | current | cookbook-authored | | Business lookup (Python) | python | `docs/recipes/examples-python-business-lookup.md` | current | [pinned import](https://github.com/browserbase/templates/tree/088ff598518cf69b4edf1e1260dd57ff11ca55d8/python/business-lookup) | | Cartesia form filling (Python) | python | `docs/recipes/examples-python-cartesia-form-filling.md` | current | [pinned import](https://github.com/browserbase/templates/tree/088ff598518cf69b4edf1e1260dd57ff11ca55d8/python/cartesia-form-filling) | diff --git a/skills/browserbase-cookbook/references/testing-and-observability.md b/skills/browserbase-cookbook/references/testing-and-observability.md index aa204e2..191ee6d 100644 --- a/skills/browserbase-cookbook/references/testing-and-observability.md +++ b/skills/browserbase-cookbook/references/testing-and-observability.md @@ -4,10 +4,11 @@ Test sites and inspect browser behavior, reliability, and performance. Paths below are relative to a local cookbook root, not to the installed skill directory. Locate the checkout first. Read the selected guide and source before adapting code. Historical source links identify the imported revision; they predate cookbook migrations and are not runnable fallbacks. -This topic contains all 7 matching entries. Browse `docs/topics/testing-and-observability.md` or run `python3 scripts/catalog.py search "" --language python --limit 5` from the cookbook root for other languages or frameworks. +This topic contains all 8 matching entries. Browse `docs/topics/testing-and-observability.md` or run `python3 scripts/catalog.py search "" --language python --limit 5` from the cookbook root for other languages or frameworks. | Recipe | Language | Local guide | Status | Historical source | | --- | --- | --- | --- | --- | +| Live View special pointers (JavaScript) | javascript | `docs/recipes/examples-javascript-live-view-cursor-ui.md` | current | cookbook-authored | | Agentic KYB registry research (Python, Stagehand v4) | python | `docs/recipes/examples-python-agentic-kyb.md` | current | cookbook-authored | | Cerebras docs checker (Python) | python | `docs/recipes/examples-python-cerebras-docs-checker.md` | current | [pinned import](https://github.com/browserbase/templates/tree/088ff598518cf69b4edf1e1260dd57ff11ca55d8/python/cerebras-docs-checker) | | Smart fetch scraper (Python) | python | `docs/recipes/examples-python-smart-fetch-scraper.md` | current | [pinned import](https://github.com/browserbase/templates/tree/088ff598518cf69b4edf1e1260dd57ff11ca55d8/python/smart-fetch-scraper) | From 983d5bd5c354f9757ba413f59eec64680c2cd6c2 Mon Sep 17 00:00:00 2001 From: shubh24 Date: Thu, 8 Oct 2026 11:23:21 -0700 Subject: [PATCH 2/3] fix: keep Live View clicks ahead of stale mouse moves --- COOKBOOK_AUTHORED.json | 4 +- .../javascript/live-view-cursor-ui/README.md | 6 +++ .../javascript/live-view-cursor-ui/ui/app.js | 48 ++++++++++++++----- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/COOKBOOK_AUTHORED.json b/COOKBOOK_AUTHORED.json index 7b9615f..36f9109 100644 --- a/COOKBOOK_AUTHORED.json +++ b/COOKBOOK_AUTHORED.json @@ -18,7 +18,7 @@ }, { "path": "examples/javascript/live-view-cursor-ui/README.md", - "sha256": "4bf38408c44420ab9639be6e99659ee08fff86c49264c91ce449cb698d8fa1f6", + "sha256": "34d0cb596846a3765c2737cabcb6846accca5302463c8aeee43533f665de1efb", "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." }, { @@ -33,7 +33,7 @@ }, { "path": "examples/javascript/live-view-cursor-ui/ui/app.js", - "sha256": "4373ed332643025748a513ec1ec322218dc783fc7ae3a8ae48ebf679655ccc69", + "sha256": "8fd52d9966eb6c26cdc4424a4254cd31dfe23e6314a056ce4462bb04a3bde853", "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." }, { diff --git a/examples/javascript/live-view-cursor-ui/README.md b/examples/javascript/live-view-cursor-ui/README.md index c1a1a31..cc5a75a 100644 --- a/examples/javascript/live-view-cursor-ui/README.md +++ b/examples/javascript/live-view-cursor-ui/README.md @@ -36,6 +36,12 @@ The overlay receives `mousemove` and `mousedown` events from the remote page whi The direct Browserbase Live View URL keeps its normal pointer. The special pointers appear only in this local UI. The server binds to `127.0.0.1`; it sends the signed Live View URL to the local page, and it keeps the Browserbase API key on the server. +## Latency + +The human pointer moves in the local page as soon as it receives a pointer event. The page sends mouse moves to the server at most once every 32 ms. If a request is still in flight, the page keeps only the newest mouse move and sends clicks before waiting moves. This prevents old moves from delaying a click on a slow connection. + +The remote page and agent pointer still depend on network delay. For a hosted app, place the input server near the Browserbase browser session. Browserbase's [performance guide](https://docs.browserbase.com/optimizations/latency/speed-optimization) explains how region choice affects command round trips. Compare the direct Live View and this UI on the same session to find out whether the delay is in the viewer or the input path. + ## Change the target Change the `page.goto()` call in `createSession()` and the browser steps in `runAgent()` in [`server.mjs`](server.mjs). The viewer is set to 1280 × 800 pixels. Change both the server viewport and UI aspect ratio if you use another size. This example handles the main browser tab. A new remote tab or a frame inside the remote page needs its own input route and pointer listener. diff --git a/examples/javascript/live-view-cursor-ui/ui/app.js b/examples/javascript/live-view-cursor-ui/ui/app.js index a77a4e9..fc5b6da 100644 --- a/examples/javascript/live-view-cursor-ui/ui/app.js +++ b/examples/javascript/live-view-cursor-ui/ui/app.js @@ -14,7 +14,9 @@ let agentRunning = false; let humanInside = false; let agentPoint = null; let buttons = 0; -let sendQueue = Promise.resolve(); +const actionQueue = []; +let latestMove = null; +let sending = false; let pendingMove = null; let moveTimer = null; @@ -42,18 +44,33 @@ function screenPoint(event) { }; } +async function flushInputs() { + if (sending) return; + sending = true; + try { + while (actionQueue.length || latestMove) { + const input = actionQueue.length ? actionQueue.shift() : latestMove; + if (input === latestMove) latestMove = null; + try { + const response = await fetch("/api/input", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(input), + }); + if (!response.ok) throw new Error("Browser input failed"); + } catch (error) { + status.textContent = String(error.message ?? error); + } + } + } finally { + sending = false; + } +} + function send(input) { - sendQueue = sendQueue - .catch(() => {}) - .then(async () => { - const response = await fetch("/api/input", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(input), - }); - if (!response.ok) throw new Error("Browser input failed"); - }); - return sendQueue; + if (input.type === "move") latestMove = input; + else actionQueue.push(input); + void flushInputs(); } function flushMove() { @@ -96,13 +113,18 @@ layer.addEventListener("pointerdown", (event) => { if (agentRunning) return; buttons = 1; pendingMove = null; - void send({ type: "move", x: point.x, y: point.y, buttons: 0 }); + latestMove = null; void send({ type: "down", x: point.x, y: point.y }); }); layer.addEventListener("pointerup", (event) => { if (event.button !== 0 || agentRunning) return; buttons = 0; const point = screenPoint(event); + if (pendingMove || latestMove) { + actionQueue.push({ type: "move", x: point.x, y: point.y, buttons: 1 }); + pendingMove = null; + latestMove = null; + } void send({ type: "up", x: point.x, y: point.y }); }); layer.addEventListener("pointercancel", (event) => { From 95b60e2714273aebd2083f0af33067c0316cd5d8 Mon Sep 17 00:00:00 2001 From: shubh24 Date: Thu, 8 Oct 2026 12:34:03 -0700 Subject: [PATCH 3/3] Show custom pointer only for agent actions --- COOKBOOK_AUTHORED.json | 12 +- catalog.json | 18 +- docs/catalog.md | 2 +- ...examples-javascript-live-view-cursor-ui.md | 11 +- docs/topics/agents-and-human-handoff.md | 2 +- docs/topics/testing-and-observability.md | 2 +- examples/README.md | 4 +- .../javascript/live-view-cursor-ui/README.md | 16 +- .../javascript/live-view-cursor-ui/server.mjs | 85 ++----- .../javascript/live-view-cursor-ui/ui/app.js | 211 +++++------------- .../live-view-cursor-ui/ui/index.html | 32 +-- .../live-view-cursor-ui/ui/styles.css | 13 +- .../references/agents-and-human-handoff.md | 2 +- .../references/testing-and-observability.md | 2 +- 14 files changed, 121 insertions(+), 291 deletions(-) diff --git a/COOKBOOK_AUTHORED.json b/COOKBOOK_AUTHORED.json index 36f9109..9542331 100644 --- a/COOKBOOK_AUTHORED.json +++ b/COOKBOOK_AUTHORED.json @@ -3,7 +3,7 @@ "files": [ { "path": "examples/README.md", - "sha256": "757e05989f94ed1929ff954abd7292c389baa293e9befeda9f73c325bbf92cc2", + "sha256": "92f2c66626bc21a74f9641616fdfe8395de6b788fb2d248cf8cf56db55d70405", "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." }, { @@ -18,7 +18,7 @@ }, { "path": "examples/javascript/live-view-cursor-ui/README.md", - "sha256": "34d0cb596846a3765c2737cabcb6846accca5302463c8aeee43533f665de1efb", + "sha256": "71fcc1931cdf76cb93fa874b426b6e6d069fb8b1ea64c31379800830bd84d8b2", "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." }, { @@ -28,22 +28,22 @@ }, { "path": "examples/javascript/live-view-cursor-ui/server.mjs", - "sha256": "f0d08d27c23d4ad133cfef3882c0c7b9e32f4108ddd422fa5c535fe6007da371", + "sha256": "1fc37d1f1247b19adf3db6a9b7a4697c104f51e300302d1878b126b0eaacbf05", "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." }, { "path": "examples/javascript/live-view-cursor-ui/ui/app.js", - "sha256": "8fd52d9966eb6c26cdc4424a4254cd31dfe23e6314a056ce4462bb04a3bde853", + "sha256": "4c68b8e8ac26912a809742b83296a56a25e52d48661dc78505a13d7cf11701ab", "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." }, { "path": "examples/javascript/live-view-cursor-ui/ui/index.html", - "sha256": "6286bc862bd28e322555a9522421962778bc94f3c25cf78bdc1e0cdf8a38052c", + "sha256": "ce1202bba1719ec1f2665da875bab354b0becfa2c7eb923ebabab5abb110626c", "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." }, { "path": "examples/javascript/live-view-cursor-ui/ui/styles.css", - "sha256": "0fb0c203ef6ef19ea62b59630d5feab754a06788c896a9dd44f91e5d99d91672", + "sha256": "999fc836f8b0a64c7e79f9dbd8058c8039740103c7a3edfa4fedb92a64b0e81a", "reason": "Cookbook-authored migration, test, configuration, or collection metadata outside the pinned upstream tree." }, { diff --git a/catalog.json b/catalog.json index b2999c8..1d48648 100644 --- a/catalog.json +++ b/catalog.json @@ -335,9 +335,9 @@ { "access": "public", "caveats": [ - "The pointer styling appears in the localhost wrapper, not in a direct Browserbase Live View URL.", - "The sample routes input and pointer events for the main browser tab only; new remote tabs and frames need more routing.", - "The local server gives its signed Live View URL to its own page; keep the server bound to loopback and review access before deployment." + "The agent pointer styling appears in the wrapper, not in a direct Browserbase Live View URL.", + "The sample tracks agent pointer events in the main browser tab only; new remote tabs and frames need their own pointer listener.", + "The local server gives its signed Live View URL to its own page; protect the page and API routes before deployment." ], "collection": "examples", "dependencies": { @@ -407,9 +407,9 @@ "kind": "cookbook-authored", "path": "examples/javascript/live-view-cursor-ui" }, - "summary": "Embed Browserbase Live View in a local page with matching human and agent pointers for Stagehand and Playwright actions.", + "summary": "Embed Browserbase Live View with a custom agent pointer while human input uses the native Live View.", "tags": [], - "title": "Live View special pointers (JavaScript)", + "title": "Live View agent pointer (JavaScript)", "topics": [ "agents-and-human-handoff", "testing-and-observability" @@ -440,6 +440,14 @@ "limits": "The Browserbase and Stagehand session initialized with no project ID in the environment or launch options and was closed. This check did not rerun the Playwright or Stagehand cart actions.", "result": "passed", "runtime": "Node.js 25.9.0 on macOS arm64; Browserbase SDK 2.18.0 and Stagehand 4.0.2" + }, + { + "command": "LIVE_VIEW_UI_PORT=4804 node server.mjs; node /private/tmp/live-view-independent-test.mjs http://127.0.0.1:4804 cookbook; node /private/tmp/cookbook-agent-only-test.mjs", + "date": "2026-10-08", + "kind": "live-tested", + "limits": "Agent-only version: a human click through the iframe showed the SauceDemo username error with no /api/input request. Playwright showed the custom agent pointer and added the backpack to the cart. Stagehand was not rerun for this version. The session was reset after testing.", + "result": "passed", + "runtime": "Node.js 25.9.0 on macOS arm64; Browserbase SDK 2.18.0, Stagehand 4.0.2, Playwright Core 1.55.0" } ], "working_directory": "examples/javascript/live-view-cursor-ui" diff --git a/docs/catalog.md b/docs/catalog.md index aacc77f..4bc9269 100644 --- a/docs/catalog.md +++ b/docs/catalog.md @@ -23,7 +23,7 @@ Search locally with `python3 scripts/catalog.py search "persistent login" --lang | Recipe | Language | Type | Status | Collection | | --- | --- | --- | --- | --- | | [Hackernews (Go)](recipes/examples-go-hackernews.md) | go | runnable example | current / public | examples | -| [Live View special pointers (JavaScript)](recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | +| [Live View agent pointer (JavaScript)](recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | | [Agentic KYB registry research (Python, Stagehand v4)](recipes/examples-python-agentic-kyb.md) | python | runnable example | current / public | examples | | [Amazon global price comparison (Python)](recipes/examples-python-amazon-global-price-comparison.md) | python | runnable example | current / public | examples | | [Amazon product scraping (Python)](recipes/examples-python-amazon-product-scraping.md) | python | runnable example | current / public | examples | diff --git a/docs/recipes/examples-javascript-live-view-cursor-ui.md b/docs/recipes/examples-javascript-live-view-cursor-ui.md index bae0f0f..4e5ec0b 100644 --- a/docs/recipes/examples-javascript-live-view-cursor-ui.md +++ b/docs/recipes/examples-javascript-live-view-cursor-ui.md @@ -1,6 +1,6 @@ -# Live View special pointers (JavaScript) +# Live View agent pointer (JavaScript) -Embed Browserbase Live View in a local page with matching human and agent pointers for Stagehand and Playwright actions. +Embed Browserbase Live View with a custom agent pointer while human input uses the native Live View. > [!CAUTION] > Demo and reference code only. This recipe is not a vetted production implementation. Independently review it, obtain authorization, and validate security, privacy, compliance, cost, and site-term requirements before use. Use at your own risk. @@ -73,10 +73,11 @@ Source and setup metadata were inspected. A scoped live workflow check is record | Clean install verified | 2026-10-08 | `Node.js 25.9.0 and npm on macOS arm64` | `npm install --ignore-scripts --no-audit --no-fund --package-lock=false` | passed. Installed inside a new recipe directory without a shared dependency tree. This did not run a browser session. | | Live workflow tested | 2026-10-08 | `Node.js 25.9.0 on macOS arm64; Browserbase SDK 2.18.0, Stagehand 4.0.2, Playwright Core 1.55.0` | `LIVE_VIEW_UI_PORT=4792 npm start; node /private/tmp/live-view-cursor-smoke.mjs playwright; node /private/tmp/live-view-cursor-smoke.mjs stagehand` | passed. One Browserbase SauceDemo session. Playwright emitted 6 pointer events and 3 clicks; Stagehand emitted 8 pointer events and 4 clicks. Both ended with the backpack in the cart. Human UI input and other browser clients were not retested in this checkout. The session was closed. | | Live workflow tested | 2026-10-08 | `Node.js 25.9.0 on macOS arm64; Browserbase SDK 2.18.0 and Stagehand 4.0.2` | `env -u BROWSERBASE_PROJECT_ID LIVE_VIEW_UI_PORT=4793 npm start` | passed. The Browserbase and Stagehand session initialized with no project ID in the environment or launch options and was closed. This check did not rerun the Playwright or Stagehand cart actions. | +| Live workflow tested | 2026-10-08 | `Node.js 25.9.0 on macOS arm64; Browserbase SDK 2.18.0, Stagehand 4.0.2, Playwright Core 1.55.0` | `LIVE_VIEW_UI_PORT=4804 node server.mjs; node /private/tmp/live-view-independent-test.mjs http://127.0.0.1:4804 cookbook; node /private/tmp/cookbook-agent-only-test.mjs` | passed. Agent-only version: a human click through the iframe showed the SauceDemo username error with no /api/input request. Playwright showed the custom agent pointer and added the backpack to the cart. Stagehand was not rerun for this version. The session was reset after testing. | -- The pointer styling appears in the localhost wrapper, not in a direct Browserbase Live View URL. -- The sample routes input and pointer events for the main browser tab only; new remote tabs and frames need more routing. -- The local server gives its signed Live View URL to its own page; keep the server bound to loopback and review access before deployment. +- The agent pointer styling appears in the wrapper, not in a direct Browserbase Live View URL. +- The sample tracks agent pointer events in the main browser tab only; new remote tabs and frames need their own pointer listener. +- The local server gives its signed Live View URL to its own page; protect the page and API routes before deployment. ## Provenance diff --git a/docs/topics/agents-and-human-handoff.md b/docs/topics/agents-and-human-handoff.md index 340e739..a4b0fe4 100644 --- a/docs/topics/agents-and-human-handoff.md +++ b/docs/topics/agents-and-human-handoff.md @@ -6,7 +6,7 @@ Open a recipe guide for its working directory, commands, environment references, | Recipe | Language | Type | Status | Collection | | --- | --- | --- | --- | --- | -| [Live View special pointers (JavaScript)](../recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | +| [Live View agent pointer (JavaScript)](../recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | | [Anthropic Claude browser agent (Python)](../recipes/examples-python-anthropic-cua.md) | python | runnable example | current / public | examples | | [Business lookup (Python)](../recipes/examples-python-business-lookup.md) | python | runnable example | current / public | examples | | [Cartesia form filling (Python)](../recipes/examples-python-cartesia-form-filling.md) | python | runnable example | current / public | examples | diff --git a/docs/topics/testing-and-observability.md b/docs/topics/testing-and-observability.md index f2d60c6..034f19b 100644 --- a/docs/topics/testing-and-observability.md +++ b/docs/topics/testing-and-observability.md @@ -6,7 +6,7 @@ Open a recipe guide for its working directory, commands, environment references, | Recipe | Language | Type | Status | Collection | | --- | --- | --- | --- | --- | -| [Live View special pointers (JavaScript)](../recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | +| [Live View agent pointer (JavaScript)](../recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | | [Agentic KYB registry research (Python, Stagehand v4)](../recipes/examples-python-agentic-kyb.md) | python | runnable example | current / public | examples | | [Cerebras docs checker (Python)](../recipes/examples-python-cerebras-docs-checker.md) | python | runnable example | current / public | examples | | [Smart fetch scraper (Python)](../recipes/examples-python-smart-fetch-scraper.md) | python | runnable example | current / public | examples | diff --git a/examples/README.md b/examples/README.md index 0eac5df..ac0d7fc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,7 +9,7 @@ Start with a standalone example in Python, JavaScript, TypeScript, or Go. These - [Saved sessions and MFA](../docs/topics/authentication.md). - [Extraction and research](../docs/topics/extraction-and-research.md). - [Human-in-the-loop agent app](typescript/agent-with-human-in-loop/). -- [Live View pointers for people and agents](javascript/live-view-cursor-ui/). +- [Live View agent pointer](javascript/live-view-cursor-ui/). ## Install the selected example @@ -32,7 +32,7 @@ All entries below were inspected at their pinned source revision. Live website b | Recipe | Language | Type | Status | Collection | | --- | --- | --- | --- | --- | | [Hackernews (Go)](../docs/recipes/examples-go-hackernews.md) | go | runnable example | current / public | examples | -| [Live View special pointers (JavaScript)](../docs/recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | +| [Live View agent pointer (JavaScript)](../docs/recipes/examples-javascript-live-view-cursor-ui.md) | javascript | runnable example | current / public | examples | | [Agentic KYB registry research (Python, Stagehand v4)](../docs/recipes/examples-python-agentic-kyb.md) | python | runnable example | current / public | examples | | [Amazon global price comparison (Python)](../docs/recipes/examples-python-amazon-global-price-comparison.md) | python | runnable example | current / public | examples | | [Amazon product scraping (Python)](../docs/recipes/examples-python-amazon-product-scraping.md) | python | runnable example | current / public | examples | diff --git a/examples/javascript/live-view-cursor-ui/README.md b/examples/javascript/live-view-cursor-ui/README.md index cc5a75a..b500b6d 100644 --- a/examples/javascript/live-view-cursor-ui/README.md +++ b/examples/javascript/live-view-cursor-ui/README.md @@ -1,6 +1,6 @@ -# Special pointers in Browserbase Live View +# Agent pointer in Browserbase Live View -This standalone example puts a Browserbase Live View inside a local page. It draws the same black pointer with a white edge and blue glow for human and automated mouse actions. The local page sends human mouse and keyboard input to the Browserbase session through CDP. It draws the agent pointer from real mouse events in the remote page. +This standalone example puts a Browserbase Live View inside a local page. It draws a black pointer with a white edge and blue glow for automated mouse actions. Human mouse and keyboard input go directly to the Live View iframe. The overlay does not intercept human input. > [!CAUTION] > Demo and reference code only. Review the code and validate it for your site before production use. @@ -21,7 +21,7 @@ Set `BROWSERBASE_API_KEY` in `.env`, then run: npm start ``` -Open `http://127.0.0.1:4790` on the same computer. The server starts a 15-minute Browserbase session on [SauceDemo](https://www.saucedemo.com/). Move, click, scroll, or type in the Live View to test the human pointer. Select **Run Playwright** or **Run Stagehand** to sign in and add Sauce Labs Backpack to the cart. Each button uses the same agent pointer overlay. The Stagehand run uses a direct locator click if its model action does not add the item. Select **New session** to reset the demo after a session ends. +Open `http://127.0.0.1:4790` on the same computer. The server starts a 15-minute Browserbase session on [SauceDemo](https://www.saucedemo.com/). Click and type in the Live View to use its normal human pointer. Select **Run Playwright** or **Run Stagehand** to sign in and add Sauce Labs Backpack to the cart. These runs show the custom agent pointer. The Stagehand run uses a direct locator click if its model action does not add the item. Select **New session** to reset the demo after a session ends. The demo uses SauceDemo's published test account. It adds one item to a test cart. It does not place an order. Stagehand uses the Browserbase Model Gateway and makes a model call. The server releases the Browserbase session when it shuts down. Browserbase selects the project from the API key. @@ -34,14 +34,14 @@ The overlay receives `mousemove` and `mousedown` events from the remote page whi `fill()`, text insertion, keyboard commands, `page.evaluate(() => element.click())`, and `dispatchEvent("click")` do not make a mouse path. They do not move the pointer. This example marks runs started by its two buttons as agent activity. If you add your own script, run its mouse actions inside the same active-run state or send their positions to the overlay. -The direct Browserbase Live View URL keeps its normal pointer. The special pointers appear only in this local UI. The server binds to `127.0.0.1`; it sends the signed Live View URL to the local page, and it keeps the Browserbase API key on the server. +The direct Browserbase Live View URL keeps its normal pointer. The custom agent pointer appears only in this UI. The server binds to `127.0.0.1`; it sends the signed Live View URL to the local page, and it keeps the Browserbase API key on the server. If you deploy the UI, protect the page and its API routes because the page receives a signed Live View URL. -## Latency +## Human input and pointer delay -The human pointer moves in the local page as soon as it receives a pointer event. The page sends mouse moves to the server at most once every 32 ms. If a request is still in flight, the page keeps only the newest mouse move and sends clicks before waiting moves. This prevents old moves from delaying a click on a slow connection. +Human clicks, typing, and scrolling stay in the Browserbase Live View. They do not pass through this example's server. The agent pointer is a visual overlay with `pointer-events: none`, so it does not block human input. -The remote page and agent pointer still depend on network delay. For a hosted app, place the input server near the Browserbase browser session. Browserbase's [performance guide](https://docs.browserbase.com/optimizations/latency/speed-optimization) explains how region choice affects command round trips. Compare the direct Live View and this UI on the same session to find out whether the delay is in the viewer or the input path. +The UI reads the latest agent position from `/api/pointer` every 100 ms. This also works when a proxy holds Server-Sent Events instead of passing them through. The agent pointer can have some display delay, but this path does not delay human input. Browserbase's [performance guide](https://docs.browserbase.com/optimizations/latency/speed-optimization) explains how region choice affects automation command round trips. ## Change the target -Change the `page.goto()` call in `createSession()` and the browser steps in `runAgent()` in [`server.mjs`](server.mjs). The viewer is set to 1280 × 800 pixels. Change both the server viewport and UI aspect ratio if you use another size. This example handles the main browser tab. A new remote tab or a frame inside the remote page needs its own input route and pointer listener. +Change the `page.goto()` call in `createSession()` and the browser steps in `runAgent()` in [`server.mjs`](server.mjs). The viewer is set to 1280 × 800 pixels. Change both the server viewport and UI aspect ratio if you use another size. This example tracks mouse events in the main remote tab. A new remote tab or a frame inside the remote page needs its own pointer listener. diff --git a/examples/javascript/live-view-cursor-ui/server.mjs b/examples/javascript/live-view-cursor-ui/server.mjs index e88f4c7..ed70fb1 100644 --- a/examples/javascript/live-view-cursor-ui/server.mjs +++ b/examples/javascript/live-view-cursor-ui/server.mjs @@ -20,9 +20,17 @@ const viewport = { width: 1280, height: 800 }; const clients = new Set(); let current; let agentRunning = false; -let inputQueue = Promise.resolve(); +let agentPoint = null; +let agentPointSeq = 0; +let statusText = "Starting a new Browserbase session"; function emit(message) { + if (message.type === "status") statusText = message.text; + if (message.type === "agent-pointer") { + agentPoint = message; + agentPointSeq++; + } + if (message.type === "agent-state" && !message.running) agentPoint = null; const line = `data: ${JSON.stringify(message)}\n\n`; for (const client of clients) client.write(line); } @@ -45,6 +53,7 @@ async function closeSession(session) { async function createSession() { agentRunning = false; + agentPoint = null; emit({ type: "status", text: "Starting a new Browserbase session" }); const old = current; current = undefined; @@ -69,8 +78,6 @@ async function createSession() { playwrightBrowser = await chromium.connectOverCDP(session.connectUrl); const context = playwrightBrowser.contexts()[0]; const page = context.pages()[0] ?? (await context.newPage()); - const cdp = await context.newCDPSession(page); - await page.exposeFunction("__liveCursorFromPage", (point) => { if (agentRunning && current?.id === sessionId) { emit({ type: "agent-pointer", x: point.x, y: point.y, click: point.click }); @@ -98,7 +105,6 @@ async function createSession() { current = { id: sessionId, page, - cdp, stagehand, stagehandBrowser, playwrightBrowser, @@ -142,60 +148,6 @@ async function readJson(request) { return JSON.parse(raw || "{}"); } -function point(input) { - const x = Number(input.x); - const y = Number(input.y); - if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("Invalid pointer position"); - return { - x: Math.max(0, Math.min(viewport.width - 1, x)), - y: Math.max(0, Math.min(viewport.height - 1, y)), - }; -} - -async function dispatchInput(input) { - if (!current || agentRunning) return; - const { cdp, page } = current; - switch (input.type) { - case "move": - await cdp.send("Input.dispatchMouseEvent", { - type: "mouseMoved", - ...point(input), - button: "none", - buttons: input.buttons ? 1 : 0, - }); - break; - case "down": - case "up": - await cdp.send("Input.dispatchMouseEvent", { - type: input.type === "down" ? "mousePressed" : "mouseReleased", - ...point(input), - button: "left", - buttons: input.type === "down" ? 1 : 0, - clickCount: 1, - }); - break; - case "wheel": - await cdp.send("Input.dispatchMouseEvent", { - type: "mouseWheel", - ...point(input), - deltaX: Number(input.deltaX) || 0, - deltaY: Number(input.deltaY) || 0, - }); - break; - case "text": - if (typeof input.text !== "string" || input.text.length > 1000) - throw new Error("Invalid text"); - await page.keyboard.insertText(input.text); - break; - case "key": - if (typeof input.key !== "string" || input.key.length > 60) throw new Error("Invalid key"); - await page.keyboard.press(input.key); - break; - default: - throw new Error("Invalid input type"); - } -} - async function runAgent(driver) { if (!current || agentRunning) return; const session = current; @@ -282,6 +234,15 @@ const server = http.createServer(async (request, response) => { : { error: "Session is starting" }, ); } + if (request.method === "GET" && url.pathname === "/api/pointer") { + return sendJson(response, 200, { + sessionId: current?.id ?? null, + running: agentRunning, + point: agentPoint, + seq: agentPointSeq, + status: statusText, + }); + } if (request.method === "GET" && url.pathname === "/api/events") { response.writeHead(200, { "Content-Type": "text/event-stream", @@ -290,7 +251,7 @@ const server = http.createServer(async (request, response) => { }); response.write("\n"); clients.add(response); - request.on("close", () => clients.delete(response)); + response.on("close", () => clients.delete(response)); return; } if (request.method === "POST") { @@ -302,12 +263,6 @@ const server = http.createServer(async (request, response) => { ) { return sendJson(response, 403, { error: "Invalid origin" }); } - if (url.pathname === "/api/input") { - const input = await readJson(request); - inputQueue = inputQueue.catch(() => {}).then(() => dispatchInput(input)); - await inputQueue; - return sendJson(response, 200, { ok: true }); - } if (url.pathname === "/api/run") { if (agentRunning) return sendJson(response, 409, { error: "Agent is running" }); const { driver } = await readJson(request); diff --git a/examples/javascript/live-view-cursor-ui/ui/app.js b/examples/javascript/live-view-cursor-ui/ui/app.js index fc5b6da..a1c1c97 100644 --- a/examples/javascript/live-view-cursor-ui/ui/app.js +++ b/examples/javascript/live-view-cursor-ui/ui/app.js @@ -1,7 +1,5 @@ const stage = document.querySelector("#stage"); -const layer = document.querySelector("#input-layer"); const frame = document.querySelector("#live-frame"); -const human = document.querySelector("#human-cursor"); const agent = document.querySelector("#agent-cursor"); const status = document.querySelector("#status"); const sessionLabel = document.querySelector("#session-id"); @@ -11,14 +9,10 @@ const playwrightButton = document.querySelector("#run-playwright"); const newButton = document.querySelector("#new-session"); let viewport = { width: 1280, height: 800 }; let agentRunning = false; -let humanInside = false; let agentPoint = null; -let buttons = 0; -const actionQueue = []; -let latestMove = null; -let sending = false; -let pendingMove = null; -let moveTimer = null; +let loadedSessionId = null; +let lastPointSeq = 0; +let pollingPointer = false; function moveCursor(element, x, y) { element.style.transform = `translate3d(${x - 2}px,${y - 2}px,0)`; @@ -32,167 +26,60 @@ function pulse(element) { ring.classList.add("pulse"); } -function screenPoint(event) { - const rect = stage.getBoundingClientRect(); - const localX = Math.max(0, Math.min(rect.width, event.clientX - rect.left)); - const localY = Math.max(0, Math.min(rect.height, event.clientY - rect.top)); - return { - x: Math.round((localX / rect.width) * viewport.width), - y: Math.round((localY / rect.height) * viewport.height), - localX, - localY, - }; -} - -async function flushInputs() { - if (sending) return; - sending = true; - try { - while (actionQueue.length || latestMove) { - const input = actionQueue.length ? actionQueue.shift() : latestMove; - if (input === latestMove) latestMove = null; - try { - const response = await fetch("/api/input", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(input), - }); - if (!response.ok) throw new Error("Browser input failed"); - } catch (error) { - status.textContent = String(error.message ?? error); - } - } - } finally { - sending = false; - } -} - -function send(input) { - if (input.type === "move") latestMove = input; - else actionQueue.push(input); - void flushInputs(); -} - -function flushMove() { - moveTimer = null; - if (!pendingMove || agentRunning) return; - const point = pendingMove; - pendingMove = null; - void send({ type: "move", x: point.x, y: point.y, buttons }); -} - function showAgentIfNeeded() { if (agentRunning && agentPoint) agent.classList.add("visible"); else agent.classList.remove("visible"); } -layer.addEventListener("pointerenter", () => { - humanInside = true; - showAgentIfNeeded(); -}); -layer.addEventListener("pointerleave", () => { - humanInside = false; - human.classList.remove("visible"); - showAgentIfNeeded(); -}); -layer.addEventListener("pointermove", (event) => { - const point = screenPoint(event); - moveCursor(human, point.localX, point.localY); - if (agentRunning) return; - pendingMove = point; - if (!moveTimer) moveTimer = setTimeout(flushMove, 32); -}); -layer.addEventListener("pointerdown", (event) => { - if (event.button !== 0) return; - event.preventDefault(); - layer.focus(); - layer.setPointerCapture(event.pointerId); - const point = screenPoint(event); - moveCursor(human, point.localX, point.localY); - pulse(human); - if (agentRunning) return; - buttons = 1; - pendingMove = null; - latestMove = null; - void send({ type: "down", x: point.x, y: point.y }); -}); -layer.addEventListener("pointerup", (event) => { - if (event.button !== 0 || agentRunning) return; - buttons = 0; - const point = screenPoint(event); - if (pendingMove || latestMove) { - actionQueue.push({ type: "move", x: point.x, y: point.y, buttons: 1 }); - pendingMove = null; - latestMove = null; - } - void send({ type: "up", x: point.x, y: point.y }); -}); -layer.addEventListener("pointercancel", (event) => { - if (buttons && !agentRunning) { - const point = screenPoint(event); - void send({ type: "up", x: point.x, y: point.y }); - } - buttons = 0; -}); -layer.addEventListener( - "wheel", - (event) => { - event.preventDefault(); - if (agentRunning) return; - const point = screenPoint(event); - void send({ - type: "wheel", - x: point.x, - y: point.y, - deltaX: event.deltaX, - deltaY: event.deltaY, - }); - }, - { passive: false }, -); -layer.addEventListener("keydown", (event) => { - if (agentRunning || ["Shift", "Control", "Alt", "Meta"].includes(event.key)) { - event.preventDefault(); - return; - } - const key = event.key; - if ((event.metaKey || event.ctrlKey) && key.toLowerCase() === "v") return; - if (!event.metaKey && !event.ctrlKey && !event.altKey && key.length === 1) return; - event.preventDefault(); - const mods = [ - event.ctrlKey && "Control", - event.metaKey && "Meta", - event.altKey && "Alt", - event.shiftKey && "Shift", - ].filter(Boolean); - void send({ type: "key", key: [...mods, key].join("+") }); -}); -layer.addEventListener("beforeinput", (event) => { - event.preventDefault(); - if (agentRunning || !event.data) return; - if (event.inputType === "insertText" || event.inputType === "insertCompositionText") { - void send({ type: "text", text: event.data }); - } -}); -layer.addEventListener("paste", (event) => { - event.preventDefault(); - if (agentRunning) return; - const text = event.clipboardData?.getData("text/plain"); - if (text) void send({ type: "text", text }); -}); -window.addEventListener("blur", () => human.classList.remove("visible")); - async function loadSession() { const response = await fetch("/api/config", { cache: "no-store" }); const config = await response.json(); if (!config.liveViewUrl) throw new Error(config.error || "Live View is not ready"); + if (config.sessionId === loadedSessionId) return; + loadedSessionId = config.sessionId; viewport = config.viewport; sessionLabel.textContent = config.sessionId; - frame.src = config.liveViewUrl; + loading.classList.remove("hidden"); frame.onload = () => loading.classList.add("hidden"); + frame.src = config.liveViewUrl; status.textContent = "Live View is ready"; } +async function pollPointer() { + if (pollingPointer || document.visibilityState !== "visible") return; + pollingPointer = true; + try { + const response = await fetch("/api/pointer", { cache: "no-store" }); + if (!response.ok) return; + const snapshot = await response.json(); + if (snapshot.sessionId && snapshot.sessionId !== loadedSessionId) { + void loadSession().catch((error) => { + status.textContent = error.message; + }); + } + agentRunning = snapshot.running; + runButton.disabled = agentRunning; + playwrightButton.disabled = agentRunning; + if (snapshot.status) status.textContent = snapshot.status; + if (snapshot.seq !== lastPointSeq && snapshot.point) { + lastPointSeq = snapshot.seq; + agentPoint = snapshot.point; + moveCursor( + agent, + (agentPoint.x / viewport.width) * stage.clientWidth, + (agentPoint.y / viewport.height) * stage.clientHeight, + ); + if (agentPoint.click) pulse(agent); + } + if (!agentRunning) agentPoint = null; + showAgentIfNeeded(); + } catch (error) { + status.textContent = String(error.message ?? error); + } finally { + pollingPointer = false; + } +} + async function startDemo(driver) { runButton.disabled = true; playwrightButton.disabled = true; @@ -230,7 +117,11 @@ const events = new EventSource("/api/events"); events.onmessage = ({ data }) => { const message = JSON.parse(data); if (message.type === "status") status.textContent = message.text; - if (message.type === "session") sessionLabel.textContent = message.sessionId; + if (message.type === "session" && message.sessionId !== loadedSessionId) { + void loadSession().catch((error) => { + status.textContent = error.message; + }); + } if (message.type === "agent-state") { agentRunning = message.running; runButton.disabled = agentRunning; @@ -252,3 +143,11 @@ events.onmessage = ({ data }) => { loadSession().catch((error) => { status.textContent = error.message; }); +setInterval(() => void pollPointer(), 100); +setInterval(() => { + if (document.visibilityState === "visible") { + void loadSession().catch((error) => { + status.textContent = error.message; + }); + } +}, 5000); diff --git a/examples/javascript/live-view-cursor-ui/ui/index.html b/examples/javascript/live-view-cursor-ui/ui/index.html index b4ea26a..b2b5b30 100644 --- a/examples/javascript/live-view-cursor-ui/ui/index.html +++ b/examples/javascript/live-view-cursor-ui/ui/index.html @@ -3,7 +3,7 @@ - Browserbase Live View · Cursor Lab + Browserbase Live View · Agent Pointer @@ -16,7 +16,7 @@ browserbase / - Live View + Live View · Agent Pointer
LOCAL VIEWER @@ -30,8 +30,7 @@
INTERACTIVE SESSION

Live browser

- Use your mouse and keyboard here. Run Stagehand or Playwright to see the agent - pointer. + Use the Live View directly. Run Stagehand or Playwright to see the agent pointer.

@@ -65,13 +64,6 @@

Live browser

title="Browserbase Live View" allow="clipboard-read; clipboard-write" > -
-
↗This local page draws both pointers. The Browserbase Live View remains inside the - frame.This page draws only the agent pointer. Human input stays inside Live View.
diff --git a/examples/javascript/live-view-cursor-ui/ui/styles.css b/examples/javascript/live-view-cursor-ui/ui/styles.css index 25856d6..f5ffdad 100644 --- a/examples/javascript/live-view-cursor-ui/ui/styles.css +++ b/examples/javascript/live-view-cursor-ui/ui/styles.css @@ -228,20 +228,9 @@ p { width: 100%; height: 100%; border: 0; - pointer-events: none; + pointer-events: auto; background: #fff; } -.input-layer { - position: absolute; - inset: 0; - z-index: 3; - cursor: none; - touch-action: none; - outline: none; - caret-color: transparent; - color: transparent; - user-select: none; -} .loading { position: absolute; inset: 0; diff --git a/skills/browserbase-cookbook/references/agents-and-human-handoff.md b/skills/browserbase-cookbook/references/agents-and-human-handoff.md index 4d5930c..09086be 100644 --- a/skills/browserbase-cookbook/references/agents-and-human-handoff.md +++ b/skills/browserbase-cookbook/references/agents-and-human-handoff.md @@ -8,7 +8,7 @@ This topic contains all 28 matching entries. Browse `docs/topics/agents-and-huma | Recipe | Language | Local guide | Status | Historical source | | --- | --- | --- | --- | --- | -| Live View special pointers (JavaScript) | javascript | `docs/recipes/examples-javascript-live-view-cursor-ui.md` | current | cookbook-authored | +| Live View agent pointer (JavaScript) | javascript | `docs/recipes/examples-javascript-live-view-cursor-ui.md` | current | cookbook-authored | | Anthropic Claude browser agent (Python) | python | `docs/recipes/examples-python-anthropic-cua.md` | current | cookbook-authored | | Business lookup (Python) | python | `docs/recipes/examples-python-business-lookup.md` | current | [pinned import](https://github.com/browserbase/templates/tree/088ff598518cf69b4edf1e1260dd57ff11ca55d8/python/business-lookup) | | Cartesia form filling (Python) | python | `docs/recipes/examples-python-cartesia-form-filling.md` | current | [pinned import](https://github.com/browserbase/templates/tree/088ff598518cf69b4edf1e1260dd57ff11ca55d8/python/cartesia-form-filling) | diff --git a/skills/browserbase-cookbook/references/testing-and-observability.md b/skills/browserbase-cookbook/references/testing-and-observability.md index 191ee6d..76f29d0 100644 --- a/skills/browserbase-cookbook/references/testing-and-observability.md +++ b/skills/browserbase-cookbook/references/testing-and-observability.md @@ -8,7 +8,7 @@ This topic contains all 8 matching entries. Browse `docs/topics/testing-and-obse | Recipe | Language | Local guide | Status | Historical source | | --- | --- | --- | --- | --- | -| Live View special pointers (JavaScript) | javascript | `docs/recipes/examples-javascript-live-view-cursor-ui.md` | current | cookbook-authored | +| Live View agent pointer (JavaScript) | javascript | `docs/recipes/examples-javascript-live-view-cursor-ui.md` | current | cookbook-authored | | Agentic KYB registry research (Python, Stagehand v4) | python | `docs/recipes/examples-python-agentic-kyb.md` | current | cookbook-authored | | Cerebras docs checker (Python) | python | `docs/recipes/examples-python-cerebras-docs-checker.md` | current | [pinned import](https://github.com/browserbase/templates/tree/088ff598518cf69b4edf1e1260dd57ff11ca55d8/python/cerebras-docs-checker) | | Smart fetch scraper (Python) | python | `docs/recipes/examples-python-smart-fetch-scraper.md` | current | [pinned import](https://github.com/browserbase/templates/tree/088ff598518cf69b4edf1e1260dd57ff11ca55d8/python/smart-fetch-scraper) |