use super::manager::BrowserManager;
use crate::brain::tools::error::Result;
use crate::brain::tools::r#trait::{Tool, ToolCapability, ToolExecutionContext, ToolResult};
use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;
pub struct BrowserNavigateTool {
manager: Arc<BrowserManager>,
}
impl BrowserNavigateTool {
pub fn new(manager: Arc<BrowserManager>) -> Self {
Self { manager }
}
fn urls_equivalent(a: &str, b: &str) -> bool {
let strip = |u: &str| -> String {
let mut s = u.split('#').next().unwrap_or(u).to_string();
if s.ends_with('/') && s.matches('/').count() > 3 {
s.pop();
}
s
};
strip(a) == strip(b)
}
}
#[async_trait]
impl Tool for BrowserNavigateTool {
fn name(&self) -> &str {
"browser_navigate"
}
fn description(&self) -> &str {
"Open a URL in a real browser (Chrome DevTools Protocol). Returns \
page title, final URL after redirects, and an automatic \
screenshot. Supports headless and headed mode. \
\n\nUSE THIS ONLY when one of these is true: \
(1) the user explicitly asks to open / view / interact with a \
page in a browser; \
(2) the task requires interaction the search tools cannot do \
(click, type, submit a form, scroll, screenshot live DOM, run \
JavaScript against the page); \
(3) last resort — every search route (`exa_search`, \
`brave_search`, `web_search`, and for GitHub the `gh` CLI \
via `bash`) has been tried and could not surface the needed \
information. \
\n\nDO NOT use for research, reading articles, fetching \
documentation, checking package versions, looking up Stack \
Overflow answers, or any GitHub operation (issues, PRs, \
releases, comments, file contents, search) — those route \
through the search tools or `gh` CLI. Browser is slow, \
visible to the user (steals window focus in headed mode), \
and consumes far more tokens than a search call."
}
fn input_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "URL to navigate to"
},
"headless": {
"type": "boolean",
"description": "Run in headless mode (no visible window). Defaults to true. Set to false to see the browser."
}
},
"required": ["url"]
})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![ToolCapability::Network]
}
fn requires_approval(&self) -> bool {
true
}
async fn execute(&self, input: Value, context: &ToolExecutionContext) -> Result<ToolResult> {
let url = match input["url"].as_str() {
Some(u) if !u.is_empty() => u,
_ => return Ok(ToolResult::error("'url' is required".into())),
};
if let Some(headless) = input["headless"].as_bool() {
self.manager.set_headless(headless).await;
}
let page = match self
.manager
.get_or_create_session_page(context.session_id)
.await
{
Ok(p) => p,
Err(e) => return Ok(ToolResult::error(format!("Browser error: {e}"))),
};
if let Ok(Some(current)) = page.url().await
&& Self::urls_equivalent(¤t, url)
{
return Ok(ToolResult::error(format!(
"Already on '{url}' — re-navigating to the same URL won't change \
anything. If the previous page state is stale, use `browser_eval` \
with `\"window.location.reload()\"`. Otherwise take a different \
action: click, type, or navigate to a different URL."
)));
}
if let Err(e) = page.goto(url).await {
return Ok(ToolResult::error(format!("Navigation failed: {e}")));
}
if let Err(e) = page
.wait_for_network_almost_idle_with_timeout(std::time::Duration::from_secs(3))
.await
{
tracing::debug!(
"browser_navigate: network-idle wait timed out after goto({url}) (proceeding anyway): {e}"
);
}
let title = match page.get_title().await {
Ok(t) => t.unwrap_or_default(),
Err(e) => {
tracing::warn!("browser_navigate: get_title failed after goto({url}): {e}");
String::new()
}
};
let final_url = match page.url().await {
Ok(u) => u.unwrap_or_default(),
Err(e) => {
tracing::warn!("browser_navigate: page.url() failed after goto({url}): {e}");
String::new()
}
};
let mut result = ToolResult::success(format!("Navigated to: {final_url}\nTitle: {title}"));
self.manager
.attach_screenshot(context.session_id, &mut result)
.await;
Ok(result)
}
}