@shiplightai/sdk 0.1.1 → 0.1.2

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 (38) hide show
  1. package/README.md +18 -10
  2. package/dist/agentHelpers-MRG6DCNX.js +4 -0
  3. package/dist/agentHelpers-MRG6DCNX.js.map +1 -0
  4. package/dist/agentLogin-QZDVIJMB.js +4 -0
  5. package/dist/agentLogin-QZDVIJMB.js.map +1 -0
  6. package/dist/chunk-DIRPNR2B.js +195 -0
  7. package/dist/chunk-DIRPNR2B.js.map +1 -0
  8. package/dist/chunk-FWACDSD6.js +17 -0
  9. package/dist/chunk-FWACDSD6.js.map +1 -0
  10. package/dist/chunk-GVEDIII4.js +25 -0
  11. package/dist/chunk-GVEDIII4.js.map +1 -0
  12. package/dist/{chunk-UHZTPBZ3.js → chunk-N54UPO3H.js} +95 -92
  13. package/dist/chunk-N54UPO3H.js.map +1 -0
  14. package/dist/chunk-ODNKMWXO.js +6 -0
  15. package/dist/chunk-ODNKMWXO.js.map +1 -0
  16. package/dist/{chunk-GPZJYXUG.js → chunk-SSPF674P.js} +19 -6
  17. package/dist/chunk-SSPF674P.js.map +1 -0
  18. package/dist/chunk-USNFIQN5.js +4 -0
  19. package/dist/chunk-USNFIQN5.js.map +1 -0
  20. package/dist/chunk-W6S73J4I.js +4 -0
  21. package/dist/chunk-W6S73J4I.js.map +1 -0
  22. package/dist/handler-O7GYRDNA.js +4 -0
  23. package/dist/handler-O7GYRDNA.js.map +1 -0
  24. package/dist/index.js +12 -9
  25. package/dist/index.js.map +1 -0
  26. package/dist/task-E5YOHPFW.js +193 -0
  27. package/dist/task-E5YOHPFW.js.map +1 -0
  28. package/package.json +13 -13
  29. package/dist/agentHelpers-UCLT5EKK.js +0 -1
  30. package/dist/agentLogin-ARB3NEO4.js +0 -1
  31. package/dist/chunk-6H2NJBNL.js +0 -1
  32. package/dist/chunk-GDTCZALZ.js +0 -192
  33. package/dist/chunk-KFC5I6R5.js +0 -14
  34. package/dist/chunk-QIBDXB3J.js +0 -22
  35. package/dist/chunk-UFLZ3URR.js +0 -1
  36. package/dist/chunk-YR4E7JSB.js +0 -3
  37. package/dist/handler-TPOFKKIB.js +0 -1
  38. package/dist/task-57MAWXLN.js +0 -190
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../sdk-core/src/llm_tools/registry.ts","../../sdk-core/src/agent/llm/anthropic.ts","../../sdk-core/src/agent/llm/google.ts","../../sdk-core/src/agent/llm/index.ts","../../sdk-core/src/utils/imageUtils.ts","../../sdk-core/src/dom/axtree-shared.ts","../../sdk-core/src/dom/utils/textUtils.ts","../../sdk-core/src/dom/types.ts","../../sdk-core/src/dom/nodes.ts","raw-loader:/Users/feng/Shiplight/monots/packages/sdk-core/src/dom/dom-tree/index.js?raw","raw-loader:/Users/feng/Shiplight/monots/packages/sdk-core/src/dom/dom-tree/dist/index.js?raw","../../sdk-core/src/dom/service.ts"],"sourcesContent":["/**\n * LLM Tools - Tool Registry\n *\n * Central registry for managing tool registration, validation, and execution.\n * Similar to browser-use's Registry pattern but adapted for TypeScript + Zod.\n */\n\nimport { z } from 'zod';\nimport {\n RegisteredTool,\n ToolExecutionContext,\n ToolRegistrationConfig,\n ToolResult,\n} from './types';\n\n/**\n * ToolRegistry - Manages tool registration and execution\n *\n * Example usage:\n * ```typescript\n * const registry = new ToolRegistry();\n *\n * // Register a tool\n * registry.register({\n * name: 'click',\n * description: 'Click an element by index',\n * schema: z.object({ index: z.number() }),\n * execute: async (args, ctx) => {\n * // Implementation\n * return { success: true, actionEntity, message: 'Clicked!' };\n * },\n * });\n *\n * // Execute a tool\n * const result = await registry.execute('click', { index: 5 }, context);\n * ```\n */\nexport class ToolRegistry {\n private tools = new Map<string, RegisteredTool>();\n\n /**\n * Register a tool with schema and execution logic\n *\n * @param config - Tool configuration including name, description, schema, and execute function\n * @throws {Error} If a tool with the same name is already registered\n */\n register<T extends z.ZodType>(config: ToolRegistrationConfig<T>): void {\n if (this.tools.has(config.name)) {\n throw new Error(`Tool '${config.name}' is already registered`);\n }\n\n const tool: RegisteredTool = {\n name: config.name,\n description: config.description,\n schema: config.schema,\n execute: config.execute as any, // Type erasure for storage\n usesElementIndex: config.usesElementIndex ?? false,\n availability: {\n openai: config.availability?.openai ?? true,\n mcp: config.availability?.mcp ?? true,\n },\n };\n\n this.tools.set(config.name, tool);\n }\n\n /**\n * Get a tool by name\n *\n * @param name - Tool name\n * @returns The registered tool or undefined if not found\n */\n get(name: string): RegisteredTool | undefined {\n return this.tools.get(name);\n }\n\n /**\n * Check if a tool exists\n *\n * @param name - Tool name\n * @returns True if the tool is registered\n */\n has(name: string): boolean {\n return this.tools.has(name);\n }\n\n /**\n * Get all registered tool names\n *\n * @returns Array of tool names\n */\n getToolNames(): string[] {\n return Array.from(this.tools.keys());\n }\n\n /**\n * Get all registered tools\n *\n * @returns Array of registered tools\n */\n getTools(): RegisteredTool[] {\n return Array.from(this.tools.values());\n }\n\n /**\n * Execute a tool by name with argument validation\n *\n * @param toolName - Name of the tool to execute\n * @param args - Arguments to pass to the tool (will be validated against schema)\n * @param context - Execution context (page, agent, domService, etc.)\n * @returns Tool execution result with ActionEntity\n * @throws {Error} If tool not found or validation fails\n */\n async execute(\n toolName: string,\n args: any,\n context: ToolExecutionContext\n ): Promise<ToolResult> {\n const tool = this.tools.get(toolName);\n\n if (!tool) {\n throw new Error(`Tool not found: ${toolName}`);\n }\n\n try {\n // Extract description from args before validation (if present)\n // Description is added by VercelAIToolProvider but not part of the original schema\n const actionDescription = args?.description;\n const argsWithoutDescription = { ...args };\n delete argsWithoutDescription.description;\n\n // Validate arguments with Zod schema (without description field)\n const validatedArgs = tool.schema.parse(argsWithoutDescription);\n\n // Add description to context so tool implementations can use it\n const contextWithDescription = {\n ...context,\n actionDescription,\n };\n\n // Execute tool with validated arguments and enhanced context\n return await tool.execute(validatedArgs, contextWithDescription);\n } catch (error) {\n // Handle Zod validation errors\n if (error instanceof z.ZodError) {\n const errorMessages = error.issues\n .map((err: any) => `${err.path.join('.')}: ${err.message}`)\n .join(', ');\n\n return {\n success: false,\n error: `Invalid arguments for tool '${toolName}': ${errorMessages}`,\n actionEntity: {\n action_description: args?.description || `${toolName} (validation failed)`,\n action_data: {\n action_name: toolName,\n kwargs: args,\n },\n feedback: `Validation error: ${errorMessages}`,\n },\n };\n }\n\n // Re-throw unexpected errors\n throw error;\n }\n }\n\n /**\n * Clear all registered tools (useful for testing)\n */\n clear(): void {\n this.tools.clear();\n }\n\n /**\n * Get the count of registered tools\n */\n size(): number {\n return this.tools.size;\n }\n\n /**\n * Build a Zod union schema for all registered actions\n * Each action becomes: z.object({ actionName: actionSchema })\n * Returns a union of all these wrapped schemas\n *\n * This is used by generateText with Output.object() to enforce structured output from LLMs.\n * Gemini requires OBJECT types to have non-empty properties, so we can't\n * use z.record(z.any()). Instead, we build an explicit union of all actions.\n *\n * For actions with no parameters (empty z.object({})), we add a dummy\n * optional property since Gemini rejects objects with empty properties.\n */\n buildActionUnionSchema(): z.ZodType {\n const tools = this.getTools().filter(tool => tool.availability.openai);\n return this.buildUnionSchemaFromTools(tools);\n }\n\n /**\n * Build a Zod union schema for a specific subset of tools by name.\n * Useful for exposing only certain actions to external consumers (e.g., MCP tools).\n * Each action schema is automatically extended with a description field.\n *\n * @param toolNames - Array of tool names to include in the schema\n * @returns Zod union schema for the specified tools\n */\n buildActionUnionSchemaForTools(toolNames: string[], withDescriptionField: boolean = false): z.ZodType {\n const toolNameSet = new Set(toolNames);\n const tools = this.getTools().filter(tool => toolNameSet.has(tool.name));\n return this.buildUnionSchemaFromTools(tools, withDescriptionField);\n }\n\n /**\n * Internal helper to build union schema from a list of tools\n */\n private buildUnionSchemaFromTools(tools: RegisteredTool[], withDescriptionField: boolean = false): z.ZodType {\n if (tools.length === 0) {\n // Fallback if no tools registered\n return z.object({ done: z.any() });\n }\n\n // Wrap each tool's schema as { toolName: schema }\n // For empty schemas (no properties), add a dummy optional field since:\n // 1. Gemini rejects objects with empty properties\n // 2. z.any() causes issues with Vercel AI SDK's union validation (no \"required\" field)\n const wrappedSchemas = tools.map(tool => {\n let schema: z.ZodType = tool.schema;\n\n // Extend schema with description field if requested\n if (withDescriptionField && schema instanceof z.ZodObject) {\n const shape = schema._def.shape();\n schema = z.object({\n ...shape,\n description: z.string().describe('Semantic, human-readable description of the action'),\n });\n }\n\n // Check if schema is an empty object (no properties)\n // ZodObject with empty shape has _def.shape() returning {}\n if (schema instanceof z.ZodObject) {\n const shape = schema._def.shape();\n if (Object.keys(shape).length === 0) {\n // Add a dummy optional field instead of z.any()\n // This ensures proper \"required\" field in JSON Schema for union validation\n schema = z.object({ _empty: z.boolean().optional() });\n }\n }\n\n return z.object({ [tool.name]: schema });\n });\n\n // Create union of all wrapped schemas\n // z.union requires at least 2 elements\n if (wrappedSchemas.length === 1) {\n return wrappedSchemas[0];\n }\n\n // TypeScript needs explicit casting for z.union's tuple requirement\n const [first, second, ...rest] = wrappedSchemas;\n return z.union([first, second, ...rest]);\n }\n}\n\n/**\n * Singleton instance of the tool registry\n * Use this for global tool registration\n */\nexport const toolRegistry = new ToolRegistry();\n","/**\n * Anthropic Provider\n *\n * Provides Anthropic Claude model instances via Vercel AI SDK.\n */\n\nimport { createAnthropic } from '@ai-sdk/anthropic';\nimport type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { getSdkConfig } from '../../config';\nimport logger from '../../utils/logger';\n\n/**\n * Get Anthropic Claude model\n *\n * Supported models:\n * - claude-sonnet-4-5 (or with date suffix, e.g., claude-sonnet-4-5-20250101)\n * - claude-haiku-4-5 (or with date suffix)\n * - claude-opus-4-5 (or with date suffix)\n *\n * Environment variables (in SDK config):\n * - ANTHROPIC_API_KEY: Anthropic API key (required)\n */\nexport function getAnthropicModel(modelName: string): LanguageModelV3 {\n\tconst config = getSdkConfig();\n\tconst apiKey = config.env?.ANTHROPIC_API_KEY;\n\n\tif (!apiKey) {\n\t\tthrow new Error('ANTHROPIC_API_KEY not configured in SDK config');\n\t}\n\n\tlogger.debug(`Using Anthropic provider: model=${modelName}`);\n\tconst anthropic = createAnthropic({ apiKey });\n\treturn anthropic(modelName);\n}\n\n/**\n * Provider options type for Anthropic\n */\nexport type AnthropicProviderOptionsResult = {\n\tanthropic?: {\n\t\tstructuredOutputMode?: 'outputFormat' | 'jsonTool' | 'auto';\n\t};\n};\n\n/**\n * Get Anthropic provider options\n *\n * Uses 'jsonTool' structured output mode for Claude models.\n * This mode uses tool calling internally to enforce JSON schema.\n */\nexport function getAnthropicProviderOptions(_modelName: string): AnthropicProviderOptionsResult {\n\treturn {\n\t\tanthropic: {\n\t\t\tstructuredOutputMode: 'jsonTool',\n\t\t},\n\t};\n}\n","/**\n * Google AI Provider\n *\n * Provides Google AI and Vertex AI model instances via Vercel AI SDK.\n */\n\nimport { createGoogleGenerativeAI, GoogleGenerativeAIProviderOptions } from '@ai-sdk/google';\nimport { createVertex } from '@ai-sdk/google-vertex';\nimport type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { getSdkConfig } from '../../config';\nimport logger from '../../utils/logger';\n\n// Inline MediaResolution values to avoid importing @google/genai which pulls in child_process\nconst MediaResolution = {\n\tMEDIA_RESOLUTION_HIGH: 'MEDIA_RESOLUTION_HIGH',\n\tMEDIA_RESOLUTION_MEDIUM: 'MEDIA_RESOLUTION_MEDIUM',\n\tMEDIA_RESOLUTION_LOW: 'MEDIA_RESOLUTION_LOW',\n} as const;\n\n/**\n * Check if Vertex AI is being used based on SDK config\n * Only checks explicit GOOGLE_GENAI_USE_VERTEXAI flag for clarity\n */\nexport function isUsingVertexAI(): boolean {\n\tconst config = getSdkConfig();\n\tconst env = config.env || {};\n\tconst useVertexAIFlag = env.GOOGLE_GENAI_USE_VERTEXAI;\n\treturn useVertexAIFlag === 'True' || useVertexAIFlag === 'true';\n}\n\n/**\n * Get Google AI model with support for:\n * 1. Vertex AI (via GOOGLE_GENAI_USE_VERTEXAI flag or GOOGLE_CLOUD_PROJECT)\n * 2. API key from SDK config\n * 3. Default Google AI SDK\n *\n * Environment variables (in SDK config first):\n * - GOOGLE_GENAI_USE_VERTEXAI: Set to \"True\" or \"true\" to force Vertex AI\n * - GOOGLE_CLOUD_PROJECT: GCP project ID for Vertex AI\n * - GOOGLE_CLOUD_LOCATION: GCP region (default: us-central1)\n * - GOOGLE_APPLICATION_CREDENTIALS: Path to service account JSON (read by google-auth-library)\n */\nexport function getGoogleModel(modelName: string): LanguageModelV3 {\n\tconst config = getSdkConfig();\n\tconst env = config.env || {};\n\n\tif (isUsingVertexAI()) {\n\t\tconst vertexProject = env.GOOGLE_CLOUD_PROJECT;\n\t\tif (!vertexProject) {\n\t\t\tthrow new Error('GOOGLE_CLOUD_PROJECT is required when using Vertex AI');\n\t\t}\n\t\t// gemini-3-flash-preview is only in global location\n\t\tconst vertexLocation = modelName === 'gemini-3-flash-preview' ? 'global' : env.GOOGLE_CLOUD_LOCATION;\n\t\tif (!vertexLocation) {\n\t\t\tthrow new Error('GOOGLE_CLOUD_LOCATION is required when using Vertex AI');\n\t\t}\n\t\tlogger.debug(`Using Vertex AI provider: model=${modelName}, location=${vertexLocation}`);\n\t\tconst vertexProvider = createVertex({\n\t\t\tproject: vertexProject,\n\t\t\tlocation: vertexLocation,\n\t\t});\n\t\treturn vertexProvider(modelName);\n\t}\n\n\tconst apiKey = env.GOOGLE_API_KEY;\n\n\tif (!apiKey) {\n\t\tthrow new Error(\n\t\t\t'Google API key is missing. Set GOOGLE_API_KEY in SDK config or environment.'\n\t\t);\n\t}\n\n\tlogger.debug(`Using Google AI provider (API key): model=${modelName}`);\n\tconst googleProvider = createGoogleGenerativeAI({ apiKey });\n\treturn googleProvider(modelName);\n}\n\n/**\n * Provider options type for Google/Vertex AI\n * In AI SDK v6, Vertex AI uses 'vertex' key while Google AI uses 'google' key\n */\nexport type GoogleProviderOptionsResult = {\n\tgoogle?: GoogleGenerativeAIProviderOptions;\n\tvertex?: GoogleGenerativeAIProviderOptions;\n};\n\n/**\n * Get provider options based on image count in the request\n * - Exactly 1 image: HIGH resolution for best quality\n * - 0 images (PDFs, text only): No mediaResolution needed\n * - Multiple images: Default resolution (Vertex AI limitation)\n *\n * In AI SDK v6, provider options key must match the provider:\n * - 'vertex' for Vertex AI (createVertex)\n * - 'google' for Google AI (createGoogleGenerativeAI)\n */\nexport function getGoogleProviderOptions(imageCount: number, modelName: string): GoogleProviderOptionsResult {\n\tconst providerOptionsGemini2_5Pro: GoogleGenerativeAIProviderOptions = {\n\t\tthinkingConfig: {\n\t\t\tthinkingBudget: 512,\n\t\t\tincludeThoughts: true,\n\t\t},\n\t};\n\n\tconst providerOptionsGemini3FlashPreview: GoogleGenerativeAIProviderOptions = {\n\t\tthinkingConfig: {\n\t\t\tthinkingLevel: 'minimal',\n\t\t\tincludeThoughts: true,\n\t\t},\n\t\tmediaResolution: MediaResolution.MEDIA_RESOLUTION_HIGH,\n\t};\n\n\tlet providerOptions: GoogleGenerativeAIProviderOptions;\n\tswitch (modelName) {\n\t\tcase 'gemini-3-flash-preview':\n\t\t\tproviderOptions = { ...providerOptionsGemini3FlashPreview };\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tproviderOptions = { ...providerOptionsGemini2_5Pro };\n\t\t\t// Vertex AI only supports HIGH resolution for single images\n\t\t\t// Use HIGH for single image, default (unspecified) for multiple images\n\t\t\tif (imageCount === 1) {\n\t\t\t\tproviderOptions.mediaResolution = MediaResolution.MEDIA_RESOLUTION_HIGH;\n\t\t\t}\n\t}\n\n\t// AI SDK v6: Use 'vertex' key for Vertex AI, 'google' key for Google AI\n\tif (isUsingVertexAI()) {\n\t\treturn { vertex: providerOptions };\n\t}\n\treturn { google: providerOptions };\n}\n","/**\n * LLM Provider Dispatcher\n *\n * Unified interface for getting LLM model instances and provider options.\n * Detects provider from model name prefix and delegates to appropriate provider.\n */\n\nimport type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { getGoogleModel, getGoogleProviderOptions, GoogleProviderOptionsResult } from './google';\nimport { getAnthropicModel, getAnthropicProviderOptions, AnthropicProviderOptionsResult } from './anthropic';\n\n/**\n * Get LLM model instance based on model name\n *\n * Detects provider from model name prefix:\n * - 'claude-*' → Anthropic (e.g., claude-sonnet-4-5, claude-haiku-4-5, claude-opus-4-5)\n * - 'gemini-*' → Google AI / Vertex AI (e.g., gemini-2.5-pro, gemini-3-flash-preview)\n *\n * @param modelName - Full model name (e.g., 'claude-sonnet-4-5', 'gemini-2.5-pro')\n * @returns Language model instance\n * @throws Error if model name doesn't match a supported provider\n */\nexport function getModel(modelName: string): LanguageModelV3 {\n\tif (modelName.startsWith('claude-')) {\n\t\treturn getAnthropicModel(modelName);\n\t}\n\tif (modelName.startsWith('gemini-')) {\n\t\treturn getGoogleModel(modelName);\n\t}\n\tthrow new Error(`Unsupported model: ${modelName}. Use 'claude-*' or 'gemini-*' models.`);\n}\n\n/**\n * Provider options result type\n * Can contain Google/Vertex options or Anthropic options\n */\nexport type ProviderOptionsResult = GoogleProviderOptionsResult | AnthropicProviderOptionsResult;\n\n/**\n * Get provider-specific options for the model\n *\n * @param modelName - Full model name\n * @param imageCount - Number of images in the request (used by Google for mediaResolution)\n * @returns Provider options object to pass to generateText()\n */\nexport function getProviderOptions(modelName: string, imageCount: number): ProviderOptionsResult {\n\tif (modelName.startsWith('claude-')) {\n\t\treturn getAnthropicProviderOptions(modelName);\n\t}\n\t// Default to Google provider options for gemini-* and any other models\n\treturn getGoogleProviderOptions(imageCount, modelName);\n}\n\n// Re-export provider-specific functions for direct access and backward compatibility\nexport { getGoogleModel, getGoogleProviderOptions, isUsingVertexAI, type GoogleProviderOptionsResult } from './google';\nexport { getAnthropicModel, getAnthropicProviderOptions, type AnthropicProviderOptionsResult } from './anthropic';\n","/**\n * Image utilities for screenshot processing\n */\n\nasync function getSharp() {\n\tconst { default: sharp } = await import('sharp');\n\treturn sharp;\n}\n\nconst TILE_SIZE = 768;\n\n/**\n * Default label dimensions for hot map generation\n * Based on typical label size: ~8px per digit + padding\n */\nconst DEFAULT_LABEL_WIDTH = 26;\nconst DEFAULT_LABEL_HEIGHT = 22;\n\nexport interface SliceScreenshotOptions {\n\t/** Resize each patch to 768x768 for token optimization */\n\tresize?: boolean;\n}\n\n/**\n * Slice a screenshot into 3 overlapping square patches (left, middle, right)\n *\n * Creates three height x height square patches that overlap horizontally\n * to cover the full width of the image. This maximizes token usage while\n * ensuring no information is lost.\n *\n * For a 1920x1080 image: creates three 1080x1080 overlapping patches\n * With resize=true: patches are resized to 768x768\n *\n * @param imageBuffer - Screenshot image as Buffer\n * @param options - Slicing options (resize, etc.)\n * @returns Array of 3 image buffers [left, middle, right]\n */\nexport async function sliceScreenshot(imageBuffer: Buffer, options?: SliceScreenshotOptions): Promise<Buffer[]> {\n\tconst sharp = await getSharp();\n\tconst image = sharp(imageBuffer);\n\tconst metadata = await image.metadata();\n\n\tconst width = metadata.width || 0;\n\tconst height = metadata.height || 0;\n\n\tif (width === 0 || height === 0) {\n\t\tthrow new Error('Invalid image dimensions');\n\t}\n\n\t// Patch size is the full height (creates square patches)\n\tconst patchSize = height;\n\n\t// Calculate starting positions for overlapping patches\n\t// Left: start from left edge\n\tconst leftStart = 0;\n\t// Middle: centered\n\tconst middleStart = Math.floor((width - patchSize) / 2);\n\t// Right: aligned to right edge\n\tconst rightStart = Math.max(0, width - patchSize);\n\n\t// Helper to create a patch (extract and optionally resize)\n\tconst createPatch = (left: number, extractWidth: number) => {\n\t\tlet pipeline = sharp(imageBuffer)\n\t\t\t.extract({ left, top: 0, width: extractWidth, height: patchSize });\n\n\t\tif (options?.resize) {\n\t\t\tpipeline = pipeline.resize(TILE_SIZE, TILE_SIZE);\n\t\t}\n\n\t\treturn pipeline.png().toBuffer();\n\t};\n\n\t// Crop the three overlapping regions\n\tconst [leftCrop, middleCrop, rightCrop] = await Promise.all([\n\t\tcreatePatch(leftStart, Math.min(patchSize, width)),\n\t\tcreatePatch(middleStart, Math.min(patchSize, width - middleStart)),\n\t\tcreatePatch(rightStart, Math.min(patchSize, width - rightStart)),\n\t]);\n\n\treturn [leftCrop, middleCrop, rightCrop];\n}\n\n/**\n * Slice a base64 screenshot into 3 square regions\n *\n * @param base64Image - Screenshot as base64 string (without data URL prefix)\n * @returns Array of 3 base64 strings [left, middle, right]\n */\nexport async function sliceScreenshotBase64(base64Image: string): Promise<string[]> {\n\tconst imageBuffer = Buffer.from(base64Image, 'base64');\n\tconst slices = await sliceScreenshot(imageBuffer);\n\treturn slices.map(slice => slice.toString('base64'));\n}\n\n/**\n * Raw pixel data from an image\n */\nexport interface RawPixelData {\n\t/** Raw RGBA pixel data */\n\tdata: Uint8Array;\n\t/** Image width in pixels */\n\twidth: number;\n\t/** Image height in pixels */\n\theight: number;\n}\n\n/**\n * Options for hot map generation\n */\nexport interface HotMapOptions {\n\t/** Width of the label in pixels (default: 24) */\n\tlabelWidth?: number;\n\t/** Height of the label in pixels (default: 16) */\n\tlabelHeight?: number;\n}\n\n/**\n * Extract raw RGBA pixel data from a PNG buffer using sharp\n *\n * @param pngBuffer - PNG image as Buffer\n * @returns Raw RGBA pixel data with dimensions\n */\nexport async function pngToRawPixels(pngBuffer: Buffer): Promise<RawPixelData> {\n\tconst sharp = await getSharp();\n\tconst image = sharp(pngBuffer);\n\tconst metadata = await image.metadata();\n\n\tconst width = metadata.width || 0;\n\tconst height = metadata.height || 0;\n\n\tif (width === 0 || height === 0) {\n\t\tthrow new Error('Invalid image dimensions');\n\t}\n\n\t// Extract raw RGBA pixel data\n\tconst { data } = await image.ensureAlpha().raw().toBuffer({ resolveWithObject: true });\n\n\treturn {\n\t\tdata: new Uint8Array(data),\n\t\twidth,\n\t\theight,\n\t};\n}\n\n/**\n * Default tolerance for color comparison in isPatchUniform\n * Allows for slight color variations (e.g., anti-aliasing, compression artifacts)\n */\nconst DEFAULT_COLOR_TOLERANCE = 32;\n\n/**\n * Check if a patch of pixels is uniform (all similar color within tolerance)\n *\n * @param data - Raw RGBA pixel data\n * @param width - Image width\n * @param height - Image height\n * @param startX - Starting X coordinate of patch\n * @param startY - Starting Y coordinate of patch\n * @param patchWidth - Width of patch to check\n * @param patchHeight - Height of patch to check\n * @param tolerance - Maximum allowed difference per RGB channel (default: 32)\n * @returns true if all pixels in patch are within tolerance of reference color\n */\nfunction isPatchUniform(\n\tdata: Uint8Array,\n\twidth: number,\n\theight: number,\n\tstartX: number,\n\tstartY: number,\n\tpatchWidth: number,\n\tpatchHeight: number,\n\ttolerance: number = DEFAULT_COLOR_TOLERANCE\n): boolean {\n\t// Handle edge cases - treat out-of-bounds as non-uniform\n\tif (startX + patchWidth > width || startY + patchHeight > height) {\n\t\treturn false;\n\t}\n\tif (startX < 0 || startY < 0) {\n\t\treturn false;\n\t}\n\n\t// Get reference color from first pixel (RGBA format, 4 bytes per pixel)\n\tconst refIdx = (startY * width + startX) * 4;\n\tconst refR = data[refIdx];\n\tconst refG = data[refIdx + 1];\n\tconst refB = data[refIdx + 2];\n\n\t// Check all pixels in patch\n\tfor (let dy = 0; dy < patchHeight; dy++) {\n\t\tfor (let dx = 0; dx < patchWidth; dx++) {\n\t\t\tconst idx = ((startY + dy) * width + (startX + dx)) * 4;\n\t\t\tconst diffR = Math.abs(data[idx] - refR);\n\t\t\tconst diffG = Math.abs(data[idx + 1] - refG);\n\t\t\tconst diffB = Math.abs(data[idx + 2] - refB);\n\t\t\tif (diffR > tolerance || diffG > tolerance || diffB > tolerance) {\n\t\t\t\treturn false; // Not uniform within tolerance\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true; // All pixels within tolerance\n}\n\n/**\n * Generate a hot map indicating valid label positions\n *\n * Creates a 2D boolean array where each cell indicates whether a label\n * with its top-left corner at that position would be over a uniform color patch.\n * This is used for finding good positions to place numbered labels.\n *\n * Algorithm: Convolution with a kernel the size of the expected label.\n * If all pixels in the kernel-sized patch are the same color, that position is \"hot\" (true).\n *\n * @param pixelData - Raw RGBA pixel data from screenshot\n * @param options - Label dimensions for kernel size\n * @returns 2D boolean array where hotMap[y][x] = true means safe to place label at (x, y)\n */\nexport function generateHotMap(pixelData: RawPixelData, options?: HotMapOptions): boolean[][] {\n\tconst { data, width, height } = pixelData;\n\tconst labelWidth = options?.labelWidth ?? DEFAULT_LABEL_WIDTH;\n\tconst labelHeight = options?.labelHeight ?? DEFAULT_LABEL_HEIGHT;\n\n\tconst hotMap: boolean[][] = [];\n\n\t// For each pixel position\n\tfor (let y = 0; y < height; y++) {\n\t\thotMap[y] = [];\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\t// Check if kernel-sized patch starting at (x, y) is uniform color\n\t\t\thotMap[y][x] = isPatchUniform(data, width, height, x, y, labelWidth, labelHeight);\n\t\t}\n\t}\n\n\treturn hotMap;\n}\n\n/**\n * Generate hot map from a PNG buffer (convenience function)\n *\n * @param pngBuffer - PNG image as Buffer\n * @param options - Label dimensions for kernel size\n * @returns 2D boolean array for label placement\n */\nexport async function generateHotMapFromPng(pngBuffer: Buffer, options?: HotMapOptions): Promise<boolean[][]> {\n\tconst pixelData = await pngToRawPixels(pngBuffer);\n\treturn generateHotMap(pixelData, options);\n}\n\n/**\n * Grayscale image data\n */\nexport interface GrayscaleImageData {\n\t/** 2D array of grayscale pixel values (0-255) */\n\tpixels: number[][];\n\t/** Image width in pixels */\n\twidth: number;\n\t/** Image height in pixels */\n\theight: number;\n}\n\n/**\n * Generate a grayscale image from a PNG buffer\n *\n * This is used for dynamic label placement with convolution at placement time.\n * Instead of pre-computing a boolean hot map with fixed kernel size, we return\n * the raw grayscale image and perform uniformity checks with dynamic label sizes.\n *\n * @param pngBuffer - PNG image as Buffer\n * @returns Grayscale image as 2D number array where pixels[y][x] is intensity 0-255\n */\nexport async function generateGrayscaleFromPng(pngBuffer: Buffer): Promise<GrayscaleImageData> {\n\tconst sharp = await getSharp();\n\tconst image = sharp(pngBuffer);\n\tconst metadata = await image.metadata();\n\n\tconst width = metadata.width || 0;\n\tconst height = metadata.height || 0;\n\n\tif (width === 0 || height === 0) {\n\t\tthrow new Error('Invalid image dimensions');\n\t}\n\n\t// Convert to grayscale and extract raw pixel data\n\tconst { data } = await image.grayscale().raw().toBuffer({ resolveWithObject: true });\n\n\t// Convert flat Uint8Array to 2D array\n\tconst pixels: number[][] = [];\n\tfor (let y = 0; y < height; y++) {\n\t\tpixels[y] = [];\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\tpixels[y][x] = data[y * width + x];\n\t\t}\n\t}\n\n\treturn { pixels, width, height };\n}\n","/**\n * Shared AXTree constants and logic\n *\n * This module contains the authoritative definitions used by both:\n * - service.ts (production Playwright-based detection)\n * - Chrome extension (debugging tool)\n *\n * IMPORTANT: Any changes here affect both production and debug tooling.\n */\n\n/**\n * Interactive ARIA roles that indicate an element can receive user interaction.\n * Elements with these roles are considered interactive by the Accessibility Tree.\n */\nexport const INTERACTIVE_ROLES = new Set([\n\t'button',\n\t'link',\n\t'textbox',\n\t'checkbox',\n\t'radio',\n\t'combobox',\n\t'listbox',\n\t'menuitem',\n\t'menuitemcheckbox',\n\t'menuitemradio',\n\t'option',\n\t'tab',\n\t'switch',\n\t'slider',\n\t'spinbutton',\n\t'searchbox',\n\t'scrollbar',\n\t'treeitem',\n\t'gridcell',\n]);\n\n/**\n * Event types that indicate user interaction intent.\n * Used for CDP DOMDebugger.getEventListeners detection.\n */\nexport const INTERACTION_EVENT_TYPES = new Set([\n\t'click',\n\t'mousedown',\n\t'mouseup',\n\t'dblclick',\n\t'pointerdown',\n\t'pointerup',\n\t'touchstart',\n\t'touchend',\n]);\n\n/**\n * CSS selectors for elements likely to have JS event handlers.\n * Used to limit the scope of event listener detection for performance.\n */\nexport const EVENT_LISTENER_CANDIDATE_SELECTORS = [\n\t// Inline handlers (always interactive)\n\t'[onclick]',\n\t'[onmousedown]',\n\t'[ontouchstart]',\n\t// Common JS-handler targets\n\t'div',\n\t'span',\n\t'li',\n\t'tr',\n\t'td',\n\t// Likely interactive patterns\n\t'[role]',\n\t'[class*=\"btn\"]',\n\t'[class*=\"button\"]',\n\t'[class*=\"click\"]',\n\t'[data-action]',\n\t'[data-click]',\n];\n\n/**\n * Default limit for event listener detection.\n * Higher values find more elements but take longer.\n */\nexport const DEFAULT_EVENT_LISTENER_LIMIT = 500;\n\n/**\n * Check if an ARIA role is considered interactive\n */\nexport function isInteractiveRole(role: string | undefined | null): boolean {\n\treturn role ? INTERACTIVE_ROLES.has(role) : false;\n}\n\n/**\n * Check if an event type indicates user interaction\n */\nexport function isInteractionEventType(eventType: string): boolean {\n\treturn INTERACTION_EVENT_TYPES.has(eventType);\n}\n\n/**\n * Filter event listeners to only interaction types\n */\nexport function filterInteractionListeners<T extends { type: string }>(\n\tlisteners: T[]\n): T[] {\n\treturn listeners.filter((l) => INTERACTION_EVENT_TYPES.has(l.type));\n}\n","/**\n * Text utility functions for DOM processing\n */\n\n/**\n * Cap text length with ellipsis\n */\nexport function capTextLength(text: string, maxLength: number): string {\n\tif (text.length > maxLength) {\n\t\treturn text.slice(0, maxLength) + '...';\n\t}\n\treturn text;\n}\n","/**\n * DOM module types\n * Ported from browser-use Python implementation\n */\n\n/**\n * Coordinates for an element\n */\nexport interface Coordinates {\n\tx: number;\n\ty: number;\n}\n\n/**\n * Set of coordinate points for an element\n */\nexport interface CoordinateSet {\n\ttopLeft: Coordinates;\n\ttopRight: Coordinates;\n\tbottomLeft: Coordinates;\n\tbottomRight: Coordinates;\n\tcenter: Coordinates;\n\twidth: number;\n\theight: number;\n}\n\n/**\n * Viewport information\n */\nexport interface ViewportInfo {\n\tscrollX?: number;\n\tscrollY?: number;\n\twidth: number;\n\theight: number;\n}\n\n/**\n * Base DOM node interface\n */\nexport interface DOMBaseNode {\n\tisVisible: boolean;\n\tparent: DOMElementNode | null;\n}\n\n/**\n * DOM text node\n */\nexport interface DOMTextNode extends DOMBaseNode {\n\ttype: 'TEXT_NODE';\n\ttext: string;\n\n\thasParentWithHighlightIndex(): boolean;\n\tisParentInViewport(): boolean;\n\tisParentTopElement(): boolean;\n}\n\n/**\n * Configuration for clickableElementsToString\n */\nexport interface StringifyConfig {\n\tincludeAttributes?: string[];\n\tincludeClassesWithRename?: Record<string, string>;\n}\n\n/**\n * DOM element node\n */\nexport interface DOMElementNode extends DOMBaseNode {\n\ttagName: string;\n\txpath: string;\n\tattributes: Record<string, string>;\n\tchildren: DOMBaseNode[];\n\tisInteractive: boolean;\n\tisScrollable: boolean;\n\tmarkAsClickable: boolean;\n\tisTopElement: boolean;\n\tisInViewport: boolean;\n\tshadowRoot: boolean;\n\thighlightIndex: number | null;\n\tviewportCoordinates: CoordinateSet | null;\n\tpageCoordinates: CoordinateSet | null;\n\tviewportInfo: ViewportInfo | null;\n\n\t/** State injected by browser context - indicates if element is new */\n\tisNew: boolean | null;\n\n\t/**\n\t * Get all text content until the next clickable element\n\t */\n\tgetAllTextTillNextClickableElement(maxDepth?: number): string;\n\n\t/**\n\t * Convert clickable elements to string format for LLM\n\t */\n\tclickableElementsToString(config?: StringifyConfig): string;\n}\n\n/**\n * Selector map: highlightIndex -> DOMElementNode\n */\nexport type SelectorMap = Map<number, DOMElementNode>;\n\n/**\n * DOM state containing element tree and selector map\n */\nexport interface DOMState {\n\telementTree: DOMElementNode;\n\tselectorMap: SelectorMap;\n}\n\n/**\n * Action intent types for filtering DOM elements.\n * - 'click': buttons, links, elements with click handlers\n * - 'input': text inputs, textareas, contenteditable elements\n * - 'scroll': scrollable containers\n * - 'all': all interactive elements (default)\n */\nexport type ActionIntent = 'click' | 'input' | 'scroll' | 'all';\n\n/**\n * Options for DOM extraction\n */\nexport interface DOMExtractionOptions {\n\thighlightElements?: boolean;\n\tfocusElement?: number;\n\tviewportExpansion?: number;\n\tinteractiveClassNames?: string[];\n\talwaysHighlightFileInput?: boolean;\n\tuseCleanScreenshot?: boolean;\n\tuseSlicedScreenshots?: boolean;\n\tresizeSlicedScreenshots?: boolean;\n\t/**\n\t * Use Chrome Accessibility Tree for element detection (experimental).\n\t * When enabled, uses CDP Accessibility API to get authoritative\n\t * interactive elements, then enriches with visual data from DOM.\n\t */\n\tuseAccessibilityTree?: boolean;\n\t/**\n\t * Maximum number of elements to check for event listeners when\n\t * useAccessibilityTree is enabled. Higher values find more elements\n\t * but take longer. Default: 500\n\t */\n\teventListenerLimit?: number;\n\t/**\n\t * IoU (Intersection over Union) threshold for considering two element\n\t * rects as \"the same\". Used to deduplicate parent/child elements with\n\t * nearly identical bounds. Range: 0-1. Default: 0.85\n\t */\n\tsameRectIoUThreshold?: number;\n\t/**\n\t * Action intent for filtering elements. Only elements relevant to this\n\t * action type are highlighted and included. Falls back to 'all' if no\n\t * elements match the specified intent.\n\t */\n\tactionIntent?: ActionIntent;\n}\n\n/**\n * Raw JavaScript evaluation result from page.evaluate\n */\nexport interface DOMEvalResult {\n\tmap: Record<string, any>;\n\trootId: string;\n\tperfMetrics?: any;\n}\n\n/**\n * Hashed DOM element for comparison\n */\nexport interface HashedDomElement {\n\tbranchPathHash: string;\n\tattributesHash: string;\n\txpathHash: string;\n}\n\n/**\n * DOM history element for tracking state changes\n */\nexport interface DOMHistoryElement {\n\ttagName: string;\n\txpath: string;\n\thighlightIndex: number | null;\n\tentireParentBranchPath: string[];\n\tattributes: Record<string, string>;\n\tshadowRoot: boolean;\n\tcssSelector: string | null;\n\tpageCoordinates: CoordinateSet | null;\n\tviewportCoordinates: CoordinateSet | null;\n\tviewportInfo: ViewportInfo | null;\n}\n\n/**\n * Default attributes to include in string representation\n */\nexport const DEFAULT_INCLUDE_ATTRIBUTES = [\n\t'title',\n\t'type',\n\t'checked',\n\t'name',\n\t'role',\n\t'value',\n\t'placeholder',\n\t'data-date-format',\n\t'alt',\n\t'aria-label',\n\t'aria-expanded',\n\t'data-state',\n\t'aria-checked',\n\t'data-id',\n\t'data-testid',\n\t'data-test-id',\n\t'data-handlepos',\n\t'data-item-id',\n];\n\n/**\n * Default class name patterns to include with rename rules\n */\nexport const DEFAULT_INCLUDE_CLASSES_WITH_RENAME: Record<string, string> = {\n\t// Regex pattern -> replacement (use \\1, \\2 etc for capture groups)\n\t'react-flow__(\\\\S+)': '$1', // Captures 'textBlock', 'staticImageBlock', etc.\n};\n","/**\n * DOM node implementations\n * Ported from browser-use views.py\n */\n\nimport {\n\tDOMBaseNode,\n\tDOMTextNode,\n\tDOMElementNode,\n\tCoordinateSet,\n\tViewportInfo,\n\tStringifyConfig,\n\tDEFAULT_INCLUDE_ATTRIBUTES,\n\tDEFAULT_INCLUDE_CLASSES_WITH_RENAME,\n} from './types';\nimport { capTextLength } from './utils';\n\n/**\n * Base DOM node implementation\n */\nabstract class DOMBaseNodeImpl implements DOMBaseNode {\n\tconstructor(\n\t\tpublic isVisible: boolean,\n\t\tpublic parent: DOMElementNode | null = null\n\t) {}\n}\n\n/**\n * DOM text node implementation\n */\nexport class DOMTextNodeImpl extends DOMBaseNodeImpl implements DOMTextNode {\n\treadonly type = 'TEXT_NODE' as const;\n\n\tconstructor(\n\t\tpublic text: string,\n\t\tisVisible: boolean,\n\t\tparent: DOMElementNode | null = null\n\t) {\n\t\tsuper(isVisible, parent);\n\t}\n\n\thasParentWithHighlightIndex(): boolean {\n\t\tlet current = this.parent;\n\t\twhile (current !== null) {\n\t\t\tif (current.highlightIndex !== null) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tcurrent = current.parent;\n\t\t}\n\t\treturn false;\n\t}\n\n\tisParentInViewport(): boolean {\n\t\tif (this.parent === null) {\n\t\t\treturn false;\n\t\t}\n\t\treturn this.parent.isInViewport;\n\t}\n\n\tisParentTopElement(): boolean {\n\t\tif (this.parent === null) {\n\t\t\treturn false;\n\t\t}\n\t\treturn this.parent.isTopElement;\n\t}\n}\n\n/**\n * DOM element node implementation\n */\nexport class DOMElementNodeImpl extends DOMBaseNodeImpl implements DOMElementNode {\n\tconstructor(\n\t\tpublic tagName: string,\n\t\tpublic xpath: string,\n\t\tpublic attributes: Record<string, string>,\n\t\tpublic children: DOMBaseNode[],\n\t\tisVisible: boolean,\n\t\tpublic isInteractive: boolean = false,\n\t\tpublic isScrollable: boolean = false,\n\t\tpublic markAsClickable: boolean = false,\n\t\tpublic isTopElement: boolean = false,\n\t\tpublic isInViewport: boolean = false,\n\t\tpublic shadowRoot: boolean = false,\n\t\tpublic highlightIndex: number | null = null,\n\t\tpublic viewportCoordinates: CoordinateSet | null = null,\n\t\tpublic pageCoordinates: CoordinateSet | null = null,\n\t\tpublic viewportInfo: ViewportInfo | null = null,\n\t\tparent: DOMElementNode | null = null\n\t) {\n\t\tsuper(isVisible, parent);\n\t}\n\n\tpublic isNew: boolean | null = null;\n\n\tgetAllTextTillNextClickableElement(maxDepth: number = -1): string {\n\t\tconst textParts: string[] = [];\n\n\t\tconst collectText = (node: DOMBaseNode, currentDepth: number): void => {\n\t\t\tif (maxDepth !== -1 && currentDepth > maxDepth) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Skip this branch if we hit a highlighted element (except for current node)\n\t\t\tif (node instanceof DOMElementNodeImpl && node !== this && node.highlightIndex !== null) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (node instanceof DOMTextNodeImpl) {\n\t\t\t\ttextParts.push(node.text);\n\t\t\t} else if (node instanceof DOMElementNodeImpl) {\n\t\t\t\tfor (const child of node.children) {\n\t\t\t\t\tcollectText(child, currentDepth + 1);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tcollectText(this, 0);\n\t\treturn textParts.join('\\n').trim();\n\t}\n\n\tclickableElementsToString(config?: StringifyConfig): string {\n\t\tconst includeAttributes = config?.includeAttributes ?? DEFAULT_INCLUDE_ATTRIBUTES;\n\t\tconst includeClassesWithRename = config?.includeClassesWithRename ?? DEFAULT_INCLUDE_CLASSES_WITH_RENAME;\n\n\t\tconst formattedText: string[] = [];\n\n\t\tconst processNode = (node: DOMBaseNode, depth: number): void => {\n\t\t\tlet nextDepth = depth;\n\t\t\tconst depthStr = '\\t'.repeat(depth);\n\n\t\t\tif (node instanceof DOMElementNodeImpl) {\n\t\t\t\t// Add element with highlight_index\n\t\t\t\tif (node.highlightIndex !== null) {\n\t\t\t\t\tnextDepth += 1;\n\n\t\t\t\t\tconst text = node.isScrollable ? '' : node.getAllTextTillNextClickableElement();\n\t\t\t\t\tlet attributesHtmlStr: string | null = null;\n\n\t\t\t\t\tif (includeAttributes.length > 0) {\n\t\t\t\t\t\tconst attributesToInclude: Record<string, string> = {};\n\n\t\t\t\t\t\tfor (const key of Object.keys(node.attributes)) {\n\t\t\t\t\t\t\tif (includeAttributes.includes(key)) {\n\t\t\t\t\t\t\t\tconst value = node.attributes[key].trim();\n\t\t\t\t\t\t\t\tif (value !== '') {\n\t\t\t\t\t\t\t\t\tattributesToInclude[key] = value;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Remove duplicate attribute values\n\t\t\t\t\t\tconst orderedKeys = includeAttributes.filter((key) => key in attributesToInclude);\n\n\t\t\t\t\t\tif (orderedKeys.length > 1) {\n\t\t\t\t\t\t\tconst keysToRemove = new Set<string>();\n\t\t\t\t\t\t\tconst seenValues: Record<string, string> = {};\n\n\t\t\t\t\t\t\tfor (const key of orderedKeys) {\n\t\t\t\t\t\t\t\tconst value = attributesToInclude[key];\n\t\t\t\t\t\t\t\tif (value.length > 5) {\n\t\t\t\t\t\t\t\t\tif (value in seenValues) {\n\t\t\t\t\t\t\t\t\t\tkeysToRemove.add(key);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tseenValues[value] = key;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tfor (const key of keysToRemove) {\n\t\t\t\t\t\t\t\tdelete attributesToInclude[key];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// If tag == role attribute, don't include it\n\t\t\t\t\t\tif (node.tagName === attributesToInclude['role']) {\n\t\t\t\t\t\t\tdelete attributesToInclude['role'];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Remove attributes that duplicate the node's text content\n\t\t\t\t\t\tconst attrsToRemoveIfTextMatches = ['aria-label', 'placeholder', 'title'];\n\t\t\t\t\t\tfor (const attr of attrsToRemoveIfTextMatches) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tattributesToInclude[attr] &&\n\t\t\t\t\t\t\t\tattributesToInclude[attr].trim().toLowerCase() === text.trim().toLowerCase()\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tdelete attributesToInclude[attr];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (Object.keys(attributesToInclude).length > 0) {\n\t\t\t\t\t\t\tattributesHtmlStr = Object.entries(attributesToInclude)\n\t\t\t\t\t\t\t\t.map(([key, value]) => `${key}=${capTextLength(value, 200)}`)\n\t\t\t\t\t\t\t\t.join(' ');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Build the line\n\t\t\t\t\tconst highlightIndicator = node.isNew ? `*[${node.highlightIndex}]` : `[${node.highlightIndex}]`;\n\n\t\t\t\t\t// Handle class filtering with regex patterns\n\t\t\t\t\tconst filteredClasses: string[] = [];\n\t\t\t\t\tif (Object.keys(includeClassesWithRename).length > 0 && node.attributes['class']) {\n\t\t\t\t\t\tconst classString = node.attributes['class'];\n\t\t\t\t\t\tconst classes = classString.split(/\\s+/);\n\n\t\t\t\t\t\tfor (const cssClass of classes) {\n\t\t\t\t\t\t\tfor (const [pattern, replacement] of Object.entries(includeClassesWithRename)) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst regex = new RegExp(`^${pattern}$`);\n\t\t\t\t\t\t\t\t\tconst match = cssClass.match(regex);\n\t\t\t\t\t\t\t\t\tif (match) {\n\t\t\t\t\t\t\t\t\t\tconst renamed = cssClass.replace(regex, replacement);\n\t\t\t\t\t\t\t\t\t\tif (renamed) {\n\t\t\t\t\t\t\t\t\t\t\tfilteredClasses.push(renamed);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\t\t\t// Invalid regex pattern, skip it\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst scrollableIndicator = node.isScrollable ? ' (SCROLLABLE)' : '';\n\t\t\t\t\tconst clickableIndicator = node.markAsClickable ? ' (CLICKABLE)' : '';\n\t\t\t\t\tlet line = `${depthStr}${highlightIndicator}${scrollableIndicator}${clickableIndicator}<${node.tagName}`;\n\n\t\t\t\t\tif (filteredClasses.length > 0) {\n\t\t\t\t\t\tline += ` ${filteredClasses.join(' ')}`;\n\t\t\t\t\t}\n\t\t\t\t\tif (attributesHtmlStr) {\n\t\t\t\t\t\tline += ` ${attributesHtmlStr}`;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (text) {\n\t\t\t\t\t\tconst trimmedText = text.trim();\n\t\t\t\t\t\tif (!attributesHtmlStr) {\n\t\t\t\t\t\t\tline += ' ';\n\t\t\t\t\t\t}\n\t\t\t\t\t\tline += `>${trimmedText}`;\n\t\t\t\t\t} else if (!attributesHtmlStr) {\n\t\t\t\t\t\tline += ' ';\n\t\t\t\t\t}\n\n\t\t\t\t\tline += ' />';\n\t\t\t\t\tformattedText.push(line);\n\t\t\t\t} else {\n\t\t\t\t\tconst semanticAttributes = ['data-testid', 'data-test-id'];\n\t\t\t\t\tconst filteredAttributes = semanticAttributes.filter(attr => node.attributes[attr]);\n\t\t\t\t\tif (filteredAttributes.length > 0) {\n\t\t\t\t\t\tnextDepth += 1;\n\t\t\t\t\t\tformattedText.push(`${depthStr}<${node.tagName} ${filteredAttributes.map(attr => `${attr}=\"${node.attributes[attr]}\"`).join(' ')} />`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Process children regardless\n\t\t\t\tfor (const child of node.children) {\n\t\t\t\t\tprocessNode(child, nextDepth);\n\t\t\t\t}\n\t\t\t} else if (node instanceof DOMTextNodeImpl) {\n\t\t\t\t// Add text only if it doesn't have a highlighted parent\n\t\t\t\tif (node.hasParentWithHighlightIndex()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (node.parent && node.parent.isVisible && node.parent.isTopElement) {\n\t\t\t\t\tformattedText.push(`${depthStr}${node.text}`);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tprocessNode(this, 0);\n\t\treturn formattedText.join('\\n');\n\t}\n}\n","(\n args = {\n doHighlightElements: true,\n focusHighlightIndex: -1,\n viewportExpansion: 0,\n debugMode: false,\n interactiveClassNames: [],\n alwaysHighlightFileInput: false,\n }\n) => {\n const SEMANTIC_ATTRIBUTES = ['data-testid', 'data-test-id'];\n const EVENT_LISTENER_MAPPING = {\n 'onclick': 'click',\n 'onmousedown': 'mousedown',\n 'onmouseup': 'mouseup',\n 'ondblclick': 'dblclick',\n 'onmouseenter': 'mouseenter',\n 'onmouseleave': 'mouseleave',\n 'onmousemove': 'mousemove',\n 'onmouseout': 'mouseout',\n 'onmouseover': 'mouseover',\n 'onmouseup': 'mouseup',\n 'onmousewheel': 'mousewheel',\n 'onscroll': 'scroll',\n 'onselect': 'select',\n 'onchange': 'change',\n 'onfocus': 'focus',\n 'onblur': 'blur',\n 'onkeydown': 'keydown',\n 'onkeyup': 'keyup',\n 'onkeypress': 'keypress',\n 'oninput': 'input',\n }\n\n const INTERACTION_EVENTS = ['click', 'mousedown', 'mouseup', 'dblclick', 'input', 'mouseenter', 'mouseleave'];\n\n const {\n doHighlightElements,\n focusHighlightIndex,\n viewportExpansion,\n debugMode,\n interactiveClassNames,\n alwaysHighlightFileInput,\n } = args;\n\n const buttonClassNames = ['button', 'dropdown-toggle'];\n const heuristicClassPattern = /\\b(btn|const clickable|menu|item|entry|link)\\b/i;\n const containerSelectors = 'button,a,[role=\"button\"],.menu,.dropdown,.list,.toolbar';\n\n let highlightIndex = 0; // Reset highlight index\n\n /**\n * Helper function to check if element has any of the specified class names.\n *\n * @param {HTMLElement} element - The element to check.\n * @param {string[]} classNames - Array of class names to check for.\n * @returns {boolean} Whether the element has any of the specified class names.\n */\n function hasAnyClassName(element, classNames) {\n if (!element.classList || !classNames || classNames.length === 0) return false;\n return classNames.some(className => element.classList.contains(className));\n }\n\n // Add caching mechanisms at the top level\n const DOM_CACHE = {\n boundingRects: new WeakMap(),\n clientRects: new WeakMap(),\n computedStyles: new WeakMap(),\n nodeEventListeners: new WeakMap(),\n clearCache: () => {\n DOM_CACHE.boundingRects = new WeakMap();\n DOM_CACHE.clientRects = new WeakMap();\n DOM_CACHE.computedStyles = new WeakMap();\n DOM_CACHE.nodeEventListeners = new WeakMap();\n }\n };\n\n /**\n * Gets the cached bounding rect for an element.\n *\n * @param {HTMLElement} element - The element to get the bounding rect for.\n * @returns {DOMRect | null} The cached bounding rect, or null if the element is not found.\n */\n function getCachedBoundingRect(element) {\n if (!element) return null;\n\n if (DOM_CACHE.boundingRects.has(element)) {\n return DOM_CACHE.boundingRects.get(element);\n }\n\n const rect = element.getBoundingClientRect();\n\n if (rect) {\n DOM_CACHE.boundingRects.set(element, rect);\n }\n return rect;\n }\n\n /**\n * Gets the cached computed style for an element.\n *\n * @param {HTMLElement} element - The element to get the computed style for.\n * @returns {CSSStyleDeclaration | null} The cached computed style, or null if the element is not found.\n */\n function getCachedComputedStyle(element) {\n if (!element) return null;\n\n if (DOM_CACHE.computedStyles.has(element)) {\n return DOM_CACHE.computedStyles.get(element);\n }\n\n const style = window.getComputedStyle(element);\n\n if (style) {\n DOM_CACHE.computedStyles.set(element, style);\n }\n return style;\n }\n\n /**\n * Gets the cached client rects for an element.\n *\n * @param {HTMLElement} element - The element to get the client rects for.\n * @returns {DOMRectList | null} The cached client rects, or null if the element is not found.\n */\n function getCachedClientRects(element) {\n if (!element) return null;\n\n if (DOM_CACHE.clientRects.has(element)) {\n return DOM_CACHE.clientRects.get(element);\n }\n\n const rects = element.getClientRects();\n\n if (rects) {\n DOM_CACHE.clientRects.set(element, rects);\n }\n return rects;\n }\n\n /**\n * Gets the event listeners for a node.\n *\n * @param {HTMLElement} element - The element to get the event listeners for.\n * @returns {string[]} The event listeners for the element.\n */\n function getNodeEventListeners(element) {\n const set = new Set();\n try {\n if (typeof getEventListeners === 'function') {\n const listeners = getEventListeners(element);\n for (const eventType in listeners) {\n if (listeners[eventType] && listeners[eventType].length > 0) {\n set.add(eventType);\n }\n }\n }\n\n const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;\n if (typeof getEventListenersForNode === 'function') {\n const listeners = getEventListenersForNode(element);\n for (const listener of listeners) {\n if (listener.type) {\n set.add(listener.type);\n }\n }\n }\n\n for (const attr in EVENT_LISTENER_MAPPING) {\n if (element.hasAttribute(attr) || typeof element[attr] === 'function') {\n set.add(EVENT_LISTENER_MAPPING[attr]);\n }\n }\n } catch (error) {\n }\n\n const listenedEvents = Array.from(set);\n return listenedEvents;\n }\n\n function getCachedNodeEventListeners(element) {\n if (!element) return null;\n if (DOM_CACHE.nodeEventListeners.has(element)) {\n return DOM_CACHE.nodeEventListeners.get(element);\n }\n const listenedEvents = getNodeEventListeners(element);\n if (listenedEvents) {\n DOM_CACHE.nodeEventListeners.set(element, listenedEvents);\n }\n return listenedEvents;\n }\n\n /**\n * Hash map of DOM nodes indexed by their highlight index.\n *\n * @type {Object<string, any>}\n */\n const DOM_HASH_MAP = {};\n\n const ID = { current: 0 };\n\n const HIGHLIGHT_CONTAINER_ID = \"playwright-highlight-container\";\n\n // Add a WeakMap cache for XPath strings\n const xpathCache = new WeakMap();\n\n const existingLabelBoundingBoxes = [];\n\n /**\n * Highlights an element in the DOM and returns the index of the next element.\n *\n * @param {HTMLElement} element - The element to highlight.\n * @param {number} index - The index of the element.\n * @param {HTMLElement | null} parentIframe - The parent iframe node.\n * @returns {number} The index of the next element.\n */\n function highlightElement(element, index, parentIframe = null) {\n if (!element) return index;\n\n const overlays = [];\n /**\n * @type {HTMLElement | null}\n */\n let label = null;\n let labelWidth = 20;\n let labelHeight = 16;\n let cleanupFn = null;\n\n try {\n // Create or get highlight container\n let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);\n if (!container) {\n container = document.createElement(\"div\");\n container.id = HIGHLIGHT_CONTAINER_ID;\n container.style.position = \"fixed\";\n container.style.pointerEvents = \"none\";\n container.style.top = \"0\";\n container.style.left = \"0\";\n container.style.width = \"100%\";\n container.style.height = \"100%\";\n // Use the maximum valid value in zIndex to ensure the element is not blocked by overlapping elements.\n container.style.zIndex = \"2147483647\";\n container.style.backgroundColor = 'transparent';\n document.body.appendChild(container);\n }\n\n // Get element client rects\n let rects = element.getClientRects(); // Use getClientRects()\n\n if (!rects || rects.length === 0) return index; // Exit if no rects\n\n // If element is inside an iframe, we need to transform the rects to main document coordinates\n if (parentIframe) {\n const transformedRects = [];\n const iframeRect = parentIframe.getBoundingClientRect();\n\n // Get iframe's content area offset and CSS transforms\n const iframeStyle = window.getComputedStyle(parentIframe);\n const borderLeft = parseFloat(iframeStyle.borderLeftWidth) || 0;\n const borderTop = parseFloat(iframeStyle.borderTopWidth) || 0;\n const paddingLeft = parseFloat(iframeStyle.paddingLeft) || 0;\n const paddingTop = parseFloat(iframeStyle.paddingTop) || 0;\n\n const contentOffsetX = borderLeft + paddingLeft;\n const contentOffsetY = borderTop + paddingTop;\n\n // Extract scale factor from CSS transform\n let scaleX = 1, scaleY = 1;\n const transform = iframeStyle.transform;\n if (transform && transform !== 'none') {\n // Parse scale from transform matrix or scale() function\n const scaleMatch = transform.match(/scale\\(([^)]+)\\)/);\n if (scaleMatch) {\n const scaleValues = scaleMatch[1].split(',').map(v => parseFloat(v.trim()));\n scaleX = scaleValues[0] || 1;\n scaleY = scaleValues[1] || scaleX; // If only one value, use it for both X and Y\n } else {\n // Try to parse matrix() transform\n const matrixMatch = transform.match(/matrix\\(([^)]+)\\)/);\n if (matrixMatch) {\n const values = matrixMatch[1].split(',').map(v => parseFloat(v.trim()));\n if (values.length >= 6) {\n scaleX = values[0]; // a value in matrix(a, b, c, d, e, f)\n scaleY = values[3]; // d value in matrix(a, b, c, d, e, f)\n }\n }\n }\n }\n\n // Get iframe scroll position\n let scrollLeft = 0, scrollTop = 0;\n try {\n const iframeDoc = parentIframe.contentDocument || parentIframe.contentWindow?.document;\n if (iframeDoc) {\n scrollLeft = iframeDoc.documentElement?.scrollLeft || iframeDoc.body?.scrollLeft || 0;\n scrollTop = iframeDoc.documentElement?.scrollTop || iframeDoc.body?.scrollTop || 0;\n }\n } catch (e) {\n console.warn(\"Cannot access iframe scroll position (cross-origin):\", e);\n }\n\n // Transform each rect accounting for iframe scaling\n for (const rect of rects) {\n // Apply scale factor to coordinates and dimensions\n const scaledWidth = rect.width * scaleX;\n const scaledHeight = rect.height * scaleY;\n const scaledTop = rect.top * scaleY;\n const scaledLeft = rect.left * scaleX;\n const scaledScrollTop = scrollTop * scaleY;\n const scaledScrollLeft = scrollLeft * scaleX;\n\n const transformedRect = {\n top: scaledTop + iframeRect.top + contentOffsetY - scaledScrollTop,\n left: scaledLeft + iframeRect.left + contentOffsetX - scaledScrollLeft,\n bottom: scaledTop + scaledHeight + iframeRect.top + contentOffsetY - scaledScrollTop,\n right: scaledLeft + scaledWidth + iframeRect.left + contentOffsetX - scaledScrollLeft,\n width: scaledWidth,\n height: scaledHeight,\n x: scaledLeft + iframeRect.left + contentOffsetX - scaledScrollLeft,\n y: scaledTop + iframeRect.top + contentOffsetY - scaledScrollTop\n };\n transformedRects.push(transformedRect);\n }\n\n rects = transformedRects;\n }\n\n // Generate a color based on the index\n const colors = [\n \"#FF0000\",\n \"#00FF00\",\n \"#0000FF\",\n \"#FFA500\",\n \"#800080\",\n \"#008080\",\n \"#FF69B4\",\n \"#4B0082\",\n \"#FF4500\",\n \"#2E8B57\",\n \"#DC143C\",\n \"#4682B4\",\n ];\n const colorIndex = index % colors.length;\n const baseColor = colors[colorIndex];\n const backgroundColor = baseColor + \"1A\"; // 10% opacity version of the color\n\n // No need for iframe offset calculation since rects are already transformed to main document coordinates\n const iframeOffset = { x: 0, y: 0 };\n\n // Create fragment to hold overlay elements\n const fragment = document.createDocumentFragment();\n\n // Create highlight overlays for each client rect\n for (const rect of rects) {\n if (rect.width === 0 || rect.height === 0) continue; // Skip empty rects\n\n const overlay = document.createElement(\"div\");\n overlay.style.position = \"fixed\";\n overlay.style.border = `1px solid ${baseColor}`;\n overlay.style.backgroundColor = \"none\";\n overlay.style.pointerEvents = \"none\";\n overlay.style.boxSizing = \"border-box\";\n\n const top = rect.top + iframeOffset.y;\n const left = rect.left + iframeOffset.x;\n\n overlay.style.top = `${top}px`;\n overlay.style.left = `${left}px`;\n overlay.style.width = `${rect.width}px`;\n overlay.style.height = `${rect.height}px`;\n\n fragment.appendChild(overlay);\n overlays.push({ element: overlay, initialRect: rect }); // Store overlay and its rect\n }\n\n // Create and position a single label relative to the first rect\n const firstRect = rects[0];\n label = document.createElement(\"div\");\n label.className = \"playwright-highlight-label\";\n label.style.position = \"fixed\";\n label.style.background = baseColor;\n label.style.color = \"white\";\n label.style.padding = \"1px 4px\";\n label.style.borderRadius = \"4px\";\n const fontSize = Math.min(index >= 100 ? 8 : 12, Math.max(8, firstRect.height / 2));\n label.style.fontSize = `${fontSize}px`;\n label.textContent = index;\n\n // labelWidth = label.offsetWidth > 0 ? label.offsetWidth : labelWidth; // Update actual width if possible\n // labelHeight = label.offsetHeight > 0 ? label.offsetHeight : labelHeight; // Update actual height if possible\n labelWidth *= fontSize / 12;\n labelHeight *= fontSize / 12;\n const digits = index.toString().length;\n labelWidth += (digits - 2) * fontSize / 1.5;\n labelHeight *= fontSize / 12;\n\n const firstRectTop = firstRect.top + iframeOffset.y;\n const firstRectLeft = firstRect.left + iframeOffset.x;\n\n let labelTop = firstRectTop - labelHeight - 4 * 12 / fontSize;\n let labelLeft = firstRectLeft + firstRect.width - labelWidth - 2;\n\n // Adjust label position if first rect is too small\n if (firstRect.width < labelWidth + 4 || firstRect.height < labelHeight + 4) {\n labelTop = firstRectTop - labelHeight;\n labelLeft = firstRectLeft + firstRect.width - labelWidth; // Align with right edge\n if (labelLeft < iframeOffset.x) labelLeft = firstRectLeft; // Prevent going off-left\n }\n\n // // Check if the label is too close to any existing label\n // const minDistance = 2; // Minimum distance between labels\n // let hasOverlap = true;\n // let attempts = 0;\n // const maxAttempts = 5;\n\n // const alignmentPositions = [\n // // align with right edge (current default)\n // firstRectLeft + firstRect.width - labelWidth - 2,\n // // 1/4 of the way from the right edge\n // firstRectLeft + (firstRect.width - labelWidth) / 2 + firstRect.width / 4,\n // // in middle\n // firstRectLeft + (firstRect.width - labelWidth) / 2,\n // firstRectLeft + (firstRect.width - labelWidth) / 2 - firstRect.width / 4,\n // // align with left edge\n // firstRectLeft,\n // ];\n\n // while (hasOverlap && attempts < maxAttempts) {\n // hasOverlap = false;\n\n // // Use the current alignment position\n // labelLeft = alignmentPositions[attempts];\n\n // // Current label bounding box\n // const currentLabel = {\n // xmin: labelLeft,\n // ymin: labelTop,\n // xmax: labelLeft + labelWidth,\n // ymax: labelTop + labelHeight,\n // };\n\n // for (const existingLabel of existingLabelBoundingBoxes) {\n // // Check if labels overlap or are too close (using minDistance buffer)\n // if (!(currentLabel.xmax + minDistance < existingLabel.xmin ||\n // currentLabel.xmin > existingLabel.xmax + minDistance ||\n // currentLabel.ymax + minDistance < existingLabel.ymin ||\n // currentLabel.ymin > existingLabel.ymax + minDistance)) {\n // hasOverlap = true;\n // break;\n // }\n // }\n\n // if (hasOverlap) {\n // attempts++;\n // }\n // }\n\n // Ensure label stays within viewport bounds slightly better\n labelTop = Math.max(0, Math.min(labelTop, window.innerHeight - labelHeight));\n labelLeft = Math.max(0, Math.min(labelLeft, window.innerWidth - labelWidth - 2));\n\n\n label.style.top = `${labelTop}px`;\n label.style.left = `${labelLeft}px`;\n\n // Add the label to the existing label bounding boxes\n existingLabelBoundingBoxes.push({\n xmin: labelLeft,\n ymin: labelTop,\n xmax: labelLeft + labelWidth,\n ymax: labelTop + labelHeight,\n });\n\n fragment.appendChild(label);\n\n // Update positions on scroll/resize\n const updatePositions = () => {\n let newRects = element.getClientRects(); // Get fresh rects\n\n // Transform rects if element is inside an iframe (same logic as initial highlighting)\n if (parentIframe) {\n const transformedRects = [];\n const iframeRect = parentIframe.getBoundingClientRect();\n\n // Get iframe's content area offset and CSS transforms\n const iframeStyle = window.getComputedStyle(parentIframe);\n const borderLeft = parseFloat(iframeStyle.borderLeftWidth) || 0;\n const borderTop = parseFloat(iframeStyle.borderTopWidth) || 0;\n const paddingLeft = parseFloat(iframeStyle.paddingLeft) || 0;\n const paddingTop = parseFloat(iframeStyle.paddingTop) || 0;\n\n const contentOffsetX = borderLeft + paddingLeft;\n const contentOffsetY = borderTop + paddingTop;\n\n // Extract scale factor from CSS transform\n let scaleX = 1, scaleY = 1;\n const transform = iframeStyle.transform;\n if (transform && transform !== 'none') {\n // Parse scale from transform matrix or scale() function\n const scaleMatch = transform.match(/scale\\(([^)]+)\\)/);\n if (scaleMatch) {\n const scaleValues = scaleMatch[1].split(',').map(v => parseFloat(v.trim()));\n scaleX = scaleValues[0] || 1;\n scaleY = scaleValues[1] || scaleX; // If only one value, use it for both X and Y\n } else {\n // Try to parse matrix() transform\n const matrixMatch = transform.match(/matrix\\(([^)]+)\\)/);\n if (matrixMatch) {\n const values = matrixMatch[1].split(',').map(v => parseFloat(v.trim()));\n if (values.length >= 6) {\n scaleX = values[0]; // a value in matrix(a, b, c, d, e, f)\n scaleY = values[3]; // d value in matrix(a, b, c, d, e, f)\n }\n }\n }\n }\n\n // Get iframe scroll position\n let scrollLeft = 0, scrollTop = 0;\n try {\n const iframeDoc = parentIframe.contentDocument || parentIframe.contentWindow?.document;\n if (iframeDoc) {\n scrollLeft = iframeDoc.documentElement?.scrollLeft || iframeDoc.body?.scrollLeft || 0;\n scrollTop = iframeDoc.documentElement?.scrollTop || iframeDoc.body?.scrollTop || 0;\n }\n } catch (e) {\n console.warn(\"Cannot access iframe scroll position (cross-origin):\", e);\n }\n\n // Transform each rect accounting for iframe scaling\n for (const rect of newRects) {\n // Apply scale factor to coordinates and dimensions\n const scaledWidth = rect.width * scaleX;\n const scaledHeight = rect.height * scaleY;\n const scaledTop = rect.top * scaleY;\n const scaledLeft = rect.left * scaleX;\n const scaledScrollTop = scrollTop * scaleY;\n const scaledScrollLeft = scrollLeft * scaleX;\n\n const transformedRect = {\n top: scaledTop + iframeRect.top + contentOffsetY - scaledScrollTop,\n left: scaledLeft + iframeRect.left + contentOffsetX - scaledScrollLeft,\n bottom: scaledTop + scaledHeight + iframeRect.top + contentOffsetY - scaledScrollTop,\n right: scaledLeft + scaledWidth + iframeRect.left + contentOffsetX - scaledScrollLeft,\n width: scaledWidth,\n height: scaledHeight,\n x: scaledLeft + iframeRect.left + contentOffsetX - scaledScrollLeft,\n y: scaledTop + iframeRect.top + contentOffsetY - scaledScrollTop\n };\n transformedRects.push(transformedRect);\n }\n\n newRects = transformedRects;\n }\n\n const newIframeOffset = { x: 0, y: 0 }; // No offset needed since rects are already transformed\n\n // Update each overlay\n overlays.forEach((overlayData, i) => {\n if (i < newRects.length) { // Check if rect still exists\n const newRect = newRects[i];\n const newTop = newRect.top + newIframeOffset.y;\n const newLeft = newRect.left + newIframeOffset.x;\n\n overlayData.element.style.top = `${newTop}px`;\n overlayData.element.style.left = `${newLeft}px`;\n overlayData.element.style.width = `${newRect.width}px`;\n overlayData.element.style.height = `${newRect.height}px`;\n overlayData.element.style.display = (newRect.width === 0 || newRect.height === 0) ? 'none' : 'block';\n } else {\n // If fewer rects now, hide extra overlays\n overlayData.element.style.display = 'none';\n }\n });\n\n // If there are fewer new rects than overlays, hide the extras\n if (newRects.length < overlays.length) {\n for (let i = newRects.length; i < overlays.length; i++) {\n overlays[i].element.style.display = 'none';\n }\n }\n\n // Update label position based on the first new rect\n if (label && newRects.length > 0) {\n const firstNewRect = newRects[0];\n const firstNewRectTop = firstNewRect.top + newIframeOffset.y;\n const firstNewRectLeft = firstNewRect.left + newIframeOffset.x;\n\n let newLabelTop = firstNewRectTop + 2;\n let newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth - 2;\n\n if (firstNewRect.width < labelWidth + 4 || firstNewRect.height < labelHeight + 4) {\n newLabelTop = firstNewRectTop - labelHeight - 2;\n newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth;\n if (newLabelLeft < newIframeOffset.x) newLabelLeft = firstNewRectLeft;\n }\n\n // Ensure label stays within viewport bounds\n newLabelTop = Math.max(0, Math.min(newLabelTop, window.innerHeight - labelHeight));\n newLabelLeft = Math.max(0, Math.min(newLabelLeft, window.innerWidth - labelWidth));\n\n label.style.top = `${newLabelTop}px`;\n label.style.left = `${newLabelLeft}px`;\n label.style.display = 'block';\n } else if (label) {\n // Hide label if element has no rects anymore\n label.style.display = 'none';\n }\n };\n\n const throttleFunction = (func, delay) => {\n let lastCall = 0;\n return (...args) => {\n const now = performance.now();\n if (now - lastCall < delay) return;\n lastCall = now;\n return func(...args);\n };\n };\n\n // const throttledUpdatePositions = throttleFunction(updatePositions, 16); // ~60fps\n // window.addEventListener('scroll', throttledUpdatePositions, true);\n // window.addEventListener('resize', throttledUpdatePositions);\n\n // Add cleanup function\n cleanupFn = () => {\n // window.removeEventListener('scroll', throttledUpdatePositions, true);\n // window.removeEventListener('resize', throttledUpdatePositions);\n // Remove overlay elements if needed\n overlays.forEach(overlay => overlay.element.remove());\n if (label) label.remove();\n };\n\n // Then add fragment to container in one operation\n container.appendChild(fragment);\n\n return index + 1;\n } finally {\n // Store cleanup function for later use\n if (cleanupFn) {\n // Keep a reference to cleanup functions in a global array\n (window._highlightCleanupFunctions = window._highlightCleanupFunctions || []).push(cleanupFn);\n }\n }\n }\n\n\n /**\n * Gets the position of an element in its parent.\n *\n * @param {HTMLElement} currentElement - The element to get the position for.\n * @returns {number} The position of the element in its parent.\n */\n function getElementPosition(currentElement) {\n if (!currentElement.parentElement) {\n return 0; // No parent means no siblings\n }\n\n const tagName = currentElement.nodeName.toLowerCase();\n\n const siblings = Array.from(currentElement.parentElement.children)\n .filter((sib) => sib.nodeName.toLowerCase() === tagName);\n\n if (siblings.length === 1) {\n return 0; // Only element of its type\n }\n\n const index = siblings.indexOf(currentElement) + 1; // 1-based index\n return index;\n }\n\n\n function getXPathTree(element, stopAtBoundary = true) {\n if (xpathCache.has(element)) return xpathCache.get(element);\n\n const segments = [];\n let currentElement = element;\n\n while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {\n // Stop if we hit a shadow root or iframe\n if (\n stopAtBoundary &&\n (currentElement.parentNode instanceof ShadowRoot ||\n currentElement.parentNode instanceof HTMLIFrameElement)\n ) {\n break;\n }\n\n const position = getElementPosition(currentElement);\n const tagName = currentElement.nodeName.toLowerCase();\n const xpathIndex = position > 0 ? `[${position}]` : \"\";\n segments.unshift(`${tagName}${xpathIndex}`);\n\n currentElement = currentElement.parentNode;\n }\n\n const result = segments.join(\"/\");\n xpathCache.set(element, result);\n return result;\n }\n\n /**\n * Checks if a text node is visible.\n *\n * @param {Text} textNode - The text node to check.\n * @returns {boolean} Whether the text node is visible.\n */\n function isTextNodeVisible(textNode) {\n try {\n // Special case: when viewportExpansion is -1, consider all text nodes as visible\n if (viewportExpansion === -1) {\n // Still check parent visibility for basic filtering\n const parentElement = textNode.parentElement;\n if (!parentElement) return false;\n\n try {\n return parentElement.checkVisibility({\n checkOpacity: true,\n checkVisibilityCSS: true,\n });\n } catch (e) {\n // Fallback if checkVisibility is not supported\n const style = window.getComputedStyle(parentElement);\n return style.display !== 'none' &&\n style.visibility !== 'hidden' &&\n style.opacity !== '0';\n }\n }\n\n const range = document.createRange();\n range.selectNodeContents(textNode);\n const rects = range.getClientRects(); // Use getClientRects for Range\n\n if (!rects || rects.length === 0) {\n return false;\n }\n\n let isAnyRectVisible = false;\n let isAnyRectInViewport = false;\n\n for (const rect of rects) {\n // Check size\n if (rect.width > 0 && rect.height > 0) {\n isAnyRectVisible = true;\n\n // Viewport check for this rect\n if (!(\n rect.bottom < -viewportExpansion ||\n rect.top > window.innerHeight + viewportExpansion ||\n rect.right < -viewportExpansion ||\n rect.left > window.innerWidth + viewportExpansion\n )) {\n isAnyRectInViewport = true;\n break; // Found a visible rect in viewport, no need to check others\n }\n }\n }\n\n if (!isAnyRectVisible || !isAnyRectInViewport) {\n return false;\n }\n\n // Check parent visibility\n const parentElement = textNode.parentElement;\n if (!parentElement) return false;\n\n try {\n return parentElement.checkVisibility({\n checkOpacity: true,\n checkVisibilityCSS: true,\n });\n } catch (e) {\n // Fallback if checkVisibility is not supported\n const style = window.getComputedStyle(parentElement);\n return style.display !== 'none' &&\n style.visibility !== 'hidden' &&\n style.opacity !== '0';\n }\n } catch (e) {\n console.warn('Error checking text node visibility:', e);\n return false;\n }\n }\n\n /**\n * Checks if an element is accepted.\n *\n * @param {HTMLElement} element - The element to check.\n * @returns {boolean} Whether the element is accepted.\n */\n function isElementAccepted(element) {\n if (!element || !element.tagName) return false;\n\n // Always accept body and common container elements\n const alwaysAccept = new Set([\n \"body\", \"div\", \"main\", \"article\", \"section\", \"nav\", \"header\", \"footer\"\n ]);\n const tagName = element.tagName.toLowerCase();\n\n if (alwaysAccept.has(tagName)) return true;\n\n const leafElementDenyList = new Set([\n \"svg\",\n \"script\",\n \"style\",\n \"link\",\n \"meta\",\n \"noscript\",\n \"template\",\n ]);\n\n return !leafElementDenyList.has(tagName);\n }\n\n /**\n * Checks if an element is visible.\n *\n * @param {HTMLElement} element - The element to check.\n * @returns {boolean} Whether the element is visible.\n */\n function isElementVisible(element) {\n if (element.tagName.toLowerCase() === \"input\" && element.type === \"date\") {\n return true;\n }\n\n if (alwaysHighlightFileInput && element.tagName.toLowerCase() === \"input\" && element.type === \"file\") return true;\n\n // SVG elements need special handling for visibility\n if (element.tagName.toLowerCase() === \"svg\") {\n const rect = getCachedBoundingRect(element);\n const style = getCachedComputedStyle(element);\n return (\n rect &&\n rect.width > 0 &&\n rect.height > 0 &&\n style?.visibility !== \"hidden\" &&\n style?.display !== \"none\"\n );\n }\n\n const style = getCachedComputedStyle(element);\n return (\n element.offsetWidth > 0 &&\n element.offsetHeight > 0 &&\n style?.visibility !== \"hidden\" &&\n style?.display !== \"none\"\n );\n }\n\n /**\n * Checks if an element is clickable (responds to click events).\n *\n * @param {HTMLElement} element - The element to check.\n * @returns {boolean} Whether the element is clickable.\n */\n function shouldMarkAsClickable(element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return false;\n }\n\n const tagName = element.tagName.toLowerCase();\n\n // Primarily clickable elements\n const primaryClickableElements = new Set([\n \"a\", // Links\n \"button\", // Buttons\n \"details\", // Expandable details\n \"summary\", // Summary element (clickable part of details)\n \"label\", // Form labels (often clickable)\n \"option\", // Select options\n \"optgroup\", // Option groups\n ]);\n\n if (primaryClickableElements.has(tagName)) {\n return false;\n }\n\n const role = element.getAttribute(\"role\");\n\n // Clickable roles\n const clickableRoles = new Set([\n 'button', // Directly clickable element\n 'link', // Clickable link\n 'menuitem', // Clickable menu item\n 'menuitemradio', // Radio-style menu item (selectable)\n 'menuitemcheckbox', // Checkbox-style menu item (toggleable)\n 'radio', // Radio button (selectable)\n 'checkbox', // Checkbox (toggleable)\n 'tab', // Tab (clickable to switch content)\n 'switch', // Toggle switch (clickable to change state)\n 'option', // Selectable option in a list\n ]);\n\n if (role && clickableRoles.has(role)) {\n return true;\n }\n\n // Check for dropdown indicators\n if (hasAnyClassName(element, buttonClassNames)) {\n return true; // Return true for dropdown elements\n }\n\n if (\n element.getAttribute('data-toggle') === 'dropdown' ||\n element.getAttribute('aria-haspopup')\n ) {\n return true;\n }\n\n const clickEvents = ['click', 'mousedown', 'mouseup', 'dblclick'];\n const listenedEvents = getCachedNodeEventListeners(element);\n if (listenedEvents && listenedEvents.length > 0) {\n for (const eventType of clickEvents) {\n if (listenedEvents.includes(eventType)) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Checks if an element is interactive.\n *\n * lots of comments, and uncommented code - to show the logic of what we already tried\n *\n * One of the things we tried at the beginning was also to use event listeners, and other fancy class, style stuff -> what actually worked best was just combining most things with computed cursor style :)\n *\n * @param {HTMLElement} element - The element to check.\n */\n function isInteractiveElement(element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return false;\n }\n\n // Cache the tagName and style lookups\n const tagName = element.tagName.toLowerCase();\n const style = getCachedComputedStyle(element);\n\n // Define interactive cursors\n const interactiveCursors = new Set([\n 'pointer', // Link/clickable elements\n 'move', // Movable elements\n 'text', // Text selection\n 'grab', // Grabbable elements\n 'grabbing', // Currently grabbing\n 'cell', // Table cell selection\n 'copy', // Copy operation\n 'alias', // Alias creation\n 'all-scroll', // Scrollable content\n 'col-resize', // Column resize\n 'context-menu', // Context menu available\n 'crosshair', // Precise selection\n 'e-resize', // East resize\n 'ew-resize', // East-west resize\n 'help', // Help available\n 'n-resize', // North resize\n 'ne-resize', // Northeast resize\n 'nesw-resize', // Northeast-southwest resize\n 'ns-resize', // North-south resize\n 'nw-resize', // Northwest resize\n 'nwse-resize', // Northwest-southeast resize\n 'row-resize', // Row resize\n 's-resize', // South resize\n 'se-resize', // Southeast resize\n 'sw-resize', // Southwest resize\n 'vertical-text', // Vertical text selection\n 'w-resize', // West resize\n 'zoom-in', // Zoom in\n 'zoom-out' // Zoom out\n ]);\n\n // Define non-interactive cursors\n const nonInteractiveCursors = new Set([\n 'not-allowed', // Action not allowed\n 'no-drop', // Drop not allowed\n 'wait', // Processing\n 'progress', // In progress\n 'initial', // Initial value\n 'inherit' // Inherited value\n //? Let's just include all potentially clickable elements that are not specifically blocked\n // 'none', // No cursor\n // 'default', // Default cursor\n // 'auto', // Browser default\n ]);\n\n /**\n * Checks if an element has an interactive pointer.\n *\n * @param {HTMLElement} element - The element to check.\n * @returns {boolean} Whether the element has an interactive pointer.\n */\n function doesElementHaveInteractivePointer(element) {\n if (element.tagName.toLowerCase() === \"html\") return false;\n\n if (style?.cursor && interactiveCursors.has(style.cursor)) return true;\n\n return false;\n }\n // Disabled for now, since it adds too many false positives\n // let isInteractiveCursor = doesElementHaveInteractivePointer(element);\n\n // // Genius fix for almost all interactive elements\n // if (isInteractiveCursor) {\n // return true;\n // }\n\n const interactiveElements = new Set([\n \"a\", // Links\n \"button\", // Buttons\n \"input\", // All input types (text, checkbox, radio, etc.)\n \"select\", // Dropdown menus\n \"textarea\", // Text areas\n \"details\", // Expandable details\n \"summary\", // Summary element (clickable part of details)\n \"label\", // Form labels (often clickable)\n \"option\", // Select options\n \"optgroup\", // Option groups\n \"fieldset\", // Form fieldsets (can be interactive with legend)\n \"legend\", // Fieldset legends\n ]);\n\n // Define explicit disable attributes and properties\n const explicitDisableTags = new Set([\n 'disabled', // Standard disabled attribute\n // 'aria-disabled', // ARIA disabled state\n // 'readonly', // Read-only state\n // 'aria-readonly', // ARIA read-only state\n // 'aria-hidden', // Hidden from accessibility\n // 'hidden', // Hidden attribute\n // 'inert', // Inert attribute\n // 'aria-inert', // ARIA inert state\n // 'tabindex=\"-1\"', // Removed from tab order\n // 'aria-hidden=\"true\"' // Hidden from screen readers\n ]);\n\n // Check for non-interactive cursor\n if (style?.cursor && nonInteractiveCursors.has(style.cursor)) {\n return false;\n }\n\n // handle inputs, select, checkbox, radio, textarea, button and make sure they are not cursor style disabled/not-allowed\n if (interactiveElements.has(tagName)) {\n // Check for explicit disable attributes\n for (const disableTag of explicitDisableTags) {\n if (element.hasAttribute(disableTag) ||\n element.getAttribute(disableTag) === 'true' ||\n element.getAttribute(disableTag) === '') {\n return false;\n }\n }\n\n // Check for disabled property on form elements\n if (element.disabled) {\n return false;\n }\n\n // Don't mark as non-interactive yet\n // Check for readonly property on form elements\n if (element.readOnly) {\n // return false;\n }\n\n // Check for inert property\n if (element.inert) {\n return false;\n }\n\n return true;\n }\n\n const role = element.getAttribute(\"role\");\n const ariaRole = element.getAttribute(\"aria-role\");\n\n // Check for contenteditable attribute\n if (element.getAttribute(\"contenteditable\") === \"true\" || element.isContentEditable) {\n return true;\n }\n\n // Added enhancement to capture dropdown interactive elements\n if (hasAnyClassName(element, buttonClassNames) ||\n hasAnyClassName(element, interactiveClassNames) ||\n element.getAttribute('data-index') ||\n element.getAttribute('data-toggle') === 'dropdown' ||\n element.getAttribute('aria-haspopup')) {\n return true;\n }\n\n\n const interactiveRoles = new Set([\n 'button', // Directly clickable element\n 'link', // Clickable link\n 'menuitem', // Clickable menu item\n 'menuitemradio', // Radio-style menu item (selectable)\n 'menuitemcheckbox', // Checkbox-style menu item (toggleable)\n 'radio', // Radio button (selectable)\n 'checkbox', // Checkbox (toggleable)\n 'tab', // Tab (clickable to switch content)\n 'switch', // Toggle switch (clickable to change state)\n 'slider', // Slider control (draggable)\n 'spinbutton', // Number input with up/down controls\n 'combobox', // Dropdown with text input\n 'searchbox', // Search input field\n 'textbox', // Text input field\n 'listbox', // Selectable list\n 'option', // Selectable option in a list\n 'scrollbar' // Scrollable control\n ]);\n\n\n // Basic role/attribute checks\n const hasInteractiveRole =\n (role && interactiveRoles.has(role)) ||\n (ariaRole && interactiveRoles.has(ariaRole));\n\n if (hasInteractiveRole) return true;\n\n const listenedEvents = getCachedNodeEventListeners(element);\n if (listenedEvents && listenedEvents.length > 0) {\n for (const eventType of INTERACTION_EVENTS) {\n if (listenedEvents.includes(eventType)) {\n return true;\n }\n }\n }\n\n return false\n }\n\n\n /**\n * Checks if an element is the topmost element at its position.\n *\n * @param {HTMLElement} element - The element to check.\n * @returns {boolean} Whether the element is the topmost element at its position.\n */\n function isTopElement(element) {\n if (element.tagName.toLowerCase() === \"input\" && element.type === \"date\") {\n return true;\n }\n // Special case: when viewportExpansion is -1, consider all elements as \"top\" elements\n if (viewportExpansion === -1) {\n return true;\n }\n\n const rects = getCachedClientRects(element); // Replace element.getClientRects()\n\n if (!rects || rects.length === 0) {\n return false; // No geometry, cannot be top\n }\n\n let isAnyRectInViewport = false;\n for (const rect of rects) {\n // Use the same logic as isInExpandedViewport check\n if (rect.width > 0 && rect.height > 0 && !( // Only check non-empty rects\n rect.bottom < -viewportExpansion ||\n rect.top > window.innerHeight + viewportExpansion ||\n rect.right < -viewportExpansion ||\n rect.left > window.innerWidth + viewportExpansion\n )) {\n isAnyRectInViewport = true;\n break;\n }\n }\n\n if (!isAnyRectInViewport) {\n return false; // All rects are outside the viewport area\n }\n\n\n // Find the correct document context and root element\n let doc = element.ownerDocument;\n\n // If we're in an iframe, elements are considered top by default\n if (doc !== window.document) {\n return true;\n }\n\n // For shadow DOM, we need to check within its own root context\n const shadowRoot = element.getRootNode();\n if (shadowRoot instanceof ShadowRoot) {\n const centerX = rects[Math.floor(rects.length / 2)].left + rects[Math.floor(rects.length / 2)].width / 2;\n const centerY = rects[Math.floor(rects.length / 2)].top + rects[Math.floor(rects.length / 2)].height / 2;\n\n try {\n const topEl = shadowRoot.elementFromPoint(centerX, centerY);\n if (!topEl) return false;\n\n let current = topEl;\n while (current && current !== shadowRoot) {\n if (current === element) return true;\n current = current.parentElement;\n }\n return false;\n } catch (e) {\n return true;\n }\n }\n\n // For elements in viewport, check if they're topmost\n const centerX = rects[Math.floor(rects.length / 2)].left + rects[Math.floor(rects.length / 2)].width / 2;\n const centerY = rects[Math.floor(rects.length / 2)].top + rects[Math.floor(rects.length / 2)].height / 2;\n\n try {\n const topEl = document.elementFromPoint(centerX, centerY);\n if (!topEl) return false;\n\n let current = topEl;\n while (current && current !== document.documentElement) {\n if (current === element) return true;\n current = current.parentElement;\n }\n return false;\n } catch (e) {\n return true;\n }\n }\n\n /**\n * Checks if an element is within the expanded viewport.\n *\n * @param {HTMLElement} element - The element to check.\n * @param {number} viewportExpansion - The viewport expansion.\n * @returns {boolean} Whether the element is within the expanded viewport.\n */\n function isInExpandedViewport(element, viewportExpansion) {\n if (viewportExpansion === -1) {\n return true;\n }\n\n const rects = element.getClientRects(); // Use getClientRects\n\n if (!rects || rects.length === 0) {\n // Fallback to getBoundingClientRect if getClientRects is empty,\n // useful for elements like <svg> that might not have client rects but have a bounding box.\n const boundingRect = getCachedBoundingRect(element);\n if (!boundingRect || boundingRect.width === 0 || boundingRect.height === 0) {\n return false;\n }\n return !(\n boundingRect.bottom < -viewportExpansion ||\n boundingRect.top > window.innerHeight + viewportExpansion ||\n boundingRect.right < -viewportExpansion ||\n boundingRect.left > window.innerWidth + viewportExpansion\n );\n }\n\n // Check if *any* client rect is within the viewport\n for (const rect of rects) {\n if (rect.width === 0 || rect.height === 0) continue; // Skip empty rects\n\n if (!(\n rect.bottom < -viewportExpansion ||\n rect.top > window.innerHeight + viewportExpansion ||\n rect.right < -viewportExpansion ||\n rect.left > window.innerWidth + viewportExpansion\n )) {\n return true; // Found at least one rect in the viewport\n }\n }\n\n return false; // No rects were found in the viewport\n }\n\n // /**\n // * Gets the effective scroll of an element.\n // *\n // * @param {HTMLElement} element - The element to get the effective scroll for.\n // * @returns {Object} The effective scroll of the element.\n // */\n // function getEffectiveScroll(element) {\n // let currentEl = element;\n // let scrollX = 0;\n // let scrollY = 0;\n\n // while (currentEl && currentEl !== document.documentElement) {\n // if (currentEl.scrollLeft || currentEl.scrollTop) {\n // scrollX += currentEl.scrollLeft;\n // scrollY += currentEl.scrollTop;\n // }\n // currentEl = currentEl.parentElement;\n // }\n\n // scrollX += window.scrollX;\n // scrollY += window.scrollY;\n\n // return { scrollX, scrollY };\n // }\n\n /**\n * Checks if an element is an interactive candidate.\n *\n * @param {HTMLElement} element - The element to check.\n * @returns {boolean} Whether the element is an interactive candidate.\n */\n function isInteractiveCandidate(element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;\n\n const tagName = element.tagName.toLowerCase();\n\n // Fast-path for common interactive elements\n const interactiveElements = new Set([\n \"a\", \"button\", \"input\", \"select\", \"textarea\", \"details\", \"summary\", \"label\"\n ]);\n\n if (interactiveElements.has(tagName)) return true;\n\n // Quick attribute checks without getting full lists\n const hasQuickInteractiveAttr = element.hasAttribute(\"onclick\") ||\n element.hasAttribute(\"role\") ||\n element.hasAttribute(\"tabindex\") ||\n element.hasAttribute(\"aria-\") ||\n element.hasAttribute(\"data-action\") ||\n element.getAttribute(\"contenteditable\") === \"true\";\n\n return hasQuickInteractiveAttr;\n }\n\n // --- Define constants for distinct interaction check ---\n const DISTINCT_INTERACTIVE_TAGS = new Set([\n 'a', 'button', 'input', 'select', 'textarea', 'summary', 'details', 'label', 'option'\n ]);\n const INTERACTIVE_ROLES = new Set([\n 'button', 'link', 'menuitem', 'menuitemradio', 'menuitemcheckbox',\n 'radio', 'checkbox', 'tab', 'switch', 'slider', 'spinbutton',\n 'combobox', 'searchbox', 'textbox', 'listbox', 'option', 'scrollbar'\n ]);\n\n\n /**\n * Heuristically determines if an element should be considered as independently interactive,\n * even if it's nested inside another interactive container.\n *\n * This function helps detect deeply nested actionable elements (e.g., menu items within a button)\n * that may not be picked up by strict interactivity checks.\n *\n * @param {HTMLElement} element - The element to check.\n * @returns {boolean} Whether the element is heuristically interactive.\n */\n function isHeuristicallyInteractive(element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;\n\n // Skip non-visible elements early for performance\n if (!isElementVisible(element)) return false;\n\n // Check for common attributes that often indicate interactivity\n const hasInteractiveAttributes =\n element.hasAttribute('role') ||\n element.hasAttribute('tabindex') ||\n element.hasAttribute('onclick') ||\n typeof element.onclick === 'function';\n\n // Check for semantic class names suggesting interactivity\n const hasInteractiveClass = heuristicClassPattern.test(element.className || '');\n\n // Determine whether the element is inside a known interactive container\n const isInKnownContainer = Boolean(\n element.closest(containerSelectors)\n );\n\n // Ensure the element has at least one visible child (to avoid marking empty wrappers)\n const hasVisibleChildren = [...element.children].some(isElementVisible);\n\n // Avoid highlighting elements whose parent is <body> (top-level wrappers)\n const isParentBody = element.parentElement && element.parentElement.isSameNode(document.body);\n\n return (\n (isInteractiveElement(element) || hasInteractiveAttributes || hasInteractiveClass) &&\n hasVisibleChildren &&\n isInKnownContainer &&\n !isParentBody\n );\n }\n\n\n /**\n * Checks if an element likely represents a distinct interaction\n * separate from its parent (if the parent is also interactive).\n *\n * @param {HTMLElement} element - The element to check.\n * @param {Object} nodeData - The node data object.\n * @returns {boolean} Whether the element is a distinct interaction.\n */\n function isElementDistinctInteraction(element, nodeData) {\n if (nodeData.isScrollable) {\n return true;\n }\n\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return false;\n }\n\n const tagName = element.tagName.toLowerCase();\n const role = element.getAttribute('role');\n\n // Check if it's an iframe - always distinct boundary\n if (tagName === 'iframe') {\n return true;\n }\n\n // Check tag name\n if (DISTINCT_INTERACTIVE_TAGS.has(tagName)) {\n return true;\n }\n // Check interactive roles\n if (role && INTERACTIVE_ROLES.has(role)) {\n return true;\n }\n // Check contenteditable\n if (element.isContentEditable || element.getAttribute('contenteditable') === 'true') {\n return true;\n }\n // Check for common testing/automation attributes\n if (element.hasAttribute('data-testid') || element.hasAttribute('data-cy') || element.hasAttribute('data-test')) {\n return true;\n }\n // Check for explicit onclick handler (attribute or property)\n if (element.hasAttribute('onclick') || typeof element.onclick === 'function') {\n return true;\n }\n\n if (element.hasAttribute('aria-haspopup')) {\n return true;\n }\n\n if (hasAnyClassName(element, interactiveClassNames)) {\n return true;\n }\n\n // return false\n\n // Check for other common interaction event listeners\n try {\n const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;\n if (typeof getEventListenersForNode === 'function') {\n const listeners = getEventListenersForNode(element);\n const interactionEvents = ['click', 'mousedown', 'mouseup', 'dblclick', 'input', 'mouseenter', 'mouseleave', 'keydown', 'keyup', 'submit', 'change', 'focus', 'blur'];\n for (const eventType of interactionEvents) {\n for (const listener of listeners) {\n if (listener.type === eventType) {\n return true; // Found a common interaction listener\n }\n }\n }\n }\n // Fallback: Check common event attributes if getEventListeners is not available (getEventListenersForNode doesn't work in page.evaluate context)\n const commonEventAttrs = ['onmousedown', 'onmouseup', 'onkeydown', 'onkeyup', 'onsubmit', 'onmouseenter', 'onmouseleave', 'onchange', 'oninput', 'onfocus', 'onblur'];\n if (commonEventAttrs.some(attr => element.hasAttribute(attr))) {\n return true;\n }\n } catch (e) {\n // console.warn(`Could not check event listeners for ${element.tagName}:`, e);\n // If checking listeners fails, rely on other checks\n }\n\n\n\n // if the element is not strictly interactive but appears clickable based on heuristic signals\n if (isHeuristicallyInteractive(element)) {\n return true;\n }\n\n // Default to false: if it's interactive but doesn't match above,\n // assume it triggers the same action as the parent.\n return false;\n }\n // --- End distinct interaction check ---\n\n /**\n * Handles the logic for deciding whether to highlight an element and performing the highlight.\n * @param {\n {\n tagName: string;\n attributes: Record<string, string>;\n xpath: any;\n children: never[];\n isVisible?: boolean;\n isTopElement?: boolean;\n isInteractive?: boolean;\n isInViewport?: boolean;\n highlightIndex?: number;\n shadowRoot?: boolean;\n }} nodeData - The node data object.\n * @param {HTMLElement} node - The node to highlight.\n * @param {HTMLElement | null} parentIframe - The parent iframe node.\n * @param {boolean} isParentHighlighted - Whether the parent node is highlighted.\n * @returns {boolean} Whether the element was highlighted.\n */\n function handleHighlighting(nodeData, node, parentIframe, isParentHighlighted) {\n if (!nodeData.isInteractive) return false; // Not interactive, definitely don't highlight\n\n let shouldHighlight = false;\n if (!isParentHighlighted) {\n // Parent wasn't highlighted, this interactive node can be highlighted.\n shouldHighlight = true;\n } else {\n // Parent *was* highlighted. Only highlight this node if it represents a distinct interaction.\n if (isElementDistinctInteraction(node, nodeData)) {\n shouldHighlight = true;\n } else {\n // console.log(`Skipping highlight for ${nodeData.tagName} (parent highlighted)`);\n shouldHighlight = false;\n }\n }\n\n if (shouldHighlight) {\n const attributeNames = node.getAttributeNames?.() || [];\n for (const name of attributeNames) {\n const value = node.getAttribute(name);\n nodeData.attributes[name] = value;\n }\n\n // Check viewport status before assigning index and highlighting\n if (nodeData.isInViewport === undefined) {\n nodeData.isInViewport = isInExpandedViewport(node, viewportExpansion);\n }\n\n // When viewportExpansion is -1, all interactive elements should get a highlight index\n // regardless of viewport status\n if (nodeData.isInViewport || viewportExpansion === -1) {\n nodeData.highlightIndex = highlightIndex++;\n\n if (doHighlightElements) {\n if (focusHighlightIndex >= 0) {\n if (focusHighlightIndex === nodeData.highlightIndex) {\n highlightElement(node, nodeData.highlightIndex, parentIframe);\n }\n } else {\n highlightElement(node, nodeData.highlightIndex, parentIframe);\n }\n return true; // Successfully highlighted\n }\n } else {\n // console.log(`Skipping highlight for ${nodeData.tagName} (outside viewport)`);\n }\n }\n\n return false; // Did not highlight\n }\n\n function isElementScrollable(element) {\n const listenedEvents = getCachedNodeEventListeners(element);\n if (listenedEvents && listenedEvents.includes('scroll')) {\n const hasScrollableX = element.scrollWidth > element.clientWidth;\n const hasScrollableY = element.scrollHeight > element.clientHeight;\n return hasScrollableX || hasScrollableY;\n }\n\n const style = getCachedComputedStyle(element);\n const hasScrollableX = ['auto', 'scroll'].includes(style.overflowX) &&\n element.scrollWidth > element.clientWidth;\n const hasScrollableY = ['auto', 'scroll'].includes(style.overflowY) &&\n element.scrollHeight > element.clientHeight;\n return hasScrollableX || hasScrollableY;\n }\n\n function addSemanticAttributesToNodeData(node, nodeData) {\n for (const attr of SEMANTIC_ATTRIBUTES) {\n if (node.hasAttribute(attr)) {\n nodeData.attributes[attr] = node.getAttribute(attr);\n }\n }\n }\n\n /**\n * Creates a node data object for a given node and its descendants.\n *\n * @param {HTMLElement} node - The node to process.\n * @param {HTMLElement | null} parentIframe - The parent iframe node.\n * @param {boolean} isParentHighlighted - Whether the parent node is highlighted.\n * @returns {string | null} The ID of the node data object, or null if the node is not processed.\n */\n function buildDomTree(node, parentIframe = null, isParentHighlighted = false) {\n\n // Fast rejection checks first\n if (!node || node.id === HIGHLIGHT_CONTAINER_ID) {\n return null;\n }\n\n if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {\n return null;\n }\n\n // Special handling for root node (body)\n if (node === document.body) {\n const nodeData = {\n tagName: 'body',\n attributes: {},\n xpath: '/body',\n children: [],\n };\n\n // Process children of body\n for (const child of node.childNodes) {\n const domElement = buildDomTree(child, parentIframe, false); // Body's children have no highlighted parent initially\n if (domElement) nodeData.children.push(domElement);\n }\n\n const id = `${ID.current++}`;\n DOM_HASH_MAP[id] = nodeData;\n return id;\n }\n\n // Process text nodes\n if (node.nodeType === Node.TEXT_NODE) {\n const textContent = node.textContent?.trim();\n if (!textContent) {\n return null;\n }\n\n // Only check visibility for text nodes that might be visible\n const parentElement = node.parentElement;\n if (!parentElement || parentElement.tagName.toLowerCase() === 'script') {\n return null;\n }\n\n const id = `${ID.current++}`;\n DOM_HASH_MAP[id] = {\n type: \"TEXT_NODE\",\n text: textContent,\n isVisible: isTextNodeVisible(node),\n };\n return id;\n }\n\n // Quick checks for element nodes\n if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {\n return null;\n }\n\n /**\n * @type {\n {\n tagName: string;\n attributes: Record<string, string | null>;\n xpath: any;\n children: never[];\n isVisible?: boolean;\n isTopElement?: boolean;\n isInteractive?: boolean;\n isInViewport?: boolean;\n highlightIndex?: number;\n shadowRoot?: boolean;\n }\n } nodeData - The node data object.\n */\n const nodeData = {\n tagName: node.tagName.toLowerCase(),\n attributes: {},\n xpath: getXPathTree(node, true),\n children: [],\n };\n\n // Get attributes for interactive elements or potential text containers\n if (node.tagName.toLowerCase() === 'iframe' || node.tagName.toLowerCase() === 'body') {\n const attributeNames = node.getAttributeNames?.() || [];\n for (const name of attributeNames) {\n const value = node.getAttribute(name);\n nodeData.attributes[name] = value;\n }\n }\n\n let nodeWasHighlighted = false;\n // Perform visibility, interactivity, and highlighting checks\n if (node.nodeType === Node.ELEMENT_NODE) {\n if (alwaysHighlightFileInput && node.tagName.toLowerCase() === 'input' && node.type === 'file') {\n nodeData.isTopElement = true;\n if (nodeData.isTopElement) {\n nodeData.isInteractive = true;\n nodeData.isInViewport = true; // File inputs should always be considered in viewport\n // Call the dedicated highlighting function\n nodeWasHighlighted = handleHighlighting(nodeData, node, parentIframe, isParentHighlighted);\n }\n } else {\n nodeData.isVisible = isElementVisible(node); // isElementVisible uses offsetWidth/Height, which is fine\n\n if (nodeData.isVisible) {\n nodeData.isTopElement = isTopElement(node);\n if (nodeData.isTopElement) {\n addSemanticAttributesToNodeData(node, nodeData);\n let isScrollable = isElementScrollable(node);\n nodeData.isInteractive = isInteractiveElement(node) || isScrollable;\n nodeData.isScrollable = isScrollable;\n nodeData.markAsClickable = shouldMarkAsClickable(node);\n // Call the dedicated highlighting function\n nodeWasHighlighted = handleHighlighting(nodeData, node, parentIframe, isParentHighlighted);\n }\n }\n }\n }\n\n // Process children, with special handling for iframes and rich text editors\n if (node.tagName) {\n const tagName = node.tagName.toLowerCase();\n\n // Handle iframes\n if (tagName === \"iframe\") {\n try {\n const iframeDoc = node.contentDocument || node.contentWindow?.document;\n if (iframeDoc) {\n for (const child of iframeDoc.childNodes) {\n const domElement = buildDomTree(child, node, false);\n if (domElement) nodeData.children.push(domElement);\n }\n }\n } catch (e) {\n console.warn(\"Unable to access iframe:\", e);\n }\n }\n // Handle rich text editors and contenteditable elements\n else if (\n node.isContentEditable ||\n node.getAttribute(\"contenteditable\") === \"true\" ||\n node.id === \"tinymce\" ||\n node.classList.contains(\"mce-content-body\") ||\n (tagName === \"body\" && node.getAttribute(\"data-id\")?.startsWith(\"mce_\"))\n ) {\n // Process all child nodes to capture formatted text\n for (const child of node.childNodes) {\n const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);\n if (domElement) nodeData.children.push(domElement);\n }\n }\n else {\n // Handle shadow DOM\n if (node.shadowRoot) {\n nodeData.shadowRoot = true;\n for (const child of node.shadowRoot.childNodes) {\n const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);\n if (domElement) nodeData.children.push(domElement);\n }\n }\n // Handle regular elements\n for (const child of node.childNodes) {\n // Pass the highlighted status of the *current* node to its children\n const passHighlightStatusToChild = nodeWasHighlighted || isParentHighlighted;\n const domElement = buildDomTree(child, parentIframe, passHighlightStatusToChild);\n if (domElement) nodeData.children.push(domElement);\n }\n }\n }\n\n const id = `${ID.current++}`;\n DOM_HASH_MAP[id] = nodeData;\n return id;\n }\n\n const rootId = buildDomTree(document.body);\n\n // Clear the cache before starting\n DOM_CACHE.clearCache();\n\n return { rootId, map: DOM_HASH_MAP };\n}\n","((args = {\n doHighlightElements: true,\n focusHighlightIndex: -1,\n viewportExpansion: 0,\n debugMode: false,\n interactiveClassNames: [],\n alwaysHighlightFileInput: false,\n sameRectIoUThreshold: 0.85,\n}) => {\n // Default threshold if not provided\n const sameRectIoUThreshold = args.sameRectIoUThreshold ?? 0.85;\n const EVENT_LISTENER_MAPPING = {\n 'onclick': 'click',\n 'onmousedown': 'mousedown',\n 'onmouseup': 'mouseup',\n 'ondblclick': 'dblclick',\n 'onmouseenter': 'mouseenter',\n 'onmouseleave': 'mouseleave',\n 'onmousemove': 'mousemove',\n 'onmouseout': 'mouseout',\n 'onmouseover': 'mouseover',\n 'onmousewheel': 'mousewheel',\n 'onscroll': 'scroll',\n 'onselect': 'select',\n 'onchange': 'change',\n 'onfocus': 'focus',\n 'onblur': 'blur',\n 'onkeydown': 'keydown',\n 'onkeyup': 'keyup',\n 'onkeypress': 'keypress',\n 'oninput': 'input',\n };\n const INTERACTION_EVENTS = ['click', 'mousedown', 'mouseup', 'dblclick', 'input', 'mouseenter', 'mouseleave'];\n const { doHighlightElements, focusHighlightIndex, viewportExpansion, debugMode, interactiveClassNames, alwaysHighlightFileInput, grayscaleImage, uniformityTolerance = 32, captureDebugSnapshots = false, onSnapshot, onLog, phase, elementData: inputElementData, } = args;\n // Helper to stream logs if callback provided\n const streamLog = (msg) => {\n if (onLog)\n onLog(msg);\n };\n streamLog(`[dom-tree] Starting phase=${phase || 'legacy'}, grayscaleImage=${!!grayscaleImage}, captureDebugSnapshots=${captureDebugSnapshots}`);\n const buttonClassNames = ['button', 'dropdown-toggle'];\n const cursorPointerClassNames = ['cursor-pointer', 'tw-cursor-pointer', 'clickable'];\n const heuristicClassPattern = /\\b(btn|const clickable|menu|item|entry|link)\\b/i;\n const containerSelectors = 'button,a,[role=\"button\"],.menu,.dropdown,.list,.toolbar';\n let highlightIndex = 0; // Reset highlight index\n /**\n * Helper function to check if element has any of the specified class names.\n */\n function hasAnyClassName(element, classNames) {\n if (!element.classList || !classNames || classNames.length === 0)\n return false;\n return classNames.some(className => element.classList.contains(className));\n }\n // Add caching mechanisms at the top level\n const DOM_CACHE = {\n boundingRects: new WeakMap(),\n clientRects: new WeakMap(),\n computedStyles: new WeakMap(),\n nodeEventListeners: new WeakMap(),\n clearCache: () => {\n DOM_CACHE.boundingRects = new WeakMap();\n DOM_CACHE.clientRects = new WeakMap();\n DOM_CACHE.computedStyles = new WeakMap();\n DOM_CACHE.nodeEventListeners = new WeakMap();\n }\n };\n /**\n * Gets the cached bounding rect for an element.\n */\n function getCachedBoundingRect(element) {\n if (!element)\n return null;\n if (DOM_CACHE.boundingRects.has(element)) {\n return DOM_CACHE.boundingRects.get(element);\n }\n const rect = element.getBoundingClientRect();\n if (rect) {\n DOM_CACHE.boundingRects.set(element, rect);\n }\n return rect;\n }\n /**\n * Gets the cached computed style for an element.\n */\n function getCachedComputedStyle(element) {\n if (!element)\n return null;\n if (DOM_CACHE.computedStyles.has(element)) {\n return DOM_CACHE.computedStyles.get(element);\n }\n const style = window.getComputedStyle(element);\n if (style) {\n DOM_CACHE.computedStyles.set(element, style);\n }\n return style;\n }\n /**\n * Gets the cached client rects for an element.\n */\n function getCachedClientRects(element) {\n if (!element)\n return null;\n if (DOM_CACHE.clientRects.has(element)) {\n return DOM_CACHE.clientRects.get(element);\n }\n const rects = element.getClientRects();\n if (rects) {\n DOM_CACHE.clientRects.set(element, rects);\n }\n return rects;\n }\n /**\n * Gets the event listeners for a node.\n */\n function getNodeEventListeners(element) {\n const set = new Set();\n try {\n if (typeof getEventListeners === 'function') {\n const listeners = getEventListeners(element);\n for (const eventType in listeners) {\n if (listeners[eventType] && listeners[eventType].length > 0) {\n set.add(eventType);\n }\n }\n }\n const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;\n if (typeof getEventListenersForNode === 'function') {\n const listeners = getEventListenersForNode(element);\n for (const listener of listeners) {\n if (listener.type) {\n set.add(listener.type);\n }\n }\n }\n for (const attr in EVENT_LISTENER_MAPPING) {\n if (element.hasAttribute(attr) || typeof element[attr] === 'function') {\n set.add(EVENT_LISTENER_MAPPING[attr]);\n }\n }\n }\n catch (error) {\n // Silently ignore errors\n }\n const listenedEvents = Array.from(set);\n return listenedEvents;\n }\n function getCachedNodeEventListeners(element) {\n if (!element)\n return null;\n if (DOM_CACHE.nodeEventListeners.has(element)) {\n return DOM_CACHE.nodeEventListeners.get(element);\n }\n const listenedEvents = getNodeEventListeners(element);\n if (listenedEvents) {\n DOM_CACHE.nodeEventListeners.set(element, listenedEvents);\n }\n return listenedEvents;\n }\n // ============================================================================\n // Action Intent Predicates\n // ============================================================================\n const CLICKABLE_TAGS = new Set(['a', 'button', 'summary', 'label', 'option', 'optgroup']);\n const CLICKABLE_ROLES = new Set([\n 'button', 'link', 'menuitem', 'menuitemradio', 'menuitemcheckbox',\n 'radio', 'checkbox', 'tab', 'switch', 'option', 'treeitem'\n ]);\n const CLICK_EVENTS = ['click', 'mousedown', 'mouseup', 'dblclick'];\n const TEXT_INPUT_TYPES = new Set([\n 'text', 'email', 'password', 'search', 'tel', 'url', 'number',\n 'date', 'datetime-local', 'month', 'week', 'time'\n ]);\n const INPUT_ROLES = new Set(['textbox', 'searchbox', 'spinbutton', 'combobox']);\n /**\n * Checks if an element matches the 'click' intent.\n * Includes: buttons, links, elements with click handlers, clickable roles\n */\n function isClickIntentElement(element) {\n const tagName = element.tagName.toLowerCase();\n if (CLICKABLE_TAGS.has(tagName))\n return true;\n const role = element.getAttribute('role');\n if (role && CLICKABLE_ROLES.has(role))\n return true;\n // Check for click event listeners\n const listeners = getCachedNodeEventListeners(element);\n if (listeners?.some(e => CLICK_EVENTS.includes(e)))\n return true;\n // Dropdown/popup triggers\n if (element.getAttribute('aria-haspopup') ||\n element.getAttribute('data-toggle') === 'dropdown')\n return true;\n return false;\n }\n /**\n * Checks if an element matches the 'input' intent.\n * Includes: text inputs, textareas, contenteditable elements\n */\n function isInputIntentElement(element) {\n const tagName = element.tagName.toLowerCase();\n if (tagName === 'textarea')\n return true;\n if (tagName === 'input') {\n const type = element.type?.toLowerCase() || 'text';\n return TEXT_INPUT_TYPES.has(type);\n }\n if (element.isContentEditable ||\n element.getAttribute('contenteditable') === 'true')\n return true;\n const role = element.getAttribute('role');\n return role ? INPUT_ROLES.has(role) : false;\n }\n /**\n * Checks if an element matches the 'scroll' intent.\n * Delegates to existing isElementScrollable function.\n */\n function isScrollIntentElement(element) {\n // Note: isElementScrollable is defined later but hoisted due to function declaration\n return isElementScrollable(element);\n }\n /**\n * Checks if an element matches the specified action intent.\n */\n function matchesActionIntent(element, intent) {\n if (intent === 'all')\n return true;\n if (intent === 'click')\n return isClickIntentElement(element);\n if (intent === 'input')\n return isInputIntentElement(element);\n if (intent === 'scroll')\n return isScrollIntentElement(element);\n return true;\n }\n /**\n * Hash map of DOM nodes indexed by their highlight index.\n */\n const DOM_HASH_MAP = {};\n const ID = { current: 0 };\n const HIGHLIGHT_CONTAINER_ID = \"playwright-highlight-container\";\n // Add a WeakMap cache for XPath strings\n const xpathCache = new WeakMap();\n const debugLogs = [];\n const debugLog = (msg) => {\n debugLogs.push(msg);\n };\n // ============================================================================\n // Grayscale Image Label Placement (Dynamic Convolution)\n // ============================================================================\n /**\n * 1D sliding window min/max using monotonic deque (Lemire algorithm).\n * O(n) time complexity - each element is pushed and popped at most once.\n *\n * @param arr - Array of values\n * @param k - Window size\n * @returns Object with maxResults and minResults arrays\n */\n function slidingWindowMinMax(arr, k) {\n if (k <= 0 || arr.length === 0 || k > arr.length) {\n return { maxResults: [], minResults: [] };\n }\n const maxDeque = []; // Indices, values in descending order\n const minDeque = []; // Indices, values in ascending order\n const maxResults = [];\n const minResults = [];\n for (let i = 0; i < arr.length; i++) {\n const val = arr[i];\n // Update max deque - remove smaller elements from back\n while (maxDeque.length > 0 && arr[maxDeque[maxDeque.length - 1]] <= val) {\n maxDeque.pop();\n }\n maxDeque.push(i);\n // Remove front if outside window\n if (maxDeque[0] <= i - k)\n maxDeque.shift();\n // Update min deque - remove larger elements from back\n while (minDeque.length > 0 && arr[minDeque[minDeque.length - 1]] >= val) {\n minDeque.pop();\n }\n minDeque.push(i);\n // Remove front if outside window\n if (minDeque[0] <= i - k)\n minDeque.shift();\n // Record result once window is full\n if (i >= k - 1) {\n maxResults.push(arr[maxDeque[0]]);\n minResults.push(arr[minDeque[0]]);\n }\n }\n return { maxResults, minResults };\n }\n /**\n * Compute 2D min/max over sliding windows within a bounded region.\n * Uses two-pass decomposition: horizontal pass then vertical pass.\n *\n * @param image - Full grayscale image (2D array where image[y][x] is intensity 0-255)\n * @param regionX - Starting X of the region to scan\n * @param regionY - Starting Y of the region to scan\n * @param regionWidth - Width of the region to scan\n * @param regionHeight - Height of the region to scan\n * @param labelWidth - Width of the label (kernel width)\n * @param labelHeight - Height of the label (kernel height)\n * @returns Object with windowMax and windowMin 2D arrays\n */\n function compute2DMinMaxInRegion(image, regionX, regionY, regionWidth, regionHeight, labelWidth, labelHeight) {\n // Handle edge cases\n if (regionWidth < labelWidth || regionHeight < labelHeight) {\n return { windowMax: [], windowMin: [] };\n }\n const imageHeight = image.length;\n const imageWidth = image[0]?.length || 0;\n // Step 1: Horizontal pass - compute row-wise min/max for each row in region\n const rowMax = [];\n const rowMin = [];\n for (let dy = 0; dy < regionHeight; dy++) {\n const y = regionY + dy;\n if (y < 0 || y >= imageHeight) {\n // Out of bounds - use empty arrays\n rowMax[dy] = [];\n rowMin[dy] = [];\n continue;\n }\n // Extract the row slice from the region\n const rowSlice = [];\n for (let dx = 0; dx < regionWidth; dx++) {\n const x = regionX + dx;\n // Use 0 for out of bounds pixels (they'll fail uniformity check)\n rowSlice.push(x >= 0 && x < imageWidth ? (image[y][x] ?? 0) : 0);\n }\n const { maxResults, minResults } = slidingWindowMinMax(rowSlice, labelWidth);\n rowMax[dy] = maxResults;\n rowMin[dy] = minResults;\n }\n // Step 2: Vertical pass - compute column-wise min/max on intermediate buffers\n const resultWidth = regionWidth - labelWidth + 1;\n const resultHeight = regionHeight - labelHeight + 1;\n if (resultWidth <= 0 || resultHeight <= 0) {\n return { windowMax: [], windowMin: [] };\n }\n const windowMax = [];\n const windowMin = [];\n for (let x = 0; x < resultWidth; x++) {\n // Extract column from intermediate buffers\n const colMax = [];\n const colMin = [];\n for (let y = 0; y < regionHeight; y++) {\n colMax.push(rowMax[y]?.[x] ?? 0);\n colMin.push(rowMin[y]?.[x] ?? 255);\n }\n const { maxResults: colMaxResults } = slidingWindowMinMax(colMax, labelHeight);\n const { minResults: colMinResults } = slidingWindowMinMax(colMin, labelHeight);\n for (let y = 0; y < resultHeight; y++) {\n if (!windowMax[y])\n windowMax[y] = [];\n if (!windowMin[y])\n windowMin[y] = [];\n windowMax[y][x] = colMaxResults[y] ?? 0;\n windowMin[y][x] = colMinResults[y] ?? 255;\n }\n }\n return { windowMax, windowMin };\n }\n /**\n * Draw a border on the grayscale image.\n * Used to mark element bounding boxes after they've been processed.\n *\n * @param image - Grayscale image (modified in place)\n * @param x - Left edge of the border\n * @param y - Top edge of the border\n * @param width - Width of the bordered region\n * @param height - Height of the bordered region\n * @param borderWidth - Width of the border in pixels (default 2)\n * @param borderColor - Gray value for the border (default 128)\n */\n function drawBorderOnImage(image, x, y, width, height, borderWidth = 2, borderColor = 128) {\n const imageHeight = image.length;\n const imageWidth = image[0]?.length || 0;\n const x1 = Math.floor(x);\n const y1 = Math.floor(y);\n const x2 = Math.floor(x + width);\n const y2 = Math.floor(y + height);\n // Top edge\n for (let dy = 0; dy < borderWidth; dy++) {\n const py = y1 + dy;\n if (py >= 0 && py < imageHeight) {\n for (let px = x1; px < x2; px++) {\n if (px >= 0 && px < imageWidth) {\n image[py][px] = borderColor;\n }\n }\n }\n }\n // Bottom edge\n for (let dy = 0; dy < borderWidth; dy++) {\n const py = y2 - 1 - dy;\n if (py >= 0 && py < imageHeight) {\n for (let px = x1; px < x2; px++) {\n if (px >= 0 && px < imageWidth) {\n image[py][px] = borderColor;\n }\n }\n }\n }\n // Left edge (excluding corners)\n for (let py = y1 + borderWidth; py < y2 - borderWidth; py++) {\n if (py >= 0 && py < imageHeight) {\n for (let dx = 0; dx < borderWidth; dx++) {\n const px = x1 + dx;\n if (px >= 0 && px < imageWidth) {\n image[py][px] = borderColor;\n }\n }\n }\n }\n // Right edge (excluding corners)\n for (let py = y1 + borderWidth; py < y2 - borderWidth; py++) {\n if (py >= 0 && py < imageHeight) {\n for (let dx = 0; dx < borderWidth; dx++) {\n const px = x2 - 1 - dx;\n if (px >= 0 && px < imageWidth) {\n image[py][px] = borderColor;\n }\n }\n }\n }\n }\n /**\n * Mark a region as occupied with a striped pattern (0, 255, 0, 255...).\n * This guarantees max - min = 255 > tolerance, blocking any future label placement.\n *\n * Called after an element is done processing (bbox + label placed).\n *\n * @param image - Grayscale image (modified in place)\n * @param x1 - Left edge of element bbox\n * @param y1 - Top edge of element bbox\n * @param x2 - Right edge of element bbox\n * @param y2 - Bottom edge of element bbox\n * @param labelX - Label top-left X\n * @param labelY - Label top-left Y\n * @param labelW - Label width\n * @param labelH - Label height\n */\n function markRegionAsOccupied(image, x1, y1, x2, y2, labelX, labelY, labelW, labelH) {\n const imageHeight = image.length;\n const imageWidth = image[0]?.length || 0;\n // Compute combined region (element + label)\n const minX = Math.floor(Math.min(x1, labelX));\n const minY = Math.floor(Math.min(y1, labelY));\n const maxX = Math.floor(Math.max(x2, labelX + labelW));\n const maxY = Math.floor(Math.max(y2, labelY + labelH));\n // Fill with interleaving 0, 255 pattern to guarantee non-uniformity\n for (let py = minY; py < maxY; py++) {\n if (py >= 0 && py < imageHeight) {\n for (let px = minX; px < maxX; px++) {\n if (px >= 0 && px < imageWidth) {\n image[py][px] = ((px + py) % 2 === 0) ? 0 : 255;\n }\n }\n }\n }\n }\n /**\n * Find a valid label position using dynamic convolution on grayscale image.\n *\n * Algorithm:\n * 1. Define candidate region based on element bbox and label dimensions\n * 2. Compute 2D min/max over the region using sliding window\n * 3. Find first position where max - min <= tolerance (uniform region)\n * 4. Return that position or fallback\n *\n * @param elementRect - The element's bounding rect\n * @param labelW - Label width in pixels\n * @param labelH - Label height in pixels\n * @returns Position { x, y } for label top-left corner and whether grayscale was used\n */\n function findLabelPosition(elementRect, labelW, labelH) {\n // If no grayscale image available, return fallback\n if (!grayscaleImage || grayscaleImage.length === 0) {\n return {\n x: Math.max(0, elementRect.x),\n y: Math.max(0, elementRect.y),\n usedGrayscale: false,\n };\n }\n const imageHeight = grayscaleImage.length;\n const imageWidth = grayscaleImage[0]?.length || 0;\n if (imageWidth === 0) {\n return {\n x: Math.max(0, elementRect.x),\n y: Math.max(0, elementRect.y),\n usedGrayscale: false,\n };\n }\n const { x: X, y: Y, width: N, height: M } = elementRect;\n const x1 = Math.floor(X);\n const y1 = Math.floor(Y);\n const x2 = Math.floor(X + N);\n const y2 = Math.floor(Y + M);\n // Define candidate region:\n // Label can be placed from (x1 - labelW, y1 - labelH) to (x2, y2)\n // This ensures label can touch any edge of the element\n // +1 on right/bottom to allow labels adjacent to (not overlapping) the border\n const regionX = Math.max(0, x1 - labelW);\n const regionY = Math.max(0, y1 - labelH);\n const regionX2 = Math.min(imageWidth, x2 + labelW);\n const regionY2 = Math.min(imageHeight, y2 + labelH);\n const regionWidth = regionX2 - regionX;\n const regionHeight = regionY2 - regionY;\n // Compute 2D min/max for the candidate region\n const { windowMax, windowMin } = compute2DMinMaxInRegion(grayscaleImage, regionX, regionY, regionWidth, regionHeight, labelW, labelH);\n if (windowMax.length === 0 || windowMin.length === 0) {\n // Region too small for label, use fallback\n const fallbackX = Math.max(0, Math.min(imageWidth - labelW, x2 - labelW));\n const fallbackY = Math.max(0, y1 - labelH);\n return { x: fallbackX, y: fallbackY, usedGrayscale: true };\n }\n // Search for a uniform position (max - min <= tolerance)\n // Prefer positions near element center\n const resultHeight = windowMax.length;\n const resultWidth = windowMax[0]?.length || 0;\n // Calculate center of candidate region (in result coordinates)\n const centerResultX = Math.floor(resultWidth / 2);\n const centerResultY = Math.floor(resultHeight / 2);\n // BFS from center to find nearest uniform position\n const visited = new Set();\n const queue = [{ rx: centerResultX, ry: centerResultY }];\n while (queue.length > 0) {\n const pos = queue.shift();\n const key = `${pos.rx},${pos.ry}`;\n if (visited.has(key))\n continue;\n visited.add(key);\n // Skip if already visited too many positions (performance limit)\n if (visited.size > 5000)\n break;\n // Check bounds\n if (pos.rx < 0 || pos.rx >= resultWidth || pos.ry < 0 || pos.ry >= resultHeight)\n continue;\n // Convert result coordinates to absolute image coordinates\n const absX = regionX + pos.rx;\n const absY = regionY + pos.ry;\n // Check uniformity: max - min <= tolerance\n const maxVal = windowMax[pos.ry]?.[pos.rx] ?? 255;\n const minVal = windowMin[pos.ry]?.[pos.rx] ?? 0;\n const diff = maxVal - minVal;\n if (diff <= uniformityTolerance) {\n // Uniform position found!\n return { x: absX, y: absY, usedGrayscale: true };\n }\n // Position not uniform, keep searching\n queue.push({ rx: pos.rx - 1, ry: pos.ry });\n queue.push({ rx: pos.rx + 1, ry: pos.ry });\n queue.push({ rx: pos.rx, ry: pos.ry - 1 });\n queue.push({ rx: pos.rx, ry: pos.ry + 1 });\n }\n // No uniform position found - fallback to outside element (top-right)\n const fallbackX = Math.max(0, Math.min(imageWidth - labelW, x2 - labelW));\n const fallbackY = Math.max(0, y1 - labelH);\n return { x: fallbackX, y: fallbackY, usedGrayscale: true };\n }\n /**\n * Mark both the element and label region as occupied after processing.\n * This prevents future labels from overlapping with already-labeled elements.\n *\n * In post-order traversal, children are labeled first. By marking the combined\n * region as occupied, we ensure parent labels don't cross child element areas.\n */\n function markElementAndLabelAsOccupied(elementRect, labelX, labelY, labelW, labelH) {\n if (!grayscaleImage)\n return;\n const { x, y, width, height } = elementRect;\n markRegionAsOccupied(grayscaleImage, Math.floor(x), Math.floor(y), Math.floor(x + width), Math.floor(y + height), Math.floor(labelX), Math.floor(labelY), labelW, labelH);\n }\n const elementsToHighlight = [];\n /**\n * Reorder elements for post-order traversal (children before parents).\n * This ensures child elements get their labels placed before their parent containers.\n *\n * Builds a tree based on actual DOM ancestry between highlighted elements,\n * not the layout parent grouping used for flow detection.\n */\n function getPostOrderElements(elements) {\n if (elements.length === 0)\n return [];\n // Build set of highlighted elements for quick lookup\n const highlightedSet = new Set();\n for (const info of elements) {\n highlightedSet.add(info.element);\n }\n // For each element, find its nearest highlighted ancestor (if any)\n // This builds a tree of highlighted elements based on DOM ancestry\n const childrenOfHighlighted = new Map();\n for (const info of elements) {\n let highlightedParent = null;\n let current = info.element.parentElement;\n // Walk up the DOM to find nearest highlighted ancestor\n while (current && current !== document.body) {\n if (highlightedSet.has(current)) {\n highlightedParent = current;\n break;\n }\n current = current.parentElement;\n }\n // Group elements by their highlighted parent\n if (!childrenOfHighlighted.has(highlightedParent)) {\n childrenOfHighlighted.set(highlightedParent, []);\n }\n childrenOfHighlighted.get(highlightedParent).push(info.element);\n }\n // Build element -> ElementToHighlight lookup\n const elementToInfo = new Map();\n for (const info of elements) {\n elementToInfo.set(info.element, info);\n }\n // Recursive post-order collection\n const result = [];\n const visited = new Set();\n function visit(element) {\n if (visited.has(element))\n return;\n visited.add(element);\n // First, visit all highlighted children (post-order: children before parent)\n const children = childrenOfHighlighted.get(element) || [];\n for (const child of children) {\n visit(child);\n }\n // Then add this element\n const info = elementToInfo.get(element);\n if (info)\n result.push(info);\n }\n // Start from roots (elements with no highlighted parent)\n const roots = childrenOfHighlighted.get(null) || [];\n for (const root of roots) {\n visit(root);\n }\n return result;\n }\n /**\n * Process elements in recursive tree order:\n * - Pre-order: Create bounding boxes and mark in hot map\n * - Post-order: Create labels and mark in hot map\n *\n * This ensures children's boxes are visible before parent places its label,\n * and children's labels are placed before parent's label.\n */\n // Collected element data during boxes phase, for return\n const collectedElementData = [];\n function processElementTreeRecursively(elements) {\n if (elements.length === 0)\n return;\n // Build tree structure based on DOM ancestry (same as getPostOrderElements)\n const highlightedSet = new Set();\n for (const info of elements) {\n highlightedSet.add(info.element);\n }\n const childrenOfHighlighted = new Map();\n for (const info of elements) {\n let highlightedParent = null;\n let current = info.element.parentElement;\n while (current && current !== document.body) {\n if (highlightedSet.has(current)) {\n highlightedParent = current;\n break;\n }\n current = current.parentElement;\n }\n if (!childrenOfHighlighted.has(highlightedParent)) {\n childrenOfHighlighted.set(highlightedParent, []);\n }\n childrenOfHighlighted.get(highlightedParent).push(info.element);\n }\n // Build element -> ElementToHighlight lookup\n const elementToInfo = new Map();\n for (const info of elements) {\n elementToInfo.set(info.element, info);\n }\n // Get or create highlight container\n let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);\n if (!container) {\n container = document.createElement(\"div\");\n container.id = HIGHLIGHT_CONTAINER_ID;\n container.style.position = \"fixed\";\n container.style.pointerEvents = \"none\";\n container.style.top = \"0\";\n container.style.left = \"0\";\n container.style.width = \"100%\";\n container.style.height = \"100%\";\n container.style.zIndex = \"2147483647\";\n container.style.backgroundColor = 'transparent';\n document.body.appendChild(container);\n }\n // Store element info after box creation for later label creation\n const renderInfoMap = new Map();\n // Recursive function that processes elements in correct order\n function processElement(element) {\n const info = elementToInfo.get(element);\n if (!info)\n return;\n // === PRE-ORDER: Create bounding box ===\n const renderInfo = createBoundingBoxForElement(info.element, info.index, info.parentIframe, container);\n if (renderInfo) {\n renderInfoMap.set(element, renderInfo);\n // In boxes phase, collect element data for return (no grayscale drawing needed)\n if (phase === 'boxes') {\n collectedElementData.push({\n index: renderInfo.index,\n xpath: getXPathTree(info.element),\n rect: { ...renderInfo.elementRect },\n color: renderInfo.color,\n });\n }\n // In legacy mode (no phase), draw border on grayscale image\n // This ensures child labels don't get placed crossing this element's border\n if (!phase && grayscaleImage) {\n drawBorderOnImage(grayscaleImage, renderInfo.elementRect.x, renderInfo.elementRect.y, renderInfo.elementRect.width, renderInfo.elementRect.height, 2, // borderWidth\n 128 // borderColor (mid-gray to break uniformity)\n );\n }\n }\n // === RECURSE: Process children ===\n const children = childrenOfHighlighted.get(element) || [];\n for (const child of children) {\n processElement(child);\n }\n // === POST-ORDER: Create label (skip in boxes phase) ===\n if (renderInfo && phase !== 'boxes') {\n createLabelForElement(renderInfo);\n }\n }\n // Process roots (elements with no highlighted parent)\n const roots = childrenOfHighlighted.get(null) || [];\n for (const root of roots) {\n processElement(root);\n }\n }\n /**\n * Process labels phase using pre-collected element data.\n * Called when phase === 'labels' with inputElementData.\n */\n function processLabelsPhase() {\n if (!inputElementData || inputElementData.length === 0)\n return;\n streamLog(`[dom-tree] Labels phase: processing ${inputElementData.length} elements`);\n // Get or create highlight container (should already exist from boxes phase)\n let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);\n if (!container) {\n container = document.createElement(\"div\");\n container.id = HIGHLIGHT_CONTAINER_ID;\n container.style.position = \"fixed\";\n container.style.pointerEvents = \"none\";\n container.style.top = \"0\";\n container.style.left = \"0\";\n container.style.width = \"100%\";\n container.style.height = \"100%\";\n container.style.zIndex = \"2147483647\";\n container.style.backgroundColor = 'transparent';\n document.body.appendChild(container);\n }\n // Sort by index to maintain correct order\n const sortedData = [...inputElementData].sort((a, b) => a.index - b.index);\n // Create labels for each element\n for (const data of sortedData) {\n const renderInfo = {\n element: null, // Not needed for label creation\n index: data.index,\n parentIframe: null,\n elementRect: data.rect,\n color: data.color,\n container,\n };\n createLabelForElement(renderInfo);\n }\n }\n /**\n * Create bounding box overlays for an element and append to container.\n * Returns element render info for later label creation.\n */\n function createBoundingBoxForElement(element, index, parentIframe, container) {\n if (!element)\n return null;\n // Get element client rects\n let rects = element.getClientRects();\n if (!rects || rects.length === 0)\n return null;\n // Transform rects if inside an iframe\n if (parentIframe) {\n const transformedRects = [];\n const iframeRect = parentIframe.getBoundingClientRect();\n const iframeStyle = window.getComputedStyle(parentIframe);\n const borderLeft = parseFloat(iframeStyle.borderLeftWidth) || 0;\n const borderTop = parseFloat(iframeStyle.borderTopWidth) || 0;\n const paddingLeft = parseFloat(iframeStyle.paddingLeft) || 0;\n const paddingTop = parseFloat(iframeStyle.paddingTop) || 0;\n const contentOffsetX = borderLeft + paddingLeft;\n const contentOffsetY = borderTop + paddingTop;\n let scaleX = 1, scaleY = 1;\n const transform = iframeStyle.transform;\n if (transform && transform !== 'none') {\n const scaleMatch = transform.match(/scale\\(([^)]+)\\)/);\n if (scaleMatch) {\n const scaleValues = scaleMatch[1].split(',').map(v => parseFloat(v.trim()));\n scaleX = scaleValues[0] || 1;\n scaleY = scaleValues[1] || scaleX;\n }\n else {\n const matrixMatch = transform.match(/matrix\\(([^)]+)\\)/);\n if (matrixMatch) {\n const values = matrixMatch[1].split(',').map(v => parseFloat(v.trim()));\n if (values.length >= 6) {\n scaleX = values[0];\n scaleY = values[3];\n }\n }\n }\n }\n let scrollLeft = 0, scrollTop = 0;\n try {\n const iframeDoc = parentIframe.contentDocument || parentIframe.contentWindow?.document;\n if (iframeDoc) {\n scrollLeft = iframeDoc.documentElement?.scrollLeft || iframeDoc.body?.scrollLeft || 0;\n scrollTop = iframeDoc.documentElement?.scrollTop || iframeDoc.body?.scrollTop || 0;\n }\n }\n catch (e) {\n console.warn(\"Cannot access iframe scroll position:\", e);\n }\n for (const rect of rects) {\n const scaledWidth = rect.width * scaleX;\n const scaledHeight = rect.height * scaleY;\n const scaledTop = rect.top * scaleY;\n const scaledLeft = rect.left * scaleX;\n const scaledScrollTop = scrollTop * scaleY;\n const scaledScrollLeft = scrollLeft * scaleX;\n transformedRects.push({\n top: scaledTop + iframeRect.top + contentOffsetY - scaledScrollTop,\n left: scaledLeft + iframeRect.left + contentOffsetX - scaledScrollLeft,\n bottom: scaledTop + scaledHeight + iframeRect.top + contentOffsetY - scaledScrollTop,\n right: scaledLeft + scaledWidth + iframeRect.left + contentOffsetX - scaledScrollLeft,\n width: scaledWidth,\n height: scaledHeight,\n x: scaledLeft + iframeRect.left + contentOffsetX - scaledScrollLeft,\n y: scaledTop + iframeRect.top + contentOffsetY - scaledScrollTop\n });\n }\n rects = transformedRects;\n }\n // Generate color based on index\n const colors = [\n \"#E53935\", \"#1E88E5\", \"#7B1FA2\", \"#00897B\", \"#F4511E\",\n \"#3949AB\", \"#C2185B\", \"#00796B\", \"#5E35B1\", \"#D81B60\",\n \"#039BE5\", \"#388E3C\"\n ];\n const color = colors[index % colors.length];\n // Create bounding box overlays\n for (const rect of rects) {\n if (rect.width === 0 || rect.height === 0)\n continue;\n const overlay = document.createElement(\"div\");\n overlay.style.position = \"fixed\";\n overlay.style.border = `1px solid ${color}`;\n overlay.style.backgroundColor = \"none\";\n overlay.style.pointerEvents = \"none\";\n overlay.style.boxSizing = \"border-box\";\n overlay.setAttribute(\"data-element-index\", String(index));\n overlay.style.top = `${rect.top}px`;\n overlay.style.left = `${rect.left}px`;\n overlay.style.width = `${rect.width}px`;\n overlay.style.height = `${rect.height}px`;\n container.appendChild(overlay);\n }\n // Return element info for label creation\n const firstRect = rects[0];\n return {\n element,\n index,\n parentIframe,\n elementRect: {\n x: firstRect.left,\n y: firstRect.top,\n width: firstRect.width,\n height: firstRect.height\n },\n color,\n container\n };\n }\n /**\n * Create and position label for an element and append to container.\n */\n function createLabelForElement(renderInfo) {\n const { index, elementRect, color, container } = renderInfo;\n // Capture BEFORE snapshot if debug mode enabled (stream via callback)\n streamLog(`[dom-tree] Element ${index}: captureDebugSnapshots=${captureDebugSnapshots}, hasGrayscale=${!!grayscaleImage}, hasOnSnapshot=${!!onSnapshot}`);\n if (captureDebugSnapshots && grayscaleImage && onSnapshot) {\n streamLog(`[dom-tree] Calling onSnapshot for element ${index} (before)`);\n onSnapshot({ elementIndex: index, type: 'before', data: grayscaleImage.map(row => [...row]) });\n }\n // Calculate label dimensions based on digit count\n const digits = index.toString().length;\n const labelWidth = (digits * 8) + 8;\n const labelHeight = 16;\n // Find label position using grayscale image or fallback to heuristics\n let labelTop = 0;\n let labelLeft = 0;\n const grayscaleResult = findLabelPosition(elementRect, labelWidth, labelHeight);\n if (grayscaleResult.usedGrayscale) {\n labelTop = grayscaleResult.y;\n labelLeft = grayscaleResult.x;\n }\n else {\n // Fallback to heuristic positioning (assume vertical flow)\n const isNarrow = elementRect.width <= 32;\n const centerX = elementRect.x + elementRect.width / 2;\n let positions;\n if (isNarrow) {\n positions = [\n { top: elementRect.y + (elementRect.height - labelHeight) / 2, anchorX: elementRect.x, hAlign: 'right', name: 'left' },\n { top: elementRect.y + (elementRect.height - labelHeight) / 2, anchorX: elementRect.x + elementRect.width, hAlign: 'left', name: 'right' },\n { top: elementRect.y - labelHeight, anchorX: centerX, hAlign: 'center', name: 'top' },\n { top: elementRect.y + elementRect.height, anchorX: centerX, hAlign: 'center', name: 'bottom' },\n ];\n }\n else {\n positions = [\n { top: elementRect.y + (elementRect.height - labelHeight) / 2, anchorX: elementRect.x, hAlign: 'right', name: 'left' },\n { top: elementRect.y + (elementRect.height - labelHeight) / 2, anchorX: elementRect.x + elementRect.width, hAlign: 'left', name: 'right' },\n { top: elementRect.y - labelHeight, anchorX: elementRect.x + elementRect.width, hAlign: 'right', name: 'top-right' },\n { top: elementRect.y - labelHeight, anchorX: elementRect.x, hAlign: 'left', name: 'top-left' },\n ];\n }\n // Use first position as default\n const pos = positions[0];\n labelTop = Math.max(0, Math.min(pos.top, window.innerHeight - labelHeight));\n if (pos.hAlign === 'right') {\n labelLeft = pos.anchorX - labelWidth;\n }\n else if (pos.hAlign === 'center') {\n labelLeft = pos.anchorX - labelWidth / 2;\n }\n else {\n labelLeft = pos.anchorX;\n }\n labelLeft = Math.max(0, Math.min(labelLeft, window.innerWidth - labelWidth));\n }\n // Always mark the label position on grayscale image to prevent future overlaps\n markElementAndLabelAsOccupied(elementRect, labelLeft, labelTop, labelWidth, labelHeight);\n // Capture AFTER snapshot if debug mode enabled (stream via callback)\n if (captureDebugSnapshots && grayscaleImage && onSnapshot) {\n streamLog(`[dom-tree] Calling onSnapshot for element ${index} (after)`);\n onSnapshot({ elementIndex: index, type: 'after', data: grayscaleImage.map(row => [...row]) });\n }\n // Create label element with explicit dimensions matching theoretical calculation\n const label = document.createElement(\"div\");\n label.className = \"playwright-highlight-label\";\n label.style.position = \"fixed\";\n label.style.background = color;\n label.style.color = \"white\";\n label.style.padding = \"1px 4px\";\n label.style.fontSize = \"12px\";\n label.style.width = `${labelWidth}px`;\n label.style.height = `${labelHeight}px`;\n label.style.boxSizing = \"border-box\";\n label.style.textAlign = \"center\";\n label.style.lineHeight = `${labelHeight - 2}px`;\n label.textContent = String(index);\n label.setAttribute(\"data-element-index\", String(index));\n label.style.top = `${labelTop}px`;\n label.style.left = `${labelLeft}px`;\n container.appendChild(label);\n }\n /**\n * Build tree entries for debug output showing element hierarchy.\n *\n * The tree collapses single-child chains: if A contains only B, which contains only C (highlighted),\n * then A, B, C collapse into just C.\n *\n * Parents are only included if they have more than one highlighted descendant.\n */\n function buildTreeEntries() {\n const treeEntries = [];\n if (elementsToHighlight.length === 0)\n return treeEntries;\n // For each highlighted element, find all ancestors and count highlighted descendants\n const ancestorCounts = new Map();\n for (const { element } of elementsToHighlight) {\n let current = element.parentElement;\n while (current && current !== document.body) {\n ancestorCounts.set(current, (ancestorCounts.get(current) || 0) + 1);\n current = current.parentElement;\n }\n }\n // Build tree structure: map each element to its layout parent\n const childrenMap = new Map(); // parent -> children\n for (const { element } of elementsToHighlight) {\n let layoutParent = null;\n let current = element.parentElement;\n while (current && current !== document.body) {\n const count = ancestorCounts.get(current) || 0;\n if (count > 1) {\n layoutParent = current;\n break;\n }\n current = current.parentElement;\n }\n // Group children by parent\n if (!childrenMap.has(layoutParent)) {\n childrenMap.set(layoutParent, []);\n }\n childrenMap.get(layoutParent).push(element);\n }\n // Output tree structure to debug logs and build tree entries\n const elementToIndex = new Map();\n for (const { element, index } of elementsToHighlight) {\n elementToIndex.set(element, index);\n }\n const describeElement = (el) => {\n const tag = el.tagName.toLowerCase();\n const id = el.id ? `#${el.id}` : '';\n const cls = el.className && typeof el.className === 'string'\n ? '.' + el.className.split(' ').slice(0, 2).join('.')\n : '';\n return `<${tag}${id}${cls.slice(0, 30)}>`;\n };\n debugLog(`[Tree] Detected elements tree:`);\n // Get unique layout parents and sort by DOM order\n const parents = Array.from(childrenMap.keys());\n for (const parent of parents) {\n const children = childrenMap.get(parent) || [];\n const parentDesc = parent ? describeElement(parent) : '(root)';\n debugLog(`[Tree] ${parentDesc}`);\n // Add parent entry to tree\n treeEntries.push({\n type: 'parent',\n label: parentDesc,\n });\n for (const child of children) {\n const idx = elementToIndex.get(child) ?? -1;\n debugLog(`[Tree] [${idx}] ${describeElement(child)}`);\n // Add element entry to tree\n treeEntries.push({\n type: 'element',\n highlightIndex: idx,\n label: describeElement(child),\n });\n }\n }\n return treeEntries;\n }\n /**\n * Gets the position of an element in its parent.\n */\n function getElementPosition(currentElement) {\n if (!currentElement.parentElement) {\n return 0; // No parent means no siblings\n }\n const tagName = currentElement.nodeName.toLowerCase();\n const siblings = Array.from(currentElement.parentElement.children)\n .filter((sib) => sib.nodeName.toLowerCase() === tagName);\n if (siblings.length === 1) {\n return 0; // Only element of its type\n }\n const index = siblings.indexOf(currentElement) + 1; // 1-based index\n return index;\n }\n function getXPathTree(element, stopAtBoundary = true) {\n if (xpathCache.has(element))\n return xpathCache.get(element);\n const segments = [];\n let currentElement = element;\n while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {\n // Stop if we hit a shadow root or iframe\n if (stopAtBoundary &&\n (currentElement.parentNode instanceof ShadowRoot ||\n currentElement.parentNode instanceof HTMLIFrameElement)) {\n break;\n }\n const position = getElementPosition(currentElement);\n const tagName = currentElement.nodeName.toLowerCase();\n const xpathIndex = position > 0 ? `[${position}]` : \"\";\n segments.unshift(`${tagName}${xpathIndex}`);\n currentElement = currentElement.parentNode;\n }\n const result = segments.join(\"/\");\n xpathCache.set(element, result);\n return result;\n }\n /**\n * Checks if a text node is visible.\n */\n function isTextNodeVisible(textNode) {\n try {\n // Special case: when viewportExpansion is -1, consider all text nodes as visible\n if (viewportExpansion === -1) {\n // Still check parent visibility for basic filtering\n const parentElement = textNode.parentElement;\n if (!parentElement)\n return false;\n try {\n return parentElement.checkVisibility({\n checkOpacity: true,\n checkVisibilityCSS: true,\n });\n }\n catch (e) {\n // Fallback if checkVisibility is not supported\n const style = window.getComputedStyle(parentElement);\n return style.display !== 'none' &&\n style.visibility !== 'hidden' &&\n style.opacity !== '0';\n }\n }\n const range = document.createRange();\n range.selectNodeContents(textNode);\n const rects = range.getClientRects(); // Use getClientRects for Range\n if (!rects || rects.length === 0) {\n return false;\n }\n let isAnyRectVisible = false;\n let isAnyRectInViewport = false;\n for (const rect of rects) {\n // Check size\n if (rect.width > 0 && rect.height > 0) {\n isAnyRectVisible = true;\n // Viewport check for this rect\n if (!(rect.bottom < -viewportExpansion ||\n rect.top > window.innerHeight + viewportExpansion ||\n rect.right < -viewportExpansion ||\n rect.left > window.innerWidth + viewportExpansion)) {\n isAnyRectInViewport = true;\n break; // Found a visible rect in viewport, no need to check others\n }\n }\n }\n if (!isAnyRectVisible || !isAnyRectInViewport) {\n return false;\n }\n // Check parent visibility\n const parentElement = textNode.parentElement;\n if (!parentElement)\n return false;\n try {\n return parentElement.checkVisibility({\n checkOpacity: true,\n checkVisibilityCSS: true,\n });\n }\n catch (e) {\n // Fallback if checkVisibility is not supported\n const style = window.getComputedStyle(parentElement);\n return style.display !== 'none' &&\n style.visibility !== 'hidden' &&\n style.opacity !== '0';\n }\n }\n catch (e) {\n console.warn('Error checking text node visibility:', e);\n return false;\n }\n }\n /**\n * Checks if an element is accepted.\n */\n function isElementAccepted(element) {\n if (!element || !element.tagName)\n return false;\n // Always accept body and common container elements\n const alwaysAccept = new Set([\n \"body\", \"div\", \"main\", \"article\", \"section\", \"nav\", \"header\", \"footer\"\n ]);\n const tagName = element.tagName.toLowerCase();\n if (alwaysAccept.has(tagName))\n return true;\n const leafElementDenyList = new Set([\n \"svg\",\n \"script\",\n \"style\",\n \"link\",\n \"meta\",\n \"noscript\",\n \"template\",\n ]);\n return !leafElementDenyList.has(tagName);\n }\n /**\n * Checks if an element is visible.\n */\n function isElementVisible(element) {\n if (alwaysHighlightFileInput && element.tagName.toLowerCase() === \"input\" && element.type === \"file\")\n return true;\n // SVG elements need special handling for visibility\n if (element.tagName.toLowerCase() === \"svg\") {\n const rect = getCachedBoundingRect(element);\n const style = getCachedComputedStyle(element);\n return (rect !== null &&\n rect.width > 0 &&\n rect.height > 0 &&\n style?.visibility !== \"hidden\" &&\n style?.display !== \"none\");\n }\n const style = getCachedComputedStyle(element);\n return (element.offsetWidth > 0 &&\n element.offsetHeight > 0 &&\n style?.visibility !== \"hidden\" &&\n style?.display !== \"none\");\n }\n /**\n * Checks if an element is clickable (responds to click events).\n */\n function shouldMarkAsClickable(element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return false;\n }\n const tagName = element.tagName.toLowerCase();\n // Primarily clickable elements\n const primaryClickableElements = new Set([\n \"a\", // Links\n \"button\", // Buttons\n \"summary\", // Summary element (clickable part of details)\n \"label\", // Form labels (often clickable)\n \"option\", // Select options\n \"optgroup\", // Option groups\n ]);\n if (primaryClickableElements.has(tagName)) {\n return false;\n }\n const role = element.getAttribute(\"role\");\n // Clickable roles\n const clickableRoles = new Set([\n 'button', // Directly clickable element\n 'link', // Clickable link\n 'menuitem', // Clickable menu item\n 'menuitemradio', // Radio-style menu item (selectable)\n 'menuitemcheckbox', // Checkbox-style menu item (toggleable)\n 'radio', // Radio button (selectable)\n 'checkbox', // Checkbox (toggleable)\n 'tab', // Tab (clickable to switch content)\n 'switch', // Toggle switch (clickable to change state)\n 'option', // Selectable option in a list\n ]);\n if (role && clickableRoles.has(role)) {\n return true;\n }\n // Check for dropdown indicators\n if (hasAnyClassName(element, buttonClassNames)) {\n return true; // Return true for dropdown elements\n }\n if (element.getAttribute('data-toggle') === 'dropdown' ||\n element.getAttribute('aria-haspopup')) {\n return true;\n }\n const clickEvents = ['click', 'mousedown', 'mouseup', 'dblclick'];\n const listenedEvents = getCachedNodeEventListeners(element);\n if (listenedEvents && listenedEvents.length > 0) {\n for (const eventType of clickEvents) {\n if (listenedEvents.includes(eventType)) {\n return true;\n }\n }\n }\n return false;\n }\n /**\n * Checks if an element is interactive.\n *\n * lots of comments, and uncommented code - to show the logic of what we already tried\n *\n * One of the things we tried at the beginning was also to use event listeners, and other fancy class, style stuff -> what actually worked best was just combining most things with computed cursor style :)\n */\n function isInteractiveElement(element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return false;\n }\n // Cache the tagName and style lookups\n const tagName = element.tagName.toLowerCase();\n const style = getCachedComputedStyle(element);\n // Define interactive cursors\n const interactiveCursors = new Set([\n 'pointer', // Link/clickable elements\n 'move', // Movable elements\n 'text', // Text selection\n 'grab', // Grabbable elements\n 'grabbing', // Currently grabbing\n 'cell', // Table cell selection\n 'copy', // Copy operation\n 'alias', // Alias creation\n 'all-scroll', // Scrollable content\n 'col-resize', // Column resize\n 'context-menu', // Context menu available\n 'crosshair', // Precise selection\n 'e-resize', // East resize\n 'ew-resize', // East-west resize\n 'help', // Help available\n 'n-resize', // North resize\n 'ne-resize', // Northeast resize\n 'nesw-resize', // Northeast-southwest resize\n 'ns-resize', // North-south resize\n 'nw-resize', // Northwest resize\n 'nwse-resize', // Northwest-southeast resize\n 'row-resize', // Row resize\n 's-resize', // South resize\n 'se-resize', // Southeast resize\n 'sw-resize', // Southwest resize\n 'vertical-text', // Vertical text selection\n 'w-resize', // West resize\n 'zoom-in', // Zoom in\n 'zoom-out' // Zoom out\n ]);\n // Define non-interactive cursors\n const nonInteractiveCursors = new Set([\n 'not-allowed', // Action not allowed\n 'no-drop', // Drop not allowed\n 'wait', // Processing\n 'progress', // In progress\n 'initial', // Initial value\n 'inherit' // Inherited value\n //? Let's just include all potentially clickable elements that are not specifically blocked\n // 'none', // No cursor\n // 'default', // Default cursor\n // 'auto', // Browser default\n ]);\n /**\n * Checks if an element has an interactive pointer.\n */\n function doesElementHaveInteractivePointer(element) {\n if (element.tagName.toLowerCase() === \"html\")\n return false;\n if (style?.cursor && interactiveCursors.has(style.cursor))\n return true;\n return false;\n }\n // Disabled for now, since it adds too many false positives\n // let isInteractiveCursor = doesElementHaveInteractivePointer(element);\n // // Genius fix for almost all interactive elements\n // if (isInteractiveCursor) {\n // return true;\n // }\n const interactiveElements = new Set([\n \"a\", // Links\n \"button\", // Buttons\n \"input\", // All input types (text, checkbox, radio, etc.)\n \"select\", // Dropdown menus\n \"textarea\", // Text areas\n \"summary\", // Summary element (clickable part of details)\n \"label\", // Form labels (often clickable)\n \"option\", // Select options\n \"optgroup\", // Option groups\n \"fieldset\", // Form fieldsets (can be interactive with legend)\n \"legend\", // Fieldset legends\n ]);\n // Define explicit disable attributes and properties\n const explicitDisableTags = new Set([\n 'disabled', // Standard disabled attribute\n // 'aria-disabled', // ARIA disabled state\n // 'readonly', // Read-only state\n // 'aria-readonly', // ARIA read-only state\n // 'aria-hidden', // Hidden from accessibility\n // 'hidden', // Hidden attribute\n // 'inert', // Inert attribute\n // 'aria-inert', // ARIA inert state\n // 'tabindex=\"-1\"', // Removed from tab order\n // 'aria-hidden=\"true\"' // Hidden from screen readers\n ]);\n // Check for non-interactive cursor\n if (style?.cursor && nonInteractiveCursors.has(style.cursor)) {\n return false;\n }\n // handle inputs, select, checkbox, radio, textarea, button and make sure they are not cursor style disabled/not-allowed\n if (interactiveElements.has(tagName)) {\n // Check for explicit disable attributes\n for (const disableTag of explicitDisableTags) {\n if (element.hasAttribute(disableTag) ||\n element.getAttribute(disableTag) === 'true' ||\n element.getAttribute(disableTag) === '') {\n return false;\n }\n }\n // Check for disabled property on form elements\n if (element.disabled) {\n return false;\n }\n // Don't mark as non-interactive yet\n // Check for readonly property on form elements\n if (element.readOnly) {\n // return false;\n }\n // Check for inert property\n if (element.inert) {\n return false;\n }\n return true;\n }\n const role = element.getAttribute(\"role\");\n const ariaRole = element.getAttribute(\"aria-role\");\n // Check for contenteditable attribute\n if (element.getAttribute(\"contenteditable\") === \"true\" || element.isContentEditable) {\n return true;\n }\n // Added enhancement to capture dropdown interactive elements\n if (hasAnyClassName(element, buttonClassNames) ||\n hasAnyClassName(element, interactiveClassNames) ||\n hasAnyClassName(element, cursorPointerClassNames) ||\n element.getAttribute('data-index') ||\n element.getAttribute('data-toggle') === 'dropdown' ||\n element.getAttribute('aria-haspopup')) {\n return true;\n }\n const interactiveRoles = new Set([\n 'button', // Directly clickable element\n 'link', // Clickable link\n 'menuitem', // Clickable menu item\n 'menuitemradio', // Radio-style menu item (selectable)\n 'menuitemcheckbox', // Checkbox-style menu item (toggleable)\n 'radio', // Radio button (selectable)\n 'checkbox', // Checkbox (toggleable)\n 'tab', // Tab (clickable to switch content)\n 'switch', // Toggle switch (clickable to change state)\n 'slider', // Slider control (draggable)\n 'spinbutton', // Number input with up/down controls\n 'combobox', // Dropdown with text input\n 'searchbox', // Search input field\n 'textbox', // Text input field\n 'listbox', // Selectable list\n 'option', // Selectable option in a list\n 'scrollbar' // Scrollable control\n ]);\n // Basic role/attribute checks\n const hasInteractiveRole = (role && interactiveRoles.has(role)) ||\n (ariaRole && interactiveRoles.has(ariaRole));\n if (hasInteractiveRole)\n return true;\n const listenedEvents = getCachedNodeEventListeners(element);\n if (listenedEvents && listenedEvents.length > 0) {\n for (const eventType of INTERACTION_EVENTS) {\n if (listenedEvents.includes(eventType)) {\n return true;\n }\n }\n }\n return false;\n }\n /**\n * Checks if an element is the topmost element at its position.\n */\n function isTopElement(element) {\n // Special case: when viewportExpansion is -1, consider all elements as \"top\" elements\n if (viewportExpansion === -1) {\n return true;\n }\n const rects = getCachedClientRects(element);\n if (!rects || rects.length === 0) {\n return false; // No geometry, cannot be top\n }\n let isAnyRectInViewport = false;\n for (const rect of rects) {\n // Use the same logic as isInExpandedViewport check\n if (rect.width > 0 && rect.height > 0 && !( // Only check non-empty rects\n rect.bottom < -viewportExpansion ||\n rect.top > window.innerHeight + viewportExpansion ||\n rect.right < -viewportExpansion ||\n rect.left > window.innerWidth + viewportExpansion)) {\n isAnyRectInViewport = true;\n break;\n }\n }\n if (!isAnyRectInViewport) {\n return false; // All rects are outside the viewport area\n }\n // Find the correct document context and root element\n let doc = element.ownerDocument;\n // If we're in an iframe, elements are considered top by default\n if (doc !== window.document) {\n return true;\n }\n // For shadow DOM, we need to check within its own root context\n const shadowRoot = element.getRootNode();\n if (shadowRoot instanceof ShadowRoot) {\n const centerX = rects[Math.floor(rects.length / 2)].left + rects[Math.floor(rects.length / 2)].width / 2;\n const centerY = rects[Math.floor(rects.length / 2)].top + rects[Math.floor(rects.length / 2)].height / 2;\n try {\n const topEl = shadowRoot.elementFromPoint(centerX, centerY);\n if (!topEl)\n return false;\n let current = topEl;\n while (current && current !== shadowRoot) {\n if (current === element)\n return true;\n current = current.parentElement;\n }\n return false;\n }\n catch (e) {\n return true;\n }\n }\n // For elements in viewport, check if they're topmost\n const centerX = rects[Math.floor(rects.length / 2)].left + rects[Math.floor(rects.length / 2)].width / 2;\n const centerY = rects[Math.floor(rects.length / 2)].top + rects[Math.floor(rects.length / 2)].height / 2;\n try {\n const topEl = document.elementFromPoint(centerX, centerY);\n if (!topEl)\n return false;\n let current = topEl;\n while (current && current !== document.documentElement) {\n if (current === element)\n return true;\n current = current.parentElement;\n }\n return false;\n }\n catch (e) {\n return true;\n }\n }\n /**\n * Checks if an element is within the expanded viewport.\n */\n function isInExpandedViewport(element, viewportExpansion) {\n if (viewportExpansion === -1) {\n return true;\n }\n const rects = element.getClientRects();\n if (!rects || rects.length === 0) {\n // Fallback to getBoundingClientRect if getClientRects is empty,\n // useful for elements like <svg> that might not have client rects but have a bounding box.\n const boundingRect = getCachedBoundingRect(element);\n if (!boundingRect || boundingRect.width === 0 || boundingRect.height === 0) {\n return false;\n }\n return !(boundingRect.bottom < -viewportExpansion ||\n boundingRect.top > window.innerHeight + viewportExpansion ||\n boundingRect.right < -viewportExpansion ||\n boundingRect.left > window.innerWidth + viewportExpansion);\n }\n // Check if *any* client rect is within the viewport\n for (const rect of rects) {\n if (rect.width === 0 || rect.height === 0)\n continue; // Skip empty rects\n if (!(rect.bottom < -viewportExpansion ||\n rect.top > window.innerHeight + viewportExpansion ||\n rect.right < -viewportExpansion ||\n rect.left > window.innerWidth + viewportExpansion)) {\n return true; // Found at least one rect in the viewport\n }\n }\n return false; // No rects were found in the viewport\n }\n // /**\n // * Gets the effective scroll of an element.\n // *\n // * @param {HTMLElement} element - The element to get the effective scroll for.\n // * @returns {Object} The effective scroll of the element.\n // */\n // function getEffectiveScroll(element) {\n // let currentEl = element;\n // let scrollX = 0;\n // let scrollY = 0;\n // while (currentEl && currentEl !== document.documentElement) {\n // if (currentEl.scrollLeft || currentEl.scrollTop) {\n // scrollX += currentEl.scrollLeft;\n // scrollY += currentEl.scrollTop;\n // }\n // currentEl = currentEl.parentElement;\n // }\n // scrollX += window.scrollX;\n // scrollY += window.scrollY;\n // return { scrollX, scrollY };\n // }\n /**\n * Checks if an element is an interactive candidate.\n */\n function isInteractiveCandidate(element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE)\n return false;\n const tagName = element.tagName.toLowerCase();\n // Fast-path for common interactive elements\n const interactiveElements = new Set([\n \"a\", \"button\", \"input\", \"select\", \"textarea\", \"summary\", \"label\"\n ]);\n if (interactiveElements.has(tagName))\n return true;\n // Quick attribute checks without getting full lists\n const hasQuickInteractiveAttr = element.hasAttribute(\"onclick\") ||\n element.hasAttribute(\"role\") ||\n element.hasAttribute(\"tabindex\") ||\n element.hasAttribute(\"aria-\") ||\n element.hasAttribute(\"data-action\") ||\n element.getAttribute(\"contenteditable\") === \"true\";\n return hasQuickInteractiveAttr;\n }\n // --- Define constants for distinct interaction check ---\n const DISTINCT_INTERACTIVE_TAGS = new Set([\n 'a', 'button', 'input', 'select', 'textarea', 'summary', 'label', 'option'\n ]);\n const INTERACTIVE_ROLES = new Set([\n 'button', 'link', 'menuitem', 'menuitemradio', 'menuitemcheckbox',\n 'radio', 'checkbox', 'tab', 'switch', 'slider', 'spinbutton',\n 'combobox', 'searchbox', 'textbox', 'listbox', 'option', 'scrollbar'\n ]);\n /**\n * Heuristically determines if an element should be considered as independently interactive,\n * even if it's nested inside another interactive container.\n *\n * This function helps detect deeply nested actionable elements (e.g., menu items within a button)\n * that may not be picked up by strict interactivity checks.\n */\n function isHeuristicallyInteractive(element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE)\n return false;\n // Skip non-visible elements early for performance\n if (!isElementVisible(element))\n return false;\n // Check for common attributes that often indicate interactivity\n const hasInteractiveAttributes = element.hasAttribute('role') ||\n element.hasAttribute('tabindex') ||\n element.hasAttribute('onclick') ||\n typeof element.onclick === 'function';\n // Check for semantic class names suggesting interactivity\n const hasInteractiveClass = heuristicClassPattern.test(element.className || '');\n // Determine whether the element is inside a known interactive container\n const isInKnownContainer = Boolean(element.closest(containerSelectors));\n // Ensure the element has at least one visible child (to avoid marking empty wrappers)\n const hasVisibleChildren = [...element.children].some(child => isElementVisible(child));\n // Avoid highlighting elements whose parent is <body> (top-level wrappers)\n const isParentBody = element.parentElement && element.parentElement.isSameNode(document.body);\n return ((isInteractiveElement(element) || hasInteractiveAttributes || hasInteractiveClass) &&\n hasVisibleChildren &&\n isInKnownContainer &&\n !isParentBody);\n }\n /**\n * Checks if an element likely represents a distinct interaction\n * separate from its parent (if the parent is also interactive).\n */\n function isElementDistinctInteraction(element, nodeData) {\n if (nodeData.isScrollable) {\n return true;\n }\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return false;\n }\n const tagName = element.tagName.toLowerCase();\n const role = element.getAttribute('role');\n // Check if it's an iframe - always distinct boundary\n if (tagName === 'iframe') {\n return true;\n }\n // Check tag name\n if (DISTINCT_INTERACTIVE_TAGS.has(tagName)) {\n return true;\n }\n // Check interactive roles\n if (role && INTERACTIVE_ROLES.has(role)) {\n return true;\n }\n // Check contenteditable\n if (element.isContentEditable || element.getAttribute('contenteditable') === 'true') {\n return true;\n }\n // Check for common testing/automation attributes\n if (element.hasAttribute('data-testid') || element.hasAttribute('data-cy') || element.hasAttribute('data-test')) {\n return true;\n }\n // Check for explicit onclick handler (attribute or property)\n if (element.hasAttribute('onclick') || typeof element.onclick === 'function') {\n return true;\n }\n if (element.hasAttribute('aria-haspopup')) {\n return true;\n }\n if (hasAnyClassName(element, interactiveClassNames)) {\n return true;\n }\n // Check for cursor-pointer class names that indicate clickability\n if (hasAnyClassName(element, cursorPointerClassNames)) {\n return true;\n }\n // return false\n // Check for other common interaction event listeners\n try {\n const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;\n if (typeof getEventListenersForNode === 'function') {\n const listeners = getEventListenersForNode(element);\n const interactionEvents = ['click', 'mousedown', 'mouseup', 'dblclick', 'input', 'mouseenter', 'mouseleave', 'keydown', 'keyup', 'submit', 'change', 'focus', 'blur'];\n for (const eventType of interactionEvents) {\n for (const listener of listeners) {\n if (listener.type === eventType) {\n return true; // Found a common interaction listener\n }\n }\n }\n }\n // Fallback: Check common event attributes if getEventListeners is not available (getEventListenersForNode doesn't work in page.evaluate context)\n const commonEventAttrs = ['onmousedown', 'onmouseup', 'onkeydown', 'onkeyup', 'onsubmit', 'onmouseenter', 'onmouseleave', 'onchange', 'oninput', 'onfocus', 'onblur'];\n if (commonEventAttrs.some(attr => element.hasAttribute(attr))) {\n return true;\n }\n }\n catch (e) {\n // console.warn(`Could not check event listeners for ${element.tagName}:`, e);\n // If checking listeners fails, rely on other checks\n }\n // if the element is not strictly interactive but appears clickable based on heuristic signals\n if (isHeuristicallyInteractive(element)) {\n return true;\n }\n // Default to false: if it's interactive but doesn't match above,\n // assume it triggers the same action as the parent.\n return false;\n }\n // --- End distinct interaction check ---\n /**\n * Calculates Intersection over Union (IoU) for two rectangles.\n * Returns a value between 0 (no overlap) and 1 (identical).\n */\n function calculateIoU(rect1, rect2) {\n // Calculate intersection\n const xOverlap = Math.max(0, Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left));\n const yOverlap = Math.max(0, Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top));\n const intersectionArea = xOverlap * yOverlap;\n // Calculate union\n const area1 = rect1.width * rect1.height;\n const area2 = rect2.width * rect2.height;\n const unionArea = area1 + area2 - intersectionArea;\n // Avoid division by zero\n if (unionArea === 0)\n return 0;\n return intersectionArea / unionArea;\n }\n /**\n * Checks if two rects are effectively the same using IoU threshold.\n */\n function areRectsEqual(rect1, rect2) {\n return calculateIoU(rect1, rect2) >= sameRectIoUThreshold;\n }\n /**\n * Checks if an element can actually receive pointer events.\n * Returns false if the element is hidden, disabled, or has pointer-events: none.\n */\n function canReceivePointerEvents(element) {\n const style = getCachedComputedStyle(element);\n // Check CSS properties that prevent events\n if (style?.pointerEvents === 'none')\n return false;\n if (style?.visibility === 'hidden')\n return false;\n if (style?.display === 'none')\n return false;\n // Check disabled attribute for form elements\n if (element.disabled)\n return false;\n return true;\n }\n /**\n * Checks if an element has an interactive descendant with the same bounding rect.\n * If so, the descendant should be highlighted instead of this element (innermost wins).\n */\n function hasInteractiveDescendantWithSameRect(element, rect) {\n for (const child of element.children) {\n if (!(child instanceof HTMLElement))\n continue;\n if (!canReceivePointerEvents(child))\n continue;\n const childRect = child.getBoundingClientRect();\n if (!areRectsEqual(rect, childRect))\n continue;\n // Child has same rect - check if it's interactive\n if (isInteractiveElement(child) || isElementScrollable(child)) {\n return true;\n }\n // Child has same rect but isn't interactive - check its descendants\n if (hasInteractiveDescendantWithSameRect(child, rect)) {\n return true;\n }\n }\n return false;\n }\n /**\n * Handles the logic for deciding whether to highlight an element and performing the highlight.\n */\n function handleHighlighting(nodeData, node, parentIframe, parentHighlightedRect) {\n if (!nodeData.isInteractive)\n return { highlighted: false, rect: null }; // Not interactive, definitely don't highlight\n // Apply action intent filter - skip elements that don't match the specified intent\n const actionIntent = args.actionIntent || 'all';\n if (actionIntent !== 'all' && !matchesActionIntent(node, actionIntent)) {\n return { highlighted: false, rect: null };\n }\n // Check if element can actually receive pointer events\n if (!canReceivePointerEvents(node)) {\n return { highlighted: false, rect: null };\n }\n const currentRect = node.getBoundingClientRect();\n // Check if there's an interactive descendant with the same rect\n // If so, skip this element - let the innermost element be highlighted\n if (hasInteractiveDescendantWithSameRect(node, currentRect)) {\n return { highlighted: false, rect: null };\n }\n let shouldHighlight = false;\n if (!parentHighlightedRect) {\n // Parent wasn't highlighted, this interactive node can be highlighted.\n shouldHighlight = true;\n }\n else {\n // Parent *was* highlighted. Only highlight this node if it represents a distinct interaction.\n if (areRectsEqual(currentRect, parentHighlightedRect)) {\n // Same rect as parent - this is the innermost element, should be highlighted\n // (parent should have been skipped by hasInteractiveDescendantWithSameRect)\n shouldHighlight = true;\n }\n else if (isElementDistinctInteraction(node, nodeData)) {\n shouldHighlight = true;\n }\n else {\n // console.log(`Skipping highlight for ${nodeData.tagName} (parent highlighted)`);\n shouldHighlight = false;\n }\n }\n if (shouldHighlight) {\n const attributeNames = node.getAttributeNames?.() || [];\n for (const name of attributeNames) {\n const value = node.getAttribute(name);\n nodeData.attributes[name] = value;\n }\n // Check viewport status before assigning index and highlighting\n if (nodeData.isInViewport === undefined) {\n nodeData.isInViewport = isInExpandedViewport(node, viewportExpansion);\n }\n // When viewportExpansion is -1, all interactive elements should get a highlight index\n // regardless of viewport status\n if (nodeData.isInViewport || viewportExpansion === -1) {\n nodeData.highlightIndex = highlightIndex++;\n if (doHighlightElements) {\n // Collect elements for deferred highlighting (after tree-based flow detection)\n if (focusHighlightIndex >= 0) {\n if (focusHighlightIndex === nodeData.highlightIndex) {\n elementsToHighlight.push({ element: node, index: nodeData.highlightIndex, parentIframe });\n }\n }\n else {\n elementsToHighlight.push({ element: node, index: nodeData.highlightIndex, parentIframe });\n }\n return { highlighted: true, rect: currentRect }; // Will be highlighted after traversal\n }\n // Even if not drawing highlights, we still \"highlighted\" for tracking purposes\n return { highlighted: true, rect: currentRect };\n }\n else {\n // console.log(`Skipping highlight for ${nodeData.tagName} (outside viewport)`);\n }\n }\n return { highlighted: false, rect: null }; // Did not highlight\n }\n function isElementScrollable(element) {\n const listenedEvents = getCachedNodeEventListeners(element);\n if (listenedEvents && listenedEvents.includes('scroll')) {\n const hasScrollableX = element.scrollWidth > element.clientWidth;\n const hasScrollableY = element.scrollHeight > element.clientHeight;\n return hasScrollableX || hasScrollableY;\n }\n const style = getCachedComputedStyle(element);\n const hasScrollableX = ['auto', 'scroll'].includes(style?.overflowX || '') &&\n element.scrollWidth > element.clientWidth;\n const hasScrollableY = ['auto', 'scroll'].includes(style?.overflowY || '') &&\n element.scrollHeight > element.clientHeight;\n return hasScrollableX || hasScrollableY;\n }\n /**\n * Creates a node data object for a given node and its descendants.\n */\n function buildDomTree(node, parentIframe = null, parentHighlightedRect = null) {\n // Fast rejection checks first\n if (!node || node.id === HIGHLIGHT_CONTAINER_ID) {\n return null;\n }\n if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {\n return null;\n }\n // Special handling for root node (body)\n if (node === document.body) {\n const nodeData = {\n tagName: 'body',\n attributes: {},\n xpath: '/body',\n children: [],\n };\n // Process children of body\n for (const child of node.childNodes) {\n const domElement = buildDomTree(child, parentIframe, null); // Body's children have no highlighted parent initially\n if (domElement)\n nodeData.children.push(domElement);\n }\n const id = `${ID.current++}`;\n DOM_HASH_MAP[id] = nodeData;\n return id;\n }\n // Process text nodes\n if (node.nodeType === Node.TEXT_NODE) {\n const textContent = node.textContent?.trim();\n if (!textContent) {\n return null;\n }\n // Only check visibility for text nodes that might be visible\n const parentElement = node.parentElement;\n if (!parentElement || parentElement.tagName.toLowerCase() === 'script') {\n return null;\n }\n const id = `${ID.current++}`;\n DOM_HASH_MAP[id] = {\n type: \"TEXT_NODE\",\n text: textContent,\n isVisible: isTextNodeVisible(node),\n };\n return id;\n }\n // Quick checks for element nodes\n if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {\n return null;\n }\n const element = node;\n const nodeData = {\n tagName: element.tagName.toLowerCase(),\n attributes: {},\n xpath: getXPathTree(element, true),\n children: [],\n };\n // Get attributes for interactive elements or potential text containers\n if (element.tagName.toLowerCase() === 'iframe' || element.tagName.toLowerCase() === 'body') {\n const attributeNames = element.getAttributeNames?.() || [];\n for (const name of attributeNames) {\n const value = element.getAttribute(name);\n nodeData.attributes[name] = value;\n }\n }\n let highlightResult = { highlighted: false, rect: null };\n // Perform visibility, interactivity, and highlighting checks\n if (node.nodeType === Node.ELEMENT_NODE) {\n if (alwaysHighlightFileInput && element.tagName.toLowerCase() === 'input' && element.type === 'file') {\n nodeData.isTopElement = true;\n if (nodeData.isTopElement) {\n nodeData.isInteractive = true;\n nodeData.isInViewport = true; // File inputs should always be considered in viewport\n // Call the dedicated highlighting function\n highlightResult = handleHighlighting(nodeData, element, parentIframe, parentHighlightedRect);\n }\n }\n else {\n nodeData.isVisible = isElementVisible(element); // isElementVisible uses offsetWidth/Height, which is fine\n if (nodeData.isVisible) {\n nodeData.isTopElement = isTopElement(element);\n if (nodeData.isTopElement) {\n let isScrollable = isElementScrollable(element);\n nodeData.isInteractive = isInteractiveElement(element) || isScrollable;\n nodeData.isScrollable = isScrollable;\n nodeData.markAsClickable = shouldMarkAsClickable(element);\n // Call the dedicated highlighting function\n highlightResult = handleHighlighting(nodeData, element, parentIframe, parentHighlightedRect);\n }\n }\n }\n }\n // Determine the rect to pass to children: use this element's rect if highlighted, otherwise parent's\n const rectForChildren = highlightResult.highlighted ? highlightResult.rect : parentHighlightedRect;\n // Process children, with special handling for iframes and rich text editors\n if (element.tagName) {\n const tagName = element.tagName.toLowerCase();\n // Handle iframes\n if (tagName === \"iframe\") {\n try {\n const iframeEl = element;\n const iframeDoc = iframeEl.contentDocument || iframeEl.contentWindow?.document;\n if (iframeDoc) {\n for (const child of iframeDoc.childNodes) {\n const domElement = buildDomTree(child, iframeEl, null); // iframes start fresh\n if (domElement)\n nodeData.children.push(domElement);\n }\n }\n }\n catch (e) {\n console.warn(\"Unable to access iframe:\", e);\n }\n }\n // Handle rich text editors and contenteditable elements\n else if (element.isContentEditable ||\n element.getAttribute(\"contenteditable\") === \"true\" ||\n element.id === \"tinymce\" ||\n element.classList.contains(\"mce-content-body\") ||\n (tagName === \"body\" && element.getAttribute(\"data-id\")?.startsWith(\"mce_\"))) {\n // Process all child nodes to capture formatted text\n for (const child of element.childNodes) {\n const domElement = buildDomTree(child, parentIframe, rectForChildren);\n if (domElement)\n nodeData.children.push(domElement);\n }\n }\n else {\n // Handle shadow DOM\n if (element.shadowRoot) {\n nodeData.shadowRoot = true;\n for (const child of element.shadowRoot.childNodes) {\n const domElement = buildDomTree(child, parentIframe, rectForChildren);\n if (domElement)\n nodeData.children.push(domElement);\n }\n }\n // Handle regular elements\n for (const child of element.childNodes) {\n const domElement = buildDomTree(child, parentIframe, rectForChildren);\n if (domElement)\n nodeData.children.push(domElement);\n }\n }\n }\n const id = `${ID.current++}`;\n DOM_HASH_MAP[id] = nodeData;\n return id;\n }\n const rootId = buildDomTree(document.body);\n // After traversal, render all highlights\n let treeEntries = [];\n if (doHighlightElements) {\n if (phase === 'labels') {\n // Labels phase: use provided element data to place labels only\n processLabelsPhase();\n }\n else if (elementsToHighlight.length > 0) {\n // Boxes phase or legacy mode: traverse tree to draw boxes and optionally labels\n treeEntries = buildTreeEntries();\n // Use recursive tree processing for correct hot map behavior:\n // - Pre-order: Mark bounding boxes in hot map (so children avoid parent borders)\n // - Post-order: Place labels (so parent labels avoid children's labels, skipped in boxes phase)\n processElementTreeRecursively(elementsToHighlight);\n }\n }\n // Clear the cache before returning\n DOM_CACHE.clearCache();\n return {\n rootId,\n map: DOM_HASH_MAP,\n debugLogs,\n treeEntries,\n highlightCount: highlightIndex,\n // Return the final grayscale image state (after all boxes and labels marked) for debugging\n finalGrayscaleImage: grayscaleImage ?? undefined,\n // Note: labelSnapshots are now streamed via onSnapshot callback instead of returned\n // Return element data from boxes phase for use in labels phase\n elementData: phase === 'boxes' ? collectedElementData : undefined,\n };\n})","/**\n * DOM Service - Main orchestrator for DOM extraction\n * Ported from browser-use dom/service.py\n *\n * Supports two modes:\n * 1. Traditional: JavaScript-based DOM traversal with heuristic interactivity detection\n * 2. Hybrid (experimental): CDP Accessibility Tree for authoritative interactivity + DOM for visuals\n */\n\nimport { Page, CDPSession } from 'playwright';\nimport logger from '../utils/logger';\nimport { sliceScreenshot, generateGrayscaleFromPng } from '../utils/imageUtils';\nimport {\n\tDOMState,\n\tDOMExtractionOptions,\n\tDOMEvalResult,\n\tSelectorMap,\n\tDOMElementNode,\n\tDOMBaseNode,\n\tActionIntent,\n} from './types';\nimport { DOMElementNodeImpl, DOMTextNodeImpl } from './nodes';\nimport {\n\tINTERACTIVE_ROLES,\n\tINTERACTION_EVENT_TYPES,\n\tEVENT_LISTENER_CANDIDATE_SELECTORS,\n\tDEFAULT_EVENT_LISTENER_LIMIT,\n} from './axtree-shared';\n\n// @ts-ignore - Original JavaScript implementation\nimport domTreeJs from './dom-tree/index.js?raw';\n// @ts-ignore - TypeScript implementation (built from index.ts via build.ts)\nimport domTreeTs from './dom-tree/dist/index.js?raw';\n\n/**\n * CDP Accessibility Tree types\n */\ninterface AXValue {\n\ttype: string;\n\tvalue?: any;\n}\n\ninterface AXProperty {\n\tname: string;\n\tvalue: AXValue;\n}\n\ninterface AXNode {\n\tnodeId: string;\n\tignored: boolean;\n\trole?: AXValue;\n\tname?: AXValue;\n\tdescription?: AXValue;\n\tvalue?: AXValue;\n\tproperties?: AXProperty[];\n\tchildIds?: string[];\n\tbackendDOMNodeId?: number;\n}\n\n\n/**\n * Result from resolving an AXNode to DOM with visual info\n */\ninterface ResolvedElement {\n\taxNode: AXNode;\n\ttagName: string;\n\txpath: string;\n\tattributes: Record<string, string>;\n\tisVisible: boolean;\n\tisInViewport: boolean;\n\tisTopElement: boolean;\n\tboundingRect: { x: number; y: number; width: number; height: number } | null;\n\tclientRects: Array<{ x: number; y: number; width: number; height: number }>;\n}\n\n/**\n * Check if URL is a new tab page\n */\nfunction isNewTabPage(url: string): boolean {\n\treturn (\n\t\turl === 'about:blank' ||\n\t\turl === 'chrome://newtab/' ||\n\t\turl === 'edge://newtab/' ||\n\t\turl === 'about:newtab'\n\t);\n}\n\n/**\n * Options for DomService constructor\n */\nexport interface DomServiceOptions {\n\t/**\n\t * Use the TypeScript implementation (compiled from index.ts)\n\t * Default: false (uses original JavaScript implementation)\n\t */\n\tuseDomTreeTs?: boolean;\n}\n\n/**\n * DOM Service for extracting clickable elements from pages\n *\n * Note: This service does NOT store a page reference. All methods that need\n * a page accept it as a parameter. This ensures the correct page is always\n * used, even when tabs change during a session.\n */\nexport class DomService {\n\tprivate jsCode: string;\n\tprivate useDomTreeTs: boolean;\n\n\tconstructor(options: DomServiceOptions = {}) {\n\t\tlogger.debug('🌳 Initializing DomService with options:', options);\n\t\t// Select DOM tree implementation based on options\n\t\tthis.useDomTreeTs = options.useDomTreeTs ?? false;\n\t\tthis.jsCode = this.useDomTreeTs ? domTreeTs : domTreeJs;\n\t}\n\n\t/**\n\t * Get clickable elements from the page\n\t */\n\tasync getClickableElements(page: Page, options: DOMExtractionOptions = {}): Promise<DOMState> {\n\t\tconst {\n\t\t\thighlightElements = true,\n\t\t\tfocusElement = -1,\n\t\t\tviewportExpansion = 0,\n\t\t\tinteractiveClassNames = [],\n\t\t\talwaysHighlightFileInput = false,\n\t\t\tsameRectIoUThreshold,\n\t\t\tactionIntent = 'all',\n\t\t} = options;\n\n\t\tconst [elementTree, selectorMap] = await this.buildDomTree(\n\t\t\tpage,\n\t\t\thighlightElements,\n\t\t\tfocusElement,\n\t\t\tviewportExpansion,\n\t\t\tinteractiveClassNames,\n\t\t\talwaysHighlightFileInput,\n\t\t\tsameRectIoUThreshold,\n\t\t\tactionIntent\n\t\t);\n\n\t\treturn {\n\t\t\telementTree,\n\t\t\tselectorMap,\n\t\t};\n\t}\n\n\t/**\n\t * Get clickable elements and capture SoM (Set-of-Mark) screenshot\n\t * Returns both the DOM state and a base64-encoded screenshot with highlights\n\t * Optionally slices the screenshot into 3 parts (left/middle/right) for token optimization\n\t */\n\tasync getClickableElementsWithScreenshot(\n\t\tpage: Page,\n\t\toptions: DOMExtractionOptions = {}\n\t): Promise<{ domState: DOMState; screenshotBase64: string; slicedScreenshotsBase64?: string[] }> {\n\t\t// Use hybrid AXTree approach if enabled\n\t\tif (options.useAccessibilityTree) {\n\t\t\treturn this.getClickableElementsWithAXTree(page, options);\n\t\t}\n\n\t\tlet screenshotBuffer: Buffer;\n\t\tif (options.useCleanScreenshot) {\n\t\t\tscreenshotBuffer = await page.screenshot({\n\t\t\t\ttype: 'png',\n\t\t\t\tfullPage: false,\n\t\t\t});\n\t\t}\n\t\t// Get clickable elements (this will add highlights to the page)\n\t\tconst domState = await this.getClickableElements(page, options);\n\n\t\t// Wait a moment for highlights to render\n\t\tawait page.waitForTimeout(100);\n\n\t\tif (!options.useCleanScreenshot) {\n\t\t\t// Capture screenshot with highlights\n\t\t\tscreenshotBuffer = await page.screenshot({\n\t\t\t\ttype: 'png',\n\t\t\t\tfullPage: false, // Only visible viewport\n\t\t\t});\n\t\t}\n\n\t\t// Clean up highlights after screenshot\n\t\tawait this.removeHighlights(page);\n\n\t\tconst screenshotBase64 = screenshotBuffer!.toString('base64');\n\n\t\t// Optionally slice the screenshot\n\t\tlet slicedScreenshotsBase64: string[] | undefined;\n\t\tif (options.useSlicedScreenshots) {\n\t\t\ttry {\n\t\t\t\tconst slices = await sliceScreenshot(screenshotBuffer!, {\n\t\t\t\t\tresize: options.resizeSlicedScreenshots,\n\t\t\t\t});\n\t\t\t\tslicedScreenshotsBase64 = slices.map(slice => slice.toString('base64'));\n\t\t\t} catch (error) {\n\t\t\t\tlogger.warn('Failed to slice screenshot:', error);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tdomState,\n\t\t\tscreenshotBase64,\n\t\t\tslicedScreenshotsBase64,\n\t\t};\n\t}\n\n\t/**\n\t * EXPERIMENTAL: Get clickable elements using Chrome Accessibility Tree\n\t *\n\t * This hybrid approach:\n\t * 1. Uses CDP Accessibility API to get authoritative interactive elements\n\t * 2. Resolves AXNodes to DOM elements for visual info (coordinates, visibility)\n\t * 3. Renders SoM highlights using the same visual approach\n\t *\n\t * Benefits over heuristic approach:\n\t * - Browser-native interactivity detection (no guessing about event listeners)\n\t * - Proper ARIA role handling\n\t * - Framework-agnostic (React, Vue, Angular all produce valid AXTree)\n\t */\n\tprivate async getClickableElementsWithAXTree(\n\t\tpage: Page,\n\t\toptions: DOMExtractionOptions = {}\n\t): Promise<{ domState: DOMState; screenshotBase64: string; slicedScreenshotsBase64?: string[] }> {\n\t\tlogger.debug('🌳 Using CDP Accessibility Tree for element detection');\n\n\t\tlet screenshotBuffer: Buffer | undefined;\n\t\tif (options.useCleanScreenshot) {\n\t\t\tscreenshotBuffer = await page.screenshot({\n\t\t\t\ttype: 'png',\n\t\t\t\tfullPage: false,\n\t\t\t});\n\t\t}\n\n\t\t// Create CDP session for this operation\n\t\tconst cdp = await page.context().newCDPSession(page);\n\n\t\t// Step 1: Get full accessibility tree\n\t\tconst { nodes } = await cdp.send('Accessibility.getFullAXTree', {\n\t\t\tdepth: -1, // Full depth\n\t\t}) as { nodes: AXNode[] };\n\n\t\tlogger.debug(`📊 Got ${nodes.length} AXNodes from accessibility tree`);\n\n\t\t// Step 2: Filter to interactive nodes\n\t\tconst interactiveNodes = nodes.filter((node) => {\n\t\t\tif (node.ignored) return false;\n\n\t\t\tconst role = node.role?.value;\n\t\t\tif (!role) return false;\n\n\t\t\t// Check if role is interactive\n\t\t\tif (!INTERACTIVE_ROLES.has(role)) return false;\n\n\t\t\t// Check if disabled\n\t\t\tconst isDisabled = node.properties?.find((p) => p.name === 'disabled')?.value?.value;\n\t\t\tif (isDisabled) return false;\n\n\t\t\t// Must have a backend DOM node reference\n\t\t\tif (!node.backendDOMNodeId) return false;\n\n\t\t\treturn true;\n\t\t});\n\n\t\tlogger.debug(`✅ Found ${interactiveNodes.length} interactive elements from AXTree`);\n\n\t\t// Log all buttons for debugging\n\t\tconst allButtons = nodes.filter((n) => n.role?.value === 'button');\n\t\tlogger.debug(`🔘 Total buttons in AXTree: ${allButtons.length}`);\n\t\tfor (const btn of allButtons) {\n\t\t\tconst reasons: string[] = [];\n\t\t\tif (btn.ignored) reasons.push('ignored');\n\t\t\tif (!btn.backendDOMNodeId) reasons.push('no-backendDOMNodeId');\n\t\t\tconst isDisabled = btn.properties?.find((p) => p.name === 'disabled')?.value?.value;\n\t\t\tif (isDisabled) reasons.push('disabled');\n\t\t\tlogger.debug(` - \"${btn.name?.value || '(no name)'}\" ${reasons.length > 0 ? `[SKIPPED: ${reasons.join(', ')}]` : '[INCLUDED]'}`);\n\t\t}\n\n\t\t// Step 2b: Get elements with event listeners (CDP DOMDebugger)\n\t\tconst axTreeNodeIds = new Set(interactiveNodes.map((n) => n.backendDOMNodeId));\n\t\tconst eventListenerElements = await this.getElementsWithEventListeners(\n\t\t\tcdp,\n\t\t\toptions.eventListenerLimit ?? 500\n\t\t);\n\n\t\t// Merge: Add event listener elements not already in AXTree\n\t\tlet addedFromEventListeners = 0;\n\t\tfor (const el of eventListenerElements) {\n\t\t\tif (!axTreeNodeIds.has(el.backendNodeId)) {\n\t\t\t\t// Create synthetic AXNode for this element\n\t\t\t\tinteractiveNodes.push({\n\t\t\t\t\tnodeId: `synthetic-${el.backendNodeId}`,\n\t\t\t\t\tignored: false,\n\t\t\t\t\tbackendDOMNodeId: el.backendNodeId,\n\t\t\t\t\trole: { type: 'role', value: 'generic' },\n\t\t\t\t\tname: { type: 'string', value: '' },\n\t\t\t\t\tproperties: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'eventListeners',\n\t\t\t\t\t\t\tvalue: { type: 'string', value: el.eventTypes.join(',') },\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t});\n\t\t\t\taxTreeNodeIds.add(el.backendNodeId);\n\t\t\t\taddedFromEventListeners++;\n\t\t\t}\n\t\t}\n\n\t\tlogger.debug(\n\t\t\t`🎯 Added ${addedFromEventListeners} elements from event listeners (total: ${interactiveNodes.length})`\n\t\t);\n\n\t\t// Step 3: Resolve to DOM elements and get visual info\n\t\tconst resolvedElements = await this.resolveAXNodesToDOM(cdp, interactiveNodes);\n\n\t\t// Step 4: Filter to visible, in-viewport, top elements\n\t\tconst visibleElements = resolvedElements.filter(\n\t\t\t(el) => el.isVisible && el.isInViewport && el.isTopElement && el.boundingRect\n\t\t);\n\n\t\t// Log filtered out elements for debugging\n\t\tconst filteredOut = resolvedElements.filter(\n\t\t\t(el) => !(el.isVisible && el.isInViewport && el.isTopElement && el.boundingRect)\n\t\t);\n\t\tif (filteredOut.length > 0) {\n\t\t\tlogger.debug(`🚫 Filtered out ${filteredOut.length} elements:`);\n\t\t\tfor (const el of filteredOut) {\n\t\t\t\tconst reasons: string[] = [];\n\t\t\t\tif (!el.isVisible) reasons.push('not-visible');\n\t\t\t\tif (!el.isInViewport) reasons.push('not-in-viewport');\n\t\t\t\tif (!el.isTopElement) reasons.push('not-top-element');\n\t\t\t\tif (!el.boundingRect) reasons.push('no-bounding-rect');\n\t\t\t\tlogger.debug(` - <${el.tagName}> \"${el.axNode.name?.value || ''}\" [${reasons.join(', ')}]`);\n\t\t\t}\n\t\t}\n\n\t\tlogger.debug(`👁️ ${visibleElements.length} elements are visible and in viewport`);\n\n\t\t// Step 5: Build DOM state and render highlights\n\t\tconst { domState, highlightIndex } = await this.buildDomStateFromAXTree(visibleElements);\n\n\t\t// Step 6: Render highlights on page\n\t\tif (options.highlightElements !== false && highlightIndex > 0) {\n\t\t\tawait this.renderHighlightsForAXElements(page, visibleElements.slice(0, highlightIndex));\n\t\t\tawait page.waitForTimeout(100);\n\t\t}\n\n\t\t// Step 7: Capture screenshot\n\t\tif (!options.useCleanScreenshot) {\n\t\t\tscreenshotBuffer = await page.screenshot({\n\t\t\t\ttype: 'png',\n\t\t\t\tfullPage: false,\n\t\t\t});\n\t\t}\n\n\t\t// Step 8: Clean up highlights\n\t\tawait this.removeHighlights(page);\n\n\t\t// Step 9: Detach CDP session\n\t\ttry {\n\t\t\tawait cdp.detach();\n\t\t} catch {\n\t\t\t// Ignore errors when detaching\n\t\t}\n\n\t\tconst screenshotBase64 = screenshotBuffer!.toString('base64');\n\n\t\t// Step 10: Optionally slice screenshot\n\t\tlet slicedScreenshotsBase64: string[] | undefined;\n\t\tif (options.useSlicedScreenshots) {\n\t\t\ttry {\n\t\t\t\tconst slices = await sliceScreenshot(screenshotBuffer!, {\n\t\t\t\t\tresize: options.resizeSlicedScreenshots,\n\t\t\t\t});\n\t\t\t\tslicedScreenshotsBase64 = slices.map((slice) => slice.toString('base64'));\n\t\t\t} catch (error) {\n\t\t\t\tlogger.warn('Failed to slice screenshot:', error);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tdomState,\n\t\t\tscreenshotBase64,\n\t\t\tslicedScreenshotsBase64,\n\t\t};\n\t}\n\n\t/**\n\t * Resolve AXNodes to DOM elements with visual information\n\t */\n\tprivate async resolveAXNodesToDOM(\n\t\tcdp: CDPSession,\n\t\taxNodes: AXNode[]\n\t): Promise<ResolvedElement[]> {\n\t\tconst results: ResolvedElement[] = [];\n\n\t\t// Batch resolve: collect all backend node IDs and resolve in one page.evaluate\n\t\tconst backendNodeIds = axNodes\n\t\t\t.map((n) => n.backendDOMNodeId)\n\t\t\t.filter((id): id is number => id !== undefined);\n\n\t\tif (backendNodeIds.length === 0) {\n\t\t\treturn results;\n\t\t}\n\n\t\t// Use DOM.resolveNode to get object IDs for each backend node\n\t\tconst objectIds: (string | null)[] = [];\n\t\tfor (const backendNodeId of backendNodeIds) {\n\t\t\ttry {\n\t\t\t\tconst { object } = await cdp.send('DOM.resolveNode', {\n\t\t\t\t\tbackendNodeId,\n\t\t\t\t});\n\t\t\t\tobjectIds.push(object.objectId || null);\n\t\t\t} catch {\n\t\t\t\tobjectIds.push(null);\n\t\t\t}\n\t\t}\n\n\t\t// Now get visual info for each resolved element\n\t\tfor (let i = 0; i < axNodes.length; i++) {\n\t\t\tconst axNode = axNodes[i];\n\t\t\tconst objectId = objectIds[i];\n\n\t\t\tif (!objectId) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// Get element info via Runtime.callFunctionOn\n\t\t\t\tconst { result } = await cdp.send('Runtime.callFunctionOn', {\n\t\t\t\t\tobjectId,\n\t\t\t\t\tfunctionDeclaration: `function() {\n\t\t\t\t\t\tconst el = this;\n\t\t\t\t\t\tconst rect = el.getBoundingClientRect();\n\t\t\t\t\t\tconst rects = el.getClientRects();\n\t\t\t\t\t\tconst style = window.getComputedStyle(el);\n\n\t\t\t\t\t\t// Check visibility\n\t\t\t\t\t\tconst isVisible =\n\t\t\t\t\t\t\tel.offsetWidth > 0 &&\n\t\t\t\t\t\t\tel.offsetHeight > 0 &&\n\t\t\t\t\t\t\tstyle.visibility !== 'hidden' &&\n\t\t\t\t\t\t\tstyle.display !== 'none' &&\n\t\t\t\t\t\t\tstyle.opacity !== '0';\n\n\t\t\t\t\t\t// Check if in viewport\n\t\t\t\t\t\tconst viewportWidth = window.innerWidth;\n\t\t\t\t\t\tconst viewportHeight = window.innerHeight;\n\t\t\t\t\t\tconst isInViewport = !(\n\t\t\t\t\t\t\trect.bottom < 0 ||\n\t\t\t\t\t\t\trect.top > viewportHeight ||\n\t\t\t\t\t\t\trect.right < 0 ||\n\t\t\t\t\t\t\trect.left > viewportWidth\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// Check if topmost element\n\t\t\t\t\t\tlet isTopElement = false;\n\t\t\t\t\t\tif (isVisible && isInViewport) {\n\t\t\t\t\t\t\tconst centerX = rect.left + rect.width / 2;\n\t\t\t\t\t\t\tconst centerY = rect.top + rect.height / 2;\n\t\t\t\t\t\t\tconst topEl = document.elementFromPoint(centerX, centerY);\n\t\t\t\t\t\t\tif (topEl) {\n\t\t\t\t\t\t\t\tlet current = topEl;\n\t\t\t\t\t\t\t\twhile (current && current !== document.documentElement) {\n\t\t\t\t\t\t\t\t\tif (current === el) {\n\t\t\t\t\t\t\t\t\t\tisTopElement = true;\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tcurrent = current.parentElement;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Get XPath\n\t\t\t\t\t\tfunction getXPath(element) {\n\t\t\t\t\t\t\tconst segments = [];\n\t\t\t\t\t\t\tlet current = element;\n\t\t\t\t\t\t\twhile (current && current.nodeType === Node.ELEMENT_NODE) {\n\t\t\t\t\t\t\t\tconst tagName = current.nodeName.toLowerCase();\n\t\t\t\t\t\t\t\tconst siblings = current.parentElement\n\t\t\t\t\t\t\t\t\t? Array.from(current.parentElement.children).filter(c => c.nodeName.toLowerCase() === tagName)\n\t\t\t\t\t\t\t\t\t: [];\n\t\t\t\t\t\t\t\tconst index = siblings.length > 1 ? siblings.indexOf(current) + 1 : 0;\n\t\t\t\t\t\t\t\tsegments.unshift(index > 0 ? tagName + '[' + index + ']' : tagName);\n\t\t\t\t\t\t\t\tcurrent = current.parentNode;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn segments.join('/');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Get attributes\n\t\t\t\t\t\tconst attributes = {};\n\t\t\t\t\t\tfor (const attr of el.attributes) {\n\t\t\t\t\t\t\tattributes[attr.name] = attr.value;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Convert client rects to plain objects\n\t\t\t\t\t\tconst clientRectsArray = [];\n\t\t\t\t\t\tfor (const r of rects) {\n\t\t\t\t\t\t\tclientRectsArray.push({\n\t\t\t\t\t\t\t\tx: r.x, y: r.y, width: r.width, height: r.height\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttagName: el.tagName.toLowerCase(),\n\t\t\t\t\t\t\txpath: getXPath(el),\n\t\t\t\t\t\t\tattributes: attributes,\n\t\t\t\t\t\t\tisVisible: isVisible,\n\t\t\t\t\t\t\tisInViewport: isInViewport,\n\t\t\t\t\t\t\tisTopElement: isTopElement,\n\t\t\t\t\t\t\tboundingRect: rect.width > 0 && rect.height > 0 ? {\n\t\t\t\t\t\t\t\tx: rect.x, y: rect.y, width: rect.width, height: rect.height\n\t\t\t\t\t\t\t} : null,\n\t\t\t\t\t\t\tclientRects: clientRectsArray\n\t\t\t\t\t\t};\n\t\t\t\t\t}`,\n\t\t\t\t\treturnByValue: true,\n\t\t\t\t});\n\n\t\t\t\tif (result.value) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\taxNode,\n\t\t\t\t\t\t...result.value,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// Element may have been removed from DOM\n\t\t\t\tlogger.debug(`Failed to resolve element: ${error}`);\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * Get elements with interaction event listeners using CDP DOMDebugger\n\t * Returns elements that have click/mousedown/etc handlers attached\n\t */\n\tprivate async getElementsWithEventListeners(\n\t\tcdp: CDPSession,\n\t\tlimit: number = DEFAULT_EVENT_LISTENER_LIMIT\n\t): Promise<Array<{ backendNodeId: number; eventTypes: string[] }>> {\n\t\tconst results: Array<{ backendNodeId: number; eventTypes: string[] }> = [];\n\n\t\ttry {\n\t\t\t// Step 1: Get document root\n\t\t\tconst { root } = (await cdp.send('DOM.getDocument', { depth: 0 })) as {\n\t\t\t\troot: { nodeId: number };\n\t\t\t};\n\n\t\t\t// Step 2: Query potentially interactive elements (from shared config)\n\t\t\tconst selectors = EVENT_LISTENER_CANDIDATE_SELECTORS.join(',');\n\n\t\t\tconst { nodeIds } = (await cdp.send('DOM.querySelectorAll', {\n\t\t\t\tnodeId: root.nodeId,\n\t\t\t\tselector: selectors,\n\t\t\t})) as { nodeIds: number[] };\n\n\t\t\tlogger.debug(`🔍 Checking ${Math.min(nodeIds.length, limit)} elements for event listeners`);\n\n\t\t\t// Step 3: Check each element for event listeners\n\t\t\tfor (const nodeId of nodeIds.slice(0, limit)) {\n\t\t\t\ttry {\n\t\t\t\t\t// Resolve node to get objectId\n\t\t\t\t\tconst { object } = (await cdp.send('DOM.resolveNode', { nodeId })) as {\n\t\t\t\t\t\tobject: { objectId?: string };\n\t\t\t\t\t};\n\t\t\t\t\tif (!object.objectId) continue;\n\n\t\t\t\t\t// Get event listeners for this element\n\t\t\t\t\tconst { listeners } = (await cdp.send('DOMDebugger.getEventListeners', {\n\t\t\t\t\t\tobjectId: object.objectId,\n\t\t\t\t\t})) as { listeners: Array<{ type: string }> };\n\n\t\t\t\t\t// Filter to interaction event types\n\t\t\t\t\tconst interactionListeners = listeners.filter((l) =>\n\t\t\t\t\t\tINTERACTION_EVENT_TYPES.has(l.type)\n\t\t\t\t\t);\n\n\t\t\t\t\tif (interactionListeners.length > 0) {\n\t\t\t\t\t\t// Get backendNodeId for this element\n\t\t\t\t\t\tconst { node } = (await cdp.send('DOM.describeNode', { nodeId })) as {\n\t\t\t\t\t\t\tnode: { backendNodeId: number };\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tresults.push({\n\t\t\t\t\t\t\tbackendNodeId: node.backendNodeId,\n\t\t\t\t\t\t\teventTypes: interactionListeners.map((l) => l.type),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Release object to free memory\n\t\t\t\t\tawait cdp.send('Runtime.releaseObject', { objectId: object.objectId });\n\t\t\t\t} catch {\n\t\t\t\t\t// Element may have been removed, skip\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlogger.debug(`✅ Found ${results.length} elements with interaction event listeners`);\n\t\t} catch (error) {\n\t\t\tlogger.warn('Failed to get elements with event listeners:', error);\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * Build DOMState from resolved AXTree elements\n\t */\n\tprivate async buildDomStateFromAXTree(\n\t\telements: ResolvedElement[]\n\t): Promise<{ domState: DOMState; highlightIndex: number }> {\n\t\tconst selectorMap = new Map<number, DOMElementNode>();\n\n\t\t// Create a root body element\n\t\tconst rootElement = new DOMElementNodeImpl(\n\t\t\t'body',\n\t\t\t'/body',\n\t\t\t{},\n\t\t\t[],\n\t\t\ttrue, // isVisible\n\t\t\tfalse, // isInteractive (body itself is not interactive)\n\t\t\tfalse, // isScrollable\n\t\t\tfalse, // markAsClickable\n\t\t\ttrue, // isTopElement\n\t\t\ttrue, // isInViewport\n\t\t\tfalse, // shadowRoot\n\t\t\tnull // highlightIndex\n\t\t);\n\n\t\t// Add each element as a child with highlight index\n\t\tlet highlightIndex = 0;\n\t\tfor (const el of elements) {\n\t\t\tconst role = el.axNode.role?.value || '';\n\t\t\tconst name = el.axNode.name?.value || '';\n\n\t\t\t// Determine if this should be marked as clickable based on role\n\t\t\tconst isClickable = ['button', 'link', 'menuitem', 'tab', 'switch'].includes(role);\n\n\t\t\t// Create element node\n\t\t\tconst elementNode = new DOMElementNodeImpl(\n\t\t\t\tel.tagName,\n\t\t\t\tel.xpath,\n\t\t\t\tel.attributes,\n\t\t\t\t[], // children - we don't track children in AXTree mode\n\t\t\t\tel.isVisible,\n\t\t\t\ttrue, // isInteractive - all AXTree elements we include are interactive\n\t\t\t\trole === 'scrollbar', // isScrollable\n\t\t\t\tisClickable,\n\t\t\t\tel.isTopElement,\n\t\t\t\tel.isInViewport,\n\t\t\t\tfalse, // shadowRoot\n\t\t\t\thighlightIndex,\n\t\t\t\tel.boundingRect\n\t\t\t\t\t? {\n\t\t\t\t\t\t\ttopLeft: { x: el.boundingRect.x, y: el.boundingRect.y },\n\t\t\t\t\t\t\ttopRight: { x: el.boundingRect.x + el.boundingRect.width, y: el.boundingRect.y },\n\t\t\t\t\t\t\tbottomLeft: { x: el.boundingRect.x, y: el.boundingRect.y + el.boundingRect.height },\n\t\t\t\t\t\t\tbottomRight: {\n\t\t\t\t\t\t\t\tx: el.boundingRect.x + el.boundingRect.width,\n\t\t\t\t\t\t\t\ty: el.boundingRect.y + el.boundingRect.height,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tcenter: {\n\t\t\t\t\t\t\t\tx: el.boundingRect.x + el.boundingRect.width / 2,\n\t\t\t\t\t\t\t\ty: el.boundingRect.y + el.boundingRect.height / 2,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\twidth: el.boundingRect.width,\n\t\t\t\t\t\t\theight: el.boundingRect.height,\n\t\t\t\t\t\t}\n\t\t\t\t\t: null,\n\t\t\t\tnull, // pageCoordinates\n\t\t\t\tnull, // viewportInfo\n\t\t\t\trootElement // parent\n\t\t\t);\n\n\t\t\t// Add accessible name as a text node child if present\n\t\t\tif (name) {\n\t\t\t\tconst textNode = new DOMTextNodeImpl(name, true, elementNode);\n\t\t\t\telementNode.children.push(textNode);\n\t\t\t}\n\n\t\t\trootElement.children.push(elementNode);\n\t\t\tselectorMap.set(highlightIndex, elementNode);\n\t\t\thighlightIndex++;\n\t\t}\n\n\t\treturn {\n\t\t\tdomState: {\n\t\t\t\telementTree: rootElement,\n\t\t\t\tselectorMap,\n\t\t\t},\n\t\t\thighlightIndex,\n\t\t};\n\t}\n\n\t/**\n\t * Render SoM highlights for AXTree elements\n\t */\n\tprivate async renderHighlightsForAXElements(page: Page, elements: ResolvedElement[]): Promise<void> {\n\t\t// Colors for highlighting (same as dom-tree/index.js)\n\t\tconst colors = [\n\t\t\t'#FF0000',\n\t\t\t'#00FF00',\n\t\t\t'#0000FF',\n\t\t\t'#FFA500',\n\t\t\t'#800080',\n\t\t\t'#008080',\n\t\t\t'#FF69B4',\n\t\t\t'#4B0082',\n\t\t\t'#FF4500',\n\t\t\t'#2E8B57',\n\t\t\t'#DC143C',\n\t\t\t'#4682B4',\n\t\t];\n\n\t\tawait page.evaluate(\n\t\t\t({ elements, colors }) => {\n\t\t\t\t// Create or get highlight container\n\t\t\t\tconst HIGHLIGHT_CONTAINER_ID = 'playwright-highlight-container';\n\t\t\t\tlet container = document.getElementById(HIGHLIGHT_CONTAINER_ID);\n\t\t\t\tif (!container) {\n\t\t\t\t\tcontainer = document.createElement('div');\n\t\t\t\t\tcontainer.id = HIGHLIGHT_CONTAINER_ID;\n\t\t\t\t\tcontainer.style.position = 'fixed';\n\t\t\t\t\tcontainer.style.pointerEvents = 'none';\n\t\t\t\t\tcontainer.style.top = '0';\n\t\t\t\t\tcontainer.style.left = '0';\n\t\t\t\t\tcontainer.style.width = '100%';\n\t\t\t\t\tcontainer.style.height = '100%';\n\t\t\t\t\tcontainer.style.zIndex = '2147483647';\n\t\t\t\t\tcontainer.style.backgroundColor = 'transparent';\n\t\t\t\t\tdocument.body.appendChild(container);\n\t\t\t\t}\n\n\t\t\t\t// Create highlights for each element\n\t\t\t\telements.forEach((el: any, index: number) => {\n\t\t\t\t\tif (!el.boundingRect) return;\n\n\t\t\t\t\tconst color = colors[index % colors.length];\n\t\t\t\t\tconst rect = el.boundingRect;\n\n\t\t\t\t\t// Create overlay\n\t\t\t\t\tconst overlay = document.createElement('div');\n\t\t\t\t\toverlay.style.position = 'fixed';\n\t\t\t\t\toverlay.style.border = `1px solid ${color}`;\n\t\t\t\t\toverlay.style.backgroundColor = 'transparent';\n\t\t\t\t\toverlay.style.pointerEvents = 'none';\n\t\t\t\t\toverlay.style.boxSizing = 'border-box';\n\t\t\t\t\toverlay.style.top = `${rect.y}px`;\n\t\t\t\t\toverlay.style.left = `${rect.x}px`;\n\t\t\t\t\toverlay.style.width = `${rect.width}px`;\n\t\t\t\t\toverlay.style.height = `${rect.height}px`;\n\t\t\t\t\tcontainer!.appendChild(overlay);\n\n\t\t\t\t\t// Create label\n\t\t\t\t\tconst label = document.createElement('div');\n\t\t\t\t\tlabel.style.position = 'fixed';\n\t\t\t\t\tlabel.style.background = color;\n\t\t\t\t\tlabel.style.color = 'white';\n\t\t\t\t\tlabel.style.padding = '1px 4px';\n\t\t\t\t\tlabel.style.borderRadius = '4px';\n\t\t\t\t\tlabel.style.fontSize = index >= 100 ? '8px' : '12px';\n\t\t\t\t\tlabel.textContent = String(index);\n\n\t\t\t\t\t// Position label above element, aligned to right\n\t\t\t\t\tconst labelTop = Math.max(0, rect.y - 16);\n\t\t\t\t\tconst labelLeft = Math.max(0, Math.min(rect.x + rect.width - 20, window.innerWidth - 25));\n\t\t\t\t\tlabel.style.top = `${labelTop}px`;\n\t\t\t\t\tlabel.style.left = `${labelLeft}px`;\n\t\t\t\t\tcontainer!.appendChild(label);\n\t\t\t\t});\n\t\t\t},\n\t\t\t{ elements, colors }\n\t\t);\n\t}\n\n\t/**\n\t * Remove all highlights from the page\n\t */\n\tasync removeHighlights(page: Page): Promise<void> {\n\t\ttry {\n\t\t\tawait page.evaluate(() => {\n\t\t\t\t// Remove the highlight container\n\t\t\t\tconst container = document.getElementById('playwright-highlight-container');\n\t\t\t\tif (container) {\n\t\t\t\t\tcontainer.remove();\n\t\t\t\t}\n\n\t\t\t\t// Call cleanup functions if they exist\n\t\t\t\tif ((window as any)._highlightCleanupFunctions) {\n\t\t\t\t\t(window as any)._highlightCleanupFunctions.forEach((fn: () => void) => fn());\n\t\t\t\t\t(window as any)._highlightCleanupFunctions = [];\n\t\t\t\t}\n\t\t\t});\n\t\t\tlogger.debug('✅ Highlights removed from page');\n\t\t} catch (error: any) {\n\t\t\tlogger.warn('Failed to remove highlights:', error.message);\n\t\t}\n\t}\n\n\t/**\n\t * Get cross-origin iframes (for multi-frame navigation)\n\t */\n\tasync getCrossOriginIframes(page: Page): Promise<string[]> {\n\t\t// Get hidden frame URLs\n\t\tconst hiddenFrameUrls = await page\n\t\t\t.locator('iframe')\n\t\t\t.filter({ hasNot: page.locator(':visible') })\n\t\t\t.evaluateAll((iframes) => iframes.map((iframe) => (iframe as HTMLIFrameElement).src));\n\n\t\tconst isAdUrl = (url: string): boolean => {\n\t\t\ttry {\n\t\t\t\tconst urlObj = new URL(url);\n\t\t\t\tconst adDomains = ['doubleclick.net', 'adroll.com', 'googletagmanager.com'];\n\t\t\t\treturn adDomains.some((domain) => urlObj.hostname.includes(domain));\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t};\n\n\t\tconst pageUrl = page.url();\n\t\tconst pageHostname = new URL(pageUrl).hostname;\n\n\t\tconst frames = page.frames();\n\t\tconst crossOriginUrls: string[] = [];\n\n\t\tfor (const frame of frames) {\n\t\t\tconst frameUrl = frame.url();\n\n\t\t\ttry {\n\t\t\t\tconst frameHostname = new URL(frameUrl).hostname;\n\n\t\t\t\t// Exclude same-origin, hidden frames, and ad frames\n\t\t\t\tif (\n\t\t\t\t\tframeHostname && // exclude data:urls and new tab pages\n\t\t\t\t\tframeHostname !== pageHostname && // exclude same-origin iframes\n\t\t\t\t\t!hiddenFrameUrls.includes(frameUrl) && // exclude hidden frames\n\t\t\t\t\t!isAdUrl(frameUrl) // exclude ad network frames\n\t\t\t\t) {\n\t\t\t\t\tcrossOriginUrls.push(frameUrl);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip invalid URLs\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\treturn crossOriginUrls;\n\t}\n\n\t/**\n\t * Build DOM tree by executing JavaScript in the browser\n\t */\n\tprivate async buildDomTree(\n\t\tpage: Page,\n\t\thighlightElements: boolean,\n\t\tfocusElement: number,\n\t\tviewportExpansion: number,\n\t\tinteractiveClassNames: string[],\n\t\talwaysHighlightFileInput: boolean,\n\t\tsameRectIoUThreshold?: number,\n\t\tactionIntent: ActionIntent = 'all'\n\t): Promise<[DOMElementNode, SelectorMap]> {\n\t\t// Test if page can evaluate JavaScript\n\t\tconst canEvaluate = await page.evaluate('1+1');\n\t\tif (canEvaluate !== 2) {\n\t\t\tthrow new Error('The page cannot evaluate javascript code properly');\n\t\t}\n\n\t\t// Short-circuit if the page is a new empty tab\n\t\tif (isNewTabPage(page.url())) {\n\t\t\tconst emptyElement = new DOMElementNodeImpl(\n\t\t\t\t'body',\n\t\t\t\t'',\n\t\t\t\t{},\n\t\t\t\t[],\n\t\t\t\tfalse, // isVisible\n\t\t\t\tfalse, // isInteractive\n\t\t\t\tfalse, // isScrollable\n\t\t\t\tfalse, // markAsClickable\n\t\t\t\tfalse, // isTopElement\n\t\t\t\tfalse, // isInViewport\n\t\t\t\tfalse, // shadowRoot\n\t\t\t\tnull // highlightIndex\n\t\t\t);\n\t\t\treturn [emptyElement, new Map()];\n\t\t}\n\n\t\t// Base args for JavaScript DOM analysis\n\t\tconst baseArgs = {\n\t\t\tdoHighlightElements: highlightElements,\n\t\t\tfocusHighlightIndex: focusElement,\n\t\t\tviewportExpansion: viewportExpansion,\n\t\t\tdebugMode: false,\n\t\t\tinteractiveClassNames: interactiveClassNames,\n\t\t\talwaysHighlightFileInput: alwaysHighlightFileInput,\n\t\t\tsameRectIoUThreshold: sameRectIoUThreshold,\n\t\t\tactionIntent: actionIntent,\n\t\t};\n\n\t\tlogger.debug(`🔧 Starting JavaScript DOM analysis for ${page.url().slice(0, 50)}...`);\n\n\t\tlet evalPage: any;\n\t\tlet grayscaleImage: number[][] | null = null;\n\n\t\t// Two-phase rendering with grayscale-based label placement\n\t\t// Only available when using the TypeScript DOM tree implementation\n\t\tconst useTwoPhaseRendering = this.useDomTreeTs && highlightElements;\n\n\t\tif (useTwoPhaseRendering) {\n\t\t\t// Two-phase rendering when highlighting:\n\t\t\t// Phase 1: Draw all bounding boxes (no labels yet)\n\t\t\t// Phase 2: Capture screenshot (boxes visible), generate grayscale, place labels\n\t\t\ttry {\n\t\t\t\t// Phase 1: Draw all bounding boxes\n\t\t\t\tconst boxesArgs = {\n\t\t\t\t\t...baseArgs,\n\t\t\t\t\tphase: 'boxes' as const,\n\t\t\t\t\tgrayscaleImage: null, // Not needed for boxes phase\n\t\t\t\t};\n\n\t\t\t\tconst boxesResult = await page.evaluate(\n\t\t\t\t\t({ code, argsObj }: { code: string; argsObj: any }) => {\n\t\t\t\t\t\tconst fn = (new Function('return ' + code))();\n\t\t\t\t\t\treturn fn(argsObj);\n\t\t\t\t\t},\n\t\t\t\t\t{ code: this.jsCode, argsObj: boxesArgs }\n\t\t\t\t);\n\t\t\t\tlogger.debug(`📦 Phase 1: Drew ${boxesResult.elementData?.length || 0} bounding boxes`);\n\n\t\t\t\t// Capture screenshot WITH boxes visible\n\t\t\t\tconst screenshotBuffer = await page.screenshot({\n\t\t\t\t\ttype: 'png',\n\t\t\t\t\tfullPage: false,\n\t\t\t\t});\n\t\t\t\tlogger.debug('📸 Captured screenshot with bounding boxes');\n\n\t\t\t\t// Generate grayscale from screenshot (boxes are \"baked in\")\n\t\t\t\tconst startTime = performance.now();\n\t\t\t\tconst grayscaleData = await generateGrayscaleFromPng(screenshotBuffer);\n\t\t\t\tgrayscaleImage = grayscaleData.pixels;\n\t\t\t\tconst elapsed = Math.round(performance.now() - startTime);\n\t\t\t\tlogger.debug(`🖼️ Generated grayscale image (${grayscaleData.width}x${grayscaleData.height}) in ${elapsed}ms`);\n\n\t\t\t\t// Phase 2: Place labels using grayscale\n\t\t\t\tconst labelsArgs = {\n\t\t\t\t\t...baseArgs,\n\t\t\t\t\tphase: 'labels' as const,\n\t\t\t\t\tgrayscaleImage: grayscaleImage,\n\t\t\t\t\telementData: boxesResult.elementData,\n\t\t\t\t};\n\n\t\t\t\tevalPage = await page.evaluate(\n\t\t\t\t\t({ code, argsObj }: { code: string; argsObj: any }) => {\n\t\t\t\t\t\tconst fn = (new Function('return ' + code))();\n\t\t\t\t\t\treturn fn(argsObj);\n\t\t\t\t\t},\n\t\t\t\t\t{ code: this.jsCode, argsObj: labelsArgs }\n\t\t\t\t);\n\n\t\t\t\t// Merge results from boxes phase (map, rootId) with labels phase\n\t\t\t\tevalPage.map = boxesResult.map;\n\t\t\t\tevalPage.rootId = boxesResult.rootId;\n\t\t\t\tevalPage.highlightCount = boxesResult.highlightCount;\n\t\t\t\tevalPage.perfMetrics = boxesResult.perfMetrics;\n\n\t\t\t\tlogger.debug('✅ Phase 2: Labels placed using grayscale-based positioning');\n\t\t\t} catch (error: any) {\n\t\t\t\tlogger.warn('Two-phase rendering failed, falling back to legacy mode:', error.message);\n\t\t\t\t// Fallback to legacy single-pass mode\n\t\t\t\tgrayscaleImage = null;\n\t\t\t\tconst legacyArgs = {\n\t\t\t\t\t...baseArgs,\n\t\t\t\t\tgrayscaleImage: null,\n\t\t\t\t};\n\n\t\t\t\tevalPage = await page.evaluate(\n\t\t\t\t\t({ code, argsObj }: { code: string; argsObj: any }) => {\n\t\t\t\t\t\tconst fn = (new Function('return ' + code))();\n\t\t\t\t\t\treturn fn(argsObj);\n\t\t\t\t\t},\n\t\t\t\t\t{ code: this.jsCode, argsObj: legacyArgs }\n\t\t\t\t);\n\t\t\t\tlogger.debug('✅ JavaScript DOM analysis completed (legacy mode)');\n\t\t\t}\n\t\t} else {\n\t\t\t// Legacy single-pass mode (JS implementation or no highlighting)\n\t\t\ttry {\n\t\t\t\tevalPage = await page.evaluate(\n\t\t\t\t\t({ code, argsObj }: { code: string; argsObj: any }) => {\n\t\t\t\t\t\tconst fn = (new Function('return ' + code))();\n\t\t\t\t\t\treturn fn(argsObj);\n\t\t\t\t\t},\n\t\t\t\t\t{ code: this.jsCode, argsObj: baseArgs }\n\t\t\t\t);\n\t\t\t\tlogger.debug('✅ JavaScript DOM analysis completed');\n\t\t\t} catch (error: any) {\n\t\t\t\tlogger.error('Error evaluating JavaScript:', error.message);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\t// Validate the result\n\t\tif (!evalPage || typeof evalPage !== 'object') {\n\t\t\tlogger.error('JavaScript returned invalid result:', evalPage);\n\t\t\tthrow new Error('JavaScript DOM analysis returned invalid result');\n\t\t}\n\n\t\tif (!evalPage.map || !evalPage.rootId) {\n\t\t\tlogger.error('JavaScript result missing map or rootId:', JSON.stringify(evalPage, null, 2));\n\t\t\tthrow new Error('JavaScript result missing required fields (map or rootId)');\n\t\t}\n\n\t\t// Fallback: If intent filtering found no elements, re-run with 'all' intent\n\t\tif (actionIntent !== 'all' && evalPage.highlightCount === 0) {\n\t\t\tlogger.debug(`⚠️ No elements matched intent '${actionIntent}', falling back to 'all'`);\n\t\t\t// Re-run with single pass for simplicity in fallback case\n\t\t\tconst fallbackArgs = { ...baseArgs, actionIntent: 'all' as const };\n\t\t\tevalPage = await page.evaluate(\n\t\t\t\t({ code, argsObj }: { code: string; argsObj: any }) => {\n\t\t\t\t\tconst fn = (new Function('return ' + code))();\n\t\t\t\t\treturn fn(argsObj);\n\t\t\t\t},\n\t\t\t\t{ code: this.jsCode, argsObj: fallbackArgs }\n\t\t\t);\n\t\t}\n\n\t\t// Log performance metrics if available\n\t\tif (evalPage && evalPage.perfMetrics) {\n\t\t\tconst perf = evalPage.perfMetrics;\n\t\t\tconst totalNodes = perf.nodeMetrics?.totalNodes ?? 0;\n\n\t\t\t// Count interactive elements from the DOM map\n\t\t\tlet interactiveCount = 0;\n\t\t\tif (evalPage.map) {\n\t\t\t\tfor (const nodeData of Object.values(evalPage.map)) {\n\t\t\t\t\tif (typeof nodeData === 'object' && nodeData !== null && (nodeData as any).isInteractive) {\n\t\t\t\t\t\tinteractiveCount++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst urlShort = page.url().length > 50 ? page.url().slice(0, 50) + '...' : page.url();\n\t\t\tlogger.debug(\n\t\t\t\t`🔎 Ran buildDOMTree.js interactive element detection on: ${urlShort} interactive=${interactiveCount}/${totalNodes}`\n\t\t\t);\n\t\t}\n\n\t\tlogger.debug('🔄 Starting TypeScript DOM tree construction...');\n\t\tconst result = await this.constructDomTree(evalPage);\n\t\tlogger.debug('✅ TypeScript DOM tree construction completed');\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Construct DOM tree from JavaScript evaluation result\n\t */\n\tprivate async constructDomTree(evalPage: DOMEvalResult): Promise<[DOMElementNode, SelectorMap]> {\n\t\tconst jsNodeMap = evalPage.map;\n\t\tconst jsRootId = evalPage.rootId;\n\n\t\tconst selectorMap = new Map<number, DOMElementNode>();\n\t\tconst nodeMap = new Map<string, DOMBaseNode>();\n\n\t\t// Build tree bottom-up\n\t\tfor (const [id, nodeData] of Object.entries(jsNodeMap)) {\n\t\t\tconst [node, childrenIds] = this.parseNode(nodeData);\n\t\t\tif (node === null) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tnodeMap.set(id, node);\n\n\t\t\tif (node instanceof DOMElementNodeImpl && node.highlightIndex !== null) {\n\t\t\t\tselectorMap.set(node.highlightIndex, node);\n\t\t\t}\n\n\t\t\t// Attach children (we know they're already processed)\n\t\t\tif (node instanceof DOMElementNodeImpl) {\n\t\t\t\tfor (const childId of childrenIds) {\n\t\t\t\t\tconst childNode = nodeMap.get(childId);\n\t\t\t\t\tif (!childNode) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tchildNode.parent = node;\n\t\t\t\t\tnode.children.push(childNode);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst htmlToDict = nodeMap.get(jsRootId);\n\n\t\tif (!htmlToDict || !(htmlToDict instanceof DOMElementNodeImpl)) {\n\t\t\tthrow new Error('Failed to parse HTML to dictionary');\n\t\t}\n\n\t\treturn [htmlToDict, selectorMap];\n\t}\n\n\t/**\n\t * Parse a single node from JavaScript result\n\t */\n\tprivate parseNode(nodeData: any): [DOMBaseNode | null, string[]] {\n\t\tif (!nodeData) {\n\t\t\treturn [null, []];\n\t\t}\n\n\t\t// Process text nodes\n\t\tif (nodeData.type === 'TEXT_NODE') {\n\t\t\tconst textNode = new DOMTextNodeImpl(nodeData.text, nodeData.isVisible, null);\n\t\t\treturn [textNode, []];\n\t\t}\n\n\t\t// Process viewport info if present\n\t\tlet viewportInfo = null;\n\t\tif (nodeData.viewport) {\n\t\t\tviewportInfo = {\n\t\t\t\twidth: nodeData.viewport.width,\n\t\t\t\theight: nodeData.viewport.height,\n\t\t\t\tscrollX: nodeData.viewport.scrollX,\n\t\t\t\tscrollY: nodeData.viewport.scrollY,\n\t\t\t};\n\t\t}\n\n\t\t// Create element node\n\t\tconst elementNode = new DOMElementNodeImpl(\n\t\t\tnodeData.tagName,\n\t\t\tnodeData.xpath,\n\t\t\tnodeData.attributes || {},\n\t\t\t[], // children will be added later\n\t\t\tnodeData.isVisible ?? false,\n\t\t\tnodeData.isInteractive ?? false,\n\t\t\tnodeData.isScrollable ?? false,\n\t\t\tnodeData.markAsClickable ?? false,\n\t\t\tnodeData.isTopElement ?? false,\n\t\t\tnodeData.isInViewport ?? false,\n\t\t\tnodeData.shadowRoot ?? false,\n\t\t\tnodeData.highlightIndex ?? null,\n\t\t\tnodeData.viewportCoordinates ?? null,\n\t\t\tnodeData.pageCoordinates ?? null,\n\t\t\tviewportInfo,\n\t\t\tnull // parent\n\t\t);\n\n\t\tconst childrenIds = nodeData.children || [];\n\n\t\treturn [elementNode, childrenIds];\n\t}\n}\n"],"mappings":"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECAAC,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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","names":["z","ToolRegistry","config","tool","name","toolName","args","context","actionDescription","argsWithoutDescription","validatedArgs","contextWithDescription","error","errorMessages","err","tools","toolNames","withDescriptionField","toolNameSet","wrappedSchemas","schema","shape","first","second","rest","toolRegistry","createAnthropic","getAnthropicModel","modelName","apiKey","getSdkConfig","logger_default","getAnthropicProviderOptions","_modelName","createGoogleGenerativeAI","createVertex","MediaResolution","isUsingVertexAI","useVertexAIFlag","getSdkConfig","getGoogleModel","modelName","env","vertexProject","vertexLocation","logger_default","apiKey","getGoogleProviderOptions","imageCount","providerOptionsGemini2_5Pro","providerOptionsGemini3FlashPreview","providerOptions","getModel","modelName","getAnthropicModel","getGoogleModel","getProviderOptions","imageCount","getAnthropicProviderOptions","getGoogleProviderOptions","getSharp","sharp","TILE_SIZE","sliceScreenshot","imageBuffer","options","sharp","getSharp","metadata","width","height","patchSize","leftStart","middleStart","rightStart","createPatch","left","extractWidth","pipeline","TILE_SIZE","leftCrop","middleCrop","rightCrop","generateGrayscaleFromPng","pngBuffer","image","getSharp","metadata","width","height","data","pixels","y","x","INTERACTIVE_ROLES","INTERACTION_EVENT_TYPES","EVENT_LISTENER_CANDIDATE_SELECTORS","DEFAULT_EVENT_LISTENER_LIMIT","capTextLength","text","maxLength","DEFAULT_INCLUDE_ATTRIBUTES","DEFAULT_INCLUDE_CLASSES_WITH_RENAME","DOMBaseNodeImpl","isVisible","parent","DOMTextNodeImpl","text","current","DOMElementNodeImpl","_DOMElementNodeImpl","tagName","xpath","attributes","children","isInteractive","isScrollable","markAsClickable","isTopElement","isInViewport","shadowRoot","highlightIndex","viewportCoordinates","pageCoordinates","viewportInfo","maxDepth","textParts","collectText","node","currentDepth","child","config","includeAttributes","DEFAULT_INCLUDE_ATTRIBUTES","includeClassesWithRename","DEFAULT_INCLUDE_CLASSES_WITH_RENAME","formattedText","processNode","depth","nextDepth","depthStr","attributesHtmlStr","attributesToInclude","key","value","orderedKeys","keysToRemove","seenValues","attrsToRemoveIfTextMatches","attr","capTextLength","highlightIndicator","filteredClasses","classes","cssClass","pattern","replacement","regex","renamed","scrollableIndicator","clickableIndicator","line","trimmedText","filteredAttributes","dom_tree_default","dist_default","isNewTabPage","url","DomService","options","logger_default","page","highlightElements","focusElement","viewportExpansion","interactiveClassNames","alwaysHighlightFileInput","sameRectIoUThreshold","actionIntent","elementTree","selectorMap","screenshotBuffer","domState","screenshotBase64","slicedScreenshotsBase64","sliceScreenshot","slice","error","cdp","nodes","interactiveNodes","node","role","INTERACTIVE_ROLES","p","allButtons","n","btn","reasons","axTreeNodeIds","eventListenerElements","addedFromEventListeners","el","resolvedElements","visibleElements","filteredOut","highlightIndex","axNodes","results","backendNodeIds","id","objectIds","backendNodeId","object","i","axNode","objectId","result","limit","DEFAULT_EVENT_LISTENER_LIMIT","root","selectors","EVENT_LISTENER_CANDIDATE_SELECTORS","nodeIds","nodeId","listeners","interactionListeners","l","INTERACTION_EVENT_TYPES","elements","rootElement","DOMElementNodeImpl","name","isClickable","elementNode","textNode","DOMTextNodeImpl","colors","HIGHLIGHT_CONTAINER_ID","container","index","color","rect","overlay","label","labelTop","labelLeft","fn","hiddenFrameUrls","iframes","iframe","isAdUrl","urlObj","domain","pageUrl","pageHostname","frames","crossOriginUrls","frame","frameUrl","frameHostname","baseArgs","evalPage","grayscaleImage","boxesArgs","boxesResult","code","argsObj","startTime","grayscaleData","generateGrayscaleFromPng","elapsed","labelsArgs","legacyArgs","fallbackArgs","totalNodes","interactiveCount","nodeData","urlShort","jsNodeMap","jsRootId","nodeMap","childrenIds","childId","childNode","htmlToDict","viewportInfo"]}