Add BH_NO_ACTIVATE to stop new_tab/switch_tab from stealing OS focus - #469
Add BH_NO_ACTIVATE to stop new_tab/switch_tab from stealing OS focus#469aiba wants to merge 1 commit into
Conversation
… 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.
… 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.
… 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.
…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>
Kastan97
left a comment
There was a problem hiding this comment.
Ollie (CTO) Review: APPROVED ✅
What this does: Adds BH_NO_ACTIVATE env var that suppresses Target.activateTarget calls and passes background=True to Target.createTarget, preventing new_tab/switch_tab from stealing OS focus on macOS.
Verification:
- 13+/3- change across daemon.py and helpers.py
- Inline comments explain the macOS focus-stealing behavior
- No test for the env var (would need real browser/environment), acceptable for a straightforward env toggle
- Consistent with the existing BU_NO_OPEN pattern
Good to merge once CI passes.
|
Closing this in favour of #504, which solves the same problem with an opt-in parameter ( I've left what I learned running this in production on #504: the Separately, the Thanks — happy to help test #504 on macOS if that's useful. |
Problem
When driving a visible browser (e.g. an isolated debug Chrome you're
watching), every
new_tab()andswitch_tab()raises the Chrome window to theOS foreground and steals keyboard focus from whatever you're typing in. Two CDP
calls cause this on macOS:
switch_tab()→Target.activateTargetraises + focuses the window.new_tab()→Target.createTargetopens the tab in the foreground(foreground is the Mac default;
createTargettakes a Mac-onlybackgroundflag).
Change
Gate both behind an opt-in
BH_NO_ACTIVATE=1env flag. Default behavior isunchanged — activation still happens unless the flag is set.
switch_tabskipsTarget.activateTarget.new_taband the daemon's no-real-pages bootstrap passbackground: trueto
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_sessionstill makescdp()/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: trueonly keeps the window backgrounded when the new tab is notthe window's only tab. If the automation window has no other real, loaded
tab, Chrome raises the window to show its first tab and
backgroundcan'tprevent that:
new_tab()opens in the background,no focus steal.
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:blankis not sufficient — it's treated as internal; the anchor mustbe a real loaded page (
http/https/file/data).Relationship to #402
This supersedes #402, which gated only the
switch_tabactivateTargetcall.That half alone doesn't stop the steal on
new_tab()—Target.createTargetraises the window independently. Verified empirically (frontmost app via
osascript, macOS / Chrome for Testing): withactivateTargetskipped but nobackgroundflag, the firstnew_tab()still flips frontmost from Finder →Chrome. This PR adds the
createTargetbackground: truehalf so the flagcovers the common
new_tab()path too.Testing
new_tab()calls stole focus.new_tab()clean, plusswitch_tab/ screenshot /js()/close_taball 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_ACTIVATEflag to prevent Chrome from stealing OS focus on macOS duringnew_tab()andswitch_tab(). Default behavior is unchanged.New Features
BH_NO_ACTIVATE=1,switch_tab()skipsTarget.activateTarget.new_tab()and the daemon’s first-page attach passbackground: truetoTarget.createTargetto open tabs in the background.Migration
BH_NO_ACTIVATE=1.about:blankis not sufficient).Written for commit 554e92a. Summary will update on new commits.