use crate::color; use crate::connection::Response; pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) { if json_mode { println!("{}", serde_json::to_string(resp).unwrap_or_default()); return; } if !resp.success { eprintln!( "{} {}", color::error_indicator(), resp.error.as_deref().unwrap_or("Unknown error") ); return; } if let Some(data) = &resp.data { // Navigation response if let Some(url) = data.get("url").and_then(|v| v.as_str()) { if let Some(title) = data.get("title").and_then(|v| v.as_str()) { println!("{} {}", color::success_indicator(), color::bold(title)); println!(" {}", color::dim(url)); if let Some(warning) = data.get("warning").and_then(|v| v.as_str()) { println!("{} {}", color::warning_indicator(), warning); } return; } println!("{}", url); return; } // Diff responses -- route by action to avoid fragile shape probing if let Some(obj) = data.as_object() { match action { Some("diff_snapshot") => { print_snapshot_diff(obj); return; } Some("diff_screenshot") => { print_screenshot_diff(obj); return; } Some("diff_url") => { if let Some(snap_data) = obj.get("snapshot").and_then(|v| v.as_object()) { println!("{}", color::bold("Snapshot diff:")); print_snapshot_diff(snap_data); } if let Some(ss_data) = obj.get("screenshot").and_then(|v| v.as_object()) { println!("\n{}", color::bold("Screenshot diff:")); print_screenshot_diff(ss_data); } return; } _ => {} } } // Snapshot if let Some(snapshot) = data.get("snapshot").and_then(|v| v.as_str()) { println!("{}", snapshot); return; } // Title if let Some(title) = data.get("title").and_then(|v| v.as_str()) { println!("{}", title); return; } // Text if let Some(text) = data.get("text").and_then(|v| v.as_str()) { println!("{}", text); return; } // HTML if let Some(html) = data.get("html").and_then(|v| v.as_str()) { println!("{}", html); return; } // Value if let Some(value) = data.get("value").and_then(|v| v.as_str()) { println!("{}", value); return; } // Count if let Some(count) = data.get("count").and_then(|v| v.as_i64()) { println!("{}", count); return; } // Boolean results if let Some(visible) = data.get("visible").and_then(|v| v.as_bool()) { println!("{}", visible); return; } if let Some(enabled) = data.get("enabled").and_then(|v| v.as_bool()) { println!("{}", enabled); return; } if let Some(checked) = data.get("checked").and_then(|v| v.as_bool()) { println!("{}", checked); return; } // Eval result if let Some(result) = data.get("result") { println!( "{}", serde_json::to_string_pretty(result).unwrap_or_default() ); return; } // iOS Devices if let Some(devices) = data.get("devices").and_then(|v| v.as_array()) { if devices.is_empty() { println!("No iOS devices available. Open Xcode to download simulator runtimes."); return; } // Separate real devices from simulators let real_devices: Vec<_> = devices .iter() .filter(|d| { d.get("isRealDevice") .and_then(|v| v.as_bool()) .unwrap_or(false) }) .collect(); let simulators: Vec<_> = devices .iter() .filter(|d| { !d.get("isRealDevice") .and_then(|v| v.as_bool()) .unwrap_or(false) }) .collect(); if !real_devices.is_empty() { println!("Connected Devices:\n"); for device in real_devices.iter() { let name = device .get("name") .and_then(|v| v.as_str()) .unwrap_or("Unknown"); let runtime = device.get("runtime").and_then(|v| v.as_str()).unwrap_or(""); let udid = device.get("udid").and_then(|v| v.as_str()).unwrap_or(""); println!(" {} {} ({})", color::green("●"), name, runtime); println!(" {}", color::dim(udid)); } println!(); } if !simulators.is_empty() { println!("Simulators:\n"); for device in simulators.iter() { let name = device .get("name") .and_then(|v| v.as_str()) .unwrap_or("Unknown"); let runtime = device.get("runtime").and_then(|v| v.as_str()).unwrap_or(""); let state = device .get("state") .and_then(|v| v.as_str()) .unwrap_or("Unknown"); let udid = device.get("udid").and_then(|v| v.as_str()).unwrap_or(""); let state_indicator = if state == "Booted" { color::green("●") } else { color::dim("○") }; println!(" {} {} ({})", state_indicator, name, runtime); println!(" {}", color::dim(udid)); } } return; } // Tabs if let Some(tabs) = data.get("tabs").and_then(|v| v.as_array()) { for (i, tab) in tabs.iter().enumerate() { let title = tab .get("title") .and_then(|v| v.as_str()) .unwrap_or("Untitled"); let url = tab.get("url").and_then(|v| v.as_str()).unwrap_or(""); let active = tab.get("active").and_then(|v| v.as_bool()).unwrap_or(false); let marker = if active { color::cyan("→") } else { " ".to_string() }; println!("{} [{}] {} - {}", marker, i, title, url); } return; } // Console logs if let Some(logs) = data.get("messages").and_then(|v| v.as_array()) { for log in logs { let level = log.get("type").and_then(|v| v.as_str()).unwrap_or("log"); let text = log.get("text").and_then(|v| v.as_str()).unwrap_or(""); println!("{} {}", color::console_level_prefix(level), text); } return; } // Errors if let Some(errors) = data.get("errors").and_then(|v| v.as_array()) { for err in errors { let msg = err.get("message").and_then(|v| v.as_str()).unwrap_or(""); println!("{} {}", color::error_indicator(), msg); } return; } // Cookies if let Some(cookies) = data.get("cookies").and_then(|v| v.as_array()) { for cookie in cookies { let name = cookie.get("name").and_then(|v| v.as_str()).unwrap_or(""); let value = cookie.get("value").and_then(|v| v.as_str()).unwrap_or(""); println!("{}={}", name, value); } return; } // Network requests if let Some(requests) = data.get("requests").and_then(|v| v.as_array()) { if requests.is_empty() { println!("No requests captured"); } else { for req in requests { let method = req.get("method").and_then(|v| v.as_str()).unwrap_or("GET"); let url = req.get("url").and_then(|v| v.as_str()).unwrap_or(""); let resource_type = req .get("resourceType") .and_then(|v| v.as_str()) .unwrap_or(""); println!("{} {} ({})", method, url, resource_type); } } return; } // Cleared requests if let Some(cleared) = data.get("cleared").and_then(|v| v.as_bool()) { if cleared { println!("{} Request log cleared", color::success_indicator()); return; } } // Bounding box if let Some(box_data) = data.get("box") { println!( "{}", serde_json::to_string_pretty(box_data).unwrap_or_default() ); return; } // Element styles if let Some(elements) = data.get("elements").and_then(|v| v.as_array()) { for (i, el) in elements.iter().enumerate() { let tag = el.get("tag").and_then(|v| v.as_str()).unwrap_or("?"); let text = el.get("text").and_then(|v| v.as_str()).unwrap_or(""); println!("[{}] {} \"{}\"", i, tag, text); if let Some(box_data) = el.get("box") { let w = box_data.get("width").and_then(|v| v.as_i64()).unwrap_or(0); let h = box_data.get("height").and_then(|v| v.as_i64()).unwrap_or(0); let x = box_data.get("x").and_then(|v| v.as_i64()).unwrap_or(0); let y = box_data.get("y").and_then(|v| v.as_i64()).unwrap_or(0); println!(" box: {}x{} at ({}, {})", w, h, x, y); } if let Some(styles) = el.get("styles") { let font_size = styles .get("fontSize") .and_then(|v| v.as_str()) .unwrap_or(""); let font_weight = styles .get("fontWeight") .and_then(|v| v.as_str()) .unwrap_or(""); let font_family = styles .get("fontFamily") .and_then(|v| v.as_str()) .unwrap_or(""); let color = styles.get("color").and_then(|v| v.as_str()).unwrap_or(""); let bg = styles .get("backgroundColor") .and_then(|v| v.as_str()) .unwrap_or(""); let radius = styles .get("borderRadius") .and_then(|v| v.as_str()) .unwrap_or(""); println!(" font: {} {} {}", font_size, font_weight, font_family); println!(" color: {}", color); println!(" background: {}", bg); if radius != "0px" { println!(" border-radius: {}", radius); } } println!(); } return; } // Closed if data.get("closed").is_some() { println!("{} Browser closed", color::success_indicator()); return; } // Recording start (has "started" field) if let Some(started) = data.get("started").and_then(|v| v.as_bool()) { if started { match action { Some("profiler_start") => { println!("{} Profiling started", color::success_indicator()); } _ => { if let Some(path) = data.get("path").and_then(|v| v.as_str()) { println!("{} Recording started: {}", color::success_indicator(), path); } else { println!("{} Recording started", color::success_indicator()); } } } return; } } // Recording restart (has "stopped" field - from recording_restart action) if data.get("stopped").is_some() { let path = data .get("path") .and_then(|v| v.as_str()) .unwrap_or("unknown"); if let Some(prev_path) = data.get("previousPath").and_then(|v| v.as_str()) { println!( "{} Recording restarted: {} (previous saved to {})", color::success_indicator(), path, prev_path ); } else { println!("{} Recording started: {}", color::success_indicator(), path); } return; } // Recording stop (has "frames" field - from recording_stop action) if data.get("frames").is_some() { if let Some(path) = data.get("path").and_then(|v| v.as_str()) { if let Some(error) = data.get("error").and_then(|v| v.as_str()) { println!( "{} Recording saved to {} - {}", color::warning_indicator(), path, error ); } else { println!("{} Recording saved to {}", color::success_indicator(), path); } } else { println!("{} Recording stopped", color::success_indicator()); } return; } // Download response (has "suggestedFilename" or "filename" field) if data.get("suggestedFilename").is_some() || data.get("filename").is_some() { if let Some(path) = data.get("path").and_then(|v| v.as_str()) { let filename = data .get("suggestedFilename") .or_else(|| data.get("filename")) .and_then(|v| v.as_str()) .unwrap_or(""); if filename.is_empty() { println!( "{} Downloaded to {}", color::success_indicator(), color::green(path) ); } else { println!( "{} Downloaded to {} ({})", color::success_indicator(), color::green(path), filename ); } return; } } // Trace stop without path if data.get("traceStopped").is_some() { println!("{} Trace stopped", color::success_indicator()); return; } // Path-based operations (screenshot/pdf/trace/har/download/state/video) if let Some(path) = data.get("path").and_then(|v| v.as_str()) { match action.unwrap_or("") { "screenshot" => { println!( "{} Screenshot saved to {}", color::success_indicator(), color::green(path) ); if let Some(annotations) = data.get("annotations").and_then(|v| v.as_array()) { for ann in annotations { let num = ann.get("number").and_then(|n| n.as_u64()).unwrap_or(0); let ref_id = ann.get("ref").and_then(|r| r.as_str()).unwrap_or(""); let role = ann.get("role").and_then(|r| r.as_str()).unwrap_or(""); let name = ann.get("name").and_then(|n| n.as_str()).unwrap_or(""); if name.is_empty() { println!( " {} @{} {}", color::dim(&format!("[{}]", num)), ref_id, role, ); } else { println!( " {} @{} {} {:?}", color::dim(&format!("[{}]", num)), ref_id, role, name, ); } } } } "pdf" => println!( "{} PDF saved to {}", color::success_indicator(), color::green(path) ), "trace_stop" => println!( "{} Trace saved to {}", color::success_indicator(), color::green(path) ), "profiler_stop" => println!( "{} Profile saved to {} ({} events)", color::success_indicator(), color::green(path), data.get("eventCount").and_then(|c| c.as_u64()).unwrap_or(0) ), "har_stop" => println!( "{} HAR saved to {}", color::success_indicator(), color::green(path) ), "download" | "waitfordownload" => println!( "{} Download saved to {}", color::success_indicator(), color::green(path) ), "video_stop" => println!( "{} Video saved to {}", color::success_indicator(), color::green(path) ), "state_save" => println!( "{} State saved to {}", color::success_indicator(), color::green(path) ), "state_load" => { if let Some(note) = data.get("note").and_then(|v| v.as_str()) { println!("{}", note); } println!( "{} State path set to {}", color::success_indicator(), color::green(path) ); } // video_start and other commands that provide a path with a note "video_start" => { if let Some(note) = data.get("note").and_then(|v| v.as_str()) { println!("{}", note); } println!("Path: {}", path); } _ => println!( "{} Saved to {}", color::success_indicator(), color::green(path) ), } return; } // State list if let Some(files) = data.get("files").and_then(|v| v.as_array()) { if let Some(dir) = data.get("directory").and_then(|v| v.as_str()) { println!("{}", color::bold(&format!("Saved states in {}", dir))); } if files.is_empty() { println!("{}", color::dim(" No state files found")); } else { for file in files { let filename = file.get("filename").and_then(|v| v.as_str()).unwrap_or(""); let size = file.get("size").and_then(|v| v.as_i64()).unwrap_or(0); let modified = file.get("modified").and_then(|v| v.as_str()).unwrap_or(""); let encrypted = file .get("encrypted") .and_then(|v| v.as_bool()) .unwrap_or(false); let size_str = if size > 1024 { format!("{:.1}KB", size as f64 / 1024.0) } else { format!("{}B", size) }; let date_str = modified.split('T').next().unwrap_or(modified); let enc_str = if encrypted { " [encrypted]" } else { "" }; println!( " {} {}", filename, color::dim(&format!("({}, {}){}", size_str, date_str, enc_str)) ); } } return; } // State rename if let Some(true) = data.get("renamed").and_then(|v| v.as_bool()) { let old_name = data.get("oldName").and_then(|v| v.as_str()).unwrap_or(""); let new_name = data.get("newName").and_then(|v| v.as_str()).unwrap_or(""); println!( "{} Renamed {} -> {}", color::success_indicator(), old_name, new_name ); return; } // State clear if let Some(cleared) = data.get("cleared").and_then(|v| v.as_i64()) { println!( "{} Cleared {} state file(s)", color::success_indicator(), cleared ); return; } // State show summary if let Some(summary) = data.get("summary") { let cookies = summary.get("cookies").and_then(|v| v.as_i64()).unwrap_or(0); let origins = summary.get("origins").and_then(|v| v.as_i64()).unwrap_or(0); let encrypted = data .get("encrypted") .and_then(|v| v.as_bool()) .unwrap_or(false); let enc_str = if encrypted { " (encrypted)" } else { "" }; println!("State file summary{}:", enc_str); println!(" Cookies: {}", cookies); println!(" Origins with localStorage: {}", origins); return; } // State clean if let Some(cleaned) = data.get("cleaned").and_then(|v| v.as_i64()) { println!( "{} Cleaned {} old state file(s)", color::success_indicator(), cleaned ); return; } // Informational note if let Some(note) = data.get("note").and_then(|v| v.as_str()) { println!("{}", note); return; } // Default success println!("{} Done", color::success_indicator()); } } /// Print command-specific help. Returns true if help was printed, false if command unknown. pub fn print_command_help(command: &str) -> bool { let help = match command { // === Navigation === "open" | "goto" | "navigate" => { r##" agent-browser open - Navigate to a URL Usage: agent-browser open Navigates the browser to the specified URL. If no protocol is provided, https:// is automatically prepended. Aliases: goto, navigate Global Options: --json Output as JSON --session Use specific session --headers Set HTTP headers (scoped to this origin) --headed Show browser window Examples: agent-browser open example.com agent-browser open https://github.com agent-browser open localhost:3000 agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}' # ^ Headers only sent to api.example.com, not other domains "## } "back" => { r##" agent-browser back - Navigate back in history Usage: agent-browser back Goes back one page in the browser history, equivalent to clicking the browser's back button. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser back "## } "forward" => { r##" agent-browser forward - Navigate forward in history Usage: agent-browser forward Goes forward one page in the browser history, equivalent to clicking the browser's forward button. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser forward "## } "reload" => { r##" agent-browser reload - Reload the current page Usage: agent-browser reload Reloads the current page, equivalent to pressing F5 or clicking the browser's reload button. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser reload "## } // === Core Actions === "click" => { r##" agent-browser click - Click an element Usage: agent-browser click [--new-tab] Clicks on the specified element. The selector can be a CSS selector, XPath, or an element reference from snapshot (e.g., @e1). Options: --new-tab Open link in a new tab instead of navigating current tab (only works on elements with href attribute) Global Options: --json Output as JSON --session Use specific session Examples: agent-browser click "#submit-button" agent-browser click @e1 agent-browser click "button.primary" agent-browser click "//button[@type='submit']" agent-browser click @e3 --new-tab "## } "dblclick" => { r##" agent-browser dblclick - Double-click an element Usage: agent-browser dblclick Double-clicks on the specified element. Useful for text selection or triggering double-click handlers. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser dblclick "#editable-text" agent-browser dblclick @e5 "## } "fill" => { r##" agent-browser fill - Clear and fill an input field Usage: agent-browser fill Clears the input field and fills it with the specified text. This replaces any existing content in the field. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser fill "#email" "user@example.com" agent-browser fill @e3 "Hello World" agent-browser fill "input[name='search']" "query" "## } "type" => { r##" agent-browser type - Type text into an element Usage: agent-browser type [--delay ] Types text into the specified element character by character. Unlike fill, this does not clear existing content first. Use --delay to add per-character delay (milliseconds). Global Options: --json Output as JSON --session Use specific session Examples: agent-browser type "#search" "hello" agent-browser type "#search" "iphone" --delay 120 agent-browser type @e2 "additional text" agent-browser type @e2 -- "--delay 120 (literal text)" See Also: For typing into contenteditable editors (Lexical, ProseMirror, etc.) without a selector, use 'keyboard type' instead: agent-browser keyboard type "# My Heading" "## } "hover" => { r##" agent-browser hover - Hover over an element Usage: agent-browser hover Moves the mouse to hover over the specified element. Useful for triggering hover states or dropdown menus. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser hover "#dropdown-trigger" agent-browser hover @e4 "## } "focus" => { r##" agent-browser focus - Focus an element Usage: agent-browser focus Sets keyboard focus to the specified element. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser focus "#input-field" agent-browser focus @e2 "## } "check" => { r##" agent-browser check - Check a checkbox Usage: agent-browser check Checks a checkbox element. If already checked, no action is taken. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser check "#terms-checkbox" agent-browser check @e7 "## } "uncheck" => { r##" agent-browser uncheck - Uncheck a checkbox Usage: agent-browser uncheck Unchecks a checkbox element. If already unchecked, no action is taken. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser uncheck "#newsletter-opt-in" agent-browser uncheck @e8 "## } "select" => { r##" agent-browser select - Select a dropdown option Usage: agent-browser select Selects one or more options in a