Skip to content

feat(agent): actuate CUA clicks as touch on mobile sessions - #2384

Closed
alonle wants to merge 1 commit into
browserbase:contrib/2384from
alonle:feat/cua-touch-tap-on-mobile
Closed

feat(agent): actuate CUA clicks as touch on mobile sessions#2384
alonle wants to merge 1 commit into
browserbase:contrib/2384from
alonle:feat/cua-touch-tap-on-mobile

Conversation

@alonle

@alonle alonle commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

The computer-use agent actuates every pointer action through Input.dispatchMouseEvent (via page.click(x, y) in V3CuaAgentHandler). On a mobile session the browser renders the site's touch-gated mobile layout, whose handlers listen for touch/pointer events (pointerType: "touch") — a synthesized mouse click doesn't register there.

Concrete repro on adidas.co.il (Browserbase browserSettings.verified: true, os: "mobile"), same product/size, empty cart:

Actuation Result
trusted touch (Input.dispatchTouchEvent) added to cart
mouse click (Input.dispatchMouseEvent) blocked — "אנא בחרו מידה" / "please choose a size"
CUA hybrid agent (mouse) blocked — size highlights but never registers; add-to-cart errors

The size visually highlights but is never registered, so add-to-cart fails. This blocks mobile e-commerce flows (size selectors, add-to-cart) on any touch-gated layout.

Fix

  • Add a coordinate page.tap(x, y) to the understudy Page — a trusted touch tap via Input.dispatchTouchEvent, mirroring the existing coordinate page.click.
  • In V3CuaAgentHandler, route the click action to page.tap when the session presents as mobile, detected once per run via navigator.userAgentData.mobile. Desktop sessions are unchanged (still page.click).

Detection note: Browserbase os: "mobile" reports maxTouchPoints: 0 and pointer: fine, but navigator.userAgentData.mobile === true — so userAgentData.mobile is the reliable signal (verified on live sessions). The trusted CDP touch works even though the context doesn't advertise touch.

Scope / open questions for maintainers

  • Only the click action is routed. double_click (and drag) could get the same treatment if desired.
  • Detection is a runtime heuristic (userAgentData.mobile). Happy to switch to an explicit config flag (e.g. agentConfig option) if you'd prefer opt-in over auto-detect.
  • Recording/replay for taps: the touch path returns no xpath (unlike page.click(returnXpath)), so a tapped step isn't captured for replay. Can extend page.tap to resolve+return the xpath if that matters.

Test plan

  • pnpm --filter @browserbasehq/stagehand typecheck — clean
  • prettier --check / eslint on changed files — clean
  • Manual: on a verified + os:"mobile" Browserbase session, the CUA agent selects a size and adds to cart on adidas.co.il (previously blocked with mouse).

🤖 Generated with Claude Code


Summary by cubic

Actuate CUA clicks as real touch on mobile sessions so touch-gated UIs register correctly. Routes a single left click to a trusted tap and records it for replay; other click types are unchanged.

  • New Features

    • Added page.tap(x, y, { returnXpath }) and locator.tap(); registered tap in the act method map. tap updates the cursor overlay and returns XPath for deterministic replay.
    • On mobile (navigator.userAgentData.mobile, cached), only a single left click (count = 1) becomes tap; right/middle/multi-clicks stay mouse. Recording stores a tap step with XPath.
  • Bug Fixes

    • Pipelined touchStart + touchEnd via Promise.all to avoid long-press on slow sessions.
    • tap errors now throw UnderstudyCommandException (not StagehandClickError) for correct classification.

Written for commit 8d8f463. Summary will update on new commits.

Review in cubic

@changeset-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 8d8f463

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run.
Approving the latest commit mirrors it into an internal PR owned by the approver.
If new commits are pushed later, the internal PR stays open but is marked stale until someone approves the latest external commit and refreshes it.

@github-actions github-actions Bot added external-contributor Tracks PRs mirrored from external contributor forks. external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. labels Jul 23, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files

Confidence score: 4/5

  • In packages/core/lib/v3/understudy/page.ts, the new Page.tap() plus mobile detection/routing in V3CuaAgentHandler introduce complex CDP touch dispatch and conditional action paths, so mismatches in capability detection could route interactions incorrectly and cause flaky or broken mobile actions; add focused coverage for touch dispatch and both routing branches to de-risk regressions.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/lib/v3/understudy/page.ts">

<violation number="1" location="packages/core/lib/v3/understudy/page.ts:1908">
P2: The new `tap()` method on `Page` and the mobile detection/routing logic in `V3CuaAgentHandler` add non-trivial behavior (CDP touch event dispatching, per-run capability detection, conditional action routing) without accompanying unit tests. Consider adding focused tests that cover: the `tap()` method's CDP touch event dispatch (happy path), the `usesTouch` detection with `userAgentData.mobile` (true/false), and the graceful catch path when `userAgentData` is unavailable.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant CUA as CUA Agent
    participant Handler as V3CuaAgentHandler
    participant Page as Understudy Page
    participant Browser as CDP Browser
    participant Site as Touch-Gated Site

    Note over CUA,Site: Click Action Flow

    CUA->>Handler: click(x, y)
    Handler->>Handler: Check isMobile cache
    
    alt usesTouch === undefined (first click)
        Handler->>Page: evaluate(navigator.userAgentData.mobile)
        Page->>Browser: Runtime.evaluate
        Browser-->>Page: boolean result
        Page-->>Handler: mobile flag
        Handler->>Handler: Cache in this.usesTouch
    end

    alt usesTouch === true (mobile session)
        Handler->>Page: tap(x, y)
        Page->>Browser: Input.dispatchTouchEvent(type: touchStart, touchPoints: [{x, y}])
        Page->>Browser: Input.dispatchTouchEvent(type: touchEnd, touchPoints: [])
        Browser-->>Page: Success
        Page-->>Handler: void
        Handler-->>CUA: { success: true }
        Note over Browser,Site: Site registers touch/pointer events<br/>Size selector responds correctly
    else usesTouch === false (desktop session)
        Handler->>Page: click(x, y, {button, clickCount})
        alt recording enabled
            Page->>Page: Resolve xpath
            Page->>Browser: Input.dispatchMouseEvent
            Browser-->>Page: Success + xpath
            Page-->>Handler: xpath
        else recording disabled
            Page->>Browser: Input.dispatchMouseEvent
            Browser-->>Page: Success
            Page-->>Handler: void
        end
        Handler-->>CUA: { success: true }
    end

    Note over Handler: Detection fallback<br/>If evaluate fails → usesTouch = false<br/>(safe default for desktop)
Loading

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

Comment thread packages/core/lib/v3/handlers/v3CuaAgentHandler.ts Outdated
* never registers — respond correctly. Coordinates are relative to the viewport
* origin (top-left). Does not scroll. Requires a touch-capable (e.g. mobile) session.
*/
@FlowLogger.wrapWithLogging({ eventType: "PageTap" })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new tap() method on Page and the mobile detection/routing logic in V3CuaAgentHandler add non-trivial behavior (CDP touch event dispatching, per-run capability detection, conditional action routing) without accompanying unit tests. Consider adding focused tests that cover: the tap() method's CDP touch event dispatch (happy path), the usesTouch detection with userAgentData.mobile (true/false), and the graceful catch path when userAgentData is unavailable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/understudy/page.ts, line 1908:

<comment>The new `tap()` method on `Page` and the mobile detection/routing logic in `V3CuaAgentHandler` add non-trivial behavior (CDP touch event dispatching, per-run capability detection, conditional action routing) without accompanying unit tests. Consider adding focused tests that cover: the `tap()` method's CDP touch event dispatch (happy path), the `usesTouch` detection with `userAgentData.mobile` (true/false), and the graceful catch path when `userAgentData` is unavailable.</comment>

<file context>
@@ -1897,6 +1897,26 @@ export class Page {
+   * never registers — respond correctly. Coordinates are relative to the viewport
+   * origin (top-left). Does not scroll. Requires a touch-capable (e.g. mobile) session.
+   */
+  @FlowLogger.wrapWithLogging({ eventType: "PageTap" })
+  async tap(x: number, y: number): Promise<void> {
+    await this.mainSession.send<never>("Input.dispatchTouchEvent", {
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an integration test for the tap actuation path in perform-understudy-method.spec.ts (drives performUnderstudyMethod("tap", …)locator.tap end to end), following the repo's existing pattern for pointer actions — click/fill/drag are covered the same way (browser-backed integration, run against the built dist). There's no in-repo unit harness that mocks a CDP session to assert Input.* payloads, so I didn't introduce net-new mocking infra for this.

Comment thread packages/core/lib/v3/handlers/v3CuaAgentHandler.ts Outdated
Comment thread packages/core/lib/v3/understudy/page.ts Outdated
@miguelg719
miguelg719 changed the base branch from main to contrib/2384 July 23, 2026 07:55
@alonle

alonle commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all four points addressed in the latest commit.

1. Right/middle/multi-click routed to tap (v3CuaAgentHandler) — fixed. Only a single left click now routes to tap (button === "left" && (clickCount ?? 1) === 1); right/middle-click and multi-click keep the existing page.click path, so context-menu and double-click behavior is unchanged.

2. Recording flow bypassed on mobile (v3CuaAgentHandler) — fixed. On a replay-recording run the tap is now recorded as a deterministic step: page.tap gained a returnXpath option, and the handler records a method: "tap" step. To make that step replayable I added locator.tap() (selector-based touch, mirrors locator.click) and registered tap in METHOD_HANDLER_MAP. Note I deliberately did not add tap to SupportedUnderstudyAction: replay dispatches by map lookup in performUnderstudyMethod (no enum check), while the enum only gates live observe/act LLM inference — so recorded mobile taps replay correctly without the model ever being offered tap during normal runs (avoids it choosing touch on desktop).

3. Cursor overlay stale after tap (page.ts) — fixed. page.tap now calls updateCursor(x, y) before dispatching, consistent with click/hover/drag.

4. Missing tests (page.ts) — added an integration test for tap actuation in perform-understudy-method.spec.ts, following the existing pattern (it exercises METHOD_HANDLER_MAP.tap → locator.tap end to end).

One transparency note on #4: I couldn't run the browser-backed integration suite in my environment (Playwright browsers not installed; the suite runs against a built dist), so the new spec is written to the existing passing pattern but validated by CI rather than locally. typecheck, prettier --check, and eslint all pass locally.

Open questions from the PR description still stand if you'd prefer a different shape: routing double_click too, an explicit config flag instead of userAgentData.mobile auto-detect, and whether tap replay should also capture the XPath in the non-recording path.

@alonle
alonle force-pushed the feat/cua-touch-tap-on-mobile branch from c20830d to ca253d4 Compare July 23, 2026 08:07

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 6 files (changes from recent commits).

Confidence score: 5/5

  • In packages/core/lib/v3/understudy/page.ts, mobile replay now relies on tap returning a hit XPath, and without focused coverage this contract could drift and cause touch replays to target the wrong element or fail silently on mobile flows — add tap tests that validate touch dispatch and both explicit and default XPath return paths.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/lib/v3/understudy/page.ts">

<violation number="1" location="packages/core/lib/v3/understudy/page.ts:1912">
P3: Mobile replay now depends on `tap` returning the hit XPath, but this new contract has no coverage. Add focused tap tests for touch dispatch plus both requested and default XPath return values to catch replay regressions.

(Based on your team's feedback about unit tests for new behavior.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/core/lib/v3/understudy/locator.ts Outdated
Comment thread packages/core/lib/v3/handlers/handlerUtils/actHandlerUtils.ts Outdated
async tap(
x: number,
y: number,
options?: { returnXpath?: boolean },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Mobile replay now depends on tap returning the hit XPath, but this new contract has no coverage. Add focused tap tests for touch dispatch plus both requested and default XPath return values to catch replay regressions.

(Based on your team's feedback about unit tests for new behavior.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/understudy/page.ts, line 1912:

<comment>Mobile replay now depends on `tap` returning the hit XPath, but this new contract has no coverage. Add focused tap tests for touch dispatch plus both requested and default XPath return values to catch replay regressions.

(Based on your team's feedback about unit tests for new behavior.) </comment>

<file context>
@@ -1906,7 +1906,28 @@ export class Page {
+  async tap(
+    x: number,
+    y: number,
+    options?: { returnXpath?: boolean },
+  ): Promise<string> {
+    let xpathResult: string | undefined;
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tap actuation is covered by the new integration test in perform-understudy-method.spec.ts. The returnXpath contract on page.tap mirrors page.click's existing (also integration-tested) returnXpath path. Happy to add a focused case asserting the returned-vs-empty XPath if you'd prefer it called out explicitly.

Comment thread packages/core/lib/v3/handlers/v3CuaAgentHandler.ts
@alonle
alonle force-pushed the feat/cua-touch-tap-on-mobile branch from 1762ac3 to fb58929 Compare July 23, 2026 08:54
- locator.tap / page.tap: dispatch touchStart+touchEnd via Promise.all (like
  click) so a slow round trip on a remote session can't stretch a tap into a
  long press.
- tapElement: throw UnderstudyCommandException (like doubleClick/dragAndDrop)
  instead of StagehandClickError, so a tap failure isn't mislabeled as a click.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alonle
alonle force-pushed the feat/cua-touch-tap-on-mobile branch from 8a552b1 to 8d8f463 Compare July 23, 2026 11:09
@miguelg719 miguelg719 mentioned this pull request Aug 3, 2026
miguelg719 added a commit that referenced this pull request Aug 4, 2026
# why

The CUA and hybrid agents dispatch every pointer action as mouse input.
Mobile layouts commonly gate their handlers on touch/pointer events, so
a synthesized mouse click never registers — e.g. on `adidas.co.il`
(Browserbase `os: "mobile"`) a size selector highlights but keeps
reporting "please choose a size", blocking add-to-cart. A trusted touch
(`Input.dispatchTouchEvent`) works.

Originates from #2384 by @alonle.

# what changed

- New `useTouch` option (explicit opt-in, default `false`). Forwarded to
the server on the API execution path.
- `page.tap(x, y)` and `locator.tap()`: trusted touch taps mirroring
their click counterparts.
- When `useTouch` is set: CUA routes single left clicks to tap
(right/middle/multi-click, hover, and drag stay on mouse); hybrid
`click`/`type`/`fillFormVision` tap for coordinate focus/activation.
- Replay: taps record as `tap` steps and replay as touch. `tap` is not
exposed to the LLM during live inference.

# test plan

- Unit: `touch-actuation` (7), `touch-tool-routing` (9),
`touch-cua-routing` (7) — resolution, tool routing, recording.
- E2E: `perform-understudy-method` (8) — asserts
`pointerdown:touch`/`touchstart`/`touchend` delivery for both tap paths
(event-level, since Chromium synthesizes a click from a tap and
activation-only assertions can't tell touch from mouse).
- Server: `start.test.ts` covers `useTouch` on `/sessions/start`.
- Manual: with `useTouch: true` on a Browserbase `os: "mobile"` session,
the agent completes the adidas.co.il size + add-to-cart flow.

---------

Co-authored-by: alonl <alonl@wix.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@miguelg719

Copy link
Copy Markdown
Collaborator

merged and released per #2385

@miguelg719 miguelg719 closed this Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. external-contributor Tracks PRs mirrored from external contributor forks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants