Electron Stagewrightdocs

ADR-029: Renderer surface targeting on the CDP transport

Context

ADR-022 defines the renderer-surface model: a flat, parent-first list of opaque surface ids, explicit selection, and snapshot/ref state keyed by (sessionId, surfaceId). Only the Playwright launch path implemented it. Packaged-app launch and electron_attach use the CDP transport, which could drive only its selected root page target, so an agent driving a packaged app could not reach an iframe, a <webview> guest or a WebContentsView.

A probe against Electron 42.11.10 with the ADR-022 surface fixture, attached through --remote-debugging-port, established these shapes:

Decision

1. Discover surfaces from /json/list and per-target frame trees

CDP keeps ADR-003's one-socket-per-target pool. A surface listing reads /json/list for page, webview and iframe targets, opens or reuses each target's socket, and reads Page.getFrameTree to add in-process child frames. Each node is keyed by its frame id and linked to its parent through /json/list parentId (targets) or the frame tree (in-process frames). The list is ordered parent-first from the page roots in discovery order; a node whose parent is missing or part of a cycle is listed as a root instead of being given an invented parent.

Kinds are reported only as far as CDP can prove them: page targets are other, webview targets are webview, and both OOPIF targets and in-process frames are frame. CDP never reports window or webcontents_view. originRelation compares a frame's URL with its parent's, as on Playwright.

Flattened Target.setAutoAttach sessions were rejected: they need a session-multiplexing client in place of the existing pool, page-level auto-attach does not expose webview guests, and /json/list already carries the parent links for both target kinds.

2. Keep opaque ids and tombstones keyed by frame id

The transport maps each frame id to an opaque <sessionId>-surface-<n> id. The id survives navigation and a frame moving between in-process and OOPIF hosting, because the frame id does. A surface missing from a listing becomes a tombstone: selecting it reports SURFACE_CLOSED, an unknown id reports SURFACE_NOT_FOUND, as ADR-022 requires. A frame id that reappears in a later listing is the same frame and its record becomes live again.

3. Resolve the active surface to an eval socket, a context and an input root

Every renderer operation resolves the selected surface to:

Pointer coordinates are the element point inside the frame plus the owner content boxes summed from the surface up to its input root. Dispatching through the input root keeps real hit-testing, so an overlay that covers a frame still intercepts the click. Before a key press without a selector, a non-root frame receives window.focus(), matching the Playwright transport.

Snapshot, find, renderer eval, reads, waits, expectations and selector/ref interactions use this resolution. The snapshot store and the session ref lock are unchanged: refs are already keyed by (sessionId, surfaceId) and every walk, reconcile, retag and ref-targeted action still runs under the per-session lock, so switching surfaces cannot let a ref from one frame act in another.

4. Treat webview guests as window roots

electron_windows_list and WindowRef resolution include webview targets beside page targets, matching the Playwright transport, which exposes guests through ElectronApplication.windows(). Selecting a surface also selects its input root as the active window, so a window screenshot and an element screenshot capture the guest that the element coordinates belong to. Console, dialog and network capture attach to every window root, so they now include webview guest traffic. OOPIF sockets are opened only to drive frames and do not join capture.

5. Declare support only with real evidence

supportsSurfaceTargeting becomes true for CDP. A real-Electron scenario launches the ADR-022 fixture through the CDP transport and proves discovery, parent-first order, origin relation, eval, snapshot and click in in-process, OOPIF and webview surfaces, ref isolation between surfaces, SURFACE_CLOSED after a frame detaches, and clean stop.

Alternatives considered

Alternative Why rejected
Flattened Target.setAutoAttach sessions on the browser socket Replaces the per-target pool with a multiplexed client for no added information; page-level auto-attach omits webview guests.
Page.createIsolatedWorld per frame Agent eval and the snapshot walker expect the page's main world; an isolated world cannot see application globals.
Dispatch input on each OOPIF's own target It works with frame-relative points, but bypasses hit-testing in the embedding page, so a click could reach a frame an overlay covers. In-process frames still need offsets.
Guess window vs webcontents_view from bounds or titles CDP exposes no ownership metadata for page targets. ADR-022 requires other over a guess.
Use raw CDP target or frame ids as surface ids ADR-022 requires transport-generated ids; keeping the mapping in the transport lets the frame id change hosting without the agent noticing.

Consequences

References