camel-ai 0.2.67__py3-none-any.whl → 0.2.80a2__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 (224) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/_types.py +6 -2
  3. camel/agents/_utils.py +38 -0
  4. camel/agents/chat_agent.py +4014 -410
  5. camel/agents/mcp_agent.py +30 -27
  6. camel/agents/repo_agent.py +2 -1
  7. camel/benchmarks/browsecomp.py +6 -6
  8. camel/configs/__init__.py +15 -0
  9. camel/configs/aihubmix_config.py +88 -0
  10. camel/configs/amd_config.py +70 -0
  11. camel/configs/cometapi_config.py +104 -0
  12. camel/configs/minimax_config.py +93 -0
  13. camel/configs/nebius_config.py +103 -0
  14. camel/configs/vllm_config.py +2 -0
  15. camel/data_collectors/alpaca_collector.py +15 -6
  16. camel/datagen/self_improving_cot.py +1 -1
  17. camel/datasets/base_generator.py +39 -10
  18. camel/environments/__init__.py +12 -0
  19. camel/environments/rlcards_env.py +860 -0
  20. camel/environments/single_step.py +28 -3
  21. camel/environments/tic_tac_toe.py +1 -1
  22. camel/interpreters/__init__.py +2 -0
  23. camel/interpreters/docker/Dockerfile +4 -16
  24. camel/interpreters/docker_interpreter.py +3 -2
  25. camel/interpreters/e2b_interpreter.py +34 -1
  26. camel/interpreters/internal_python_interpreter.py +51 -2
  27. camel/interpreters/microsandbox_interpreter.py +395 -0
  28. camel/loaders/__init__.py +11 -2
  29. camel/loaders/base_loader.py +85 -0
  30. camel/loaders/chunkr_reader.py +9 -0
  31. camel/loaders/firecrawl_reader.py +4 -4
  32. camel/logger.py +1 -1
  33. camel/memories/agent_memories.py +84 -1
  34. camel/memories/base.py +34 -0
  35. camel/memories/blocks/chat_history_block.py +122 -4
  36. camel/memories/blocks/vectordb_block.py +8 -1
  37. camel/memories/context_creators/score_based.py +29 -237
  38. camel/memories/records.py +88 -8
  39. camel/messages/base.py +166 -40
  40. camel/messages/func_message.py +32 -5
  41. camel/models/__init__.py +10 -0
  42. camel/models/aihubmix_model.py +83 -0
  43. camel/models/aiml_model.py +1 -16
  44. camel/models/amd_model.py +101 -0
  45. camel/models/anthropic_model.py +117 -18
  46. camel/models/aws_bedrock_model.py +2 -33
  47. camel/models/azure_openai_model.py +205 -91
  48. camel/models/base_audio_model.py +3 -1
  49. camel/models/base_model.py +189 -24
  50. camel/models/cohere_model.py +5 -17
  51. camel/models/cometapi_model.py +83 -0
  52. camel/models/crynux_model.py +1 -16
  53. camel/models/deepseek_model.py +6 -16
  54. camel/models/fish_audio_model.py +6 -0
  55. camel/models/gemini_model.py +71 -20
  56. camel/models/groq_model.py +1 -17
  57. camel/models/internlm_model.py +1 -16
  58. camel/models/litellm_model.py +49 -32
  59. camel/models/lmstudio_model.py +1 -17
  60. camel/models/minimax_model.py +83 -0
  61. camel/models/mistral_model.py +1 -16
  62. camel/models/model_factory.py +27 -1
  63. camel/models/model_manager.py +24 -6
  64. camel/models/modelscope_model.py +1 -16
  65. camel/models/moonshot_model.py +185 -19
  66. camel/models/nebius_model.py +83 -0
  67. camel/models/nemotron_model.py +0 -5
  68. camel/models/netmind_model.py +1 -16
  69. camel/models/novita_model.py +1 -16
  70. camel/models/nvidia_model.py +1 -16
  71. camel/models/ollama_model.py +4 -19
  72. camel/models/openai_compatible_model.py +171 -46
  73. camel/models/openai_model.py +205 -77
  74. camel/models/openrouter_model.py +1 -17
  75. camel/models/ppio_model.py +1 -16
  76. camel/models/qianfan_model.py +1 -16
  77. camel/models/qwen_model.py +1 -16
  78. camel/models/reka_model.py +1 -16
  79. camel/models/samba_model.py +34 -47
  80. camel/models/sglang_model.py +64 -31
  81. camel/models/siliconflow_model.py +1 -16
  82. camel/models/stub_model.py +0 -4
  83. camel/models/togetherai_model.py +1 -16
  84. camel/models/vllm_model.py +1 -16
  85. camel/models/volcano_model.py +0 -17
  86. camel/models/watsonx_model.py +1 -16
  87. camel/models/yi_model.py +1 -16
  88. camel/models/zhipuai_model.py +60 -16
  89. camel/parsers/__init__.py +18 -0
  90. camel/parsers/mcp_tool_call_parser.py +176 -0
  91. camel/retrievers/auto_retriever.py +1 -0
  92. camel/runtimes/configs.py +11 -11
  93. camel/runtimes/daytona_runtime.py +15 -16
  94. camel/runtimes/docker_runtime.py +6 -6
  95. camel/runtimes/remote_http_runtime.py +5 -5
  96. camel/services/agent_openapi_server.py +380 -0
  97. camel/societies/__init__.py +2 -0
  98. camel/societies/role_playing.py +26 -28
  99. camel/societies/workforce/__init__.py +2 -0
  100. camel/societies/workforce/events.py +122 -0
  101. camel/societies/workforce/prompts.py +249 -38
  102. camel/societies/workforce/role_playing_worker.py +82 -20
  103. camel/societies/workforce/single_agent_worker.py +634 -34
  104. camel/societies/workforce/structured_output_handler.py +512 -0
  105. camel/societies/workforce/task_channel.py +169 -23
  106. camel/societies/workforce/utils.py +176 -9
  107. camel/societies/workforce/worker.py +77 -23
  108. camel/societies/workforce/workflow_memory_manager.py +772 -0
  109. camel/societies/workforce/workforce.py +3168 -478
  110. camel/societies/workforce/workforce_callback.py +74 -0
  111. camel/societies/workforce/workforce_logger.py +203 -175
  112. camel/societies/workforce/workforce_metrics.py +33 -0
  113. camel/storages/__init__.py +4 -0
  114. camel/storages/key_value_storages/json.py +15 -2
  115. camel/storages/key_value_storages/mem0_cloud.py +48 -47
  116. camel/storages/object_storages/google_cloud.py +1 -1
  117. camel/storages/vectordb_storages/__init__.py +6 -0
  118. camel/storages/vectordb_storages/chroma.py +731 -0
  119. camel/storages/vectordb_storages/oceanbase.py +13 -13
  120. camel/storages/vectordb_storages/pgvector.py +349 -0
  121. camel/storages/vectordb_storages/qdrant.py +3 -3
  122. camel/storages/vectordb_storages/surreal.py +365 -0
  123. camel/storages/vectordb_storages/tidb.py +8 -6
  124. camel/tasks/task.py +244 -27
  125. camel/toolkits/__init__.py +46 -8
  126. camel/toolkits/aci_toolkit.py +64 -19
  127. camel/toolkits/arxiv_toolkit.py +6 -6
  128. camel/toolkits/base.py +63 -5
  129. camel/toolkits/code_execution.py +28 -1
  130. camel/toolkits/context_summarizer_toolkit.py +684 -0
  131. camel/toolkits/craw4ai_toolkit.py +93 -0
  132. camel/toolkits/dappier_toolkit.py +10 -6
  133. camel/toolkits/dingtalk.py +1135 -0
  134. camel/toolkits/edgeone_pages_mcp_toolkit.py +49 -0
  135. camel/toolkits/excel_toolkit.py +901 -67
  136. camel/toolkits/file_toolkit.py +1402 -0
  137. camel/toolkits/function_tool.py +30 -6
  138. camel/toolkits/github_toolkit.py +107 -20
  139. camel/toolkits/gmail_toolkit.py +1839 -0
  140. camel/toolkits/google_calendar_toolkit.py +38 -4
  141. camel/toolkits/google_drive_mcp_toolkit.py +54 -0
  142. camel/toolkits/human_toolkit.py +34 -10
  143. camel/toolkits/hybrid_browser_toolkit/__init__.py +18 -0
  144. camel/toolkits/hybrid_browser_toolkit/config_loader.py +185 -0
  145. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +246 -0
  146. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1973 -0
  147. camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
  148. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +3749 -0
  149. camel/toolkits/hybrid_browser_toolkit/ts/package.json +32 -0
  150. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
  151. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +1815 -0
  152. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +233 -0
  153. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +590 -0
  154. camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
  155. camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
  156. camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
  157. camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
  158. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +130 -0
  159. camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +26 -0
  160. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +319 -0
  161. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +1032 -0
  162. camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
  163. camel/toolkits/hybrid_browser_toolkit_py/actions.py +575 -0
  164. camel/toolkits/hybrid_browser_toolkit_py/agent.py +311 -0
  165. camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +787 -0
  166. camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +490 -0
  167. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2390 -0
  168. camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +233 -0
  169. camel/toolkits/hybrid_browser_toolkit_py/stealth_script.js +0 -0
  170. camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +1043 -0
  171. camel/toolkits/image_generation_toolkit.py +390 -0
  172. camel/toolkits/jina_reranker_toolkit.py +3 -4
  173. camel/toolkits/klavis_toolkit.py +5 -1
  174. camel/toolkits/markitdown_toolkit.py +104 -0
  175. camel/toolkits/math_toolkit.py +64 -10
  176. camel/toolkits/mcp_toolkit.py +370 -45
  177. camel/toolkits/memory_toolkit.py +5 -1
  178. camel/toolkits/message_agent_toolkit.py +608 -0
  179. camel/toolkits/message_integration.py +724 -0
  180. camel/toolkits/minimax_mcp_toolkit.py +195 -0
  181. camel/toolkits/note_taking_toolkit.py +277 -0
  182. camel/toolkits/notion_mcp_toolkit.py +224 -0
  183. camel/toolkits/openbb_toolkit.py +5 -1
  184. camel/toolkits/origene_mcp_toolkit.py +56 -0
  185. camel/toolkits/playwright_mcp_toolkit.py +12 -31
  186. camel/toolkits/pptx_toolkit.py +25 -12
  187. camel/toolkits/resend_toolkit.py +168 -0
  188. camel/toolkits/screenshot_toolkit.py +213 -0
  189. camel/toolkits/search_toolkit.py +437 -142
  190. camel/toolkits/slack_toolkit.py +104 -50
  191. camel/toolkits/sympy_toolkit.py +1 -1
  192. camel/toolkits/task_planning_toolkit.py +3 -3
  193. camel/toolkits/terminal_toolkit/__init__.py +18 -0
  194. camel/toolkits/terminal_toolkit/terminal_toolkit.py +957 -0
  195. camel/toolkits/terminal_toolkit/utils.py +532 -0
  196. camel/toolkits/thinking_toolkit.py +1 -1
  197. camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
  198. camel/toolkits/video_analysis_toolkit.py +106 -26
  199. camel/toolkits/video_download_toolkit.py +17 -14
  200. camel/toolkits/web_deploy_toolkit.py +1219 -0
  201. camel/toolkits/wechat_official_toolkit.py +483 -0
  202. camel/toolkits/zapier_toolkit.py +5 -1
  203. camel/types/__init__.py +2 -2
  204. camel/types/agents/tool_calling_record.py +4 -1
  205. camel/types/enums.py +316 -40
  206. camel/types/openai_types.py +2 -2
  207. camel/types/unified_model_type.py +31 -4
  208. camel/utils/commons.py +36 -5
  209. camel/utils/constants.py +3 -0
  210. camel/utils/context_utils.py +1003 -0
  211. camel/utils/mcp.py +138 -4
  212. camel/utils/mcp_client.py +45 -1
  213. camel/utils/message_summarizer.py +148 -0
  214. camel/utils/token_counting.py +43 -20
  215. camel/utils/tool_result.py +44 -0
  216. {camel_ai-0.2.67.dist-info → camel_ai-0.2.80a2.dist-info}/METADATA +296 -85
  217. {camel_ai-0.2.67.dist-info → camel_ai-0.2.80a2.dist-info}/RECORD +219 -146
  218. camel/loaders/pandas_reader.py +0 -368
  219. camel/toolkits/dalle_toolkit.py +0 -175
  220. camel/toolkits/file_write_toolkit.py +0 -444
  221. camel/toolkits/openai_agent_toolkit.py +0 -135
  222. camel/toolkits/terminal_toolkit.py +0 -1037
  223. {camel_ai-0.2.67.dist-info → camel_ai-0.2.80a2.dist-info}/WHEEL +0 -0
  224. {camel_ai-0.2.67.dist-info → camel_ai-0.2.80a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1043 @@
1
+ ((viewport_limit = false) => {
2
+ // Unified analyzer that combines visual and structural analysis
3
+ // Preserves complete snapshot.js logic while adding visual coordinate information
4
+
5
+ // Memory management constants and configuration
6
+ const MAX_REFS = 2000; // Maximum number of refs to keep in memory
7
+ const MAX_UNUSED_AGE_MS = 90000; // Remove refs unused for more than xx seconds
8
+ const CLEANUP_THRESHOLD = 0.8; // Start aggressive cleanup when 80% of max refs reached
9
+
10
+ // Persistent ref management across page analysis calls with memory leak prevention
11
+ let refCounter = window.__camelRefCounter || 1;
12
+ let elementRefMap = window.__camelElementRefMap || new WeakMap();
13
+ let refElementMap = window.__camelRefElementMap || new Map();
14
+ let elementSignatureMap = window.__camelElementSignatureMap || new Map();
15
+
16
+ // LRU tracking for ref access times
17
+ let refAccessTimes = window.__camelRefAccessTimes || new Map();
18
+ let lastNavigationUrl = window.__camelLastNavigationUrl || window.location.href;
19
+
20
+ // Initialize navigation event listeners for automatic cleanup
21
+ if (!window.__camelNavigationListenersInitialized) {
22
+ window.__camelNavigationListenersInitialized = true;
23
+
24
+ // Listen for page navigation events
25
+ window.addEventListener('beforeunload', clearAllRefs);
26
+ window.addEventListener('pagehide', clearAllRefs);
27
+
28
+ // Listen for pushState/replaceState navigation (SPA navigation)
29
+ const originalPushState = history.pushState;
30
+ const originalReplaceState = history.replaceState;
31
+
32
+ history.pushState = function(...args) {
33
+ clearAllRefs();
34
+ return originalPushState.apply(this, args);
35
+ };
36
+
37
+ history.replaceState = function(...args) {
38
+ clearAllRefs();
39
+ return originalReplaceState.apply(this, args);
40
+ };
41
+
42
+ // Listen for popstate (back/forward navigation)
43
+ window.addEventListener('popstate', clearAllRefs);
44
+
45
+ // Check for URL changes periodically (fallback for other navigation types)
46
+ setInterval(() => {
47
+ if (window.location.href !== lastNavigationUrl) {
48
+ clearAllRefs();
49
+ lastNavigationUrl = window.location.href;
50
+ window.__camelLastNavigationUrl = lastNavigationUrl;
51
+ }
52
+ }, 1000);
53
+ }
54
+
55
+ function generateRef() {
56
+ const ref = `e${refCounter++}`;
57
+ // Persist counter globally
58
+ window.__camelRefCounter = refCounter;
59
+ return ref;
60
+ }
61
+
62
+ // Clear all refs and reset memory state
63
+ function clearAllRefs() {
64
+ try {
65
+ // Clear all DOM aria-ref attributes
66
+ document.querySelectorAll('[aria-ref]').forEach(element => {
67
+ element.removeAttribute('aria-ref');
68
+ });
69
+
70
+ // Clear all maps and reset counters
71
+ elementRefMap.clear();
72
+ refElementMap.clear();
73
+ elementSignatureMap.clear();
74
+ refAccessTimes.clear();
75
+
76
+ // Reset global state
77
+ window.__camelElementRefMap = elementRefMap;
78
+ window.__camelRefElementMap = refElementMap;
79
+ window.__camelElementSignatureMap = elementSignatureMap;
80
+ window.__camelRefAccessTimes = refAccessTimes;
81
+
82
+ // Clear cached analysis results
83
+ delete window.__camelLastAnalysisResult;
84
+ delete window.__camelLastAnalysisTime;
85
+
86
+ console.log('CAMEL: Cleared all refs due to navigation');
87
+ } catch (error) {
88
+ console.warn('CAMEL: Error clearing refs:', error);
89
+ }
90
+ }
91
+
92
+ // LRU eviction: Remove least recently used refs when limit exceeded
93
+ function evictLRURefs() {
94
+ const refsToEvict = refAccessTimes.size - MAX_REFS + Math.floor(MAX_REFS * 0.1); // Remove 10% extra for breathing room
95
+ if (refsToEvict <= 0) return 0;
96
+
97
+ // Sort refs by access time (oldest first)
98
+ const sortedRefs = Array.from(refAccessTimes.entries())
99
+ .sort((a, b) => a[1] - b[1])
100
+ .slice(0, refsToEvict);
101
+
102
+ let evictedCount = 0;
103
+ for (const [ref, _] of sortedRefs) {
104
+ const element = refElementMap.get(ref);
105
+ if (element) {
106
+ // Remove aria-ref attribute from DOM
107
+ try {
108
+ element.removeAttribute('aria-ref');
109
+ } catch (e) {
110
+ // Element might be detached from DOM
111
+ }
112
+ elementRefMap.delete(element);
113
+
114
+ // Remove from signature map
115
+ const signature = generateElementSignature(element);
116
+ if (signature && elementSignatureMap.get(signature) === ref) {
117
+ elementSignatureMap.delete(signature);
118
+ }
119
+ }
120
+
121
+ refElementMap.delete(ref);
122
+ refAccessTimes.delete(ref);
123
+ evictedCount++;
124
+ }
125
+
126
+ // Persist updated maps
127
+ window.__camelElementRefMap = elementRefMap;
128
+ window.__camelRefElementMap = refElementMap;
129
+ window.__camelElementSignatureMap = elementSignatureMap;
130
+ window.__camelRefAccessTimes = refAccessTimes;
131
+
132
+ return evictedCount;
133
+ }
134
+
135
+ // Update ref access time for LRU tracking
136
+ function updateRefAccessTime(ref) {
137
+ refAccessTimes.set(ref, Date.now());
138
+ window.__camelRefAccessTimes = refAccessTimes;
139
+ }
140
+
141
+ // Generate a unique signature for an element based on its characteristics
142
+ function generateElementSignature(element) {
143
+ if (!element || !element.tagName) return null;
144
+
145
+ const tagName = element.tagName.toLowerCase();
146
+ const textContent = (element.textContent || '').trim().substring(0, 50);
147
+ const className = element.className || '';
148
+ const id = element.id || '';
149
+ const href = element.href || '';
150
+ const src = element.src || '';
151
+ const value = element.value || '';
152
+ const type = element.type || '';
153
+ const placeholder = element.placeholder || '';
154
+
155
+ // Include position in DOM tree for uniqueness
156
+ let pathElements = [];
157
+ let current = element;
158
+ let depth = 0;
159
+ while (current && current.parentElement && depth < 5) {
160
+ const siblings = Array.from(current.parentElement.children);
161
+ const index = siblings.indexOf(current);
162
+ pathElements.unshift(`${current.tagName.toLowerCase()}[${index}]`);
163
+ current = current.parentElement;
164
+ depth++;
165
+ }
166
+ const domPath = pathElements.join('>');
167
+
168
+ return `${tagName}|${textContent}|${className}|${id}|${href}|${src}|${value}|${type}|${placeholder}|${domPath}`;
169
+ }
170
+
171
+ // Get or assign a persistent ref for an element
172
+ function getOrAssignRef(element) {
173
+ // Check if element already has a ref assigned
174
+ if (elementRefMap.has(element)) {
175
+ const existingRef = elementRefMap.get(element);
176
+ // Verify the ref is still valid
177
+ if (refElementMap.get(existingRef) === element) {
178
+ updateRefAccessTime(existingRef);
179
+ return existingRef;
180
+ }
181
+ }
182
+
183
+ // Check if element has aria-ref attribute (from previous analysis)
184
+ const existingAriaRef = element.getAttribute('aria-ref');
185
+ if (existingAriaRef && refElementMap.get(existingAriaRef) === element) {
186
+ // Re-establish mappings
187
+ elementRefMap.set(element, existingAriaRef);
188
+ updateRefAccessTime(existingAriaRef);
189
+ return existingAriaRef;
190
+ }
191
+
192
+ // Try to find element by signature (in case DOM was modified)
193
+ const signature = generateElementSignature(element);
194
+ if (signature && elementSignatureMap.has(signature)) {
195
+ const existingRef = elementSignatureMap.get(signature);
196
+ // Verify the old element is no longer in DOM or has changed
197
+ const oldElement = refElementMap.get(existingRef);
198
+ if (!oldElement || !document.contains(oldElement) || generateElementSignature(oldElement) !== signature) {
199
+ // Reassign the ref to the new element
200
+ elementRefMap.set(element, existingRef);
201
+ refElementMap.set(existingRef, element);
202
+ elementSignatureMap.set(signature, existingRef);
203
+ element.setAttribute('aria-ref', existingRef);
204
+ updateRefAccessTime(existingRef);
205
+ return existingRef;
206
+ }
207
+ }
208
+
209
+ // Check if we need to evict refs before creating new ones
210
+ if (refElementMap.size >= MAX_REFS) {
211
+ evictLRURefs();
212
+ }
213
+
214
+ // Generate new ref for new element
215
+ const newRef = generateRef();
216
+ elementRefMap.set(element, newRef);
217
+ refElementMap.set(newRef, element);
218
+ if (signature) {
219
+ elementSignatureMap.set(signature, newRef);
220
+ }
221
+ element.setAttribute('aria-ref', newRef);
222
+ updateRefAccessTime(newRef);
223
+ return newRef;
224
+ }
225
+
226
+ // Enhanced cleanup function with aggressive stale ref removal
227
+ function cleanupStaleRefs() {
228
+ const staleRefs = [];
229
+ const currentTime = Date.now();
230
+ const isAggressiveCleanup = refElementMap.size > (MAX_REFS * CLEANUP_THRESHOLD);
231
+
232
+ // Check all mapped elements to see if they're still in DOM or too old
233
+ for (const [ref, element] of refElementMap.entries()) {
234
+ let shouldRemove = false;
235
+
236
+ // Standard checks: element not in DOM
237
+ if (!element || !document.contains(element)) {
238
+ shouldRemove = true;
239
+ }
240
+ // Aggressive cleanup: remove refs unused for too long
241
+ else if (isAggressiveCleanup) {
242
+ const lastAccess = refAccessTimes.get(ref) || 0;
243
+ const age = currentTime - lastAccess;
244
+ if (age > MAX_UNUSED_AGE_MS) {
245
+ shouldRemove = true;
246
+ }
247
+ }
248
+ // Additional checks for aggressive cleanup
249
+ else if (isAggressiveCleanup) {
250
+ // Remove refs for elements that are hidden or have no meaningful content
251
+ try {
252
+ const style = window.getComputedStyle(element);
253
+ const hasNoVisibleContent = !element.textContent?.trim() &&
254
+ !element.value?.trim() &&
255
+ !element.src &&
256
+ !element.href;
257
+
258
+ if ((style.display === 'none' || style.visibility === 'hidden') && hasNoVisibleContent) {
259
+ shouldRemove = true;
260
+ }
261
+ } catch (e) {
262
+ // If we can't get computed style, element might be detached
263
+ shouldRemove = true;
264
+ }
265
+ }
266
+
267
+ if (shouldRemove) {
268
+ staleRefs.push(ref);
269
+ }
270
+ }
271
+
272
+ // Remove stale mappings
273
+ for (const ref of staleRefs) {
274
+ const element = refElementMap.get(ref);
275
+ if (element) {
276
+ // Remove aria-ref attribute from DOM
277
+ try {
278
+ element.removeAttribute('aria-ref');
279
+ } catch (e) {
280
+ // Element might be detached from DOM
281
+ }
282
+ elementRefMap.delete(element);
283
+
284
+ // Remove from signature map
285
+ const signature = generateElementSignature(element);
286
+ if (signature && elementSignatureMap.get(signature) === ref) {
287
+ elementSignatureMap.delete(signature);
288
+ }
289
+ }
290
+ refElementMap.delete(ref);
291
+ refAccessTimes.delete(ref);
292
+ }
293
+
294
+ // Persist maps globally
295
+ window.__camelElementRefMap = elementRefMap;
296
+ window.__camelRefElementMap = refElementMap;
297
+ window.__camelElementSignatureMap = elementSignatureMap;
298
+ window.__camelRefAccessTimes = refAccessTimes;
299
+
300
+ return staleRefs.length;
301
+ }
302
+
303
+ // === Complete snapshot.js logic preservation ===
304
+
305
+ function isVisible(node) {
306
+ // Check if node is null or not a valid DOM node
307
+ if (!node || typeof node.nodeType === 'undefined') return false;
308
+ if (node.nodeType !== Node.ELEMENT_NODE) return true;
309
+
310
+ try {
311
+ const style = window.getComputedStyle(node);
312
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
313
+ return false;
314
+ // An element with `display: contents` is not rendered itself, but its children are.
315
+ if (style.display === 'contents')
316
+ return true;
317
+ const rect = node.getBoundingClientRect();
318
+ return rect.width > 0 && rect.height > 0;
319
+ } catch (e) {
320
+ // If there's an error getting computed style or bounding rect, assume element is not visible
321
+ return false;
322
+ }
323
+ }
324
+
325
+ // Optimized occlusion detection with fewer test points
326
+ function isOccluded(element) {
327
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
328
+
329
+ try {
330
+ const rect = element.getBoundingClientRect();
331
+ if (rect.width === 0 || rect.height === 0) return true;
332
+
333
+ // Simplified: Use fewer test points for better performance
334
+ const testPoints = [
335
+ // Center point (most important)
336
+ { x: rect.left + rect.width * 0.5, y: rect.top + rect.height * 0.5, weight: 4 },
337
+ // Only test 4 strategic points instead of 9
338
+ { x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.25, weight: 1 },
339
+ { x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.25, weight: 1 },
340
+ { x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.75, weight: 1 },
341
+ { x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.75, weight: 1 }
342
+ ];
343
+
344
+ let totalWeight = 0;
345
+ let visibleWeight = 0;
346
+
347
+ for (const point of testPoints) {
348
+ // Skip points outside viewport
349
+ if (point.x < 0 || point.y < 0 ||
350
+ point.x >= window.innerWidth || point.y >= window.innerHeight) {
351
+ continue;
352
+ }
353
+
354
+ const hitElement = document.elementFromPoint(point.x, point.y);
355
+ totalWeight += point.weight;
356
+
357
+ // Simplified visibility check
358
+ if (hitElement && (hitElement === element || element.contains(hitElement) || hitElement.contains(element))) {
359
+ visibleWeight += point.weight;
360
+ }
361
+ }
362
+
363
+ // If no valid test points, assume not occluded
364
+ if (totalWeight === 0) return false;
365
+
366
+ // Element is occluded if less than 40% of weighted points are visible
367
+ return (visibleWeight / totalWeight) < 0.4;
368
+
369
+ } catch (e) {
370
+ return false;
371
+ }
372
+ }
373
+
374
+ function getRole(node) {
375
+ // Check if node is null or doesn't have required properties
376
+ if (!node || !node.tagName || !node.getAttribute) {
377
+ return 'generic';
378
+ }
379
+
380
+ const role = node.getAttribute('role');
381
+ if (role) return role;
382
+
383
+ const tagName = node.tagName.toLowerCase();
384
+
385
+ // Extended role mapping to better match Playwright
386
+ if (tagName === 'a') return 'link';
387
+ if (tagName === 'button') return 'button';
388
+ if (tagName === 'input') {
389
+ const type = node.getAttribute('type')?.toLowerCase();
390
+ if (['button', 'checkbox', 'radio', 'reset', 'submit'].includes(type)) return type;
391
+ return 'textbox';
392
+ }
393
+ if (['select', 'textarea'].includes(tagName)) return tagName;
394
+ if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) return 'heading';
395
+
396
+ // Additional roles for better Playwright compatibility
397
+ if (tagName === 'img') return 'img';
398
+ if (tagName === 'main') return 'main';
399
+ if (tagName === 'nav') return 'navigation';
400
+ if (tagName === 'ul' || tagName === 'ol') return 'list';
401
+ if (tagName === 'li') return 'listitem';
402
+ if (tagName === 'em') return 'emphasis';
403
+ if (tagName === 'form' && node.getAttribute('role') === 'search') return 'search';
404
+ if (tagName === 'section' || tagName === 'article') return 'region';
405
+ if (tagName === 'aside') return 'complementary';
406
+ if (tagName === 'header') return 'banner';
407
+ if (tagName === 'footer') return 'contentinfo';
408
+ if (tagName === 'fieldset') return 'group';
409
+
410
+ // Enhanced role mappings for table elements
411
+ if (tagName === 'table') return 'table';
412
+ if (tagName === 'tr') return 'row';
413
+ if (tagName === 'td' || tagName === 'th') return 'cell';
414
+
415
+ return 'generic';
416
+ }
417
+
418
+ // Playwright-inspired function to check if element receives pointer events
419
+ function receivesPointerEvents(element) {
420
+ if (!element || !element.nodeType || element.nodeType !== Node.ELEMENT_NODE) return false;
421
+
422
+ try {
423
+ let e = element;
424
+ while (e) {
425
+ const style = window.getComputedStyle(e);
426
+ if (!style) break;
427
+
428
+ const pointerEvents = style.pointerEvents;
429
+ if (pointerEvents === 'none') return false;
430
+ if (pointerEvents && pointerEvents !== 'auto') return true;
431
+
432
+ e = e.parentElement;
433
+ }
434
+ return true;
435
+ } catch (error) {
436
+ return false;
437
+ }
438
+ }
439
+
440
+ // Playwright-inspired function to check if element has pointer cursor
441
+ function hasPointerCursor(element) {
442
+ if (!element || !element.nodeType || element.nodeType !== Node.ELEMENT_NODE) return false;
443
+
444
+ try {
445
+ const style = window.getComputedStyle(element);
446
+ return style.cursor === 'pointer';
447
+ } catch (error) {
448
+ return false;
449
+ }
450
+ }
451
+
452
+ // Playwright-inspired function to get aria level
453
+ function getAriaLevel(element) {
454
+ if (!element || !element.tagName) return 0;
455
+
456
+ // Native HTML heading levels (H1=1, H2=2, etc.)
457
+ const tagName = element.tagName.toUpperCase();
458
+ const nativeLevel = { 'H1': 1, 'H2': 2, 'H3': 3, 'H4': 4, 'H5': 5, 'H6': 6 }[tagName];
459
+ if (nativeLevel) return nativeLevel;
460
+
461
+ // Check aria-level attribute for roles that support it
462
+ const role = getRole(element);
463
+ const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem'];
464
+ if (kAriaLevelRoles.includes(role)) {
465
+ const ariaLevel = element.getAttribute('aria-level');
466
+ if (ariaLevel !== null) {
467
+ const value = Number(ariaLevel);
468
+ if (Number.isInteger(value) && value >= 1) {
469
+ return value;
470
+ }
471
+ }
472
+ }
473
+
474
+ return 0;
475
+ }
476
+
477
+ function getAccessibleName(node) {
478
+ // Check if node is null or doesn't have required methods
479
+ if (!node || !node.hasAttribute || !node.getAttribute) return '';
480
+
481
+ if (node.hasAttribute('aria-label')) return node.getAttribute('aria-label') || '';
482
+ if (node.hasAttribute('aria-labelledby')) {
483
+ const id = node.getAttribute('aria-labelledby');
484
+ const labelEl = document.getElementById(id);
485
+ if (labelEl) return labelEl.textContent || '';
486
+ }
487
+ // This is the new, visibility-aware text extraction logic.
488
+ const text = getVisibleTextContent(node);
489
+
490
+ // Add a heuristic to ignore code-like text that might be in the DOM
491
+ if ((text.match(/[;:{}]/g)?.length || 0) > 2) return '';
492
+
493
+
494
+
495
+ return text;
496
+ }
497
+
498
+ const textCache = new Map();
499
+ function getVisibleTextContent(_node) {
500
+ // Check if node is null or doesn't have nodeType
501
+ if (!_node || typeof _node.nodeType === 'undefined') return '';
502
+
503
+ if (textCache.has(_node)) return textCache.get(_node);
504
+
505
+ if (_node.nodeType === Node.TEXT_NODE) {
506
+ // For a text node, its content is visible if its parent is.
507
+ // The isVisible check on the parent happens before this recursion.
508
+ return _node.nodeValue || '';
509
+ }
510
+
511
+ if (_node.nodeType !== Node.ELEMENT_NODE || !isVisible(_node) || ['SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'HEAD'].includes(_node.tagName)) {
512
+ return '';
513
+ }
514
+
515
+ let result = '';
516
+ for (const child of _node.childNodes) {
517
+ result += getVisibleTextContent(child);
518
+ }
519
+
520
+ // Caching the result for performance.
521
+ textCache.set(_node, result);
522
+ return result;
523
+ }
524
+
525
+ /**
526
+ * Phase 1: Build an in-memory representation of the accessibility tree.
527
+ * Complete preservation of snapshot.js buildAriaTree logic
528
+ */
529
+ function buildAriaTree(rootElement) {
530
+ const visited = new Set();
531
+
532
+ function toAriaNode(element) {
533
+ // Check if element is null or not a valid DOM element
534
+ if (!element || !element.tagName) return null;
535
+
536
+ // Only consider visible elements
537
+ if (!isVisible(element)) return null;
538
+
539
+ const role = getRole(element);
540
+ // 'presentation' and 'none' roles are ignored, but their children are processed.
541
+ if (['presentation', 'none'].includes(role)) return null;
542
+
543
+ const name = getAccessibleName(element);
544
+
545
+ // Get persistent ref for this element
546
+ const ref = getOrAssignRef(element);
547
+
548
+ // Create the node
549
+ const node = {
550
+ role,
551
+ name,
552
+ children: [],
553
+ element: element,
554
+ ref: ref,
555
+ };
556
+
557
+ // Add states for interactive elements, similar to Playwright
558
+ if (element.hasAttribute('disabled') || element.disabled) node.disabled = true;
559
+
560
+ // NEW: Check if element is occluded and mark with occluded tag
561
+ if (isOccluded(element)) {
562
+ node.occluded = true; // Mark as occluded but don't disable
563
+ }
564
+
565
+ // Handle aria-checked and native checked
566
+ const ariaChecked = element.getAttribute('aria-checked');
567
+ if (ariaChecked) {
568
+ node.checked = ariaChecked;
569
+ } else if (element.type === 'checkbox' || element.type === 'radio') {
570
+ node.checked = element.checked;
571
+ }
572
+
573
+ // Handle aria-expanded
574
+ const ariaExpanded = element.getAttribute('aria-expanded');
575
+ if (ariaExpanded) {
576
+ node.expanded = ariaExpanded === 'true';
577
+ }
578
+
579
+ // Handle aria-selected
580
+ const ariaSelected = element.getAttribute('aria-selected');
581
+ if (ariaSelected === 'true') {
582
+ node.selected = true;
583
+ }
584
+
585
+ // Add level support following Playwright's implementation
586
+ const level = getAriaLevel(element);
587
+ if (level > 0) node.level = level;
588
+
589
+
590
+
591
+ return node;
592
+ }
593
+
594
+ function traverse(element, parentNode) {
595
+ // Check if element is null or not a valid DOM element
596
+ if (!element || !element.tagName || visited.has(element)) return;
597
+ visited.add(element);
598
+
599
+ // FIX: Completely skip script and style tags and their children.
600
+ const tagName = element.tagName.toLowerCase();
601
+ if (['script', 'style', 'meta', 'noscript'].includes(tagName))
602
+ return;
603
+
604
+ // Check if element is explicitly hidden by CSS - if so, skip entirely including children
605
+ const style = window.getComputedStyle(element);
606
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
607
+ return;
608
+ }
609
+
610
+ const ariaNode = toAriaNode(element);
611
+ // If the element is not rendered or is presentational, its children
612
+ // are attached directly to the parent.
613
+ const newParent = ariaNode || parentNode;
614
+ if (ariaNode) parentNode.children.push(ariaNode);
615
+
616
+ for (const child of element.childNodes) {
617
+ if (child.nodeType === Node.ELEMENT_NODE) {
618
+ traverse(child, newParent);
619
+ } else if (child.nodeType === Node.TEXT_NODE) {
620
+ const text = (child.textContent || '').trim();
621
+ if (text) newParent.children.push(text);
622
+ }
623
+ }
624
+
625
+ // Also traverse into shadow DOM if it exists
626
+ if (element.shadowRoot) {
627
+ for (const child of element.shadowRoot.childNodes) {
628
+ if (child.nodeType === Node.ELEMENT_NODE) {
629
+ traverse(child, newParent);
630
+ } else if (child.nodeType === Node.TEXT_NODE) {
631
+ const text = (child.textContent || '').trim();
632
+ if (text) newParent.children.push(text);
633
+ }
634
+ }
635
+ }
636
+
637
+ // FIX: Remove redundant text children that match the element's name
638
+ if (ariaNode && ariaNode.children.length > 0) {
639
+ // Remove text children that are the same as the parent's name or are contained in it
640
+ ariaNode.children = ariaNode.children.filter(child => {
641
+ if (typeof child === 'string') {
642
+ const childText = child.trim();
643
+ const parentName = ariaNode.name.trim();
644
+
645
+ // Remove if text child exactly matches parent name
646
+ if (childText === parentName) {
647
+ return false;
648
+ }
649
+
650
+ // Also remove if the child text is completely contained in parent name
651
+ // and represents a significant portion (to avoid removing important partial text)
652
+ if (childText.length > 3 && parentName.includes(childText)) {
653
+ return false;
654
+ }
655
+
656
+ return true;
657
+ }
658
+ return true;
659
+ });
660
+
661
+ // If after filtering, we have only one text child that equals the name, remove it
662
+ if (ariaNode.children.length === 1 && typeof ariaNode.children[0] === 'string' && ariaNode.name === ariaNode.children[0]) {
663
+ ariaNode.children = [];
664
+ }
665
+ }
666
+ }
667
+
668
+ const root = { role: 'Root', name: '', children: [], element: rootElement };
669
+ traverse(rootElement, root);
670
+ return root;
671
+ }
672
+
673
+ /**
674
+ * Phase 2: Normalize the tree by removing redundant generic wrappers.
675
+ * Complete preservation of snapshot.js normalizeTree logic with cursor inheritance
676
+ */
677
+ function normalizeTree(node) {
678
+ if (typeof node === 'string') return [node];
679
+
680
+ const newChildren = [];
681
+ for (const child of node.children) {
682
+ newChildren.push(...normalizeTree(child));
683
+ }
684
+ node.children = newChildren;
685
+
686
+ // Remove child elements that have the same name as their parent
687
+ // and inherit cursor=pointer property if child had it
688
+ const filteredChildren = [];
689
+ for (const child of node.children) {
690
+ if (typeof child !== 'string' && child.name && node.name) {
691
+ const childName = child.name.trim();
692
+ const parentName = node.name.trim();
693
+ if (childName === parentName) {
694
+ // If child has same name as parent, merge its children into parent
695
+ filteredChildren.push(...(child.children || []));
696
+
697
+ // Inherit cursor=pointer from merged child
698
+ if (child.element && receivesPointerEvents(child.element) && hasPointerCursor(child.element)) {
699
+ node.inheritedCursor = true;
700
+ }
701
+
702
+ // Also inherit other properties if needed
703
+ if (child.disabled && !node.disabled) node.disabled = child.disabled;
704
+ if (child.selected && !node.selected) node.selected = child.selected;
705
+ } else {
706
+ filteredChildren.push(child);
707
+ }
708
+ } else {
709
+ filteredChildren.push(child);
710
+ }
711
+ }
712
+ node.children = filteredChildren;
713
+
714
+ // Also handle the case where we have only one child with same name
715
+ if (node.children.length === 1 && typeof node.children[0] !== 'string') {
716
+ const child = node.children[0];
717
+ if (child.name && node.name && child.name.trim() === node.name.trim()) {
718
+ // Inherit cursor=pointer from the child being merged
719
+ if (child.element && receivesPointerEvents(child.element) && hasPointerCursor(child.element)) {
720
+ node.inheritedCursor = true;
721
+ }
722
+
723
+ // Also inherit other properties
724
+ if (child.disabled && !node.disabled) node.disabled = child.disabled;
725
+ if (child.selected && !node.selected) node.selected = child.selected;
726
+
727
+ // Merge child's children into parent and remove the redundant child
728
+ node.children = child.children || [];
729
+ }
730
+ }
731
+
732
+ // A 'generic' role that just wraps a single other element is redundant.
733
+ // We lift its child up to replace it, simplifying the hierarchy.
734
+ const isRedundantWrapper = node.role === 'generic' && node.children.length === 1 && typeof node.children[0] !== 'string';
735
+ if (isRedundantWrapper) {
736
+ return node.children;
737
+ }
738
+
739
+
740
+
741
+ return [node];
742
+ }
743
+
744
+ /**
745
+ * Phase 3: Render the normalized tree into the final string format.
746
+ * Complete preservation of snapshot.js renderTree logic with Playwright enhancements
747
+ */
748
+ function renderTree(node, indent = '') {
749
+ const lines = [];
750
+ let meaningfulProps = '';
751
+ if (node.disabled) meaningfulProps += ' [disabled]';
752
+ if (node.occluded) meaningfulProps += ' [occluded]';
753
+ if (node.checked !== undefined) meaningfulProps += ` checked=${node.checked}`;
754
+ if (node.expanded !== undefined) meaningfulProps += ` expanded=${node.expanded}`;
755
+ if (node.selected) meaningfulProps += ' [selected]';
756
+
757
+ // Add level attribute following Playwright's format
758
+ if (node.level !== undefined) meaningfulProps += ` [level=${node.level}]`;
759
+
760
+ const ref = node.ref ? ` [ref=${node.ref}]` : '';
761
+
762
+ // Add cursor=pointer detection following Playwright's implementation
763
+ // Check both direct cursor and inherited cursor from merged children
764
+ let cursor = '';
765
+ const hasDirectCursor = node.element && receivesPointerEvents(node.element) && hasPointerCursor(node.element);
766
+ const hasInheritedCursor = node.inheritedCursor;
767
+
768
+ // Only add cursor=pointer if element is not occluded
769
+ if ((hasDirectCursor || hasInheritedCursor) && !node.occluded) {
770
+ cursor = ' [cursor=pointer]';
771
+ }
772
+
773
+ const name = (node.name || '').replace(/\s+/g, ' ').trim();
774
+
775
+ // Skip elements with empty names and no meaningful props (ref and cursor are not considered meaningful for this check)
776
+ if (!name && !meaningfulProps) {
777
+ // If element has no name and no meaningful props, render its children directly at current level
778
+ for (const child of node.children) {
779
+ if (typeof child === 'string') {
780
+ const childText = child.replace(/\s+/g, ' ').trim();
781
+ if (childText) { // Only add non-empty text
782
+ lines.push(`${indent}- text "${childText}"`);
783
+ }
784
+ } else {
785
+ lines.push(...renderTree(child, indent));
786
+ }
787
+ }
788
+ return lines;
789
+ }
790
+
791
+ lines.push(`${indent}- ${node.role}${name ? ` "${name}"` : ''}${meaningfulProps}${ref}${cursor}`);
792
+
793
+ for (const child of node.children) {
794
+ if (typeof child === 'string') {
795
+ const childText = child.replace(/\s+/g, ' ').trim();
796
+ if (childText) { // Only add non-empty text
797
+ lines.push(`${indent} - text "${childText}"`);
798
+ }
799
+ } else {
800
+ lines.push(...renderTree(child, indent + ' '));
801
+ }
802
+ }
803
+ return lines;
804
+ }
805
+
806
+ function processDocument(doc) {
807
+ if (!doc.body) return [];
808
+
809
+ // Clear cache for each new document processing.
810
+ textCache.clear();
811
+ let tree = buildAriaTree(doc.body);
812
+ [tree] = normalizeTree(tree);
813
+
814
+ const lines = renderTree(tree).slice(1); // Skip the root node line
815
+
816
+ const frames = doc.querySelectorAll('iframe');
817
+ for (const frame of frames) {
818
+ try {
819
+ if (frame.contentDocument) {
820
+ lines.push(...processDocument(frame.contentDocument));
821
+ }
822
+ } catch (e) {
823
+ // Skip cross-origin iframes
824
+ }
825
+ }
826
+ return lines;
827
+ }
828
+
829
+ // === Visual analysis functions from page_script.js ===
830
+
831
+ // Check if element is within the current viewport
832
+ function isInViewport(element) {
833
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
834
+
835
+ try {
836
+ const rect = element.getBoundingClientRect();
837
+ return (
838
+ rect.top >= 0 &&
839
+ rect.left >= 0 &&
840
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
841
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)
842
+ );
843
+ } catch (e) {
844
+ return false;
845
+ }
846
+ }
847
+
848
+ // From page_script.js - check if element is topmost at coordinates
849
+ function isTopmost(element, x, y) {
850
+ let hit = document.elementFromPoint(x, y);
851
+ if (hit === null) return true;
852
+
853
+ while (hit) {
854
+ if (hit == element) return true;
855
+ hit = hit.parentNode;
856
+ }
857
+ return false;
858
+ }
859
+
860
+ // From page_script.js - get visual coordinates
861
+ function getElementCoordinates(element) {
862
+ let rects = element.getClientRects();
863
+ let scale = window.devicePixelRatio || 1;
864
+ let validRects = [];
865
+
866
+ for (const rect of rects) {
867
+ let x = rect.left + rect.width / 2;
868
+ let y = rect.top + rect.height / 2;
869
+ if (isTopmost(element, x, y)) {
870
+ validRects.push({
871
+ x: rect.x * scale,
872
+ y: rect.y * scale,
873
+ width: rect.width * scale,
874
+ height: rect.height * scale,
875
+ top: rect.top * scale,
876
+ left: rect.left * scale,
877
+ right: rect.right * scale,
878
+ bottom: rect.bottom * scale
879
+ });
880
+ }
881
+ }
882
+
883
+ return validRects;
884
+ }
885
+
886
+ // === Unified analysis function ===
887
+
888
+ function collectElementsFromTree(node, elementsMap, viewportLimitEnabled = false) {
889
+ if (typeof node === 'string') return;
890
+
891
+ if (node.element && node.ref) {
892
+ // If viewport_limit is enabled, only include elements that are in the viewport
893
+ if (viewportLimitEnabled && !isInViewport(node.element)) {
894
+ // Skip this element but still process its children
895
+ if (node.children) {
896
+ for (const child of node.children) {
897
+ collectElementsFromTree(child, elementsMap, viewportLimitEnabled);
898
+ }
899
+ }
900
+ return;
901
+ }
902
+
903
+ // Get visual coordinates for this element
904
+ const coordinates = getElementCoordinates(node.element);
905
+
906
+ // Store comprehensive element information
907
+ elementsMap[node.ref] = {
908
+ // Structural information (preserved from snapshot.js)
909
+ role: node.role,
910
+ name: node.name,
911
+ tagName: node.element.tagName.toLowerCase(),
912
+ disabled: node.disabled,
913
+ checked: node.checked,
914
+ expanded: node.expanded,
915
+ level: node.level,
916
+
917
+ // Visual information (from page_script.js)
918
+ coordinates: coordinates,
919
+
920
+ // Additional metadata
921
+ href: node.element.href || null,
922
+ value: node.element.value || null,
923
+ placeholder: node.element.placeholder || null,
924
+ scrollable: node.element.scrollHeight > node.element.clientHeight,
925
+
926
+ // Playwright-inspired properties
927
+ receivesPointerEvents: receivesPointerEvents(node.element),
928
+ hasPointerCursor: hasPointerCursor(node.element)
929
+ };
930
+ }
931
+
932
+ // Recursively process children
933
+ if (node.children) {
934
+ for (const child of node.children) {
935
+ collectElementsFromTree(child, elementsMap, viewportLimitEnabled);
936
+ }
937
+ }
938
+ }
939
+
940
+ function analyzePageElements() {
941
+ // Clean up stale refs before analysis
942
+ const cleanedRefCount = cleanupStaleRefs();
943
+
944
+ // Performance optimization: Check if we can reuse recent analysis
945
+ const currentTime = Date.now();
946
+ const lastAnalysisTime = window.__camelLastAnalysisTime || 0;
947
+ const timeSinceLastAnalysis = currentTime - lastAnalysisTime;
948
+
949
+ // If less than 1 second since last analysis and page hasn't changed significantly
950
+ if (timeSinceLastAnalysis < 1000 && window.__camelLastAnalysisResult && cleanedRefCount === 0) {
951
+ const cachedResult = window.__camelLastAnalysisResult;
952
+ // Update timestamp and memory info in cached result
953
+ cachedResult.metadata.timestamp = new Date().toISOString();
954
+ cachedResult.metadata.memoryInfo = {
955
+ currentRefCount: refElementMap.size,
956
+ maxRefs: MAX_REFS,
957
+ memoryUtilization: (refElementMap.size / MAX_REFS * 100).toFixed(1) + '%',
958
+ lruAccessTimesCount: refAccessTimes.size
959
+ };
960
+ cachedResult.metadata.cacheHit = true;
961
+ return cachedResult;
962
+ }
963
+
964
+ // Generate the complete structured snapshot using original snapshot.js logic
965
+ const outputLines = processDocument(document);
966
+ const snapshotText = outputLines.join('\n');
967
+
968
+ // Build the tree again to collect element information with visual data
969
+ textCache.clear();
970
+ // Note: Don't reset refCounter anymore - use persistent counters
971
+ let tree = buildAriaTree(document.body);
972
+ [tree] = normalizeTree(tree);
973
+
974
+ const elementsMap = {};
975
+ collectElementsFromTree(tree, elementsMap, viewport_limit);
976
+
977
+ // Verify uniqueness of aria-ref attributes (debugging aid)
978
+ const ariaRefCounts = {};
979
+ document.querySelectorAll('[aria-ref]').forEach(element => {
980
+ const ref = element.getAttribute('aria-ref');
981
+ ariaRefCounts[ref] = (ariaRefCounts[ref] || 0) + 1;
982
+ });
983
+
984
+ // Log any duplicates for debugging
985
+ const duplicateRefs = Object.entries(ariaRefCounts).filter(([ref, count]) => count > 1);
986
+ if (duplicateRefs.length > 0) {
987
+ console.warn('Duplicate aria-ref attributes found:', duplicateRefs);
988
+ }
989
+
990
+ // Validate ref consistency
991
+ const refValidationErrors = [];
992
+ for (const [ref, elementInfo] of Object.entries(elementsMap)) {
993
+ const mappedElement = refElementMap.get(ref);
994
+ if (!mappedElement || !document.contains(mappedElement)) {
995
+ refValidationErrors.push(`Ref ${ref} points to invalid or removed element`);
996
+ }
997
+ }
998
+
999
+ const result = {
1000
+ url: window.location.href,
1001
+ elements: elementsMap,
1002
+ snapshotText: snapshotText,
1003
+ metadata: {
1004
+ timestamp: new Date().toISOString(),
1005
+ elementCount: Object.keys(elementsMap).length,
1006
+ screenInfo: {
1007
+ width: window.innerWidth,
1008
+ height: window.innerHeight,
1009
+ devicePixelRatio: window.devicePixelRatio || 1
1010
+ },
1011
+ // Enhanced debugging information
1012
+ ariaRefCounts: ariaRefCounts,
1013
+ duplicateRefsFound: duplicateRefs.length > 0,
1014
+ staleRefsCleanedUp: cleanedRefCount,
1015
+ refValidationErrors: refValidationErrors,
1016
+ totalMappedRefs: refElementMap.size,
1017
+ refCounterValue: refCounter,
1018
+ // Memory management information
1019
+ memoryInfo: {
1020
+ currentRefCount: refElementMap.size,
1021
+ maxRefs: MAX_REFS,
1022
+ memoryUtilization: (refElementMap.size / MAX_REFS * 100).toFixed(1) + '%',
1023
+ lruAccessTimesCount: refAccessTimes.size,
1024
+ unusedAgeThreshold: MAX_UNUSED_AGE_MS + 'ms',
1025
+ cleanupThreshold: (CLEANUP_THRESHOLD * 100).toFixed(0) + '%',
1026
+ isAggressiveCleanup: refElementMap.size > (MAX_REFS * CLEANUP_THRESHOLD)
1027
+ },
1028
+ // Performance information
1029
+ cacheHit: false,
1030
+ analysisTime: Date.now() - currentTime
1031
+ }
1032
+ };
1033
+
1034
+ // Cache the result for potential reuse
1035
+ window.__camelLastAnalysisResult = result;
1036
+ window.__camelLastAnalysisTime = currentTime;
1037
+
1038
+ return result;
1039
+ }
1040
+
1041
+ // Execute analysis and return result
1042
+ return analyzePageElements();
1043
+ })();