feat(tabs): reuse an open tab instead of duplicating a page
- 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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user