feat(tabs): reuse an open tab instead of duplicating a page
Testing / remote-protocol-compat (0.15.0) (push) Successful in 42s
Testing / remote-protocol-compat (0.16.0) (push) Successful in 47s
Testing / test (push) Successful in 50s

- Add browser_cli/url_match.py with a canonical URL key for tab matching
- Canonicalize scheme, host, default ports, www., trailing slash, query order
- Support reuse modes none, exact, similar and origin via normalize_mode()
- Pick the closest match when several tabs qualify, preferring identical URLs
- Skip the reload when the reused tab already shows the requested page
- Expose nav.reusable_tab() so callers can look up a tab without side effects
- Accept reuse/reuse_domain/reuse_title in tabs.open(), not just nav.open()
- Add --reuse-exact to nav open and nav open-wait; --reuse is now fuzzy
- Default MCP tabs_open to reuse="similar" and return a reused flag
- Tell MCP agents in the server instructions to reuse before opening
- Document the reuse modes in README for MCP, CLI and Python API
- Cover url_match, SDK reuse paths and MCP reuse behaviour with new tests
- Bump package and extension version to 0.16.9
- Refresh uv.lock for the updated dependency set
This commit is contained in:
2026-10-19 10:47:56 +02:00
parent 6c0f77e416
commit 7cb034dfc2
12 changed files with 600 additions and 203 deletions
+36 -1
View File
@@ -27,6 +27,7 @@ class FakeClient:
open=self.tabs_open,
close=self.tabs_close,
active=self.tabs_active,
activate=self.tabs_activate,
status=self.tabs_status,
screenshot=self.tabs_screenshot,
)
@@ -54,6 +55,9 @@ class FakeClient:
self.calls.append(("active",))
return SimpleNamespace(id=7)
def tabs_activate(self, tab_id):
self.calls.append(("activate", tab_id))
def tabs_status(self, tab_id):
self.calls.append(("status", tab_id))
return {"id": tab_id, "title": "Navigated", "url": "https://example.com/next"}
@@ -207,6 +211,7 @@ async def test_mutating_tools_use_sdk_and_return_fresh_state(client):
"url": "https://example.com",
"wait": True,
"focus": True,
"reuse": "none",
})
navigated = await client.call_tool("browser_navigate", {
"tab_id": 8,
@@ -216,7 +221,7 @@ async def test_mutating_tools_use_sdk_and_return_fresh_state(client):
typed = await client.call_tool("browser_dom_type", {"selector": "#name", "text": "Daniel"})
assert opened.structured_content == {
"id": 8, "title": "Opened", "url": "https://example.com"
"id": 8, "title": "Opened", "url": "https://example.com", "reused": False
}
assert navigated.structured_content["id"] == 8
assert clicked.structured_content["url"] == "https://example.com"
@@ -230,6 +235,36 @@ async def test_mutating_tools_use_sdk_and_return_fresh_state(client):
("navigate", 8, "https://example.com/next"), ("status", 8)
]
@pytest.mark.anyio
async def test_tabs_open_reuses_a_tab_that_already_shows_the_page(client):
opened = await client.call_tool("browser_tabs_open", {"url": "https://www.example.com/#top"})
assert opened.structured_content == {
"id": 7, "title": "Navigated", "url": "https://example.com/next", "reused": True
}
assert FakeClient.instances[0].calls == [("status", 7)] # already there → no navigation
@pytest.mark.anyio
async def test_tabs_open_reuses_a_site_tab_and_navigates_it(client):
opened = await client.call_tool(
"browser_tabs_open",
{"url": "https://browser-host.example/app?view=logs", "focus": True},
)
assert opened.structured_content["reused"] is True
assert FakeClient.instances[0].calls == [
("navigate", 9, "https://browser-host.example/app?view=logs"),
("activate", 9),
("status", 9),
]
@pytest.mark.anyio
async def test_tabs_open_does_not_reuse_an_unrelated_tab(client):
opened = await client.call_tool("browser_tabs_open", {"url": "https://other.example/docs"})
assert opened.structured_content["reused"] is False
assert FakeClient.instances[0].calls[0][0] == "open"
@pytest.mark.anyio
async def test_tab_tools_default_to_the_active_tab(client):
navigated = await client.call_tool("browser_navigate", {"url": "https://example.com/next"})