feat: improve the tab switching tool

This commit is contained in:
Richard Tang
2026-04-20 10:21:32 -07:00
parent c7cc031060
commit 923e773c14
4 changed files with 39 additions and 16 deletions
+1 -1
View File
@@ -458,7 +458,7 @@ let currentView = 'grid';
// Tool categories for sidebar grouping
const CATEGORIES = {
'Lifecycle': ['browser_setup', 'browser_start', 'browser_stop', 'browser_status'],
'Tabs': ['browser_tabs', 'browser_open', 'browser_close', 'browser_close_all', 'browser_close_finished', 'browser_focus'],
'Tabs': ['browser_tabs', 'browser_open', 'browser_close', 'browser_close_all', 'browser_close_finished', 'browser_activate_tab'],
'Navigation': ['browser_navigate', 'browser_go_back', 'browser_go_forward', 'browser_reload'],
'Interactions': ['browser_click', 'browser_click_coordinate', 'browser_type', 'browser_type_focused', 'browser_fill', 'browser_press', 'browser_press_at', 'browser_hover', 'browser_hover_coordinate', 'browser_select', 'browser_scroll', 'browser_drag'],
'Inspection': ['browser_screenshot', 'browser_snapshot', 'browser_console', 'browser_html', 'browser_get_text', 'browser_get_attribute', 'browser_get_rect', 'browser_shadow_query', 'browser_evaluate', 'browser_wait'],
+1 -1
View File
@@ -42,7 +42,7 @@ def register_tools(mcp: FastMCP) -> None:
Tools are organized into categories:
- Lifecycle: browser_start, browser_stop, browser_status
- Tabs: browser_tabs, browser_open, browser_close, browser_focus
- Tabs: browser_tabs, browser_open, browser_close, browser_activate_tab
- Navigation: browser_navigate, browser_go_back, browser_go_forward, browser_reload
- Inspection: browser_screenshot, browser_snapshot, browser_console
- Interactions: browser_click, browser_click_coordinate, browser_type, browser_type_focused,
+33 -10
View File
@@ -1,5 +1,5 @@
"""
Browser tab management tools - tabs, open, close, focus.
Browser tab management tools - tabs, open, close, activate.
All operations go through the Beeline extension - no Playwright required.
"""
@@ -8,9 +8,10 @@ from __future__ import annotations
import logging
import time
from typing import Any
from typing import Annotated, Any
from fastmcp import FastMCP
from pydantic import Field
from ..bridge import get_bridge
from ..session import _active_profile
@@ -232,16 +233,33 @@ def register_tab_tools(mcp: FastMCP) -> None:
return result
@mcp.tool()
async def browser_focus(tab_id: int, profile: str | None = None) -> dict:
async def browser_activate_tab(
tab_id: Annotated[
int,
Field(
description=(
"REQUIRED. Integer Chrome tab ID of the tab to switch to. "
"Must be a concrete integer (not null). "
"Call browser_tabs first to list available tabs and their IDs."
),
),
],
profile: str | None = None,
) -> dict:
"""
Focus a browser tab.
Switch the active browser tab to the given tab ID.
Use this to bring an existing tab to the foreground before interacting
with it. The ``tab_id`` argument is required and must be an integer
returned by ``browser_tabs``; passing null/None is not supported (use
``browser_tabs`` to discover a valid ID first).
Args:
tab_id: Chrome tab ID to focus
tab_id: Chrome tab ID to activate. Required integer.
profile: Browser profile name (default: "default")
Returns:
Dict with focus status
Dict with activation status
"""
start = time.perf_counter()
params = {"tab_id": tab_id, "profile": profile}
@@ -249,13 +267,13 @@ def register_tab_tools(mcp: FastMCP) -> None:
bridge = get_bridge()
if not bridge or not bridge.is_connected:
result = {"ok": False, "error": "Browser extension not connected"}
log_tool_call("browser_focus", params, result=result)
log_tool_call("browser_activate_tab", params, result=result)
return result
ctx = _get_context(profile)
if not ctx:
result = {"ok": False, "error": "Browser not started. Call browser_start first."}
log_tool_call("browser_focus", params, result=result)
log_tool_call("browser_activate_tab", params, result=result)
return result
try:
@@ -263,7 +281,7 @@ def register_tab_tools(mcp: FastMCP) -> None:
ctx["activeTabId"] = tab_id
result = {"ok": True, "tabId": tab_id}
log_tool_call(
"browser_focus",
"browser_activate_tab",
params,
result=result,
duration_ms=(time.perf_counter() - start) * 1000,
@@ -271,7 +289,12 @@ def register_tab_tools(mcp: FastMCP) -> None:
return result
except Exception as e:
result = {"ok": False, "error": str(e)}
log_tool_call("browser_focus", params, error=e, duration_ms=(time.perf_counter() - start) * 1000)
log_tool_call(
"browser_activate_tab",
params,
error=e,
duration_ms=(time.perf_counter() - start) * 1000,
)
return result
@mcp.tool()
@@ -411,19 +411,19 @@ class TestTabLifecycle:
assert close_result.get("ok") is True
@pytest.mark.asyncio
async def test_tab_focus_switching(self, mcp: FastMCP, mock_bridge: MagicMock):
"""Test switching focus between tabs."""
async def test_tab_activate_switching(self, mcp: FastMCP, mock_bridge: MagicMock):
"""Test switching the active tab."""
mock_bridge.activate_tab = AsyncMock(return_value={"ok": True})
register_tab_tools(mcp)
browser_focus = mcp._tool_manager._tools["browser_focus"].fn
browser_activate_tab = mcp._tool_manager._tools["browser_activate_tab"].fn
with patch("gcu.browser.tools.tabs.get_bridge", return_value=mock_bridge):
with patch(
"gcu.browser.tools.tabs._get_context",
return_value={"groupId": 1, "activeTabId": 100},
):
result = await browser_focus(tab_id=200)
result = await browser_activate_tab(tab_id=200)
assert result.get("ok") is True
mock_bridge.activate_tab.assert_awaited_once_with(200)