Skip to content

Commit 5ae360d

Browse files
Merge branch 'main' into dont-close-new-tabs
2 parents 4eb67cd + 890ffcc commit 5ae360d

File tree

11 files changed

+273
-23
lines changed

11 files changed

+273
-23
lines changed

.changeset/evil-snakes-juggle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
return "not-supported" for elements inside the shadow-dom

.changeset/upset-socks-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
Filter attaching to target worker / shared_worker

.github/workflows/feature-parity.yml

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
name: Feature Parity
2+
3+
on:
4+
pull_request:
5+
types:
6+
- opened
7+
- synchronize
8+
- labeled
9+
- unlabeled
10+
paths-ignore:
11+
- "docs/**"
12+
13+
jobs:
14+
check-parity-label:
15+
runs-on: ubuntu-latest
16+
if: github.event.action == 'labeled' && github.event.label.name == 'parity'
17+
permissions:
18+
contents: read
19+
pull-requests: write
20+
issues: write
21+
steps:
22+
- name: Check out repository code
23+
uses: actions/checkout@v4
24+
25+
- name: Check user permissions
26+
uses: actions/github-script@v7
27+
with:
28+
github-token: ${{ secrets.GITHUB_TOKEN }}
29+
script: |
30+
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
31+
owner: context.repo.owner,
32+
repo: context.repo.repo,
33+
username: context.actor
34+
});
35+
36+
const hasWriteAccess = ['admin', 'write'].includes(permission.permission);
37+
38+
if (!hasWriteAccess) {
39+
// Remove the parity label if user doesn't have write access
40+
await github.rest.issues.removeLabel({
41+
owner: context.repo.owner,
42+
repo: context.repo.repo,
43+
issue_number: context.issue.number,
44+
name: 'parity'
45+
});
46+
47+
// Add a comment explaining why the label was removed
48+
await github.rest.issues.createComment({
49+
owner: context.repo.owner,
50+
repo: context.repo.repo,
51+
issue_number: context.issue.number,
52+
body: `❌ **Parity Label Removed**\n\n@${context.actor}, you do not have sufficient permissions to add the 'parity' label. Only users with write access can trigger feature parity issues.\n\nIf you believe this feature should be implemented in the Python SDK, please ask a maintainer to add the label.`
53+
});
54+
55+
throw new Error(`User ${context.actor} does not have write access to add parity label`);
56+
}
57+
58+
console.log(`User ${context.actor} has ${permission.permission} access - proceeding with parity workflow`);
59+
60+
- name: Generate GitHub App token
61+
id: generate-token
62+
uses: actions/create-github-app-token@v1
63+
with:
64+
app-id: ${{ secrets.PARITY_APP_ID }}
65+
private-key: ${{ secrets.PARITY_APP_PRIVATE_KEY }}
66+
owner: browserbase
67+
repositories: stagehand
68+
69+
- name: Create issue in Python SDK repository
70+
uses: actions/github-script@v7
71+
with:
72+
github-token: ${{ steps.generate-token.outputs.token }}
73+
script: |
74+
const { data: pullRequest } = await github.rest.pulls.get({
75+
owner: context.repo.owner,
76+
repo: context.repo.repo,
77+
pull_number: context.issue.number,
78+
});
79+
80+
// Get PR comments for additional context
81+
const { data: comments } = await github.rest.issues.listComments({
82+
owner: context.repo.owner,
83+
repo: context.repo.repo,
84+
issue_number: context.issue.number,
85+
});
86+
87+
// Format comments for the issue description
88+
let commentsSection = '';
89+
if (comments.length > 0) {
90+
commentsSection = '\n\n## Recent Comments\n\n';
91+
comments.slice(-3).forEach(comment => {
92+
commentsSection += `**@${comment.user.login}** commented:\n`;
93+
commentsSection += `${comment.body.substring(0, 500)}${comment.body.length > 500 ? '...' : ''}\n\n`;
94+
});
95+
}
96+
97+
// Get list of changed files for context
98+
const { data: files } = await github.rest.pulls.listFiles({
99+
owner: context.repo.owner,
100+
repo: context.repo.repo,
101+
pull_number: context.issue.number,
102+
});
103+
104+
const changedFiles = files.map(file => `- \`${file.filename}\``).join('\n');
105+
106+
const issueTitle = `[Feature Parity] ${pullRequest.title}`;
107+
const issueBody = `## Feature Parity Request
108+
109+
This issue was automatically created from a pull request in the TypeScript Stagehand repository that was labeled with 'parity'.
110+
111+
### Original PR Details
112+
- **PR**: #${context.issue.number} - ${pullRequest.title}
113+
- **Author**: @${pullRequest.user.login}
114+
- **Link**: ${pullRequest.html_url}
115+
116+
### Description
117+
${pullRequest.body || 'No description provided.'}
118+
119+
### Changed Files
120+
${changedFiles}
121+
122+
${commentsSection}
123+
124+
### Action Required
125+
Please review the changes in the original PR and implement equivalent functionality in the Python SDK if applicable.
126+
127+
---
128+
*This issue was automatically generated by the Feature Parity workflow.*`;
129+
130+
// Create the issue in the Python repository
131+
const { data: issue } = await github.rest.issues.create({
132+
owner: 'browserbase',
133+
repo: 'stagehand-python',
134+
title: issueTitle,
135+
body: issueBody,
136+
labels: ['feature-parity']
137+
});
138+
139+
console.log(`Created issue: ${issue.html_url}`);
140+
141+
// Add a comment to the original PR confirming the issue was created
142+
await github.rest.issues.createComment({
143+
owner: context.repo.owner,
144+
repo: context.repo.repo,
145+
issue_number: context.issue.number,
146+
body: `🔄 **Feature Parity Issue Created**\n\nAn issue has been automatically created in the Python SDK repository to track parity implementation:\n${issue.html_url}`
147+
});

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<img alt="MIT License" src="media/light_license.svg" />
2121
</picture>
2222
</a>
23-
<a href="https://stagehand.dev/slack">
23+
<a href="https://join.slack.com/t/stagehand-dev/shared_invite/zt-38khc8iv5-T2acb50_0OILUaX7lxeBOg">
2424
<picture>
2525
<source media="(prefers-color-scheme: dark)" srcset="media/dark_slack.svg" />
2626
<img alt="Slack Community" src="media/light_slack.svg" />
@@ -32,6 +32,23 @@
3232
<a href="https://trendshift.io/repositories/12122" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12122" alt="browserbase%2Fstagehand | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
3333
</p>
3434

35+
<p align="center">
36+
If you're looking for the Python implementation, you can find it
37+
<a href="https://github.com/browserbase/stagehand-python"> here</a>
38+
</p>
39+
40+
<div align="center" style="display: flex; align-items: center; justify-content: center; gap: 4px; margin-bottom: 0;">
41+
<b>Vibe code</b>
42+
<span style="font-size: 1.05em;"> Stagehand with </span>
43+
<a href="https://director.ai" style="display: flex; align-items: center;">
44+
<span>Director</span>
45+
</a>
46+
<span> </span>
47+
<picture>
48+
<img alt="Director" src="media/director_icon.svg" width="25" />
49+
</picture>
50+
</div>
51+
3552
## Why Stagehand?
3653

3754
Most existing browser automation tools either require you to write low-level code in a framework like Selenium, Playwright, or Puppeteer, or use high-level agents that can be unpredictable in production. By letting developers choose what to write in code vs. natural language, Stagehand is the natural choice for browser automations in production.
@@ -120,9 +137,9 @@ nano .env # Edit the .env file to add API keys
120137
## Contributing
121138

122139
> [!NOTE]
123-
> We highly value contributions to Stagehand! For questions or support, please join our [Slack community](https://stagehand.dev/slack).
140+
> We highly value contributions to Stagehand! For questions or support, please join our [Slack community](https://join.slack.com/t/stagehand-dev/shared_invite/zt-38khc8iv5-T2acb50_0OILUaX7lxeBOg).
124141
125-
At a high level, we're focused on improving reliability, speed, and cost in that order of priority. If you're interested in contributing, we strongly recommend reaching out to [Anirudh Kamath](https://x.com/kamathematic) or [Paul Klein](https://x.com/pk_iv) in our [Slack community](https://stagehand.dev/slack) before starting to ensure that your contribution aligns with our goals.
142+
At a high level, we're focused on improving reliability, speed, and cost in that order of priority. If you're interested in contributing, we strongly recommend reaching out to [Miguel Gonzalez](https://x.com/miguel_gonzf) or [Paul Klein](https://x.com/pk_iv) in our [Slack community](https://join.slack.com/t/stagehand-dev/shared_invite/zt-38khc8iv5-T2acb50_0OILUaX7lxeBOg) before starting to ensure that your contribution aligns with our goals.
126143

127144
For more information, please see our [Contributing Guide](https://docs.stagehand.dev/examples/contributing).
128145

docs/examples/contributing.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Any contribution must be explicitly approved by a codeowner. Officially, Stageha
1616

1717
Special thanks to [Jeremy Press](https://github.com/jeremypress), [Navid Pour](https://github.com/navidkpr), and [all the contributors](https://github.com/browserbase/stagehand/graphs/contributors) for your help in making Stagehand the best browser automation framework.
1818

19-
***Please do not hesitate to reach out to anyone listed here in the [public Slack channel](https://stagehand.dev/slack)***
19+
***Please do not hesitate to reach out to anyone listed here in the [public Slack channel](https://join.slack.com/t/stagehand-dev/shared_invite/zt-38khc8iv5-T2acb50_0OILUaX7lxeBOg)***
2020

2121
## General Workflow
2222

evals/evals.config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,10 @@
378378
{
379379
"name": "multi_tab",
380380
"categories": ["act"]
381+
},
382+
{
383+
"name": "shadow_dom",
384+
"categories": ["act"]
381385
}
382386
]
383387
}

evals/taskConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const DEFAULT_EVAL_MODELS = process.env.EVAL_MODELS
102102

103103
const DEFAULT_AGENT_MODELS = process.env.EVAL_AGENT_MODELS
104104
? process.env.EVAL_AGENT_MODELS.split(",")
105-
: ["computer-use-preview", "claude-3-7-sonnet-20250219"];
105+
: ["computer-use-preview-2025-03-11", "claude-3-7-sonnet-latest"];
106106

107107
/**
108108
* getModelList:

evals/tasks/shadow_dom.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { EvalFunction } from "@/types/evals";
2+
3+
export const shadow_dom: EvalFunction = async ({
4+
debugUrl,
5+
sessionUrl,
6+
stagehand,
7+
logger,
8+
}) => {
9+
const page = stagehand.page;
10+
try {
11+
await page.goto(
12+
"https://browserbase.github.io/stagehand-eval-sites/sites/shadow-dom/",
13+
);
14+
const result = await page.act("click the button");
15+
16+
if (!result.success && result.message.includes("not-supported")) {
17+
return {
18+
_success: true,
19+
debugUrl,
20+
sessionUrl,
21+
logs: logger.getLogs(),
22+
};
23+
}
24+
return {
25+
_success: false,
26+
debugUrl,
27+
sessionUrl,
28+
logs: logger.getLogs(),
29+
};
30+
} catch (error) {
31+
return {
32+
_success: false,
33+
message: `error: ${error.message}`,
34+
debugUrl,
35+
sessionUrl,
36+
logs: logger.getLogs(),
37+
};
38+
} finally {
39+
await stagehand.close();
40+
}
41+
};

lib/StagehandPage.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -447,9 +447,9 @@ ${scriptContent} \
447447
* `_waitForSettledDom` waits until the DOM is settled, and therefore is
448448
* ready for actions to be taken.
449449
*
450-
* **Definition of settled**
450+
* **Definition of "settled"**
451451
* • No in-flight network requests (except WebSocket / Server-Sent-Events).
452-
* • That idle state lasts for at least **500 ms** (the quiet-window).
452+
* • That idle state lasts for at least **500 ms** (the "quiet-window").
453453
*
454454
* **How it works**
455455
* 1. Subscribes to CDP Network and Page events for the main target and all
@@ -488,6 +488,10 @@ ${scriptContent} \
488488
autoAttach: true,
489489
waitForDebuggerOnStart: false,
490490
flatten: true,
491+
filter: [
492+
{ type: "worker", exclude: true },
493+
{ type: "shared_worker", exclude: true },
494+
],
491495
});
492496

493497
return new Promise<void>((resolve) => {
@@ -967,7 +971,7 @@ ${scriptContent} \
967971
if (msg.includes("does not have a separate CDP session")) {
968972
// Re-use / create the top-level session instead
969973
const rootSession = await this.getCDPClient(this.page);
970-
// cache the alias so we dont try again for this frame
974+
// cache the alias so we don't try again for this frame
971975
this.cdpClients.set(target, rootSession);
972976
return rootSession;
973977
}

lib/handlers/observeHandler.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -168,25 +168,39 @@ export class StagehandObserveHandler {
168168
},
169169
});
170170

171-
const lookUpIndex = elementId as EncodedId;
172-
const xpath = combinedXpathMap[lookUpIndex];
173-
174-
const trimmedXpath = trimTrailingTextNode(xpath);
175-
176-
if (!trimmedXpath || trimmedXpath === "") {
171+
if (elementId.includes("-")) {
172+
const lookUpIndex = elementId as EncodedId;
173+
const xpath = combinedXpathMap[lookUpIndex];
174+
175+
const trimmedXpath = trimTrailingTextNode(xpath);
176+
177+
if (!trimmedXpath || trimmedXpath === "") {
178+
this.logger({
179+
category: "observation",
180+
message: `Empty xpath returned for element: ${elementId}`,
181+
level: 1,
182+
});
183+
}
184+
185+
return {
186+
...rest,
187+
selector: `xpath=${trimmedXpath}`,
188+
// Provisioning or future use if we want to use direct CDP
189+
// backendNodeId: elementId,
190+
};
191+
} else {
177192
this.logger({
178193
category: "observation",
179-
message: `Empty xpath returned for element: ${elementId}`,
180-
level: 1,
194+
message: `Element is inside a shadow DOM: ${elementId}`,
195+
level: 0,
181196
});
197+
return {
198+
description: "an element inside a shadow DOM",
199+
method: "not-supported",
200+
arguments: [] as string[],
201+
selector: "not-supported",
202+
};
182203
}
183-
184-
return {
185-
...rest,
186-
selector: `xpath=${trimmedXpath}`,
187-
// Provisioning or future use if we want to use direct CDP
188-
// backendNodeId: elementId,
189-
};
190204
}),
191205
);
192206

0 commit comments

Comments
 (0)