Skip to main content

mermaid_cli/providers/tool/computer_use/
scroll.rs

1//! `scroll` — wheel scroll in "up" or "down" direction. Amount is
2//! clamped to `[1, MAX_SCROLL_AMOUNT]` so a runaway model can't
3//! request a million ticks (would blow ARG_MAX building xdotool's
4//! argv on the X11 path).
5
6use std::sync::Arc;
7use std::time::Instant;
8
9use async_trait::async_trait;
10use serde_json::Value;
11
12use crate::constants::MAX_SCROLL_AMOUNT;
13use crate::domain::{ToolDefinition, ToolOutcome};
14use crate::providers::ctx::ExecContext;
15
16use super::super::ToolExecutor;
17use super::computer_use_success;
18use super::driver::ComputerUseDriver;
19
20pub struct ScrollTool {
21    driver: Arc<ComputerUseDriver>,
22}
23
24impl ScrollTool {
25    pub fn new(driver: Arc<ComputerUseDriver>) -> Self {
26        Self { driver }
27    }
28}
29
30#[async_trait]
31impl ToolExecutor for ScrollTool {
32    fn name(&self) -> &'static str {
33        "scroll"
34    }
35
36    fn schema(&self) -> ToolDefinition {
37        ToolDefinition {
38            name: "scroll".to_string(),
39            description: "Scroll the focused window. `direction` is 'up' or 'down'; `amount` is \
40                 clamped to 1..=100 wheel ticks. Scrolls in the currently-focused window; \
41                 click on the scroll target first if it isn't focused."
42                .to_string(),
43            input_schema: serde_json::json!({
44                "type": "object",
45                "properties": {
46                    "direction": { "type": "string", "enum": ["up", "down"] },
47                    "amount": { "type": "integer", "minimum": 1, "maximum": MAX_SCROLL_AMOUNT }
48                },
49                "required": ["direction", "amount"]
50            }),
51        }
52    }
53
54    async fn execute(&self, args: Value, ctx: ExecContext) -> ToolOutcome {
55        let started = Instant::now();
56        if let Err(error) = self.driver.ensure_alive() {
57            return ToolOutcome::error(error, started.elapsed().as_secs_f64());
58        }
59        if let Some(blocked) = super::super::policy_gate::gate_external(
60            &ctx,
61            "scroll",
62            crate::runtime::ToolCategory::ComputerUse,
63            "computer-use: scroll".to_string(),
64            &args,
65        )
66        .await
67        {
68            return blocked;
69        }
70
71        let direction = args
72            .get("direction")
73            .and_then(|v| v.as_str())
74            .unwrap_or("down")
75            .to_string();
76        if direction != "up" && direction != "down" {
77            return ToolOutcome::error(
78                format!(
79                    "scroll: direction must be 'up' or 'down', got '{}'",
80                    direction
81                ),
82                started.elapsed().as_secs_f64(),
83            );
84        }
85        let requested = args
86            .get("amount")
87            .and_then(|v| v.as_i64())
88            .map(|n| n as i32)
89            .unwrap_or(3);
90        let amount = requested.clamp(1, MAX_SCROLL_AMOUNT);
91
92        let res = tokio::select! {
93            biased;
94            _ = ctx.token.cancelled() => return ToolOutcome::cancelled(),
95            r = self.driver.scroll(&direction, amount, &ctx.token) => r,
96        };
97        if let Err(e) = res {
98            return ToolOutcome::error(
99                format!("scroll failed: {}", e),
100                started.elapsed().as_secs_f64(),
101            );
102        }
103
104        let clamp_note = if requested != amount {
105            format!(" (clamped from {} to {})", requested, amount)
106        } else {
107            String::new()
108        };
109        computer_use_success(
110            "scroll",
111            args,
112            format!("Scrolled {} by {}{}", direction, amount, clamp_note),
113            started.elapsed().as_secs_f64(),
114        )
115    }
116}