//! Craft mode tools: study a target in a headless browser and compare a build //! with the original visually. use super::{Tool, ToolCtx, ToolOutput, arg_str}; use crate::types::ImageData; use anyhow::{Context, Result, bail}; use async_trait::async_trait; use base64::Engine; use image::{GenericImageView, ImageFormat, Rgba, RgbaImage}; use serde_json::{Value, json}; use std::io::Cursor; use std::path::{Path, PathBuf}; use std::time::Duration; use tokio::process::Command; const SNAPSHOT_HEIGHT: u32 = 2000; /// Long-side limit for images sent to the model. const MAX_IMAGE_SIDE: u32 = 1568; fn png_for_model(img: &image::DynamicImage) -> Result { let (w, h) = img.dimensions(); let img = if w.max(h) > MAX_IMAGE_SIDE { img.resize(MAX_IMAGE_SIDE, MAX_IMAGE_SIDE, image::imageops::FilterType::Triangle) } else { img.clone() }; let mut buf = Vec::new(); img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png)?; Ok(ImageData { media_type: "image/png".into(), data: base64::engine::general_purpose::STANDARD.encode(&buf), }) } fn slug(s: &str) -> String { let mut out: String = s .chars() .map(|c| if c.is_ascii_alphanumeric() { c.to_ascii_lowercase() } else { '-' }) .collect(); while out.contains("--") { out = out.replace("--", "-"); } out.trim_matches('-').chars().take(60).collect() } async fn chromium(ctx: &ToolCtx, args: &[String]) -> Result { let bin = ctx.app.config().craft.chromium.clone(); let fut = Command::new(&bin) .args([ "--headless=new", "--no-sandbox", "--disable-gpu", "--hide-scrollbars", "--mute-audio", "--disable-dev-shm-usage", "--virtual-time-budget=8000", ]) .args(args) .kill_on_drop(true) .output(); tokio::select! { r = tokio::time::timeout(Duration::from_secs(60), fut) => { r.context("chromium timed out")?.with_context(|| format!("could not start '{bin}' (set craft.chromium in the config)")) } _ = ctx.cancel.cancelled() => bail!("aborted"), } } pub struct BrowserSnapshot; #[async_trait] impl Tool for BrowserSnapshot { fn name(&self) -> &str { "browser_snapshot" } fn description(&self) -> String { "Open a URL in a headless browser and capture what a visitor sees: screenshots at the \ configured screen widths (or the given ones) plus the visible text. Works for public \ sites and for local builds (http://localhost:PORT or file:///path). Screenshots are saved \ under .craft/snapshots/