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
+29 -7
View File
@@ -12,12 +12,12 @@ import json
import os
from collections.abc import Callable
from typing import Any, Literal
from urllib.parse import urlsplit
from browser_cli import BrowserCLI
from browser_cli.mcp.naming import resolve_tool_prefix, tool_name
from browser_cli.mcp.serialization import structured
from browser_cli.mcp.targets import resolve_tab_id
from browser_cli.url_match import find_reusable_tab, needs_navigation, tab_id as read_tab_id, tab_url
ClientFactory = Callable[..., BrowserCLI]
@@ -25,6 +25,11 @@ _SERVER_INSTRUCTIONS = """Control a real, user-visible browser through browser-c
The server is stateless: pass browser, remote, and key on each tool call when a
specific target is required. Tool calls affect the user's actual browser. Read
current tabs/page state instead of assuming IDs or content from an earlier call.
Reuse before opening: tabs_open takes over an already-open tab whose URL is
nearly the same (same site and page, or any tab of that site when you ask for
its root) and reports reused=true with that tab's ID. Pass reuse="none" only
when a separate tab is really wanted, and reuse="origin" to take over any tab
of the site. Use tabs_list(search=...) first when you want to pick the tab.
"""
def _client(factory: ClientFactory, browser: str | None, remote: str | None, key: str | None) -> BrowserCLI:
@@ -74,7 +79,10 @@ def create_server(*, client_factory: ClientFactory = BrowserCLI, tool_prefix: st
remote: str | None = None,
key: str | None = None,
) -> list[dict[str, Any]]:
"""List current tabs, optionally filtering by URL or title substring."""
"""List current tabs, optionally filtering by URL or title substring.
Check this before opening anything so an already-open page gets reused.
"""
tabs = structured(_client(client_factory, browser, remote, key).tabs.list())
if not search:
return tabs
@@ -92,15 +100,29 @@ def create_server(*, client_factory: ClientFactory = BrowserCLI, tool_prefix: st
timeout: float = 30.0,
background: bool = False,
focus: bool = False,
reuse: Literal["similar", "exact", "origin", "none"] = "similar",
browser: str | None = None,
remote: str | None = None,
key: str | None = None,
) -> dict[str, Any]:
"""Open a URL in a new real-browser tab and return its current metadata."""
tab = _client(client_factory, browser, remote, key).tabs.open(
url, wait=wait, timeout=timeout, background=background, focus=focus
)
return structured(tab)
"""Open a URL in the real browser, reusing an already-open tab with a nearly identical URL.
Returns the tab metadata plus ``reused``. ``reuse`` selects the match:
``similar`` (default: same site and page, or any tab of that site when the
requested URL is its root), ``exact``, ``origin`` (any tab of the site), or
``none`` to always create a new tab.
"""
client = _client(client_factory, browser, remote, key)
existing = find_reusable_tab(structured(client.tabs.list()), url, reuse)
if existing is not None:
target = read_tab_id(existing)
if needs_navigation(tab_url(existing), url):
client.nav.to(target, url)
if focus:
client.tabs.activate(target)
return {**structured(client.tabs.status(target)), "reused": True}
tab = client.tabs.open(url, wait=wait, timeout=timeout, background=background, focus=focus)
return {**structured(tab), "reused": False}
@mcp.tool(name=tool_name("tabs_close", prefix))
def tabs_close(