@@ -14,6 +14,8 @@ def __init__(self, context: BrowserContext, stagehand):
14
14
# Use a weak key dictionary to map Playwright Pages to our StagehandPage wrappers
15
15
self .page_map = weakref .WeakKeyDictionary ()
16
16
self .active_stagehand_page = None
17
+ # Map frame IDs to StagehandPage instances
18
+ self .frame_id_map = {}
17
19
18
20
async def new_page (self ) -> StagehandPage :
19
21
pw_page : Page = await self ._context .new_page ()
@@ -23,9 +25,13 @@ async def new_page(self) -> StagehandPage:
23
25
24
26
async def create_stagehand_page (self , pw_page : Page ) -> StagehandPage :
25
27
# Create a StagehandPage wrapper for the given Playwright page
26
- stagehand_page = StagehandPage (pw_page , self .stagehand )
28
+ stagehand_page = StagehandPage (pw_page , self .stagehand , self )
27
29
await self .inject_custom_scripts (pw_page )
28
30
self .page_map [pw_page ] = stagehand_page
31
+
32
+ # Initialize frame tracking for this page
33
+ await self ._attach_frame_navigated_listener (pw_page , stagehand_page )
34
+
29
35
return stagehand_page
30
36
31
37
async def inject_custom_scripts (self , pw_page : Page ):
@@ -69,6 +75,25 @@ def set_active_page(self, stagehand_page: StagehandPage):
69
75
def get_active_page (self ) -> StagehandPage :
70
76
return self .active_stagehand_page
71
77
78
+ def register_frame_id (self , frame_id : str , page : StagehandPage ):
79
+ """Register a frame ID to StagehandPage mapping."""
80
+ self .frame_id_map [frame_id ] = page
81
+ self .stagehand .logger .debug (
82
+ f"Registered frame ID { frame_id } to page" , category = "context"
83
+ )
84
+
85
+ def unregister_frame_id (self , frame_id : str ):
86
+ """Unregister a frame ID from the mapping."""
87
+ if frame_id in self .frame_id_map :
88
+ del self .frame_id_map [frame_id ]
89
+ self .stagehand .logger .debug (
90
+ f"Unregistered frame ID { frame_id } " , category = "context"
91
+ )
92
+
93
+ def get_stagehand_page_by_frame_id (self , frame_id : str ) -> StagehandPage :
94
+ """Get StagehandPage by frame ID."""
95
+ return self .frame_id_map .get (frame_id )
96
+
72
97
@classmethod
73
98
async def init (cls , context : BrowserContext , stagehand ):
74
99
stagehand .logger .debug ("StagehandContext.init() called" , category = "context" )
@@ -150,3 +175,65 @@ async def wrapped_pages():
150
175
151
176
return wrapped_pages
152
177
return attr
178
+
179
+ async def _attach_frame_navigated_listener (self , pw_page : Page , stagehand_page : StagehandPage ):
180
+ """
181
+ Attach CDP listener for frame navigation events to track frame IDs.
182
+ This mirrors the TypeScript implementation's frame tracking.
183
+ """
184
+ try :
185
+ # Create CDP session for the page
186
+ cdp_session = await self ._context .new_cdp_session (pw_page )
187
+ await cdp_session .send ("Page.enable" )
188
+
189
+ # Get the current root frame ID
190
+ frame_tree = await cdp_session .send ("Page.getFrameTree" )
191
+ root_frame_id = frame_tree .get ("frameTree" , {}).get ("frame" , {}).get ("id" )
192
+
193
+ if root_frame_id :
194
+ # Initialize the page with its frame ID
195
+ stagehand_page .update_root_frame_id (root_frame_id )
196
+ self .register_frame_id (root_frame_id , stagehand_page )
197
+
198
+ # Set up event listener for frame navigation
199
+ def on_frame_navigated (params ):
200
+ """Handle Page.frameNavigated events"""
201
+ frame = params .get ("frame" , {})
202
+ frame_id = frame .get ("id" )
203
+ parent_id = frame .get ("parentId" )
204
+
205
+ # Only track root frames (no parent)
206
+ if not parent_id and frame_id :
207
+ # Skip if it's the same frame ID
208
+ if frame_id == stagehand_page .frame_id :
209
+ return
210
+
211
+ # Unregister old frame ID if exists
212
+ old_id = stagehand_page .frame_id
213
+ if old_id :
214
+ self .unregister_frame_id (old_id )
215
+
216
+ # Register new frame ID
217
+ self .register_frame_id (frame_id , stagehand_page )
218
+ stagehand_page .update_root_frame_id (frame_id )
219
+
220
+ self .stagehand .logger .debug (
221
+ f"Frame navigated from { old_id } to { frame_id } " ,
222
+ category = "context"
223
+ )
224
+
225
+ # Register the event listener
226
+ cdp_session .on ("Page.frameNavigated" , on_frame_navigated )
227
+
228
+ # Clean up frame ID when page closes
229
+ def on_page_close ():
230
+ if stagehand_page .frame_id :
231
+ self .unregister_frame_id (stagehand_page .frame_id )
232
+
233
+ pw_page .once ("close" , on_page_close )
234
+
235
+ except Exception as e :
236
+ self .stagehand .logger .error (
237
+ f"Failed to attach frame navigation listener: { str (e )} " ,
238
+ category = "context"
239
+ )
0 commit comments