Skip to main content

winx_code_agent/tools/
read_image.rs

1//! Implementation of the `ReadImage` tool.
2//!
3//! This module provides the implementation for the `ReadImage` tool, which is used
4//! to read image files and return their contents as base64-encoded data with
5//! the appropriate MIME type.
6
7use base64::{engine::general_purpose, Engine};
8use mime_guess::MimeGuess;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use tokio::sync::Mutex;
12use tracing::{debug, error, info, instrument};
13
14use crate::errors::{Result, WinxError};
15use crate::state::bash_state::BashState;
16use crate::types::ReadImage;
17use crate::utils::path::expand_user;
18
19/// Supported MIME types for images
20pub const SUPPORTED_MIME_TYPES: [&str; 4] = ["image/jpeg", "image/png", "image/gif", "image/webp"];
21
22/// Read an image from the file system
23///
24/// This function reads an image file, base64 encodes its contents, and
25/// determines the MIME type based on the file extension.
26///
27/// # Arguments
28///
29/// * `file_path` - Path to the image file
30/// * `cwd` - Current working directory for resolving relative paths
31///
32/// # Returns
33///
34/// A tuple containing:
35/// - The MIME type of the image
36/// - The base64-encoded image data
37///
38/// # Errors
39///
40/// Returns an error if the file cannot be accessed or read
41#[instrument(level = "debug", skip(file_path))]
42fn read_image_from_path(file_path: &str, cwd: &Path) -> Result<(String, String)> {
43    debug!("Reading image: {}", file_path);
44
45    // Expand the path
46    let file_path = expand_user(file_path);
47
48    // Ensure path is absolute
49    let path = if Path::new(&file_path).is_absolute() {
50        PathBuf::from(&file_path)
51    } else {
52        // Use current working directory if path is relative
53        cwd.join(&file_path)
54    };
55
56    // Check if path exists
57    if !path.exists() {
58        return Err(WinxError::FileAccessError {
59            path: path.clone(),
60            message: "File does not exist".to_string(),
61        });
62    }
63
64    // Ensure it's a file
65    if !path.is_file() {
66        return Err(WinxError::FileAccessError {
67            path: path.clone(),
68            message: "Path exists but is not a file".to_string(),
69        });
70    }
71
72    // Read the file as bytes
73    let image_bytes = std::fs::read(&path).map_err(|e| WinxError::FileAccessError {
74        path: path.clone(),
75        message: format!("Error reading file: {e}"),
76    })?;
77
78    // Encode the bytes to base64
79    let image_b64 = general_purpose::STANDARD.encode(&image_bytes);
80
81    // Guess the MIME type from the file extension
82    let mime_type =
83        MimeGuess::from_path(&path).first_raw().unwrap_or("application/octet-stream").to_string();
84
85    // Verify the MIME type is a supported image type
86    if SUPPORTED_MIME_TYPES.contains(&mime_type.as_str()) {
87        Ok((mime_type, image_b64))
88    } else {
89        debug!("Detected MIME type '{}' is not in the supported list", mime_type);
90        // Fall back to a best effort based on common extensions
91        let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("").to_lowercase();
92
93        let mime_type = match extension.as_str() {
94            "png" => "image/png",
95            "gif" => "image/gif",
96            "webp" => "image/webp",
97            _ => "image/jpeg", // Default fallback
98        };
99
100        debug!("Using fallback MIME type: {}", mime_type);
101        Ok((mime_type.to_string(), image_b64))
102    }
103}
104
105/// Handle the `ReadImage` tool call
106///
107/// This function processes the `ReadImage` tool call, which reads an image file
108/// and returns its contents as base64-encoded data with the appropriate MIME type.
109///
110/// # Arguments
111///
112/// * `bash_state_arc` - Shared reference to the bash state
113/// * `read_image` - The read image parameters
114///
115/// # Returns
116///
117/// A Result containing a tuple with the MIME type and base64-encoded image data
118///
119/// # Errors
120///
121/// Returns an error if the image file cannot be accessed or read
122#[instrument(level = "info", skip(bash_state_arc, read_image))]
123pub async fn handle_tool_call(
124    bash_state_arc: &Arc<Mutex<Option<BashState>>>,
125    read_image: ReadImage,
126) -> Result<(String, String)> {
127    info!("ReadImage tool called with: {:?}", read_image);
128
129    // We need to extract data from the bash state before awaiting
130    // to avoid holding the MutexGuard across await points
131    let cwd: PathBuf;
132
133    // Lock bash state to extract data
134    {
135        let bash_state_guard = bash_state_arc.lock().await;
136
137        // Ensure bash state is initialized
138        let Some(bash_state) = &*bash_state_guard else {
139            error!("BashState not initialized");
140            return Err(WinxError::BashStateNotInitialized);
141        };
142
143        // Extract needed data
144        cwd = bash_state.cwd.clone();
145    }
146
147    // Read the image file
148    read_image_from_path(&read_image.file_path, &cwd)
149}