Skip to content

fix(browser): keep local Chromium headless on Windows Desktop (#64867) - #66523

Open
stantheman0128 wants to merge 2 commits into
NousResearch:mainfrom
stantheman0128:fix/64867-browser-blank-window
Open

fix(browser): keep local Chromium headless on Windows Desktop (#64867)#66523
stantheman0128 wants to merge 2 commits into
NousResearch:mainfrom
stantheman0128:fix/64867-browser-blank-window

Conversation

@stantheman0128

Copy link
Copy Markdown
Contributor

What does this PR do?

Local browser automation is documented as headless Chromium via agent-browser, but on Windows Desktop a large blank top-level window can still appear while browser_navigate runs (and sometimes around nearby tool activity). Automation still succeeds; the window just obscures the chat.

Two cooperating causes:

  1. Local agent-browser can inherit headed mode from user/project config (AGENT_BROWSER_HEADED / agent-browser.json), and Chrome's --headless=new has historically painted a blank HWND on some Windows hosts.
  2. Desktop tier-2 link-title resolution opens a 1280x800 BrowserWindow when curl cannot read a title (common for bot-walled sites like GitHub, the issue's repro URL). A show: false leak there looks like a blank framed window over the chat.

This PR forces local sessions to --headed false (unless the operator explicitly sets AGENT_BROWSER_HEADED), parks any Windows ghost HWND off-screen via AGENT_BROWSER_ARGS, and hardens the link-title window (off-screen bounds, skipTaskbar, focusable: false, hide + opacity 0).

Related Issue

Fixes #64867

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Security fix
  • Documentation update
  • Tests (adding or improving test coverage)
  • Refactor (no behavior change)
  • New skill (bundled or hub)

Changes Made

  • tools/browser_tool.py: _apply_local_browser_visibility_guards for local Chromium; insert --headed false; on Windows append --window-position=-2400,-2400
  • apps/desktop/electron/link-title-window.ts: off-screen + non-focusable hidden title-fetch window
  • tests/tools/test_browser_windows_blank_window.py: unit coverage for the guards and Popen argv
  • apps/desktop/electron/link-title-window.test.ts: options + hide/opacity regression coverage

How to Test

  1. On Windows, leave local browser mode (no Browserbase / CDP / Camofox).
  2. In Hermes Desktop, ask the agent to browser_navigate to https://github.com/NousResearch/hermes-agent.
  3. Confirm no blank top-level window covers the chat while the tool runs.
  4. Optional: with AGENT_BROWSER_HEADED=1, headed mode still works (guards skip).

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature
  • I've run targeted pytest for the new tests (see Evidence)
  • I've added tests for my changes
  • I've tested on my platform: Windows 11

Documentation & Housekeeping

  • N/A for docs / config example / AGENTS.md / tool schemas
  • I've considered cross-platform impact (guards no-op for CDP; headed opt-in preserved)

AI assistance

Drafted with Cursor/Grok assistance. Human author: Stan Shih (stantheman0128). Reviewed and verified on Windows before opening.

Evidence

$ python -m pytest tests/tools/test_browser_windows_blank_window.py -q --tb=short
......                                                                   [100%]
6 passed in 2.20s

$ python scripts/check-windows-footguns.py tools/browser_tool.py tests/tools/test_browser_windows_blank_window.py
OK No Windows footguns found (2 file(s) scanned).

Live agent-browser check (Chrome 151 via agent-browser 0.27.0):
  --headed false + AGENT_BROWSER_ARGS=--window-position=-2400,-2400
  open https://example.com -> exit=0, cmdline has --headless=new and
  --window-position=-2400,-2400, MainWindowHandle=0

scripts/run_tests.sh was also attempted; on this Windows host the stock env -i path drops USERPROFILE, which breaks Path.home() during hermes imports. Targeted pytest above uses the same test file the runner would execute.

@alt-glitch alt-glitch added type/bug Something isn't working tool/browser Browser automation (CDP, Playwright) comp/desktop Electron desktop app (apps/desktop/*) platform/windows Native Windows-specific behavior or breakage P3 Low — cosmetic, nice to have sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Jul 17, 2026

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

Thanks for isolating both the local Chromium and Desktop title-resolution paths. The title-window hardening targets a current-main path: apps/desktop/electron/link-title-window.ts:6-20 still creates a 1280x800 hidden window with no off-screen or focus/taskbar guards.

Problems

  • tools/browser_tool.py:146 in this PR conflicts with current main's headed-browser contract. Commit 29899c2aa added _is_headed_mode() (tools/browser_tool.py:892-919) and appends --headed for local browser.headed: true sessions (tools/browser_tool.py:2384-2387). The new guard only examines browser_env, so config-driven headed mode is not recognized and the PR adds a contradictory --headed false.

Suggested changes

  • Use _is_headed_mode() as the authoritative opt-out before injecting headless flags, and add a regression test covering browser.headed: true with no forced false flag. Current coverage for that contract is in tests/tools/test_browser_headed_mode.py:40-64 and :195-224.

Automated hermes-sweeper review.

Comment thread tools/browser_tool.py
1. ``~/.agent-browser/config.json`` (or ``AGENT_BROWSER_HEADED``) enables
headed mode without Hermes knowing about it, or
2. Chrome's ``--headless=new`` paints a ghost HWND on some Windows hosts.

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.

Current main now supports browser.headed: true through _is_headed_mode() and adds --headed before this point (commit 29899c2aa). That config value is not represented in browser_env, so this branch adds a contradictory --headed false. Consult _is_headed_mode() here and preserve explicit config-driven headed mode.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 18, 2026
@stantheman0128
stantheman0128 force-pushed the fix/64867-browser-blank-window branch from 1e52608 to cfc5004 Compare July 19, 2026 11:26
@stantheman0128

Copy link
Copy Markdown
Contributor Author

Thanks for the catch on the headed-mode contract.

Pushed a follow-up on this branch (rebased onto current main):

  • _apply_local_browser_visibility_guards now opts out via _is_headed_mode() (config browser.headed / AGENT_BROWSER_HEADED) instead of only inspecting browser_env
  • So a config-driven headed session keeps main's --headed and no longer gets a contradictory --headed false
  • Regression coverage: browser.headed: true with no forced false (unit + local Popen path); also tightened test_browser_headed_mode so headless may pin --headed false while headed must not

Evidence:

$ scripts/run_tests.sh tests/tools/test_browser_windows_blank_window.py tests/tools/test_browser_headed_mode.py -q --tb=short
=== Summary: 2 files, 22 tests passed, 0 failed (100% complete) in 6.0s (24 workers) ===

$ python scripts/check-windows-footguns.py tools/browser_tool.py tests/tools/test_browser_windows_blank_window.py tests/tools/test_browser_headed_mode.py
OK No Windows footguns found (3 file(s) scanned).

Commit: cfc5004a1

stantheman0128 and others added 2 commits July 23, 2026 15:38
Force --headed false for local agent-browser sessions and park any
Windows --headless=new ghost HWND off-screen. Also harden the Desktop
link-title BrowserWindow so bot-walled URLs cannot flash a blank frame
over chat during browser tool turns (NousResearch#64867).

Co-authored-by: Cursor <cursoragent@cursor.com>
Use the shared headed-mode resolver (config browser.headed / AGENT_BROWSER_HEADED)
as the opt-out before injecting --headed false, so config-driven headed sessions
are not contradicted. Add regression coverage for headed:true without a forced false.

Co-authored-by: Cursor <cursoragent@cursor.com>
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

One PR addresses #64867. Its diff covers both proposed window paths by forcing local Chromium headless unless headed mode is explicitly enabled, parking possible Windows ghost windows off-screen, and hardening Desktop’s hidden link-title BrowserWindow; the supplied evidence does not determine which path produced the reported window.

Related pull requests

  • fix(browser): keep local Chromium headless on Windows Desktop (#64867) #66523 best fix — (+321/-3) — n/a: The diff adds local-browser visibility guards and hardens the hidden link-title BrowserWindow, with tests for Windows-window suppression and preservation of explicit headed mode. The contributor’s keep_open review identified a conflict with config-driven headed mode; the shown follow-up now uses _is_headed_mode() as the opt-out and adds regression coverage for browser.headed: true, directly addressing that objection.

Suggested consolidation

Keep #66523 open with a salvage path: retain the current-main link-title-window hardening and the revised browser guards that preserve explicit headed mode, then verify the current PR head and test the original Windows Desktop reproduction to determine whether both guarded paths are needed. There are no duplicate PRs to close.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I64867(["issue #64867 (open)"])
    P66523["PR #66523 (open)"]
    P66523 -->|best fix| I64867
    class I64867 open
    class P66523 open
    class P66523 best
    class P66523 target
    click I64867 "https://github.com/NousResearch/hermes-agent/issues/64867"
    click P66523 "https://github.com/NousResearch/hermes-agent/pull/66523"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 1 pull request and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 18 kB of PR diffs, 7 kB of issue/PR text, 2 kB of discussion (2 comments), 2 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@rafalelele

Copy link
Copy Markdown

Supplementary reproduction (Windows 11, hermes.exe launcher + browser-use CLI via uvx)

We fully reproduced the "local mode auto-launch of the browser fails" problem on Windows 11 (build 10.0.26200), and located the behaviour. Posting for upstream reference.

Note on version sensitivity: the line numbers below are from the source tree we inspected on 2026-08-14. The browser-launch path has since been refactored (the old browser_harness/admin.py _launch_browser() / daemon.py BROWSER_KIND structure is no longer present). The symptom and the workaround below are still valid on current builds; the mechanism explanation is updated to the current code.

Symptom

After the PYTHONPATH crash was fixed, browser_exec still popped up the "install Chrome from the Microsoft Store" dialog, and the browser never started.

Current behaviour (verified against current source)

browser_exec itself does not launch a browser. In tools/browser_use_cli.py:

  • browser_exec() (def at L299) just runs subprocess.run(cmd) against the browser-use CLI.
  • _resolve_backend_cdp(env, task_id) (def at L220) only wires up a CDP endpoint into the child env:
    • If BU_CDP_WS / BU_CDP_URL is already set, it passes through untouched (L239–240).
    • It honors browser.cdp_url / BROWSER_CDP_URL (L256–257).
    • With nothing configured, it returns None and the comment at L234 states the harness "attaches to local Chrome" — i.e. launching/finding that local Chrome is left to the browser-use package internals, not Hermes.

Meanwhile tools/browser_tool.py _build_browser_env() (def at L125) only points browser-use at a browser without starting one:

  • It auto-detects Playwright's Chromium or system Edge when BH_CHROME_PATH / CHROME_PATH are unset (L143–149, _detect_browser_executable at L157).
  • It still pops PYTHONPATH (L153).

What we actually observed on Windows 11

  • The system has the official store/App-Paths-registered Chrome on C:, and cmd /c start chrome --version launches it fine from a normal agent shell.
  • Yet under browser_exec (local mode, BU_CDP_URL unset) the same start chrome resolution failed inside the subprocess environment and fell through to the Microsoft Store dialog. So the failure is in how the browser-launch subprocess resolves the executable, not "Chrome isn't installed."
  • The net effect: on this Windows build, local/auto-launch of the browser is unreliable; the daemon does not start a browser on its own and relies on browser-use finding one.

Workaround we use (verified)

Configure browser.cdp_url: 'http://localhost:9222' and pre-launch a CDP-capable Chromium externally (headless-new, --remote-debugging-port=9222, user-data-dir using Windows backslash paths). browser_exec then works for real web access. This is the "cdp mode + externally launched browser" approach — consistent with #83920 (auto-spawn a local headless Chrome with CDP) but more controllable, since Hermes/current code does not auto-spawn it for us.

Suggestions for upstream

  1. When BH_CHROME_PATH / CHROME_PATH is honored, also pass --remote-debugging-port (and the needed CDP args) so the launched browser is actually drivable, not just "opened without a debug port."
  2. Make the Windows start chrome / local-browser resolution robust inside the Hermes subprocess environment (App Paths / known-install-path probing), and surface a clear error instead of silently falling through to the Microsoft Store.
  3. Consider feat(browser): auto-spawn local headless Chrome with CDP when browser.cdp_url is configured but unreachable #83920's "auto-spawn a local Chromium when cdp_url is configured" so users are not required to manually launch a browser out of band.

Our local code changes (for reference only, not a PR)

In tools/browser_tool.py _build_browser_env() (def L125):

These are only a fallback for local mode; what actually bypassed the launch defect for us was the cdp_url + externally launched Chromium described above.

@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

fix(browser): keep local Chromium headless on Windows Desktop (#64867)

Force --headed false for local sessions unless the user has explicitly opted into headed mode, park Windows ghost windows off-screen, and harden the desktop link-title window — a sensible layered fix with good tests. Observations:

  1. tools/browser_tool.py _run_browser_command — the visibility flags are inserted at cmd_parts.index("--json"). If any command path ever builds cmd_parts without --json, this raises ValueError. The current call sites always include --json, but a defensive if "--json" in cmd_parts (or a try/except) would make the guard robust to future command-shape changes — cheap insurance for a security-relevant helper.
  2. _append_agent_browser_arg dedupes by flag key (--window-position), so a user's explicit --window-position in AGENT_BROWSER_ARGS is preserved and the parking position is skipped. Good. Note the parking value -2400,-2400 is a fixed coordinate; on exotic multi-monitor layouts (or very high-DPI scaling) a ghost window could still land on-screen. Acceptable as a best-effort guard; the --headed false pin is the real fix.
  3. _apply_local_browser_visibility_guards sets AGENT_BROWSER_HEADED=false in the child env even when the user's ~/.agent-browser/config.json declares "headed": true — intentional (documented) so the local-mode contract wins. Worth confirming the agent-browser daemon doesn't require headed mode for any local feature (e.g. screenshot capture quality) — if a future agent-browser release gates a capability on the config's headed flag, the env override could silently disable it.
  4. apps/desktop/electron/link-title-window.ts — parking at x: -20000, y: -20000 with skipTaskbar + focusable: false + setOpacity(0) + hide() is good belt-and-suspenders. One note: hide() is called immediately after construction but the window is never shown, so the show: false default should hold; the extra calls are defense against platform quirks. Fine.
  5. Tests are strong (dedupe, posix/nt variants, explicit headed opt-out, config headed opt-out, CDP skip, end-to-end argv insertion). Minor: the E2E FakePopen's wait() writes into tmp_path.rglob("_stdout_open") — a slightly odd simulation, but it works and is test-internal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have platform/windows Native Windows-specific behavior or breakage sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows tool/browser Browser automation (CDP, Playwright) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(windows/desktop): blank visible window opens during local browser automation

6 participants