feat: add multi-tab support to WebAgent#295
Merged
Merged
Conversation
Track all browser pages in PlaywrightAgentOs via a `_pages` list and a
`context.on("page")` listener that captures every new tab as it opens.
New tabs automatically become the active tab when `auto_follow_new_tab=True`
(the default); set it to `False` to keep the current tab active and switch
manually.
Three new tools let the LLM manage tabs autonomously:
- `list_tabs` — returns index, title, and URL for every open tab
- `switch_tab` — makes the tab at the given index active
- `close_tab` — closes the tab at the given index (guards the last tab)
`WebAgent` exposes `auto_follow_new_tab` as a constructor parameter and
includes the three new tools in its default tool set. The system prompt is
updated to reflect multi-tab capability.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs prevented auto-follow from working: 1. bring_to_front() deadlock: the _on_new_page callback runs in Playwright's background asyncio thread. Calling sync Playwright methods from that context deadlocks the greenlet dispatcher. Removed bring_to_front() from the handler; only the thread-safe self._page = page assignment is kept. 2. Race condition: the background-thread event fires after the main thread has already moved on to the next call (e.g. screenshot()), so self._page isn't updated in time. Added _sync_pages(), which is called at the start of every screenshot() from the main thread. It polls context.pages (safe from main thread), picks up any pages the event handler missed, and calls bring_to_front() safely there. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous fix removed bring_to_front() from the event handler but left a timing race: the "page" event only fires when the Playwright event loop is pumped, which happens during page.screenshot(). By that point the CDP screenshot command has already been sent to the OLD page, so auto-follow had no effect on what the agent saw. Fix: pump the event loop (wait_for_timeout(0)) at the start of screenshot() BEFORE _sync_pages() is called. This forces the dispatcher to process all queued events — including new-page events — so that by the time the screenshot is taken, self._page already points to the new tab. Also introduce a _needs_bring_to_front flag. _on_new_page sets it instead of calling bring_to_front() directly; _sync_pages() clears it and calls bring_to_front() from the main greenlet after the pump. This avoids silent error-deferral: any exception raised by a sync Playwright call inside an EventGreenlet is stored in connection._error and re-raised at the next API call, which could corrupt an unrelated operation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PlaywrightAgentOsnow tracks all open browser pages via a_pageslist and acontext.on("page", ...)listener that fires whenever a new tab is opened (e.g.target="_blank"links,window.open())auto_follow_new_tabparameter (defaultTrue) controls whether newly opened tabs automatically become active; set toFalseto switch manuallylist_tabs,switch_tab,close_tabWebAgentexposesauto_follow_new_tabas a constructor parameter and includes the three new tools in its default tool setChanged files
tools/playwright/agent_os.py_pageslist,_on_new_pagehandler,list_tabs/switch_tab/close_tabmethods,auto_follow_new_tabparamtools/playwright/tools.pyPlaywrightListTabsTool,PlaywrightSwitchTabTool,PlaywrightCloseTabToolweb_agent.pyauto_follow_new_tabparam, import + register new tools inget_default_tools()prompts/act_prompts.pyWEB_BROWSER_CAPABILITIESandWEB_AGENT_DEVICE_INFORMATIONto mention tab managementTest plan
pdm run test:unit— 647/647 ✅)WebAgentwithauto_follow_new_tab=True(default): click atarget="_blank"link → agent automatically follows the new tabWebAgentwithauto_follow_new_tab=False: click atarget="_blank"link → active tab stays; calllist_tabsto confirm new tab is tracked; callswitch_tab(1)to move to itclose_tabon the last remaining tab raisesRuntimeErrorclose_tabon the active tab switches focus to an adjacent tab🤖 Generated with Claude Code