Files
gyandoot-v2/crates/gyandoot-browser/src/snapshot.rs
Tara Menon 605747023b Add camofox-browser integration: gyandoot-browser crate + agent tools
New crate: crates/gyandoot-browser/
  - client.rs: Full REST client for camofox-browser API (health, tabs,
    navigation, snapshots, click/type/scroll, search macros)
  - macros.rs: 14 SearchMacro enum (Google, YouTube, Amazon, Reddit,
    Wikipedia, Twitter, Yelp, Spotify, Netflix, LinkedIn, Instagram,
    TikTok, Twitch) with parse/display
  - snapshot.rs: PageSnapshot, PageLinks, NavigationResult, HealthResponse
  - session.rs: BrowserSession, SessionManager with idle cleanup

New module: crates/gyandoot-agent/src/browser_tools.rs
  - WebSearchTool: Search the web using anti-detection browser
  - WebBrowseTool: Browse URLs, click, type, scroll with element refs
  - register_browser_tools(): Wire both tools into ToolExecutor

CLI: Added
                                 o
                                 |
               - - - - - -     .---.     - - - - - -
          - - - - - - - - -  [ ^   ^ ]  - - - - - - - - -
             - - - - - - -   |  ___  |   - - - - - - -
                              `-----'
           ____ __   __  ___   _   _  ____   ___   ___  _____
          / ___|\ \ / / / _ \ | \ | ||  _ \ / _ \ / _ \|_   _|
         | |  _  \ V / / /_\ \|  \| || | | | | | | | | | | |
         | |_| |  | |  |  _  || |\  || |_| | |_| | |_| | | |
          \____/  |_|  |_| |_||_| \_||____/ \___/ \___/  |_|

        o - - - - - - -  knowledge. delivered. - - - - - - - o

Unknown command: browser
Run 'gyandoot help' for usage. subcommand with health/search/browse actions

Installer: Updated install.sh with camofox-browser cloning, Node.js
detection, npm dependency install, and systemd service setup

Camofox-browser running at http://localhost:9377 (Docker, verified working)

Workspace: 12 crates + CLI binary — cargo check passes with 0 errors.
2026-06-11 20:23:35 +05:30

180 lines
5.3 KiB
Rust

//! Types representing accessibility tree snapshots from camofox-browser.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// A page snapshot returned by the camofox-browser API.
///
/// The snapshot is an accessibility tree with stable element refs
/// (e.g., `e1`, `e2`) that can be used for clicking and typing.
/// This format is ~90% more token-efficient than raw HTML.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageSnapshot {
/// Accessibility tree text with element refs.
pub snapshot: String,
/// Optional URL of the page.
#[serde(default)]
pub url: String,
/// Optional page title.
#[serde(default)]
pub title: String,
/// Optional screenshot as base64 PNG.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub screenshot: Option<String>,
/// Element reference map: ref -> element info.
#[serde(default)]
pub elements: HashMap<String, ElementInfo>,
}
/// Information about a single element in the snapshot.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElementInfo {
/// The element's tag name (e.g., "a", "button", "input").
#[serde(default)]
pub tag: String,
/// Visible text content.
#[serde(default)]
pub text: String,
/// URL for links.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub href: Option<String>,
/// Whether the element is interactive.
#[serde(default)]
pub interactive: bool,
}
impl PageSnapshot {
/// Create a new snapshot from raw text.
pub fn new(snapshot: String) -> Self {
Self {
snapshot,
url: String::new(),
title: String::new(),
screenshot: None,
elements: HashMap::new(),
}
}
/// Whether the snapshot is empty (no content).
pub fn is_empty(&self) -> bool {
self.snapshot.is_empty()
}
/// Number of characters in the snapshot text.
pub fn len(&self) -> usize {
self.snapshot.len()
}
/// Extract all element refs from the snapshot text.
///
/// Refs look like `[e1]`, `[e2]` in the accessibility tree.
pub fn extract_refs(&self) -> Vec<String> {
let mut refs = Vec::new();
for line in self.snapshot.lines() {
// Pattern: [eN] or eN
if let Some(start) = line.find("[e") {
let end = line[start + 2..].find(']').map(|i| start + 2 + i);
if let Some(end_pos) = end {
let ref_str = &line[start + 1..start + 1 + end_pos + 1];
refs.push(ref_str.to_string());
}
}
}
refs.sort();
refs.dedup();
refs
}
}
/// Links extracted from a page.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageLinks {
/// List of links on the page.
pub links: Vec<LinkItem>,
/// Total count of links.
pub total: usize,
}
/// A single link item.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkItem {
/// Link text.
pub text: String,
/// Target URL.
pub href: String,
/// Optional title attribute.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
/// Response from page navigation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NavigationResult {
/// Final URL after navigation.
pub url: String,
/// Page title.
pub title: String,
/// Whether navigation succeeded.
pub ok: bool,
}
/// Response from element interaction (click/type).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InteractionResult {
/// Whether the interaction succeeded.
pub ok: bool,
/// New snapshot after interaction (if available).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snapshot: Option<PageSnapshot>,
}
/// Health check response from camofox-browser.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthResponse {
/// Server is OK.
pub ok: bool,
/// Engine type (e.g., "camoufox").
pub engine: String,
/// Whether browser is connected.
#[serde(default)]
pub browser_connected: bool,
/// Whether browser is running.
#[serde(default)]
pub browser_running: bool,
/// Number of active tabs.
#[serde(default)]
pub active_tabs: u32,
/// Number of active sessions.
#[serde(default)]
pub active_sessions: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_refs_from_snapshot() {
let snap = PageSnapshot::new(
"- heading \"Example\" [level=1]\n- link \"Click\" [e1]:\n - /url: https://example.com\n- paragraph: text [e2]\n".into(),
);
let refs = snap.extract_refs();
assert!(refs.contains(&"[e1]".to_string()));
assert!(refs.contains(&"[e2]".to_string()));
}
#[test]
fn test_empty_snapshot() {
let snap = PageSnapshot::new(String::new());
assert!(snap.is_empty());
assert_eq!(snap.len(), 0);
}
#[test]
fn test_health_response_deserialize() {
let json = r#"{"ok":true,"engine":"camoufox","browserConnected":true,"browserRunning":true,"activeTabs":3,"activeSessions":2}"#;
let health: HealthResponse = serde_json::from_str(json).unwrap();
assert!(health.ok);
assert_eq!(health.active_tabs, 3);
}
}