feat: Add clipboard image paste support and fix image previews in CC sessions
- Add save_clipboard_image and cleanup_temp_images commands in Rust backend - Implement paste event handler in FloatingPromptInput to capture pasted images - Save pasted images to .claude_temp/session_id/ directory - Add automatic cleanup of temp images when session ends - Fix image preview display for file paths containing spaces - Update regex patterns to handle both quoted (@"path with spaces") and unquoted (@path) mentions - Automatically wrap paths with spaces in quotes when inserting - Update remove handler to properly handle both quoted and unquoted paths Users can now paste images directly from clipboard (e.g., screenshots) and see proper previews for all image files regardless of filename format.
This commit is contained in:
@@ -922,6 +922,105 @@ pub async fn load_session_history(
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
/// Saves a clipboard image to a temporary location for a session
|
||||
///
|
||||
/// This command handles pasted images by saving them to a temp directory
|
||||
/// within the project structure, making them accessible for Claude.
|
||||
#[tauri::command]
|
||||
pub async fn save_clipboard_image(
|
||||
project_path: String,
|
||||
session_id: String,
|
||||
image_data: String, // Base64 encoded image data
|
||||
mime_type: String,
|
||||
) -> Result<String, String> {
|
||||
log::info!(
|
||||
"Saving clipboard image for session: {} in project: {}",
|
||||
session_id,
|
||||
project_path
|
||||
);
|
||||
|
||||
// Parse the base64 data (remove data URL prefix if present)
|
||||
let base64_data = if image_data.starts_with("data:") {
|
||||
image_data
|
||||
.split(',')
|
||||
.nth(1)
|
||||
.ok_or("Invalid data URL format")?
|
||||
} else {
|
||||
&image_data
|
||||
};
|
||||
|
||||
// Decode base64 to bytes
|
||||
use base64::Engine;
|
||||
let image_bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(base64_data)
|
||||
.map_err(|e| format!("Failed to decode base64 image: {}", e))?;
|
||||
|
||||
// Determine file extension from MIME type
|
||||
let extension = match mime_type.as_str() {
|
||||
"image/png" => "png",
|
||||
"image/jpeg" | "image/jpg" => "jpg",
|
||||
"image/gif" => "gif",
|
||||
"image/webp" => "webp",
|
||||
"image/svg+xml" => "svg",
|
||||
_ => "png", // Default to PNG
|
||||
};
|
||||
|
||||
// Create temp directory for the project if it doesn't exist
|
||||
let project_path_buf = PathBuf::from(&project_path);
|
||||
let temp_dir = project_path_buf.join(".claude_temp").join(&session_id);
|
||||
fs::create_dir_all(&temp_dir)
|
||||
.map_err(|e| format!("Failed to create temp directory: {}", e))?;
|
||||
|
||||
// Generate unique filename with timestamp
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
let filename = format!("pasted_image_{}.{}", timestamp, extension);
|
||||
let file_path = temp_dir.join(&filename);
|
||||
|
||||
// Write image to file
|
||||
fs::write(&file_path, image_bytes)
|
||||
.map_err(|e| format!("Failed to write image file: {}", e))?;
|
||||
|
||||
// Return the absolute path
|
||||
let absolute_path = file_path
|
||||
.canonicalize()
|
||||
.map_err(|e| format!("Failed to get absolute path: {}", e))?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
log::info!("Saved clipboard image to: {}", absolute_path);
|
||||
Ok(absolute_path)
|
||||
}
|
||||
|
||||
/// Cleans up temporary images for a session
|
||||
///
|
||||
/// This command removes the temporary directory created for pasted images
|
||||
/// when a session ends to avoid accumulating temporary files.
|
||||
#[tauri::command]
|
||||
pub async fn cleanup_temp_images(
|
||||
project_path: String,
|
||||
session_id: String,
|
||||
) -> Result<(), String> {
|
||||
log::info!(
|
||||
"Cleaning up temp images for session: {} in project: {}",
|
||||
session_id,
|
||||
project_path
|
||||
);
|
||||
|
||||
let project_path_buf = PathBuf::from(&project_path);
|
||||
let temp_dir = project_path_buf.join(".claude_temp").join(&session_id);
|
||||
|
||||
if temp_dir.exists() {
|
||||
fs::remove_dir_all(&temp_dir)
|
||||
.map_err(|e| format!("Failed to remove temp directory: {}", e))?;
|
||||
log::info!("Cleaned up temp directory: {}", temp_dir.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute a new interactive Claude Code session with streaming output
|
||||
#[tauri::command]
|
||||
pub async fn execute_claude_code(
|
||||
|
@@ -18,13 +18,13 @@ use commands::agents::{
|
||||
};
|
||||
use commands::claude::{
|
||||
cancel_claude_execution, check_auto_checkpoint, check_claude_version, cleanup_old_checkpoints,
|
||||
clear_checkpoint_manager, continue_claude_code, create_checkpoint, execute_claude_code,
|
||||
cleanup_temp_images, clear_checkpoint_manager, continue_claude_code, create_checkpoint, execute_claude_code,
|
||||
find_claude_md_files, fork_from_checkpoint, get_checkpoint_diff, get_checkpoint_settings,
|
||||
get_checkpoint_state_stats, get_claude_session_output, get_claude_settings, get_project_sessions,
|
||||
get_recently_modified_files, get_session_timeline, get_system_prompt, list_checkpoints,
|
||||
list_directory_contents, list_projects, list_running_claude_sessions, load_session_history,
|
||||
open_new_session, read_claude_md_file, restore_checkpoint, resume_claude_code,
|
||||
save_claude_md_file, save_claude_settings, save_system_prompt, search_files,
|
||||
save_claude_md_file, save_claude_settings, save_clipboard_image, save_system_prompt, search_files,
|
||||
track_checkpoint_message, track_session_messages, update_checkpoint_settings,
|
||||
get_hooks_config, update_hooks_config, validate_hook_command,
|
||||
ClaudeProcessState,
|
||||
@@ -101,6 +101,8 @@ fn main() {
|
||||
find_claude_md_files,
|
||||
read_claude_md_file,
|
||||
save_claude_md_file,
|
||||
save_clipboard_image,
|
||||
cleanup_temp_images,
|
||||
load_session_history,
|
||||
execute_claude_code,
|
||||
continue_claude_code,
|
||||
|
Reference in New Issue
Block a user