Add BH_NO_ACTIVATE to stop new_tab/switch_tab from stealing OS focus#469
Open
aiba wants to merge 1 commit into
Open
Add BH_NO_ACTIVATE to stop new_tab/switch_tab from stealing OS focus#469aiba wants to merge 1 commit into
aiba wants to merge 1 commit into
Conversation
…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>
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 daemon bootstrap passbackground: truetoTarget.createTargetto open tabs in the background.Migration
BH_NO_ACTIVATE=1.about:blankis not sufficient).Written for commit 6c7e155. Summary will update on new commits.