mermaid_cli/providers/tool/computer_use/
scroll.rs1use 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}