@mearl/browser-core 3.0.0-beta.4 → 3.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +2 -2
  2. package/dist/agentChat.d.ts +2 -0
  3. package/dist/agentSkills.d.ts +1 -1
  4. package/dist/app-profile-runtime/types.d.ts +10 -12
  5. package/dist/appProfileDefinition.d.ts +32 -14
  6. package/dist/appProfileDefinition.js +63 -69
  7. package/dist/axSnapshot.js +1 -1
  8. package/dist/boundedRequestBuffer.d.ts +4 -4
  9. package/dist/browserActionProtocol.d.ts +65 -42
  10. package/dist/browserActionProtocol.js +15 -15
  11. package/dist/browserCapabilities.d.ts +47 -0
  12. package/dist/browserCapabilities.js +63 -0
  13. package/dist/browserDiscovery.d.ts +3 -0
  14. package/dist/browserDiscovery.js +20 -0
  15. package/dist/browserEnhancement.d.ts +161 -0
  16. package/dist/browserEnhancement.js +80 -0
  17. package/dist/cookie.d.ts +6 -0
  18. package/dist/cookie.js +20 -5
  19. package/dist/dataStructure.d.ts +8 -0
  20. package/dist/dataStructure.js +58 -0
  21. package/dist/deviceEnvironmentObservation.d.ts +48 -0
  22. package/dist/deviceEnvironmentObservation.js +77 -0
  23. package/dist/domClicking.d.ts +14 -0
  24. package/dist/domClicking.js +98 -10
  25. package/dist/domInspection.js +13 -1
  26. package/dist/extensionServiceProtocol.d.ts +7 -0
  27. package/dist/extensionServiceProtocol.js +1 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.js +8 -0
  30. package/dist/pageAct.d.ts +2 -0
  31. package/dist/pageAct.js +42 -10
  32. package/dist/pageDialog.d.ts +66 -0
  33. package/dist/pageDialog.js +175 -0
  34. package/dist/pageObserver.js +16 -5
  35. package/dist/pagePressing.d.ts +1 -1
  36. package/dist/providerAppResources.d.ts +39 -0
  37. package/dist/providerAppResources.js +2 -0
  38. package/dist/proxyPermissions.js +4 -3
  39. package/dist/requestMetadata.d.ts +9 -0
  40. package/dist/requestMetadata.js +8 -0
  41. package/dist/requestSerializer.d.ts +2 -2
  42. package/dist/requestSerializer.js +1 -29
  43. package/dist/requestStreamCapture.d.ts +5 -3
  44. package/dist/requestStreamCapture.js +2 -0
  45. package/dist/runActions.js +1 -1
  46. package/dist/types.d.ts +4 -2
  47. package/package.json +2 -2
@@ -0,0 +1,80 @@
1
+ import { isDeviceEnvironmentObservation } from './deviceEnvironmentObservation.js';
2
+ export const BROWSER_ENHANCEMENT_PROTOCOL_VERSION = 1;
3
+ export const BROWSER_ENHANCEMENT_NAVIGATION_ORIGIN_VERSION = 1;
4
+ export const BROWSER_ENHANCEMENT_PREPARATION_VERSION = 1;
5
+ export const BROWSER_ENHANCEMENT_HOST_ENVIRONMENT_VERSION = 1;
6
+ export const BROWSER_ENHANCEMENT_PREPARATION_PATH = 'src/pages/browser-transport/index.html';
7
+ /** Only the installed Extension's inert top-level page can prepare a new document. */
8
+ export function isBrowserEnhancementPreparationUrl(value) {
9
+ if (typeof value !== 'string')
10
+ return false;
11
+ try {
12
+ const url = new URL(value);
13
+ return /^[a-p]{32}$/.test(url.hostname) && value === `chrome-extension://${url.hostname}/${BROWSER_ENHANCEMENT_PREPARATION_PATH}`;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ export function isBrowserEnhancementAction(value) {
20
+ return value === 'set_device_emulation' || value === 'set_app_profile' || value === 'set_timezone';
21
+ }
22
+ export function requiredBrowserEnhancementActions(invocation) {
23
+ return invocation.action === 'set_app_profile' && invocation.data?.emulation !== undefined
24
+ ? ['set_app_profile', 'set_device_emulation'] : [invocation.action];
25
+ }
26
+ export function isProviderInitialEnvironment(value) {
27
+ if (!value || typeof value !== 'object')
28
+ return false;
29
+ const device = value.device;
30
+ const only = (record, keys) => Object.keys(record).every(key => keys.includes(key));
31
+ try {
32
+ return isDeviceEnvironmentObservation(device) && device.userAgent.length <= 4096 &&
33
+ device.platform.length <= 256 && JSON.stringify(value).length <= 32_768 && only(value, ['device', 'preserveHostPlatform', 'preserveHostInput']) &&
34
+ (value.preserveHostPlatform === undefined || value.preserveHostPlatform === true) &&
35
+ (value.preserveHostInput === undefined || value.preserveHostInput === true) &&
36
+ only(device, ['userAgent', 'platform', 'userAgentMetadata', 'screen', 'touchPoints', 'safeAreaInsets']) &&
37
+ only(device.screen, ['width', 'height', 'deviceScaleFactor', 'orientation', 'angle']) && only(device.safeAreaInsets, ['top', 'right', 'bottom', 'left']) &&
38
+ (device.userAgentMetadata === null || (only(device.userAgentMetadata, ['brands', 'fullVersionList', 'fullVersion', 'platform', 'platformVersion', 'architecture', 'model', 'mobile', 'bitness', 'wow64', 'formFactors']) &&
39
+ [...device.userAgentMetadata.brands, ...device.userAgentMetadata.fullVersionList].every(brand => only(brand, ['brand', 'version']))));
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ export function isEnhancementNavigationOrigin(value) {
46
+ if (typeof value !== 'string' || value.length > 4096)
47
+ return false;
48
+ try {
49
+ const url = new URL(value);
50
+ return ['http:', 'https:'].includes(url.protocol) && url.origin === value;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ export function isProviderEnhancementTarget(value) {
57
+ if (!value || typeof value !== 'object')
58
+ return false;
59
+ const target = value;
60
+ return Number.isSafeInteger(target.tabId) && target.tabId >= 0 &&
61
+ (value.ready === false || (target.ready === true &&
62
+ (target.mainFrameId === undefined || validIdentity(target.mainFrameId)) &&
63
+ (target.targetId === undefined || validIdentity(target.targetId)) &&
64
+ (target.initialEnvironment === undefined || isProviderInitialEnvironment(target.initialEnvironment)) &&
65
+ (target.mainFrameId !== undefined || target.targetId !== undefined) && typeof target.url === 'string' && Boolean(target.url)));
66
+ }
67
+ function validIdentity(value) { return typeof value === 'string' && value.length > 0 && value.length <= 256; }
68
+ /** Match every identity asserted by the external owner; URL alone is never proof. */
69
+ export function matchesProviderEnhancementTarget(target, physical) {
70
+ return Boolean(target.mainFrameId || target.targetId) &&
71
+ (target.mainFrameId === undefined || target.mainFrameId === physical.mainFrameId) &&
72
+ (target.targetId === undefined || target.targetId === physical.targetId);
73
+ }
74
+ export function requireReadyProviderEnhancementTarget(value, tabId) {
75
+ if (!isProviderEnhancementTarget(value) || value.tabId !== tabId)
76
+ throw new Error('BROWSER_ENHANCEMENT_PAGE_IDENTITY_UNAVAILABLE');
77
+ if (!value.ready)
78
+ throw new Error('EXTERNAL_CONTROL_PAUSED: the external page owner has not granted Agent control');
79
+ return value;
80
+ }
package/dist/cookie.d.ts CHANGED
@@ -25,6 +25,12 @@ export interface CookieCandidate {
25
25
  partitionKey?: unknown;
26
26
  partitionKeyOpaque?: boolean;
27
27
  }
28
+ export type RequestCookie = Pick<CookieCandidate, 'name' | 'value'>;
29
+ export declare function serializeRequestCookies(cookies: readonly RequestCookie[]): string;
30
+ /** Token lookup follows the first (most specific path) occurrence in the Cookie header. */
31
+ export declare function requestCookiesToMap(cookies: readonly RequestCookie[]): Map<string, string>;
32
+ /** Chromium also treats localhost and loopback URLs as trustworthy for Secure cookies. */
33
+ export declare function isSecureCookieUrl(url: URL): boolean;
28
34
  /** Filter browser-context storage results to the cookies applicable to the requested URL. */
29
35
  export declare function filterCookiesForUrl<T extends CookieCandidate>(cookies: readonly T[], cookieUrl: string): T[];
30
36
  /** Select one readable, non-partitioned named cookie from URL-filtered backend results. */
package/dist/cookie.js CHANGED
@@ -1,3 +1,15 @@
1
+ export function serializeRequestCookies(cookies) {
2
+ return cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ');
3
+ }
4
+ /** Token lookup follows the first (most specific path) occurrence in the Cookie header. */
5
+ export function requestCookiesToMap(cookies) {
6
+ const values = new Map();
7
+ for (const cookie of cookies) {
8
+ if (!values.has(cookie.name))
9
+ values.set(cookie.name, cookie.value);
10
+ }
11
+ return values;
12
+ }
1
13
  function normalizeCookieDomain(input, action) {
2
14
  const value = input.trim();
3
15
  if (!value)
@@ -14,21 +26,24 @@ function normalizeCookieDomain(input, action) {
14
26
  throw new Error(`${action}: invalid domain "${input}"`);
15
27
  return hostname;
16
28
  }
17
- /** Filter browser-context storage results to the cookies applicable to the requested URL. */
18
- export function filterCookiesForUrl(cookies, cookieUrl) {
19
- const url = new URL(cookieUrl);
29
+ /** Chromium also treats localhost and loopback URLs as trustworthy for Secure cookies. */
30
+ export function isSecureCookieUrl(url) {
20
31
  const hostname = url.hostname.replace(/\.$/, '');
21
32
  const isLocalhost = hostname === 'localhost' ||
22
33
  hostname.endsWith('.localhost') ||
23
34
  hostname === '[::1]' ||
24
35
  /^127\.\d+\.\d+\.\d+$/.test(hostname);
36
+ return url.protocol === 'https:' || isLocalhost;
37
+ }
38
+ /** Filter browser-context storage results to the cookies applicable to the requested URL. */
39
+ export function filterCookiesForUrl(cookies, cookieUrl) {
40
+ const url = new URL(cookieUrl);
25
41
  return cookies.filter(item => {
26
42
  const domain = item.domain.replace(/^\./, '').toLowerCase();
27
43
  const hostOnly = item.hostOnly ?? !item.domain.startsWith('.');
28
44
  if (url.hostname !== domain && (hostOnly || !url.hostname.endsWith(`.${domain}`)))
29
45
  return false;
30
- // Chromium treats localhost and loopback URLs as trustworthy for Secure cookies.
31
- if (item.secure && url.protocol !== 'https:' && !isLocalhost)
46
+ if (item.secure && !isSecureCookieUrl(url))
32
47
  return false;
33
48
  return (url.pathname === item.path ||
34
49
  (url.pathname.startsWith(item.path) &&
@@ -0,0 +1,8 @@
1
+ export interface StructureOptions {
2
+ maxDepth?: number;
3
+ maxKeys?: number;
4
+ arraySamples?: number;
5
+ maxNodes?: number;
6
+ }
7
+ /** Describes a bounded sample of a value without invoking accessors or exposing leaf values. */
8
+ export declare function getStructure(data: unknown, options?: StructureOptions): unknown;
@@ -0,0 +1,58 @@
1
+ /** Describes a bounded sample of a value without invoking accessors or exposing leaf values. */
2
+ export function getStructure(data, options = {}) {
3
+ const { maxDepth = 5, maxKeys = 50, arraySamples = 1, maxNodes = 2000 } = options;
4
+ for (const [name, value, limit] of [
5
+ ['maxDepth', maxDepth, 20],
6
+ ['maxKeys', maxKeys, 1000],
7
+ ['arraySamples', arraySamples, 20],
8
+ ['maxNodes', maxNodes, 10000],
9
+ ]) {
10
+ if (!Number.isSafeInteger(value) || value < 1 || value > limit)
11
+ throw new Error(`${name} must be an integer between 1 and ${limit}`);
12
+ }
13
+ const ancestors = new WeakSet();
14
+ let nodes = 0;
15
+ const visit = (value, depth) => {
16
+ if (++nodes > maxNodes)
17
+ return '<max nodes reached>';
18
+ if (depth >= maxDepth)
19
+ return '<max depth reached>';
20
+ if (value === null)
21
+ return 'null';
22
+ if (typeof value !== 'object')
23
+ return typeof value;
24
+ if (ancestors.has(value))
25
+ return '<circular>';
26
+ ancestors.add(value);
27
+ try {
28
+ if (Array.isArray(value)) {
29
+ if (!value.length)
30
+ return 'array[0]';
31
+ const samples = Array.from({ length: Math.min(value.length, arraySamples) }, (_, index) => {
32
+ const descriptor = Object.getOwnPropertyDescriptor(value, index);
33
+ return descriptor && 'value' in descriptor
34
+ ? visit(descriptor.value, depth + 1)
35
+ : descriptor
36
+ ? '<accessor>'
37
+ : 'undefined';
38
+ });
39
+ return {
40
+ _type: `array[${value.length}]`,
41
+ ...(arraySamples === 1 ? { _sample: samples[0] } : { _samples: samples }),
42
+ };
43
+ }
44
+ const keys = Object.keys(value);
45
+ const entries = keys.slice(0, maxKeys).map(key => {
46
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
47
+ return [key, 'value' in descriptor ? visit(descriptor.value, depth + 1) : '<accessor>'];
48
+ });
49
+ if (keys.length > maxKeys)
50
+ entries.push(['<truncated keys>', keys.length - maxKeys]);
51
+ return Object.fromEntries(entries);
52
+ }
53
+ finally {
54
+ ancestors.delete(value);
55
+ }
56
+ };
57
+ return visit(data, 0);
58
+ }
@@ -0,0 +1,48 @@
1
+ import type { ResolvedEmulationConfig } from './deviceEmulation.js';
2
+ export interface DeviceUserAgentMetadata {
3
+ brands: Array<{
4
+ brand: string;
5
+ version: string;
6
+ }>;
7
+ fullVersionList: Array<{
8
+ brand: string;
9
+ version: string;
10
+ }>;
11
+ fullVersion?: string;
12
+ platform: string;
13
+ platformVersion: string;
14
+ architecture: string;
15
+ model: string;
16
+ mobile: boolean;
17
+ bitness?: string;
18
+ wow64?: boolean;
19
+ formFactors?: string[];
20
+ }
21
+ export interface DeviceEnvironmentObservation {
22
+ userAgent: string;
23
+ platform: string;
24
+ userAgentMetadata: DeviceUserAgentMetadata | null;
25
+ screen: {
26
+ width: number;
27
+ height: number;
28
+ deviceScaleFactor: number;
29
+ orientation: string;
30
+ angle: number;
31
+ };
32
+ touchPoints: number;
33
+ safeAreaInsets: {
34
+ top: number;
35
+ right: number;
36
+ bottom: number;
37
+ left: number;
38
+ };
39
+ }
40
+ /** Explicitly disables UA Client Hints for an emulated UA, matching Chrome's omitted-metadata behavior. */
41
+ export declare function emptyDeviceUserAgentMetadata(): DeviceUserAgentMetadata;
42
+ /** Self-contained observation for an isolated page world; no retained DOM or page globals. */
43
+ export declare function observeDeviceEnvironment(): Promise<DeviceEnvironmentObservation>;
44
+ export declare function isDeviceUserAgentMetadata(value: unknown): value is DeviceUserAgentMetadata;
45
+ export declare function isDeviceEnvironmentObservation(value: unknown): value is DeviceEnvironmentObservation;
46
+ /** CDP transports can reorder object keys. Values and array order remain significant. */
47
+ export declare function deviceEnvironmentObservationsEqual(left: DeviceEnvironmentObservation, right: DeviceEnvironmentObservation): boolean;
48
+ export declare function matchesEmulatedDeviceEnvironment(config: ResolvedEmulationConfig, observed: DeviceEnvironmentObservation): boolean;
@@ -0,0 +1,77 @@
1
+ /** Explicitly disables UA Client Hints for an emulated UA, matching Chrome's omitted-metadata behavior. */
2
+ export function emptyDeviceUserAgentMetadata() {
3
+ return { brands: [], fullVersionList: [], fullVersion: '', platform: '', platformVersion: '', architecture: '', model: '',
4
+ mobile: false, bitness: '', wow64: false, formFactors: [] };
5
+ }
6
+ /** Self-contained observation for an isolated page world; no retained DOM or page globals. */
7
+ export async function observeDeviceEnvironment() {
8
+ const data = navigator.userAgentData;
9
+ let userAgentMetadata = null;
10
+ if (data) {
11
+ const values = await data.getHighEntropyValues(['architecture', 'bitness', 'fullVersionList', 'model', 'platformVersion', 'uaFullVersion', 'wow64', 'formFactors']);
12
+ userAgentMetadata = {
13
+ brands: values.brands, fullVersionList: values.fullVersionList,
14
+ ...(typeof values.uaFullVersion === 'string' ? { fullVersion: values.uaFullVersion } : {}),
15
+ platform: values.platform, platformVersion: values.platformVersion, architecture: values.architecture, model: values.model, mobile: values.mobile,
16
+ ...(typeof values.bitness === 'string' ? { bitness: values.bitness } : {}),
17
+ ...(typeof values.wow64 === 'boolean' ? { wow64: values.wow64 } : {}),
18
+ ...(Array.isArray(values.formFactors) ? { formFactors: values.formFactors } : {}),
19
+ };
20
+ }
21
+ const probe = document.createElement('div');
22
+ probe.style.cssText = 'all:initial!important;position:fixed!important;visibility:hidden!important;pointer-events:none!important;width:0!important;height:0!important;padding-top:env(safe-area-inset-top)!important;padding-right:env(safe-area-inset-right)!important;padding-bottom:env(safe-area-inset-bottom)!important;padding-left:env(safe-area-inset-left)!important;';
23
+ document.documentElement.appendChild(probe);
24
+ try {
25
+ const style = getComputedStyle(probe);
26
+ return {
27
+ userAgent: navigator.userAgent, platform: navigator.platform, userAgentMetadata,
28
+ screen: { width: screen.width, height: screen.height, deviceScaleFactor: devicePixelRatio, orientation: screen.orientation.type, angle: screen.orientation.angle },
29
+ touchPoints: navigator.maxTouchPoints,
30
+ safeAreaInsets: { top: parseFloat(style.paddingTop) || 0, right: parseFloat(style.paddingRight) || 0, bottom: parseFloat(style.paddingBottom) || 0, left: parseFloat(style.paddingLeft) || 0 },
31
+ };
32
+ }
33
+ finally {
34
+ probe.remove();
35
+ }
36
+ }
37
+ export function isDeviceUserAgentMetadata(value) {
38
+ if (!value || typeof value !== 'object')
39
+ return false;
40
+ const metadata = value;
41
+ const brands = (value) => Array.isArray(value) && value.every(item => item && typeof item.brand === 'string' && typeof item.version === 'string');
42
+ return brands(metadata.brands) && brands(metadata.fullVersionList) &&
43
+ [metadata.platform, metadata.platformVersion, metadata.architecture, metadata.model].every(value => typeof value === 'string') &&
44
+ typeof metadata.mobile === 'boolean' && (metadata.fullVersion === undefined || typeof metadata.fullVersion === 'string') &&
45
+ (metadata.bitness === undefined || typeof metadata.bitness === 'string') && (metadata.wow64 === undefined || typeof metadata.wow64 === 'boolean') &&
46
+ (metadata.formFactors === undefined || (Array.isArray(metadata.formFactors) && metadata.formFactors.every(value => typeof value === 'string')));
47
+ }
48
+ export function isDeviceEnvironmentObservation(value) {
49
+ if (!value || typeof value !== 'object')
50
+ return false;
51
+ const current = value;
52
+ return typeof current.userAgent === 'string' && typeof current.platform === 'string' &&
53
+ (current.userAgentMetadata === null || isDeviceUserAgentMetadata(current.userAgentMetadata)) &&
54
+ Boolean(current.screen) && [current.screen.width, current.screen.height, current.screen.deviceScaleFactor].every(value => Number.isFinite(value) && value > 0) &&
55
+ typeof current.screen.orientation === 'string' && Number.isFinite(current.screen.angle) &&
56
+ Number.isSafeInteger(current.touchPoints) && current.touchPoints >= 0 && Boolean(current.safeAreaInsets) &&
57
+ [current.safeAreaInsets.top, current.safeAreaInsets.right, current.safeAreaInsets.bottom, current.safeAreaInsets.left].every(value => Number.isFinite(value) && value >= 0);
58
+ }
59
+ /** CDP transports can reorder object keys. Values and array order remain significant. */
60
+ export function deviceEnvironmentObservationsEqual(left, right) {
61
+ const ordered = (value) => {
62
+ if (Array.isArray(value))
63
+ return value.map(ordered);
64
+ if (value && typeof value === 'object')
65
+ return Object.fromEntries(Object.keys(value).sort().map(key => [key, ordered(value[key])]));
66
+ return value;
67
+ };
68
+ return JSON.stringify(ordered(left)) === JSON.stringify(ordered(right));
69
+ }
70
+ export function matchesEmulatedDeviceEnvironment(config, observed) {
71
+ return observed.userAgent === config.userAgent && observed.platform === config.platform &&
72
+ observed.screen.width === config.width && observed.screen.height === config.height &&
73
+ Math.abs(observed.screen.deviceScaleFactor - config.deviceScaleFactor) < 0.0001 &&
74
+ observed.screen.orientation.startsWith(config.orientation) && observed.screen.angle === (config.orientation === 'landscape' ? 90 : 0) &&
75
+ observed.touchPoints === (config.touch ? 5 : 0) &&
76
+ ['top', 'right', 'bottom', 'left'].every(edge => observed.safeAreaInsets[edge] === config.safeAreaInsets[edge]);
77
+ }
@@ -40,6 +40,7 @@ export interface PageClickDispatchTarget {
40
40
  frame?: PageClickFrameTarget;
41
41
  coordinates?: PageClickCoordinates;
42
42
  controlState?: PageClickControlState;
43
+ selectionState?: PageClickSelectionState;
43
44
  dispatchMode?: PageClickDispatchMode;
44
45
  pointerType?: PageClickPointerType;
45
46
  }
@@ -84,6 +85,15 @@ export interface PageClickControlState {
84
85
  checkedBefore: boolean;
85
86
  checkedAfter: boolean;
86
87
  }
88
+ export interface PageClickSelectionState {
89
+ valueBefore: string;
90
+ valueAfter: string;
91
+ selectedAfter: boolean;
92
+ }
93
+ /** Native options need value selection; clicking their select only opens the menu. */
94
+ export declare function isStateAwareClickTarget(target: PageClickDispatchTarget & {
95
+ inputType?: string;
96
+ }): boolean;
87
97
  export interface PageClickResolvedTarget {
88
98
  tag: string;
89
99
  text: string;
@@ -102,6 +112,7 @@ export interface PageClickResultData {
102
112
  coordinates?: PageClickCoordinates;
103
113
  actionableAncestorCandidates?: PageClickAncestorCandidate[];
104
114
  controlState?: PageClickControlState;
115
+ selectionState?: PageClickSelectionState;
105
116
  dispatchMode?: PageClickDispatchMode;
106
117
  pointerType?: PageClickPointerType;
107
118
  fallbackReason?: PageClickFallbackReason;
@@ -127,6 +138,7 @@ export interface PageClickDispatchAttemptEvidence {
127
138
  coordinates?: PageClickCoordinates;
128
139
  actionableAncestorCandidates?: Array<Pick<PageClickAncestorCandidate, 'selector' | 'distance' | 'reason'>>;
129
140
  controlState?: PageClickControlState;
141
+ selectionState?: PageClickSelectionState;
130
142
  dispatchMode?: PageClickDispatchMode;
131
143
  pointerType?: PageClickPointerType;
132
144
  }
@@ -161,6 +173,7 @@ export declare function clickElementViaJS(this: any, expectedText?: string): {
161
173
  text: string;
162
174
  };
163
175
  controlState?: PageClickControlState;
176
+ selectionState?: PageClickSelectionState;
164
177
  error?: string;
165
178
  } | null;
166
179
  /**
@@ -179,6 +192,7 @@ export declare function clickElementBySelector(selector: string, invalidSelector
179
192
  strategy?: PageClickTargetStrategy;
180
193
  distance?: number;
181
194
  controlState?: PageClickControlState;
195
+ selectionState?: PageClickSelectionState;
182
196
  error?: string;
183
197
  };
184
198
  /**
@@ -12,6 +12,12 @@ export const PAGE_CLICK_INPUT_TIMEOUT_MS = 10_000;
12
12
  export const PAGE_CLICK_HIDDEN_TOUCH_ERROR = 'page_click: clickMode "touch" requires a visible page, but the target is hidden or in a background tab. ' +
13
13
  'Use clickMode "auto" for DOM fallback, or call tab_open for the same URL with reuse "require" and active true. ' +
14
14
  'Extension activation uses background focus emulation while Do not disturb is enabled; check activation.visibilityState before retrying trusted input; use "mouse" only when touch semantics are not required.';
15
+ /** Native options need value selection; clicking their select only opens the menu. */
16
+ export function isStateAwareClickTarget(target) {
17
+ return (target.tag === 'option' ||
18
+ target.sourceTarget?.tag === 'option' ||
19
+ /^(checkbox|radio)$/i.test(target.inputType ?? ''));
20
+ }
15
21
  /** Resolve the click path from the requested mode, page visibility, and device emulation state. */
16
22
  export function resolvePageClickDispatchPolicy(clickMode, visibilityState, touchEmulationEnabled) {
17
23
  if (clickMode !== undefined && clickMode !== 'auto') {
@@ -62,6 +68,7 @@ export function createPageClickResult(requestedTarget, dispatchTarget) {
62
68
  ? { actionableAncestorCandidates: evidence.actionableAncestorCandidates }
63
69
  : {}),
64
70
  ...(evidence.controlState ? { controlState: evidence.controlState } : {}),
71
+ ...(evidence.selectionState ? { selectionState: evidence.selectionState } : {}),
65
72
  ...(evidence.dispatchMode ? { dispatchMode: evidence.dispatchMode } : {}),
66
73
  ...(evidence.pointerType ? { pointerType: evidence.pointerType } : {}),
67
74
  };
@@ -90,6 +97,7 @@ export function describePageClickDispatchTarget(dispatchTarget) {
90
97
  ? { actionableAncestorCandidates: dispatchTarget.actionableAncestorCandidates }
91
98
  : {}),
92
99
  ...(dispatchTarget.controlState ? { controlState: dispatchTarget.controlState } : {}),
100
+ ...(dispatchTarget.selectionState ? { selectionState: dispatchTarget.selectionState } : {}),
93
101
  ...(dispatchTarget.dispatchMode ? { dispatchMode: dispatchTarget.dispatchMode } : {}),
94
102
  ...(dispatchTarget.pointerType ? { pointerType: dispatchTarget.pointerType } : {}),
95
103
  };
@@ -126,6 +134,7 @@ export function describePageClickDispatchAttempt(dispatchTarget) {
126
134
  }
127
135
  : {}),
128
136
  ...(evidence.controlState ? { controlState: evidence.controlState } : {}),
137
+ ...(evidence.selectionState ? { selectionState: evidence.selectionState } : {}),
129
138
  ...(evidence.dispatchMode ? { dispatchMode: evidence.dispatchMode } : {}),
130
139
  ...(evidence.pointerType ? { pointerType: evidence.pointerType } : {}),
131
140
  };
@@ -149,11 +158,24 @@ export function clickElementViaJS(expectedText) {
149
158
  if (el.tagName === 'OPTION') {
150
159
  const select = el.closest('select');
151
160
  if (select) {
152
- const desc = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value');
161
+ if (select.matches(':disabled') || el.disabled || el.closest('optgroup')?.disabled) {
162
+ return {
163
+ tag: 'option',
164
+ text: sourceTarget.text,
165
+ cx: 0,
166
+ cy: 0,
167
+ strategy: 'self',
168
+ distance: 0,
169
+ sourceTarget,
170
+ error: 'Cannot select a disabled option',
171
+ };
172
+ }
173
+ const valueBefore = select.value;
174
+ const desc = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'selectedIndex');
153
175
  if (desc && desc.set)
154
- desc.set.call(select, el.value);
176
+ desc.set.call(select, el.index);
155
177
  else
156
- select.value = el.value;
178
+ select.selectedIndex = el.index;
157
179
  select.dispatchEvent(new Event('input', { bubbles: true }));
158
180
  select.dispatchEvent(new Event('change', { bubbles: true }));
159
181
  const rect = select.getBoundingClientRect();
@@ -165,6 +187,7 @@ export function clickElementViaJS(expectedText) {
165
187
  strategy: 'self',
166
188
  distance: 0,
167
189
  sourceTarget,
190
+ selectionState: { valueBefore, valueAfter: select.value, selectedAfter: el.selected },
168
191
  };
169
192
  }
170
193
  }
@@ -365,9 +388,22 @@ export function clickElementViaJS(expectedText) {
365
388
  let hitTarget = null;
366
389
  try {
367
390
  hitTarget = document.elementFromPoint(cx, cy);
391
+ const visited = new Set();
392
+ while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
393
+ visited.add(hitTarget);
394
+ const inner = hitTarget.shadowRoot.elementFromPoint(cx, cy);
395
+ if (!inner || inner === hitTarget)
396
+ break;
397
+ hitTarget = inner;
398
+ }
368
399
  }
369
400
  catch { }
401
+ let composedHit = hitTarget;
402
+ while (composedHit && composedHit !== target) {
403
+ composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
404
+ }
370
405
  if (hitTarget &&
406
+ !composedHit &&
371
407
  hitTarget !== target &&
372
408
  !target.contains?.(hitTarget) &&
373
409
  !hitTarget.contains?.(target)) {
@@ -511,11 +547,23 @@ export function clickElementBySelector(selector, invalidSelectorHint) {
511
547
  if (element.tagName === 'OPTION') {
512
548
  const select = element.closest('select');
513
549
  if (select) {
514
- const desc = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value');
550
+ const option = element;
551
+ if (select.matches(':disabled') || option.disabled || option.closest('optgroup')?.disabled) {
552
+ return {
553
+ clicked: false,
554
+ tag: 'option',
555
+ text: (option.textContent || '').trim().slice(0, 100),
556
+ cx: 0,
557
+ cy: 0,
558
+ error: 'Cannot select a disabled option',
559
+ };
560
+ }
561
+ const valueBefore = select.value;
562
+ const desc = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'selectedIndex');
515
563
  if (desc && desc.set)
516
- desc.set.call(select, element.value);
564
+ desc.set.call(select, option.index);
517
565
  else
518
- select.value = element.value;
566
+ select.selectedIndex = option.index;
519
567
  select.dispatchEvent(new Event('input', { bubbles: true }));
520
568
  select.dispatchEvent(new Event('change', { bubbles: true }));
521
569
  const rect = select.getBoundingClientRect();
@@ -527,6 +575,7 @@ export function clickElementBySelector(selector, invalidSelectorHint) {
527
575
  cy: rect.y + rect.height / 2,
528
576
  strategy: 'self',
529
577
  distance: 0,
578
+ selectionState: { valueBefore, valueAfter: select.value, selectedAfter: option.selected },
530
579
  };
531
580
  }
532
581
  }
@@ -680,9 +729,22 @@ export function clickElementBySelector(selector, invalidSelectorHint) {
680
729
  let hitTarget = null;
681
730
  try {
682
731
  hitTarget = document.elementFromPoint(cx, cy);
732
+ const visited = new Set();
733
+ while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
734
+ visited.add(hitTarget);
735
+ const inner = hitTarget.shadowRoot.elementFromPoint(cx, cy);
736
+ if (!inner || inner === hitTarget)
737
+ break;
738
+ hitTarget = inner;
739
+ }
683
740
  }
684
741
  catch { }
742
+ let composedHit = hitTarget;
743
+ while (composedHit && composedHit !== target) {
744
+ composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
745
+ }
685
746
  if (hitTarget &&
747
+ !composedHit &&
686
748
  hitTarget !== target &&
687
749
  !target.contains(hitTarget) &&
688
750
  !hitTarget.contains(target)) {
@@ -1001,9 +1063,22 @@ export function getClickCoordsFromSelector(selector, invalidSelectorHint) {
1001
1063
  let hitTarget = null;
1002
1064
  try {
1003
1065
  hitTarget = document.elementFromPoint(x, y);
1066
+ const visited = new Set();
1067
+ while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
1068
+ visited.add(hitTarget);
1069
+ const inner = hitTarget.shadowRoot.elementFromPoint(x, y);
1070
+ if (!inner || inner === hitTarget)
1071
+ break;
1072
+ hitTarget = inner;
1073
+ }
1004
1074
  }
1005
1075
  catch { }
1076
+ let composedHit = hitTarget;
1077
+ while (composedHit && composedHit !== target) {
1078
+ composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
1079
+ }
1006
1080
  if (hitTarget &&
1081
+ !composedHit &&
1007
1082
  hitTarget !== target &&
1008
1083
  !target.contains(hitTarget) &&
1009
1084
  !hitTarget.contains(target)) {
@@ -1295,9 +1370,22 @@ export function getClickCoordsFromNode(expectedText) {
1295
1370
  let hitTarget = null;
1296
1371
  try {
1297
1372
  hitTarget = document.elementFromPoint(x, y);
1373
+ const visited = new Set();
1374
+ while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
1375
+ visited.add(hitTarget);
1376
+ const inner = hitTarget.shadowRoot.elementFromPoint(x, y);
1377
+ if (!inner || inner === hitTarget)
1378
+ break;
1379
+ hitTarget = inner;
1380
+ }
1298
1381
  }
1299
1382
  catch { }
1383
+ let composedHit = hitTarget;
1384
+ while (composedHit && composedHit !== target) {
1385
+ composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
1386
+ }
1300
1387
  if (hitTarget &&
1388
+ !composedHit &&
1301
1389
  hitTarget !== target &&
1302
1390
  !target.contains?.(hitTarget) &&
1303
1391
  !hitTarget.contains?.(target)) {
@@ -1353,12 +1441,12 @@ export function showClickIndicator(px, py, mode) {
1353
1441
  ring +
1354
1442
  ';' +
1355
1443
  'pointer-events:none; z-index:2147483647;' +
1356
- 'animation:__mtop_click_fade 600ms ease-out forwards;';
1357
- if (!document.getElementById('__mtop_click_style')) {
1444
+ 'animation:__mearl_click_fade 600ms ease-out forwards;';
1445
+ if (!document.getElementById('__mearl_click_style')) {
1358
1446
  const style = document.createElement('style');
1359
- style.id = '__mtop_click_style';
1447
+ style.id = '__mearl_click_style';
1360
1448
  style.textContent =
1361
- '@keyframes __mtop_click_fade {' +
1449
+ '@keyframes __mearl_click_fade {' +
1362
1450
  '0% { opacity:1; transform:scale(1); }' +
1363
1451
  '50% { opacity:0.8; transform:scale(1.8); }' +
1364
1452
  '100% { opacity:0; transform:scale(2.2); }' +
@@ -211,9 +211,21 @@ export function inspectElementsInPage(selector, filter, styles, maxResults, inva
211
211
  let hit = null;
212
212
  try {
213
213
  hit = visible ? (documentRef.elementFromPoint?.(x, y) ?? null) : null;
214
+ const visited = new Set();
215
+ while (hit?.shadowRoot?.elementFromPoint && !visited.has(hit)) {
216
+ visited.add(hit);
217
+ const inner = hit.shadowRoot.elementFromPoint(x, y);
218
+ if (!inner || inner === hit)
219
+ break;
220
+ hit = inner;
221
+ }
214
222
  }
215
223
  catch { }
216
- const topmost = !!hit && (hit === element || element.contains?.(hit));
224
+ let composedHit = hit;
225
+ while (composedHit && composedHit !== element) {
226
+ composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
227
+ }
228
+ const topmost = !!hit && (!!composedHit || element.contains?.(hit));
217
229
  const target = hit
218
230
  ? {
219
231
  selector: selectorFor(hit),
@@ -1,5 +1,6 @@
1
1
  import type { BrowserAccessResult, BrowserActionMap, BrowserJsonValue, BrowserProviderListResult, BrowserProviderLaunchForm, ControlSessionInfo, CookieCopyScope, ManagedBrowserInfo, BrowserReleaseParams, BrowserReleaseResult, PageDownloadParams } from './browserActionProtocol.js';
2
2
  import { type AgentServiceActionMap } from './agentServiceProtocol.js';
3
+ import type { BrowserEnhancementRequest, BrowserEnhancementResponse } from './browserEnhancement.js';
3
4
  import { type ToolboxServiceActionMap, type ToolboxTaskRequest, type ToolboxTaskResponse } from './toolbox.js';
4
5
  export declare const EXTENSION_RPC_VERSION = 1;
5
6
  export type OperationRequest<M> = {
@@ -171,6 +172,10 @@ export type ExtensionUiServiceRequest<A extends ExtensionUiServiceAction> = Exte
171
172
  export type ExtensionUiServiceResponse<A extends ExtensionUiServiceAction, R> = A extends BrowserManagementAction ? BrowserManagementResponse<A, R> : A extends 'toolbox_task' ? R extends ToolboxTaskRequest ? ToolboxTaskResponse<R> : never : ExtensionUiServiceActionMap[A]['response'];
172
173
  export declare function isExtensionUiServiceAction(value: unknown): value is ExtensionUiServiceAction;
173
174
  export interface HostToExtensionActionMap {
175
+ __browser_enhancement: {
176
+ request: BrowserEnhancementRequest;
177
+ response: BrowserEnhancementResponse;
178
+ };
174
179
  __preset_installation: {
175
180
  request: Record<string, unknown>;
176
181
  response: unknown;
@@ -206,6 +211,7 @@ export interface HostToExtensionActionMap {
206
211
  __control_cleanup: {
207
212
  request: BrowserReleaseParams & {
208
213
  tabIds: number[];
214
+ returningToAgent?: boolean;
209
215
  };
210
216
  response: BrowserReleaseResult;
211
217
  };
@@ -249,6 +255,7 @@ export interface HostToExtensionActionMap {
249
255
  };
250
256
  }
251
257
  export declare const HOST_TO_EXTENSION_ACTIONS: {
258
+ readonly __browser_enhancement: true;
252
259
  readonly __preset_installation: true;
253
260
  readonly __preset_browser: true;
254
261
  readonly __export_cookies: true;