@@ -14,6 +14,8 @@ def __init__(self, context: BrowserContext, stagehand):
1414 # Use a weak key dictionary to map Playwright Pages to our StagehandPage wrappers
1515 self .page_map = weakref .WeakKeyDictionary ()
1616 self .active_stagehand_page = None
17+ # Map frame IDs to StagehandPage instances
18+ self .frame_id_map = {}
1719
1820 async def new_page (self ) -> StagehandPage :
1921 pw_page : Page = await self ._context .new_page ()
@@ -23,9 +25,13 @@ async def new_page(self) -> StagehandPage:
2325
2426 async def create_stagehand_page (self , pw_page : Page ) -> StagehandPage :
2527 # 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 )
2729 await self .inject_custom_scripts (pw_page )
2830 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+
2935 return stagehand_page
3036
3137 async def inject_custom_scripts (self , pw_page : Page ):
@@ -69,6 +75,25 @@ def set_active_page(self, stagehand_page: StagehandPage):
6975 def get_active_page (self ) -> StagehandPage :
7076 return self .active_stagehand_page
7177
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+
7297 @classmethod
7398 async def init (cls , context : BrowserContext , stagehand ):
7499 stagehand .logger .debug ("StagehandContext.init() called" , category = "context" )
@@ -150,3 +175,65 @@ async def wrapped_pages():
150175
151176 return wrapped_pages
152177 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