From 923e773c140d773853447c94e0ee772bb9a511ad Mon Sep 17 00:00:00 2001 From: Richard Tang Date: Mon, 20 Apr 2026 10:21:32 -0700 Subject: [PATCH] feat: improve the tab switching tool --- scripts/browser_remote_ui.html | 2 +- tools/src/gcu/browser/__init__.py | 2 +- tools/src/gcu/browser/tools/tabs.py | 43 ++++++++++++++----- .../tests/test_browser_tools_comprehensive.py | 8 ++-- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/scripts/browser_remote_ui.html b/scripts/browser_remote_ui.html index 60a99b7c..b0333976 100644 --- a/scripts/browser_remote_ui.html +++ b/scripts/browser_remote_ui.html @@ -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'], diff --git a/tools/src/gcu/browser/__init__.py b/tools/src/gcu/browser/__init__.py index 74adc3c4..d95ddff8 100644 --- a/tools/src/gcu/browser/__init__.py +++ b/tools/src/gcu/browser/__init__.py @@ -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, diff --git a/tools/src/gcu/browser/tools/tabs.py b/tools/src/gcu/browser/tools/tabs.py index f64065fc..97b6dd0c 100644 --- a/tools/src/gcu/browser/tools/tabs.py +++ b/tools/src/gcu/browser/tools/tabs.py @@ -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() diff --git a/tools/tests/test_browser_tools_comprehensive.py b/tools/tests/test_browser_tools_comprehensive.py index b82d9dc8..c5f45edd 100644 --- a/tools/tests/test_browser_tools_comprehensive.py +++ b/tools/tests/test_browser_tools_comprehensive.py @@ -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)