optexity-browser-use 0.9.5__py3-none-any.whl

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 (147) hide show
  1. browser_use/__init__.py +157 -0
  2. browser_use/actor/__init__.py +11 -0
  3. browser_use/actor/element.py +1175 -0
  4. browser_use/actor/mouse.py +134 -0
  5. browser_use/actor/page.py +561 -0
  6. browser_use/actor/playground/flights.py +41 -0
  7. browser_use/actor/playground/mixed_automation.py +54 -0
  8. browser_use/actor/playground/playground.py +236 -0
  9. browser_use/actor/utils.py +176 -0
  10. browser_use/agent/cloud_events.py +282 -0
  11. browser_use/agent/gif.py +424 -0
  12. browser_use/agent/judge.py +170 -0
  13. browser_use/agent/message_manager/service.py +473 -0
  14. browser_use/agent/message_manager/utils.py +52 -0
  15. browser_use/agent/message_manager/views.py +98 -0
  16. browser_use/agent/prompts.py +413 -0
  17. browser_use/agent/service.py +2316 -0
  18. browser_use/agent/system_prompt.md +185 -0
  19. browser_use/agent/system_prompt_flash.md +10 -0
  20. browser_use/agent/system_prompt_no_thinking.md +183 -0
  21. browser_use/agent/views.py +743 -0
  22. browser_use/browser/__init__.py +41 -0
  23. browser_use/browser/cloud/cloud.py +203 -0
  24. browser_use/browser/cloud/views.py +89 -0
  25. browser_use/browser/events.py +578 -0
  26. browser_use/browser/profile.py +1158 -0
  27. browser_use/browser/python_highlights.py +548 -0
  28. browser_use/browser/session.py +3225 -0
  29. browser_use/browser/session_manager.py +399 -0
  30. browser_use/browser/video_recorder.py +162 -0
  31. browser_use/browser/views.py +200 -0
  32. browser_use/browser/watchdog_base.py +260 -0
  33. browser_use/browser/watchdogs/__init__.py +0 -0
  34. browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
  35. browser_use/browser/watchdogs/crash_watchdog.py +335 -0
  36. browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
  37. browser_use/browser/watchdogs/dom_watchdog.py +817 -0
  38. browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
  39. browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
  40. browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
  41. browser_use/browser/watchdogs/popups_watchdog.py +143 -0
  42. browser_use/browser/watchdogs/recording_watchdog.py +126 -0
  43. browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
  44. browser_use/browser/watchdogs/security_watchdog.py +280 -0
  45. browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
  46. browser_use/cli.py +2359 -0
  47. browser_use/code_use/__init__.py +16 -0
  48. browser_use/code_use/formatting.py +192 -0
  49. browser_use/code_use/namespace.py +665 -0
  50. browser_use/code_use/notebook_export.py +276 -0
  51. browser_use/code_use/service.py +1340 -0
  52. browser_use/code_use/system_prompt.md +574 -0
  53. browser_use/code_use/utils.py +150 -0
  54. browser_use/code_use/views.py +171 -0
  55. browser_use/config.py +505 -0
  56. browser_use/controller/__init__.py +3 -0
  57. browser_use/dom/enhanced_snapshot.py +161 -0
  58. browser_use/dom/markdown_extractor.py +169 -0
  59. browser_use/dom/playground/extraction.py +312 -0
  60. browser_use/dom/playground/multi_act.py +32 -0
  61. browser_use/dom/serializer/clickable_elements.py +200 -0
  62. browser_use/dom/serializer/code_use_serializer.py +287 -0
  63. browser_use/dom/serializer/eval_serializer.py +478 -0
  64. browser_use/dom/serializer/html_serializer.py +212 -0
  65. browser_use/dom/serializer/paint_order.py +197 -0
  66. browser_use/dom/serializer/serializer.py +1170 -0
  67. browser_use/dom/service.py +825 -0
  68. browser_use/dom/utils.py +129 -0
  69. browser_use/dom/views.py +906 -0
  70. browser_use/exceptions.py +5 -0
  71. browser_use/filesystem/__init__.py +0 -0
  72. browser_use/filesystem/file_system.py +619 -0
  73. browser_use/init_cmd.py +376 -0
  74. browser_use/integrations/gmail/__init__.py +24 -0
  75. browser_use/integrations/gmail/actions.py +115 -0
  76. browser_use/integrations/gmail/service.py +225 -0
  77. browser_use/llm/__init__.py +155 -0
  78. browser_use/llm/anthropic/chat.py +242 -0
  79. browser_use/llm/anthropic/serializer.py +312 -0
  80. browser_use/llm/aws/__init__.py +36 -0
  81. browser_use/llm/aws/chat_anthropic.py +242 -0
  82. browser_use/llm/aws/chat_bedrock.py +289 -0
  83. browser_use/llm/aws/serializer.py +257 -0
  84. browser_use/llm/azure/chat.py +91 -0
  85. browser_use/llm/base.py +57 -0
  86. browser_use/llm/browser_use/__init__.py +3 -0
  87. browser_use/llm/browser_use/chat.py +201 -0
  88. browser_use/llm/cerebras/chat.py +193 -0
  89. browser_use/llm/cerebras/serializer.py +109 -0
  90. browser_use/llm/deepseek/chat.py +212 -0
  91. browser_use/llm/deepseek/serializer.py +109 -0
  92. browser_use/llm/exceptions.py +29 -0
  93. browser_use/llm/google/__init__.py +3 -0
  94. browser_use/llm/google/chat.py +542 -0
  95. browser_use/llm/google/serializer.py +120 -0
  96. browser_use/llm/groq/chat.py +229 -0
  97. browser_use/llm/groq/parser.py +158 -0
  98. browser_use/llm/groq/serializer.py +159 -0
  99. browser_use/llm/messages.py +238 -0
  100. browser_use/llm/models.py +271 -0
  101. browser_use/llm/oci_raw/__init__.py +10 -0
  102. browser_use/llm/oci_raw/chat.py +443 -0
  103. browser_use/llm/oci_raw/serializer.py +229 -0
  104. browser_use/llm/ollama/chat.py +97 -0
  105. browser_use/llm/ollama/serializer.py +143 -0
  106. browser_use/llm/openai/chat.py +264 -0
  107. browser_use/llm/openai/like.py +15 -0
  108. browser_use/llm/openai/serializer.py +165 -0
  109. browser_use/llm/openrouter/chat.py +211 -0
  110. browser_use/llm/openrouter/serializer.py +26 -0
  111. browser_use/llm/schema.py +176 -0
  112. browser_use/llm/views.py +48 -0
  113. browser_use/logging_config.py +330 -0
  114. browser_use/mcp/__init__.py +18 -0
  115. browser_use/mcp/__main__.py +12 -0
  116. browser_use/mcp/client.py +544 -0
  117. browser_use/mcp/controller.py +264 -0
  118. browser_use/mcp/server.py +1114 -0
  119. browser_use/observability.py +204 -0
  120. browser_use/py.typed +0 -0
  121. browser_use/sandbox/__init__.py +41 -0
  122. browser_use/sandbox/sandbox.py +637 -0
  123. browser_use/sandbox/views.py +132 -0
  124. browser_use/screenshots/__init__.py +1 -0
  125. browser_use/screenshots/service.py +52 -0
  126. browser_use/sync/__init__.py +6 -0
  127. browser_use/sync/auth.py +357 -0
  128. browser_use/sync/service.py +161 -0
  129. browser_use/telemetry/__init__.py +51 -0
  130. browser_use/telemetry/service.py +112 -0
  131. browser_use/telemetry/views.py +101 -0
  132. browser_use/tokens/__init__.py +0 -0
  133. browser_use/tokens/custom_pricing.py +24 -0
  134. browser_use/tokens/mappings.py +4 -0
  135. browser_use/tokens/service.py +580 -0
  136. browser_use/tokens/views.py +108 -0
  137. browser_use/tools/registry/service.py +572 -0
  138. browser_use/tools/registry/views.py +174 -0
  139. browser_use/tools/service.py +1675 -0
  140. browser_use/tools/utils.py +82 -0
  141. browser_use/tools/views.py +100 -0
  142. browser_use/utils.py +670 -0
  143. optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
  144. optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
  145. optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
  146. optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
  147. optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,200 @@
1
+ from browser_use.dom.views import EnhancedDOMTreeNode, NodeType
2
+
3
+
4
+ class ClickableElementDetector:
5
+ @staticmethod
6
+ def is_interactive(node: EnhancedDOMTreeNode) -> bool:
7
+ """Check if this node is clickable/interactive using enhanced scoring."""
8
+
9
+ # Skip non-element nodes
10
+ if node.node_type != NodeType.ELEMENT_NODE:
11
+ return False
12
+
13
+ # # if ax ignored skip
14
+ # if node.ax_node and node.ax_node.ignored:
15
+ # return False
16
+
17
+ # remove html and body nodes
18
+ if node.tag_name in {'html', 'body'}:
19
+ return False
20
+
21
+ # IFRAME elements should be interactive if they're large enough to potentially need scrolling
22
+ # Small iframes (< 100px width or height) are unlikely to have scrollable content
23
+ if node.tag_name and node.tag_name.upper() == 'IFRAME' or node.tag_name.upper() == 'FRAME':
24
+ if node.snapshot_node and node.snapshot_node.bounds:
25
+ width = node.snapshot_node.bounds.width
26
+ height = node.snapshot_node.bounds.height
27
+ # Only include iframes larger than 100x100px
28
+ if width > 100 and height > 100:
29
+ return True
30
+
31
+ # RELAXED SIZE CHECK: Allow all elements including size 0 (they might be interactive overlays, etc.)
32
+ # Note: Size 0 elements can still be interactive (e.g., invisible clickable overlays)
33
+ # Visibility is determined separately by CSS styles, not just bounding box size
34
+
35
+ # SEARCH ELEMENT DETECTION: Check for search-related classes and attributes
36
+ if node.attributes:
37
+ search_indicators = {
38
+ 'search',
39
+ 'magnify',
40
+ 'glass',
41
+ 'lookup',
42
+ 'find',
43
+ 'query',
44
+ 'search-icon',
45
+ 'search-btn',
46
+ 'search-button',
47
+ 'searchbox',
48
+ }
49
+
50
+ # Check class names for search indicators
51
+ class_list = node.attributes.get('class', '').lower().split()
52
+ if any(indicator in ' '.join(class_list) for indicator in search_indicators):
53
+ return True
54
+
55
+ # Check id for search indicators
56
+ element_id = node.attributes.get('id', '').lower()
57
+ if any(indicator in element_id for indicator in search_indicators):
58
+ return True
59
+
60
+ # Check data attributes for search functionality
61
+ for attr_name, attr_value in node.attributes.items():
62
+ if attr_name.startswith('data-') and any(indicator in attr_value.lower() for indicator in search_indicators):
63
+ return True
64
+
65
+ # Enhanced accessibility property checks - direct clear indicators only
66
+ if node.ax_node and node.ax_node.properties:
67
+ for prop in node.ax_node.properties:
68
+ try:
69
+ # aria disabled
70
+ if prop.name == 'disabled' and prop.value:
71
+ return False
72
+
73
+ # aria hidden
74
+ if prop.name == 'hidden' and prop.value:
75
+ return False
76
+
77
+ # Direct interactiveness indicators
78
+ if prop.name in ['focusable', 'editable', 'settable'] and prop.value:
79
+ return True
80
+
81
+ # Interactive state properties (presence indicates interactive widget)
82
+ if prop.name in ['checked', 'expanded', 'pressed', 'selected']:
83
+ # These properties only exist on interactive elements
84
+ return True
85
+
86
+ # Form-related interactiveness
87
+ if prop.name in ['required', 'autocomplete'] and prop.value:
88
+ return True
89
+
90
+ # Elements with keyboard shortcuts are interactive
91
+ if prop.name == 'keyshortcuts' and prop.value:
92
+ return True
93
+ except (AttributeError, ValueError):
94
+ # Skip properties we can't process
95
+ continue
96
+
97
+ # ENHANCED TAG CHECK: Include truly interactive elements
98
+ # Note: 'label' removed - labels are handled by other attribute checks below - other wise labels with "for" attribute can destroy the real clickable element on apartments.com
99
+ interactive_tags = {
100
+ 'button',
101
+ 'input',
102
+ 'select',
103
+ 'textarea',
104
+ 'a',
105
+ 'details',
106
+ 'summary',
107
+ 'option',
108
+ 'optgroup',
109
+ }
110
+ # Check with case-insensitive comparison
111
+ if node.tag_name and node.tag_name.lower() in interactive_tags:
112
+ return True
113
+
114
+ # SVG elements need special handling - only interactive if they have explicit handlers
115
+ # svg_tags = {'svg', 'path', 'circle', 'rect', 'polygon', 'ellipse', 'line', 'polyline', 'g'}
116
+ # if node.tag_name in svg_tags:
117
+ # # Only consider SVG elements interactive if they have:
118
+ # # 1. Explicit event handlers
119
+ # # 2. Interactive role attributes
120
+ # # 3. Cursor pointer style
121
+ # if node.attributes:
122
+ # # Check for event handlers
123
+ # if any(attr.startswith('on') for attr in node.attributes):
124
+ # return True
125
+ # # Check for interactive roles
126
+ # if node.attributes.get('role') in {'button', 'link', 'menuitem'}:
127
+ # return True
128
+ # # Check for cursor pointer (indicating clickability)
129
+ # if node.attributes.get('style') and 'cursor: pointer' in node.attributes.get('style', ''):
130
+ # return True
131
+ # # Otherwise, SVG elements are decorative
132
+ # return False
133
+
134
+ # Tertiary check: elements with interactive attributes
135
+ if node.attributes:
136
+ # Check for event handlers or interactive attributes
137
+ interactive_attributes = {'onclick', 'onmousedown', 'onmouseup', 'onkeydown', 'onkeyup', 'tabindex'}
138
+ if any(attr in node.attributes for attr in interactive_attributes):
139
+ return True
140
+
141
+ # Check for interactive ARIA roles
142
+ if 'role' in node.attributes:
143
+ interactive_roles = {
144
+ 'button',
145
+ 'link',
146
+ 'menuitem',
147
+ 'option',
148
+ 'radio',
149
+ 'checkbox',
150
+ 'tab',
151
+ 'textbox',
152
+ 'combobox',
153
+ 'slider',
154
+ 'spinbutton',
155
+ 'search',
156
+ 'searchbox',
157
+ }
158
+ if node.attributes['role'] in interactive_roles:
159
+ return True
160
+
161
+ # Quaternary check: accessibility tree roles
162
+ if node.ax_node and node.ax_node.role:
163
+ interactive_ax_roles = {
164
+ 'button',
165
+ 'link',
166
+ 'menuitem',
167
+ 'option',
168
+ 'radio',
169
+ 'checkbox',
170
+ 'tab',
171
+ 'textbox',
172
+ 'combobox',
173
+ 'slider',
174
+ 'spinbutton',
175
+ 'listbox',
176
+ 'search',
177
+ 'searchbox',
178
+ }
179
+ if node.ax_node.role in interactive_ax_roles:
180
+ return True
181
+
182
+ # ICON AND SMALL ELEMENT CHECK: Elements that might be icons
183
+ if (
184
+ node.snapshot_node
185
+ and node.snapshot_node.bounds
186
+ and 10 <= node.snapshot_node.bounds.width <= 50 # Icon-sized elements
187
+ and 10 <= node.snapshot_node.bounds.height <= 50
188
+ ):
189
+ # Check if this small element has interactive properties
190
+ if node.attributes:
191
+ # Small elements with these attributes are likely interactive icons
192
+ icon_attributes = {'class', 'role', 'onclick', 'data-action', 'aria-label'}
193
+ if any(attr in node.attributes for attr in icon_attributes):
194
+ return True
195
+
196
+ # Final fallback: cursor style indicates interactivity (for cases Chrome missed)
197
+ if node.snapshot_node and node.snapshot_node.cursor_style and node.snapshot_node.cursor_style == 'pointer':
198
+ return True
199
+
200
+ return False
@@ -0,0 +1,287 @@
1
+ # @file purpose: Ultra-compact serializer optimized for code-use agents
2
+ # Focuses on minimal token usage while preserving essential interactive context
3
+
4
+ from browser_use.dom.utils import cap_text_length
5
+ from browser_use.dom.views import (
6
+ EnhancedDOMTreeNode,
7
+ NodeType,
8
+ SimplifiedNode,
9
+ )
10
+
11
+ # Minimal but sufficient attribute list for code agents
12
+ CODE_USE_KEY_ATTRIBUTES = [
13
+ 'id', # Essential for element selection
14
+ 'name', # For form inputs
15
+ 'type', # For input types
16
+ 'placeholder', # For empty inputs
17
+ 'aria-label', # For buttons without text
18
+ 'value', # Current values
19
+ 'alt', # For images
20
+ 'class', # Keep top 2 classes for common selectors
21
+ ]
22
+
23
+ # Interactive elements agent can use
24
+ INTERACTIVE_ELEMENTS = {
25
+ 'a',
26
+ 'button',
27
+ 'input',
28
+ 'textarea',
29
+ 'select',
30
+ 'form',
31
+ }
32
+
33
+ # Semantic structure elements - expanded to include more content containers
34
+ SEMANTIC_STRUCTURE = {
35
+ 'h1',
36
+ 'h2',
37
+ 'h3',
38
+ 'h4',
39
+ 'h5',
40
+ 'h6',
41
+ 'nav',
42
+ 'main',
43
+ 'header',
44
+ 'footer',
45
+ 'article',
46
+ 'section',
47
+ 'p', # Paragraphs often contain prices and product info
48
+ 'span', # Spans often contain prices and labels
49
+ 'div', # Divs with useful attributes (id/class) should be shown
50
+ 'ul',
51
+ 'ol',
52
+ 'li',
53
+ 'label',
54
+ 'img',
55
+ }
56
+
57
+
58
+ class DOMCodeAgentSerializer:
59
+ """Optimized DOM serializer for code-use agents - balances token efficiency with context."""
60
+
61
+ @staticmethod
62
+ def serialize_tree(node: SimplifiedNode | None, include_attributes: list[str], depth: int = 0) -> str:
63
+ """
64
+ Serialize DOM tree with smart token optimization.
65
+
66
+ Strategy:
67
+ - Keep top 2 CSS classes for querySelector compatibility
68
+ - Show div/span/p elements with useful attributes or text
69
+ - Show all interactive + semantic elements
70
+ - Inline text up to 80 chars for better context
71
+ """
72
+ if not node:
73
+ return ''
74
+
75
+ # Skip excluded/hidden nodes
76
+ if hasattr(node, 'excluded_by_parent') and node.excluded_by_parent:
77
+ return DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth)
78
+
79
+ if not node.should_display:
80
+ return DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth)
81
+
82
+ formatted_text = []
83
+ depth_str = ' ' * depth # Use 2 spaces instead of tabs for compactness
84
+
85
+ if node.original_node.node_type == NodeType.ELEMENT_NODE:
86
+ tag = node.original_node.tag_name.lower()
87
+ is_visible = node.original_node.snapshot_node and node.original_node.is_visible
88
+
89
+ # Skip invisible (except iframes)
90
+ if not is_visible and tag not in ['iframe', 'frame']:
91
+ return DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth)
92
+
93
+ # Special handling for iframes
94
+ if tag in ['iframe', 'frame']:
95
+ return DOMCodeAgentSerializer._serialize_iframe(node, include_attributes, depth)
96
+
97
+ # Build minimal attributes
98
+ attributes_str = DOMCodeAgentSerializer._build_minimal_attributes(node.original_node)
99
+
100
+ # Decide if element should be shown
101
+ is_interactive = tag in INTERACTIVE_ELEMENTS
102
+ is_semantic = tag in SEMANTIC_STRUCTURE
103
+ has_useful_attrs = bool(attributes_str)
104
+ has_text = DOMCodeAgentSerializer._has_direct_text(node)
105
+
106
+ # Skip non-semantic, non-interactive containers without attributes
107
+ if not is_interactive and not is_semantic and not has_useful_attrs and not has_text:
108
+ return DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth)
109
+
110
+ # Collapse pointless wrappers
111
+ if tag in {'div', 'span'} and not has_useful_attrs and not has_text and len(node.children) == 1:
112
+ return DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth)
113
+
114
+ # Build element
115
+ line = f'{depth_str}<{tag}'
116
+
117
+ if attributes_str:
118
+ line += f' {attributes_str}'
119
+
120
+ # Inline text
121
+ inline_text = DOMCodeAgentSerializer._get_inline_text(node)
122
+ if inline_text:
123
+ line += f'>{inline_text}'
124
+ else:
125
+ line += '>'
126
+
127
+ formatted_text.append(line)
128
+
129
+ # Children (only if no inline text)
130
+ if node.children and not inline_text:
131
+ children_text = DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth + 1)
132
+ if children_text:
133
+ formatted_text.append(children_text)
134
+
135
+ elif node.original_node.node_type == NodeType.TEXT_NODE:
136
+ # Handled inline with parent
137
+ pass
138
+
139
+ elif node.original_node.node_type == NodeType.DOCUMENT_FRAGMENT_NODE:
140
+ # Shadow DOM - minimal marker
141
+ if node.children:
142
+ formatted_text.append(f'{depth_str}#shadow')
143
+ children_text = DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth + 1)
144
+ if children_text:
145
+ formatted_text.append(children_text)
146
+
147
+ return '\n'.join(formatted_text)
148
+
149
+ @staticmethod
150
+ def _serialize_children(node: SimplifiedNode, include_attributes: list[str], depth: int) -> str:
151
+ """Serialize children."""
152
+ children_output = []
153
+ for child in node.children:
154
+ child_text = DOMCodeAgentSerializer.serialize_tree(child, include_attributes, depth)
155
+ if child_text:
156
+ children_output.append(child_text)
157
+ return '\n'.join(children_output)
158
+
159
+ @staticmethod
160
+ def _build_minimal_attributes(node: EnhancedDOMTreeNode) -> str:
161
+ """Build minimal but useful attributes - keep top 2 classes for selectors."""
162
+ attrs = []
163
+
164
+ if node.attributes:
165
+ for attr in CODE_USE_KEY_ATTRIBUTES:
166
+ if attr in node.attributes:
167
+ value = str(node.attributes[attr]).strip()
168
+ if value:
169
+ # Special handling for class - keep only first 2 classes
170
+ if attr == 'class':
171
+ classes = value.split()[:2]
172
+ value = ' '.join(classes)
173
+ # Cap at 25 chars
174
+ value = cap_text_length(value, 25)
175
+ attrs.append(f'{attr}="{value}"')
176
+
177
+ return ' '.join(attrs)
178
+
179
+ @staticmethod
180
+ def _has_direct_text(node: SimplifiedNode) -> bool:
181
+ """Check if node has direct text children."""
182
+ for child in node.children:
183
+ if child.original_node.node_type == NodeType.TEXT_NODE:
184
+ text = child.original_node.node_value.strip() if child.original_node.node_value else ''
185
+ if len(text) > 1:
186
+ return True
187
+ return False
188
+
189
+ @staticmethod
190
+ def _get_inline_text(node: SimplifiedNode) -> str:
191
+ """Get inline text (max 80 chars for better context)."""
192
+ text_parts = []
193
+ for child in node.children:
194
+ if child.original_node.node_type == NodeType.TEXT_NODE:
195
+ text = child.original_node.node_value.strip() if child.original_node.node_value else ''
196
+ if text and len(text) > 1:
197
+ text_parts.append(text)
198
+
199
+ if not text_parts:
200
+ return ''
201
+
202
+ combined = ' '.join(text_parts)
203
+ return cap_text_length(combined, 40)
204
+
205
+ @staticmethod
206
+ def _serialize_iframe(node: SimplifiedNode, include_attributes: list[str], depth: int) -> str:
207
+ """Handle iframe minimally."""
208
+ formatted_text = []
209
+ depth_str = ' ' * depth
210
+ tag = node.original_node.tag_name.lower()
211
+
212
+ # Minimal iframe marker
213
+ attributes_str = DOMCodeAgentSerializer._build_minimal_attributes(node.original_node)
214
+ line = f'{depth_str}<{tag}'
215
+ if attributes_str:
216
+ line += f' {attributes_str}'
217
+ line += '>'
218
+ formatted_text.append(line)
219
+
220
+ # Iframe content
221
+ if node.original_node.content_document:
222
+ formatted_text.append(f'{depth_str} #iframe-content')
223
+
224
+ # Find and serialize body content only
225
+ for child_node in node.original_node.content_document.children_nodes or []:
226
+ if child_node.tag_name.lower() == 'html':
227
+ for html_child in child_node.children:
228
+ if html_child.tag_name.lower() == 'body':
229
+ for body_child in html_child.children:
230
+ DOMCodeAgentSerializer._serialize_document_node(
231
+ body_child, formatted_text, include_attributes, depth + 2
232
+ )
233
+ break
234
+
235
+ return '\n'.join(formatted_text)
236
+
237
+ @staticmethod
238
+ def _serialize_document_node(
239
+ dom_node: EnhancedDOMTreeNode, output: list[str], include_attributes: list[str], depth: int
240
+ ) -> None:
241
+ """Serialize document node without SimplifiedNode wrapper."""
242
+ depth_str = ' ' * depth
243
+
244
+ if dom_node.node_type == NodeType.ELEMENT_NODE:
245
+ tag = dom_node.tag_name.lower()
246
+
247
+ # Skip invisible
248
+ is_visible = dom_node.snapshot_node and dom_node.is_visible
249
+ if not is_visible:
250
+ return
251
+
252
+ # Check if worth showing
253
+ is_interactive = tag in INTERACTIVE_ELEMENTS
254
+ is_semantic = tag in SEMANTIC_STRUCTURE
255
+ attributes_str = DOMCodeAgentSerializer._build_minimal_attributes(dom_node)
256
+
257
+ if not is_interactive and not is_semantic and not attributes_str:
258
+ # Skip but process children
259
+ for child in dom_node.children:
260
+ DOMCodeAgentSerializer._serialize_document_node(child, output, include_attributes, depth)
261
+ return
262
+
263
+ # Build element
264
+ line = f'{depth_str}<{tag}'
265
+ if attributes_str:
266
+ line += f' {attributes_str}'
267
+
268
+ # Get text
269
+ text_parts = []
270
+ for child in dom_node.children:
271
+ if child.node_type == NodeType.TEXT_NODE and child.node_value:
272
+ text = child.node_value.strip()
273
+ if text and len(text) > 1:
274
+ text_parts.append(text)
275
+
276
+ if text_parts:
277
+ combined = ' '.join(text_parts)
278
+ line += f'>{cap_text_length(combined, 25)}'
279
+ else:
280
+ line += '>'
281
+
282
+ output.append(line)
283
+
284
+ # Process non-text children
285
+ for child in dom_node.children:
286
+ if child.node_type != NodeType.TEXT_NODE:
287
+ DOMCodeAgentSerializer._serialize_document_node(child, output, include_attributes, depth + 1)