browsing 0.1.7

Browser automation: navigate, click, extract, screenshot. Standalone browser control via CDP.
Documentation
//! Interaction action handlers

use super::Handler;
use crate::agent::views::ActionResult;
use crate::error::{BrowsingError, Result};
use crate::tools::resolver::{ElementResolver, ResolutionStrategy};
use crate::tools::views::{ActionContext, ActionParams};
use async_trait::async_trait;
use tracing::info;

/// Handler for user interaction actions
/// Handles click, input, and send_keys operations
pub struct InteractionHandler;

#[async_trait]
impl Handler for InteractionHandler {
    async fn handle(
        &self,
        params: &ActionParams<'_>,
        context: &mut ActionContext<'_>,
    ) -> Result<ActionResult> {
        match params.get_action_type().unwrap_or("unknown") {
            "click" => self.click(params, context).await,
            "input" => self.input(params, context).await,
            "send_keys" => self.send_keys(params, context).await,
            _ => Err(BrowsingError::Tool("Unknown interaction action".into())),
        }
    }
}

impl InteractionHandler {
    async fn click(
        &self,
        params: &ActionParams<'_>,
        context: &mut ActionContext<'_>,
    ) -> Result<ActionResult> {
        let index = params.get_required_u32("index")?;

        let page = context.browser.get_page()?;
        let resolver = ElementResolver::new();
        let result = resolver.resolve(index, context.selector_map, &page).await?;

        let element = page.get_element(result.backend_node_id).await;
        element
            .click(crate::actor::mouse::MouseButton::Left, 1, None)
            .await?;

        let strategy_label = match result.strategy {
            ResolutionStrategy::Index => "index",
            ResolutionStrategy::Selector => "selector",
            ResolutionStrategy::TextMatch => "text-match",
            ResolutionStrategy::SemanticRole => "semantic-role",
            ResolutionStrategy::JavaScript => "javascript",
        };

        let memory = format!(
            "Clicked element {} via {} (backend_node_id: {}, confidence: {:.0}%)",
            index,
            strategy_label,
            result.backend_node_id,
            result.confidence * 100.0
        );
        info!("🖱️ {}", memory);
        Ok(ActionResult::success_with_memory(memory))
    }

    async fn input(
        &self,
        params: &ActionParams<'_>,
        context: &mut ActionContext<'_>,
    ) -> Result<ActionResult> {
        let index = params.get_required_u32("index")?;
        let text = params.get_required_str("text")?;

        let page = context.browser.get_page()?;
        let resolver = ElementResolver::new();
        let result = resolver.resolve(index, context.selector_map, &page).await?;

        let element = page.get_element(result.backend_node_id).await;
        element.fill(text).await?;

        let strategy_label = match result.strategy {
            ResolutionStrategy::Index => "index",
            ResolutionStrategy::Selector => "selector",
            ResolutionStrategy::TextMatch => "text-match",
            ResolutionStrategy::SemanticRole => "semantic-role",
            ResolutionStrategy::JavaScript => "javascript",
        };

        let memory = format!(
            "Input text into element {} via {} (backend_node_id: {}, confidence: {:.0}%)",
            index,
            strategy_label,
            result.backend_node_id,
            result.confidence * 100.0
        );
        info!("⌨️ {}", memory);
        Ok(ActionResult::success_with_memory(memory))
    }

    async fn send_keys(
        &self,
        params: &ActionParams<'_>,
        context: &mut ActionContext<'_>,
    ) -> Result<ActionResult> {
        let keys = params.get_required_str("keys")?;
        let page = context.browser.get_page()?;

        for key in keys.split_whitespace() {
            page.press(key).await?;
            tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
        }

        let memory = format!("Sent keys: {}", keys);
        info!("⌨️ {}", memory);
        Ok(ActionResult::success_with_memory(memory))
    }
}