1use super::*;
7
8pub(super) async fn call(name: &str, arguments: &Value, cx: &ToolCtx<'_>) -> Result<String> {
9 let ToolCtx { local_url, auth_key, block_id, client, .. } = *cx;
10 match name {
11 "UIScreenshot" => {
12 require_agent_env(local_url, auth_key, block_id)?;
13 let auth = sign_ui_automation_auth()?;
14 let url = format!("{}/api/v1/ui/screenshot", local_url.trim_end_matches('/'));
15 let resp = client
16 .post(&url)
17 .header(AUTH_KEY_HEADER, auth_key)
18 .json(&UiScreenshotRequest { auth, pane: pane_arg(arguments) })
19 .send()
20 .await
21 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
22 if !resp.status().is_success() {
23 let status = resp.status();
24 let text = resp.text().await.unwrap_or_default();
25 anyhow::bail!("screenshot failed: HTTP {status} — {text}");
26 }
27 let result: UiScreenshotResponse = resp
28 .json()
29 .await
30 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
31 Ok(format!(
32 "Screenshot saved to {} — use Read on that path to view it yourself, or OpenMedia to show it to the user.",
33 result.path
34 ))
35 }
36 "CaptureWindow" => {
37 let title_contains = arguments
45 .get("title_contains")
46 .and_then(|v| v.as_str())
47 .map(|s| s.to_string());
48 let index = arguments
49 .get("index")
50 .and_then(|v| v.as_u64())
51 .map(|v| v as usize);
52 let pid = arguments
53 .get("pid")
54 .and_then(|v| v.as_u64())
55 .map(|v| v as u32);
56
57 let query_desc = match (pid, title_contains.as_deref()) {
58 (Some(p), _) => format!("pid={p}"),
59 (None, Some(t)) => format!("title_contains={t:?}"),
60 (None, None) => "<no target given>".to_string(),
61 };
62
63 let mut resolved: Option<(CaptureTier, String)> = None;
76 let outcome = capture_window_impl(title_contains.as_deref(), index, pid, &mut resolved);
77 audit_log_capture_window(&query_desc, &outcome, &resolved);
78 return outcome.map(|c| c.message);
79 }
80 "DiscoverWindows" => {
81 let include_self = arguments
82 .get("include_self")
83 .and_then(|v| v.as_bool())
84 .unwrap_or(false);
85 let include_foreign = arguments
91 .get("include_foreign")
92 .and_then(|v| v.as_bool())
93 .unwrap_or(false);
94 let windows = enumerate_agentmux_windows()?;
95 let list: Vec<Value> = windows
96 .iter()
97 .filter(|w| include_self || !w.is_self)
106 .filter(|w| include_foreign || w.is_agentmux)
107 .map(window_listing_entry)
108 .collect();
109 audit_log_discover_windows(include_self, include_foreign, &list);
114 return Ok(serde_json::to_string_pretty(&json!({ "windows": list }))
115 .unwrap_or_else(|_| "{\"windows\":[]}".to_string()));
116 }
117 "UIClick" => {
118 require_agent_env(local_url, auth_key, block_id)?;
119 let auth = sign_ui_automation_auth()?;
120 let selector = arguments
121 .get("selector")
122 .and_then(|v| v.as_str())
123 .ok_or_else(|| anyhow::anyhow!("missing required parameter: selector"))?;
124 let url = format!("{}/api/v1/ui/click", local_url.trim_end_matches('/'));
125 let resp = client
126 .post(&url)
127 .header(AUTH_KEY_HEADER, auth_key)
128 .json(&UiClickRequest {
129 auth,
130 pane: pane_arg(arguments),
131 selector: selector.to_string(),
132 })
133 .send()
134 .await
135 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
136 if !resp.status().is_success() {
137 let status = resp.status();
138 let text = resp.text().await.unwrap_or_default();
139 anyhow::bail!("click failed: HTTP {status} — {text}");
140 }
141 Ok(format!("Clicked {selector:?}"))
142 }
143 "UIQuery" => {
144 require_agent_env(local_url, auth_key, block_id)?;
145 let auth = sign_ui_automation_auth()?;
146 let selector = arguments
147 .get("selector")
148 .and_then(|v| v.as_str())
149 .ok_or_else(|| anyhow::anyhow!("missing required parameter: selector"))?;
150 let limit = arguments.get("limit").and_then(|v| v.as_u64()).map(|n| n as u32);
151 let url = format!("{}/api/v1/ui/query", local_url.trim_end_matches('/'));
152 let resp = client
153 .post(&url)
154 .header(AUTH_KEY_HEADER, auth_key)
155 .json(&UiQueryRequest {
156 auth,
157 pane: pane_arg(arguments),
158 selector: selector.to_string(),
159 limit,
160 })
161 .send()
162 .await
163 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
164 if !resp.status().is_success() {
165 let status = resp.status();
166 let text = resp.text().await.unwrap_or_default();
167 anyhow::bail!("query failed: HTTP {status} — {text}");
168 }
169 let body: Value = resp
170 .json()
171 .await
172 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
173 let matches = body
174 .get("data")
175 .and_then(|d| d.get("matches"))
176 .cloned()
177 .unwrap_or_else(|| json!([]));
178 Ok(serde_json::to_string_pretty(&matches).unwrap_or_else(|_| matches.to_string()))
179 }
180 "ListShortcuts" => {
181 require_agent_env(local_url, auth_key, block_id)?;
182 let auth = sign_ui_automation_auth()?;
183 let data = post_shortcuts(cx, "list", &UiShortcutsListRequest { auth }).await?;
184 let list = data.get("shortcuts").cloned().unwrap_or_else(|| json!([]));
185 Ok(serde_json::to_string_pretty(&list).unwrap_or_else(|_| list.to_string()))
186 }
187 "RunCommand" => {
188 require_agent_env(local_url, auth_key, block_id)?;
189 let auth = sign_ui_automation_auth()?;
190 let command = arguments
191 .get("command")
192 .and_then(|v| v.as_str())
193 .ok_or_else(|| anyhow::anyhow!("missing required parameter: command"))?;
194 let req = UiRunCommandRequest { auth, command: command.to_string(), target: str_arg(arguments, "target") };
195 let data = post_shortcuts(cx, "run", &req).await?;
196 if data.get("ran").and_then(|v| v.as_bool()) == Some(true) {
197 Ok(format!("Ran {command}."))
198 } else if let Some(pending) = data.get("pending") {
199 let why = data.get("reason").and_then(|v| v.as_str()).unwrap_or("waiting for the user");
201 let id = pending.get("request_id").and_then(|v| v.as_str()).unwrap_or("?");
202 Ok(format!("{command} is pending: {why} (request {id})."))
203 } else {
204 let why = data.get("reason").and_then(|v| v.as_str()).unwrap_or("no reason given");
205 Ok(format!("Didn't run {command}: {why}"))
206 }
207 }
208 "PressKeys" => {
209 require_agent_env(local_url, auth_key, block_id)?;
210 let auth = sign_ui_automation_auth()?;
211 let keys = arguments
212 .get("keys")
213 .and_then(|v| v.as_str())
214 .ok_or_else(|| anyhow::anyhow!("missing required parameter: keys"))?;
215 let req = UiPressKeysRequest { auth, keys: keys.to_string(), target: str_arg(arguments, "target") };
216 let data = post_shortcuts(cx, "press", &req).await?;
217 Ok(serde_json::to_string_pretty(&data).unwrap_or_else(|_| data.to_string()))
218 }
219 _ => Err(not_in_family()),
220 }
221}
222
223fn str_arg(arguments: &Value, name: &str) -> Option<String> {
224 arguments
225 .get(name)
226 .and_then(|v| v.as_str())
227 .map(str::trim)
228 .filter(|s| !s.is_empty())
229 .map(str::to_string)
230}
231
232async fn post_shortcuts<T: serde::Serialize>(cx: &ToolCtx<'_>, route: &str, body: &T) -> Result<Value> {
234 let url = format!("{}/api/v1/ui/shortcuts/{route}", cx.local_url.trim_end_matches('/'));
235 let resp = cx
236 .client
237 .post(&url)
238 .header(AUTH_KEY_HEADER, cx.auth_key)
239 .json(body)
240 .send()
241 .await
242 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
243 let status = resp.status();
244 let body: Value = resp.json().await.unwrap_or(Value::Null);
245 if !status.is_success() || body.get("ok").and_then(|v| v.as_bool()) != Some(true) {
246 let err = body.get("error").and_then(|v| v.as_str()).unwrap_or("no error message");
247 anyhow::bail!("shortcuts/{route} failed: HTTP {status} — {err}");
248 }
249 Ok(body.get("data").cloned().unwrap_or(Value::Null))
250}