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

1 participant