Skip to content

Commit c20adb9

Browse files
[api] update root frame ID on init (#920)
# why - tab handling for `act`, `extract` and `observe` was failing because we sometimes sent `frameId: undefined` after a new page had been opened - the `rootFrameId` only became available after `Page.frameNavigated` fired, so the time between the event firing and a stagehand API call caused undefined `frameId`s # what changed - Added `getCurrentRootFrameId` helper which calls `Page.getFrameTree` to fetch the current root frame ID on page creation - `attachFrameNavigatedListener` is now called right after a page is wrapped with `createStagehandPage` - `handleNewPlaywrightPage` now runs before attaching the listener # test plan - tested in local dev - run all evals
1 parent b1b83a1 commit c20adb9

File tree

3 files changed

+47
-30
lines changed

3 files changed

+47
-30
lines changed

.changeset/clean-olives-wash.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+
fix: tab handling on API

lib/StagehandContext.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class StagehandContext {
2727
return async (): Promise<Page> => {
2828
const pwPage = await target.newPage();
2929
const stagehandPage = await this.createStagehandPage(pwPage);
30+
await this.attachFrameNavigatedListener(pwPage);
3031
// Set as active page when created
3132
this.setActivePage(stagehandPage);
3233
return stagehandPage.page;
@@ -81,19 +82,8 @@ export class StagehandContext {
8182
stagehand: Stagehand,
8283
): Promise<StagehandContext> {
8384
const instance = new StagehandContext(context, stagehand);
84-
85-
// Initialize existing pages
86-
const existingPages = context.pages();
87-
for (const page of existingPages) {
88-
const stagehandPage = await instance.createStagehandPage(page);
89-
await instance.attachFrameNavigatedListener(page);
90-
// Set the first page as active
91-
if (!instance.activeStagehandPage) {
92-
instance.setActivePage(stagehandPage);
93-
}
94-
}
95-
96-
context.on("page", (pwPage) => {
85+
context.on("page", async (pwPage) => {
86+
await instance.handleNewPlaywrightPage(pwPage);
9787
instance
9888
.attachFrameNavigatedListener(pwPage)
9989
.catch((err) =>
@@ -114,6 +104,17 @@ export class StagehandContext {
114104
);
115105
});
116106

107+
// Initialize existing pages
108+
const existingPages = context.pages();
109+
for (const page of existingPages) {
110+
const stagehandPage = await instance.createStagehandPage(page);
111+
await instance.attachFrameNavigatedListener(page);
112+
// Set the first page as active
113+
if (!instance.activeStagehandPage) {
114+
instance.setActivePage(stagehandPage);
115+
}
116+
}
117+
117118
return instance;
118119
}
119120
public get frameIdLookup(): ReadonlyMap<string, StagehandPage> {
@@ -180,22 +181,19 @@ export class StagehandContext {
180181
await session.send("Page.enable");
181182

182183
pwPage.once("close", () => {
183-
this.unregisterFrameId(shPage.frameId);
184+
if (shPage.frameId) this.unregisterFrameId(shPage.frameId);
184185
});
185186

186187
session.on(
187188
"Page.frameNavigated",
188189
(evt: Protocol.Page.FrameNavigatedEvent): void => {
189-
const { frame } = evt;
190-
191-
if (!frame.parentId) {
192-
const oldId = shPage.frameId;
193-
if (frame.id !== oldId) {
194-
if (oldId) this.unregisterFrameId(oldId);
195-
this.registerFrameId(frame.id, shPage);
196-
shPage.updateRootFrameId(frame.id);
197-
}
198-
}
190+
if (evt.frame.parentId) return;
191+
if (evt.frame.id === shPage.frameId) return;
192+
193+
const oldId = shPage.frameId;
194+
if (oldId) this.unregisterFrameId(oldId);
195+
this.registerFrameId(evt.frame.id, shPage);
196+
shPage.updateRootFrameId(evt.frame.id);
199197
},
200198
);
201199
}

lib/StagehandPage.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ import { StagehandAPIError } from "@/types/stagehandApiErrors";
3333
import { scriptContent } from "@/lib/dom/build/scriptContent";
3434
import type { Protocol } from "devtools-protocol";
3535

36+
async function getCurrentRootFrameId(session: CDPSession): Promise<string> {
37+
const { frameTree } = (await session.send(
38+
"Page.getFrameTree",
39+
)) as Protocol.Page.GetFrameTreeResponse;
40+
return frameTree.frame.id;
41+
}
42+
3643
export class StagehandPage {
3744
private stagehand: Stagehand;
3845
private rawPage: PlaywrightPage;
@@ -366,7 +373,7 @@ ${scriptContent} \
366373
const result = this.api
367374
? await this.api.goto(url, {
368375
...options,
369-
frameId: this.frameId,
376+
frameId: this.rootFrameId,
370377
})
371378
: await rawGoto(url, options);
372379

@@ -435,6 +442,13 @@ ${scriptContent} \
435442
},
436443
};
437444

445+
const session = await this.getCDPClient(this.rawPage);
446+
await session.send("Page.enable");
447+
448+
const rootId = await getCurrentRootFrameId(session);
449+
this.updateRootFrameId(rootId);
450+
this.intContext.registerFrameId(rootId, this);
451+
438452
this.intPage = new Proxy(page, handler) as unknown as Page;
439453
this.initialized = true;
440454
return this;
@@ -639,7 +653,7 @@ ${scriptContent} \
639653
if (this.api) {
640654
const result = await this.api.act({
641655
...observeResult,
642-
frameId: this.frameId,
656+
frameId: this.rootFrameId,
643657
});
644658
await this._refreshPageFromAPI();
645659
this.stagehand.addToHistory("act", observeResult, result);
@@ -672,7 +686,7 @@ ${scriptContent} \
672686
const { action, modelName, modelClientOptions } = actionOrOptions;
673687

674688
if (this.api) {
675-
const opts = { ...actionOrOptions, frameId: this.frameId };
689+
const opts = { ...actionOrOptions, frameId: this.rootFrameId };
676690
const result = await this.api.act(opts);
677691
await this._refreshPageFromAPI();
678692
this.stagehand.addToHistory("act", actionOrOptions, result);
@@ -734,7 +748,7 @@ ${scriptContent} \
734748
if (!instructionOrOptions) {
735749
let result: ExtractResult<T>;
736750
if (this.api) {
737-
result = await this.api.extract<T>({ frameId: this.frameId });
751+
result = await this.api.extract<T>({ frameId: this.rootFrameId });
738752
} else {
739753
result = await this.extractHandler.extract();
740754
}
@@ -767,7 +781,7 @@ ${scriptContent} \
767781
} = options;
768782

769783
if (this.api) {
770-
const opts = { ...options, frameId: this.frameId };
784+
const opts = { ...options, frameId: this.rootFrameId };
771785
const result = await this.api.extract<T>(opts);
772786
this.stagehand.addToHistory("extract", instructionOrOptions, result);
773787
return result;
@@ -871,7 +885,7 @@ ${scriptContent} \
871885
} = options;
872886

873887
if (this.api) {
874-
const opts = { ...options, frameId: this.frameId };
888+
const opts = { ...options, frameId: this.rootFrameId };
875889
const result = await this.api.observe(opts);
876890
this.stagehand.addToHistory("observe", instructionOrOptions, result);
877891
return result;

0 commit comments

Comments
 (0)