diff --git a/.changeset/calm-cursors-appear.md b/.changeset/calm-cursors-appear.md
new file mode 100644
index 000000000..3cf8523a8
--- /dev/null
+++ b/.changeset/calm-cursors-appear.md
@@ -0,0 +1,9 @@
+---
+"@browserbasehq/stagehand-protocol": patch
+"@browserbasehq/stagehand-python": patch
+"@browserbasehq/stagehand-extension": patch
+"@browserbasehq/stagehand-go": patch
+"@browserbasehq/stagehand": patch
+---
+
+expose page cursor overlay enablement across the V4 SDKs
diff --git a/packages/docs/v4/reference/page.mdx b/packages/docs/v4/reference/page.mdx
index fff407414..768f5c5fe 100644
--- a/packages/docs/v4/reference/page.mdx
+++ b/packages/docs/v4/reference/page.mdx
@@ -389,6 +389,18 @@ await page.addInitScript(() => {
Resolves after the operation completes.
+## enableCursorOverlay()
+
+Show the Stagehand cursor overlay used for coordinate-based interactions.
+
+```typescript
+await page.enableCursorOverlay();
+```
+
+
+ Resolves after the overlay is enabled.
+
+
## setExtraHTTPHeaders()
Set additional HTTP headers for this page.
@@ -1064,6 +1076,18 @@ await page.add_init_script("window.localStorage.clear()")
Resolves after the operation completes.
+## enable_cursor_overlay()
+
+Show the Stagehand cursor overlay used for coordinate-based interactions.
+
+```python
+await page.enable_cursor_overlay()
+```
+
+
+ Resolves after the overlay is enabled.
+
+
## set_extra_http_headers()
Set additional HTTP headers for this page.
@@ -1778,6 +1802,20 @@ if err := page.AddInitScript(ctx, "window.localStorage.clear()"); err != nil {
Returns `nil` after the operation completes.
+## EnableCursorOverlay()
+
+Show the Stagehand cursor overlay used for coordinate-based interactions.
+
+```go
+if err := page.EnableCursorOverlay(ctx); err != nil {
+ return err
+}
+```
+
+
+ Returns `nil` after the overlay is enabled.
+
+
## SetExtraHTTPHeaders()
Set additional HTTP headers for this page.
diff --git a/packages/extension/controllers/pageController.ts b/packages/extension/controllers/pageController.ts
index a3b4b776b..499cebe07 100644
--- a/packages/extension/controllers/pageController.ts
+++ b/packages/extension/controllers/pageController.ts
@@ -90,6 +90,11 @@ export function createPageController(runtime: StagehandRuntime) {
return runtime.pageAddInitScript(params);
}
+ async function enableCursorOverlay(params: PageIdParams, { logger }: HandlerContext) {
+ logger.debug("page.enable_cursor_overlay", {});
+ return runtime.pageEnableCursorOverlay(params);
+ }
+
async function setExtraHTTPHeaders(
params: PageSetExtraHTTPHeadersParams,
{ logger }: HandlerContext,
@@ -192,6 +197,7 @@ export function createPageController(runtime: StagehandRuntime) {
keyPress,
evaluate,
addInitScript,
+ enableCursorOverlay,
setExtraHTTPHeaders,
setViewportSize,
waitForLoadState,
diff --git a/packages/extension/rpcRouter.ts b/packages/extension/rpcRouter.ts
index c796321fc..56218ed48 100644
--- a/packages/extension/rpcRouter.ts
+++ b/packages/extension/rpcRouter.ts
@@ -323,6 +323,11 @@ export class RPCRouter {
parseParams(StagehandMethods.pageAddInitScript, request.params),
context,
);
+ case "page.enable_cursor_overlay":
+ return this.pageController.enableCursorOverlay(
+ parseParams(StagehandMethods.pageEnableCursorOverlay, request.params),
+ context,
+ );
case "page.on":
return this.pageController.on(
parseParams(StagehandMethods.pageOn, request.params),
diff --git a/packages/extension/runtime.ts b/packages/extension/runtime.ts
index 403cd7fd3..c98181b93 100644
--- a/packages/extension/runtime.ts
+++ b/packages/extension/runtime.ts
@@ -148,6 +148,7 @@ export type UnderstudyRuntimePage = {
keyPress(key: string, options?: PageKeyPressParams["options"]): Promise;
evaluate(expression: string): Promise;
addInitScript(source: string): Promise;
+ enableCursorOverlay(): Promise;
setExtraHTTPHeaders(headers: PageSetExtraHTTPHeadersParams["headers"]): Promise;
setViewportSize(
width: number,
@@ -739,6 +740,11 @@ export class StagehandRuntime {
return { closed: true };
}
+ async pageEnableCursorOverlay(params: PageIdParams): Promise {
+ await this.resolvePage(params.pageId).enableCursorOverlay();
+ return { ok: true };
+ }
+
pageOn(params: PageOnParams): PageVoidResult {
if (this.pageEventSubscriptions.has(params.subscriptionId)) {
throw new DuplicatePageEventSubscriptionError();
diff --git a/packages/extension/tests/stagehand-clients.test.ts b/packages/extension/tests/stagehand-clients.test.ts
index 3c1c78d37..21669a429 100644
--- a/packages/extension/tests/stagehand-clients.test.ts
+++ b/packages/extension/tests/stagehand-clients.test.ts
@@ -216,6 +216,7 @@ class FakeUnderstudyRuntimePage implements UnderstudyRuntimePage {
readonly keyPressCalls: Array<{ key: string; options?: PageKeyPressParams["options"] }> = [];
readonly evaluateCalls: string[] = [];
readonly addInitScriptCalls: string[] = [];
+ enableCursorOverlayCalls = 0;
readonly setExtraHTTPHeadersCalls: Array = [];
readonly setViewportSizeCalls: Array<{
width: number;
@@ -349,6 +350,10 @@ class FakeUnderstudyRuntimePage implements UnderstudyRuntimePage {
this.addInitScriptCalls.push(source);
}
+ async enableCursorOverlay(): Promise {
+ this.enableCursorOverlayCalls += 1;
+ }
+
async setExtraHTTPHeaders(headers: PageSetExtraHTTPHeadersParams["headers"]): Promise {
this.setExtraHTTPHeadersCalls.push(headers);
}
@@ -1832,6 +1837,22 @@ describe("Stagehand worker clients", () => {
expect(page.addInitScriptCalls).toStrictEqual(["globalThis.ready = true"]);
});
+ it("routes cursor overlay enablement", async () => {
+ const page = new FakeUnderstudyRuntimePage("page-a", "https://example.test/current");
+ const handle = await createConfiguredHandler(new FakeBrowserSession([page]));
+
+ await expect(
+ handle({
+ jsonrpc: "2.0",
+ id: 240,
+ method: "page.enable_cursor_overlay",
+ params: { page_id: "page-a" },
+ }),
+ ).resolves.toStrictEqual({ jsonrpc: "2.0", id: 240, result: { ok: true } });
+
+ expect(page.enableCursorOverlayCalls).toBe(1);
+ });
+
it("routes page headers and viewport configuration", async () => {
const page = new FakeUnderstudyRuntimePage("page-a", "https://example.test/current");
const handle = await createConfiguredHandler(new FakeBrowserSession([page]));
diff --git a/packages/protocol/schema-registry.ts b/packages/protocol/schema-registry.ts
index 1ebef362e..9040ec7c6 100644
--- a/packages/protocol/schema-registry.ts
+++ b/packages/protocol/schema-registry.ts
@@ -324,6 +324,11 @@ export const StagehandMethods = {
params: PageAddInitScriptParamsSchema,
result: PageVoidResultSchema,
},
+ pageEnableCursorOverlay: {
+ name: "page.enable_cursor_overlay",
+ params: PageIdParamsSchema,
+ result: PageVoidResultSchema,
+ },
pageOn: {
name: "page.on",
params: PageOnParamsSchema,
diff --git a/packages/protocol/stagehand.v4.json b/packages/protocol/stagehand.v4.json
index 35ab62440..08a9403e7 100644
--- a/packages/protocol/stagehand.v4.json
+++ b/packages/protocol/stagehand.v4.json
@@ -526,6 +526,19 @@
"required": ["params", "result"],
"additionalProperties": false
},
+ "page.enable_cursor_overlay": {
+ "type": "object",
+ "properties": {
+ "params": {
+ "$ref": "#/$defs/PageIdParams"
+ },
+ "result": {
+ "$ref": "#/$defs/PageVoidResult"
+ }
+ },
+ "required": ["params", "result"],
+ "additionalProperties": false
+ },
"page.on": {
"type": "object",
"properties": {
@@ -1036,6 +1049,7 @@
"page.key_press",
"page.evaluate",
"page.add_init_script",
+ "page.enable_cursor_overlay",
"page.on",
"page.off",
"page.set_extra_http_headers",
@@ -6257,6 +6271,33 @@
"required": ["jsonrpc", "id", "method", "params"],
"additionalProperties": false
},
+ {
+ "type": "object",
+ "properties": {
+ "jsonrpc": {
+ "type": "string",
+ "const": "2.0"
+ },
+ "id": {
+ "$ref": "#/$defs/JSONRPCRequestId"
+ },
+ "method": {
+ "type": "string",
+ "const": "page.enable_cursor_overlay"
+ },
+ "params": {
+ "$ref": "#/$defs/PageIdParams"
+ },
+ "traceparent": {
+ "type": "string"
+ },
+ "tracestate": {
+ "type": "string"
+ }
+ },
+ "required": ["jsonrpc", "id", "method", "params"],
+ "additionalProperties": false
+ },
{
"type": "object",
"properties": {
diff --git a/packages/protocol/tests/protocol/object-model-protocol.test.ts b/packages/protocol/tests/protocol/object-model-protocol.test.ts
index d2c708ffa..241909e59 100644
--- a/packages/protocol/tests/protocol/object-model-protocol.test.ts
+++ b/packages/protocol/tests/protocol/object-model-protocol.test.ts
@@ -393,6 +393,7 @@ describe("Stagehand object-model protocol", () => {
"page.key_press",
"page.evaluate",
"page.add_init_script",
+ "page.enable_cursor_overlay",
"page.on",
"page.off",
"page.set_extra_http_headers",
diff --git a/packages/sdk-go/internal/extensionassets/stagehand-extension.zip b/packages/sdk-go/internal/extensionassets/stagehand-extension.zip
index dbac537ee..001ca5a31 100644
Binary files a/packages/sdk-go/internal/extensionassets/stagehand-extension.zip and b/packages/sdk-go/internal/extensionassets/stagehand-extension.zip differ
diff --git a/packages/sdk-go/page.go b/packages/sdk-go/page.go
index 6a98cbc7c..36b35b2a8 100644
--- a/packages/sdk-go/page.go
+++ b/packages/sdk-go/page.go
@@ -243,6 +243,13 @@ func (p *Page) AddInitScript(ctx context.Context, source string) error {
return p.rpc.call(ctx, "page.add_init_script", params, &result)
}
+// EnableCursorOverlay renders the Stagehand cursor for coordinate-based interactions.
+func (p *Page) EnableCursorOverlay(ctx context.Context) error {
+ params := PageIDParams{PageID: p.PageID()}
+ var result PageVoidResult
+ return p.rpc.call(ctx, "page.enable_cursor_overlay", params, &result)
+}
+
// On subscribes to console events for this page and its OOPIF sessions.
func (p *Page) On(
ctx context.Context,
diff --git a/packages/sdk-go/page_test.go b/packages/sdk-go/page_test.go
index 7d99d1aa9..2f8d78d32 100644
--- a/packages/sdk-go/page_test.go
+++ b/packages/sdk-go/page_test.go
@@ -103,6 +103,25 @@ func TestPageCoordinateInteractionsReturnOnlyErrors(t *testing.T) {
}
}
+func TestPageEnableCursorOverlay(t *testing.T) {
+ t.Parallel()
+
+ rpc := &recordingProtocolClient{responses: map[string]any{
+ "page.enable_cursor_overlay": PageVoidResult{Ok: true},
+ }}
+ page := &Page{rpc: rpc, ref: PageRef{PageID: "page-1"}}
+
+ if err := page.EnableCursorOverlay(context.Background()); err != nil {
+ t.Fatalf("EnableCursorOverlay() error = %v", err)
+ }
+ if len(rpc.calls) != 1 || rpc.calls[0].method != "page.enable_cursor_overlay" {
+ t.Fatalf("EnableCursorOverlay() calls = %#v", rpc.calls)
+ }
+ if got := rpc.calls[0].params; !reflect.DeepEqual(got, PageIDParams{PageID: "page-1"}) {
+ t.Fatalf("EnableCursorOverlay() params = %#v", got)
+ }
+}
+
func TestPageOnDeliversCanonicalConsoleEventsAndUnsubscribes(t *testing.T) {
t.Parallel()
diff --git a/packages/sdk-python/src/stagehand/page.py b/packages/sdk-python/src/stagehand/page.py
index d7df05010..21290c936 100644
--- a/packages/sdk-python/src/stagehand/page.py
+++ b/packages/sdk-python/src/stagehand/page.py
@@ -385,6 +385,13 @@ async def add_init_script(self, source: str | Path) -> None:
PageVoidResult,
)
+ async def enable_cursor_overlay(self) -> None:
+ await self._rpc_client.send(
+ "page.enable_cursor_overlay",
+ PageIdParams(page_id=self.page_id),
+ PageVoidResult,
+ )
+
async def on(
self,
event: PageEventName,
diff --git a/packages/sdk-python/tests/test_page.py b/packages/sdk-python/tests/test_page.py
index 8abf82dbe..fbff82f47 100644
--- a/packages/sdk-python/tests/test_page.py
+++ b/packages/sdk-python/tests/test_page.py
@@ -54,6 +54,19 @@ class EvaluationResult(BaseModel):
RootResultT = TypeVar("RootResultT")
+@pytest.mark.asyncio
+async def test_page_enables_cursor_overlay() -> None:
+ recording = RecordingRPCClient({"page.enable_cursor_overlay": PageVoidResult(ok=True)})
+ page = Page(cast(RPCClient, recording), PageRef(page_id="page-1"))
+
+ await page.enable_cursor_overlay()
+
+ method, params, result_model = recording.calls[0]
+ assert method == "page.enable_cursor_overlay"
+ assert params == PageIdParams(page_id="page-1")
+ assert result_model is PageVoidResult
+
+
@pytest.mark.asyncio
async def test_page_navigation_uses_generated_wire_models_and_updates_the_page_reference() -> None:
recording = RecordingRPCClient({
diff --git a/packages/sdk-ts/src/page.ts b/packages/sdk-ts/src/page.ts
index 8a0fc8f76..4362daf23 100644
--- a/packages/sdk-ts/src/page.ts
+++ b/packages/sdk-ts/src/page.ts
@@ -209,6 +209,12 @@ export class Page {
});
}
+ async enableCursorOverlay(): Promise {
+ await this.rpcClient.send(StagehandMethods.pageEnableCursorOverlay, {
+ pageId: this.pageId,
+ });
+ }
+
async on(event: PageEventName, listener: PageEventListener): Promise {
const subscriptionId = crypto.randomUUID();
const removeNotificationListener = this.rpcClient.onNotification((notification) => {
diff --git a/packages/sdk-ts/tests/objectWrapper.test.ts b/packages/sdk-ts/tests/objectWrapper.test.ts
index 15ac59d4f..d9bd606c3 100644
--- a/packages/sdk-ts/tests/objectWrapper.test.ts
+++ b/packages/sdk-ts/tests/objectWrapper.test.ts
@@ -866,6 +866,18 @@ describe("Stagehand TS object wrapper", () => {
]);
});
+ it("enables the page cursor overlay", async () => {
+ const client = new FakeProtocolClient();
+ client.queueResponse(StagehandMethods.pageEnableCursorOverlay, { ok: true });
+ const page = new Page(client, { pageId: "page-1" });
+
+ await page.enableCursorOverlay();
+
+ expect(client.calls).toStrictEqual([
+ requestCall(StagehandMethods.pageEnableCursorOverlay, { pageId: "page-1" }),
+ ]);
+ });
+
it("routes page headers and viewport configuration", async () => {
const client = new FakeProtocolClient();
client.queueResponse(StagehandMethods.pageSetExtraHTTPHeaders, { ok: true });