Merge pull request #1312 from 3clyp50/browse-extract

browser-use builtin plugin, tool message hooks, Plugin Hub, WebUI polish
This commit is contained in:
Jan Tomášek 2026-03-23 21:35:57 +01:00 committed by GitHub
commit a5921397ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 782 additions and 258 deletions

View file

@ -93,13 +93,20 @@ docker run -p 50001:80 agent0ai/agent-zero
![Multi-agent](docs/res/usage/multi-agent.png)
### Browser Agent
- Browser automation is provided by the built-in `_browser_agent` plugin.
- It uses the effective Main Model resolved by `_model_config`; there is no separate browser model slot.
- Browser vision follows the Main Model's vision setting.
- Playwright Chromium: **Docker** images ship the headless shell preinstalled. **Local development** installs it on first Browser Agent use via `ensure_playwright_binary()` in `plugins/_browser_agent/helpers/playwright.py` (into `tmp/playwright`); you can pre-install manually (see [Development Setup](docs/setup/dev-setup.md)) to skip the wait.
4. **Completely Customizable and Extensible**
- Almost nothing in this framework is hard-coded. Nothing is hidden. Everything can be extended or changed by the user.
- The whole behavior is defined by a system prompt in the **prompts/default/agent.system.md** file. Change this prompt and change the framework dramatically.
- The framework does not guide or limit the agent in any way. There are no hard-coded rails that agents have to follow.
- Every prompt, every small message template sent to the agent in its communication loop can be found in the **prompts/** folder and changed.
- Every default tool can be found in the **python/tools/** folder and changed or copied to create new predefined tools.
- Built-in tools live in the core **tools/** folder or in built-in plugins under **plugins/** and can be adapted or extended.
- **Automated configuration** via `A0_SET_` environment variables for deployment automation and easy setup.
![Prompts](/docs/res/profiles.png)
@ -238,7 +245,7 @@ docker run -p 50001:80 agent0ai/agent-zero
- Secrets management - agent can use credentials without seeing them
- Agent can copy paste messages and files without rewriting them
- LiteLLM global configuration field
- Custom HTTP headers field for browser agent
- Browser agent configuration improvements
- Progressive web app support
- Extra model params support for JSON
- Short IDs for files and memories to prevent LLM errors

View file

@ -170,6 +170,8 @@ Place *.js files in extensions/webui/<extension_point>/ and export a default asy
Core JS hooks can also expose runtime UI surfaces when static HTML breakpoints are not a fit. For example, `confirm_dialog_after_render` runs after the shared confirm dialog is built and receives the rendered dialog/body/footer nodes plus any caller-provided `extensionContext`.
For tool chat rows (`type === "tool"`), after built-in badge rules, core calls `get_tool_message_handler` with a mutable object containing `tool_name`, `kvps`, and `handler`. Plugins can set `handler` to entirely take over rendering for their `_tool_name`. A plugin can import `drawMessageToolSimple` and call it internally (e.g., passing `{ ...args, code: "WWW" }`) if it just wants standard tool row styling with a custom badge.
### User Feedback: Notifications, Not Inline Errors
Plugin UI must use the **A0 notification system** for errors, success, and warnings. Do not render dedicated error/success boxes (e.g. a red block bound to `store.error`). Use the notification store so toasts and notification history stay consistent across the app.

View file

@ -114,4 +114,4 @@ Community-tested and reliable MCP servers:
- **VSCode MCP** - IDE workflows
> [!TIP]
> For browser automation tasks, MCP-based browser tools are more reliable than the built-in browser agent.
> For browser automation tasks, the built-in Browser Agent plugin covers the default workflow. MCP-based browser tools are still useful when you need a different browser stack, remote browser control, or an alternative to the built-in Playwright Chromium (preinstalled in Docker; on demand via `ensure_playwright_binary()` in local dev).

View file

@ -228,7 +228,7 @@ SMTP_PASSWORD=email_pwd_here
### Subagent Configuration
Projects can enable or disable specific subagents (like the Browser Agent). This is configured via the UI and stored in `.a0proj/agents.json`.
Projects can enable or disable specific subagents. This is configured via the UI and stored in `.a0proj/agents.json`. The Browser Agent is not a subagent; it is a built-in plugin.
### Knowledge Files

View file

@ -26,8 +26,8 @@ Refer to the [Choosing your LLMs](../setup/installation.md#installing-and-using-
**7. How can I make Agent Zero retain memory between sessions?**
Use **Settings → Backup & Restore** and avoid mapping the entire `/a0` directory. See [How to update Agent Zero](../setup/installation.md#how-to-update-agent-zero).
**8. My browser agent fails or is unreliable. What now?**
The built-in browser agent is currently unstable on some systems. Use Skills or MCP alternatives such as Browser OS, Chrome DevTools, or Vercel's Agent Browser. See [MCP Setup](mcp-setup.md).
**8. My browser agent fails or says Playwright is missing. What now?**
The built-in Browser Agent is a plugin that uses the Main Model from `_model_config`. **Docker:** the Chromium headless shell is shipped preinstalled (typically under `/a0/tmp/playwright`). **Local development:** if the binary is missing, `ensure_playwright_binary()` in `plugins/_browser_agent/helpers/playwright.py` runs `playwright install chromium --only-shell` into `tmp/playwright` on first Browser Agent use (you may see UI notifications). To install ahead of time, run `PLAYWRIGHT_BROWSERS_PATH=tmp/playwright playwright install chromium --only-shell` after `pip install -r requirements.txt`. If you prefer an external browser stack, use MCP alternatives such as Browser OS, Chrome DevTools, or Playwright MCP. See [MCP Setup](mcp-setup.md).
**9. My secrets disappeared after a backup restore.**
Secrets are stored in `/a0/usr/secrets.env` and are not always included in backup archives. Copy them manually.
@ -36,7 +36,7 @@ Secrets are stored in `/a0/usr/secrets.env` and are not always included in backu
- Join the Agent Zero [Skool](https://www.skool.com/agent-zero) or [Discord](https://discord.gg/B8KZKNsPpj) community.
**11. How do I adjust API rate limits?**
Use the model rate limit fields in Settings (Chat/Utility/Browser model sections) to set request/input/output limits. These map to the model config limits (for example `limit_requests`, `limit_input`, `limit_output`).
Use the model rate limit fields in Settings (Main Model and Utility Model sections) to set request/input/output limits. The Browser Agent inherits the Main Model limits. These map to the model config limits (for example `limit_requests`, `limit_input`, `limit_output`).
**12. My `code_execution_tool` doesn't work, what's wrong?**
- Ensure Docker is installed and running.

View file

@ -127,7 +127,9 @@ Agent Zero's power comes from its ability to use [tools](../developer/architectu
- **Understand Tools:** Agent Zero includes default tools like knowledge (powered by SearXNG), code execution, and communication. Understand the capabilities of these tools and how to invoke them.
### Browser Agent Status & MCP Alternatives
The built-in browser agent currently has dependency issues on some systems. If web automation is critical, prefer MCP-based browser tools instead:
The built-in Browser Agent is provided by the `_browser_agent` plugin. It uses the effective Main Model from `_model_config`, including per-chat overrides and the Main Model vision flag. Playwright Chromium is preinstalled in **Docker**; in **local development** it is installed on demand when needed via `ensure_playwright_binary()` (see [Development Setup](../setup/dev-setup.md) to pre-install).
If you need a different browser stack or want external browser tooling, MCP-based browser tools are still a strong option:
- **Browser OS MCP**
- **Chrome DevTools MCP**

View file

@ -67,9 +67,9 @@ Now when you select one of the python files in the project, you should see prope
3. Install dependencies. Run these two commands in the terminal:
```bash
pip install -r requirements.txt
playwright install chromium
PLAYWRIGHT_BROWSERS_PATH=tmp/playwright playwright install chromium --only-shell
```
These will install all the python packages and browser binaries for playwright (browser agent).
The first command installs Python dependencies. The second installs the Chromium headless shell into `tmp/playwright` ahead of time (same path in Docker: `/a0/tmp/playwright`). If you skip the second command, **local development** still downloads the shell on first Browser Agent use through `ensure_playwright_binary()` in `plugins/_browser_agent/helpers/playwright.py`. Pre-installing avoids that wait. **Docker** images ship the shell preinstalled; runtime install is for local dev when the binary is missing.
Errors in the code editor caused by missing packages should now be gone. If not, try reloading the window.

View file

@ -330,17 +330,19 @@ The Settings page is the control center for selecting the Large Language Models
| LLM Role | Description |
| --- | --- |
| `chat_llm` | This is the primary LLM used for conversations and generating responses. |
| `chat_llm` | This is the primary LLM used for conversations, agent reasoning, tool use, and the built-in browser agent. Vision support controls browser vision and image understanding. |
| `utility_llm` | This LLM handles internal tasks like summarizing messages, managing memory, and processing internal prompts. Using a smaller, less expensive model here can improve efficiency. |
| `browser_llm` | This LLM powers the browser agent for web navigation and interaction tasks. Vision support is recommended for better page understanding. |
| `embedding_llm` | The embedding model shipped with A0 runs on CPU and is responsible for generating embeddings used for memory retrieval and knowledge base lookups. Changing the `embedding_llm` will re-index all of A0's memory. |
**How to Change:**
1. Open Settings page in the Web UI.
2. Choose the provider for the LLM for each role (Chat model, Utility model, Browser model, Embedding model) and write the model name.
2. Choose the provider for the LLM for each role (Main Model, Utility Model, Embedding Model) and write the model name.
3. Click "Save" to apply the changes.
> [!NOTE]
> The Browser Agent does not have a separate model slot. It uses the effective Main Model resolved by `_model_config`, including per-chat overrides and the Main Model vision flag.
### Important Considerations
#### Model Naming by Provider

View file

@ -2,16 +2,15 @@
## LLM Roles
Agent Zero uses four distinct LLM roles, each configurable independently:
Agent Zero uses three configurable LLM roles:
| Role | Purpose |
|------|---------|
| `chat_llm` | Primary model for all agent reasoning and tool use |
| `chat_llm` | Primary model for all agent reasoning, tool use, and the Browser Agent |
| `utility_llm` | Secondary model for internal framework tasks: memory summarization, query generation, history compression, memory recall filtering |
| `browser_llm` | Model used by the browser agent; vision capability recommended |
| `embedding_llm` | Produces vector embeddings for memory and knowledge indexing |
The utility model handles high-volume, lower-stakes operations and can be a cheaper/faster model than the chat model. Changing the embedding model invalidates the existing vector index - the entire knowledge base is re-indexed automatically.
The utility model handles high-volume, lower-stakes operations and can be a cheaper/faster model than the chat model. The Browser Agent uses the effective chat model resolved by `_model_config`, including per-chat overrides and the chat model vision flag. Changing the embedding model invalidates the existing vector index - the entire knowledge base is re-indexed automatically.
## Model Providers

111
models.py
View file

@ -25,7 +25,7 @@ from helpers.dotenv import load_dotenv
from helpers.providers import ModelType as ProviderModelType, get_provider_config
from helpers.rate_limiter import RateLimiter
from helpers.tokens import approximate_tokens
from helpers import dirty_json, browser_use_monkeypatch
from helpers import dirty_json
from langchain_core.language_models.chat_models import SimpleChatModel
from langchain_core.outputs.chat_generation import ChatGenerationChunk
@ -57,9 +57,6 @@ def turn_off_logging():
# init
load_dotenv()
turn_off_logging()
browser_use_monkeypatch.apply()
litellm.modify_params = True # helps fix anthropic tool calls by browser-use
class ModelType(Enum):
CHAT = "Chat"
@ -578,101 +575,6 @@ class LiteLLMChatWrapper(SimpleChatModel):
await asyncio.sleep(retry_delay_s)
class AsyncAIChatReplacement:
class _Completions:
def __init__(self, wrapper):
self._wrapper = wrapper
async def create(self, *args, **kwargs):
# call the async _acall method on the wrapper
return await self._wrapper._acall(*args, **kwargs)
class _Chat:
def __init__(self, wrapper):
self.completions = AsyncAIChatReplacement._Completions(wrapper)
def __init__(self, wrapper, *args, **kwargs):
self._wrapper = wrapper
self.chat = AsyncAIChatReplacement._Chat(wrapper)
from browser_use.llm import ChatOllama, ChatOpenRouter, ChatGoogle, ChatAnthropic, ChatGroq, ChatOpenAI
class BrowserCompatibleChatWrapper(ChatOpenRouter):
"""
A wrapper for browser agent that can filter/sanitize messages
before sending them to the LLM.
"""
def __init__(self, *args, **kwargs):
turn_off_logging()
# Create the underlying LiteLLM wrapper
self._wrapper = LiteLLMChatWrapper(*args, **kwargs)
# Browser-use may expect a 'model' attribute
self.model = self._wrapper.model_name
self.kwargs = self._wrapper.kwargs
@property
def model_name(self) -> str:
return self._wrapper.model_name
@property
def provider(self) -> str:
return self._wrapper.provider
def get_client(self, *args, **kwargs): # type: ignore
return AsyncAIChatReplacement(self, *args, **kwargs)
async def _acall(
self,
messages: List[BaseMessage],
stop: Optional[List[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any,
):
# Apply rate limiting if configured
apply_rate_limiter_sync(self._wrapper.a0_model_conf, str(messages))
# Call the model
try:
model = kwargs.pop("model", None)
kwrgs = {**self._wrapper.kwargs, **kwargs}
# hack from browser-use to fix json schema for gemini (additionalProperties, $defs, $ref)
if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] and model.startswith("gemini/"):
kwrgs["response_format"]["json_schema"] = ChatGoogle("")._fix_gemini_schema(kwrgs["response_format"]["json_schema"])
resp = await acompletion(
model=self._wrapper.model_name,
messages=messages,
stop=stop,
**kwrgs,
)
# Gemini: strip triple backticks and conform schema
try:
msg = resp.choices[0].message # type: ignore
if self.provider == "gemini" and isinstance(getattr(msg, "content", None), str):
cleaned = browser_use_monkeypatch.gemini_clean_and_conform(msg.content) # type: ignore
if cleaned:
msg.content = cleaned
except Exception:
pass
except Exception as e:
raise e
# another hack for browser-use post process invalid jsons
try:
if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] or "json_object" in kwrgs["response_format"]:
if resp.choices[0].message.content is not None and not resp.choices[0].message.content.startswith("{"): # type: ignore
js = dirty_json.parse(resp.choices[0].message.content) # type: ignore
resp.choices[0].message.content = dirty_json.stringify(js) # type: ignore
except Exception as e:
pass
return resp
class LiteLLMEmbeddingWrapper(Embeddings):
model_name: str
kwargs: dict = {}
@ -910,17 +812,6 @@ def get_chat_model(
LiteLLMChatWrapper, name, provider_name, model_config, **kwargs
)
def get_browser_model(
provider: str, name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any
) -> BrowserCompatibleChatWrapper:
orig = provider.lower()
provider_name, kwargs = _merge_provider_defaults("chat", orig, kwargs)
return _get_litellm_chat(
BrowserCompatibleChatWrapper, name, provider_name, model_config, **kwargs
)
def get_embedding_model(
provider: str, name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any
) -> LiteLLMEmbeddingWrapper | LocalSentenceTransformerWrapper:

View file

@ -0,0 +1,45 @@
import importlib.metadata
from helpers.api import ApiHandler, Request, Response
from plugins._browser_agent.helpers.playwright import (
get_playwright_binary,
get_playwright_cache_dir,
)
from plugins._model_config.helpers.model_config import get_chat_model_config
class Status(ApiHandler):
async def process(self, input: dict, request: Request) -> dict | Response:
cfg = get_chat_model_config()
binary = get_playwright_binary()
browser_use_ok = False
browser_use_error = ""
browser_use_version = ""
try:
import browser_use # noqa: F401
browser_use_ok = True
browser_use_version = importlib.metadata.version("browser-use")
except Exception as e:
browser_use_error = str(e)
return {
"plugin": "_browser_agent",
"model_source": "Main Model via _model_config",
"model": {
"provider": cfg.get("provider", ""),
"name": cfg.get("name", ""),
"vision": bool(cfg.get("vision", False)),
},
"playwright": {
"cache_dir": get_playwright_cache_dir(),
"binary_found": bool(binary),
"binary_path": str(binary) if binary else "",
},
"browser_use": {
"import_ok": browser_use_ok,
"version": browser_use_version,
"error": browser_use_error,
},
}

View file

@ -243,4 +243,4 @@
// }
// return element;
// };
// })();
// })();

View file

@ -0,0 +1,54 @@
import {
createActionButton,
copyToClipboard,
} from "/components/messages/action-buttons/simple-action-buttons.js";
import { store as stepDetailStore } from "/components/modals/process-step-detail/step-detail-store.js";
import { store as speechStore } from "/components/chat/speech/speech-store.js";
import {
buildDetailPayload,
cleanStepTitle,
drawProcessStep,
} from "/js/messages.js";
export default async function registerBrowserAgentHandler(extData) {
if (extData?.type === "browser") {
extData.handler = drawMessageBrowserAgent;
}
}
function drawMessageBrowserAgent({
id,
type,
heading,
content,
kvps,
timestamp,
agentno = 0,
...additional
}) {
const title = cleanStepTitle(heading);
const displayKvps = { ...kvps };
const answerText = String(kvps?.answer ?? "");
const actionButtons = answerText.trim()
? [
createActionButton("detail", "", () =>
stepDetailStore.showStepDetail(
buildDetailPayload(arguments[0], { headerLabels: [] }),
),
),
createActionButton("speak", "", () => speechStore.speak(answerText)),
createActionButton("copy", "", () => copyToClipboard(answerText)),
].filter(Boolean)
: [];
return drawProcessStep({
id,
title,
code: "WWW",
classes: undefined,
kvps: displayKvps,
content,
actionButtons,
log: arguments[0],
});
}

View file

@ -0,0 +1,15 @@
import { drawMessageToolSimple } from "/js/messages.js";
/**
* Registers the browser_agent tool message handler to set the custom badge.
* @param {object} extData
*/
export default async function registerBrowserToolHandler(extData) {
if (extData?.tool_name === "browser_agent") {
extData.handler = drawBrowserTool;
}
}
function drawBrowserTool(args) {
return drawMessageToolSimple({ ...args, code: "WWW" });
}

View file

@ -0,0 +1 @@
# Built-in browser agent helpers.

View file

@ -0,0 +1,131 @@
from typing import Any, List, Optional
import litellm
from litellm import acompletion
from langchain_core.callbacks.manager import CallbackManagerForLLMRun
from langchain_core.messages import BaseMessage
import models
from browser_use.llm import ChatGoogle, ChatOpenRouter
from helpers import dirty_json
from plugins._browser_agent.helpers import browser_use_monkeypatch
_BROWSER_USE_PATCHED = False
def apply_browser_use_patches() -> None:
global _BROWSER_USE_PATCHED
if _BROWSER_USE_PATCHED:
return
browser_use_monkeypatch.apply()
litellm.modify_params = True
_BROWSER_USE_PATCHED = True
class AsyncAIChatReplacement:
class _Completions:
def __init__(self, wrapper):
self._wrapper = wrapper
async def create(self, *args, **kwargs):
return await self._wrapper._acall(*args, **kwargs)
class _Chat:
def __init__(self, wrapper):
self.completions = AsyncAIChatReplacement._Completions(wrapper)
def __init__(self, wrapper, *args, **kwargs):
self._wrapper = wrapper
self.chat = AsyncAIChatReplacement._Chat(wrapper)
class BrowserCompatibleChatWrapper(ChatOpenRouter):
"""
A wrapper for browser agent that can filter/sanitize messages
before sending them to the LLM.
"""
def __init__(self, *args, **kwargs):
apply_browser_use_patches()
models.turn_off_logging()
self._wrapper = models.LiteLLMChatWrapper(*args, **kwargs)
self.model = self._wrapper.model_name
self.kwargs = self._wrapper.kwargs
@property
def model_name(self) -> str:
return self._wrapper.model_name
@property
def provider(self) -> str:
return self._wrapper.provider
def get_client(self, *args, **kwargs): # type: ignore
return AsyncAIChatReplacement(self, *args, **kwargs)
async def _acall(
self,
messages: List[BaseMessage],
stop: Optional[List[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any,
):
models.apply_rate_limiter_sync(self._wrapper.a0_model_conf, str(messages))
try:
model = kwargs.pop("model", None)
kwrgs = {**self._wrapper.kwargs, **kwargs}
# hack from browser-use to fix json schema for gemini (additionalProperties, $defs, $ref)
if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] and model and model.startswith("gemini/"):
kwrgs["response_format"]["json_schema"] = ChatGoogle("")._fix_gemini_schema(kwrgs["response_format"]["json_schema"])
resp = await acompletion(
model=self._wrapper.model_name,
messages=messages,
stop=stop,
**kwrgs,
)
# Gemini: strip triple backticks and conform schema
try:
msg = resp.choices[0].message # type: ignore
if self.provider == "gemini" and isinstance(getattr(msg, "content", None), str):
cleaned = browser_use_monkeypatch.gemini_clean_and_conform(msg.content) # type: ignore
if cleaned:
msg.content = cleaned
except Exception:
pass
except Exception as e:
raise e
# another hack for browser-use post process invalid jsons
try:
if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] or "json_object" in kwrgs["response_format"]:
if resp.choices[0].message.content is not None and not resp.choices[0].message.content.startswith("{"): # type: ignore
js = dirty_json.parse(resp.choices[0].message.content) # type: ignore
resp.choices[0].message.content = dirty_json.stringify(js) # type: ignore
except Exception as e:
pass
return resp
def build_browser_model_from_config(
model_config: models.ModelConfig,
) -> BrowserCompatibleChatWrapper:
apply_browser_use_patches()
original_provider = model_config.provider.lower()
provider_name, kwargs = models._merge_provider_defaults( # type: ignore[attr-defined]
"chat", original_provider, model_config.build_kwargs()
)
return models._get_litellm_chat( # type: ignore[attr-defined]
BrowserCompatibleChatWrapper,
model_config.name,
provider_name,
model_config,
**kwargs,
)

View file

@ -1,4 +1,4 @@
from helpers import dotenv
dotenv.save_dotenv_value("ANONYMIZED_TELEMETRY", "false")
import browser_use
import browser_use.utils
import browser_use.utils

View file

@ -1,4 +1,3 @@
import os
import sys
from pathlib import Path
@ -36,4 +35,4 @@ def ensure_playwright_binary():
bin = get_playwright_binary()
if not bin:
raise Exception("Playwright binary not found after installation")
return bin
return bin

View file

@ -0,0 +1,8 @@
name: _browser_agent
title: Browser Agent
description: Built-in browser-use automation tool.
version: 1.0.0
always_enabled: false
settings_sections: []
per_project_config: false
per_agent_config: false

View file

@ -19,4 +19,4 @@ When you have completed the assigned task OR are waiting for further instruction
- Response field is used to answer to user's task or ask additional questions
- If you navigate to a website and no further actions are requested, call "Complete task" immediately
- If you complete any requested interaction (clicking, typing, etc.), call "Complete task"
- Never leave a task running indefinitely - always conclude with "Complete task"
- Never leave a task running indefinitely - always conclude with "Complete task"

View file

@ -6,9 +6,9 @@ from pathlib import Path
from helpers.tool import Tool, Response
from helpers import files, defer, persist_chat, strings
from helpers.browser_use import browser_use # type: ignore[attr-defined]
from plugins._browser_agent.helpers.browser_use import browser_use # type: ignore[attr-defined]
from helpers.print_style import PrintStyle
from helpers.playwright import ensure_playwright_binary
from plugins._browser_agent.helpers.playwright import ensure_playwright_binary
from helpers.secrets import get_secrets_manager
from extensions.python.message_loop_start._10_iteration_no import get_iter_no
from pydantic import BaseModel
@ -16,6 +16,9 @@ import uuid
from helpers.dirty_json import DirtyJson
PLUGIN_DIR = Path(__file__).resolve().parents[1]
class State:
@staticmethod
async def create(agent: Agent):
@ -105,7 +108,7 @@ class State:
# Add init script to the browser session
if self.browser_session and self.browser_session.browser_context:
js_override = files.get_abs_path("lib/browser/init_override.js")
js_override = str(PLUGIN_DIR / "assets" / "init_override.js")
await self.browser_session.browser_context.add_init_script(path=js_override) if self.browser_session else None
def start_task(self, task: str):

View file

@ -0,0 +1,204 @@
<html>
<head>
<title>Browser Agent</title>
<script type="module">
import { callJsonApi } from "/js/api.js";
import "/components/plugins/list/pluginListStore.js";
globalThis.browserAgentStatusApi = { callJsonApi };
</script>
</head>
<body>
<div
x-data="{
loading: true,
error: '',
status: null,
async init() {
try {
this.status = await browserAgentStatusApi.callJsonApi('/plugins/_browser_agent/status', {});
} catch (error) {
this.error = error instanceof Error ? error.message : String(error);
} finally {
this.loading = false;
}
}
}"
x-init="init()"
class="browser-agent-page"
>
<div class="section-title">Browser Agent</div>
<div class="section-description">
Built-in browser automation plugin backed by `browser-use` and Playwright.
Model selection stays in `_model_config`; the browser agent follows the effective Main Model.
</div>
<div class="browser-agent-card" x-show="loading">
<div class="status-row">
<span class="material-symbols-outlined spinning">progress_activity</span>
<span>Loading browser status...</span>
</div>
</div>
<div class="browser-agent-card error" x-show="!loading && error">
<div class="field-title">Status check failed</div>
<div class="field-description" x-text="error"></div>
</div>
<template x-if="!loading && status">
<div class="browser-agent-grid">
<div class="browser-agent-card">
<div class="field-title">Model Source</div>
<div class="field-description" x-text="status.model_source"></div>
</div>
<div class="browser-agent-card">
<div class="field-title">Resolved Main Model</div>
<div class="status-row">
<span class="status-key">Provider</span>
<span class="status-value" x-text="status.model.provider || 'Not configured'"></span>
</div>
<div class="status-row">
<span class="status-key">Model</span>
<span class="status-value" x-text="status.model.name || 'Not configured'"></span>
</div>
<div class="status-row">
<span class="status-key">Vision</span>
<span class="status-badge" :class="status.model.vision ? 'ok' : 'warn'" x-text="status.model.vision ? 'Enabled' : 'Disabled'"></span>
</div>
</div>
<div class="browser-agent-card">
<div class="field-title">Playwright Runtime</div>
<div class="status-row">
<span class="status-key">Binary</span>
<span class="status-badge" :class="status.playwright.binary_found ? 'ok' : 'fail'" x-text="status.playwright.binary_found ? 'Found' : 'Missing'"></span>
</div>
<div class="status-row">
<span class="status-key">Cache</span>
<span class="status-value mono" x-text="status.playwright.cache_dir"></span>
</div>
<div class="status-row" x-show="status.playwright.binary_path">
<span class="status-key">Path</span>
<span class="status-value mono" x-text="status.playwright.binary_path"></span>
</div>
<div class="field-description" x-show="!status.playwright.binary_found">
Docker images ship the Playwright Chromium shell preinstalled. In local development, the first run installs it on demand via <span class="mono">ensure_playwright_binary()</span> if missing.
</div>
</div>
<div class="browser-agent-card">
<div class="field-title">browser-use</div>
<div class="status-row">
<span class="status-key">Import</span>
<span class="status-badge" :class="status.browser_use.import_ok ? 'ok' : 'fail'" x-text="status.browser_use.import_ok ? 'Ready' : 'Error'"></span>
</div>
<div class="status-row" x-show="status.browser_use.version">
<span class="status-key">Version</span>
<span class="status-value" x-text="status.browser_use.version"></span>
</div>
<div class="field-description mono" x-show="status.browser_use.error" x-text="status.browser_use.error"></div>
</div>
</div>
</template>
<div class="browser-agent-actions">
<button
class="btn btn-field"
@click="$store.pluginListStore.openPluginConfig({ name: '_model_config', has_config_screen: true })"
>
Open Model Settings
</button>
<button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/main.html')">
Open Presets
</button>
<button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/api-keys.html')">
Open API Keys
</button>
</div>
</div>
<style>
.browser-agent-page {
display: flex;
flex-direction: column;
gap: 14px;
}
.browser-agent-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.browser-agent-card {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px;
background: var(--color-input);
border: 1px solid var(--color-border);
border-radius: 10px;
}
.browser-agent-card.error {
border-color: rgba(214, 40, 40, 0.35);
}
.browser-agent-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.status-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
font-size: 0.84rem;
}
.status-key {
opacity: 0.7;
min-width: 64px;
}
.status-value {
text-align: right;
word-break: break-word;
}
.status-badge {
padding: 2px 8px;
border-radius: 999px;
font-size: 0.76rem;
font-weight: 600;
border: 1px solid transparent;
}
.status-badge.ok {
color: #1b5e20;
background: rgba(46, 125, 50, 0.14);
border-color: rgba(46, 125, 50, 0.24);
}
.status-badge.warn {
color: #8a6100;
background: rgba(191, 144, 0, 0.14);
border-color: rgba(191, 144, 0, 0.24);
}
.status-badge.fail {
color: #9f1239;
background: rgba(190, 24, 93, 0.12);
border-color: rgba(190, 24, 93, 0.24);
}
.mono {
font-family: var(--font-mono);
font-size: 0.78rem;
}
</style>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -187,13 +187,15 @@ def build_utility_model(agent=None):
def build_browser_model(agent=None):
"""Build and return a BrowserCompatibleChatWrapper using chat model config."""
"""Build and return the browser-use adapter using chat model config."""
cfg = get_chat_model_config(agent)
mc = build_model_config(cfg, models.ModelType.CHAT)
return models.get_browser_model(
mc.provider, mc.name, model_config=mc, **mc.build_kwargs()
from plugins._browser_agent.helpers.browser_llm import (
build_browser_model_from_config,
)
return build_browser_model_from_config(mc)
def build_embedding_model(agent=None):
"""Build and return an embedding model wrapper."""

View file

@ -8,8 +8,7 @@
<body>
<div x-data>
<template x-if="$store.pluginInstallStore">
<div class="pi-browse-shell"
x-create="$store.pluginInstallStore.resetIndex(); $store.pluginInstallStore.fetchIndex()">
<div class="pi-browse-shell">
<section class="pi-browse-hero">
<div class="pi-browse-copy">

View file

@ -64,6 +64,10 @@ const model = {
setTab(tab) {
this.activeTab = tab;
this.result = null;
if (tab === "store") {
this.resetIndex();
void this.fetchIndex();
}
},
setBrowseFilter(filter) {

View file

@ -0,0 +1,80 @@
import asyncio
import importlib
import json
import sys
from pathlib import Path
from types import SimpleNamespace
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
import plugins._browser_agent.helpers.browser_use_monkeypatch as browser_use_monkeypatch
import plugins._browser_agent.tools.browser_agent as browser_agent_module
def test_gemini_clean_and_conform_normalizes_known_single_action_shapes():
raw = (
'{"action":['
'{"complete_task":{"title":"T","response":"R","page_summary":"S"}}'
']}'
)
cleaned = browser_use_monkeypatch.gemini_clean_and_conform(raw)
assert cleaned is not None
parsed = json.loads(cleaned)
assert parsed["action"] == [
{
"done": {
"success": True,
"data": {
"title": "T",
"response": "R",
"page_summary": "S",
},
}
},
]
class DummyBrowserSession:
def __init__(self) -> None:
self.kill_called = False
self.close_called = False
async def kill(self) -> None:
self.kill_called = True
async def close(self) -> None:
self.close_called = True
class DummyAgent:
def __init__(self) -> None:
self.context = SimpleNamespace(id="ctx", task=None)
def test_browser_session_teardown_prefers_kill_for_keep_alive_sessions():
state = browser_agent_module.State(DummyAgent())
session = DummyBrowserSession()
state.browser_session = session
state.kill_task()
assert session.kill_called is True
assert session.close_called is False
def test_browser_cleanup_extensions_follow_new_extensible_path_layout():
extension = importlib.import_module("helpers.extension")
remove_classes = extension._get_extension_classes( # type: ignore[attr-defined]
"_functions/agent/AgentContext/remove/start"
)
reset_classes = extension._get_extension_classes( # type: ignore[attr-defined]
"_functions/agent/AgentContext/reset/start"
)
assert any(cls.__name__ == "CleanupBrowserStateOnRemove" for cls in remove_classes)
assert any(cls.__name__ == "CleanupBrowserStateOnReset" for cls in reset_classes)

View file

@ -85,7 +85,7 @@
/* Critical for allowing flex children to shrink */
overflow: hidden;
justify-content: space-between;
margin-top: 3.5rem;
margin-top: 6rem;
padding: var(--spacing-md) var(--spacing-md) 0 var(--spacing-md);
}

View file

@ -6,86 +6,212 @@
</script>
</head>
<body>
<div class="icons-section" id="hide-button" x-data>
<!-- Sidebar Toggle Button -->
<button id="toggle-sidebar" class="toggle-sidebar-button" @click="$store.sidebar.toggle()" aria-label="Toggle Sidebar" aria-expanded="false">
<span class="material-symbols-outlined" aria-hidden="true">menu</span>
</button>
<div class="sidebar-header-stack">
<!--
Single chrome block for logo + toolbar (tokens aligned with .config-button in quick-actions).
Spacer reserves in-flow height so quick-actions clears the fixed overlay (see .left-panel-top margin-top).
-->
<div id="sidebar-header-panel" class="sidebar-header-panel">
<div id="logo-bar">
<a href="https://github.com/agent0ai/agent-zero" target="_blank" rel="noopener noreferrer" aria-label="Agent Zero on GitHub">
<span id="a0-logo" role="img" aria-label="Agent Zero"></span>
</a>
</div>
<div id="logo-container">
<a href="https://github.com/agent0ai/agent-zero" target="_blank" rel="noopener noreferrer" aria-label="Agent Zero on GitHub">
<span id="a0-logo" role="img" aria-label="Agent Zero"></span>
</a>
<div class="icons-section" id="hide-button" x-data>
<!-- Sidebar Toggle Button -->
<button id="toggle-sidebar" class="toggle-sidebar-button" @click="$store.sidebar.toggle()" aria-label="Toggle Sidebar" aria-expanded="false">
<span class="material-symbols-outlined" aria-hidden="true">menu</span>
</button>
<button class="config-button header-action-button" id="header-dashboard" @click="deselectChat()" title="Dashboard" aria-label="Dashboard">
<span class="material-symbols-outlined" aria-hidden="true">home</span>
</button>
<button class="config-button header-action-button" id="header-plugins" @click="openModal('components/plugins/list/plugin-list.html')" title="Plugins" aria-label="Plugins">
<span class="material-symbols-outlined" aria-hidden="true">extension</span>
</button>
<button class="config-button header-action-button" id="header-settings" @click="openModal('settings/settings.html')" title="Settings" aria-label="Settings">
<span class="material-symbols-outlined" aria-hidden="true">settings</span>
</button>
</div>
</div>
</div>
<style>
/* Sidebar toggle button + logo container (migrated) */
.toggle-sidebar-button {
height: 2.6rem;
width: 2.6rem;
.sidebar-header-stack {
/*
Panel top (viewport) minus .left-panel-top margin-top: flow starts at 5rem; reserve overlap region.
Tuned for: padding + logo 1.47rem + gap + icon row ~2.2rem + padding.
*/
--sidebar-header-flow-clearance: calc(1.9rem + (var(--spacing-xs) * 5) + 1.47rem + 2.2rem - 5rem);
}
/* Shell: same language as .config-button (quick-actions.html) */
.sidebar-header-panel {
position: fixed;
top: var(--spacing-md);
left: var(--spacing-md);
width: calc(250px - (var(--spacing-md) * 2));
box-sizing: border-box;
z-index: 1004;
display: flex;
flex-direction: column;
align-items: stretch;
padding: var(--spacing-xs) var(--spacing-sm);
background-color: var(--color-panel);
border: 0.05rem solid var(--color-border);
border-radius: 8px;
transition: background-color var(--transition-speed),
border-color var(--transition-speed), box-shadow var(--transition-speed),
opacity var(--transition-speed) ease-in-out;
}
#logo-bar {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding-top: var(--spacing-xxs);
}
#logo-bar a {
color: inherit;
text-decoration: none;
width: min(100%, 11.75rem);
}
#hide-button {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-xs);
width: 100%;
}
/* Toolbar icons sit on the panel surface (no nested config-button boxes) */
.sidebar-header-panel #hide-button .config-button,
.sidebar-header-panel .toggle-sidebar-button {
background-color: transparent;
border: none;
border-radius: 4px;
box-shadow: none;
}
.sidebar-header-panel #hide-button .config-button {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.8;
padding: var(--spacing-xs);
min-width: 0;
min-height: 2.2rem;
transition: background-color var(--transition-speed),
border-color var(--transition-speed), box-shadow var(--transition-speed),
opacity var(--transition-speed);
}
.sidebar-header-panel #hide-button .config-button .material-symbols-outlined {
font-size: 22px;
transition: transform 0.2s ease;
}
.sidebar-header-panel #hide-button .config-button:hover {
background-color: var(--color-background-hover);
opacity: 1;
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 18%, transparent);
z-index: 1;
position: relative;
}
.sidebar-header-panel #hide-button .config-button:active {
opacity: 0.5;
box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.06);
}
.toggle-sidebar-button {
color: var(--color-text);
opacity: 0.8;
cursor: pointer;
left: var(--spacing-md);
padding: 0.47rem 0.56rem;
position: absolute;
top: 1.4rem;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 2.2rem;
padding: var(--spacing-xs);
-webkit-transition: all var(--transition-speed) ease-in-out;
transition: all var(--transition-speed) ease-in-out;
}
.toggle-sidebar-button:hover { background-color: transparent; opacity: 1; }
.toggle-sidebar-button:active { opacity: 0.5; }
.toggle-sidebar-button .material-symbols-outlined {
.toggle-sidebar-button:hover {
background-color: var(--color-background-hover);
opacity: 1;
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 18%, transparent);
}
.toggle-sidebar-button:active {
opacity: 0.5;
box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.06);
}
.toggle-sidebar-button .material-symbols-outlined {
font-size: 22px;
-webkit-transition: all var(--transition-speed) ease;
transition: all var(--transition-speed) ease;
-webkit-transition: all var(--transition-speed) ease;
transition: all var(--transition-speed) ease;
}
.toggle-sidebar-button:active .material-symbols-outlined { -webkit-transform: scaleY(0.8); transform: scaleY(0.8); }
#logo-container {
display: -webkit-flex;
display: flex;
align-items: center;
justify-content: space-between;
position: fixed;
left: 4rem;
top: var(--spacing-md);
z-index: 1004;
-webkit-transition: margin-left var(--transition-speed) ease-in-out;
transition: margin-left var(--transition-speed) ease-in-out;
.toggle-sidebar-button:active .material-symbols-outlined {
-webkit-transform: scaleY(0.8);
transform: scaleY(0.8);
}
#logo-container a { color: inherit; text-decoration: none; }
#a0-logo {
display: block;
height: 2.6rem;
aspect-ratio: 120 / 50;
-webkit-mask-image: url('/public/a0-fullDark.svg');
mask-image: url('/public/a0-fullDark.svg');
width: 100%;
height: 1.64rem;
-webkit-mask-image: url(/public/a0-fullDark.svg);
mask-image: url(/public/a0-fullDark.svg);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-position: center;
-webkit-mask-position: left;
mask-position: center;
background-color: var(--color-text);
opacity: 0.8;
-webkit-transition: opacity var(--transition-speed) ease, background-color var(--transition-speed) ease;
transition: opacity var(--transition-speed) ease, background-color var(--transition-speed) ease;
}
#logo-container a:hover #a0-logo { opacity: 1; }
#logo-bar a:hover #a0-logo {
opacity: 1;
}
.light-mode .sidebar-header-panel {
background-color: var(--color-background);
color: #333333;
}
.light-mode .sidebar-header-panel #hide-button .config-button:hover,
.light-mode .sidebar-header-panel .toggle-sidebar-button:hover {
background-color: var(--color-background-hover);
}
.light-mode .sidebar-header-panel #hide-button .config-button:active,
.light-mode .sidebar-header-panel .toggle-sidebar-button:active {
background-color: var(--color-background-hover);
}
@media (max-width: 768px) {
.toggle-sidebar-button { position: fixed; left: var(--spacing-md); z-index: 1004; }
#logo-container { -webkit-transition: all 0.3s ease; transition: all 0.3s ease; z-index: 1004; }
.sidebar-header-panel {
-webkit-transition: all 0.3s ease;
transition: all 0.3s ease;
}
}
</style>
</body>
</html>

View file

@ -14,21 +14,6 @@
<x-extension id="sidebar-quick-actions-main-start"></x-extension>
<!-- Dashboard -->
<button class="config-button" id="dashboard" @click="deselectChat()" title="Dashboard">
<span class="material-symbols-outlined">home</span>
</button>
<!-- Scheduler -->
<button class="config-button" id="plugins" @click="openModal('components/plugins/list/plugin-list.html')" title="Plugins">
<span class="material-symbols-outlined">extension</span>
</button>
<!-- Settings -->
<button class="config-button" id="settings" @click="openModal('settings/settings.html')" title="Settings">
<span class="material-symbols-outlined">settings</span>
</button>
<x-extension id="sidebar-quick-actions-main-end"></x-extension>
<!-- Dropdown Toggle -->
@ -126,6 +111,11 @@
gap: var(--spacing-xs);
}
.quick-actions-anchor {
display: none;
pointer-events: none;
}
#quick-actions > x-extension:not(:empty) {
display: contents;
}

View file

@ -200,11 +200,7 @@ const model = {
window.openModal("modals/scheduler/scheduler-modal.html");
break;
case "settings":
// Open settings modal
const settingsButton = document.getElementById("settings");
if (settingsButton) {
settingsButton.click();
}
window.openModal("settings/settings.html");
break;
case "projects":
projectsStore.openProjectsModal();

View file

@ -94,8 +94,6 @@ export async function getMessageHandler(type) {
return drawMessageResponse;
case "tool":
return drawMessageTool;
case "browser":
return drawMessageBrowser;
case "progress":
return drawMessageProgress;
case "mcp":
@ -1124,9 +1122,9 @@ export function drawMessageUser({
/**
* @param {MessageHandlerArgs & Record<string, any>} param0
* @returns {MessageHandlerResult}
* @returns {Promise<MessageHandlerResult>}
*/
export function drawMessageTool({
export async function drawMessageTool({
id,
type,
heading,
@ -1148,13 +1146,21 @@ export function drawMessageTool({
return drawMessageToolSimple({ ...arguments[0], code: "EYE" });
} else if (kvps._tool_name === "search_engine") {
return drawMessageToolSimple({ ...arguments[0], code: "WEB" });
} else if (kvps._tool_name === "browser_agent") {
return drawMessageToolSimple({ ...arguments[0], code: "WWW" });
} else if (kvps._tool_name.startsWith("memory_")) {
return drawMessageToolSimple({ ...arguments[0], code: "MEM" });
} else {
return drawMessageToolSimple({ ...arguments[0] });
}
/** @type {{ tool_name: string, kvps: any, handler: Function | undefined }} */
const extData = {
tool_name,
kvps,
handler: undefined,
};
await callJsExtensions("get_tool_message_handler", extData);
if (typeof extData.handler === "function") {
return extData.handler(arguments[0]);
}
return drawMessageToolSimple({ ...arguments[0] });
}
/**
@ -1204,48 +1210,6 @@ export function drawMessageToolSimple({
});
}
/**
* @param {MessageHandlerArgs & Record<string, any>} param0
* @returns {MessageHandlerResult}
*/
export function drawMessageBrowser({
id,
type,
heading,
content,
kvps,
timestamp,
agentno = 0,
...additional
}) {
const title = cleanStepTitle(heading);
let displayKvps = { ...kvps };
const answerText = String(kvps?.answer ?? "");
const actionButtons = answerText.trim()
? [
createActionButton("detail", "", () =>
stepDetailStore.showStepDetail(
buildDetailPayload(arguments[0], { headerLabels: [] }),
),
),
createActionButton("speak", "", () => speechStore.speak(answerText)),
createActionButton("copy", "", () => copyToClipboard(answerText)),
].filter(Boolean)
: [];
return drawProcessStep({
id,
title,
code: "WWW",
classes: undefined,
kvps: displayKvps,
content,
// contentClasses: [],
actionButtons,
log: arguments[0],
});
}
/**
* @param {MessageHandlerArgs & Record<string, any>} param0
* @returns {MessageHandlerResult}

View file

@ -1 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 104 50" width="104" height="50"><style>.a{fill:#7a7a7a}</style><path class="a" d="m31.9 40.7c-4.1-6.9-8.1-13.8-12.2-20.9-4.1 7.1-8.1 14-12.2 20.9h-6.2c6.1-10.5 18.4-31.4 18.4-31.4 0 0 12.3 20.8 18.4 31.4 0 0-6.2 0-6.2 0z"/><path class="a" d="m27.5 40.7h-15.8q1.6-2.7 3.1-5.3h9.7c1 1.7 2 3.5 3 5.3zm52.3-6.5c0.6 1 1.2 1.9 1.9 2.9h-2.1c-0.6-0.8-1.2-1.7-1.8-2.5h-2.4v2.5h-1.6v-8.6q1.4 0 2.8 0c0.7 0 1.5-0.1 2.3 0 1.2 0.2 2.1 0.9 2.4 2.1 0.2 1.3 0 2.5-1.2 3.4-0.1 0.1-0.2 0.1-0.3 0.2q0 0 0 0zm-4.4-1.3c1.1 0 2.2 0.1 3.2 0 0.7 0 1-0.6 1-1.3 0.1-0.7-0.3-1.3-1-1.3-1-0.1-2.1 0-3.2 0zm-22.1-4.5v1.3c-1.7 1.8-3.4 3.7-5.2 5.7h5.1v1.6h-7.3v-1.7c1.5-1.6 3-3.3 4.7-5.2h-4.7v-1.7zm29.2-15.7c2.1 2.1 4 4 6.1 6v-4.9h1.5v8.7c-2.1-2-4-3.9-6.1-6v5.2h-1.5v-9q0 0 0 0zm6.3 22.2c0.4-0.2 1-0.5 1.4-0.7 1.4 1.8 3.4 1.4 4.3 0.6 0.9-0.8 1.1-1.9 0.8-3.1-0.4-1.1-1.4-1.9-2.8-1.9-1.4 0.1-2.6 1.1-2.7 2.7h-1.7c0-1.6 1.2-3.5 3.1-4 2.3-0.7 4.6 0.4 5.5 2.7 0.8 2-0.2 4.4-2.3 5.4-2 1-4.5 0.3-5.6-1.7q0 0 0 0zm-26.2-19.9c-0.3 0.2-0.8 0.5-1.1 0.8-1.8-1.5-3.8-1-4.6 0.9-0.4 1.2 0.2 2.8 1.6 3.3 1.2 0.4 2.5 0 2.8-1-0.3-0.1-0.5-0.2-0.9-0.3 0-0.4-0.1-0.8-0.1-1.3h3.1c0.3 1.8-1.2 3.8-3 4.2-2 0.5-4.2-0.6-4.9-2.5-0.8-2 0.1-4.2 2-5.2 1.8-0.9 4-0.5 5.1 1.1zm-17.1-1.8c1.6 3 3 5.7 4.5 8.6h-1.7c-0.9-1.6-1.8-3.3-2.8-5-0.9 1.7-1.8 3.3-2.7 5h-1.8c1.5-2.8 3-5.6 4.5-8.6zm52.9 2h-2.9v-1.5h7.2v1.5h-2.7v6.6h-1.6v-6.6zm-36.3 14.8v2.1h2c0 0 0 1 0 1.6-0.8 0.1-1.7 0-2.5-0.1-0.7-0.2-1.2-0.8-1.2-1.5 0-1.2 0-2.5 0-3.7h7v1.6h-5.3q0 0 0 0zm-1.7 7v-2.2q0.7 0 1.3-0.1c0.1 0.3 0.1 0.7 0.3 0.7 0.3 0 3.5 0 5.3 0v1.6c0 0-6.9 0-6.9 0zm11-21.8v2h1.9c0 0 0 0.9 0 1.5-0.8 0-1.6 0-2.3-0.2-0.7-0.1-1.1-0.6-1.1-1.3-0.1-1.1-0.1-2.3-0.1-3.4h6.5v1.4h-4.9zm-1.5 6.5v-2q0.6 0 1.2-0.1c0.1 0.2 0.1 0.6 0.3 0.6 0.2 0 3.2 0 4.9 0v1.5h-6.4z"/></svg>
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 163 26" width="163" height="26"><style>.a{fill:#7a7a7a}</style><path class="a" d="m138.7 15.3c0.8 1.2 1.6 2.4 2.5 3.7h-2.7c-0.8-1-1.6-2.2-2.4-3.3h-3.1v3.3h-2.1v-11.3q1.8 0 3.6 0c1 0 2-0.1 3 0.1 1.6 0.2 2.8 1.1 3.2 2.7 0.3 1.7 0 3.3-1.5 4.4-0.2 0.1-0.3 0.2-0.5 0.4zm-5.7-1.7c1.5 0 2.8 0 4.2-0.1 0.9 0 1.3-0.7 1.3-1.6 0-1-0.4-1.7-1.3-1.8-1.4-0.2-2.8-0.1-4.2-0.1zm-29-5.8v1.6c-2.2 2.5-4.4 5-6.7 7.6h6.7v2.1h-9.6v-2.2c2-2.2 4-4.4 6.2-6.9h-6.2v-2.2zm-48.2-0.7c2.8 2.7 5.3 5.2 8 7.9v-6.5h2v11.4c-2.7-2.6-5.2-5.1-8-7.8v6.8h-2zm94.6 9.3c0.6-0.3 1.4-0.7 1.9-0.9 1.7 2.3 4.4 1.8 5.6 0.7 1.1-1 1.5-2.5 1-4-0.5-1.4-1.8-2.6-3.6-2.5-1.8 0.1-3.4 1.4-3.6 3.5h-2.1c0-2.1 1.5-4.6 3.9-5.3 3.1-0.9 6.1 0.6 7.3 3.5 1 2.8-0.2 5.9-3 7.2-2.6 1.3-6 0.3-7.4-2.2zm-120.5-6.3c-0.5 0.3-1.1 0.7-1.5 1-2.3-1.9-5-1.3-6 1.2-0.5 1.6 0.3 3.7 2.1 4.3 1.5 0.6 3.2 0 3.6-1.3-0.3-0.1-0.7-0.2-1.1-0.3-0.1-0.6-0.1-1.1-0.2-1.8h4.1c0.3 2.4-1.6 5-4 5.6-2.6 0.7-5.4-0.8-6.4-3.4-0.9-2.5 0.2-5.5 2.6-6.7 2.4-1.3 5.4-0.7 6.8 1.4zm-22.4-2.3c2 3.8 3.9 7.5 5.9 11.2h-2.3q-1.8-3.2-3.6-6.6c-1.3 2.3-2.4 4.4-3.6 6.6h-2.3c2-3.7 3.9-7.4 5.9-11.2zm69.1 2.5h-3.8v-1.9h9.5v1.9h-3.6v8.7h-2.1zm38.9-0.4v2.7l2.7 0.1c0 0 0 1.2 0 2.1-1.1 0-2.3 0-3.3-0.2-0.9-0.2-1.6-1-1.6-2-0.1-1.5 0-3.2 0-4.8h9.1v2.1zm-2.2 9.2v-2.9c0.5 0 1.1-0.1 1.7-0.1 0.1 0.3 0.1 0.9 0.4 0.9 0.3 0 4.5 0 6.9 0v2.1zm-71.9-8.7v2.6h2.5c0 0 0 1.2 0 2-1 0-2.1 0-3.1-0.2-0.8-0.2-1.4-0.9-1.5-1.8q0-2.2 0-4.5h8.5v1.9zm-2.1 8.5v-2.6c0.6-0.1 1.1-0.1 1.6-0.1 0.2 0.3 0.2 0.8 0.4 0.8 0.3 0 4.2 0 6.4 0v1.9z"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After