browser: replace browser-use agent with native browser

Introduce the new built-in Browser plugin for Agent Zero, replacing the legacy
browser-use-based browser agent with a direct Playwright-powered browser tool,
live WebUI viewer, browser session controls, status APIs, configuration, and
extension-management support.

Add browser-specific modal behavior so the browser can run as a floating,
resizable, no-backdrop window, including modal focus, toggle, and idempotent
open helpers for richer WebUI surfaces.

Remove the old `_browser_agent` core plugin and the `browser-use` dependency,
then clean up stale browser-model wiring and references across agent code,
model configuration docs, setup guides, troubleshooting docs, skills, and
Agent Zero knowledge.

Update regression and WebUI extension-surface coverage for the new browser
architecture and modal behavior.

The legacy browser-use implementation has been extracted from core so it can
continue separately as a community plugin published through the A0 Plugin Index for any user or professional that were relying on it for workflow.
This commit is contained in:
Alessandro 2026-04-24 15:43:52 +02:00
parent 603fc2064b
commit 983d431a5e
65 changed files with 6936 additions and 1926 deletions

View file

@ -0,0 +1,10 @@
from helpers.extension import Extension
from plugins._browser.helpers.runtime import close_runtime_sync
class CleanupBrowserRuntimeOnRemove(Extension):
def execute(self, data: dict = {}, **kwargs):
args = data.get("args", ())
context_id = args[0] if isinstance(args, tuple) and args else ""
if context_id:
close_runtime_sync(str(context_id), delete_profile=True)

View file

@ -0,0 +1,11 @@
from helpers.extension import Extension
from plugins._browser.helpers.runtime import close_runtime_sync
class CleanupBrowserRuntimeOnReset(Extension):
def execute(self, data: dict = {}, **kwargs):
args = data.get("args", ())
context = args[0] if isinstance(args, tuple) and args else None
context_id = getattr(context, "id", "")
if context_id:
close_runtime_sync(context_id, delete_profile=True)

View file

@ -0,0 +1,59 @@
from __future__ import annotations
from typing import Any
from agent import LoopData
from helpers.extension import Extension
from plugins._browser.helpers.runtime import get_runtime
class BrowserContextPrompt(Extension):
async def execute(
self,
system_prompt: list[str] = [],
loop_data: LoopData = LoopData(),
**kwargs: Any,
):
if not self.agent:
return
runtime = await get_runtime(self.agent.context.id, create=False)
if not runtime:
return
try:
listing = await runtime.call("list")
except Exception:
return
browsers = listing.get("browsers") or []
if not browsers:
return
rows = ["browser id|url|title"]
for browser in browsers:
rows.append(
f"{browser.get('id')}|{browser.get('currentUrl', '')}|{browser.get('title', '')}"
)
section = ["currently open web browsers", "\n".join(rows)]
last_id = listing.get("last_interacted_browser_id")
if last_id:
try:
state = await runtime.call("state", last_id)
content = await runtime.call("content", last_id, None)
document = content.get("document") if isinstance(content, dict) else ""
if document:
section.extend(
[
"",
"last interacted web browser",
f"browser id|url|title\n{state.get('id')}|{state.get('currentUrl', '')}|{state.get('title', '')}",
"page content↓",
str(document),
]
)
except Exception:
pass
system_prompt.append("\n".join(section))

View file

@ -0,0 +1,24 @@
from __future__ import annotations
from typing import Any
from helpers.extension import Extension
from plugins._browser.api.ws_browser import WsBrowser
class BrowserWebuiWsDisconnect(Extension):
async def execute(
self,
instance: Any = None,
sid: str = "",
**kwargs: Any,
) -> None:
if instance is None:
return
handler = WsBrowser(
instance.socketio,
instance.lock,
manager=instance.manager,
namespace=instance.namespace,
)
await handler.on_disconnect(sid)

View file

@ -0,0 +1,47 @@
from __future__ import annotations
from typing import Any
from helpers.extension import Extension
from helpers.ws_manager import WsResult
from plugins._browser.api.ws_browser import WsBrowser
class BrowserWebuiWsEvents(Extension):
async def execute(
self,
instance: Any = None,
sid: str = "",
event_type: str = "",
data: dict[str, Any] | None = None,
response_data: dict[str, Any] | None = None,
**kwargs: Any,
) -> None:
if not event_type.startswith("browser_") or instance is None or response_data is None:
return
handler = WsBrowser(
instance.socketio,
instance.lock,
manager=instance.manager,
namespace=instance.namespace,
)
result = await handler.process(event_type, data or {}, sid)
if result is None:
return
if isinstance(result, WsResult):
payload = result.as_result(
handler_id=handler.identifier,
fallback_correlation_id=(data or {}).get("correlationId"),
)
if payload.get("ok"):
response_data.update(payload.get("data") or {})
else:
response_data["browser_error"] = payload.get("error") or {
"code": "BROWSER_ERROR",
"error": "Browser request failed",
}
return
response_data.update(result)