Skip to content

feat: don't close new opened tabs #161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions stagehand/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,18 @@ async def connect_local_browser(

# Apply stealth scripts
await apply_stealth_scripts(context, logger)

# Initialize StagehandContext
stagehand_context = await StagehandContext.init(context, stagehand_instance)

# Get the initial page (usually one is created by default)
if context.pages:
playwright_page = context.pages[0]
logger.debug("Using initial page from local context.")
page = await stagehand_context.get_stagehand_page(playwright_page)
else:
logger.debug("No initial page found, creating a new one.")
playwright_page = await context.new_page()

page = StagehandPage(playwright_page, stagehand_instance)
page = await stagehand_context.new_page()

return browser, context, stagehand_context, page, temp_user_data_dir

Expand Down
79 changes: 77 additions & 2 deletions stagehand/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ async def inject_custom_scripts(self, pw_page: Page):
async def get_stagehand_page(self, pw_page: Page) -> StagehandPage:
if pw_page not in self.page_map:
return await self.create_stagehand_page(pw_page)
return self.page_map[pw_page]
stagehand_page = self.page_map[pw_page]
# Update active page when getting a page
self.set_active_page(stagehand_page)
return stagehand_page

async def get_stagehand_pages(self) -> list:
# Return a list of StagehandPage wrappers for all pages in the context
Expand All @@ -53,24 +56,96 @@ async def get_stagehand_pages(self) -> list:

def set_active_page(self, stagehand_page: StagehandPage):
self.active_stagehand_page = stagehand_page
# Optionally update the active page in the stagehand client if needed
# Update the active page in the stagehand client
if hasattr(self.stagehand, "_set_active_page"):
self.stagehand._set_active_page(stagehand_page)
self.stagehand.logger.debug(
f"Set active page to: {stagehand_page.url}",
category="context"
)
else:
self.stagehand.logger.debug(
"Stagehand does not have _set_active_page method",
category="context"
)

def get_active_page(self) -> StagehandPage:
return self.active_stagehand_page

@classmethod
async def init(cls, context: BrowserContext, stagehand):
stagehand.logger.debug("StagehandContext.init() called", category="context")
instance = cls(context, stagehand)
# Pre-initialize StagehandPages for any existing pages
stagehand.logger.debug(f"Found {len(instance._context.pages)} existing pages", category="context")
for pw_page in instance._context.pages:
await instance.create_stagehand_page(pw_page)
if instance._context.pages:
first_page = instance._context.pages[0]
stagehand_page = await instance.get_stagehand_page(first_page)
instance.set_active_page(stagehand_page)

# Add event listener for new pages (popups, new tabs from window.open, etc.)
def handle_page_event(pw_page):
instance.stagehand.logger.debug(
f"Page event fired for URL: {pw_page.url}",
category="context"
)
instance._handle_new_page(pw_page)

instance.stagehand.logger.debug(
f"Setting up page event listener on context (ID: {id(context)})",
category="context"
)
context.on("page", handle_page_event)
instance.stagehand.logger.debug(
"Page event listener setup complete",
category="context"
)

return instance

def _handle_new_page(self, pw_page: Page):
"""
Handle new pages created by the browser (popups, window.open, etc.).
This runs synchronously in the event handler context.
"""
async def _async_handle():
try:
self.stagehand.logger.debug(
f"Creating StagehandPage for new page with URL: {pw_page.url}",
category="context"
)
stagehand_page = await self.create_stagehand_page(pw_page)
self.set_active_page(stagehand_page)
self.stagehand.logger.log(
"New page detected and initialized",
level=2,
category="context",
auxiliary={"url": {"value": pw_page.url, "type": "string"}}
)
except Exception as e:
self.stagehand.logger.error(
f"Failed to initialize new page: {str(e)}",
category="context"
)
import traceback
self.stagehand.logger.error(
f"Traceback: {traceback.format_exc()}",
category="context"
)

# Schedule the async work
import asyncio
try:
loop = asyncio.get_running_loop()
loop.create_task(_async_handle())
except RuntimeError:
# No event loop running, which shouldn't happen in normal operation
self.stagehand.logger.error(
"No event loop available to handle new page",
category="context"
)

def __getattr__(self, name):
# Forward attribute lookups to the underlying BrowserContext
Expand Down
4 changes: 0 additions & 4 deletions stagehand/handlers/act_handler_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,10 +471,6 @@ async def handle_possible_page_navigation(
category="action",
auxiliary={"url": {"value": new_opened_tab.url, "type": "string"}},
)
new_tab_url = new_opened_tab.url
await new_opened_tab.close()
await stagehand_page._page.goto(new_tab_url)
await stagehand_page._page.wait_for_load_state("domcontentloaded")

try:
await stagehand_page._wait_for_settled_dom(dom_settle_timeout_ms)
Expand Down
16 changes: 7 additions & 9 deletions stagehand/handlers/cua_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,21 +559,19 @@ async def handle_page_navigation(
pass # The action that might open a page has already run. We check if one was caught.
newly_opened_page = await new_page_info.value

new_page_url = newly_opened_page.url
await newly_opened_page.close()
await self.page.goto(new_page_url, timeout=dom_settle_timeout_ms)
# After navigating, the DOM needs to settle on the new URL.
await self._wait_for_settled_dom(timeout_ms=dom_settle_timeout_ms)
# Don't close the new tab - let it remain open and be handled by the context
# The StagehandContext will automatically make this the active page via its event listener
self.logger.debug(
f"New page detected with URL: {newly_opened_page.url}",
category=StagehandFunctionName.AGENT,
)

except asyncio.TimeoutError:
newly_opened_page = None
except Exception:
newly_opened_page = None

# If no new tab was opened and handled by navigating, or if we are on the original page after handling a new tab,
# then proceed to wait for DOM settlement on the current page.
if not newly_opened_page:
await self._wait_for_settled_dom(timeout_ms=dom_settle_timeout_ms)
await self._wait_for_settled_dom(timeout_ms=dom_settle_timeout_ms)

final_url = self.page.url
if final_url != initial_url:
Expand Down
117 changes: 112 additions & 5 deletions stagehand/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,81 @@
load_dotenv()


class LivePageProxy:
"""
A proxy object that dynamically delegates all operations to the current active page.
This mimics the behavior of the JavaScript Proxy in the original implementation.
"""

def __init__(self, stagehand_instance):
# Use object.__setattr__ to avoid infinite recursion
object.__setattr__(self, '_stagehand', stagehand_instance)

def __getattr__(self, name):
"""Delegate all attribute access to the current active page."""
stagehand = object.__getattribute__(self, '_stagehand')

# Get the current active page
if hasattr(stagehand, '_active_page') and stagehand._active_page:
active_page = stagehand._active_page
elif hasattr(stagehand, '_original_page') and stagehand._original_page:
active_page = stagehand._original_page
else:
raise RuntimeError("No active page available")

# Get the attribute from the active page
attr = getattr(active_page, name)

# If it's a method, bind it to the active page
if callable(attr):
return attr

return attr

def __setattr__(self, name, value):
"""Delegate all attribute setting to the current active page."""
if name.startswith('_'):
# Internal attributes are set on the proxy itself
object.__setattr__(self, name, value)
else:
stagehand = object.__getattribute__(self, '_stagehand')

# Get the current active page
if hasattr(stagehand, '_active_page') and stagehand._active_page:
active_page = stagehand._active_page
elif hasattr(stagehand, '_original_page') and stagehand._original_page:
active_page = stagehand._original_page
else:
raise RuntimeError("No active page available")

# Set the attribute on the active page
setattr(active_page, name, value)

def __dir__(self):
"""Return attributes of the current active page."""
stagehand = object.__getattribute__(self, '_stagehand')

if hasattr(stagehand, '_active_page') and stagehand._active_page:
active_page = stagehand._active_page
elif hasattr(stagehand, '_original_page') and stagehand._original_page:
active_page = stagehand._original_page
else:
return []

return dir(active_page)

def __repr__(self):
"""Return representation of the current active page."""
stagehand = object.__getattribute__(self, '_stagehand')

if hasattr(stagehand, '_active_page') and stagehand._active_page:
return f"<LivePageProxy -> {repr(stagehand._active_page)}>"
elif hasattr(stagehand, '_original_page') and stagehand._original_page:
return f"<LivePageProxy -> {repr(stagehand._original_page)}>"
else:
return "<LivePageProxy -> No active page>"


class Stagehand:
"""
Main Stagehand class.
Expand Down Expand Up @@ -166,7 +241,8 @@ def __init__(
self._browser = None
self._context: Optional[BrowserContext] = None
self._playwright_page: Optional[PlaywrightPage] = None
self.page: Optional[StagehandPage] = None
self._original_page: Optional[StagehandPage] = None
self._active_page: Optional[StagehandPage] = None
self.context: Optional[StagehandContext] = None
self.use_api = self.config.use_api
self.experimental = self.config.experimental
Expand All @@ -181,6 +257,7 @@ def __init__(

self._initialized = False # Flag to track if init() has run
self._closed = False # Flag to track if resources have been closed
self._live_page_proxy = None # Live page proxy

# Setup LLM client if LOCAL mode
self.llm = None
Expand Down Expand Up @@ -415,15 +492,16 @@ async def init(self):
self._browser,
self._context,
self.context,
self.page,
self._original_page,
) = await connect_browserbase_browser(
self._playwright,
self.session_id,
self.browserbase_api_key,
self,
self.logger,
)
self._playwright_page = self.page._page
self._playwright_page = self._original_page._page
self._active_page = self._original_page
except Exception:
await self.close()
raise
Expand All @@ -435,15 +513,16 @@ async def init(self):
self._browser,
self._context,
self.context,
self.page,
self._original_page,
self._local_user_data_dir_temp,
) = await connect_local_browser(
self._playwright,
self.local_browser_launch_options,
self,
self.logger,
)
self._playwright_page = self.page._page
self._playwright_page = self._original_page._page
self._active_page = self._original_page
except Exception:
await self.close()
raise
Expand Down Expand Up @@ -623,6 +702,34 @@ def _handle_llm_metrics(

self.update_metrics_from_response(function_enum, response, inference_time_ms)

def _set_active_page(self, stagehand_page: StagehandPage):
"""
Internal method called by StagehandContext to update the active page.

Args:
stagehand_page: The StagehandPage to set as active
"""
self._active_page = stagehand_page


@property
def page(self) -> Optional[StagehandPage]:
"""
Get the current active page. This property returns a live proxy that
always points to the currently focused page when multiple tabs are open.

Returns:
A LivePageProxy that delegates to the active StagehandPage or None if not initialized
"""
if not self._initialized:
return None

# Create the live page proxy if it doesn't exist
if not self._live_page_proxy:
self._live_page_proxy = LivePageProxy(self)

return self._live_page_proxy


# Bind the imported API methods to the Stagehand class
Stagehand._create_session = _create_session
Expand Down