Skip to content
9 changes: 7 additions & 2 deletions crates/sandbox-cli/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,16 @@ pub async fn daemon_list_sandboxes() -> Result<Vec<DaemonSandbox>> {
}

/// Take a screenshot of a sandbox via the daemon HTTP API. Returns PNG bytes.
pub async fn daemon_screenshot(sandbox_id: &str) -> Result<Vec<u8>> {
pub async fn daemon_screenshot(sandbox_id: &str, with_frame: bool) -> Result<Vec<u8>> {
let base = daemon_base_url()?;
let client = reqwest_client();
let url = if with_frame {
format!("{base}/sandbox/{sandbox_id}/screenshot?with_frame=true")
} else {
format!("{base}/sandbox/{sandbox_id}/screenshot")
};
let resp = client
.get(format!("{base}/sandbox/{sandbox_id}/screenshot"))
.get(url)
.send()
.await
.with_context(|| "screenshot request to daemon failed")?;
Expand Down
17 changes: 13 additions & 4 deletions crates/sandbox-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ enum Commands {
/// Window ID to capture (overrides auto-detection)
#[arg(long)]
window_id: Option<u32>,

/// Capture full macOS window frame (title bar, tabs, status bar)
#[arg(long)]
with_frame: bool,
},

/// List all visible windows on the system
Expand Down Expand Up @@ -248,8 +252,9 @@ async fn main() -> anyhow::Result<()> {
output,
id,
window_id: _window_id,
with_frame,
} => {
cmd_screenshot_daemon(&output, id.as_deref()).await?;
cmd_screenshot_daemon(&output, id.as_deref(), with_frame).await?;
}
Commands::Windows => {
cmd_windows()?;
Expand Down Expand Up @@ -636,14 +641,18 @@ async fn cmd_click_daemon(x: f64, y: f64, id: &str, button: &str) -> anyhow::Res
}

/// Take a screenshot via the daemon API.
async fn cmd_screenshot_daemon(output: &std::path::Path, id: Option<&str>) -> anyhow::Result<()> {
async fn cmd_screenshot_daemon(
output: &std::path::Path,
id: Option<&str>,
with_frame: bool,
) -> anyhow::Result<()> {
let sandbox_id = id.ok_or_else(|| {
anyhow::anyhow!(
"--id is required for screenshots. Use: sandbox screenshot --id <sandbox-id>"
)
})?;

let png = client::daemon_screenshot(sandbox_id).await?;
let png = client::daemon_screenshot(sandbox_id, with_frame).await?;
std::fs::write(output, &png)
.with_context(|| format!("Failed to write screenshot to {:?}", output))?;
println!("Screenshot saved to {:?} ({} bytes)", output, png.len());
Expand Down Expand Up @@ -1303,7 +1312,7 @@ async fn handle_mcp_tool(name: &str, args: &serde_json::Value) -> serde_json::Va
}
"screenshot_sandbox" => {
let id = args["sandbox_id"].as_str().unwrap_or("");
let png = client::daemon_screenshot(id).await?;
let png = client::daemon_screenshot(id, false).await?;
let b64 = base64_encode(&png);
Ok(serde_json::json!({ "sandbox_id": id, "image_base64": b64 }))
}
Expand Down
3 changes: 2 additions & 1 deletion crates/sandbox-core/src/capture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ mod macos_impl {

let config = SCStreamConfiguration::new()
.with_width(window.frame().width as u32)
.with_height(window.frame().height as u32);
.with_height(window.frame().height as u32)
.with_ignores_shadows_single_window(true);

let image = SCScreenshotManager::capture_image(&filter, &config)
.map_err(|e| AppError::Screenshot(format!("Failed to capture image: {e:?}")))?;
Expand Down
Loading