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"})
+44 -5
View File
@@ -34,21 +34,60 @@ def test_session_export_cli_prints_json():
assert result.exit_code == 0
assert '"name": "work"' in result.output
def test_nav_open_reuse_navigates_existing_tab_instead_of_opening_new():
calls = []
def _reuse_sender(calls, tab_url="https://example.com"):
tab = {"id": 7, "windowId": 1, "active": False, "muted": False, "title": "Example", "url": tab_url}
def sender(command, args=None, **kwargs):
calls.append((command, args))
if command == "tabs.list":
return [{"id": 7, "windowId": 1, "active": False, "muted": False, "title": "Example", "url": "https://example.com"}]
return [tab]
if command in {"tabs.status", "navigate.open"}:
return dict(tab)
return {}
return sender
BrowserCLI(browser="testing", _command_sender=sender).nav.open("https://example.com", reuse=True)
def test_nav_open_reuse_navigates_existing_tab_instead_of_opening_new():
calls = []
BrowserCLI(browser="testing", _command_sender=_reuse_sender(calls, "https://example.com/app?a=1")).nav.open(
"https://example.com/app?a=2", reuse=True
)
assert calls == [
("tabs.list", {}),
("navigate.to", {"tabId": 7, "url": "https://example.com"}),
("navigate.to", {"tabId": 7, "url": "https://example.com/app?a=2"}),
]
def test_nav_open_reuse_matches_nearly_identical_urls():
calls = []
BrowserCLI(browser="testing", _command_sender=_reuse_sender(calls, "https://example.com/app?a=1&b=2")).nav.open(
"https://www.example.com/app/?b=2&a=1#top", reuse=True
)
assert calls == [("tabs.list", {})] # same page already open → no reload, no new tab
def test_nav_open_reuse_exact_opens_a_new_tab_for_a_different_url():
calls = []
BrowserCLI(browser="testing", _command_sender=_reuse_sender(calls)).nav.open(
"https://example.com/", reuse="exact"
)
assert [command for command, _ in calls] == ["tabs.list", "navigate.open"]
def test_tabs_open_reuse_returns_the_existing_tab():
calls = []
tab = BrowserCLI(browser="testing", _command_sender=_reuse_sender(calls)).tabs.open(
"https://example.com/deep/page", reuse="origin"
)
assert tab.id == 7
assert [command for command, _ in calls] == ["tabs.list", "navigate.to", "tabs.status"]
def test_tabs_open_without_reuse_always_creates_a_tab():
calls = []
BrowserCLI(browser="testing", _command_sender=_reuse_sender(calls)).tabs.open("https://example.com")
assert [command for command, _ in calls] == ["navigate.open"]
def _tree_sender(tabs, groups):
def sender(command, args=None, **kwargs):
if command == "tabs.list":
+84
View File
@@ -0,0 +1,84 @@
"""Matching a requested URL against already-open tabs."""
from __future__ import annotations
import pytest
from browser_cli.url_match import (
find_reusable_tab,
needs_navigation,
normalize_mode,
url_key,
urls_match,
)
def test_canonical_key_ignores_cosmetic_differences():
assert url_key("https://Example.com:443/app/?b=2&a=1#top") == url_key("https://www.example.com/app?a=1&b=2")
def test_canonical_key_keeps_meaningful_differences():
assert url_key("https://example.com/app") != url_key("http://example.com/app")
assert url_key("https://example.com/app") != url_key("https://example.com:8080/app")
assert url_key("https://example.com/a") != url_key("https://example.com/b")
assert url_key("https://example.com/a?x=1") != url_key("https://example.com/a?x=2")
def test_non_urls_have_no_key():
assert url_key(None) is None
assert url_key("") is None
assert url_key("not a url") is None
@pytest.mark.parametrize("reuse,reuse_domain,expected", [
(False, False, "none"),
(None, False, "none"),
(True, False, "similar"),
("exact", False, "exact"),
("ORIGIN", False, "origin"),
(False, True, "origin"),
(True, True, "origin"),
])
def test_flags_fold_into_modes(reuse, reuse_domain, expected):
assert normalize_mode(reuse, reuse_domain) == expected
def test_unknown_mode_is_rejected():
with pytest.raises(ValueError, match="Unknown reuse mode"):
normalize_mode("fuzzy")
def test_exact_mode_only_matches_identical_strings():
assert urls_match("https://example.com/app", "https://example.com/app", "exact")
assert not urls_match("https://example.com/app", "https://example.com/app/", "exact")
def test_similar_mode_matches_the_same_page():
assert urls_match("https://example.com/app/", "https://example.com/app#section", "similar")
assert not urls_match("https://example.com/app", "https://example.com/other", "similar")
assert not urls_match("https://example.com/app", "https://other.example/app", "similar")
def test_similar_mode_treats_a_site_root_request_as_the_site():
assert urls_match("https://example.com/", "https://example.com/web/streams", "similar")
assert not urls_match("https://example.com/web", "https://example.com/web/streams", "similar")
def test_origin_mode_matches_any_page_of_the_site():
assert urls_match("https://example.com/web", "https://example.com/other", "origin")
assert not urls_match("https://example.com/web", "http://example.com/web", "origin")
def test_none_mode_never_matches():
assert not urls_match("https://example.com", "https://example.com", "none")
assert find_reusable_tab([{"id": 1, "url": "https://example.com"}], "https://example.com", "none") is None
def test_closest_tab_wins():
tabs = [
{"id": 1, "url": "https://example.com/web/streams/detail"},
{"id": 2, "url": "https://example.com/web"},
{"id": 3, "url": "https://example.com/web/?tab=1#x"},
]
assert find_reusable_tab(tabs, "https://example.com/web", "similar")["id"] == 2
assert find_reusable_tab(tabs, "https://example.com/", "similar")["id"] == 2
def test_matching_works_on_sdk_tab_objects():
from browser_cli.models import Tab
tabs = [Tab(id=4, window_id=1, active=False, url="https://example.com/app?a=1")]
assert find_reusable_tab(tabs, "https://example.com/app/#top?", "none") is None
assert find_reusable_tab(tabs, "https://example.com/app?a=1", "similar").id == 4
def test_navigation_is_skipped_only_for_the_same_page():
assert not needs_navigation("https://example.com/app/", "https://example.com/app#top")
assert needs_navigation("https://example.com/app", "https://example.com/app/next")
assert needs_navigation(None, "https://example.com/app")