feat: add native clipboard support for screenshot paste #67

Merged
naomi merged 8 commits from feat/keep-workin into main 2026-01-25 13:08:38 -08:00
5 changed files with 243 additions and 4 deletions
Showing only changes of commit 2e5de9dc5e - Show all commits
+75
View File
@@ -1,3 +1,4 @@
use std::path::PathBuf;
use tauri::{AppHandle, State};
use tauri_plugin_http::reqwest;
use tauri_plugin_store::StoreExt;
@@ -6,6 +7,7 @@ use crate::achievements::{get_achievement_info, load_achievements, AchievementUn
use crate::bridge_manager::SharedBridgeManager;
use crate::config::{ClaudeStartOptions, HikariConfig};
use crate::stats::UsageStats;
use crate::temp_manager::SharedTempFileManager;
const CONFIG_STORE_KEY: &str = "config";
@@ -298,3 +300,76 @@ pub async fn check_for_updates() -> Result<UpdateInfo, String> {
release_notes: latest.body.clone(),
})
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SavedFileInfo {
pub path: String,
pub filename: String,
}
#[tauri::command]
pub async fn save_temp_file(
temp_manager: State<'_, SharedTempFileManager>,
conversation_id: String,
data: Vec<u8>,
filename: Option<String>,
) -> Result<SavedFileInfo, String> {
let mut manager = temp_manager.lock();
let path = manager.save_file(&conversation_id, &data, filename.as_deref())?;
let filename = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
Ok(SavedFileInfo {
path: path.to_string_lossy().to_string(),
filename,
})
}
#[tauri::command]
pub async fn register_temp_file(
temp_manager: State<'_, SharedTempFileManager>,
conversation_id: String,
file_path: String,
) -> Result<(), String> {
let mut manager = temp_manager.lock();
manager.register_file(&conversation_id, PathBuf::from(file_path));
Ok(())
}
#[tauri::command]
pub async fn get_temp_files(
temp_manager: State<'_, SharedTempFileManager>,
conversation_id: String,
) -> Result<Vec<String>, String> {
let manager = temp_manager.lock();
let files = manager.get_files_for_conversation(&conversation_id);
Ok(files.iter().map(|p| p.to_string_lossy().to_string()).collect())
}
#[tauri::command]
pub async fn cleanup_temp_files(
temp_manager: State<'_, SharedTempFileManager>,
conversation_id: String,
) -> Result<(), String> {
let mut manager = temp_manager.lock();
manager.cleanup_conversation(&conversation_id)
}
#[tauri::command]
pub async fn cleanup_all_temp_files(
temp_manager: State<'_, SharedTempFileManager>,
) -> Result<(), String> {
let mut manager = temp_manager.lock();
manager.cleanup_all()
}
#[tauri::command]
pub async fn cleanup_orphaned_temp_files(
temp_manager: State<'_, SharedTempFileManager>,
) -> Result<usize, String> {
let mut manager = temp_manager.lock();
manager.cleanup_orphaned_files()
}
+18
View File
@@ -4,6 +4,7 @@ mod commands;
mod config;
mod notifications;
mod stats;
mod temp_manager;
mod types;
mod vbs_notification;
mod windows_toast;
@@ -14,6 +15,7 @@ use bridge_manager::create_shared_bridge_manager;
use commands::load_saved_achievements;
use commands::*;
use notifications::*;
use temp_manager::create_shared_temp_manager;
use vbs_notification::*;
use windows_toast::*;
use wsl_notifications::*;
@@ -21,6 +23,7 @@ use wsl_notifications::*;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let bridge_manager = create_shared_bridge_manager();
let temp_manager = create_shared_temp_manager().expect("Failed to create temp file manager");
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
@@ -31,9 +34,18 @@ pub fn run() {
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_http::init())
.manage(bridge_manager.clone())
.manage(temp_manager.clone())
.setup(move |app| {
// Initialize the app handle in the bridge manager
bridge_manager.lock().set_app_handle(app.handle().clone());
// Clean up any orphaned temp files from previous sessions
if let Ok(count) = temp_manager.lock().cleanup_orphaned_files() {
if count > 0 {
println!("Cleaned up {} orphaned temp files", count);
}
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -58,6 +70,12 @@ pub fn run() {
validate_directory,
list_skills,
check_for_updates,
save_temp_file,
register_temp_file,
get_temp_files,
cleanup_temp_files,
cleanup_all_temp_files,
cleanup_orphaned_temp_files,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
+139
View File
@@ -0,0 +1,139 @@
use parking_lot::Mutex;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use uuid::Uuid;
const TEMP_DIR_NAME: &str = "hikari-uploads";
pub struct TempFileManager {
base_dir: PathBuf,
files: HashMap<String, Vec<PathBuf>>,
}
impl TempFileManager {
pub fn new() -> Result<Self, String> {
let base_dir = std::env::temp_dir().join(TEMP_DIR_NAME);
if !base_dir.exists() {
fs::create_dir_all(&base_dir)
.map_err(|e| format!("Failed to create temp directory: {}", e))?;
}
Ok(TempFileManager {
base_dir,
files: HashMap::new(),
})
}
#[allow(dead_code)]
pub fn get_base_dir(&self) -> &Path {
&self.base_dir
}
pub fn save_file(
&mut self,
conversation_id: &str,
data: &[u8],
original_filename: Option<&str>,
) -> Result<PathBuf, String> {
let unique_id = Uuid::new_v4();
let extension = original_filename
.and_then(|name| Path::new(name).extension())
.and_then(|ext| ext.to_str())
.unwrap_or("bin");
let filename = format!("{}_{}.{}", conversation_id, unique_id, extension);
let file_path = self.base_dir.join(&filename);
fs::write(&file_path, data)
.map_err(|e| format!("Failed to write temp file: {}", e))?;
self.files
.entry(conversation_id.to_string())
.or_default()
.push(file_path.clone());
Ok(file_path)
}
pub fn register_file(&mut self, conversation_id: &str, file_path: PathBuf) {
self.files
.entry(conversation_id.to_string())
.or_default()
.push(file_path);
}
pub fn get_files_for_conversation(&self, conversation_id: &str) -> Vec<PathBuf> {
self.files
.get(conversation_id)
.cloned()
.unwrap_or_default()
}
pub fn cleanup_conversation(&mut self, conversation_id: &str) -> Result<(), String> {
if let Some(files) = self.files.remove(conversation_id) {
for file_path in files {
if file_path.exists() {
if let Err(e) = fs::remove_file(&file_path) {
eprintln!(
"Warning: Failed to remove temp file {:?}: {}",
file_path, e
);
}
}
}
}
Ok(())
}
pub fn cleanup_all(&mut self) -> Result<(), String> {
let conversation_ids: Vec<String> = self.files.keys().cloned().collect();
for conversation_id in conversation_ids {
self.cleanup_conversation(&conversation_id)?;
}
Ok(())
}
pub fn cleanup_orphaned_files(&mut self) -> Result<usize, String> {
let mut cleaned_count = 0;
if !self.base_dir.exists() {
return Ok(0);
}
let tracked_files: std::collections::HashSet<PathBuf> =
self.files.values().flatten().cloned().collect();
let entries = fs::read_dir(&self.base_dir)
.map_err(|e| format!("Failed to read temp directory: {}", e))?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && !tracked_files.contains(&path) {
if let Err(e) = fs::remove_file(&path) {
eprintln!("Warning: Failed to remove orphaned file {:?}: {}", path, e);
} else {
cleaned_count += 1;
}
}
}
Ok(cleaned_count)
}
}
impl Default for TempFileManager {
fn default() -> Self {
Self::new().expect("Failed to create TempFileManager")
}
}
pub type SharedTempFileManager = Arc<Mutex<TempFileManager>>;
pub fn create_shared_temp_manager() -> Result<SharedTempFileManager, String> {
Ok(Arc::new(Mutex::new(TempFileManager::new()?)))
}
+3 -3
View File
@@ -272,7 +272,7 @@ function createConversationsStore() {
return newConv.id;
},
deleteConversation: (id: string) => {
deleteConversation: async (id: string) => {
ensureInitialized();
const convs = get(conversations);
const activeId = get(activeConversationId);
@@ -282,8 +282,8 @@ function createConversationsStore() {
return false;
}
// Clean up tracking for this conversation
cleanupConversationTracking(id);
// Clean up tracking for this conversation (including temp files)
await cleanupConversationTracking(id);
conversations.update((c) => {
c.delete(id);
+8 -1
View File
@@ -107,8 +107,15 @@ interface WorkingDirectoryPayload {
conversation_id?: string;
}
export function cleanupConversationTracking(conversationId: string) {
export async function cleanupConversationTracking(conversationId: string) {
connectedConversations.delete(conversationId);
// Clean up any temp files associated with this conversation
try {
await invoke("cleanup_temp_files", { conversationId });
} catch (error) {
console.error("Failed to cleanup temp files for conversation:", error);
}
}
export async function initializeTauriListeners() {