Skip to content

[Bug]: Native popups opened with window features (pop-out players, OAuth) from desktop preview tabs are invisible to agents #30384

Open

Description

@sndstone

Note

🤖 Claude Opus 5.5 writing on behalf of sndstone, with a review by GPT 6.1 Sol.

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/desktop

Steps to reproduce

Desktop app (macOS) with its local environment, so the agent's preview tab is rendered by the desktop.

  1. Serve these two files locally (for example python3 -m http.server 8765):
    <!-- index.html -->
    <button id="popout" onclick="window.open('popup.html?kind=popout', 'player', 'width=520,height=360')">Pop-out player</button>
    <button id="auth" onclick="window.open('popup.html?kind=auth', 'auth', 'popup,width=420,height=520')">Sign in</button>
    <!-- popup.html -->
    <!doctype html><title>Popup</title><p>Popup loaded</p>
  2. Agent: preview_open({ url: "http://127.0.0.1:8765/index.html", reuseExistingTab: false }) → tab A.
  3. Agent: preview_act({ tabId: A, action: "click", selector: "#popout" }), then the same for #auth.
  4. Agent: preview_status({ tabId: A }).
  5. Agent: preview_evaluate({ tabId: A, expression: "window.open('', 'player').location.href" }).

Expected behavior

Each popup becomes a tab the agent owns, with window.opener intact, as popups from headless server tabs already do through adoptPopup. The agent can snapshot it, act in it, and see it close.

Actual behavior

  • Step 3: a native window appears for each click.
  • Step 4: tabs lists only A. Neither popup is addressable as a preview tab, so the agent has no snapshot, act, or close for it.
  • Step 5 returns http://127.0.0.1:8765/popup.html?kind=popout: the popup is loaded and working. A same-origin popup can sometimes be scripted or closed indirectly through preview_evaluate on the opener, but that gives no normal tab tooling. Cross-origin restrictions prevent reading or interacting with the popup's DOM through the opener, although limited operations such as close() and postMessage() may remain available.

This can block any flow that continues in an HTTP(S) popup opened with window features, such as a content player's pop-out, a sign-in, payment or share window. The repro uses a synthetic player/sign-in page; I have not tested real OAuth providers.

HTTP(S) requests classified as foreground-tab or background-tab, without a POST body, follow the separate new-tab path. Ordinary target="_blank" links commonly take this path. Ownership of those tabs is #26250, which also notes the relay limitation; #29198 covers closing unclaimed tabs in general. This issue concerns native new-window popups. Related: #6561 (earlier OAuth popup blocking) and #13348 (open PR that parents native popups to the T3 window and disables FedCM for preview guests).

Root cause (main 12ec17c)

  • previewWindowOpenAction returns "popup" for an HTTP(S) disposition: "new-window", and the guest's window-open handler allows a native BrowserWindow, kept native so OAuth keeps window.opener (Manager.ts#L447-L465, Manager.ts#L1540-L1544).
  • The server drives desktop-rendered tabs over Playwright connectOverCDP through the desktop's CDP relay, which lists, attaches and announces only the tab's own page (CdpRelay.ts#L157-L186; its test is "announces only the tab's page, under its real target id"). The popup's webContents is never exposed.
  • So page.on("popup") → adoptPopup (ServerBrowser.ts#L979-L980, ServerBrowser.ts#L1305) never runs for desktop-rendered tabs. This omission does not affect headless server tabs, where Playwright's own Chromium reports popups.

Suggested direction

Make native HTTP(S) popups discoverable and controllable through the preview tools, inheriting the opener's agent ownership while preserving browser popup semantics and desktop behavior. Exposing their targets through the CDP relay appears to let the existing adoptPopup path handle ownership; adopted native popups would also need to retain desktop-specific handling. The local candidate keeps the native windows; named-window reuse and full desktop integration still need verification.

I have a local candidate fix along these lines, described below, and can open a PR for it once the direction is triaged.

Candidate fix, evidence and open work

What it does

  • Desktop: did-create-window attaches a debugger to the popup's webContents and hands it to the opener tab's relay (DesktopBrowserHost.attachPopup).
  • Relay: CdpRelay.addPopup announces the popup once, after the tab, as a page target with openerId set to the tab. The relay includes it in getTargets, lets attachToTarget and closeTarget reach it, and routes its own and child-frame sessions to the popup's debugger. On close it sends detachedFromTarget (with targetId, and on the parent session for newCDPSession sessions) and targetDestroyed.
  • Server: createTab also adopts desktop popups that already existed when it connected, because their popup event fired before the listener existed.

Evidence

  • vp run check:changed passes. New tests fail on main: a relay case (announce once after the opener, routing, close and detach routing) and a server case (a pre-existing desktop popup becomes the agent's tab).
  • An Electron 44 harness drove the real DesktopBrowserHost and relay with Playwright 1.60 connectOverCDP directly. On main, Playwright saw only the opener and no popup event. With the candidate fix, in five recorded runs, Playwright listed a popup opened before it connected, got page.on("popup") for the pop-out click, read and clicked inside the popup, saw window.opener and the popup's postMessage reach the opener, and saw the popup close.
  • The harness bypasses ServerBrowser, adoptPopup and the MCP tools, so full agent-tool integration in the desktop app is still unverified.

Open work before a PR

  • Person-opened tabs: the relay is also connected for desktop tabs a person opened. The candidate adopts their popups as unclaimed preview tabs alongside the native windows. Whether adoption should be limited to agent-owned openers needs triage.
  • Desktop provenance after adoption: an adopted popup has desktop === null, so createTab treats it as headless. It emulates a 1280×800 viewport onto the small native window, always intercepts file choosers (ServerBrowser.ts ~L531, so the OS picker is lost even for a person), and applies presentAsChrome and clipboard instrumentation meant for headless pages.
  • Focus and painting: popup commands skip DesktopBrowserHost's focus emulation, painting, and one-at-a-time screenshot handling, so a hidden or minimized popup could stall input or capture.
  • Downloads: downloads started inside the popup are not placed or reported.
  • Release and reconnect: these must stop popup streams, release paused child targets, and restore adopted popup tabs without duplicates.

Alternatives considered

  • Opening new-window popups as preview tabs instead breaks window.opener, returned window handles, named-window reuse and POST semantics.
  • Electron's createWindow override, hosting the popup's webContents inside the preview, is promising long term. It doesn't fit the current <webview> model, and did-create-window no longer fires with it.

Impact

Major degradation or frequent failure

Version or commit

T3 Code (Nightly) 0.1.7-nightly.20261103.4106; source analysis on main 12ec17c

Environment

macOS 27.0 (arm64), desktop app with its local environment, Claude Code provider.

Logs or stack traces

# preview_status for tab A after both clicks (trimmed)
"tabs": [
  { "tabId": "tab_1_…", "url": "http://127.0.0.1:8765/index.html", "owner": "agent", "ownedByCaller": true }
]
# server trace for tab A shows DesktopBrowserChannel.awaitAttached / endpoint / pointer, so the tab is desktop-rendered

Workaround

A person can finish the flow in the native popup window. An agent can script a same-origin popup only indirectly, through preview_evaluate on the opener; it cannot read or act in a cross-origin popup's page.

Activity

  1. Wladefant commented on Nov 5, 2026

    @Wladefant

    Changed-head review result for the native desktop popup issue.

    Upstream PR #30460 remains OPEN / UNMERGED, author-owned by shivamhwp on branch issue-watch/30384, exact head 65b0e26. Separate current-head review #5779978791 was submitted and read back as CHANGES_REQUESTED.

    New P2 confirmed in apps/desktop/src/preview/DesktopBrowserHost.ts:509: when the popup's Electron debugger detaches (including DevTools takeover), hidePopup removes the relay and listener, but openedPopups retains the still-live WebContents. No event retries showPopup when DevTools closes or the debugger becomes available; the only showPopups triggers are relay creation, initial popups command and a new openedPopup. Existing DesktopBrowserHost.test.ts exercises agent CDP Target.detachFromTarget, not the Electron debugger detach/re-attach lifecycle. Required regression: same popup remains open through DevTools takeover/close, host reattaches exactly once and resumes routing, opener/siblings remain usable, and destroying the popup does not retry. No native-window close/recreation.

    Exact-head normal PR workflow #41964181949 succeeded on synthetic merge candidate 53b4656 (head 65b0e + main 5a906). Selected logs read: DesktopBrowserHost 36, CdpRelay 22, ServerBrowser 39; Test job 515 files / 5,484 tests; Lint, Build/Typecheck, Release Smoke and Check succeeded. This is not current-main composition: latest merged main observed 67eebac, candidate 10 ahead / 4 behind from merge base e313a12; main has an overlapping ServerBrowser.ts change. Normal forward composition and exact-head CI remain needed before landing.

    Prior author-reported macOS package acceptance was at 9f6d46, not this current head. Current-head native popup/browser acceptance remains NOT RUN. Review only; no source or branch custody, CI dispatch, merge, installation, host/device action or landing authority.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions