Skip to content

Add BH_NO_ACTIVATE to stop new_tab/switch_tab from stealing OS focus#469

Open
aiba wants to merge 1 commit into
browser-use:mainfrom
aiba:bh-no-activate
Open

Add BH_NO_ACTIVATE to stop new_tab/switch_tab from stealing OS focus#469
aiba wants to merge 1 commit into
browser-use:mainfrom
aiba:bh-no-activate

Conversation

@aiba

@aiba aiba commented Jun 21, 2026

Copy link
Copy Markdown

Problem

When driving a visible browser (e.g. an isolated debug Chrome you're
watching), every new_tab() and switch_tab() raises the Chrome window to the
OS foreground and steals keyboard focus from whatever you're typing in. Two CDP
calls cause this on macOS:

  • switch_tab()Target.activateTarget raises + focuses the window.
  • new_tab()Target.createTarget opens the tab in the foreground
    (foreground is the Mac default; createTarget takes a Mac-only background
    flag).

Change

Gate both behind an opt-in BH_NO_ACTIVATE=1 env flag. Default behavior is
unchanged
— activation still happens unless the flag is set.

  • switch_tab skips Target.activateTarget.
  • new_tab and the daemon's no-real-pages bootstrap pass background: true
    to Target.createTarget.

The flag is read client-side, so callers just prefix runs with
BH_NO_ACTIVATE=1 — no daemon restart needed. Routing is unaffected:
set_session still makes cdp()/js() default to the controlled tab, and the
🐴 title marker still shows which tab the agent drives.

Caveat: needs a non-empty window (sentinel tab)

background: true only keeps the window backgrounded when the new tab is not
the window's only tab
. If the automation window has no other real, loaded
tab, Chrome raises the window to show its first tab and background can't
prevent that:

  • Window already has ≥1 real loaded tab → new_tab() opens in the background,
    no focus steal.
  • Window is empty (just launched, or all tabs closed) → the first new_tab()
    foregrounds the window once.

For fully focus-free behavior, keep one real loaded "sentinel" tab open in the
automation browser (e.g. launch Chrome pointed at a static local page).
about:blank is not sufficient — it's treated as internal; the anchor must
be a real loaded page (http/https/file/data).

Relationship to #402

This supersedes #402, which gated only the switch_tab activateTarget call.
That half alone doesn't stop the steal on new_tab()Target.createTarget
raises the window independently. Verified empirically (frontmost app via
osascript, macOS / Chrome for Testing): with activateTarget skipped but no
background flag, the first new_tab() still flips frontmost from Finder →
Chrome. This PR adds the createTarget background: true half so the flag
covers the common new_tab() path too.

Testing

  • Empty window: 5/5 new_tab() calls stole focus.
  • With one real sentinel tab kept open: 6/6 new_tab() clean, plus
    switch_tab / screenshot / js() / close_tab all clean.

Possible follow-up

The empty-window foreground could be eliminated entirely by having the daemon
maintain a persistent keepalive tab, making the flag self-sufficient without a
user-managed sentinel. Happy to add that here or as a follow-up if you'd prefer.


Summary by cubic

Adds an opt-in BH_NO_ACTIVATE flag to prevent Chrome from stealing OS focus on macOS during new_tab() and switch_tab(). Default behavior is unchanged.

  • New Features

    • With BH_NO_ACTIVATE=1, switch_tab() skips Target.activateTarget.
    • new_tab() and daemon bootstrap pass background: true to Target.createTarget to open tabs in the background.
    • Flag is read client-side; no daemon restart needed.
  • Migration

    • Enable by prefixing runs with BH_NO_ACTIVATE=1.
    • On an empty window, the first tab may still foreground; keep one real loaded sentinel tab open (about:blank is not sufficient).

Written for commit 6c7e155. Summary will update on new commits.

Review in cubic

…itch

When driving a visible browser, every new_tab()/switch_tab() raises the
Chrome window to the OS foreground and steals keyboard focus from whatever
the user is typing in. Two CDP calls cause this on macOS:

  - switch_tab(): Target.activateTarget raises + focuses the window.
  - new_tab(): Target.createTarget opens the tab in the foreground
    (foreground is the Mac default; createTarget takes a Mac-only
    `background` flag).

Gate both behind an opt-in BH_NO_ACTIVATE=1 env flag (default off, so the
existing watch-along behavior is unchanged):

  - switch_tab skips Target.activateTarget.
  - new_tab / the daemon's no-real-pages bootstrap pass background:true.

The flag is read client-side, so callers just prefix runs with
BH_NO_ACTIVATE=1 -- no daemon restart needed. Routing is unaffected:
set_session still makes cdp()/js() default to the controlled tab, and the
horse-emoji title marker still shows which tab the agent drives.

Note: background:true only keeps the window backgrounded when the new tab is
not the window's only tab. With an empty window the first new_tab() still
foregrounds once; keep one real loaded sentinel tab open for fully
focus-free behavior.

Completes PR browser-use#402, which gated only the switch_tab activateTarget call;
verified empirically that createTarget alone still raises the window
(frontmost: Finder -> Chrome for Testing) unless background:true is set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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.

No issues found across 2 files

Re-trigger cubic

undeemed added a commit to undeemed/browser-harness that referenced this pull request Jun 22, 2026
… check

- new_tab(url, background=True|False) overrides BH_NO_ACTIVATE per call;
  switch_tab gains an `activate` override so backgrounding also skips the
  activateTarget raise (backgrounding createTarget alone doesn't stop it).
- daemon bootstrap anchors a real data: page (not about:blank, which Chrome
  treats as internal) when no real pages exist, so new_tab stays backgrounded
  without a user-managed sentinel (follow-up suggested in browser-use#469).
- centralize the env check in _no_activate().
- document BH_NO_ACTIVATE in SKILL.md + install.md.

Tests for the per-call override (incl. the activate-skip regression) and the
data: anchor; verified end-to-end on Chrome for Testing.
undeemed added a commit to undeemed/browser-harness that referenced this pull request Jun 22, 2026
… check

- new_tab(url, background=True|False) overrides BH_NO_ACTIVATE per call;
  switch_tab gains an `activate` override so backgrounding also skips the
  activateTarget raise (backgrounding createTarget alone doesn't stop it).
- daemon bootstrap anchors a real data: page (not about:blank, which Chrome
  treats as internal) when no real pages exist, so new_tab stays backgrounded
  without a user-managed sentinel (follow-up suggested in browser-use#469).
- centralize the env check in _no_activate().
- document BH_NO_ACTIVATE in SKILL.md + install.md.

Tests for the per-call override (incl. the activate-skip regression) and the
data: anchor; verified end-to-end on Chrome for Testing.
undeemed added a commit to undeemed/browser-harness that referenced this pull request Jun 22, 2026
… check

- new_tab(url, background=True|False) overrides BH_NO_ACTIVATE per call;
  switch_tab gains an `activate` override so backgrounding also skips the
  activateTarget raise (backgrounding createTarget alone doesn't stop it).
- daemon bootstrap anchors a real data: page (not about:blank, which Chrome
  treats as internal) when no real pages exist, so new_tab stays backgrounded
  without a user-managed sentinel (follow-up suggested in browser-use#469).
- centralize the env check in _no_activate().
- document BH_NO_ACTIVATE in SKILL.md + install.md.

Tests for the per-call override (incl. the activate-skip regression) and the
data: anchor; verified end-to-end on Chrome for Testing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant