@@ -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,9 +75,21 @@ 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
+
82
+ def unregister_frame_id (self , frame_id : str ):
83
+ """Unregister a frame ID from the mapping."""
84
+ if frame_id in self .frame_id_map :
85
+ del self .frame_id_map [frame_id ]
86
+
87
+ def get_stagehand_page_by_frame_id (self , frame_id : str ) -> StagehandPage :
88
+ """Get StagehandPage by frame ID."""
89
+ return self .frame_id_map .get (frame_id )
90
+
72
91
@classmethod
73
92
async def init (cls , context : BrowserContext , stagehand ):
74
- stagehand .logger .debug ("StagehandContext.init() called" , category = "context" )
75
93
instance = cls (context , stagehand )
76
94
# Pre-initialize StagehandPages for any existing pages
77
95
stagehand .logger .debug (
@@ -150,3 +168,67 @@ async def wrapped_pages():
150
168
151
169
return wrapped_pages
152
170
return attr
171
+
172
+ async def _attach_frame_navigated_listener (
173
+ self , pw_page : Page , stagehand_page : StagehandPage
174
+ ):
175
+ """
176
+ Attach CDP listener for frame navigation events to track frame IDs.
177
+ This mirrors the TypeScript implementation's frame tracking.
178
+ """
179
+ try :
180
+ # Create CDP session for the page
181
+ cdp_session = await self ._context .new_cdp_session (pw_page )
182
+ await cdp_session .send ("Page.enable" )
183
+
184
+ # Get the current root frame ID
185
+ frame_tree = await cdp_session .send ("Page.getFrameTree" )
186
+ root_frame_id = frame_tree .get ("frameTree" , {}).get ("frame" , {}).get ("id" )
187
+
188
+ if root_frame_id :
189
+ # Initialize the page with its frame ID
190
+ stagehand_page .update_root_frame_id (root_frame_id )
191
+ self .register_frame_id (root_frame_id , stagehand_page )
192
+
193
+ # Set up event listener for frame navigation
194
+ def on_frame_navigated (params ):
195
+ """Handle Page.frameNavigated events"""
196
+ frame = params .get ("frame" , {})
197
+ frame_id = frame .get ("id" )
198
+ parent_id = frame .get ("parentId" )
199
+
200
+ # Only track root frames (no parent)
201
+ if not parent_id and frame_id :
202
+ # Skip if it's the same frame ID
203
+ if frame_id == stagehand_page .frame_id :
204
+ return
205
+
206
+ # Unregister old frame ID if exists
207
+ old_id = stagehand_page .frame_id
208
+ if old_id :
209
+ self .unregister_frame_id (old_id )
210
+
211
+ # Register new frame ID
212
+ self .register_frame_id (frame_id , stagehand_page )
213
+ stagehand_page .update_root_frame_id (frame_id )
214
+
215
+ self .stagehand .logger .debug (
216
+ f"Frame navigated from { old_id } to { frame_id } " ,
217
+ category = "context" ,
218
+ )
219
+
220
+ # Register the event listener
221
+ cdp_session .on ("Page.frameNavigated" , on_frame_navigated )
222
+
223
+ # Clean up frame ID when page closes
224
+ def on_page_close ():
225
+ if stagehand_page .frame_id :
226
+ self .unregister_frame_id (stagehand_page .frame_id )
227
+
228
+ pw_page .once ("close" , on_page_close )
229
+
230
+ except Exception as e :
231
+ self .stagehand .logger .error (
232
+ f"Failed to attach frame navigation listener: { str (e )} " ,
233
+ category = "context" ,
234
+ )
0 commit comments