diff --git a/plugins/_browser/AGENTS.md b/plugins/_browser/AGENTS.md
index 6a434bbd5..075bdede5 100644
--- a/plugins/_browser/AGENTS.md
+++ b/plugins/_browser/AGENTS.md
@@ -22,6 +22,7 @@
- Paint live screencast frames through the Browser panel canvas/ImageBitmap path when available; keep the ``/data URL path for snapshots and fallback rendering.
- Push internal screencast frames from the runtime to the WebSocket consumer after subscription; keep `read/pop_screencast_frame` as fallback/tooling APIs, not the WebUI hot path.
- Keep Browser viewer frame transport capability-negotiated: updated clients may request binary/slim screencast frames, while older clients must keep the base64/full-metadata fallback. Do not let the WebUI advertise binary frames unless its Socket.IO client reconstructs attachments as real `Blob`, `ArrayBuffer`, or typed-array values.
+- Keep WebUI Browser tabs scoped to the active chat context by default; aggregate tabs from other AgentContext runtimes only when the Browser settings tab scope is `shared`.
- Keep narrow WebUI Browser controls usable by grouping navigation with Annotate/settings above a full-width address bar.
- Browser URL-intent handling must only claim web URL schemes and leave custom Agent Zero schemes to their owning surfaces.
- Prefer DOM/CDP browser actions with refs, selectors, frame-chain refs, and screenshots over viewport coordinate input. Coordinates remain a visual fallback.
diff --git a/plugins/_browser/api/ws_browser.py b/plugins/_browser/api/ws_browser.py
index f1c28c42d..be7092826 100644
--- a/plugins/_browser/api/ws_browser.py
+++ b/plugins/_browser/api/ws_browser.py
@@ -9,6 +9,11 @@ from typing import Any, ClassVar
from agent import AgentContext
from helpers.ws import WsHandler
from helpers.ws_manager import WsResult
+from plugins._browser.helpers.config import (
+ DEFAULT_BROWSER_TAB_SCOPE,
+ TAB_SCOPE_KEY,
+ get_browser_config,
+)
from plugins._browser.helpers.runtime import get_runtime, list_runtime_sessions
@@ -119,13 +124,16 @@ class WsBrowser(WsHandler):
self._streams[stream_key] = asyncio.create_task(stream_task)
snapshot = await self._snapshot_for_browser(runtime, active_id)
+ browsers, all_browsers, tab_scope = await self._tabs_for_scope(context_id, browsers)
+
return {
"context_id": context_id,
"active_browser_context_id": context_id,
"active_browser_id": active_id,
"snapshot": snapshot,
- "browsers": await self._all_browser_tabs(),
- "all_browsers": True,
+ "browsers": browsers,
+ "all_browsers": all_browsers,
+ "tab_scope": tab_scope,
"viewer_id": viewer_id,
"viewer_transport": viewer_transport,
"binary_frames": binary_frames,
@@ -142,10 +150,23 @@ class WsBrowser(WsHandler):
return {"context_id": context_id, "unsubscribed": True}
async def _sessions(self, data: dict[str, Any]) -> dict[str, Any]:
+ context_id = self._context_id(data)
+ tab_scope = self._tab_scope()
+ if tab_scope == "shared":
+ return {
+ "context_id": context_id,
+ "browsers": await self._all_browser_tabs(),
+ "all_browsers": True,
+ "tab_scope": tab_scope,
+ }
+
+ runtime = await get_runtime(context_id, create=False) if context_id else None
+ listing = await runtime.call("list") if runtime else {}
return {
- "context_id": self._context_id(data),
- "browsers": await self._all_browser_tabs(),
- "all_browsers": True,
+ "context_id": context_id,
+ "browsers": listing.get("browsers") or [],
+ "all_browsers": False,
+ "tab_scope": tab_scope,
}
async def _snapshot(self, data: dict[str, Any]) -> dict[str, Any] | WsResult:
@@ -157,13 +178,15 @@ class WsBrowser(WsHandler):
runtime = await get_runtime(context_id, create=False)
if not runtime:
+ browsers, all_browsers, tab_scope = await self._tabs_for_scope(context_id, [])
return {
"context_id": context_id,
"active_browser_context_id": context_id,
"active_browser_id": None,
"snapshot": None,
- "browsers": await self._all_browser_tabs(),
- "all_browsers": True,
+ "browsers": browsers,
+ "all_browsers": all_browsers,
+ "tab_scope": tab_scope,
}
listing = await runtime.call("list")
@@ -182,13 +205,16 @@ class WsBrowser(WsHandler):
quality=quality,
)
+ browsers, all_browsers, tab_scope = await self._tabs_for_scope(context_id, browsers)
+
return {
"context_id": context_id,
"active_browser_context_id": context_id,
"active_browser_id": active_id,
"snapshot": snapshot,
- "browsers": await self._all_browser_tabs(),
- "all_browsers": True,
+ "browsers": browsers,
+ "all_browsers": all_browsers,
+ "tab_scope": tab_scope,
}
async def _command(self, data: dict[str, Any], sid: str) -> dict[str, Any] | WsResult:
@@ -223,7 +249,10 @@ class WsBrowser(WsHandler):
listing = await runtime.call("list")
last_interacted_browser_id = listing.get("last_interacted_browser_id")
snapshot = await self._snapshot_for_result(runtime, result)
- all_browsers = await self._all_browser_tabs()
+ browsers, all_browsers, tab_scope = await self._tabs_for_scope(
+ context_id,
+ listing.get("browsers") or [],
+ )
await self.emit_to(
sid,
"browser_viewer_state",
@@ -235,8 +264,9 @@ class WsBrowser(WsHandler):
"browser_id": browser_id,
"result": result,
"snapshot": snapshot,
- "browsers": all_browsers,
- "all_browsers": True,
+ "browsers": browsers,
+ "all_browsers": all_browsers,
+ "tab_scope": tab_scope,
"last_interacted_browser_id": last_interacted_browser_id,
"viewer_transport": self._viewer_transport(data),
},
@@ -245,8 +275,9 @@ class WsBrowser(WsHandler):
return {
"result": result,
"snapshot": snapshot,
- "browsers": all_browsers,
- "all_browsers": True,
+ "browsers": browsers,
+ "all_browsers": all_browsers,
+ "tab_scope": tab_scope,
"active_browser_context_id": context_id,
"last_interacted_browser_id": last_interacted_browser_id,
"command": command,
@@ -376,6 +407,24 @@ class WsBrowser(WsHandler):
return await runtime.call("screenshot", browser_id, quality=SCREENSHOT_QUALITY)
return None
+ @staticmethod
+ def _tab_scope() -> str:
+ scope = str(
+ (get_browser_config() or {}).get(TAB_SCOPE_KEY, DEFAULT_BROWSER_TAB_SCOPE)
+ or DEFAULT_BROWSER_TAB_SCOPE
+ ).strip().lower().replace("-", "_")
+ return "shared" if scope == "shared" else DEFAULT_BROWSER_TAB_SCOPE
+
+ async def _tabs_for_scope(
+ self,
+ context_id: str,
+ browsers: list[dict[str, Any]] | None,
+ ) -> tuple[list[dict[str, Any]], bool, str]:
+ tab_scope = self._tab_scope()
+ if tab_scope == "shared":
+ return await self._all_browser_tabs(), True, tab_scope
+ return browsers or [], False, tab_scope
+
async def _all_browser_tabs(self) -> list[dict[str, Any]]:
browsers: list[dict[str, Any]] = []
for session in await list_runtime_sessions():
@@ -693,6 +742,7 @@ class WsBrowser(WsHandler):
state: dict[str, Any] | None = None,
viewer_transport: str = VIEWER_TRANSPORT_SNAPSHOT,
) -> None:
+ browsers, all_browsers, tab_scope = await self._tabs_for_scope(context_id, browsers or [])
await self._emit_to_connected_viewer(
sid,
"browser_viewer_state",
@@ -704,7 +754,8 @@ class WsBrowser(WsHandler):
"active_browser_id": browser_id,
"browsers": browsers or [],
"state": state,
- "all_browsers": False,
+ "all_browsers": all_browsers,
+ "tab_scope": tab_scope,
"viewer_transport": viewer_transport,
},
)
@@ -718,6 +769,7 @@ class WsBrowser(WsHandler):
viewer_id: str = "",
frame_source: str = "",
) -> None:
+ browsers, all_browsers, tab_scope = await self._tabs_for_scope(context_id, browsers or [])
await self._emit_to_connected_viewer(
sid,
"browser_viewer_frame",
@@ -726,6 +778,8 @@ class WsBrowser(WsHandler):
"viewer_id": viewer_id,
"browser_id": None,
"browsers": browsers or [],
+ "all_browsers": all_browsers,
+ "tab_scope": tab_scope,
"image": "",
"mime": "",
"state": None,
diff --git a/plugins/_browser/default_config.yaml b/plugins/_browser/default_config.yaml
index b87c040e1..e4416d0fa 100644
--- a/plugins/_browser/default_config.yaml
+++ b/plugins/_browser/default_config.yaml
@@ -8,6 +8,11 @@ default_homepage: "about:blank"
# When the Browser surface is already open, keep it synced to agent Browser tool results.
autofocus_active_page: true
+# Browser tab visibility in the WebUI:
+# - per_context: each chat shows only its own Browser tabs.
+# - shared: show Browser tabs from all active chats.
+browser_tab_scope: "per_context"
+
# Maximum number of Browser tabs/pages a single chat context may keep open.
# Raise this only for deliberate parallel browsing workflows.
max_open_tabs: 32
diff --git a/plugins/_browser/helpers/config.py b/plugins/_browser/helpers/config.py
index f39e52e51..a4a052aed 100644
--- a/plugins/_browser/helpers/config.py
+++ b/plugins/_browser/helpers/config.py
@@ -11,13 +11,16 @@ PLUGIN_NAME = "_browser"
MODEL_PRESET_KEY = "model_preset"
DEFAULT_HOMEPAGE_KEY = "default_homepage"
AUTOFOCUS_ACTIVE_PAGE_KEY = "autofocus_active_page"
+TAB_SCOPE_KEY = "browser_tab_scope"
MAX_OPEN_TABS_KEY = "max_open_tabs"
RUNTIME_BACKEND_KEY = "runtime_backend"
HOST_BROWSER_PRIVACY_POLICY_KEY = "host_browser_privacy_policy"
HOST_BROWSER_PROFILE_MODE_KEY = "host_browser_profile_mode"
RUNTIME_BACKENDS = {"container", "host_required"}
+BROWSER_TAB_SCOPES = {"per_context", "shared"}
HOST_BROWSER_PRIVACY_POLICIES = {"enforce_local", "warn", "allow"}
HOST_BROWSER_PROFILE_MODES = {"existing", "agent"}
+DEFAULT_BROWSER_TAB_SCOPE = "per_context"
DEFAULT_MAX_OPEN_TABS = 32
MIN_MAX_OPEN_TABS = 1
HARD_MAX_OPEN_TABS = 50
@@ -117,6 +120,11 @@ def normalize_browser_config(settings: dict[str, Any] | None) -> dict[str, Any]:
raw.get(AUTOFOCUS_ACTIVE_PAGE_KEY, True),
default=True,
),
+ TAB_SCOPE_KEY: _normalize_choice(
+ raw.get(TAB_SCOPE_KEY, DEFAULT_BROWSER_TAB_SCOPE),
+ allowed=BROWSER_TAB_SCOPES,
+ default=DEFAULT_BROWSER_TAB_SCOPE,
+ ),
MAX_OPEN_TABS_KEY: _normalize_int(
raw.get(MAX_OPEN_TABS_KEY, DEFAULT_MAX_OPEN_TABS),
default=DEFAULT_MAX_OPEN_TABS,
diff --git a/plugins/_browser/webui/browser-config-store.js b/plugins/_browser/webui/browser-config-store.js
index 81fee2e85..aea9fac14 100644
--- a/plugins/_browser/webui/browser-config-store.js
+++ b/plugins/_browser/webui/browser-config-store.js
@@ -4,8 +4,12 @@ import { callJsonApi } from "/js/api.js";
const BROWSER_EXTENSIONS_API = "/plugins/_browser/extensions";
const BROWSER_STATUS_API = "/plugins/_browser/status";
const RUNTIME_BACKENDS = new Set(["container", "host_required"]);
+const BROWSER_TAB_SCOPES = new Set(["per_context", "shared"]);
const HOST_PRIVACY_POLICIES = new Set(["enforce_local", "warn", "allow"]);
const HOST_PROFILE_MODES = new Set(["existing", "agent"]);
+const DEFAULT_MAX_OPEN_TABS = 32;
+const MIN_MAX_OPEN_TABS = 1;
+const HARD_MAX_OPEN_TABS = 50;
function normalizePathList(value) {
const source = Array.isArray(value)
@@ -27,6 +31,8 @@ function ensureConfig(config) {
config.extension_paths = normalizePathList(config.extension_paths);
config.default_homepage = String(config.default_homepage || "about:blank").trim() || "about:blank";
config.autofocus_active_page = normalizeBoolean(config.autofocus_active_page, true);
+ config.browser_tab_scope = normalizeChoice(config.browser_tab_scope, BROWSER_TAB_SCOPES, "per_context");
+ config.max_open_tabs = normalizeInt(config.max_open_tabs, DEFAULT_MAX_OPEN_TABS, MIN_MAX_OPEN_TABS, HARD_MAX_OPEN_TABS);
config.runtime_backend = normalizeRuntimeBackend(config.runtime_backend);
config.host_browser_privacy_policy = normalizeChoice(
config.host_browser_privacy_policy,
@@ -48,6 +54,12 @@ function normalizeChoice(value, allowed, fallback) {
return allowed.has(normalized) ? normalized : fallback;
}
+function normalizeInt(value, fallback, minimum, maximum) {
+ const number = Number.parseInt(value, 10);
+ if (!Number.isFinite(number)) return fallback;
+ return Math.max(minimum, Math.min(maximum, number));
+}
+
function normalizeRuntimeBackend(value) {
const normalized = String(value || "").trim().toLowerCase().replace(/-/g, "_");
if (normalized === "host_when_available") return "host_required";
@@ -132,6 +144,27 @@ export const store = createStore("browserConfig", {
return this.config?.autofocus_active_page === false ? "Off" : "On";
},
+ setBrowserTabScope(value) {
+ const safeConfig = ensureConfig(this.config);
+ if (!safeConfig) return;
+ safeConfig.browser_tab_scope = normalizeChoice(value, BROWSER_TAB_SCOPES, "per_context");
+ },
+
+ browserTabScopeLabel() {
+ return this.config?.browser_tab_scope === "shared" ? "Shared" : "Per chat";
+ },
+
+ normalizeMaxOpenTabs() {
+ const safeConfig = ensureConfig(this.config);
+ if (!safeConfig) return;
+ safeConfig.max_open_tabs = normalizeInt(
+ safeConfig.max_open_tabs,
+ DEFAULT_MAX_OPEN_TABS,
+ MIN_MAX_OPEN_TABS,
+ HARD_MAX_OPEN_TABS,
+ );
+ },
+
runtimeBackendLabel() {
const value = this.config?.runtime_backend || "container";
if (value === "host_required") return "Bring Your Own Browser";
diff --git a/plugins/_browser/webui/browser-panel.html b/plugins/_browser/webui/browser-panel.html
index 1abcde86f..c92a1acec 100644
--- a/plugins/_browser/webui/browser-panel.html
+++ b/plugins/_browser/webui/browser-panel.html
@@ -16,7 +16,7 @@