diff --git a/COOKBOOK_AUTHORED.json b/COOKBOOK_AUTHORED.json index d681933..9542331 100644 --- a/COOKBOOK_AUTHORED.json +++ b/COOKBOOK_AUTHORED.json @@ -3,7 +3,7 @@ "files": [ { "path": "examples/README.md", - "sha256": "d4b80442ca66ab0f2c3a4d9666ff6b16bdfeed6aa4396ee76ebef5f3253ea63c", + "sha256": "92f2c66626bc21a74f9641616fdfe8395de6b788fb2d248cf8cf56db55d70405", "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": "71fcc1931cdf76cb93fa874b426b6e6d069fb8b1ea64c31379800830bd84d8b2", + "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": "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": "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": "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": "999fc836f8b0a64c7e79f9dbd8058c8039740103c7a3edfa4fedb92a64b0e81a", + "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..1d48648 100644 --- a/catalog.json +++ b/catalog.json @@ -332,6 +332,126 @@ "verification": "source-inspected", "working_directory": "examples/go/hackernews" }, + { + "access": "public", + "caveats": [ + "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": { + "@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 with a custom agent pointer while human input uses the native Live View.", + "tags": [], + "title": "Live View agent pointer (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" + }, + { + "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" + }, { "access": "public", "caveats": [], diff --git a/docs/catalog.md b/docs/catalog.md index 9dd6828..4bc9269 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 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 new file mode 100644 index 0000000..4e5ec0b --- /dev/null +++ b/docs/recipes/examples-javascript-live-view-cursor-ui.md @@ -0,0 +1,86 @@ +# Live View agent pointer (JavaScript) + +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. + +**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. | +| 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 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 + +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..a4b0fe4 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 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 bc9a785..034f19b 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 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 6191854..ac0d7fc 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 agent pointer](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 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/.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..b500b6d --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/README.md @@ -0,0 +1,47 @@ +# Agent pointer in Browserbase Live View + +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. + +## 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/). 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. + +## 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 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. + +## Human input and pointer delay + +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 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 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/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..ed70fb1 --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/server.mjs @@ -0,0 +1,314 @@ +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 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); +} + +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; + agentPoint = null; + 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()); + 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, + 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 || "{}"); +} + +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/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", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + response.write("\n"); + clients.add(response); + response.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/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..a1c1c97 --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/ui/app.js @@ -0,0 +1,153 @@ +const stage = document.querySelector("#stage"); +const frame = document.querySelector("#live-frame"); +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 agentPoint = 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)`; + 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 showAgentIfNeeded() { + if (agentRunning && agentPoint) agent.classList.add("visible"); + else agent.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; + 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; + 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" && message.sessionId !== loadedSessionId) { + void loadSession().catch((error) => { + status.textContent = error.message; + }); + } + 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; +}); +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 new file mode 100644 index 0000000..b2b5b30 --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/ui/index.html @@ -0,0 +1,98 @@ + + + + + + Browserbase Live View · Agent Pointer + + + + +
+
+
+ + browserbase + / + Live View · Agent Pointer +
+
+ LOCAL VIEWER + +
+
+ +
+
+
+
INTERACTIVE SESSION
+

Live browser

+

+ Use the Live View directly. Run Stagehand or Playwright to see the agent pointer. +

+
+
+ + +
+
+ +
+
+
+ Live ViewStarting session… +
+
+ 1280 × 800INTERACTIVE +
+
+
+
+
+ Connecting to Browserbase… +
+ + +
+ +
+
+ ↗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 new file mode 100644 index 0000000..f5ffdad --- /dev/null +++ b/examples/javascript/live-view-cursor-ui/ui/styles.css @@ -0,0 +1,408 @@ +: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: auto; + background: #fff; +} +.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..09086be 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 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 aa204e2..76f29d0 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 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) |