@browserbasehq/stagehand 1.0.1

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.
@@ -0,0 +1,474 @@
1
+ (() => {
2
+ // lib/dom/process.ts
3
+ async function processDom(chunksSeen) {
4
+ const { chunk, chunksArray } = await pickChunk(chunksSeen);
5
+ const { outputString, selectorMap } = await processElements2(chunk);
6
+ console.log(
7
+ `Stagehand (Browser Process): Extracted dom elements:
8
+ ${outputString}`
9
+ );
10
+ return {
11
+ outputString,
12
+ selectorMap,
13
+ chunk,
14
+ chunks: chunksArray
15
+ };
16
+ }
17
+ async function processAllOfDom() {
18
+ console.log("Stagehand (Browser Process): Processing all of DOM");
19
+ const viewportHeight = window.innerHeight;
20
+ const documentHeight = document.documentElement.scrollHeight;
21
+ const totalChunks = Math.ceil(documentHeight / viewportHeight);
22
+ let index = 0;
23
+ const results = [];
24
+ for (let chunk = 0; chunk < totalChunks; chunk++) {
25
+ const result = await processElements2(chunk, true, index);
26
+ results.push(result);
27
+ index += Object.keys(result.selectorMap).length;
28
+ }
29
+ await scrollToHeight(0);
30
+ const allOutputString = results.map((result) => result.outputString).join("");
31
+ const allSelectorMap = results.reduce(
32
+ (acc, result) => ({ ...acc, ...result.selectorMap }),
33
+ {}
34
+ );
35
+ console.log(
36
+ `Stagehand (Browser Process): All dom elements: ${allOutputString}`
37
+ );
38
+ return {
39
+ outputString: allOutputString,
40
+ selectorMap: allSelectorMap
41
+ };
42
+ }
43
+ async function scrollToHeight(height) {
44
+ window.scrollTo({ top: height, left: 0, behavior: "smooth" });
45
+ await new Promise((resolve) => {
46
+ let scrollEndTimer;
47
+ const handleScrollEnd = () => {
48
+ clearTimeout(scrollEndTimer);
49
+ scrollEndTimer = window.setTimeout(() => {
50
+ window.removeEventListener("scroll", handleScrollEnd);
51
+ resolve();
52
+ }, 200);
53
+ };
54
+ window.addEventListener("scroll", handleScrollEnd, { passive: true });
55
+ handleScrollEnd();
56
+ });
57
+ }
58
+ async function processElements2(chunk, scrollToChunk = true, indexOffset = 0) {
59
+ console.time("processElements:total");
60
+ const viewportHeight = window.innerHeight;
61
+ const chunkHeight = viewportHeight * chunk;
62
+ const maxScrollTop = document.documentElement.scrollHeight - window.innerHeight;
63
+ const offsetTop = Math.min(chunkHeight, maxScrollTop);
64
+ if (scrollToChunk) {
65
+ console.time("processElements:scroll");
66
+ await scrollToHeight(offsetTop);
67
+ console.timeEnd("processElements:scroll");
68
+ }
69
+ const candidateElements = [];
70
+ const DOMQueue = [...document.body.childNodes];
71
+ const xpathCache = /* @__PURE__ */ new Map();
72
+ console.log("Stagehand (Browser Process): Generating candidate elements");
73
+ console.time("processElements:findCandidates");
74
+ while (DOMQueue.length > 0) {
75
+ const element = DOMQueue.pop();
76
+ let shouldAddElement = false;
77
+ if (element && isElementNode(element)) {
78
+ const childrenCount = element.childNodes.length;
79
+ for (let i = childrenCount - 1; i >= 0; i--) {
80
+ const child = element.childNodes[i];
81
+ DOMQueue.push(child);
82
+ }
83
+ if (isInteractiveElement(element)) {
84
+ if (isActive(element) && isVisible(element)) {
85
+ shouldAddElement = true;
86
+ }
87
+ }
88
+ if (isLeafElement(element)) {
89
+ if (isActive(element) && isVisible(element)) {
90
+ shouldAddElement = true;
91
+ }
92
+ }
93
+ }
94
+ if (element && isTextNode(element) && isTextVisible(element)) {
95
+ shouldAddElement = true;
96
+ }
97
+ if (shouldAddElement) {
98
+ candidateElements.push(element);
99
+ }
100
+ }
101
+ console.timeEnd("processElements:findCandidates");
102
+ const selectorMap = {};
103
+ let outputString = "";
104
+ console.log(
105
+ `Stagehand (Browser Process): Processing candidate elements: ${candidateElements.length}`
106
+ );
107
+ console.time("processElements:processCandidates");
108
+ candidateElements.forEach((element, index) => {
109
+ let xpath = xpathCache.get(element);
110
+ if (!xpath) {
111
+ xpath = generateXPath(element);
112
+ xpathCache.set(element, xpath);
113
+ }
114
+ if (isTextNode(element)) {
115
+ const textContent = element.textContent?.trim();
116
+ if (textContent) {
117
+ outputString += `${index + indexOffset}:${textContent}
118
+ `;
119
+ }
120
+ } else if (isElementNode(element)) {
121
+ const tagName = element.tagName.toLowerCase();
122
+ const attributes = collectEssentialAttributes(element);
123
+ const openingTag = `<${tagName}${attributes ? " " + attributes : ""}>`;
124
+ const closingTag = `</${tagName}>`;
125
+ const textContent = element.textContent?.trim() || "";
126
+ outputString += `${index + indexOffset}:${openingTag}${textContent}${closingTag}
127
+ `;
128
+ }
129
+ selectorMap[index + indexOffset] = xpath;
130
+ });
131
+ console.timeEnd("processElements:processCandidates");
132
+ console.timeEnd("processElements:total");
133
+ return {
134
+ outputString,
135
+ selectorMap
136
+ };
137
+ }
138
+ function collectEssentialAttributes(element) {
139
+ const essentialAttributes = [
140
+ "id",
141
+ "class",
142
+ "href",
143
+ "src",
144
+ "aria-label",
145
+ "aria-name",
146
+ "aria-role",
147
+ "aria-description",
148
+ "aria-expanded",
149
+ "aria-haspopup"
150
+ ];
151
+ const attrs = essentialAttributes.map((attr) => {
152
+ const value = element.getAttribute(attr);
153
+ return value ? `${attr}="${value}"` : "";
154
+ }).filter((attr) => attr !== "");
155
+ Array.from(element.attributes).forEach((attr) => {
156
+ if (attr.name.startsWith("data-")) {
157
+ attrs.push(`${attr.name}="${attr.value}"`);
158
+ }
159
+ });
160
+ return attrs.join(" ");
161
+ }
162
+ window.processDom = processDom;
163
+ window.processAllOfDom = processAllOfDom;
164
+ window.processElements = processElements2;
165
+ window.scrollToHeight = scrollToHeight;
166
+ function generateXPath(element) {
167
+ if (isElementNode(element) && element.id) {
168
+ return `//*[@id='${element.id}']`;
169
+ }
170
+ const parts = [];
171
+ while (element && (isTextNode(element) || isElementNode(element))) {
172
+ let index = 0;
173
+ let hasSameTypeSiblings = false;
174
+ const siblings = element.parentElement ? Array.from(element.parentElement.childNodes) : [];
175
+ for (let i = 0; i < siblings.length; i++) {
176
+ const sibling = siblings[i];
177
+ if (sibling.nodeType === element.nodeType && sibling.nodeName === element.nodeName) {
178
+ index = index + 1;
179
+ hasSameTypeSiblings = true;
180
+ if (sibling.isSameNode(element)) {
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ if (element.nodeName !== "#text") {
186
+ const tagName = element.nodeName.toLowerCase();
187
+ const pathIndex = hasSameTypeSiblings ? `[${index}]` : "";
188
+ parts.unshift(`${tagName}${pathIndex}`);
189
+ }
190
+ element = element.parentElement;
191
+ }
192
+ return parts.length ? `/${parts.join("/")}` : "";
193
+ }
194
+ var leafElementDenyList = ["SVG", "IFRAME", "SCRIPT", "STYLE", "LINK"];
195
+ var interactiveElementTypes = [
196
+ "A",
197
+ "BUTTON",
198
+ "DETAILS",
199
+ "EMBED",
200
+ "INPUT",
201
+ "LABEL",
202
+ "MENU",
203
+ "MENUITEM",
204
+ "OBJECT",
205
+ "SELECT",
206
+ "TEXTAREA",
207
+ "SUMMARY"
208
+ ];
209
+ var interactiveRoles = [
210
+ "button",
211
+ "menu",
212
+ "menuitem",
213
+ "link",
214
+ "checkbox",
215
+ "radio",
216
+ "slider",
217
+ "tab",
218
+ "tabpanel",
219
+ "textbox",
220
+ "combobox",
221
+ "grid",
222
+ "listbox",
223
+ "option",
224
+ "progressbar",
225
+ "scrollbar",
226
+ "searchbox",
227
+ "switch",
228
+ "tree",
229
+ "treeitem",
230
+ "spinbutton",
231
+ "tooltip"
232
+ ];
233
+ var interactiveAriaRoles = ["menu", "menuitem", "button"];
234
+ function isElementNode(node) {
235
+ return node.nodeType === Node.ELEMENT_NODE;
236
+ }
237
+ function isTextNode(node) {
238
+ const trimmedText = node.textContent?.trim().replace(/\s/g, "");
239
+ return node.nodeType === Node.TEXT_NODE && trimmedText !== "";
240
+ }
241
+ var isVisible = (element) => {
242
+ const rect = element.getBoundingClientRect();
243
+ if (rect.width === 0 || rect.height === 0 || rect.top < 0 || rect.top > window.innerHeight) {
244
+ return false;
245
+ }
246
+ if (!isTopElement(element, rect)) {
247
+ return false;
248
+ }
249
+ const visible = element.checkVisibility({
250
+ checkOpacity: true,
251
+ checkVisibilityCSS: true
252
+ });
253
+ return visible;
254
+ };
255
+ var isTextVisible = (element) => {
256
+ const range = document.createRange();
257
+ range.selectNodeContents(element);
258
+ const rect = range.getBoundingClientRect();
259
+ if (rect.width === 0 || rect.height === 0 || rect.top < 0 || rect.top > window.innerHeight) {
260
+ return false;
261
+ }
262
+ const parent = element.parentElement;
263
+ if (!parent) {
264
+ return false;
265
+ }
266
+ if (!isTopElement(parent, rect)) {
267
+ return false;
268
+ }
269
+ const visible = parent.checkVisibility({
270
+ checkOpacity: true,
271
+ checkVisibilityCSS: true
272
+ });
273
+ return visible;
274
+ };
275
+ function isTopElement(elem, rect) {
276
+ const points = [
277
+ { x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.25 },
278
+ { x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.25 },
279
+ { x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.75 },
280
+ { x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.75 },
281
+ { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
282
+ ];
283
+ return points.some((point) => {
284
+ const topEl = document.elementFromPoint(point.x, point.y);
285
+ let current = topEl;
286
+ while (current && current !== document.body) {
287
+ if (current.isSameNode(elem)) {
288
+ return true;
289
+ }
290
+ current = current.parentElement;
291
+ }
292
+ return false;
293
+ });
294
+ }
295
+ var isActive = (element) => {
296
+ if (element.hasAttribute("disabled") || element.hasAttribute("hidden") || element.getAttribute("aria-disabled") === "true") {
297
+ return false;
298
+ }
299
+ return true;
300
+ };
301
+ var isInteractiveElement = (element) => {
302
+ const elementType = element.tagName;
303
+ const elementRole = element.getAttribute("role");
304
+ const elementAriaRole = element.getAttribute("aria-role");
305
+ return elementType && interactiveElementTypes.includes(elementType) || elementRole && interactiveRoles.includes(elementRole) || elementAriaRole && interactiveAriaRoles.includes(elementAriaRole);
306
+ };
307
+ var isLeafElement = (element) => {
308
+ if (element.textContent === "") {
309
+ return false;
310
+ }
311
+ if (element.childNodes.length === 0) {
312
+ return !leafElementDenyList.includes(element.tagName);
313
+ }
314
+ if (element.childNodes.length === 1 && isTextNode(element.childNodes[0])) {
315
+ return true;
316
+ }
317
+ return false;
318
+ };
319
+ async function pickChunk(chunksSeen) {
320
+ const viewportHeight = window.innerHeight;
321
+ const documentHeight = document.documentElement.scrollHeight;
322
+ const chunks = Math.ceil(documentHeight / viewportHeight);
323
+ const chunksArray = Array.from({ length: chunks }, (_, i) => i);
324
+ const chunksRemaining = chunksArray.filter((chunk2) => {
325
+ return !chunksSeen.includes(chunk2);
326
+ });
327
+ const currentScrollPosition = window.scrollY;
328
+ const closestChunk = chunksRemaining.reduce((closest, current) => {
329
+ const currentChunkTop = viewportHeight * current;
330
+ const closestChunkTop = viewportHeight * closest;
331
+ return Math.abs(currentScrollPosition - currentChunkTop) < Math.abs(currentScrollPosition - closestChunkTop) ? current : closest;
332
+ }, chunksRemaining[0]);
333
+ const chunk = closestChunk;
334
+ if (chunk === void 0) {
335
+ throw new Error(`No chunks remaining to check: ${chunksRemaining}`);
336
+ }
337
+ return {
338
+ chunk,
339
+ chunksArray
340
+ };
341
+ }
342
+
343
+ // lib/dom/utils.ts
344
+ async function waitForDomSettle() {
345
+ return new Promise((resolve) => {
346
+ const createTimeout = () => {
347
+ return setTimeout(() => {
348
+ resolve();
349
+ }, 2e3);
350
+ };
351
+ let timeout = createTimeout();
352
+ const observer = new MutationObserver(() => {
353
+ clearTimeout(timeout);
354
+ timeout = createTimeout();
355
+ });
356
+ observer.observe(window.document.body, { childList: true, subtree: true });
357
+ });
358
+ }
359
+ window.waitForDomSettle = waitForDomSettle;
360
+
361
+ // lib/dom/debug.ts
362
+ async function debugDom() {
363
+ window.chunkNumber = 0;
364
+ const { selectorMap, outputString } = await window.processElements(
365
+ window.chunkNumber
366
+ );
367
+ drawChunk(selectorMap);
368
+ setupChunkNav();
369
+ }
370
+ function drawChunk(selectorMap) {
371
+ cleanupMarkers();
372
+ Object.entries(selectorMap).forEach(([_index, selector]) => {
373
+ const element = document.evaluate(
374
+ selector,
375
+ document,
376
+ null,
377
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
378
+ null
379
+ ).singleNodeValue;
380
+ if (element) {
381
+ let rect;
382
+ if (element.nodeType === Node.ELEMENT_NODE) {
383
+ rect = element.getBoundingClientRect();
384
+ } else {
385
+ const range = document.createRange();
386
+ range.selectNodeContents(element);
387
+ rect = range.getBoundingClientRect();
388
+ }
389
+ const color = "grey";
390
+ const overlay = document.createElement("div");
391
+ overlay.style.position = "absolute";
392
+ overlay.style.left = `${rect.left + window.scrollX}px`;
393
+ overlay.style.top = `${rect.top + window.scrollY}px`;
394
+ overlay.style.padding = "2px";
395
+ overlay.style.width = `${rect.width}px`;
396
+ overlay.style.height = `${rect.height}px`;
397
+ overlay.style.backgroundColor = color;
398
+ overlay.className = "stagehand-marker";
399
+ overlay.style.opacity = "0.3";
400
+ overlay.style.zIndex = "1000000000";
401
+ overlay.style.border = "1px solid";
402
+ overlay.style.pointerEvents = "none";
403
+ document.body.appendChild(overlay);
404
+ }
405
+ });
406
+ }
407
+ async function cleanupDebug() {
408
+ cleanupMarkers();
409
+ cleanupNav();
410
+ }
411
+ function cleanupMarkers() {
412
+ const markers = document.querySelectorAll(".stagehand-marker");
413
+ markers.forEach((marker) => {
414
+ marker.remove();
415
+ });
416
+ }
417
+ function cleanupNav() {
418
+ const stagehandNavElements = document.querySelectorAll(".stagehand-nav");
419
+ stagehandNavElements.forEach((element) => {
420
+ element.remove();
421
+ });
422
+ }
423
+ function setupChunkNav() {
424
+ const viewportHeight = window.innerHeight;
425
+ const documentHeight = document.documentElement.scrollHeight;
426
+ const totalChunks = Math.ceil(documentHeight / viewportHeight);
427
+ if (window.chunkNumber > 0) {
428
+ const prevChunkButton = document.createElement("button");
429
+ prevChunkButton.className = "stagehand-nav";
430
+ prevChunkButton.textContent = "Previous";
431
+ prevChunkButton.style.marginLeft = "50px";
432
+ prevChunkButton.style.position = "fixed";
433
+ prevChunkButton.style.bottom = "10px";
434
+ prevChunkButton.style.left = "50%";
435
+ prevChunkButton.style.transform = "translateX(-50%)";
436
+ prevChunkButton.style.zIndex = "1000000000";
437
+ prevChunkButton.onclick = async () => {
438
+ cleanupMarkers();
439
+ cleanupNav();
440
+ window.chunkNumber -= 1;
441
+ window.scrollTo(0, window.chunkNumber * window.innerHeight);
442
+ await window.waitForDomSettle();
443
+ const { selectorMap } = await processElements(window.chunkNumber);
444
+ drawChunk(selectorMap);
445
+ setupChunkNav();
446
+ };
447
+ document.body.appendChild(prevChunkButton);
448
+ }
449
+ if (totalChunks > window.chunkNumber) {
450
+ const nextChunkButton = document.createElement("button");
451
+ nextChunkButton.className = "stagehand-nav";
452
+ nextChunkButton.textContent = "Next";
453
+ nextChunkButton.style.marginRight = "50px";
454
+ nextChunkButton.style.position = "fixed";
455
+ nextChunkButton.style.bottom = "10px";
456
+ nextChunkButton.style.right = "50%";
457
+ nextChunkButton.style.transform = "translateX(50%)";
458
+ nextChunkButton.style.zIndex = "1000000000";
459
+ nextChunkButton.onclick = async () => {
460
+ cleanupMarkers();
461
+ cleanupNav();
462
+ window.chunkNumber += 1;
463
+ window.scrollTo(0, window.chunkNumber * window.innerHeight);
464
+ await window.waitForDomSettle();
465
+ const { selectorMap } = await processElements(window.chunkNumber);
466
+ drawChunk(selectorMap);
467
+ setupChunkNav();
468
+ };
469
+ document.body.appendChild(nextChunkButton);
470
+ }
471
+ }
472
+ window.debugDom = debugDom;
473
+ window.cleanupDebug = cleanupDebug;
474
+ })();