1use axum::extract::{Query, State};
25use axum::http::StatusCode;
26use axum::response::IntoResponse;
27use axum::Json;
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31use crate::server::DevServerState;
32
33#[derive(Debug, Deserialize)]
35pub struct ElementQuery {
36 pub selector: Option<String>,
38
39 pub text: Option<String>,
41
42 pub role: Option<String>,
44}
45
46#[derive(Debug, Deserialize, Serialize)]
48pub struct AgentAction {
49 pub action: ActionType,
51
52 pub selector: String,
54
55 pub value: Option<String>,
57
58 pub coordinates: Option<(f64, f64)>,
60}
61
62#[derive(Debug, Deserialize, Serialize)]
64#[serde(rename_all = "snake_case")]
65pub enum ActionType {
66 Click,
67 DoubleClick,
68 RightClick,
69 Type,
70 Fill,
71 Clear,
72 Scroll,
73 Hover,
74 Focus,
75 Select,
76}
77
78#[derive(Debug, Serialize)]
80pub struct DomSnapshot {
81 pub html: String,
83
84 pub url: String,
86
87 pub title: String,
89
90 pub timestamp: String,
92}
93
94#[derive(Debug, Serialize)]
96pub struct ElementInfo {
97 pub selector: String,
99
100 pub tag: String,
102
103 pub text: String,
105
106 pub attributes: HashMap<String, String>,
108
109 pub visible: bool,
111
112 pub enabled: bool,
114
115 pub role: Option<String>,
117
118 pub label: Option<String>,
120}
121
122#[derive(Debug, Serialize)]
124pub struct ActionResult {
125 pub success: bool,
127
128 pub error: Option<String>,
130
131 pub side_effects: Vec<String>,
133}
134
135pub async fn get_dom(
140 State(state): State<DevServerState>,
141) -> impl IntoResponse {
142 let snapshot = state.last_dom_snapshot.read().await;
143
144 let html = snapshot.clone().unwrap_or_else(|| {
145 r#"<!DOCTYPE html>
146<html>
147<head><title>rdesktop</title></head>
148<body>
149 <p>No DOM snapshot available yet. Make sure the app is loaded in the browser.</p>
150 <p>The bridge script will send DOM updates automatically.</p>
151</body>
152</html>"#
153 .to_string()
154 });
155
156 let dom = DomSnapshot {
157 html,
158 url: "http://localhost".to_string(),
159 title: "rdesktop App".to_string(),
160 timestamp: timestamp(),
161 };
162
163 Json(dom).into_response()
164}
165
166pub async fn query_elements(
170 State(state): State<DevServerState>,
171 Query(query): Query<ElementQuery>,
172) -> impl IntoResponse {
173 let snapshot = state.last_dom_snapshot.read().await;
174
175 let elements: Vec<ElementInfo> = if let Some(ref html) = *snapshot {
177 find_elements(html, &query)
178 } else {
179 vec![]
180 };
181
182 Json(serde_json::json!({
183 "query": {
184 "selector": query.selector,
185 "text": query.text,
186 "role": query.role,
187 },
188 "count": elements.len(),
189 "elements": elements,
190 }))
191 .into_response()
192}
193
194pub async fn execute_action(
199 State(_state): State<DevServerState>,
200 Json(action): Json<AgentAction>,
201) -> impl IntoResponse {
202 tracing::info!(
203 action = ?action.action,
204 selector = %action.selector,
205 "Agent action received"
206 );
207
208 let result = ActionResult {
215 success: true,
216 error: None,
217 side_effects: vec![format!("Action {:?} on '{}' queued", action.action, action.selector)],
218 };
219
220 Json(result).into_response()
221}
222
223pub async fn get_state(
227 State(state): State<DevServerState>,
228) -> impl IntoResponse {
229 let app_state = state.last_app_state.read().await;
230
231 match app_state.as_ref() {
232 Some(state) => Json(state.clone()).into_response(),
233 None => Json(serde_json::json!({
234 "message": "No application state available yet.",
235 "hint": "Use fetch('/__rdesktop__/state', { method: 'POST', body: JSON.stringify(state) }) from your app."
236 }))
237 .into_response(),
238 }
239}
240
241pub async fn send_ipc(
245 State(_state): State<DevServerState>,
246 Json(message): Json<serde_json::Value>,
247) -> impl IntoResponse {
248 let cmd = message["cmd"].as_str().unwrap_or("unknown");
249 let payload = message["payload"].clone();
250 let id = message["id"].as_str().unwrap_or("0");
251
252 tracing::info!(cmd = cmd, "Agent IPC message received");
253
254 let response = match cmd {
257 "greet" => {
258 let name = payload["name"].as_str().unwrap_or("World");
259 serde_json::json!({
260 "id": id,
261 "success": true,
262 "data": { "message": format!("Hello, {}!", name) }
263 })
264 }
265 "ping" => {
266 serde_json::json!({
267 "id": id,
268 "success": true,
269 "data": { "pong": true }
270 })
271 }
272 _ => {
273 serde_json::json!({
274 "id": id,
275 "success": false,
276 "data": { "error": format!("Unknown command: {}", cmd) }
277 })
278 }
279 };
280
281 Json(response).into_response()
282}
283
284pub async fn take_screenshot(
288 State(_state): State<DevServerState>,
289) -> impl IntoResponse {
290 (
291 StatusCode::NOT_IMPLEMENTED,
292 Json(serde_json::json!({
293 "message": "Screenshot not implemented in browser mode.",
294 "hint": "Use Playwright's page.screenshot() directly."
295 })),
296 )
297 .into_response()
298}
299
300fn timestamp() -> String {
302 let now = std::time::SystemTime::now()
303 .duration_since(std::time::UNIX_EPOCH)
304 .unwrap_or_default();
305 format!("{}", now.as_secs())
306}
307
308fn find_elements(html: &str, query: &ElementQuery) -> Vec<ElementInfo> {
311 let mut elements = vec![];
312
313 if let Some(ref selector) = query.selector {
314 let tag = selector.trim_start_matches('<').trim_end_matches('>');
316 let open_tag = format!("<{}", tag);
317
318 let mut start = 0;
319 while let Some(pos) = html[start..].find(&open_tag) {
320 let abs_pos = start + pos;
321 let end = html[abs_pos..].find('>').unwrap_or(0);
322 let _tag_content = &html[abs_pos..abs_pos + end + 1];
323
324 let close_tag = format!("</{}>", tag);
326 let text_start = abs_pos + end + 1;
327 let text = if let Some(text_end) = html[text_start..].find(&close_tag) {
328 html[text_start..text_start + text_end].trim().to_string()
329 } else {
330 String::new()
331 };
332
333 elements.push(ElementInfo {
334 selector: format!("{}:nth-of-type({})", tag, elements.len() + 1),
335 tag: tag.to_string(),
336 text,
337 attributes: HashMap::new(),
338 visible: true,
339 enabled: true,
340 role: None,
341 label: None,
342 });
343
344 start = abs_pos + end + 1;
345 }
346 }
347
348 if let Some(ref text_query) = query.text {
349 let lower_html = html.to_lowercase();
351 let lower_query = text_query.to_lowercase();
352 if lower_html.contains(&lower_query) {
353 elements.push(ElementInfo {
354 selector: format!("*:contains(\"{}\")", text_query),
355 tag: "*".to_string(),
356 text: text_query.clone(),
357 attributes: HashMap::new(),
358 visible: true,
359 enabled: true,
360 role: None,
361 label: None,
362 });
363 }
364 }
365
366 elements
367}